This commit is contained in:
Chris McCord 2023-01-26 19:39:59 -05:00
parent b676f7aeb4
commit 7b43323813
6 changed files with 88 additions and 53 deletions

View file

@ -3,6 +3,7 @@ import {Socket} from "phoenix"
// import {LiveSocket} from "phoenix_live_view" // import {LiveSocket} from "phoenix_live_view"
import {LiveSocket} from "/Users/chris/oss/phoenix_live_view/assets/js/phoenix_live_view" import {LiveSocket} from "/Users/chris/oss/phoenix_live_view/assets/js/phoenix_live_view"
import topbar from "../vendor/topbar" import topbar from "../vendor/topbar"
import Sortable from "../vendor/sortable"
let nowSeconds = () => Math.round(Date.now() / 1000) let nowSeconds = () => Math.round(Date.now() / 1000)
let rand = (min, max) => Math.floor(Math.random() * (max - min) + min) let rand = (min, max) => Math.floor(Math.random() * (max - min) + min)

View file

@ -161,18 +161,24 @@ defmodule LiveBeats.MediaLibrary do
# refetch user for fresh song count # refetch user for fresh song count
user = Accounts.get_user!(user.id) user = Accounts.get_user!(user.id)
multi =
Ecto.Multi.new()
|> Ecto.Multi.run(:starting_position, fn repo, _changes ->
count = repo.one(from s in Song, where: s.user_id == ^user.id, select: count(s.id))
{:ok, count - 1}
end)
multi = multi =
changesets changesets
|> Enum.with_index() |> Enum.with_index()
|> Enum.reduce(Ecto.Multi.new(), fn {{ref, chset}, idx}, acc -> |> Enum.reduce(multi, fn {{ref, chset}, i}, acc ->
chset = Ecto.Multi.insert(acc, {:song, ref}, fn %{starting_position: pos_start} ->
chset chset
|> Song.put_user(user) |> Song.put_user(user)
|> Song.put_mp3_path() |> Song.put_mp3_path()
|> Song.put_server_ip() |> Song.put_server_ip()
|> Ecto.Changeset.put_change(:position, idx) |> Ecto.Changeset.put_change(:position, pos_start + i + 1)
end)
Ecto.Multi.insert(acc, {:song, ref}, chset)
end) end)
|> Ecto.Multi.run(:valid_songs_count, fn _repo, changes -> |> Ecto.Multi.run(:valid_songs_count, fn _repo, changes ->
new_songs_count = changes |> Enum.filter(&match?({{:song, _ref}, _}, &1)) |> Enum.count() new_songs_count = changes |> Enum.filter(&match?({{:song, _ref}, _}, &1)) |> Enum.count()
@ -244,7 +250,10 @@ defmodule LiveBeats.MediaLibrary do
end end
def list_profile_songs(%Profile{} = profile, limit \\ 100) do def list_profile_songs(%Profile{} = profile, limit \\ 100) do
from(s in Song, where: s.user_id == ^profile.user_id, limit: ^limit, order_by: [asc: :position]) from(s in Song,
where: s.user_id == ^profile.user_id,
limit: ^limit
)
|> order_by_playlist(:asc) |> order_by_playlist(:asc)
|> Repo.replica().all() |> Repo.replica().all()
end end
@ -323,7 +332,7 @@ defmodule LiveBeats.MediaLibrary do
def get_next_song(%Song{} = song, %Profile{} = profile) do def get_next_song(%Song{} = song, %Profile{} = profile) do
next = next =
from(s in Song, from(s in Song,
where: s.user_id == ^song.user_id and s.id > ^song.id, where: s.user_id == ^song.user_id and s.position > ^song.position,
limit: 1 limit: 1
) )
|> order_by_playlist(:asc) |> order_by_playlist(:asc)
@ -335,8 +344,7 @@ defmodule LiveBeats.MediaLibrary do
def get_prev_song(%Song{} = song, %Profile{} = profile) do def get_prev_song(%Song{} = song, %Profile{} = profile) do
prev = prev =
from(s in Song, from(s in Song,
where: s.user_id == ^song.user_id and s.id < ^song.id, where: s.user_id == ^song.user_id and s.position < ^song.position,
order_by: [desc: s.inserted_at, desc: s.id],
limit: 1 limit: 1
) )
|> order_by_playlist(:desc) |> order_by_playlist(:desc)
@ -356,26 +364,26 @@ defmodule LiveBeats.MediaLibrary do
_count -> {:error, :index_out_of_range} _count -> {:error, :index_out_of_range}
end end
end) end)
|> Ecto.Multi.update_all(:dec_positions, fn _ -> |> multi_update_all(:dec_positions, fn _ ->
from(s in Song, from(s in Song,
where: s.user_id == ^song.user_id and s.id != ^song.id, where: s.user_id == ^song.user_id and s.id != ^song.id,
where: s.position > ^old_index and s.position <= ^new_index, where: s.position > ^old_index and s.position <= ^new_index,
update: [inc: [position: -1]] update: [inc: [position: -1]]
) )
end, []) end)
|> Ecto.Multi.update_all(:inc_positions, fn _ -> |> multi_update_all(:inc_positions, fn _ ->
from(s in Song, from(s in Song,
where: s.user_id == ^song.user_id and s.id != ^song.id, where: s.user_id == ^song.user_id and s.id != ^song.id,
where: s.position < ^old_index and s.position >= ^new_index, where: s.position < ^old_index and s.position >= ^new_index,
update: [inc: [position: 1]] update: [inc: [position: 1]]
) )
end, []) end)
|> Ecto.Multi.update_all(:position, fn _ -> |> multi_update_all(:position, fn _ ->
from(s in Song, from(s in Song,
where: s.id == ^song.id, where: s.id == ^song.id,
update: [set: [position: ^new_index]] update: [set: [position: ^new_index]]
) )
end, []) end)
case LiveBeats.Repo.transaction(multi) do case LiveBeats.Repo.transaction(multi) do
{:ok, _} -> {:ok, _} ->
@ -395,14 +403,26 @@ defmodule LiveBeats.MediaLibrary do
def delete_song(%Song{} = song) do def delete_song(%Song{} = song) do
delete_song_file(song) delete_song_file(song)
old_index = song.position
Ecto.Multi.new() Ecto.Multi.new()
|> Ecto.Multi.delete(:delete, song) |> Ecto.Multi.delete(:delete, song)
|> multi_update_all(:dec_positions, fn _ ->
from(s in Song,
where: s.user_id == ^song.user_id,
where: s.position > ^old_index,
update: [inc: [position: -1]]
)
end)
|> update_user_songs_count(song.user_id, -1) |> update_user_songs_count(song.user_id, -1)
|> Repo.transaction() |> Repo.transaction()
|> case do |> case do
{:ok, _} -> :ok {:ok, _} ->
other -> other broadcast!(song.user_id, %Events.SongDeleted{song: song})
:ok
other ->
other
end end
end end
@ -493,7 +513,7 @@ defmodule LiveBeats.MediaLibrary do
end end
defp order_by_playlist(%Ecto.Query{} = query, direction) when direction in [:asc, :desc] do defp order_by_playlist(%Ecto.Query{} = query, direction) when direction in [:asc, :desc] do
from(s in query, order_by: [{^direction, s.inserted_at}, {^direction, s.id}]) from(s in query, order_by: [{^direction, s.position}])
end end
defp broadcast!(user_id, msg) when is_integer(user_id) do defp broadcast!(user_id, msg) when is_integer(user_id) do
@ -509,4 +529,8 @@ defmodule LiveBeats.MediaLibrary do
{:error, :songs_limit_exceeded} {:error, :songs_limit_exceeded}
end end
end end
defp multi_update_all(multi, name, func, opts \\ []) do
Ecto.Multi.update_all(multi, name, func, opts)
end
end end

