From 75e2ec5c7019609cd03e1d762fd6ef92abbd8631 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Fri, 29 May 2026 08:50:27 +0300 Subject: [PATCH] ML-199: extract shared record show logic --- ...tract-shared-record-show-LiveView-logic.md | 93 ++++ docs/architecture.md | 45 +- .../components/record_components.ex | 275 +++++++++++ .../live/collection_live/show.ex | 455 ++++-------------- .../live/wishlist_live/show.ex | 407 +++------------- .../live_helpers/record_show.ex | 151 ++++++ priv/gettext/default.pot | 56 +-- priv/gettext/en/LC_MESSAGES/default.po | 56 +-- 8 files changed, 726 insertions(+), 812 deletions(-) create mode 100644 backlog/tasks/ml-199 - Extract-shared-record-show-LiveView-logic.md create mode 100644 lib/music_library_web/live_helpers/record_show.ex diff --git a/backlog/tasks/ml-199 - Extract-shared-record-show-LiveView-logic.md b/backlog/tasks/ml-199 - Extract-shared-record-show-LiveView-logic.md new file mode 100644 index 00000000..387fa438 --- /dev/null +++ b/backlog/tasks/ml-199 - Extract-shared-record-show-LiveView-logic.md @@ -0,0 +1,93 @@ +--- +id: ML-199 +title: Extract shared record show LiveView logic +status: Done +assignee: + - pi +created_date: "2026-05-29 05:29" +updated_date: "2026-05-29 05:39" +labels: + - refactor + - liveview +dependencies: [] +references: + - lib/music_library_web/live/collection_live/show.ex + - lib/music_library_web/live/wishlist_live/show.ex + - lib/music_library_web/live_helpers/record_actions.ex + - lib/music_library_web/components/record_components.ex +documentation: + - docs/architecture.md + - docs/project-conventions.md + - .agents/skills/ui-framework/SKILL.md +modified_files: + - lib/music_library_web/live/collection_live/show.ex + - lib/music_library_web/live/wishlist_live/show.ex + - lib/music_library_web/live_helpers/record_show.ex + - lib/music_library_web/components/record_components.ex + - docs/architecture.md + - priv/gettext/default.pot + - priv/gettext/en/LC_MESSAGES/default.po +priority: medium +ordinal: 32000 +--- + +## Description + + + +Refactor the duplicated Collection and Wishlist record show LiveViews so common record detail rendering and event handling live in shared web-layer helpers/components while preserving page-specific collection and wishlist behavior. + + + +## Acceptance Criteria + + + +- [x] #1 Collection and Wishlist show pages reuse shared rendering for the common action bar, selected release row, release sheet, chat component, and edit modal. +- [x] #2 Collection and Wishlist show pages reuse shared handlers for common record actions, scrobble async handling, saved-record handling, chat-count refresh, and background record updates. +- [x] #3 Collection-specific behavior remains intact: notes, regenerate embeddings, purchased/listening details, similar records, print-enabled release sheet, and editable purchased_at field. +- [x] #4 Wishlist-specific behavior remains intact: purchase action, online store links, unreleased metadata, print-disabled release sheet, and hidden purchased_at field. +- [x] #5 Relevant focused tests or compile checks pass after the refactor. +- [x] #6 Architecture documentation reflects any new shared helper module added by the refactor. + + +## Implementation Plan + + + +1. Add `MusicLibraryWeb.LiveHelpers.RecordShow` for shared Collection/Wishlist show-page logic: common record assigns, delete navigation, scrobble release start/async result handling, saved-record assignment, chat-count refresh, background-update guard/toast, and shared page-title construction. +2. Extend `MusicLibraryWeb.RecordComponents` with focused shared components for the common show-page action bar, selected-release row, release sheet, chat component, and edit modal. Use slots for page-specific dropdown actions so collection and wishlist behavior stays explicit. +3. Refactor `CollectionLive.Show` and `WishlistLive.Show` to call the shared helper/components while keeping local page-specific sections and handlers: collection notes/regenerate/similar/listening details and wishlist purchase/online-store/current-date behavior. +4. Update `docs/architecture.md` to mention the new shared `LiveHelpers.RecordShow` module in the web utility modules table. +5. Run focused formatting/compile/tests for the touched LiveViews/components, then mark acceptance criteria based on the results. + + +## Implementation Notes + + + +Implemented shared `LiveHelpers.RecordShow` for common record loading, delete navigation, scrobble async handling, saved-record handling, chat refresh, and background update behavior. Added shared record show components for the action bar, selected release row, release sheet, chat component, and edit modal. Preserved collection-only notes/regenerate/listening/similar-record behavior and wishlist-only purchase/online-store/unreleased behavior. Verification: `mix compile --warnings-as-errors`, `mix test test/music_library_web/live/collection_live/show_test.exs test/music_library_web/live/wishlist_live/show_test.exs`, `mix gettext.extract --check-up-to-date`, `mix format --check-formatted ...`, and `mix credo --strict` all passed. + + + +## Final Summary + + + +Refactored the Collection and Wishlist record show LiveViews to remove duplicated show-page rendering and lifecycle logic. + +Changes: + +- Added `MusicLibraryWeb.LiveHelpers.RecordShow` for shared record loading, page title assignment, delete navigation, common action dispatch, scrobble async handling, saved-record handling, chat refreshes, and guarded background record updates. +- Added shared `RecordComponents` functions for the common show action bar, selected release row, release sheet, record chat component, and edit modal. +- Updated `CollectionLive.Show` and `WishlistLive.Show` to compose the shared helpers/components while keeping collection-only and wishlist-only sections explicit. +- Updated gettext references after moving user-facing strings and documented `LiveHelpers.RecordShow` in `docs/architecture.md`. + +Verification: + +- `mix compile --warnings-as-errors` +- `mix test test/music_library_web/live/collection_live/show_test.exs test/music_library_web/live/wishlist_live/show_test.exs` +- `mix gettext.extract --check-up-to-date` +- `mix format --check-formatted lib/music_library_web/live_helpers/record_show.ex lib/music_library_web/components/record_components.ex lib/music_library_web/live/collection_live/show.ex lib/music_library_web/live/wishlist_live/show.ex` +- `mix credo --strict` + diff --git a/docs/architecture.md b/docs/architecture.md index 270ff124..9e0194a6 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -313,31 +313,32 @@ All authenticated routes live inside a single `live_session` with three `on_moun ### Shared Component Modules (lib/music_library_web/components/) -| Module | Purpose | -| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `CoreComponents` | Forms, buttons, icons, tables, flash messages | -| `Layouts` | Application layout templates, navigation components (`dropdown_nav/1`) | -| `RecordComponents` | Record cards, cover images, artist images, labels, grids, release status icon badge, shared show-page sections (title, external links, genres, releases, timestamps, debug) | -| `ChartComponents` | Charts for stats dashboard | -| `StatsComponents` | Stats dashboard widgets (`section/1` layout, counters, album preview, records on this day) | -| `ScrobbleComponents` | Scrobble activity displays: status badges, import dropdowns, metadata tooltips, and record-matching UI | -| `SearchComponents` | Search result rendering | -| `CartComponents` | `cart_sidebar/1` — shared cart aside used by `AddRecord` and `BarcodeScanner` | -| `Pagination` | Pagination UI and logic | +| Module | Purpose | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `CoreComponents` | Forms, buttons, icons, tables, flash messages | +| `Layouts` | Application layout templates, navigation components (`dropdown_nav/1`) | +| `RecordComponents` | Record cards, cover images, artist images, labels, grids, release status badges, and shared record show-page sections/actions/sheets | +| `ChartComponents` | Charts for stats dashboard | +| `StatsComponents` | Stats dashboard widgets (`section/1` layout, counters, album preview, records on this day) | +| `ScrobbleComponents` | Scrobble activity displays: status badges, import dropdowns, metadata tooltips, and record-matching UI | +| `SearchComponents` | Search result rendering | +| `CartComponents` | `cart_sidebar/1` — shared cart aside used by `AddRecord` and `BarcodeScanner` | +| `Pagination` | Pagination UI and logic | ### Web Utility Modules (lib/music_library_web/) -| Module | Purpose | -| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| `ErrorMessages` | Maps internal error terms (atoms, structs) to user-friendly gettext strings via `friendly_message/1` | -| `Markdown` | Markdown-to-HTML conversion (MDEx with ammonia sanitization) with `[[double bracket]]` link syntax and streaming document support for chat | -| `Duration` | Milliseconds to human-readable duration formatting | -| `Auth` | Authentication plugs: login password check, API token validation, session enforcement | -| `ArtistLive.Biography` | Artist biography building/rendering from Wikipedia and Last.fm data | -| `LiveHelpers.Params` | URL query param parsing: pagination, search query, sort order, display mode, fallback index | -| `LiveHelpers.IndexActions` | Shared index page logic (search, pagination, import, delete, display mode) for Collection/Wishlist index pages, parameterized by config map | -| `LiveHelpers.RecordActions` | Shared record action handlers (refresh cover, genres, embeddings, MusicBrainz data) for Collection/Wishlist show pages | -| — | Logster v2 handles `[:phoenix, :socket_connected]` telemetry — not a custom module | +| Module | Purpose | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `ErrorMessages` | Maps internal error terms (atoms, structs) to user-friendly gettext strings via `friendly_message/1` | +| `Markdown` | Markdown-to-HTML conversion (MDEx with ammonia sanitization) with `[[double bracket]]` link syntax and streaming document support for chat | +| `Duration` | Milliseconds to human-readable duration formatting | +| `Auth` | Authentication plugs: login password check, API token validation, session enforcement | +| `ArtistLive.Biography` | Artist biography building/rendering from Wikipedia and Last.fm data | +| `LiveHelpers.Params` | URL query param parsing: pagination, search query, sort order, display mode, fallback index | +| `LiveHelpers.IndexActions` | Shared index page logic (search, pagination, import, delete, display mode) for Collection/Wishlist index pages, parameterized by config map | +| `LiveHelpers.RecordActions` | Shared record action handlers (refresh cover, genres, embeddings, MusicBrainz data) for Collection/Wishlist show pages | +| `LiveHelpers.RecordShow` | Shared Collection/Wishlist show-page loading, page titles, delete navigation, scrobble async handling, and background record update handling | +| — | Logster v2 handles `[:phoenix, :socket_connected]` telemetry — not a custom module | ### Controllers diff --git a/lib/music_library_web/components/record_components.ex b/lib/music_library_web/components/record_components.ex index 3327f0b0..2bb8eed8 100644 --- a/lib/music_library_web/components/record_components.ex +++ b/lib/music_library_web/components/record_components.ex @@ -553,6 +553,281 @@ defmodule MusicLibraryWeb.RecordComponents do """ end + attr :record, Records.Record, required: true + attr :can_scrobble?, :boolean, required: true + attr :chat_count, :integer, required: true + attr :edit_path, :string, required: true + slot :dropdown_start + slot :dropdown_extra + + def record_show_action_bar(assigns) do + ~H""" +
+

