From 3f7da55e5065e312447a22e8b3587e6d6c7044a6 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Thu, 2 Apr 2026 11:52:07 +0100 Subject: [PATCH] Add a release tracked badge to records in lists --- docs/architecture.md | 2 +- lib/music_library/records/search_index.ex | 1 + .../components/record_components.ex | 24 +- .../components/search_components.ex | 10 +- .../components/stats_components.ex | 9 +- .../live/record_set_live/index.ex | 7 +- .../live/record_set_live/record_picker.ex | 5 +- .../live/record_set_live/show.ex | 6 +- ...ted_release_id_to_records_search_index.exs | 324 ++++++++++++++++++ 9 files changed, 374 insertions(+), 14 deletions(-) create mode 100644 priv/repo/migrations/20260402104145_add_selected_release_id_to_records_search_index.exs diff --git a/docs/architecture.md b/docs/architecture.md index f455bf45..b0a778a1 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -284,7 +284,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun |--------|---------| | `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, shared show-page sections (title, external links, genres, releases, timestamps, debug) | +| `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 | diff --git a/lib/music_library/records/search_index.ex b/lib/music_library/records/search_index.ex index b100b187..a617d131 100644 --- a/lib/music_library/records/search_index.ex +++ b/lib/music_library/records/search_index.ex @@ -26,6 +26,7 @@ defmodule MusicLibrary.Records.SearchIndex do field :cover_hash, :string field :release_ids, {:array, :string}, default: [] field :included_release_group_ids, {:array, :string}, default: [] + field :selected_release_id, :string embeds_many :artists, Artist diff --git a/lib/music_library_web/components/record_components.ex b/lib/music_library_web/components/record_components.ex index b5dce4a3..021ed54d 100644 --- a/lib/music_library_web/components/record_components.ex +++ b/lib/music_library_web/components/record_components.ex @@ -30,6 +30,25 @@ defmodule MusicLibraryWeb.RecordComponents do """ end + attr :record, :map, required: true + + def release_status_icon(assigns) do + ~H""" + <.icon + :if={@record.selected_release_id} + name="hero-check-circle-solid" + class="inline-block size-4 text-green-500" + aria-hidden="true" + /> + <.icon + :if={!@record.selected_release_id} + name="hero-question-mark-circle-solid" + class="inline-block size-4 text-yellow-500" + aria-hidden="true" + /> + """ + end + attr :record, :map, required: true attr :class, :string, required: false, default: "rounded-lg" attr :width, :integer, default: nil @@ -107,8 +126,9 @@ defmodule MusicLibraryWeb.RecordComponents do

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

-

+

{record.title} + <.release_status_icon record={record} />

{Records.Record.format_release_date(record.release_date)} @@ -356,7 +376,7 @@ defmodule MusicLibraryWeb.RecordComponents do <.artist_links joinphrase_class="text-xs" artists={record.artists} />

- {record.title} + {record.title} <.release_status_icon record={record} />

diff --git a/lib/music_library_web/components/search_components.ex b/lib/music_library_web/components/search_components.ex index 85d08f31..08d04ff7 100644 --- a/lib/music_library_web/components/search_components.ex +++ b/lib/music_library_web/components/search_components.ex @@ -6,7 +6,13 @@ defmodule MusicLibraryWeb.SearchComponents do use MusicLibraryWeb, :html import MusicLibraryWeb.RecordComponents, - only: [format_label: 1, type_label: 1, record_cover: 1, artist_image: 1] + only: [ + format_label: 1, + type_label: 1, + record_cover: 1, + artist_image: 1, + release_status_icon: 1 + ] alias MusicLibrary.Records.Record alias MusicLibraryWeb.Components.BarcodeScanner @@ -44,7 +50,7 @@ defmodule MusicLibraryWeb.SearchComponents do

- {@record.title} + {@record.title} <.release_status_icon record={@record} />

{Record.artist_names(@record)} diff --git a/lib/music_library_web/components/stats_components.ex b/lib/music_library_web/components/stats_components.ex index 037b4717..43584fa6 100644 --- a/lib/music_library_web/components/stats_components.ex +++ b/lib/music_library_web/components/stats_components.ex @@ -7,7 +7,8 @@ defmodule MusicLibraryWeb.StatsComponents do type_label: 1, artist_links: 1, record_cover: 1, - release_groups_badge: 1 + release_groups_badge: 1, + release_status_icon: 1 ] alias MusicLibrary.Records @@ -185,8 +186,9 @@ defmodule MusicLibraryWeb.StatsComponents do

<.artist_links joinphrase_class="text-xs" artists={rep.artists} />

-

+

{rep.title} + <.release_status_icon record={rep} />

<.released_how_long_ago record={rep} current_date={@current_date} /> @@ -257,8 +259,9 @@ defmodule MusicLibraryWeb.StatsComponents do

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

-

+

{@record.title} + <.release_status_icon record={@record} />

<.released_how_long_ago record={@record} current_date={@current_date} /> diff --git a/lib/music_library_web/live/record_set_live/index.ex b/lib/music_library_web/live/record_set_live/index.ex index 97c29ab4..6864e0e7 100644 --- a/lib/music_library_web/live/record_set_live/index.ex +++ b/lib/music_library_web/live/record_set_live/index.ex @@ -3,7 +3,9 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do import MusicLibraryWeb.Components.Pagination import MusicLibraryWeb.LiveHelpers.Params - import MusicLibraryWeb.RecordComponents, only: [artist_links: 1, type_label: 1, format_label: 1] + + import MusicLibraryWeb.RecordComponents, + only: [artist_links: 1, type_label: 1, format_label: 1, release_status_icon: 1] alias MusicLibrary.{Records, RecordSets} alias MusicLibrary.RecordSets.RecordSet @@ -395,8 +397,9 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do

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

-

+

{item.record.title} + <.release_status_icon record={item.record} />

{format_label(item.record.format)} · {type_label(item.record.type)} diff --git a/lib/music_library_web/live/record_set_live/record_picker.ex b/lib/music_library_web/live/record_set_live/record_picker.ex index 988ee25e..01fae516 100644 --- a/lib/music_library_web/live/record_set_live/record_picker.ex +++ b/lib/music_library_web/live/record_set_live/record_picker.ex @@ -5,7 +5,8 @@ defmodule MusicLibraryWeb.RecordSetLive.RecordPicker do alias MusicLibrary.Records.Record alias MusicLibrary.Wishlist - import MusicLibraryWeb.RecordComponents, only: [format_label: 1, type_label: 1] + import MusicLibraryWeb.RecordComponents, + only: [format_label: 1, type_label: 1, release_status_icon: 1] @impl true def render(assigns) do @@ -93,7 +94,7 @@ defmodule MusicLibraryWeb.RecordSetLive.RecordPicker do

- {@record.title} + {@record.title} <.release_status_icon record={@record} />

{Record.artist_names(@record)} diff --git a/lib/music_library_web/live/record_set_live/show.ex b/lib/music_library_web/live/record_set_live/show.ex index 3d6aa1a6..c7ece2c4 100644 --- a/lib/music_library_web/live/record_set_live/show.ex +++ b/lib/music_library_web/live/record_set_live/show.ex @@ -1,7 +1,8 @@ defmodule MusicLibraryWeb.RecordSetLive.Show do use MusicLibraryWeb, :live_view - import MusicLibraryWeb.RecordComponents, only: [artist_links: 1, type_label: 1, format_label: 1] + import MusicLibraryWeb.RecordComponents, + only: [artist_links: 1, type_label: 1, format_label: 1, release_status_icon: 1] alias MusicLibrary.{Records, RecordSets} alias MusicLibrary.RecordSets.RecordSet @@ -130,8 +131,9 @@ defmodule MusicLibraryWeb.RecordSetLive.Show do

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

-

+

{item.record.title} + <.release_status_icon record={item.record} />

{format_label(item.record.format)} · {type_label(item.record.type)} diff --git a/priv/repo/migrations/20260402104145_add_selected_release_id_to_records_search_index.exs b/priv/repo/migrations/20260402104145_add_selected_release_id_to_records_search_index.exs new file mode 100644 index 00000000..aa938257 --- /dev/null +++ b/priv/repo/migrations/20260402104145_add_selected_release_id_to_records_search_index.exs @@ -0,0 +1,324 @@ +defmodule MusicLibrary.Repo.Migrations.AddSelectedReleaseIdToRecordsSearchIndex do + use Ecto.Migration + + def up do + execute "DROP TRIGGER IF EXISTS records_after_update" + execute "DROP TRIGGER IF EXISTS records_after_insert" + execute "DROP TABLE records_search_index" + + flush() + + execute """ + CREATE VIRTUAL TABLE records_search_index USING fts5( + id UNINDEXED, + type, + format, + title, + normalized_title, + artists, + normalized_artists, + genres, + musicbrainz_id, + release_ids UNINDEXED, + included_release_group_ids UNINDEXED, + selected_release_id UNINDEXED, + cover_hash UNINDEXED, + purchased_at UNINDEXED, + release_date, + inserted_at UNINDEXED, + updated_at UNINDEXED + ); + """ + + flush() + + execute """ + CREATE TRIGGER records_after_insert + AFTER INSERT ON records + BEGIN + INSERT INTO records_search_index( + id, + type, + format, + title, + normalized_title, + artists, + normalized_artists, + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + selected_release_id, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + ) SELECT + id, + type, + format, + title, + unaccent(title), + artists, + unaccent(artists), + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + selected_release_id, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + FROM records + WHERE NEW.id = records.id; + END; + """ + + execute """ + CREATE TRIGGER records_after_update + AFTER UPDATE ON records + BEGIN + INSERT INTO records_search_index( + id, + type, + format, + title, + normalized_title, + artists, + normalized_artists, + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + selected_release_id, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + ) SELECT + id, + type, + format, + title, + unaccent(title), + artists, + unaccent(artists), + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + selected_release_id, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + FROM records + WHERE NEW.id = records.id; + END; + """ + + flush() + + execute """ + INSERT INTO records_search_index( + id, + type, + format, + title, + normalized_title, + artists, + normalized_artists, + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + selected_release_id, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + ) SELECT + id, + type, + format, + title, + unaccent(title), + artists, + unaccent(artists), + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + selected_release_id, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + FROM records; + """ + end + + def down do + execute "DROP TRIGGER IF EXISTS records_after_update" + execute "DROP TRIGGER IF EXISTS records_after_insert" + execute "DROP TABLE records_search_index" + + flush() + + execute """ + CREATE VIRTUAL TABLE records_search_index USING fts5( + id UNINDEXED, + type, + format, + title, + normalized_title, + artists, + normalized_artists, + genres, + musicbrainz_id, + release_ids UNINDEXED, + included_release_group_ids UNINDEXED, + cover_hash UNINDEXED, + purchased_at UNINDEXED, + release_date, + inserted_at UNINDEXED, + updated_at UNINDEXED + ); + """ + + flush() + + execute """ + CREATE TRIGGER records_after_insert + AFTER INSERT ON records + BEGIN + INSERT INTO records_search_index( + id, + type, + format, + title, + normalized_title, + artists, + normalized_artists, + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + ) SELECT + id, + type, + format, + title, + unaccent(title), + artists, + unaccent(artists), + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + FROM records + WHERE NEW.id = records.id; + END; + """ + + execute """ + CREATE TRIGGER records_after_update + AFTER UPDATE ON records + BEGIN + INSERT INTO records_search_index( + id, + type, + format, + title, + normalized_title, + artists, + normalized_artists, + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + ) SELECT + id, + type, + format, + title, + unaccent(title), + artists, + unaccent(artists), + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + FROM records + WHERE NEW.id = records.id; + END; + """ + + flush() + + execute """ + INSERT INTO records_search_index( + id, + type, + format, + title, + normalized_title, + artists, + normalized_artists, + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + ) SELECT + id, + type, + format, + title, + unaccent(title), + artists, + unaccent(artists), + genres, + musicbrainz_id, + release_ids, + included_release_group_ids, + cover_hash, + purchased_at, + release_date, + inserted_at, + updated_at + FROM records; + """ + end +end