diff --git a/lib/music_library_web/live/artist_live/show.ex b/lib/music_library_web/live/artist_live/show.ex
index 0eca1b14..a011e592 100644
--- a/lib/music_library_web/live/artist_live/show.ex
+++ b/lib/music_library_web/live/artist_live/show.ex
@@ -82,6 +82,384 @@ defmodule MusicLibraryWeb.ArtistLive.Show do
"""
end
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+
+
+ {@artist.name}
+ <.country_flag country={@country} />
+
+
+
+ <.button_group>
+ <.button
+ variant="soft"
+ phx-click={MusicLibraryWeb.Components.Notes.open("artist-notes-sheet")}
+ >
+ {gettext("Open Notes")}
+ <.icon
+ name="hero-pencil"
+ class="h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+ <.button
+ variant="soft"
+ phx-click={MusicLibraryWeb.Components.Chat.open("artist-chat-sheet")}
+ >
+ {gettext("Chat about artist")}
+ <.icon
+ name="hero-chat-bubble-left-right"
+ class="h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+ <.dropdown id={"actions-#{@artist.musicbrainz_id}"} placement="bottom-end">
+ <:toggle>
+ <.button variant="soft">
+ {gettext("Actions")}
+ <.icon
+ name="hero-ellipsis-vertical"
+ class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+
+ <.focus_wrap id={"actions-#{@artist.musicbrainz_id}-focus-wrap"}>
+ <.dropdown_link
+ id={"actions-#{@artist.musicbrainz_id}-edit"}
+ patch={~p"/artists/#{@artist.musicbrainz_id}/edit"}
+ >
+ <.icon
+ name="hero-pencil-square"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Edit")}
+
+ <.dropdown_link
+ id={"actions-#{@artist.musicbrainz_id}-refresh-image"}
+ phx-click={
+ JS.push("refresh_artist_image", value: %{id: @artist.musicbrainz_id})
+ }
+ >
+ <.icon
+ name="hero-photo"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Refresh image")}
+
+
+ <.dropdown_link
+ id={"actions-#{@artist.musicbrainz_id}-refresh-artist-info"}
+ phx-click={JS.push("refresh_artist_info", value: %{id: @artist.musicbrainz_id})}
+ >
+ <.icon
+ name="hero-arrow-path"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Refresh info")}
+
+ <.dropdown_link
+ id={"actions-#{@artist.musicbrainz_id}-refresh-wikipedia"}
+ phx-click={
+ JS.push("refresh_wikipedia_data", value: %{id: @artist.musicbrainz_id})
+ }
+ >
+ <.icon
+ name="hero-arrow-path"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Refresh Wikipedia")}
+
+
+
+
+
+
+
+
+ <.async_result :let={lastfm_artist_info} assign={@lastfm_artist_info}>
+ <:loading>
+
{gettext("Loading play count")}
+ <.loading />
+
+ <:failed :let={_failure}>
+
+ <.icon
+ name="hero-exclamation-triangle"
+ class="-mt-1 mr-1 ml-2 h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Error loading play count")}
+
+
+ <.on_tour_link lastfm_artist_info={lastfm_artist_info} />
+ <.play_count play_count={lastfm_artist_info.play_count} />
+
+
+
+
+
+
+
+ {gettext("Meta")}
+
+ <.artist_image
+ class="w-full rounded-md shadow-sm mt-4 cursor-pointer"
+ artist={@artist}
+ image_hash={@artist_info.image_data_hash}
+ phx-click={Fluxon.open_dialog("artist-image-modal")}
+ />
+ <.modal
+ id="artist-image-modal"
+ class="mx-auto sm:min-w-2xl max-w-sm md:max-w-3xl lg:max-w-5xl mt-8"
+ placement="center"
+ open={false}
+ >
+ <.artist_image
+ class="w-full rounded-md shadow-sm mt-8"
+ artist={@artist}
+ image_hash={@artist_info.image_data_hash}
+ />
+
+
+
+ {gettext("MusicBrainz ID")}
+
+
+
+
+ {@artist.musicbrainz_id}
+
+
+ <.copy_to_clipboard
+ target_id={"mb-#{@artist.musicbrainz_id}"}
+ label={gettext("Copy MusicBrainz ID to clipboard")}
+ />
+
+
+ <%= if @biography do %>
+
+ {gettext("Biography")}
+ <.badge variant="soft" class="ml-1">{@biography.source}
+
+
+
+ {@biography.description}
+
+ {@biography.summary_html}
+ <.link
+ class="block mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-400"
+ phx-click={Fluxon.open_dialog("bio")}
+ >
+ <.icon
+ name="hero-arrow-right-end-on-rectangle"
+ class="-mt-1 mr-1 h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Read more")}
+
+
+ <.sheet
+ id="bio"
+ class="max-w-2xl text-zinc-700 dark:text-zinc-300"
+ placement="left"
+ >
+
+ {Phoenix.HTML.raw(@biography.bio_html)}
+
+
+ {gettext("Read full article on Wikipedia")}
+ <.icon
+ name="hero-arrow-top-right-on-square"
+ class="-mt-1 ml-1 h-4 w-4"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+
+ <% else %>
+ <.async_result :let={lastfm_artist_info} assign={@lastfm_artist_info}>
+ <:loading>
+
+ {gettext("Loading biography")}
+
+
+ <:failed :let={_failure}>
+
+ <.icon
+ name="hero-exclamation-triangle"
+ class="-mt-1 mr-1 h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Error loading biography")}
+
+
+
+ {gettext("Biography")}
+
+
+ {remove_read_more_link(lastfm_artist_info.summary)}
+ <.link
+ class="block mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-400"
+ phx-click={Fluxon.open_dialog("lastfm-bio")}
+ >
+ <.icon
+ name="hero-arrow-right-end-on-rectangle"
+ class="-mt-1 mr-1 h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Read more")}
+
+
+ <.sheet
+ :if={lastfm_artist_info.bio not in [nil, ""]}
+ id="lastfm-bio"
+ class="max-w-2xl text-zinc-700 dark:text-zinc-300"
+ placement="left"
+ >
+ {render_bio(lastfm_artist_info.bio)}
+
+
+ <% end %>
+ <.external_links external_links={@external_links} />
+
+
+ <.record_grid
+ :if={@collection_records_count > 0}
+ title={gettext("Collection")}
+ id="collection"
+ records={@streams.collection_records}
+ records_count={@collection_records_count}
+ record_show_path={fn record -> ~p"/collection/#{record}" end}
+ record_edit_path={fn record -> ~p"/collection/#{record}/show/edit" end}
+ />
+ <.separator
+ :if={@collection_records_count > 0 && @wishlist_records_count > 0}
+ class="mt-8 mb-8"
+ />
+ <.record_grid
+ :if={@wishlist_records_count > 0}
+ title={gettext("Wishlist")}
+ id="wishlist"
+ records={@streams.wishlist_records}
+ records_count={@wishlist_records_count}
+ record_show_path={fn record -> ~p"/wishlist/#{record}" end}
+ record_edit_path={fn record -> ~p"/wishlist/#{record}/show/edit" end}
+ />
+
+ <.async_result :let={similar_artists} assign={@similar_artists}>
+ <:loading>
+
+ {gettext("Loading similar artists")}
+
+
+ <:failed :let={_failure}>
+
+ <.icon
+ name="hero-exclamation-triangle"
+ class="-mt-1 mr-1 h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Error loading similar artists")}
+
+
+ <.separator class="mt-8 mb-8" />
+ <.artist_grid title={gettext("Similar artists")} artists={similar_artists} />
+
+
+
+
+ <.json_viewer
+ :if={@artist_info.musicbrainz_data}
+ title={gettext("MusicBrainz data")}
+ data={@artist_info.musicbrainz_data}
+ />
+
+ <.json_viewer
+ :if={@artist_info.discogs_data}
+ title={gettext("Discogs data")}
+ data={@artist_info.discogs_data}
+ />
+
+ <.json_viewer
+ :if={@artist_info.wikipedia_data != %{}}
+ title={gettext("Wikipedia data")}
+ data={@artist_info.wikipedia_data}
+ />
+
+
+ <.live_component
+ id="artist-notes"
+ sheet_id="artist-notes-sheet"
+ module={MusicLibraryWeb.Components.Notes}
+ entity={:artist}
+ musicbrainz_id={@artist.musicbrainz_id}
+ />
+
+ <.live_component
+ id="artist-chat"
+ sheet_id="artist-chat-sheet"
+ module={MusicLibraryWeb.Components.Chat}
+ title={@artist.name}
+ chat_module={MusicLibrary.ArtistChat}
+ chat_context={{@artist, @artist_info}}
+ placeholder={gettext("Ask about this artist...")}
+ empty_prompt={gettext("Ask anything about this artist")}
+ />
+
+ <.structured_modal
+ :if={@live_action == :edit}
+ id="artist-info-modal"
+ on_close={JS.patch(~p"/artists/#{@artist.musicbrainz_id}")}
+ >
+ <.live_component
+ module={MusicLibraryWeb.ArtistLive.Form}
+ id={@artist_info.id}
+ action={@live_action}
+ artist_info={@artist_info}
+ artist={@artist}
+ patch={~p"/artists/#{@artist.musicbrainz_id}"}
+ />
+
+
+ """
+ end
+
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
diff --git a/lib/music_library_web/live/artist_live/show.html.heex b/lib/music_library_web/live/artist_live/show.html.heex
deleted file mode 100644
index bddec309..00000000
--- a/lib/music_library_web/live/artist_live/show.html.heex
+++ /dev/null
@@ -1,372 +0,0 @@
-
-
-
-
-
- {@artist.name}
- <.country_flag country={@country} />
-
-
-
- <.button_group>
- <.button
- variant="soft"
- phx-click={MusicLibraryWeb.Components.Notes.open("artist-notes-sheet")}
- >
- {gettext("Open Notes")}
- <.icon
- name="hero-pencil"
- class="h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
-
- <.button
- variant="soft"
- phx-click={MusicLibraryWeb.Components.Chat.open("artist-chat-sheet")}
- >
- {gettext("Chat about artist")}
- <.icon
- name="hero-chat-bubble-left-right"
- class="h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
-
- <.dropdown id={"actions-#{@artist.musicbrainz_id}"} placement="bottom-end">
- <:toggle>
- <.button variant="soft">
- {gettext("Actions")}
- <.icon
- name="hero-ellipsis-vertical"
- class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
- aria-hidden="true"
- data-slot="icon"
- />
-
-
- <.focus_wrap id={"actions-#{@artist.musicbrainz_id}-focus-wrap"}>
- <.dropdown_link
- id={"actions-#{@artist.musicbrainz_id}-edit"}
- patch={~p"/artists/#{@artist.musicbrainz_id}/edit"}
- >
- <.icon
- name="hero-pencil-square"
- class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Edit")}
-
- <.dropdown_link
- id={"actions-#{@artist.musicbrainz_id}-refresh-image"}
- phx-click={
- JS.push("refresh_artist_image", value: %{id: @artist.musicbrainz_id})
- }
- >
- <.icon
- name="hero-photo"
- class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Refresh image")}
-
-
- <.dropdown_link
- id={"actions-#{@artist.musicbrainz_id}-refresh-artist-info"}
- phx-click={JS.push("refresh_artist_info", value: %{id: @artist.musicbrainz_id})}
- >
- <.icon
- name="hero-arrow-path"
- class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Refresh info")}
-
- <.dropdown_link
- id={"actions-#{@artist.musicbrainz_id}-refresh-wikipedia"}
- phx-click={
- JS.push("refresh_wikipedia_data", value: %{id: @artist.musicbrainz_id})
- }
- >
- <.icon
- name="hero-arrow-path"
- class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Refresh Wikipedia")}
-
-
-
-
-
-
-
-
- <.async_result :let={lastfm_artist_info} assign={@lastfm_artist_info}>
- <:loading>
-
{gettext("Loading play count")}
- <.loading />
-
- <:failed :let={_failure}>
-
- <.icon
- name="hero-exclamation-triangle"
- class="-mt-1 mr-1 ml-2 h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Error loading play count")}
-
-
- <.on_tour_link lastfm_artist_info={lastfm_artist_info} />
- <.play_count play_count={lastfm_artist_info.play_count} />
-
-
-
-
-
-
-
- {gettext("Meta")}
-
- <.artist_image
- class="w-full rounded-md shadow-sm mt-4 cursor-pointer"
- artist={@artist}
- image_hash={@artist_info.image_data_hash}
- phx-click={Fluxon.open_dialog("artist-image-modal")}
- />
- <.modal
- id="artist-image-modal"
- class="mx-auto sm:min-w-2xl max-w-sm md:max-w-3xl lg:max-w-5xl mt-8"
- placement="center"
- open={false}
- >
- <.artist_image
- class="w-full rounded-md shadow-sm mt-8"
- artist={@artist}
- image_hash={@artist_info.image_data_hash}
- />
-
-
-
- {gettext("MusicBrainz ID")}
-
-
-
-
- {@artist.musicbrainz_id}
-
-
- <.copy_to_clipboard
- target_id={"mb-#{@artist.musicbrainz_id}"}
- label={gettext("Copy MusicBrainz ID to clipboard")}
- />
-
-
- <%= if @biography do %>
-
- {gettext("Biography")}
- <.badge variant="soft" class="ml-1">{@biography.source}
-
-
-
- {@biography.description}
-
- {@biography.summary_html}
- <.link
- class="block mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-400"
- phx-click={Fluxon.open_dialog("bio")}
- >
- <.icon
- name="hero-arrow-right-end-on-rectangle"
- class="-mt-1 mr-1 h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Read more")}
-
-
- <.sheet
- id="bio"
- class="max-w-2xl text-zinc-700 dark:text-zinc-300"
- placement="left"
- >
-
- {Phoenix.HTML.raw(@biography.bio_html)}
-
-
- {gettext("Read full article on Wikipedia")}
- <.icon
- name="hero-arrow-top-right-on-square"
- class="-mt-1 ml-1 h-4 w-4"
- aria-hidden="true"
- data-slot="icon"
- />
-
-
- <% else %>
- <.async_result :let={lastfm_artist_info} assign={@lastfm_artist_info}>
- <:loading>
-
- {gettext("Loading biography")}
-
-
- <:failed :let={_failure}>
-
- <.icon
- name="hero-exclamation-triangle"
- class="-mt-1 mr-1 h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Error loading biography")}
-
-
-
- {gettext("Biography")}
-
-
- {remove_read_more_link(lastfm_artist_info.summary)}
- <.link
- class="block mt-2 text-sm font-medium text-zinc-900 dark:text-zinc-400"
- phx-click={Fluxon.open_dialog("lastfm-bio")}
- >
- <.icon
- name="hero-arrow-right-end-on-rectangle"
- class="-mt-1 mr-1 h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Read more")}
-
-
- <.sheet
- :if={lastfm_artist_info.bio not in [nil, ""]}
- id="lastfm-bio"
- class="max-w-2xl text-zinc-700 dark:text-zinc-300"
- placement="left"
- >
- {render_bio(lastfm_artist_info.bio)}
-
-
- <% end %>
- <.external_links external_links={@external_links} />
-
-
- <.record_grid
- :if={@collection_records_count > 0}
- title={gettext("Collection")}
- id="collection"
- records={@streams.collection_records}
- records_count={@collection_records_count}
- record_show_path={fn record -> ~p"/collection/#{record}" end}
- record_edit_path={fn record -> ~p"/collection/#{record}/show/edit" end}
- />
- <.separator
- :if={@collection_records_count > 0 && @wishlist_records_count > 0}
- class="mt-8 mb-8"
- />
- <.record_grid
- :if={@wishlist_records_count > 0}
- title={gettext("Wishlist")}
- id="wishlist"
- records={@streams.wishlist_records}
- records_count={@wishlist_records_count}
- record_show_path={fn record -> ~p"/wishlist/#{record}" end}
- record_edit_path={fn record -> ~p"/wishlist/#{record}/show/edit" end}
- />
-
- <.async_result :let={similar_artists} assign={@similar_artists}>
- <:loading>
-
- {gettext("Loading similar artists")}
-
-
- <:failed :let={_failure}>
-
- <.icon
- name="hero-exclamation-triangle"
- class="-mt-1 mr-1 h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Error loading similar artists")}
-
-
- <.separator class="mt-8 mb-8" />
- <.artist_grid title={gettext("Similar artists")} artists={similar_artists} />
-
-
-
-
- <.json_viewer
- :if={@artist_info.musicbrainz_data}
- title={gettext("MusicBrainz data")}
- data={@artist_info.musicbrainz_data}
- />
-
- <.json_viewer
- :if={@artist_info.discogs_data}
- title={gettext("Discogs data")}
- data={@artist_info.discogs_data}
- />
-
- <.json_viewer
- :if={@artist_info.wikipedia_data != %{}}
- title={gettext("Wikipedia data")}
- data={@artist_info.wikipedia_data}
- />
-
-
- <.live_component
- id="artist-notes"
- sheet_id="artist-notes-sheet"
- module={MusicLibraryWeb.Components.Notes}
- entity={:artist}
- musicbrainz_id={@artist.musicbrainz_id}
- />
-
- <.live_component
- id="artist-chat"
- sheet_id="artist-chat-sheet"
- module={MusicLibraryWeb.Components.Chat}
- title={@artist.name}
- chat_module={MusicLibrary.ArtistChat}
- chat_context={{@artist, @artist_info}}
- placeholder={gettext("Ask about this artist...")}
- empty_prompt={gettext("Ask anything about this artist")}
- />
-
- <.structured_modal
- :if={@live_action == :edit}
- id="artist-info-modal"
- on_close={JS.patch(~p"/artists/#{@artist.musicbrainz_id}")}
- >
- <.live_component
- module={MusicLibraryWeb.ArtistLive.Form}
- id={@artist_info.id}
- action={@live_action}
- artist_info={@artist_info}
- artist={@artist}
- patch={~p"/artists/#{@artist.musicbrainz_id}"}
- />
-
-
diff --git a/lib/music_library_web/live/collection_live/index.ex b/lib/music_library_web/live/collection_live/index.ex
index 7957ac5a..ffeed660 100644
--- a/lib/music_library_web/live/collection_live/index.ex
+++ b/lib/music_library_web/live/collection_live/index.ex
@@ -17,6 +17,176 @@ defmodule MusicLibraryWeb.CollectionLive.Index do
order: :purchase
}
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ <.search_form query={@record_list_params.query} />
+ <.button_group>
+ <.button
+ variant="solid"
+ size="sm"
+ patch={~p"/collection/import"}
+ >
+ <.icon name="hero-plus" class="icon" aria-hidden="true" data-slot="icon" />
+ {gettext("Add")}
+
+ <.button
+ variant="solid"
+ size="sm"
+ patch={~p"/collection/scan"}
+ >
+ <.barcode_icon class="icon fill-current" />
+ {gettext("Scan")}
+
+
+
+
+
+
+ <.button_group>
+ <.button
+ patch={order_path(@record_list_params, :purchase)}
+ size="sm"
+ class={[
+ @record_list_params.order == :purchase && "bg-zinc-100! dark:bg-zinc-700!"
+ ]}
+ >
+ <.icon
+ name="hero-banknotes-solid"
+ class="icon"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Purchase")}
+
+ <.button
+ patch={order_path(@record_list_params, :alphabetical)}
+ size="sm"
+ class={[
+ @record_list_params.order == :alphabetical && "bg-zinc-100! dark:bg-zinc-700!"
+ ]}
+ >
+ <.icon name="hero-user-solid" class="icon" aria-hidden="true" data-slot="icon" />
+ {gettext("A->Z")}
+
+ <.button
+ patch={order_path(@record_list_params, :release)}
+ size="sm"
+ class={[
+ @record_list_params.order == :release && "bg-zinc-100! dark:bg-zinc-700!"
+ ]}
+ >
+ <.icon name="hero-calendar-days" class="icon" aria-hidden="true" data-slot="icon" />
+ {gettext("Release")}
+
+
+
+ <.button_group>
+ <.button
+ phx-click="set_display"
+ phx-value-mode="grid"
+ size="sm"
+ class={[
+ @display == :grid && "bg-zinc-100! dark:bg-zinc-700!"
+ ]}
+ >
+ <.icon
+ name="hero-squares-2x2"
+ class="icon"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+ {gettext("Grid")}
+
+
+ <.button
+ phx-click="set_display"
+ phx-value-mode="list"
+ size="sm"
+ class={[
+ @display == :list && "bg-zinc-100! dark:bg-zinc-700!"
+ ]}
+ >
+ <.icon name="hero-list-bullet" class="icon" aria-hidden="true" data-slot="icon" />
+
+ {gettext("List")}
+
+
+
+
+
+ <.record_grid
+ :if={@display == :grid}
+ id="collection"
+ records={@streams.records}
+ record_show_path={fn record -> ~p"/collection/#{record}" end}
+ record_edit_path={fn record -> ~p"/collection/#{record}/edit" end}
+ display_artist_names
+ density={:high}
+ />
+
+ <.record_list
+ :if={@display == :list}
+ records={@streams.records}
+ record_show_path={fn record -> ~p"/collection/#{record}" end}
+ record_edit_path={fn record -> ~p"/collection/#{record}/edit" end}
+ />
+
+ <.structured_modal
+ :if={@live_action == :edit}
+ id="record-modal"
+ on_close={JS.patch(back_path(@record_list_params))}
+ >
+ <.live_component
+ module={MusicLibraryWeb.Components.RecordForm}
+ id={@record.id}
+ action={@live_action}
+ show_purchased_at={true}
+ record={@record}
+ patch={back_path(@record_list_params)}
+ />
+
+
+ <.structured_modal
+ :if={@live_action == :import}
+ id="record-modal"
+ on_close={JS.patch(back_path(@record_list_params))}
+ >
+ <.live_component
+ module={MusicLibraryWeb.Components.AddRecord}
+ id={:search}
+ title={@page_title}
+ action={@live_action}
+ record={@record}
+ patch={back_path(@record_list_params)}
+ initial_query=""
+ icon_name="hero-plus"
+ />
+
+
+ <.structured_modal
+ :if={@live_action == :barcode_scan}
+ id="barcode-scanner-modal"
+ on_close={JS.patch(back_path(@record_list_params))}
+ >
+ <.live_component
+ module={MusicLibraryWeb.Components.BarcodeScanner}
+ id={:barcode_scanner}
+ title={@page_title}
+ action={@live_action}
+ patch={back_path(@record_list_params)}
+ />
+
+
+ <.pagination id={:bottom_pagination} pagination_params={@record_list_params} />
+
+ """
+ end
+
@impl true
def mount(_params, _session, socket) do
{:ok,
diff --git a/lib/music_library_web/live/collection_live/index.html.heex b/lib/music_library_web/live/collection_live/index.html.heex
deleted file mode 100644
index 69102149..00000000
--- a/lib/music_library_web/live/collection_live/index.html.heex
+++ /dev/null
@@ -1,164 +0,0 @@
-
-
-
- <.search_form query={@record_list_params.query} />
- <.button_group>
- <.button
- variant="solid"
- size="sm"
- patch={~p"/collection/import"}
- >
- <.icon name="hero-plus" class="icon" aria-hidden="true" data-slot="icon" />
- {gettext("Add")}
-
- <.button
- variant="solid"
- size="sm"
- patch={~p"/collection/scan"}
- >
- <.barcode_icon class="icon fill-current" />
- {gettext("Scan")}
-
-
-
-
-
-
- <.button_group>
- <.button
- patch={order_path(@record_list_params, :purchase)}
- size="sm"
- class={[
- @record_list_params.order == :purchase && "bg-zinc-100! dark:bg-zinc-700!"
- ]}
- >
- <.icon
- name="hero-banknotes-solid"
- class="icon"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Purchase")}
-
- <.button
- patch={order_path(@record_list_params, :alphabetical)}
- size="sm"
- class={[
- @record_list_params.order == :alphabetical && "bg-zinc-100! dark:bg-zinc-700!"
- ]}
- >
- <.icon name="hero-user-solid" class="icon" aria-hidden="true" data-slot="icon" />
- {gettext("A->Z")}
-
- <.button
- patch={order_path(@record_list_params, :release)}
- size="sm"
- class={[
- @record_list_params.order == :release && "bg-zinc-100! dark:bg-zinc-700!"
- ]}
- >
- <.icon name="hero-calendar-days" class="icon" aria-hidden="true" data-slot="icon" />
- {gettext("Release")}
-
-
-
- <.button_group>
- <.button
- phx-click="set_display"
- phx-value-mode="grid"
- size="sm"
- class={[
- @display == :grid && "bg-zinc-100! dark:bg-zinc-700!"
- ]}
- >
- <.icon
- name="hero-squares-2x2"
- class="icon"
- aria-hidden="true"
- data-slot="icon"
- />
-
- {gettext("Grid")}
-
-
- <.button
- phx-click="set_display"
- phx-value-mode="list"
- size="sm"
- class={[
- @display == :list && "bg-zinc-100! dark:bg-zinc-700!"
- ]}
- >
- <.icon name="hero-list-bullet" class="icon" aria-hidden="true" data-slot="icon" />
-
- {gettext("List")}
-
-
-
-
-
- <.record_grid
- :if={@display == :grid}
- id="collection"
- records={@streams.records}
- record_show_path={fn record -> ~p"/collection/#{record}" end}
- record_edit_path={fn record -> ~p"/collection/#{record}/edit" end}
- display_artist_names
- density={:high}
- />
-
- <.record_list
- :if={@display == :list}
- records={@streams.records}
- record_show_path={fn record -> ~p"/collection/#{record}" end}
- record_edit_path={fn record -> ~p"/collection/#{record}/edit" end}
- />
-
- <.structured_modal
- :if={@live_action == :edit}
- id="record-modal"
- on_close={JS.patch(back_path(@record_list_params))}
- >
- <.live_component
- module={MusicLibraryWeb.Components.RecordForm}
- id={@record.id}
- action={@live_action}
- show_purchased_at={true}
- record={@record}
- patch={back_path(@record_list_params)}
- />
-
-
- <.structured_modal
- :if={@live_action == :import}
- id="record-modal"
- on_close={JS.patch(back_path(@record_list_params))}
- >
- <.live_component
- module={MusicLibraryWeb.Components.AddRecord}
- id={:search}
- title={@page_title}
- action={@live_action}
- record={@record}
- patch={back_path(@record_list_params)}
- initial_query=""
- icon_name="hero-plus"
- />
-
-
- <.structured_modal
- :if={@live_action == :barcode_scan}
- id="barcode-scanner-modal"
- on_close={JS.patch(back_path(@record_list_params))}
- >
- <.live_component
- module={MusicLibraryWeb.Components.BarcodeScanner}
- id={:barcode_scanner}
- title={@page_title}
- action={@live_action}
- patch={back_path(@record_list_params)}
- />
-
-
- <.pagination id={:bottom_pagination} pagination_params={@record_list_params} />
-
diff --git a/lib/music_library_web/live/collection_live/show.ex b/lib/music_library_web/live/collection_live/show.ex
index a7f6e9f9..b1d632c9 100644
--- a/lib/music_library_web/live/collection_live/show.ex
+++ b/lib/music_library_web/live/collection_live/show.ex
@@ -20,6 +20,379 @@ defmodule MusicLibraryWeb.CollectionLive.Show do
alias MusicLibrary.RecordSets.RecordSet
alias Phoenix.LiveView.JS
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ <.record_cover
+ record={@record}
+ class="w-full rounded-lg drop-shadow-sm"
+ />
+
+
+
+
+
+ <.artist_links joinphrase_class="text-sm" artists={@record.artists} />
+
+
+ <.button_group>
+ <.button
+ variant="soft"
+ phx-click={MusicLibraryWeb.Components.Notes.open("record-notes-sheet")}
+ >
+ {gettext("Open Notes")}
+ <.icon
+ name="hero-pencil"
+ class="h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+ <.button
+ variant="soft"
+ phx-click={MusicLibraryWeb.Components.Chat.open("record-chat-sheet")}
+ >
+ {gettext("Chat about album")}
+ <.icon
+ name="hero-chat-bubble-left-right"
+ class="h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+ <.button
+ :if={@record.selected_release_id}
+ variant="soft"
+ phx-click={MusicLibraryWeb.Components.Release.open("release-with-tracks-sheet")}
+ >
+ {gettext("Show Tracks")}
+ <.icon
+ name="hero-numbered-list"
+ class="h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+ <.dropdown id={"actions-#{@record.id}"} placement="bottom-end">
+ <:toggle>
+ <.button variant="soft">
+ {gettext("Actions")}
+ <.icon
+ name="hero-ellipsis-vertical"
+ class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+
+ <.focus_wrap id={"actions-#{@record.id}-focus-wrap"}>
+ <.dropdown_link
+ id={"actions-#{@record.id}-edit"}
+ patch={~p"/collection/#{@record}/show/edit"}
+ >
+ <.icon
+ name="hero-pencil-square"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Edit")}
+
+
+ <.dropdown_link
+ id={"actions-#{@record.id}-refresh-cover"}
+ phx-click={JS.push("refresh_cover", value: %{id: @record.id})}
+ >
+ <.icon
+ name="hero-photo"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Refresh cover")}
+
+
+ <.dropdown_link
+ id={"actions-#{@record.id}-refresh-mb-data"}
+ phx-click={JS.push("refresh_musicbrainz_data", value: %{id: @record.id})}
+ >
+ <.icon
+ name="hero-arrow-path"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Refresh MB data")}
+
+
+ <.dropdown_link
+ id={"actions-#{@record.id}-populate-genres"}
+ phx-click={JS.push("populate_genres", value: %{id: @record.id})}
+ >
+ <.icon
+ name="hero-sparkles"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Populate genres")}
+
+
+ <.dropdown_link
+ id={"actions-#{@record.id}-regenerate-embeddings"}
+ phx-click={JS.push("regenerate_embeddings", value: %{id: @record.id})}
+ >
+ <.icon
+ name="hero-sparkles"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Regenerate embeddings")}
+
+
+ <.dropdown_link
+ id={"actions-#{@record.id}-extract-colors-fast"}
+ phx-click={JS.push("extract_colors", value: %{id: @record.id, method: :fast})}
+ >
+ <.icon
+ name="hero-paint-brush"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Extract colors (fast)")}
+
+
+ <.dropdown_link
+ id={"actions-#{@record.id}-extract-colors-slow"}
+ phx-click={JS.push("extract_colors", value: %{id: @record.id, method: :slow})}
+ >
+ <.icon
+ name="hero-paint-brush"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Extract colors (slow)")}
+
+
+ <.dropdown_separator />
+ <.dropdown_link
+ id={"actions-#{@record.id}-delete"}
+ phx-click={JS.push("delete", value: %{id: @record.id})}
+ data-confirm={gettext("Are you sure?")}
+ class="text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
+ >
+ <.icon
+ name="hero-trash"
+ class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Delete")}
+
+
+
+
+
+
+
+
+ {@record.title}
+
+
+ <.record_colors record={@record} />
+
+ {Records.Record.format_release_date(@record.release_date)} · {format_label(
+ @record.format
+ )} · {type_label(@record.type)}
+
+
+
+
+
+ <.dl_row label={gettext("ID")}>
+
+ {@record.id}
+ <.copy_to_clipboard
+ target_id={"record-#{@record.id}"}
+ label={gettext("Copy record ID to clipboard")}
+ />
+
+
+ <.dl_row label={gettext("Genres")}>
+ <.link
+ :for={genre <- @record.genres}
+ class="mr-2 text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
+ patch={~p"/collection?#{%{query: ~s(genre:"#{genre}")}}"}
+ >
+ {genre}
+
+
+ <.dl_row label={gettext("MusicBrainz ID")}>
+
+
+ {@record.musicbrainz_id}
+
+ <.copy_to_clipboard
+ target_id={"mb-#{@record.musicbrainz_id}"}
+ label={gettext("Copy MusicBrainz ID to clipboard")}
+ />
+
+
+ <.dl_row label={gettext("Purchased on")}>
+ {Records.Record.format_as_date(@record.purchased_at)}
+
+ <.dl_row label={gettext("Published releases")}>
+
+ {Records.Record.release_count(@record)}
+ <.release_list record={@record} />
+ @record.id)}>
+
+ {gettext("Show releases included in the record")}
+
+ <.icon
+ name="hero-magnifying-glass-plus"
+ class="-mt-1 h-5 w-5"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+
+
+ <.dl_row label={gettext("Collected release")}>
+
+
+ {gettext("No release selected")}
+
+ <.release_summary
+ :if={@record.selected_release_id}
+ release={Records.Record.selected_release(@record)}
+ />
+ @record.id}
+ class="hidden"
+ >
+ {@record.selected_release_id}
+
+ <.copy_to_clipboard
+ :if={@record.selected_release_id}
+ target_id={"record-selected-release-" <> @record.id}
+ label={gettext("Copy record selected release ID to clipboard")}
+ />
+
+
+ <.dl_row
+ :if={Records.Record.included_release_groups_count(@record) > 0}
+ label={gettext("Includes")}
+ >
+
+
+ {included_release_group.artists} - {included_release_group.title}
+
+
+
+ <.dl_row :if={@record_sets != []} label={gettext("Record sets")}>
+
+
+ <.link
+ navigate={~p"/record-sets/#{record_set}"}
+ class="text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300 hover:underline"
+ >
+ {record_set.name}
+
+
+ {gettext(
+ "%{collected}/%{total} collected",
+ RecordSet.count_by_status(record_set)
+ )}
+
+
+
+
+ <.dl_row label={gettext("Inserted at")}>
+ {Records.Record.format_as_date(@record.inserted_at)}
+
+ <.dl_row label={gettext("Updated at")}>
+ {Records.Record.format_as_date(@record.updated_at)}
+
+ <.dl_row label={gettext("Last listened at")}>
+
+ {localize_scrobbled_at(@last_listened_track.scrobbled_at_uts, @timezone)}
+
+
+ {gettext("Never")}
+
+ 0}>
+ {ngettext("(1 scrobble)", "(%{count} scrobbles)", @play_count)}
+
+
+
+
+
+
+
+ <.similar_records
+ similar_records={@similar_records}
+ record_show_path={fn record -> ~p"/collection/#{record}" end}
+ section={:collection}
+ />
+
+ <.json_viewer title={gettext("MusicBrainz data")} data={@record.musicbrainz_data} />
+ <.text_viewer title={gettext("Record Embedding")} data={@embedding_text} />
+
+ <.live_component
+ id="release-with-tracks"
+ sheet_id="release-with-tracks-sheet"
+ module={MusicLibraryWeb.Components.Release}
+ record={@record}
+ />
+
+ <.live_component
+ id="record-notes"
+ sheet_id="record-notes-sheet"
+ module={MusicLibraryWeb.Components.Notes}
+ entity={:record}
+ musicbrainz_id={@record.musicbrainz_id}
+ />
+
+ <.live_component
+ id="record-chat"
+ sheet_id="record-chat-sheet"
+ module={MusicLibraryWeb.Components.Chat}
+ title={@record.title}
+ chat_module={MusicLibrary.RecordChat}
+ chat_context={{@record, @embedding_text}}
+ placeholder={gettext("Ask about this album...")}
+ empty_prompt={gettext("Ask anything about this album")}
+ />
+
+ <.structured_modal
+ :if={@live_action == :edit}
+ id="record-modal"
+ on_close={JS.patch(~p"/collection/#{@record}")}
+ >
+ <.live_component
+ module={MusicLibraryWeb.Components.RecordForm}
+ id={@record.id}
+ action={@live_action}
+ show_purchased_at={true}
+ record={@record}
+ patch={~p"/collection/#{@record}"}
+ />
+
+
+ """
+ end
+
@impl true
def mount(%{"id" => record_id}, _session, socket) do
if connected?(socket) do
diff --git a/lib/music_library_web/live/collection_live/show.html.heex b/lib/music_library_web/live/collection_live/show.html.heex
deleted file mode 100644
index 4efccc54..00000000
--- a/lib/music_library_web/live/collection_live/show.html.heex
+++ /dev/null
@@ -1,367 +0,0 @@
-
-
-
- <.record_cover
- record={@record}
- class="w-full rounded-lg drop-shadow-sm"
- />
-
-
-
-
-
- <.artist_links joinphrase_class="text-sm" artists={@record.artists} />
-
-
- <.button_group>
- <.button
- variant="soft"
- phx-click={MusicLibraryWeb.Components.Notes.open("record-notes-sheet")}
- >
- {gettext("Open Notes")}
- <.icon
- name="hero-pencil"
- class="h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
-
- <.button
- variant="soft"
- phx-click={MusicLibraryWeb.Components.Chat.open("record-chat-sheet")}
- >
- {gettext("Chat about album")}
- <.icon
- name="hero-chat-bubble-left-right"
- class="h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
-
- <.button
- :if={@record.selected_release_id}
- variant="soft"
- phx-click={MusicLibraryWeb.Components.Release.open("release-with-tracks-sheet")}
- >
- {gettext("Show Tracks")}
- <.icon
- name="hero-numbered-list"
- class="h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
-
- <.dropdown id={"actions-#{@record.id}"} placement="bottom-end">
- <:toggle>
- <.button variant="soft">
- {gettext("Actions")}
- <.icon
- name="hero-ellipsis-vertical"
- class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
- aria-hidden="true"
- data-slot="icon"
- />
-
-
- <.focus_wrap id={"actions-#{@record.id}-focus-wrap"}>
- <.dropdown_link
- id={"actions-#{@record.id}-edit"}
- patch={~p"/collection/#{@record}/show/edit"}
- >
- <.icon
- name="hero-pencil-square"
- class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Edit")}
-
-
- <.dropdown_link
- id={"actions-#{@record.id}-refresh-cover"}
- phx-click={JS.push("refresh_cover", value: %{id: @record.id})}
- >
- <.icon
- name="hero-photo"
- class="h-4 w-4 mr-1 phx-click-loading:animate-bounce"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Refresh cover")}
-
-
- <.dropdown_link
- id={"actions-#{@record.id}-refresh-mb-data"}
- phx-click={JS.push("refresh_musicbrainz_data", value: %{id: @record.id})}
- >
- <.icon
- name="hero-arrow-path"
- class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Refresh MB data")}
-
-
- <.dropdown_link
- id={"actions-#{@record.id}-populate-genres"}
- phx-click={JS.push("populate_genres", value: %{id: @record.id})}
- >
- <.icon
- name="hero-sparkles"
- class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Populate genres")}
-
-
- <.dropdown_link
- id={"actions-#{@record.id}-regenerate-embeddings"}
- phx-click={JS.push("regenerate_embeddings", value: %{id: @record.id})}
- >
- <.icon
- name="hero-sparkles"
- class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Regenerate embeddings")}
-
-
- <.dropdown_link
- id={"actions-#{@record.id}-extract-colors-fast"}
- phx-click={JS.push("extract_colors", value: %{id: @record.id, method: :fast})}
- >
- <.icon
- name="hero-paint-brush"
- class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Extract colors (fast)")}
-
-
- <.dropdown_link
- id={"actions-#{@record.id}-extract-colors-slow"}
- phx-click={JS.push("extract_colors", value: %{id: @record.id, method: :slow})}
- >
- <.icon
- name="hero-paint-brush"
- class="h-4 w-4 mr-1 phx-click-loading:animate-shake"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Extract colors (slow)")}
-
-
- <.dropdown_separator />
- <.dropdown_link
- id={"actions-#{@record.id}-delete"}
- phx-click={JS.push("delete", value: %{id: @record.id})}
- data-confirm={gettext("Are you sure?")}
- class="text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
- >
- <.icon
- name="hero-trash"
- class="h-4 w-4 mr-1 phx-click-loading:animate-spin"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Delete")}
-
-
-
-
-
-
-
-
- {@record.title}
-
-
- <.record_colors record={@record} />
-
- {Records.Record.format_release_date(@record.release_date)} · {format_label(
- @record.format
- )} · {type_label(@record.type)}
-
-
-
-
-
- <.dl_row label={gettext("ID")}>
-
- {@record.id}
- <.copy_to_clipboard
- target_id={"record-#{@record.id}"}
- label={gettext("Copy record ID to clipboard")}
- />
-
-
- <.dl_row label={gettext("Genres")}>
- <.link
- :for={genre <- @record.genres}
- class="mr-2 text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
- patch={~p"/collection?#{%{query: ~s(genre:"#{genre}")}}"}
- >
- {genre}
-
-
- <.dl_row label={gettext("MusicBrainz ID")}>
-
-
- {@record.musicbrainz_id}
-
- <.copy_to_clipboard
- target_id={"mb-#{@record.musicbrainz_id}"}
- label={gettext("Copy MusicBrainz ID to clipboard")}
- />
-
-
- <.dl_row label={gettext("Purchased on")}>
- {Records.Record.format_as_date(@record.purchased_at)}
-
- <.dl_row label={gettext("Published releases")}>
-
- {Records.Record.release_count(@record)}
- <.release_list record={@record} />
- @record.id)}>
-
- {gettext("Show releases included in the record")}
-
- <.icon
- name="hero-magnifying-glass-plus"
- class="-mt-1 h-5 w-5"
- aria-hidden="true"
- data-slot="icon"
- />
-
-
-
- <.dl_row label={gettext("Collected release")}>
-
-
- {gettext("No release selected")}
-
- <.release_summary
- :if={@record.selected_release_id}
- release={Records.Record.selected_release(@record)}
- />
- @record.id}
- class="hidden"
- >
- {@record.selected_release_id}
-
- <.copy_to_clipboard
- :if={@record.selected_release_id}
- target_id={"record-selected-release-" <> @record.id}
- label={gettext("Copy record selected release ID to clipboard")}
- />
-
-
- <.dl_row
- :if={Records.Record.included_release_groups_count(@record) > 0}
- label={gettext("Includes")}
- >
-
-
- {included_release_group.artists} - {included_release_group.title}
-
-
-
- <.dl_row :if={@record_sets != []} label={gettext("Record sets")}>
-
-
- <.link
- navigate={~p"/record-sets/#{record_set}"}
- class="text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300 hover:underline"
- >
- {record_set.name}
-
-
- {gettext(
- "%{collected}/%{total} collected",
- RecordSet.count_by_status(record_set)
- )}
-
-
-
-
- <.dl_row label={gettext("Inserted at")}>
- {Records.Record.format_as_date(@record.inserted_at)}
-
- <.dl_row label={gettext("Updated at")}>
- {Records.Record.format_as_date(@record.updated_at)}
-
- <.dl_row label={gettext("Last listened at")}>
-
- {localize_scrobbled_at(@last_listened_track.scrobbled_at_uts, @timezone)}
-
-
- {gettext("Never")}
-
- 0}>
- {ngettext("(1 scrobble)", "(%{count} scrobbles)", @play_count)}
-
-
-
-
-
-
-
- <.similar_records
- similar_records={@similar_records}
- record_show_path={fn record -> ~p"/collection/#{record}" end}
- section={:collection}
- />
-
- <.json_viewer title={gettext("MusicBrainz data")} data={@record.musicbrainz_data} />
- <.text_viewer title={gettext("Record Embedding")} data={@embedding_text} />
-
- <.live_component
- id="release-with-tracks"
- sheet_id="release-with-tracks-sheet"
- module={MusicLibraryWeb.Components.Release}
- record={@record}
- />
-
- <.live_component
- id="record-notes"
- sheet_id="record-notes-sheet"
- module={MusicLibraryWeb.Components.Notes}
- entity={:record}
- musicbrainz_id={@record.musicbrainz_id}
- />
-
- <.live_component
- id="record-chat"
- sheet_id="record-chat-sheet"
- module={MusicLibraryWeb.Components.Chat}
- title={@record.title}
- chat_module={MusicLibrary.RecordChat}
- chat_context={{@record, @embedding_text}}
- placeholder={gettext("Ask about this album...")}
- empty_prompt={gettext("Ask anything about this album")}
- />
-
- <.structured_modal
- :if={@live_action == :edit}
- id="record-modal"
- on_close={JS.patch(~p"/collection/#{@record}")}
- >
- <.live_component
- module={MusicLibraryWeb.Components.RecordForm}
- id={@record.id}
- action={@live_action}
- show_purchased_at={true}
- record={@record}
- patch={~p"/collection/#{@record}"}
- />
-
-
diff --git a/lib/music_library_web/live/maintenance_live/index.ex b/lib/music_library_web/live/maintenance_live/index.ex
index 79fe03fa..a97d6ac6 100644
--- a/lib/music_library_web/live/maintenance_live/index.ex
+++ b/lib/music_library_web/live/maintenance_live/index.ex
@@ -12,6 +12,142 @@ defmodule MusicLibraryWeb.MaintenanceLive.Index do
@poll_interval 2_000
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ {gettext("Maintenance")}
+
+
+ {gettext("Records")}
+
+
+ {gettext(
+ "Run operations on the entire record database. Monitor execution via the Oban dashboard."
+ )}
+
+
+
+ <.button_group>
+ <.button
+ type="button"
+ phx-click="refresh_records_musicbrainz_data"
+ disabled={@refresh_records_musicbrainz_jobs > 0}
+ data-confirm={
+ gettext(
+ "Are you sure you want to refresh MusicBrainz data for all records? This operation can take a long time to complete."
+ )
+ }
+ >
+ <.loading :if={@refresh_records_musicbrainz_jobs > 0} class="size-4" />
+ {gettext("Refresh MusicBrainz data")}
+
+ <.button
+ type="button"
+ phx-click="generate_record_embeddings"
+ disabled={@generate_record_embeddings_jobs > 0}
+ data-confirm={
+ gettext(
+ "Are you sure you want to regenerate embeddings for all records? This operation can take a long time to complete."
+ )
+ }
+ >
+ <.loading :if={@generate_record_embeddings_jobs > 0} class="size-4" />
+ {gettext("Regenerate record embeddings")}
+
+
+
+
+
+ {gettext("Artists")}
+
+
+ {gettext(
+ "Run operations on the entire artist database. Monitor execution via the Oban dashboard."
+ )}
+
+
+
+ <.button_group>
+ <.button
+ type="button"
+ phx-click="refresh_artists_musicbrainz_data"
+ disabled={@refresh_artists_musicbrainz_jobs > 0}
+ data-confirm={
+ gettext(
+ "Are you sure you want to refresh MusicBrainz data for all artists? This operation can take a long time to complete."
+ )
+ }
+ >
+ <.loading :if={@refresh_artists_musicbrainz_jobs > 0} class="size-4" />
+ {gettext("Refresh MusicBrainz data")}
+
+ <.button
+ type="button"
+ phx-click="refresh_artists_discogs_data"
+ disabled={@refresh_artists_discogs_jobs > 0}
+ data-confirm={
+ gettext(
+ "Are you sure you want to refresh Discogs data for all artists? This operation can take a long time to complete."
+ )
+ }
+ >
+ <.loading :if={@refresh_artists_discogs_jobs > 0} class="size-4" />
+ {gettext("Refresh Discogs data")}
+
+ <.button
+ type="button"
+ phx-click="refresh_artists_wikipedia_data"
+ disabled={@refresh_artists_wikipedia_jobs > 0}
+ data-confirm={
+ gettext(
+ "Are you sure you want to refresh Wikipedia data for all artists? This operation can take a long time to complete."
+ )
+ }
+ >
+ <.loading :if={@refresh_artists_wikipedia_jobs > 0} class="size-4" />
+ {gettext("Refresh Wikipedia data")}
+
+
+
+
+
+ {gettext("Database")}
+
+
+ {gettext("Run lower-level operations on the database")}
+
+
+
+ <.button_group>
+ <.button
+ type="button"
+ phx-click="db_vacuum"
+ phx-disable-with={gettext("Running vacuum...")}
+ >
+ {gettext("Vacuum")}
+
+ <.button
+ type="button"
+ phx-click="db_optimize"
+ phx-disable-with={gettext("Running optimize...")}
+ >
+ {gettext("Optimize")}
+
+ <.button href={~p"/backup"}>
+ {gettext("Backup")}
+
+
+
+
+
+
+ """
+ end
+
+ @impl true
def mount(_params, _session, socket) do
if connected?(socket) do
Process.send_after(self(), :update_job_counts, @poll_interval)
@@ -26,6 +162,7 @@ defmodule MusicLibraryWeb.MaintenanceLive.Index do
|> assign_job_counts()}
end
+ @impl true
def handle_info(:update_job_counts, socket) do
Process.send_after(self(), :update_job_counts, @poll_interval)
@@ -66,6 +203,7 @@ defmodule MusicLibraryWeb.MaintenanceLive.Index do
BackgroundRepo.one(query)
end
+ @impl true
def handle_event("refresh_records_musicbrainz_data", _params, socket) do
Records.Batch.refresh_musicbrainz_data()
diff --git a/lib/music_library_web/live/maintenance_live/index.html.heex b/lib/music_library_web/live/maintenance_live/index.html.heex
deleted file mode 100644
index 6e784796..00000000
--- a/lib/music_library_web/live/maintenance_live/index.html.heex
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
- {gettext("Maintenance")}
-
-
- {gettext("Records")}
-
-
- {gettext(
- "Run operations on the entire record database. Monitor execution via the Oban dashboard."
- )}
-
-
-
- <.button_group>
- <.button
- type="button"
- phx-click="refresh_records_musicbrainz_data"
- disabled={@refresh_records_musicbrainz_jobs > 0}
- data-confirm={
- gettext(
- "Are you sure you want to refresh MusicBrainz data for all records? This operation can take a long time to complete."
- )
- }
- >
- <.loading :if={@refresh_records_musicbrainz_jobs > 0} class="size-4" />
- {gettext("Refresh MusicBrainz data")}
-
- <.button
- type="button"
- phx-click="generate_record_embeddings"
- disabled={@generate_record_embeddings_jobs > 0}
- data-confirm={
- gettext(
- "Are you sure you want to regenerate embeddings for all records? This operation can take a long time to complete."
- )
- }
- >
- <.loading :if={@generate_record_embeddings_jobs > 0} class="size-4" />
- {gettext("Regenerate record embeddings")}
-
-
-
-
-
- {gettext("Artists")}
-
-
- {gettext(
- "Run operations on the entire artist database. Monitor execution via the Oban dashboard."
- )}
-
-
-
- <.button_group>
- <.button
- type="button"
- phx-click="refresh_artists_musicbrainz_data"
- disabled={@refresh_artists_musicbrainz_jobs > 0}
- data-confirm={
- gettext(
- "Are you sure you want to refresh MusicBrainz data for all artists? This operation can take a long time to complete."
- )
- }
- >
- <.loading :if={@refresh_artists_musicbrainz_jobs > 0} class="size-4" />
- {gettext("Refresh MusicBrainz data")}
-
- <.button
- type="button"
- phx-click="refresh_artists_discogs_data"
- disabled={@refresh_artists_discogs_jobs > 0}
- data-confirm={
- gettext(
- "Are you sure you want to refresh Discogs data for all artists? This operation can take a long time to complete."
- )
- }
- >
- <.loading :if={@refresh_artists_discogs_jobs > 0} class="size-4" />
- {gettext("Refresh Discogs data")}
-
- <.button
- type="button"
- phx-click="refresh_artists_wikipedia_data"
- disabled={@refresh_artists_wikipedia_jobs > 0}
- data-confirm={
- gettext(
- "Are you sure you want to refresh Wikipedia data for all artists? This operation can take a long time to complete."
- )
- }
- >
- <.loading :if={@refresh_artists_wikipedia_jobs > 0} class="size-4" />
- {gettext("Refresh Wikipedia data")}
-
-
-
-
-
- {gettext("Database")}
-
-
- {gettext("Run lower-level operations on the database")}
-
-
-
- <.button_group>
- <.button
- type="button"
- phx-click="db_vacuum"
- phx-disable-with={gettext("Running vacuum...")}
- >
- {gettext("Vacuum")}
-
- <.button
- type="button"
- phx-click="db_optimize"
- phx-disable-with={gettext("Running optimize...")}
- >
- {gettext("Optimize")}
-
- <.button href={~p"/backup"}>
- {gettext("Backup")}
-
-
-
-
-
-
diff --git a/lib/music_library_web/live/online_store_template_live/index.ex b/lib/music_library_web/live/online_store_template_live/index.ex
index 6971c13d..6d02b865 100644
--- a/lib/music_library_web/live/online_store_template_live/index.ex
+++ b/lib/music_library_web/live/online_store_template_live/index.ex
@@ -4,6 +4,120 @@ defmodule MusicLibraryWeb.OnlineStoreTemplateLive.Index do
alias MusicLibrary.OnlineStoreTemplates
alias MusicLibrary.OnlineStoreTemplates.OnlineStoreTemplate
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+
+
+
+
+
+
+
+ {template.name}
+
+ <.badge :if={template.enabled} color="success">
+ {gettext("Enabled")}
+
+ <.badge :if={!template.enabled} color="warning">
+ {gettext("Disabled")}
+
+
+
+
{template.url_template}
+
+
+ {template.description}
+
+
+
+ <.dropdown id={"actions-#{template.id}"} placement="bottom-end">
+ <:toggle>
+ <.button variant="ghost">
+ {gettext("Actions")}
+ <.icon
+ name="hero-ellipsis-vertical"
+ class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+
+ <.dropdown_button phx-click="toggle-enabled" phx-value-id={template.id}>
+ {if template.enabled,
+ do: gettext("Disable template"),
+ else: gettext("Enable template")}
+
+ <.dropdown_link
+ id={"actions-#{template.id}-edit"}
+ patch={~p"/online-store-templates/#{template}/edit"}
+ >
+ {gettext("Edit")}
+
+ <.separator />
+ <.dropdown_button
+ phx-click="delete"
+ phx-value-id={template.id}
+ data-confirm={gettext("Are you sure?")}
+ class={[
+ "text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
+ ]}
+ >
+ {gettext("Delete")}
+
+
+
+
+
+
+
+ <.structured_modal
+ :if={@live_action in [:new, :edit]}
+ id="template-modal"
+ on_close={JS.patch(~p"/online-store-templates")}
+ >
+ <.live_component
+ module={MusicLibraryWeb.OnlineStoreTemplateLive.Form}
+ id={@template.id || :new}
+ title={@page_title}
+ action={@live_action}
+ template={@template}
+ patch={~p"/online-store-templates"}
+ />
+
+
+ """
+ end
+
@impl true
def mount(_params, _session, socket) do
{:ok,
diff --git a/lib/music_library_web/live/online_store_template_live/index.html.heex b/lib/music_library_web/live/online_store_template_live/index.html.heex
deleted file mode 100644
index 9c90a402..00000000
--- a/lib/music_library_web/live/online_store_template_live/index.html.heex
+++ /dev/null
@@ -1,108 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {template.name}
-
- <.badge :if={template.enabled} color="success">
- {gettext("Enabled")}
-
- <.badge :if={!template.enabled} color="warning">
- {gettext("Disabled")}
-
-
-
-
{template.url_template}
-
-
- {template.description}
-
-
-
- <.dropdown id={"actions-#{template.id}"} placement="bottom-end">
- <:toggle>
- <.button variant="ghost">
- {gettext("Actions")}
- <.icon
- name="hero-ellipsis-vertical"
- class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
- aria-hidden="true"
- data-slot="icon"
- />
-
-
- <.dropdown_button phx-click="toggle-enabled" phx-value-id={template.id}>
- {if template.enabled,
- do: gettext("Disable template"),
- else: gettext("Enable template")}
-
- <.dropdown_link
- id={"actions-#{template.id}-edit"}
- patch={~p"/online-store-templates/#{template}/edit"}
- >
- {gettext("Edit")}
-
- <.separator />
- <.dropdown_button
- phx-click="delete"
- phx-value-id={template.id}
- data-confirm={gettext("Are you sure?")}
- class={[
- "text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
- ]}
- >
- {gettext("Delete")}
-
-
-
-
-
-
-
- <.structured_modal
- :if={@live_action in [:new, :edit]}
- id="template-modal"
- on_close={JS.patch(~p"/online-store-templates")}
- >
- <.live_component
- module={MusicLibraryWeb.OnlineStoreTemplateLive.Form}
- id={@template.id || :new}
- title={@page_title}
- action={@live_action}
- template={@template}
- patch={~p"/online-store-templates"}
- />
-
-
diff --git a/lib/music_library_web/live/record_set_live/index.ex b/lib/music_library_web/live/record_set_live/index.ex
index 3cb5b1f7..ca0a258d 100644
--- a/lib/music_library_web/live/record_set_live/index.ex
+++ b/lib/music_library_web/live/record_set_live/index.ex
@@ -16,6 +16,285 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do
order: :updated_at
}
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+
+ <.button_group>
+ <.button
+ patch={order_path(@list_params, :updated_at)}
+ size="sm"
+ class={[@list_params.order == :updated_at && "bg-zinc-100! dark:bg-zinc-700!"]}
+ >
+ <.icon name="hero-clock" class="icon" aria-hidden="true" data-slot="icon" />
+ {gettext("Updated")}
+
+ <.button
+ patch={order_path(@list_params, :alphabetical)}
+ size="sm"
+ class={[@list_params.order == :alphabetical && "bg-zinc-100! dark:bg-zinc-700!"]}
+ >
+ <.icon name="hero-user-solid" class="icon" aria-hidden="true" data-slot="icon" />
+ {gettext("A->Z")}
+
+
+
+
+
+
+
+ <.pagination id={:bottom_pagination} pagination_params={@list_params} />
+
+
+ <.structured_modal
+ :if={@live_action in [:new, :edit]}
+ id="record-set-modal"
+ on_close={JS.patch(back_path(@list_params))}
+ >
+ <.live_component
+ module={MusicLibraryWeb.RecordSetLive.Form}
+ id={@record_set.id || :new}
+ title={@page_title}
+ action={@live_action}
+ record_set={@record_set}
+ patch={back_path(@list_params)}
+ />
+
+
+ <.structured_modal
+ :if={@live_action == :add_record}
+ id="record-picker-modal"
+ on_close={JS.patch(back_path(@list_params))}
+ >
+ <.live_component
+ module={MusicLibraryWeb.RecordSetLive.RecordPicker}
+ id={"record-picker-#{@record_set.id}"}
+ title={@page_title}
+ record_set={@record_set}
+ patch={back_path(@list_params)}
+ />
+
+
+ """
+ end
+
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :current_section, :record_sets)}
diff --git a/lib/music_library_web/live/record_set_live/index.html.heex b/lib/music_library_web/live/record_set_live/index.html.heex
deleted file mode 100644
index aa3c6e91..00000000
--- a/lib/music_library_web/live/record_set_live/index.html.heex
+++ /dev/null
@@ -1,273 +0,0 @@
-
-
-
-
- <.button_group>
- <.button
- patch={order_path(@list_params, :updated_at)}
- size="sm"
- class={[@list_params.order == :updated_at && "bg-zinc-100! dark:bg-zinc-700!"]}
- >
- <.icon name="hero-clock" class="icon" aria-hidden="true" data-slot="icon" />
- {gettext("Updated")}
-
- <.button
- patch={order_path(@list_params, :alphabetical)}
- size="sm"
- class={[@list_params.order == :alphabetical && "bg-zinc-100! dark:bg-zinc-700!"]}
- >
- <.icon name="hero-user-solid" class="icon" aria-hidden="true" data-slot="icon" />
- {gettext("A->Z")}
-
-
-
-
-
-
-
- <.pagination id={:bottom_pagination} pagination_params={@list_params} />
-
-
- <.structured_modal
- :if={@live_action in [:new, :edit]}
- id="record-set-modal"
- on_close={JS.patch(back_path(@list_params))}
- >
- <.live_component
- module={MusicLibraryWeb.RecordSetLive.Form}
- id={@record_set.id || :new}
- title={@page_title}
- action={@live_action}
- record_set={@record_set}
- patch={back_path(@list_params)}
- />
-
-
- <.structured_modal
- :if={@live_action == :add_record}
- id="record-picker-modal"
- on_close={JS.patch(back_path(@list_params))}
- >
- <.live_component
- module={MusicLibraryWeb.RecordSetLive.RecordPicker}
- id={"record-picker-#{@record_set.id}"}
- title={@page_title}
- record_set={@record_set}
- patch={back_path(@list_params)}
- />
-
-
diff --git a/lib/music_library_web/live/record_set_live/show.ex b/lib/music_library_web/live/record_set_live/show.ex
index 0693718d..7890da6f 100644
--- a/lib/music_library_web/live/record_set_live/show.ex
+++ b/lib/music_library_web/live/record_set_live/show.ex
@@ -8,6 +8,221 @@ defmodule MusicLibraryWeb.RecordSetLive.Show do
alias MusicLibraryWeb.Markdown
alias Phoenix.LiveView.JS
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+
+
+ {@record_set.name}
+
+
+ {gettext("%{collected}/%{total} records", RecordSet.count_by_status(@record_set))}
+
+
+
+ <.dropdown id="set-actions" placement="bottom-end">
+ <:toggle>
+ <.button variant="soft">
+ {gettext("Actions")}
+ <.icon
+ name="hero-ellipsis-vertical"
+ class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+
+ <.dropdown_link
+ id="set-actions-edit"
+ patch={~p"/record-sets/#{@record_set}/show/edit"}
+ >
+ {gettext("Edit")}
+
+ <.separator />
+ <.dropdown_button
+ phx-click="delete_set"
+ data-confirm={gettext("Are you sure?")}
+ class={[
+ "text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
+ ]}
+ >
+ {gettext("Delete")}
+
+
+
+
+
+
+ {render_description(@record_set.description)}
+
+
+
+
+
+ <.link
+ :if={item.record.purchased_at}
+ navigate={~p"/collection/#{item.record}"}
+ >
+
+
+ <.link
+ :if={!item.record.purchased_at}
+ navigate={~p"/wishlist/#{item.record}"}
+ >
+
+
+
+ <.icon
+ name="hero-bars-2"
+ class="size-3.5 text-zinc-800 dark:text-zinc-200"
+ aria-hidden="true"
+ />
+
+
+ <.dropdown
+ id={"item-actions-#{item.record.id}"}
+ placement="bottom-end"
+ >
+ <:toggle>
+ {gettext("Actions")}
+ <.icon
+ name="hero-ellipsis-vertical"
+ class="size-5 text-zinc-800 dark:text-zinc-200 cursor-pointer"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+ <.dropdown_button
+ :if={item.position > 0}
+ phx-click="move_up"
+ phx-value-record-id={item.record.id}
+ >
+ {gettext("Move left")}
+
+ <.dropdown_button
+ :if={item.position < length(@record_set.items) - 1}
+ phx-click="move_down"
+ phx-value-record-id={item.record.id}
+ >
+ {gettext("Move right")}
+
+ <.separator />
+ <.dropdown_button
+ phx-click="remove_record"
+ phx-value-record-id={item.record.id}
+ data-confirm={gettext("Remove this record from the set?")}
+ class={[
+ "text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
+ ]}
+ >
+ {gettext("Remove")}
+
+
+
+
+ <.artist_links joinphrase_class="text-sm" artists={item.record.artists} />
+
+
+ {item.record.title}
+
+
+ {format_label(item.record.format)} · {type_label(item.record.type)}
+
+
+ <.icon
+ name="hero-calendar-days"
+ class="-mt-1 h-4 w-4"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {Records.Record.format_release_date(item.record.release_date)}
+
+
+ <.link
+ patch={~p"/record-sets/#{@record_set}/show/add-record"}
+ class={[
+ "aspect-square",
+ "border-2 border-dashed border-zinc-300 dark:border-zinc-600",
+ "rounded-lg flex items-center justify-center",
+ "hover:border-zinc-400 dark:hover:border-zinc-500",
+ "hover:bg-zinc-50 dark:hover:bg-zinc-800",
+ "transition-colors cursor-pointer"
+ ]}
+ >
+ <.icon
+ name="hero-plus"
+ class="h-8 w-8 text-zinc-400 dark:text-zinc-500"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+
+
+
+
+ <.structured_modal
+ :if={@live_action == :edit}
+ id="record-set-modal"
+ on_close={JS.patch(~p"/record-sets/#{@record_set}")}
+ >
+ <.live_component
+ module={MusicLibraryWeb.RecordSetLive.Form}
+ id={@record_set.id}
+ title={@page_title}
+ action={@live_action}
+ record_set={@record_set}
+ patch={~p"/record-sets/#{@record_set}"}
+ />
+
+
+ <.structured_modal
+ :if={@live_action == :add_record}
+ id="record-picker-modal"
+ on_close={JS.patch(~p"/record-sets/#{@record_set}")}
+ >
+ <.live_component
+ module={MusicLibraryWeb.RecordSetLive.RecordPicker}
+ id={"record-picker-#{@record_set.id}"}
+ title={@page_title}
+ record_set={@record_set}
+ patch={~p"/record-sets/#{@record_set}"}
+ />
+
+
+ """
+ end
+
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :current_section, :record_sets)}
diff --git a/lib/music_library_web/live/record_set_live/show.html.heex b/lib/music_library_web/live/record_set_live/show.html.heex
deleted file mode 100644
index 1b6bc6d3..00000000
--- a/lib/music_library_web/live/record_set_live/show.html.heex
+++ /dev/null
@@ -1,209 +0,0 @@
-
-
-
-
-
- {@record_set.name}
-
-
- {gettext("%{collected}/%{total} records", RecordSet.count_by_status(@record_set))}
-
-
-
- <.dropdown id="set-actions" placement="bottom-end">
- <:toggle>
- <.button variant="soft">
- {gettext("Actions")}
- <.icon
- name="hero-ellipsis-vertical"
- class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
- aria-hidden="true"
- data-slot="icon"
- />
-
-
- <.dropdown_link
- id="set-actions-edit"
- patch={~p"/record-sets/#{@record_set}/show/edit"}
- >
- {gettext("Edit")}
-
- <.separator />
- <.dropdown_button
- phx-click="delete_set"
- data-confirm={gettext("Are you sure?")}
- class={[
- "text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
- ]}
- >
- {gettext("Delete")}
-
-
-
-
-
-
- {render_description(@record_set.description)}
-
-
-
-
-
- <.link
- :if={item.record.purchased_at}
- navigate={~p"/collection/#{item.record}"}
- >
-
-
- <.link
- :if={!item.record.purchased_at}
- navigate={~p"/wishlist/#{item.record}"}
- >
-
-
-
- <.icon
- name="hero-bars-2"
- class="size-3.5 text-zinc-800 dark:text-zinc-200"
- aria-hidden="true"
- />
-
-
- <.dropdown
- id={"item-actions-#{item.record.id}"}
- placement="bottom-end"
- >
- <:toggle>
- {gettext("Actions")}
- <.icon
- name="hero-ellipsis-vertical"
- class="size-5 text-zinc-800 dark:text-zinc-200 cursor-pointer"
- aria-hidden="true"
- data-slot="icon"
- />
-
- <.dropdown_button
- :if={item.position > 0}
- phx-click="move_up"
- phx-value-record-id={item.record.id}
- >
- {gettext("Move left")}
-
- <.dropdown_button
- :if={item.position < length(@record_set.items) - 1}
- phx-click="move_down"
- phx-value-record-id={item.record.id}
- >
- {gettext("Move right")}
-
- <.separator />
- <.dropdown_button
- phx-click="remove_record"
- phx-value-record-id={item.record.id}
- data-confirm={gettext("Remove this record from the set?")}
- class={[
- "text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
- ]}
- >
- {gettext("Remove")}
-
-
-
-
- <.artist_links joinphrase_class="text-sm" artists={item.record.artists} />
-
-
- {item.record.title}
-
-
- {format_label(item.record.format)} · {type_label(item.record.type)}
-
-
- <.icon
- name="hero-calendar-days"
- class="-mt-1 h-4 w-4"
- aria-hidden="true"
- data-slot="icon"
- />
- {Records.Record.format_release_date(item.record.release_date)}
-
-
- <.link
- patch={~p"/record-sets/#{@record_set}/show/add-record"}
- class={[
- "aspect-square",
- "border-2 border-dashed border-zinc-300 dark:border-zinc-600",
- "rounded-lg flex items-center justify-center",
- "hover:border-zinc-400 dark:hover:border-zinc-500",
- "hover:bg-zinc-50 dark:hover:bg-zinc-800",
- "transition-colors cursor-pointer"
- ]}
- >
- <.icon
- name="hero-plus"
- class="h-8 w-8 text-zinc-400 dark:text-zinc-500"
- aria-hidden="true"
- data-slot="icon"
- />
-
-
-
-
-
- <.structured_modal
- :if={@live_action == :edit}
- id="record-set-modal"
- on_close={JS.patch(~p"/record-sets/#{@record_set}")}
- >
- <.live_component
- module={MusicLibraryWeb.RecordSetLive.Form}
- id={@record_set.id}
- title={@page_title}
- action={@live_action}
- record_set={@record_set}
- patch={~p"/record-sets/#{@record_set}"}
- />
-
-
- <.structured_modal
- :if={@live_action == :add_record}
- id="record-picker-modal"
- on_close={JS.patch(~p"/record-sets/#{@record_set}")}
- >
- <.live_component
- module={MusicLibraryWeb.RecordSetLive.RecordPicker}
- id={"record-picker-#{@record_set.id}"}
- title={@page_title}
- record_set={@record_set}
- patch={~p"/record-sets/#{@record_set}"}
- />
-
-
diff --git a/lib/music_library_web/live/scrobble_live/index.ex b/lib/music_library_web/live/scrobble_live/index.ex
index 9986d3de..e6e200e4 100644
--- a/lib/music_library_web/live/scrobble_live/index.ex
+++ b/lib/music_library_web/live/scrobble_live/index.ex
@@ -7,6 +7,141 @@ defmodule MusicLibraryWeb.ScrobbleLive.Index do
alias MusicLibrary.Records
alias MusicLibrary.ScrobbleActivity
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ <%= if @search_results != [] && @selected_release_group == nil do %>
+
+
{gettext("Release Groups")}
+
+
+
+
~p"/images/cover-not-found.png" <> "';"}
+ />
+
+
+ {release_group.artists}
+
+
+ {release_group.title}
+
+
+ {Records.Record.format_release_date(release_group.release_date)}
+ ·
+ <.badge variant="soft" size="xs">{type_label(release_group.type)}
+
+
+
+
+
+
+ <% end %>
+
+ <%= if @selected_release_group && @releases != [] do %>
+
+
+ <.button
+ variant="ghost"
+ size="sm"
+ phx-click="clear_selection"
+ >
+ <.icon name="hero-arrow-left" class="icon" aria-hidden="true" data-slot="icon" />
+ {gettext("Back")}
+
+
+ {gettext("Releases for \"%{title}\"", title: @selected_release_group.title)}
+
+
+
+
+
+ <.link
+ navigate={~p"/scrobble/#{release.id}"}
+ class="flex items-center gap-x-4 py-5 px-4 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors"
+ >
+ ~p"/images/cover-not-found.png" <> "';"}
+ />
+
+
+ {release.title}
+
+
+ {release.date}
+
+ {country_label(release.country)}
+
+ <.badge :if={release.catalog_number} variant="soft" size="xs">
+ {release.catalog_number}
+
+
+ {ngettext("1 disc", "%{count} discs", Release.media_count(release))}
+
+
+
+
+
+
+
+ <% end %>
+
+ <%= if @loading && @search_query != "" do %>
+
+ <.loading class="size-8 mx-auto text-zinc-400" />
+
+ <%= if @selected_release_group do %>
+ {gettext("Loading releases...")}
+ <% else %>
+ {gettext("Searching...")}
+ <% end %>
+
+
+ <% end %>
+
+ <%= if @search_query != "" && @search_results == [] && not @loading do %>
+
+ <.icon
+ name="hero-magnifying-glass"
+ class="h-12 w-12 mx-auto text-zinc-300 dark:text-zinc-600"
+ />
+
+ {gettext("No release groups found for \"%{query}\"", query: @search_query)}
+
+
+ {gettext("Try a different search term or check the spelling")}
+
+
+ <% end %>
+
+
+ """
+ end
+
@impl true
def mount(_params, _session, socket) do
{:ok,
diff --git a/lib/music_library_web/live/scrobble_live/index.html.heex b/lib/music_library_web/live/scrobble_live/index.html.heex
deleted file mode 100644
index ce476da1..00000000
--- a/lib/music_library_web/live/scrobble_live/index.html.heex
+++ /dev/null
@@ -1,129 +0,0 @@
-
-
-
- <%= if @search_results != [] && @selected_release_group == nil do %>
-
-
{gettext("Release Groups")}
-
-
-
-
~p"/images/cover-not-found.png" <> "';"}
- />
-
-
- {release_group.artists}
-
-
- {release_group.title}
-
-
- {Records.Record.format_release_date(release_group.release_date)}
- ·
- <.badge variant="soft" size="xs">{type_label(release_group.type)}
-
-
-
-
-
-
- <% end %>
-
- <%= if @selected_release_group && @releases != [] do %>
-
-
- <.button
- variant="ghost"
- size="sm"
- phx-click="clear_selection"
- >
- <.icon name="hero-arrow-left" class="icon" aria-hidden="true" data-slot="icon" />
- {gettext("Back")}
-
-
- {gettext("Releases for \"%{title}\"", title: @selected_release_group.title)}
-
-
-
-
-
- <.link
- navigate={~p"/scrobble/#{release.id}"}
- class="flex items-center gap-x-4 py-5 px-4 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-colors"
- >
- ~p"/images/cover-not-found.png" <> "';"}
- />
-
-
- {release.title}
-
-
- {release.date}
-
- {country_label(release.country)}
-
- <.badge :if={release.catalog_number} variant="soft" size="xs">
- {release.catalog_number}
-
-
- {ngettext("1 disc", "%{count} discs", Release.media_count(release))}
-
-
-
-
-
-
-
- <% end %>
-
- <%= if @loading && @search_query != "" do %>
-
- <.loading class="size-8 mx-auto text-zinc-400" />
-
- <%= if @selected_release_group do %>
- {gettext("Loading releases...")}
- <% else %>
- {gettext("Searching...")}
- <% end %>
-
-
- <% end %>
-
- <%= if @search_query != "" && @search_results == [] && not @loading do %>
-
- <.icon
- name="hero-magnifying-glass"
- class="h-12 w-12 mx-auto text-zinc-300 dark:text-zinc-600"
- />
-
- {gettext("No release groups found for \"%{query}\"", query: @search_query)}
-
-
- {gettext("Try a different search term or check the spelling")}
-
-
- <% end %>
-
-
diff --git a/lib/music_library_web/live/scrobble_live/show.ex b/lib/music_library_web/live/scrobble_live/show.ex
index ed3a3b1e..0b87be83 100644
--- a/lib/music_library_web/live/scrobble_live/show.ex
+++ b/lib/music_library_web/live/scrobble_live/show.ex
@@ -6,6 +6,105 @@ defmodule MusicLibraryWeb.ScrobbleLive.Show do
alias MusicLibrary.ScrobbleActivity
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+ <.alert
+ :if={not @can_scrobble}
+ color="warning"
+ title={gettext("Last.fm not connected")}
+ hide_close
+ >
+ {gettext(
+ "You need to connect your Last.fm account to scrobble. Please set up your Last.fm session key in the settings."
+ )}
+
+
+
+ <.button variant="ghost" size="sm" navigate={~p"/scrobble"}>
+ <.icon name="hero-arrow-left" class="icon" aria-hidden="true" data-slot="icon" />
+ {gettext("Back to search")}
+
+
+
+
+
+
~p"/images/cover-not-found.png" <> "';"}
+ />
+
+
+
+
+
+ {@release.artists |> Enum.map(& &1.name) |> Enum.join(", ")}
+
+
+
+ {@release.title}
+
+
+
+
+ <.dl_row :if={@release.date} label={gettext("Release Date")}>
+ {@release.date}
+
+ <.dl_row :if={@release.country} label={gettext("Country")}>
+ {country_label(@release.country)} {@release.country}
+
+ <.dl_row :if={@release.barcode} label={gettext("Barcode")}>
+ {@release.barcode}
+
+ <.dl_row :if={@release.catalog_number} label={gettext("Catalog Number")}>
+ {@release.catalog_number}
+
+ <.dl_row :if={@release.media != []} label={gettext("Media")}>
+ {ngettext("1 disc", "%{count} discs", MusicBrainz.Release.media_count(@release))}
+
+
+
+
+
+
+
{gettext("Tracks")}
+
+ <.button
+ :if={@can_scrobble}
+ phx-click={
+ if MapSet.size(@selected_tracks) > 0,
+ do: "scrobble_selected_tracks",
+ else: "scrobble_release"
+ }
+ size="sm"
+ >
+ {scrobble_button_label(@selected_tracks)}
+
+
+
+ <.medium
+ :for={medium <- @release.media}
+ can_scrobble?={@can_scrobble}
+ already_scrobbled={false}
+ medium={medium}
+ release_artists={@release.artists}
+ selected_tracks={@selected_tracks}
+ media_count={MusicBrainz.Release.media_count(@release)}
+ myself={nil}
+ />
+
+
+
+
+
+ """
+ end
+
@impl true
def mount(_params, _session, socket) do
{:ok,
diff --git a/lib/music_library_web/live/scrobble_live/show.html.heex b/lib/music_library_web/live/scrobble_live/show.html.heex
deleted file mode 100644
index e32d3eb3..00000000
--- a/lib/music_library_web/live/scrobble_live/show.html.heex
+++ /dev/null
@@ -1,93 +0,0 @@
-
-
- <.alert
- :if={not @can_scrobble}
- color="warning"
- title={gettext("Last.fm not connected")}
- hide_close
- >
- {gettext(
- "You need to connect your Last.fm account to scrobble. Please set up your Last.fm session key in the settings."
- )}
-
-
-
- <.button variant="ghost" size="sm" navigate={~p"/scrobble"}>
- <.icon name="hero-arrow-left" class="icon" aria-hidden="true" data-slot="icon" />
- {gettext("Back to search")}
-
-
-
-
-
-
~p"/images/cover-not-found.png" <> "';"}
- />
-
-
-
-
-
- {@release.artists |> Enum.map(& &1.name) |> Enum.join(", ")}
-
-
-
- {@release.title}
-
-
-
-
- <.dl_row :if={@release.date} label={gettext("Release Date")}>
- {@release.date}
-
- <.dl_row :if={@release.country} label={gettext("Country")}>
- {country_label(@release.country)} {@release.country}
-
- <.dl_row :if={@release.barcode} label={gettext("Barcode")}>
- {@release.barcode}
-
- <.dl_row :if={@release.catalog_number} label={gettext("Catalog Number")}>
- {@release.catalog_number}
-
- <.dl_row :if={@release.media != []} label={gettext("Media")}>
- {ngettext("1 disc", "%{count} discs", MusicBrainz.Release.media_count(@release))}
-
-
-
-
-
-
-
{gettext("Tracks")}
-
- <.button
- :if={@can_scrobble}
- phx-click={
- if MapSet.size(@selected_tracks) > 0,
- do: "scrobble_selected_tracks",
- else: "scrobble_release"
- }
- size="sm"
- >
- {scrobble_button_label(@selected_tracks)}
-
-
-
- <.medium
- :for={medium <- @release.media}
- can_scrobble?={@can_scrobble}
- already_scrobbled={false}
- medium={medium}
- release_artists={@release.artists}
- selected_tracks={@selected_tracks}
- media_count={MusicBrainz.Release.media_count(@release)}
- myself={nil}
- />
-
-
-
-
-
diff --git a/lib/music_library_web/live/scrobble_rules_live/index.ex b/lib/music_library_web/live/scrobble_rules_live/index.ex
index f95488ba..348a69e9 100644
--- a/lib/music_library_web/live/scrobble_rules_live/index.ex
+++ b/lib/music_library_web/live/scrobble_rules_live/index.ex
@@ -14,6 +14,143 @@ defmodule MusicLibraryWeb.ScrobbleRulesLive.Index do
order: :inserted_at
}
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+
+
+
+ <.icon name="hero-beaker" class="h-12 w-12 text-zinc-400 mx-auto mb-4" />
+
+ {gettext("No scrobble rules found")}
+
+
+
+
+
+ <.type_badge type={scrobble_rule.type} />
+
+
+
+ {scrobble_rule.match_value}
+
+
+ {scrobble_rule.target_musicbrainz_id}
+
+
+ {scrobble_rule.description}
+
+
+
+
+ <.status_badge enabled={scrobble_rule.enabled} />
+
+
+ {Calendar.strftime(scrobble_rule.inserted_at, "%Y-%m-%d")}
+
+
+
+ <.dropdown id={"actions-#{scrobble_rule.id}"} placement="bottom-end">
+ <:toggle>
+ <.button variant="ghost">
+ {gettext("Actions")}
+ <.icon
+ name="hero-ellipsis-vertical"
+ class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+
+ <.dropdown_button phx-click="apply_rule" phx-value-id={scrobble_rule.id}>
+ {gettext("Apply rule")}
+
+ <.dropdown_button phx-click="toggle_enabled" phx-value-id={scrobble_rule.id}>
+ {if scrobble_rule.enabled, do: gettext("Disable rule"), else: gettext("Enable rule")}
+
+ <.dropdown_link
+ id={"actions-#{scrobble_rule.id}-edit"}
+ patch={
+ ~p"/scrobble-rules/#{scrobble_rule}/edit?#{Map.take(@list_params, [:page, :page_size])}"
+ }
+ >
+ {gettext("Edit")}
+
+ <.separator />
+ <.dropdown_button
+ phx-click="delete"
+ phx-value-id={scrobble_rule.id}
+ data-confirm={gettext("Are you sure?")}
+ class={[
+ "text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
+ ]}
+ >
+ {gettext("Delete")}
+
+
+
+
+
+
+ <.pagination id={:bottom_pagination} pagination_params={@list_params} />
+
+
+ <.structured_modal
+ :if={@live_action in [:new, :edit]}
+ id="scrobble_rule-modal"
+ on_close={JS.patch(back_path(@list_params))}
+ >
+ <.live_component
+ module={MusicLibraryWeb.ScrobbleRulesLive.Form}
+ id={@scrobble_rule.id || :new}
+ title={@page_title}
+ action={@live_action}
+ scrobble_rule={@scrobble_rule}
+ patch={back_path(@list_params)}
+ />
+
+
+ """
+ end
+
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, :current_section, :scrobble_rules)}
diff --git a/lib/music_library_web/live/scrobble_rules_live/index.html.heex b/lib/music_library_web/live/scrobble_rules_live/index.html.heex
deleted file mode 100644
index 8eaeb64e..00000000
--- a/lib/music_library_web/live/scrobble_rules_live/index.html.heex
+++ /dev/null
@@ -1,131 +0,0 @@
-
-
-
-
-
-
- <.icon name="hero-beaker" class="h-12 w-12 text-zinc-400 mx-auto mb-4" />
-
- {gettext("No scrobble rules found")}
-
-
-
-
-
- <.type_badge type={scrobble_rule.type} />
-
-
-
- {scrobble_rule.match_value}
-
-
- {scrobble_rule.target_musicbrainz_id}
-
-
- {scrobble_rule.description}
-
-
-
-
- <.status_badge enabled={scrobble_rule.enabled} />
-
-
- {Calendar.strftime(scrobble_rule.inserted_at, "%Y-%m-%d")}
-
-
-
- <.dropdown id={"actions-#{scrobble_rule.id}"} placement="bottom-end">
- <:toggle>
- <.button variant="ghost">
- {gettext("Actions")}
- <.icon
- name="hero-ellipsis-vertical"
- class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
- aria-hidden="true"
- data-slot="icon"
- />
-
-
- <.dropdown_button phx-click="apply_rule" phx-value-id={scrobble_rule.id}>
- {gettext("Apply rule")}
-
- <.dropdown_button phx-click="toggle_enabled" phx-value-id={scrobble_rule.id}>
- {if scrobble_rule.enabled, do: gettext("Disable rule"), else: gettext("Enable rule")}
-
- <.dropdown_link
- id={"actions-#{scrobble_rule.id}-edit"}
- patch={
- ~p"/scrobble-rules/#{scrobble_rule}/edit?#{Map.take(@list_params, [:page, :page_size])}"
- }
- >
- {gettext("Edit")}
-
- <.separator />
- <.dropdown_button
- phx-click="delete"
- phx-value-id={scrobble_rule.id}
- data-confirm={gettext("Are you sure?")}
- class={[
- "text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
- ]}
- >
- {gettext("Delete")}
-
-
-
-
-
-
- <.pagination id={:bottom_pagination} pagination_params={@list_params} />
-
-
- <.structured_modal
- :if={@live_action in [:new, :edit]}
- id="scrobble_rule-modal"
- on_close={JS.patch(back_path(@list_params))}
- >
- <.live_component
- module={MusicLibraryWeb.ScrobbleRulesLive.Form}
- id={@scrobble_rule.id || :new}
- title={@page_title}
- action={@live_action}
- scrobble_rule={@scrobble_rule}
- patch={back_path(@list_params)}
- />
-
-
diff --git a/lib/music_library_web/live/scrobbled_tracks_live/index.ex b/lib/music_library_web/live/scrobbled_tracks_live/index.ex
index 7c297966..5b5555a8 100644
--- a/lib/music_library_web/live/scrobbled_tracks_live/index.ex
+++ b/lib/music_library_web/live/scrobbled_tracks_live/index.ex
@@ -16,6 +16,197 @@ defmodule MusicLibraryWeb.ScrobbledTracksLive.Index do
order: :scrobbled_at
}
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+
+
+ <.button_group>
+ <.button
+ patch={order_path(@track_list_params, :scrobbled_at)}
+ size="xs"
+ class={[
+ @track_list_params.order == :scrobbled_at && "bg-zinc-100! dark:bg-zinc-700!"
+ ]}
+ >
+ <.icon
+ name="hero-clock-solid"
+ class="icon"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+ {gettext("Scrobbled")}
+
+ <.button
+ patch={order_path(@track_list_params, :title)}
+ size="xs"
+ class={[
+ @track_list_params.order == :title && "bg-zinc-100! dark:bg-zinc-700!"
+ ]}
+ >
+ <.icon name="hero-musical-note-solid" class="icon" aria-hidden="true" data-slot="icon" />
+ {gettext("Title")}
+
+ <.button
+ patch={order_path(@track_list_params, :artist)}
+ size="xs"
+ class={[
+ @track_list_params.order == :artist && "bg-zinc-100! dark:bg-zinc-700!"
+ ]}
+ >
+ <.icon name="hero-user-solid" class="icon" aria-hidden="true" data-slot="icon" />
+ {gettext("Artist")}
+
+ <.button
+ patch={order_path(@track_list_params, :album)}
+ size="xs"
+ class={[
+ @track_list_params.order == :album && "bg-zinc-100! dark:bg-zinc-700!"
+ ]}
+ >
+ <.icon name="hero-musical-note-solid" class="icon" aria-hidden="true" data-slot="icon" />
+ {gettext("Album")}
+
+
+ <.refresh_lastfm_feed_button />
+
+
+
+
+
+
+ <.icon name="hero-musical-note" class="h-12 w-12 text-zinc-400 mx-auto mb-4" />
+
+ {gettext("No scrobbled tracks found")}
+
+
+
+
+
+
+
~p"/images/cover-not-found.png" <> "';"}
+ />
+
+
+
+
+ {track.title}
+
+
+ {track.artist.name}
+
+
+ {track.album.title}
+
+
+ {track.scrobbled_at_label}
+
+ <.track_metadata_tooltip track={track} />
+
+
+
+
+ <.record_status_badges
+ musicbrainz_id={track.album.musicbrainz_id}
+ collected_record_id={collected_record_id}
+ wishlisted_record_id={wishlisted_record_id}
+ />
+
+ <.import_format_dropdown
+ :if={
+ track.album.musicbrainz_id !== "" and !collected_record_id and
+ !wishlisted_record_id
+ }
+ id={"actions-#{track.scrobbled_at_uts}-tracks"}
+ musicbrainz_id={track.album.musicbrainz_id}
+ />
+ <.dropdown id={"actions-#{track.scrobbled_at_uts}"} placement="bottom-end">
+ <:toggle>
+ <.button variant="ghost">
+ {gettext("Actions")}
+ <.icon
+ name="hero-ellipsis-vertical"
+ class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
+ aria-hidden="true"
+ data-slot="icon"
+ />
+
+
+ <.dropdown_link patch={~p"/scrobbled-tracks/#{track.scrobbled_at_uts}/edit"}>
+ {gettext("Edit")}
+
+ <.dropdown_button
+ phx-click={
+ JS.push("delete", value: %{"scrobbled-at-uts": track.scrobbled_at_uts})
+ |> JS.hide(to: "##{id}")
+ }
+ data-confirm={gettext("Are you sure?")}
+ class={[
+ "text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
+ ]}
+ >
+ {gettext("Delete")}
+
+
+
+
+
+
+ <.pagination id={:bottom_pagination} pagination_params={@track_list_params} />
+
+
+ <.structured_modal
+ :if={@live_action == :edit}
+ id="track-modal"
+ on_close={JS.patch(back_path(@track_list_params))}
+ >
+ <.live_component
+ module={MusicLibraryWeb.ScrobbledTracksLive.Form}
+ id={@track.scrobbled_at_uts}
+ action={@live_action}
+ track={@track}
+ patch={back_path(@track_list_params)}
+ />
+
+
+ """
+ end
+
@impl true
def mount(_params, _session, socket) do
socket =
diff --git a/lib/music_library_web/live/scrobbled_tracks_live/index.html.heex b/lib/music_library_web/live/scrobbled_tracks_live/index.html.heex
deleted file mode 100644
index a2ad6fa8..00000000
--- a/lib/music_library_web/live/scrobbled_tracks_live/index.html.heex
+++ /dev/null
@@ -1,185 +0,0 @@
-
-
-
-
-
- <.button_group>
- <.button
- patch={order_path(@track_list_params, :scrobbled_at)}
- size="xs"
- class={[
- @track_list_params.order == :scrobbled_at && "bg-zinc-100! dark:bg-zinc-700!"
- ]}
- >
- <.icon
- name="hero-clock-solid"
- class="icon"
- aria-hidden="true"
- data-slot="icon"
- />
- {gettext("Scrobbled")}
-
- <.button
- patch={order_path(@track_list_params, :title)}
- size="xs"
- class={[
- @track_list_params.order == :title && "bg-zinc-100! dark:bg-zinc-700!"
- ]}
- >
- <.icon name="hero-musical-note-solid" class="icon" aria-hidden="true" data-slot="icon" />
- {gettext("Title")}
-
- <.button
- patch={order_path(@track_list_params, :artist)}
- size="xs"
- class={[
- @track_list_params.order == :artist && "bg-zinc-100! dark:bg-zinc-700!"
- ]}
- >
- <.icon name="hero-user-solid" class="icon" aria-hidden="true" data-slot="icon" />
- {gettext("Artist")}
-
- <.button
- patch={order_path(@track_list_params, :album)}
- size="xs"
- class={[
- @track_list_params.order == :album && "bg-zinc-100! dark:bg-zinc-700!"
- ]}
- >
- <.icon name="hero-musical-note-solid" class="icon" aria-hidden="true" data-slot="icon" />
- {gettext("Album")}
-
-
- <.refresh_lastfm_feed_button />
-
-
-
-
-
-
- <.icon name="hero-musical-note" class="h-12 w-12 text-zinc-400 mx-auto mb-4" />
-
- {gettext("No scrobbled tracks found")}
-
-
-
-
-
-
-
~p"/images/cover-not-found.png" <> "';"}
- />
-
-
-
-
- {track.title}
-
-
- {track.artist.name}
-
-
- {track.album.title}
-
-
- {track.scrobbled_at_label}
-
- <.track_metadata_tooltip track={track} />
-
-
-
-
- <.record_status_badges
- musicbrainz_id={track.album.musicbrainz_id}
- collected_record_id={collected_record_id}
- wishlisted_record_id={wishlisted_record_id}
- />
-
- <.import_format_dropdown
- :if={
- track.album.musicbrainz_id !== "" and !collected_record_id and
- !wishlisted_record_id
- }
- id={"actions-#{track.scrobbled_at_uts}-tracks"}
- musicbrainz_id={track.album.musicbrainz_id}
- />
- <.dropdown id={"actions-#{track.scrobbled_at_uts}"} placement="bottom-end">
- <:toggle>
- <.button variant="ghost">
- {gettext("Actions")}
- <.icon
- name="hero-ellipsis-vertical"
- class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
- aria-hidden="true"
- data-slot="icon"
- />
-
-
- <.dropdown_link patch={~p"/scrobbled-tracks/#{track.scrobbled_at_uts}/edit"}>
- {gettext("Edit")}
-
- <.dropdown_button
- phx-click={
- JS.push("delete", value: %{"scrobbled-at-uts": track.scrobbled_at_uts})
- |> JS.hide(to: "##{id}")
- }
- data-confirm={gettext("Are you sure?")}
- class={[
- "text-red-900! hover:bg-red-50! dark:text-red-500! dark:hover:bg-red-900/30! dark:hover:text-red-600!"
- ]}
- >
- {gettext("Delete")}
-
-
-
-
-
-
- <.pagination id={:bottom_pagination} pagination_params={@track_list_params} />
-
-
- <.structured_modal
- :if={@live_action == :edit}
- id="track-modal"
- on_close={JS.patch(back_path(@track_list_params))}
- >
- <.live_component
- module={MusicLibraryWeb.ScrobbledTracksLive.Form}
- id={@track.scrobbled_at_uts}
- action={@live_action}
- track={@track}
- patch={back_path(@track_list_params)}
- />
-
-
diff --git a/lib/music_library_web/live/stats_live/index.ex b/lib/music_library_web/live/stats_live/index.ex
index c5245f20..c533bbcb 100644
--- a/lib/music_library_web/live/stats_live/index.ex
+++ b/lib/music_library_web/live/stats_live/index.ex
@@ -11,6 +11,337 @@ defmodule MusicLibraryWeb.StatsLive.Index do
alias MusicLibrary.{Collection, Records, ScrobbleActivity, Wishlist}
alias MusicLibraryWeb.StatsLive.{TopAlbums, TopArtists}
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+
+
+ {gettext("Records")}
+
+
+ <.album_preview
+ record={@latest_record}
+ title={gettext("Latest purchase")}
+ class="col-span-3 sm:col-span-2"
+ />
+ <.counter
+ title={gettext("Collection")}
+ count={@collection_count}
+ path={~p"/collection"}
+ />
+ <.counter title={gettext("Wishlist")} count={@wishlist_count} path={~p"/wishlist"} />
+ <.counter
+ title={gettext("Scrobbles")}
+ count={to_compact(@scrobble_count)}
+ tooltip={@scrobble_count}
+ />
+
+
+
+
+
+
+ {gettext("Formats")}
+
+ <.counters_by_category
+ categories_with_counts={@collection_count_by_format}
+ category_format_fn={&format_label/1}
+ category_path_fn={fn format -> ~p"/collection?query=format:#{format}" end}
+ />
+
+
+
+
+ {gettext("Types")}
+
+ <.counters_by_category
+ categories_with_counts={@collection_count_by_type}
+ category_format_fn={&type_label/1}
+ category_path_fn={fn type -> ~p"/collection?query=type:#{type}" end}
+ />
+
+
+
+
+
+
+
+
+ {gettext("Scrobble activity")}
+
+ <.refresh_lastfm_feed_button />
+
+ <.tabs id="scrobble-activity" class="mt-4">
+ <.tabs_list active_tab={@scrobble_activity_mode} variant="segmented" class="w-48">
+ <:tab
+ name="albums"
+ phx-click={JS.push("set_scrobble_activity_mode", value: %{mode: "albums"})}
+ >
+ {gettext("Albums")}
+
+ <:tab
+ name="tracks"
+ phx-click={JS.push("set_scrobble_activity_mode", value: %{mode: "tracks"})}
+ >
+ {gettext("Tracks")}
+
+
+
+ <.tabs_panel name="albums" active={@scrobble_activity_mode == "albums"}>
+
+
+
+
+
+
+
+
+
+
+ {album.artist.name}
+
+ <.link
+ :if={artist_id(album, artist_id)}
+ class="font-semibold text-sm block text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
+ navigate={~p"/artists/#{artist_id(album, artist_id)}"}
+ >
+ {album.artist.name}
+
+
+ {album.metadata.title}
+
+
+ {album.scrobbled_at_label}
+
+ <.album_metadata_tooltip album={album} />
+
+
+
+ <.record_status_badges
+ musicbrainz_id={album.metadata.musicbrainz_id}
+ collected_record_id={collected_record_id}
+ wishlisted_record_id={wishlisted_record_id}
+ />
+
+ <.import_format_dropdown
+ :if={
+ album.metadata.musicbrainz_id !== "" and !collected_record_id and
+ !wishlisted_record_id
+ }
+ id={"actions-#{album.scrobbled_at_uts}-albums"}
+ musicbrainz_id={album.metadata.musicbrainz_id}
+ />
+
+
+
+
+
+ <.tabs_panel name="tracks" active={@scrobble_activity_mode == "tracks"}>
+
+
+
+
+
+
+
+
+
+
+ {track.artist.name}
+
+ <.link
+ :if={artist_id(track, artist_id)}
+ class="font-semibold text-sm block text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
+ navigate={~p"/artists/#{artist_id(track, artist_id)}"}
+ >
+ {track.artist.name}
+
+
+ {track.title}
+
+
+ {track.album.title}
+
+
+ {track.scrobbled_at_label}
+
+ <.track_metadata_tooltip track={track} />
+
+
+
+ <.record_status_badges
+ musicbrainz_id={track.album.musicbrainz_id}
+ collected_record_id={collected_record_id}
+ wishlisted_record_id={wishlisted_record_id}
+ />
+
+ <.import_format_dropdown
+ :if={
+ track.album.musicbrainz_id !== "" and !collected_record_id and
+ !wishlisted_record_id
+ }
+ id={"actions-#{track.scrobbled_at_uts}-tracks"}
+ musicbrainz_id={track.album.musicbrainz_id}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ {gettext("On This day")}
+
+ <.form
+ :let={f}
+ for={to_form(%{"current_date" => @current_date})}
+ phx-change="set_current_date"
+ >
+ <.date_picker size="xs" field={f[:current_date]}>
+ <:outer_suffix>
+ <.button
+ size="xs"
+ type="button"
+ phx-click={
+ JS.push("set_current_date", value: %{"current_date" => Date.utc_today()})
+ }
+ >
+ Today
+
+
+
+
+
+
+ <.records_on_this_day
+ current_date={@current_date}
+ records={@records_on_this_day}
+ record_show_path={fn record -> ~p"/collection/#{record}" end}
+ />
+
+
+
+
+ {gettext("Top %{n} Collection Artists", %{n: length(@records_by_artist)})}
+
+
+ <.vertical_bar_chart
+ data={@records_by_artist}
+ width={600}
+ height={26 * length(@records_by_artist)}
+ color_class="fill-red-500"
+ label_fn={fn datum -> datum.name end}
+ value_fn={fn datum -> datum.count end}
+ datum_click={
+ fn datum ->
+ JS.navigate(~p"/artists/#{datum.id}")
+ end
+ }
+ class="w-full"
+ />
+
+
+
+
+
+ {gettext("Top %{n} Collection Genres", %{n: length(@records_by_genre)})}
+
+
+ <.vertical_bar_chart
+ data={@records_by_genre}
+ width={600}
+ height={26 * length(@records_by_genre)}
+ color_class="fill-zinc-500"
+ label_fn={fn {genre, _count} -> genre end}
+ value_fn={fn {_genre, count} -> count end}
+ datum_click={
+ fn {genre, _count} ->
+ JS.navigate(~p"/collection?#{%{query: ~s(genre:"#{genre}")}}")
+ end
+ }
+ class="w-full"
+ />
+
+
+
+
+ """
+ end
+
+ @impl true
def mount(_params, _session, socket) do
current_date = DateTime.now!(socket.assigns.timezone) |> DateTime.to_date()
latest_record = Collection.get_latest_record()
@@ -44,6 +375,7 @@ defmodule MusicLibraryWeb.StatsLive.Index do
)}
end
+ @impl true
def handle_event("refresh_lastfm_feed", _, socket) do
LastFm.refresh_scrobbled_tracks()
{:noreply, socket}
@@ -96,6 +428,7 @@ defmodule MusicLibraryWeb.StatsLive.Index do
end
end
+ @impl true
def handle_info(%{track_count: 0}, socket) do
{:noreply, socket}
end
diff --git a/lib/music_library_web/live/stats_live/index.html.heex b/lib/music_library_web/live/stats_live/index.html.heex
deleted file mode 100644
index 3ebc85d8..00000000
--- a/lib/music_library_web/live/stats_live/index.html.heex
+++ /dev/null
@@ -1,324 +0,0 @@
-
-
-
- {gettext("Records")}
-
-
- <.album_preview
- record={@latest_record}
- title={gettext("Latest purchase")}
- class="col-span-3 sm:col-span-2"
- />
- <.counter
- title={gettext("Collection")}
- count={@collection_count}
- path={~p"/collection"}
- />
- <.counter title={gettext("Wishlist")} count={@wishlist_count} path={~p"/wishlist"} />
- <.counter
- title={gettext("Scrobbles")}
- count={to_compact(@scrobble_count)}
- tooltip={@scrobble_count}
- />
-
-
-
-
-
-
- {gettext("Formats")}
-
- <.counters_by_category
- categories_with_counts={@collection_count_by_format}
- category_format_fn={&format_label/1}
- category_path_fn={fn format -> ~p"/collection?query=format:#{format}" end}
- />
-
-
-
-
- {gettext("Types")}
-
- <.counters_by_category
- categories_with_counts={@collection_count_by_type}
- category_format_fn={&type_label/1}
- category_path_fn={fn type -> ~p"/collection?query=type:#{type}" end}
- />
-
-
-
-
-
-
-
-
- {gettext("Scrobble activity")}
-
- <.refresh_lastfm_feed_button />
-
- <.tabs id="scrobble-activity" class="mt-4">
- <.tabs_list active_tab={@scrobble_activity_mode} variant="segmented" class="w-48">
- <:tab
- name="albums"
- phx-click={JS.push("set_scrobble_activity_mode", value: %{mode: "albums"})}
- >
- {gettext("Albums")}
-
- <:tab
- name="tracks"
- phx-click={JS.push("set_scrobble_activity_mode", value: %{mode: "tracks"})}
- >
- {gettext("Tracks")}
-
-
-
- <.tabs_panel name="albums" active={@scrobble_activity_mode == "albums"}>
-
-
-
-
-
-
-
-
-
-
- {album.artist.name}
-
- <.link
- :if={artist_id(album, artist_id)}
- class="font-semibold text-sm block text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
- navigate={~p"/artists/#{artist_id(album, artist_id)}"}
- >
- {album.artist.name}
-
-
- {album.metadata.title}
-
-
- {album.scrobbled_at_label}
-
- <.album_metadata_tooltip album={album} />
-
-
-
- <.record_status_badges
- musicbrainz_id={album.metadata.musicbrainz_id}
- collected_record_id={collected_record_id}
- wishlisted_record_id={wishlisted_record_id}
- />
-
- <.import_format_dropdown
- :if={
- album.metadata.musicbrainz_id !== "" and !collected_record_id and
- !wishlisted_record_id
- }
- id={"actions-#{album.scrobbled_at_uts}-albums"}
- musicbrainz_id={album.metadata.musicbrainz_id}
- />
-
-
-
-
-
- <.tabs_panel name="tracks" active={@scrobble_activity_mode == "tracks"}>
-
-
-
-
-
-
-
-
-
-
- {track.artist.name}
-
- <.link
- :if={artist_id(track, artist_id)}
- class="font-semibold text-sm block text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
- navigate={~p"/artists/#{artist_id(track, artist_id)}"}
- >
- {track.artist.name}
-
-
- {track.title}
-
-
- {track.album.title}
-
-
- {track.scrobbled_at_label}
-
- <.track_metadata_tooltip track={track} />
-
-
-
- <.record_status_badges
- musicbrainz_id={track.album.musicbrainz_id}
- collected_record_id={collected_record_id}
- wishlisted_record_id={wishlisted_record_id}
- />
-
- <.import_format_dropdown
- :if={
- track.album.musicbrainz_id !== "" and !collected_record_id and
- !wishlisted_record_id
- }
- id={"actions-#{track.scrobbled_at_uts}-tracks"}
- musicbrainz_id={track.album.musicbrainz_id}
- />
-
-
-
-
-
-
-
-
-
-
-
-
- {gettext("On This day")}
-
- <.form
- :let={f}
- for={to_form(%{"current_date" => @current_date})}
- phx-change="set_current_date"
- >
- <.date_picker size="xs" field={f[:current_date]}>
- <:outer_suffix>
- <.button
- size="xs"
- type="button"
- phx-click={
- JS.push("set_current_date", value: %{"current_date" => Date.utc_today()})
- }
- >
- Today
-
-
-
-
-
-
- <.records_on_this_day
- current_date={@current_date}
- records={@records_on_this_day}
- record_show_path={fn record -> ~p"/collection/#{record}" end}
- />
-
-
-
-
- {gettext("Top %{n} Collection Artists", %{n: length(@records_by_artist)})}
-
-
- <.vertical_bar_chart
- data={@records_by_artist}
- width={600}
- height={26 * length(@records_by_artist)}
- color_class="fill-red-500"
- label_fn={fn datum -> datum.name end}
- value_fn={fn datum -> datum.count end}
- datum_click={
- fn datum ->
- JS.navigate(~p"/artists/#{datum.id}")
- end
- }
- class="w-full"
- />
-
-
-
-
-
- {gettext("Top %{n} Collection Genres", %{n: length(@records_by_genre)})}
-
-
- <.vertical_bar_chart
- data={@records_by_genre}
- width={600}
- height={26 * length(@records_by_genre)}
- color_class="fill-zinc-500"
- label_fn={fn {genre, _count} -> genre end}
- value_fn={fn {_genre, count} -> count end}
- datum_click={
- fn {genre, _count} ->
- JS.navigate(~p"/collection?#{%{query: ~s(genre:"#{genre}")}}")
- end
- }
- class="w-full"
- />
-
-
-
-
diff --git a/lib/music_library_web/live/universal_search_live/index.ex b/lib/music_library_web/live/universal_search_live/index.ex
index 900f942a..e25bef60 100644
--- a/lib/music_library_web/live/universal_search_live/index.ex
+++ b/lib/music_library_web/live/universal_search_live/index.ex
@@ -6,6 +6,137 @@ defmodule MusicLibraryWeb.UniversalSearchLive.Index do
alias MusicLibrary.Search
alias Phoenix.LiveView.ColocatedHook
+ @impl true
+ def render(assigns) do
+ ~H"""
+
+ <.structured_modal
+ :if={@show_modal}
+ id="universal-search-root"
+ open={@show_modal}
+ on_close={JS.push("close_modal", value: %{}, target: "#universal-search")}
+ >
+
+
+ <.empty_state :if={@search_query == ""} />
+
+ <.no_results :if={@search_query != "" and @total_results == 0} query={@search_query} />
+
+
0} class="max-h-148 md:max-h-164 overflow-y-auto mt-4">
+ <.search_result_group
+ :if={length(@search_results.artists) > 0}
+ title="Artists"
+ count={length(@search_results.artists)}
+ total_count={@search_counts.artists_count}
+ >
+ <.search_result_artist
+ :for={%{artist: artist, image_data_hash: image_data_hash} <- @search_results.artists}
+ artist={artist}
+ image_data_hash={image_data_hash}
+ phx-click="navigate_to_artist"
+ phx-value-id={artist.musicbrainz_id}
+ phx-target={@myself}
+ />
+
+
+ <.search_result_group
+ :if={length(@search_results.collection) > 0}
+ title="Collection"
+ count={length(@search_results.collection)}
+ total_count={@search_counts.collection_count}
+ >
+ <.search_result_record
+ :for={record <- @search_results.collection}
+ record={record}
+ type={:collection}
+ phx-click="navigate_to_record"
+ phx-value-id={record.id}
+ phx-value-type="collection"
+ phx-target={@myself}
+ />
+ <:actions>
+ <.view_all_button
+ :if={@search_counts.collection_count > length(@search_results.collection)}
+ count={@search_counts.collection_count}
+ target="collection"
+ phx-click="navigate_to_collection"
+ phx-value-query={@search_query}
+ phx-target={@myself}
+ />
+
+
+ <.search_result_group
+ :if={length(@search_results.wishlist) > 0}
+ title="Wishlist"
+ count={length(@search_results.wishlist)}
+ total_count={@search_counts.wishlist_count}
+ >
+ <.search_result_record
+ :for={record <- @search_results.wishlist}
+ record={record}
+ type={:wishlist}
+ phx-click="navigate_to_record"
+ phx-value-id={record.id}
+ phx-value-type="wishlist"
+ phx-target={@myself}
+ />
+ <:actions>
+ <.view_all_button
+ :if={@search_counts.wishlist_count > length(@search_results.wishlist)}
+ count={@search_counts.wishlist_count}
+ target="wishlist"
+ phx-click="navigate_to_wishlist"
+ phx-value-query={@search_query}
+ phx-target={@myself}
+ />
+
+
+ <.search_result_group
+ :if={length(@search_results.record_sets) > 0}
+ title="Record Sets"
+ count={length(@search_results.record_sets)}
+ total_count={@search_counts.record_sets_count}
+ >
+ <.search_result_record_set
+ :for={record_set <- @search_results.record_sets}
+ record_set={record_set}
+ phx-click="navigate_to_record_set"
+ phx-value-id={record_set.id}
+ phx-target={@myself}
+ />
+ <:actions>
+ <.view_all_button
+ :if={@search_counts.record_sets_count > length(@search_results.record_sets)}
+ count={@search_counts.record_sets_count}
+ target="record set"
+ phx-click="navigate_to_record_sets"
+ phx-value-query={@search_query}
+ phx-target={@myself}
+ />
+
+
+ <.results_footer total_results={@total_results} />
+
+
+ """
+ end
+
def universal_search_trigger(assigns) do
~H"""