ML-199: extract shared record show logic

This commit is contained in:
Claudio Ortolina
2026-05-29 08:50:27 +03:00
parent f4648ce5d9
commit 75e2ec5c70
8 changed files with 726 additions and 812 deletions
@@ -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
<!-- SECTION:DESCRIPTION:BEGIN -->
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.
<!-- SECTION:DESCRIPTION:END -->
## Acceptance Criteria
<!-- AC:BEGIN -->
- [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.
<!-- AC:END -->
## Implementation Plan
<!-- SECTION:PLAN:BEGIN -->
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.
<!-- SECTION:PLAN:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
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.
<!-- SECTION:NOTES:END -->
## Final Summary
<!-- SECTION:FINAL_SUMMARY:BEGIN -->
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`
<!-- SECTION:FINAL_SUMMARY:END -->
+23 -22
View File
@@ -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
@@ -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"""
<div class="mt-4 flex items-center justify-between md:mt-0">
<h1 class="text-base/6 font-medium text-zinc-700">
<.artist_links joinphrase_class="text-sm" artists={@record.artists} />
</h1>
<div class="min-w-12">
<.button_group>
<.button
:if={@can_scrobble? and @record.selected_release_id}
variant="soft"
phx-click="scrobble_release"
>
<span class="sr-only">{gettext("Scrobble release")}</span>
<.icon
name="hero-play"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
</.button>
<.button
variant="soft"
phx-click={MusicLibraryWeb.Components.Chat.open("record-chat-sheet")}
>
<span class="sr-only">{gettext("Chat about album")}</span>
<.icon
name="hero-chat-bubble-left-right"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
<span :if={@chat_count > 0} class="text-xs font-medium">{@chat_count}</span>
</.button>
<.button
:if={@record.selected_release_id}
variant="soft"
phx-click={MusicLibraryWeb.Components.Release.open("release-with-tracks-sheet")}
>
<span class="sr-only">{gettext("Show Tracks")}</span>
<.icon
name="hero-numbered-list"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
</.button>
<.dropdown id={"actions-#{@record.id}"} placement="bottom-end">
<:toggle>
<.button variant="soft">
<span class="sr-only">{gettext("Actions")}</span>
<.icon
name="hero-ellipsis-vertical"
class="icon cursor-pointer text-zinc-500 dark:text-zinc-400"
aria-hidden="true"
data-slot="icon"
/>
</.button>
</:toggle>
<.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_link>
<.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>
<.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>
<.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>
<.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>
{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_link>
<.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")}
</.dropdown_link>
</.focus_wrap>
</.dropdown>
</.button_group>
</div>
</div>
"""
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}>
<div class="flex justify-between space-x-2">
<span
:if={!@record.selected_release_id}
class="text-xs text-zinc-700 md:text-sm dark:text-zinc-300"
>
{gettext("No release selected")}
</span>
<.release_summary
:if={@record.selected_release_id}
release={Records.Record.selected_release(@record)}
/>
<span
:if={@copy_selected_release_id? and @record.selected_release_id}
id={"record-selected-release-" <> @record.id}
class="hidden"
>
{@record.selected_release_id}
</span>
<.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")}
/>
</div>
</.dl_row>
"""
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}
/>
</.sheet>
"""
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}
/>
</.structured_modal>
"""
end
attr :record, Records.Record, required: true
attr :section, :atom, values: [:collection, :wishlist], required: true
@@ -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
</div>
<div class="xl:col-span-3">
<div class="mt-4 flex items-center justify-between md:mt-0">
<h1 class="text-base/6 font-medium text-zinc-700">
<.artist_links joinphrase_class="text-sm" artists={@record.artists} />
</h1>
<div class="min-w-12">
<.button_group>
<.button
:if={@can_scrobble? and @record.selected_release_id}
variant="soft"
phx-click="scrobble_release"
>
<span class="sr-only">{gettext("Scrobble release")}</span>
<.icon
name="hero-play"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
</.button>
<.button
variant="soft"
phx-click={MusicLibraryWeb.Components.Chat.open("record-chat-sheet")}
>
<span class="sr-only">{gettext("Chat about album")}</span>
<.icon
name="hero-chat-bubble-left-right"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
<span :if={@chat_count > 0} class="text-xs font-medium">{@chat_count}</span>
</.button>
<.button
:if={@record.selected_release_id}
variant="soft"
phx-click={MusicLibraryWeb.Components.Release.open("release-with-tracks-sheet")}
>
<span class="sr-only">{gettext("Show Tracks")}</span>
<.icon
name="hero-numbered-list"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
</.button>
<.dropdown id={"actions-#{@record.id}"} placement="bottom-end">
<:toggle>
<.button variant="soft">
<span class="sr-only">{gettext("Actions")}</span>
<.icon
name="hero-ellipsis-vertical"
class="icon cursor-pointer text-zinc-500 dark:text-zinc-400"
aria-hidden="true"
data-slot="icon"
/>
</.button>
</:toggle>
<.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")}
</.dropdown_link>
<.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>
</: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_link>
<: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_link>
</:dropdown_extra>
</.record_show_action_bar>
<.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>
<.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>
<.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>
<.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>
<.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>
<.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_link>
<.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")}
</.dropdown_link>
</.focus_wrap>
</.dropdown>
</.button_group>
</div>
</div>
<.record_title_and_metadata record={@record} />
<.record_external_links record={@record} />
<div class="mt-4 md:mt-8">
@@ -254,32 +112,11 @@ defmodule MusicLibraryWeb.CollectionLive.Show do
</.link>
</.dl_row>
<.record_published_releases record={@record} />
<.dl_row label={gettext("Collected release")}>
<div class="flex justify-between space-x-2">
<span
:if={!@record.selected_release_id}
class="text-xs text-zinc-700 md:text-sm dark:text-zinc-300"
>
{gettext("No release selected")}
</span>
<.release_summary
:if={@record.selected_release_id}
release={Records.Record.selected_release(@record)}
/>
<span
:if={@record.selected_release_id}
id={"record-selected-release-" <> @record.id}
class="hidden"
>
{@record.selected_release_id}
</span>
<.copy_to_clipboard
:if={@record.selected_release_id}
target_id={"record-selected-release-" <> @record.id}
label={gettext("Copy record selected release ID to clipboard")}
/>
</div>
</.dl_row>
<.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} />
</dl>
@@ -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}
/>
</.sheet>
<.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}"}
/>
</.structured_modal>
<.record_show_edit_modal
record={@record}
live_action={@live_action}
show_purchased_at={true}
close_path={~p"/collection/#{@record}"}
patch_path={~p"/collection/#{@record}"}
/>
</Layouts.app>
"""
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)
+65 -342
View File
@@ -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
</div>
<div class="xl:col-span-3">
<div class="mt-4 flex items-center justify-between md:mt-0">
<h1 class="text-base/6 font-medium text-zinc-700">
<.artist_links joinphrase_class="text-sm" artists={@record.artists} />
</h1>
<div class="min-w-12">
<.button_group>
<.button
:if={@can_scrobble? and @record.selected_release_id}
variant="soft"
phx-click="scrobble_release"
>
<span class="sr-only">{gettext("Scrobble release")}</span>
<.icon
name="hero-play"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
</.button>
<.button
variant="soft"
phx-click={MusicLibraryWeb.Components.Chat.open("record-chat-sheet")}
>
<span class="sr-only">{gettext("Chat about album")}</span>
<.icon
name="hero-chat-bubble-left-right"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
<span :if={@chat_count > 0} class="text-xs font-medium">{@chat_count}</span>
</.button>
<.button
:if={@record.selected_release_id}
variant="soft"
phx-click={MusicLibraryWeb.Components.Release.open("release-with-tracks-sheet")}
>
<span class="sr-only">{gettext("Show Tracks")}</span>
<.icon
name="hero-numbered-list"
class="icon"
aria-hidden="true"
data-slot="icon"
/>
</.button>
<.dropdown id={"actions-#{@record.id}"} placement="bottom-end">
<:toggle>
<.button variant="soft">
<span class="sr-only">{gettext("Actions")}</span>
<.icon
name="hero-ellipsis-vertical"
class="icon cursor-pointer text-zinc-500 dark:text-zinc-400"
aria-hidden="true"
data-slot="icon"
/>
</.button>
</:toggle>
<.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")}
</.dropdown_link>
<.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_link>
</:dropdown_extra>
</.record_show_action_bar>
<.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>
<.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>
<.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>
<.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>
<.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>
<.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_link>
<.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")}
</.dropdown_link>
</.focus_wrap>
</.dropdown>
</.button_group>
</div>
</div>
<.record_title_and_metadata record={@record} current_date={@current_date} />
<.record_external_links record={@record} />
<div class="mt-4 md:mt-8">
<dl class="divide-y divide-zinc-100 dark:divide-slate-300/30">
<.record_genres record={@record} section={:wishlist} />
<.record_published_releases record={@record} />
<.dl_row label={gettext("Wishlisted release")}>
<div class="flex justify-between space-x-2">
<span
:if={!@record.selected_release_id}
class="text-xs text-zinc-700 md:text-sm dark:text-zinc-300"
>
{gettext("No release selected")}
</span>
<.release_summary
:if={@record.selected_release_id}
release={Records.Record.selected_release(@record)}
/>
</div>
</.dl_row>
<.record_show_selected_release_row
record={@record}
label={gettext("Wishlisted release")}
/>
<.record_includes record={@record} />
<.record_sets_list record_sets={@record_sets} />
</dl>
@@ -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}
/>
</.sheet>
<.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}"}
/>
</.structured_modal>
</Layouts.app>
"""
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
@@ -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
+17 -39
View File
@@ -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 ""
+17 -39
View File
@@ -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 ""