+ <.artist_links joinphrase_class="text-sm" artists={@record.artists} /> +

+
+ <.button_group> + <.button + :if={@can_scrobble? and @record.selected_release_id} + variant="soft" + phx-click="scrobble_release" + > + {gettext("Scrobble release")} + <.icon + name="hero-play" + class="icon" + 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="icon" + aria-hidden="true" + data-slot="icon" + /> + 0} class="text-xs font-medium">{@chat_count} + + <.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="icon" + 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="icon cursor-pointer text-zinc-500 dark:text-zinc-400" + aria-hidden="true" + data-slot="icon" + /> + + + <.focus_wrap id={"actions-#{@record.id}-focus-wrap"}> + {render_slot(@dropdown_start)} + + <.dropdown_link + id={"actions-#{@record.id}-debug"} + phx-click={Fluxon.open_dialog("debug-data")} + > + <.icon + name="hero-code-bracket" + class="mr-1 size-4" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Debug data")} + + + <.dropdown_separator /> + + <.dropdown_link id={"actions-#{@record.id}-edit"} patch={@edit_path}> + <.icon + name="hero-pencil-square" + class="phx-click-loading:animate-bounce mr-1 size-4" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Edit")} + + + <.dropdown_link + id={"actions-#{@record.id}-refresh-cover"} + phx-click="refresh_cover" + > + <.icon + name="hero-photo" + class="phx-click-loading:animate-bounce mr-1 size-4" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Refresh cover")} + + + <.dropdown_link + id={"actions-#{@record.id}-refresh-mb-data"} + phx-click="refresh_musicbrainz_data" + > + <.icon + name="hero-cloud-arrow-down" + class="phx-click-loading:animate-bounce mr-1 size-4" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Refresh MB data")} + + + <.dropdown_link + id={"actions-#{@record.id}-populate-genres"} + phx-click="populate_genres" + > + <.icon + name="hero-sparkles" + class="phx-click-loading:animate-shake mr-1 size-4" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Populate genres")} + + + {render_slot(@dropdown_extra)} + + <.dropdown_link + id={"actions-#{@record.id}-extract-colors"} + phx-click="extract_colors" + > + <.icon + name="hero-paint-brush" + class="phx-click-loading:animate-shake mr-1 size-4" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Extract colors")} + + + <.dropdown_separator /> + <.dropdown_link + id={"actions-#{@record.id}-delete"} + phx-click="delete" + 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="phx-click-loading:animate-spin mr-1 size-4" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Delete")} + + + + +
+
+ """ + end + + attr :record, Records.Record, required: true + attr :label, :string, required: true + attr :copy_selected_release_id?, :boolean, default: false + + def record_show_selected_release_row(assigns) do + ~H""" + <.dl_row label={@label}> +
+ + {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={@copy_selected_release_id? and @record.selected_release_id} + target_id={"record-selected-release-" <> @record.id} + label={gettext("Copy record selected release ID to clipboard")} + /> +
+ + """ + end + + attr :record, Records.Record, required: true + attr :show_print?, :boolean, required: true + attr :timezone, :string, required: true + + def record_show_release_sheet(assigns) do + ~H""" + <.sheet + :if={@record.selected_release_id} + id="release-with-tracks-sheet" + placement="right" + class="flex min-w-xs flex-col overflow-hidden p-0 sm:min-w-sm" + > + <.live_component + id="release-with-tracks" + sheet_id="release-with-tracks-sheet" + module={MusicLibraryWeb.Components.Release} + release_id={@record.selected_release_id} + show_print?={@show_print?} + timezone={@timezone} + /> + + """ + end + + attr :record, Records.Record, required: true + attr :embedding_text, :string, required: true + + def record_show_chat(assigns) do + ~H""" + <.live_component + id="record-chat" + sheet_id="record-chat-sheet" + module={MusicLibraryWeb.Components.Chat} + title={@record.title} + entity={:record} + musicbrainz_id={@record.musicbrainz_id} + chat_module={MusicLibrary.Chats.RecordChat} + chat_context={{@record, @embedding_text}} + placeholder={gettext("Ask about this album...")} + empty_prompt={gettext("Ask anything about this album")} + /> + """ + end + + attr :record, Records.Record, required: true + attr :live_action, :atom, required: true + attr :show_purchased_at, :boolean, required: true + attr :close_path, :string, required: true + attr :patch_path, :string, required: true + + def record_show_edit_modal(assigns) do + ~H""" + <.structured_modal + :if={@live_action == :edit} + id="record-modal" + on_close={JS.patch(@close_path)} + > + <.live_component + module={MusicLibraryWeb.Components.RecordForm} + id={@record.id} + action={@live_action} + show_purchased_at={@show_purchased_at} + record={@record} + patch={@patch_path} + /> + + """ + end + attr :record, Records.Record, required: true attr :section, :atom, values: [:collection, :wishlist], required: true diff --git a/lib/music_library_web/live/collection_live/show.ex b/lib/music_library_web/live/collection_live/show.ex index 773cae09..e3a9ec9a 100644 --- a/lib/music_library_web/live/collection_live/show.ex +++ b/lib/music_library_web/live/collection_live/show.ex @@ -1,13 +1,10 @@ defmodule MusicLibraryWeb.CollectionLive.Show do use MusicLibraryWeb, :live_view - require Logger - import MusicLibrary.ListeningStats, only: [localize_scrobbled_at: 2] import MusicLibraryWeb.RecordComponents, only: [ - artist_links: 1, record_cover: 1, record_debug_sheet: 1, record_external_links: 1, @@ -15,18 +12,26 @@ defmodule MusicLibraryWeb.CollectionLive.Show do record_includes: 1, record_published_releases: 1, record_sets_list: 1, + record_show_action_bar: 1, + record_show_chat: 1, + record_show_edit_modal: 1, + record_show_release_sheet: 1, + record_show_selected_release_row: 1, record_timestamps: 1, - record_title_and_metadata: 1, - release_summary: 1 + record_title_and_metadata: 1 ] - alias MusicLibrary.{Chats, ListeningStats, Records, RecordSets, ScrobbleActivity} + alias MusicLibrary.{ListeningStats, Records, ScrobbleActivity} alias MusicLibrary.Records.Similarity alias MusicLibraryWeb.ErrorMessages - alias MusicLibraryWeb.LiveHelpers.RecordActions - alias Phoenix.LiveView.JS + alias MusicLibraryWeb.LiveHelpers.RecordShow - alias MusicBrainz + @common_record_events [ + "refresh_musicbrainz_data", + "refresh_cover", + "populate_genres", + "extract_colors" + ] @impl true def render(assigns) do @@ -46,190 +51,43 @@ defmodule MusicLibraryWeb.CollectionLive.Show do
-
-

- <.artist_links joinphrase_class="text-sm" artists={@record.artists} /> -

-
- <.button_group> - <.button - :if={@can_scrobble? and @record.selected_release_id} - variant="soft" - phx-click="scrobble_release" - > - {gettext("Scrobble release")} - <.icon - name="hero-play" - class="icon" - 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="icon" - aria-hidden="true" - data-slot="icon" - /> - 0} class="text-xs font-medium">{@chat_count} - - <.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="icon" - 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="icon cursor-pointer text-zinc-500 dark:text-zinc-400" - aria-hidden="true" - data-slot="icon" - /> - - - <.focus_wrap id={"actions-#{@record.id}-focus-wrap"}> - <.dropdown_link - id={"actions-#{@record.id}-notes"} - phx-click={MusicLibraryWeb.Components.Notes.open("record-notes-sheet")} - > - <.icon - name="hero-pencil" - class="mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Notes")} - + <.record_show_action_bar + record={@record} + can_scrobble?={@can_scrobble?} + chat_count={@chat_count} + edit_path={~p"/collection/#{@record}/show/edit"} + > + <:dropdown_start> + <.dropdown_link + id={"actions-#{@record.id}-notes"} + phx-click={MusicLibraryWeb.Components.Notes.open("record-notes-sheet")} + > + <.icon + name="hero-pencil" + class="mr-1 size-4" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Notes")} + + - <.dropdown_link - id={"actions-#{@record.id}-debug"} - phx-click={Fluxon.open_dialog("debug-data")} - > - <.icon - name="hero-code-bracket" - class="mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Debug data")} - + <:dropdown_extra> + <.dropdown_link + id={"actions-#{@record.id}-regenerate-embeddings"} + phx-click="regenerate_embeddings" + > + <.icon + name="hero-sparkles" + class="phx-click-loading:animate-shake mr-1 size-4" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Regenerate embeddings")} + + + - <.dropdown_separator /> - - <.dropdown_link - id={"actions-#{@record.id}-edit"} - patch={~p"/collection/#{@record}/show/edit"} - > - <.icon - name="hero-pencil-square" - class="phx-click-loading:animate-bounce mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Edit")} - - - <.dropdown_link - id={"actions-#{@record.id}-refresh-cover"} - phx-click="refresh_cover" - > - <.icon - name="hero-photo" - class="phx-click-loading:animate-bounce mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Refresh cover")} - - - <.dropdown_link - id={"actions-#{@record.id}-refresh-mb-data"} - phx-click="refresh_musicbrainz_data" - > - <.icon - name="hero-cloud-arrow-down" - class="phx-click-loading:animate-bounce mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Refresh MB data")} - - - <.dropdown_link - id={"actions-#{@record.id}-populate-genres"} - phx-click="populate_genres" - > - <.icon - name="hero-sparkles" - class="phx-click-loading:animate-shake mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Populate genres")} - - - <.dropdown_link - id={"actions-#{@record.id}-regenerate-embeddings"} - phx-click="regenerate_embeddings" - > - <.icon - name="hero-sparkles" - class="phx-click-loading:animate-shake mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Regenerate embeddings")} - - - <.dropdown_link - id={"actions-#{@record.id}-extract-colors"} - phx-click="extract_colors" - > - <.icon - name="hero-paint-brush" - class="phx-click-loading:animate-shake mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Extract colors")} - - - <.dropdown_separator /> - <.dropdown_link - id={"actions-#{@record.id}-delete"} - phx-click="delete" - 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="phx-click-loading:animate-spin mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Delete")} - - - - -
-
<.record_title_and_metadata record={@record} /> <.record_external_links record={@record} />
@@ -254,32 +112,11 @@ defmodule MusicLibraryWeb.CollectionLive.Show do <.record_published_releases record={@record} /> - <.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")} - /> -
- + <.record_show_selected_release_row + record={@record} + label={gettext("Collected release")} + copy_selected_release_id?={true} + /> <.record_includes record={@record} /> <.record_sets_list record_sets={@record_sets} /> @@ -292,21 +129,7 @@ defmodule MusicLibraryWeb.CollectionLive.Show do <.record_debug_sheet record={@record} embedding_text={@embedding_text} /> - <.sheet - :if={@record.selected_release_id} - id="release-with-tracks-sheet" - placement="right" - class="flex min-w-xs flex-col overflow-hidden p-0 sm:min-w-sm" - > - <.live_component - id="release-with-tracks" - sheet_id="release-with-tracks-sheet" - module={MusicLibraryWeb.Components.Release} - release_id={@record.selected_release_id} - show_print?={true} - timezone={@timezone} - /> - + <.record_show_release_sheet record={@record} show_print?={true} timezone={@timezone} /> <.live_component id="record-notes" @@ -316,33 +139,15 @@ defmodule MusicLibraryWeb.CollectionLive.Show do musicbrainz_id={@record.musicbrainz_id} /> - <.live_component - id="record-chat" - sheet_id="record-chat-sheet" - module={MusicLibraryWeb.Components.Chat} - title={@record.title} - entity={:record} - musicbrainz_id={@record.musicbrainz_id} - chat_module={MusicLibrary.Chats.RecordChat} - chat_context={{@record, @embedding_text}} - placeholder={gettext("Ask about this album...")} - empty_prompt={gettext("Ask anything about this album")} - /> + <.record_show_chat record={@record} embedding_text={@embedding_text} /> - <.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}"} - /> - + <.record_show_edit_modal + record={@record} + live_action={@live_action} + show_purchased_at={true} + close_path={~p"/collection/#{@record}"} + patch_path={~p"/collection/#{@record}"} + /> """ end @@ -352,52 +157,24 @@ defmodule MusicLibraryWeb.CollectionLive.Show do {:ok, socket |> assign(:current_section, :collection) - |> assign(:can_scrobble?, ScrobbleActivity.can_scrobble?()) - |> assign(:release_with_tracks, nil)} + |> assign(:can_scrobble?, ScrobbleActivity.can_scrobble?())} end @impl true def handle_params(%{"id" => id}, _, socket) do - RecordActions.manage_subscription(socket, id) - - record = Records.get_record!(id) - last_listened_track = ListeningStats.get_last_listened_track(record) - play_count = ListeningStats.play_count(record) - record_sets = RecordSets.list_record_sets_for_record(record.id) - {:noreply, socket - |> assign(:page_title, page_title(socket.assigns.live_action, record)) - |> assign(:record, record) - |> assign(:last_listened_track, last_listened_track) - |> assign(:play_count, play_count) - |> assign(:record_sets, record_sets) - |> assign(:chat_count, Chats.count_chats(:record, record.musicbrainz_id)) - |> RecordActions.assign_embedding_text() - |> assign_similar_records()} + |> RecordShow.assign_common_record(id, gettext("Collection")) + |> assign_collection_record_context()} end @impl true def handle_event("delete", _params, socket) do - {:ok, _} = Records.delete_record(socket.assigns.record) - - {:noreply, push_navigate(socket, to: ~p"/collection")} + RecordShow.delete_record(socket, ~p"/collection") end - def handle_event("refresh_musicbrainz_data", _params, socket) do - RecordActions.refresh_musicbrainz_data(socket) - end - - def handle_event("populate_genres", _params, socket) do - RecordActions.populate_genres(socket) - end - - def handle_event("refresh_cover", _params, socket) do - RecordActions.refresh_cover(socket) - end - - def handle_event("extract_colors", _params, socket) do - RecordActions.extract_colors(socket) + def handle_event(event, _params, socket) when event in @common_record_events do + RecordShow.handle_common_event(event, socket) end def handle_event("regenerate_embeddings", _params, socket) do @@ -420,102 +197,31 @@ defmodule MusicLibraryWeb.CollectionLive.Show do end def handle_event("scrobble_release", _params, socket) do - record = socket.assigns.record - - {:noreply, - start_async(socket, :scrobble_release, fn -> - with {:ok, release} <- MusicBrainz.get_release(record.selected_release_id) do - release_with_tracks = MusicBrainz.Release.from_api_response(release) - - ScrobbleActivity.scrobble_release(release_with_tracks, :finished_at, DateTime.utc_now()) - end - end)} + RecordShow.scrobble_release(socket) end @impl true - def handle_async(:scrobble_release, {:ok, {:ok, _result}}, socket) do - {:noreply, put_toast(socket, :info, gettext("Release scrobbled successfully"))} - end - - def handle_async(:scrobble_release, {:ok, {:error, reason}}, socket) do - {:noreply, - put_toast( - socket, - :error, - gettext("Error scrobbling release") <> ": " <> ErrorMessages.friendly_message(reason) - )} - end - - def handle_async(:scrobble_release, {:exit, reason}, socket) do - Logger.error("Scrobble release failed: #{inspect(reason)}") - - {:noreply, - put_toast( - socket, - :error, - gettext("Error scrobbling release") <> ": " <> ErrorMessages.friendly_message(reason) - )} + def handle_async(:scrobble_release, result, socket) do + RecordShow.handle_scrobble_release(result, socket) end @impl true def handle_info({MusicLibraryWeb.Components.RecordForm, {:saved, record}}, socket) do - {:noreply, - socket - |> assign(:record, record) - |> assign_similar_records() - |> RecordActions.assign_embedding_text()} + RecordShow.handle_saved_record(socket, record, &assign_similar_records/1) end def handle_info({MusicLibraryWeb.Components.Chat, :chats_changed}, socket) do - RecordActions.handle_chats_changed(socket) + RecordShow.handle_chats_changed(socket) end def handle_info({MusicLibraryWeb.Components.Release, {:loaded, _release}}, socket) do - {:noreply, socket} + RecordShow.handle_release_loaded(socket) end - @impl true def handle_info({:update, record}, socket) do - cond do - record.id != socket.assigns.record.id -> - {:noreply, socket} - - socket.assigns.live_action == :edit -> - {:noreply, - socket - |> put_toast( - :warning, - gettext( - "Record was updated in the background. Your edits may be stale — save and re-open to see the latest data." - ) - )} - - true -> - {:noreply, - socket - |> RecordActions.handle_record_updated(record) - |> assign_similar_records()} - end + RecordShow.handle_record_update(socket, record, &assign_similar_records/1) end - defp page_title(action, record) do - Enum.join( - [ - Records.Record.artist_names(record), - "-", - record.title, - "·", - title_segment(action), - "·", - gettext("Collection") - ], - " " - ) - end - - defp title_segment(:show), do: gettext("Details") - defp title_segment(:edit), do: gettext("Edit") - attr :similar_records, :list, required: true defp similar_records(assigns) do @@ -562,6 +268,15 @@ defmodule MusicLibraryWeb.CollectionLive.Show do """ end + defp assign_collection_record_context(socket) do + record = socket.assigns.record + + socket + |> assign(:last_listened_track, ListeningStats.get_last_listened_track(record)) + |> assign(:play_count, ListeningStats.play_count(record)) + |> assign_similar_records() + end + defp assign_similar_records(socket) do similar_records = Similarity.find_similar(socket.assigns.record.id, limit: 6, scope: :collection) diff --git a/lib/music_library_web/live/wishlist_live/show.ex b/lib/music_library_web/live/wishlist_live/show.ex index 676c3c52..7e3d38d8 100644 --- a/lib/music_library_web/live/wishlist_live/show.ex +++ b/lib/music_library_web/live/wishlist_live/show.ex @@ -1,11 +1,8 @@ defmodule MusicLibraryWeb.WishlistLive.Show do use MusicLibraryWeb, :live_view - require Logger - import MusicLibraryWeb.RecordComponents, only: [ - artist_links: 1, record_cover: 1, record_debug_sheet: 1, record_external_links: 1, @@ -13,16 +10,25 @@ defmodule MusicLibraryWeb.WishlistLive.Show do record_includes: 1, record_published_releases: 1, record_sets_list: 1, + record_show_action_bar: 1, + record_show_chat: 1, + record_show_edit_modal: 1, + record_show_release_sheet: 1, + record_show_selected_release_row: 1, record_timestamps: 1, - record_title_and_metadata: 1, - release_summary: 1 + record_title_and_metadata: 1 ] - alias MusicLibrary.Chats alias MusicLibrary.OnlineStoreTemplates - alias MusicLibrary.{Records, RecordSets, ScrobbleActivity} - alias MusicLibraryWeb.ErrorMessages - alias MusicLibraryWeb.LiveHelpers.RecordActions + alias MusicLibrary.{Records, ScrobbleActivity} + alias MusicLibraryWeb.LiveHelpers.RecordShow + + @common_record_events [ + "refresh_musicbrainz_data", + "refresh_cover", + "populate_genres", + "extract_colors" + ] @impl true def render(assigns) do @@ -42,201 +48,42 @@ defmodule MusicLibraryWeb.WishlistLive.Show do
-
-

- <.artist_links joinphrase_class="text-sm" artists={@record.artists} /> -

-
- <.button_group> - <.button - :if={@can_scrobble? and @record.selected_release_id} - variant="soft" - phx-click="scrobble_release" - > - {gettext("Scrobble release")} - <.icon - name="hero-play" - class="icon" - 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="icon" - aria-hidden="true" - data-slot="icon" - /> - 0} class="text-xs font-medium">{@chat_count} - - <.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="icon" - 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="icon cursor-pointer text-zinc-500 dark:text-zinc-400" - aria-hidden="true" - data-slot="icon" - /> - - - <.focus_wrap id={"actions-#{@record.id}-focus-wrap"}> - <.dropdown_link - id={"actions-#{@record.id}-debug"} - phx-click={Fluxon.open_dialog("debug-data")} - > - <.icon - name="hero-code-bracket" - class="mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Debug data")} - + <.record_show_action_bar + record={@record} + can_scrobble?={@can_scrobble?} + chat_count={@chat_count} + edit_path={~p"/wishlist/#{@record}/show/edit"} + > + <:dropdown_extra> + <.dropdown_link + :if={!@record.purchased_at} + id={"actions-#{@record.id}-purchase"} + phx-click={ + JS.dispatch("music_library:confetti") + |> JS.push("add-to-collection") + } + > + <.icon + name="hero-banknotes" + class="phx-click-loading:animate-shake mr-1 size-4" + aria-hidden="true" + data-slot="icon" + /> + {gettext("Purchased")} + + + - <.dropdown_separator /> - - <.dropdown_link - id={"actions-#{@record.id}-edit"} - patch={~p"/wishlist/#{@record}/show/edit"} - > - <.icon - name="hero-pencil-square" - class="phx-click-loading:animate-bounce mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Edit")} - - - <.dropdown_link - id={"actions-#{@record.id}-refresh-cover"} - phx-click="refresh_cover" - > - <.icon - name="hero-photo" - class="phx-click-loading:animate-bounce mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Refresh cover")} - - - <.dropdown_link - id={"actions-#{@record.id}-refresh-mb-data"} - phx-click="refresh_musicbrainz_data" - > - <.icon - name="hero-cloud-arrow-down" - class="phx-click-loading:animate-bounce mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Refresh MB data")} - - - <.dropdown_link - id={"actions-#{@record.id}-populate-genres"} - phx-click="populate_genres" - > - <.icon - name="hero-sparkles" - class="phx-click-loading:animate-shake mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Populate genres")} - - - <.dropdown_link - :if={!@record.purchased_at} - id={"actions-#{@record.id}-purchase"} - phx-click={ - JS.dispatch("music_library:confetti") - |> JS.push("add-to-collection") - } - > - <.icon - name="hero-banknotes" - class="phx-click-loading:animate-shake mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Purchased")} - - - <.dropdown_link - id={"actions-#{@record.id}-extract-colors"} - phx-click="extract_colors" - > - <.icon - name="hero-paint-brush" - class="phx-click-loading:animate-shake mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Extract colors")} - - - <.dropdown_separator /> - <.dropdown_link - id={"actions-#{@record.id}-delete"} - phx-click="delete" - 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="phx-click-loading:animate-spin mr-1 size-4" - aria-hidden="true" - data-slot="icon" - /> - {gettext("Delete")} - - - - -
-
<.record_title_and_metadata record={@record} current_date={@current_date} /> <.record_external_links record={@record} />
<.record_genres record={@record} section={:wishlist} /> <.record_published_releases record={@record} /> - <.dl_row label={gettext("Wishlisted release")}> -
- - {gettext("No release selected")} - - <.release_summary - :if={@record.selected_release_id} - release={Records.Record.selected_release(@record)} - /> -
- + <.record_show_selected_release_row + record={@record} + label={gettext("Wishlisted release")} + /> <.record_includes record={@record} /> <.record_sets_list record_sets={@record_sets} />
@@ -281,49 +128,17 @@ defmodule MusicLibraryWeb.WishlistLive.Show do <.record_debug_sheet record={@record} embedding_text={@embedding_text} /> - <.sheet - :if={@record.selected_release_id} - id="release-with-tracks-sheet" - placement="right" - class="flex min-w-xs flex-col overflow-hidden p-0 sm:min-w-sm" - > - <.live_component - id="release-with-tracks" - sheet_id="release-with-tracks-sheet" - module={MusicLibraryWeb.Components.Release} - release_id={@record.selected_release_id} - show_print?={false} - timezone={@timezone} - /> - + <.record_show_release_sheet record={@record} show_print?={false} timezone={@timezone} /> - <.live_component - id="record-chat" - sheet_id="record-chat-sheet" - module={MusicLibraryWeb.Components.Chat} - title={@record.title} - entity={:record} - musicbrainz_id={@record.musicbrainz_id} - chat_module={MusicLibrary.Chats.RecordChat} - chat_context={{@record, @embedding_text}} - placeholder={gettext("Ask about this album...")} - empty_prompt={gettext("Ask anything about this album")} + <.record_show_chat record={@record} embedding_text={@embedding_text} /> + + <.record_show_edit_modal + record={@record} + live_action={@live_action} + show_purchased_at={false} + close_path={~p"/wishlist/#{@record}"} + patch_path={~p"/wishlist/#{@record}"} /> - - <.structured_modal - :if={@live_action == :edit} - id="record-modal" - on_close={JS.patch(~p"/wishlist/#{@record}")} - > - <.live_component - module={MusicLibraryWeb.Components.RecordForm} - id={@record.id} - action={@live_action} - show_purchased_at={false} - record={@record} - patch={~p"/wishlist/#{@record}"} - /> - """ end @@ -341,40 +156,19 @@ defmodule MusicLibraryWeb.WishlistLive.Show do @impl true def handle_params(%{"id" => id}, _, socket) do - RecordActions.manage_subscription(socket, id) - - record = Records.get_record!(id) - online_store_templates = OnlineStoreTemplates.list_enabled_templates() - - record_sets = RecordSets.list_record_sets_for_record(record.id) - {:noreply, socket - |> assign(:page_title, page_title(socket.assigns.live_action, record)) - |> assign(:record, record) - |> assign(:online_store_templates, online_store_templates) - |> assign(:record_sets, record_sets) - |> assign(:chat_count, Chats.count_chats(:record, record.musicbrainz_id)) - |> RecordActions.assign_embedding_text()} + |> RecordShow.assign_common_record(id, gettext("Wishlist")) + |> assign(:online_store_templates, OnlineStoreTemplates.list_enabled_templates())} end @impl true def handle_event("delete", _params, socket) do - {:ok, _} = Records.delete_record(socket.assigns.record) - - {:noreply, push_navigate(socket, to: ~p"/wishlist")} + RecordShow.delete_record(socket, ~p"/wishlist") end - def handle_event("refresh_musicbrainz_data", _params, socket) do - RecordActions.refresh_musicbrainz_data(socket) - end - - def handle_event("refresh_cover", _params, socket) do - RecordActions.refresh_cover(socket) - end - - def handle_event("populate_genres", _params, socket) do - RecordActions.populate_genres(socket) + def handle_event(event, _params, socket) when event in @common_record_events do + RecordShow.handle_common_event(event, socket) end def handle_event("add-to-collection", _params, socket) do @@ -393,100 +187,29 @@ defmodule MusicLibraryWeb.WishlistLive.Show do end end - def handle_event("extract_colors", _params, socket) do - RecordActions.extract_colors(socket) - end - def handle_event("scrobble_release", _params, socket) do - record = socket.assigns.record - - {:noreply, - start_async(socket, :scrobble_release, fn -> - with {:ok, release} <- MusicBrainz.get_release(record.selected_release_id) do - release_with_tracks = MusicBrainz.Release.from_api_response(release) - - ScrobbleActivity.scrobble_release(release_with_tracks, :finished_at, DateTime.utc_now()) - end - end)} + RecordShow.scrobble_release(socket) end @impl true - def handle_async(:scrobble_release, {:ok, {:ok, _result}}, socket) do - {:noreply, put_toast(socket, :info, gettext("Release scrobbled successfully"))} - end - - def handle_async(:scrobble_release, {:ok, {:error, reason}}, socket) do - {:noreply, - put_toast( - socket, - :error, - gettext("Error scrobbling release") <> ": " <> ErrorMessages.friendly_message(reason) - )} - end - - def handle_async(:scrobble_release, {:exit, reason}, socket) do - Logger.error("Scrobble release failed: #{inspect(reason)}") - - {:noreply, - put_toast( - socket, - :error, - gettext("Error scrobbling release") <> ": " <> ErrorMessages.friendly_message(reason) - )} + def handle_async(:scrobble_release, result, socket) do + RecordShow.handle_scrobble_release(result, socket) end @impl true def handle_info({MusicLibraryWeb.Components.RecordForm, {:saved, record}}, socket) do - {:noreply, - socket - |> assign(:record, record) - |> RecordActions.assign_embedding_text()} + RecordShow.handle_saved_record(socket, record) end def handle_info({MusicLibraryWeb.Components.Chat, :chats_changed}, socket) do - RecordActions.handle_chats_changed(socket) + RecordShow.handle_chats_changed(socket) end def handle_info({MusicLibraryWeb.Components.Release, {:loaded, _release}}, socket) do - {:noreply, socket} + RecordShow.handle_release_loaded(socket) end - @impl true def handle_info({:update, record}, socket) do - cond do - record.id != socket.assigns.record.id -> - {:noreply, socket} - - socket.assigns.live_action == :edit -> - {:noreply, - socket - |> put_toast( - :warning, - gettext( - "Record was updated in the background. Your edits may be stale — save and re-open to see the latest data." - ) - )} - - true -> - {:noreply, RecordActions.handle_record_updated(socket, record)} - end + RecordShow.handle_record_update(socket, record) end - - defp page_title(action, record) do - Enum.join( - [ - Records.Record.artist_names(record), - "-", - record.title, - "·", - title_segment(action), - "·", - gettext("Wishlist") - ], - " " - ) - end - - defp title_segment(:show), do: gettext("Details") - defp title_segment(:edit), do: gettext("Edit") end diff --git a/lib/music_library_web/live_helpers/record_show.ex b/lib/music_library_web/live_helpers/record_show.ex new file mode 100644 index 00000000..53df5e45 --- /dev/null +++ b/lib/music_library_web/live_helpers/record_show.ex @@ -0,0 +1,151 @@ +defmodule MusicLibraryWeb.LiveHelpers.RecordShow do + @moduledoc """ + Shared show-page helpers for collection and wishlist record LiveViews. + + The helpers keep common record loading, event handling, async scrobble + handling, and background-update behaviour in one place while allowing each + LiveView to compose page-specific assigns and UI around the shared flow. + """ + + require Logger + + import LiveToast, only: [put_toast: 3] + import Phoenix.Component, only: [assign: 3] + import Phoenix.LiveView, only: [push_navigate: 2, start_async: 3] + + use Gettext, backend: MusicLibraryWeb.Gettext + + alias MusicLibrary.{Chats, Records, RecordSets, ScrobbleActivity} + alias MusicLibraryWeb.ErrorMessages + alias MusicLibraryWeb.LiveHelpers.RecordActions + + def assign_common_record(socket, id, section_title) do + RecordActions.manage_subscription(socket, id) + + record = Records.get_record!(id) + record_sets = RecordSets.list_record_sets_for_record(record.id) + + socket + |> assign(:page_title, page_title(socket.assigns.live_action, record, section_title)) + |> assign(:record, record) + |> assign(:record_sets, record_sets) + |> assign(:chat_count, Chats.count_chats(:record, record.musicbrainz_id)) + |> RecordActions.assign_embedding_text() + end + + def handle_common_event("refresh_musicbrainz_data", socket) do + RecordActions.refresh_musicbrainz_data(socket) + end + + def handle_common_event("refresh_cover", socket) do + RecordActions.refresh_cover(socket) + end + + def handle_common_event("populate_genres", socket) do + RecordActions.populate_genres(socket) + end + + def handle_common_event("extract_colors", socket) do + RecordActions.extract_colors(socket) + end + + def delete_record(socket, path) do + {:ok, _} = Records.delete_record(socket.assigns.record) + + {:noreply, push_navigate(socket, to: path)} + end + + def scrobble_release(socket) do + record = socket.assigns.record + + {:noreply, + start_async(socket, :scrobble_release, fn -> + with {:ok, release} <- MusicBrainz.get_release(record.selected_release_id) do + release_with_tracks = MusicBrainz.Release.from_api_response(release) + + ScrobbleActivity.scrobble_release(release_with_tracks, :finished_at, DateTime.utc_now()) + end + end)} + end + + def handle_scrobble_release({:ok, {:ok, _result}}, socket) do + {:noreply, put_toast(socket, :info, gettext("Release scrobbled successfully"))} + end + + def handle_scrobble_release({:ok, {:error, reason}}, socket) do + {:noreply, + put_toast( + socket, + :error, + gettext("Error scrobbling release") <> ": " <> ErrorMessages.friendly_message(reason) + )} + end + + def handle_scrobble_release({:exit, reason}, socket) do + Logger.error("Scrobble release failed: #{inspect(reason)}") + + {:noreply, + put_toast( + socket, + :error, + gettext("Error scrobbling release") <> ": " <> ErrorMessages.friendly_message(reason) + )} + end + + def handle_saved_record(socket, record, after_assign \\ &Function.identity/1) do + {:noreply, + socket + |> assign(:record, record) + |> after_assign.() + |> RecordActions.assign_embedding_text()} + end + + def handle_chats_changed(socket) do + RecordActions.handle_chats_changed(socket) + end + + def handle_release_loaded(socket) do + {:noreply, socket} + end + + def handle_record_update(socket, record, after_update \\ &Function.identity/1) do + cond do + record.id != socket.assigns.record.id -> + {:noreply, socket} + + socket.assigns.live_action == :edit -> + {:noreply, + socket + |> put_toast( + :warning, + gettext( + "Record was updated in the background. Your edits may be stale — save and re-open to see the latest data." + ) + )} + + true -> + {:noreply, + socket + |> RecordActions.handle_record_updated(record) + |> after_update.()} + end + end + + defp page_title(action, record, section_title) do + Enum.join( + [ + Records.Record.artist_names(record), + "-", + record.title, + "·", + title_segment(action), + "·", + section_title + ], + " " + ) + end + + defp title_segment(:show), do: gettext("Details") + defp title_segment(:edit), do: gettext("Edit") +end diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot index 7b09ebbc..56e8762a 100644 --- a/priv/gettext/default.pot +++ b/priv/gettext/default.pot @@ -12,13 +12,11 @@ msgid "" msgstr "" #: lib/music_library_web/components/record_components.ex -#: lib/music_library_web/live/collection_live/show.ex #: lib/music_library_web/live/online_store_template_live/index.ex #: lib/music_library_web/live/record_set_live/index.ex #: lib/music_library_web/live/record_set_live/show.ex #: lib/music_library_web/live/scrobble_rules_live/index.ex #: lib/music_library_web/live/scrobbled_tracks_live/index.ex -#: lib/music_library_web/live/wishlist_live/show.ex #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "" @@ -38,13 +36,11 @@ msgid "Cover art" msgstr "" #: lib/music_library_web/components/record_components.ex -#: lib/music_library_web/live/collection_live/show.ex #: lib/music_library_web/live/online_store_template_live/index.ex #: lib/music_library_web/live/record_set_live/index.ex #: lib/music_library_web/live/record_set_live/show.ex #: lib/music_library_web/live/scrobble_rules_live/index.ex #: lib/music_library_web/live/scrobbled_tracks_live/index.ex -#: lib/music_library_web/live/wishlist_live/show.ex #, elixir-autogen, elixir-format msgid "Delete" msgstr "" @@ -52,14 +48,13 @@ msgstr "" #: lib/music_library_web/components/notes.ex #: lib/music_library_web/components/record_components.ex #: lib/music_library_web/live/artist_live/show.ex -#: lib/music_library_web/live/collection_live/show.ex #: lib/music_library_web/live/online_store_template_live/index.ex #: lib/music_library_web/live/record_set_live/index.ex #: lib/music_library_web/live/record_set_live/show.ex #: lib/music_library_web/live/scrobble_rules_live/index.ex #: lib/music_library_web/live/scrobbled_tracks_live/index.ex -#: lib/music_library_web/live/wishlist_live/show.ex #: lib/music_library_web/live_helpers/index_actions.ex +#: lib/music_library_web/live_helpers/record_show.ex #, elixir-autogen, elixir-format msgid "Edit" msgstr "" @@ -280,8 +275,7 @@ msgid "No MB ID" msgstr "" #: lib/music_library_web/live/artist_live/show.ex -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/live_helpers/record_show.ex #, elixir-autogen, elixir-format msgid "Details" msgstr "" @@ -296,20 +290,17 @@ msgstr "" msgid "Loading biography" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Populate genres" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Refresh MB data" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Refresh cover" msgstr "" @@ -601,8 +592,7 @@ msgstr "" msgid "Collected release" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "No release selected" msgstr "" @@ -622,8 +612,7 @@ msgstr "" msgid "Loading release with tracks" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Show Tracks" msgstr "" @@ -639,15 +628,13 @@ msgid "Successfully connected your Last.fm account" msgstr "" #: lib/music_library_web/components/release.ex -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/live_helpers/record_show.ex #, elixir-autogen, elixir-format msgid "Error scrobbling release" msgstr "" #: lib/music_library_web/components/release.ex -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/live_helpers/record_show.ex #, elixir-autogen, elixir-format msgid "Release scrobbled successfully" msgstr "" @@ -662,9 +649,8 @@ msgstr "" msgid "Scrobbling..." msgstr "" +#: lib/music_library_web/components/record_components.ex #: lib/music_library_web/components/release.ex -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex #, elixir-autogen, elixir-format msgid "Scrobble release" msgstr "" @@ -795,13 +781,11 @@ msgstr "" #: lib/music_library_web/components/layouts/app.html.heex #: lib/music_library_web/components/record_components.ex #: lib/music_library_web/live/artist_live/show.ex -#: lib/music_library_web/live/collection_live/show.ex #: lib/music_library_web/live/online_store_template_live/index.ex #: lib/music_library_web/live/record_set_live/index.ex #: lib/music_library_web/live/record_set_live/show.ex #: lib/music_library_web/live/scrobble_rules_live/index.ex #: lib/music_library_web/live/scrobbled_tracks_live/index.ex -#: lib/music_library_web/live/wishlist_live/show.ex #, elixir-autogen, elixir-format msgid "Actions" msgstr "" @@ -1013,7 +997,7 @@ msgstr "" msgid "to open this search" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Copy record selected release ID to clipboard" msgstr "" @@ -1736,14 +1720,12 @@ msgstr "" msgid "Based on genres, artists, and musical style" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Ask about this album..." msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Ask anything about this album" msgstr "" @@ -1753,8 +1735,7 @@ msgstr "" msgid "Chat about %{title}" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Chat about album" msgstr "" @@ -1822,9 +1803,8 @@ msgid "Copy MB ID" msgstr "" #: lib/music_library_web/components/core_components.ex +#: lib/music_library_web/components/record_components.ex #: lib/music_library_web/live/artist_live/show.ex -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex #, elixir-autogen, elixir-format msgid "Debug data" msgstr "" @@ -1910,8 +1890,7 @@ msgstr "" msgid "Error extracting colors" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Extract colors" msgstr "" @@ -2561,8 +2540,7 @@ msgstr "" msgid "ISO format" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/live_helpers/record_show.ex #, elixir-autogen, elixir-format msgid "Record was updated in the background. Your edits may be stale — save and re-open to see the latest data." msgstr "" diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po index f622bc6f..9db03153 100644 --- a/priv/gettext/en/LC_MESSAGES/default.po +++ b/priv/gettext/en/LC_MESSAGES/default.po @@ -12,13 +12,11 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: lib/music_library_web/components/record_components.ex -#: lib/music_library_web/live/collection_live/show.ex #: lib/music_library_web/live/online_store_template_live/index.ex #: lib/music_library_web/live/record_set_live/index.ex #: lib/music_library_web/live/record_set_live/show.ex #: lib/music_library_web/live/scrobble_rules_live/index.ex #: lib/music_library_web/live/scrobbled_tracks_live/index.ex -#: lib/music_library_web/live/wishlist_live/show.ex #, elixir-autogen, elixir-format msgid "Are you sure?" msgstr "" @@ -38,13 +36,11 @@ msgid "Cover art" msgstr "" #: lib/music_library_web/components/record_components.ex -#: lib/music_library_web/live/collection_live/show.ex #: lib/music_library_web/live/online_store_template_live/index.ex #: lib/music_library_web/live/record_set_live/index.ex #: lib/music_library_web/live/record_set_live/show.ex #: lib/music_library_web/live/scrobble_rules_live/index.ex #: lib/music_library_web/live/scrobbled_tracks_live/index.ex -#: lib/music_library_web/live/wishlist_live/show.ex #, elixir-autogen, elixir-format msgid "Delete" msgstr "" @@ -52,14 +48,13 @@ msgstr "" #: lib/music_library_web/components/notes.ex #: lib/music_library_web/components/record_components.ex #: lib/music_library_web/live/artist_live/show.ex -#: lib/music_library_web/live/collection_live/show.ex #: lib/music_library_web/live/online_store_template_live/index.ex #: lib/music_library_web/live/record_set_live/index.ex #: lib/music_library_web/live/record_set_live/show.ex #: lib/music_library_web/live/scrobble_rules_live/index.ex #: lib/music_library_web/live/scrobbled_tracks_live/index.ex -#: lib/music_library_web/live/wishlist_live/show.ex #: lib/music_library_web/live_helpers/index_actions.ex +#: lib/music_library_web/live_helpers/record_show.ex #, elixir-autogen, elixir-format msgid "Edit" msgstr "" @@ -280,8 +275,7 @@ msgid "No MB ID" msgstr "" #: lib/music_library_web/live/artist_live/show.ex -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/live_helpers/record_show.ex #, elixir-autogen, elixir-format msgid "Details" msgstr "" @@ -296,20 +290,17 @@ msgstr "" msgid "Loading biography" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Populate genres" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Refresh MB data" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Refresh cover" msgstr "" @@ -601,8 +592,7 @@ msgstr "" msgid "Collected release" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "No release selected" msgstr "" @@ -622,8 +612,7 @@ msgstr "" msgid "Loading release with tracks" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Show Tracks" msgstr "" @@ -639,15 +628,13 @@ msgid "Successfully connected your Last.fm account" msgstr "" #: lib/music_library_web/components/release.ex -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/live_helpers/record_show.ex #, elixir-autogen, elixir-format msgid "Error scrobbling release" msgstr "" #: lib/music_library_web/components/release.ex -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/live_helpers/record_show.ex #, elixir-autogen, elixir-format msgid "Release scrobbled successfully" msgstr "" @@ -662,9 +649,8 @@ msgstr "" msgid "Scrobbling..." msgstr "" +#: lib/music_library_web/components/record_components.ex #: lib/music_library_web/components/release.ex -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex #, elixir-autogen, elixir-format msgid "Scrobble release" msgstr "" @@ -795,13 +781,11 @@ msgstr "" #: lib/music_library_web/components/layouts/app.html.heex #: lib/music_library_web/components/record_components.ex #: lib/music_library_web/live/artist_live/show.ex -#: lib/music_library_web/live/collection_live/show.ex #: lib/music_library_web/live/online_store_template_live/index.ex #: lib/music_library_web/live/record_set_live/index.ex #: lib/music_library_web/live/record_set_live/show.ex #: lib/music_library_web/live/scrobble_rules_live/index.ex #: lib/music_library_web/live/scrobbled_tracks_live/index.ex -#: lib/music_library_web/live/wishlist_live/show.ex #, elixir-autogen, elixir-format msgid "Actions" msgstr "" @@ -1013,7 +997,7 @@ msgstr "" msgid "to open this search" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Copy record selected release ID to clipboard" msgstr "" @@ -1736,14 +1720,12 @@ msgstr "" msgid "Based on genres, artists, and musical style" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Ask about this album..." msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Ask anything about this album" msgstr "" @@ -1753,8 +1735,7 @@ msgstr "" msgid "Chat about %{title}" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Chat about album" msgstr "" @@ -1822,9 +1803,8 @@ msgid "Copy MB ID" msgstr "" #: lib/music_library_web/components/core_components.ex +#: lib/music_library_web/components/record_components.ex #: lib/music_library_web/live/artist_live/show.ex -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex #, elixir-autogen, elixir-format msgid "Debug data" msgstr "" @@ -1910,8 +1890,7 @@ msgstr "" msgid "Error extracting colors" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/components/record_components.ex #, elixir-autogen, elixir-format msgid "Extract colors" msgstr "" @@ -2561,8 +2540,7 @@ msgstr "" msgid "ISO format" msgstr "" -#: lib/music_library_web/live/collection_live/show.ex -#: lib/music_library_web/live/wishlist_live/show.ex +#: lib/music_library_web/live_helpers/record_show.ex #, elixir-autogen, elixir-format msgid "Record was updated in the background. Your edits may be stale — save and re-open to see the latest data." msgstr ""