live_beats/lib/live_beats_web/live/profile_live.ex

442 lines
13 KiB
Elixir
Raw Normal View History

2021-11-22 14:57:24 +00:00
defmodule LiveBeatsWeb.ProfileLive do
2021-10-29 16:12:23 +00:00
use LiveBeatsWeb, :live_view
2021-11-12 03:42:10 +00:00
alias LiveBeats.{Accounts, MediaLibrary, MP3Stat}
2021-11-16 16:58:22 +00:00
alias LiveBeatsWeb.{LayoutComponent, Presence}
2023-01-26 19:05:24 +00:00
alias LiveBeatsWeb.ProfileLive.{UploadFormComponent}
2021-10-29 16:12:23 +00:00
2022-01-31 19:27:06 +00:00
@max_presences 20
2021-10-29 16:12:23 +00:00
def render(assigns) do
~H"""
<.title_bar>
2021-11-16 16:58:22 +00:00
<div>
<div class="block">
2022-08-03 13:40:11 +00:00
<%= @profile.tagline %>
<%= if @owns_profile? do %>
(you)
<% end %>
2023-02-01 21:04:34 +00:00
<%= ngettext("%{count} song", "%{count} songs", @songs_count) %>
2021-11-16 16:58:22 +00:00
</div>
<.link href={@profile.external_homepage_url} target="_blank" class="text-sm text-gray-600">
2022-08-03 13:40:11 +00:00
<.icon name={:code} /> <span class=""><%= url_text(@profile.external_homepage_url) %></span>
2021-11-16 16:58:22 +00:00
</.link>
</div>
2021-10-29 16:12:23 +00:00
<:actions>
2021-11-12 03:42:10 +00:00
<%= if @active_profile_id == @profile.user_id do %>
2022-08-03 13:40:11 +00:00
<.button
primary
phx-click={
JS.push("switch_profile", value: %{user_id: nil}, target: "#player", loading: "#player")
}
2021-11-12 03:42:10 +00:00
>
2022-08-03 13:40:11 +00:00
<.icon name={:stop} /><span class="ml-2">Stop Listening</span>
2021-11-12 03:42:10 +00:00
</.button>
<% else %>
2022-08-03 13:40:11 +00:00
<.button
primary
phx-click={
JS.push("switch_profile",
value: %{user_id: @profile.user_id},
target: "#player",
loading: "#player"
)
}
2021-11-12 03:42:10 +00:00
>
2022-08-03 13:40:11 +00:00
<.icon name={:play} /><span class="ml-2">Listen</span>
2021-11-12 03:42:10 +00:00
</.button>
<% end %>
<%= if @owns_profile? do %>
2022-11-17 20:36:58 +00:00
<.button id="upload-btn" primary patch={profile_upload_path(@current_user)}>
2022-08-03 13:40:11 +00:00
<.icon name={:upload} /><span class="ml-2">Upload Songs</span>
2021-11-12 03:42:10 +00:00
</.button>
<% end %>
2021-10-29 16:12:23 +00:00
</:actions>
</.title_bar>
2023-03-09 14:29:22 +00:00
<div
id={"text-to-speech-#{@active_song_id}"}
phx-update="stream"
class="p-6 max-h-[200px] overflow-y-scroll"
>
<div :for={{id, segment} <- @streams.speech_segments} id={id}>
<span class="min-w-[40px] inline-block text-gray-400">
[<%= seconds_to_mm_ss(segment.start_time) %>]
</span>
<%= segment.text %>
</div>
</div>
2022-01-31 19:27:06 +00:00
<Presence.listening_now
2022-01-31 13:21:27 +00:00
presences={@presences}
2022-01-31 19:27:06 +00:00
presence_ids={@presence_ids}
total_count={@presences_count}
/>
2021-11-16 16:58:22 +00:00
2023-01-28 19:18:34 +00:00
<div id="dialogs" phx-update="stream">
<%= for {_id, song} <- if(@owns_profile?, do: @streams.songs, else: []), id = "delete-modal-#{song.id}" do %>
2021-12-16 02:51:09 +00:00
<.modal
id={id}
on_confirm={
JS.push("delete", value: %{id: song.id})
|> hide_modal(id)
|> focus_closest("#song-#{song.id}")
2022-02-07 17:03:35 +00:00
|> hide("#song-#{song.id}")
}
on_cancel={focus("##{id}", "#delete-song-#{song.id}")}
2021-12-16 02:51:09 +00:00
>
Are you sure you want to delete "<%= song.title %>"?
<:cancel>Cancel</:cancel>
<:confirm>Delete</:confirm>
</.modal>
<% end %>
</div>
2021-10-29 16:12:23 +00:00
2023-01-26 19:05:24 +00:00
<.table
2021-12-16 02:51:09 +00:00
id="songs"
2023-01-28 19:18:34 +00:00
rows={@streams.songs}
2023-01-26 19:05:24 +00:00
row_id={fn {id, _song} -> id end}
row_click={fn {_id, song} -> JS.push("play_or_pause", value: %{id: song.id}) end}
streamable
sortable_drop="row_dropped"
2021-11-05 00:49:19 +00:00
>
2023-01-28 19:18:34 +00:00
<:col
:let={{_id, song}}
label="Title"
2023-02-01 21:04:34 +00:00
class!="px-6 py-3 text-sm font-medium text-gray-900 min-w-[200px] md:min-w-[20rem] cursor-pointer"
2023-01-28 19:18:34 +00:00
>
2023-01-26 19:05:24 +00:00
<span :if={song.status == :playing} class="flex pt-1 relative mr-2 w-4">
<span class="w-3 h-3 animate-ping bg-purple-400 rounded-full absolute"></span>
<.icon name={:volume_up} class="h-5 w-5 -mt-1 -ml-1" aria-label="Playing" role="button" />
</span>
<span :if={song.status == :paused} class="flex pt-1 relative mr-2 w-4">
<.icon
name={:volume_up}
class="h-5 w-5 -mt-1 -ml-1 text-gray-400"
aria-label="Paused"
role="button"
/>
</span>
<span :if={song.status == :stopped} class="flex relative w-6 -translate-x-1">
<.icon
:if={@owns_profile?}
name={:play}
class="h-5 w-5 text-gray-400"
aria-label="Play"
role="button"
/>
</span>
2023-01-27 00:39:59 +00:00
<%= song.title %>
2023-01-26 19:05:24 +00:00
</:col>
<:col :let={{_id, song}} label="Artist"><%= song.artist %></:col>
2022-08-03 13:40:11 +00:00
<:col
2023-01-26 19:05:24 +00:00
:let={{_id, song}}
2022-08-03 13:40:11 +00:00
label="Attribution"
class="max-w-5xl break-words text-gray-600 font-light"
>
2023-02-01 21:04:34 +00:00
<%= song.attribution %>
2022-08-03 13:40:11 +00:00
</:col>
2023-01-26 19:05:24 +00:00
<:col :let={{_id, song}} label="Duration"><%= MP3Stat.to_mmss(song.duration) %></:col>
<:col :let={{_id, song}} :if={@owns_profile?} label="">
<.link
id={"delete-song-#{song.id}"}
phx-click={show_modal("delete-modal-#{song.id}")}
class="inline-flex items-center px-3 py-2 text-sm leading-4 font-medium"
>
2022-08-03 13:40:11 +00:00
<.icon name={:trash} class="-ml-0.5 mr-2 h-4 w-4" /> Delete
2021-11-05 00:49:19 +00:00
</.link>
2021-10-29 16:12:23 +00:00
</:col>
2023-01-26 19:05:24 +00:00
</.table>
2021-10-29 16:12:23 +00:00
"""
end
2021-11-12 03:42:10 +00:00
def mount(%{"profile_username" => profile_username}, _session, socket) do
2021-11-05 19:57:33 +00:00
%{current_user: current_user} = socket.assigns
2021-11-12 03:42:10 +00:00
profile =
Accounts.get_user_by!(username: profile_username)
|> MediaLibrary.get_profile!()
2021-11-05 00:49:19 +00:00
if connected?(socket) do
MediaLibrary.subscribe_to_profile(profile)
2021-11-12 03:42:10 +00:00
Accounts.subscribe(current_user.id)
2022-04-06 16:20:23 +00:00
Presence.subscribe(profile)
2021-11-05 00:49:19 +00:00
end
2021-11-05 12:57:48 +00:00
2023-03-09 14:29:22 +00:00
active_song = MediaLibrary.get_current_active_song(profile)
2023-03-09 14:34:43 +00:00
speech_segments = if active_song, do: active_song.speech_segments, else: []
2021-11-05 19:57:33 +00:00
2023-01-28 19:18:34 +00:00
songs = MediaLibrary.list_profile_songs(profile, 50)
2021-11-12 03:42:10 +00:00
socket =
socket
|> assign(
2023-03-09 14:29:22 +00:00
active_song_id: active_song && active_song.id,
2021-11-12 03:42:10 +00:00
active_profile_id: current_user.active_profile_user_id,
profile: profile,
2023-01-28 19:18:34 +00:00
owns_profile?: MediaLibrary.owns_profile?(current_user, profile),
songs_count: Enum.count(songs)
2021-11-12 03:42:10 +00:00
)
2023-01-28 19:18:34 +00:00
|> stream(:songs, songs)
2023-03-09 14:29:22 +00:00
|> stream(:speech_segments, speech_segments, dom_id: &"ss-#{&1.start_time}")
2021-11-16 16:58:22 +00:00
|> assign_presences()
2021-11-12 03:42:10 +00:00
2023-01-26 19:05:24 +00:00
{:ok, socket, temporary_assigns: [presences: %{}]}
2021-10-29 16:12:23 +00:00
end
def handle_params(params, _url, socket) do
2021-11-18 16:50:15 +00:00
LayoutComponent.hide_modal()
{:noreply, socket |> apply_action(socket.assigns.live_action, params)}
2021-11-05 00:49:19 +00:00
end
2021-11-05 19:57:33 +00:00
def handle_event("play_or_pause", %{"id" => id}, socket) do
song = MediaLibrary.get_song!(id)
2021-11-12 03:42:10 +00:00
can_playback? = MediaLibrary.can_control_playback?(socket.assigns.current_user, song)
cond do
can_playback? and socket.assigns.active_song_id == id and MediaLibrary.playing?(song) ->
MediaLibrary.pause_song(song)
can_playback? ->
MediaLibrary.play_song(id)
true ->
:noop
2021-11-05 19:57:33 +00:00
end
2021-11-05 00:49:19 +00:00
{:noreply, socket}
end
def handle_event("delete", %{"id" => id}, socket) do
song = MediaLibrary.get_song!(id)
2021-11-12 11:42:07 +00:00
2021-11-12 03:42:10 +00:00
if song.user_id == socket.assigns.current_user.id do
2021-12-14 20:29:07 +00:00
:ok = MediaLibrary.delete_song(song)
2021-11-12 03:42:10 +00:00
end
2021-11-12 11:42:07 +00:00
2023-03-09 14:29:22 +00:00
if song.id == socket.assigns.active_song_id do
{:noreply, assign(socket, :active_song_id, nil)}
else
{:noreply, socket}
end
2021-11-05 00:49:19 +00:00
end
2023-01-26 19:05:24 +00:00
def handle_event("row_dropped", %{"id" => dom_id, "old" => old_idx, "new" => new_idx}, socket) do
"songs-" <> id = dom_id
song = MediaLibrary.get_song!(id)
2023-01-26 19:05:24 +00:00
if song.user_id == socket.assigns.current_user.id and song.position == old_idx do
:ok = MediaLibrary.update_song_position(song, new_idx)
{:noreply, socket}
else
{:noreply, socket}
end
end
2022-04-06 16:20:23 +00:00
def handle_info({LiveBeatsWeb.Presence, %{user_joined: presence}}, socket) do
2022-01-31 19:27:06 +00:00
{:noreply, assign_presence(socket, presence)}
end
2022-04-06 16:20:23 +00:00
def handle_info({LiveBeatsWeb.Presence, %{user_left: presence}}, socket) do
%{user: user} = presence
2022-01-31 13:21:27 +00:00
if presence.metas == [] do
2022-01-31 19:27:06 +00:00
{:noreply, remove_presence(socket, user)}
else
{:noreply, socket}
end
end
def handle_info({Accounts, %Accounts.Events.ActiveProfileChanged{} = event}, socket) do
{:noreply, assign(socket, active_profile_id: event.new_profile_user_id)}
2021-11-12 03:42:10 +00:00
end
def handle_info({MediaLibrary, %MediaLibrary.Events.PublicProfileUpdated{} = update}, socket) do
{:noreply,
socket
|> assign(profile: update.profile)
|> push_patch(to: profile_path(update.profile))}
end
2023-01-26 19:05:24 +00:00
def handle_info({MediaLibrary, %MediaLibrary.Events.NewPosition{song: song}}, socket) do
{:noreply, stream_insert(socket, :songs, song, at: song.position)}
end
def handle_info({MediaLibrary, %MediaLibrary.Events.Play{song: song}}, socket) do
2021-11-05 00:49:19 +00:00
{:noreply, play_song(socket, song)}
end
def handle_info({MediaLibrary, %MediaLibrary.Events.Pause{song: song}}, socket) do
2023-01-26 19:05:24 +00:00
{:noreply, pause_song(socket, song)}
2021-11-05 00:49:19 +00:00
end
2021-12-16 02:51:09 +00:00
def handle_info({MediaLibrary, %MediaLibrary.Events.SongsImported{songs: songs}}, socket) do
2023-03-09 14:29:22 +00:00
%{current_user: current_user, active_song_id: active_song_id} = socket.assigns
first = hd(songs)
if !active_song_id && MediaLibrary.can_control_playback?(current_user, first) do
MediaLibrary.play_song(first.id)
end
2023-01-28 19:18:34 +00:00
{:noreply,
Enum.reduce(songs, socket, fn song, acc ->
acc
|> update(:songs_count, &(&1 + 1))
|> stream_insert(:songs, song)
end)}
2023-01-27 00:39:59 +00:00
end
2023-03-09 14:29:22 +00:00
def handle_info(
{MediaLibrary, %MediaLibrary.Events.SpeechToText{song_id: id, segment: segment}},
socket
) do
if socket.assigns.active_song_id == id do
{:noreply, stream_insert(socket, :speech_segments, segment)}
else
{:noreply, socket}
end
end
2023-01-27 00:39:59 +00:00
def handle_info({MediaLibrary, %MediaLibrary.Events.SongDeleted{song: song}}, socket) do
2023-01-28 19:18:34 +00:00
{:noreply,
socket
|> update(:songs_count, &(&1 - 1))
|> stream_delete(:songs, song)}
2021-12-16 02:51:09 +00:00
end
2022-01-31 13:21:27 +00:00
def handle_info({MediaLibrary, {:ping, ping}}, socket) do
%{user: user, rtt: rtt, region: region} = ping
2022-01-31 19:27:06 +00:00
send_update(Presence.BadgeComponent,
id: user.id,
2022-01-31 13:21:27 +00:00
action: {:ping, %{user: user, ping: rtt, region: region}}
)
2022-01-29 01:40:48 +00:00
{:noreply, socket}
end
def handle_info({MediaLibrary, _}, socket), do: {:noreply, socket}
def handle_info({Accounts, _}, socket), do: {:noreply, socket}
2021-11-05 19:57:33 +00:00
defp stop_song(socket, song_id) do
2023-01-26 19:05:24 +00:00
song = MediaLibrary.get_song!(song_id)
2021-11-05 19:57:33 +00:00
2023-01-26 19:05:24 +00:00
socket =
if socket.assigns.active_song_id == song_id do
assign(socket, :active_song_id, nil)
else
socket
end
stream_insert(socket, :songs, %MediaLibrary.Song{song | status: :stopped})
2021-11-05 19:57:33 +00:00
end
2023-01-26 19:05:24 +00:00
defp pause_song(socket, %MediaLibrary.Song{} = song) do
stream_insert(socket, :songs, %MediaLibrary.Song{song | status: :paused})
2021-11-05 00:49:19 +00:00
end
2021-11-05 19:57:33 +00:00
defp play_song(socket, %MediaLibrary.Song{} = song) do
2021-11-12 03:42:10 +00:00
%{active_song_id: active_song_id} = socket.assigns
2021-11-05 00:49:19 +00:00
2021-11-05 19:57:33 +00:00
cond do
2021-11-12 03:42:10 +00:00
active_song_id == song.id ->
2023-01-26 19:05:24 +00:00
stream_insert(socket, :songs, %MediaLibrary.Song{song | status: :playing})
2021-11-05 19:57:33 +00:00
2021-11-12 03:42:10 +00:00
active_song_id ->
2023-03-09 14:34:43 +00:00
Enum.reduce(song.speech_segments, socket, fn seg, acc ->
2023-03-09 14:29:22 +00:00
stream_insert(acc, :speech_segments, seg)
end)
2021-11-12 03:42:10 +00:00
|> stop_song(active_song_id)
2023-01-26 19:05:24 +00:00
|> stream_insert(:songs, %MediaLibrary.Song{song | status: :playing})
2021-11-12 03:42:10 +00:00
|> assign(active_song_id: song.id)
2021-11-05 19:57:33 +00:00
true ->
2023-01-26 19:05:24 +00:00
socket
|> stream_insert(:songs, %MediaLibrary.Song{song | status: :playing})
|> assign(active_song_id: song.id)
2021-11-05 00:49:19 +00:00
end
end
2021-11-18 16:50:15 +00:00
defp apply_action(socket, :new, _params) do
if socket.assigns.owns_profile? do
socket
|> assign(:page_title, "Add Music")
2021-11-18 16:50:15 +00:00
|> assign(:song, %MediaLibrary.Song{})
|> show_upload_modal()
2021-11-05 00:49:19 +00:00
else
2021-11-18 16:50:15 +00:00
socket
|> put_flash(:error, "You can't do that")
|> redirect(to: profile_path(socket.assigns.current_user))
2021-11-05 00:49:19 +00:00
end
2021-10-29 16:12:23 +00:00
end
2021-12-14 15:35:51 +00:00
defp apply_action(socket, :show, _params) do
2021-10-29 16:12:23 +00:00
socket
|> assign(:page_title, "Listing Songs")
|> assign(:song, nil)
end
2021-11-18 16:50:15 +00:00
defp show_upload_modal(socket) do
LayoutComponent.show_modal(UploadFormComponent, %{
id: :new,
confirm: {"Save", type: "submit", form: "song-form"},
2021-11-23 14:34:34 +00:00
patch: profile_path(socket.assigns.current_user),
2021-11-18 16:50:15 +00:00
song: socket.assigns.song,
title: socket.assigns.page_title,
current_user: socket.assigns.current_user
})
socket
end
2021-11-16 16:58:22 +00:00
defp assign_presences(socket) do
2022-01-31 19:27:06 +00:00
socket = assign(socket, presences_count: 0, presences: %{}, presence_ids: %{})
2022-01-31 19:27:06 +00:00
if profile = connected?(socket) && socket.assigns.profile do
profile
2022-04-06 16:20:23 +00:00
|> LiveBeatsWeb.Presence.list_profile_users()
2022-01-31 19:27:06 +00:00
|> Enum.reduce(socket, fn {_, presence}, acc -> assign_presence(acc, presence) end)
2022-01-12 17:27:30 +00:00
else
2022-01-31 19:27:06 +00:00
socket
end
end
defp assign_presence(socket, presence) do
%{user: user} = presence
%{presence_ids: presence_ids} = socket.assigns
cond do
Map.has_key?(presence_ids, user.id) ->
socket
Enum.count(presence_ids) < @max_presences ->
socket
|> update(:presences, &Map.put(&1, user.id, user))
|> update(:presence_ids, &Map.put(&1, user.id, System.system_time()))
|> update(:presences_count, &(&1 + 1))
true ->
update(socket, :presences_count, &(&1 + 1))
2022-01-12 17:27:30 +00:00
end
2021-11-16 16:58:22 +00:00
end
2022-01-31 19:27:06 +00:00
defp remove_presence(socket, user) do
socket
|> update(:presences, &Map.delete(&1, user.id))
|> update(:presence_ids, &Map.delete(&1, user.id))
|> update(:presences_count, &(&1 - 1))
end
2021-11-16 16:58:22 +00:00
defp url_text(nil), do: ""
2021-11-18 16:50:15 +00:00
2021-11-16 16:58:22 +00:00
defp url_text(url_str) do
uri = URI.parse(url_str)
uri.host <> uri.path
end
2023-03-09 14:29:22 +00:00
defp seconds_to_mm_ss(seconds) do
seconds |> trunc() |> Time.from_seconds_after_midnight() |> Calendar.strftime("%M:%S")
end
2021-10-29 16:12:23 +00:00
end