f4727bdcbd
Use a combination of ghost and soft, with button groups where appropriate. Also remove unnecessary negative margins, and pad around for ease of use on mobile.
563 lines
23 KiB
Plaintext
563 lines
23 KiB
Plaintext
<Layouts.app flash={@flash} current_section={@current_section} socket={@socket}>
|
|
<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-3 gap-5 sm:grid-cols-5">
|
|
<.album_preview
|
|
record={@latest_record}
|
|
title={gettext("Latest purchase")}
|
|
class="col-span-3 sm:col-span-2"
|
|
/>
|
|
<.counter
|
|
title={gettext("Collection")}
|
|
count={@collection_count}
|
|
path={~p"/collection"}
|
|
/>
|
|
<.counter title={gettext("Wishlist")} count={@wishlist_count} path={~p"/wishlist"} />
|
|
<.counter
|
|
title={gettext("Scrobbles")}
|
|
count={to_compact(@scrobble_count)}
|
|
tooltip={@scrobble_count}
|
|
/>
|
|
</dl>
|
|
</div>
|
|
|
|
<div class="grid lg:grid-cols-2 gap-x-5">
|
|
<div>
|
|
<h1 class="mt-5 text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
|
|
{gettext("Formats")}
|
|
</h1>
|
|
<.counters_by_category
|
|
categories_with_counts={@collection_count_by_format}
|
|
category_format_fn={&format_label/1}
|
|
category_path_fn={fn format -> ~p"/collection?query=format:#{format}" end}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<h1 class="mt-5 text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
|
|
{gettext("Types")}
|
|
</h1>
|
|
<.counters_by_category
|
|
categories_with_counts={@collection_count_by_type}
|
|
category_format_fn={&type_label/1}
|
|
category_path_fn={fn type -> ~p"/collection?query=type:#{type}" end}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<div class="mt-5 grid grid-cols-1 lg:grid-cols-2 gap-5">
|
|
<TopArtists.live id="top-artists" timezone={@timezone} last_updated_uts={@last_updated_uts} />
|
|
<TopAlbums.live id="top-albums" timezone={@timezone} last_updated_uts={@last_updated_uts} />
|
|
</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>
|
|
<.refresh_lastfm_feed_button />
|
|
</div>
|
|
<.tabs id="scrobble-activity" class="mt-4">
|
|
<.tabs_list active_tab={@scrobble_activity_mode} variant="segmented" class="w-48">
|
|
<:tab
|
|
name="albums"
|
|
phx-click={JS.push("set_scrobble_activity_mode", value: %{mode: "albums"})}
|
|
>
|
|
{gettext("Albums")}
|
|
</:tab>
|
|
<:tab
|
|
name="tracks"
|
|
phx-click={JS.push("set_scrobble_activity_mode", value: %{mode: "tracks"})}
|
|
>
|
|
{gettext("Tracks")}
|
|
</:tab>
|
|
</.tabs_list>
|
|
|
|
<.tabs_panel name="albums" active={@scrobble_activity_mode == "albums"}>
|
|
<ul
|
|
id="scrobble-activity-albums"
|
|
role="list"
|
|
class="mt-5 p-6 bg-white dark:bg-zinc-800 rounded-md shadow-sm"
|
|
phx-update="stream"
|
|
>
|
|
<li
|
|
:for={
|
|
{id,
|
|
%{
|
|
album: album,
|
|
artist_id: artist_id,
|
|
collected_record_id: collected_record_id,
|
|
wishlisted_record_id: wishlisted_record_id,
|
|
cover_hash: cover_hash
|
|
}} <- @streams.recent_albums
|
|
}
|
|
id={id}
|
|
class="group"
|
|
>
|
|
<div class="relative pb-4 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_or_album_cover_url(album, cover_hash)}
|
|
alt={album.metadata.title}
|
|
/>
|
|
<div>
|
|
<p
|
|
:if={!artist_id(album, artist_id)}
|
|
class="font-semibold text-sm block text-zinc-500 dark:text-zinc-400"
|
|
>
|
|
{album.artist.name}
|
|
</p>
|
|
<.link
|
|
:if={artist_id(album, artist_id)}
|
|
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/#{artist_id(album, artist_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}
|
|
<.tooltip>
|
|
<.icon
|
|
name="hero-information-circle"
|
|
class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
|
|
aria-hidden="true"
|
|
data-slot="icon"
|
|
/>
|
|
<:content>
|
|
<dl class="p-2">
|
|
<div class="flex gap-2 w-full">
|
|
<dt>{gettext("Album ID:")}</dt>
|
|
<dd class="font-mono">
|
|
<code id={"tooltip-album-#{album.scrobbled_at_uts}"}>
|
|
{album.metadata.musicbrainz_id || gettext("Unknown")}
|
|
</code>
|
|
<button
|
|
:if={album.metadata.musicbrainz_id not in ["", nil]}
|
|
phx-click={
|
|
JS.dispatch("music_library:clipcopy",
|
|
to: "#tooltip-album-#{album.scrobbled_at_uts}"
|
|
)
|
|
|> JS.transition("animate-shake")
|
|
}
|
|
>
|
|
<span class="sr-only">
|
|
{gettext("Copy album MusicBrainz ID to clipboard")}
|
|
</span>
|
|
<.icon
|
|
name="hero-clipboard-document"
|
|
class="h-5 w-5"
|
|
aria-hidden="true"
|
|
data-slot="icon"
|
|
/>
|
|
</button>
|
|
</dd>
|
|
</div>
|
|
<div class="flex gap-2 w-full mt-2">
|
|
<dt>{gettext("Artist ID:")}</dt>
|
|
<dd class="font-mono">
|
|
<code id={"tooltip-artist-#{album.scrobbled_at_uts}"}>
|
|
{album.artist.musicbrainz_id || gettext("Unknown")}
|
|
</code>
|
|
<button
|
|
:if={album.artist.musicbrainz_id not in ["", nil]}
|
|
phx-click={
|
|
JS.dispatch("music_library:clipcopy",
|
|
to: "#tooltip-artist-#{album.scrobbled_at_uts}"
|
|
)
|
|
|> JS.transition("animate-shake")
|
|
}
|
|
>
|
|
<span class="sr-only">
|
|
{gettext("Copy artist MusicBrainz ID to clipboard")}
|
|
</span>
|
|
<.icon
|
|
name="hero-clipboard-document"
|
|
class="h-5 w-5"
|
|
aria-hidden="true"
|
|
data-slot="icon"
|
|
/>
|
|
</button>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</:content>
|
|
</.tooltip>
|
|
</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={collected_record_id}
|
|
navigate={~p"/collection/#{collected_record_id}"}
|
|
>
|
|
<.badge color="success">
|
|
{gettext("Collected")}
|
|
</.badge>
|
|
</.link>
|
|
<.link
|
|
:if={wishlisted_record_id}
|
|
navigate={~p"/wishlist/#{wishlisted_record_id}"}
|
|
>
|
|
<.badge color="warning">
|
|
{gettext("Wishlisted")}
|
|
</.badge>
|
|
</.link>
|
|
</div>
|
|
|
|
<.dropdown
|
|
:if={
|
|
album.metadata.musicbrainz_id !== "" and !collected_record_id and
|
|
!wishlisted_record_id
|
|
}
|
|
id={"actions-#{album.scrobbled_at_uts}-albums"}
|
|
placement="bottom-end"
|
|
>
|
|
<:toggle>
|
|
<.button variant="ghost">
|
|
<span class="sr-only">{gettext("Choose which format to import")}</span>
|
|
<.icon
|
|
name="hero-star"
|
|
class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
|
|
aria-hidden="true"
|
|
data-slot="icon"
|
|
/>
|
|
</.button>
|
|
</:toggle>
|
|
<.focus_wrap id={"actions-#{album.scrobbled_at_uts}-albums-focus-wrap"}>
|
|
<.dropdown_link
|
|
:for={format <- Records.Record.formats()}
|
|
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)}
|
|
</.dropdown_link>
|
|
</.focus_wrap>
|
|
</.dropdown>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</.tabs_panel>
|
|
<.tabs_panel name="tracks" active={@scrobble_activity_mode == "tracks"}>
|
|
<ul
|
|
id="scrobble-activity-tracks"
|
|
role="list"
|
|
class="mt-5 p-6 bg-white dark:bg-zinc-800 rounded-md shadow-sm"
|
|
phx-update="stream"
|
|
>
|
|
<li
|
|
:for={
|
|
{id,
|
|
%{
|
|
track: track,
|
|
artist_id: artist_id,
|
|
collected_record_id: collected_record_id,
|
|
wishlisted_record_id: wishlisted_record_id,
|
|
cover_hash: cover_hash
|
|
}} <- @streams.recent_tracks
|
|
}
|
|
id={id}
|
|
class="group"
|
|
>
|
|
<div class="relative pb-4 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_or_album_cover_url(track, cover_hash)}
|
|
alt={track.title}
|
|
/>
|
|
<div>
|
|
<p
|
|
:if={!artist_id(track, artist_id)}
|
|
class="font-semibold text-sm block text-zinc-500 dark:text-zinc-400"
|
|
>
|
|
{track.artist.name}
|
|
</p>
|
|
<.link
|
|
:if={artist_id(track, artist_id)}
|
|
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/#{artist_id(track, artist_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>
|
|
<.tooltip>
|
|
<.icon
|
|
name="hero-information-circle"
|
|
class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
|
|
aria-hidden="true"
|
|
data-slot="icon"
|
|
/>
|
|
<:content>
|
|
<dl class="p-2">
|
|
<div class="flex gap-2 w-full">
|
|
<dt>{gettext("Track ID:")}</dt>
|
|
<dd class="font-mono">
|
|
<code id={"tooltip-track-#{track.scrobbled_at_uts}"}>
|
|
{track.musicbrainz_id || gettext("Unknown")}
|
|
</code>
|
|
<button
|
|
:if={track.musicbrainz_id not in ["", nil]}
|
|
phx-click={
|
|
JS.dispatch("music_library:clipcopy",
|
|
to: "#tooltip-track-#{track.scrobbled_at_uts}"
|
|
)
|
|
|> JS.transition("animate-shake")
|
|
}
|
|
>
|
|
<span class="sr-only">
|
|
{gettext("Copy track MusicBrainz ID to clipboard")}
|
|
</span>
|
|
<.icon
|
|
name="hero-clipboard-document"
|
|
class="h-5 w-5"
|
|
aria-hidden="true"
|
|
data-slot="icon"
|
|
/>
|
|
</button>
|
|
</dd>
|
|
</div>
|
|
<div class="flex gap-2 w-full mt-2">
|
|
<dt>{gettext("Album ID:")}</dt>
|
|
<dd class="font-mono">
|
|
<code id={"tooltip-track-album-#{track.scrobbled_at_uts}"}>
|
|
{track.album.musicbrainz_id || gettext("Unknown")}
|
|
</code>
|
|
<button
|
|
:if={track.album.musicbrainz_id not in ["", nil]}
|
|
phx-click={
|
|
JS.dispatch("music_library:clipcopy",
|
|
to: "#tooltip-track-album-#{track.scrobbled_at_uts}"
|
|
)
|
|
|> JS.transition("animate-shake")
|
|
}
|
|
>
|
|
<span class="sr-only">
|
|
{gettext("Copy album MusicBrainz ID to clipboard")}
|
|
</span>
|
|
<.icon
|
|
name="hero-clipboard-document"
|
|
class="h-5 w-5"
|
|
aria-hidden="true"
|
|
data-slot="icon"
|
|
/>
|
|
</button>
|
|
</dd>
|
|
</div>
|
|
<div class="flex gap-2 w-full mt-2">
|
|
<dt>{gettext("Artist ID:")}</dt>
|
|
<dd class="font-mono">
|
|
<code id={"tooltip-track-artist-#{track.scrobbled_at_uts}"}>
|
|
{track.artist.musicbrainz_id || gettext("Unknown")}
|
|
</code>
|
|
<button
|
|
:if={track.artist.musicbrainz_id not in ["", nil]}
|
|
phx-click={
|
|
JS.dispatch("music_library:clipcopy",
|
|
to: "#tooltip-track-artist-#{track.scrobbled_at_uts}"
|
|
)
|
|
|> JS.transition("animate-shake")
|
|
}
|
|
>
|
|
<span class="sr-only">
|
|
{gettext("Copy artist MusicBrainz ID to clipboard")}
|
|
</span>
|
|
<.icon
|
|
name="hero-clipboard-document"
|
|
class="h-5 w-5"
|
|
aria-hidden="true"
|
|
data-slot="icon"
|
|
/>
|
|
</button>
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</:content>
|
|
</.tooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex gap-1 flex-col text-right">
|
|
<.badge :if={track.album.musicbrainz_id == ""}>
|
|
{gettext("No MB ID")}
|
|
</.badge>
|
|
<.link
|
|
:if={collected_record_id}
|
|
navigate={~p"/collection/#{collected_record_id}"}
|
|
>
|
|
<.badge color="success">{gettext("Collected")}</.badge>
|
|
</.link>
|
|
<.link
|
|
:if={wishlisted_record_id}
|
|
navigate={~p"/wishlist/#{wishlisted_record_id}"}
|
|
>
|
|
<.badge color="warning">{gettext("Wishlisted")}</.badge>
|
|
</.link>
|
|
</div>
|
|
|
|
<.dropdown
|
|
:if={
|
|
track.album.musicbrainz_id !== "" and !collected_record_id and
|
|
!wishlisted_record_id
|
|
}
|
|
id={"actions-#{track.scrobbled_at_uts}-tracks"}
|
|
placement="bottom-end"
|
|
>
|
|
<:toggle>
|
|
<.button variant="ghost">
|
|
<span class="sr-only">{gettext("Choose which format to import")}</span>
|
|
<.icon
|
|
name="hero-star"
|
|
class="h-5 w-5 text-zinc-500 dark:text-zinc-400 cursor-pointer"
|
|
aria-hidden="true"
|
|
data-slot="icon"
|
|
/>
|
|
</.button>
|
|
</:toggle>
|
|
<.focus_wrap id={"actions-#{track.scrobbled_at_uts}-tracks-focus-wrap"}>
|
|
<.dropdown_link
|
|
:for={format <- Records.Record.formats()}
|
|
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)}
|
|
</.dropdown_link>
|
|
</.focus_wrap>
|
|
</.dropdown>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</.tabs_panel>
|
|
</.tabs>
|
|
</div>
|
|
|
|
<div class="mt-5 grid grid-cols-1 lg:grid-cols-11 gap-4">
|
|
<div class="lg:col-span-3">
|
|
<div class="flex justify-between items-center">
|
|
<h1 class="text-base lg:text-2xl text-zinc-900 dark:text-zinc-200 font-semibold">
|
|
{gettext("On This day")}
|
|
</h1>
|
|
<.form
|
|
:let={f}
|
|
for={to_form(%{"current_date" => @current_date})}
|
|
phx-change="set_current_date"
|
|
>
|
|
<.date_picker size="xs" field={f[:current_date]}>
|
|
<:outer_suffix>
|
|
<.button
|
|
size="xs"
|
|
type="button"
|
|
phx-click={
|
|
JS.push("set_current_date", value: %{"current_date" => Date.utc_today()})
|
|
}
|
|
>
|
|
Today
|
|
</.button>
|
|
</:outer_suffix>
|
|
</.date_picker>
|
|
</.form>
|
|
</div>
|
|
<div class="bg-white dark:bg-zinc-800 rounded-md shadow-sm">
|
|
<.records_on_this_day
|
|
current_date={@current_date}
|
|
records={@streams.records_on_this_day}
|
|
record_show_path={fn record -> ~p"/collection/#{record}" end}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="lg:col-span-4">
|
|
<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={26 * length(@records_by_artist)}
|
|
color_class="fill-red-500"
|
|
label_fn={fn datum -> datum.name end}
|
|
value_fn={fn datum -> datum.count end}
|
|
datum_click={
|
|
fn datum ->
|
|
JS.navigate(~p"/artists/#{datum.id}")
|
|
end
|
|
}
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="lg:col-span-4">
|
|
<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={26 * length(@records_by_genre)}
|
|
color_class="fill-zinc-500"
|
|
label_fn={fn {genre, _count} -> genre end}
|
|
value_fn={fn {_genre, count} -> count end}
|
|
datum_click={
|
|
fn {genre, _count} ->
|
|
JS.navigate(~p"/collection?#{%{query: ~s(genre:"#{genre}")}}")
|
|
end
|
|
}
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Layouts.app>
|