+
);
}
function buildCrumbs(
pathname: string,
+ search: string,
params: Record,
playlists: Playlist[],
-): { label: string; to?: string; current?: boolean }[] {
+): Crumb[] {
if (pathname === "/") return [{ label: "Home", current: true }];
if (pathname.startsWith("/library")) {
+ // On an album or artist page it's the way back to the list (and the
+ // search) the page was opened from, like Library's own back step.
+ const query = new URLSearchParams(search);
+ if (hasLibraryDrill(query)) {
+ const rest = withoutLibraryDrill(query).toString();
+ return [{ label: "Library", to: rest ? `/library?${rest}` : "/library", replace: true }];
+ }
return [{ label: "Library", current: true }];
}
if (pathname.startsWith("/favorites")) {
diff --git a/frontend/src/lib/libraryDrill.ts b/frontend/src/lib/libraryDrill.ts
new file mode 100644
index 00000000..2f705fef
--- /dev/null
+++ b/frontend/src/lib/libraryDrill.ts
@@ -0,0 +1,14 @@
+/** The search params that open an album or artist page over the library list. */
+const DRILL_PARAMS = ["album", "artist", "tidalAlbum", "tidalArtist"] as const;
+
+/** An empty value (`?album=`) opens nothing, as on the Library page. */
+export function hasLibraryDrill(params: URLSearchParams): boolean {
+ return DRILL_PARAMS.some((key) => Boolean(params.get(key)));
+}
+
+/** The library list (tab and search) the album or artist page was opened from. */
+export function withoutLibraryDrill(params: URLSearchParams): URLSearchParams {
+ const next = new URLSearchParams(params);
+ for (const key of [...DRILL_PARAMS, "artistName"]) next.delete(key);
+ return next;
+}
diff --git a/frontend/src/pages/Library.tsx b/frontend/src/pages/Library.tsx
index 7d94c511..c3f47cc1 100644
--- a/frontend/src/pages/Library.tsx
+++ b/frontend/src/pages/Library.tsx
@@ -13,6 +13,7 @@ import {
type TrackListItem,
} from "../api";
import { displayText } from "../lib/format";
+import { withoutLibraryDrill } from "../lib/libraryDrill";
import type { EntityTarget } from "@music-library/core/entity-target";
import TrackList from "../components/TrackList";
import CoverArt from "../components/CoverArt";
@@ -158,11 +159,7 @@ export default function Library() {
setParams(next);
};
- const clearDrill = () => {
- const next = new URLSearchParams(params);
- for (const key of ["album", "artist", "tidalAlbum", "tidalArtist", "artistName"]) next.delete(key);
- setParams(next, { replace: true });
- };
+ const clearDrill = () => setParams(withoutLibraryDrill(params), { replace: true });
if (albumID) {
return ;
@@ -173,14 +170,13 @@ export default function Library() {
key={tidalAlbumID}
id={tidalAlbumID}
artistId={tidalArtistID}
- onBack={clearDrill}
onOpenAlbum={openAlbum}
onOpenVersion={openVersion}
/>
);
}
if (tidalArtistID) {
- return ;
+ return ;
}
if (artistID) {
return ;
diff --git a/frontend/src/pages/library/ArtistDetail.tsx b/frontend/src/pages/library/ArtistDetail.tsx
index 9e10cc5a..30b6270b 100644
--- a/frontend/src/pages/library/ArtistDetail.tsx
+++ b/frontend/src/pages/library/ArtistDetail.tsx
@@ -1,6 +1,5 @@
import { useEffect, useMemo, useState, type ReactNode } from "react";
import {
- ArrowLeft as ArrowLeftIcon,
Shuffle as ArrowsRightLeftIcon,
Pause as PauseIcon,
Play as PlayIcon,
@@ -75,21 +74,13 @@ export function ArtistDetailView({
return ;
}
// A failed load leaves both artist and tracks empty; without this the page
- // would sit on the loading state with no way back. A failed refresh of a
- // cached artist keeps the page and shows the error in it.
+ // would sit on the loading state for good. The Library crumb is the way
+ // back. A failed refresh of a cached artist keeps the page and shows the
+ // error in it.
if (error && (!artist || !tracks)) {
return (
@@ -116,8 +105,6 @@ export function ArtistDetailView({
imageUrl={artistImageUrl(artist, tracks)}
kind="Artist"
meta={artistMetaParts(artist)}
- backLabel="Library"
- onBack={onBack}
/>
void;
onOpenAlbum: (target: EntityTarget) => void;
}) {
// Keyed on the id by the caller, so this only has to seed a revisit.
@@ -217,8 +202,6 @@ export function TidalArtistDetailView({
>
}
meta={data ? tidalArtistMetaParts(releases.length, tracks.length) : undefined}
- backLabel="Back to search"
- onBack={onBack}
/>
{(error || warning) && (
@@ -261,8 +244,6 @@ function ArtistHero({
imageUrl,
kind,
meta,
- backLabel,
- onBack,
}: {
/** Null while loading: a bar in place of the name. */
name: string | null;
@@ -270,23 +251,10 @@ function ArtistHero({
kind: ReactNode;
/** Undefined while loading; the row keeps its height either way. */
meta?: (string | false)[];
- backLabel: string;
- onBack: () => void;
}) {
const metaItems = meta?.filter((item): item is string => Boolean(item));
return (
-
- }
- >
- {backLabel}
-
-
{name === null ? (
// No initial to show yet; a "?" would flash before the real one.
diff --git a/frontend/src/pages/library/LibraryDetail.tsx b/frontend/src/pages/library/LibraryDetail.tsx
index 73a6f959..15dc19d7 100644
--- a/frontend/src/pages/library/LibraryDetail.tsx
+++ b/frontend/src/pages/library/LibraryDetail.tsx
@@ -89,13 +89,11 @@ export function AlbumDetailView({
return ;
}
if (!sleeve || !album || !tracks) {
- // A failed first load: the way back and the error, not a frame that
- // would never fill.
- if (error) return ;
+ // A failed first load: the error, not a frame that would never fill.
+ if (error) return ;
return (
setEditing(true) : undefined}
@@ -148,7 +145,6 @@ export function AlbumDetailView({
export function TidalAlbumDetailView({
id,
artistId,
- onBack,
onOpenAlbum,
onOpenVersion,
}: {
@@ -158,7 +154,6 @@ export function TidalAlbumDetailView({
* Without one, the album's own artist is asked once it loads.
*/
artistId: string | null;
- onBack: () => void;
onOpenAlbum: (target: EntityTarget) => void;
/**
* Swaps the page for another version of the release (a TIDAL album id),
@@ -248,8 +243,7 @@ export function TidalAlbumDetailView({
if (!album || !sleeve) {
if (error) {
return (
-
-
+
void }) {
+/** A failed first load. The Library crumb is the way back. */
+function DetailLoadError({ error }: { error: string }) {
return (
-