@behindthemusictree/app-kit-ui 26.1.0 → 29.0.0
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/dist/chunk-DKQQLLMY.js +246 -0
- package/dist/chunk-DKQQLLMY.js.map +1 -0
- package/dist/{chunk-KPWV4LEJ.js → chunk-TFLX6C5Q.js} +1551 -863
- package/dist/chunk-TFLX6C5Q.js.map +1 -0
- package/dist/{chunk-ZLCRD7OK.js → chunk-XUC4UJDQ.js} +2 -2
- package/dist/genre-tree/index.cjs +1954 -1164
- package/dist/genre-tree/index.cjs.map +1 -1
- package/dist/genre-tree/index.d.ts +151 -17
- package/dist/genre-tree/index.js +14 -4
- package/dist/genre-tree/styles.css +80 -0
- package/dist/index.cjs +1855 -1065
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +18 -6
- package/dist/popup/index.cjs +179 -73
- package/dist/popup/index.cjs.map +1 -1
- package/dist/popup/index.d.ts +22 -2
- package/dist/popup/index.js +5 -3
- package/package.json +7 -7
- package/dist/chunk-KPWV4LEJ.js.map +0 -1
- package/dist/chunk-VDXUW7PP.js +0 -141
- package/dist/chunk-VDXUW7PP.js.map +0 -1
- /package/dist/{chunk-ZLCRD7OK.js.map → chunk-XUC4UJDQ.js.map} +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BasePopup_default,
|
|
3
|
-
InternalErrorPopup
|
|
4
|
-
|
|
3
|
+
InternalErrorPopup,
|
|
4
|
+
Toast
|
|
5
|
+
} from "./chunk-DKQQLLMY.js";
|
|
5
6
|
|
|
6
7
|
// src/genre-tree/GenreTreeView.tsx
|
|
7
|
-
import { useCallback as
|
|
8
|
+
import { useCallback as useCallback6, useDeferredValue, useEffect as useEffect6, useMemo as useMemo7, useRef as useRef4, useState as useState5 } from "react";
|
|
8
9
|
import { useIsMutating } from "@tanstack/react-query";
|
|
9
10
|
import { Plus as Plus2 } from "lucide-react";
|
|
10
11
|
import { IconTextButton, Button, SegmentedControl as SegmentedControl2 } from "@behindthemusictree/ui";
|
|
@@ -15,331 +16,8 @@ import {
|
|
|
15
16
|
import {
|
|
16
17
|
useArtistSearch
|
|
17
18
|
} from "@behindthemusictree/app-kit/genre-tree";
|
|
18
|
-
import { useGenreTreeViewState, useArtist as useArtist2, genreUpdateMutationKey } from "@behindthemusictree/app-kit/genre-tree";
|
|
19
|
-
import {
|
|
20
|
-
import { usePopup as usePopup4 } from "@behindthemusictree/app-kit/popup";
|
|
21
|
-
|
|
22
|
-
// src/genre-tree/GenreDetailExtras.tsx
|
|
23
|
-
import { Skeleton as Skeleton2 } from "@behindthemusictree/ui";
|
|
24
|
-
import { useFetchGenreOverview } from "@behindthemusictree/app-kit/genre-tree";
|
|
25
|
-
|
|
26
|
-
// src/genre-tree/ClampedText.tsx
|
|
27
|
-
import { useLayoutEffect, useRef, useState } from "react";
|
|
28
|
-
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
29
|
-
function ClampedText({ children, className = "" }) {
|
|
30
|
-
const ref = useRef(null);
|
|
31
|
-
const [expanded, setExpanded] = useState(false);
|
|
32
|
-
const [overflows, setOverflows] = useState(false);
|
|
33
|
-
useLayoutEffect(() => {
|
|
34
|
-
const element = ref.current;
|
|
35
|
-
if (!element || expanded) return;
|
|
36
|
-
setOverflows(element.scrollHeight > element.clientHeight);
|
|
37
|
-
}, [children, expanded]);
|
|
38
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
39
|
-
/* @__PURE__ */ jsx("p", { ref, className: `${className} ${expanded ? "" : "line-clamp-3"}`, children }),
|
|
40
|
-
(overflows || expanded) && /* @__PURE__ */ jsx(
|
|
41
|
-
"button",
|
|
42
|
-
{
|
|
43
|
-
type: "button",
|
|
44
|
-
"aria-expanded": expanded,
|
|
45
|
-
onClick: () => setExpanded((value) => !value),
|
|
46
|
-
className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
|
|
47
|
-
children: expanded ? "Show less" : "Show more"
|
|
48
|
-
}
|
|
49
|
-
)
|
|
50
|
-
] });
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// src/genre-tree/GenrePlaylistTracks.tsx
|
|
54
|
-
import { useEffect, useRef as useRef2 } from "react";
|
|
55
|
-
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
56
|
-
import { z } from "zod";
|
|
57
|
-
import { FaPlay, FaPause } from "react-icons/fa";
|
|
58
|
-
import { RingLoader, Skeleton } from "@behindthemusictree/ui";
|
|
59
|
-
import { useSession } from "@behindthemusictree/app-kit/auth";
|
|
60
|
-
import { useFetchWrapper } from "@behindthemusictree/app-kit/transport";
|
|
61
|
-
import { PaginatedResponseSchema } from "@behindthemusictree/app-kit/transport";
|
|
62
|
-
import { parseWithLog } from "@behindthemusictree/app-kit/transport";
|
|
63
|
-
import { usePlayer } from "@behindthemusictree/app-kit/player";
|
|
64
|
-
import { PlayStates } from "@behindthemusictree/app-kit/player";
|
|
65
|
-
import { usePopup } from "@behindthemusictree/app-kit/popup";
|
|
66
|
-
|
|
67
|
-
// src/genre-tree/UnplayableReason.tsx
|
|
68
|
-
import { FaYoutube } from "react-icons/fa";
|
|
69
|
-
import { NO_VIDEO_LABEL } from "@behindthemusictree/app-kit/genre-tree";
|
|
70
|
-
import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
71
|
-
function UnplayableReason({ reason }) {
|
|
72
|
-
if (reason !== NO_VIDEO_LABEL) return /* @__PURE__ */ jsx2(Fragment2, { children: reason });
|
|
73
|
-
return /* @__PURE__ */ jsxs2("span", { role: "img", "aria-label": reason, className: "relative inline-flex shrink-0 items-center", children: [
|
|
74
|
-
/* @__PURE__ */ jsx2(FaYoutube, { size: 16, "aria-hidden": "true" }),
|
|
75
|
-
/* @__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" })
|
|
76
|
-
] });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// src/genre-tree/GenrePlaylistTracks.tsx
|
|
80
|
-
import { ErrorCode } from "@behindthemusictree/app-kit/transport";
|
|
81
|
-
import { genrePlaylistQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
|
|
82
|
-
import { libraryEndpoints, libraryQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
|
|
83
|
-
import { isSong, songUnplayableReason, SongDetailedSchema } from "@behindthemusictree/app-kit/genre-tree";
|
|
84
|
-
import { useTrackList } from "@behindthemusictree/app-kit/genre-tree";
|
|
85
|
-
|
|
86
|
-
// src/genre-tree/MusicBrainzRecordingLink.tsx
|
|
87
|
-
import { FaExternalLinkAlt } from "react-icons/fa";
|
|
88
|
-
import { musicBrainzRecordingUrl } from "@behindthemusictree/app-kit/genre-tree";
|
|
89
|
-
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
90
|
-
function MusicBrainzRecordingLink({ song }) {
|
|
91
|
-
const url = musicBrainzRecordingUrl(song);
|
|
92
|
-
if (!url) return null;
|
|
93
|
-
return /* @__PURE__ */ jsx3(
|
|
94
|
-
"a",
|
|
95
|
-
{
|
|
96
|
-
href: url,
|
|
97
|
-
target: "_blank",
|
|
98
|
-
rel: "noopener noreferrer",
|
|
99
|
-
"aria-label": `View ${song.title} on MusicBrainz`,
|
|
100
|
-
title: "View on MusicBrainz",
|
|
101
|
-
className: "inline-flex shrink-0 items-center opacity-50 hover:opacity-100 focus-visible:opacity-100",
|
|
102
|
-
onClick: (event) => event.stopPropagation(),
|
|
103
|
-
children: /* @__PURE__ */ jsx3(FaExternalLinkAlt, { size: 10 })
|
|
104
|
-
}
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// src/genre-tree/GenrePlaylistTracks.tsx
|
|
109
|
-
import { ArtistNames } from "@behindthemusictree/app-kit/genre-tree";
|
|
110
|
-
import { capitalize, useTrackNoun } from "@behindthemusictree/app-kit/track-noun";
|
|
111
|
-
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
112
|
-
var CATALOGUE_PAGE_SIZE = 100;
|
|
113
|
-
var SongPageSchema = PaginatedResponseSchema(SongDetailedSchema).extend({
|
|
114
|
-
overallTotal: z.number().nullable(),
|
|
115
|
-
page: z.number().nullable(),
|
|
116
|
-
totalPages: z.number().nullable()
|
|
117
|
-
});
|
|
118
|
-
var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
|
|
119
|
-
var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
|
|
120
|
-
var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
|
|
121
|
-
var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
|
|
122
|
-
var SEPARATOR = "[@container(min-width:40rem)]:hidden";
|
|
123
|
-
var nextCursor = (next) => {
|
|
124
|
-
const cursor = new URL(next).searchParams.get("cursor");
|
|
125
|
-
if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
|
|
126
|
-
return cursor;
|
|
127
|
-
};
|
|
128
|
-
function GenrePlaylistTracks({
|
|
129
|
-
genreUuid,
|
|
130
|
-
genrePlaylist,
|
|
131
|
-
scope,
|
|
132
|
-
getBackendBaseUrl,
|
|
133
|
-
renderArtistLink,
|
|
134
|
-
renderSongLeading,
|
|
135
|
-
renderSongActions,
|
|
136
|
-
renderTracksToolbar,
|
|
137
|
-
artist = null
|
|
138
|
-
}) {
|
|
139
|
-
const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = useTrackList();
|
|
140
|
-
const { session, sessionRestored } = useSession();
|
|
141
|
-
const { playerTrackObject, playState, handlePlayPauseAction } = usePlayer();
|
|
142
|
-
const { showPopup } = usePopup();
|
|
143
|
-
const trackNoun = useTrackNoun();
|
|
144
|
-
const { uuid } = genrePlaylist;
|
|
145
|
-
const { fetch } = useFetchWrapper(getBackendBaseUrl);
|
|
146
|
-
const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({
|
|
147
|
-
queryKey: scope === "me" ? genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
|
|
148
|
-
queryFn: async ({ pageParam }) => {
|
|
149
|
-
if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
|
|
150
|
-
const response = await fetch(
|
|
151
|
-
libraryEndpoints.reference.songs.list(),
|
|
152
|
-
true,
|
|
153
|
-
false,
|
|
154
|
-
{},
|
|
155
|
-
{ genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
|
|
156
|
-
);
|
|
157
|
-
const parsed = parseWithLog(SongPageSchema, response, "GenrePlaylistTracks");
|
|
158
|
-
return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
|
|
159
|
-
},
|
|
160
|
-
initialPageParam: scope === "me" ? 1 : "",
|
|
161
|
-
getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
|
|
162
|
-
enabled: scope !== "me" || sessionRestored && !!session?.accessToken
|
|
163
|
-
});
|
|
164
|
-
const sentinelRef = useRef2(null);
|
|
165
|
-
useEffect(() => {
|
|
166
|
-
const sentinel = sentinelRef.current;
|
|
167
|
-
if (!hasNextPage || isFetchingNextPage || !sentinel) return;
|
|
168
|
-
const observer = new IntersectionObserver((entries) => {
|
|
169
|
-
if (entries.some((entry) => entry.isIntersecting)) {
|
|
170
|
-
void fetchNextPage();
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
observer.observe(sentinel);
|
|
174
|
-
return () => observer.disconnect();
|
|
175
|
-
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
|
176
|
-
const pages = data?.pages ?? [];
|
|
177
|
-
const tracks = pages.flatMap((page) => page.tracks);
|
|
178
|
-
const total = pages[0]?.total ?? void 0;
|
|
179
|
-
const play = (startSongUuid) => {
|
|
180
|
-
const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
|
|
181
|
-
started.catch((error) => {
|
|
182
|
-
console.error("Failed to play genre playlist:", error);
|
|
183
|
-
showPopup(/* @__PURE__ */ jsx4(InternalErrorPopup, { errorCode: ErrorCode.CLIENT_UNKNOWN }));
|
|
184
|
-
});
|
|
185
|
-
};
|
|
186
|
-
return /* @__PURE__ */ jsxs3("div", { className: "gtv-info-panel-children", children: [
|
|
187
|
-
/* @__PURE__ */ jsxs3("span", { className: "gtv-info-panel-children-title", children: [
|
|
188
|
-
capitalize(trackNoun.plural),
|
|
189
|
-
total !== void 0 && ` (${total})`
|
|
190
|
-
] }),
|
|
191
|
-
renderTracksToolbar?.(),
|
|
192
|
-
isPending ? /* @__PURE__ */ jsx4(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs3("p", { role: "alert", children: [
|
|
193
|
-
"Couldn't load ",
|
|
194
|
-
trackNoun.plural,
|
|
195
|
-
".",
|
|
196
|
-
" ",
|
|
197
|
-
/* @__PURE__ */ jsx4("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
|
|
198
|
-
] }) : 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: [
|
|
199
|
-
tracks.map((track, index) => {
|
|
200
|
-
const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
|
|
201
|
-
const isCurrent = playerTrackObject?.track.id === track.uuid;
|
|
202
|
-
const isPlaying = isCurrent && playState === PlayStates.PLAYING;
|
|
203
|
-
const text = /* @__PURE__ */ jsxs3("span", { className: TEXT_CELL, children: [
|
|
204
|
-
/* @__PURE__ */ jsx4("span", { className: TITLE_CELL, children: track.title }),
|
|
205
|
-
track.artists?.length ? /* @__PURE__ */ jsx4("span", { className: SEPARATOR, children: " \u2014 " }) : null,
|
|
206
|
-
/* @__PURE__ */ jsx4("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx4(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
|
|
207
|
-
track.album && /* @__PURE__ */ jsx4("span", { className: ALBUM_CELL, children: track.album.name })
|
|
208
|
-
] });
|
|
209
|
-
const leading = renderSongLeading?.(track, index);
|
|
210
|
-
const actions = renderSongActions?.(track);
|
|
211
|
-
const unplayableReason = isSong(track) ? songUnplayableReason(track) : null;
|
|
212
|
-
if (unplayableReason) {
|
|
213
|
-
return /* @__PURE__ */ jsxs3("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", title: unplayableReason, children: [
|
|
214
|
-
leading,
|
|
215
|
-
/* @__PURE__ */ jsx4(
|
|
216
|
-
"button",
|
|
217
|
-
{
|
|
218
|
-
type: "button",
|
|
219
|
-
disabled: true,
|
|
220
|
-
"aria-disabled": "true",
|
|
221
|
-
"aria-label": `${label} \u2014 ${unplayableReason}`,
|
|
222
|
-
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
|
|
223
|
-
children: /* @__PURE__ */ jsx4(FaPlay, { size: 10 })
|
|
224
|
-
}
|
|
225
|
-
),
|
|
226
|
-
text,
|
|
227
|
-
isSong(track) && /* @__PURE__ */ jsx4(MusicBrainzRecordingLink, { song: track }),
|
|
228
|
-
/* @__PURE__ */ jsx4("span", { className: "shrink-0 text-xs", children: /* @__PURE__ */ jsx4(UnplayableReason, { reason: unplayableReason }) }),
|
|
229
|
-
actions
|
|
230
|
-
] }, track.uuid);
|
|
231
|
-
}
|
|
232
|
-
return /* @__PURE__ */ jsxs3(
|
|
233
|
-
"li",
|
|
234
|
-
{
|
|
235
|
-
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)]" : ""}`,
|
|
236
|
-
children: [
|
|
237
|
-
leading,
|
|
238
|
-
/* @__PURE__ */ jsx4(
|
|
239
|
-
"button",
|
|
240
|
-
{
|
|
241
|
-
type: "button",
|
|
242
|
-
"aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
|
|
243
|
-
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"}`,
|
|
244
|
-
onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
|
|
245
|
-
children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx4(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx4(FaPause, { size: 10 }) : /* @__PURE__ */ jsx4(FaPlay, { size: 10 })
|
|
246
|
-
}
|
|
247
|
-
),
|
|
248
|
-
text,
|
|
249
|
-
isSong(track) && /* @__PURE__ */ jsx4(MusicBrainzRecordingLink, { song: track }),
|
|
250
|
-
actions
|
|
251
|
-
]
|
|
252
|
-
},
|
|
253
|
-
track.uuid
|
|
254
|
-
);
|
|
255
|
-
}),
|
|
256
|
-
hasNextPage && /* @__PURE__ */ jsx4("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
|
|
257
|
-
] })
|
|
258
|
-
] });
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// src/genre-tree/GenreDetailExtras.tsx
|
|
262
|
-
import { useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
|
|
263
|
-
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
264
|
-
function GenreDetailExtras({
|
|
265
|
-
genreUuid,
|
|
266
|
-
genrePlaylist,
|
|
267
|
-
scope,
|
|
268
|
-
getBackendBaseUrl,
|
|
269
|
-
criteriaOverviewSchema,
|
|
270
|
-
renderGenreDetailExtras,
|
|
271
|
-
renderArtistLink,
|
|
272
|
-
renderSummary,
|
|
273
|
-
renderEssentialTracks,
|
|
274
|
-
family,
|
|
275
|
-
...trackSlots
|
|
276
|
-
}) {
|
|
277
|
-
const trackNoun = useTrackNoun2();
|
|
278
|
-
const { data: overview, isPending } = useFetchGenreOverview(
|
|
279
|
-
genreUuid,
|
|
280
|
-
scope,
|
|
281
|
-
getBackendBaseUrl,
|
|
282
|
-
criteriaOverviewSchema
|
|
283
|
-
);
|
|
284
|
-
if (isPending) {
|
|
285
|
-
return /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
286
|
-
/* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
|
|
287
|
-
/* @__PURE__ */ jsx5("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
|
|
288
|
-
/* @__PURE__ */ jsx5(Skeleton2, { className: "h-4 w-3/4" })
|
|
289
|
-
] }),
|
|
290
|
-
family
|
|
291
|
-
] });
|
|
292
|
-
}
|
|
293
|
-
if (!overview) return /* @__PURE__ */ jsx5(Fragment3, { children: family });
|
|
294
|
-
const { summary, description, summarySourceUrl, essentialSongs } = overview;
|
|
295
|
-
return /* @__PURE__ */ jsxs4(Fragment3, { children: [
|
|
296
|
-
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) }),
|
|
297
|
-
renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
|
|
298
|
-
/* @__PURE__ */ jsx5("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
|
|
299
|
-
/* @__PURE__ */ jsx5(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid),
|
|
300
|
-
summarySourceUrl && /* @__PURE__ */ jsx5(
|
|
301
|
-
"a",
|
|
302
|
-
{
|
|
303
|
-
href: summarySourceUrl,
|
|
304
|
-
target: "_blank",
|
|
305
|
-
rel: "noopener noreferrer",
|
|
306
|
-
className: "mt-1 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
|
|
307
|
-
children: "Source: Wikipedia"
|
|
308
|
-
}
|
|
309
|
-
)
|
|
310
|
-
] }),
|
|
311
|
-
renderSummary && summarySourceUrl && /* @__PURE__ */ jsx5(
|
|
312
|
-
"a",
|
|
313
|
-
{
|
|
314
|
-
href: summarySourceUrl,
|
|
315
|
-
target: "_blank",
|
|
316
|
-
rel: "noopener noreferrer",
|
|
317
|
-
className: "mx-3.5 mb-2.5 inline-block text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
|
|
318
|
-
children: "Source: Wikipedia"
|
|
319
|
-
}
|
|
320
|
-
),
|
|
321
|
-
family,
|
|
322
|
-
renderGenreDetailExtras?.(overview),
|
|
323
|
-
renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ jsxs4("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
|
|
324
|
-
/* @__PURE__ */ jsxs4("span", { className: "gtv-info-panel-children-title", children: [
|
|
325
|
-
"Essential ",
|
|
326
|
-
trackNoun.plural
|
|
327
|
-
] }),
|
|
328
|
-
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" })
|
|
329
|
-
] }),
|
|
330
|
-
/* @__PURE__ */ jsx5(
|
|
331
|
-
GenrePlaylistTracks,
|
|
332
|
-
{
|
|
333
|
-
genreUuid,
|
|
334
|
-
genrePlaylist,
|
|
335
|
-
scope,
|
|
336
|
-
getBackendBaseUrl,
|
|
337
|
-
renderArtistLink,
|
|
338
|
-
...trackSlots
|
|
339
|
-
}
|
|
340
|
-
)
|
|
341
|
-
] });
|
|
342
|
-
}
|
|
19
|
+
import { useGenreTreeViewState, useArtist as useArtist2, useUpdateGenre as useUpdateGenre2, genreUpdateMutationKey } from "@behindthemusictree/app-kit/genre-tree";
|
|
20
|
+
import { usePopup as usePopup5 } from "@behindthemusictree/app-kit/popup";
|
|
343
21
|
|
|
344
22
|
// src/genre-tree/playlist-tree/TreePerRoot.tsx
|
|
345
23
|
import { useCallback as useCallback2, useMemo as useMemo2 } from "react";
|
|
@@ -347,22 +25,22 @@ import {
|
|
|
347
25
|
GenreTree,
|
|
348
26
|
getGenreTreeColor
|
|
349
27
|
} from "@behindthemusictree/genre-tree-view";
|
|
350
|
-
import { usePlayer
|
|
351
|
-
import { usePopup
|
|
352
|
-
import { ErrorCode
|
|
28
|
+
import { usePlayer } from "@behindthemusictree/app-kit/player";
|
|
29
|
+
import { usePopup } from "@behindthemusictree/app-kit/popup";
|
|
30
|
+
import { ErrorCode } from "@behindthemusictree/app-kit/transport";
|
|
353
31
|
|
|
354
32
|
// src/genre-tree/playlist-tree/useGenrePlaylistPlayback.ts
|
|
355
33
|
import { useCallback, useMemo } from "react";
|
|
356
34
|
import {
|
|
357
35
|
TrackListOriginType,
|
|
358
|
-
useTrackList
|
|
36
|
+
useTrackList
|
|
359
37
|
} from "@behindthemusictree/app-kit/genre-tree";
|
|
360
38
|
function useGenrePlaylistPlayback({
|
|
361
39
|
scope,
|
|
362
40
|
genrePlaylists,
|
|
363
41
|
artist = null
|
|
364
42
|
}) {
|
|
365
|
-
const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } =
|
|
43
|
+
const { trackList, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = useTrackList();
|
|
366
44
|
const origin = trackList?.origin;
|
|
367
45
|
const playingNodeId = useMemo(() => {
|
|
368
46
|
if (!origin) return null;
|
|
@@ -383,11 +61,11 @@ function useGenrePlaylistPlayback({
|
|
|
383
61
|
}
|
|
384
62
|
|
|
385
63
|
// src/genre-tree/playlist-tree/TreePerRoot.tsx
|
|
386
|
-
import { capitalize
|
|
387
|
-
import { jsx
|
|
64
|
+
import { capitalize, useTrackNoun } from "@behindthemusictree/app-kit/track-noun";
|
|
65
|
+
import { jsx } from "react/jsx-runtime";
|
|
388
66
|
function GenrePlaylistTreePerRoot({
|
|
389
67
|
scope,
|
|
390
|
-
className,
|
|
68
|
+
className: className2,
|
|
391
69
|
rootUuid,
|
|
392
70
|
genrePlaylistTreePerRoot,
|
|
393
71
|
handleGenreCreationAction,
|
|
@@ -403,17 +81,17 @@ function GenrePlaylistTreePerRoot({
|
|
|
403
81
|
readOnly = false,
|
|
404
82
|
showToolbar,
|
|
405
83
|
wheelZoom,
|
|
406
|
-
controlsOrientation,
|
|
407
84
|
defaultViewport,
|
|
85
|
+
viewportRestore,
|
|
408
86
|
onViewportChange,
|
|
409
87
|
playback,
|
|
410
88
|
...infoPanelCuration
|
|
411
89
|
}) {
|
|
412
|
-
const { isPlaying, setIsPlaying } =
|
|
90
|
+
const { isPlaying, setIsPlaying } = usePlayer();
|
|
413
91
|
const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists: genrePlaylistTreePerRoot });
|
|
414
92
|
const { playingNodeId, play } = playback ?? defaultPlayback;
|
|
415
|
-
const trackNoun =
|
|
416
|
-
const { showPopup } =
|
|
93
|
+
const trackNoun = useTrackNoun();
|
|
94
|
+
const { showPopup } = usePopup();
|
|
417
95
|
const nodes = useMemo2(
|
|
418
96
|
() => genrePlaylistTreePerRoot.map((genrePlaylist) => ({
|
|
419
97
|
id: genrePlaylist.uuid,
|
|
@@ -440,7 +118,7 @@ function GenrePlaylistTreePerRoot({
|
|
|
440
118
|
}
|
|
441
119
|
play(genrePlaylist).catch((error) => {
|
|
442
120
|
console.error("Failed to load genre playlist tracks:", error);
|
|
443
|
-
showPopup(/* @__PURE__ */
|
|
121
|
+
showPopup(/* @__PURE__ */ jsx(InternalErrorPopup, { errorCode: ErrorCode.CLIENT_UNKNOWN }));
|
|
444
122
|
});
|
|
445
123
|
},
|
|
446
124
|
[
|
|
@@ -472,12 +150,12 @@ function GenrePlaylistTreePerRoot({
|
|
|
472
150
|
if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
|
|
473
151
|
}
|
|
474
152
|
}, []);
|
|
475
|
-
return /* @__PURE__ */
|
|
153
|
+
return /* @__PURE__ */ jsx(
|
|
476
154
|
GenreTree,
|
|
477
155
|
{
|
|
478
|
-
className,
|
|
156
|
+
className: className2,
|
|
479
157
|
nodes,
|
|
480
|
-
itemCountLabel: `${
|
|
158
|
+
itemCountLabel: `${capitalize(trackNoun.singular)} count`,
|
|
481
159
|
rootColor: getGenreTreeColor(rootUuid),
|
|
482
160
|
playingNodeId,
|
|
483
161
|
playState: isPlaying ? "playing" : "paused",
|
|
@@ -495,8 +173,8 @@ function GenrePlaylistTreePerRoot({
|
|
|
495
173
|
hideInfoPanelTitle,
|
|
496
174
|
showToolbar,
|
|
497
175
|
wheelZoom,
|
|
498
|
-
controlsOrientation,
|
|
499
176
|
defaultViewport,
|
|
177
|
+
viewportRestore,
|
|
500
178
|
onViewportChange,
|
|
501
179
|
...infoPanelCuration,
|
|
502
180
|
readOnly
|
|
@@ -509,19 +187,26 @@ import { useCallback as useCallback3, useMemo as useMemo3 } from "react";
|
|
|
509
187
|
import {
|
|
510
188
|
GenreTreeWheel
|
|
511
189
|
} from "@behindthemusictree/genre-tree-view";
|
|
512
|
-
import { usePlayer as
|
|
513
|
-
import { usePopup as
|
|
514
|
-
import { ErrorCode as
|
|
190
|
+
import { usePlayer as usePlayer2 } from "@behindthemusictree/app-kit/player";
|
|
191
|
+
import { usePopup as usePopup2 } from "@behindthemusictree/app-kit/popup";
|
|
192
|
+
import { ErrorCode as ErrorCode2 } from "@behindthemusictree/app-kit/transport";
|
|
515
193
|
|
|
516
194
|
// src/genre-tree/playlist-tree/size-metric.ts
|
|
517
|
-
var
|
|
195
|
+
var hasViewsCount = (genrePlaylists) => genrePlaylists.every((genrePlaylist) => genrePlaylist.viewsCount !== void 0);
|
|
196
|
+
var genrePlaylistSize = (genrePlaylist, metric) => {
|
|
197
|
+
if (metric === "songs") return genrePlaylist.tracksCount;
|
|
198
|
+
if (genrePlaylist.viewsCount === void 0) {
|
|
199
|
+
throw new Error(`Genre playlist ${genrePlaylist.uuid} has no viewsCount to size by views`);
|
|
200
|
+
}
|
|
201
|
+
return genrePlaylist.viewsCount;
|
|
202
|
+
};
|
|
518
203
|
|
|
519
204
|
// src/genre-tree/playlist-tree/TreeWheel.tsx
|
|
520
|
-
import { capitalize as
|
|
521
|
-
import { jsx as
|
|
205
|
+
import { capitalize as capitalize2, useTrackNoun as useTrackNoun2 } from "@behindthemusictree/app-kit/track-noun";
|
|
206
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
522
207
|
function GenrePlaylistTreeWheel({
|
|
523
208
|
scope,
|
|
524
|
-
className,
|
|
209
|
+
className: className2,
|
|
525
210
|
genrePlaylists,
|
|
526
211
|
handleGenreCreationAction,
|
|
527
212
|
handleGenreRenameAction,
|
|
@@ -536,20 +221,20 @@ function GenrePlaylistTreeWheel({
|
|
|
536
221
|
readOnly = false,
|
|
537
222
|
allowWheelRotation,
|
|
538
223
|
showToolbar,
|
|
539
|
-
controlsOrientation,
|
|
540
224
|
sizeMetric = "songs",
|
|
541
225
|
floatingControlsLeading,
|
|
542
226
|
busy,
|
|
543
227
|
defaultViewport,
|
|
228
|
+
viewportRestore,
|
|
544
229
|
onViewportChange,
|
|
545
230
|
playback,
|
|
546
231
|
...infoPanelCuration
|
|
547
232
|
}) {
|
|
548
|
-
const { isPlaying, setIsPlaying } =
|
|
233
|
+
const { isPlaying, setIsPlaying } = usePlayer2();
|
|
549
234
|
const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
|
|
550
235
|
const { playingNodeId, play } = playback ?? defaultPlayback;
|
|
551
|
-
const trackNoun =
|
|
552
|
-
const { showPopup } =
|
|
236
|
+
const trackNoun = useTrackNoun2();
|
|
237
|
+
const { showPopup } = usePopup2();
|
|
553
238
|
const nodes = useMemo3(
|
|
554
239
|
() => genrePlaylists.map((genrePlaylist) => ({
|
|
555
240
|
id: genrePlaylist.uuid,
|
|
@@ -575,7 +260,7 @@ function GenrePlaylistTreeWheel({
|
|
|
575
260
|
}
|
|
576
261
|
play(genrePlaylist).catch((error) => {
|
|
577
262
|
console.error("Failed to load genre playlist tracks:", error);
|
|
578
|
-
showPopup(/* @__PURE__ */
|
|
263
|
+
showPopup(/* @__PURE__ */ jsx2(InternalErrorPopup, { errorCode: ErrorCode2.CLIENT_UNKNOWN }));
|
|
579
264
|
});
|
|
580
265
|
},
|
|
581
266
|
[
|
|
@@ -607,12 +292,12 @@ function GenrePlaylistTreeWheel({
|
|
|
607
292
|
if (confirm(`Are you sure you want to delete "${node.name}"?`)) {
|
|
608
293
|
}
|
|
609
294
|
}, []);
|
|
610
|
-
return /* @__PURE__ */
|
|
295
|
+
return /* @__PURE__ */ jsx2(
|
|
611
296
|
GenreTreeWheel,
|
|
612
297
|
{
|
|
613
|
-
className,
|
|
298
|
+
className: className2,
|
|
614
299
|
nodes,
|
|
615
|
-
itemCountLabel: sizeMetric === "views" ? "Views" : `${
|
|
300
|
+
itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize2(trackNoun.singular)} count`,
|
|
616
301
|
playingNodeId,
|
|
617
302
|
playState: isPlaying ? "playing" : "paused",
|
|
618
303
|
onPlayPause: handlePlayPause,
|
|
@@ -629,10 +314,10 @@ function GenrePlaylistTreeWheel({
|
|
|
629
314
|
hideInfoPanelTitle,
|
|
630
315
|
allowWheelRotation,
|
|
631
316
|
showToolbar,
|
|
632
|
-
controlsOrientation,
|
|
633
317
|
floatingControlsLeading,
|
|
634
318
|
busy,
|
|
635
319
|
defaultViewport,
|
|
320
|
+
viewportRestore,
|
|
636
321
|
onViewportChange,
|
|
637
322
|
...infoPanelCuration,
|
|
638
323
|
readOnly
|
|
@@ -646,13 +331,13 @@ import {
|
|
|
646
331
|
GenreTreeOutline,
|
|
647
332
|
GenreTreeWheelRadialPopCore
|
|
648
333
|
} from "@behindthemusictree/genre-tree-view";
|
|
649
|
-
import { usePlayer as
|
|
650
|
-
import { capitalize as
|
|
651
|
-
import { jsx as
|
|
334
|
+
import { usePlayer as usePlayer3 } from "@behindthemusictree/app-kit/player";
|
|
335
|
+
import { capitalize as capitalize3, useTrackNoun as useTrackNoun3 } from "@behindthemusictree/app-kit/track-noun";
|
|
336
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
652
337
|
function GenrePlaylistTreeWheelRadialPopCore(props) {
|
|
653
338
|
const {
|
|
654
339
|
scope,
|
|
655
|
-
className,
|
|
340
|
+
className: className2,
|
|
656
341
|
genrePlaylists,
|
|
657
342
|
handleGenreCreationAction,
|
|
658
343
|
handleGenreRenameAction,
|
|
@@ -667,12 +352,12 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
|
|
|
667
352
|
readOnly = false,
|
|
668
353
|
allowWheelRotation,
|
|
669
354
|
showToolbar,
|
|
670
|
-
controlsOrientation,
|
|
671
355
|
sizeMetric = "songs",
|
|
672
356
|
floatingControlsLeading,
|
|
673
357
|
busy,
|
|
674
358
|
outline = false,
|
|
675
359
|
defaultViewport,
|
|
360
|
+
viewportRestore,
|
|
676
361
|
onViewportChange,
|
|
677
362
|
playback
|
|
678
363
|
} = props;
|
|
@@ -685,10 +370,10 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
|
|
|
685
370
|
hideExpandButton: props.hideExpandButton,
|
|
686
371
|
overlayPanel: props.overlayPanel
|
|
687
372
|
};
|
|
688
|
-
const { isPlaying, setIsPlaying } =
|
|
373
|
+
const { isPlaying, setIsPlaying } = usePlayer3();
|
|
689
374
|
const defaultPlayback = useGenrePlaylistPlayback({ scope, genrePlaylists });
|
|
690
375
|
const { playingNodeId, play } = playback ?? defaultPlayback;
|
|
691
|
-
const trackNoun =
|
|
376
|
+
const trackNoun = useTrackNoun3();
|
|
692
377
|
const nodes = useMemo4(
|
|
693
378
|
() => genrePlaylists.map((genrePlaylist) => ({
|
|
694
379
|
id: genrePlaylist.uuid,
|
|
@@ -742,9 +427,9 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
|
|
|
742
427
|
}
|
|
743
428
|
}, []);
|
|
744
429
|
const treeProps = {
|
|
745
|
-
className,
|
|
430
|
+
className: className2,
|
|
746
431
|
nodes,
|
|
747
|
-
itemCountLabel: sizeMetric === "views" ? "Views" : `${
|
|
432
|
+
itemCountLabel: sizeMetric === "views" ? "Views" : `${capitalize3(trackNoun.singular)} count`,
|
|
748
433
|
playingNodeId,
|
|
749
434
|
playState: isPlaying ? "playing" : "paused",
|
|
750
435
|
onPlayPause: handlePlayPause,
|
|
@@ -763,15 +448,15 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
|
|
|
763
448
|
...infoPanelCuration,
|
|
764
449
|
readOnly
|
|
765
450
|
};
|
|
766
|
-
return outline ? /* @__PURE__ */
|
|
451
|
+
return outline ? /* @__PURE__ */ jsx3(GenreTreeOutline, { ...treeProps }) : /* @__PURE__ */ jsx3(
|
|
767
452
|
GenreTreeWheelRadialPopCore,
|
|
768
453
|
{
|
|
769
454
|
...treeProps,
|
|
770
455
|
allowWheelRotation,
|
|
771
|
-
controlsOrientation,
|
|
772
456
|
floatingControlsLeading,
|
|
773
457
|
busy,
|
|
774
458
|
defaultViewport,
|
|
459
|
+
viewportRestore,
|
|
775
460
|
onViewportChange,
|
|
776
461
|
...focus.onFocusGenreChange ? { focusNodeId: focus.focusGenreUuid, onFocusChange: focus.onFocusGenreChange } : { focusNodeId: void 0, onFocusChange: void 0 }
|
|
777
462
|
}
|
|
@@ -779,13 +464,13 @@ function GenrePlaylistTreeWheelRadialPopCore(props) {
|
|
|
779
464
|
}
|
|
780
465
|
|
|
781
466
|
// src/genre-tree/GenreTreeWheelHandoff.tsx
|
|
782
|
-
import { useEffect
|
|
783
|
-
import { jsx as
|
|
467
|
+
import { useEffect, useRef, useState } from "react";
|
|
468
|
+
import { jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
784
469
|
function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
|
|
785
|
-
const [revealed, setRevealed] =
|
|
786
|
-
const onRevealedChangeRef =
|
|
470
|
+
const [revealed, setRevealed] = useState(false);
|
|
471
|
+
const onRevealedChangeRef = useRef(onRevealedChange);
|
|
787
472
|
onRevealedChangeRef.current = onRevealedChange;
|
|
788
|
-
|
|
473
|
+
useEffect(() => {
|
|
789
474
|
let innerFrame = 0;
|
|
790
475
|
const outerFrame = requestAnimationFrame(() => {
|
|
791
476
|
innerFrame = requestAnimationFrame(() => {
|
|
@@ -799,8 +484,8 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
|
|
|
799
484
|
cancelAnimationFrame(innerFrame);
|
|
800
485
|
};
|
|
801
486
|
}, []);
|
|
802
|
-
return /* @__PURE__ */
|
|
803
|
-
/* @__PURE__ */
|
|
487
|
+
return /* @__PURE__ */ jsxs("div", { className: "relative w-full h-full", children: [
|
|
488
|
+
/* @__PURE__ */ jsx4(
|
|
804
489
|
"div",
|
|
805
490
|
{
|
|
806
491
|
className: "absolute inset-0 transition-opacity duration-150 ease-out",
|
|
@@ -809,7 +494,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
|
|
|
809
494
|
children
|
|
810
495
|
}
|
|
811
496
|
),
|
|
812
|
-
!revealed && /* @__PURE__ */
|
|
497
|
+
!revealed && /* @__PURE__ */ jsx4("div", { className: "absolute inset-0", "aria-hidden": "true", children: skeleton })
|
|
813
498
|
] });
|
|
814
499
|
}
|
|
815
500
|
|
|
@@ -817,7 +502,7 @@ function GenreTreeWheelHandoff({ skeleton, children, onRevealedChange }) {
|
|
|
817
502
|
import { ArrowLeft, LoaderCircle, Search, X } from "lucide-react";
|
|
818
503
|
import { Input } from "@behindthemusictree/ui";
|
|
819
504
|
import { useGenreSearch } from "@behindthemusictree/app-kit/genre-tree";
|
|
820
|
-
import { jsx as
|
|
505
|
+
import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
821
506
|
function GenreSearch({
|
|
822
507
|
genrePlaylists,
|
|
823
508
|
onSelect,
|
|
@@ -839,19 +524,19 @@ function GenreSearch({
|
|
|
839
524
|
const loading = query !== "" && query !== selectedName && (extraResults?.loading ?? false);
|
|
840
525
|
const showList = results.length > 0 || extraItems.length > 0;
|
|
841
526
|
const heading = "px-3 pt-2 pb-1 text-xs font-medium uppercase text-[var(--color-text-muted)]";
|
|
842
|
-
return /* @__PURE__ */
|
|
843
|
-
onPrevious && /* @__PURE__ */
|
|
527
|
+
return /* @__PURE__ */ jsxs2("div", { className: "genre-search relative w-[calc(var(--layout-detail-panel-w,25.5rem)-2*var(--layout-panel-inset,1rem))] shrink-0 max-md:w-full", children: [
|
|
528
|
+
onPrevious && /* @__PURE__ */ jsx5(
|
|
844
529
|
"button",
|
|
845
530
|
{
|
|
846
531
|
type: "button",
|
|
847
532
|
"aria-label": "Previous",
|
|
848
533
|
title: "Previous",
|
|
849
|
-
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)]",
|
|
534
|
+
className: "genre-search-previous absolute left-2 top-1/2 z-10 hidden items-center justify-center p-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full -translate-y-1/2 hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
|
|
850
535
|
onClick: onPrevious,
|
|
851
|
-
children: /* @__PURE__ */
|
|
536
|
+
children: /* @__PURE__ */ jsx5(ArrowLeft, { "aria-hidden": "true", className: "w-4 h-4" })
|
|
852
537
|
}
|
|
853
538
|
),
|
|
854
|
-
/* @__PURE__ */
|
|
539
|
+
/* @__PURE__ */ jsx5(
|
|
855
540
|
Input,
|
|
856
541
|
{
|
|
857
542
|
type: "text",
|
|
@@ -863,24 +548,27 @@ function GenreSearch({
|
|
|
863
548
|
placeholder: placeholder ?? `${label}\u2026`,
|
|
864
549
|
"aria-label": label,
|
|
865
550
|
"aria-busy": loading,
|
|
551
|
+
inputMode: "search",
|
|
552
|
+
enterKeyHint: "search",
|
|
553
|
+
autoComplete: "off",
|
|
866
554
|
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"
|
|
867
555
|
}
|
|
868
556
|
),
|
|
869
|
-
/* @__PURE__ */
|
|
870
|
-
/* @__PURE__ */
|
|
557
|
+
/* @__PURE__ */ jsxs2("div", { className: "absolute right-3 top-1/2 flex items-center -translate-y-1/2", children: [
|
|
558
|
+
/* @__PURE__ */ jsxs2(
|
|
871
559
|
"span",
|
|
872
560
|
{
|
|
873
561
|
"data-loading": loading,
|
|
874
562
|
className: "relative p-2 text-[var(--color-text-muted)] pointer-events-none",
|
|
875
563
|
children: [
|
|
876
|
-
/* @__PURE__ */
|
|
564
|
+
/* @__PURE__ */ jsx5(
|
|
877
565
|
Search,
|
|
878
566
|
{
|
|
879
567
|
"aria-hidden": "true",
|
|
880
568
|
className: `w-6 h-6 transition-opacity duration-200 motion-reduce:transition-none ${loading ? "opacity-0" : ""}`
|
|
881
569
|
}
|
|
882
570
|
),
|
|
883
|
-
/* @__PURE__ */
|
|
571
|
+
/* @__PURE__ */ jsx5(
|
|
884
572
|
LoaderCircle,
|
|
885
573
|
{
|
|
886
574
|
"aria-hidden": "true",
|
|
@@ -890,44 +578,44 @@ function GenreSearch({
|
|
|
890
578
|
]
|
|
891
579
|
}
|
|
892
580
|
),
|
|
893
|
-
canClear && /* @__PURE__ */
|
|
581
|
+
canClear && /* @__PURE__ */ jsx5(
|
|
894
582
|
"button",
|
|
895
583
|
{
|
|
896
584
|
type: "button",
|
|
897
585
|
"aria-label": "Clear search",
|
|
898
|
-
className: "p-2 text-[var(--color-text-muted)] rounded-full hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
|
|
586
|
+
className: "flex items-center justify-center p-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] [@media(pointer:coarse)]:min-w-[var(--layout-touch-target)] text-[var(--color-text-muted)] rounded-full hover:bg-[var(--color-surface-muted)] hover:text-[var(--color-text)]",
|
|
899
587
|
onClick: () => {
|
|
900
588
|
clear();
|
|
901
589
|
onQueryChange?.("");
|
|
902
590
|
},
|
|
903
|
-
children: /* @__PURE__ */
|
|
591
|
+
children: /* @__PURE__ */ jsx5(X, { "aria-hidden": "true", className: "w-6 h-6" })
|
|
904
592
|
}
|
|
905
593
|
)
|
|
906
594
|
] }),
|
|
907
|
-
extraResults?.status && /* @__PURE__ */
|
|
908
|
-
showList && /* @__PURE__ */
|
|
909
|
-
extraResults && results.length > 0 && /* @__PURE__ */
|
|
910
|
-
results.map((genrePlaylist) => /* @__PURE__ */
|
|
595
|
+
extraResults?.status && /* @__PURE__ */ jsx5("p", { role: "status", "aria-live": "polite", className: "sr-only", children: extraResults.status }),
|
|
596
|
+
showList && /* @__PURE__ */ jsxs2("ul", { className: "genre-search-results absolute z-40 w-full max-h-[min(16rem,50dvh)] mt-1 overflow-y-auto bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl shadow-md", children: [
|
|
597
|
+
extraResults && results.length > 0 && /* @__PURE__ */ jsx5("li", { className: heading, children: "Genres" }),
|
|
598
|
+
results.map((genrePlaylist) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsxs2(
|
|
911
599
|
"button",
|
|
912
600
|
{
|
|
913
601
|
type: "button",
|
|
914
|
-
className: "w-full px-3 py-1.5 text-left hover:bg-[var(--color-surface-muted)]",
|
|
602
|
+
className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
|
|
915
603
|
onClick: () => {
|
|
916
604
|
select(genrePlaylist);
|
|
917
605
|
onQueryChange?.("");
|
|
918
606
|
},
|
|
919
607
|
children: [
|
|
920
608
|
genrePlaylist.name,
|
|
921
|
-
genrePlaylist.isUmbrella && /* @__PURE__ */
|
|
609
|
+
genrePlaylist.isUmbrella && /* @__PURE__ */ jsx5("span", { className: "ml-2 text-xs italic text-[var(--color-text-muted)]", children: "umbrella" })
|
|
922
610
|
]
|
|
923
611
|
}
|
|
924
612
|
) }, genrePlaylist.uuid)),
|
|
925
|
-
extraResults && extraItems.length > 0 && /* @__PURE__ */
|
|
926
|
-
extraItems.map((item) => /* @__PURE__ */
|
|
613
|
+
extraResults && extraItems.length > 0 && /* @__PURE__ */ jsx5("li", { className: heading, children: extraResults.label }),
|
|
614
|
+
extraItems.map((item) => /* @__PURE__ */ jsx5("li", { children: /* @__PURE__ */ jsx5(
|
|
927
615
|
"button",
|
|
928
616
|
{
|
|
929
617
|
type: "button",
|
|
930
|
-
className: "w-full px-3 py-1.5 text-left hover:bg-[var(--color-surface-muted)]",
|
|
618
|
+
className: "w-full px-3 py-1.5 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] text-left hover:bg-[var(--color-surface-muted)]",
|
|
931
619
|
onClick: () => {
|
|
932
620
|
setQuery("");
|
|
933
621
|
onQueryChange?.("");
|
|
@@ -940,9 +628,231 @@ function GenreSearch({
|
|
|
940
628
|
] });
|
|
941
629
|
}
|
|
942
630
|
|
|
631
|
+
// src/genre-tree/GenreInfoPanelBody.tsx
|
|
632
|
+
import { usePlayer as usePlayer5 } from "@behindthemusictree/app-kit/player";
|
|
633
|
+
|
|
634
|
+
// src/genre-tree/GenreActionBand.tsx
|
|
635
|
+
import { ArrowLeftRight, MoveUpRight, Pause, Pencil, Play, Plus, ZoomIn } from "lucide-react";
|
|
636
|
+
|
|
637
|
+
// src/genre-tree/ShareActionButton.tsx
|
|
638
|
+
import { Share2 as Share22 } from "lucide-react";
|
|
639
|
+
import { usePopup as usePopup3 } from "@behindthemusictree/app-kit/popup";
|
|
640
|
+
|
|
641
|
+
// src/genre-tree/SharePopup.tsx
|
|
642
|
+
import { useCallback as useCallback5, useEffect as useEffect2, useMemo as useMemo5, useState as useState2 } from "react";
|
|
643
|
+
import { Link2, Share2 } from "lucide-react";
|
|
644
|
+
import { FaWhatsapp } from "react-icons/fa";
|
|
645
|
+
import { SiGmail } from "react-icons/si";
|
|
646
|
+
|
|
647
|
+
// src/genre-tree/ShareDestinationButton.tsx
|
|
648
|
+
import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
649
|
+
var className = "group flex min-w-0 flex-1 flex-col items-center justify-center gap-1.5 rounded-xl px-2 py-2 text-xs text-[var(--color-text)] transition-colors hover:bg-[var(--color-surface-muted)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:cursor-not-allowed disabled:opacity-50";
|
|
650
|
+
function Contents({ icon, label }) {
|
|
651
|
+
return /* @__PURE__ */ jsxs3(Fragment, { children: [
|
|
652
|
+
/* @__PURE__ */ jsx6("span", { className: "flex h-12 w-12 items-center justify-center rounded-full bg-[var(--color-surface-muted)] transition-colors group-hover:bg-[var(--color-border)]", children: icon }),
|
|
653
|
+
/* @__PURE__ */ jsx6("span", { className: "max-w-full truncate", children: label })
|
|
654
|
+
] });
|
|
655
|
+
}
|
|
656
|
+
function ShareDestinationButton({
|
|
657
|
+
label,
|
|
658
|
+
icon,
|
|
659
|
+
href,
|
|
660
|
+
onClick,
|
|
661
|
+
disabled = false,
|
|
662
|
+
ariaLabel
|
|
663
|
+
}) {
|
|
664
|
+
if (href) {
|
|
665
|
+
return /* @__PURE__ */ jsx6("a", { href, target: "_blank", rel: "noopener noreferrer", "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx6(Contents, { icon, label }) });
|
|
666
|
+
}
|
|
667
|
+
return /* @__PURE__ */ jsx6("button", { type: "button", onClick, disabled, "aria-label": ariaLabel, className, children: /* @__PURE__ */ jsx6(Contents, { icon, label }) });
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// src/genre-tree/SharePreviewCard.tsx
|
|
671
|
+
import { Music2, UserRound } from "lucide-react";
|
|
672
|
+
import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
673
|
+
function SharePreviewCard({ target, url }) {
|
|
674
|
+
const Icon = target.kind === "genre" ? Music2 : UserRound;
|
|
675
|
+
const typeLabel = target.kind === "genre" ? "Genre" : "Artist";
|
|
676
|
+
return /* @__PURE__ */ jsx7("div", { className: "rounded-3xl bg-[var(--color-surface-muted)] p-4", children: /* @__PURE__ */ jsxs4("div", { className: "flex min-h-32 overflow-hidden rounded-2xl bg-[var(--color-surface)] shadow-lg", children: [
|
|
677
|
+
/* @__PURE__ */ jsx7("div", { className: "flex w-28 shrink-0 items-center justify-center bg-[var(--color-primary)]/15 text-[var(--color-primary)]", children: /* @__PURE__ */ jsx7(Icon, { "aria-hidden": "true", className: "h-10 w-10" }) }),
|
|
678
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 flex-1 flex-col justify-center gap-1 p-4", children: [
|
|
679
|
+
/* @__PURE__ */ jsx7("span", { className: "text-xs font-medium uppercase tracking-wide text-[var(--color-text-muted)]", children: typeLabel }),
|
|
680
|
+
/* @__PURE__ */ jsx7("span", { className: "truncate text-base font-semibold text-[var(--color-text)]", title: target.name, children: target.name }),
|
|
681
|
+
/* @__PURE__ */ jsx7("span", { className: "truncate text-sm text-[var(--color-primary)]", title: url, children: url })
|
|
682
|
+
] })
|
|
683
|
+
] }) });
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/genre-tree/SharePopup.tsx
|
|
687
|
+
import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
688
|
+
function resolveAbsoluteUrl(url) {
|
|
689
|
+
try {
|
|
690
|
+
const parsed = new URL(url, window.location.href);
|
|
691
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:" ? parsed.href : void 0;
|
|
692
|
+
} catch {
|
|
693
|
+
return void 0;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async function copyText(text) {
|
|
697
|
+
try {
|
|
698
|
+
if (navigator.clipboard?.writeText) {
|
|
699
|
+
await navigator.clipboard.writeText(text);
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
const input = document.createElement("textarea");
|
|
705
|
+
input.value = text;
|
|
706
|
+
input.setAttribute("readonly", "");
|
|
707
|
+
input.style.position = "fixed";
|
|
708
|
+
input.style.opacity = "0";
|
|
709
|
+
document.body.append(input);
|
|
710
|
+
input.select();
|
|
711
|
+
try {
|
|
712
|
+
return document.execCommand("copy");
|
|
713
|
+
} catch {
|
|
714
|
+
return false;
|
|
715
|
+
} finally {
|
|
716
|
+
input.remove();
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
function SharePopup({ target, url, onClose }) {
|
|
720
|
+
const [absoluteUrl, setAbsoluteUrl] = useState2(url);
|
|
721
|
+
const [copyStatus, setCopyStatus] = useState2("idle");
|
|
722
|
+
const [canUseNativeShare, setCanUseNativeShare] = useState2(false);
|
|
723
|
+
const [shareError, setShareError] = useState2(null);
|
|
724
|
+
const [toastId, setToastId] = useState2(0);
|
|
725
|
+
useEffect2(() => {
|
|
726
|
+
setAbsoluteUrl(resolveAbsoluteUrl(url) ?? "");
|
|
727
|
+
setCopyStatus("idle");
|
|
728
|
+
setShareError(null);
|
|
729
|
+
}, [url]);
|
|
730
|
+
useEffect2(() => {
|
|
731
|
+
setCanUseNativeShare(typeof navigator !== "undefined" && typeof navigator.share === "function");
|
|
732
|
+
}, []);
|
|
733
|
+
const destinationUrls = useMemo5(() => {
|
|
734
|
+
if (!absoluteUrl) return null;
|
|
735
|
+
const message = `${target.name} \u2014 ${absoluteUrl}`;
|
|
736
|
+
return {
|
|
737
|
+
whatsapp: `https://wa.me/?text=${encodeURIComponent(message)}`,
|
|
738
|
+
gmail: `https://mail.google.com/mail/?view=cm&fs=1&su=${encodeURIComponent(`Share: ${target.name}`)}&body=${encodeURIComponent(message)}`
|
|
739
|
+
};
|
|
740
|
+
}, [absoluteUrl, target.name]);
|
|
741
|
+
const handleCopy = async () => {
|
|
742
|
+
if (!absoluteUrl) {
|
|
743
|
+
setCopyStatus("failed");
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
setShareError(null);
|
|
747
|
+
const copied = await copyText(absoluteUrl);
|
|
748
|
+
setCopyStatus(copied ? "idle" : "failed");
|
|
749
|
+
if (copied) setToastId((id) => id + 1);
|
|
750
|
+
};
|
|
751
|
+
const dismissToast = useCallback5(() => setToastId(0), []);
|
|
752
|
+
const handleNativeShare = async () => {
|
|
753
|
+
if (!absoluteUrl || !navigator.share) return;
|
|
754
|
+
setShareError(null);
|
|
755
|
+
try {
|
|
756
|
+
await navigator.share({ title: target.name, url: absoluteUrl });
|
|
757
|
+
} catch (error) {
|
|
758
|
+
if (error instanceof Error && error.name === "AbortError") return;
|
|
759
|
+
setShareError("Could not open the share options.");
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
return /* @__PURE__ */ jsxs5(Fragment2, { children: [
|
|
763
|
+
/* @__PURE__ */ jsx8(
|
|
764
|
+
BasePopup_default,
|
|
765
|
+
{
|
|
766
|
+
title: "Share",
|
|
767
|
+
onClose,
|
|
768
|
+
isDismissable: true,
|
|
769
|
+
trapFocus: true,
|
|
770
|
+
closeOnEscape: true,
|
|
771
|
+
closeOnBackdrop: true,
|
|
772
|
+
className: "!w-full !max-w-[28rem] overflow-hidden !rounded-[2rem] !bg-[var(--color-surface)] shadow-2xl",
|
|
773
|
+
overlayClassName: "!bg-black/50 p-4 backdrop-blur-[2px]",
|
|
774
|
+
titleClassName: "!rounded-none !bg-transparent !px-6 !pb-2 !pt-6",
|
|
775
|
+
titleTextClassName: "!text-xl !font-semibold !text-[var(--color-text)]",
|
|
776
|
+
closeButtonClassName: "!h-10 !w-10 !rounded-none !border-0 !bg-transparent !p-0 !text-[var(--color-text)] hover:!bg-transparent hover:!text-[var(--color-text)]",
|
|
777
|
+
contentClassName: "!rounded-none !bg-transparent !px-6 !pb-6 !pt-2",
|
|
778
|
+
children: /* @__PURE__ */ jsxs5("div", { className: "flex flex-col gap-4", children: [
|
|
779
|
+
/* @__PURE__ */ jsx8(SharePreviewCard, { target, url: absoluteUrl || url }),
|
|
780
|
+
/* @__PURE__ */ jsxs5("div", { className: "grid grid-cols-3 gap-1 rounded-3xl bg-[var(--color-surface)] p-3 sm:grid-cols-4", children: [
|
|
781
|
+
/* @__PURE__ */ jsx8(
|
|
782
|
+
ShareDestinationButton,
|
|
783
|
+
{
|
|
784
|
+
label: "Copy Link",
|
|
785
|
+
icon: /* @__PURE__ */ jsx8(Link2, { "aria-hidden": "true", className: "h-6 w-6" }),
|
|
786
|
+
onClick: () => void handleCopy(),
|
|
787
|
+
ariaLabel: "Copy link",
|
|
788
|
+
disabled: !absoluteUrl
|
|
789
|
+
}
|
|
790
|
+
),
|
|
791
|
+
/* @__PURE__ */ jsx8(
|
|
792
|
+
ShareDestinationButton,
|
|
793
|
+
{
|
|
794
|
+
label: "WhatsApp",
|
|
795
|
+
icon: /* @__PURE__ */ jsx8(FaWhatsapp, { "aria-hidden": "true", className: "h-9 w-9 text-[#25D366]" }),
|
|
796
|
+
href: destinationUrls?.whatsapp,
|
|
797
|
+
ariaLabel: `Share ${target.name} via WhatsApp`,
|
|
798
|
+
disabled: !absoluteUrl
|
|
799
|
+
}
|
|
800
|
+
),
|
|
801
|
+
/* @__PURE__ */ jsx8(
|
|
802
|
+
ShareDestinationButton,
|
|
803
|
+
{
|
|
804
|
+
label: "Gmail",
|
|
805
|
+
icon: /* @__PURE__ */ jsx8(SiGmail, { "aria-hidden": "true", className: "h-8 w-8 text-[#EA4335]" }),
|
|
806
|
+
href: destinationUrls?.gmail,
|
|
807
|
+
ariaLabel: `Email ${target.name} with Gmail`,
|
|
808
|
+
disabled: !absoluteUrl
|
|
809
|
+
}
|
|
810
|
+
),
|
|
811
|
+
canUseNativeShare && /* @__PURE__ */ jsx8(
|
|
812
|
+
ShareDestinationButton,
|
|
813
|
+
{
|
|
814
|
+
label: "More",
|
|
815
|
+
icon: /* @__PURE__ */ jsx8(Share2, { "aria-hidden": "true", className: "h-6 w-6" }),
|
|
816
|
+
onClick: () => void handleNativeShare(),
|
|
817
|
+
ariaLabel: "More sharing options",
|
|
818
|
+
disabled: !absoluteUrl
|
|
819
|
+
}
|
|
820
|
+
)
|
|
821
|
+
] }),
|
|
822
|
+
copyStatus === "failed" && /* @__PURE__ */ jsx8("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: "Could not copy the link." }),
|
|
823
|
+
shareError && /* @__PURE__ */ jsx8("p", { role: "alert", className: "text-center text-sm text-[var(--color-red-500)]", children: shareError })
|
|
824
|
+
] })
|
|
825
|
+
}
|
|
826
|
+
),
|
|
827
|
+
toastId > 0 && /* @__PURE__ */ jsx8(Toast, { message: "Link copied", onClose: dismissToast }, toastId)
|
|
828
|
+
] });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// src/genre-tree/ShareActionButton.tsx
|
|
832
|
+
import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
833
|
+
function ShareActionButton({ target, url }) {
|
|
834
|
+
const { showPopup, hidePopup } = usePopup3();
|
|
835
|
+
const openSharePopup = () => {
|
|
836
|
+
showPopup(/* @__PURE__ */ jsx9(SharePopup, { target, url, onClose: hidePopup }));
|
|
837
|
+
};
|
|
838
|
+
return /* @__PURE__ */ jsxs6(
|
|
839
|
+
"button",
|
|
840
|
+
{
|
|
841
|
+
type: "button",
|
|
842
|
+
onClick: openSharePopup,
|
|
843
|
+
className: "group flex flex-col items-center gap-1 w-16 text-xs text-center text-[var(--color-text)] focus-visible:outline-none",
|
|
844
|
+
"aria-label": `Share ${target.name}`,
|
|
845
|
+
title: `Share ${target.name}`,
|
|
846
|
+
children: [
|
|
847
|
+
/* @__PURE__ */ jsx9("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)]", children: /* @__PURE__ */ jsx9(Share22, { "aria-hidden": "true", className: "w-5 h-5" }) }),
|
|
848
|
+
"Share"
|
|
849
|
+
]
|
|
850
|
+
}
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
|
|
943
854
|
// src/genre-tree/GenreActionBand.tsx
|
|
944
|
-
import {
|
|
945
|
-
import { jsx as jsx11, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
855
|
+
import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
946
856
|
function GenreActionBand({
|
|
947
857
|
node,
|
|
948
858
|
onPlayPause,
|
|
@@ -951,7 +861,12 @@ function GenreActionBand({
|
|
|
951
861
|
onAddChild,
|
|
952
862
|
onRename,
|
|
953
863
|
onChangeParent,
|
|
954
|
-
|
|
864
|
+
side,
|
|
865
|
+
onToggleSide,
|
|
866
|
+
toggleSideDisabledReason,
|
|
867
|
+
additionalActions,
|
|
868
|
+
shareTarget,
|
|
869
|
+
shareUrl
|
|
955
870
|
}) {
|
|
956
871
|
const items = [];
|
|
957
872
|
const adminItems = [];
|
|
@@ -959,7 +874,7 @@ function GenreActionBand({
|
|
|
959
874
|
items.push({
|
|
960
875
|
key: "play-pause",
|
|
961
876
|
label: isPlaying ? "Pause" : "Play",
|
|
962
|
-
icon: isPlaying ? /* @__PURE__ */
|
|
877
|
+
icon: isPlaying ? /* @__PURE__ */ jsx10(Pause, { "aria-hidden": "true", className: "w-5 h-5" }) : /* @__PURE__ */ jsx10(Play, { "aria-hidden": "true", className: "w-5 h-5" }),
|
|
963
878
|
onClick: onPlayPause
|
|
964
879
|
});
|
|
965
880
|
}
|
|
@@ -967,18 +882,28 @@ function GenreActionBand({
|
|
|
967
882
|
items.push({
|
|
968
883
|
key: "zoom-in",
|
|
969
884
|
label: "Focus",
|
|
970
|
-
icon: /* @__PURE__ */
|
|
885
|
+
icon: /* @__PURE__ */ jsx10(ZoomIn, { "aria-hidden": "true", className: "w-5 h-5" }),
|
|
971
886
|
onClick: onZoomIn
|
|
972
887
|
});
|
|
973
888
|
}
|
|
974
889
|
if (onAddChild) {
|
|
975
|
-
adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */
|
|
890
|
+
adminItems.push({ key: "add-child", label: "Add", icon: /* @__PURE__ */ jsx10(Plus, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onAddChild });
|
|
976
891
|
}
|
|
977
892
|
if (onRename) {
|
|
978
|
-
adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */
|
|
893
|
+
adminItems.push({ key: "rename", label: "Rename", icon: /* @__PURE__ */ jsx10(Pencil, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onRename });
|
|
979
894
|
}
|
|
980
895
|
if (onChangeParent) {
|
|
981
|
-
adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */
|
|
896
|
+
adminItems.push({ key: "change-parent", label: "Parent", icon: /* @__PURE__ */ jsx10(MoveUpRight, { "aria-hidden": "true", className: "w-5 h-5" }), onClick: onChangeParent });
|
|
897
|
+
}
|
|
898
|
+
if (onToggleSide) {
|
|
899
|
+
adminItems.push({
|
|
900
|
+
key: "toggle-side",
|
|
901
|
+
label: side === "pop" ? "Make core" : "Make pop",
|
|
902
|
+
icon: /* @__PURE__ */ jsx10(ArrowLeftRight, { "aria-hidden": "true", className: "w-5 h-5" }),
|
|
903
|
+
onClick: onToggleSide,
|
|
904
|
+
disabled: !!toggleSideDisabledReason,
|
|
905
|
+
title: toggleSideDisabledReason
|
|
906
|
+
});
|
|
982
907
|
}
|
|
983
908
|
for (const action of additionalActions?.(node) ?? []) {
|
|
984
909
|
items.push({
|
|
@@ -989,48 +914,427 @@ function GenreActionBand({
|
|
|
989
914
|
disabled: action.enabled ? !action.enabled(node) : false
|
|
990
915
|
});
|
|
991
916
|
}
|
|
992
|
-
if (items.length === 0 && adminItems.length === 0) return null;
|
|
917
|
+
if (items.length === 0 && (!shareTarget || !shareUrl) && adminItems.length === 0) return null;
|
|
993
918
|
const renderItems = (list) => list.map((item) => /* @__PURE__ */ jsxs7(
|
|
994
919
|
"button",
|
|
995
920
|
{
|
|
996
921
|
type: "button",
|
|
997
922
|
disabled: item.disabled,
|
|
923
|
+
title: item.title,
|
|
998
924
|
onClick: item.onClick,
|
|
999
925
|
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",
|
|
1000
926
|
children: [
|
|
1001
|
-
/* @__PURE__ */
|
|
927
|
+
/* @__PURE__ */ jsx10("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 }),
|
|
1002
928
|
item.label
|
|
1003
929
|
]
|
|
1004
930
|
},
|
|
1005
931
|
item.key
|
|
1006
932
|
));
|
|
1007
933
|
return /* @__PURE__ */ jsxs7("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: [
|
|
1008
|
-
items.length > 0 && /* @__PURE__ */
|
|
934
|
+
(items.length > 0 || shareTarget && shareUrl) && /* @__PURE__ */ jsxs7("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: [
|
|
935
|
+
renderItems(items),
|
|
936
|
+
shareTarget && shareUrl && /* @__PURE__ */ jsx10(ShareActionButton, { target: shareTarget, url: shareUrl })
|
|
937
|
+
] }),
|
|
1009
938
|
adminItems.length > 0 && /* @__PURE__ */ jsxs7(
|
|
1010
939
|
"section",
|
|
1011
940
|
{
|
|
1012
941
|
"aria-label": "Admin actions",
|
|
1013
942
|
className: `px-3.5 py-3 ${items.length > 0 ? "border-t border-[var(--color-border)]" : ""}`,
|
|
1014
943
|
children: [
|
|
1015
|
-
/* @__PURE__ */
|
|
1016
|
-
/* @__PURE__ */
|
|
944
|
+
/* @__PURE__ */ jsx10("h3", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-[var(--color-text-muted)]", children: "Admin" }),
|
|
945
|
+
/* @__PURE__ */ jsx10("div", { className: "flex flex-wrap gap-1", children: renderItems(adminItems) })
|
|
1017
946
|
]
|
|
1018
947
|
}
|
|
1019
948
|
)
|
|
1020
949
|
] });
|
|
1021
950
|
}
|
|
1022
951
|
|
|
952
|
+
// src/genre-tree/GenreDetailExtras.tsx
|
|
953
|
+
import { Skeleton as Skeleton2 } from "@behindthemusictree/ui";
|
|
954
|
+
import { useFetchGenreOverview } from "@behindthemusictree/app-kit/genre-tree";
|
|
955
|
+
|
|
956
|
+
// src/genre-tree/ClampedText.tsx
|
|
957
|
+
import { useLayoutEffect, useRef as useRef2, useState as useState3 } from "react";
|
|
958
|
+
import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
959
|
+
function ClampedText({ children, className: className2 = "" }) {
|
|
960
|
+
const ref = useRef2(null);
|
|
961
|
+
const [expanded, setExpanded] = useState3(false);
|
|
962
|
+
const [overflows, setOverflows] = useState3(false);
|
|
963
|
+
useLayoutEffect(() => {
|
|
964
|
+
const element = ref.current;
|
|
965
|
+
if (!element || expanded) return;
|
|
966
|
+
setOverflows(element.scrollHeight > element.clientHeight);
|
|
967
|
+
}, [children, expanded]);
|
|
968
|
+
return /* @__PURE__ */ jsxs8(Fragment3, { children: [
|
|
969
|
+
/* @__PURE__ */ jsx11("p", { ref, className: `${className2} ${expanded ? "" : "line-clamp-3"}`, children }),
|
|
970
|
+
(overflows || expanded) && /* @__PURE__ */ jsx11(
|
|
971
|
+
"button",
|
|
972
|
+
{
|
|
973
|
+
type: "button",
|
|
974
|
+
"aria-expanded": expanded,
|
|
975
|
+
onClick: () => setExpanded((value) => !value),
|
|
976
|
+
className: "mt-1 text-xs text-[var(--color-text-muted)] underline hover:text-[var(--color-text)]",
|
|
977
|
+
children: expanded ? "Show less" : "Show more"
|
|
978
|
+
}
|
|
979
|
+
)
|
|
980
|
+
] });
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// src/genre-tree/GenrePlaylistTracks.tsx
|
|
984
|
+
import { useEffect as useEffect3, useRef as useRef3 } from "react";
|
|
985
|
+
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
986
|
+
import { z } from "zod";
|
|
987
|
+
import { FaPlay, FaPause } from "react-icons/fa";
|
|
988
|
+
import { RingLoader, Skeleton } from "@behindthemusictree/ui";
|
|
989
|
+
import { useSession } from "@behindthemusictree/app-kit/auth";
|
|
990
|
+
import { useFetchWrapper } from "@behindthemusictree/app-kit/transport";
|
|
991
|
+
import { PaginatedResponseSchema } from "@behindthemusictree/app-kit/transport";
|
|
992
|
+
import { parseWithLog } from "@behindthemusictree/app-kit/transport";
|
|
993
|
+
import { usePlayer as usePlayer4 } from "@behindthemusictree/app-kit/player";
|
|
994
|
+
import { PlayStates } from "@behindthemusictree/app-kit/player";
|
|
995
|
+
import { usePopup as usePopup4 } from "@behindthemusictree/app-kit/popup";
|
|
996
|
+
import { ErrorCode as ErrorCode3 } from "@behindthemusictree/app-kit/transport";
|
|
997
|
+
import { genrePlaylistQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
|
|
998
|
+
import { libraryEndpoints, libraryQueryKeys } from "@behindthemusictree/app-kit/genre-tree";
|
|
999
|
+
import { isSong, songUnplayableReason, SongDetailedSchema } from "@behindthemusictree/app-kit/genre-tree";
|
|
1000
|
+
import { useTrackList as useTrackList2 } from "@behindthemusictree/app-kit/genre-tree";
|
|
1001
|
+
|
|
1002
|
+
// src/genre-tree/MusicBrainzRecordingLink.tsx
|
|
1003
|
+
import { musicBrainzRecordingUrl } from "@behindthemusictree/app-kit/genre-tree";
|
|
1004
|
+
import { Fragment as Fragment4, jsx as jsx12 } from "react/jsx-runtime";
|
|
1005
|
+
function MusicBrainzRecordingLink({
|
|
1006
|
+
song,
|
|
1007
|
+
children,
|
|
1008
|
+
className: className2 = ""
|
|
1009
|
+
}) {
|
|
1010
|
+
const url = musicBrainzRecordingUrl(song);
|
|
1011
|
+
if (!url) return /* @__PURE__ */ jsx12(Fragment4, { children });
|
|
1012
|
+
return /* @__PURE__ */ jsx12(
|
|
1013
|
+
"a",
|
|
1014
|
+
{
|
|
1015
|
+
href: url,
|
|
1016
|
+
target: "_blank",
|
|
1017
|
+
rel: "noopener noreferrer",
|
|
1018
|
+
"aria-label": `View ${song.title} on MusicBrainz`,
|
|
1019
|
+
className: className2,
|
|
1020
|
+
onClick: (event) => event.stopPropagation(),
|
|
1021
|
+
children
|
|
1022
|
+
}
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/genre-tree/GenrePlaylistTracks.tsx
|
|
1027
|
+
import { ArtistNames } from "@behindthemusictree/app-kit/genre-tree";
|
|
1028
|
+
import { capitalize as capitalize4, useTrackNoun as useTrackNoun4 } from "@behindthemusictree/app-kit/track-noun";
|
|
1029
|
+
import { jsx as jsx13, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
1030
|
+
var CATALOGUE_PAGE_SIZE = 100;
|
|
1031
|
+
var SongPageSchema = PaginatedResponseSchema(SongDetailedSchema).extend({
|
|
1032
|
+
overallTotal: z.number().nullable(),
|
|
1033
|
+
page: z.number().nullable(),
|
|
1034
|
+
totalPages: z.number().nullable()
|
|
1035
|
+
});
|
|
1036
|
+
var TEXT_CELL = "min-w-0 flex-1 truncate [@container(min-width:40rem)]:flex [@container(min-width:40rem)]:gap-2";
|
|
1037
|
+
var TITLE_CELL = "[@container(min-width:40rem)]:flex-[2] [@container(min-width:40rem)]:truncate";
|
|
1038
|
+
var COLUMN_CELL = "[@container(min-width:40rem)]:flex-1 [@container(min-width:40rem)]:truncate";
|
|
1039
|
+
var ALBUM_CELL = `hidden [@container(min-width:40rem)]:block ${COLUMN_CELL}`;
|
|
1040
|
+
var SEPARATOR = "[@container(min-width:40rem)]:hidden";
|
|
1041
|
+
var nextCursor = (next) => {
|
|
1042
|
+
const cursor = new URL(next).searchParams.get("cursor");
|
|
1043
|
+
if (!cursor) throw new Error(`Songs next link has no cursor: ${next}`);
|
|
1044
|
+
return cursor;
|
|
1045
|
+
};
|
|
1046
|
+
function GenrePlaylistTracks({
|
|
1047
|
+
genreUuid,
|
|
1048
|
+
genrePlaylist,
|
|
1049
|
+
scope,
|
|
1050
|
+
getBackendBaseUrl,
|
|
1051
|
+
renderArtistLink,
|
|
1052
|
+
renderSongLeading,
|
|
1053
|
+
renderSongActions,
|
|
1054
|
+
renderTracksToolbar,
|
|
1055
|
+
artist = null
|
|
1056
|
+
}) {
|
|
1057
|
+
const { fetchGenrePlaylistTracksPage, playNewTrackListFromGenrePlaylist, playNewTrackListFromArtist } = useTrackList2();
|
|
1058
|
+
const { session, sessionRestored } = useSession();
|
|
1059
|
+
const { playerTrackObject, playState, handlePlayPauseAction } = usePlayer4();
|
|
1060
|
+
const { showPopup } = usePopup4();
|
|
1061
|
+
const trackNoun = useTrackNoun4();
|
|
1062
|
+
const { uuid } = genrePlaylist;
|
|
1063
|
+
const { fetch } = useFetchWrapper(getBackendBaseUrl);
|
|
1064
|
+
const { data, isPending, isError, refetch, hasNextPage, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({
|
|
1065
|
+
queryKey: scope === "me" ? genrePlaylistQueryKeys.me.tracks(uuid) : artist ? [...libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid), "artist", artist.uuid] : libraryQueryKeys.reference.songs.byGenre(getBackendBaseUrl(), genreUuid),
|
|
1066
|
+
queryFn: async ({ pageParam }) => {
|
|
1067
|
+
if (scope === "me") return fetchGenrePlaylistTracksPage(uuid, scope, Number(pageParam));
|
|
1068
|
+
const response = await fetch(
|
|
1069
|
+
libraryEndpoints.reference.songs.list(),
|
|
1070
|
+
true,
|
|
1071
|
+
false,
|
|
1072
|
+
{},
|
|
1073
|
+
{ genre: genreUuid, ...artist && { artist: artist.uuid }, pageSize: CATALOGUE_PAGE_SIZE, withTotal: false, ...pageParam && { cursor: pageParam } }
|
|
1074
|
+
);
|
|
1075
|
+
const parsed = parseWithLog(SongPageSchema, response, "GenrePlaylistTracks");
|
|
1076
|
+
return { tracks: parsed.results, total: parsed.overallTotal, nextPage: parsed.next ? nextCursor(parsed.next) : null };
|
|
1077
|
+
},
|
|
1078
|
+
initialPageParam: scope === "me" ? 1 : "",
|
|
1079
|
+
getNextPageParam: (lastPage) => lastPage.nextPage ?? void 0,
|
|
1080
|
+
enabled: scope !== "me" || sessionRestored && !!session?.accessToken
|
|
1081
|
+
});
|
|
1082
|
+
const sentinelRef = useRef3(null);
|
|
1083
|
+
useEffect3(() => {
|
|
1084
|
+
const sentinel = sentinelRef.current;
|
|
1085
|
+
if (!hasNextPage || isFetchingNextPage || !sentinel) return;
|
|
1086
|
+
const observer = new IntersectionObserver((entries) => {
|
|
1087
|
+
if (entries.some((entry) => entry.isIntersecting)) {
|
|
1088
|
+
void fetchNextPage();
|
|
1089
|
+
}
|
|
1090
|
+
});
|
|
1091
|
+
observer.observe(sentinel);
|
|
1092
|
+
return () => observer.disconnect();
|
|
1093
|
+
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);
|
|
1094
|
+
const pages = data?.pages ?? [];
|
|
1095
|
+
const tracks = pages.flatMap((page) => page.tracks);
|
|
1096
|
+
const total = pages[0]?.total ?? void 0;
|
|
1097
|
+
const play = (startSongUuid) => {
|
|
1098
|
+
const started = artist ? playNewTrackListFromArtist(artist, scope, { startSongUuid, ordering: "title", genreUuid }) : playNewTrackListFromGenrePlaylist(genrePlaylist, scope, { startSongUuid });
|
|
1099
|
+
started.catch((error) => {
|
|
1100
|
+
console.error("Failed to play genre playlist:", error);
|
|
1101
|
+
showPopup(/* @__PURE__ */ jsx13(InternalErrorPopup, { errorCode: ErrorCode3.CLIENT_UNKNOWN }));
|
|
1102
|
+
});
|
|
1103
|
+
};
|
|
1104
|
+
return /* @__PURE__ */ jsxs9("div", { className: "gtv-info-panel-children", children: [
|
|
1105
|
+
/* @__PURE__ */ jsxs9("span", { className: "gtv-info-panel-children-title", children: [
|
|
1106
|
+
capitalize4(trackNoun.plural),
|
|
1107
|
+
total !== void 0 && ` (${total})`
|
|
1108
|
+
] }),
|
|
1109
|
+
renderTracksToolbar?.(),
|
|
1110
|
+
isPending ? /* @__PURE__ */ jsx13(Skeleton, { className: "h-4 w-3/4", "aria-busy": "true" }) : isError && tracks.length === 0 ? /* @__PURE__ */ jsxs9("p", { role: "alert", children: [
|
|
1111
|
+
"Couldn't load ",
|
|
1112
|
+
trackNoun.plural,
|
|
1113
|
+
".",
|
|
1114
|
+
" ",
|
|
1115
|
+
/* @__PURE__ */ jsx13("button", { type: "button", className: "underline", onClick: () => void refetch(), children: "Retry" })
|
|
1116
|
+
] }) : tracks.length === 0 ? /* @__PURE__ */ jsx13("p", { children: "\u2014" }) : /* @__PURE__ */ jsxs9("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: [
|
|
1117
|
+
tracks.map((track, index) => {
|
|
1118
|
+
const label = track.artists?.length ? `${track.title} \u2014 ${track.artists.map((artist2) => artist2.name).join(", ")}` : track.title;
|
|
1119
|
+
const isCurrent = playerTrackObject?.track.id === track.uuid;
|
|
1120
|
+
const isPlaying = isCurrent && playState === PlayStates.PLAYING;
|
|
1121
|
+
const text = /* @__PURE__ */ jsxs9("span", { className: TEXT_CELL, children: [
|
|
1122
|
+
/* @__PURE__ */ jsx13("span", { className: TITLE_CELL, children: isSong(track) ? /* @__PURE__ */ jsx13(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }),
|
|
1123
|
+
track.artists?.length ? /* @__PURE__ */ jsx13("span", { className: SEPARATOR, children: " \u2014 " }) : null,
|
|
1124
|
+
/* @__PURE__ */ jsx13("span", { className: COLUMN_CELL, children: track.artists?.length ? /* @__PURE__ */ jsx13(ArtistNames, { artists: track.artists, renderArtistLink }) : null }),
|
|
1125
|
+
track.album && /* @__PURE__ */ jsx13("span", { className: ALBUM_CELL, children: track.album.name })
|
|
1126
|
+
] });
|
|
1127
|
+
const leading = renderSongLeading?.(track, index);
|
|
1128
|
+
const actions = renderSongActions?.(track);
|
|
1129
|
+
const isUnplayable = isSong(track) && songUnplayableReason(track) !== null;
|
|
1130
|
+
if (isUnplayable) {
|
|
1131
|
+
return /* @__PURE__ */ jsxs9("li", { className: "flex min-h-8 items-center gap-1 px-1 opacity-50", children: [
|
|
1132
|
+
leading,
|
|
1133
|
+
/* @__PURE__ */ jsx13(
|
|
1134
|
+
"button",
|
|
1135
|
+
{
|
|
1136
|
+
type: "button",
|
|
1137
|
+
disabled: true,
|
|
1138
|
+
"aria-disabled": "true",
|
|
1139
|
+
"aria-label": label,
|
|
1140
|
+
className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full opacity-0 cursor-not-allowed",
|
|
1141
|
+
children: /* @__PURE__ */ jsx13(FaPlay, { size: 10 })
|
|
1142
|
+
}
|
|
1143
|
+
),
|
|
1144
|
+
text,
|
|
1145
|
+
actions
|
|
1146
|
+
] }, track.uuid);
|
|
1147
|
+
}
|
|
1148
|
+
return /* @__PURE__ */ jsxs9(
|
|
1149
|
+
"li",
|
|
1150
|
+
{
|
|
1151
|
+
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)]" : ""}`,
|
|
1152
|
+
children: [
|
|
1153
|
+
leading,
|
|
1154
|
+
/* @__PURE__ */ jsx13(
|
|
1155
|
+
"button",
|
|
1156
|
+
{
|
|
1157
|
+
type: "button",
|
|
1158
|
+
"aria-label": `${isPlaying ? "Pause" : "Play"} ${label}`,
|
|
1159
|
+
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"}`,
|
|
1160
|
+
onClick: () => isCurrent ? handlePlayPauseAction() : play(track.uuid),
|
|
1161
|
+
children: isCurrent && playState === PlayStates.LOADING ? /* @__PURE__ */ jsx13(RingLoader, { size: 10 }) : isPlaying ? /* @__PURE__ */ jsx13(FaPause, { size: 10 }) : /* @__PURE__ */ jsx13(FaPlay, { size: 10 })
|
|
1162
|
+
}
|
|
1163
|
+
),
|
|
1164
|
+
text,
|
|
1165
|
+
actions
|
|
1166
|
+
]
|
|
1167
|
+
},
|
|
1168
|
+
track.uuid
|
|
1169
|
+
);
|
|
1170
|
+
}),
|
|
1171
|
+
hasNextPage && /* @__PURE__ */ jsx13("li", { ref: sentinelRef, "aria-hidden": "true", className: "h-4" })
|
|
1172
|
+
] })
|
|
1173
|
+
] });
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// src/genre-tree/GenreDetailExtras.tsx
|
|
1177
|
+
import { useTrackNoun as useTrackNoun5 } from "@behindthemusictree/app-kit/track-noun";
|
|
1178
|
+
import { SiWikidata, SiWikipedia } from "react-icons/si";
|
|
1179
|
+
import { Fragment as Fragment5, jsx as jsx14, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
1180
|
+
function GenreDetailExtras({
|
|
1181
|
+
genreUuid,
|
|
1182
|
+
genrePlaylist,
|
|
1183
|
+
wikidataId,
|
|
1184
|
+
scope,
|
|
1185
|
+
getBackendBaseUrl,
|
|
1186
|
+
criteriaOverviewSchema,
|
|
1187
|
+
renderGenreDetailExtras,
|
|
1188
|
+
renderArtistLink,
|
|
1189
|
+
renderSummary,
|
|
1190
|
+
renderEssentialTracks,
|
|
1191
|
+
family,
|
|
1192
|
+
...trackSlots
|
|
1193
|
+
}) {
|
|
1194
|
+
const trackNoun = useTrackNoun5();
|
|
1195
|
+
const { data: overview, isPending } = useFetchGenreOverview(
|
|
1196
|
+
genreUuid,
|
|
1197
|
+
scope,
|
|
1198
|
+
getBackendBaseUrl,
|
|
1199
|
+
criteriaOverviewSchema
|
|
1200
|
+
);
|
|
1201
|
+
if (isPending) {
|
|
1202
|
+
return /* @__PURE__ */ jsxs10(Fragment5, { children: [
|
|
1203
|
+
/* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children", "aria-busy": "true", "data-testid": "genre-detail-extras-skeleton", children: [
|
|
1204
|
+
/* @__PURE__ */ jsx14("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
|
|
1205
|
+
/* @__PURE__ */ jsx14(Skeleton2, { className: "h-4 w-3/4" })
|
|
1206
|
+
] }),
|
|
1207
|
+
family
|
|
1208
|
+
] });
|
|
1209
|
+
}
|
|
1210
|
+
if (!overview) return /* @__PURE__ */ jsx14(Fragment5, { children: family });
|
|
1211
|
+
const { summary, summarySourceUrl, essentialSongs } = overview;
|
|
1212
|
+
const wikidataUrl = wikidataId ? `https://www.wikidata.org/wiki/${encodeURIComponent(wikidataId)}` : null;
|
|
1213
|
+
return /* @__PURE__ */ jsxs10(Fragment5, { children: [
|
|
1214
|
+
(summarySourceUrl || wikidataUrl) && /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
|
|
1215
|
+
/* @__PURE__ */ jsx14("span", { className: "gtv-info-panel-children-title", children: "Links" }),
|
|
1216
|
+
/* @__PURE__ */ jsxs10("div", { className: "mt-1 flex flex-wrap gap-x-3 gap-y-1 text-xs", children: [
|
|
1217
|
+
summarySourceUrl && /* @__PURE__ */ jsxs10(
|
|
1218
|
+
"a",
|
|
1219
|
+
{
|
|
1220
|
+
href: summarySourceUrl,
|
|
1221
|
+
target: "_blank",
|
|
1222
|
+
rel: "noopener noreferrer",
|
|
1223
|
+
className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
|
|
1224
|
+
children: [
|
|
1225
|
+
/* @__PURE__ */ jsx14(SiWikipedia, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
|
|
1226
|
+
"Wikipedia"
|
|
1227
|
+
]
|
|
1228
|
+
}
|
|
1229
|
+
),
|
|
1230
|
+
wikidataUrl && /* @__PURE__ */ jsxs10(
|
|
1231
|
+
"a",
|
|
1232
|
+
{
|
|
1233
|
+
href: wikidataUrl,
|
|
1234
|
+
target: "_blank",
|
|
1235
|
+
rel: "noopener noreferrer",
|
|
1236
|
+
className: "inline-flex items-center gap-1.5 text-[var(--color-text-muted)] hover:text-[var(--color-text)]",
|
|
1237
|
+
children: [
|
|
1238
|
+
/* @__PURE__ */ jsx14(SiWikidata, { "aria-hidden": "true", className: "h-3.5 w-3.5" }),
|
|
1239
|
+
"Wikidata"
|
|
1240
|
+
]
|
|
1241
|
+
}
|
|
1242
|
+
)
|
|
1243
|
+
] })
|
|
1244
|
+
] }),
|
|
1245
|
+
renderSummary ? renderSummary(overview) : /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
|
|
1246
|
+
/* @__PURE__ */ jsx14("span", { className: "gtv-info-panel-children-title", children: "Summary" }),
|
|
1247
|
+
/* @__PURE__ */ jsx14(ClampedText, { className: "mt-1", children: summary ?? "\u2014" }, genreUuid)
|
|
1248
|
+
] }),
|
|
1249
|
+
family,
|
|
1250
|
+
renderGenreDetailExtras?.(overview),
|
|
1251
|
+
renderEssentialTracks ? renderEssentialTracks(overview) : essentialSongs && /* @__PURE__ */ jsxs10("div", { className: "gtv-info-panel-children text-[0.8125rem] leading-[1.45] text-[var(--color-text)]", children: [
|
|
1252
|
+
/* @__PURE__ */ jsxs10("span", { className: "gtv-info-panel-children-title", children: [
|
|
1253
|
+
"Essential ",
|
|
1254
|
+
trackNoun.plural
|
|
1255
|
+
] }),
|
|
1256
|
+
essentialSongs.length > 0 ? /* @__PURE__ */ jsx14("ul", { className: "mt-1 divide-y divide-[var(--color-border)]", children: essentialSongs.map((track) => /* @__PURE__ */ jsx14("li", { className: "py-1", children: track.title }, track.uuid)) }) : /* @__PURE__ */ jsx14("p", { className: "mt-1", children: "\u2014" })
|
|
1257
|
+
] }),
|
|
1258
|
+
/* @__PURE__ */ jsx14(
|
|
1259
|
+
GenrePlaylistTracks,
|
|
1260
|
+
{
|
|
1261
|
+
genreUuid,
|
|
1262
|
+
genrePlaylist,
|
|
1263
|
+
scope,
|
|
1264
|
+
getBackendBaseUrl,
|
|
1265
|
+
renderArtistLink,
|
|
1266
|
+
...trackSlots
|
|
1267
|
+
}
|
|
1268
|
+
)
|
|
1269
|
+
] });
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
// src/genre-tree/GenreInfoPanelBody.tsx
|
|
1273
|
+
import { Fragment as Fragment6, jsx as jsx15, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
1274
|
+
import { createElement } from "react";
|
|
1275
|
+
function GenreNodeActionBand({ genrePlaylist, playback, ...bandProps }) {
|
|
1276
|
+
const { isPlaying, setIsPlaying } = usePlayer5();
|
|
1277
|
+
const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
|
|
1278
|
+
const handlePlayPause = () => {
|
|
1279
|
+
if (isCurrentTrackList) {
|
|
1280
|
+
setIsPlaying(!isPlaying);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
playback.play(genrePlaylist).catch((error) => {
|
|
1284
|
+
console.error("Failed to load genre playlist tracks:", error);
|
|
1285
|
+
});
|
|
1286
|
+
};
|
|
1287
|
+
return /* @__PURE__ */ jsx15(
|
|
1288
|
+
GenreActionBand,
|
|
1289
|
+
{
|
|
1290
|
+
...bandProps,
|
|
1291
|
+
onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
|
|
1292
|
+
isPlaying: isCurrentTrackList && isPlaying
|
|
1293
|
+
}
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
function GenreInfoPanelBody({
|
|
1297
|
+
toolbar,
|
|
1298
|
+
family,
|
|
1299
|
+
genreUuid,
|
|
1300
|
+
genrePlaylist,
|
|
1301
|
+
wikidataId,
|
|
1302
|
+
scope,
|
|
1303
|
+
getBackendBaseUrl,
|
|
1304
|
+
playback,
|
|
1305
|
+
band,
|
|
1306
|
+
extras
|
|
1307
|
+
}) {
|
|
1308
|
+
return /* @__PURE__ */ jsxs11(Fragment6, { children: [
|
|
1309
|
+
toolbar,
|
|
1310
|
+
/* @__PURE__ */ jsx15(GenreNodeActionBand, { ...band, genrePlaylist, playback }),
|
|
1311
|
+
/* @__PURE__ */ createElement(
|
|
1312
|
+
GenreDetailExtras,
|
|
1313
|
+
{
|
|
1314
|
+
...extras,
|
|
1315
|
+
key: genreUuid,
|
|
1316
|
+
family,
|
|
1317
|
+
genreUuid,
|
|
1318
|
+
genrePlaylist,
|
|
1319
|
+
wikidataId,
|
|
1320
|
+
scope,
|
|
1321
|
+
getBackendBaseUrl
|
|
1322
|
+
}
|
|
1323
|
+
)
|
|
1324
|
+
] });
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1023
1327
|
// src/genre-tree/PanelEdgeTab.tsx
|
|
1024
1328
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
1025
1329
|
import { twMerge } from "tailwind-merge";
|
|
1026
|
-
import { jsx as
|
|
1330
|
+
import { jsx as jsx16, jsxs as jsxs12 } from "react/jsx-runtime";
|
|
1027
1331
|
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)]";
|
|
1028
|
-
function PanelEdgeTab({ side, edge, center, controls, className }) {
|
|
1332
|
+
function PanelEdgeTab({ side, edge, center, controls, className: className2 }) {
|
|
1029
1333
|
if (!edge && !center) return null;
|
|
1030
1334
|
const isLeft = side === "left";
|
|
1031
1335
|
const EdgeIcon = isLeft ? ChevronLeft : ChevronRight;
|
|
1032
1336
|
const CenterIcon = isLeft ? ChevronRight : ChevronLeft;
|
|
1033
|
-
const renderButton = (action, icon) => /* @__PURE__ */
|
|
1337
|
+
const renderButton = (action, icon) => /* @__PURE__ */ jsx16(
|
|
1034
1338
|
"button",
|
|
1035
1339
|
{
|
|
1036
1340
|
type: "button",
|
|
@@ -1043,33 +1347,46 @@ function PanelEdgeTab({ side, edge, center, controls, className }) {
|
|
|
1043
1347
|
children: icon
|
|
1044
1348
|
}
|
|
1045
1349
|
);
|
|
1046
|
-
const edgeButton = edge && renderButton(edge, /* @__PURE__ */
|
|
1047
|
-
const centerButton = center && renderButton(center, /* @__PURE__ */
|
|
1350
|
+
const edgeButton = edge && renderButton(edge, /* @__PURE__ */ jsx16(EdgeIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
|
|
1351
|
+
const centerButton = center && renderButton(center, /* @__PURE__ */ jsx16(CenterIcon, { "aria-hidden": "true", className: "h-5 w-5" }));
|
|
1048
1352
|
const buttons = isLeft ? [edgeButton, centerButton] : [centerButton, edgeButton];
|
|
1049
|
-
return /* @__PURE__ */
|
|
1353
|
+
return /* @__PURE__ */ jsxs12(
|
|
1050
1354
|
"div",
|
|
1051
1355
|
{
|
|
1052
1356
|
className: twMerge(
|
|
1053
1357
|
"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)]",
|
|
1054
1358
|
isLeft ? "rounded-r-lg" : "rounded-l-lg [clip-path:inset(-0.5rem_0_-0.5rem_-0.5rem)]",
|
|
1055
|
-
|
|
1359
|
+
className2
|
|
1056
1360
|
),
|
|
1057
1361
|
children: [
|
|
1058
1362
|
buttons[0],
|
|
1059
|
-
buttons[0] && buttons[1] && /* @__PURE__ */
|
|
1363
|
+
buttons[0] && buttons[1] && /* @__PURE__ */ jsx16("div", { role: "presentation", className: "h-px shrink-0 bg-[var(--color-border)]" }),
|
|
1060
1364
|
buttons[1]
|
|
1061
1365
|
]
|
|
1062
1366
|
}
|
|
1063
1367
|
);
|
|
1064
1368
|
}
|
|
1065
1369
|
|
|
1370
|
+
// src/genre-tree/useLeftDetailPanelLayout.ts
|
|
1371
|
+
import { useEffect as useEffect4 } from "react";
|
|
1372
|
+
function useLeftDetailPanelLayout(isPanelShown, enabled = true) {
|
|
1373
|
+
useEffect4(() => {
|
|
1374
|
+
if (!enabled || isPanelShown) return void 0;
|
|
1375
|
+
const root = document.documentElement;
|
|
1376
|
+
root.style.setProperty("--layout-left-w", "0px");
|
|
1377
|
+
return () => {
|
|
1378
|
+
root.style.removeProperty("--layout-left-w");
|
|
1379
|
+
};
|
|
1380
|
+
}, [enabled, isPanelShown]);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1066
1383
|
// src/genre-tree/ChangeParentPopup.tsx
|
|
1067
|
-
import { useEffect as
|
|
1384
|
+
import { useEffect as useEffect5, useMemo as useMemo6 } from "react";
|
|
1068
1385
|
import {
|
|
1069
1386
|
getReparentCandidates,
|
|
1070
1387
|
useUpdateGenre
|
|
1071
1388
|
} from "@behindthemusictree/app-kit/genre-tree";
|
|
1072
|
-
import { jsx as
|
|
1389
|
+
import { jsx as jsx17, jsxs as jsxs13 } from "react/jsx-runtime";
|
|
1073
1390
|
function ChangeParentPopup({
|
|
1074
1391
|
genrePlaylist,
|
|
1075
1392
|
genrePlaylists,
|
|
@@ -1079,7 +1396,7 @@ function ChangeParentPopup({
|
|
|
1079
1396
|
onParentChanged
|
|
1080
1397
|
}) {
|
|
1081
1398
|
const { mutate, isPending, formErrors } = useUpdateGenre(scope, getBackendBaseUrl);
|
|
1082
|
-
const candidates =
|
|
1399
|
+
const candidates = useMemo6(
|
|
1083
1400
|
() => getReparentCandidates(genrePlaylists, genrePlaylist.uuid),
|
|
1084
1401
|
[genrePlaylists, genrePlaylist.uuid]
|
|
1085
1402
|
);
|
|
@@ -1097,19 +1414,19 @@ function ChangeParentPopup({
|
|
|
1097
1414
|
}
|
|
1098
1415
|
);
|
|
1099
1416
|
};
|
|
1100
|
-
|
|
1417
|
+
useEffect5(() => {
|
|
1101
1418
|
const handleKeyDown = (event) => {
|
|
1102
1419
|
if (event.key === "Escape") onClose();
|
|
1103
1420
|
};
|
|
1104
1421
|
window.addEventListener("keydown", handleKeyDown);
|
|
1105
1422
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
1106
1423
|
}, [onClose]);
|
|
1107
|
-
return /* @__PURE__ */
|
|
1108
|
-
/* @__PURE__ */
|
|
1424
|
+
return /* @__PURE__ */ jsxs13(BasePopup_default, { title: `Change parent of ${genrePlaylist.name}`, onClose, showCancelButton: true, fitContent: true, children: [
|
|
1425
|
+
/* @__PURE__ */ jsxs13("p", { className: "mb-3 text-sm text-[var(--color-text-muted)]", children: [
|
|
1109
1426
|
"Current parent: ",
|
|
1110
1427
|
genrePlaylist.parent?.name ?? "none"
|
|
1111
1428
|
] }),
|
|
1112
|
-
/* @__PURE__ */
|
|
1429
|
+
/* @__PURE__ */ jsx17("div", { "aria-busy": isPending, className: isPending ? "pointer-events-none opacity-60" : void 0, children: /* @__PURE__ */ jsx17(
|
|
1113
1430
|
GenreSearch,
|
|
1114
1431
|
{
|
|
1115
1432
|
genrePlaylists: candidates,
|
|
@@ -1117,7 +1434,7 @@ function ChangeParentPopup({
|
|
|
1117
1434
|
placeholder: "Search a new parent\u2026"
|
|
1118
1435
|
}
|
|
1119
1436
|
) }),
|
|
1120
|
-
formErrors.length > 0 && /* @__PURE__ */
|
|
1437
|
+
formErrors.length > 0 && /* @__PURE__ */ jsx17("p", { role: "alert", className: "mt-3 text-sm text-[var(--color-red-500)]", children: formErrors.map(({ message }) => message).join(" ") })
|
|
1121
1438
|
] });
|
|
1122
1439
|
}
|
|
1123
1440
|
|
|
@@ -1129,29 +1446,29 @@ import { useArtist } from "@behindthemusictree/app-kit/genre-tree";
|
|
|
1129
1446
|
import { FieldList } from "@behindthemusictree/ui";
|
|
1130
1447
|
|
|
1131
1448
|
// src/genre-tree/artist/ArtistLinks.tsx
|
|
1132
|
-
import { Globe, Share2 } from "lucide-react";
|
|
1449
|
+
import { Globe, Share2 as Share23 } from "lucide-react";
|
|
1133
1450
|
import {
|
|
1134
1451
|
SiBandcamp,
|
|
1135
1452
|
SiDiscogs,
|
|
1136
1453
|
SiMusicbrainz,
|
|
1137
1454
|
SiSoundcloud,
|
|
1138
1455
|
SiSpotify,
|
|
1139
|
-
SiWikidata,
|
|
1140
|
-
SiWikipedia,
|
|
1456
|
+
SiWikidata as SiWikidata2,
|
|
1457
|
+
SiWikipedia as SiWikipedia2,
|
|
1141
1458
|
SiYoutube
|
|
1142
1459
|
} from "react-icons/si";
|
|
1143
1460
|
import { Chip } from "@behindthemusictree/ui";
|
|
1144
|
-
import { jsx as
|
|
1461
|
+
import { jsx as jsx18 } from "react/jsx-runtime";
|
|
1145
1462
|
var LINK_KINDS = {
|
|
1146
1463
|
official: { label: "Official website", Icon: Globe },
|
|
1147
|
-
wikipedia: { label: "Wikipedia", Icon:
|
|
1148
|
-
wikidata: { label: "Wikidata", Icon:
|
|
1464
|
+
wikipedia: { label: "Wikipedia", Icon: SiWikipedia2 },
|
|
1465
|
+
wikidata: { label: "Wikidata", Icon: SiWikidata2 },
|
|
1149
1466
|
spotify: { label: "Spotify", Icon: SiSpotify },
|
|
1150
1467
|
youtube: { label: "YouTube", Icon: SiYoutube },
|
|
1151
1468
|
bandcamp: { label: "Bandcamp", Icon: SiBandcamp },
|
|
1152
1469
|
soundcloud: { label: "SoundCloud", Icon: SiSoundcloud },
|
|
1153
1470
|
discogs: { label: "Discogs", Icon: SiDiscogs },
|
|
1154
|
-
social: { label: "Social", Icon:
|
|
1471
|
+
social: { label: "Social", Icon: Share23 }
|
|
1155
1472
|
};
|
|
1156
1473
|
function socialLabel(url) {
|
|
1157
1474
|
try {
|
|
@@ -1178,7 +1495,7 @@ function artistLinks(artist) {
|
|
|
1178
1495
|
return links;
|
|
1179
1496
|
}
|
|
1180
1497
|
function ArtistLinks({ links }) {
|
|
1181
|
-
return /* @__PURE__ */
|
|
1498
|
+
return /* @__PURE__ */ jsx18("ul", { className: "flex flex-wrap gap-1.5", children: links.map(({ key, url, Icon, label }) => /* @__PURE__ */ jsx18("li", { children: /* @__PURE__ */ jsx18(Chip, { href: url, icon: /* @__PURE__ */ jsx18(Icon, { className: "h-3.5 w-3.5 shrink-0", "aria-hidden": "true" }), children: label }) }, key)) });
|
|
1182
1499
|
}
|
|
1183
1500
|
|
|
1184
1501
|
// src/genre-tree/artist/formatYears.ts
|
|
@@ -1189,7 +1506,7 @@ function formatYears(beginYear, endYear, ended) {
|
|
|
1189
1506
|
}
|
|
1190
1507
|
|
|
1191
1508
|
// src/genre-tree/artist/ArtistHeader.tsx
|
|
1192
|
-
import { jsx as
|
|
1509
|
+
import { jsx as jsx19 } from "react/jsx-runtime";
|
|
1193
1510
|
var countryNames = new Intl.DisplayNames(["en"], { type: "region" });
|
|
1194
1511
|
function ArtistHeader({ artist }) {
|
|
1195
1512
|
const fields = [];
|
|
@@ -1199,35 +1516,35 @@ function ArtistHeader({ artist }) {
|
|
|
1199
1516
|
const years = formatYears(artist.beginYear, artist.endYear, artist.ended);
|
|
1200
1517
|
if (years) fields.push({ label: "Active", value: years });
|
|
1201
1518
|
const links = artistLinks(artist);
|
|
1202
|
-
if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */
|
|
1519
|
+
if (links.length > 0) fields.push({ label: "Links", value: /* @__PURE__ */ jsx19(ArtistLinks, { links }) });
|
|
1203
1520
|
if (fields.length === 0) return null;
|
|
1204
|
-
return /* @__PURE__ */
|
|
1521
|
+
return /* @__PURE__ */ jsx19(FieldList, { items: fields });
|
|
1205
1522
|
}
|
|
1206
1523
|
|
|
1207
1524
|
// src/genre-tree/artist/ArtistPhoto.tsx
|
|
1208
|
-
import { jsx as
|
|
1525
|
+
import { jsx as jsx20, jsxs as jsxs14 } from "react/jsx-runtime";
|
|
1209
1526
|
function ExternalLink({ href, children }) {
|
|
1210
|
-
return /* @__PURE__ */
|
|
1527
|
+
return /* @__PURE__ */ jsx20("a", { href, target: "_blank", rel: "noopener noreferrer", className: "underline-offset-2 hover:underline", children });
|
|
1211
1528
|
}
|
|
1212
1529
|
function ArtistPhoto({ image, name }) {
|
|
1213
|
-
return /* @__PURE__ */
|
|
1214
|
-
/* @__PURE__ */
|
|
1215
|
-
/* @__PURE__ */
|
|
1530
|
+
return /* @__PURE__ */ jsxs14("figure", { className: "flex flex-col gap-1.5 px-6 py-2.5", children: [
|
|
1531
|
+
/* @__PURE__ */ jsx20("img", { src: image.url, alt: name, className: "h-64 w-full object-contain bg-[var(--color-surface-muted)] rounded" }),
|
|
1532
|
+
/* @__PURE__ */ jsxs14("figcaption", { className: "text-xs text-[var(--color-text-muted)]", children: [
|
|
1216
1533
|
"Photo: ",
|
|
1217
|
-
/* @__PURE__ */
|
|
1534
|
+
/* @__PURE__ */ jsx20(ExternalLink, { href: image.pageUrl, children: image.author ?? "Wikimedia Commons" }),
|
|
1218
1535
|
",",
|
|
1219
1536
|
" ",
|
|
1220
|
-
image.licenseUrl ? /* @__PURE__ */
|
|
1537
|
+
image.licenseUrl ? /* @__PURE__ */ jsx20(ExternalLink, { href: image.licenseUrl, children: image.license }) : image.license
|
|
1221
1538
|
] })
|
|
1222
1539
|
] });
|
|
1223
1540
|
}
|
|
1224
1541
|
|
|
1225
1542
|
// src/genre-tree/artist/ArtistMembers.tsx
|
|
1226
1543
|
import { PanelGroup, PanelSection } from "@behindthemusictree/ui";
|
|
1227
|
-
import { jsx as
|
|
1544
|
+
import { jsx as jsx21, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
1228
1545
|
function MembershipList({ title, memberships, renderArtistLink }) {
|
|
1229
1546
|
if (memberships.length === 0) return null;
|
|
1230
|
-
return /* @__PURE__ */
|
|
1547
|
+
return /* @__PURE__ */ jsx21(PanelGroup, { title, children: /* @__PURE__ */ jsx21("ul", { children: memberships.map((membership, index) => {
|
|
1231
1548
|
const details = [
|
|
1232
1549
|
formatYears(membership.beginYear, membership.endYear, membership.ended),
|
|
1233
1550
|
...membership.roles
|
|
@@ -1235,9 +1552,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
|
|
|
1235
1552
|
const detailsText = details.filter(Boolean).join(" \xB7 ");
|
|
1236
1553
|
return (
|
|
1237
1554
|
// A member can have several periods in the same group.
|
|
1238
|
-
/* @__PURE__ */
|
|
1239
|
-
/* @__PURE__ */
|
|
1240
|
-
detailsText && /* @__PURE__ */
|
|
1555
|
+
/* @__PURE__ */ jsxs15("li", { className: "truncate py-0.5", title: detailsText || void 0, children: [
|
|
1556
|
+
/* @__PURE__ */ jsx21("span", { className: "font-medium text-[var(--color-text)]", children: renderArtistLink(membership.artist) }),
|
|
1557
|
+
detailsText && /* @__PURE__ */ jsxs15("span", { className: "text-[var(--color-text-muted)]", children: [
|
|
1241
1558
|
" \u2014 ",
|
|
1242
1559
|
detailsText
|
|
1243
1560
|
] })
|
|
@@ -1248,9 +1565,9 @@ function MembershipList({ title, memberships, renderArtistLink }) {
|
|
|
1248
1565
|
function ArtistMembers({ title, memberships, renderArtistLink }) {
|
|
1249
1566
|
if (memberships.length === 0) return null;
|
|
1250
1567
|
const isCurrent = (membership) => !membership.ended && membership.endYear === null;
|
|
1251
|
-
return /* @__PURE__ */
|
|
1252
|
-
/* @__PURE__ */
|
|
1253
|
-
/* @__PURE__ */
|
|
1568
|
+
return /* @__PURE__ */ jsxs15(PanelSection, { title, children: [
|
|
1569
|
+
/* @__PURE__ */ jsx21(MembershipList, { title: "Current", memberships: memberships.filter(isCurrent), renderArtistLink }),
|
|
1570
|
+
/* @__PURE__ */ jsx21(
|
|
1254
1571
|
MembershipList,
|
|
1255
1572
|
{
|
|
1256
1573
|
title: "Past",
|
|
@@ -1263,7 +1580,7 @@ function ArtistMembers({ title, memberships, renderArtistLink }) {
|
|
|
1263
1580
|
|
|
1264
1581
|
// src/genre-tree/artist/ArtistDiscography.tsx
|
|
1265
1582
|
import { PanelGroup as PanelGroup2, PanelSection as PanelSection2 } from "@behindthemusictree/ui";
|
|
1266
|
-
import { jsx as
|
|
1583
|
+
import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
|
|
1267
1584
|
var GROUPS = ["Albums", "EPs", "Singles", "Live", "Compilations", "Other"];
|
|
1268
1585
|
var PRIMARY_GROUPS = { album: "Albums", ep: "EPs", single: "Singles" };
|
|
1269
1586
|
function discographyGroup({ primaryType, secondaryTypes }) {
|
|
@@ -1283,22 +1600,22 @@ function byYear(a, b) {
|
|
|
1283
1600
|
function ArtistDiscography({ albums }) {
|
|
1284
1601
|
if (albums.length === 0) return null;
|
|
1285
1602
|
const sorted = [...albums].sort(byYear);
|
|
1286
|
-
return /* @__PURE__ */
|
|
1603
|
+
return /* @__PURE__ */ jsx22(PanelSection2, { title: "Discography", children: GROUPS.map((group) => {
|
|
1287
1604
|
const groupAlbums = sorted.filter((album) => discographyGroup(album) === group);
|
|
1288
1605
|
if (groupAlbums.length === 0) return null;
|
|
1289
|
-
return /* @__PURE__ */
|
|
1290
|
-
/* @__PURE__ */
|
|
1291
|
-
/* @__PURE__ */
|
|
1606
|
+
return /* @__PURE__ */ jsx22(PanelGroup2, { title: group, children: /* @__PURE__ */ jsx22("ul", { children: groupAlbums.map((album) => /* @__PURE__ */ jsxs16("li", { className: "grid grid-cols-[2.5rem_minmax(0,1fr)] gap-2 py-0.5", children: [
|
|
1607
|
+
/* @__PURE__ */ jsx22("span", { className: "tabular-nums text-[var(--color-text-muted)]", children: album.year ?? "\u2014" }),
|
|
1608
|
+
/* @__PURE__ */ jsx22("span", { className: "truncate text-[var(--color-text)]", title: album.name, children: album.name })
|
|
1292
1609
|
] }, album.uuid)) }) }, group);
|
|
1293
1610
|
}) });
|
|
1294
1611
|
}
|
|
1295
1612
|
|
|
1296
1613
|
// src/genre-tree/artist/ArtistSongs.tsx
|
|
1297
|
-
import { useState as
|
|
1614
|
+
import { useState as useState4 } from "react";
|
|
1298
1615
|
import { keepPreviousData } from "@tanstack/react-query";
|
|
1299
1616
|
import { Play as Play2 } from "lucide-react";
|
|
1300
1617
|
import { Pagination, PanelSection as PanelSection3, Skeleton as Skeleton3 } from "@behindthemusictree/ui";
|
|
1301
|
-
import { usePlayer as
|
|
1618
|
+
import { usePlayer as usePlayer8 } from "@behindthemusictree/app-kit/player";
|
|
1302
1619
|
import {
|
|
1303
1620
|
SongDetailedSchema as SongDetailedSchema2,
|
|
1304
1621
|
libraryEndpoints as libraryEndpoints2,
|
|
@@ -1308,48 +1625,56 @@ import {
|
|
|
1308
1625
|
import { PaginatedResponseSchema as PaginatedResponseSchema2, useFetchWrapper as useFetchWrapper2, useQueryWithParse } from "@behindthemusictree/app-kit/transport";
|
|
1309
1626
|
|
|
1310
1627
|
// src/genre-tree/track-list-sidebar/TrackItem.tsx
|
|
1311
|
-
import { usePlayer as
|
|
1628
|
+
import { usePlayer as usePlayer7 } from "@behindthemusictree/app-kit/player";
|
|
1312
1629
|
import { useTrackList as useTrackList3 } from "@behindthemusictree/app-kit/genre-tree";
|
|
1313
1630
|
|
|
1314
1631
|
// src/genre-tree/TrackPositionPlayPause.tsx
|
|
1315
1632
|
import { FaPlay as FaPlay2, FaPause as FaPause2 } from "react-icons/fa";
|
|
1316
|
-
import { usePlayer as
|
|
1633
|
+
import { usePlayer as usePlayer6 } from "@behindthemusictree/app-kit/player";
|
|
1317
1634
|
import { PlayStates as PlayStates2 } from "@behindthemusictree/app-kit/player";
|
|
1318
|
-
import {
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1635
|
+
import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
|
|
1636
|
+
function TrackPositionPlayPause({
|
|
1637
|
+
position,
|
|
1638
|
+
uuid,
|
|
1639
|
+
handlePlayPauseClick,
|
|
1640
|
+
isSelected = false
|
|
1641
|
+
}) {
|
|
1642
|
+
const { playerTrackObject, playbackConfirmedTrackId, playState, isLoading: isPlayerLoading } = usePlayer6();
|
|
1322
1643
|
const isThisTrack = playerTrackObject?.track.id === uuid;
|
|
1323
|
-
|
|
1644
|
+
const isConfirmedPlayable = playbackConfirmedTrackId === uuid;
|
|
1645
|
+
const isAwaitingPlayback = (isSelected || isThisTrack) && !isConfirmedPlayable && !(isThisTrack && playerTrackObject?.loadError) && (!isThisTrack || isPlayerLoading || playState === PlayStates2.LOADING || playState === PlayStates2.PLAYING);
|
|
1646
|
+
const isPlaying = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PLAYING;
|
|
1647
|
+
const isPaused = isThisTrack && isConfirmedPlayable && playState === PlayStates2.PAUSED;
|
|
1648
|
+
const isCurrentTrackActive = isPlaying || isPaused;
|
|
1649
|
+
return /* @__PURE__ */ jsxs17(
|
|
1324
1650
|
"div",
|
|
1325
1651
|
{
|
|
1326
|
-
className: "track-position-play-pause flex items-center justify-center text-lg w-16 cursor-pointer",
|
|
1652
|
+
className: "track-position-play-pause relative flex items-center justify-center gap-2 text-lg w-16 cursor-pointer",
|
|
1327
1653
|
style: { minWidth: "4rem", maxWidth: "4rem" },
|
|
1328
1654
|
onClick: handlePlayPauseClick,
|
|
1329
1655
|
children: [
|
|
1330
|
-
/* @__PURE__ */
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
/* @__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, {}) })
|
|
1656
|
+
/* @__PURE__ */ jsx23("div", { className: "track-position-play-pause__status group-hover:hidden flex items-center justify-center", children: isAwaitingPlayback ? /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__spinner", role: "status", "aria-label": "Loading track" }) : isCurrentTrackActive ? /* @__PURE__ */ jsxs17(
|
|
1657
|
+
"span",
|
|
1658
|
+
{
|
|
1659
|
+
className: "track-position-play-pause__bars",
|
|
1660
|
+
"data-playing": isPlaying,
|
|
1661
|
+
role: "img",
|
|
1662
|
+
"aria-label": isPlaying ? "Playing" : "Paused",
|
|
1663
|
+
children: [
|
|
1664
|
+
/* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__bar" }),
|
|
1665
|
+
/* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__bar" }),
|
|
1666
|
+
/* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__bar" })
|
|
1667
|
+
]
|
|
1668
|
+
}
|
|
1669
|
+
) : position }),
|
|
1670
|
+
/* @__PURE__ */ jsx23(
|
|
1671
|
+
"div",
|
|
1672
|
+
{
|
|
1673
|
+
className: `hidden group-hover:flex items-center justify-center ${isCurrentTrackActive ? "" : "absolute inset-0"}`,
|
|
1674
|
+
"aria-hidden": "true",
|
|
1675
|
+
children: isAwaitingPlayback ? /* @__PURE__ */ jsx23("span", { className: "track-position-play-pause__spinner" }) : isPlaying ? /* @__PURE__ */ jsx23(FaPause2, {}) : /* @__PURE__ */ jsx23(FaPlay2, {})
|
|
1676
|
+
}
|
|
1677
|
+
)
|
|
1353
1678
|
]
|
|
1354
1679
|
}
|
|
1355
1680
|
);
|
|
@@ -1358,7 +1683,7 @@ function TrackPositionPlayPause({ position, uuid, handlePlayPauseClick }) {
|
|
|
1358
1683
|
// src/genre-tree/track-list-sidebar/TrackItem.tsx
|
|
1359
1684
|
import { isSong as isSong2, songUnplayableReason as songUnplayableReason2 } from "@behindthemusictree/app-kit/genre-tree";
|
|
1360
1685
|
import { ArtistNames as ArtistNames2 } from "@behindthemusictree/app-kit/genre-tree";
|
|
1361
|
-
import { jsx as
|
|
1686
|
+
import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
|
|
1362
1687
|
function TrackItem({
|
|
1363
1688
|
track,
|
|
1364
1689
|
position,
|
|
@@ -1366,26 +1691,27 @@ function TrackItem({
|
|
|
1366
1691
|
renderActions,
|
|
1367
1692
|
renderArtistLink
|
|
1368
1693
|
}) {
|
|
1369
|
-
const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } =
|
|
1370
|
-
const { trackList, toTrackAtPosition } = useTrackList3();
|
|
1694
|
+
const { handlePlayPauseAction, playerTrackObject, loadTrackForPlayer } = usePlayer7();
|
|
1695
|
+
const { trackList, selectedTrack, toTrackAtPosition, runtimeUnplayableTrackIds } = useTrackList3();
|
|
1371
1696
|
const scope = trackList?.origin?.scope ?? null;
|
|
1372
|
-
const
|
|
1697
|
+
const isCurrentTrack = playerTrackObject?.track.id === track.uuid;
|
|
1698
|
+
const hasPlaybackError = isCurrentTrack && !!playerTrackObject?.loadError;
|
|
1699
|
+
const isUnplayable = hasPlaybackError || runtimeUnplayableTrackIds?.has(track.uuid) === true || isSong2(track) && songUnplayableReason2(track) !== null;
|
|
1373
1700
|
const handlePlayPauseClick = (event) => {
|
|
1374
1701
|
event.stopPropagation();
|
|
1375
|
-
if (
|
|
1702
|
+
if (isCurrentTrack) {
|
|
1376
1703
|
handlePlayPauseAction();
|
|
1377
1704
|
} else if (scope != null) {
|
|
1378
1705
|
toTrackAtPosition(position);
|
|
1379
1706
|
loadTrackForPlayer(track.uuid);
|
|
1380
1707
|
}
|
|
1381
1708
|
};
|
|
1382
|
-
return /* @__PURE__ */
|
|
1709
|
+
return /* @__PURE__ */ jsxs18(
|
|
1383
1710
|
"div",
|
|
1384
1711
|
{
|
|
1385
|
-
className: `track-item flex h-14 text-[var(--color-text)] ${
|
|
1386
|
-
title: unplayableReason ?? void 0,
|
|
1712
|
+
className: `track-item flex h-14 text-[var(--color-text)] ${isUnplayable ? "opacity-50" : "hover:bg-[var(--color-surface-muted)] group"}`,
|
|
1387
1713
|
children: [
|
|
1388
|
-
|
|
1714
|
+
isUnplayable ? /* @__PURE__ */ jsx24(
|
|
1389
1715
|
"div",
|
|
1390
1716
|
{
|
|
1391
1717
|
className: "flex items-center justify-center text-lg w-16",
|
|
@@ -1393,18 +1719,21 @@ function TrackItem({
|
|
|
1393
1719
|
"aria-disabled": "true",
|
|
1394
1720
|
children: position
|
|
1395
1721
|
}
|
|
1396
|
-
) : /* @__PURE__ */
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1722
|
+
) : /* @__PURE__ */ jsx24(
|
|
1723
|
+
TrackPositionPlayPause,
|
|
1724
|
+
{
|
|
1725
|
+
position,
|
|
1726
|
+
uuid: track.uuid,
|
|
1727
|
+
handlePlayPauseClick,
|
|
1728
|
+
isSelected: selectedTrack?.uuid === track.uuid
|
|
1729
|
+
}
|
|
1730
|
+
),
|
|
1731
|
+
/* @__PURE__ */ jsxs18("div", { className: "title-artist-container flex flex-col items-start justify-center min-w-0 flex-1", children: [
|
|
1732
|
+
/* @__PURE__ */ jsx24("div", { className: "flex max-w-full items-center gap-2", children: /* @__PURE__ */ jsx24("div", { className: "title text-lg font-bold text-[var(--color-text)] max-w-full truncate", children: isSong2(track) ? /* @__PURE__ */ jsx24(MusicBrainzRecordingLink, { song: track, className: "hover:underline focus-visible:underline", children: track.title }) : track.title }) }),
|
|
1733
|
+
track.artists && track.artists.length > 0 ? /* @__PURE__ */ jsx24("div", { className: "artist text-base max-w-full truncate text-[var(--color-text-muted)]", children: /* @__PURE__ */ jsx24(ArtistNames2, { artists: track.artists, renderArtistLink }) }) : ""
|
|
1404
1734
|
] }),
|
|
1405
|
-
/* @__PURE__ */
|
|
1406
|
-
|
|
1407
|
-
renderDuration && /* @__PURE__ */ jsx20(
|
|
1735
|
+
/* @__PURE__ */ jsx24("div", { className: "genre-name-container flex items-center justify-start w-[21.6%] min-w-0 shrink-0", children: track.genre ? /* @__PURE__ */ jsx24("div", { className: "genre-name max-w-full whitespace-normal break-words p-1 text-xs font-bold", children: track.genre.name }) : "" }),
|
|
1736
|
+
renderDuration && /* @__PURE__ */ jsx24(
|
|
1408
1737
|
"div",
|
|
1409
1738
|
{
|
|
1410
1739
|
className: "duration flex text-base w-16 items-center justify-center",
|
|
@@ -1412,25 +1741,25 @@ function TrackItem({
|
|
|
1412
1741
|
children: renderDuration(track)
|
|
1413
1742
|
}
|
|
1414
1743
|
),
|
|
1415
|
-
renderActions && /* @__PURE__ */
|
|
1744
|
+
renderActions && /* @__PURE__ */ jsx24("div", { className: "edit flex text-base w-6 items-center justify-center mr-2 cursor-pointer", children: renderActions(track) })
|
|
1416
1745
|
]
|
|
1417
1746
|
}
|
|
1418
1747
|
);
|
|
1419
1748
|
}
|
|
1420
1749
|
|
|
1421
1750
|
// src/genre-tree/artist/ArtistSongs.tsx
|
|
1422
|
-
import { Fragment as
|
|
1751
|
+
import { Fragment as Fragment7, jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
|
|
1423
1752
|
var SongPageSchema2 = PaginatedResponseSchema2(SongDetailedSchema2);
|
|
1424
1753
|
var SORTS = [
|
|
1425
1754
|
{ ordering: "title", label: "Title" },
|
|
1426
1755
|
{ ordering: "-albumYear", label: "Year" }
|
|
1427
1756
|
];
|
|
1428
1757
|
function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
|
|
1429
|
-
const [page, setPage] =
|
|
1430
|
-
const [ordering, setOrdering] =
|
|
1758
|
+
const [page, setPage] = useState4(1);
|
|
1759
|
+
const [ordering, setOrdering] = useState4("title");
|
|
1431
1760
|
const { fetch } = useFetchWrapper2(getBackendBaseUrl);
|
|
1432
1761
|
const { playNewTrackListFromArtist } = useTrackList4();
|
|
1433
|
-
const { playerTrackObject } =
|
|
1762
|
+
const { playerTrackObject } = usePlayer8();
|
|
1434
1763
|
const { data, isPending, isError } = useQueryWithParse({
|
|
1435
1764
|
queryKey: ["referenceSongs", "artist", artist.uuid, ordering, page],
|
|
1436
1765
|
queryFn: () => fetch(libraryEndpoints2.reference.songs.list(), true, false, void 0, {
|
|
@@ -1452,21 +1781,21 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
|
|
|
1452
1781
|
event.stopPropagation();
|
|
1453
1782
|
void play(song.uuid);
|
|
1454
1783
|
};
|
|
1455
|
-
return /* @__PURE__ */
|
|
1456
|
-
/* @__PURE__ */
|
|
1457
|
-
/* @__PURE__ */
|
|
1784
|
+
return /* @__PURE__ */ jsx25(PanelSection3, { title: "Songs", children: isPending ? /* @__PURE__ */ jsx25(Skeleton3, { className: "h-14", "aria-busy": "true" }) : isError ? /* @__PURE__ */ jsx25("p", { className: "text-red-600", children: "Could not load songs." }) : data.results.length === 0 ? /* @__PURE__ */ jsx25("p", { className: "text-[var(--color-text-muted)]", children: "No songs yet." }) : /* @__PURE__ */ jsxs19(Fragment7, { children: [
|
|
1785
|
+
/* @__PURE__ */ jsxs19("div", { className: "flex flex-wrap items-center justify-between gap-2 mb-2", children: [
|
|
1786
|
+
/* @__PURE__ */ jsxs19(
|
|
1458
1787
|
"button",
|
|
1459
1788
|
{
|
|
1460
1789
|
type: "button",
|
|
1461
1790
|
onClick: () => void play(),
|
|
1462
1791
|
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",
|
|
1463
1792
|
children: [
|
|
1464
|
-
/* @__PURE__ */
|
|
1793
|
+
/* @__PURE__ */ jsx25(Play2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }),
|
|
1465
1794
|
"Play all"
|
|
1466
1795
|
]
|
|
1467
1796
|
}
|
|
1468
1797
|
),
|
|
1469
|
-
/* @__PURE__ */
|
|
1798
|
+
/* @__PURE__ */ jsx25("div", { role: "group", "aria-label": "Sort songs by", className: "flex gap-1 text-xs", children: SORTS.map((sort) => /* @__PURE__ */ jsx25(
|
|
1470
1799
|
"button",
|
|
1471
1800
|
{
|
|
1472
1801
|
type: "button",
|
|
@@ -1481,7 +1810,7 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
|
|
|
1481
1810
|
sort.ordering
|
|
1482
1811
|
)) })
|
|
1483
1812
|
] }),
|
|
1484
|
-
/* @__PURE__ */
|
|
1813
|
+
/* @__PURE__ */ jsx25("ul", { className: "overflow-hidden mb-2 bg-zinc-950 rounded-lg", children: data.results.map((song, index) => /* @__PURE__ */ jsx25("li", { onClickCapture: (event) => handleRowClickCapture(event, song), children: /* @__PURE__ */ jsx25(
|
|
1485
1814
|
TrackItem,
|
|
1486
1815
|
{
|
|
1487
1816
|
track: song,
|
|
@@ -1489,15 +1818,17 @@ function ArtistSongs({ artist, getBackendBaseUrl, renderArtistLink }) {
|
|
|
1489
1818
|
renderArtistLink
|
|
1490
1819
|
}
|
|
1491
1820
|
) }, song.uuid)) }),
|
|
1492
|
-
/* @__PURE__ */
|
|
1821
|
+
/* @__PURE__ */ jsx25(Pagination, { currentPage: page, totalPages: data.totalPages, onPageChange: setPage })
|
|
1493
1822
|
] }) });
|
|
1494
1823
|
}
|
|
1495
1824
|
|
|
1496
1825
|
// src/genre-tree/artist/useArtistPanel.tsx
|
|
1497
|
-
import { Fragment as
|
|
1826
|
+
import { Fragment as Fragment8, jsx as jsx26, jsxs as jsxs20 } from "react/jsx-runtime";
|
|
1498
1827
|
function useArtistPanel({
|
|
1499
1828
|
uuid,
|
|
1829
|
+
scope,
|
|
1500
1830
|
getBackendBaseUrl,
|
|
1831
|
+
getShareUrl,
|
|
1501
1832
|
renderArtistLink,
|
|
1502
1833
|
expanded,
|
|
1503
1834
|
onExpandedChange,
|
|
@@ -1512,38 +1843,41 @@ function useArtistPanel({
|
|
|
1512
1843
|
return {
|
|
1513
1844
|
...panel,
|
|
1514
1845
|
title: "Artist",
|
|
1515
|
-
content: /* @__PURE__ */
|
|
1516
|
-
/* @__PURE__ */
|
|
1517
|
-
/* @__PURE__ */
|
|
1518
|
-
/* @__PURE__ */
|
|
1519
|
-
/* @__PURE__ */
|
|
1520
|
-
/* @__PURE__ */
|
|
1521
|
-
/* @__PURE__ */
|
|
1846
|
+
content: /* @__PURE__ */ jsxs20("div", { "aria-busy": "true", className: "flex flex-col gap-2.5 px-6 py-2.5", children: [
|
|
1847
|
+
/* @__PURE__ */ jsx26(Skeleton4, { className: "h-3 w-16" }),
|
|
1848
|
+
/* @__PURE__ */ jsx26(Skeleton4, { className: "h-4 w-32" }),
|
|
1849
|
+
/* @__PURE__ */ jsx26(Skeleton4, { className: "h-3 w-16" }),
|
|
1850
|
+
/* @__PURE__ */ jsx26(Skeleton4, { className: "h-4 w-24" }),
|
|
1851
|
+
/* @__PURE__ */ jsx26(Skeleton4, { className: "h-16 mt-2.5" }),
|
|
1852
|
+
/* @__PURE__ */ jsx26(Skeleton4, { className: "h-16" })
|
|
1522
1853
|
] })
|
|
1523
1854
|
};
|
|
1524
1855
|
case "not-found":
|
|
1525
1856
|
return {
|
|
1526
1857
|
...panel,
|
|
1527
1858
|
title: "Artist not found",
|
|
1528
|
-
content: /* @__PURE__ */
|
|
1859
|
+
content: /* @__PURE__ */ jsx26("p", { className: "px-6 py-2.5 text-[var(--color-text-muted)]", children: "This artist does not exist." })
|
|
1529
1860
|
};
|
|
1530
1861
|
case "error":
|
|
1531
1862
|
return {
|
|
1532
1863
|
...panel,
|
|
1533
1864
|
title: "Artist",
|
|
1534
|
-
content: /* @__PURE__ */
|
|
1865
|
+
content: /* @__PURE__ */ jsx26("p", { className: "px-6 py-2.5 text-red-600", children: "Could not load this artist." })
|
|
1535
1866
|
};
|
|
1536
1867
|
case "success": {
|
|
1537
1868
|
const { artist } = result;
|
|
1869
|
+
const shareTarget = scope ? { kind: "artist", uuid: artist.uuid, name: artist.name, scope } : void 0;
|
|
1870
|
+
const shareUrl = shareTarget ? getShareUrl?.(shareTarget) : void 0;
|
|
1538
1871
|
return {
|
|
1539
1872
|
...panel,
|
|
1540
1873
|
title: artist.name,
|
|
1541
|
-
content: /* @__PURE__ */
|
|
1542
|
-
|
|
1543
|
-
/* @__PURE__ */
|
|
1544
|
-
/* @__PURE__ */
|
|
1545
|
-
/* @__PURE__ */
|
|
1546
|
-
|
|
1874
|
+
content: /* @__PURE__ */ jsxs20(Fragment8, { children: [
|
|
1875
|
+
shareUrl && /* @__PURE__ */ jsx26("div", { className: "genre-action-band border-b border-[var(--color-border)]", children: /* @__PURE__ */ jsx26("div", { className: "flex flex-wrap gap-1 px-3.5 py-3", children: shareTarget && /* @__PURE__ */ jsx26(ShareActionButton, { target: shareTarget, url: shareUrl }) }) }),
|
|
1876
|
+
artist.image && /* @__PURE__ */ jsx26(ArtistPhoto, { image: artist.image, name: artist.name }),
|
|
1877
|
+
/* @__PURE__ */ jsx26(ArtistHeader, { artist }),
|
|
1878
|
+
/* @__PURE__ */ jsx26(ArtistMembers, { title: "Members", memberships: artist.members, renderArtistLink }),
|
|
1879
|
+
/* @__PURE__ */ jsx26(ArtistMembers, { title: "Member of", memberships: artist.memberOf, renderArtistLink }),
|
|
1880
|
+
artist.tracksCount > 0 && /* @__PURE__ */ jsx26(
|
|
1547
1881
|
ArtistSongs,
|
|
1548
1882
|
{
|
|
1549
1883
|
artist: { uuid: artist.uuid, name: artist.name },
|
|
@@ -1552,7 +1886,7 @@ function useArtistPanel({
|
|
|
1552
1886
|
}
|
|
1553
1887
|
)
|
|
1554
1888
|
] }),
|
|
1555
|
-
extraContent: artist.albums.length > 0 ? /* @__PURE__ */
|
|
1889
|
+
extraContent: artist.albums.length > 0 ? /* @__PURE__ */ jsx26(ArtistDiscography, { albums: artist.albums }) : void 0
|
|
1556
1890
|
};
|
|
1557
1891
|
}
|
|
1558
1892
|
}
|
|
@@ -1561,27 +1895,27 @@ function useArtistPanel({
|
|
|
1561
1895
|
// src/genre-tree/SizeMetricToggle.tsx
|
|
1562
1896
|
import { MdMusicNote, MdVisibility } from "react-icons/md";
|
|
1563
1897
|
import { SegmentedControl } from "@behindthemusictree/ui";
|
|
1564
|
-
import { jsx as
|
|
1898
|
+
import { jsx as jsx27 } from "react/jsx-runtime";
|
|
1565
1899
|
function SizeMetricToggle({
|
|
1566
1900
|
value,
|
|
1567
1901
|
onChange
|
|
1568
1902
|
}) {
|
|
1569
|
-
return /* @__PURE__ */
|
|
1903
|
+
return /* @__PURE__ */ jsx27(
|
|
1570
1904
|
SegmentedControl,
|
|
1571
1905
|
{
|
|
1572
1906
|
"aria-label": "Size nodes by",
|
|
1573
1907
|
value,
|
|
1574
1908
|
onChange,
|
|
1575
1909
|
options: [
|
|
1576
|
-
{ value: "songs", label: /* @__PURE__ */
|
|
1577
|
-
{ value: "views", label: /* @__PURE__ */
|
|
1910
|
+
{ value: "songs", label: /* @__PURE__ */ jsx27(MdMusicNote, { size: 18, "aria-hidden": true }), "aria-label": "Size by songs", title: "Node size by song count" },
|
|
1911
|
+
{ value: "views", label: /* @__PURE__ */ jsx27(MdVisibility, { size: 18, "aria-hidden": true }), "aria-label": "Size by views", title: "Node size by song views" }
|
|
1578
1912
|
]
|
|
1579
1913
|
}
|
|
1580
1914
|
);
|
|
1581
1915
|
}
|
|
1582
1916
|
|
|
1583
1917
|
// src/genre-tree/GenreTreeView.tsx
|
|
1584
|
-
import { Fragment as
|
|
1918
|
+
import { Fragment as Fragment9, jsx as jsx28, jsxs as jsxs21 } from "react/jsx-runtime";
|
|
1585
1919
|
var PREVIOUS_BUTTON_CSS = `
|
|
1586
1920
|
.genre-tree-view .search-overlay .gtv-info-panel-previous { display: none; }
|
|
1587
1921
|
.genre-tree-view:has(.search-overlay .gtv-info-panel-previous) .genre-search input { padding-left: 2.75rem; }
|
|
@@ -1591,27 +1925,6 @@ var PREVIOUS_BUTTON_CSS = `
|
|
|
1591
1925
|
.genre-search-previous { animation: genre-tree-view-previous-in 200ms ease-out; }
|
|
1592
1926
|
}
|
|
1593
1927
|
`;
|
|
1594
|
-
function GenreNodeActionBand({ genrePlaylist, scope, playback, ...bandProps }) {
|
|
1595
|
-
const { isPlaying, setIsPlaying } = usePlayer8();
|
|
1596
|
-
const isCurrentTrackList = playback.playingNodeId === genrePlaylist.uuid;
|
|
1597
|
-
const handlePlayPause = () => {
|
|
1598
|
-
if (isCurrentTrackList) {
|
|
1599
|
-
setIsPlaying(!isPlaying);
|
|
1600
|
-
return;
|
|
1601
|
-
}
|
|
1602
|
-
playback.play(genrePlaylist).catch((error) => {
|
|
1603
|
-
console.error("Failed to load genre playlist tracks:", error);
|
|
1604
|
-
});
|
|
1605
|
-
};
|
|
1606
|
-
return /* @__PURE__ */ jsx24(
|
|
1607
|
-
GenreActionBand,
|
|
1608
|
-
{
|
|
1609
|
-
...bandProps,
|
|
1610
|
-
onPlayPause: genrePlaylist.tracksCount > 0 ? handlePlayPause : void 0,
|
|
1611
|
-
isPlaying: isCurrentTrackList && isPlaying
|
|
1612
|
-
}
|
|
1613
|
-
);
|
|
1614
|
-
}
|
|
1615
1928
|
var NO_POP_ROOT_TITLE = "This genre tree has no 'Mainstream Pop' root yet";
|
|
1616
1929
|
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]";
|
|
1617
1930
|
function GenreTreeView({
|
|
@@ -1625,15 +1938,16 @@ function GenreTreeView({
|
|
|
1625
1938
|
selectedNodeId: controlledSelectedNodeId,
|
|
1626
1939
|
onSelectedNodeIdChange,
|
|
1627
1940
|
defaultViewport,
|
|
1941
|
+
viewportRestore,
|
|
1628
1942
|
onViewportChange,
|
|
1629
1943
|
readOnly = false,
|
|
1630
|
-
className,
|
|
1944
|
+
className: className2,
|
|
1631
1945
|
showDisplayOptions = true,
|
|
1632
1946
|
showAddRoot = true,
|
|
1633
1947
|
allowWheelRotation: controlledAllowWheelRotation,
|
|
1634
1948
|
criteriaOverviewSchema,
|
|
1635
1949
|
renderGenreDetailExtras,
|
|
1636
|
-
|
|
1950
|
+
floatingControlsLeading,
|
|
1637
1951
|
sizeMetric: controlledSizeMetric,
|
|
1638
1952
|
onSizeMetricChange,
|
|
1639
1953
|
renderArtistLink,
|
|
@@ -1641,6 +1955,7 @@ function GenreTreeView({
|
|
|
1641
1955
|
onArtistUuidChange,
|
|
1642
1956
|
artistExpanded: controlledArtistExpanded,
|
|
1643
1957
|
onArtistExpandedChange,
|
|
1958
|
+
getShareUrl,
|
|
1644
1959
|
artistSearch = true,
|
|
1645
1960
|
onArtistSelect,
|
|
1646
1961
|
artistSearchBaseUrl,
|
|
@@ -1658,10 +1973,12 @@ function GenreTreeView({
|
|
|
1658
1973
|
placement = "default",
|
|
1659
1974
|
...focus
|
|
1660
1975
|
}) {
|
|
1661
|
-
const
|
|
1976
|
+
const getShareUrlRef = useRef4(getShareUrl);
|
|
1977
|
+
getShareUrlRef.current = getShareUrl;
|
|
1978
|
+
const [internalArtistUuid, setInternalArtistUuid] = useState5(null);
|
|
1662
1979
|
const isArtistControlled = controlledArtistUuid !== void 0;
|
|
1663
1980
|
const artistUuid = isArtistControlled ? controlledArtistUuid : internalArtistUuid;
|
|
1664
|
-
const [internalSizeMetric, setInternalSizeMetric] =
|
|
1981
|
+
const [internalSizeMetric, setInternalSizeMetric] = useState5("songs");
|
|
1665
1982
|
const sizeMetric = controlledSizeMetric ?? internalSizeMetric;
|
|
1666
1983
|
const deferredSizeMetric = useDeferredValue(sizeMetric);
|
|
1667
1984
|
const changeSizeMetric = (metric) => {
|
|
@@ -1699,25 +2016,41 @@ function GenreTreeView({
|
|
|
1699
2016
|
criteriaOverviewSchema,
|
|
1700
2017
|
artistUuid
|
|
1701
2018
|
});
|
|
2019
|
+
const restoreRevision = viewportRestore?.revision;
|
|
2020
|
+
const [stackedRestoreOwner, setStackedRestoreOwner] = useState5({ revision: restoreRevision, rootUuid: selectedGenrePlaylist?.root.uuid });
|
|
2021
|
+
let restoreRootUuid = stackedRestoreOwner.rootUuid;
|
|
2022
|
+
if (stackedRestoreOwner.revision !== restoreRevision) {
|
|
2023
|
+
restoreRootUuid = selectedGenrePlaylist?.root.uuid;
|
|
2024
|
+
setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
|
|
2025
|
+
} else if (restoreRootUuid === void 0 && selectedGenrePlaylist) {
|
|
2026
|
+
restoreRootUuid = selectedGenrePlaylist.root.uuid;
|
|
2027
|
+
setStackedRestoreOwner({ revision: restoreRevision, rootUuid: restoreRootUuid });
|
|
2028
|
+
}
|
|
1702
2029
|
const isGenreUpdating = useIsMutating({ mutationKey: genreUpdateMutationKey }) > 0;
|
|
2030
|
+
const { mutate: updateGenre } = useUpdateGenre2(scope, getBackendBaseUrl);
|
|
1703
2031
|
const busy = deferredSizeMetric !== sizeMetric || isTreeFetching || isGenreUpdating;
|
|
2032
|
+
const isArtistFiltered = artistUuid !== null;
|
|
2033
|
+
const canSizeByViews = !isArtistFiltered && hasViewsCount(visibleGenrePlaylists);
|
|
1704
2034
|
const artistResult = useArtist2(artistUuid, artistSearchBaseUrl ?? getBackendBaseUrl);
|
|
1705
2035
|
const filterArtistName = artistResult.status === "success" ? artistResult.artist.name : null;
|
|
1706
|
-
const filterArtist =
|
|
2036
|
+
const filterArtist = useMemo7(
|
|
1707
2037
|
() => artistUuid !== null && filterArtistName !== null ? { uuid: artistUuid, name: filterArtistName } : null,
|
|
1708
2038
|
[artistUuid, filterArtistName]
|
|
1709
2039
|
);
|
|
2040
|
+
const hasSelectedGenreNode = selectedGenrePlaylist !== void 0 && visibleGenrePlaylists.some(({ uuid }) => uuid === selectedGenrePlaylist.uuid);
|
|
1710
2041
|
const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: visibleGenrePlaylists, artist: filterArtist });
|
|
1711
2042
|
const openArtist = (uuid) => {
|
|
1712
2043
|
if (selectedNodeId !== null) clearSelection();
|
|
1713
2044
|
changeArtist(uuid);
|
|
1714
2045
|
};
|
|
1715
|
-
const [expandedArtistUuid, setExpandedArtistUuid] =
|
|
2046
|
+
const [expandedArtistUuid, setExpandedArtistUuid] = useState5(null);
|
|
1716
2047
|
const artistBaseUrl = artistSearchBaseUrl ?? getBackendBaseUrl;
|
|
1717
2048
|
const artistPanel = useArtistPanel({
|
|
1718
2049
|
uuid: artistUuid,
|
|
2050
|
+
scope,
|
|
1719
2051
|
getBackendBaseUrl: artistBaseUrl,
|
|
1720
|
-
|
|
2052
|
+
getShareUrl: getShareUrlRef.current,
|
|
2053
|
+
renderArtistLink: renderArtistLink ?? ((artist) => artist.isSpecialPurpose ? artist.name : /* @__PURE__ */ jsx28("button", { type: "button", onClick: () => openArtist(artist.uuid), className: "hover:underline", children: artist.name })),
|
|
1721
2054
|
expanded: controlledArtistExpanded ?? (artistUuid !== null && expandedArtistUuid === artistUuid),
|
|
1722
2055
|
onExpandedChange: (on) => {
|
|
1723
2056
|
if (controlledArtistExpanded === void 0) setExpandedArtistUuid(on ? artistUuid : null);
|
|
@@ -1725,10 +2058,10 @@ function GenreTreeView({
|
|
|
1725
2058
|
},
|
|
1726
2059
|
onClose: () => changeArtist(null)
|
|
1727
2060
|
});
|
|
1728
|
-
const builtInArtistPanel = overlayPanel ||
|
|
2061
|
+
const builtInArtistPanel = overlayPanel || hasSelectedGenreNode ? void 0 : artistPanel;
|
|
1729
2062
|
const activeOverlayPanel = overlayPanel ?? (builtInArtistPanel && { ...builtInArtistPanel, hideTitle: true, hideClose: true });
|
|
1730
|
-
const [uncontrolledCollapsed, setUncontrolledCollapsed] =
|
|
1731
|
-
const [uncontrolledExpanded, setUncontrolledExpanded] =
|
|
2063
|
+
const [uncontrolledCollapsed, setUncontrolledCollapsed] = useState5(false);
|
|
2064
|
+
const [uncontrolledExpanded, setUncontrolledExpanded] = useState5(false);
|
|
1732
2065
|
const isCollapsed = collapsed ?? uncontrolledCollapsed;
|
|
1733
2066
|
const isExpanded = expanded ?? uncontrolledExpanded;
|
|
1734
2067
|
const changeCollapsed = (next) => {
|
|
@@ -1739,18 +2072,11 @@ function GenreTreeView({
|
|
|
1739
2072
|
setUncontrolledExpanded(next);
|
|
1740
2073
|
onExpandedChange?.(next);
|
|
1741
2074
|
};
|
|
1742
|
-
|
|
2075
|
+
useEffect6(() => {
|
|
1743
2076
|
setUncontrolledCollapsed(false);
|
|
1744
2077
|
}, [selectedNodeId, artistUuid]);
|
|
1745
|
-
const isDetailHidden = isCollapsed || !
|
|
1746
|
-
|
|
1747
|
-
if (placement !== "regions" || !isDetailHidden) return void 0;
|
|
1748
|
-
const root = document.documentElement;
|
|
1749
|
-
root.style.setProperty("--layout-left-w", "0px");
|
|
1750
|
-
return () => {
|
|
1751
|
-
root.style.removeProperty("--layout-left-w");
|
|
1752
|
-
};
|
|
1753
|
-
}, [placement, isDetailHidden]);
|
|
2078
|
+
const isDetailHidden = isCollapsed || !hasSelectedGenreNode && !activeOverlayPanel;
|
|
2079
|
+
useLeftDetailPanelLayout(!isDetailHidden, placement === "regions");
|
|
1754
2080
|
const detailOverlay = placement === "regions" && activeOverlayPanel?.onExpandedChange ? activeOverlayPanel : void 0;
|
|
1755
2081
|
const detailExpanded = detailOverlay ? !!detailOverlay.expanded : isExpanded;
|
|
1756
2082
|
const onDetailExpandedChange = detailOverlay ? detailOverlay.onExpandedChange : changeExpanded;
|
|
@@ -1762,29 +2088,38 @@ function GenreTreeView({
|
|
|
1762
2088
|
hideExpandButton: placement === "regions" ? true : void 0,
|
|
1763
2089
|
overlayPanel: detailOverlay ? { ...detailOverlay, collapsed: isCollapsed, hideExpandButton: true } : activeOverlayPanel
|
|
1764
2090
|
};
|
|
1765
|
-
const [isWheelRevealed, setIsWheelRevealed] =
|
|
2091
|
+
const [isWheelRevealed, setIsWheelRevealed] = useState5(false);
|
|
1766
2092
|
const usesHandoff = viewMode === "wheel" || viewMode === "pop-core";
|
|
1767
2093
|
const isTreeReady = !isLoading && (!usesHandoff || isWheelRevealed);
|
|
1768
2094
|
const rootRef = useRef4(null);
|
|
1769
2095
|
const handlePrevious = () => {
|
|
1770
2096
|
rootRef.current?.querySelector(".search-overlay .gtv-info-panel-previous")?.click();
|
|
1771
2097
|
};
|
|
1772
|
-
|
|
1773
|
-
|
|
2098
|
+
useEffect6(() => {
|
|
2099
|
+
if (!isTreeReady || viewMode !== "pop-core" || !hasSelectedGenreNode || !selectedGenrePlaylist) return;
|
|
2100
|
+
const root = rootRef.current;
|
|
2101
|
+
const centerChip = root?.querySelector(".gtv-wheel-center-node .gtv-wheel-chip");
|
|
2102
|
+
if (!centerChip || centerChip.getAttribute("aria-label") !== selectedGenrePlaylist.name) return;
|
|
2103
|
+
const panelTitle = root?.querySelector(".gtv-info-panel-title");
|
|
2104
|
+
if (panelTitle?.textContent?.trim().startsWith(selectedGenrePlaylist.name)) return;
|
|
2105
|
+
centerChip.click();
|
|
2106
|
+
}, [hasSelectedGenreNode, isTreeReady, selectedGenrePlaylist, viewMode]);
|
|
2107
|
+
const { showPopup, hidePopup } = usePopup5();
|
|
2108
|
+
const [searchText, setSearchText] = useState5("");
|
|
1774
2109
|
const artistSearchResult = useArtistSearch(artistBaseUrl, artistSearch ? searchText : "");
|
|
1775
2110
|
const artistExtraResults = artistSearch ? {
|
|
1776
2111
|
label: "Artists",
|
|
1777
2112
|
items: artistSearchResult.artists.map((artist) => ({
|
|
1778
2113
|
key: artist.uuid,
|
|
1779
|
-
content: /* @__PURE__ */
|
|
1780
|
-
/* @__PURE__ */
|
|
2114
|
+
content: /* @__PURE__ */ jsxs21(Fragment9, { children: [
|
|
2115
|
+
/* @__PURE__ */ jsxs21("span", { className: "block truncate font-medium", children: [
|
|
1781
2116
|
artist.name,
|
|
1782
|
-
artist.disambiguation && /* @__PURE__ */
|
|
2117
|
+
artist.disambiguation && /* @__PURE__ */ jsxs21("span", { className: "font-normal text-[var(--color-neutral-500)]", children: [
|
|
1783
2118
|
" \xB7 ",
|
|
1784
2119
|
artist.disambiguation
|
|
1785
2120
|
] })
|
|
1786
2121
|
] }),
|
|
1787
|
-
artist.matchedAlias && /* @__PURE__ */
|
|
2122
|
+
artist.matchedAlias && /* @__PURE__ */ jsxs21("span", { className: "block truncate text-xs text-[var(--color-neutral-500)]", children: [
|
|
1788
2123
|
"also known as ",
|
|
1789
2124
|
artist.matchedAlias
|
|
1790
2125
|
] })
|
|
@@ -1812,29 +2147,50 @@ function GenreTreeView({
|
|
|
1812
2147
|
};
|
|
1813
2148
|
const zoomInRef = useRef4(void 0);
|
|
1814
2149
|
zoomInRef.current = canZoomIn ? handleGenreSearchZoomIn : void 0;
|
|
1815
|
-
const renderInfoPanelBody =
|
|
2150
|
+
const renderInfoPanelBody = useCallback6(
|
|
1816
2151
|
(node, { toolbar, ancestors, family }) => {
|
|
1817
2152
|
const genrePlaylist = genrePlaylistByNodeId.get(node.id);
|
|
1818
2153
|
const criteria = genrePlaylist?.criteria;
|
|
1819
|
-
if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */
|
|
2154
|
+
if (!genrePlaylist || !criteria) return { primary: /* @__PURE__ */ jsxs21(Fragment9, { children: [
|
|
1820
2155
|
toolbar,
|
|
1821
2156
|
family
|
|
1822
2157
|
] }), aside: ancestors };
|
|
1823
2158
|
const genreUuid = criteria.uuid;
|
|
2159
|
+
const parentPlaylist = visibleGenrePlaylists.find(({ uuid }) => uuid === genrePlaylist.parent?.uuid);
|
|
2160
|
+
const canToggleSide = !readOnly && parentPlaylist !== void 0 && parentPlaylist.parent === null;
|
|
2161
|
+
const side = criteria.side ?? "core";
|
|
2162
|
+
const shareTarget = {
|
|
2163
|
+
kind: "genre",
|
|
2164
|
+
genrePlaylistUuid: genrePlaylist.uuid,
|
|
2165
|
+
name: criteria.name,
|
|
2166
|
+
scope,
|
|
2167
|
+
artistUuid
|
|
2168
|
+
};
|
|
2169
|
+
const shareUrl = getShareUrlRef.current?.(shareTarget) ?? void 0;
|
|
2170
|
+
const popSibling = canToggleSide && side === "core" ? visibleGenrePlaylists.find(
|
|
2171
|
+
(sibling) => sibling.parent?.uuid === parentPlaylist.uuid && sibling.uuid !== genrePlaylist.uuid && sibling.criteria?.side === "pop"
|
|
2172
|
+
) : void 0;
|
|
1824
2173
|
return {
|
|
1825
|
-
primary: /* @__PURE__ */
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
2174
|
+
primary: /* @__PURE__ */ jsx28(
|
|
2175
|
+
GenreInfoPanelBody,
|
|
2176
|
+
{
|
|
2177
|
+
toolbar,
|
|
2178
|
+
family,
|
|
2179
|
+
genreUuid,
|
|
2180
|
+
genrePlaylist,
|
|
2181
|
+
wikidataId: criteria.wikidataId,
|
|
2182
|
+
scope,
|
|
2183
|
+
getBackendBaseUrl,
|
|
2184
|
+
playback,
|
|
2185
|
+
band: {
|
|
1830
2186
|
node,
|
|
1831
|
-
|
|
1832
|
-
|
|
2187
|
+
shareTarget: shareUrl ? shareTarget : void 0,
|
|
2188
|
+
shareUrl,
|
|
1833
2189
|
onZoomIn: zoomInRef.current,
|
|
1834
2190
|
onAddChild: readOnly ? void 0 : () => handleGenreCreationAction(criteria),
|
|
1835
2191
|
onRename: readOnly ? void 0 : () => handleGenreRenameAction(criteria),
|
|
1836
2192
|
onChangeParent: readOnly ? void 0 : () => showPopup(
|
|
1837
|
-
/* @__PURE__ */
|
|
2193
|
+
/* @__PURE__ */ jsx28(
|
|
1838
2194
|
ChangeParentPopup,
|
|
1839
2195
|
{
|
|
1840
2196
|
genrePlaylist: { ...genrePlaylist, criteria },
|
|
@@ -1846,18 +2202,12 @@ function GenreTreeView({
|
|
|
1846
2202
|
}
|
|
1847
2203
|
)
|
|
1848
2204
|
),
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
{
|
|
1856
|
-
family,
|
|
1857
|
-
genreUuid,
|
|
1858
|
-
genrePlaylist,
|
|
1859
|
-
scope,
|
|
1860
|
-
getBackendBaseUrl,
|
|
2205
|
+
side,
|
|
2206
|
+
onToggleSide: canToggleSide ? () => updateGenre({ uuid: genreUuid, data: { side: side === "pop" ? "core" : "pop" } }) : void 0,
|
|
2207
|
+
toggleSideDisabledReason: popSibling ? `${popSibling.name} is already the pop side of ${parentPlaylist?.name}` : void 0,
|
|
2208
|
+
additionalActions
|
|
2209
|
+
},
|
|
2210
|
+
extras: {
|
|
1861
2211
|
criteriaOverviewSchema,
|
|
1862
2212
|
renderGenreDetailExtras,
|
|
1863
2213
|
renderArtistLink,
|
|
@@ -1867,10 +2217,9 @@ function GenreTreeView({
|
|
|
1867
2217
|
renderSongLeading,
|
|
1868
2218
|
renderSongActions,
|
|
1869
2219
|
renderTracksToolbar
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
] }),
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
),
|
|
1874
2223
|
aside: ancestors
|
|
1875
2224
|
};
|
|
1876
2225
|
},
|
|
@@ -1884,6 +2233,7 @@ function GenreTreeView({
|
|
|
1884
2233
|
hidePopup,
|
|
1885
2234
|
onParentChanged,
|
|
1886
2235
|
visibleGenrePlaylists,
|
|
2236
|
+
updateGenre,
|
|
1887
2237
|
playback,
|
|
1888
2238
|
filterArtist,
|
|
1889
2239
|
scope,
|
|
@@ -1900,7 +2250,7 @@ function GenreTreeView({
|
|
|
1900
2250
|
);
|
|
1901
2251
|
const isRotationControlled = controlledAllowWheelRotation !== void 0;
|
|
1902
2252
|
const allowWheelRotation = controlledAllowWheelRotation ?? internalAllowWheelRotation;
|
|
1903
|
-
const detailTab = placement === "regions" && (
|
|
2253
|
+
const detailTab = placement === "regions" && (hasSelectedGenreNode || detailOverlay) ? /* @__PURE__ */ jsx28(
|
|
1904
2254
|
PanelEdgeTab,
|
|
1905
2255
|
{
|
|
1906
2256
|
side: "left",
|
|
@@ -1908,25 +2258,14 @@ function GenreTreeView({
|
|
|
1908
2258
|
center: detailExpanded ? null : isCollapsed ? { label: "Show panel", onClick: () => changeCollapsed(false) } : onDetailExpandedChange ? { label: "Extend panel", onClick: () => onDetailExpandedChange(true) } : null
|
|
1909
2259
|
}
|
|
1910
2260
|
) : null;
|
|
1911
|
-
const
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
genrePlaylists: visibleGenrePlaylists,
|
|
1916
|
-
onSelect: handleGenreSearchSelect,
|
|
1917
|
-
selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
|
|
1918
|
-
onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
|
|
1919
|
-
onPrevious: handlePrevious,
|
|
1920
|
-
onQueryChange: artistSearch ? setSearchText : void 0,
|
|
1921
|
-
extraResults: artistExtraResults
|
|
1922
|
-
}
|
|
1923
|
-
),
|
|
1924
|
-
!isLoading && filterArtist && /* @__PURE__ */ jsxs17("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
|
|
1925
|
-
/* @__PURE__ */ jsx24(
|
|
2261
|
+
const renderArtistFilter = () => {
|
|
2262
|
+
if (isLoading || !filterArtist) return null;
|
|
2263
|
+
return /* @__PURE__ */ jsxs21("span", { className: "inline-flex items-center overflow-hidden rounded-full bg-[var(--color-neutral-100)] text-sm", children: [
|
|
2264
|
+
/* @__PURE__ */ jsx28(
|
|
1926
2265
|
"button",
|
|
1927
2266
|
{
|
|
1928
2267
|
type: "button",
|
|
1929
|
-
className: "py-1 pl-3 pr-2 hover:underline",
|
|
2268
|
+
className: "py-1 pl-3 pr-2 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:underline",
|
|
1930
2269
|
"aria-label": `Show artist ${filterArtist.name}`,
|
|
1931
2270
|
onClick: () => {
|
|
1932
2271
|
if (selectedNodeId !== null) clearSelection();
|
|
@@ -1934,86 +2273,174 @@ function GenreTreeView({
|
|
|
1934
2273
|
children: filterArtist.name
|
|
1935
2274
|
}
|
|
1936
2275
|
),
|
|
1937
|
-
/* @__PURE__ */
|
|
2276
|
+
/* @__PURE__ */ jsx28(
|
|
1938
2277
|
"button",
|
|
1939
2278
|
{
|
|
1940
2279
|
type: "button",
|
|
1941
|
-
className: "py-1 pl-1 pr-3 hover:text-[var(--color-text)]",
|
|
2280
|
+
className: "py-1 pl-1 pr-3 [@media(pointer:coarse)]:min-h-[var(--layout-touch-target)] hover:text-[var(--color-text)]",
|
|
1942
2281
|
"aria-label": `Clear artist filter ${filterArtist.name}`,
|
|
1943
2282
|
onClick: () => changeArtist(null),
|
|
1944
2283
|
children: "\xD7"
|
|
1945
2284
|
}
|
|
1946
2285
|
)
|
|
1947
|
-
] })
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
value: viewMode,
|
|
1953
|
-
onChange: setViewMode,
|
|
1954
|
-
options: [
|
|
1955
|
-
{ value: "pop-core", label: "Pop/Core", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE },
|
|
1956
|
-
{ value: "wheel", label: "Wheel" },
|
|
1957
|
-
{ value: "stacked", label: "Stacked" },
|
|
1958
|
-
{ value: "outline", label: "Outline", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE }
|
|
1959
|
-
]
|
|
1960
|
-
}
|
|
1961
|
-
),
|
|
1962
|
-
!isLoading && showDisplayOptions && /* @__PURE__ */ jsx24(
|
|
1963
|
-
"div",
|
|
2286
|
+
] });
|
|
2287
|
+
};
|
|
2288
|
+
const actions = /* @__PURE__ */ jsxs21(Fragment9, { children: [
|
|
2289
|
+
!isLoading && /* @__PURE__ */ jsx28(
|
|
2290
|
+
GenreSearch,
|
|
1964
2291
|
{
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
size: "sm",
|
|
1973
|
-
onClick: toggleWheelRotation,
|
|
1974
|
-
children: "Rotation"
|
|
1975
|
-
}
|
|
1976
|
-
)
|
|
2292
|
+
genrePlaylists: visibleGenrePlaylists,
|
|
2293
|
+
onSelect: handleGenreSearchSelect,
|
|
2294
|
+
selectedName: selectedGenrePlaylist?.name ?? builtInArtistPanel?.title ?? null,
|
|
2295
|
+
onClear: builtInArtistPanel ? () => changeArtist(null) : handleGenreSearchClear,
|
|
2296
|
+
onPrevious: handlePrevious,
|
|
2297
|
+
onQueryChange: artistSearch ? setSearchText : void 0,
|
|
2298
|
+
extraResults: artistExtraResults
|
|
1977
2299
|
}
|
|
1978
2300
|
),
|
|
1979
|
-
!isLoading &&
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
2301
|
+
!isLoading && filterArtist && /* @__PURE__ */ jsx28("div", { className: "hidden w-full justify-center max-md:flex", children: renderArtistFilter() }),
|
|
2302
|
+
/* @__PURE__ */ jsxs21("div", { className: "contents max-md:flex max-md:w-full max-md:items-center max-md:gap-2 max-md:overflow-x-auto max-md:[scrollbar-width:none] max-md:[&>*]:shrink-0", children: [
|
|
2303
|
+
!isLoading && !isControlled && /* @__PURE__ */ jsx28(
|
|
2304
|
+
SegmentedControl2,
|
|
2305
|
+
{
|
|
2306
|
+
"aria-label": "Tree view mode",
|
|
2307
|
+
value: viewMode,
|
|
2308
|
+
onChange: setViewMode,
|
|
2309
|
+
options: [
|
|
2310
|
+
{ value: "pop-core", label: "Pop/Core", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE },
|
|
2311
|
+
{ value: "wheel", label: "Wheel" },
|
|
2312
|
+
{ value: "stacked", label: "Stacked" },
|
|
2313
|
+
{ value: "outline", label: "Outline", disabled: !canShowPopCore, title: canShowPopCore ? void 0 : NO_POP_ROOT_TITLE }
|
|
2314
|
+
]
|
|
2315
|
+
}
|
|
2316
|
+
),
|
|
2317
|
+
!isLoading && showDisplayOptions && /* @__PURE__ */ jsx28(
|
|
2318
|
+
"div",
|
|
2319
|
+
{
|
|
2320
|
+
className: "flex items-center gap-1",
|
|
2321
|
+
role: "group",
|
|
2322
|
+
"aria-label": "Tree display options",
|
|
2323
|
+
children: !isRotationControlled && viewMode !== "stacked" && viewMode !== "outline" && /* @__PURE__ */ jsx28(
|
|
2324
|
+
Button,
|
|
2325
|
+
{
|
|
2326
|
+
variant: allowWheelRotation ? "default" : "outline",
|
|
2327
|
+
size: "sm",
|
|
2328
|
+
onClick: toggleWheelRotation,
|
|
2329
|
+
children: "Rotation"
|
|
2330
|
+
}
|
|
2331
|
+
)
|
|
2332
|
+
}
|
|
2333
|
+
),
|
|
2334
|
+
!isLoading && !readOnly && showAddRoot && /* @__PURE__ */ jsx28(
|
|
2335
|
+
IconTextButton,
|
|
2336
|
+
{
|
|
2337
|
+
icon: Plus2,
|
|
2338
|
+
text: "Add root",
|
|
2339
|
+
onClick: () => handleGenreCreationAction(null)
|
|
2340
|
+
}
|
|
2341
|
+
)
|
|
2342
|
+
] })
|
|
1987
2343
|
] });
|
|
1988
|
-
return /* @__PURE__ */
|
|
2344
|
+
return /* @__PURE__ */ jsxs21(
|
|
1989
2345
|
"div",
|
|
1990
2346
|
{
|
|
1991
2347
|
ref: rootRef,
|
|
1992
|
-
className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS,
|
|
2348
|
+
className: ["genre-tree-view relative flex flex-col h-full", placement === "regions" && REGIONS_CLASS, className2].filter(Boolean).join(" "),
|
|
1993
2349
|
children: [
|
|
1994
|
-
/* @__PURE__ */
|
|
1995
|
-
/* @__PURE__ */
|
|
2350
|
+
/* @__PURE__ */ jsx28("style", { children: PREVIOUS_BUTTON_CSS }),
|
|
2351
|
+
/* @__PURE__ */ jsx28(
|
|
1996
2352
|
"div",
|
|
1997
2353
|
{
|
|
1998
|
-
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"}`,
|
|
2354
|
+
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))] max-md:right-[calc(var(--layout-gutter)+var(--layout-panel-inset))] max-md:top-[var(--layout-band-top)] max-md:flex-col max-md:items-stretch" : "left-5 top-5"}`,
|
|
1999
2355
|
children: actions
|
|
2000
2356
|
}
|
|
2001
2357
|
),
|
|
2002
|
-
|
|
2358
|
+
!isLoading && filterArtist && /* @__PURE__ */ jsx28(
|
|
2359
|
+
"div",
|
|
2360
|
+
{
|
|
2361
|
+
className: `absolute left-1/2 z-30 hidden -translate-x-1/2 md:block ${placement === "regions" ? "top-[calc(var(--layout-gutter)+var(--layout-panel-inset))]" : "top-5"}`,
|
|
2362
|
+
children: renderArtistFilter()
|
|
2363
|
+
}
|
|
2364
|
+
),
|
|
2365
|
+
detailTab && /* @__PURE__ */ jsx28(
|
|
2003
2366
|
"div",
|
|
2004
2367
|
{
|
|
2005
|
-
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))]"}`,
|
|
2368
|
+
className: `absolute top-1/2 z-30 -translate-y-1/2 max-md:hidden 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))]"}`,
|
|
2006
2369
|
children: detailTab
|
|
2007
2370
|
}
|
|
2008
2371
|
),
|
|
2009
|
-
/* @__PURE__ */
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2372
|
+
/* @__PURE__ */ jsx28("div", { className: "content-container flex-1 min-h-0 flex flex-row gap-4", children: /* @__PURE__ */ jsxs21("div", { className: "tree-view-container relative flex-1 min-w-0 flex flex-col h-full", children: [
|
|
2373
|
+
!isLoading && viewMode === "outline" && floatingControlsLeading && /* @__PURE__ */ jsx28("div", { className: "absolute bottom-3 right-3 z-10", children: floatingControlsLeading }),
|
|
2374
|
+
isLoading ? /* @__PURE__ */ jsx28(GenreTreeViewSkeleton, { viewMode }) : viewMode === "wheel" ? (
|
|
2375
|
+
// The info panel's top edge aligns with the tree's; the search bar floats `--layout-panel-inset` below it,
|
|
2376
|
+
// and the header inset (the bar's height plus that inset) extends the header's fill behind the bar,
|
|
2377
|
+
// Google Maps style; the 8px bottom padding mirrors the inset above the bar, and the hidden title's height is zeroed.
|
|
2378
|
+
// The panel's own Zoom in button is hidden: the search bar carries it.
|
|
2379
|
+
/* @__PURE__ */ jsx28("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__ */ jsx28(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx28(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx28(
|
|
2380
|
+
GenrePlaylistTreeWheel,
|
|
2381
|
+
{
|
|
2382
|
+
scope,
|
|
2383
|
+
genrePlaylists: visibleGenrePlaylists,
|
|
2384
|
+
handleGenreCreationAction,
|
|
2385
|
+
handleGenreRenameAction,
|
|
2386
|
+
additionalActions,
|
|
2387
|
+
onSelectedNodeChange: handleSelectedNodeChange,
|
|
2388
|
+
hideInfoPanelClose: true,
|
|
2389
|
+
hideInfoPanelTitle: true,
|
|
2390
|
+
onNodeHover: handleNodeHover,
|
|
2391
|
+
renderInfoPanelBody,
|
|
2392
|
+
playback,
|
|
2393
|
+
selectedNodeId,
|
|
2394
|
+
readOnly,
|
|
2395
|
+
allowWheelRotation,
|
|
2396
|
+
sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
|
|
2397
|
+
busy,
|
|
2398
|
+
floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs21("div", { className: "flex items-center gap-2", children: [
|
|
2399
|
+
floatingControlsLeading,
|
|
2400
|
+
canSizeByViews && /* @__PURE__ */ jsx28(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
|
|
2401
|
+
] }) : void 0,
|
|
2402
|
+
defaultViewport,
|
|
2403
|
+
viewportRestore,
|
|
2404
|
+
onViewportChange,
|
|
2405
|
+
...infoPanelCuration
|
|
2406
|
+
}
|
|
2407
|
+
) }) })
|
|
2408
|
+
) : viewMode === "pop-core" ? (
|
|
2409
|
+
// Same as the wheel: the header inset extends its fill behind the floating search bar, Google Maps style.
|
|
2410
|
+
/* @__PURE__ */ jsx28("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__ */ jsx28(GenreTreeWheelHandoff, { skeleton: /* @__PURE__ */ jsx28(GenreTreeWheelSkeleton, {}), onRevealedChange: setIsWheelRevealed, children: /* @__PURE__ */ jsx28(
|
|
2411
|
+
GenrePlaylistTreeWheelRadialPopCore,
|
|
2412
|
+
{
|
|
2413
|
+
scope,
|
|
2414
|
+
genrePlaylists: visibleGenrePlaylists,
|
|
2415
|
+
handleGenreCreationAction,
|
|
2416
|
+
handleGenreRenameAction,
|
|
2417
|
+
additionalActions,
|
|
2418
|
+
onSelectedNodeChange: handleSelectedNodeChange,
|
|
2419
|
+
hideInfoPanelClose: true,
|
|
2420
|
+
hideInfoPanelTitle: true,
|
|
2421
|
+
onNodeHover: handleNodeHover,
|
|
2422
|
+
renderInfoPanelBody,
|
|
2423
|
+
playback,
|
|
2424
|
+
selectedNodeId,
|
|
2425
|
+
readOnly,
|
|
2426
|
+
allowWheelRotation,
|
|
2427
|
+
sizeMetric: isArtistFiltered || !canSizeByViews ? "songs" : deferredSizeMetric,
|
|
2428
|
+
busy,
|
|
2429
|
+
floatingControlsLeading: floatingControlsLeading || canSizeByViews ? /* @__PURE__ */ jsxs21("div", { className: "flex items-center gap-2", children: [
|
|
2430
|
+
floatingControlsLeading,
|
|
2431
|
+
canSizeByViews && /* @__PURE__ */ jsx28(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric })
|
|
2432
|
+
] }) : void 0,
|
|
2433
|
+
defaultViewport,
|
|
2434
|
+
viewportRestore,
|
|
2435
|
+
onViewportChange,
|
|
2436
|
+
...infoPanelCuration,
|
|
2437
|
+
...focus
|
|
2438
|
+
}
|
|
2439
|
+
) }) })
|
|
2440
|
+
) : viewMode === "outline" ? /* @__PURE__ */ jsx28("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__ */ jsx28(
|
|
2441
|
+
GenrePlaylistTreeWheelRadialPopCore,
|
|
2016
2442
|
{
|
|
2443
|
+
outline: true,
|
|
2017
2444
|
scope,
|
|
2018
2445
|
genrePlaylists: visibleGenrePlaylists,
|
|
2019
2446
|
handleGenreCreationAction,
|
|
@@ -2027,105 +2454,167 @@ function GenreTreeView({
|
|
|
2027
2454
|
playback,
|
|
2028
2455
|
selectedNodeId,
|
|
2029
2456
|
readOnly,
|
|
2030
|
-
allowWheelRotation,
|
|
2031
|
-
controlsOrientation,
|
|
2032
|
-
sizeMetric: filterArtist ? "songs" : deferredSizeMetric,
|
|
2033
|
-
busy,
|
|
2034
|
-
floatingControlsLeading: filterArtist ? void 0 : /* @__PURE__ */ jsx24(SizeMetricToggle, { value: sizeMetric, onChange: changeSizeMetric }),
|
|
2035
|
-
defaultViewport,
|
|
2036
|
-
onViewportChange,
|
|
2037
2457
|
...infoPanelCuration
|
|
2038
2458
|
}
|
|
2039
|
-
) })
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2459
|
+
) }) : /* @__PURE__ */ jsx28("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(
|
|
2460
|
+
([uuid, genrePlaylistTreePerRoot], rootIndex) => {
|
|
2461
|
+
return /* @__PURE__ */ jsx28(
|
|
2462
|
+
"div",
|
|
2463
|
+
{
|
|
2464
|
+
className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
|
|
2465
|
+
children: /* @__PURE__ */ jsx28("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx28(
|
|
2466
|
+
GenrePlaylistTreePerRoot,
|
|
2467
|
+
{
|
|
2468
|
+
scope,
|
|
2469
|
+
rootUuid: uuid,
|
|
2470
|
+
genrePlaylistTreePerRoot,
|
|
2471
|
+
handleGenreCreationAction,
|
|
2472
|
+
handleGenreRenameAction,
|
|
2473
|
+
additionalActions,
|
|
2474
|
+
onSelectedNodeChange: handleSelectedNodeChange,
|
|
2475
|
+
hideInfoPanelClose: true,
|
|
2476
|
+
hideInfoPanelTitle: true,
|
|
2477
|
+
onNodeHover: handleNodeHover,
|
|
2478
|
+
renderInfoPanelBody,
|
|
2479
|
+
playback,
|
|
2480
|
+
selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
|
|
2481
|
+
readOnly,
|
|
2482
|
+
wheelZoom: "modifier",
|
|
2483
|
+
defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
|
|
2484
|
+
viewportRestore: uuid === selectedGenrePlaylist?.root.uuid && uuid === restoreRootUuid ? viewportRestore : void 0,
|
|
2485
|
+
onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
|
|
2486
|
+
...infoPanelCuration,
|
|
2487
|
+
overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
|
|
2488
|
+
}
|
|
2489
|
+
) })
|
|
2490
|
+
},
|
|
2491
|
+
uuid
|
|
2492
|
+
);
|
|
2067
2493
|
}
|
|
2068
|
-
) })
|
|
2069
|
-
)
|
|
2070
|
-
GenrePlaylistTreeWheelRadialPopCore,
|
|
2071
|
-
{
|
|
2072
|
-
outline: true,
|
|
2073
|
-
scope,
|
|
2074
|
-
genrePlaylists: visibleGenrePlaylists,
|
|
2075
|
-
handleGenreCreationAction,
|
|
2076
|
-
handleGenreRenameAction,
|
|
2077
|
-
additionalActions,
|
|
2078
|
-
onSelectedNodeChange: handleSelectedNodeChange,
|
|
2079
|
-
hideInfoPanelClose: true,
|
|
2080
|
-
hideInfoPanelTitle: true,
|
|
2081
|
-
onNodeHover: handleNodeHover,
|
|
2082
|
-
renderInfoPanelBody,
|
|
2083
|
-
playback,
|
|
2084
|
-
selectedNodeId,
|
|
2085
|
-
readOnly,
|
|
2086
|
-
...infoPanelCuration
|
|
2087
|
-
}
|
|
2088
|
-
) }) : /* @__PURE__ */ jsx24("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(
|
|
2089
|
-
([uuid, genrePlaylistTreePerRoot], rootIndex) => {
|
|
2090
|
-
return /* @__PURE__ */ jsx24(
|
|
2091
|
-
"div",
|
|
2092
|
-
{
|
|
2093
|
-
className: "tree-per-root-container relative shrink-0 h-[28rem] mt-2 mr-16 p-2 bg-[var(--color-neutral-100)] rounded-lg",
|
|
2094
|
-
children: /* @__PURE__ */ jsx24("div", { className: "graph-container relative z-10 h-full", children: /* @__PURE__ */ jsx24(
|
|
2095
|
-
GenrePlaylistTreePerRoot,
|
|
2096
|
-
{
|
|
2097
|
-
scope,
|
|
2098
|
-
rootUuid: uuid,
|
|
2099
|
-
genrePlaylistTreePerRoot,
|
|
2100
|
-
handleGenreCreationAction,
|
|
2101
|
-
handleGenreRenameAction,
|
|
2102
|
-
additionalActions,
|
|
2103
|
-
onSelectedNodeChange: handleSelectedNodeChange,
|
|
2104
|
-
hideInfoPanelClose: true,
|
|
2105
|
-
hideInfoPanelTitle: true,
|
|
2106
|
-
onNodeHover: handleNodeHover,
|
|
2107
|
-
renderInfoPanelBody,
|
|
2108
|
-
playback,
|
|
2109
|
-
selectedNodeId: uuid === selectedGenrePlaylist?.root.uuid ? selectedNodeId : null,
|
|
2110
|
-
readOnly,
|
|
2111
|
-
wheelZoom: "modifier",
|
|
2112
|
-
controlsOrientation,
|
|
2113
|
-
defaultViewport: uuid === selectedGenrePlaylist?.root.uuid ? defaultViewport : void 0,
|
|
2114
|
-
onViewportChange: uuid === selectedGenrePlaylist?.root.uuid ? onViewportChange : void 0,
|
|
2115
|
-
...infoPanelCuration,
|
|
2116
|
-
overlayPanel: rootIndex === 0 ? activeOverlayPanel : void 0
|
|
2117
|
-
}
|
|
2118
|
-
) })
|
|
2119
|
-
},
|
|
2120
|
-
uuid
|
|
2121
|
-
);
|
|
2122
|
-
}
|
|
2123
|
-
) }) }) })
|
|
2494
|
+
) })
|
|
2495
|
+
] }) })
|
|
2124
2496
|
]
|
|
2125
2497
|
}
|
|
2126
2498
|
);
|
|
2127
2499
|
}
|
|
2128
2500
|
|
|
2501
|
+
// src/genre-tree/GenreDetailPanel.tsx
|
|
2502
|
+
import { useMemo as useMemo8 } from "react";
|
|
2503
|
+
import {
|
|
2504
|
+
ACCENT_TEXT_COLOR,
|
|
2505
|
+
getGenreTreeColor as getGenreTreeColor2,
|
|
2506
|
+
InfoPanel,
|
|
2507
|
+
TEXT_COLOR,
|
|
2508
|
+
tintSurface
|
|
2509
|
+
} from "@behindthemusictree/genre-tree-view";
|
|
2510
|
+
import { Skeleton as Skeleton5 } from "@behindthemusictree/ui";
|
|
2511
|
+
import {
|
|
2512
|
+
useFetchGenreDetail
|
|
2513
|
+
} from "@behindthemusictree/app-kit/genre-tree";
|
|
2514
|
+
import { jsx as jsx29, jsxs as jsxs22 } from "react/jsx-runtime";
|
|
2515
|
+
var ITEM_COUNT_LABEL = "Songs";
|
|
2516
|
+
var RELATIVE_ITEM_COUNT = 1;
|
|
2517
|
+
function toNode(genre, parentId) {
|
|
2518
|
+
return {
|
|
2519
|
+
id: genre.uuid,
|
|
2520
|
+
parentId,
|
|
2521
|
+
name: genre.name,
|
|
2522
|
+
itemCount: RELATIVE_ITEM_COUNT,
|
|
2523
|
+
side: genre.side ?? void 0,
|
|
2524
|
+
wikidataId: genre.wikidataId
|
|
2525
|
+
};
|
|
2526
|
+
}
|
|
2527
|
+
function GenreDetailPanel({
|
|
2528
|
+
genreUuid,
|
|
2529
|
+
scope,
|
|
2530
|
+
getBackendBaseUrl,
|
|
2531
|
+
onClose,
|
|
2532
|
+
onSelectGenre,
|
|
2533
|
+
onZoomIn,
|
|
2534
|
+
shareUrl,
|
|
2535
|
+
shareTarget,
|
|
2536
|
+
collapsed,
|
|
2537
|
+
expanded,
|
|
2538
|
+
onExpandedChange,
|
|
2539
|
+
extras
|
|
2540
|
+
}) {
|
|
2541
|
+
const { data: detail, isPending, isError } = useFetchGenreDetail(genreUuid, scope, getBackendBaseUrl);
|
|
2542
|
+
const playback = useGenrePlaylistPlayback({ scope, genrePlaylists: [] });
|
|
2543
|
+
const view = useMemo8(() => {
|
|
2544
|
+
if (!detail) return null;
|
|
2545
|
+
const rootColor = getGenreTreeColor2(detail.root.wikidataId);
|
|
2546
|
+
const relative = (genre, parentId) => ({
|
|
2547
|
+
node: toNode(genre, parentId),
|
|
2548
|
+
fill: tintSurface(rootColor),
|
|
2549
|
+
textColor: TEXT_COLOR
|
|
2550
|
+
});
|
|
2551
|
+
const ancestors = [...detail.ascendants].sort((a, b) => b.degree - a.degree);
|
|
2552
|
+
return {
|
|
2553
|
+
node: {
|
|
2554
|
+
id: detail.uuid,
|
|
2555
|
+
parentId: detail.parent?.uuid ?? null,
|
|
2556
|
+
name: detail.name,
|
|
2557
|
+
itemCount: detail.tracksCount,
|
|
2558
|
+
wikidataId: detail.wikidataId
|
|
2559
|
+
},
|
|
2560
|
+
fill: rootColor,
|
|
2561
|
+
parentNode: detail.parent ? relative(detail.parent, null) : null,
|
|
2562
|
+
childNodes: detail.children.map((child) => relative(child, detail.uuid)),
|
|
2563
|
+
ancestorNodes: ancestors.map(
|
|
2564
|
+
({ ascendant }, index) => relative(ascendant, index === 0 ? null : ancestors[index - 1].ascendant.uuid)
|
|
2565
|
+
)
|
|
2566
|
+
};
|
|
2567
|
+
}, [detail]);
|
|
2568
|
+
if (isError) {
|
|
2569
|
+
return /* @__PURE__ */ jsx29("div", { role: "alert", className: "p-4 text-sm text-[var(--color-text)]", children: "This genre could not be loaded." });
|
|
2570
|
+
}
|
|
2571
|
+
if (isPending || !detail || !view) {
|
|
2572
|
+
return /* @__PURE__ */ jsxs22("div", { className: "flex flex-col gap-3 p-4", "aria-busy": "true", "data-testid": "genre-detail-panel-skeleton", children: [
|
|
2573
|
+
/* @__PURE__ */ jsx29(Skeleton5, { className: "h-6 w-2/3" }),
|
|
2574
|
+
/* @__PURE__ */ jsx29(Skeleton5, { className: "h-4 w-full" }),
|
|
2575
|
+
/* @__PURE__ */ jsx29(Skeleton5, { className: "h-4 w-3/4" })
|
|
2576
|
+
] });
|
|
2577
|
+
}
|
|
2578
|
+
const renderBody = (node, sections) => ({
|
|
2579
|
+
primary: /* @__PURE__ */ jsx29(
|
|
2580
|
+
GenreInfoPanelBody,
|
|
2581
|
+
{
|
|
2582
|
+
toolbar: sections.toolbar,
|
|
2583
|
+
family: sections.family,
|
|
2584
|
+
genreUuid: detail.uuid,
|
|
2585
|
+
genrePlaylist: { ...detail.criteriaPlaylist, tracksCount: detail.tracksCount },
|
|
2586
|
+
wikidataId: detail.wikidataId,
|
|
2587
|
+
scope,
|
|
2588
|
+
getBackendBaseUrl,
|
|
2589
|
+
playback,
|
|
2590
|
+
band: { node, onZoomIn, shareUrl, shareTarget: shareUrl ? shareTarget : void 0 },
|
|
2591
|
+
extras
|
|
2592
|
+
}
|
|
2593
|
+
),
|
|
2594
|
+
aside: null
|
|
2595
|
+
});
|
|
2596
|
+
return /* @__PURE__ */ jsx29(
|
|
2597
|
+
InfoPanel,
|
|
2598
|
+
{
|
|
2599
|
+
node: view.node,
|
|
2600
|
+
fill: view.fill,
|
|
2601
|
+
textColor: ACCENT_TEXT_COLOR,
|
|
2602
|
+
parentNode: view.parentNode,
|
|
2603
|
+
childNodes: view.childNodes,
|
|
2604
|
+
ancestorNodes: view.ancestorNodes,
|
|
2605
|
+
side: "left",
|
|
2606
|
+
onClose,
|
|
2607
|
+
onSelectNode: (nodeId) => onSelectGenre(nodeId),
|
|
2608
|
+
itemCountLabel: ITEM_COUNT_LABEL,
|
|
2609
|
+
renderInfoPanelBody: renderBody,
|
|
2610
|
+
expanded,
|
|
2611
|
+
onExpandedChange,
|
|
2612
|
+
collapsed,
|
|
2613
|
+
hideExpandButton: onExpandedChange === void 0
|
|
2614
|
+
}
|
|
2615
|
+
);
|
|
2616
|
+
}
|
|
2617
|
+
|
|
2129
2618
|
// src/genre-tree/index.ts
|
|
2130
2619
|
import {
|
|
2131
2620
|
GenreTreeSkeleton,
|
|
@@ -2135,24 +2624,35 @@ import {
|
|
|
2135
2624
|
} from "@behindthemusictree/genre-tree-view";
|
|
2136
2625
|
|
|
2137
2626
|
// src/genre-tree/track-list-sidebar/TrackListSidebar.tsx
|
|
2138
|
-
import { useEffect as
|
|
2627
|
+
import { useEffect as useEffect7, useLayoutEffect as useLayoutEffect2, useRef as useRef5 } from "react";
|
|
2628
|
+
import { ListMusic } from "lucide-react";
|
|
2139
2629
|
import { useTrackList as useTrackList5 } from "@behindthemusictree/app-kit/genre-tree";
|
|
2140
|
-
import { useTrackListSidebarVisibility } from "@behindthemusictree/app-kit/genre-tree";
|
|
2141
2630
|
import { TrackListOriginType as TrackListOriginType2 } from "@behindthemusictree/app-kit/genre-tree";
|
|
2142
2631
|
import { useTrackNoun as useTrackNoun6 } from "@behindthemusictree/app-kit/track-noun";
|
|
2143
|
-
import { jsx as
|
|
2632
|
+
import { jsx as jsx30, jsxs as jsxs23 } from "react/jsx-runtime";
|
|
2633
|
+
var useBrowserLayoutEffect = typeof window === "undefined" ? useEffect7 : useLayoutEffect2;
|
|
2144
2634
|
function TrackListSidebar({
|
|
2145
|
-
className = "",
|
|
2635
|
+
className: className2 = "",
|
|
2146
2636
|
renderDuration,
|
|
2147
2637
|
renderActions,
|
|
2148
2638
|
layout = "fixed",
|
|
2639
|
+
isCollapsed = false,
|
|
2640
|
+
onExpand,
|
|
2641
|
+
onMinimize,
|
|
2149
2642
|
renderArtistLink
|
|
2150
2643
|
}) {
|
|
2151
|
-
const { trackList, loadMore } = useTrackList5();
|
|
2644
|
+
const { trackList, loadMore, autoAdvanceScrollRequest } = useTrackList5();
|
|
2152
2645
|
const trackNoun = useTrackNoun6();
|
|
2646
|
+
const trackListRef = useRef5(null);
|
|
2647
|
+
const previousOriginRef = useRef5(trackList?.origin ?? null);
|
|
2648
|
+
const handledAutoAdvanceRequestIdRef = useRef5(null);
|
|
2649
|
+
const shouldFollowPlaybackRef = useRef5(true);
|
|
2650
|
+
const programmaticScrollTopRef = useRef5(null);
|
|
2651
|
+
const userScrollIntentRef = useRef5(false);
|
|
2652
|
+
const userScrollIntentTimerRef = useRef5(null);
|
|
2153
2653
|
const loadMoreSentinelRef = useRef5(null);
|
|
2154
2654
|
const hasMore = !!trackList && trackList.nextPage !== null;
|
|
2155
|
-
|
|
2655
|
+
useEffect7(() => {
|
|
2156
2656
|
const sentinel = loadMoreSentinelRef.current;
|
|
2157
2657
|
if (!hasMore || !sentinel) return;
|
|
2158
2658
|
const observer = new IntersectionObserver((entries) => {
|
|
@@ -2163,44 +2663,115 @@ function TrackListSidebar({
|
|
|
2163
2663
|
observer.observe(sentinel);
|
|
2164
2664
|
return () => observer.disconnect();
|
|
2165
2665
|
}, [hasMore, loadMore]);
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2666
|
+
useBrowserLayoutEffect(() => {
|
|
2667
|
+
const origin = trackList?.origin ?? null;
|
|
2668
|
+
if (previousOriginRef.current === origin) return;
|
|
2669
|
+
previousOriginRef.current = origin;
|
|
2670
|
+
handledAutoAdvanceRequestIdRef.current = null;
|
|
2671
|
+
shouldFollowPlaybackRef.current = true;
|
|
2672
|
+
programmaticScrollTopRef.current = null;
|
|
2673
|
+
if (trackListRef.current) trackListRef.current.scrollTop = 0;
|
|
2674
|
+
}, [trackList?.origin]);
|
|
2675
|
+
useEffect7(() => {
|
|
2676
|
+
const list = trackListRef.current;
|
|
2677
|
+
if (!isCollapsed && list?.scrollTop === 0) shouldFollowPlaybackRef.current = true;
|
|
2678
|
+
}, [isCollapsed]);
|
|
2679
|
+
useEffect7(
|
|
2680
|
+
() => () => {
|
|
2681
|
+
if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
|
|
2682
|
+
},
|
|
2683
|
+
[]
|
|
2684
|
+
);
|
|
2685
|
+
useBrowserLayoutEffect(() => {
|
|
2686
|
+
if (!autoAdvanceScrollRequest || handledAutoAdvanceRequestIdRef.current === autoAdvanceScrollRequest.id) {
|
|
2687
|
+
return;
|
|
2688
|
+
}
|
|
2689
|
+
if (isCollapsed) return;
|
|
2690
|
+
const list = trackListRef.current;
|
|
2691
|
+
if (!list) return;
|
|
2692
|
+
if (list.scrollTop <= 1) shouldFollowPlaybackRef.current = true;
|
|
2693
|
+
if (!shouldFollowPlaybackRef.current) {
|
|
2694
|
+
handledAutoAdvanceRequestIdRef.current = autoAdvanceScrollRequest.id;
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
const row = Array.from(list.querySelectorAll("[data-track-uuid]")).find(
|
|
2698
|
+
(element) => element.dataset.trackUuid === autoAdvanceScrollRequest.trackUuid
|
|
2699
|
+
);
|
|
2700
|
+
if (!row) return;
|
|
2701
|
+
handledAutoAdvanceRequestIdRef.current = autoAdvanceScrollRequest.id;
|
|
2702
|
+
const rowTop = row.getBoundingClientRect().top - list.getBoundingClientRect().top;
|
|
2703
|
+
const targetScrollTop = list.scrollTop + rowTop;
|
|
2704
|
+
programmaticScrollTopRef.current = targetScrollTop;
|
|
2705
|
+
list.scrollTop = targetScrollTop;
|
|
2706
|
+
programmaticScrollTopRef.current = list.scrollTop;
|
|
2707
|
+
}, [autoAdvanceScrollRequest, isCollapsed, trackList]);
|
|
2708
|
+
const markUserScrollIntent = () => {
|
|
2709
|
+
userScrollIntentRef.current = true;
|
|
2710
|
+
if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
|
|
2711
|
+
userScrollIntentTimerRef.current = setTimeout(() => {
|
|
2712
|
+
userScrollIntentRef.current = false;
|
|
2713
|
+
userScrollIntentTimerRef.current = null;
|
|
2714
|
+
}, 250);
|
|
2715
|
+
};
|
|
2716
|
+
const handleTrackListScroll = (event) => {
|
|
2717
|
+
const list = event.currentTarget;
|
|
2718
|
+
if (userScrollIntentRef.current) {
|
|
2719
|
+
userScrollIntentRef.current = false;
|
|
2720
|
+
if (userScrollIntentTimerRef.current !== null) clearTimeout(userScrollIntentTimerRef.current);
|
|
2721
|
+
userScrollIntentTimerRef.current = null;
|
|
2722
|
+
programmaticScrollTopRef.current = null;
|
|
2723
|
+
shouldFollowPlaybackRef.current = list.scrollTop <= 1;
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
const programmaticScrollTop = programmaticScrollTopRef.current;
|
|
2727
|
+
if (programmaticScrollTop !== null) {
|
|
2728
|
+
if (Math.abs(list.scrollTop - programmaticScrollTop) <= 1) programmaticScrollTopRef.current = null;
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
};
|
|
2732
|
+
const positionClasses = layout === "inline" ? "relative w-full flex h-full flex-col" : "fixed right-0 w-[50rem] flex flex-col";
|
|
2733
|
+
return trackList ? /* @__PURE__ */ jsxs23(
|
|
2169
2734
|
"div",
|
|
2170
2735
|
{
|
|
2171
2736
|
"data-theme": "dark",
|
|
2172
|
-
className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${
|
|
2173
|
-
style: layout === "fixed" ? {
|
|
2174
|
-
bottom: "79px"
|
|
2175
|
-
/* Match player height exactly */
|
|
2176
|
-
} : void 0,
|
|
2737
|
+
className: `track-list-sidebar ${positionClasses} rounded-2xl bg-[var(--color-surface)] pb-1 ${className2}`,
|
|
2738
|
+
style: layout === "fixed" ? { top: "63.5px", maxHeight: "calc(100vh - 63.5px - 79px)" } : void 0,
|
|
2177
2739
|
children: [
|
|
2178
|
-
/* @__PURE__ */
|
|
2179
|
-
/* @__PURE__ */
|
|
2180
|
-
/* @__PURE__ */
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2740
|
+
/* @__PURE__ */ jsxs23("div", { className: "header flex h-16 items-center px-4 py-2 text-[var(--color-text)]", children: [
|
|
2741
|
+
/* @__PURE__ */ jsxs23("div", { className: "min-w-0 flex flex-1 flex-col justify-center", children: [
|
|
2742
|
+
/* @__PURE__ */ jsxs23("div", { className: "origin flex text-xl", children: [
|
|
2743
|
+
/* @__PURE__ */ jsx30("div", { className: "from h-auto flex flex-col justify-center items-center mr-2", children: "From" }),
|
|
2744
|
+
/* @__PURE__ */ jsx30("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__ */ jsx30("div", { className: "name max-w-full truncate", children: trackList.origin.label }) })
|
|
2745
|
+
] }),
|
|
2746
|
+
/* @__PURE__ */ jsxs23("div", { className: "info flex items-center text-sm text-[var(--color-text-muted)]", children: [
|
|
2747
|
+
trackList.origin.type === TrackListOriginType2.GENRE_PLAYLIST ? "Genre \u2022 " : trackList.origin.type === TrackListOriginType2.ARTIST ? "Artist \u2022 " : `${trackNoun.singular} playlist \u2022 `,
|
|
2748
|
+
`${trackList.total} ${trackList.total > 1 ? trackNoun.plural : trackNoun.singular}`
|
|
2749
|
+
] })
|
|
2186
2750
|
] }),
|
|
2187
|
-
/* @__PURE__ */
|
|
2751
|
+
(isCollapsed && onExpand || !isCollapsed && onMinimize) && /* @__PURE__ */ jsx30(
|
|
2188
2752
|
"button",
|
|
2189
2753
|
{
|
|
2190
2754
|
type: "button",
|
|
2191
|
-
"aria-label": "
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2755
|
+
"aria-label": isCollapsed ? "Show tracks" : "Hide tracks",
|
|
2756
|
+
title: isCollapsed ? "Show tracks" : "Hide tracks",
|
|
2757
|
+
"aria-expanded": !isCollapsed,
|
|
2758
|
+
className: `ml-2 flex h-8 w-8 shrink-0 items-center justify-center rounded-md hover:bg-[var(--color-surface-muted)] ${isCollapsed ? "text-[var(--color-text-muted)]" : "text-white"}`,
|
|
2759
|
+
onClick: isCollapsed ? onExpand : onMinimize,
|
|
2760
|
+
children: /* @__PURE__ */ jsx30(ListMusic, { "aria-hidden": "true", className: "h-5 w-5" })
|
|
2195
2761
|
}
|
|
2196
2762
|
)
|
|
2197
2763
|
] }),
|
|
2198
|
-
/* @__PURE__ */
|
|
2764
|
+
!isCollapsed && /* @__PURE__ */ jsxs23(
|
|
2199
2765
|
"ul",
|
|
2200
2766
|
{
|
|
2201
|
-
|
|
2767
|
+
ref: trackListRef,
|
|
2768
|
+
onScroll: handleTrackListScroll,
|
|
2769
|
+
onWheel: markUserScrollIntent,
|
|
2770
|
+
onTouchMove: markUserScrollIntent,
|
|
2771
|
+
onPointerDown: markUserScrollIntent,
|
|
2772
|
+
className: layout === "inline" ? "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0" : "track-list flex-1 min-h-0 overflow-auto list-none p-0 m-0",
|
|
2202
2773
|
children: [
|
|
2203
|
-
trackList.tracks.map((track, index) => /* @__PURE__ */
|
|
2774
|
+
trackList.tracks.map((track, index) => /* @__PURE__ */ jsx30("li", { "data-track-uuid": track.uuid, children: /* @__PURE__ */ jsx30(
|
|
2204
2775
|
TrackItem,
|
|
2205
2776
|
{
|
|
2206
2777
|
track,
|
|
@@ -2210,7 +2781,7 @@ function TrackListSidebar({
|
|
|
2210
2781
|
renderArtistLink
|
|
2211
2782
|
}
|
|
2212
2783
|
) }, track.uuid)),
|
|
2213
|
-
hasMore && /* @__PURE__ */
|
|
2784
|
+
hasMore && /* @__PURE__ */ jsx30("li", { ref: loadMoreSentinelRef, "aria-hidden": "true", className: "h-px" })
|
|
2214
2785
|
]
|
|
2215
2786
|
}
|
|
2216
2787
|
)
|
|
@@ -2220,47 +2791,167 @@ function TrackListSidebar({
|
|
|
2220
2791
|
}
|
|
2221
2792
|
|
|
2222
2793
|
// src/genre-tree/PlayerPanel.tsx
|
|
2223
|
-
import { useState as
|
|
2224
|
-
import { X as X2 } from "lucide-react";
|
|
2794
|
+
import { useEffect as useEffect8, useState as useState6 } from "react";
|
|
2225
2795
|
import { twMerge as twMerge2 } from "tailwind-merge";
|
|
2226
2796
|
import {
|
|
2227
|
-
usePlayer as
|
|
2797
|
+
usePlayer as usePlayer10,
|
|
2228
2798
|
PlayerVideoSurface
|
|
2229
2799
|
} from "@behindthemusictree/app-kit/player";
|
|
2230
2800
|
import {
|
|
2231
|
-
useTrackListSidebarVisibility
|
|
2801
|
+
useTrackListSidebarVisibility
|
|
2232
2802
|
} from "@behindthemusictree/app-kit/genre-tree";
|
|
2233
|
-
|
|
2803
|
+
|
|
2804
|
+
// src/genre-tree/PlayerControls.tsx
|
|
2805
|
+
import { Pause as Pause2, Play as Play3, SkipBack, SkipForward, Volume2, VolumeX } from "lucide-react";
|
|
2806
|
+
import { formatTime, useTrackList as useTrackList6 } from "@behindthemusictree/app-kit/genre-tree";
|
|
2807
|
+
import { PlayStates as PlayStates3, useCurrentTime, usePlayer as usePlayer9 } from "@behindthemusictree/app-kit/player";
|
|
2808
|
+
import { jsx as jsx31, jsxs as jsxs24 } from "react/jsx-runtime";
|
|
2809
|
+
function PlayerControls() {
|
|
2810
|
+
const {
|
|
2811
|
+
duration,
|
|
2812
|
+
handlePlayPauseAction,
|
|
2813
|
+
isLoading,
|
|
2814
|
+
playState,
|
|
2815
|
+
playerTrackObject,
|
|
2816
|
+
seekTo,
|
|
2817
|
+
setVolume,
|
|
2818
|
+
volume
|
|
2819
|
+
} = usePlayer9();
|
|
2820
|
+
const { hasNextTrack, hasPreviousTrack, playNextTrack, playPreviousTrack } = useTrackList6();
|
|
2821
|
+
const currentTime = useCurrentTime();
|
|
2822
|
+
const isBusy = isLoading || playState === PlayStates3.LOADING;
|
|
2823
|
+
const isReady = !!playerTrackObject?.isReady && !isBusy && !playerTrackObject.loadError;
|
|
2824
|
+
const safeDuration = Number.isFinite(duration) && duration > 0 ? duration : 0;
|
|
2825
|
+
const safeCurrentTime = Number.isFinite(currentTime) ? Math.max(0, Math.min(currentTime, safeDuration)) : 0;
|
|
2826
|
+
const isPlaying = playState === PlayStates3.PLAYING;
|
|
2827
|
+
return /* @__PURE__ */ jsxs24(
|
|
2828
|
+
"section",
|
|
2829
|
+
{
|
|
2830
|
+
"aria-label": "Player controls",
|
|
2831
|
+
className: "player-controls flex shrink-0 flex-col gap-2 border-b border-zinc-800 px-3 py-2 text-zinc-200",
|
|
2832
|
+
children: [
|
|
2833
|
+
/* @__PURE__ */ jsxs24("div", { className: "flex min-w-0 items-center gap-2 text-xs tabular-nums text-zinc-400", children: [
|
|
2834
|
+
/* @__PURE__ */ jsx31("span", { "aria-label": "Elapsed time", children: formatTime(safeCurrentTime) }),
|
|
2835
|
+
/* @__PURE__ */ jsx31(
|
|
2836
|
+
"input",
|
|
2837
|
+
{
|
|
2838
|
+
"aria-label": "Seek",
|
|
2839
|
+
className: "h-1 min-w-0 flex-1 cursor-pointer accent-white disabled:cursor-not-allowed disabled:opacity-40",
|
|
2840
|
+
type: "range",
|
|
2841
|
+
min: 0,
|
|
2842
|
+
max: safeDuration,
|
|
2843
|
+
step: 0.1,
|
|
2844
|
+
value: safeCurrentTime,
|
|
2845
|
+
disabled: !isReady || safeDuration <= 0,
|
|
2846
|
+
onChange: (event) => seekTo(Number(event.currentTarget.value))
|
|
2847
|
+
}
|
|
2848
|
+
),
|
|
2849
|
+
/* @__PURE__ */ jsx31("span", { "aria-label": "Duration", children: formatTime(safeDuration) })
|
|
2850
|
+
] }),
|
|
2851
|
+
/* @__PURE__ */ jsxs24("div", { className: "grid min-h-9 grid-cols-[1fr_auto_1fr] items-center gap-3", children: [
|
|
2852
|
+
/* @__PURE__ */ jsxs24("div", { className: "col-start-2 flex items-center justify-center gap-2", children: [
|
|
2853
|
+
/* @__PURE__ */ jsx31(
|
|
2854
|
+
"button",
|
|
2855
|
+
{
|
|
2856
|
+
type: "button",
|
|
2857
|
+
"aria-label": "Previous track",
|
|
2858
|
+
title: "Previous track",
|
|
2859
|
+
disabled: !isReady || !hasPreviousTrack,
|
|
2860
|
+
onClick: playPreviousTrack,
|
|
2861
|
+
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
|
|
2862
|
+
children: /* @__PURE__ */ jsx31(SkipBack, { "aria-hidden": "true", className: "h-4 w-4" })
|
|
2863
|
+
}
|
|
2864
|
+
),
|
|
2865
|
+
/* @__PURE__ */ jsx31(
|
|
2866
|
+
"button",
|
|
2867
|
+
{
|
|
2868
|
+
type: "button",
|
|
2869
|
+
"aria-label": isPlaying ? "Pause" : "Play",
|
|
2870
|
+
title: isPlaying ? "Pause" : "Play",
|
|
2871
|
+
disabled: !isReady,
|
|
2872
|
+
onClick: handlePlayPauseAction,
|
|
2873
|
+
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white text-zinc-950 hover:bg-zinc-200 disabled:cursor-not-allowed disabled:opacity-40",
|
|
2874
|
+
children: isPlaying ? /* @__PURE__ */ jsx31(Pause2, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" }) : /* @__PURE__ */ jsx31(Play3, { "aria-hidden": "true", className: "h-4 w-4", fill: "currentColor" })
|
|
2875
|
+
}
|
|
2876
|
+
),
|
|
2877
|
+
/* @__PURE__ */ jsx31(
|
|
2878
|
+
"button",
|
|
2879
|
+
{
|
|
2880
|
+
type: "button",
|
|
2881
|
+
"aria-label": "Next track",
|
|
2882
|
+
title: "Next track",
|
|
2883
|
+
disabled: !isReady || !hasNextTrack,
|
|
2884
|
+
onClick: playNextTrack,
|
|
2885
|
+
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-zinc-300 hover:bg-zinc-800 hover:text-white disabled:cursor-not-allowed disabled:opacity-40",
|
|
2886
|
+
children: /* @__PURE__ */ jsx31(SkipForward, { "aria-hidden": "true", className: "h-4 w-4" })
|
|
2887
|
+
}
|
|
2888
|
+
)
|
|
2889
|
+
] }),
|
|
2890
|
+
/* @__PURE__ */ jsxs24("label", { className: "col-start-3 flex w-[4.2rem] shrink-0 items-center gap-[0.35rem] justify-self-end text-zinc-400 sm:w-[4.9rem]", children: [
|
|
2891
|
+
volume === 0 ? /* @__PURE__ */ jsx31(VolumeX, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }) : /* @__PURE__ */ jsx31(Volume2, { "aria-hidden": "true", className: "h-[0.7rem] w-[0.7rem] shrink-0" }),
|
|
2892
|
+
/* @__PURE__ */ jsx31("span", { className: "sr-only", children: "Volume" }),
|
|
2893
|
+
/* @__PURE__ */ jsx31(
|
|
2894
|
+
"input",
|
|
2895
|
+
{
|
|
2896
|
+
"aria-label": "Volume",
|
|
2897
|
+
className: "h-[3px] min-w-0 flex-1 cursor-pointer accent-white disabled:cursor-not-allowed disabled:opacity-40",
|
|
2898
|
+
type: "range",
|
|
2899
|
+
min: 0,
|
|
2900
|
+
max: 100,
|
|
2901
|
+
step: 1,
|
|
2902
|
+
value: volume,
|
|
2903
|
+
disabled: !isReady,
|
|
2904
|
+
onChange: (event) => setVolume(Number(event.currentTarget.value))
|
|
2905
|
+
}
|
|
2906
|
+
)
|
|
2907
|
+
] })
|
|
2908
|
+
] })
|
|
2909
|
+
]
|
|
2910
|
+
}
|
|
2911
|
+
);
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
// src/genre-tree/PlayerPanel.tsx
|
|
2915
|
+
import { jsx as jsx32, jsxs as jsxs25 } from "react/jsx-runtime";
|
|
2234
2916
|
var PANEL_ID = "player-panel";
|
|
2235
|
-
var iconButtonClassName = "flex h-6 w-6 shrink-0 items-center justify-center p-0 rounded-md hover:bg-white/10";
|
|
2236
2917
|
var DEFAULT_EXTENDED_WIDTH = "calc(100vw - var(--layout-center-left) - var(--layout-gutter))";
|
|
2237
2918
|
function PlayerPanel({
|
|
2238
|
-
className,
|
|
2919
|
+
className: className2,
|
|
2239
2920
|
isVisible = true,
|
|
2240
|
-
onClose,
|
|
2241
2921
|
expanded,
|
|
2242
2922
|
onExpandedChange,
|
|
2243
2923
|
extendedWidth = DEFAULT_EXTENDED_WIDTH,
|
|
2244
2924
|
renderArtistLink
|
|
2245
2925
|
}) {
|
|
2246
|
-
const { playerTrackObject } =
|
|
2247
|
-
const { toggleTrackListSidebar, isTrackListSidebarVisible } =
|
|
2248
|
-
const [
|
|
2926
|
+
const { playerTrackObject } = usePlayer10();
|
|
2927
|
+
const { toggleTrackListSidebar, isTrackListSidebarVisible } = useTrackListSidebarVisibility();
|
|
2928
|
+
const [lastLoadedTrackTitle, setLastLoadedTrackTitle] = useState6("");
|
|
2929
|
+
const loadedTrackTitle = playerTrackObject?.track.title;
|
|
2930
|
+
useEffect8(() => {
|
|
2931
|
+
if (loadedTrackTitle) setLastLoadedTrackTitle(loadedTrackTitle);
|
|
2932
|
+
}, [loadedTrackTitle]);
|
|
2933
|
+
const [uncontrolledExtended, setUncontrolledExtended] = useState6(false);
|
|
2249
2934
|
const isExtendRequested = expanded ?? uncontrolledExtended;
|
|
2250
2935
|
const setIsExtendRequested = (next) => {
|
|
2251
2936
|
setUncontrolledExtended(next);
|
|
2252
2937
|
onExpandedChange?.(next);
|
|
2253
2938
|
};
|
|
2254
2939
|
const isExtended = isExtendRequested && isTrackListSidebarVisible;
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
} : { label: "Hide song list", onClick: toggleTrackListSidebar };
|
|
2259
|
-
const tabCenter = !isTrackListSidebarVisible ? { label: "Show song list", onClick: toggleTrackListSidebar } : isExtended ? null : {
|
|
2260
|
-
label: "Extend song list panel",
|
|
2261
|
-
onClick: () => setIsExtendRequested(true)
|
|
2940
|
+
const hideSongList = () => {
|
|
2941
|
+
setIsExtendRequested(false);
|
|
2942
|
+
if (isTrackListSidebarVisible) toggleTrackListSidebar();
|
|
2262
2943
|
};
|
|
2263
|
-
|
|
2944
|
+
const tabEdge = isTrackListSidebarVisible && isExtended ? {
|
|
2945
|
+
label: "Shrink player panel",
|
|
2946
|
+
onClick: () => setIsExtendRequested(false),
|
|
2947
|
+
expanded: true
|
|
2948
|
+
} : null;
|
|
2949
|
+
const tabCenter = isTrackListSidebarVisible && !isExtended ? {
|
|
2950
|
+
label: "Extend player panel",
|
|
2951
|
+
onClick: () => setIsExtendRequested(true),
|
|
2952
|
+
expanded: false
|
|
2953
|
+
} : null;
|
|
2954
|
+
return /* @__PURE__ */ jsxs25(
|
|
2264
2955
|
"div",
|
|
2265
2956
|
{
|
|
2266
2957
|
id: PANEL_ID,
|
|
@@ -2269,11 +2960,11 @@ function PlayerPanel({
|
|
|
2269
2960
|
!isExtended && "w-[var(--layout-right-w)]",
|
|
2270
2961
|
isTrackListSidebarVisible && "h-[calc(100vh-var(--layout-band-top)-3.5rem)]",
|
|
2271
2962
|
!isVisible && "hidden",
|
|
2272
|
-
|
|
2963
|
+
className2
|
|
2273
2964
|
),
|
|
2274
2965
|
style: isExtended ? { width: extendedWidth } : void 0,
|
|
2275
2966
|
children: [
|
|
2276
|
-
/* @__PURE__ */
|
|
2967
|
+
/* @__PURE__ */ jsx32(
|
|
2277
2968
|
PanelEdgeTab,
|
|
2278
2969
|
{
|
|
2279
2970
|
side: "right",
|
|
@@ -2283,7 +2974,7 @@ function PlayerPanel({
|
|
|
2283
2974
|
className: "absolute right-full top-1/2 -translate-y-1/2"
|
|
2284
2975
|
}
|
|
2285
2976
|
),
|
|
2286
|
-
/* @__PURE__ */
|
|
2977
|
+
/* @__PURE__ */ jsxs25(
|
|
2287
2978
|
"div",
|
|
2288
2979
|
{
|
|
2289
2980
|
className: twMerge2(
|
|
@@ -2291,29 +2982,21 @@ function PlayerPanel({
|
|
|
2291
2982
|
isExtended ? "flex-row" : "flex-col"
|
|
2292
2983
|
),
|
|
2293
2984
|
children: [
|
|
2294
|
-
/* @__PURE__ */
|
|
2295
|
-
/* @__PURE__ */
|
|
2296
|
-
|
|
2297
|
-
/* @__PURE__ */
|
|
2298
|
-
|
|
2299
|
-
{
|
|
2300
|
-
type: "button",
|
|
2301
|
-
onClick: onClose,
|
|
2302
|
-
className: iconButtonClassName,
|
|
2303
|
-
"aria-label": "Close player",
|
|
2304
|
-
children: /* @__PURE__ */ jsx26(X2, { size: 16 })
|
|
2305
|
-
}
|
|
2306
|
-
) })
|
|
2985
|
+
/* @__PURE__ */ jsxs25("div", { className: "flex w-[var(--layout-right-w)] shrink-0 flex-col", children: [
|
|
2986
|
+
/* @__PURE__ */ jsx32("div", { className: "flex shrink-0 items-center justify-between gap-2 border-b border-zinc-800 p-2", children: /* @__PURE__ */ jsx32("span", { className: "min-w-0 truncate text-sm font-semibold", children: loadedTrackTitle || lastLoadedTrackTitle }) }),
|
|
2987
|
+
/* @__PURE__ */ jsxs25("div", { className: "relative w-full shrink-0", children: [
|
|
2988
|
+
/* @__PURE__ */ jsx32(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
|
|
2989
|
+
playerTrackObject?.loadError && /* @__PURE__ */ jsx32("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
|
|
2307
2990
|
] }),
|
|
2308
|
-
/* @__PURE__ */
|
|
2309
|
-
/* @__PURE__ */ jsx26(PlayerVideoSurface, { className: "aspect-video w-full bg-black" }),
|
|
2310
|
-
playerTrackObject?.loadError && /* @__PURE__ */ jsx26("span", { className: "absolute bottom-2 left-2 right-2 text-xs text-red-400", children: playerTrackObject.loadError })
|
|
2311
|
-
] })
|
|
2991
|
+
/* @__PURE__ */ jsx32(PlayerControls, {})
|
|
2312
2992
|
] }),
|
|
2313
|
-
|
|
2993
|
+
/* @__PURE__ */ jsx32(
|
|
2314
2994
|
TrackListSidebar,
|
|
2315
2995
|
{
|
|
2316
2996
|
layout: "inline",
|
|
2997
|
+
isCollapsed: !isTrackListSidebarVisible,
|
|
2998
|
+
onExpand: toggleTrackListSidebar,
|
|
2999
|
+
onMinimize: hideSongList,
|
|
2317
3000
|
className: twMerge2(
|
|
2318
3001
|
"min-h-0 min-w-0 flex-1",
|
|
2319
3002
|
isExtended && "border-l border-zinc-800"
|
|
@@ -2333,11 +3016,11 @@ function PlayerPanel({
|
|
|
2333
3016
|
import { v4 as uuidv4 } from "uuid";
|
|
2334
3017
|
import { AiFillStar, AiOutlineStar } from "react-icons/ai";
|
|
2335
3018
|
import { FORM_RATING_NULL_VALUE } from "@behindthemusictree/app-kit/genre-tree";
|
|
2336
|
-
import { jsx as
|
|
3019
|
+
import { jsx as jsx33, jsxs as jsxs26 } from "react/jsx-runtime";
|
|
2337
3020
|
function Rating({ rating, handleChange }) {
|
|
2338
3021
|
const instanceId = uuidv4();
|
|
2339
|
-
return /* @__PURE__ */
|
|
2340
|
-
/* @__PURE__ */
|
|
3022
|
+
return /* @__PURE__ */ jsx33("div", { className: "flex items-center", children: [...Array(6)].map((_, i) => /* @__PURE__ */ jsxs26("div", { className: `relative ${i === 0 ? "mr-4" : "mr-1"}`, children: [
|
|
3023
|
+
/* @__PURE__ */ jsx33(
|
|
2341
3024
|
"input",
|
|
2342
3025
|
{
|
|
2343
3026
|
id: `star-${instanceId}-${i}`,
|
|
@@ -2349,16 +3032,16 @@ function Rating({ rating, handleChange }) {
|
|
|
2349
3032
|
onChange: handleChange
|
|
2350
3033
|
}
|
|
2351
3034
|
),
|
|
2352
|
-
/* @__PURE__ */
|
|
2353
|
-
/* @__PURE__ */
|
|
3035
|
+
/* @__PURE__ */ jsx33("label", { htmlFor: `star-${instanceId}-${i}`, className: "cursor-pointer", children: i === 0 ? /* @__PURE__ */ jsxs26("div", { className: "flex", children: [
|
|
3036
|
+
/* @__PURE__ */ jsx33(
|
|
2354
3037
|
AiOutlineStar,
|
|
2355
3038
|
{
|
|
2356
3039
|
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)]"}`
|
|
2357
3040
|
}
|
|
2358
3041
|
),
|
|
2359
|
-
/* @__PURE__ */
|
|
3042
|
+
/* @__PURE__ */ jsx33("span", { className: "flex-grow w-8" }),
|
|
2360
3043
|
" "
|
|
2361
|
-
] }) : /* @__PURE__ */
|
|
3044
|
+
] }) : /* @__PURE__ */ jsx33(
|
|
2362
3045
|
AiFillStar,
|
|
2363
3046
|
{
|
|
2364
3047
|
className: `w-7 h-7 absolute top-0 left-0 ${(rating ?? -1) >= i ? "text-[var(--color-accent-rating)]" : "text-[var(--color-text-muted)]"}`
|
|
@@ -2368,12 +3051,17 @@ function Rating({ rating, handleChange }) {
|
|
|
2368
3051
|
}
|
|
2369
3052
|
|
|
2370
3053
|
export {
|
|
3054
|
+
useGenrePlaylistPlayback,
|
|
2371
3055
|
GenrePlaylistTreePerRoot,
|
|
2372
3056
|
GenreSearch,
|
|
3057
|
+
GenreNodeActionBand,
|
|
3058
|
+
GenreInfoPanelBody,
|
|
2373
3059
|
PanelEdgeTab,
|
|
3060
|
+
useLeftDetailPanelLayout,
|
|
2374
3061
|
TrackPositionPlayPause,
|
|
2375
3062
|
TrackItem,
|
|
2376
3063
|
GenreTreeView,
|
|
3064
|
+
GenreDetailPanel,
|
|
2377
3065
|
TrackListSidebar,
|
|
2378
3066
|
PlayerPanel,
|
|
2379
3067
|
Rating,
|
|
@@ -2382,4 +3070,4 @@ export {
|
|
|
2382
3070
|
GenreTreeViewSkeleton2 as GenreTreeViewSkeleton,
|
|
2383
3071
|
GenreTreeOutlineSkeleton
|
|
2384
3072
|
};
|
|
2385
|
-
//# sourceMappingURL=chunk-
|
|
3073
|
+
//# sourceMappingURL=chunk-TFLX6C5Q.js.map
|