Add a release tracked badge to records in lists

This commit is contained in:
Claudio Ortolina
2026-04-02 11:52:07 +01:00
parent b3af5be00d
commit 3f7da55e50
9 changed files with 374 additions and 14 deletions
+1 -1
View File
@@ -284,7 +284,7 @@ All authenticated routes live inside a single `live_session` with three `on_moun
|--------|---------| |--------|---------|
| `CoreComponents` | Forms, buttons, icons, tables, flash messages | | `CoreComponents` | Forms, buttons, icons, tables, flash messages |
| `Layouts` | Application layout templates, navigation components (`dropdown_nav/1`) | | `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 | | `ChartComponents` | Charts for stats dashboard |
| `StatsComponents` | Stats dashboard widgets (`section/1` layout, counters, album preview, records on this day) | | `StatsComponents` | Stats dashboard widgets (`section/1` layout, counters, album preview, records on this day) |
| `ScrobbleComponents` | Scrobble activity displays | | `ScrobbleComponents` | Scrobble activity displays |
@@ -26,6 +26,7 @@ defmodule MusicLibrary.Records.SearchIndex do
field :cover_hash, :string field :cover_hash, :string
field :release_ids, {:array, :string}, default: [] field :release_ids, {:array, :string}, default: []
field :included_release_group_ids, {:array, :string}, default: [] field :included_release_group_ids, {:array, :string}, default: []
field :selected_release_id, :string
embeds_many :artists, Artist embeds_many :artists, Artist
@@ -30,6 +30,25 @@ defmodule MusicLibraryWeb.RecordComponents do
""" """
end 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 :record, :map, required: true
attr :class, :string, required: false, default: "rounded-lg" attr :class, :string, required: false, default: "rounded-lg"
attr :width, :integer, default: nil attr :width, :integer, default: nil
@@ -107,8 +126,9 @@ defmodule MusicLibraryWeb.RecordComponents do
<h1 class="text-sm/6 text-zinc-700"> <h1 class="text-sm/6 text-zinc-700">
<.artist_links joinphrase_class="text-xs" artists={record.artists} /> <.artist_links joinphrase_class="text-xs" artists={record.artists} />
</h1> </h1>
<h2 class="mt-1 flex text-sm/5 font-semibold text-wrap text-zinc-700 sm:text-base dark:text-zinc-300"> <h2 class="mt-1 flex items-center gap-1 text-sm/5 font-semibold text-wrap text-zinc-700 sm:text-base dark:text-zinc-300">
{record.title} {record.title}
<.release_status_icon record={record} />
</h2> </h2>
<p class="mt-1 text-xs/5 text-zinc-500 dark:text-zinc-400"> <p class="mt-1 text-xs/5 text-zinc-500 dark:text-zinc-400">
{Records.Record.format_release_date(record.release_date)} {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} /> <.artist_links joinphrase_class="text-xs" artists={record.artists} />
</h1> </h1>
<p class="pointer-events-none text-sm font-medium text-zinc-900 dark:text-zinc-300"> <p class="pointer-events-none text-sm font-medium text-zinc-900 dark:text-zinc-300">
{record.title} {record.title} <.release_status_icon record={record} />
</p> </p>
</div> </div>
<p class="pointer-events-none block text-sm font-medium text-zinc-500"> <p class="pointer-events-none block text-sm font-medium text-zinc-500">
@@ -6,7 +6,13 @@ defmodule MusicLibraryWeb.SearchComponents do
use MusicLibraryWeb, :html use MusicLibraryWeb, :html
import MusicLibraryWeb.RecordComponents, 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 MusicLibrary.Records.Record
alias MusicLibraryWeb.Components.BarcodeScanner alias MusicLibraryWeb.Components.BarcodeScanner
@@ -44,7 +50,7 @@ defmodule MusicLibraryWeb.SearchComponents do
</div> </div>
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<p class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100"> <p class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">
{@record.title} {@record.title} <.release_status_icon record={@record} />
</p> </p>
<p class="truncate text-sm font-medium text-zinc-500 dark:text-zinc-400"> <p class="truncate text-sm font-medium text-zinc-500 dark:text-zinc-400">
{Record.artist_names(@record)} {Record.artist_names(@record)}
@@ -7,7 +7,8 @@ defmodule MusicLibraryWeb.StatsComponents do
type_label: 1, type_label: 1,
artist_links: 1, artist_links: 1,
record_cover: 1, record_cover: 1,
release_groups_badge: 1 release_groups_badge: 1,
release_status_icon: 1
] ]
alias MusicLibrary.Records alias MusicLibrary.Records
@@ -185,8 +186,9 @@ defmodule MusicLibraryWeb.StatsComponents do
<h1 class="truncate text-xs"> <h1 class="truncate text-xs">
<.artist_links joinphrase_class="text-xs" artists={rep.artists} /> <.artist_links joinphrase_class="text-xs" artists={rep.artists} />
</h1> </h1>
<h2 class="flex text-sm font-medium text-wrap text-zinc-700 dark:text-zinc-300"> <h2 class="flex items-center gap-1 text-sm font-medium text-wrap text-zinc-700 dark:text-zinc-300">
{rep.title} {rep.title}
<.release_status_icon record={rep} />
</h2> </h2>
<p class="text-xs/5 text-zinc-500 dark:text-zinc-400"> <p class="text-xs/5 text-zinc-500 dark:text-zinc-400">
<.released_how_long_ago record={rep} current_date={@current_date} /> <.released_how_long_ago record={rep} current_date={@current_date} />
@@ -257,8 +259,9 @@ defmodule MusicLibraryWeb.StatsComponents do
<h1 class="truncate text-xs"> <h1 class="truncate text-xs">
<.artist_links joinphrase_class="text-xs" artists={@record.artists} /> <.artist_links joinphrase_class="text-xs" artists={@record.artists} />
</h1> </h1>
<h2 class="flex text-sm font-medium text-wrap text-zinc-700 dark:text-zinc-300"> <h2 class="flex items-center gap-1 text-sm font-medium text-wrap text-zinc-700 dark:text-zinc-300">
{@record.title} {@record.title}
<.release_status_icon record={@record} />
</h2> </h2>
<p class="text-xs text-zinc-500 dark:text-zinc-400"> <p class="text-xs text-zinc-500 dark:text-zinc-400">
<.released_how_long_ago record={@record} current_date={@current_date} /> <.released_how_long_ago record={@record} current_date={@current_date} />
@@ -3,7 +3,9 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do
import MusicLibraryWeb.Components.Pagination import MusicLibraryWeb.Components.Pagination
import MusicLibraryWeb.LiveHelpers.Params 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.{Records, RecordSets}
alias MusicLibrary.RecordSets.RecordSet alias MusicLibrary.RecordSets.RecordSet
@@ -395,8 +397,9 @@ defmodule MusicLibraryWeb.RecordSetLive.Index do
<h2 class="mt-1 text-sm/6 text-zinc-700"> <h2 class="mt-1 text-sm/6 text-zinc-700">
<.artist_links joinphrase_class="text-sm" artists={item.record.artists} /> <.artist_links joinphrase_class="text-sm" artists={item.record.artists} />
</h2> </h2>
<h3 class="flex text-sm/5 font-semibold text-wrap text-zinc-700 dark:text-zinc-300"> <h3 class="flex items-center gap-1 text-sm/5 font-semibold text-wrap text-zinc-700 dark:text-zinc-300">
{item.record.title} {item.record.title}
<.release_status_icon record={item.record} />
</h3> </h3>
<p class="pointer-events-none block text-sm font-medium text-zinc-500"> <p class="pointer-events-none block text-sm font-medium text-zinc-500">
{format_label(item.record.format)} · {type_label(item.record.type)} {format_label(item.record.format)} · {type_label(item.record.type)}
@@ -5,7 +5,8 @@ defmodule MusicLibraryWeb.RecordSetLive.RecordPicker do
alias MusicLibrary.Records.Record alias MusicLibrary.Records.Record
alias MusicLibrary.Wishlist 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 @impl true
def render(assigns) do def render(assigns) do
@@ -93,7 +94,7 @@ defmodule MusicLibraryWeb.RecordSetLive.RecordPicker do
</div> </div>
<div class="min-w-0 flex-auto"> <div class="min-w-0 flex-auto">
<p class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100"> <p class="truncate text-sm font-medium text-zinc-900 dark:text-zinc-100">
{@record.title} {@record.title} <.release_status_icon record={@record} />
</p> </p>
<p class="truncate text-xs text-zinc-500 dark:text-zinc-400"> <p class="truncate text-xs text-zinc-500 dark:text-zinc-400">
{Record.artist_names(@record)} {Record.artist_names(@record)}
@@ -1,7 +1,8 @@
defmodule MusicLibraryWeb.RecordSetLive.Show do defmodule MusicLibraryWeb.RecordSetLive.Show do
use MusicLibraryWeb, :live_view 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.{Records, RecordSets}
alias MusicLibrary.RecordSets.RecordSet alias MusicLibrary.RecordSets.RecordSet
@@ -130,8 +131,9 @@ defmodule MusicLibraryWeb.RecordSetLive.Show do
<h2 class="mt-1 text-sm/6 text-zinc-700"> <h2 class="mt-1 text-sm/6 text-zinc-700">
<.artist_links joinphrase_class="text-sm" artists={item.record.artists} /> <.artist_links joinphrase_class="text-sm" artists={item.record.artists} />
</h2> </h2>
<h3 class="flex text-sm/5 font-semibold text-wrap text-zinc-700 dark:text-zinc-300"> <h3 class="flex items-center gap-1 text-sm/5 font-semibold text-wrap text-zinc-700 dark:text-zinc-300">
{item.record.title} {item.record.title}
<.release_status_icon record={item.record} />
</h3> </h3>
<p class="pointer-events-none block text-sm font-medium text-zinc-500"> <p class="pointer-events-none block text-sm font-medium text-zinc-500">
{format_label(item.record.format)} · {type_label(item.record.type)} {format_label(item.record.format)} · {type_label(item.record.type)}
@@ -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