268aff9918
Authored with Claude. > I'd like to have a new section in the Stats page. In this section I want to see the top albums I listened to in the past 30, 90, 365 days. Use the data in `scrobbled_tracks` to determine which albums need to be displayed. > This all looks great as a starter. I'd like to make some modifications. 1. Invert the position of the artist name and the album title (so that it's consistent with the Scrobble Activity) 2. Remove the border and shadow from around each album 3. Rename the "Last 365 days" section to "Last Year"
448 lines
16 KiB
Plaintext
448 lines
16 KiB
Plaintext
<div>
|
|
<h1 class="mt-5 text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
|
|
{gettext("Records")}
|
|
</h1>
|
|
<%!-- TODO: replace with OSS version --%>
|
|
<dl class="mt-5 grid grid-cols-2 gap-5 sm:grid-cols-5">
|
|
<.album_preview
|
|
record={@latest_record}
|
|
title={gettext("Latest purchase")}
|
|
class="col-span-2 sm:col-span-3"
|
|
/>
|
|
<.counter
|
|
title={gettext("Total collection")}
|
|
count={@collection_count}
|
|
path={~p"/collection"}
|
|
/>
|
|
<.counter title={gettext("Total wishlist")} count={@wishlist_count} path={~p"/wishlist"} />
|
|
</dl>
|
|
</div>
|
|
|
|
<div>
|
|
<h1 class="mt-5 text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
|
|
{gettext("Formats")}
|
|
</h1>
|
|
<%!-- TODO: replace with OSS version --%>
|
|
<dl class={[
|
|
"mt-5 grid divide-zinc-200 dark:divide-zinc-900 overflow-hidden rounded-md bg-white dark:bg-zinc-800 shadow-sm divide-x-2",
|
|
stats_class(@collection_count_by_format)
|
|
]}>
|
|
<div :for={{format, count} <- @collection_count_by_format} class="px-2 py-5 sm:px-4">
|
|
<dt class="text-sm font-medium text-zinc-500 dark:text-zinc-400 text-center max-sm:text-xs break-keep">
|
|
{format_label(format)}
|
|
</dt>
|
|
<dd class="mt-1 text-center">
|
|
<.link
|
|
class="text-xl lg:text-2xl font-semibold hover:text-zinc-500 dark:text-zinc-300 dark:hover:text-zinc-200"
|
|
navigate={~p"/collection?query=format:#{format}"}
|
|
>
|
|
{count}
|
|
</.link>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div>
|
|
<h1 class="mt-5 text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
|
|
{gettext("Types")}
|
|
</h1>
|
|
<%!-- TODO: replace with OSS version --%>
|
|
<dl class={[
|
|
"mt-5 grid divide-zinc-200 dark:divide-zinc-900 overflow-hidden rounded-md bg-white dark:bg-zinc-800 shadow-sm divide-x-2",
|
|
stats_class(@collection_count_by_type)
|
|
]}>
|
|
<div :for={{type, count} <- @collection_count_by_type} class="px-2 py-5 sm:px-4">
|
|
<dt class="text-sm font-medium text-zinc-500 dark:text-zinc-400 text-center max-sm:text-xs break-keep">
|
|
{type_label(type)}
|
|
</dt>
|
|
<dd class="mt-1 text-center">
|
|
<.link
|
|
class="text-xl lg:text-2xl font-semibold hover:text-zinc-500 dark:text-zinc-300 dark:hover:text-zinc-200"
|
|
navigate={~p"/collection?query=type:#{type}"}
|
|
>
|
|
{count}
|
|
</.link>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="mt-5 grid grid-cols-1 lg:grid-cols-2 gap-5">
|
|
<div>
|
|
<h1 class="text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
|
|
{gettext("Top %{n} Artists", %{n: length(@records_by_artist)})}
|
|
</h1>
|
|
<div class="mt-5 bg-white dark:bg-zinc-800 rounded-md shadow-sm">
|
|
<.vertical_bar_chart
|
|
data={@records_by_artist}
|
|
width={600}
|
|
height={35 * length(@records_by_artist)}
|
|
color_class="fill-red-500"
|
|
label_fn={fn datum -> datum.name end}
|
|
value_fn={fn datum -> datum.count end}
|
|
label_click={
|
|
fn datum ->
|
|
JS.navigate(~p"/artists/#{datum.id}")
|
|
end
|
|
}
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h1 class="text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
|
|
{gettext("Top %{n} Genres", %{n: length(@records_by_genre)})}
|
|
</h1>
|
|
<div class="mt-5 bg-white dark:bg-zinc-800 rounded-md shadow-sm">
|
|
<.vertical_bar_chart
|
|
data={@records_by_genre}
|
|
width={600}
|
|
height={35 * length(@records_by_genre)}
|
|
color_class="fill-zinc-500"
|
|
label_fn={fn {genre, _count} -> genre end}
|
|
value_fn={fn {_genre, count} -> count end}
|
|
label_click={
|
|
fn {genre, _count} -> JS.navigate(~p"/collection?#{%{query: ~s(genre:"#{genre}")}}") end
|
|
}
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h1 class="mt-5 text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
|
|
{gettext("Top Albums")}
|
|
</h1>
|
|
|
|
<div class="mt-5 grid grid-cols-1 lg:grid-cols-3 gap-5">
|
|
<div>
|
|
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-200 mb-3">
|
|
{gettext("Last 30 days")}
|
|
</h2>
|
|
<div class="space-y-2">
|
|
<div :for={album <- @top_albums.last_30_days} class="flex items-center space-x-3 p-2">
|
|
<img
|
|
class="w-12 h-12 rounded-md object-cover"
|
|
src={album.cover_url}
|
|
alt={album.album_title}
|
|
/>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-xs text-zinc-600 dark:text-zinc-400 truncate">
|
|
{album.artist_name}
|
|
</p>
|
|
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-300 truncate">
|
|
{album.album_title}
|
|
</p>
|
|
</div>
|
|
<div class="text-sm font-semibold text-zinc-900 dark:text-zinc-300">
|
|
{album.play_count}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-200 mb-3">
|
|
{gettext("Last 90 days")}
|
|
</h2>
|
|
<div class="space-y-2">
|
|
<div :for={album <- @top_albums.last_90_days} class="flex items-center space-x-3 p-2">
|
|
<img
|
|
class="w-12 h-12 rounded-md object-cover"
|
|
src={album.cover_url}
|
|
alt={album.album_title}
|
|
/>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-xs text-zinc-600 dark:text-zinc-400 truncate">
|
|
{album.artist_name}
|
|
</p>
|
|
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-300 truncate">
|
|
{album.album_title}
|
|
</p>
|
|
</div>
|
|
<div class="text-sm font-semibold text-zinc-900 dark:text-zinc-300">
|
|
{album.play_count}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h2 class="text-base font-semibold text-zinc-900 dark:text-zinc-200 mb-3">
|
|
{gettext("Last Year")}
|
|
</h2>
|
|
<div class="space-y-2">
|
|
<div :for={album <- @top_albums.last_365_days} class="flex items-center space-x-3 p-2">
|
|
<img
|
|
class="w-12 h-12 rounded-md object-cover"
|
|
src={album.cover_url}
|
|
alt={album.album_title}
|
|
/>
|
|
<div class="flex-1 min-w-0">
|
|
<p class="text-xs text-zinc-600 dark:text-zinc-400 truncate">
|
|
{album.artist_name}
|
|
</p>
|
|
<p class="text-sm font-medium text-zinc-900 dark:text-zinc-300 truncate">
|
|
{album.album_title}
|
|
</p>
|
|
</div>
|
|
<div class="text-sm font-semibold text-zinc-900 dark:text-zinc-300">
|
|
{album.play_count}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flow-root">
|
|
<div class="mt-5 flex justify-between items-center">
|
|
<h1 class="text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
|
|
{gettext("Scrobble activity")}
|
|
</h1>
|
|
<button
|
|
type="button"
|
|
class="phx-click-loading:animate-spin text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-300"
|
|
phx-click={JS.push("refresh_lastfm_feed")}
|
|
>
|
|
<span class="sr-only">{gettext("Refresh LastFm Feed")}</span>
|
|
<.icon name="hero-arrow-path" class="-mt-1 h-5 w-5" aria-hidden="true" data-slot="icon" />
|
|
</button>
|
|
</div>
|
|
<%!-- TODO: replace with OSS version --%>
|
|
<span class="mt-5 isolate inline-flex rounded-md shadow-xs">
|
|
<.button
|
|
phx-click={JS.push("set_scrobble_activity_mode", value: %{"mode" => :albums})}
|
|
class={[
|
|
"rounded-l-md rounded-r-none",
|
|
@scrobble_activity_mode == :albums && "!bg-zinc-100 dark:!bg-zinc-700"
|
|
]}
|
|
>
|
|
{gettext("Albums")}
|
|
</.button>
|
|
<.button
|
|
phx-click={JS.push("set_scrobble_activity_mode", value: %{"mode" => :tracks})}
|
|
class={[
|
|
"-ml-px rounded-r-md rounded-l-none",
|
|
@scrobble_activity_mode == :tracks && "!bg-zinc-100 dark:!bg-zinc-700"
|
|
]}
|
|
>
|
|
{gettext("Tracks")}
|
|
</.button>
|
|
</span>
|
|
<ul
|
|
:if={@scrobble_activity_mode == :albums}
|
|
id="scrobble-activity-albums"
|
|
role="list"
|
|
class="mt-5 rounded-md px-4 py-5 shadow-sm"
|
|
phx-update="stream"
|
|
>
|
|
<li :for={{id, album} <- @streams.recent_albums} id={id} class="group">
|
|
<div class="relative pb-8 group-last:pb-0">
|
|
<span
|
|
class="group-last:hidden absolute left-6 top-6 -ml-px h-full w-0.5 bg-zinc-200"
|
|
aria-hidden="true"
|
|
>
|
|
</span>
|
|
<div class="relative flex space-x-3 items-center justify-between">
|
|
<div class="flex min-w-0 justify-between space-x-4 items-center">
|
|
<img
|
|
class="h-12 w-12 rounded-md shadow-sm"
|
|
src={album.cover_url}
|
|
alt={album.metadata.title}
|
|
/>
|
|
<div>
|
|
<p
|
|
:if={album.artist.musicbrainz_id not in @artist_ids}
|
|
class="font-semibold text-sm block text-zinc-500 dark:text-zinc-400"
|
|
>
|
|
{album.artist.name}
|
|
</p>
|
|
<.link
|
|
:if={album.artist.musicbrainz_id in @artist_ids}
|
|
class="font-semibold text-sm block text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
|
|
navigate={~p"/artists/#{album.artist.musicbrainz_id}"}
|
|
>
|
|
{album.artist.name}
|
|
</.link>
|
|
<p class="font-semibold text-sm md:text-base text-zinc-700 dark:text-zinc-300">
|
|
{album.metadata.title}
|
|
</p>
|
|
<time
|
|
datetime={format_scrobbled_at_uts(album.scrobbled_at_uts)}
|
|
class="whitespace-nowrap text-right text-xs sm:text-sm text-zinc-500 dark:text-zinc-400"
|
|
>
|
|
{album.scrobbled_at_label}
|
|
</time>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-1 flex-col text-right">
|
|
<.badge :if={album.metadata.musicbrainz_id == ""}>
|
|
{gettext("No MB ID")}
|
|
</.badge>
|
|
<.link
|
|
:if={
|
|
record_id = tracked_record?(@collected_releases, album.metadata.musicbrainz_id)
|
|
}
|
|
navigate={~p"/collection/#{record_id}"}
|
|
>
|
|
<.badge color="green">
|
|
{gettext("Collected")}
|
|
</.badge>
|
|
</.link>
|
|
<.link
|
|
:if={
|
|
record_id = tracked_record?(@wishlisted_releases, album.metadata.musicbrainz_id)
|
|
}
|
|
navigate={~p"/wishlist/#{record_id}"}
|
|
>
|
|
<.badge color="yellow">
|
|
{gettext("Wishlisted")}
|
|
</.badge>
|
|
</.link>
|
|
</div>
|
|
|
|
<.actions_menu
|
|
:if={
|
|
album.metadata.musicbrainz_id !== "" and
|
|
!tracked_record?(
|
|
@collected_releases ++ @wishlisted_releases,
|
|
album.metadata.musicbrainz_id
|
|
)
|
|
}
|
|
id={album.scrobbled_at_uts}
|
|
background_container_target="#records > li"
|
|
>
|
|
<:button>
|
|
<span class="sr-only">{gettext("Choose which format to import")}</span>
|
|
<.icon name="hero-star" class="-mt-1 h-5 w-5" aria-hidden="true" data-slot="icon" />
|
|
</:button>
|
|
<:links>
|
|
<.link
|
|
:for={format <- Records.Record.formats()}
|
|
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
|
|
role="menuitem"
|
|
tabindex="0"
|
|
id={"actions-#{album.scrobbled_at_uts}-#{format}-import"}
|
|
phx-click={
|
|
JS.push("import",
|
|
value: %{id: album.metadata.musicbrainz_id, format: format},
|
|
page_loading: true
|
|
)
|
|
}
|
|
>
|
|
{format_label(format)}
|
|
</.link>
|
|
</:links>
|
|
</.actions_menu>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<ul
|
|
:if={@scrobble_activity_mode == :tracks}
|
|
id="scrobble-activity-tracks"
|
|
role="list"
|
|
class="mt-5 rounded-md px-4 py-5 shadow-sm"
|
|
phx-update="stream"
|
|
>
|
|
<li :for={{id, track} <- @streams.recent_tracks} id={id} class="group">
|
|
<div class="relative pb-8 group-last:pb-0">
|
|
<span
|
|
class="group-last:hidden absolute left-6 top-6 -ml-px h-full w-0.5 bg-zinc-200"
|
|
aria-hidden="true"
|
|
>
|
|
</span>
|
|
<div class="relative flex space-x-3 items-center justify-between">
|
|
<div class="flex min-w-0 justify-between space-x-4 items-center">
|
|
<img class="h-12 w-12 rounded-md shadow-sm" src={track.cover_url} alt={track.title} />
|
|
<div>
|
|
<p
|
|
:if={track.artist.musicbrainz_id not in @artist_ids}
|
|
class="font-semibold text-sm block text-zinc-500 dark:text-zinc-400"
|
|
>
|
|
{track.artist.name}
|
|
</p>
|
|
<.link
|
|
:if={track.artist.musicbrainz_id in @artist_ids}
|
|
class="font-semibold text-sm block text-zinc-700 hover:text-zinc-500 dark:text-zinc-400 dark:hover:text-zinc-300"
|
|
navigate={~p"/artists/#{track.artist.musicbrainz_id}"}
|
|
>
|
|
{track.artist.name}
|
|
</.link>
|
|
<p class="font-semibold text-sm md:text-base text-zinc-700 dark:text-zinc-300">
|
|
{track.title}
|
|
</p>
|
|
<p class="font-semibold text-sm text-zinc-500 dark:text-zinc-400">
|
|
{track.album.title}
|
|
</p>
|
|
<time
|
|
datetime={format_scrobbled_at_uts(track.scrobbled_at_uts)}
|
|
class="whitespace-nowrap text-right text-xs sm:text-sm text-zinc-500 dark:text-zinc-400"
|
|
>
|
|
{track.scrobbled_at_label}
|
|
</time>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-1 flex-col text-right">
|
|
<.badge :if={track.album.musicbrainz_id == ""}>
|
|
{gettext("No MB ID")}
|
|
</.badge>
|
|
<.link
|
|
:if={record_id = tracked_record?(@collected_releases, track.album.musicbrainz_id)}
|
|
navigate={~p"/collection/#{record_id}"}
|
|
>
|
|
<.badge color="green">{gettext("Collected")}</.badge>
|
|
</.link>
|
|
<.link
|
|
:if={record_id = tracked_record?(@wishlisted_releases, track.album.musicbrainz_id)}
|
|
navigate={~p"/wishlist/#{record_id}"}
|
|
>
|
|
<.badge color="yellow">{gettext("Wishlisted")}</.badge>
|
|
</.link>
|
|
</div>
|
|
|
|
<.actions_menu
|
|
:if={
|
|
track.album.musicbrainz_id !== "" and
|
|
!tracked_record?(
|
|
@collected_releases ++ @wishlisted_releases,
|
|
track.album.musicbrainz_id
|
|
)
|
|
}
|
|
id={track.scrobbled_at_uts}
|
|
background_container_target="#records > li"
|
|
>
|
|
<:button>
|
|
<span class="sr-only">{gettext("Choose which format to import")}</span>
|
|
<.icon name="hero-star" class="-mt-1 h-5 w-5" aria-hidden="true" data-slot="icon" />
|
|
</:button>
|
|
<:links>
|
|
<.link
|
|
:for={format <- Records.Record.formats()}
|
|
class="block px-3 py-1 text-sm leading-6 text-zinc-900 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:text-zinc-300 dark:hover:bg-zinc-700"
|
|
role="menuitem"
|
|
tabindex="0"
|
|
id={"actions-#{track.scrobbled_at_uts}-#{format}-import"}
|
|
phx-click={
|
|
JS.push("import",
|
|
value: %{id: track.album.musicbrainz_id, format: format},
|
|
page_loading: true
|
|
)
|
|
}
|
|
>
|
|
{format_label(format)}
|
|
</.link>
|
|
</:links>
|
|
</.actions_menu>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</div>
|