mirror of
https://github.com/fly-apps/live_beats.git
synced 2024-11-21 23:50:59 +00:00
Initial synced playback
This commit is contained in:
parent
aaa89c7c76
commit
60382feddc
25 changed files with 563 additions and 542 deletions
|
@ -27,6 +27,58 @@ Hooks.Progress = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Hooks.AudioPlayer = {
|
||||||
|
mounted(){
|
||||||
|
this.playbackBeganAt = null
|
||||||
|
this.player = this.el.querySelector("audio")
|
||||||
|
this.currentTime = this.el.querySelector("#player-time")
|
||||||
|
this.duration = this.el.querySelector("#player-duration")
|
||||||
|
this.progress = this.el.querySelector("#player-progress")
|
||||||
|
let enableAudio = () => {
|
||||||
|
document.removeEventListener("click", enableAudio)
|
||||||
|
this.player.play().catch(error => null)
|
||||||
|
this.player.pause()
|
||||||
|
}
|
||||||
|
document.addEventListener("click", enableAudio)
|
||||||
|
this.el.addEventListener("js:play_pause", () => {
|
||||||
|
this.play()
|
||||||
|
})
|
||||||
|
this.handleEvent("play", ({url, began_at}) => {
|
||||||
|
this.playbackBeganAt = began_at
|
||||||
|
this.player.src = url
|
||||||
|
this.play()
|
||||||
|
})
|
||||||
|
this.handleEvent("pause", () => {
|
||||||
|
console.log("Server Pause!")
|
||||||
|
this.pause()
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
play(){
|
||||||
|
this.player.play().then(() => {
|
||||||
|
this.player.currentTime = (Date.now() - this.playbackBeganAt) / 1000
|
||||||
|
this.progressTimer = setInterval(() => this.updateProgress(), 100)
|
||||||
|
this.pushEvent("audio-accepted", {})
|
||||||
|
}, error => {
|
||||||
|
this.pushEvent("audio-rejected", {})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
pause(){
|
||||||
|
this.player.pause()
|
||||||
|
clearInterval(this.progressTimer)
|
||||||
|
},
|
||||||
|
|
||||||
|
updateProgress(){
|
||||||
|
if(isNaN(this.player.duration)){ return false }
|
||||||
|
this.progress.style.width = `${(this.player.currentTime / (this.player.duration) * 100)}%`
|
||||||
|
this.duration.innerText = this.formatTime(this.player.duration)
|
||||||
|
this.currentTime.innerText = this.formatTime(this.player.currentTime)
|
||||||
|
},
|
||||||
|
|
||||||
|
formatTime(seconds){ return new Date(1000 * seconds).toISOString().substr(14, 5) }
|
||||||
|
}
|
||||||
|
|
||||||
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
|
||||||
let liveSocket = new LiveSocket("/live", Socket, {
|
let liveSocket = new LiveSocket("/live", Socket, {
|
||||||
hooks: Hooks,
|
hooks: Hooks,
|
||||||
|
@ -51,6 +103,8 @@ topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"})
|
||||||
window.addEventListener("phx:page-loading-start", info => topbar.show())
|
window.addEventListener("phx:page-loading-start", info => topbar.show())
|
||||||
window.addEventListener("phx:page-loading-stop", info => topbar.hide())
|
window.addEventListener("phx:page-loading-stop", info => topbar.hide())
|
||||||
|
|
||||||
|
window.addEventListener("js:exec", e => e.target[e.detail.call](...e.detail.args))
|
||||||
|
|
||||||
// connect if there are any LiveViews on the page
|
// connect if there are any LiveViews on the page
|
||||||
liveSocket.connect()
|
liveSocket.connect()
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ defmodule LiveBeats.Application do
|
||||||
@impl true
|
@impl true
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
children = [
|
children = [
|
||||||
|
{Task.Supervisor, name: LiveBeats.TaskSupervisor},
|
||||||
# Start the Ecto repository
|
# Start the Ecto repository
|
||||||
LiveBeats.Repo,
|
LiveBeats.Repo,
|
||||||
# Start the Telemetry supervisor
|
# Start the Telemetry supervisor
|
||||||
|
|
|
@ -7,30 +7,17 @@ defmodule LiveBeats.ID3 do
|
||||||
year: nil
|
year: nil
|
||||||
|
|
||||||
def parse(path) do
|
def parse(path) do
|
||||||
binary = File.read!(path)
|
with {:ok, parsed} <- :id3_tag_reader.read_tag(path) do
|
||||||
size = byte_size(binary) - 128
|
{:ok, parsed}
|
||||||
<<_::binary-size(size), id3_tag::binary>> = binary
|
# %ID3{
|
||||||
|
# title: strip(title),
|
||||||
case id3_tag do
|
# artist: strip(artist),
|
||||||
<<
|
# album: strip(album),
|
||||||
"TAG",
|
# year: 2028
|
||||||
title::binary-size(30),
|
# }}
|
||||||
artist::binary-size(30),
|
else
|
||||||
album::binary-size(30),
|
other ->
|
||||||
year::binary-size(4),
|
{:error, other}
|
||||||
_comment::binary-size(30),
|
|
||||||
_rest::binary
|
|
||||||
>> ->
|
|
||||||
{:ok,
|
|
||||||
%ID3{
|
|
||||||
title: strip(title),
|
|
||||||
artist: strip(artist),
|
|
||||||
album: strip(album),
|
|
||||||
year: year
|
|
||||||
}}
|
|
||||||
|
|
||||||
_invalid ->
|
|
||||||
{:error, :invalid}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -4,21 +4,45 @@ defmodule LiveBeats.MediaLibrary do
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Ecto.Query, warn: false
|
import Ecto.Query, warn: false
|
||||||
alias LiveBeats.Repo
|
alias LiveBeats.{Repo, MP3Stat, Accounts}
|
||||||
|
|
||||||
alias LiveBeats.MediaLibrary.{Song, Genre}
|
alias LiveBeats.MediaLibrary.{Song, Genre}
|
||||||
|
|
||||||
def store_mp3(%Song{} = song, tmp_path) do
|
@pubsub LiveBeats.PubSub
|
||||||
File.mkdir_p!("priv/static/uploads/songs")
|
|
||||||
File.cp!(tmp_path, song.mp3_path)
|
def subscribe(%Accounts.User{} = user) do
|
||||||
|
Phoenix.PubSub.subscribe(@pubsub, topic(user.id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def import_songs(changesets, consome_file)
|
def play_song(%Song{id: id}), do: play_song(id)
|
||||||
when is_map(changesets) and is_function(consome_file, 2) do
|
|
||||||
|
def play_song(id) do
|
||||||
|
song = get_song!(id)
|
||||||
|
Phoenix.PubSub.broadcast!(@pubsub, topic(song.user_id), {:play, song, %{began_at: now_ms()}})
|
||||||
|
end
|
||||||
|
|
||||||
|
def pause_song(%Song{} = song) do
|
||||||
|
Phoenix.PubSub.broadcast!(@pubsub, topic(song.user_id), {:pause, song})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp topic(user_id), do: "room:#{user_id}"
|
||||||
|
|
||||||
|
def store_mp3(%Song{} = song, tmp_path) do
|
||||||
|
dir = "priv/static/uploads/songs"
|
||||||
|
File.mkdir_p!(dir)
|
||||||
|
File.cp!(tmp_path, Path.join(dir, song.mp3_filename))
|
||||||
|
end
|
||||||
|
|
||||||
|
def put_stats(%Ecto.Changeset{} = changeset, %MP3Stat{} = stat) do
|
||||||
|
Ecto.Changeset.put_change(changeset, :duration, stat.duration)
|
||||||
|
end
|
||||||
|
|
||||||
|
def import_songs(%Accounts.User{} = user, changesets, consume_file)
|
||||||
|
when is_map(changesets) and is_function(consume_file, 2) do
|
||||||
changesets
|
changesets
|
||||||
|> Enum.reduce(Ecto.Multi.new(), fn {ref, chset}, acc ->
|
|> Enum.reduce(Ecto.Multi.new(), fn {ref, chset}, acc ->
|
||||||
chset =
|
chset =
|
||||||
chset
|
chset
|
||||||
|
|> Song.put_user(user)
|
||||||
|> Song.put_mp3_path()
|
|> Song.put_mp3_path()
|
||||||
|> Map.put(:action, nil)
|
|> Map.put(:action, nil)
|
||||||
|
|
||||||
|
@ -31,7 +55,7 @@ defmodule LiveBeats.MediaLibrary do
|
||||||
results
|
results
|
||||||
|> Enum.filter(&match?({{:song, _ref}, _}, &1))
|
|> Enum.filter(&match?({{:song, _ref}, _}, &1))
|
||||||
|> Enum.map(fn {{:song, ref}, song} ->
|
|> Enum.map(fn {{:song, ref}, song} ->
|
||||||
consome_file.(ref, fn tmp_path -> store_mp3(song, tmp_path) end)
|
consume_file.(ref, fn tmp_path -> store_mp3(song, tmp_path) end)
|
||||||
{ref, song}
|
{ref, song}
|
||||||
end)
|
end)
|
||||||
|> Enum.into(%{})}
|
|> Enum.into(%{})}
|
||||||
|
@ -58,8 +82,8 @@ defmodule LiveBeats.MediaLibrary do
|
||||||
Repo.all(Genre, order_by: [asc: :title])
|
Repo.all(Genre, order_by: [asc: :title])
|
||||||
end
|
end
|
||||||
|
|
||||||
def list_songs do
|
def list_songs(limit \\ 100) do
|
||||||
Repo.all(Song)
|
Repo.all(from s in Song, limit: ^limit, order_by: [asc: s.inserted_at])
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_song!(id), do: Repo.get!(Song, id)
|
def get_song!(id), do: Repo.get!(Song, id)
|
||||||
|
@ -83,4 +107,6 @@ defmodule LiveBeats.MediaLibrary do
|
||||||
def change_song(%Song{} = song, attrs \\ %{}) do
|
def change_song(%Song{} = song, attrs \\ %{}) do
|
||||||
Song.changeset(song, attrs)
|
Song.changeset(song, attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp now_ms, do: System.system_time() |> System.convert_time_unit(:native, :millisecond)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,8 @@ defmodule LiveBeats.MediaLibrary.Song do
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias LiveBeats.Accounts
|
||||||
|
|
||||||
schema "songs" do
|
schema "songs" do
|
||||||
field :album_artist, :string
|
field :album_artist, :string
|
||||||
field :artist, :string
|
field :artist, :string
|
||||||
|
@ -11,7 +13,7 @@ defmodule LiveBeats.MediaLibrary.Song do
|
||||||
field :title, :string
|
field :title, :string
|
||||||
field :mp3_path, :string
|
field :mp3_path, :string
|
||||||
field :mp3_filename, :string
|
field :mp3_filename, :string
|
||||||
belongs_to :user, LiveBeats.Accounts.User
|
belongs_to :user, Accounts.User
|
||||||
belongs_to :genre, LiveBeats.MediaLibrary.Genre
|
belongs_to :genre, LiveBeats.MediaLibrary.Genre
|
||||||
|
|
||||||
timestamps()
|
timestamps()
|
||||||
|
@ -22,6 +24,11 @@ defmodule LiveBeats.MediaLibrary.Song do
|
||||||
song
|
song
|
||||||
|> cast(attrs, [:album_artist, :artist, :title, :date_recorded, :date_released])
|
|> cast(attrs, [:album_artist, :artist, :title, :date_recorded, :date_released])
|
||||||
|> validate_required([:artist, :title])
|
|> validate_required([:artist, :title])
|
||||||
|
|> validate_number(:duration, greater_than: 0, less_than: 1200)
|
||||||
|
end
|
||||||
|
|
||||||
|
def put_user(%Ecto.Changeset{} = changeset, %Accounts.User{} = user) do
|
||||||
|
put_assoc(changeset, :user, user)
|
||||||
end
|
end
|
||||||
|
|
||||||
def put_mp3_path(%Ecto.Changeset{} = changeset) do
|
def put_mp3_path(%Ecto.Changeset{} = changeset) do
|
||||||
|
@ -30,7 +37,7 @@ defmodule LiveBeats.MediaLibrary.Song do
|
||||||
|
|
||||||
changeset
|
changeset
|
||||||
|> Ecto.Changeset.put_change(:mp3_filename, filename)
|
|> Ecto.Changeset.put_change(:mp3_filename, filename)
|
||||||
|> Ecto.Changeset.put_change(:mp3_path, "priv/static/uploads/songs/#{filename}")
|
|> Ecto.Changeset.put_change(:mp3_path, "uploads/songs/#{filename}")
|
||||||
else
|
else
|
||||||
changeset
|
changeset
|
||||||
end
|
end
|
||||||
|
|
46
lib/live_beats/mp3_stat.ex
Normal file
46
lib/live_beats/mp3_stat.ex
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
defmodule LiveBeats.MP3Stat do
|
||||||
|
alias LiveBeats.MP3Stat
|
||||||
|
|
||||||
|
defstruct duration: 0, path: nil
|
||||||
|
|
||||||
|
def to_mmss(duration) when is_integer(duration) do
|
||||||
|
hours = div(duration, 60 * 60)
|
||||||
|
minutes = div(duration - (hours * 60 * 60), 60)
|
||||||
|
seconds = rem(duration - (hours * 60 * 60) - (minutes * 60), 60)
|
||||||
|
|
||||||
|
[minutes, seconds]
|
||||||
|
|> Enum.map(fn count -> String.pad_leading("#{count}", 2, ["0"]) end)
|
||||||
|
|> Enum.join(":")
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse(path) do
|
||||||
|
args = ["-v", "quiet", "-stats", "-i", path, "-f", "null", "-"]
|
||||||
|
|
||||||
|
# "size=N/A time=00:03:00.00 bitrate=N/A speed= 674x"
|
||||||
|
case System.cmd("ffmpeg", args, stderr_to_stdout: true) do
|
||||||
|
{output, 0} -> parse_output(output, path)
|
||||||
|
{_, 1} -> {:error, :bad_file}
|
||||||
|
other -> {:error, other}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_output(output, path) do
|
||||||
|
with %{"time" => time} <- Regex.named_captures(~r/.*time=(?<time>[^\s]+).*/, output),
|
||||||
|
[hours, minutes, seconds, _milliseconds] <- ints(String.split(time, [":", "."])) do
|
||||||
|
duration = hours * 60 * 60 + minutes * 60 + seconds
|
||||||
|
{:ok, %MP3Stat{duration: duration, path: path}}
|
||||||
|
else
|
||||||
|
_ -> {:error, :bad_duration}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp ints(strings) when is_list(strings) do
|
||||||
|
Enum.flat_map(strings, fn str ->
|
||||||
|
case Integer.parse(str) do
|
||||||
|
{int, ""} -> [int]
|
||||||
|
{_, _} -> []
|
||||||
|
:error -> []
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
|
@ -20,7 +20,7 @@ defmodule LiveBeatsWeb.Endpoint do
|
||||||
at: "/",
|
at: "/",
|
||||||
from: :live_beats,
|
from: :live_beats,
|
||||||
gzip: false,
|
gzip: false,
|
||||||
only: ~w(assets fonts images favicon.ico robots.txt)
|
only: ~w(assets fonts images favicon.ico robots.txt uploads)
|
||||||
|
|
||||||
# Code reloading can be explicitly enabled under the
|
# Code reloading can be explicitly enabled under the
|
||||||
# :code_reloader configuration of your endpoint.
|
# :code_reloader configuration of your endpoint.
|
||||||
|
|
44
lib/live_beats_web/live/layout_component.ex
Normal file
44
lib/live_beats_web/live/layout_component.ex
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
defmodule LiveBeatsWeb.LayoutComponent do
|
||||||
|
@moduledoc """
|
||||||
|
Component for rendering content inside layout without full DOM patch.
|
||||||
|
"""
|
||||||
|
use LiveBeatsWeb, :live_component
|
||||||
|
|
||||||
|
def show_modal(module, attrs) do
|
||||||
|
send_update(__MODULE__, id: "layout", show: Enum.into(attrs, %{module: module}))
|
||||||
|
end
|
||||||
|
|
||||||
|
def hide_modal do
|
||||||
|
send_update(__MODULE__, id: "layout", show: nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(%{id: id} = assigns, socket) do
|
||||||
|
show =
|
||||||
|
case assigns[:show] do
|
||||||
|
%{module: _module, confirm: {text, attrs}} = show ->
|
||||||
|
show
|
||||||
|
|> Map.put_new(:patch_to, nil)
|
||||||
|
|> Map.put_new(:redirect_to, nil)
|
||||||
|
|> Map.merge(%{confirm_text: text, confirm_attrs: attrs})
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, assign(socket, id: id, show: show)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class={unless @show, do: "hidden"}>
|
||||||
|
<%= if @show do %>
|
||||||
|
<.modal show id={@id} redirect_to={@show.redirect_to} patch_to={@show.patch_to}>
|
||||||
|
<.live_component module={@show.module} {@show} />
|
||||||
|
<:cancel>Cancel</:cancel>
|
||||||
|
<:confirm {@show.confirm_attrs}><%= @show.confirm_text %></:confirm>
|
||||||
|
</.modal>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
end
|
|
@ -82,27 +82,7 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def show_modal(%JS{} = js, id, opts) when is_binary(id) and is_list(opts) do
|
def show_modal(js \\ %JS{}, id) when is_binary(id) do
|
||||||
on_confirm = Keyword.get(opts, :on_confirm, %JS{}) |> hide_modal(id)
|
|
||||||
title = Keyword.get(opts, :title, "")
|
|
||||||
content = Keyword.get(opts, :content, "")
|
|
||||||
|
|
||||||
js
|
|
||||||
|> JS.inner_text(title, to: "##{id}-title")
|
|
||||||
|> JS.inner_text(content, to: "##{id}-content")
|
|
||||||
|> JS.set_attribute("phx-click", to: "##{id}-confirm", value: on_confirm)
|
|
||||||
|> show_modal(id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show_modal(id) when is_binary(id) do
|
|
||||||
show_modal(%JS{}, id, [])
|
|
||||||
end
|
|
||||||
|
|
||||||
def show_modal(id, opts) when is_binary(id) and is_list(opts) do
|
|
||||||
show_modal(%JS{}, id, opts)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show_modal(%JS{} = js, id) when is_binary(id) do
|
|
||||||
js
|
js
|
||||||
|> JS.show(
|
|> JS.show(
|
||||||
to: "##{id}",
|
to: "##{id}",
|
||||||
|
@ -116,6 +96,7 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
{"ease-out duration-300", "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
|
{"ease-out duration-300", "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95",
|
||||||
"opacity-100 translate-y-0 sm:scale-100"}
|
"opacity-100 translate-y-0 sm:scale-100"}
|
||||||
)
|
)
|
||||||
|
|> js_exec("##{id}-confirm", "focus", [])
|
||||||
end
|
end
|
||||||
|
|
||||||
def hide_modal(js \\ %JS{}, id) do
|
def hide_modal(js \\ %JS{}, id) do
|
||||||
|
@ -138,8 +119,8 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
assigns =
|
assigns =
|
||||||
assigns
|
assigns
|
||||||
|> assign_new(:show, fn -> false end)
|
|> assign_new(:show, fn -> false end)
|
||||||
|> assign_new(:loading, fn -> false end)
|
|> assign_new(:patch_to, fn -> nil end)
|
||||||
|> assign_new(:return_to, fn -> nil end)
|
|> assign_new(:redirect_to, fn -> nil end)
|
||||||
|> assign_new(:on_cancel, fn -> %JS{} end)
|
|> assign_new(:on_cancel, fn -> %JS{} end)
|
||||||
|> assign_new(:on_confirm, fn -> %JS{} end)
|
|> assign_new(:on_confirm, fn -> %JS{} end)
|
||||||
# slots
|
# slots
|
||||||
|
@ -158,13 +139,16 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
phx-window-keydown={hide_modal(@on_cancel, @id)} phx-key="escape"
|
phx-window-keydown={hide_modal(@on_cancel, @id)} phx-key="escape"
|
||||||
phx-click-away={hide_modal(@on_cancel, @id)}
|
phx-click-away={hide_modal(@on_cancel, @id)}
|
||||||
>
|
>
|
||||||
<%= if @return_to do %>
|
<%= if @patch_to do %>
|
||||||
<%= live_redirect "close", to: @return_to, data: [modal_return: true], class: "hidden" %>
|
<.link patch_to={@patch_to} data-modal-return class="hidden"></.link>
|
||||||
|
<% end %>
|
||||||
|
<%= if @redirect_to do %>
|
||||||
|
<.link redirect_to={@redirect_to} data-modal-return class="hidden"></.link>
|
||||||
<% end %>
|
<% end %>
|
||||||
<div class="sm:flex sm:items-start">
|
<div class="sm:flex sm:items-start">
|
||||||
<div class={"mx-auto flex-shrink-0 flex items-center justify-center h-8 w-8 rounded-full bg-purple-100 sm:mx-0"}>
|
<div class={"mx-auto flex-shrink-0 flex items-center justify-center h-8 w-8 rounded-full bg-purple-100 sm:mx-0"}>
|
||||||
<!-- Heroicon name: outline/plus -->
|
<!-- Heroicon name: outline/plus -->
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class={"#{if @loading, do: "animate-ping"} h-6 w-6 text-purple-600"} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
@ -173,31 +157,31 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
<%= render_slot(@title) %>
|
<%= render_slot(@title) %>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
<p id={"#{@id}-content"} class={"text-sm text-gray-500 #{if @loading, do: "invisible"}"}>
|
<p id={"#{@id}-content"} class={"text-sm text-gray-500"}>
|
||||||
<%= render_slot(@inner_block) %>
|
<%= render_slot(@inner_block) %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class={"mt-5 sm:mt-4 sm:flex sm:flex-row-reverse #{if @loading, do: "invisible"}"}>
|
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
|
||||||
<%= for confirm <- @confirm do %>
|
<%= for confirm <- @confirm do %>
|
||||||
<button
|
<button
|
||||||
id={"#{@id}-confirm"}
|
id={"#{@id}-confirm"}
|
||||||
type="button"
|
|
||||||
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
|
class="w-full inline-flex justify-center rounded-md border border-transparent shadow-sm px-4 py-2 bg-red-600 text-base font-medium text-white hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 sm:ml-3 sm:w-auto sm:text-sm"
|
||||||
phx-click={@on_confirm |> hide_modal(@id)}
|
phx-click={@on_confirm}
|
||||||
|
phx-disable-with
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
autofocus
|
{assigns_to_attributes(confirm)}
|
||||||
>
|
>
|
||||||
<%= render_slot(confirm) %>
|
<%= render_slot(confirm) %>
|
||||||
</button>
|
</button>
|
||||||
<% end %>
|
<% end %>
|
||||||
<%= for cancel <- @cancel do %>
|
<%= for cancel <- @cancel do %>
|
||||||
<button
|
<button
|
||||||
type="button"
|
|
||||||
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm"
|
class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:mt-0 sm:w-auto sm:text-sm"
|
||||||
phx-click={hide_modal(@on_cancel, @id)}
|
phx-click={hide_modal(@on_cancel, @id)}
|
||||||
tabindex="2"
|
tabindex="2"
|
||||||
|
{assigns_to_attributes(cancel)}
|
||||||
>
|
>
|
||||||
<%= render_slot(cancel) %>
|
<%= render_slot(cancel) %>
|
||||||
</button>
|
</button>
|
||||||
|
@ -218,9 +202,9 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
|
|
||||||
~H"""
|
~H"""
|
||||||
<div class="bg-gray-200 flex-auto dark:bg-black rounded-full overflow-hidden" phx-update="ignore">
|
<div class="bg-gray-200 flex-auto dark:bg-black rounded-full overflow-hidden" phx-update="ignore">
|
||||||
<div id="progress"
|
<div
|
||||||
|
id={@id}
|
||||||
class="bg-lime-500 dark:bg-lime-400 h-1.5 w-0"
|
class="bg-lime-500 dark:bg-lime-400 h-1.5 w-0"
|
||||||
phx-hook="Progress"
|
|
||||||
data-min={@min}
|
data-min={@min}
|
||||||
data-max={@max}
|
data-max={@max}
|
||||||
data-val={@value}>
|
data-val={@value}>
|
||||||
|
@ -300,14 +284,11 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white divide-y divide-gray-100">
|
<tbody class="bg-white divide-y divide-gray-100">
|
||||||
<%= for row <- @rows do %>
|
<%= for {row, i} <- Enum.with_index(@rows) do %>
|
||||||
<tr id={@row_id && @row_id.(row)} class="hover:bg-gray-50">
|
<tr id={@row_id && @row_id.(row)} class="hover:bg-gray-50">
|
||||||
<%= for {col, i} <- Enum.with_index(@col) do %>
|
<%= for col <- @col do %>
|
||||||
<td class={"px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 #{if i == 0, do: "max-w-0 w-full"}"}>
|
<td class={"px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 #{if i == 0, do: "max-w-0 w-full"}"}>
|
||||||
<div class="flex items-center space-x-3 lg:pl-2">
|
<div class="flex items-center space-x-3 lg:pl-2">
|
||||||
<%= if i == 0 do %>
|
|
||||||
<div class="flex-shrink-0 w-2.5 h-2.5 rounded-full bg-pink-600 mr-2" aria-hidden="true"></div>
|
|
||||||
<% end %>
|
|
||||||
<%= render_slot(col, row) %>
|
<%= render_slot(col, row) %>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
@ -320,4 +301,52 @@ defmodule LiveBeatsWeb.LiveHelpers do
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def live_table(assigns) do
|
||||||
|
assigns =
|
||||||
|
assigns
|
||||||
|
|> assign_new(:row_id, fn -> false end)
|
||||||
|
|> assign_new(:active_id, fn -> nil end)
|
||||||
|
|
||||||
|
~H"""
|
||||||
|
<div class="hidden mt-8 sm:block">
|
||||||
|
<div class="align-middle inline-block min-w-full border-b border-gray-200">
|
||||||
|
<table class="min-w-full">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-t border-gray-200">
|
||||||
|
<%= for col <- @col do %>
|
||||||
|
<th
|
||||||
|
class="px-6 py-3 border-b border-gray-200 bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
<span class="lg:pl-2"><%= col.label %></span>
|
||||||
|
</th>
|
||||||
|
<% end %>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-100">
|
||||||
|
<%= for {row, i} <- Enum.with_index(@rows) do %>
|
||||||
|
<.live_component
|
||||||
|
module={@module}
|
||||||
|
id={@row_id.(row)}
|
||||||
|
row={row} col={@col}
|
||||||
|
index={i}
|
||||||
|
active_id={@active_id}
|
||||||
|
class="hover:bg-gray-50"
|
||||||
|
/>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Calls a wired up event listener to call a function with arguments.
|
||||||
|
|
||||||
|
window.addEventListener("js:exec", e => e.target[e.detail.call](...e.detail.args))
|
||||||
|
"""
|
||||||
|
def js_exec(js \\ %JS{}, to, call, args) do
|
||||||
|
JS.dispatch(js, "js:exec", to: to, detail: %{call: call, args: args})
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
defmodule LiveBeatsWeb.ModalComponent do
|
|
||||||
use LiveBeatsWeb, :live_component
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div
|
|
||||||
id={@id}
|
|
||||||
class="phx-modal"
|
|
||||||
phx-capture-click="close"
|
|
||||||
phx-window-keydown="close"
|
|
||||||
phx-key="escape"
|
|
||||||
phx-target={@myself}
|
|
||||||
phx-page-loading>
|
|
||||||
|
|
||||||
<div class="phx-modal-content">
|
|
||||||
<%= live_patch raw("×"), to: @return_to, class: "phx-modal-close" %>
|
|
||||||
<%= live_component @component, @opts %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("close", _, socket) do
|
|
||||||
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,30 +1,37 @@
|
||||||
defmodule LiveBeatsWeb.PlayerLive do
|
defmodule LiveBeatsWeb.PlayerLive do
|
||||||
use LiveBeatsWeb, :live_view
|
use LiveBeatsWeb, :live_view
|
||||||
|
|
||||||
|
alias LiveBeats.MediaLibrary
|
||||||
|
alias LiveBeats.MediaLibrary.Song
|
||||||
|
|
||||||
on_mount {LiveBeatsWeb.UserAuth, :current_user}
|
on_mount {LiveBeatsWeb.UserAuth, :current_user}
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<!-- player -->
|
<!-- player -->
|
||||||
<div class="w-full">
|
<div id="audio-player" phx-hook="AudioPlayer" class="w-full" >
|
||||||
|
<div phx-update="ignore">
|
||||||
|
<audio></audio>
|
||||||
|
</div>
|
||||||
<div class="bg-white dark:bg-gray-800 p-4">
|
<div class="bg-white dark:bg-gray-800 p-4">
|
||||||
<div class="flex items-center space-x-3.5 sm:space-x-5 lg:space-x-3.5 xl:space-x-5">
|
<div class="flex items-center space-x-3.5 sm:space-x-5 lg:space-x-3.5 xl:space-x-5">
|
||||||
<div class="pr-5">
|
<div class="pr-5">
|
||||||
<div class="min-w-0 flex-col space-y-0.5">
|
<div class="min-w-0 flex-col space-y-0.5">
|
||||||
<h2 class="text-black dark:text-white text-sm sm:text-sm lg:text-sm xl:text-sm font-semibold truncate">
|
<h2 class="text-black dark:text-white text-sm sm:text-sm lg:text-sm xl:text-sm font-semibold truncate">
|
||||||
Black Sands
|
<%= if @song, do: @song.title, else: raw(" ") %>
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-gray-500 dark:text-gray-400 text-sm sm:text-sm lg:text-sm xl:text-sm font-medium">
|
<p class="text-gray-500 dark:text-gray-400 text-sm sm:text-sm lg:text-sm xl:text-sm font-medium">
|
||||||
Bonobo
|
<%= if @song, do: @song.artist, else: raw(" ") %>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<.progress_bar />
|
<.progress_bar id="player-progress" />
|
||||||
|
|
||||||
<div class="text-gray-500 dark:text-gray-400 flex-row justify-between text-sm font-medium tabular-nums">
|
<div class="text-gray-500 dark:text-gray-400 flex-row justify-between text-sm font-medium tabular-nums"
|
||||||
<div><%= @time %></div>
|
phx-update="ignore">
|
||||||
<div><%= @count %></div>
|
<div id="player-time"></div>
|
||||||
|
<div id="player-duration"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -46,12 +53,21 @@ defmodule LiveBeatsWeb.PlayerLive do
|
||||||
<path d="M17 0L9 6l8 6V0z" fill="currentColor" />
|
<path d="M17 0L9 6l8 6V0z" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="mx-auto scale-75">
|
<!-- pause -->
|
||||||
<svg width="50" height="50" fill="none">
|
<button type="button" class="mx-auto scale-75" phx-click={JS.push("play_pause") |> js_play_pause()}>
|
||||||
<circle class="text-gray-300 dark:text-gray-500" cx="25" cy="25" r="24" stroke="currentColor" stroke-width="1.5" />
|
<%= if @playing do %>
|
||||||
<path d="M18 16h4v18h-4V16zM28 16h4v18h-4z" fill="currentColor" />
|
<svg id="player-pause" width="50" height="50" fill="none">
|
||||||
</svg>
|
<circle class="text-gray-300 dark:text-gray-500" cx="25" cy="25" r="24" stroke="currentColor" stroke-width="1.5" />
|
||||||
|
<path d="M18 16h4v18h-4V16zM28 16h4v18h-4z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
<% else %>
|
||||||
|
<svg id="player-play" width="50" height="50" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<circle id="svg_1" stroke-width="0.8" stroke="currentColor" r="11.4" cy="12" cx="12" class="text-gray-300 dark:text-gray-500"/>
|
||||||
|
<path stroke="null" fill="currentColor" transform="rotate(90 12.8947 12.3097)" id="svg_6" d="m9.40275,15.10014l3.49194,-5.58088l3.49197,5.58088l-6.98391,0z" stroke-width="1.5" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
<% end %>
|
||||||
</button>
|
</button>
|
||||||
|
<!-- /pause -->
|
||||||
<button type="button" class="mx-auto scale-75">
|
<button type="button" class="mx-auto scale-75">
|
||||||
<svg width="34" height="39" fill="none">
|
<svg width="34" height="39" fill="none">
|
||||||
<path d="M12.878 26.12c1.781 0 3.09-1.066 3.085-2.515.004-1.104-.665-1.896-1.824-2.075v-.068c.912-.235 1.505-.95 1.5-1.93.005-1.283-1.048-2.379-2.727-2.379-1.602 0-2.89.968-2.932 2.387h1.274c.03-.801.784-1.287 1.64-1.287.892 0 1.475.541 1.471 1.346.004.844-.673 1.398-1.64 1.398h-.738v1.074h.737c1.21 0 1.91.614 1.91 1.491 0 .848-.738 1.424-1.765 1.424-.946 0-1.683-.486-1.734-1.262H9.797c.055 1.424 1.317 2.395 3.08 2.395zm7.734.025c2.016 0 3.196-1.645 3.196-4.504 0-2.838-1.197-4.488-3.196-4.488-2.003 0-3.196 1.645-3.2 4.488 0 2.855 1.18 4.5 3.2 4.504zm0-1.138c-1.18 0-1.892-1.185-1.892-3.366.004-2.174.716-3.371 1.892-3.371 1.172 0 1.888 1.197 1.888 3.37 0 2.182-.712 3.367-1.888 3.367z" fill="currentColor" />
|
<path d="M12.878 26.12c1.781 0 3.09-1.066 3.085-2.515.004-1.104-.665-1.896-1.824-2.075v-.068c.912-.235 1.505-.95 1.5-1.93.005-1.283-1.048-2.379-2.727-2.379-1.602 0-2.89.968-2.932 2.387h1.274c.03-.801.784-1.287 1.64-1.287.892 0 1.475.541 1.471 1.346.004.844-.673 1.398-1.64 1.398h-.738v1.074h.737c1.21 0 1.91.614 1.91 1.491 0 .848-.738 1.424-1.765 1.424-.946 0-1.683-.486-1.734-1.262H9.797c.055 1.424 1.317 2.395 3.08 2.395zm7.734.025c2.016 0 3.196-1.645 3.196-4.504 0-2.838-1.197-4.488-3.196-4.488-2.003 0-3.196 1.645-3.2 4.488 0 2.855 1.18 4.5 3.2 4.504zm0-1.138c-1.18 0-1.892-1.185-1.892-3.366.004-2.174.716-3.371 1.892-3.371 1.172 0 1.888 1.197 1.888 3.37 0 2.182-.712 3.367-1.888 3.367z" fill="currentColor" />
|
||||||
|
@ -70,18 +86,68 @@ defmodule LiveBeatsWeb.PlayerLive do
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<%= if @error do %>
|
||||||
|
<.modal show id="enable-audio" on_confirm={js_play_pause() |> hide_modal("enable-audio")}>
|
||||||
|
<:title>Start Listening now</:title>
|
||||||
|
Your browser needs a click event to enable playback
|
||||||
|
<:confirm>Listen Now</:confirm>
|
||||||
|
</.modal>
|
||||||
|
<% end %>
|
||||||
</div>
|
</div>
|
||||||
<!-- /player -->
|
<!-- /player -->
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def mount(_parmas, _session, socket) do
|
def mount(_parmas, _session, socket) do
|
||||||
# if connected?(socket), do: Process.send_after(self(), :tick, 1000)
|
if connected?(socket) and socket.assigns.current_user do
|
||||||
{:ok, assign(socket, time: inspect(System.system_time()), count: 0), layout: false}
|
MediaLibrary.subscribe(socket.assigns.current_user)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, assign(socket, song: nil, playing: false, error: false), layout: false}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(:tick, socket) do
|
def handle_event("play_pause", _, socket) do
|
||||||
Process.send_after(self(), :tick, 1000)
|
%{song: song, playing: playing} = socket.assigns
|
||||||
{:noreply, update(socket, :count, &(&1 + 1))}
|
|
||||||
|
IO.inspect({:play_pause, playing})
|
||||||
|
|
||||||
|
cond do
|
||||||
|
song && playing ->
|
||||||
|
MediaLibrary.pause_song(song)
|
||||||
|
{:noreply, assign(socket, playing: false)}
|
||||||
|
|
||||||
|
song ->
|
||||||
|
MediaLibrary.play_song(song)
|
||||||
|
{:noreply, assign(socket, playing: true)}
|
||||||
|
|
||||||
|
true ->
|
||||||
|
{:noreply, assign(socket, playing: false)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("audio-rejected", _, socket) do
|
||||||
|
{:noreply, assign(socket, error: true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("audio-accepted", _, socket) do
|
||||||
|
{:noreply, assign(socket, error: false)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:pause, _}, socket) do
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> push_event("pause", %{})
|
||||||
|
|> assign(playing: false)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:play, %Song{} = song, %{began_at: at}}, socket) do
|
||||||
|
{:noreply,
|
||||||
|
socket
|
||||||
|
|> push_event("play", %{began_at: at, url: Path.join(LiveBeatsWeb.Endpoint.url(), song.mp3_path)})
|
||||||
|
|> assign(song: song, playing: true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp js_play_pause(js \\ %JS{}) do
|
||||||
|
JS.dispatch(js, "js:play_pause", to: "#audio-player")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
defmodule LiveBeatsWeb.SongLive.DeleteDialogComponent do
|
|
||||||
use LiveBeatsWeb, :live_component
|
|
||||||
|
|
||||||
alias LiveBeats.MediaLibrary
|
|
||||||
|
|
||||||
def send_show(%MediaLibrary.Song{} = song) do
|
|
||||||
send_update(__MODULE__, id: "delete-modal", show: song)
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div>
|
|
||||||
<.modal
|
|
||||||
id="delete-modal"
|
|
||||||
loading={is_nil(@song.id)}
|
|
||||||
on_confirm={JS.push("confirm-delete", value: %{id: @song.id}) |> hide("#song-#{@song.id}")}
|
|
||||||
on_cancel={JS.push("cancel", target: @myself)}>
|
|
||||||
|
|
||||||
Are you sure you want to delete "<%= @song.title %>"?
|
|
||||||
|
|
||||||
<:cancel>Cancel</:cancel>
|
|
||||||
<:confirm>Delete</:confirm>
|
|
||||||
</.modal>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def update(%{show: %MediaLibrary.Song{} = song}, socket) do
|
|
||||||
{:ok, assign(socket, song: song)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def update(%{} = _assigns, socket) do
|
|
||||||
{:ok, assign_defaults(socket)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("cancel", _, socket) do
|
|
||||||
IO.inspect({:cancel})
|
|
||||||
{:noreply, assign_defaults(socket)}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp assign_defaults(socket) do
|
|
||||||
assign(socket, song: %MediaLibrary.Song{})
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -1,102 +0,0 @@
|
||||||
defmodule LiveBeatsWeb.SongLive.FormComponent do
|
|
||||||
use LiveBeatsWeb, :live_component
|
|
||||||
|
|
||||||
alias LiveBeats.{MediaLibrary, ID3}
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def update(%{song: song} = assigns, socket) do
|
|
||||||
changeset = MediaLibrary.change_song(song)
|
|
||||||
|
|
||||||
{:ok,
|
|
||||||
socket
|
|
||||||
|> assign(assigns)
|
|
||||||
|> assign(changeset: changeset, tmp_path: nil)
|
|
||||||
|> allow_upload(:mp3,
|
|
||||||
auto_upload: true,
|
|
||||||
progress: &handle_progress/3,
|
|
||||||
accept: ~w(.mp3),
|
|
||||||
max_entries: 1,
|
|
||||||
max_file_size: 20_000_000
|
|
||||||
)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("validate", %{"song" => song_params}, socket) do
|
|
||||||
changeset =
|
|
||||||
socket.assigns.song
|
|
||||||
|> MediaLibrary.change_song(song_params)
|
|
||||||
|> Map.put(:action, :validate)
|
|
||||||
|
|
||||||
{:noreply, assign(socket, :changeset, changeset)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("save", %{"song" => song_params}, socket) do
|
|
||||||
IO.inspect({:save, song_params})
|
|
||||||
save_song(socket, socket.assigns.action, song_params)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp save_song(socket, :edit, song_params) do
|
|
||||||
case MediaLibrary.update_song(socket.assigns.song, song_params) do
|
|
||||||
{:ok, _song} ->
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:info, "Song updated successfully")
|
|
||||||
|> push_redirect(to: socket.assigns.return_to)}
|
|
||||||
|
|
||||||
{:error, %Ecto.Changeset{} = changeset} ->
|
|
||||||
{:noreply, assign(socket, :changeset, changeset)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp save_song(socket, :new, song_params) do
|
|
||||||
case MediaLibrary.create_song(song_params) do
|
|
||||||
{:ok, _song} ->
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> put_flash(:info, "Song created successfully")
|
|
||||||
|> push_redirect(to: socket.assigns.return_to)}
|
|
||||||
|
|
||||||
{:error, %Ecto.Changeset{} = changeset} ->
|
|
||||||
{:noreply, assign(socket, changeset: changeset)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp handle_progress(:mp3, entry, socket) do
|
|
||||||
changeset = socket.assigns.changeset
|
|
||||||
|
|
||||||
if entry.done? do
|
|
||||||
new_socket =
|
|
||||||
consume_uploaded_entry(socket, entry, fn %{} = meta ->
|
|
||||||
case ID3.parse(meta.path) do
|
|
||||||
{:ok, %ID3{} = id3} ->
|
|
||||||
new_changeset =
|
|
||||||
changeset
|
|
||||||
|> Ecto.Changeset.put_change(:title, id3.title)
|
|
||||||
|> Ecto.Changeset.put_change(:artist, id3.artist)
|
|
||||||
|
|
||||||
socket
|
|
||||||
|> assign(changeset: new_changeset)
|
|
||||||
|> put_tmp_mp3(meta.path)
|
|
||||||
|
|
||||||
{:error, _} ->
|
|
||||||
put_tmp_mp3(socket, meta.path)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|
|
||||||
{:noreply, new_socket}
|
|
||||||
else
|
|
||||||
{:noreply, socket}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp put_tmp_mp3(socket, path) do
|
|
||||||
if socket.assigns.tmp_path, do: File.rm!(socket.assigns.tmp_path)
|
|
||||||
{:ok, tmp_path} = Plug.Upload.random_file("temp_mp3")
|
|
||||||
File.cp!(path, tmp_path)
|
|
||||||
assign(socket, tmp_path: tmp_path)
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
defp file_error(%{kind: :too_large} = assigns), do: ~H|larger than 10MB|
|
|
||||||
defp file_error(%{kind: :not_accepted} = assigns), do: ~H|not a valid MP3 file|
|
|
||||||
end
|
|
|
@ -1,131 +0,0 @@
|
||||||
<div>
|
|
||||||
<h2><%= @title %></h2>
|
|
||||||
|
|
||||||
<.form
|
|
||||||
let={f}
|
|
||||||
for={@changeset}
|
|
||||||
id="song-form"
|
|
||||||
class="space-y-8 divide-y divide-gray-200"
|
|
||||||
phx-target={@myself}
|
|
||||||
phx-change="validate"
|
|
||||||
phx-submit="save">
|
|
||||||
|
|
||||||
<div class="space-y-8 divide-y divide-gray-200 sm:space-y-5">
|
|
||||||
<div class="pt-8 space-y-6 sm:pt-10 sm:space-y-5">
|
|
||||||
<div class="space-y-6 sm:space-y-5">
|
|
||||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
|
||||||
<label for="song-form_title" class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2">
|
|
||||||
Title
|
|
||||||
</label>
|
|
||||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
|
||||||
<%= text_input f, :title, class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<%= error_tag f, :title %>
|
|
||||||
|
|
||||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
|
||||||
<label for="song-form_artist" class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2">
|
|
||||||
Artist
|
|
||||||
</label>
|
|
||||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
|
||||||
<%= text_input f, :artist, class: "max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md" %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<%= error_tag f, :artist %>
|
|
||||||
|
|
||||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
|
||||||
<label for="country" class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2">
|
|
||||||
Genre
|
|
||||||
</label>
|
|
||||||
<div class="mt-1 sm:mt-0 sm:col-span-2">
|
|
||||||
<select id="song-form_genre_id" name="genre_id" class="max-w-lg block focus:ring-indigo-500 focus:border-indigo-500 w-full shadow-sm sm:max-w-xs sm:text-sm border-gray-300 rounded-md">
|
|
||||||
<%= for genre <- @genres do %>
|
|
||||||
<option value={genre.id}><%= genre.title %></option>
|
|
||||||
<% end %>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- upload -->
|
|
||||||
<div class="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
|
|
||||||
<label for="cover-photo" class="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2">
|
|
||||||
MP3
|
|
||||||
</label>
|
|
||||||
<div class="mt-1 sm:mt-0 sm:col-span-2" phx-drop-target={@uploads.mp3.ref}>
|
|
||||||
|
|
||||||
<%= if Enum.any?(@uploads.mp3.errors) do %>
|
|
||||||
<div class="rounded-md bg-red-50 p-4 mb-2">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<svg class="h-5 w-5 text-red-400" x-description="Heroicon name: solid/x-circle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3">
|
|
||||||
<h3 class="text-sm font-medium text-red-800">
|
|
||||||
Oops!
|
|
||||||
</h3>
|
|
||||||
<div class="mt-2 text-sm text-red-700">
|
|
||||||
<ul role="list" class="list-disc pl-5 space-y-1">
|
|
||||||
<%= for {_ref, error} <- @uploads.mp3.errors do %>
|
|
||||||
<li><.file_error kind={error} /></li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<div class="max-w-lg flex justify-center px-6 pt-5 pb-6 border-2 border-gray-300 border-dashed rounded-md">
|
|
||||||
<div class="space-y-1 text-center">
|
|
||||||
<svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48" aria-hidden="true">
|
|
||||||
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
|
||||||
</svg>
|
|
||||||
<div class="flex text-sm text-gray-600">
|
|
||||||
<label for="file-upload" class="relative cursor-pointer bg-white rounded-md font-medium text-indigo-600 hover:text-indigo-500 focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-indigo-500">
|
|
||||||
<span phx-click={JS.dispatch("click", to: "##{@uploads.mp3.ref}")}>Upload a file</span>
|
|
||||||
<%= live_file_input @uploads.mp3, class: "sr-only" %>
|
|
||||||
</label>
|
|
||||||
<p class="pl-1">or drag and drop</p>
|
|
||||||
</div>
|
|
||||||
<p class="text-xs text-gray-500">
|
|
||||||
MP3 up to 20MB
|
|
||||||
</p>
|
|
||||||
<%= if Enum.any?(@uploads.mp3.entries) do %>
|
|
||||||
<br/>
|
|
||||||
<%= for entry <- @uploads.mp3.entries do %>
|
|
||||||
<div class="ring-4 ring-purple-100 rounded-full">
|
|
||||||
<%= entry.client_name %>
|
|
||||||
</div>
|
|
||||||
<br/>
|
|
||||||
<div class="bg-gray-200 flex-auto rounded-full overflow-hidden">
|
|
||||||
<div id="progress"
|
|
||||||
class="bg-purple-500 dark:bg-purple-400 h-1.5 w-0"
|
|
||||||
style={"width: #{entry.progress}%;"}>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<% end %>
|
|
||||||
<% end %>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- /upload -->
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="pt-5">
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button type="button" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</.form>
|
|
||||||
</div>
|
|
|
@ -1,9 +1,9 @@
|
||||||
defmodule LiveBeatsWeb.SongLive.Index do
|
defmodule LiveBeatsWeb.SongLive.Index do
|
||||||
use LiveBeatsWeb, :live_view
|
use LiveBeatsWeb, :live_view
|
||||||
|
|
||||||
alias LiveBeats.MediaLibrary
|
alias LiveBeats.{MediaLibrary, MP3Stat}
|
||||||
alias LiveBeats.MediaLibrary.Song
|
alias LiveBeats.MediaLibrary.Song
|
||||||
alias LiveBeatsWeb.SongLive.DeleteDialogComponent
|
alias LiveBeatsWeb.LayoutComponent
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
|
@ -15,51 +15,107 @@ defmodule LiveBeatsWeb.SongLive.Index do
|
||||||
</:actions>
|
</:actions>
|
||||||
</.title_bar>
|
</.title_bar>
|
||||||
|
|
||||||
<%= if @live_action in [:new, :edit] do %>
|
<%= for song <- @songs, id = "delete-modal-#{song.id}" do %>
|
||||||
<.modal show id="add-songs" return_to={Routes.song_index_path(@socket, :index)}>
|
<.modal
|
||||||
<.live_component
|
id={id}
|
||||||
module={LiveBeatsWeb.SongLive.UploadFormComponent}
|
on_confirm={JS.push("delete", value: %{id: song.id}) |> hide_modal(id) |> hide("#song-#{song.id}")}
|
||||||
title={@page_title}
|
>
|
||||||
id={@song.id || :new}
|
Are you sure you want to delete "<%= song.title %>"?
|
||||||
action={@live_action}
|
<:cancel>Cancel</:cancel>
|
||||||
return_to={Routes.song_index_path(@socket, :index)}
|
<:confirm>Delete</:confirm>
|
||||||
song={@song}
|
|
||||||
genres={@genres}
|
|
||||||
/>
|
|
||||||
</.modal>
|
</.modal>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
<.live_table
|
||||||
<.modal id="delete-modal">
|
module={LiveBeatsWeb.SongLive.SongRow}
|
||||||
<:cancel>Cancel</:cancel>
|
rows={@songs}
|
||||||
<:confirm>Delete</:confirm>
|
row_id={fn song -> "song-#{song.id}" end}
|
||||||
</.modal>
|
>
|
||||||
|
<:col let={%{song: song}} label="Title"><%= song.title %></:col>
|
||||||
<.table rows={@songs} row_id={fn song -> "song-#{song.id}" end}>
|
<:col let={%{song: song}} label="Artist"><%= song.artist %></:col>
|
||||||
<:col let={song} label="Title"><%= song.title %></:col>
|
<:col let={%{song: song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col>
|
||||||
<:col let={song} label="Artist"><%= song.artist %></:col>
|
<:col let={%{song: song}} label="">
|
||||||
<:col let={song} label="Duration"><%= song.duration %></:col>
|
<.link phx-click={show_modal("delete-modal-#{song.id}")} class="inline-flex items-center px-3 py-2 text-sm leading-4 font-medium">
|
||||||
<:col let={song} label="">
|
<svg xmlns="http://www.w3.org/2000/svg" class="-ml-0.5 mr-2 h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<.link redirect_to={Routes.song_show_path(@socket, :show, song)}>Show</.link>
|
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
<.link patch_to={Routes.song_index_path(@socket, :edit, song)}>Edit</.link>
|
</svg>
|
||||||
<.link phx-click={
|
Delete
|
||||||
show_modal(
|
</.link>
|
||||||
"delete-modal",
|
|
||||||
content: "Are you sure you want to delete \"#{song.title}\"?",
|
|
||||||
on_confirm: JS.push("delete", value: %{id: song.id}) |> hide("#song-#{song.id}")
|
|
||||||
)
|
|
||||||
}>Delete</.link>
|
|
||||||
</:col>
|
</:col>
|
||||||
</.table>
|
</.live_table>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
{:ok, assign(socket, :songs, list_songs())}
|
if connected?(socket) do
|
||||||
|
MediaLibrary.subscribe(socket.assigns.current_user)
|
||||||
|
end
|
||||||
|
{:ok, assign(socket, songs: list_songs(), active_id: nil)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_params(params, _url, socket) do
|
def handle_params(params, _url, socket) do
|
||||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
{:noreply, socket |> apply_action(socket.assigns.live_action, params) |> maybe_show_modal()}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("play-song", %{"id" => id}, socket) do
|
||||||
|
MediaLibrary.play_song(id)
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("delete", %{"id" => id}, socket) do
|
||||||
|
song = MediaLibrary.get_song!(id)
|
||||||
|
{:ok, _} = MediaLibrary.delete_song(song)
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({_ref, {:duration, entry_ref, result}}, socket) do
|
||||||
|
IO.inspect({:async_duration, entry_ref, result})
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:play, %Song{} = song, _meta}, socket) do
|
||||||
|
{:noreply, play_song(socket, song)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:pause, %Song{} = song}, socket) do
|
||||||
|
{:noreply, pause_song(socket, song.id)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp pause_song(socket, song_id) do
|
||||||
|
if old = Enum.find(socket.assigns.songs, fn song -> song.id == song_id end) do
|
||||||
|
send_update(LiveBeatsWeb.SongLive.SongRow, id: "song-#{old.id}", action: :deactivate)
|
||||||
|
end
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
defp play_song(socket, %Song{} = song) do
|
||||||
|
socket = pause_song(socket, socket.assigns.active_id)
|
||||||
|
next = Enum.find(socket.assigns.songs, &(&1.id == song.id))
|
||||||
|
|
||||||
|
if next do
|
||||||
|
send_update(LiveBeatsWeb.SongLive.SongRow, id: "song-#{next.id}", action: :activate)
|
||||||
|
end
|
||||||
|
|
||||||
|
assign(socket, active_id: next.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp maybe_show_modal(socket) do
|
||||||
|
if socket.assigns.live_action in [:new, :edit] do
|
||||||
|
LayoutComponent.show_modal(LiveBeatsWeb.SongLive.UploadFormComponent, %{
|
||||||
|
confirm: {"Save", type: "submit", form: "song-form"},
|
||||||
|
patch_to: Routes.song_index_path(socket, :index),
|
||||||
|
id: socket.assigns.song.id || :new,
|
||||||
|
title: socket.assigns.page_title,
|
||||||
|
action: socket.assigns.live_action,
|
||||||
|
song: socket.assigns.song,
|
||||||
|
current_user: socket.assigns.current_user,
|
||||||
|
genres: socket.assigns.genres
|
||||||
|
})
|
||||||
|
else
|
||||||
|
LayoutComponent.hide_modal()
|
||||||
|
end
|
||||||
|
|
||||||
|
socket
|
||||||
end
|
end
|
||||||
|
|
||||||
defp apply_action(socket, :edit, %{"id" => id}) do
|
defp apply_action(socket, :edit, %{"id" => id}) do
|
||||||
|
@ -70,7 +126,7 @@ defmodule LiveBeatsWeb.SongLive.Index do
|
||||||
|
|
||||||
defp apply_action(socket, :new, _params) do
|
defp apply_action(socket, :new, _params) do
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, "New Song")
|
|> assign(:page_title, "Add Songs")
|
||||||
|> assign(:song, %Song{})
|
|> assign(:song, %Song{})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -80,13 +136,7 @@ defmodule LiveBeatsWeb.SongLive.Index do
|
||||||
|> assign(:song, nil)
|
|> assign(:song, nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("delete", %{"id" => id}, socket) do
|
|
||||||
song = MediaLibrary.get_song!(id)
|
|
||||||
{:ok, _} = MediaLibrary.delete_song(song)
|
|
||||||
{:noreply, assign(socket, :songs, list_songs())}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp list_songs do
|
defp list_songs do
|
||||||
MediaLibrary.list_songs()
|
MediaLibrary.list_songs(50)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
defmodule LiveBeatsWeb.SongLive.Show do
|
|
||||||
use LiveBeatsWeb, :live_view
|
|
||||||
|
|
||||||
alias LiveBeats.MediaLibrary
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(_params, _session, socket) do
|
|
||||||
{:ok, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_params(%{"id" => id}, _, socket) do
|
|
||||||
{:noreply,
|
|
||||||
socket
|
|
||||||
|> assign(:page_title, page_title(socket.assigns.live_action))
|
|
||||||
|> assign(:song, MediaLibrary.get_song!(id))}
|
|
||||||
end
|
|
||||||
|
|
||||||
defp page_title(:show), do: "Show Song"
|
|
||||||
defp page_title(:edit), do: "Edit Song"
|
|
||||||
end
|
|
|
@ -1,47 +0,0 @@
|
||||||
<h1>Show Song</h1>
|
|
||||||
|
|
||||||
<%= if @live_action in [:edit] do %>
|
|
||||||
<%#= live_modal LiveBeatsWeb.SongLive.FormComponent,
|
|
||||||
id: @song.id,
|
|
||||||
title: @page_title,
|
|
||||||
action: @live_action,
|
|
||||||
song: @song,
|
|
||||||
return_to: Routes.song_show_path(@socket, :show, @song) %>
|
|
||||||
<% end %>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<strong>Album artist:</strong>
|
|
||||||
<%= @song.album_artist %>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<strong>Artist:</strong>
|
|
||||||
<%= @song.artist %>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<strong>Duration:</strong>
|
|
||||||
<%= @song.duration %>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<strong>Title:</strong>
|
|
||||||
<%= @song.title %>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<strong>Date recorded:</strong>
|
|
||||||
<%= @song.date_recorded %>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li>
|
|
||||||
<strong>Date released:</strong>
|
|
||||||
<%= @song.date_released %>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<span><%= live_patch "Edit", to: Routes.song_show_path(@socket, :edit, @song), class: "button" %></span> |
|
|
||||||
<span><%= live_redirect "Back", to: Routes.song_index_path(@socket, :index) %></span>
|
|
|
@ -1,11 +1,19 @@
|
||||||
defmodule LiveBeatsWeb.SongLive.SongEntryComponent do
|
defmodule LiveBeatsWeb.SongLive.SongEntryComponent do
|
||||||
use LiveBeatsWeb, :live_component
|
use LiveBeatsWeb, :live_component
|
||||||
|
|
||||||
|
alias LiveBeats.MP3Stat
|
||||||
|
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start sm:border-t sm:border-gray-200 sm:pt-2">
|
<div class="sm:grid sm:grid-cols-2 sm:gap-2 sm:items-start sm:border-t sm:border-gray-200 sm:pt-2">
|
||||||
<div class="border border-gray-300 rounded-md px-3 py-2 mt-2 shadow-sm focus-within:ring-1 focus-within:ring-indigo-600 focus-within:border-indigo-600">
|
<div class="border border-gray-300 rounded-md px-3 py-2 mt-2 shadow-sm focus-within:ring-1 focus-within:ring-indigo-600 focus-within:border-indigo-600">
|
||||||
<label for="name" class="block text-xs font-medium text-gray-900">Title</label>
|
<label for="name" class="block text-xs font-medium text-gray-900">
|
||||||
|
<%= if @duration do %>
|
||||||
|
Title <span class="text-gray-400">(<%= MP3Stat.to_mmss(@duration) %>)</span>
|
||||||
|
<% else %>
|
||||||
|
Title <span class="text-gray-400">(calculating duration...)</span>
|
||||||
|
<% end %>
|
||||||
|
</label>
|
||||||
<input type="text" name={"songs[#{@ref}][title]"} value={@title}
|
<input type="text" name={"songs[#{@ref}][title]"} value={@title}
|
||||||
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"/>
|
class="block w-full border-0 p-0 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-sm"/>
|
||||||
<%= error_tag(@errors, :title, "songs[#{@ref}][title]") %>
|
<%= error_tag(@errors, :title, "songs[#{@ref}][title]") %>
|
||||||
|
@ -33,6 +41,7 @@ defmodule LiveBeatsWeb.SongLive.SongEntryComponent do
|
||||||
|> assign(:errors, changeset.errors)
|
|> assign(:errors, changeset.errors)
|
||||||
|> assign(title: Ecto.Changeset.get_field(changeset, :title))
|
|> assign(title: Ecto.Changeset.get_field(changeset, :title))
|
||||||
|> assign(artist: Ecto.Changeset.get_field(changeset, :artist))
|
|> assign(artist: Ecto.Changeset.get_field(changeset, :artist))
|
||||||
|
|> assign(duration: IO.inspect(Ecto.Changeset.get_field(changeset, :duration)))
|
||||||
|> assign_new(:progress, fn -> 0 end)}
|
|> assign_new(:progress, fn -> 0 end)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
60
lib/live_beats_web/live/song_live/song_row.ex
Normal file
60
lib/live_beats_web/live/song_live/song_row.ex
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
defmodule LiveBeatsWeb.SongLive.SongRow do
|
||||||
|
use LiveBeatsWeb, :live_component
|
||||||
|
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<tr id={@id} class={@class}}>
|
||||||
|
<%= for {col, i} <- Enum.with_index(@col) do %>
|
||||||
|
<td
|
||||||
|
class={"px-6 py-3 whitespace-nowrap text-sm font-medium text-gray-900 #{if i == 0, do: "max-w-0 w-full cursor-pointer"}"}
|
||||||
|
phx-click={JS.push("play-song", value: %{id: @song.id})}
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-3 lg:pl-2">
|
||||||
|
<%= if i == 0 do %>
|
||||||
|
<%= if @active do %>
|
||||||
|
<span class="flex pt-1 relative mr-2 w-4">
|
||||||
|
<span class="w-3 h-3 animate-ping bg-purple-400 rounded-full absolute"></span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 -mt-1 -ml-1" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<span class="flex relative w-6 -translate-x-1">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
<%= render_slot(col, assigns) %>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<% end %>
|
||||||
|
</tr>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(%{action: :activate}, socket) do
|
||||||
|
{:ok, assign(socket, active: true)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(%{action: :deactivate}, socket) do
|
||||||
|
{:ok, assign(socket, active: false)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(%{action: action}, _socket) do
|
||||||
|
raise ArgumentError, "unkown action #{inspect(action)}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def update(assigns, socket) do
|
||||||
|
{:ok,
|
||||||
|
assign(socket,
|
||||||
|
id: assigns.id,
|
||||||
|
song: assigns.row,
|
||||||
|
col: assigns.col,
|
||||||
|
class: assigns.class,
|
||||||
|
index: assigns.index,
|
||||||
|
active: false
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,12 +1,22 @@
|
||||||
defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
use LiveBeatsWeb, :live_component
|
use LiveBeatsWeb, :live_component
|
||||||
|
|
||||||
alias LiveBeats.{MediaLibrary, ID3}
|
alias LiveBeats.{MediaLibrary, MP3Stat}
|
||||||
alias LiveBeatsWeb.SongLive.SongEntryComponent
|
alias LiveBeatsWeb.SongLive.SongEntryComponent
|
||||||
|
|
||||||
@max_songs 10
|
@max_songs 10
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
def update(%{action: {:duration, entry_ref, result}}, socket) do
|
||||||
|
case result do
|
||||||
|
{:ok, %MP3Stat{} = stat} ->
|
||||||
|
{:ok, put_stats(socket, entry_ref, stat)}
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:ok, cancel_upload(socket, :mp3, entry_ref)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update(%{song: song} = assigns, socket) do
|
def update(%{song: song} = assigns, socket) do
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|
@ -50,9 +60,10 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("save", %{"songs" => song_params}, socket) do
|
def handle_event("save", %{"songs" => song_params}, socket) do
|
||||||
|
%{current_user: current_user} = socket.assigns
|
||||||
changesets = socket.assigns.changesets
|
changesets = socket.assigns.changesets
|
||||||
|
|
||||||
case MediaLibrary.import_songs(changesets, &consume_entry(socket, &1, &2)) do
|
case MediaLibrary.import_songs(current_user, changesets, &consume_entry(socket, &1, &2)) do
|
||||||
{:ok, songs} ->
|
{:ok, songs} ->
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|
@ -92,16 +103,35 @@ defmodule LiveBeatsWeb.SongLive.UploadFormComponent do
|
||||||
|
|
||||||
defp handle_progress(:mp3, entry, socket) do
|
defp handle_progress(:mp3, entry, socket) do
|
||||||
send_update(SongEntryComponent, id: entry.ref, progress: entry.progress)
|
send_update(SongEntryComponent, id: entry.ref, progress: entry.progress)
|
||||||
{:noreply, put_new_changeset(socket, entry)}
|
lv = self()
|
||||||
end
|
|
||||||
|
|
||||||
defp put_tmp_mp3(changeset, path) do
|
if entry.done? do
|
||||||
{:ok, tmp_path} = Plug.Upload.random_file("tmp_mp3")
|
consume_uploaded_entry(socket, entry, fn %{path: path} ->
|
||||||
File.cp!(path, tmp_path)
|
Task.Supervisor.start_child(LiveBeats.TaskSupervisor, fn ->
|
||||||
Ecto.Changeset.put_change(changeset, :tmp_mp3_path, tmp_path)
|
result = LiveBeats.MP3Stat.parse(path)
|
||||||
|
|
||||||
|
send_update(lv, __MODULE__,
|
||||||
|
id: socket.assigns.id,
|
||||||
|
action: {:duration, entry.ref, result}
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:postpone, :ok}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, put_new_changeset(socket, entry)}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp file_error(%{kind: :too_large} = assigns), do: ~H|larger than 10MB|
|
defp file_error(%{kind: :too_large} = assigns), do: ~H|larger than 10MB|
|
||||||
defp file_error(%{kind: :not_accepted} = assigns), do: ~H|not a valid MP3 file|
|
defp file_error(%{kind: :not_accepted} = assigns), do: ~H|not a valid MP3 file|
|
||||||
defp file_error(%{kind: :too_many_files} = assigns), do: ~H|too many files|
|
defp file_error(%{kind: :too_many_files} = assigns), do: ~H|too many files|
|
||||||
|
|
||||||
|
defp put_stats(socket, entry_ref, %MP3Stat{} = stat) do
|
||||||
|
if changeset = get_changeset(socket, entry_ref) do
|
||||||
|
update_changeset(socket, MediaLibrary.put_stats(changeset, stat), entry_ref)
|
||||||
|
else
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -63,19 +63,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- /upload -->
|
<!-- /upload -->
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-5">
|
|
||||||
<div class="flex justify-end">
|
|
||||||
<button type="button" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</.form>
|
</.form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -29,9 +29,6 @@ defmodule LiveBeatsWeb.Router do
|
||||||
live "/", HomeLive, :index
|
live "/", HomeLive, :index
|
||||||
live "/songs", SongLive.Index, :index
|
live "/songs", SongLive.Index, :index
|
||||||
live "/songs/new", SongLive.Index, :new
|
live "/songs/new", SongLive.Index, :new
|
||||||
live "/songs/:id/edit", SongLive.Index, :edit
|
|
||||||
live "/songs/:id", SongLive.Show, :show
|
|
||||||
live "/songs/:id/show/edit", SongLive.Show, :edit
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -7,5 +7,7 @@
|
||||||
phx-click="lv:clear-flash"
|
phx-click="lv:clear-flash"
|
||||||
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
|
phx-value-key="error"><%= live_flash(@flash, :error) %></p>
|
||||||
|
|
||||||
|
<.live_component module={LiveBeatsWeb.LayoutComponent} id="layout" />
|
||||||
|
|
||||||
<%= @inner_content %>
|
<%= @inner_content %>
|
||||||
</main>
|
</main>
|
||||||
|
|
1
mix.lock
1
mix.lock
|
@ -30,6 +30,7 @@
|
||||||
"plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
|
"plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},
|
||||||
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
|
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
|
||||||
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
|
||||||
|
"porcelain": {:hex, :porcelain, "2.0.3", "2d77b17d1f21fed875b8c5ecba72a01533db2013bd2e5e62c6d286c029150fdc", [:mix], [], "hexpm", "dc996ab8fadbc09912c787c7ab8673065e50ea1a6245177b0c24569013d23620"},
|
||||||
"postgrex": {:hex, :postgrex, "0.15.10", "2809dee1b1d76f7cbabe570b2a9285c2e7b41be60cf792f5f2804a54b838a067", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "1560ca427542f6b213f8e281633ae1a3b31cdbcd84ebd7f50628765b8f6132be"},
|
"postgrex": {:hex, :postgrex, "0.15.10", "2809dee1b1d76f7cbabe570b2a9285c2e7b41be60cf792f5f2804a54b838a067", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "1560ca427542f6b213f8e281633ae1a3b31cdbcd84ebd7f50628765b8f6132be"},
|
||||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||||
"rustler": {:hex, :rustler, "0.22.2", "f92d6dba71bef6fe5f0d955649cb071127adc92f32a78890e8fa9939e59a1b41", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.5.2", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "56b129141e86d60a2d670af9a2b55a9071e10933ef593034565af77e84655118"},
|
"rustler": {:hex, :rustler, "0.22.2", "f92d6dba71bef6fe5f0d955649cb071127adc92f32a78890e8fa9939e59a1b41", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.5.2", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "56b129141e86d60a2d670af9a2b55a9071e10933ef593034565af77e84655118"},
|
||||||
|
|
|
@ -14,7 +14,7 @@
|
||||||
# {:ok, _} = LiveBeats.MediaLibrary.create_genre(%{title: title})
|
# {:ok, _} = LiveBeats.MediaLibrary.create_genre(%{title: title})
|
||||||
# end
|
# end
|
||||||
|
|
||||||
for i <- 1..20 do
|
for i <- 1..200 do
|
||||||
{:ok, _} =
|
{:ok, _} =
|
||||||
LiveBeats.MediaLibrary.create_song(%{
|
LiveBeats.MediaLibrary.create_song(%{
|
||||||
artist: "Bonobo",
|
artist: "Bonobo",
|
||||||
|
|
Loading…
Reference in a new issue