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