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