View file

@ -18,4 +18,8 @@ defmodule LiveBeats.MediaLibrary.Events do
defmodule NewPosition do defmodule NewPosition do
defstruct song: nil defstruct song: nil
end end
defmodule SongDeleted do
defstruct song: nil
end
end end

View file

@ -554,6 +554,7 @@ defmodule LiveBeatsWeb.CoreComponents do
attr :id, :any, default: nil attr :id, :any, default: nil
attr :row_id, :any, default: false attr :row_id, :any, default: false
attr :row_click, :any, default: nil attr :row_click, :any, default: nil
attr :row_remove, :any, default: nil
attr :rows, :list, required: true attr :rows, :list, required: true
attr :streamable, :boolean, default: false attr :streamable, :boolean, default: false
attr :sortable_drop, :string, default: nil attr :sortable_drop, :string, default: nil
@ -585,10 +586,14 @@ defmodule LiveBeatsWeb.CoreComponents do
phx-hook={@sortable_drop && "Sortable"} phx-hook={@sortable_drop && "Sortable"}
data-drop={@sortable_drop} data-drop={@sortable_drop}
> >
<%= for {row, i} <- Enum.with_index(@rows) do %> <tr
<tr id={@row_id && @row_id.(row)} class="hover:bg-gray-50"> :for={{row, i} <- Enum.with_index(@rows)}
<%= for col <- @col do %> id={@row_id && @row_id.(row)}
phx-remove={@row_remove && @row_remove.(row)}
class="hover:bg-gray-50"
>
<td <td
:for={col <- @col}
phx-click={@row_click && @row_click.(row)} phx-click={@row_click && @row_click.(row)}
class={ class={
col[:class!] || col[:class!] ||
@ -599,9 +604,7 @@ defmodule LiveBeatsWeb.CoreComponents do
<%= render_slot(col, row) %> <%= render_slot(col, row) %>
</div> </div>
</td> </td>
<% end %>
</tr> </tr>
<% end %>
</tbody> </tbody>
</table> </table>
</div> </div>

View file

@ -9,7 +9,6 @@
<%= assigns[:page_title] || "LiveBeats" %> <%= assigns[:page_title] || "LiveBeats" %>
</.live_title> </.live_title>
<link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} /> <link phx-track-static rel="stylesheet" href={~p"/assets/app.css"} />
<script defer type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/Sortable/1.15.0/Sortable.min.js"></script>
<script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script> <script defer phx-track-static type="text/javascript" src={~p"/assets/app.js"}></script>
</head> </head>
<body> <body>

View file

@ -83,6 +83,7 @@ defmodule LiveBeatsWeb.ProfileLive do
rows={@songs} rows={@songs}
row_id={fn {id, _song} -> id end} row_id={fn {id, _song} -> id end}
row_click={fn {_id, song} -> JS.push("play_or_pause", value: %{id: song.id}) end} row_click={fn {_id, song} -> JS.push("play_or_pause", value: %{id: song.id}) end}
row_remove={fn {id, _song} -> hide("##{id}") end}
streamable streamable
sortable_drop="row_dropped" sortable_drop="row_dropped"
> >
@ -108,7 +109,7 @@ defmodule LiveBeatsWeb.ProfileLive do
role="button" role="button"
/> />
</span> </span>
<%= song.title %> <%= @count %> <%= song.title %>
</:col> </:col>
<:col :let={{_id, song}} label="Artist"><%= song.artist %></:col> <:col :let={{_id, song}} label="Artist"><%= song.artist %></:col>
<:col <:col
@ -153,7 +154,6 @@ defmodule LiveBeatsWeb.ProfileLive do
socket = socket =
socket socket
|> assign( |> assign(
count: 0,
active_song_id: active_song_id, active_song_id: active_song_id,
active_profile_id: current_user.active_profile_user_id, active_profile_id: current_user.active_profile_user_id,
profile: profile, profile: profile,
@ -247,7 +247,11 @@ defmodule LiveBeatsWeb.ProfileLive do
end end
def handle_info({MediaLibrary, %MediaLibrary.Events.SongsImported{songs: songs}}, socket) do def handle_info({MediaLibrary, %MediaLibrary.Events.SongsImported{songs: songs}}, socket) do
{:noreply, update(socket, :songs, &(&1 ++ songs))} {:noreply, Enum.reduce(songs, socket, fn song, acc -> stream_insert(acc, :songs, song) end)}
end
def handle_info({MediaLibrary, %MediaLibrary.Events.SongDeleted{song: song}}, socket) do
{:noreply, stream_delete(socket, :songs, song)}
end end
def handle_info({MediaLibrary, {:ping, ping}}, socket) do def handle_info({MediaLibrary, {:ping, ping}}, socket) do