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