@behindthemusictree/app-kit-ui 0.0.0-stage → 24.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +46 -2
- package/dist/auth/index.cjs +58 -0
- package/dist/auth/index.cjs.map +1 -0
- package/dist/auth/index.d.ts +14 -0
- package/dist/auth/index.js +7 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/chunk-BPCFNBFO.js +2297 -0
- package/dist/chunk-BPCFNBFO.js.map +1 -0
- package/dist/chunk-MMJCB3CJ.js +32 -0
- package/dist/chunk-MMJCB3CJ.js.map +1 -0
- package/dist/chunk-VDXUW7PP.js +141 -0
- package/dist/chunk-VDXUW7PP.js.map +1 -0
- package/dist/chunk-ZLCRD7OK.js +274 -0
- package/dist/chunk-ZLCRD7OK.js.map +1 -0
- package/dist/genre-tree/index.cjs +2424 -0
- package/dist/genre-tree/index.cjs.map +1 -0
- package/dist/genre-tree/index.d.ts +246 -0
- package/dist/genre-tree/index.js +32 -0
- package/dist/genre-tree/index.js.map +1 -0
- package/dist/genre-tree/styles.css +1 -0
- package/dist/index.cjs +2730 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/popup/index.cjs +440 -0
- package/dist/popup/index.cjs.map +1 -0
- package/dist/popup/index.d.ts +103 -0
- package/dist/popup/index.js +25 -0
- package/dist/popup/index.js.map +1 -0
- package/package.json +87 -4
|
@@ -0,0 +1,2297 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BasePopup_default,
|
|
3
|
+
InternalErrorPopup
|
|
4
|
+
} from "./chunk-VDXUW7PP.js";
|
|
5
|
+
|
|
6
|
+
// src/genre-tree/GenreTreeView.tsx
|
|
7
|
+
import { useCallback as useCallback5, useEffect as useEffect4, useMemo as useMemo6, useRef as useRef4, useState as useState4 } from "react";
|
|
8
|
+
import { Plus as Plus2 } from "lucide-react";
|
|
9
|
+
import { IconTextButton, Button, SegmentedControl } from "@behindthemusictree/ui";
|
|
10
|
+
import {
|
|
11
|
+
GenreTreeWheelSkeleton,
|
|
12
|
+
GenreTreeViewSkeleton
|
|
13
|
+
} from "@behindthemusictree/genre-tree-view";
|
|
14
|
+
import {
|
|
15
|
+
useArtistSearch
|
|
16
|
+
} from "@behindthemusictree/app-kit/genre-tree";
|
|
17
|
+
import { useGenreTreeViewState, useArtist as useArtist2 } from "@behindthemusictree/app-kit/genre-tree";
|
|
18
|
+
import { usePlayer as usePlayer8 } from "@behindthemusictree/app-kit/player";
|
|
19
|
+
import { usePopup as usePopup4 } from "@behindthemusictree/app-kit/popup";
|
|
20
|
+
|
|
21
|
+
// src/genre-tree/GenreDetailExtras.tsx
|
|
22
|
+
import { Skeleton as Skeleton2 } from "@behindthemusictree/ui";
|
|
23
|
+
import { useFetchGenreOverview } from "@behindthemusictree/app-kit/genre-tree";
|
|
24
|
+
|
|
25
|
+
// src/genre-tree/ClampedText.tsx
|
|
26
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
27
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
28
|
+
function ClampedText({ children, className = "" }) {
|
|
29
|
+
const ref = useRef(null);
|
|
30
|
+
const [expanded, setExpanded] = useState(false);
|
|
31
|
+
const [overflows, setOverflows] = useState(false);
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
const element = ref.current;
|
|
34
|
+
if (!element || expanded) return;
|
|
35
|
+
setOverflows(element.scrollHeight > element.clientHeight);
|
|
36
|
+
}, [children, expanded]);
|
|
37
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
38
|
+
/* @__PURE__ */ jsx("p", { ref, className: `${className} ${expanded ? "" : "line-clamp-3"}`, children }),
|
|
39
|
+
(overflows || expanded) && /* @__PURE__ */ jsx(
|
|
40
|
+
"button",
|
|
41
|
+
{
|
|
42
|
+
type: "button",
|
|
43
|
+
"aria-expanded": expanded,
|
|
44
|
+
onClick: () => setExpanded((value) => !value),
|
|
45
|
+
className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
|
|
46
|
+
children: expanded ? "Show less" : "Show more"
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
] });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/genre-tree/GenrePlaylistTracks.tsx
|
|
53
|
+
import { useEffect, useRef as useRef2 } from "react";
|
|
54
|
+
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
55
|
+
import { z } from "zod";
|
|
56
|
+
import { FaPlay, FaPause } from "react-icons/fa";
|
|
57
|
+
import { RingLoader, Skeleton } from "@behindthemusictree/ui";
|
|
58
|
+
import { useSession } from "@behindthemusictree/app-kit/auth";
|
|
59
|
+
import { useFetchWrapper } from "@behindthemusictree/app-kit/transport";
|
|
60
|
+
import { PaginatedResponseSchema } from "@behindthemusictree/app-kit/transport";
|
|
61
|
+
import { parseWithLog } from "@behindthemusictree/app-kit/transport";
|
|
62
|
+
import { usePlayer } from "@behindthemusictree/app-kit/player";
|
|
63
|
+
import { PlayStates } from "@behindthemusictree/app-kit/player";
|
|
64
|
+
import { usePopup } from "@behindthemusictree/app-kit/popup";
|
|
65
|
+
|
|
66
|
+
// src/genre-tree/UnplayableReason.tsx
|
|
67
|
+
import { FaYoutube } from "react-icons/fa";
|
|
68
|
+
import { NO_VIDEO_LABEL } from "@behindthemusictree/app-kit/genre-tree";
|
|
69
|
+
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
70
|
+
function UnplayableReason({ reason }) {
|
|
71
|
+
if (reason !== NO_VIDEO_LABEL) return /* @__PURE__ */ jsx2(Fragment2, { children: reason });
|
|
72
|
+
return /* @__PURE__ */ jsxs2("span", { role: "img", "aria-label": reason, className: "relative inline-flex shrink-0 items-center", children: [
|
|
73
|
+
/* @__PURE__ */ jsx2(FaYoutube, { size: 16, "aria-hidden": "true" }),
|
|
74
|
+
/* @__PURE__ */ jsx2("span", { "aria-hidden": "true", className: "absolute left-1/2 top-1/2 h-0.5 w-[130%] -translate-x-1/2 -translate-y-1/2 -rotate-45 rounded bg-current" })
|
|
75
|
+
] });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/genre-tree/GenrePlaylistTracks.tsx
|
|
79
|
+
import { ErrorCode } from "@behindthemusictree/app-kit/transport";
|
|
80
|
+
import { genrePlaylistQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
|
|
81
|
+
import { libraryEndpoints, libraryQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
|
|
82
|
+
import { isSong, songUnplayableReason, SongDetailedSchema } from "@behindthemusictree/app-kit/genre-tree";
|
|
83
|
+
import { useTrackList } from "@behindthemusictree/app-kit/genre-tree";
|
|
84
|
+
|
|
85
|
+
// src/genre-tree/MusicBrainzRecordingLink.tsx
|
|
86
|
+
import { FaExternalLinkAlt } from "react-icons/fa";
|
|
87
|
+
import { musicBrainzRecordingUrl } from "@behindthemusictree/app-kit/genre-tree";
|
|
88
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
89
|
+
function MusicBrainzRecordingLink({ song }) {
|
|
90
|
+
const url = musicBrainzRecordingUrl(song);
|
|
91
|
+
if (!url) return null;
|
|
92
|
+
return /* @__PURE__ */ jsx3(
|
|
93
|
+
"a",
|
|
94
|
+
{
|
|
95
|
+
href: url,
|
|
96
|
+
target: "_blank",
|
|
97
|
+
rel: "noopener noreferrer",
|
|
98
|
+
"aria-label": `View ${song.title} on MusicBrainz`,
|
|
99
|
+
title: "View on MusicBrainz",
|
|
100
|
+
className: "inline-flex shrink-0 items-center opacity-50 hover:opacity-100 focus-visible:opacity-100",
|
|
101
|
+
onClick: (event) => event.stopPropagation(),
|
|
102
|
+
children: /* @__PURE__ */ jsx3(FaExternalLinkAlt, { size: 10 })
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/genre-tree/GenrePlaylistTracks.tsx
|
|
108
|
+
import { ArtistNames } from "@behindthemusictree/app-kit/genre-tree";
|
|
109
|
+
import { capitalize, useTrackNoun } from "@behindthemusictree/app-kit/track-noun";
|
|
110
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
111
|
+
var CATALOGUE_PAGE_SIZE = 100;
|
|
112
|
+
var SongPageSchema = PaginatedResponseSchema(SongDetailedSchema).extend({
|
|
113
|
+
overallTotal: z.number().nullable(),
|
|
114
|
+
page: z.number().nullable(),
|
|
115
|
+
totalPages: z.number().nullable()
|
|
116
|
+
});
|
|
117
|
+
var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
|
|
118
|
+
var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
|
|
119
|
+
var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
|
|
120
|
+
var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
|
|
121
|
+
var SEPARATOR = "[@container(min-width:40rem)]:hidden";
|
|
122
|
+
var nextCursor = (next) => {
|
|
123
|
+
const cursor = new URL(next).searchParams.get("cursor");
|
|
124
|
+
if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
|
|
125
|
+
return cursor;
|
|
126
|
+
};
|
|
127
|
+
function GenrePlaylistTracks({
|
|
128
|
+
genreUuid,
|
|
129
|
+
genrePlaylist,
|
|
130
|
+
scope,
|
|
131
|
+
getBackendBaseUrl,
|
|
132
|
+
renderArtistLink,
|
|
133
|
+
renderSongLeading,
|
|
134
|
+
renderSongActions,
|
|
135
|
+
renderTracksToolbar,
|
|
136
|
+
artist = null
|
|
137
|
+
}) {
|
|
138
|
+
const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = useTrackList();
|
|
139
|
+
const { session, sessionRestored } = useSession();
|
|
140
|
+
const { playerTrackObject, playState, handlePlayPauseAction } = usePlayer();
|
|
141
|
+
const { showPopup } = usePopup();
|
|
142
|
+
const trackNoun = useTrackNoun();
|
|
143
|
+
const { uuid } = genrePlaylist;
|
|
144
|
+
const { fetch } = useFetchWrapper(getBackendBaseUrl);
|
|
145
|
+
const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({
|
|
146
|
+
queryKey: scope === "me" ? genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
|
|
147
|
+
queryFn: async ({ pageParam }) => {
|
|
148
|
+
if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
|
|
149
|
+
const response = await fetch(
|
|
150
|
+
libraryEndpoints.reference.songs.list(),
|
|
151
|
+
true,
|
|
152
|
+
false,
|
|
153
|
+
{},
|
|
154
|
+
{ genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
|
|
155
|
+
);
|
|
156
|
+
const parsed = parseWithLog(SongPageSchema, response, "GenrePlaylistTracks");
|
|
157
|
+
return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
|
|
158
|
+
},
|
|
159
|
+
initialPageParam: scope === "me" ? 1 : "",
|
|
160
|
+
getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
|
|
161
|
+
enabled: scope !== "me" || sessionRestored && !!session?.accessToken
|
|
162
|
+
});
|
|
163
|
+
const sentinelRef = useRef2(null);
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
const sentinel = sentinelRef.current;
|
|
166
|
+
if (!hasNextPage || isFetchingNextPage || !sentinel) return;
|
|
167
|
+
const observer = new IntersectionObserver((entries) => {
|
|
168
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
169
|
+
void fetchNextPage();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
observer.observe(sentinel);
|
|
173
|
+
return () => observer.disconnect();
|
|
174
|
+
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
|
175
|
+
const pages = data?.pages ?? [];
|
|
176
|
+
const tracks = pages.flatMap((page) => page.tracks);
|
|
177
|
+
const total = pages[0]?.total ?? void 0;
|
|
178
|
+
const play = (startSongUuid) => {
|
|
179
|
+
const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
|
|
180
|
+
started.catch((error) => {
|
|
181
|
+
console.error("Failed to play genre playlist:", error);
|
|
182
|
+
showPopup(/* @__PURE__ */ jsx4(InternalErrorPopup, { errorCode: ErrorCode.CLIENT_UNKNOWN }));
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
return /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children", children: [
|
|
186
|
+
/* @__PURE__ */ jsxs3("span", { className: "gtv-info-panel-children-title", children: [
|
|
187
|
+
capitalize(trackNoun.plural),
|
|
188
|
+
total !== void 0 && ` (${total})`
|
|
189
|
+
] }),
|
|
190
|
+
renderTracksToolbar?.(),
|
|
191
|
+
isPending ? /* @__PURE__ */ jsx4(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs3("p", { role: "alert", children: [
|
|
192
|
+
"Couldn't load ",
|
|
193
|
+
trackNoun.plural,
|
|
194
|
+
".",
|
|
195
|
+
" ",
|
|
196
|
+
/* @__PURE__ */ jsx4("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
|
|
197
|
+
] }) : tracks.length === 0 ? /* @__PURE__ */ jsx4("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs3("ul", { className: "mt-1 max-h-64 overflow-y-auto text-[0.8125rem] leading-[1.45] [container-type:inline-size]", "aria-label": `${genrePlaylist.name} ${trackNoun.plural}`, children: [
|
|
198
|
+
tracks.map((track, index) => {
|
|
199
|
+
const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
|
|
200
|
+
const isCurrent = playerTrackObject?.track.id === track.uuid;
|
|
201
|
+
const isPlaying = isCurrent && playState === PlayStates.PLAYING;
|
|
202
|
+
const text = /* @__PURE__ */ jsxs3("span", { className: TEXT_CELL, children: [
|
|
203
|
+
/* @__PURE__ */ jsx4("span", { className: TITLE_CELL, children: track.title }),
|
|
204
|
+
track.artists?.length ? /* @__PURE__ */ jsx4("span", { className: SEPARATOR, children: " \u2014 " }) : null,
|
|
205
|
+
/* @__PURE__ */ jsx4("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx4(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
|
|
206
|
+
track.album && /* @__PURE__ */ jsx4("span", { className: ALBUM_CELL, children: track.album.name })
|
|
207
|
+
] });
|
|
208
|
+
const leading = renderSongLeading?.(track, index);
|
|
209
|
+
const actions = renderSongActions?.(track);
|
|
210
|
+
const unplayableReason = isSong(track) ? songUnplayableReason(track) : null;
|
|
211
|
+
if (unplayableReason) {
|
|
212
|
+
return /* @__PURE__ */ jsxs3("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", title: unplayableReason, children: [
|
|
213
|
+
leading,
|
|
214
|
+
/* @__PURE__ */ jsx4(
|
|
215
|
+
"button",
|
|
216
|
+
{
|
|
217
|
+
type: "button",
|
|
218
|
+
disabled: true,
|
|
219
|
+
"aria-disabled": "true",
|
|
220
|
+
"aria-label": `${label} \u2014 ${unplayableReason}`,
|
|
221
|
+
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
|
|
222
|
+
children: /* @__PURE__ */ jsx4(FaPlay, { size: 10 })
|
|
223
|
+
}
|
|
224
|
+
),
|
|
225
|
+
text,
|
|
226
|
+
isSong(track) && /* @__PURE__ */ jsx4(MusicBrainzRecordingLink, { song: track }),
|
|
227
|
+
/* @__PURE__ */ jsx4("span", { className: "shrink-0 text-xs", children: /* @__PURE__ */ jsx4(UnplayableReason, { reason: unplayableReason }) }),
|
|
228
|
+
actions
|
|
229
|
+
] }, track.uuid);
|
|
230
|
+
}
|
|
231
|
+
return /* @__PURE__ */ jsxs3(
|
|
232
|
+
"li",
|
|
233
|
+
{
|
|
234
|
+
className: `group flex min-h-8 items-center gap-1 rounded px-1 hover:bg-[var(--color-neutral-100)] ${isCurrent ? "font-medium text-[var(--color-primary)]" : ""}`,
|
|
235
|
+
children: [
|
|
236
|
+
leading,
|
|
237
|
+
/* @__PURE__ */ jsx4(
|
|
238
|
+
"button",
|
|
239
|
+
{
|
|
240
|
+
type: "button",
|
|
241
|
+
"aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
|
|
242
|
+
className: `flex h-6 w-6 shrink-0 items-center justify-center rounded-full cursor-pointer group-hover:opacity-100 focus-visible:opacity-100 ${isCurrent && playState !== PlayStates.STOPPED ? "opacity-100" : "opacity-0 [@media(pointer:coarse)]:opacity-100"}`,
|
|
243
|
+
onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
|
|
244
|
+
children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx4(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx4(FaPause, { size: 10 }) : /* @__PURE__ */ jsx4(FaPlay, { size: 10 })
|
|
245
|
+
}
|
|
246
|
+
),
|
|
247
|
+
text,
|
|
248
|
+
isSong(track) && /* @__PURE__ */ jsx4(MusicBrainzRecordingLink, { song: track }),
|
|
249
|
+
actions
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
track.uuid
|
|
253
|
+
);
|
|
254
|
+
}),
|
|
255
|
+
hasNextPage && /* @__PURE__ */ jsx4("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
|
|
256
|
+
] })
|
|
257
|
+
] });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/genre-tree/GenreDetailExtras.tsx
|
|
261
|
+
import { useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
|
|
262
|
+
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
263
|
+
function GenreDetailExtras({
|
|
264
|
+
genreUuid,
|
|
265
|
+
genrePlaylist,
|
|
266
|
+
scope,
|
|
267
|
+
getBackendBaseUrl,
|
|
268
|
+
criteriaOverviewSchema,
|
|
269
|
+
renderGenreDetailExtras,
|
|
270
|
+
renderArtistLink,
|
|
271
|
+
renderSummary,
|
|
272
|
+
renderEssentialTracks,
|
|
273
|
+
family,
|
|
274
|
+
...trackSlots
|
|
275
|
+
}) {
|
|
276
|
+
const trackNoun = useTrackNoun2();
|
|
277
|
+
const { data: overview, isPending } = useFetchGenreOverview(
|
|
278
|
+
genreUuid,
|
|
279
|
+
scope,
|
|
280
|
+
getBackendBaseUrl,
|
|
281
|
+
criteriaOverviewSchema
|
|
282
|
+
);
|
|
283
|
+
if (isPending) {
|
|
284
|
+
return /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
285
|
+
/* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
|
|
286
|
+
/* @__PURE__ */ jsx5("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
|
|
287
|
+
/* @__PURE__ */ jsx5(Skeleton2, { className: "h-4 w-3/4" })
|
|
288
|
+
] }),
|
|
289
|
+
family
|
|
290
|
+
] });
|
|
291
|
+
}
|
|
292
|
+
if (!overview) return /* @__PURE__ */ jsx5(Fragment3, { children: family });
|
|
293
|
+
const { summary, description, summarySourceUrl, essentialSongs } = overview;
|
|
294
|
+
return /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
295
|
+
description && /* @__PURE__ */ jsx5("div", { className: "px-3.5 py-2.5", children: /* @__PURE__ */ jsx5(ClampedText, { className: "text-[0.8125rem] leading-[1.45] text-[var(--color-text-muted)]", children: description }, genreUuid) }),
|
|
296
|
+
renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
|
|
297
|
+
/* @__PURE__ */ jsx5("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
|
|
298
|
+
/* @__PURE__ */ jsx5(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid),
|
|
299
|
+
summarySourceUrl && /* @__PURE__ */ jsx5(
|
|
300
|
+
"a",
|
|
301
|
+
{
|
|
302
|
+
href: summarySourceUrl,
|
|
303
|
+
target: "_blank",
|
|
304
|
+
rel: "noopener noreferrer",
|
|
305
|
+
className: "mt-1 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
|
|
306
|
+
children: "Source: Wikipedia"
|
|
307
|
+
}
|
|
308
|
+
)
|
|
309
|
+
] }),
|
|
310
|
+
renderSummary && summarySourceUrl && /* @__PURE__ */ jsx5(
|
|
311
|
+
"a",
|
|
312
|
+
{
|
|
313
|
+
href: summarySourceUrl,
|
|
314
|
+
target: "_blank",
|
|
315
|
+
rel: "noopener noreferrer",
|
|
316
|
+
className: "mx-3.5 mb-2.5 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
|
|
317
|
+
children: "Source: Wikipedia"
|
|
318
|
+
}
|
|
319
|
+
),
|
|
320
|
+
family,
|
|
321
|
+
renderGenreDetailExtras?.(overview),
|
|
322
|
+
renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
|
|
323
|
+
/* @__PURE__ */ jsxs4("span", { className: "gtv-info-panel-children-title", children: [
|
|
324
|
+
"Essential ",
|
|
325
|
+
trackNoun.plural
|
|
326
|
+
] }),
|
|
327
|
+
essentialSongs.length > 0 ? /* @__PURE__ */ jsx5("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx5("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx5("p", { className: "mt-1", children: "\u2014" })
|
|
328
|
+
] }),
|
|
329
|
+
/* @__PURE__ */ jsx5(
|
|
330
|
+
GenrePlaylistTracks,
|
|
331
|
+
{
|
|
332
|
+
genreUuid,
|
|
333
|
+
genrePlaylist,
|
|
334
|
+
scope,
|
|
335
|
+
getBackendBaseUrl,
|
|
336
|
+
renderArtistLink,
|
|
337
|
+
...trackSlots
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
] });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/genre-tree/playlist-tree/TreePerRoot.tsx
|
|
344
|
+
import { useCallback as useCallback2, useMemo as useMemo2 } from "react";
|
|
345
|
+
import {
|
|
346
|
+
GenreTree,
|
|
347
|
+
getGenreTreeColor
|
|
348
|
+
} from "@behindthemusictree/genre-tree-view";
|
|
349
|
+
import { usePlayer as usePlayer2 } from "@behindthemusictree/app-kit/player";
|
|
350
|
+
import { usePopup as usePopup2 } from "@behindthemusictree/app-kit/popup";
|
|
351
|
+
import { ErrorCode as ErrorCode2 } from "@behindthemusictree/app-kit/transport";
|
|
352
|
+
|
|
353
|
+
// src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
|
|
354
|
+
import { useCallback, useMemo } from "react";
|
|
355
|
+
import {
|
|
356
|
+
TrackListOriginType,
|
|
357
|
+
useTrackList as useTrackList2
|
|
358
|
+
} from "@behindthemusictree/app-kit/genre-tree";
|
|
359
|
+
function useGenrePlaylistPlayback({
|
|
360
|
+
scope,
|
|
361
|
+
genrePlaylists,
|
|
362
|
+
artist = null
|
|
363
|
+
}) {
|
|
364
|
+
const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = useTrackList2();
|
|
365
|
+
const origin = trackList?.origin;
|
|
366
|
+
const playingNodeId = useMemo(() => {
|
|
367
|
+
if (!origin) return null;
|
|
368
|
+
if (!artist) return origin.type === TrackListOriginType.GENRE_PLAYLIST ? origin.uuid : null;
|
|
369
|
+
if (origin.type !== TrackListOriginType.ARTIST || origin.uuid !== artist.uuid) return null;
|
|
370
|
+
const { genreUuid } = origin;
|
|
371
|
+
return genreUuid ? genrePlaylists.find((gp) => gp.criteria?.uuid === genreUuid)?.uuid ?? null : null;
|
|
372
|
+
}, [origin, artist, genrePlaylists]);
|
|
373
|
+
const play = useCallback(
|
|
374
|
+
async (genrePlaylist) => {
|
|
375
|
+
if (!artist) return playNewTrackListFromGenrePlaylist(genrePlaylist, scope);
|
|
376
|
+
if (!genrePlaylist.criteria) return;
|
|
377
|
+
return playNewTrackListFromArtist(artist, scope, { ordering: "title", genreUuid: genrePlaylist.criteria.uuid });
|
|
378
|
+
},
|
|
379
|
+
[artist, scope, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist]
|
|
380
|
+
);
|
|
381
|
+
return useMemo(() => ({ playingNodeId, play }), [playingNodeId, play]);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// src/genre-tree/playlist-tree/TreePerRoot.tsx
|
|
385
|
+
import { capitalize as capitalize2, useTrackNoun as useTrackNoun3 } from "@behindthemusictree/app-kit/track-noun";
|
|
386
|
+
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
387
|
+
function GenrePlaylistTreePerRoot({
|
|
388
|
+
scope,
|
|
389
|
+
className,
|
|
390
|
+
rootUuid,
|
|
391
|
+
genrePlaylistTreePerRoot,
|
|
392
|
+
handleGenreCreationAction,
|
|
393
|
+
handleGenreRenameAction,
|
|
394
|
+
additionalActions,
|
|
395
|
+
onNodeClick,
|
|
396
|
+
onNodeHover,
|
|
397
|
+
renderInfoPanelBody,
|
|
398
|
+
selectedNodeId,
|
|
399
|
+
onSelectedNodeChange,
|
|
400
|
+
hideInfoPanelClose,
|
|
401
|
+
hideInfoPanelTitle,
|
|
402
|
+
readOnly = false,
|
|
403
|
+
showToolbar,
|
|
404
|
+
wheelZoom,
|
|
405
|
+
controlsOrientation,
|
|
406
|
+
defaultViewport,
|
|
407
|
+
onViewportChange,
|
|
408
|
+
playback,
|
|
409
|
+
...infoPanelCuration
|
|
410
|
+
}) {
|
|
411
|
+
const { isPlaying, setIsPlaying } = usePlayer2();
|
|
412
|
+
const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
|
|
413
|
+
const { playingNodeId, play } = playback ?? defaultPlayback;
|
|
414
|
+
const trackNoun = useTrackNoun3();
|
|
415
|
+
const { showPopup } = usePopup2();
|
|
416
|
+
const nodes = useMemo2(
|
|
417
|
+
() => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
|
|
418
|
+
id: genrePlaylist.uuid,
|
|
419
|
+
parentId: genrePlaylist.parent?.uuid ?? null,
|
|
420
|
+
name: genrePlaylist.name,
|
|
421
|
+
itemCount: genrePlaylist.tracksCount,
|
|
422
|
+
actionable: Boolean(genrePlaylist.criteria),
|
|
423
|
+
side: genrePlaylist.criteria?.side ?? void 0,
|
|
424
|
+
wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
|
|
425
|
+
umbrella: genrePlaylist.isUmbrella
|
|
426
|
+
})),
|
|
427
|
+
[genrePlaylistTreePerRoot]
|
|
428
|
+
);
|
|
429
|
+
const handlePlayPause = useCallback2(
|
|
430
|
+
(nodeId) => {
|
|
431
|
+
const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === nodeId);
|
|
432
|
+
if (!genrePlaylist) return;
|
|
433
|
+
if (nodeId === playingNodeId) {
|
|
434
|
+
setIsPlaying(!isPlaying);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (genrePlaylist.tracksCount === 0) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
play(genrePlaylist).catch((error) => {
|
|
441
|
+
console.error("Failed to load genre playlist tracks:", error);
|
|
442
|
+
showPopup(/* @__PURE__ */ jsx6(InternalErrorPopup, { errorCode: ErrorCode2.CLIENT_UNKNOWN }));
|
|
443
|
+
});
|
|
444
|
+
},
|
|
445
|
+
[
|
|
446
|
+
genrePlaylistTreePerRoot,
|
|
447
|
+
playingNodeId,
|
|
448
|
+
isPlaying,
|
|
449
|
+
setIsPlaying,
|
|
450
|
+
play,
|
|
451
|
+
showPopup
|
|
452
|
+
]
|
|
453
|
+
);
|
|
454
|
+
const handleAddChild = useCallback2(
|
|
455
|
+
(parentId) => {
|
|
456
|
+
const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === parentId);
|
|
457
|
+
if (!genrePlaylist?.criteria) return;
|
|
458
|
+
handleGenreCreationAction(genrePlaylist.criteria);
|
|
459
|
+
},
|
|
460
|
+
[genrePlaylistTreePerRoot, handleGenreCreationAction]
|
|
461
|
+
);
|
|
462
|
+
const handleRenameRequest = useCallback2(
|
|
463
|
+
(node) => {
|
|
464
|
+
const genrePlaylist = genrePlaylistTreePerRoot.find((g) => g.uuid === node.id);
|
|
465
|
+
if (!genrePlaylist?.criteria) return;
|
|
466
|
+
handleGenreRenameAction(genrePlaylist.criteria);
|
|
467
|
+
},
|
|
468
|
+
[genrePlaylistTreePerRoot, handleGenreRenameAction]
|
|
469
|
+
);
|
|
470
|
+
const handleDeleteRequest = useCallback2((node) => {
|
|
471
|
+
if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
|
|
472
|
+
}
|
|
473
|
+
}, []);
|
|
474
|
+
return /* @__PURE__ */ jsx6(
|
|
475
|
+
GenreTree,
|
|
476
|
+
{
|
|
477
|
+
className,
|
|
478
|
+
nodes,
|
|
479
|
+
itemCountLabel: `${capitalize2(trackNoun.singular)} count`,
|
|
480
|
+
rootColor: getGenreTreeColor(rootUuid),
|
|
481
|
+
playingNodeId,
|
|
482
|
+
playState: isPlaying ? "playing" : "paused",
|
|
483
|
+
onPlayPause: handlePlayPause,
|
|
484
|
+
onAddChild: readOnly ? void 0 : handleAddChild,
|
|
485
|
+
onRenameRequest: readOnly ? void 0 : handleRenameRequest,
|
|
486
|
+
onDeleteRequest: readOnly ? void 0 : handleDeleteRequest,
|
|
487
|
+
additionalActions,
|
|
488
|
+
onNodeClick,
|
|
489
|
+
onNodeHover,
|
|
490
|
+
renderInfoPanelBody,
|
|
491
|
+
selectedNodeId,
|
|
492
|
+
onSelectedNodeChange,
|
|
493
|
+
hideInfoPanelClose,
|
|
494
|
+
hideInfoPanelTitle,
|
|
495
|
+
showToolbar,
|
|
496
|
+
wheelZoom,
|
|
497
|
+
controlsOrientation,
|
|
498
|
+
defaultViewport,
|
|
499
|
+
onViewportChange,
|
|
500
|
+
...infoPanelCuration,
|
|
501
|
+
readOnly
|
|
502
|
+
}
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/genre-tree/playlist-tree/TreeWheel.tsx
|
|
507
|
+
import { useCallback as useCallback3, useMemo as useMemo3 } from "react";
|
|
508
|
+
import {
|
|
509
|
+
GenreTreeWheel
|
|
510
|
+
} from "@behindthemusictree/genre-tree-view";
|
|
511
|
+
import { usePlayer as usePlayer3 } from "@behindthemusictree/app-kit/player";
|
|
512
|
+
import { usePopup as usePopup3 } from "@behindthemusictree/app-kit/popup";
|
|
513
|
+
import { ErrorCode as ErrorCode3 } from "@behindthemusictree/app-kit/transport";
|
|
514
|
+
import { capitalize as capitalize3, useTrackNoun as useTrackNoun4 } from "@behindthemusictree/app-kit/track-noun";
|
|
515
|
+
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
516
|
+
function GenrePlaylistTreeWheel({
|
|
517
|
+
scope,
|
|
518
|
+
className,
|
|
519
|
+
genrePlaylists,
|
|
520
|
+
handleGenreCreationAction,
|
|
521
|
+
handleGenreRenameAction,
|
|
522
|
+
additionalActions,
|
|
523
|
+
onNodeClick,
|
|
524
|
+
onNodeHover,
|
|
525
|
+
renderInfoPanelBody,
|
|
526
|
+
selectedNodeId,
|
|
527
|
+
onSelectedNodeChange,
|
|
528
|
+
hideInfoPanelClose,
|
|
529
|
+
hideInfoPanelTitle,
|
|
530
|
+
readOnly = false,
|
|
531
|
+
allowWheelRotation,
|
|
532
|
+
showToolbar,
|
|
533
|
+
controlsOrientation,
|
|
534
|
+
defaultViewport,
|
|
535
|
+
onViewportChange,
|
|
536
|
+
playback,
|
|
537
|
+
...infoPanelCuration
|
|
538
|
+
}) {
|
|
539
|
+
const { isPlaying, setIsPlaying } = usePlayer3();
|
|
540
|
+
const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
|
|
541
|
+
const { playingNodeId, play } = playback ?? defaultPlayback;
|
|
542
|
+
const trackNoun = useTrackNoun4();
|
|
543
|
+
const { showPopup } = usePopup3();
|
|
544
|
+
const nodes = useMemo3(
|
|
545
|
+
() => genrePlaylists.map((genrePlaylist) => ({
|
|
546
|
+
id: genrePlaylist.uuid,
|
|
547
|
+
parentId: genrePlaylist.parent?.uuid ?? null,
|
|
548
|
+
name: genrePlaylist.name,
|
|
549
|
+
itemCount: genrePlaylist.tracksCount,
|
|
550
|
+
actionable: Boolean(genrePlaylist.criteria),
|
|
551
|
+
side: genrePlaylist.criteria?.side ?? void 0,
|
|
552
|
+
wikidataId: genrePlaylist.criteria?.wikidataId ?? null
|
|
553
|
+
})),
|
|
554
|
+
[genrePlaylists]
|
|
555
|
+
);
|
|
556
|
+
const handlePlayPause = useCallback3(
|
|
557
|
+
(nodeId) => {
|
|
558
|
+
const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
|
|
559
|
+
if (!genrePlaylist) return;
|
|
560
|
+
if (nodeId === playingNodeId) {
|
|
561
|
+
setIsPlaying(!isPlaying);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (genrePlaylist.tracksCount === 0) {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
play(genrePlaylist).catch((error) => {
|
|
568
|
+
console.error("Failed to load genre playlist tracks:", error);
|
|
569
|
+
showPopup(/* @__PURE__ */ jsx7(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
|
|
570
|
+
});
|
|
571
|
+
},
|
|
572
|
+
[
|
|
573
|
+
genrePlaylists,
|
|
574
|
+
playingNodeId,
|
|
575
|
+
isPlaying,
|
|
576
|
+
setIsPlaying,
|
|
577
|
+
play,
|
|
578
|
+
showPopup
|
|
579
|
+
]
|
|
580
|
+
);
|
|
581
|
+
const handleAddChild = useCallback3(
|
|
582
|
+
(parentId) => {
|
|
583
|
+
const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
|
|
584
|
+
if (!genrePlaylist?.criteria) return;
|
|
585
|
+
handleGenreCreationAction(genrePlaylist.criteria);
|
|
586
|
+
},
|
|
587
|
+
[genrePlaylists, handleGenreCreationAction]
|
|
588
|
+
);
|
|
589
|
+
const handleRenameRequest = useCallback3(
|
|
590
|
+
(node) => {
|
|
591
|
+
const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
|
|
592
|
+
if (!genrePlaylist?.criteria) return;
|
|
593
|
+
handleGenreRenameAction(genrePlaylist.criteria);
|
|
594
|
+
},
|
|
595
|
+
[genrePlaylists, handleGenreRenameAction]
|
|
596
|
+
);
|
|
597
|
+
const handleDeleteRequest = useCallback3((node) => {
|
|
598
|
+
if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
|
|
599
|
+
}
|
|
600
|
+
}, []);
|
|
601
|
+
return /* @__PURE__ */ jsx7(
|
|
602
|
+
GenreTreeWheel,
|
|
603
|
+
{
|
|
604
|
+
className,
|
|
605
|
+
nodes,
|
|
606
|
+
itemCountLabel: `${capitalize3(trackNoun.singular)} count`,
|
|
607
|
+
playingNodeId,
|
|
608
|
+
playState: isPlaying ? "playing" : "paused",
|
|
609
|
+
onPlayPause: handlePlayPause,
|
|
610
|
+
onAddChild: readOnly ? void 0 : handleAddChild,
|
|
611
|
+
onRenameRequest: readOnly ? void 0 : handleRenameRequest,
|
|
612
|
+
onDeleteRequest: readOnly ? void 0 : handleDeleteRequest,
|
|
613
|
+
additionalActions,
|
|
614
|
+
onNodeClick,
|
|
615
|
+
onNodeHover,
|
|
616
|
+
renderInfoPanelBody,
|
|
617
|
+
selectedNodeId,
|
|
618
|
+
onSelectedNodeChange,
|
|
619
|
+
hideInfoPanelClose,
|
|
620
|
+
hideInfoPanelTitle,
|
|
621
|
+
allowWheelRotation,
|
|
622
|
+
showToolbar,
|
|
623
|
+
controlsOrientation,
|
|
624
|
+
defaultViewport,
|
|
625
|
+
onViewportChange,
|
|
626
|
+
...infoPanelCuration,
|
|
627
|
+
readOnly
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// src/genre-tree/playlist-tree/TreeWheelRadialPopCore.tsx
|
|
633
|
+
import { useCallback as useCallback4, useMemo as useMemo4 } from "react";
|
|
634
|
+
import {
|
|
635
|
+
GenreTreeOutline,
|
|
636
|
+
GenreTreeWheelRadialPopCore
|
|
637
|
+
} from "@behindthemusictree/genre-tree-view";
|
|
638
|
+
import { usePlayer as usePlayer4 } from "@behindthemusictree/app-kit/player";
|
|
639
|
+
import { capitalize as capitalize4, useTrackNoun as useTrackNoun5 } from "@behindthemusictree/app-kit/track-noun";
|
|
640
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
641
|
+
function GenrePlaylistTreeWheelRadialPopCore(props) {
|
|
642
|
+
const {
|
|
643
|
+
scope,
|
|
644
|
+
className,
|
|
645
|
+
genrePlaylists,
|
|
646
|
+
handleGenreCreationAction,
|
|
647
|
+
handleGenreRenameAction,
|
|
648
|
+
additionalActions,
|
|
649
|
+
onNodeClick,
|
|
650
|
+
onNodeHover,
|
|
651
|
+
renderInfoPanelBody,
|
|
652
|
+
selectedNodeId,
|
|
653
|
+
onSelectedNodeChange,
|
|
654
|
+
hideInfoPanelClose,
|
|
655
|
+
hideInfoPanelTitle,
|
|
656
|
+
readOnly = false,
|
|
657
|
+
allowWheelRotation,
|
|
658
|
+
showToolbar,
|
|
659
|
+
controlsOrientation,
|
|
660
|
+
outline = false,
|
|
661
|
+
defaultViewport,
|
|
662
|
+
onViewportChange,
|
|
663
|
+
playback
|
|
664
|
+
} = props;
|
|
665
|
+
const focus = props;
|
|
666
|
+
const infoPanelCuration = {
|
|
667
|
+
editMode: props.editMode,
|
|
668
|
+
onEditModeChange: props.onEditModeChange,
|
|
669
|
+
renderTitleEditor: props.renderTitleEditor,
|
|
670
|
+
renderParentsEditor: props.renderParentsEditor,
|
|
671
|
+
renderDangerZone: props.renderDangerZone,
|
|
672
|
+
expanded: props.expanded,
|
|
673
|
+
onExpandedChange: props.onExpandedChange,
|
|
674
|
+
collapsed: props.collapsed,
|
|
675
|
+
hideExpandButton: props.hideExpandButton,
|
|
676
|
+
overlayPanel: props.overlayPanel
|
|
677
|
+
};
|
|
678
|
+
const { isPlaying, setIsPlaying } = usePlayer4();
|
|
679
|
+
const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
|
|
680
|
+
const { playingNodeId, play } = playback ?? defaultPlayback;
|
|
681
|
+
const trackNoun = useTrackNoun5();
|
|
682
|
+
const nodes = useMemo4(
|
|
683
|
+
() => genrePlaylists.map((genrePlaylist) => ({
|
|
684
|
+
id: genrePlaylist.uuid,
|
|
685
|
+
parentId: genrePlaylist.parent?.uuid ?? null,
|
|
686
|
+
name: genrePlaylist.name,
|
|
687
|
+
itemCount: genrePlaylist.tracksCount,
|
|
688
|
+
actionable: Boolean(genrePlaylist.criteria),
|
|
689
|
+
detached: !genrePlaylist.criteria,
|
|
690
|
+
side: genrePlaylist.criteria?.side ?? void 0,
|
|
691
|
+
wikidataId: genrePlaylist.criteria?.wikidataId ?? null,
|
|
692
|
+
umbrella: genrePlaylist.isUmbrella
|
|
693
|
+
})),
|
|
694
|
+
[genrePlaylists]
|
|
695
|
+
);
|
|
696
|
+
const handlePlayPause = useCallback4(
|
|
697
|
+
(nodeId) => {
|
|
698
|
+
const genrePlaylist = genrePlaylists.find((g) => g.uuid === nodeId);
|
|
699
|
+
if (!genrePlaylist) return;
|
|
700
|
+
if (nodeId === playingNodeId) {
|
|
701
|
+
setIsPlaying(!isPlaying);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (genrePlaylist.tracksCount === 0) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
play(genrePlaylist).catch((error) => {
|
|
708
|
+
console.error("Failed to load genre playlist tracks:", error);
|
|
709
|
+
});
|
|
710
|
+
},
|
|
711
|
+
[genrePlaylists, playingNodeId, isPlaying, setIsPlaying, play]
|
|
712
|
+
);
|
|
713
|
+
const handleAddChild = useCallback4(
|
|
714
|
+
(parentId) => {
|
|
715
|
+
const genrePlaylist = genrePlaylists.find((g) => g.uuid === parentId);
|
|
716
|
+
if (!genrePlaylist?.criteria) return;
|
|
717
|
+
handleGenreCreationAction(genrePlaylist.criteria);
|
|
718
|
+
},
|
|
719
|
+
[genrePlaylists, handleGenreCreationAction]
|
|
720
|
+
);
|
|
721
|
+
const handleRenameRequest = useCallback4(
|
|
722
|
+
(node) => {
|
|
723
|
+
const genrePlaylist = genrePlaylists.find((g) => g.uuid === node.id);
|
|
724
|
+
if (!genrePlaylist?.criteria) return;
|
|
725
|
+
handleGenreRenameAction(genrePlaylist.criteria);
|
|
726
|
+
},
|
|
727
|
+
[genrePlaylists, handleGenreRenameAction]
|
|
728
|
+
);
|
|
729
|
+
const handleDeleteRequest = useCallback4((node) => {
|
|
730
|
+
if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
|
|
731
|
+
}
|
|
732
|
+
}, []);
|
|
733
|
+
const treeProps = {
|
|
734
|
+
className,
|
|
735
|
+
nodes,
|
|
736
|
+
itemCountLabel: `${capitalize4(trackNoun.singular)} count`,
|
|
737
|
+
playingNodeId,
|
|
738
|
+
playState: isPlaying ? "playing" : "paused",
|
|
739
|
+
onPlayPause: handlePlayPause,
|
|
740
|
+
onAddChild: readOnly ? void 0 : handleAddChild,
|
|
741
|
+
onRenameRequest: readOnly ? void 0 : handleRenameRequest,
|
|
742
|
+
onDeleteRequest: readOnly ? void 0 : handleDeleteRequest,
|
|
743
|
+
additionalActions,
|
|
744
|
+
onNodeClick,
|
|
745
|
+
onNodeHover,
|
|
746
|
+
renderInfoPanelBody,
|
|
747
|
+
selectedNodeId,
|
|
748
|
+
onSelectedNodeChange,
|
|
749
|
+
hideInfoPanelClose,
|
|
750
|
+
hideInfoPanelTitle,
|
|
751
|
+
showToolbar,
|
|
752
|
+
...infoPanelCuration,
|
|
753
|
+
readOnly
|
|
754
|
+
};
|
|
755
|
+
return outline ? /* @__PURE__ */ jsx8(GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ jsx8(
|
|
756
|
+
GenreTreeWheelRadialPopCore,
|
|
757
|
+
{
|
|
758
|
+
...treeProps,
|
|
759
|
+
allowWheelRotation,
|
|
760
|
+
controlsOrientation,
|
|
761
|
+
defaultViewport,
|
|
762
|
+
onViewportChange,
|
|
763
|
+
...focus.onFocusGenreChange ? { focusNodeId: focus.focusGenreUuid, onFocusChange: focus.onFocusGenreChange } : { focusNodeId: void 0, onFocusChange: void 0 }
|
|
764
|
+
}
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// src/genre-tree/GenreTreeWheelHandoff.tsx
|
|
769
|
+
import { useEffect as useEffect2, useRef as useRef3, useState as useState2 } from "react";
|
|
770
|
+
import { jsx as jsx9, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
771
|
+
function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
|
|
772
|
+
const [revealed, setRevealed] = useState2(false);
|
|
773
|
+
const onRevealedChangeRef = useRef3(onRevealedChange);
|
|
774
|
+
onRevealedChangeRef.current = onRevealedChange;
|
|
775
|
+
useEffect2(() => {
|
|
776
|
+
let innerFrame = 0;
|
|
777
|
+
const outerFrame = requestAnimationFrame(() => {
|
|
778
|
+
innerFrame = requestAnimationFrame(() => {
|
|
779
|
+
setRevealed(true);
|
|
780
|
+
onRevealedChangeRef.current?.(true);
|
|
781
|
+
});
|
|
782
|
+
});
|
|
783
|
+
return () => {
|
|
784
|
+
onRevealedChangeRef.current?.(false);
|
|
785
|
+
cancelAnimationFrame(outerFrame);
|
|
786
|
+
cancelAnimationFrame(innerFrame);
|
|
787
|
+
};
|
|
788
|
+
}, []);
|
|
789
|
+
return /* @__PURE__ */ jsxs5("div", { className: "relative w-full h-full", children: [
|
|
790
|
+
/* @__PURE__ */ jsx9(
|
|
791
|
+
"div",
|
|
792
|
+
{
|
|
793
|
+
className: "absolute inset-0 transition-opacity duration-150 ease-out",
|
|
794
|
+
style: { opacity: revealed ? 1 : 0, pointerEvents: revealed ? "auto" : "none" },
|
|
795
|
+
"aria-hidden": !revealed,
|
|
796
|
+
children
|
|
797
|
+
}
|
|
798
|
+
),
|
|
799
|
+
!revealed && /* @__PURE__ */ jsx9("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
|
|
800
|
+
] });
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// src/genre-tree/GenreSearch.tsx
|
|
804
|
+
import { ArrowLeft, Search, X } from "lucide-react";
|
|
805
|
+
import { Input } from "@behindthemusictree/ui";
|
|
806
|
+
import { useGenreSearch } from "@behindthemusictree/app-kit/genre-tree";
|
|
807
|
+
import { jsx as jsx10, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
808
|
+
function GenreSearch({
|
|
809
|
+
genrePlaylists,
|
|
810
|
+
onSelect,
|
|
811
|
+
selectedName = null,
|
|
812
|
+
onClear,
|
|
813
|
+
onPrevious,
|
|
814
|
+
placeholder,
|
|
815
|
+
onQueryChange,
|
|
816
|
+
extraResults
|
|
817
|
+
}) {
|
|
818
|
+
const { query, setQuery, results, canClear, clear, select } = useGenreSearch({
|
|
819
|
+
genrePlaylists,
|
|
820
|
+
onSelect,
|
|
821
|
+
selectedName,
|
|
822
|
+
onClear
|
|
823
|
+
});
|
|
824
|
+
const extraItems = query !== "" && query !== selectedName ? extraResults?.items ?? [] : [];
|
|
825
|
+
const label = extraResults ? "Search" : "Search a genre";
|
|
826
|
+
const showList = results.length > 0 || extraItems.length > 0;
|
|
827
|
+
const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
|
|
828
|
+
return /* @__PURE__ */ jsxs6("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0", children: [
|
|
829
|
+
onPrevious && /* @__PURE__ */ jsx10(
|
|
830
|
+
"button",
|
|
831
|
+
{
|
|
832
|
+
type: "button",
|
|
833
|
+
"aria-label": "Previous",
|
|
834
|
+
title: "Previous",
|
|
835
|
+
className: "genre-search-previous absolute left-2 top-1/2 z-10 hidden p-1.5 text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
|
|
836
|
+
onClick: onPrevious,
|
|
837
|
+
children: /* @__PURE__ */ jsx10(ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
|
|
838
|
+
}
|
|
839
|
+
),
|
|
840
|
+
/* @__PURE__ */ jsx10(
|
|
841
|
+
Input,
|
|
842
|
+
{
|
|
843
|
+
type: "text",
|
|
844
|
+
value: query,
|
|
845
|
+
onChange: (event) => {
|
|
846
|
+
setQuery(event.target.value);
|
|
847
|
+
onQueryChange?.(event.target.value);
|
|
848
|
+
},
|
|
849
|
+
placeholder: placeholder ?? `${label}\u2026`,
|
|
850
|
+
"aria-label": label,
|
|
851
|
+
className: "h-[var(--layout-search-h,3rem)] pl-6 pr-20 text-base transition-[padding] duration-200 ease-out motion-reduce:transition-none bg-[var(--color-surface)] border-[var(--color-border)] rounded-full shadow-md"
|
|
852
|
+
}
|
|
853
|
+
),
|
|
854
|
+
/* @__PURE__ */ jsxs6("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
|
|
855
|
+
/* @__PURE__ */ jsx10("span", { className: "p-2 text-[var(--color-text-muted)] pointer-events-none", children: /* @__PURE__ */ jsx10(Search, { "aria-hidden": "true", className: "w-6 h-6" }) }),
|
|
856
|
+
canClear && /* @__PURE__ */ jsx10(
|
|
857
|
+
"button",
|
|
858
|
+
{
|
|
859
|
+
type: "button",
|
|
860
|
+
"aria-label": "Clear search",
|
|
861
|
+
className: "p-2 text-[var(--color-text-muted)] rounded-full hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
|
|
862
|
+
onClick: () => {
|
|
863
|
+
clear();
|
|
864
|
+
onQueryChange?.("");
|
|
865
|
+
},
|
|
866
|
+
children: /* @__PURE__ */ jsx10(X, { "aria-hidden": "true", className: "w-6 h-6" })
|
|
867
|
+
}
|
|
868
|
+
)
|
|
869
|
+
] }),
|
|
870
|
+
extraResults?.status && /* @__PURE__ */ jsx10("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
|
|
871
|
+
showList && /* @__PURE__ */ jsxs6("ul", { className: "genre-search-results absolute z-40 w-full max-h-64 mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
|
|
872
|
+
extraResults && results.length > 0 && /* @__PURE__ */ jsx10("li", { className: heading, children: "Genres" }),
|
|
873
|
+
results.map((genrePlaylist) => /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsxs6(
|
|
874
|
+
"button",
|
|
875
|
+
{
|
|
876
|
+
type: "button",
|
|
877
|
+
className: "w-full px-3 py-1.5 text-left hover:bg-[var(--color-surface-muted)]",
|
|
878
|
+
onClick: () => {
|
|
879
|
+
select(genrePlaylist);
|
|
880
|
+
onQueryChange?.("");
|
|
881
|
+
},
|
|
882
|
+
children: [
|
|
883
|
+
genrePlaylist.name,
|
|
884
|
+
genrePlaylist.isUmbrella && /* @__PURE__ */ jsx10("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
|
|
885
|
+
]
|
|
886
|
+
}
|
|
887
|
+
) }, genrePlaylist.uuid)),
|
|
888
|
+
extraResults && extraItems.length > 0 && /* @__PURE__ */ jsx10("li", { className: heading, children: extraResults.label }),
|
|
889
|
+
extraItems.map((item) => /* @__PURE__ */ jsx10("li", { children: /* @__PURE__ */ jsx10(
|
|
890
|
+
"button",
|
|
891
|
+
{
|
|
892
|
+
type: "button",
|
|
893
|
+
className: "w-full px-3 py-1.5 text-left hover:bg-[var(--color-surface-muted)]",
|
|
894
|
+
onClick: () => {
|
|
895
|
+
setQuery("");
|
|
896
|
+
onQueryChange?.("");
|
|
897
|
+
item.onSelect();
|
|
898
|
+
},
|
|
899
|
+
children: item.content
|
|
900
|
+
}
|
|
901
|
+
) }, item.key))
|
|
902
|
+
] })
|
|
903
|
+
] });
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// src/genre-tree/GenreActionBand.tsx
|
|
907
|
+
import { MoveUpRight, Pause, Pencil, Play, Plus, ZoomIn } from "lucide-react";
|
|
908
|
+
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
909
|
+
function GenreActionBand({
|
|
910
|
+
node,
|
|
911
|
+
onPlayPause,
|
|
912
|
+
isPlaying = false,
|
|
913
|
+
onZoomIn,
|
|
914
|
+
onAddChild,
|
|
915
|
+
onRename,
|
|
916
|
+
onChangeParent,
|
|
917
|
+
additionalActions
|
|
918
|
+
}) {
|
|
919
|
+
const items = [];
|
|
920
|
+
const adminItems = [];
|
|
921
|
+
if (onPlayPause) {
|
|
922
|
+
items.push({
|
|
923
|
+
key: "play-pause",
|
|
924
|
+
label: isPlaying ? "Pause" : "Play",
|
|
925
|
+
icon: isPlaying ? /* @__PURE__ */ jsx11(Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx11(Play, { "aria-hidden": "true", className: "w-5 h-5" }),
|
|
926
|
+
onClick: onPlayPause
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
if (onZoomIn) {
|
|
930
|
+
items.push({
|
|
931
|
+
key: "zoom-in",
|
|
932
|
+
label: "Focus",
|
|
933
|
+
icon: /* @__PURE__ */ jsx11(ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
|
|
934
|
+
onClick: onZoomIn
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
if (onAddChild) {
|
|
938
|
+
adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ jsx11(Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
|
|
939
|
+
}
|
|
940
|
+
if (onRename) {
|
|
941
|
+
adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ jsx11(Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
|
|
942
|
+
}
|
|
943
|
+
if (onChangeParent) {
|
|
944
|
+
adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ jsx11(MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
|
|
945
|
+
}
|
|
946
|
+
for (const action of additionalActions?.(node) ?? []) {
|
|
947
|
+
items.push({
|
|
948
|
+
key: action.key,
|
|
949
|
+
label: action.label(node),
|
|
950
|
+
icon: action.icon(node),
|
|
951
|
+
onClick: (event) => action.onClick(event, node),
|
|
952
|
+
disabled: action.enabled ? !action.enabled(node) : false
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
if (items.length === 0 && adminItems.length === 0) return null;
|
|
956
|
+
const renderItems = (list) => list.map((item) => /* @__PURE__ */ jsxs7(
|
|
957
|
+
"button",
|
|
958
|
+
{
|
|
959
|
+
type: "button",
|
|
960
|
+
disabled: item.disabled,
|
|
961
|
+
onClick: item.onClick,
|
|
962
|
+
className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none disabled:opacity-50",
|
|
963
|
+
children: [
|
|
964
|
+
/* @__PURE__ */ jsx11("span", { className: "flex items-center justify-center w-10 h-10 rounded-full group-focus-visible:ring-2 group-focus-visible:ring-[var(--color-focus-ring)] bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)] group-disabled:group-hover:bg-[var(--color-surface-muted)]", children: item.icon }),
|
|
965
|
+
item.label
|
|
966
|
+
]
|
|
967
|
+
},
|
|
968
|
+
item.key
|
|
969
|
+
));
|
|
970
|
+
return /* @__PURE__ */ jsxs7("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
|
|
971
|
+
items.length > 0 && /* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: renderItems(items) }),
|
|
972
|
+
adminItems.length > 0 && /* @__PURE__ */ jsxs7(
|
|
973
|
+
"section",
|
|
974
|
+
{
|
|
975
|
+
"aria-label": "Admin actions",
|
|
976
|
+
className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
|
|
977
|
+
children: [
|
|
978
|
+
/* @__PURE__ */ jsx11("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
|
|
979
|
+
/* @__PURE__ */ jsx11("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
|
|
980
|
+
]
|
|
981
|
+
}
|
|
982
|
+
)
|
|
983
|
+
] });
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// src/genre-tree/PanelEdgeTab.tsx
|
|
987
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
988
|
+
import { twMerge } from "tailwind-merge";
|
|
989
|
+
import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
990
|
+
var buttonClassName = "flex h-12 w-full items-center justify-center p-0 text-[var(--color-text-muted)] hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[var(--color-primary)]";
|
|
991
|
+
function PanelEdgeTab({ side, edge, center, controls, className }) {
|
|
992
|
+
if (!edge && !center) return null;
|
|
993
|
+
const isLeft = side === "left";
|
|
994
|
+
const EdgeIcon = isLeft ? ChevronLeft : ChevronRight;
|
|
995
|
+
const CenterIcon = isLeft ? ChevronRight : ChevronLeft;
|
|
996
|
+
const renderButton = (action, icon) => /* @__PURE__ */ jsx12(
|
|
997
|
+
"button",
|
|
998
|
+
{
|
|
999
|
+
type: "button",
|
|
1000
|
+
"aria-label": action.label,
|
|
1001
|
+
title: action.label,
|
|
1002
|
+
"aria-controls": controls,
|
|
1003
|
+
"aria-expanded": action.expanded,
|
|
1004
|
+
className: buttonClassName,
|
|
1005
|
+
onClick: action.onClick,
|
|
1006
|
+
children: icon
|
|
1007
|
+
}
|
|
1008
|
+
);
|
|
1009
|
+
const edgeButton = edge && renderButton(edge, /* @__PURE__ */ jsx12(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
|
|
1010
|
+
const centerButton = center && renderButton(center, /* @__PURE__ */ jsx12(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
|
|
1011
|
+
const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
|
|
1012
|
+
return /* @__PURE__ */ jsxs8(
|
|
1013
|
+
"div",
|
|
1014
|
+
{
|
|
1015
|
+
className: twMerge(
|
|
1016
|
+
"panel-edge-tab flex w-[var(--layout-edge-tab-w)] flex-col overflow-hidden bg-[var(--color-surface)] shadow-[0_1px_2px_rgba(60,64,67,0.3),0_2px_6px_2px_rgba(60,64,67,0.15)] [clip-path:inset(-0.5rem_-0.5rem_-0.5rem_0)]",
|
|
1017
|
+
isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
|
|
1018
|
+
className
|
|
1019
|
+
),
|
|
1020
|
+
children: [
|
|
1021
|
+
buttons[0],
|
|
1022
|
+
buttons[0] && buttons[1] && /* @__PURE__ */ jsx12("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
|
|
1023
|
+
buttons[1]
|
|
1024
|
+
]
|
|
1025
|
+
}
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/genre-tree/ChangeParentPopup.tsx
|
|
1030
|
+
import { useEffect as useEffect3, useMemo as useMemo5 } from "react";
|
|
1031
|
+
import {
|
|
1032
|
+
getReparentCandidates,
|
|
1033
|
+
useUpdateGenre
|
|
1034
|
+
} from "@behindthemusictree/app-kit/genre-tree";
|
|
1035
|
+
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1036
|
+
function ChangeParentPopup({
|
|
1037
|
+
genrePlaylist,
|
|
1038
|
+
genrePlaylists,
|
|
1039
|
+
scope,
|
|
1040
|
+
getBackendBaseUrl,
|
|
1041
|
+
onClose
|
|
1042
|
+
}) {
|
|
1043
|
+
const { mutate, isPending, formErrors } = useUpdateGenre(scope, getBackendBaseUrl);
|
|
1044
|
+
const candidates = useMemo5(
|
|
1045
|
+
() => getReparentCandidates(genrePlaylists, genrePlaylist.uuid),
|
|
1046
|
+
[genrePlaylists, genrePlaylist.uuid]
|
|
1047
|
+
);
|
|
1048
|
+
useEffect3(() => {
|
|
1049
|
+
const handleKeyDown = (event) => {
|
|
1050
|
+
if (event.key === "Escape") onClose();
|
|
1051
|
+
};
|
|
1052
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
1053
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1054
|
+
}, [onClose]);
|
|
1055
|
+
return /* @__PURE__ */ jsxs9(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
|
|
1056
|
+
/* @__PURE__ */ jsxs9("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
|
|
1057
|
+
"Current parent: ",
|
|
1058
|
+
genrePlaylist.parent?.name ?? "none"
|
|
1059
|
+
] }),
|
|
1060
|
+
/* @__PURE__ */ jsx13("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx13(
|
|
1061
|
+
GenreSearch,
|
|
1062
|
+
{
|
|
1063
|
+
genrePlaylists: candidates,
|
|
1064
|
+
onSelect: (newParent) => mutate({ uuid: genrePlaylist.criteria.uuid, data: { parent: newParent.criteria.uuid } }, { onSuccess: onClose }),
|
|
1065
|
+
placeholder: "Search a new parent\u2026"
|
|
1066
|
+
}
|
|
1067
|
+
) }),
|
|
1068
|
+
formErrors.length > 0 && /* @__PURE__ */ jsx13("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
|
|
1069
|
+
] });
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/genre-tree/artist/useArtistPanel.tsx
|
|
1073
|
+
import { Skeleton as Skeleton4 } from "@behindthemusictree/ui";
|
|
1074
|
+
import { useArtist } from "@behindthemusictree/app-kit/genre-tree";
|
|
1075
|
+
|
|
1076
|
+
// src/genre-tree/artist/ArtistHeader.tsx
|
|
1077
|
+
import { FieldList } from "@behindthemusictree/ui";
|
|
1078
|
+
|
|
1079
|
+
// src/genre-tree/artist/ArtistLinks.tsx
|
|
1080
|
+
import { Globe, Share2 } from "lucide-react";
|
|
1081
|
+
import {
|
|
1082
|
+
SiBandcamp,
|
|
1083
|
+
SiDiscogs,
|
|
1084
|
+
SiMusicbrainz,
|
|
1085
|
+
SiSoundcloud,
|
|
1086
|
+
SiSpotify,
|
|
1087
|
+
SiWikidata,
|
|
1088
|
+
SiWikipedia,
|
|
1089
|
+
SiYoutube
|
|
1090
|
+
} from "react-icons/si";
|
|
1091
|
+
import { Chip } from "@behindthemusictree/ui";
|
|
1092
|
+
import { jsx as jsx14 } from "react/jsx-runtime";
|
|
1093
|
+
var LINK_KINDS = {
|
|
1094
|
+
official: { label: "Official website", Icon: Globe },
|
|
1095
|
+
wikipedia: { label: "Wikipedia", Icon: SiWikipedia },
|
|
1096
|
+
wikidata: { label: "Wikidata", Icon: SiWikidata },
|
|
1097
|
+
spotify: { label: "Spotify", Icon: SiSpotify },
|
|
1098
|
+
youtube: { label: "YouTube", Icon: SiYoutube },
|
|
1099
|
+
bandcamp: { label: "Bandcamp", Icon: SiBandcamp },
|
|
1100
|
+
soundcloud: { label: "SoundCloud", Icon: SiSoundcloud },
|
|
1101
|
+
discogs: { label: "Discogs", Icon: SiDiscogs },
|
|
1102
|
+
social: { label: "Social", Icon: Share2 }
|
|
1103
|
+
};
|
|
1104
|
+
function socialLabel(url) {
|
|
1105
|
+
try {
|
|
1106
|
+
return new URL(url).hostname.replace(/^www\./, "");
|
|
1107
|
+
} catch {
|
|
1108
|
+
return LINK_KINDS.social.label;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function artistLinks(artist) {
|
|
1112
|
+
const links = artist.links.map(({ kind, url }) => ({
|
|
1113
|
+
key: `${kind}-${url}`,
|
|
1114
|
+
url,
|
|
1115
|
+
Icon: LINK_KINDS[kind].Icon,
|
|
1116
|
+
label: kind === "social" ? socialLabel(url) : LINK_KINDS[kind].label
|
|
1117
|
+
}));
|
|
1118
|
+
if (artist.musicbrainzArtistId) {
|
|
1119
|
+
links.push({
|
|
1120
|
+
key: "musicbrainz",
|
|
1121
|
+
url: `https://musicbrainz.org/artist/${artist.musicbrainzArtistId}`,
|
|
1122
|
+
Icon: SiMusicbrainz,
|
|
1123
|
+
label: "MusicBrainz"
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
return links;
|
|
1127
|
+
}
|
|
1128
|
+
function ArtistLinks({ links }) {
|
|
1129
|
+
return /* @__PURE__ */ jsx14("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx14("li", { children: /* @__PURE__ */ jsx14(Chip, { href: url, icon: /* @__PURE__ */ jsx14(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// src/genre-tree/artist/formatYears.ts
|
|
1133
|
+
function formatYears(beginYear, endYear, ended) {
|
|
1134
|
+
if (beginYear === null && endYear === null) return ended ? "ended" : null;
|
|
1135
|
+
const end = endYear ?? (ended ? "?" : "present");
|
|
1136
|
+
return `${beginYear ?? "?"}\u2013${end}`;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// src/genre-tree/artist/ArtistHeader.tsx
|
|
1140
|
+
import { jsx as jsx15 } from "react/jsx-runtime";
|
|
1141
|
+
var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
|
|
1142
|
+
function ArtistHeader({ artist }) {
|
|
1143
|
+
const fields = [];
|
|
1144
|
+
if (artist.type) fields.push({ label: "Type", value: artist.type.charAt(0).toUpperCase() + artist.type.slice(1) });
|
|
1145
|
+
const country = artist.country && countryNames.of(artist.country);
|
|
1146
|
+
if (country) fields.push({ label: "Country", value: country });
|
|
1147
|
+
const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
|
|
1148
|
+
if (years) fields.push({ label: "Active", value: years });
|
|
1149
|
+
const links = artistLinks(artist);
|
|
1150
|
+
if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx15(ArtistLinks, { links }) });
|
|
1151
|
+
if (fields.length === 0) return null;
|
|
1152
|
+
return /* @__PURE__ */ jsx15(FieldList, { items: fields });
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/genre-tree/artist/ArtistPhoto.tsx
|
|
1156
|
+
import { jsx as jsx16, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1157
|
+
function ExternalLink({ href, children }) {
|
|
1158
|
+
return /* @__PURE__ */ jsx16("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
|
|
1159
|
+
}
|
|
1160
|
+
function ArtistPhoto({ image, name }) {
|
|
1161
|
+
return /* @__PURE__ */ jsxs10("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
|
|
1162
|
+
/* @__PURE__ */ jsx16("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
|
|
1163
|
+
/* @__PURE__ */ jsxs10("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
|
|
1164
|
+
"Photo: ",
|
|
1165
|
+
/* @__PURE__ */ jsx16(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
|
|
1166
|
+
",",
|
|
1167
|
+
" ",
|
|
1168
|
+
image.licenseUrl ? /* @__PURE__ */ jsx16(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
|
|
1169
|
+
] })
|
|
1170
|
+
] });
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/genre-tree/artist/ArtistMembers.tsx
|
|
1174
|
+
import { PanelGroup, PanelSection } from "@behindthemusictree/ui";
|
|
1175
|
+
import { jsx as jsx17, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1176
|
+
function MembershipList({ title, memberships, renderArtistLink }) {
|
|
1177
|
+
if (memberships.length === 0) return null;
|
|
1178
|
+
return /* @__PURE__ */ jsx17(PanelGroup, { title, children: /* @__PURE__ */ jsx17("ul", { children: memberships.map((membership, index) => {
|
|
1179
|
+
const details = [
|
|
1180
|
+
formatYears(membership.beginYear, membership.endYear, membership.ended),
|
|
1181
|
+
...membership.roles
|
|
1182
|
+
];
|
|
1183
|
+
const detailsText = details.filter(Boolean).join(" \xB7 ");
|
|
1184
|
+
return (
|
|
1185
|
+
// A member can have several periods in the same group.
|
|
1186
|
+
/* @__PURE__ */ jsxs11("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
|
|
1187
|
+
/* @__PURE__ */ jsx17("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
|
|
1188
|
+
detailsText && /* @__PURE__ */ jsxs11("span", { className: "text-[var(--color-text-muted)]", children: [
|
|
1189
|
+
" \u2014 ",
|
|
1190
|
+
detailsText
|
|
1191
|
+
] })
|
|
1192
|
+
] }, `${membership.artist.uuid}-${index}`)
|
|
1193
|
+
);
|
|
1194
|
+
}) }) });
|
|
1195
|
+
}
|
|
1196
|
+
function ArtistMembers({ title, memberships, renderArtistLink }) {
|
|
1197
|
+
if (memberships.length === 0) return null;
|
|
1198
|
+
const isCurrent = (membership) => !membership.ended && membership.endYear === null;
|
|
1199
|
+
return /* @__PURE__ */ jsxs11(PanelSection, { title, children: [
|
|
1200
|
+
/* @__PURE__ */ jsx17(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
|
|
1201
|
+
/* @__PURE__ */ jsx17(
|
|
1202
|
+
MembershipList,
|
|
1203
|
+
{
|
|
1204
|
+
title: "Past",
|
|
1205
|
+
memberships: memberships.filter((membership) => !isCurrent(membership)),
|
|
1206
|
+
renderArtistLink
|
|
1207
|
+
}
|
|
1208
|
+
)
|
|
1209
|
+
] });
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
// src/genre-tree/artist/ArtistDiscography.tsx
|
|
1213
|
+
import { PanelGroup as PanelGroup2, PanelSection as PanelSection2 } from "@behindthemusictree/ui";
|
|
1214
|
+
import { jsx as jsx18, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1215
|
+
var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
|
|
1216
|
+
var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
|
|
1217
|
+
function discographyGroup({ primaryType, secondaryTypes }) {
|
|
1218
|
+
const secondary = secondaryTypes.map((type) => type.toLowerCase());
|
|
1219
|
+
if (secondary.includes("live")) return "Live";
|
|
1220
|
+
if (secondary.includes("compilation")) return "Compilations";
|
|
1221
|
+
return PRIMARY_GROUPS[primaryType?.toLowerCase() ?? ""] ?? "Other";
|
|
1222
|
+
}
|
|
1223
|
+
function byYear(a, b) {
|
|
1224
|
+
if (a.year !== b.year) {
|
|
1225
|
+
if (a.year === null) return 1;
|
|
1226
|
+
if (b.year === null) return -1;
|
|
1227
|
+
return a.year.localeCompare(b.year);
|
|
1228
|
+
}
|
|
1229
|
+
return a.name.localeCompare(b.name);
|
|
1230
|
+
}
|
|
1231
|
+
function ArtistDiscography({ albums }) {
|
|
1232
|
+
if (albums.length === 0) return null;
|
|
1233
|
+
const sorted = [...albums].sort(byYear);
|
|
1234
|
+
return /* @__PURE__ */ jsx18(PanelSection2, { title: "Discography", children: GROUPS.map((group) => {
|
|
1235
|
+
const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
|
|
1236
|
+
if (groupAlbums.length === 0) return null;
|
|
1237
|
+
return /* @__PURE__ */ jsx18(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx18("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs12("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
|
|
1238
|
+
/* @__PURE__ */ jsx18("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
|
|
1239
|
+
/* @__PURE__ */ jsx18("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
|
|
1240
|
+
] }, album.uuid)) }) }, group);
|
|
1241
|
+
}) });
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
// src/genre-tree/artist/ArtistSongs.tsx
|
|
1245
|
+
import { useState as useState3 } from "react";
|
|
1246
|
+
import { keepPreviousData } from "@tanstack/react-query";
|
|
1247
|
+
import { Play as Play2 } from "lucide-react";
|
|
1248
|
+
import { Pagination, PanelSection as PanelSection3, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
|
|
1249
|
+
import { usePlayer as usePlayer7 } from "@behindthemusictree/app-kit/player";
|
|
1250
|
+
import {
|
|
1251
|
+
SongDetailedSchema as SongDetailedSchema2,
|
|
1252
|
+
libraryEndpoints as libraryEndpoints2,
|
|
1253
|
+
songUnplayableReason as songUnplayableReason3,
|
|
1254
|
+
useTrackList as useTrackList4
|
|
1255
|
+
} from "@behindthemusictree/app-kit/genre-tree";
|
|
1256
|
+
import { PaginatedResponseSchema as PaginatedResponseSchema2, useFetchWrapper as useFetchWrapper2, useQueryWithParse } from "@behindthemusictree/app-kit/transport";
|
|
1257
|
+
|
|
1258
|
+
// src/genre-tree/track-list-sidebar/TrackItem.tsx
|
|
1259
|
+
import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
|
|
1260
|
+
import { useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre-tree";
|
|
1261
|
+
|
|
1262
|
+
// src/genre-tree/TrackPositionPlayPause.tsx
|
|
1263
|
+
import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
|
|
1264
|
+
import { usePlayer as usePlayer5 } from "@behindthemusictree/app-kit/player";
|
|
1265
|
+
import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
|
|
1266
|
+
import { RingLoader as RingLoader2 } from "@behindthemusictree/ui";
|
|
1267
|
+
import { jsx as jsx19, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1268
|
+
function TrackPositionPlayPause({ position, uuid, handlePlayPauseClick }) {
|
|
1269
|
+
const { playerTrackObject, playState } = usePlayer5();
|
|
1270
|
+
const isThisTrack = playerTrackObject?.track.id === uuid;
|
|
1271
|
+
return /* @__PURE__ */ jsxs13(
|
|
1272
|
+
"div",
|
|
1273
|
+
{
|
|
1274
|
+
className: "track-position-play-pause flex items-center justify-center text-lg w-16 cursor-pointer",
|
|
1275
|
+
style: { minWidth: "4rem", maxWidth: "4rem" },
|
|
1276
|
+
onClick: handlePlayPauseClick,
|
|
1277
|
+
children: [
|
|
1278
|
+
/* @__PURE__ */ jsx19("div", { className: "group-hover:hidden", children: /* @__PURE__ */ jsx19("div", { children: isThisTrack && playState !== PlayStates2.STOPPED ? /* @__PURE__ */ jsxs13("div", { className: "flex space-x-1 items-end", children: [
|
|
1279
|
+
/* @__PURE__ */ jsx19(
|
|
1280
|
+
"div",
|
|
1281
|
+
{
|
|
1282
|
+
className: `w-playingbar bg-[var(--color-green-500)] h-3 origin-bottom ${playState === PlayStates2.PLAYING ? "animate-scale-pulse" : ""}`
|
|
1283
|
+
}
|
|
1284
|
+
),
|
|
1285
|
+
/* @__PURE__ */ jsx19(
|
|
1286
|
+
"div",
|
|
1287
|
+
{
|
|
1288
|
+
className: `w-playingbar bg-[var(--color-green-500)] h-4 origin-bottom ${playState === PlayStates2.PLAYING ? "animate-scale-pulse delay-200" : ""}`,
|
|
1289
|
+
style: { animationDelay: "0.2s" }
|
|
1290
|
+
}
|
|
1291
|
+
),
|
|
1292
|
+
/* @__PURE__ */ jsx19(
|
|
1293
|
+
"div",
|
|
1294
|
+
{
|
|
1295
|
+
className: `w-playingbar bg-[var(--color-green-500)] h-3 origin-bottom ${playState === PlayStates2.PLAYING ? "animate-scale-pulse delay-400" : ""}`,
|
|
1296
|
+
style: { animationDelay: "0.3s" }
|
|
1297
|
+
}
|
|
1298
|
+
)
|
|
1299
|
+
] }) : position }) }),
|
|
1300
|
+
/* @__PURE__ */ jsx19("div", { className: "hidden group-hover:flex items-center justify-center", children: isThisTrack && playState === PlayStates2.LOADING ? /* @__PURE__ */ jsx19(RingLoader2, { size: 16 }) : isThisTrack && playState === PlayStates2.PLAYING ? /* @__PURE__ */ jsx19(FaPause2, {}) : /* @__PURE__ */ jsx19(FaPlay2, {}) })
|
|
1301
|
+
]
|
|
1302
|
+
}
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// src/genre-tree/track-list-sidebar/TrackItem.tsx
|
|
1307
|
+
import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
|
|
1308
|
+
import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
|
|
1309
|
+
import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1310
|
+
function TrackItem({
|
|
1311
|
+
track,
|
|
1312
|
+
position,
|
|
1313
|
+
renderDuration,
|
|
1314
|
+
renderActions,
|
|
1315
|
+
renderArtistLink
|
|
1316
|
+
}) {
|
|
1317
|
+
const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = usePlayer6();
|
|
1318
|
+
const { trackList, toTrackAtPosition } = useTrackList3();
|
|
1319
|
+
const scope = trackList?.origin?.scope ?? null;
|
|
1320
|
+
const unplayableReason = isSong2(track) ? songUnplayableReason2(track) : null;
|
|
1321
|
+
const handlePlayPauseClick = (event) => {
|
|
1322
|
+
event.stopPropagation();
|
|
1323
|
+
if (playerTrackObject && playerTrackObject.track.id === track.uuid) {
|
|
1324
|
+
handlePlayPauseAction();
|
|
1325
|
+
} else if (scope != null) {
|
|
1326
|
+
toTrackAtPosition(position);
|
|
1327
|
+
loadTrackForPlayer(track.uuid);
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
return /* @__PURE__ */ jsxs14(
|
|
1331
|
+
"div",
|
|
1332
|
+
{
|
|
1333
|
+
className: `track-item flex h-14 text-[var(--color-text)] ${unplayableReason ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
|
|
1334
|
+
title: unplayableReason ?? void 0,
|
|
1335
|
+
children: [
|
|
1336
|
+
unplayableReason ? /* @__PURE__ */ jsx20(
|
|
1337
|
+
"div",
|
|
1338
|
+
{
|
|
1339
|
+
className: "flex items-center justify-center text-lg w-16",
|
|
1340
|
+
style: { minWidth: "4rem", maxWidth: "4rem" },
|
|
1341
|
+
"aria-disabled": "true",
|
|
1342
|
+
children: position
|
|
1343
|
+
}
|
|
1344
|
+
) : /* @__PURE__ */ jsx20(TrackPositionPlayPause, { position, uuid: track.uuid, handlePlayPauseClick }),
|
|
1345
|
+
/* @__PURE__ */ jsxs14("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
|
|
1346
|
+
/* @__PURE__ */ jsxs14("div", { className: "flex max-w-full items-center gap-2", children: [
|
|
1347
|
+
/* @__PURE__ */ jsx20("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: track.title }),
|
|
1348
|
+
isSong2(track) && /* @__PURE__ */ jsx20(MusicBrainzRecordingLink, { song: track })
|
|
1349
|
+
] }),
|
|
1350
|
+
track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx20("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx20(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : "",
|
|
1351
|
+
unplayableReason && /* @__PURE__ */ jsx20("div", { className: "text-xs", children: /* @__PURE__ */ jsx20(UnplayableReason, { reason: unplayableReason }) })
|
|
1352
|
+
] }),
|
|
1353
|
+
/* @__PURE__ */ jsx20("div", { className: "album-name items-start justify-center w-1/3 min-w-0 ml-2 max-w-full truncate text-[var(--color-text-muted)]", children: track.album ? track.album.name : "" }),
|
|
1354
|
+
/* @__PURE__ */ jsx20("div", { className: "genre-name-container flex items-center justify-end w-1/6 min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx20("div", { className: "genre-name font-bold p-1 border border-[var(--color-text-muted)] text-xs max-w-full truncate", children: track.genre.name }) : "" }),
|
|
1355
|
+
renderDuration && /* @__PURE__ */ jsx20(
|
|
1356
|
+
"div",
|
|
1357
|
+
{
|
|
1358
|
+
className: "duration flex text-base w-16 items-center justify-center",
|
|
1359
|
+
style: { minWidth: "4rem", maxWidth: "4rem" },
|
|
1360
|
+
children: renderDuration(track)
|
|
1361
|
+
}
|
|
1362
|
+
),
|
|
1363
|
+
renderActions && /* @__PURE__ */ jsx20("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
|
|
1364
|
+
]
|
|
1365
|
+
}
|
|
1366
|
+
);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/genre-tree/artist/ArtistSongs.tsx
|
|
1370
|
+
import { Fragment as Fragment4, jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1371
|
+
var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
|
|
1372
|
+
var SORTS = [
|
|
1373
|
+
{ ordering: "title", label: "Title" },
|
|
1374
|
+
{ ordering: "-albumYear", label: "Year" }
|
|
1375
|
+
];
|
|
1376
|
+
function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
|
|
1377
|
+
const [page, setPage] = useState3(1);
|
|
1378
|
+
const [ordering, setOrdering] = useState3("title");
|
|
1379
|
+
const { fetch } = useFetchWrapper2(getBackendBaseUrl);
|
|
1380
|
+
const { playNewTrackListFromArtist } = useTrackList4();
|
|
1381
|
+
const { playerTrackObject } = usePlayer7();
|
|
1382
|
+
const { data, isPending, isError } = useQueryWithParse({
|
|
1383
|
+
queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
|
|
1384
|
+
queryFn: () => fetch(libraryEndpoints2.reference.songs.list(), true, false, void 0, {
|
|
1385
|
+
artist: artist.uuid,
|
|
1386
|
+
ordering,
|
|
1387
|
+
page
|
|
1388
|
+
}),
|
|
1389
|
+
schema: SongPageSchema2,
|
|
1390
|
+
placeholderData: keepPreviousData,
|
|
1391
|
+
context: "ArtistSongs"
|
|
1392
|
+
});
|
|
1393
|
+
const play = (startSongUuid) => playNewTrackListFromArtist({ uuid: artist.uuid, name: artist.name }, "reference", {
|
|
1394
|
+
ordering,
|
|
1395
|
+
startSongUuid
|
|
1396
|
+
});
|
|
1397
|
+
const handleRowClickCapture = (event, song) => {
|
|
1398
|
+
if (!(event.target instanceof Element) || !event.target.closest(".track-position-play-pause")) return;
|
|
1399
|
+
if (playerTrackObject?.track.id === song.uuid || songUnplayableReason3(song) !== null) return;
|
|
1400
|
+
event.stopPropagation();
|
|
1401
|
+
void play(song.uuid);
|
|
1402
|
+
};
|
|
1403
|
+
return /* @__PURE__ */ jsx21(PanelSection3, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx21(Skeleton3, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx21("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx21("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs15(Fragment4, { children: [
|
|
1404
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
|
|
1405
|
+
/* @__PURE__ */ jsxs15(
|
|
1406
|
+
"button",
|
|
1407
|
+
{
|
|
1408
|
+
type: "button",
|
|
1409
|
+
onClick: () => void play(),
|
|
1410
|
+
className: "flex w-fit items-center gap-1.5 px-3 py-1 text-xs font-medium bg-gray-900 text-white rounded-full hover:bg-gray-700",
|
|
1411
|
+
children: [
|
|
1412
|
+
/* @__PURE__ */ jsx21(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
|
|
1413
|
+
"Play all"
|
|
1414
|
+
]
|
|
1415
|
+
}
|
|
1416
|
+
),
|
|
1417
|
+
/* @__PURE__ */ jsx21("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx21(
|
|
1418
|
+
"button",
|
|
1419
|
+
{
|
|
1420
|
+
type: "button",
|
|
1421
|
+
"aria-pressed": ordering === sort.ordering,
|
|
1422
|
+
onClick: () => {
|
|
1423
|
+
setOrdering(sort.ordering);
|
|
1424
|
+
setPage(1);
|
|
1425
|
+
},
|
|
1426
|
+
className: "px-2.5 py-0.5 text-[var(--color-text)] rounded-full hover:bg-[var(--color-surface-muted)] aria-pressed:bg-gray-900 aria-pressed:text-white",
|
|
1427
|
+
children: sort.label
|
|
1428
|
+
},
|
|
1429
|
+
sort.ordering
|
|
1430
|
+
)) })
|
|
1431
|
+
] }),
|
|
1432
|
+
/* @__PURE__ */ jsx21("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx21("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx21(
|
|
1433
|
+
TrackItem,
|
|
1434
|
+
{
|
|
1435
|
+
track: song,
|
|
1436
|
+
position: (page - 1) * data.pageSize + index + 1,
|
|
1437
|
+
renderArtistLink
|
|
1438
|
+
}
|
|
1439
|
+
) }, song.uuid)) }),
|
|
1440
|
+
/* @__PURE__ */ jsx21(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
|
|
1441
|
+
] }) });
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// src/genre-tree/artist/useArtistPanel.tsx
|
|
1445
|
+
import { Fragment as Fragment5, jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1446
|
+
function useArtistPanel({
|
|
1447
|
+
uuid,
|
|
1448
|
+
getBackendBaseUrl,
|
|
1449
|
+
renderArtistLink,
|
|
1450
|
+
expanded,
|
|
1451
|
+
onExpandedChange,
|
|
1452
|
+
onClose
|
|
1453
|
+
}) {
|
|
1454
|
+
const result = useArtist(uuid, getBackendBaseUrl);
|
|
1455
|
+
const panel = { expanded, onExpandedChange, onClose };
|
|
1456
|
+
switch (result.status) {
|
|
1457
|
+
case "idle":
|
|
1458
|
+
return void 0;
|
|
1459
|
+
case "loading":
|
|
1460
|
+
return {
|
|
1461
|
+
...panel,
|
|
1462
|
+
title: "Artist",
|
|
1463
|
+
content: /* @__PURE__ */ jsxs16("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
|
|
1464
|
+
/* @__PURE__ */ jsx22(Skeleton4, { className: "h-3 w-16" }),
|
|
1465
|
+
/* @__PURE__ */ jsx22(Skeleton4, { className: "h-4 w-32" }),
|
|
1466
|
+
/* @__PURE__ */ jsx22(Skeleton4, { className: "h-3 w-16" }),
|
|
1467
|
+
/* @__PURE__ */ jsx22(Skeleton4, { className: "h-4 w-24" }),
|
|
1468
|
+
/* @__PURE__ */ jsx22(Skeleton4, { className: "h-16 mt-2.5" }),
|
|
1469
|
+
/* @__PURE__ */ jsx22(Skeleton4, { className: "h-16" })
|
|
1470
|
+
] })
|
|
1471
|
+
};
|
|
1472
|
+
case "not-found":
|
|
1473
|
+
return {
|
|
1474
|
+
...panel,
|
|
1475
|
+
title: "Artist not found",
|
|
1476
|
+
content: /* @__PURE__ */ jsx22("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
|
|
1477
|
+
};
|
|
1478
|
+
case "error":
|
|
1479
|
+
return {
|
|
1480
|
+
...panel,
|
|
1481
|
+
title: "Artist",
|
|
1482
|
+
content: /* @__PURE__ */ jsx22("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
|
|
1483
|
+
};
|
|
1484
|
+
case "success": {
|
|
1485
|
+
const { artist } = result;
|
|
1486
|
+
return {
|
|
1487
|
+
...panel,
|
|
1488
|
+
title: artist.name,
|
|
1489
|
+
content: /* @__PURE__ */ jsxs16(Fragment5, { children: [
|
|
1490
|
+
artist.image && /* @__PURE__ */ jsx22(ArtistPhoto, { image: artist.image, name: artist.name }),
|
|
1491
|
+
/* @__PURE__ */ jsx22(ArtistHeader, { artist }),
|
|
1492
|
+
/* @__PURE__ */ jsx22(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
|
|
1493
|
+
/* @__PURE__ */ jsx22(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
|
|
1494
|
+
artist.tracksCount > 0 && /* @__PURE__ */ jsx22(
|
|
1495
|
+
ArtistSongs,
|
|
1496
|
+
{
|
|
1497
|
+
artist: { uuid: artist.uuid, name: artist.name },
|
|
1498
|
+
getBackendBaseUrl,
|
|
1499
|
+
renderArtistLink
|
|
1500
|
+
}
|
|
1501
|
+
)
|
|
1502
|
+
] }),
|
|
1503
|
+
extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx22(ArtistDiscography, { albums: artist.albums }) : void 0
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
// src/genre-tree/GenreTreeView.tsx
|
|
1510
|
+
import { Fragment as Fragment6, jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
1511
|
+
var PREVIOUS_BUTTON_CSS = `
|
|
1512
|
+
.genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
|
|
1513
|
+
.genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
|
|
1514
|
+
.genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search-previous { display: flex; }
|
|
1515
|
+
@keyframes genre-tree-view-previous-in { from { opacity: 0; transform: translate(-0.5rem, -50%); } to { opacity: 1; transform: translate(0, -50%); } }
|
|
1516
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
1517
|
+
.genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
|
|
1518
|
+
}
|
|
1519
|
+
`;
|
|
1520
|
+
function GenreNodeActionBand({ genrePlaylist, scope, playback, ...bandProps }) {
|
|
1521
|
+
const { isPlaying, setIsPlaying } = usePlayer8();
|
|
1522
|
+
const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
|
|
1523
|
+
const handlePlayPause = () => {
|
|
1524
|
+
if (isCurrentTrackList) {
|
|
1525
|
+
setIsPlaying(!isPlaying);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
playback.play(genrePlaylist).catch((error) => {
|
|
1529
|
+
console.error("Failed to load genre playlist tracks:", error);
|
|
1530
|
+
});
|
|
1531
|
+
};
|
|
1532
|
+
return /* @__PURE__ */ jsx23(
|
|
1533
|
+
GenreActionBand,
|
|
1534
|
+
{
|
|
1535
|
+
...bandProps,
|
|
1536
|
+
onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
|
|
1537
|
+
isPlaying: isCurrentTrackList && isPlaying
|
|
1538
|
+
}
|
|
1539
|
+
);
|
|
1540
|
+
}
|
|
1541
|
+
var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
|
|
1542
|
+
var REGIONS_CLASS = "[--gtv-center-left:var(--layout-center-left)] [--gtv-center-right:var(--layout-center-right)] [--gtv-center-top:var(--layout-band-top)] [&_.gtv-info-panel--overlay]:[--gtv-info-panel-header-inset:0px]";
|
|
1543
|
+
function GenreTreeView({
|
|
1544
|
+
scope,
|
|
1545
|
+
handleGenreCreationAction,
|
|
1546
|
+
handleGenreRenameAction,
|
|
1547
|
+
getBackendBaseUrl,
|
|
1548
|
+
additionalActions,
|
|
1549
|
+
viewMode: controlledViewMode,
|
|
1550
|
+
selectedNodeId: controlledSelectedNodeId,
|
|
1551
|
+
onSelectedNodeIdChange,
|
|
1552
|
+
defaultViewport,
|
|
1553
|
+
onViewportChange,
|
|
1554
|
+
readOnly = false,
|
|
1555
|
+
className,
|
|
1556
|
+
showDisplayOptions = true,
|
|
1557
|
+
showAddRoot = true,
|
|
1558
|
+
allowWheelRotation: controlledAllowWheelRotation,
|
|
1559
|
+
criteriaOverviewSchema,
|
|
1560
|
+
renderGenreDetailExtras,
|
|
1561
|
+
controlsOrientation = "horizontal",
|
|
1562
|
+
renderArtistLink,
|
|
1563
|
+
artistUuid: controlledArtistUuid,
|
|
1564
|
+
onArtistUuidChange,
|
|
1565
|
+
artistExpanded: controlledArtistExpanded,
|
|
1566
|
+
onArtistExpandedChange,
|
|
1567
|
+
artistSearch = true,
|
|
1568
|
+
onArtistSelect,
|
|
1569
|
+
artistSearchBaseUrl,
|
|
1570
|
+
renderSummary,
|
|
1571
|
+
renderEssentialTracks,
|
|
1572
|
+
renderSongLeading,
|
|
1573
|
+
renderSongActions,
|
|
1574
|
+
renderTracksToolbar,
|
|
1575
|
+
editMode,
|
|
1576
|
+
onEditModeChange,
|
|
1577
|
+
renderTitleEditor,
|
|
1578
|
+
renderParentsEditor,
|
|
1579
|
+
renderDangerZone,
|
|
1580
|
+
expanded,
|
|
1581
|
+
onExpandedChange,
|
|
1582
|
+
collapsed,
|
|
1583
|
+
onCollapsedChange,
|
|
1584
|
+
overlayPanel,
|
|
1585
|
+
placement = "default",
|
|
1586
|
+
...focus
|
|
1587
|
+
}) {
|
|
1588
|
+
const [internalArtistUuid, setInternalArtistUuid] = useState4(null);
|
|
1589
|
+
const isArtistControlled = controlledArtistUuid !== void 0;
|
|
1590
|
+
const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
|
|
1591
|
+
const changeArtist = (uuid) => {
|
|
1592
|
+
if (!isArtistControlled) setInternalArtistUuid(uuid);
|
|
1593
|
+
onArtistUuidChange?.(uuid);
|
|
1594
|
+
};
|
|
1595
|
+
const {
|
|
1596
|
+
isLoading,
|
|
1597
|
+
genrePlaylistByNodeId,
|
|
1598
|
+
visibleGenrePlaylists,
|
|
1599
|
+
groupedGenrePlaylistsByRoot,
|
|
1600
|
+
selectedNodeId,
|
|
1601
|
+
selectedGenrePlaylist,
|
|
1602
|
+
selectNode,
|
|
1603
|
+
selectGenrePlaylist,
|
|
1604
|
+
clearSelection,
|
|
1605
|
+
viewMode,
|
|
1606
|
+
isViewModeControlled: isControlled,
|
|
1607
|
+
setViewMode,
|
|
1608
|
+
canShowPopCore,
|
|
1609
|
+
allowWheelRotation: internalAllowWheelRotation,
|
|
1610
|
+
toggleWheelRotation,
|
|
1611
|
+
onNodeHover: handleNodeHover
|
|
1612
|
+
} = useGenreTreeViewState({
|
|
1613
|
+
scope,
|
|
1614
|
+
getBackendBaseUrl,
|
|
1615
|
+
viewMode: controlledViewMode,
|
|
1616
|
+
selectedNodeId: controlledSelectedNodeId,
|
|
1617
|
+
onSelectedNodeIdChange,
|
|
1618
|
+
criteriaOverviewSchema,
|
|
1619
|
+
artistUuid
|
|
1620
|
+
});
|
|
1621
|
+
const artistResult = useArtist2(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
|
|
1622
|
+
const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
|
|
1623
|
+
const filterArtist = useMemo6(
|
|
1624
|
+
() => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
|
|
1625
|
+
[artistUuid, filterArtistName]
|
|
1626
|
+
);
|
|
1627
|
+
const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
|
|
1628
|
+
const openArtist = (uuid) => {
|
|
1629
|
+
if (selectedNodeId !== null) clearSelection();
|
|
1630
|
+
changeArtist(uuid);
|
|
1631
|
+
};
|
|
1632
|
+
const [expandedArtistUuid, setExpandedArtistUuid] = useState4(null);
|
|
1633
|
+
const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
|
|
1634
|
+
const artistPanel = useArtistPanel({
|
|
1635
|
+
uuid: artistUuid,
|
|
1636
|
+
getBackendBaseUrl: artistBaseUrl,
|
|
1637
|
+
renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx23("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
|
|
1638
|
+
expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
|
|
1639
|
+
onExpandedChange: (on) => {
|
|
1640
|
+
if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
|
|
1641
|
+
onArtistExpandedChange?.(on);
|
|
1642
|
+
},
|
|
1643
|
+
onClose: () => changeArtist(null)
|
|
1644
|
+
});
|
|
1645
|
+
const builtInArtistPanel = overlayPanel || selectedNodeId !== null ? void 0 : artistPanel;
|
|
1646
|
+
const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
|
|
1647
|
+
const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState4(false);
|
|
1648
|
+
const [uncontrolledExpanded, setUncontrolledExpanded] = useState4(false);
|
|
1649
|
+
const isCollapsed = collapsed ?? uncontrolledCollapsed;
|
|
1650
|
+
const isExpanded = expanded ?? uncontrolledExpanded;
|
|
1651
|
+
const changeCollapsed = (next) => {
|
|
1652
|
+
setUncontrolledCollapsed(next);
|
|
1653
|
+
onCollapsedChange?.(next);
|
|
1654
|
+
};
|
|
1655
|
+
const changeExpanded = (next) => {
|
|
1656
|
+
setUncontrolledExpanded(next);
|
|
1657
|
+
onExpandedChange?.(next);
|
|
1658
|
+
};
|
|
1659
|
+
useEffect4(() => {
|
|
1660
|
+
setUncontrolledCollapsed(false);
|
|
1661
|
+
}, [selectedNodeId, artistUuid]);
|
|
1662
|
+
const isDetailHidden = isCollapsed || !selectedNodeId && !activeOverlayPanel;
|
|
1663
|
+
useEffect4(() => {
|
|
1664
|
+
if (placement !== "regions" || !isDetailHidden) return void 0;
|
|
1665
|
+
const root = document.documentElement;
|
|
1666
|
+
root.style.setProperty("--layout-left-w", "0px");
|
|
1667
|
+
return () => {
|
|
1668
|
+
root.style.removeProperty("--layout-left-w");
|
|
1669
|
+
};
|
|
1670
|
+
}, [placement, isDetailHidden]);
|
|
1671
|
+
const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
|
|
1672
|
+
const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
|
|
1673
|
+
const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
|
|
1674
|
+
const infoPanelCuration = {
|
|
1675
|
+
editMode,
|
|
1676
|
+
onEditModeChange,
|
|
1677
|
+
renderTitleEditor,
|
|
1678
|
+
renderParentsEditor,
|
|
1679
|
+
renderDangerZone,
|
|
1680
|
+
expanded: placement === "regions" ? isExpanded : expanded,
|
|
1681
|
+
onExpandedChange: placement === "regions" ? changeExpanded : onExpandedChange,
|
|
1682
|
+
collapsed: placement === "regions" ? isCollapsed : void 0,
|
|
1683
|
+
hideExpandButton: placement === "regions" ? true : void 0,
|
|
1684
|
+
overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
|
|
1685
|
+
};
|
|
1686
|
+
const [isWheelRevealed, setIsWheelRevealed] = useState4(false);
|
|
1687
|
+
const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
|
|
1688
|
+
const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
|
|
1689
|
+
const rootRef = useRef4(null);
|
|
1690
|
+
const handlePrevious = () => {
|
|
1691
|
+
rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
|
|
1692
|
+
};
|
|
1693
|
+
const { showPopup, hidePopup } = usePopup4();
|
|
1694
|
+
const [searchText, setSearchText] = useState4("");
|
|
1695
|
+
const artistSearchResult = useArtistSearch(artistBaseUrl, artistSearch ? searchText : "");
|
|
1696
|
+
const artistExtraResults = artistSearch ? {
|
|
1697
|
+
label: "Artists",
|
|
1698
|
+
items: artistSearchResult.artists.map((artist) => ({
|
|
1699
|
+
key: artist.uuid,
|
|
1700
|
+
content: /* @__PURE__ */ jsxs17(Fragment6, { children: [
|
|
1701
|
+
/* @__PURE__ */ jsxs17("span", { className: "block truncate font-medium", children: [
|
|
1702
|
+
artist.name,
|
|
1703
|
+
artist.disambiguation && /* @__PURE__ */ jsxs17("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
|
|
1704
|
+
" \xB7 ",
|
|
1705
|
+
artist.disambiguation
|
|
1706
|
+
] })
|
|
1707
|
+
] }),
|
|
1708
|
+
artist.matchedAlias && /* @__PURE__ */ jsxs17("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
|
|
1709
|
+
"also known as ",
|
|
1710
|
+
artist.matchedAlias
|
|
1711
|
+
] })
|
|
1712
|
+
] }),
|
|
1713
|
+
onSelect: () => {
|
|
1714
|
+
openArtist(artist.uuid);
|
|
1715
|
+
onArtistSelect?.(artist);
|
|
1716
|
+
}
|
|
1717
|
+
})),
|
|
1718
|
+
status: artistSearchResult.isError ? "Search failed" : artistSearchResult.settled && artistSearchResult.artists.length === 0 ? "No artists found" : ""
|
|
1719
|
+
} : void 0;
|
|
1720
|
+
const handleSelectedNodeChange = (node) => {
|
|
1721
|
+
selectNode(node);
|
|
1722
|
+
};
|
|
1723
|
+
const handleGenreSearchClear = clearSelection;
|
|
1724
|
+
const handleGenreSearchSelect = (genrePlaylist) => {
|
|
1725
|
+
selectGenrePlaylist(genrePlaylist);
|
|
1726
|
+
};
|
|
1727
|
+
const selectedUuid = selectedGenrePlaylist?.uuid;
|
|
1728
|
+
const selectedChildren = selectedUuid ? visibleGenrePlaylists.filter((genrePlaylist) => genrePlaylist.parent?.uuid === selectedUuid) : [];
|
|
1729
|
+
const canZoomIn = viewMode === "pop-core" && focus.onFocusGenreChange !== void 0 && selectedUuid !== void 0 && focus.focusGenreUuid !== selectedUuid && selectedChildren.length > 0 && selectedChildren.filter((child) => child.criteria?.side === "pop").length <= 1;
|
|
1730
|
+
const handleGenreSearchZoomIn = () => {
|
|
1731
|
+
if (selectedUuid) focus.onFocusGenreChange?.(selectedUuid, "zoom-in");
|
|
1732
|
+
};
|
|
1733
|
+
const zoomInRef = useRef4(void 0);
|
|
1734
|
+
zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
|
|
1735
|
+
const renderInfoPanelBody = useCallback5(
|
|
1736
|
+
(node, { toolbar, ancestors, family }) => {
|
|
1737
|
+
const genrePlaylist = genrePlaylistByNodeId.get(node.id);
|
|
1738
|
+
const criteria = genrePlaylist?.criteria;
|
|
1739
|
+
if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ jsxs17(Fragment6, { children: [
|
|
1740
|
+
toolbar,
|
|
1741
|
+
family
|
|
1742
|
+
] }), aside: ancestors };
|
|
1743
|
+
const genreUuid = criteria.uuid;
|
|
1744
|
+
return {
|
|
1745
|
+
primary: /* @__PURE__ */ jsxs17(Fragment6, { children: [
|
|
1746
|
+
toolbar,
|
|
1747
|
+
/* @__PURE__ */ jsx23(
|
|
1748
|
+
GenreNodeActionBand,
|
|
1749
|
+
{
|
|
1750
|
+
node,
|
|
1751
|
+
genrePlaylist,
|
|
1752
|
+
scope,
|
|
1753
|
+
onZoomIn: zoomInRef.current,
|
|
1754
|
+
onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
|
|
1755
|
+
onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
|
|
1756
|
+
onChangeParent: readOnly ? void 0 : () => showPopup(
|
|
1757
|
+
/* @__PURE__ */ jsx23(
|
|
1758
|
+
ChangeParentPopup,
|
|
1759
|
+
{
|
|
1760
|
+
genrePlaylist: { ...genrePlaylist, criteria },
|
|
1761
|
+
genrePlaylists: visibleGenrePlaylists,
|
|
1762
|
+
scope,
|
|
1763
|
+
getBackendBaseUrl,
|
|
1764
|
+
onClose: hidePopup
|
|
1765
|
+
}
|
|
1766
|
+
)
|
|
1767
|
+
),
|
|
1768
|
+
additionalActions,
|
|
1769
|
+
playback
|
|
1770
|
+
}
|
|
1771
|
+
),
|
|
1772
|
+
/* @__PURE__ */ jsx23(
|
|
1773
|
+
GenreDetailExtras,
|
|
1774
|
+
{
|
|
1775
|
+
family,
|
|
1776
|
+
genreUuid,
|
|
1777
|
+
genrePlaylist,
|
|
1778
|
+
scope,
|
|
1779
|
+
getBackendBaseUrl,
|
|
1780
|
+
criteriaOverviewSchema,
|
|
1781
|
+
renderGenreDetailExtras,
|
|
1782
|
+
renderArtistLink,
|
|
1783
|
+
artist: filterArtist,
|
|
1784
|
+
renderSummary,
|
|
1785
|
+
renderEssentialTracks,
|
|
1786
|
+
renderSongLeading,
|
|
1787
|
+
renderSongActions,
|
|
1788
|
+
renderTracksToolbar
|
|
1789
|
+
},
|
|
1790
|
+
genreUuid
|
|
1791
|
+
)
|
|
1792
|
+
] }),
|
|
1793
|
+
aside: ancestors
|
|
1794
|
+
};
|
|
1795
|
+
},
|
|
1796
|
+
[
|
|
1797
|
+
genrePlaylistByNodeId,
|
|
1798
|
+
additionalActions,
|
|
1799
|
+
readOnly,
|
|
1800
|
+
handleGenreCreationAction,
|
|
1801
|
+
handleGenreRenameAction,
|
|
1802
|
+
showPopup,
|
|
1803
|
+
hidePopup,
|
|
1804
|
+
visibleGenrePlaylists,
|
|
1805
|
+
playback,
|
|
1806
|
+
filterArtist,
|
|
1807
|
+
scope,
|
|
1808
|
+
getBackendBaseUrl,
|
|
1809
|
+
criteriaOverviewSchema,
|
|
1810
|
+
renderGenreDetailExtras,
|
|
1811
|
+
renderArtistLink,
|
|
1812
|
+
renderSummary,
|
|
1813
|
+
renderEssentialTracks,
|
|
1814
|
+
renderSongLeading,
|
|
1815
|
+
renderSongActions,
|
|
1816
|
+
renderTracksToolbar
|
|
1817
|
+
]
|
|
1818
|
+
);
|
|
1819
|
+
const isRotationControlled = controlledAllowWheelRotation !== void 0;
|
|
1820
|
+
const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
|
|
1821
|
+
const detailTab = placement === "regions" && (selectedNodeId || detailOverlay) ? /* @__PURE__ */ jsx23(
|
|
1822
|
+
PanelEdgeTab,
|
|
1823
|
+
{
|
|
1824
|
+
side: "left",
|
|
1825
|
+
edge: detailExpanded ? { label: "Shrink panel", onClick: () => onDetailExpandedChange?.(false) } : isCollapsed ? null : { label: "Hide panel", onClick: () => changeCollapsed(true) },
|
|
1826
|
+
center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
|
|
1827
|
+
}
|
|
1828
|
+
) : null;
|
|
1829
|
+
const actions = /* @__PURE__ */ jsxs17(Fragment6, { children: [
|
|
1830
|
+
!isLoading && /* @__PURE__ */ jsx23(
|
|
1831
|
+
GenreSearch,
|
|
1832
|
+
{
|
|
1833
|
+
genrePlaylists: visibleGenrePlaylists,
|
|
1834
|
+
onSelect: handleGenreSearchSelect,
|
|
1835
|
+
selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
|
|
1836
|
+
onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
|
|
1837
|
+
onPrevious: handlePrevious,
|
|
1838
|
+
onQueryChange: artistSearch ? setSearchText : void 0,
|
|
1839
|
+
extraResults: artistExtraResults
|
|
1840
|
+
}
|
|
1841
|
+
),
|
|
1842
|
+
!isLoading && filterArtist && /* @__PURE__ */ jsxs17("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
|
|
1843
|
+
/* @__PURE__ */ jsx23(
|
|
1844
|
+
"button",
|
|
1845
|
+
{
|
|
1846
|
+
type: "button",
|
|
1847
|
+
className: "py-1 pl-3 pr-2 hover:underline",
|
|
1848
|
+
"aria-label": `Show artist ${filterArtist.name}`,
|
|
1849
|
+
onClick: () => {
|
|
1850
|
+
if (selectedNodeId !== null) clearSelection();
|
|
1851
|
+
},
|
|
1852
|
+
children: filterArtist.name
|
|
1853
|
+
}
|
|
1854
|
+
),
|
|
1855
|
+
/* @__PURE__ */ jsx23(
|
|
1856
|
+
"button",
|
|
1857
|
+
{
|
|
1858
|
+
type: "button",
|
|
1859
|
+
className: "py-1 pl-1 pr-3 hover:text-[var(--color-text)]",
|
|
1860
|
+
"aria-label": `Clear artist filter ${filterArtist.name}`,
|
|
1861
|
+
onClick: () => changeArtist(null),
|
|
1862
|
+
children: "\xD7"
|
|
1863
|
+
}
|
|
1864
|
+
)
|
|
1865
|
+
] }),
|
|
1866
|
+
!isLoading && !isControlled && /* @__PURE__ */ jsx23(
|
|
1867
|
+
SegmentedControl,
|
|
1868
|
+
{
|
|
1869
|
+
"aria-label": "Tree view mode",
|
|
1870
|
+
value: viewMode,
|
|
1871
|
+
onChange: setViewMode,
|
|
1872
|
+
options: [
|
|
1873
|
+
{ value: "pop-core", label: "Pop/Core", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE },
|
|
1874
|
+
{ value: "wheel", label: "Wheel" },
|
|
1875
|
+
{ value: "stacked", label: "Stacked" },
|
|
1876
|
+
{ value: "outline", label: "Outline", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE }
|
|
1877
|
+
]
|
|
1878
|
+
}
|
|
1879
|
+
),
|
|
1880
|
+
!isLoading && showDisplayOptions && /* @__PURE__ */ jsx23(
|
|
1881
|
+
"div",
|
|
1882
|
+
{
|
|
1883
|
+
className: "flex items-center gap-1",
|
|
1884
|
+
role: "group",
|
|
1885
|
+
"aria-label": "Tree display options",
|
|
1886
|
+
children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx23(
|
|
1887
|
+
Button,
|
|
1888
|
+
{
|
|
1889
|
+
variant: allowWheelRotation ? "default" : "outline",
|
|
1890
|
+
size: "sm",
|
|
1891
|
+
onClick: toggleWheelRotation,
|
|
1892
|
+
children: "Rotation"
|
|
1893
|
+
}
|
|
1894
|
+
)
|
|
1895
|
+
}
|
|
1896
|
+
),
|
|
1897
|
+
!isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx23(
|
|
1898
|
+
IconTextButton,
|
|
1899
|
+
{
|
|
1900
|
+
icon: Plus2,
|
|
1901
|
+
text: "Add root",
|
|
1902
|
+
onClick: () => handleGenreCreationAction(null)
|
|
1903
|
+
}
|
|
1904
|
+
)
|
|
1905
|
+
] });
|
|
1906
|
+
return /* @__PURE__ */ jsxs17(
|
|
1907
|
+
"div",
|
|
1908
|
+
{
|
|
1909
|
+
ref: rootRef,
|
|
1910
|
+
className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className].filter(Boolean).join(" "),
|
|
1911
|
+
children: [
|
|
1912
|
+
/* @__PURE__ */ jsx23("style", { children: PREVIOUS_BUTTON_CSS }),
|
|
1913
|
+
/* @__PURE__ */ jsx23(
|
|
1914
|
+
"div",
|
|
1915
|
+
{
|
|
1916
|
+
className: `actions-container absolute z-30 flex items-center gap-2 ${placement === "regions" ? "left-[calc(var(--layout-gutter)+var(--layout-panel-inset))] top-[calc(var(--layout-gutter)+var(--layout-panel-inset))]" : "left-5 top-5"}`,
|
|
1917
|
+
children: actions
|
|
1918
|
+
}
|
|
1919
|
+
),
|
|
1920
|
+
detailTab && /* @__PURE__ */ jsx23(
|
|
1921
|
+
"div",
|
|
1922
|
+
{
|
|
1923
|
+
className: `absolute top-1/2 z-30 -translate-y-1/2 transition-[opacity,visibility] duration-150 ease-out ${isTreeReady ? "opacity-100" : "pointer-events-none invisible opacity-0"} ${detailExpanded ? "right-[calc(var(--layout-center-right)-var(--layout-edge-tab-w))]" : isCollapsed ? "left-0" : "left-[calc(var(--layout-gutter)+var(--layout-left-w))]"}`,
|
|
1924
|
+
children: detailTab
|
|
1925
|
+
}
|
|
1926
|
+
),
|
|
1927
|
+
/* @__PURE__ */ jsx23("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsx23("div", { className: "tree-view-container flex-1 min-w-0 flex flex-col h-full", children: isLoading ? /* @__PURE__ */ jsx23(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
|
|
1928
|
+
// The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
|
|
1929
|
+
// and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
|
|
1930
|
+
// Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
|
|
1931
|
+
// The panel's own Zoom in button is hidden: the search bar carries it.
|
|
1932
|
+
/* @__PURE__ */ jsx23("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ jsx23(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx23(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx23(
|
|
1933
|
+
GenrePlaylistTreeWheel,
|
|
1934
|
+
{
|
|
1935
|
+
scope,
|
|
1936
|
+
genrePlaylists: visibleGenrePlaylists,
|
|
1937
|
+
handleGenreCreationAction,
|
|
1938
|
+
handleGenreRenameAction,
|
|
1939
|
+
additionalActions,
|
|
1940
|
+
onSelectedNodeChange: handleSelectedNodeChange,
|
|
1941
|
+
hideInfoPanelClose: true,
|
|
1942
|
+
hideInfoPanelTitle: true,
|
|
1943
|
+
onNodeHover: handleNodeHover,
|
|
1944
|
+
renderInfoPanelBody,
|
|
1945
|
+
playback,
|
|
1946
|
+
selectedNodeId,
|
|
1947
|
+
readOnly,
|
|
1948
|
+
allowWheelRotation,
|
|
1949
|
+
controlsOrientation,
|
|
1950
|
+
defaultViewport,
|
|
1951
|
+
onViewportChange,
|
|
1952
|
+
...infoPanelCuration
|
|
1953
|
+
}
|
|
1954
|
+
) }) })
|
|
1955
|
+
) : viewMode === "pop-core" ? (
|
|
1956
|
+
// Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
|
|
1957
|
+
/* @__PURE__ */ jsx23("div", { className: "tree-container search-overlay relative flex-1 w-full min-h-0 [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem))] [&_.gtv-info-panel-header]:pb-2 [&_.gtv-info-panel-title--hidden]:h-0 [&_.gtv-toolbar]:hidden [&_.gtv-info-panel-zoom-in]:hidden", children: /* @__PURE__ */ jsx23(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx23(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx23(
|
|
1958
|
+
GenrePlaylistTreeWheelRadialPopCore,
|
|
1959
|
+
{
|
|
1960
|
+
scope,
|
|
1961
|
+
genrePlaylists: visibleGenrePlaylists,
|
|
1962
|
+
handleGenreCreationAction,
|
|
1963
|
+
handleGenreRenameAction,
|
|
1964
|
+
additionalActions,
|
|
1965
|
+
onSelectedNodeChange: handleSelectedNodeChange,
|
|
1966
|
+
hideInfoPanelClose: true,
|
|
1967
|
+
hideInfoPanelTitle: true,
|
|
1968
|
+
onNodeHover: handleNodeHover,
|
|
1969
|
+
renderInfoPanelBody,
|
|
1970
|
+
playback,
|
|
1971
|
+
selectedNodeId,
|
|
1972
|
+
readOnly,
|
|
1973
|
+
allowWheelRotation,
|
|
1974
|
+
controlsOrientation,
|
|
1975
|
+
defaultViewport,
|
|
1976
|
+
onViewportChange,
|
|
1977
|
+
...infoPanelCuration,
|
|
1978
|
+
...focus
|
|
1979
|
+
}
|
|
1980
|
+
) }) })
|
|
1981
|
+
) : viewMode === "outline" ? /* @__PURE__ */ jsx23("div", { className: "tree-container flex-1 min-h-0 w-full overflow-y-auto relative [--gtv-info-panel-header-inset:calc(var(--layout-search-h,2.5rem)+var(--layout-panel-inset,0.5rem)+0.75rem)] [&_.gtv-toolbar]:hidden", children: /* @__PURE__ */ jsx23(
|
|
1982
|
+
GenrePlaylistTreeWheelRadialPopCore,
|
|
1983
|
+
{
|
|
1984
|
+
outline: true,
|
|
1985
|
+
scope,
|
|
1986
|
+
genrePlaylists: visibleGenrePlaylists,
|
|
1987
|
+
handleGenreCreationAction,
|
|
1988
|
+
handleGenreRenameAction,
|
|
1989
|
+
additionalActions,
|
|
1990
|
+
onSelectedNodeChange: handleSelectedNodeChange,
|
|
1991
|
+
hideInfoPanelClose: true,
|
|
1992
|
+
hideInfoPanelTitle: true,
|
|
1993
|
+
onNodeHover: handleNodeHover,
|
|
1994
|
+
renderInfoPanelBody,
|
|
1995
|
+
playback,
|
|
1996
|
+
selectedNodeId,
|
|
1997
|
+
readOnly,
|
|
1998
|
+
...infoPanelCuration
|
|
1999
|
+
}
|
|
2000
|
+
) }) : /* @__PURE__ */ jsx23("div", { className: "tree-container relative flex flex-col gap-4 w-full pt-14 overflow-x-auto overflow-y-auto text-[var(--color-text)] [&_.gtv-toolbar]:hidden", children: Object.entries(groupedGenrePlaylistsByRoot).map(
|
|
2001
|
+
([uuid, genrePlaylistTreePerRoot], rootIndex) => {
|
|
2002
|
+
return /* @__PURE__ */ jsx23(
|
|
2003
|
+
"div",
|
|
2004
|
+
{
|
|
2005
|
+
className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
|
|
2006
|
+
children: /* @__PURE__ */ jsx23("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx23(
|
|
2007
|
+
GenrePlaylistTreePerRoot,
|
|
2008
|
+
{
|
|
2009
|
+
scope,
|
|
2010
|
+
rootUuid: uuid,
|
|
2011
|
+
genrePlaylistTreePerRoot,
|
|
2012
|
+
handleGenreCreationAction,
|
|
2013
|
+
handleGenreRenameAction,
|
|
2014
|
+
additionalActions,
|
|
2015
|
+
onSelectedNodeChange: handleSelectedNodeChange,
|
|
2016
|
+
hideInfoPanelClose: true,
|
|
2017
|
+
hideInfoPanelTitle: true,
|
|
2018
|
+
onNodeHover: handleNodeHover,
|
|
2019
|
+
renderInfoPanelBody,
|
|
2020
|
+
playback,
|
|
2021
|
+
selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
|
|
2022
|
+
readOnly,
|
|
2023
|
+
wheelZoom: "modifier",
|
|
2024
|
+
controlsOrientation,
|
|
2025
|
+
defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
|
|
2026
|
+
onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
|
|
2027
|
+
...infoPanelCuration,
|
|
2028
|
+
overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
|
|
2029
|
+
}
|
|
2030
|
+
) })
|
|
2031
|
+
},
|
|
2032
|
+
uuid
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
) }) }) })
|
|
2036
|
+
]
|
|
2037
|
+
}
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// src/genre-tree/index.ts
|
|
2042
|
+
import {
|
|
2043
|
+
GenreTreeSkeleton,
|
|
2044
|
+
GenreTreeWheelSkeleton as GenreTreeWheelSkeleton2,
|
|
2045
|
+
GenreTreeViewSkeleton as GenreTreeViewSkeleton2,
|
|
2046
|
+
GenreTreeOutlineSkeleton
|
|
2047
|
+
} from "@behindthemusictree/genre-tree-view";
|
|
2048
|
+
|
|
2049
|
+
// src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
|
|
2050
|
+
import { useEffect as useEffect5, useRef as useRef5 } from "react";
|
|
2051
|
+
import { useTrackList as useTrackList5 } from "@behindthemusictree/app-kit/genre-tree";
|
|
2052
|
+
import { useTrackListSidebarVisibility } from "@behindthemusictree/app-kit/genre-tree";
|
|
2053
|
+
import { TrackListOriginType as TrackListOriginType2 } from "@behindthemusictree/app-kit/genre-tree";
|
|
2054
|
+
import { useTrackNoun as useTrackNoun6 } from "@behindthemusictree/app-kit/track-noun";
|
|
2055
|
+
import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
2056
|
+
function TrackListSidebar({
|
|
2057
|
+
className = "",
|
|
2058
|
+
renderDuration,
|
|
2059
|
+
renderActions,
|
|
2060
|
+
layout = "fixed",
|
|
2061
|
+
renderArtistLink
|
|
2062
|
+
}) {
|
|
2063
|
+
const { trackList, loadMore } = useTrackList5();
|
|
2064
|
+
const trackNoun = useTrackNoun6();
|
|
2065
|
+
const loadMoreSentinelRef = useRef5(null);
|
|
2066
|
+
const hasMore = !!trackList && trackList.nextPage !== null;
|
|
2067
|
+
useEffect5(() => {
|
|
2068
|
+
const sentinel = loadMoreSentinelRef.current;
|
|
2069
|
+
if (!hasMore || !sentinel) return;
|
|
2070
|
+
const observer = new IntersectionObserver((entries) => {
|
|
2071
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
2072
|
+
loadMore().catch((error) => console.error("Failed to load more tracks:", error));
|
|
2073
|
+
}
|
|
2074
|
+
});
|
|
2075
|
+
observer.observe(sentinel);
|
|
2076
|
+
return () => observer.disconnect();
|
|
2077
|
+
}, [hasMore, loadMore]);
|
|
2078
|
+
const { hideTrackListSidebar } = useTrackListSidebarVisibility();
|
|
2079
|
+
const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem]";
|
|
2080
|
+
return trackList ? /* @__PURE__ */ jsxs18(
|
|
2081
|
+
"div",
|
|
2082
|
+
{
|
|
2083
|
+
"data-theme": "dark",
|
|
2084
|
+
className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className}`,
|
|
2085
|
+
style: layout === "fixed" ? {
|
|
2086
|
+
bottom: "79px"
|
|
2087
|
+
/* Match player height exactly */
|
|
2088
|
+
} : void 0,
|
|
2089
|
+
children: [
|
|
2090
|
+
/* @__PURE__ */ jsxs18("div", { className: "header flex h-16 px-4 py-2 text-[var(--color-text)]", children: [
|
|
2091
|
+
/* @__PURE__ */ jsxs18("div", { className: "origin flex text-xl ", children: [
|
|
2092
|
+
/* @__PURE__ */ jsx24("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
|
|
2093
|
+
/* @__PURE__ */ jsx24("div", { className: "name-container flex flex-col justify-center items-center text-[var(--color-text)] font-bold pr-2 max-w-[27.5rem]", children: /* @__PURE__ */ jsx24("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
|
|
2094
|
+
] }),
|
|
2095
|
+
/* @__PURE__ */ jsxs18("div", { className: "info flex flex-col justify-center items-center pt-1 mr-2 text-[var(--color-text-muted)]", children: [
|
|
2096
|
+
trackList.origin.type === TrackListOriginType2.GENRE_PLAYLIST ? "\u2022 Genre playlist \u2022 " : trackList.origin.type === TrackListOriginType2.ARTIST ? "\u2022 Artist \u2022 " : `\u2022 ${trackNoun.singular} playlist \u2022 `,
|
|
2097
|
+
`${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular} \u2022`
|
|
2098
|
+
] }),
|
|
2099
|
+
/* @__PURE__ */ jsx24(
|
|
2100
|
+
"button",
|
|
2101
|
+
{
|
|
2102
|
+
type: "button",
|
|
2103
|
+
"aria-label": "Close track list",
|
|
2104
|
+
className: "flex-grow flex flex-col items-end justify-center h-full p-3 cursor-pointer text-inherit",
|
|
2105
|
+
onClick: hideTrackListSidebar,
|
|
2106
|
+
children: "\u2715"
|
|
2107
|
+
}
|
|
2108
|
+
)
|
|
2109
|
+
] }),
|
|
2110
|
+
/* @__PURE__ */ jsxs18(
|
|
2111
|
+
"ul",
|
|
2112
|
+
{
|
|
2113
|
+
className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list overflow-auto max-h-[calc(100vh-63.5px-79px-56px-3.5px)] list-none p-0 m-0",
|
|
2114
|
+
children: [
|
|
2115
|
+
trackList.tracks.map((track, index) => /* @__PURE__ */ jsx24("li", { children: /* @__PURE__ */ jsx24(
|
|
2116
|
+
TrackItem,
|
|
2117
|
+
{
|
|
2118
|
+
track,
|
|
2119
|
+
position: index + 1,
|
|
2120
|
+
renderDuration,
|
|
2121
|
+
renderActions,
|
|
2122
|
+
renderArtistLink
|
|
2123
|
+
}
|
|
2124
|
+
) }, track.uuid)),
|
|
2125
|
+
hasMore && /* @__PURE__ */ jsx24("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
|
|
2126
|
+
]
|
|
2127
|
+
}
|
|
2128
|
+
)
|
|
2129
|
+
]
|
|
2130
|
+
}
|
|
2131
|
+
) : null;
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
// src/genre-tree/PlayerPanel.tsx
|
|
2135
|
+
import { useState as useState5 } from "react";
|
|
2136
|
+
import { X as X2 } from "lucide-react";
|
|
2137
|
+
import { twMerge as twMerge2 } from "tailwind-merge";
|
|
2138
|
+
import {
|
|
2139
|
+
usePlayer as usePlayer9,
|
|
2140
|
+
PlayerVideoSurface
|
|
2141
|
+
} from "@behindthemusictree/app-kit/player";
|
|
2142
|
+
import {
|
|
2143
|
+
useTrackListSidebarVisibility as useTrackListSidebarVisibility2
|
|
2144
|
+
} from "@behindthemusictree/app-kit/genre-tree";
|
|
2145
|
+
import { jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
2146
|
+
var PANEL_ID = "player-panel";
|
|
2147
|
+
var iconButtonClassName = "flex h-6 w-6 shrink-0 items-center justify-center p-0 rounded-md hover:bg-white/10";
|
|
2148
|
+
var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
|
|
2149
|
+
function PlayerPanel({
|
|
2150
|
+
className,
|
|
2151
|
+
isVisible = true,
|
|
2152
|
+
onClose,
|
|
2153
|
+
expanded,
|
|
2154
|
+
onExpandedChange,
|
|
2155
|
+
extendedWidth = DEFAULT_EXTENDED_WIDTH,
|
|
2156
|
+
renderArtistLink
|
|
2157
|
+
}) {
|
|
2158
|
+
const { playerTrackObject } = usePlayer9();
|
|
2159
|
+
const { toggleTrackListSidebar, isTrackListSidebarVisible } = useTrackListSidebarVisibility2();
|
|
2160
|
+
const [uncontrolledExtended, setUncontrolledExtended] = useState5(false);
|
|
2161
|
+
const isExtendRequested = expanded ?? uncontrolledExtended;
|
|
2162
|
+
const setIsExtendRequested = (next) => {
|
|
2163
|
+
setUncontrolledExtended(next);
|
|
2164
|
+
onExpandedChange?.(next);
|
|
2165
|
+
};
|
|
2166
|
+
const isExtended = isExtendRequested && isTrackListSidebarVisible;
|
|
2167
|
+
const tabEdge = !isTrackListSidebarVisible ? null : isExtended ? {
|
|
2168
|
+
label: "Shrink song list panel",
|
|
2169
|
+
onClick: () => setIsExtendRequested(false)
|
|
2170
|
+
} : { label: "Hide song list", onClick: toggleTrackListSidebar };
|
|
2171
|
+
const tabCenter = !isTrackListSidebarVisible ? { label: "Show song list", onClick: toggleTrackListSidebar } : isExtended ? null : {
|
|
2172
|
+
label: "Extend song list panel",
|
|
2173
|
+
onClick: () => setIsExtendRequested(true)
|
|
2174
|
+
};
|
|
2175
|
+
return /* @__PURE__ */ jsxs19(
|
|
2176
|
+
"div",
|
|
2177
|
+
{
|
|
2178
|
+
id: PANEL_ID,
|
|
2179
|
+
className: twMerge2(
|
|
2180
|
+
"fixed right-[var(--layout-gutter)] top-[var(--layout-band-top)] z-40 max-h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
|
|
2181
|
+
!isExtended && "w-[var(--layout-right-w)]",
|
|
2182
|
+
isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
|
|
2183
|
+
!isVisible && "hidden",
|
|
2184
|
+
className
|
|
2185
|
+
),
|
|
2186
|
+
style: isExtended ? { width: extendedWidth } : void 0,
|
|
2187
|
+
children: [
|
|
2188
|
+
/* @__PURE__ */ jsx25(
|
|
2189
|
+
PanelEdgeTab,
|
|
2190
|
+
{
|
|
2191
|
+
side: "right",
|
|
2192
|
+
edge: tabEdge,
|
|
2193
|
+
center: tabCenter,
|
|
2194
|
+
controls: PANEL_ID,
|
|
2195
|
+
className: "absolute right-full top-1/2 -translate-y-1/2"
|
|
2196
|
+
}
|
|
2197
|
+
),
|
|
2198
|
+
/* @__PURE__ */ jsxs19(
|
|
2199
|
+
"div",
|
|
2200
|
+
{
|
|
2201
|
+
className: twMerge2(
|
|
2202
|
+
"flex h-full max-h-[inherit] overflow-hidden rounded-[var(--radius-panel,1rem)] bg-zinc-950 text-zinc-100 shadow-[0_1px_2px_rgba(0,0,0,0.5),0_2px_6px_2px_rgba(0,0,0,0.3)]",
|
|
2203
|
+
isExtended ? "flex-row" : "flex-col"
|
|
2204
|
+
),
|
|
2205
|
+
children: [
|
|
2206
|
+
/* @__PURE__ */ jsxs19("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
|
|
2207
|
+
/* @__PURE__ */ jsxs19("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: [
|
|
2208
|
+
/* @__PURE__ */ jsx25("span", { className: "min-w-0 truncate text-sm font-semibold", children: playerTrackObject?.track?.title }),
|
|
2209
|
+
/* @__PURE__ */ jsx25("div", { className: "flex shrink-0 items-center gap-1", children: /* @__PURE__ */ jsx25(
|
|
2210
|
+
"button",
|
|
2211
|
+
{
|
|
2212
|
+
type: "button",
|
|
2213
|
+
onClick: onClose,
|
|
2214
|
+
className: iconButtonClassName,
|
|
2215
|
+
"aria-label": "Close player",
|
|
2216
|
+
children: /* @__PURE__ */ jsx25(X2, { size: 16 })
|
|
2217
|
+
}
|
|
2218
|
+
) })
|
|
2219
|
+
] }),
|
|
2220
|
+
/* @__PURE__ */ jsxs19("div", { className: "relative w-full shrink-0", children: [
|
|
2221
|
+
/* @__PURE__ */ jsx25(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
|
|
2222
|
+
playerTrackObject?.loadError && /* @__PURE__ */ jsx25("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
|
|
2223
|
+
] })
|
|
2224
|
+
] }),
|
|
2225
|
+
isTrackListSidebarVisible && /* @__PURE__ */ jsx25(
|
|
2226
|
+
TrackListSidebar,
|
|
2227
|
+
{
|
|
2228
|
+
layout: "inline",
|
|
2229
|
+
className: twMerge2(
|
|
2230
|
+
"min-h-0 min-w-0 flex-1",
|
|
2231
|
+
isExtended && "border-l border-zinc-800"
|
|
2232
|
+
),
|
|
2233
|
+
renderArtistLink
|
|
2234
|
+
}
|
|
2235
|
+
)
|
|
2236
|
+
]
|
|
2237
|
+
}
|
|
2238
|
+
)
|
|
2239
|
+
]
|
|
2240
|
+
}
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// src/genre-tree/Rating.tsx
|
|
2245
|
+
import { v4 as uuidv4 } from "uuid";
|
|
2246
|
+
import { AiFillStar, AiOutlineStar } from "react-icons/ai";
|
|
2247
|
+
import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
|
|
2248
|
+
import { jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
2249
|
+
function Rating({ rating, handleChange }) {
|
|
2250
|
+
const instanceId = uuidv4();
|
|
2251
|
+
return /* @__PURE__ */ jsx26("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs20("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
|
|
2252
|
+
/* @__PURE__ */ jsx26(
|
|
2253
|
+
"input",
|
|
2254
|
+
{
|
|
2255
|
+
id: `star-${instanceId}-${i}`,
|
|
2256
|
+
className: "w-6 h-6 opacity-0",
|
|
2257
|
+
type: "radio",
|
|
2258
|
+
name: "rating",
|
|
2259
|
+
value: i === 0 ? FORM_RATING_NULL_VALUE : i,
|
|
2260
|
+
checked: rating === (i === 0 ? null : i),
|
|
2261
|
+
onChange: handleChange
|
|
2262
|
+
}
|
|
2263
|
+
),
|
|
2264
|
+
/* @__PURE__ */ jsx26("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs20("div", { className: "flex", children: [
|
|
2265
|
+
/* @__PURE__ */ jsx26(
|
|
2266
|
+
AiOutlineStar,
|
|
2267
|
+
{
|
|
2268
|
+
className: `w-7 h-7 absolute top-0 left-0 ${rating === FORM_RATING_NULL_VALUE ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
|
|
2269
|
+
}
|
|
2270
|
+
),
|
|
2271
|
+
/* @__PURE__ */ jsx26("span", { className: "flex-grow w-8" }),
|
|
2272
|
+
" "
|
|
2273
|
+
] }) : /* @__PURE__ */ jsx26(
|
|
2274
|
+
AiFillStar,
|
|
2275
|
+
{
|
|
2276
|
+
className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
|
|
2277
|
+
}
|
|
2278
|
+
) })
|
|
2279
|
+
] }, i)) });
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
export {
|
|
2283
|
+
GenrePlaylistTreePerRoot,
|
|
2284
|
+
GenreSearch,
|
|
2285
|
+
PanelEdgeTab,
|
|
2286
|
+
TrackPositionPlayPause,
|
|
2287
|
+
TrackItem,
|
|
2288
|
+
GenreTreeView,
|
|
2289
|
+
TrackListSidebar,
|
|
2290
|
+
PlayerPanel,
|
|
2291
|
+
Rating,
|
|
2292
|
+
GenreTreeSkeleton,
|
|
2293
|
+
GenreTreeWheelSkeleton2 as GenreTreeWheelSkeleton,
|
|
2294
|
+
GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
|
|
2295
|
+
GenreTreeOutlineSkeleton
|
|
2296
|
+
};
|
|
2297
|
+
//# sourceMappingURL=chunk-BPCFNBFO.js.map
|