1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/HomePage-q4aW8A7v.js","assets/vendor-react-Bay9Y7Lb.js","assets/SectionPreviewModal-Dtgnulvj.js","assets/utils-BYCVoSTN.js","assets/index-D0Bmh-UX.js","assets/vendor-i18n-R-bMoYp_.js","assets/vendor-dVNaLJx_.js","assets/vendor-DqKRdbjz.css","assets/index-WEfYDNmt.css","assets/bilingual-B8ftIVs5.js","assets/SectionContent-TfBXVZBi.js","assets/TrackRow-DUmbuow9.js","assets/GlobeFlag-V1uaxVEK.js","assets/UserAvatar-BQN4jlY-.js","assets/AdSlot-CPW4Sb2o.js","assets/labTools-BATZiaPB.js","assets/vendor-react-three-MDLA5jog.js","assets/synth-BE1TSTD4.js","assets/wasm-synth-adapter-BrqgBX1y.js","assets/vendor-spessasynth-DH8n6o_V.js","assets/vendor-audio-DH3w0BzW.js","assets/Waveform-MxtdejK7.js","assets/vendor-framer-C8eEgkdL.js","assets/AdvancedSearchPage-tGa2266t.js","assets/AdvancedSearchPage-DWZp9cn1.css","assets/MelodySearchPage-C8DfrA33.js","assets/WatchPage-B8_07PT2.js","assets/SharedKeyboard-B3r3Jaho.js","assets/useMidiKeyboard-DlyEIN8T.js","assets/ExplorePage-BWHkev4d.js","assets/AccountPage-mZQDQlod.js","assets/PlaylistsPage-Br-5J847.js","assets/ProjectList-DNE9O-rf.js","assets/ConverterPage-kt7PUb_r.js","assets/credit-rules-8PgauDOD.js","assets/CreditsExhaustedModal-CRFvqXo7.js","assets/EditorPage-Dsed0DcB.js","assets/ScoreEditorCanvas-hnha0PBU.js","assets/MidiEngine-BqW6hqRc.js","assets/HistoryPage-BYHoeFoZ.js","assets/ListPage-gXg1XIZF.js","assets/LikedPage-SwAUDqZq.js","assets/WatchLaterPage-32xxryAT.js","assets/SubscriptionsPage-D5-uiNqs.js","assets/ChannelPage-DAFNmqL_.js","assets/TagPage-DI5S2d93.js","assets/HelpPage-DsiYZpvH.js","assets/MessengerPage-
1D0_m_QNf.js","assets/LyricsPage-BWsBdPvc.js","assets/LyricsSearchPage-CzRGUKfj.js","assets/AdminApp-D1oXn0it.js","assets/PersonalSpacePage-BQt1uXtk.js","assets/NewlyUploadedPage-CswaPEfP.js","assets/BillingPage-B2Hb0PMo.js","assets/CollectionIndexPage-B76qFKH2.js","assets/CollectionPage-DTEimYXF.js","assets/CollectionSectionPage-B6Zl3jqi.js","assets/EmailVerification-D-JaNSfM.js","assets/ResetPassword-CpmlaEYr.js","assets/ScoreConverterPage-BvnaneVe.js","assets/VocalProcessorPage-BWIy7sro.js","assets/AlgorithmStudio-BAKVJ64e.js","assets/AlgorithmEditor-BuTd-7oq.js","assets/ScoreMusicXmlViewer-2usqegaw.js","assets/StagingArena-C9ym5iYy.js","assets/TribunalPage-yYCBAk5S.js","assets/HamieLabPage-KXwalnzA.js","assets/MidiVisualizerPage-B4L5oo98.js","assets/DMCAForm-DoA_m2iR.js","assets/SheetMusicDigitizerPage-CGO_x0iu.js","assets/AudioDigitizerPage-DjRFGvnM.js","assets/ClientAudioDigitizerPage-wnww40sz.js","assets/vendor-basicpitch-Bd1IWE9_.js","assets/index--Kd7xwwe.js","assets/KeyboardEmulatorPage-3bHJ91O7.js","assets/SoundfontsPage-Kim5OuPr.js","assets/SoundfontDetailPage-R4ZkN9OB.js","assets/SoundfontUploadPage-C9Pu1znK.js"])))=>i.map(i=>d[i]); 2import { a as api, u as useSettings, L as LANGUAGE_REGIONS, N as NATIVE_LANGUAGE_NAMES, S as SettingsProvider, b as uxTracker } from "./index-D0Bmh-UX.js"; 3import { r as reactExports, j as jsxRuntimeExports, R as React, h as ReactDOM, f as createRoot } from "./vendor-react-Bay9Y7Lb.js"; 4import { _ as __vitePreload } from "./vendor-react-three-MDLA5jog.js"; 5import { i as isPro, g as getColorsFromHash, a as generateTrackUrl, t as timeAgo, b as getInstrumentName, c as generatePseudoWaveform, d as isContentBlocked, e as isSlowMobile, f as getBestAudioMode } from "./utils-BYCVoSTN.js"; 6import { s as synth, p as parseMidiInstruments } from "./synth-BE1TSTD4.js"; 7import { u as useTranslation } from "./vendor-i18n-R-bMoYp_.js"; 8import { aW as X, aX as ArrowLeft, aY as TriangleAlert, aZ as Mail, a_ as CircleCheck, a$ as Shield, b0 as FileText, b1 as CircleAlert, b2 as Upload, b3 as FileHeadphone, b4 as FolderOpen, b5 as Trash2, b6 as LoaderCircle, b7 as Check, b8 as CheckCheck, b9 as Settings, ba as Info, bb as User, bc as MessageSquare, bd as Heart, be as Menu, bf as Search, bg as Music, bh as Bell, bi as Sparkles, bj as CreditCard, bk as LogOut, bl as UserPlus, bm as ArrowUp, bn as Globe, bo as Monitor, bp as CirclePlay, bq as Coins, br as Moon, bs as Zap, bt as Tv, bu as Sun, bv as CircleCheckBig, bw as Bug, bx as Send, by as MessageCircle, bz as EyeOff, bA as Eye, bB as ListMusic, bC as FlaskConical, bD as Link, bE as ChevronRight, bF as CircleQuestionMark, bG as Box, bH as Hash, bI as Star, bJ as PanelsTopLeft, bK as ThumbsUp, bL as Clock, bM as History, bN as Library, bO as Compass, bP as House, bQ as ExternalLink, bR as CircleStop, bS as Mic, bT as Volume2, bU as Play, bV as RefreshCcw, bW as VolumeX, bX as ListPlus, bY as FolderPlus, bZ as Pause, b_ as EllipsisVertical, b$ as Repeat, c0 as Share2, c1 as Copy, c2 as Loader, c3 as RotateCcw, c4 as SkipBack, c5 as SkipForward, c6 as Shuffle, c7 as Timer, c8 as ArrowUpDown, c9 as Cast, ca as PinOff,
8cb as Pin, cc as Save, cd as Ellipsis, ce as Plus, cf as Activity, cg as Gauge, ch as Database, ci as Cpu, cj as Layers, ck as HardDrive, cl as OctagonAlert } from "./vendor-dVNaLJx_.js"; 9import { T as TagAutocomplete, u as useWaveform, W as Waveform } from "./Waveform-MxtdejK7.js"; 10import { H as HamieLogo, D as DEFAULT_TOOLS } from "./labTools-BATZiaPB.js"; 11import { U as UserAvatar } from "./UserAvatar-BQN4jlY-.js"; 12import "./wasm-synth-adapter-BrqgBX1y.js"; 13import "./vendor-spessasynth-DH8n6o_V.js"; 14import "./vendor-audio-DH3w0BzW.js"; 15import "./vendor-framer-C8eEgkdL.js"; 16const stats = { 17 navStart: performance.now(), 18 dbQueryTime: 0, 19 dbQueryCount: 0, 20 serverNode: "Unknown", 21 dbNode: "Unknown", 22 activeFetches: 0, 23 isInitialLoadComplete: false 24}; 25function updateMetaTag() { 26 let meta = document.querySelector('meta[name="x-srv"]'); 27 if (!meta) { 28 meta = document.createElement("meta"); 29 meta.name = "x-srv"; 30 document.head.appendChild(meta); 31 } 32 const pageLoadTime = performance.now() - stats.navStart; 33 const loadSec = (pageLoadTime / 1e3).toFixed(4); 34 const dbSec = (stats.dbQueryTime / 1e3).toFixed(4); 35 const srv = stats.serverNode || "Unknown"; 36 const db = stats.dbNode || "Unknown"; 37 const count = stats.dbQueryCount; 38 meta.content = `${srv}-${loadSec}-${db}-${dbSec}-${count}`; 39} 40function onNavigation() { 41 stats.navStart = performance.now(); 42 stats.dbQueryTime = 0; 43 stats.dbQueryCount = 0; 44 stats.activeFetches = 0; 45 stats.isInitialLoadComplete = false; 46} 47const originalPushState = history.pushState; 48history.pushState = function(...args) { 49 originalPushState.apply(this, args); 50 onNavigation(); 51}; 52const originalReplaceState = history.replaceState; 53history.replaceState = function(...args) { 54 originalReplaceState.apply(this, args); 55 onNavigation(); 56}; 57window.addEventListener("popstate", onNavigation); 58const originalFetch = window.fetch; 59window.fetch = async function(...args) { 60 var _a; 61 const url = typeof args[0] === "string" ? args[0] : ((_a = args[0]) == null ? void 0 : _a.url) || ""; 62 const isBackground = url.includes("/performance") || url.includes("/monitoring-status") || url.includes("/system/") || url.includes("/stats"); 63 if (!isBackground && !stats.isInitialLoadComplete) { 64 stats.activeFetches++; 65 if (window.__telemetryIdleTimer) { 66 clearTimeout(window.__telemetryIdleTimer); 67 } 68 } 69 try { 70 const response = await originalFetch.apply(this, args); 71 const qTime = parseFloat(response.headers.get("X-Query-Time") || "0"); 72 const qCount = parseInt(response.headers.get("X-Query-Count") || "0", 10); 73 const srvNode = response.headers.get("X-Server-Node"); 74 const dbNode = response.headers.get("X-DB-Node"); 75 if (srvNode) stats.serverNode = srvNode; 76 if (dbNode) stats.dbNode = dbNode; 77 if (!isBackground && !stats.isInitialLoadComplete) { 78 if (!isNaN(qTime)) stats.dbQueryTime += qTime; 79 if (!isNaN(qCount)) stats.dbQueryCount += qCount; 80 } 81 return response; 82 } finally { 83 if (!isBackground && !stats.isInitialLoadComplete) { 84 stats.activeFetches--; 85 if (window.__telemetryIdleTimer) { 86 clearTimeout(window.__telemetryIdleTimer); 87 } 88 if (stats.activeFetches === 0) { 89 window.__telemetryIdleTimer = setTimeout(() => { 90 if (stats.activeFetches === 0 && !stats.isInitialLoadComplete) { 91 stats.isInitialLoadComplete = true; 92 updateMetaTag(); 93 } 94 }, 500); 95 } 96 } 97 } 98}; 99const AdminContext = reactExports.createContext(void 0); 100const AdminProvider = ({ children, currentUser }) => { 101 const [staffModeEnabled, setStaffModeEnabled] = reactExports.useState(false); 102 const [isSuperAdmin, setIsSuperAdmin] = reactExports.useState((currentUser == null ? void 0 : currentUser.isSuperAdmin) || false); 103 const refreshStaffStatus = async () => { 104 if (!currentUser || currentUser.role !== "ADMIN") { 105 setStaffModeEnabled(false); 106 setIsSuperAdmin(false); 107 return; 108 } 109 try { 110 const status = await api.getStaffModeStatus(); 111 setStaffModeEnabled(status.enabled); 112 const finalIsSuper = !!(status.isSuperAdmin || (currentUser == null ? void 0 : currentUser.isSuperAdmin)); 113 setIsSuperAdmin(finalIsSuper); 114 } catch (e) { 115 console.error("[AdminContext] Failed to fetch staff status", e); 116 if ((currentUser == null ? void 0 : currentUser.role) === "ADMIN") { 117 setIsSuperAdmin(currentUser.isSuperAdmin || false); 118 } 119 } 120 }; 121 reactExports.useEffect(() => { 122 refreshStaffStatus(); 123 }, [currentUser == null ? void 0 : currentUser.id]); 124 const toggleStaffMode = async () => { 125 var _a, _b; 126 try { 127 const result = await api.toggleStaffMode(); 128 setStaffModeEnabled(result.enabled); 129 window.dispatchEvent(new CustomEvent("staff-mode-change", { detail: { enabled: result.enabled } })); 130 } catch (error) { 131 console.error("[AdminContext] Toggle failed:", error); 132 const msg = ((_b = (_a = error.response) == null ? void 0 : _a.data) == null ? void 0 : _b.message) || error.message || "Failed to toggle Staff Mode."; 133 alert(`${msg} Ensure you have Super-admin privileges.`); 134 } 135 };
136 return /* @__PURE__ */ jsxRuntimeExports.jsx(AdminContext.Provider, { value: { staffModeEnabled, isSuperAdmin, toggleStaffMode, refreshStaffStatus }, children }); 137}; 138const useAdmin = () => { 139 const context = reactExports.useContext(AdminContext); 140 if (context === void 0) { 141 throw new Error("useAdmin must be used within an AdminProvider"); 142 } 143 return context; 144}; 145const AuthModal = ({ isOpen, onClose, onAuthSuccess }) => { 146 const { t, i18n } = useTranslation(); 147 const [flowState, setFlowState] = reactExports.useState("initial"); 148 const [email, setEmail] = reactExports.useState(""); 149 const [lookupValue, setLookupValue] = reactExports.useState(""); 150 const [lookupResult, setLookupResult] = reactExports.useState(null); 151 const [newEmail, setNewEmail] = reactExports.useState(""); 152 const [isEditingEmail, setIsEditingEmail] = reactExports.useState(false); 153 const [password, setPassword] = reactExports.useState(""); 154 const [name, setName] = reactExports.useState(""); 155 const [loading, setLoading] = reactExports.useState(false); 156 const [error, setError] = reactExports.useState(null); 157 const [successMsg, setSuccessMsg] = reactExports.useState(null); 158 const [resendCooldown, setResendCooldown] = reactExports.useState(0); 159 const [capsLockOn, setCapsLockOn] = reactExports.useState(false); 160 const [selectedAvatar, setSelectedAvatar] = reactExports.useState(null); 161 const [defaultAvatars, setDefaultAvatars] = reactExports.useState([]); 162 const [enabledProviders, setEnabledProviders] = reactExports.useState({ 163 google: false, 164 facebook: false, 165 apple: false 166 }); 167 const emailInputRef = reactExports.useRef(null); 168 const nameInputRef = reactExports.useRef(null); 169 const passwordInputRef = reactExports.useRef(null); 170 reactExports.useEffect(() => { 171 api.getAuthProviders().then(setEnabledProviders); 172 }, []); 173 reactExports.useEffect(() => { 174 if (!isOpen) { 175 const timer = setTimeout(() => { 176 setFlowState("initial"); 177 setEmail(""); 178 setNewEmail(""); 179 setIsEditingEmail(false); 180 setPassword(""); 181 setName(""); 182 setError(null); 183 setSuccessMsg(null); 184 setCapsLockOn(false); 185 setSelectedAvatar(null); 186 }, 300); 187 return () => clearTimeout(timer); 188 } else { 189 if (flowState === "initial") { 190 setTimeout(() => { 191 var _a; 192 return (_a = emailInputRef.current) == null ? void 0 : _a.focus(); 193 }, 100); 194 } 195 } 196 }, [isOpen, flowState]); 197 reactExports.useEffect(() => { 198 let timer; 199 if (resendCooldown > 0) { 200 timer = setInterval(() => { 201 setResendCooldown((prev) => Math.max(0, prev - 1)); 202 }, 1e3); 203 } 204 return () => clearInterval(timer); 205 }, [resendCooldown]); 206 reactExports.useEffect(() => { 207 if (flowState === "login") { 208 setTimeout(() => { 209 var _a; 210 return (_a = passwordInputRef.current) == null ? void 0 : _a.focus(); 211 }, 100); 212 } else if (flowState === "verify-pending") { 213 setResendCooldown(60); 214 } else if (flowState === "register") { 215 setTimeout(() => { 216 var _a; 217 return (_a = nameInputRef.current) == null ? void 0 : _a.focus(); 218 }, 100); 219 api.getDefaultAvatars().then((avatars) => { 220 setDefaultAvatars(avatars); 221 if (avatars.length > 0 && !selectedAvatar) { 222 setSelectedAvatar(avatars[0]); 223 } 224 }); 225 } 226 }, [flowState]); 227 const checkCapsLock = (e) => { 228 if (e.getModifierState) { 229 setCapsLockOn(e.getModifierState("CapsLock")); 230 } 231 }; 232 if (!isOpen) return null; 233 const handleContinue = async (e) => { 234 e.preventDefault(); 235 setLoading(true); 236 setError(null); 237 try { 238 const inputStr = email.trim(); 239 const exists = await api.checkUserExists(inputStr); 240 setTimeout(() => { 241 if (exists) { 242 setFlowState("login"); 243 } else { 244 if (!inputStr.includes("@")) { 245 setError(t("auth.usernameNotFound", "Username not found. Please use an email address to create a new account.")); 246 setFlowState("initial"); 247 } else { 248 setFlowState("register"); 249 } 250 }
251 setLoading(false); 252 }, 600); 253 } catch (e2) { 254 setLoading(false); 255 setFlowState("initial"); 256 setError(t("common.error", "An error occurred")); 257 } 258 }; 259 const validatePassword = (pass) => { 260 if (pass.length < 8) return t("auth.errorPasswordShort", "Password must be at least 8 characters long."); 261 if (!/[a-zA-Z]/.test(pass) || !/[0-9]/.test(pass)) { 262 return t("auth.errorPasswordSimple", "Password must contain at least one letter and one number."); 263 } 264 return null; 265 }; 266 const handleSubmit = async (e) => { 267 e.preventDefault(); 268 if (flowState === "register") { 269 const passError = validatePassword(password); 270 if (passError) { 271 setError(passError); 272 return; 273 } 274 } 275 setLoading(true); 276 setError(null); 277 try { 278 let authedUser = null; 279 const cleanEmail = email.trim(); 280 if (flowState === "login") { 281 authedUser = await api.login(cleanEmail, password); 282 } else { 283 authedUser = await api.register(name, cleanEmail, password, selectedAvatar || void 0, i18n.language); 284 } 285 if (authedUser) { 286 if (authedUser.needsEmailConfirmation) { 287 setNewEmail(authedUser.email); 288 setFlowState("confirm-email"); 289 setLoading(false); 290 return; 291 } 292 if (authedUser.pendingVerification) { 293 setFlowState("verify-pending"); 294 setLoading(false); 295 return; 296 } 297 onAuthSuccess(authedUser); 298 localStorage.setItem("hamie_session", JSON.stringify(authedUser)); 299 setLoading(false); 300 onClose(); 301 } 302 } catch (err) { 303 if (err.message === "Email verification required" || err.unverified) { 304 setFlowState("verify-pending"); 305 } else { 306 setError(err.message || t("common.error", "An error occurred")); 307 } 308 setLoading(false); 309 } 310 }; 311 const handleResendVerification = async () => { 312 if (resendCooldown > 0) return; 313 setLoading(true); 314 setError(null); 315 try { 316 await api.resendVerification(email.trim(), i18n.language); 317 setSuccessMsg("Verification link resent! Please check your inbox."); 318 setResendCooldown(60); 319 } catch (err) { 320 setError(err.message || "Failed to resend link."); 321 } finally { 322 setLoading(false); 323 } 324 }; 325 const handleSocialLogin = async (provider) => { 326 setLoading(true); 327 setError(null); 328 try { 329 const user = await api.socialLogin(provider); 330 onAuthSuccess(user); 331 localStorage.setItem("hamie_session", JSON.stringify(user)); 332 onClose(); 333 } catch (e) { 334 setError(e.message || "Social login failed."); 335 } finally { 336 setLoading(false); 337 } 338 }; 339 const handleForgotPassword = async (e) => { 340 e.preventDefault(); 341 setLoading(true); 342 setError(null); 343 try { 344 const result = await api.forgotPassword(email.trim(), i18n.language); 345 if (result.success) { 346 setSuccessMsg(result.message); 347 } else { 348 setError(result.message || "Failed to send reset link."); 349 } 350 } catch (err) { 351 setError(err.message || "Failed to send reset link."); 352 } finally { 353 setLoading(false); 354 } 355 }; 356 const handleEmailUpdate = async (e) => { 357 e.preventDefault(); 358 setLoading(true); 359 setError(null); 360 try { 361 const result = await api.updateEmail(newEmail.trim()); 362 if (result.success) { 363 setEmail(newEmail.trim()); 364 setFlowState("verify-pending"); 365 } 366 } catch (err) { 367 setError(err.message || "Failed to update email."); 368 } finally { 369 setLoading(false); 370 } 371 }; 372 const handleBack = () => { 373 setFlowState("initial"); 374 setPassword(""); 375 setName(""); 376 setError(null); 377 setSuccessMsg(null); 378 setCapsLockOn(false); 379 setSelectedAvatar(null); 380 }; 381 const GoogleIcon = () => /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { role: "img", viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", className: "w-5 h-5", children: [ 382 /* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: "Google" }), 383 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12.48 10.92v3.28h7.84c-.24 1.84-.85 3.18-1.73 4.1-1.02 1.02-2.3 1.84-4.32 1.84-5.18 0-9.4-4.22-9.4-9.4s4.22-9.4 9.4-9.4c2.86 0 5.21 1.09 6.84 2.63l2.5-2.5C20.06 2.5 16.73 1 12.48 1 5.83 1 1 5.83 1 12.48s4.83 11.48 11.48 11.48c6.48 0 11.23-4.49 11.23-11.58 0-.79-.07-1.55-.19-2.28z", fill: "currentColor" }) 384 ] }); 385 const AppleIcon = () => /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { role: "img", viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", className: "w-5 h-5", children: [
386 /* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: "Apple" }), 387 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12.15,2.44A5.32,5.32,0,0,0,8.73,4.62a5.45,5.45,0,0,0-2.3,4.24,5.83,5.83,0,0,0,3.52,5.55,4.7,4.7,0,0,0,2.68.85,1.24,1.24,0,0,1,.87,0,4.7,4.7,0,0,0,2.69-.85,5.85,5.85,0,0,0,3.52-5.55,5.4,5.4,0,0,0-2.3-4.24A5.25,5.25,0,0,0,12.15,2.44Zm-1.2,16.5A4.5,4.5,0,0,0,12,18.31a4.34,4.34,0,0,0,1.05.63,2.42,2.42,0,0,1-2.1,0Z", fill: "currentColor" }), 388 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12.15,0A10.83,10.83,0,0,0,5.2,3.71,11.23,11.23,0,0,0,1.21,12a10.45,10.45,0,0,0,4.64,8.87,10,10,0,0,0,6.3,2.83,10.15,10.15,0,0,0,10.63-10.4,10.47,10.47,0,0,0-2.73-7.14A10.81,10.81,0,0,0,12.15,0Z", fill: "currentColor" }) 389 ] }); 390 const FacebookIcon = () => /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { role: "img", viewBox: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", className: "w-5 h-5", children: [ 391 /* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: "Facebook" }), 392 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z", fill: "currentColor" }) 393 ] }); 394 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[1000] flex items-center justify-center p-4", children: [ 395 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/80 backdrop-blur-sm", onClick: onClose }), 396 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-sm bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-8 shadow-2xl animate-in zoom-in-95 text-zinc-900 dark:text-white max-h-[90vh] flex flex-col", children: [
397 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "absolute top-4 right-4 p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-colors text-zinc-500 hover:text-black dark:hover:text-white", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) }), 398 flowState !== "initial" && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleBack, className: "absolute top-4 left-4 p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-colors text-zinc-500 hover:text-black dark:hover:text-white", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 20 }) }), 399 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center mb-8 shrink-0", children: [ 400 /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-2xl font-bold mb-1", children: [ 401 flowState === "initial" && t("auth.signInOrCreate", "Sign in or create account"), 402 flowState === "login" && t("auth.welcome", "Welcome back"), 403 flowState === "register" && t("auth.createAccount", "Create your account"), 404 flowState === "forgot-password" && t("auth.resetPassword", "Reset Password"), 405 flowState === "tos" && t("auth.termsOfService", "Terms of Service"), 406 flowState === "privacy" && t("auth.privacyPolicy", "Privacy Policy"), 407 flowState === "confirm-email" && t("auth.confirmEmail", "Confirm Your Email") 408 ] }), 409 (flowState === "login" || flowState === "register" || flowState === "confirm-email") && /* @__P
409URE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400 break-all", children: email }) 410 ] }), 411 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto custom-scrollbar -mx-4 px-4 pt-1", children: [ 412 flowState === "initial" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [ 413 enabledProviders.google && /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: () => handleSocialLogin("google"), disabled: loading, className: "w-full flex items-center justify-center gap-3 bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-700/80 border border-zinc-200 dark:border-zinc-700 py-3 rounded-full font-medium text-sm transition-all text-zinc-900 dark:text-white disabled:opacity-50", children: [ 414 /* @__PURE__ */ jsxRuntimeExports.jsx(GoogleIcon, {}), 415 " ", 416 t("auth.continueGoogle", "Continue with Google") 417 ] }), 418 enabledProviders.facebook && /* @__P
418URE__ */ jsxRuntimeExports.jsxs("button", { onClick: () => handleSocialLogin("facebook"), disabled: loading, className: "w-full flex items-center justify-center gap-3 bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-700/80 border border-zinc-200 dark:border-zinc-700 py-3 rounded-full font-medium text-sm transition-all text-zinc-900 dark:text-white disabled:opacity-50", children: [ 419 /* @__PURE__ */ jsxRuntimeExports.jsx(FacebookIcon, {}), 420 " ", 421 t("auth.continueFacebook", "Continue with Facebook") 422 ] }), 423 enabledProviders.apple && /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: () => handleSocialLogin("apple"), disabled: loading, className: "w-full flex items-center justify-center gap-3 bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-700/80 border border-zinc-200 dark:border-zinc-700 py-3 rounded-full font-medium text-sm transition-all text-zinc-900 dark:text-white disabled:opacity-50", children: [ 424 /* @__PURE__ */ jsxRuntimeExports.jsx(AppleIcon, {}), 425 " ", 426 t("auth.continueApple", "Continue with Apple") 427 ] }), 428 (enabledProviders.google || enabledProviders.facebook || enabledProviders.apple) && /* @__P
428URE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center text-xs text-zinc-500 before:h-px before:flex-1 before:bg-zinc-200 dark:before:bg-zinc-700 before:mr-4 after:h-px after:flex-1 after:bg-zinc-200 dark:after:bg-zinc-700 after:ml-4", children: t("auth.or", "or") }), 429 /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleContinue, children: [ 430 error && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-red-500 dark:text-red-400 text-xs text-center bg-red-100 dark:bg-red-900/20 border border-red-200 dark:border-red-500/20 rounded-md p-2 mb-3", children: error }), 431 /* @__PURE__ */ jsxRuntimeExports.jsx( 432 "input", 433 { 434 ref: emailInputRef, 435 type: "text", 436 required: true, 437 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded-lg px-4 py-3 outline-none transition-all text-sm focus:ring-1 focus:ring-indigo-600 mb-3 text-zinc-900 dark:text-white", 438 placeholder: t("auth.emailOnly", "Email Address"), 439 value: email, 440 onChange: (e) => setEmail(e.target.value), 441 autoComplete: "username" 442 } 443 ),
444 /* @__PURE__ */ jsxRuntimeExports.jsx( 445 "button", 446 { 447 type: "submit", 448 disabled: loading, 449 className: "w-full bg-indigo-600 hover:bg-indigo-700 py-3 rounded-full font-bold text-sm transition-all disabled:opacity-50 flex items-center justify-center gap-2 text-white", 450 children: loading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" }) : t("auth.continue", "Continue") 451 } 452 ), 453 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-center mt-3", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 454 "button", 455 { 456 type: "button", 457 onClick: () => setFlowState("trouble"), 458 className: "text-xs text-indigo-600 dark:text-indigo-400 hover:underline font-medium", 459 children: t("auth.havingTrouble", "Having trouble logging in?") 460 } 461 ) }) 462 ] }),
463 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-zinc-500 text-center px-4 pt-2", children: [ 464 t("auth.termsPrefix", "By continuing, you agree to the"), 465 " ", 466 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setFlowState("tos"), className: "underline hover:text-zinc-800 dark:hover:text-zinc-300", children: t("auth.termsOfService", "Terms of Service") }), 467 " ", 468 t("auth.and", "and"), 469 " ", 470 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setFlowState("privacy"), className: "underline hover:text-zinc-800 dark:hover:text-zinc-300", children: t("auth.privacyPolicy", "Privacy Policy") }), 471 "." 472 ] }) 473 ] }), 474 (flowState === "login" || flowState === "register") && /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [ 475 error && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-red-500 dark:text-red-400 text-xs text-center bg-red-100 dark:bg-red-900/20 border border-red-200 dark:border-red-500/20 rounded-md p-2", children: error }), 476 flowState === "register" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
477 /* @__PURE__ */ jsxRuntimeExports.jsx( 478 "input", 479 { 480 ref: nameInputRef, 481 type: "text", 482 required: true, 483 maxLength: 50, 484 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded-lg px-4 py-3 outline-none transition-all text-sm focus:ring-1 focus:ring-indigo-600 text-zinc-900 dark:text-white", 485 placeholder: t("auth.fullName", "Full Name"), 486 value: name, 487 onChange: (e) => setName(e.target.value), 488 autoComplete: "name" 489 } 490 ), 491 defaultAvatars.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 492 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase mb-2", children: t("auth.chooseAvatar", "Choose Avatar") }), 493 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-5 gap-2 max-h-[120px] overflow-y-auto custom-scrollbar p-1", children: defaultAvatars.map((filename) => /* @__PURE__ */ jsxRuntimeExports.jsx( 494 "button", 495 { 496 type: "button", 497 onClick: () => setSelectedAvatar(filename), 498 className: `w-full aspect-square rounded-lg overflow-hidden border-2 transition-all hover:scale-105 ${selectedAvatar === filename ? "border-indigo-500 ring-2 ring-indigo-500/30 scale-105" : "border-zinc-200 dark:border-zinc-700 opacity-70 hover:opacity-100"}`, 499 children: /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: `/storage/avatars/defaults/${filename}`, alt: filename, className: "w-full h-full object-cover" }) 500 }, 501 filename 502 )) }) 503 ] }) 504 ] }),
505 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 506 /* @__PURE__ */ jsxRuntimeExports.jsx( 507 "input", 508 { 509 ref: passwordInputRef, 510 type: "password", 511 required: true, 512 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded-lg px-4 py-3 outline-none transition-all text-sm focus:ring-1 focus:ring-indigo-600 text-zinc-900 dark:text-white", 513 placeholder: t("auth.password", "Password"), 514 value: password, 515 onChange: (e) => setPassword(e.target.value), 516 onKeyUp: checkCapsLock, 517 onKeyDown: checkCapsLock, 518 onClick: checkCapsLock, 519 autoComplete: "off" 520 } 521 ), 522 capsLockOn && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute right-3 top-1/2 -translate-y-1/2 text-amber-500 flex items-center gap-1 text-xs font-bold animate-pulse", children: [
523 /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 14 }), 524 " ", 525 t("auth.capsLockOn", "CAPS LOCK IS ON") 526 ] }) 527 ] }), 528 /* @__PURE__ */ jsxRuntimeExports.jsx( 529 "button", 530 { 531 type: "submit", 532 disabled: loading, 533 className: "w-full bg-indigo-600 hover:bg-indigo-700 py-3 rounded-full font-bold text-sm transition-all disabled:opacity-50 flex items-center justify-center gap-2 mt-4 text-white", 534 children: loading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" }) : flowState === "login" ? t("auth.signInButton", "Sign In") : t("auth.createAccountButton", "Create Account") 535 } 536 ), 537 flowState === "login" && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: () => setFlowState("forgot-password"), className: "block w-full text-center text-xs text-indigo-500 dark:text-indigo-400 hover:underline pt-2", children: t("auth.forgotPassword", "Forgot your password?") }) 538 ] }), 539 flowState === "forgot-password" && /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleForgotPassword, className: "space-y-6", children: [
540 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center", children: [ 541 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-16 bg-indigo-50 dark:bg-indigo-900/20 rounded-full flex items-center justify-center mx-auto mb-4 text-indigo-600 dark:text-indigo-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Mail, { size: 32 }) }), 542 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-600 dark:text-zinc-400 mb-6", children: t("auth.resetPasswordDesc", "Enter your email address and we'll send you a link to reset your password.") }) 543 ] }), 544 successMsg ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 p-4 rounded-lg flex gap-3", children: [ 545 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { className: "text-emerald-500 shrink-0", size: 20 }), 546 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-emerald-800 dark:text-emerald-200", children: successMsg }) 547 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
548 /* @__PURE__ */ jsxRuntimeExports.jsx( 549 "input", 550 { 551 type: "email", 552 required: true, 553 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded-lg px-4 py-3 outline-none transition-all text-sm focus:ring-1 focus:ring-indigo-600 text-zinc-900 dark:text-white", 554 placeholder: t("auth.emailPlaceholder", "Email Address"), 555 value: email, 556 onChange: (e) => setEmail(e.target.value) 557 } 558 ), 559 /* @__PURE__ */ jsxRuntimeExports.jsx( 560 "button", 561 { 562 type: "submit", 563 disabled: loading || !email, 564 className: "w-full bg-indigo-600 hover:bg-indigo-700 py-3 rounded-full font-bold text-sm transition-all disabled:opacity-50 flex items-center justify-center gap-2 text-white", 565 children: loading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" }) : t("auth.sendResetLink", "Send Reset Link") 566 } 567 ) 568 ] }) 569 ] }), 570 flowState === "confirm-email" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
571 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center", children: [ 572 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-16 bg-indigo-50 dark:bg-indigo-900/20 rounded-full flex items-center justify-center mx-auto mb-4 text-indigo-600 dark:text-indigo-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Mail, { size: 32 }) }), 573 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-600 dark:text-zinc-400 mb-4", children: t("auth.migrationNotice", "To keep your account secure, please confirm your email address. Once verified, you can log in with either your email or username.") }) 574 ] }), 575 error && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-red-500 dark:text-red-400 text-xs text-center bg-red-100 dark:bg-red-900/20 border border-red-200 dark:border-red-500/20 rounded-md p-2 mb-3", children: error }), 576 !isEditingEmail ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
577 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 rounded-lg p-4", children: [ 578 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 uppercase font-bold mb-1", children: t("auth.currentEmail", "Current Email") }), 579 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm font-medium", children: email }) 580 ] }), 581 /* @__PURE__ */ jsxRuntimeExports.jsx( 582 "button", 583 { 584 onClick: () => setIsEditingEmail(true), 585 className: "w-full text-xs text-indigo-500 hover:underline text-center", 586 children: t("auth.changeEmail", "Use a different email address") 587 } 588 ), 589 /* @__PURE__ */ jsxRuntimeExports.jsx( 590 "button", 591 { 592 onClick: handleEmailUpdate, 593 disabled: loading, 594 className: "w-full bg-indigo-600 hover:bg-indigo-700 py-3 rounded-full font-bold text-sm transition-all text-white disabled:opacity-50",
595 children: loading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mx-auto" }) : t("auth.confirmAndSend", "Confirm & Send Verification") 596 } 597 ) 598 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleEmailUpdate, className: "space-y-4", children: [ 599 /* @__PURE__ */ jsxRuntimeExports.jsx( 600 "input", 601 { 602 type: "email", 603 required: true, 604 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 rounded-lg px-4 py-3 outline-none transition-all text-sm focus:ring-1 focus:ring-indigo-600 text-zinc-900 dark:text-white", 605 placeholder: t("auth.newEmailPlaceholder", "Enter new email address"), 606 value: newEmail, 607 onChange: (e) => setNewEmail(e.target.value) 608 } 609 ), 610 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
611 /* @__PURE__ */ jsxRuntimeExports.jsx( 612 "button", 613 { 614 type: "button", 615 onClick: () => { 616 setIsEditingEmail(false); 617 setNewEmail(email); 618 }, 619 className: "flex-1 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 py-3 rounded-full font-bold text-sm transition-all", 620 children: t("common.cancel", "Cancel") 621 } 622 ), 623 /* @__PURE__ */ jsxRuntimeExports.jsx( 624 "button", 625 { 626 type: "submit", 627 disabled: loading || !newEmail || newEmail === email, 628 className: "flex-[2] bg-indigo-600 hover:bg-indigo-700 py-3 rounded-full font-bold text-sm transition-all text-white disabled:opacity-50", 629 children: loading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mx-auto" }) : t("auth.updateAndSend", "Update & Send") 630 } 631 ) 632 ] }) 633 ] }) 634 ] }), 635 flowState === "verify-pending" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6 text-center animate-in fade-in slide-in-from-bottom-4", children: [
636 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-16 bg-amber-50 dark:bg-amber-900/20 rounded-full flex items-center justify-center mx-auto mb-4 text-amber-600 dark:text-amber-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Mail, { size: 32 }) }), 637 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 638 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xl font-bold mb-2", children: "Verify your email" }), 639 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-sm text-zinc-600 dark:text-zinc-400 mb-6", children: [ 640 "We've sent a verification link to ", 641 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-900 dark:text-white", children: email }), 642 ". Please click the link in the email to activate your account." 643 ] }) 644 ] }), 645 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [ 646 successMsg && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 p-4 rounded-lg flex gap-3 text-left mb-4", children: [
647 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { className: "text-emerald-500 shrink-0", size: 20 }), 648 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-emerald-800 dark:text-emerald-200", children: successMsg }) 649 ] }), 650 /* @__PURE__ */ jsxRuntimeExports.jsx( 651 "button", 652 { 653 onClick: handleResendVerification, 654 disabled: loading || resendCooldown > 0, 655 className: "w-full bg-indigo-600 hover:bg-indigo-700 py-3 rounded-full font-bold text-sm transition-all disabled:opacity-50 flex items-center justify-center gap-2 text-white shadow-lg shadow-indigo-500/20", 656 children: loading ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" }) : resendCooldown > 0 ? `Resend in ${resendCooldown}s` : "Resend Verification Link" 657 } 658 ), 659 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500", children: "Can't find it? Check your spam folder or try resending." }) 660 ] }),
661 /* @__PURE__ */ jsxRuntimeExports.jsx( 662 "button", 663 { 664 onClick: () => setFlowState("initial"), 665 className: "text-sm font-bold text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200 transition-colors pt-4", 666 children: "Back to Sign In" 667 } 668 ) 669 ] }), 670 (flowState === "tos" || flowState === "privacy") && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-zinc-600 dark:text-zinc-400 space-y-4", children: flowState === "tos" ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 671 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-indigo-600 dark:text-indigo-400 font-bold mb-2", children: [ 672 /* @__PURE__ */ jsxRuntimeExports.jsx(Shield, { size: 18 }), 673 " ", 674 t("auth.tosTitle", "HamieNET Terms of Service") 675 ] }), 676 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: t("auth.tosIntro", "Welcome to HamieNET. By using our website, you agree to these Terms. Please read them carefully.") }),
677 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-900 dark:text-white mt-4", children: t("auth.tosSection1", "1. Use of Service") }), 678 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: t("auth.tosSection1Desc", "You must follow any policies made available to you within the Service. You may not misuse our Service.") }), 679 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-900 dark:text-white mt-4", children: t("auth.tosSection2", "2. Your Content") }), 680 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: t("auth.tosSection2Desc", "Some of our Services allow you to upload, submit, store, send or receive content. You retain ownership of any intellectual property rights that you hold in that content.") }), 681 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-900 dark:text-white mt-4", children: t("auth.tosSection3", "3. Copyright") }), 682 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: t("auth.tosSection3Desc", "We respond to notices of alleged copyright infringement and terminate accounts of repeat infringers according to the process set out in the U.S. Digital Millennium Copyright Act.") }) 683 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
684 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-indigo-600 dark:text-indigo-400 font-bold mb-2", children: [ 685 /* @__PURE__ */ jsxRuntimeExports.jsx(FileText, { size: 18 }), 686 " ", 687 t("auth.privacyPolicy", "Privacy Policy") 688 ] }), 689 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: t("auth.privacyIntro", "Your privacy is important to us. This policy explains how we collect, use, and protect your information.") }), 690 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-900 dark:text-white mt-4", children: t("auth.privacySection1", "1. Information We Collect") }), 691 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: t("auth.privacySection1Desc", "We collect information to provide better services to all our users. This includes basic stuff like which language you speak, to more complex things like which ads youâll find most useful.") }), 692 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-zinc-900 dark:text-white mt-4", children: t("auth.privacySection2", "2. How We Use Information") }),
693 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: t("auth.privacySection2Desc", "We use the information we collect from all our services to provide, maintain, protect and improve them, to develop new ones, and to protect HamieNET and our users.") }) 694 ] }) }), 695 flowState === "trouble" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [ 696 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 697 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setFlowState("initial"), className: "p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 18, className: "text-zinc-500" }) }), 698 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-bold text-zinc-900 dark:text-white", children: t("auth.havingTroubleTitle", "Having trouble?") }) 699 ] }), 700 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-amber-50 dark:bg-amber-900/20 border border-amber-100 dark:border-amber-800 rounded-xl p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-amber-800 dark:text-amber-200 leading-relaxed", children: [
701 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "Legacy User?" }), 702 " If you had an account on the old HamieNET, we've transitioned to a secure email-login system. You can still use your old username to find your account." 703 ] }) }), 704 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 705 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 rounded-xl", children: [ 706 /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "text-sm font-bold text-zinc-900 dark:text-white mb-2 flex items-center gap-2", children: [ 707 /* @__PURE__ */ jsxRuntimeExports.jsx(Mail, { size: 16, className: "text-indigo-500" }), 708 t("auth.forgotPassword", "Forgot Password") 709 ] }), 710 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 mb-3", children: "If you know your registered email, we can send you a reset link." }),
711 /* @__PURE__ */ jsxRuntimeExports.jsx( 712 "button", 713 { 714 onClick: () => setFlowState("forgot-password"), 715 className: "w-full py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-bold transition-all", 716 children: t("auth.resetByEmail", "Reset by Email") 717 } 718 ) 719 ] }), 720 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 rounded-xl", children: [ 721 /* @__PURE__ */ jsxRuntimeExports.jsxs("h4", { className: "text-sm font-bold text-zinc-900 dark:text-white mb-2 flex items-center gap-2", children: [ 722 /* @__PURE__ */ jsxRuntimeExports.jsx(Shield, { size: 16, className: "text-emerald-500" }), 723 t("auth.lookupHandle", "Account Lookup") 724 ] }), 725 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 mb-3", children: "Don't remember your email? Find your account by your old username." }), 726 !lookupResult ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [
727 /* @__PURE__ */ jsxRuntimeExports.jsx( 728 "input", 729 { 730 type: "text", 731 placeholder: t("auth.oldUsername", "Old Username"), 732 className: "w-full bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-lg px-3 py-2 text-xs outline-none focus:ring-1 focus:ring-indigo-600 text-zinc-900 dark:text-white", 733 value: lookupValue, 734 onChange: (e) => setLookupValue(e.target.value) 735 } 736 ), 737 /* @__PURE__ */ jsxRuntimeExports.jsx( 738 "button", 739 { 740 onClick: async () => { 741 setLoading(true); 742 setError(null); 743 try { 744 const res = await api.lookupHandle(lookupValue); 745 if (res.error) setError(res.error); 746 else setLookupResult(res); 747 } catch (e) { 748 setError(e.message); 749 } finally { 750 setLoading(false); 751 } 752 }, 753 disabled: loading || !lookupValue, 754 className: "w-full py-2 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-lg text-xs font-bold transition-all disabled:opacity-50", 755 children: loading ? "..." : t("auth.findAccount", "Find Account") 756 } 757 ) 758 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-100 dark:border-emerald-800 rounded-lg p-3 text-center", children: [
759 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { size: 24, className: "text-emerald-500 mx-auto mb-2" }), 760 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-emerald-900 dark:text-emerald-100 font-bold mb-1", children: "Account Found!" }), 761 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] text-emerald-700 dark:text-emerald-300", children: [ 762 "Your new handle is ", 763 /* @__PURE__ */ jsxRuntimeExports.jsxs("strong", { children: [ 764 "@", 765 lookupResult.handle 766 ] }), 767 ".", 768 /* @__PURE__ */ jsxRuntimeExports.jsx("br", {}), 769 "Registered email: ", 770 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: lookupResult.email }) 771 ] }), 772 /* @__PURE__ */ jsxRuntimeExports.jsx( 773 "button", 774 { 775 onClick: () => { 776 setFlowState("forgot-password"); 777 }, 778 className: "mt-3 text-[10px] text-emerald-600 dark:text-emerald-400 underline font-bold", 779 children: "Go to password reset" 780 } 781 ) 782 ] }), 783 error && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-red-500 mt-2", children: error }) 784 ] }) 785 ] }),
786 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 787 "button", 788 { 789 onClick: () => setFlowState("tos"), 790 className: "text-[10px] text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 underline", 791 children: "Still need help? Contact support" 792 } 793 ) }) 794 ] }) 795 ] }) 796 ] }) 797 ] }); 798}; 799function UploadModal({ isOpen, onClose, currentUser, initialFile, initialMetadata }) { 800 var _a, _b, _c; 801 const { t } = useTranslation(); 802 const [step, setStep] = reactExports.useState(1); 803 const [file, setFile] = reactExports.useState(null); 804 const [metadata, setMetadata] = reactExports.useState({ title: "", artist: "", description: "", tags: "", genre: "" }); 805 const [artistInput, setArtistInput] = reactExports.useState(""); 806 const [tagsInput, setTagsInput] = reactExports.useState(""); 807 const [batchFiles, setBatchFiles] = reactExports.useState([]); 808 const [globalSettings, setGlobalSettings] = reactExports.useState({ artist: "", genre: "", tags: "" }); 809 const [availableGenres, setAvailableGenres] = reactExports.useState([]); 810 const [uploading, setUploading] = reactExports.useState(false); 811 const [validationErrors, setValidationErrors] = reactExports.useState({}); 812 const [isValidating, setIsValidating] = reactExports.useState(false); 813 const [filterConfig, setFilterConfig] = reactExports.useState(null); 814 const fileInputRef = reactExports.useRef(null); 815 const folderInputRef = reactExports.useRef(null); 816 const validationTimeoutRef = reactExports.useRef(null); 817 const lastAutofilledFor = reactExports.useRef({ artist: "", title: "" }); 818 const [fairUseAgreed, setFairUseAgreed] = reactExports.useState(false); 819 const [isDragging, setIsDragging] = reactExports.useState(false); 820 const [duplicateConflict, setDuplicateConflict] = reactExports.useState(null); 821 const [suggestedArtists, setSuggestedArtists] = reactExports.useState([]); 822 const [canonicalWorks, setCanonicalWorks] = reactExports.useState([]); 823 const [selectedPathParts, setSelectedPathParts] = reactExports.useState([]); 824 const [selectedWorkId, setSelectedWorkId] = reactExports.useState(""); 825 const handleDragOver = (e) => { 826 e.preventDefault(); 827 setIsDragging(true); 828 }; 829 const handleDragLeave = (e) => { 830 e.preventDefault(); 831 setIsDragging(false); 832 }; 833 const processSelectedFiles = (filesList) => { 834 const validFiles = Array.from(filesList).filter((f) => { 835 var _a2; 836 const ext = (_a2 = f.name.split(".").pop()) == null ? void 0 : _a2.toLowerCase(); 837 return ext === "mid" || ext === "midi" || ext === "kar"; 838 }); 839 if (validFiles.length === 0) { 840 alert(t("upload.invalidType", "Please select a valid MIDI file (.mid, .midi, .kar)")); 841 return; 842 } 843 if (validFiles.length > 10) { 844 alert(t("upload.maxFilesLimit", "Max 10 files allowed")); 845 validFiles.length = 10; 846 } 847 if (validFiles.length === 1) { 848 const f = validFiles[0]; 849 setFile(f); 850 setBatchFiles([]); 851 if (f.name) { 852 setMetadata((prev) => ({ 853 ...prev, 854 title: prev.title || f.name.replace(".mid", "").replace(".midi", "").replace(".kar", "").replace(/_/g, " ") 855 })); 856 } 857 } else { 858 setFile(null); 859 const batchData = validFiles.map((f) => ({ 860 id: Math.random().toString(36).substring(2, 9), 861 file: f, 862 title: f.name.replace(".mid", "").replace(".midi", "").replace(".kar", "").replace(/_/g, " "), 863 artist: "", 864 genre: "", 865 description: "", 866 tags: "", 867 status: "pending" 868 })); 869 setBatchFiles(batchData); 870 } 871 setStep(2); 872 }; 873 const handleDrop = (e) => { 874 e.preventDefault(); 875 setIsDragging(false); 876 if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { 877 processSelectedFiles(e.dataTransfer.files); 878 } 879 }; 880 reactExports.useEffect(() => { 881 if (isOpen) { 882 api.getTags({ type: "GENRE" }).then((tags) => { 883 const sorted = [...tags].sort((a, b) => a.name.localeCompare(b.name)); 884 setAvailableGenres(sorted); 885 }); 886 api.getSystemSetting("content_filter_config").then((setting) => { 887 if (setting) setFilterConfig(setting.value); 888 }); 889 if (initialFile) { 890 processSelectedFiles([initialFile]); 891 setArtistInput(""); 892 setTagsInput(""); 893 } else { 894 setStep(1); 895 setFile(null); 896 setBatchFiles([]); 897 setGlobalSettings({ artist: "", genre: "", tags: "" }); 898 if (initialMetadata) { 899 setMetadata({ 900 title: initialMetadata.title || "", 901 artist: initialMetadata.artist || "", 902 description: "", 903 tags: initialMetadata.tags || "", 904 genre: "" 905 }); 906 setArtistInput(initialMetadata.artist || ""); 907 setTagsInput(initialMetadata.tags || ""); 908 if (initialMetadata.canonicalWorkId) { 909 setSelectedWorkId(initialMetadata.canonicalWorkId); 910 } else { 911 setSelectedWorkId(""); 912 } 913 } else { 914 setMetadata({ title: "", artist: "", description: "", tags: "", genre: "" }); 915 setArtistInput(""); 916 setTagsInput(""); 917 setSelectedWorkId(""); 918 } 919 } 920 setDuplicateConflict(null); 921 setSuggestedArtists([]); 922 } 923 }, [isOpen, initialFile, initialMetadata]); 924 reactExports.useEffect(() => { 925 if (step === 2 && file && !batchFiles.length) { 926 if (validationTimeoutRef.current) clearTimeout(validationTimeoutRef.current); 927 setIsValidating(true); 928 validationTimeoutRef.current = setTimeout(async () => { 929 try { 930 const res = await api.validateContent({ 931 title: metadata.title, 932 artist: metadata.artist, 933 description: metadata.description 934 }); 935 const errors = {};
936 Object.entries(res.results).forEach(([key, val]) => { 937 if (!val.valid) errors[key] = val.error || "Invalid content"; 938 }); 939 setValidationErrors(errors); 940 } catch (e) { 941 console.error("Validation failed", e); 942 } finally { 943 setIsValidating(false); 944 } 945 }, 500); 946 } 947 return () => { 948 if (validationTimeoutRef.current) clearTimeout(validationTimeoutRef.current); 949 }; 950 }, [metadata.title, metadata.artist, metadata.description, step, file, batchFiles.length]); 951 reactExports.useEffect(() => { 952 if (step !== 2 || !file || batchFiles.length) return; 953 const timeout = setTimeout(async () => { 954 const currentArtist = (metadata.artist || "").trim(); 955 const currentTitle = (metadata.title || "").trim(); 956 if (!currentArtist && !currentTitle) return; 957 if (lastAutofilledFor.current.artist === currentArtist && lastAutofilledFor.current.title === currentTitle) return; 958 try { 959 const autoTags = await api.autofillTags(currentArtist, currentTitle); 960 if (autoTags && autoTags.length > 0) { 961 setMetadata((prev) => { 962 const currentTagsList = prev.tags ? prev.tags.split(",").map((t2) => t2.trim()).filter(Boolean) : []; 963 let addedAny = false; 964 for (const t2 of autoTags) { 965 if (!currentTagsList.some((existing) => existing.toLowerCase() === t2.toLowerCase())) { 966 currentTagsList.push(t2); 967 addedAny = true; 968 } 969 } 970 if (addedAny) return { ...prev, tags: currentTagsList.join(", ") }; 971 return prev; 972 }); 973 } 974 } catch (err) { 975 console.error("Autofill failed", err); 976 } finally { 977 lastAutofilledFor.current = { artist: currentArtist, title: currentTitle }; 978 } 979 }, 1e3); 980 return () => clearTimeout(timeout); 981 }, [metadata.artist, metadata.title, step, file, batchFiles.length]); 982 reactExports.useEffect(() => { 983 if (step !== 2 || !file || batchFiles.length) return; 984 const title = (metadata.title || "").trim(); 985 if (title.length >= 3) { 986 const timeout = setTimeout(async () => { 987 try { 988 const artists = await api.suggestArtistsByTitle(title); 989 setSuggestedArtists(artists); 990 } catch (err) { 991 console.error("Failed to fetch suggested artists for title", err); 992 } 993 }, 600); 994 return () => clearTimeout(timeout); 995 } else { 996 setSuggestedArtists([]); 997 } 998 }, [metadata.title, step, file, batchFiles.length]); 999 reactExports.useEffect(() => { 1000 if (step !== 2 || !file || batchFiles.length) return; 1001 const artist = (metadata.artist || "").trim(); 1002 if (artist.length >= 3) { 1003 const timeout = setTimeout(async () => { 1004 try { 1005 const works = await api.getCanonicalWorksByComposer(artist); 1006 setCanonicalWorks(works); 1007 if (works.length === 0) { 1008 setSelectedPathParts([]); 1009 setSelectedWorkId(""); 1010 } 1011 } catch (err) { 1012 console.error("Failed to fetch canonical works for composer", err); 1013 setCanonicalWorks([]); 1014 } 1015 }, 600); 1016 return () => clearTimeout(timeout); 1017 } else { 1018 setCanonicalWorks([]); 1019 setSelectedPathParts([]); 1020 setSelectedWorkId(""); 1021 } 1022 }, [metadata.artist, step, file, batchFiles.length]); 1023 if (!isOpen) return null; 1024 const handleFileSelect = (e) => { 1025 if (e.target.files && e.target.files.length > 0) { 1026 processSelectedFiles(e.target.files); 1027 } 1028 }; 1029 const handleUploadSingle = async (force = false) => { 1030 if (!file) return; 1031 setUploading(true); 1032 const formData = new FormData(); 1033 formData.append("midiFile", file); 1034 formData.append("title", metadata.title); 1035 const finalArtist = metadata.artist.trim() || artistInput.trim(); 1036 formData.append("artist", finalArtist); 1037 formData.append("description", metadata.description); 1038 let finalTags = metadata.tags; 1039 if (tagsInput.trim()) { 1040 finalTags = finalTags ? finalTags + "," + tagsInput.trim() : tagsInput.trim(); 1041 } 1042 if (metadata.genre) { 1043 finalTags = metadata.genre + (finalTags ? "," + finalTags : ""); 1044 } 1045 formData.append("tags", finalTags); 1046 formData.append("userId", currentUser ? currentUser.id.toString() : "-1"); 1047 if (force) { 1048 formData.append("overrideDuplicate", "true"); 1049 } 1050 if (selectedWorkId) { 1051 formData.append("canonicalWorkId", selectedWorkId); 1052 } 1053 try { 1054 await api.uploadTrack(formData); 1055 setStep(3); 1056 setDuplicateConflict(null); 1057 if (typeof window !== "undefined") { 1058 window.dispatchEvent(new CustomEvent("hamienet:midi_uploaded", { 1059 detail: { canonicalWorkId: selectedWorkId } 1060 })); 1061 } 1062 } catch (e) { 1063 if (e.duplicate || e.message && e.message.includes("similar MIDI file")) { 1064 setDuplicateConflict({ 1065 message: e.message || "A highly similar MIDI file already exists.", 1066 existingTracks: e.existingTracks || [] 1067 }); 1068 } else { 1069 alert(e.message || t("upload.failed", "Upload failed. Please try again.")); 1070 } 1071 } finally { 1072 setUploading(false); 1073 } 1074 }; 1075 const handleUploadBatch = async () => { 1076 if (batchFiles.length === 0) return; 1077 setUploading(true); 1078 let successCount = 0; 1079 for (let i = 0; i < batchFiles.length; i++) { 1080 const bf = batchFiles[i]; 1081 if (bf.status === "success") { 1082 successCount++; 1083 continue; 1084 } 1085 setBatchFiles((prev) => prev.map((p) => p.id === bf.id ? { ...p, status: "uploading" } : p)); 1086 const formData = new FormData(); 1087 formData.append("midiFile", bf.file); 1088 formData.append("title", bf.title); 1089 formData.append("artist", bf.artist.trim()); 1090 formData.append("description", bf.description); 1091 let finalTags = bf.tags; 1092 if (bf.genre) { 1093 finalTags = bf.genre + (finalTags ? "," + finalTags : ""); 1094 } 1095 formData.append("tags", finalTags); 1096 formData.append("userId", currentUser ? currentUser.id.toString() : "-1"); 1097 if (selectedWorkId) { 1098 formData.append("canonicalWorkId", selectedWorkId); 1099 }
1100 try { 1101 await api.uploadTrack(formData); 1102 setBatchFiles((prev) => prev.map((p) => p.id === bf.id ? { ...p, status: "success" } : p)); 1103 successCount++; 1104 } catch (e) { 1105 const errorMsg = e.duplicate ? t("upload.duplicateFound", "Duplicate") : e.message || t("upload.failed", "Error"); 1106 setBatchFiles((prev) => prev.map((p) => p.id === bf.id ? { ...p, status: "error", error: errorMsg } : p)); 1107 } 1108 } 1109 setUploading(false); 1110 if (successCount > 0 && typeof window !== "undefined") { 1111 window.dispatchEvent(new CustomEvent("hamienet:midi_uploaded", { 1112 detail: { canonicalWorkId: selectedWorkId } 1113 })); 1114 } 1115 if (successCount === batchFiles.length) { 1116 setStep(3); 1117 } 1118 }; 1119 const handleApplyGlobal = () => { 1120 setBatchFiles((prev) => prev.map((bf) => ({ 1121 ...bf, 1122 artist: globalSettings.artist || bf.artist, 1123 genre: globalSettings.genre || bf.genre, 1124 tags: globalSettings.tags || bf.tags 1125 }))); 1126 }; 1127 const handleUpdateBatchFile = (id, field, value) => { 1128 setBatchFiles((prev) => prev.map((bf) => bf.id === id ? { ...bf, [field]: value } : bf)); 1129 }; 1130 const handleRemoveBatchFile = (id) => { 1131 const remaining = batchFiles.filter((bf) => bf.id !== id); 1132 setBatchFiles(remaining); 1133 if (remaining.length === 1) { 1134 const f = remaining[0]; 1135 setFile(f.file); 1136 setMetadata((prev) => ({ 1137 ...prev, 1138 title: f.title, 1139 artist: f.artist, 1140 genre: f.genre, 1141 description: f.description, 1142 tags: f.tags 1143 })); 1144 setArtistInput(f.artist); 1145 setTagsInput(f.tags); 1146 setBatchFiles([]); 1147 } else if (remaining.length === 0) { 1148 setStep(1); 1149 } 1150 }; 1151 const handleClose = () => { 1152 setStep(1); 1153 setFile(null); 1154 setBatchFiles([]); 1155 setMetadata({ title: "", artist: "", description: "", tags: "", genre: "" }); 1156 setDuplicateConflict(null); 1157 setArtistInput(""); 1158 setTagsInput(""); 1159 onClose(); 1160 }; 1161 const isBatchMode = batchFiles.length > 0; 1162 const modalMaxWidthClass = step === 2 && isBatchMode ? "max-w-6xl" : "max-w-2xl"; 1163 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[1000] flex items-center justify-center p-4", children: [ 1164 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/80 backdrop-blur-sm", onClick: handleClose }), 1165 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `relative w-full ${modalMaxWidthClass} bg-white dark:bg-zinc-900 rounded-xl shadow-2xl flex flex-col max-h-[90vh] overflow-hidden animate-in zoom-in-95 transition-all duration-300`, children: [
1166 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-4 border-b border-zinc-200 dark:border-zinc-800", children: [ 1167 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white", children: t("upload.modalTitle", "Upload MIDI") }), 1168 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleClose, children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200" }) }) 1169 ] }), 1170 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-8 flex-1 overflow-y-auto", children: [ 1171 !currentUser && step !== 3 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-4 flex gap-3 text-amber-800 dark:text-amber-200 text-sm", children: [ 1172 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "shrink-0 mt-0.5", size: 18 }),
1173 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1174 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-bold", children: "You are uploading as a Community Contributor (Guest)." }), 1175 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-1 space-y-1 opacity-90", children: [ 1176 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [ 1177 "⢠Max file size: ", 1178 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold text-amber-900 dark:text-amber-100", children: [ 1179 ((_a = filterConfig == null ? void 0 : filterConfig.guestLimits) == null ? void 0 : _a.maxFileSizeKB) || 500, 1180 "KB" 1181 ] }) 1182 ] }), 1183 ((_b = filterConfig == null ? void 0 : filterConfig.guestLimits) == null ? void 0 : _b.blockUrls) !== false && /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [ 1184 "⢠Links/URLs are ", 1185 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-amber-900 dark:text-amber-100", children: "strictly prohibited" }), 1186 " in metadata." 1187 ] }),
1188 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [ 1189 "⢠Daily limit: ", 1190 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold text-amber-900 dark:text-amber-100", children: [ 1191 ((_c = filterConfig == null ? void 0 : filterConfig.guestLimits) == null ? void 0 : _c.maxUploadsPerDay) || 3, 1192 " uploads" 1193 ] }), 1194 " per IP." 1195 ] }), 1196 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "pt-2 text-[11px] leading-relaxed italic", children: [ 1197 "Guest uploads are wiki-style and can be improved by the community. ", 1198 /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "#", onClick: (e) => { 1199 e.preventDefault(); 1200 onClose(); 1201 }, className: "underline font-bold not-italic", children: "Log in" }), 1202 " to remove these limits and claim ownership." 1203 ] }) 1204 ] }) 1205 ] }) 1206 ] }), 1207 step === 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1208 "div", 1209 { 1210 onDragOver: handleDragOver, 1211 onDragLeave: handleDragLeave, 1212 onDrop: handleDrop, 1213 onClick: () => { 1214 var _a2; 1215 return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click(); 1216 }, 1217 className: `h-64 border-2 border-dashed rounded-2xl flex flex-col items-center justify-center cursor-pointer transition-colors ${isDragging ? "border-indigo-500 bg-indigo-50 dark:bg-indigo-950/20" : "border-zinc-300 dark:border-zinc-700 hover:bg-zinc-50 dark:hover:bg-zinc-800/50"}`, 1218 children: [
1219 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-16 bg-zinc-100 dark:bg-zinc-800 rounded-full flex items-center justify-center mb-4", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 32, className: isDragging ? "text-indigo-500" : "text-zinc-500" }) }), 1220 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-medium text-zinc-900 dark:text-white", children: t("upload.selectFiles", "Select MIDI files to upload") }), 1221 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500 mt-2 mb-4", children: t("upload.dragDropSub", "or drag and drop them here") }), 1222 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-amber-600 dark:text-amber-400 font-bold bg-amber-50 dark:bg-amber-900/30 px-3 py-1 rounded-full mb-4", children: t("upload.maxFilesLimit", "Max 10 files allowed") }), 1223 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-4 mt-2", onClick: (e) => e.stopPropagation(), children: [
1224 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1225 "button", 1226 { 1227 onClick: () => { 1228 var _a2; 1229 return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click(); 1230 }, 1231 className: "px-4 py-2 bg-indigo-600 text-white rounded-lg font-bold text-sm hover:bg-indigo-700 flex items-center gap-2", 1232 children: [ 1233 /* @__PURE__ */ jsxRuntimeExports.jsx(FileHeadphone, { size: 16 }), 1234 " Select Files" 1235 ] 1236 } 1237 ), 1238 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1239 "button", 1240 { 1241 onClick: () => { 1242 var _a2; 1243 return (_a2 = folderInputRef.current) == null ? void 0 : _a2.click(); 1244 }, 1245 className: "px-4 py-2 bg-zinc-200 dark:bg-zinc-800 text-zinc-900 dark:text-white rounded-lg font-bold text-sm hover:bg-zinc-300 dark:hover:bg-zinc-700 flex items-center gap-2", 1246 children: [
1247 /* @__PURE__ */ jsxRuntimeExports.jsx(FolderOpen, { size: 16 }), 1248 " ", 1249 t("upload.selectFolder", "Select Folder") 1250 ] 1251 } 1252 ) 1253 ] }), 1254 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "file", ref: fileInputRef, className: "hidden", accept: ".mid,.midi,.kar", multiple: true, onChange: handleFileSelect }), 1255 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "file", ref: folderInputRef, className: "hidden", accept: ".mid,.midi,.kar", webkitdirectory: "", directory: "", multiple: true, onChange: handleFileSelect }) 1256 ] 1257 } 1258 ), 1259 step === 2 && !isBatchMode && !duplicateConflict && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 1260 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1261 "div", 1262 { 1263 onDragOver: handleDragOver, 1264 onDragLeave: handleDragLeave, 1265 onDrop: handleDrop, 1266 onClick: () => { 1267 var _a2; 1268 return (_a2 = fileInputRef.current) == null ? void 0 : _a2.click(); 1269 }, 1270 className: `flex items-center gap-3 p-4 rounded-lg border-2 border-dashed transition-all cursor-pointer ${isDragging ? "border-indigo-500 bg-indigo-50 dark:bg-indigo-950/20" : "border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 hover:bg-zinc-100 dark:hover:bg-zinc-700"}`, 1271 children: [
1272 /* @__PURE__ */ jsxRuntimeExports.jsx(FileHeadphone, { className: file ? "text-indigo-500" : "text-zinc-400" }), 1273 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-sm flex-1 truncate text-zinc-900 dark:text-white", children: file ? file.name : t("common.selectFile", "Select a file") }), 1274 /* @__PURE__ */ jsxRuntimeExports.jsx( 1275 "button", 1276 { 1277 type: "button", 1278 onClick: (e) => { 1279 var _a2; 1280 e.stopPropagation(); 1281 if (file) { 1282 setStep(1); 1283 } else { 1284 (_a2 = fileInputRef.current) == null ? void 0 : _a2.click(); 1285 } 1286 }, 1287 className: `text-xs font-bold hover:underline ${file ? "text-indigo-500" : "text-indigo-500"}`, 1288 children: file ? t("common.change", "Change") : t("common.selectFile", "Select a file") 1289 } 1290 ) 1291 ] 1292 } 1293 ),
1294 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [ 1295 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1296 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "block text-sm font-medium mb-1 text-zinc-900 dark:text-white", children: [ 1297 t("upload.fields.title", "Title"), 1298 " ", 1299 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-0.5", children: "*" }) 1300 ] }), 1301 /* @__PURE__ */ jsxRuntimeExports.jsx( 1302 "input", 1303 { 1304 type: "text", 1305 maxLength: 200, 1306 value: metadata.title, 1307 onChange: (e) => setMetadata({ ...metadata, title: e.target.value }), 1308 className: `w-full bg-zinc-50 dark:bg-zinc-800 border ${validationErrors.title ? "border-red-500" : "border-zinc-300 dark:border-zinc-700"} rounded p-2 text-zinc-900 dark:text-white` 1309 } 1310 ), 1311 validationErrors.title && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-red-500 mt-0.5", children: validationErrors.title }) 1312 ] }),
1313 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1314 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "block text-sm font-medium mb-1 text-zinc-900 dark:text-white", children: [ 1315 t("upload.fields.genre", "Primary Genre"), 1316 " ", 1317 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-0.5", children: "*" }) 1318 ] }), 1319 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1320 "select", 1321 { 1322 value: metadata.genre, 1323 onChange: (e) => setMetadata({ ...metadata, genre: e.target.value }), 1324 className: "w-full bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded p-2 text-zinc-900 dark:text-white", 1325 children: [ 1326 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: t("upload.fields.selectGenre", "-- Select Genre --") }), 1327 availableGenres.map((g) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: g.name, children: g.name }, g.id)) 1328 ] 1329 } 1330 ) 1331 ] }) 1332 ] }),
1333 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1334 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "block text-sm font-medium mb-1 text-zinc-900 dark:text-white", children: [ 1335 t("upload.fields.artist", "Composer/Artist"), 1336 " ", 1337 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-500 ml-0.5", children: "*" }) 1338 ] }), 1339 /* @__PURE__ */ jsxRuntimeExports.jsx( 1340 TagAutocomplete, 1341 { 1342 value: metadata.artist, 1343 onChange: (val) => setMetadata({ ...metadata, artist: val }), 1344 inputValue: artistInput, 1345 onInputChange: setArtistInput, 1346 suggestedArtists, 1347 onSelectSuggestion: (s) => { 1348 if (s.source === "MusicBrainz" || s.type === "GENRE") { 1349 const match = availableGenres.find((g) => g.name.toLowerCase() === s.name.toLowerCase()); 1350 if (match) { 1351 if (!metadata.genre) { 1352 setMetadata((prev) => ({ ...prev, genre: match.name })); 1353 } 1354 } else { 1355 setMetadata((prev) => { 1356 const currentTags = prev.tags ? prev.tags.split(",").map((t2) => t2.trim()).filter(Boolean) : []; 1357 if (!currentTags.some((t2) => t2.toLowerCase() === s.name.toLowerCase())) { 1358 currentTags.push(s.name); 1359 return { ...prev, tags: currentTags.join(", ") }; 1360 } 1361 return prev; 1362 }); 1363 } 1364 } 1365 }, 1366 placeholder: "Andrew Lloyd Webber, Joe Hisaishi...", 1367 maxLength: 100, 1368 className: validationErrors.artist ? "border-red-500" : "" 1369 } 1370 ), 1371 validationErrors.artist && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-red-500 mt-0.5", children: validationErrors.artist }) 1372 ] }),
1373 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1374 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-sm font-medium mb-1 text-zinc-900 dark:text-white", children: t("upload.fields.description", "Description") }), 1375 /* @__PURE__ */ jsxRuntimeExports.jsx( 1376 "textarea", 1377 { 1378 value: metadata.description, 1379 onChange: (e) => setMetadata({ ...metadata, description: e.target.value }), 1380 className: `w-full bg-zinc-50 dark:bg-zinc-800 border ${validationErrors.description ? "border-red-500" : "border-zinc-300 dark:border-zinc-700"} rounded p-2 h-24 text-zinc-900 dark:text-white` 1381 } 1382 ), 1383 validationErrors.description && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-red-500 mt-0.5", children: validationErrors.description }) 1384 ] }), 1385 canonicalWorks.length > 0 && (() => { 1386 const levelDropdowns = []; 1387 for (let i = 0; i <= selectedPathParts.length; i++) { 1388 const currentPrefix = selectedPathParts.slice(0, i).join(" > "); 1389 const parts = /* @__PURE__ */ new Set(); 1390 canonicalWorks.forEach((w) => { 1391 const group = (w.groupName || "Other").replace(/ > /g, ">").replace(/>/g, " > "); 1392 const groupParts = group.split(" > ").map((s) => s.trim()); 1393 if (groupParts.length > i) { 1394 if (i === 0 || groupParts.slice(0, i).join(" > ") === currentPrefix) { 1395 parts.add(groupParts[i]); 1396 } 1397 } 1398 }); 1399 const options = Array.from(parts).sort(); 1400 if (options.length > 0) { 1401 levelDropdowns.push({ 1402 level: i, 1403 options, 1404 selectedValue: selectedPathParts[i] || "" 1405 }); 1406 } else { 1407 break; 1408 } 1409 } 1410 const currentFullGroup = selectedPathParts.join(" > "); 1411 const availableWorks = canonicalWorks.filter((w) => { 1412 const group = (w.groupName || "Other").replace(/ > /g, ">").replace(/>/g, " > "); 1413 return group === currentFullGroup; 1414 }); 1415 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-indigo-50/50 dark:bg-indigo-950/20 border border-indigo-100 dark:border-indigo-900/50 rounded-lg p-4 space-y-4", children: [
1416 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-indigo-900 dark:text-indigo-200 text-sm flex items-center gap-2", children: [ 1417 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "ðï¸" }), 1418 " Classical Work Binding (Optional)" 1419 ] }), 1420 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3", children: [ 1421 levelDropdowns.map((dropdown, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1422 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold mb-1 text-indigo-800 dark:text-indigo-300", children: idx === 0 ? "Type of Work" : "Sub-Category" }), 1423 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1424 "select", 1425 { 1426 value: dropdown.selectedValue, 1427 onChange: (e) => { 1428 const newParts = selectedPathParts.slice(0, idx); 1429 if (e.target.value) { 1430 newParts.push(e.target.value); 1431 } 1432 setSelectedPathParts(newParts); 1433 setSelectedWorkId(""); 1434 setMetadata((m) => ({ ...m, sectionSongId: void 0 })); 1435 }, 1436 className: "w-full bg-white dark:bg-zinc-800 border border-indigo-200 dark:border-indigo-800 rounded p-2 text-sm text-zinc-900 dark:text-white", 1437 children: [
1438 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: "-- Select --" }), 1439 dropdown.options.map((opt) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: opt, children: opt }, opt)) 1440 ] 1441 } 1442 ) 1443 ] }, idx)), 1444 availableWorks.length > 0 && selectedPathParts.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pt-2 border-t border-indigo-100 dark:border-indigo-900/50", children: [ 1445 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold mb-1 text-indigo-800 dark:text-indigo-300", children: "Work" }), 1446 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1447 "select", 1448 { 1449 value: selectedWorkId, 1450 onChange: (e) => setSelectedWorkId(e.target.value), 1451 className: "w-full bg-white dark:bg-zinc-800 border border-indigo-200 dark:border-indigo-800 rounded p-2 text-sm text-zinc-900 dark:text-white", 1452 children: [
1453 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: "-- Select Work --" }), 1454 availableWorks.map((w) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: w.id, children: w.title }, w.id)) 1455 ] 1456 } 1457 ) 1458 ] }) 1459 ] }) 1460 ] }); 1461 })(), 1462 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1463 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-sm font-medium mb-1 text-zinc-900 dark:text-white", children: t("upload.fields.tags", "Additional Tags") }), 1464 /* @__PURE__ */ jsxRuntimeExports.jsx( 1465 TagAutocomplete, 1466 { 1467 value: metadata.tags, 1468 onChange: (val) => setMetadata({ ...metadata, tags: val }), 1469 inputValue: tagsInput, 1470 onInputChange: setTagsInput, 1471 placeholder: "piano, virtuoso, 8-bit" 1472 } 1473 ) 1474 ] }),
1475 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 p-3 bg-zinc-100 dark:bg-zinc-800/50 rounded-lg border border-zinc-200 dark:border-zinc-700 flex items-start gap-3", children: [ 1476 /* @__PURE__ */ jsxRuntimeExports.jsx( 1477 "input", 1478 { 1479 type: "checkbox", 1480 id: "fairUseAgree", 1481 checked: fairUseAgreed, 1482 onChange: (e) => setFairUseAgreed(e.target.checked), 1483 className: "mt-1 w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 bg-white dark:bg-zinc-900 border-zinc-300 dark:border-zinc-600 cursor-pointer" 1484 } 1485 ), 1486 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "fairUseAgree", className: "text-xs text-zinc-600 dark:text-zinc-400 leading-relaxed cursor-pointer select-none", children: [ 1487 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "text-zinc-900 dark:text-zinc-200", children: "Fair Use & Educational Notice:" }), 1488 " I claim this upload is for non-profit musicological research, study, or educational purposes. I automatically authorize HamieNET to issue a DMCA Counter-Notice on my behalf against any takedown attempts." 1489 ] }) 1490 ] }) 1491 ] }), 1492 step === 2 && isBatchMode && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-6", children: [
1493 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-indigo-50 dark:bg-indigo-950/30 border border-indigo-100 dark:border-indigo-900/50 rounded-lg p-4", children: [ 1494 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-indigo-900 dark:text-indigo-200 mb-3 text-sm flex items-center gap-2", children: [ 1495 /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 16 }), 1496 t("upload.globalSettings", "Global Settings (Apply to All)") 1497 ] }), 1498 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4", children: [ 1499 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1500 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold mb-1 text-indigo-800 dark:text-indigo-300", children: t("upload.fields.genre", "Primary Genre") }), 1501 /* @__PURE__ */ jsxRuntimeExports.jsxs( 1502 "select", 1503 { 1504 value: globalSettings.genre, 1505 onChange: (e) => setGlobalSettings({ ...globalSettings, genre: e.target.value }), 1506 className: "w-full bg-white dark:bg-zinc-800 border border-indigo-200 dark:border-indigo-800 rounded p-1.5 text-sm", 1507 children: [
1508 /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: "", children: [ 1509 "-- ", 1510 t("common.optional", "Optional"), 1511 " --" 1512 ] }), 1513 availableGenres.map((g) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: g.name, children: g.name }, g.id)) 1514 ] 1515 } 1516 ) 1517 ] }), 1518 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1519 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold mb-1 text-indigo-800 dark:text-indigo-300", children: t("upload.fields.artist", "Composer/Artist") }), 1520 /* @__PURE__ */ jsxRuntimeExports.jsx( 1521 "input", 1522 { 1523 type: "text", 1524 value: globalSettings.artist, 1525 onChange: (e) => setGlobalSettings({ ...globalSettings, artist: e.target.value }), 1526 className: "w-full bg-white dark:bg-zinc-800 border border-indigo-200 dark:border-indigo-800 rounded p-1.5 text-sm", 1527 placeholder: "Set artist for all..." 1528 } 1529 ) 1530 ] }),
1531 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 1532 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold mb-1 text-indigo-800 dark:text-indigo-300", children: t("upload.fields.tags", "Tags") }), 1533 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [ 1534 /* @__PURE__ */ jsxRuntimeExports.jsx( 1535 "input", 1536 { 1537 type: "text", 1538 value: globalSettings.tags, 1539 onChange: (e) => setGlobalSettings({ ...globalSettings, tags: e.target.value }), 1540 className: "w-full bg-white dark:bg-zinc-800 border border-indigo-200 dark:border-indigo-800 rounded p-1.5 text-sm", 1541 placeholder: "Set tags for all..." 1542 } 1543 ), 1544 /* @__PURE__ */ jsxRuntimeExports.jsx( 1545 "button", 1546 { 1547 onClick: handleApplyGlobal, 1548 className: "px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded font-bold text-xs whitespace-nowrap", 1549 children: t("upload.applyToAll", "Apply to All") 1550 } 1551 ) 1552 ] }) 1553 ] }) 1554 ] }) 1555 ] }),
1556 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border border-zinc-200 dark:border-zinc-800 rounded-lg overflow-x-auto shadow-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-left text-sm", children: [ 1557 /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-zinc-100 dark:bg-zinc-800/80 text-zinc-600 dark:text-zinc-400 font-medium", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 1558 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "p-3 w-48", children: "File" }), 1559 /* @__PURE__ */ jsxRuntimeExports.jsxs("th", { className: "p-3 w-48", children: [ 1560 t("upload.fields.title", "Title"), 1561 "*" 1562 ] }), 1563 /* @__PURE__ */ jsxRuntimeExports.jsxs("th", { className: "p-3 w-48", children: [ 1564 t("upload.fields.artist", "Artist"), 1565 "*" 1566 ] }), 1567 /* @__PURE__ */ jsxRuntimeExports.jsxs("th", { className: "p-3 w-32", children: [ 1568 t("upload.fields.genre", "Genre"), 1569 "*" 1570 ] }),
1571 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "p-3 w-32", children: t("upload.fields.tags", "Tags") }), 1572 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "p-3 w-16 text-center", children: "Status" }) 1573 ] }) }), 1574 /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-zinc-200 dark:divide-zinc-800", children: batchFiles.map((bf) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: bf.status === "success" ? "bg-emerald-50 dark:bg-emerald-900/10" : bf.status === "error" ? "bg-red-50 dark:bg-red-900/10" : "bg-white dark:bg-zinc-900", children: [ 1575 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-2 align-top", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between group", children: [ 1576 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs truncate max-w-[140px]", title: bf.file.name, children: bf.file.name }), 1577 bf.status !== "uploading" && bf.status !== "success" && /* @__PURE__ */ jsxRuntimeExports.jsx( 1578 "button", 1579 { 1580 onClick: () => handleRemoveBatchFile(bf.id), 1581 className: "text-zinc-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity p-1", 1582 title: t("upload.remove", "Remove"), 1583 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 14 }) 1584 } 1585 ) 1586 ] }) }),
1587 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-2 align-top", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1588 "input", 1589 { 1590 type: "text", 1591 value: bf.title, 1592 onChange: (e) => handleUpdateBatchFile(bf.id, "title", e.target.value), 1593 className: `w-full bg-zinc-50 dark:bg-zinc-800 border ${!bf.title.trim() ? "border-red-400" : "border-zinc-300 dark:border-zinc-700"} rounded p-1.5 text-xs`, 1594 disabled: bf.status === "uploading" || bf.status === "success" 1595 } 1596 ) }), 1597 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-2 align-top", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1598 "input", 1599 { 1600 type: "text", 1601 value: bf.artist, 1602 onChange: (e) => handleUpdateBatchFile(bf.id, "artist", e.target.value), 1603 className: `w-full bg-zinc-50 dark:bg-zinc-800 border ${!bf.artist.trim() ? "border-red-400" : "border-zinc-300 dark:border-zinc-700"} rounded p-1.5 text-xs`, 1604 disabled: bf.status === "uploading" || bf.status === "success", 1605 placeholder: "Composer..." 1606 } 1607 ) }),
1608 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-2 align-top", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 1609 "select", 1610 { 1611 value: bf.genre, 1612 onChange: (e) => handleUpdateBatchFile(bf.id, "genre", e.target.value), 1613 className: `w-full bg-zinc-50 dark:bg-zinc-800 border ${!bf.genre ? "border-red-400" : "border-zinc-300 dark:border-zinc-700"} rounded p-1.5 text-xs`, 1614 disabled: bf.status === "uploading" || bf.status === "success", 1615 children: [ 1616 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: "-" }), 1617 availableGenres.map((g) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: g.name, children: g.name }, g.id)) 1618 ] 1619 } 1620 ) }), 1621 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "p-2 align-top", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1622 "input", 1623 { 1624 type: "text", 1625 value: bf.tags, 1626 onChange: (e) => handleUpdateBatchFile(bf.id, "tags", e.target.value), 1627 className: "w-full bg-zinc-50 dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 rounded p-1.5 text-xs", 1628 disabled: bf.status === "uploading" || bf.status === "success" 1629 } 1630 ) }),
1631 /* @__PURE__ */ jsxRuntimeExports.jsxs("td", { className: "p-2 align-top text-center", children: [ 1632 bf.status === "pending" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-500", children: t("upload.statusPending", "Pending") }), 1633 bf.status === "uploading" && /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "animate-spin text-indigo-500 mx-auto", size: 16 }), 1634 bf.status === "success" && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { className: "text-emerald-500 mx-auto", size: 16 }), 1635 bf.status === "error" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-red-500 block truncate", title: bf.error, children: bf.error }) 1636 ] }) 1637 ] }, bf.id)) }) 1638 ] }) }), 1639 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 p-3 bg-zinc-100 dark:bg-zinc-800/50 rounded-lg border border-zinc-200 dark:border-zinc-700 flex items-start gap-3", children: [
1640 /* @__PURE__ */ jsxRuntimeExports.jsx( 1641 "input", 1642 { 1643 type: "checkbox", 1644 id: "fairUseAgreeBatch", 1645 checked: fairUseAgreed, 1646 onChange: (e) => setFairUseAgreed(e.target.checked), 1647 className: "mt-1 w-4 h-4 rounded text-indigo-600 focus:ring-indigo-500 bg-white dark:bg-zinc-900 border-zinc-300 dark:border-zinc-600 cursor-pointer" 1648 } 1649 ), 1650 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { htmlFor: "fairUseAgreeBatch", className: "text-xs text-zinc-600 dark:text-zinc-400 leading-relaxed cursor-pointer select-none", children: [ 1651 /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "text-zinc-900 dark:text-zinc-200", children: "Fair Use & Educational Notice:" }), 1652 " I claim these uploads are for non-profit musicological research, study, or educational purposes. I automatically authorize HamieNET to issue a DMCA Counter-Notice on my behalf against any takedown attempts." 1653 ] }) 1654 ] }) 1655 ] }), 1656 step === 2 && !isBatchMode && duplicateConflict && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4 text-center py-6", children: [
1657 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-16 bg-amber-50 dark:bg-amber-950/20 rounded-full flex items-center justify-center mx-auto mb-2 text-amber-500 animate-bounce", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 32 }) }), 1658 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-bold text-zinc-900 dark:text-white", children: "Duplicate MIDI Detected" }), 1659 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-505 dark:text-zinc-400 text-sm max-w-md mx-auto", children: duplicateConflict.message }), 1660 duplicateConflict.existingTracks && duplicateConflict.existingTracks.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-50 dark:bg-zinc-800 p-4 rounded-lg border border-zinc-200 dark:border-zinc-700 text-left max-w-md mx-auto space-y-2", children: [ 1661 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400 font-bold block uppercase tracking-wider", children: "Existing Match" }), 1662 duplicateConflict.existingTracks.map((t2) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm font-semibold text-zinc-900 dark:text-white flex items-center justify-between", children: [
1663 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 1664 t2.title, 1665 " ", 1666 t2.artist ? `by ${t2.artist}` : "" 1667 ] }), 1668 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-zinc-400 font-mono", children: [ 1669 "ID: ", 1670 t2.id 1671 ] }) 1672 ] }, t2.id)) 1673 ] }), 1674 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-400 dark:text-zinc-500 max-w-sm mx-auto leading-relaxed", children: "You can choose to publish this upload as an educational/variation group variant of the existing file." }), 1675 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-3 justify-center pt-4", children: [ 1676 /* @__PURE__ */ jsxRuntimeExports.jsx( 1677 "button", 1678 { 1679 onClick: () => setDuplicateConflict(null), 1680 className: "px-5 py-2 bg-zinc-200 dark:bg-zinc-800 hover:bg-zinc-300 dark:hover:bg-zinc-700 rounded-lg font-bold text-zinc-900 dark:text-white text-sm", 1681 children: "Cancel & Edit" 1682 } 1683 ),
1684 /* @__PURE__ */ jsxRuntimeExports.jsx( 1685 "button", 1686 { 1687 onClick: () => handleUploadSingle(true), 1688 disabled: uploading, 1689 className: "px-5 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-bold text-sm disabled:opacity-50", 1690 children: uploading ? "Uploading..." : "Publish as Variation" 1691 } 1692 ) 1693 ] }) 1694 ] }), 1695 step === 3 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center py-10", children: [ 1696 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { size: 64, className: "mx-auto text-emerald-500 mb-4" }), 1697 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-2xl font-bold text-zinc-900 dark:text-white", children: t("upload.complete", "Upload Complete!") }), 1698 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 mt-2", children: isBatchMode ? `Successfully uploaded ${batchFiles.filter((b) => b.status === "success").length} out of ${batchFiles.length} files.` : t("upload.successMsg", "Your MIDI is now processing. It will appear on your channel shortly.") }) 1699 ] }) 1700 ] }),
1701 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-t border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900 flex justify-end gap-3", children: [ 1702 step === 2 && !duplicateConflict && (isBatchMode ? /* @__PURE__ */ jsxRuntimeExports.jsx( 1703 "button", 1704 { 1705 disabled: uploading || !fairUseAgreed || batchFiles.some((bf) => !bf.title.trim() || !bf.artist.trim() || !bf.genre), 1706 onClick: handleUploadBatch, 1707 className: "px-6 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-bold disabled:opacity-50 flex items-center gap-2 transition-opacity", 1708 children: uploading ? t("upload.uploading", "Uploading...") : t("upload.uploadAll", "Upload All") 1709 } 1710 ) : /* @__PURE__ */ jsxRuntimeExports.jsx( 1711 "button", 1712 { 1713 disabled: !file || uploading || isValidating || Object.keys(validationErrors).length > 0 || !fairUseAgreed || !metadata.title.trim() || !(metadata.artist.trim() || artistInput.trim()) || !metadata.genre, 1714 onClick: handleUploadSingle, 1715 className: "px-6 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg font-bold disabled:opacity-50 flex items-center gap-2 transition-opacity", 1716 children: uploading ? t("upload.uploading", "Uploading...") : isValidating ? t("common.validating", "Validating...") : t("upload.publish", "Publish") 1717 } 1718 )), 1719 (step === 3 || step === 1 || step === 2 && duplicateConflict) && /* @__P
1719URE__ */ jsxRuntimeExports.jsx("button", { onClick: handleClose, className: "px-6 py-2 bg-zinc-200 dark:bg-zinc-800 hover:bg-zinc-300 dark:hover:bg-zinc-700 rounded-lg font-bold text-zinc-900 dark:text-white", children: t("common.close", "Close") }) 1720 ] }) 1721 ] }) 1722 ] }); 1723} 1724const PlayerContext = reactExports.createContext(void 0); 1725const PlayerProvider = ({ children }) => { 1726 const [activeTrack, setActiveTrack] = reactExports.useState(null); 1727 const [isPlaying, setIsPlaying] = reactExports.useState(false); 1728 const [currentTime, setCurrentTime] = reactExports.useState(0); 1729 const [duration, setDuration] = reactExports.useState(0); 1730 const [queue, setQueue] = reactExports.useState([]); 1731 const [activePlaylist, setActivePlaylist] = reactExports.useState(null); 1732 const [playlistLabel, setPlaylistLabel] = reactExports.useState(null); 1733 const [seekRequest, setSeekRequest] = reactExports.useState(null); 1734 const [audioMode, setAudioMode] = reactExports.useState("wasmsynth"); 1735 const [engineOrder, setEngineOrder] = reactExports.useState(null); 1736 const [isBuffering, setIsBuffering] = reactExports.useState(false); 1737 const [trackStates, setTrackStates] = reactExports.useState({}); 1738 const [isLiveSynthFallback, setIsLiveSynthFallback] = reactExports.useState(false); 1739 const [soundfont, setSoundfont] = reactExports.useState("MusyngKite"); 1740 const [hasStartedLoading, setHasStartedLoading] = reactExports.useState(false); 1741 reactExports.useEffect(() => { 1742 const fetchEngineOrder = async () => { 1743 try { 1744 const res = await api.getSoundfonts(); 1745 if (res && res.engineOrder) { 1746 setEngineOrder(res.engineOrder); 1747 } 1748 } catch (err) { 1749 console.warn("Failed to fetch engine order in PlayerContext:", err); 1750 } 1751 }; 1752 fetchEngineOrder(); 1753 }, []); 1754 const isSynthReady = reactExports.useMemo(() => { 1755 if (!hasStartedLoading) return false; 1756 const isLiveMode = isLiveSynthFallback || ["synth", "webmidi", "chiptune", "wasmsynth", "fmsynth"].includes(audioMode); 1757 if (isLiveMode) { 1758 const keys = Object.keys(trackStates); 1759 if (keys.length === 0) return false; 1760 return keys.every((key) => { 1761 const status = trackStates[parseInt(key)]; 1762 return status && (status.ready || status.error); 1763 }); 1764 } 1765 if (!(activeTrack == null ? void 0 : activeTrack.instruments) || activeTrack.instruments.length === 0) return true; 1766 const allLoaded = activeTrack.instruments.every((inst, idx) => { 1767 if (!inst.stemPath) return true; 1768 const status = trackStates[idx]; 1769 return status && (status.ready || status.error); 1770 }); 1771 return allLoaded; 1772 }, [trackStates, activeTrack, hasStartedLoading, isLiveSynthFallback, audioMode]); 1773 const togglePlay = reactExports.useCallback(() => { 1774 setIsPlaying((prev) => !prev); 1775 }, [setIsPlaying]); 1776 const playTrack = reactExports.useCallback((track, playlist) => { 1777 if (playlist !== void 0) { 1778 setActivePlaylist(playlist); 1779 } 1780 setActiveTrack((prevActiveTrack) => { 1781 if ((prevActiveTrack == null ? void 0 : prevActiveTrack.id) === track.id) { 1782 setIsPlaying((prevIsPlaying) => !prevIsPlaying); 1783 return prevActiveTrack; 1784 } else { 1785 setIsPlaying(true); 1786 return track; 1787 } 1788 }); 1789 }, [setActiveTrack, setIsPlaying, setActivePlaylist]); 1790 const seekTo = reactExports.useCallback((time) => { 1791 setSeekRequest({ time, timestamp: Date.now() }); 1792 setCurrentTime(time); 1793 }, [setSeekRequest, setCurrentTime]); 1794 const addToQueue = reactExports.useCallback((track) => { 1795 setQueue((prev) => { 1796 if (!prev.find((t) => t.id === track.id)) { 1797 return [...prev, track]; 1798 } 1799 return prev; 1800 }); 1801 }, [setQueue]); 1802 const value = reactExports.useMemo(() => ({ 1803 activeTrack, 1804 setActiveTrack, 1805 isPlaying, 1806 setIsPlaying, 1807 currentTime, 1808 setCurrentTime, 1809 duration, 1810 setDuration, 1811 queue, 1812 setQueue, 1813 activePlaylist, 1814 setActivePlaylist, 1815 playlistLabel, 1816 setPlaylistLabel, 1817 seekRequest, 1818 setSeekRequest, 1819 togglePlay, 1820 playTrack, 1821 seekTo, 1822 addToQueue, 1823 audioMode,
1824 setAudioMode, 1825 engineOrder, 1826 setEngineOrder, 1827 isBuffering, 1828 setIsBuffering, 1829 trackStates, 1830 setTrackStates, 1831 isLiveSynthFallback, 1832 setIsLiveSynthFallback, 1833 isSynthReady, 1834 soundfont, 1835 setSoundfont, 1836 hasStartedLoading, 1837 setHasStartedLoading 1838 }), [ 1839 activeTrack, 1840 setActiveTrack, 1841 isPlaying, 1842 setIsPlaying, 1843 currentTime, 1844 setCurrentTime, 1845 duration, 1846 setDuration, 1847 queue, 1848 setQueue, 1849 activePlaylist, 1850 setActivePlaylist, 1851 seekRequest, 1852 setSeekRequest, 1853 togglePlay, 1854 playTrack, 1855 seekTo, 1856 addToQueue, 1857 audioMode, 1858 engineOrder, 1859 isBuffering, 1860 trackStates, 1861 isLiveSynthFallback, 1862 isSynthReady, 1863 soundfont, 1864 hasStartedLoading 1865 ]); 1866 return /* @__PURE__ */ jsxRuntimeExports.jsx(PlayerContext.Provider, { value, children }); 1867}; 1868const usePlayer = () => { 1869 const context = reactExports.useContext(PlayerContext); 1870 if (context === void 0) { 1871 throw new Error("usePlayer must be used within a PlayerProvider"); 1872 } 1873 return context; 1874}; 1875function formatRelativeTime$1(date) { 1876 const now = Date.now(); 1877 const then = new Date(date).getTime(); 1878 const diff = Math.floor((now - then) / 1e3); 1879 if (diff < 60) return "just now"; 1880 if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; 1881 if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; 1882 if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`; 1883 return new Date(date).toLocaleDateString(); 1884} 1885const getIcon$1 = (type) => { 1886 switch (type) { 1887 case "upload": 1888 return /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 14, className: "text-zinc-600 dark:text-white" }); 1889 case "like": 1890 return /* @__PURE__ */ jsxRuntimeExports.jsx(Heart, { size: 14, className: "text-red-500 fill-red-500" }); 1891 case "reply": 1892 return /* @__PURE__ */ jsxRuntimeExports.jsx(MessageSquare, { size: 14, className: "text-blue-500 dark:text-blue-400 fill-blue-500 dark:fill-blue-400" }); 1893 case "subscriber": 1894 return /* @__PURE__ */ jsxRuntimeExports.jsx(User, { size: 14, className: "text-emerald-500 dark:text-emerald-400" }); 1895 default: 1896 return /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { size: 14, className: "text-zinc-400" }); 1897 } 1898}; 1899const NotificationsDropdown = ({ notifications, onMarkAsRead, onMarkAllRead, onSeeAll, onClose, onNavigate }) => { 1900 const { t } = useTranslation(); 1901 const unreadCount = notifications.filter((n) => !n.isRead).length; 1902 if (notifications.length === 0) { 1903 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-12 right-0 w-96 bg-white dark:bg-[#212121] border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-2xl z-50 p-6 text-center animate-in fade-in slide-in-from-top-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 dark:text-zinc-400 text-sm", children: t("notifications.empty", "No notifications here.") }) }); 1904 } 1905 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-12 right-0 w-96 bg-white dark:bg-[#212121] border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-2xl z-50 flex flex-col animate-in fade-in slide-in-from-top-2 custom-scrollbar", children: [ 1906 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-3 px-4 border-b border-zinc-200 dark:border-zinc-700 sticky top-0 bg-white dark:bg-[#212121] z-10", children: [
1907 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-medium text-base text-zinc-900 dark:text-white", children: t("notifications.title", "Notifications") }), 1908 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 1909 unreadCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs( 1910 "button", 1911 { 1912 onClick: onMarkAllRead, 1913 className: "flex items-center gap-1 text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 px-2 py-1 rounded-md hover:bg-indigo-50 dark:hover:bg-indigo-900/20 transition-colors", 1914 title: t("notifications.markAllRead", "Mark all as read"), 1915 children: [ 1916 /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCheck, { size: 13 }), 1917 t("notifications.markAllShort", "All read") 1918 ] 1919 } 1920 ), 1921 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "p-2 hover:bg-zinc-100 dark:hover:bg-zinc-700 rounded-full text-zinc-500 hover:text-black dark:text-zinc-400 dark:hover:text-white transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 18 }) }) 1922 ] }) 1923 ] }), 1924 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col py-2", children: notifications.map((note) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 1925 "div", 1926 { 1927 onClick: () => { 1928 if (!note.isRead) onMarkAsRead(note.id); 1929 if (note.link && onNavigate) { 1930 onNavigate(note.link); 1931 onClose(); 1932 } 1933 }, 1934 className: `flex gap-3 px-4 py-3 hover:bg-zinc-100 dark:hover:bg-zinc-700/50 cursor-pointer transition-colors relative group ${!note.isRead ? "bg-indigo-50 dark:bg-indigo-900/10" : ""}`, 1935 children: [ 1936 !note.isRead && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-1 top-1/2 -translate-y-1/2 w-1.5 h-1.5 bg-blue-500 rounded-full" }),
1937 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative shrink-0", children: [ 1938 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: note.actor.avatar, name: note.actor.name, size: 40 }), 1939 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -bottom-1 -right-1 p-1 bg-white dark:bg-[#212121] rounded-full ring-2 ring-white dark:ring-[#212121]", children: getIcon$1(note.type) }) 1940 ] }), 1941 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [ 1942 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-sm leading-snug text-zinc-900 dark:text-white", children: [ 1943 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold mr-1", children: note.actor.name }), 1944 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600 dark:text-zinc-300", dangerouslySetInnerHTML: { __html: note.message } }) 1945 ] }), 1946 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-500 mt-1 block", children: formatRelativeTime$1(note.timestamp) }) 1947 ] }), 1948 note.thumbnail ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 w-16 h-10 bg-zinc-200 dark:bg-zinc-800 rounded overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: note.thumbnail, className: "w-full h-full object-cover opacity-80", alt: "" }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-10" }) 1949 ] 1950 }, 1951 note.id 1952 )) }), 1953 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-200 dark:border-zinc-700", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1954 "button", 1955 { 1956 onClick: () => { 1957 onSeeAll(); 1958 onClose(); 1959 }, 1960 className: "w-full py-3 text-sm font-medium text-indigo-600 dark:text-indigo-400 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors rounded-b-xl", 1961 children: t("notifications.seeAll", "See all notifications") 1962 } 1963 ) }) 1964 ] }); 1965}; 1966function useDebounce(value, delay) { 1967 const [debouncedValue, setDebouncedValue] = reactExports.useState(value); 1968 reactExports.useEffect(() => { 1969 const handler = setTimeout(() => { 1970 setDebouncedValue(value); 1971 }, delay); 1972 return () => clearTimeout(handler); 1973 }, [value, delay]); 1974 return debouncedValue; 1975} 1976function Header({ 1977 isSidebarOpen, 1978 setIsSidebarOpen, 1979 handleGoHome, 1980 searchQuery, 1981 setSearchQuery, 1982 handleSearch, 1983 currentUser, 1984 setIsAuthModalOpen, 1985 onAccountClick, 1986 onPersonalSpaceClick, 1987 onLogout, 1988 notifications, 1989 onMarkNotificationRead, 1990 onMarkAllNotificationsRead, 1991 onSeeAllNotifications, 1992 onUploadClick, 1993 onMelodySearchClick, 1994 onNavigateNotification, 1995 onNavigate, 1996 disableNearHeaderExpand = false 1997}) { 1998 var _a, _b; 1999 const { t } = useTranslation(); 2000 const { graphics } = useSettings(); 2001 const [showNotifications, setShowNotifications] = reactExports.useState(false); 2002 const [activeLogo, setActiveLogo] = reactExports.useState(null); 2003 const notificationRef = reactExports.useRef(null); 2004 const [isScrolled, setIsScrolled] = reactExports.useState(false); 2005 const [isHovered, setIsHovered] = reactExports.useState(false); 2006 const [isSearchFocused, setIsSearchFocused] = reactExports.useState(false); 2007 const searchInputRef = reactExports.useRef(null); 2008 const [suggestions, setSuggestions] = reactExports.useState([]); 2009 const [showDropdown, setShowDropdown] = reactExports.useState(false); 2010 const [isMobileSearchOpen, setIsMobileSearchOpen] = reactExports.useState(false); 2011 const [activeIndex, setActiveIndex] = reactExports.useState(-1); 2012 const dropdownRef = reactExports.useRef(null); 2013 const [showUserDropdown, setShowUserDropdown] = reactExports.useState(false); 2014 const userDropdownRef = reactExports.useRef(null); 2015 reactExports.useEffect(() => { 2016 const handleScroll = (e) => { 2017 const target = e.target; 2018 if (target && target.tagName === "MAIN" && typeof target.scrollTop === "number") { 2019 setIsScrolled(target.scrollTop > 50); 2020 } 2021 }; 2022 window.addEventListener("scroll", handleScroll, { passive: true, capture: true }); 2023 return () => { 2024 window.removeEventListener("scroll", handleScroll, true); 2025 }; 2026 }, []); 2027 const isCollapsed = isScrolled && !(isSearchFocused || showDropdown || showNotifications || showUserDropdown); 2028 const isHoverExpanded = isCollapsed && isHovered; 2029 const debouncedQuery = useDebounce(searchQuery, 250); 2030 const unreadCount = notifications.filter((n) => !n.isRead).length; 2031 reactExports.useEffect(() => { 2032 const handleClickOutside = (event) => { 2033 if (notificationRef.current && !notificationRef.current.contains(event.target)) { 2034 setShowNotifications(false); 2035 } 2036 if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { 2037 setShowDropdown(false); 2038 } 2039 if (userDropdownRef.current && !userDropdownRef.current.contains(event.target)) { 2040 setShowUserDropdown(false); 2041 } 2042 }; 2043 document.addEventListener("mousedown", handleClickOutside); 2044 return () => document.removeEventListener("mousedown", handleClickOutside); 2045 }, []); 2046 reactExports.useEffect(() => { 2047 if (!debouncedQuery || debouncedQuery.trim().length < 2) { 2048 setSuggestions([]); 2049 setShowDropdown(false); 2050 return; 2051 } 2052 const fetchSuggestions = async () => {
2053 try { 2054 const results = await api.getSearchSuggestions(debouncedQuery); 2055 setSuggestions(results); 2056 if (document.activeElement === searchInputRef.current) { 2057 setShowDropdown(results.length > 0); 2058 } 2059 setActiveIndex(-1); 2060 } catch (err) { 2061 console.error("Failed to load search suggestions", err); 2062 } 2063 }; 2064 fetchSuggestions(); 2065 }, [debouncedQuery]); 2066 const handleSelectSuggestion = (item) => { 2067 var _a2; 2068 setSearchQuery(item.title); 2069 setShowDropdown(false); 2070 (_a2 = searchInputRef.current) == null ? void 0 : _a2.blur(); 2071 if (item.type === "TRACK") { 2072 const trackId = item.id.replace("track-", ""); 2073 if (onNavigate) { 2074 onNavigate("watch", trackId); 2075 } else { 2076 window.location.href = `/?v=${trackId}`; 2077 } 2078 } else if (item.type === "LYRIC") { 2079 const lyricId = item.id.replace("lyric-", ""); 2080 if (onNavigate) { 2081 onNavigate("lyrics", lyricId); 2082 } else { 2083 window.location.href = `/lyrics/${lyricId}`; 2084 } 2085 } else { 2086 if (onNavigate) { 2087 try { 2088 const url = new URL(item.target, window.location.origin); 2089 const view = url.pathname.replace(/^\/+/, "") || "home"; 2090 const search = url.search; 2091 onNavigate(view + search); 2092 } catch { 2093 window.location.href = item.target; 2094 } 2095 } else { 2096 window.location.href = item.target; 2097 } 2098 } 2099 }; 2100 const handleKeyDown = (e) => { 2101 if (!showDropdown || suggestions.length === 0) return; 2102 if (e.key === "ArrowDown") { 2103 e.preventDefault(); 2104 setActiveIndex((prev) => prev + 1 < suggestions.length ? prev + 1 : 0); 2105 } else if (e.key === "ArrowUp") { 2106 e.preventDefault(); 2107 setActiveIndex((prev) => prev - 1 >= 0 ? prev - 1 : suggestions.length - 1); 2108 } else if (e.key === "Enter") { 2109 if (activeIndex >= 0 && activeIndex < suggestions.length) { 2110 e.preventDefault(); 2111 handleSelectSuggestion(suggestions[activeIndex]); 2112 } 2113 } else if (e.key === "Escape") { 2114 setShowDropdown(false); 2115 } 2116 }; 2117 reactExports.useEffect(() => { 2118 const fetchDynamicLogo = async () => { 2119 try { 2120 const res = await fetch("/api/logo/current"); 2121 if (res.ok) { 2122 const data = await res.json(); 2123 if (data && data.imageUrl) {
2124 setActiveLogo(data.imageUrl); 2125 return; 2126 } 2127 } 2128 } catch (err) { 2129 console.error("Failed to fetch dynamic logo", err); 2130 } 2131 if (graphics.mainLogo) { 2132 setActiveLogo(graphics.mainLogo); 2133 } 2134 }; 2135 fetchDynamicLogo(); 2136 }, [graphics]); 2137 const highlightMatch = (text, query) => { 2138 if (!query) return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: text }); 2139 const parts = text.split(new RegExp(`(${query.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")})`, "gi")); 2140 return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: parts.map( 2141 (part, i) => part.toLowerCase() === query.toLowerCase() ? /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { className: "text-zinc-900 dark:text-white font-extrabold", children: part }, i) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: part }, i) 2142 ) }); 2143 }; 2144 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 2145 "header", 2146 { 2147 className: `absolute top-0 left-0 right-0 md:right-[14px] z-50 bg-white dark:bg-[#1a1a1a] border-b border-zinc-200 dark:border-zinc-800 h-16 transition-all duration-200 banner-collapsible ${isCollapsed ? "banner-collapsed" : ""} ${isHoverExpanded ? "banner-expanded-hover" : ""}`, 2148 onMouseEnter: () => setIsHovered(true), 2149 onMouseLeave: () => setIsHovered(false), 2150 children: [
2151 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "banner-inner-container px-4", children: [ 2152 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center gap-4 ${isMobileSearchOpen ? "hidden md:flex" : "flex"}`, children: [ 2153 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setIsSidebarOpen(!isSidebarOpen), className: "p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors text-zinc-700 dark:text-zinc-200", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Menu, { size: 24 }) }), 2154 /* @__PURE__ */ jsxRuntimeExports.jsx(HamieLogo, { onClick: handleGoHome, overrideSrc: activeLogo, href: "/" }) 2155 ] }), 2156 isMobileSearchOpen && /* @__PURE__ */ jsxRuntimeExports.jsx( 2157 "button", 2158 { 2159 className: "md:hidden p-2 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors mr-2", 2160 onClick: () => setIsMobileSearchOpen(false), 2161 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowLeft, { size: 20 }) 2162 } 2163 ),
2164 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `flex-1 max-w-2xl md:mx-8 ${isMobileSearchOpen ? "block" : "hidden md:block"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: (e) => { 2165 var _a2; 2166 setShowDropdown(false); 2167 if (isMobileSearchOpen) setIsMobileSearchOpen(false); 2168 (_a2 = searchInputRef.current) == null ? void 0 : _a2.blur(); 2169 handleSearch(e); 2170 }, className: "flex gap-2 w-full", children: [ 2171 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 relative", ref: dropdownRef, children: [ 2172 /* @__PURE__ */ jsxRuntimeExports.jsx( 2173 "input", 2174 { 2175 ref: searchInputRef, 2176 type: "text", 2177 placeholder: t("common.search", "Search archives..."), 2178 className: "w-full bg-zinc-100 dark:bg-zinc-900 border border-zinc-300 dark:border-zinc-700 rounded-md h-10 pl-10 pr-4 text-sm focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none transition-all", 2179 value: searchQuery, 2180 onChange: (e) => setSearchQuery(e.target.value), 2181 onKeyDown: handleKeyDown, 2182 onFocus: () => { 2183 setIsSearchFocused(true); 2184 if (suggestions.length > 0) setShowDropdown(true); 2185 }, 2186 onBlur: () => { 2187 setIsSearchFocused(false); 2188 } 2189 } 2190 ),
2191 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "submit", className: "absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-indigo-500 transition-colors", title: t("common.search", "Search"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { size: 18 }) }), 2192 searchQuery && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: () => { 2193 setSearchQuery(""); 2194 setSuggestions([]); 2195 setShowDropdown(false); 2196 }, className: "absolute right-3 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-600", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 16 }) }), 2197 showDropdown && suggestions.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-full left-0 right-0 mt-1 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-md shadow-xl overflow-hidden z-50 animate-in fade-in slide-in-from-top-1 duration-150", children: /* @__PURE__ */ jsxRuntimeExports.jsx("ul", { children: suggestions.map((item, index) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 2198 "li", 2199 { 2200 onClick: () => handleSelectSuggestion(item), 2201 className: `flex items-center justify-between px-4 py-2.5 cursor-pointer text-sm transition-all ${index === activeIndex ? "bg-zinc-100 dark:bg-zinc-800 text-indigo-600 dark:text-white" : "hover:bg-zinc-50 dark:hover:bg-zinc-800 text-zinc-700 dark:text-zinc-300"}`, 2202 children: [
2203 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [ 2204 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-800 dark:text-zinc-200", children: highlightMatch(item.title, searchQuery) }), 2205 item.subtitle && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400 font-normal", children: item.subtitle }) 2206 ] }), 2207 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-1.5 items-center shrink-0", children: item.types && item.types.length > 0 ? item.types.map((tLabel) => /* @__PURE__ */ jsxRuntimeExports.jsx( 2208 "span", 2209 { 2210 className: `text-[9px] font-extrabold uppercase tracking-wider px-1.5 py-0.5 rounded border ${tLabel === "MIDI" ? "bg-blue-50 dark:bg-blue-950/20 text-blue-600 dark:text-blue-400 border-blue-100 dark:border-blue-900/30" : "bg-purple-50 dark:bg-purple-950/20 text-purple-600 dark:text-purple-400 border-purple-100 dark:border-purple-900/30"}`, 2211 children: tLabel 2212 }, 2213 tLabel 2214 )) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded text-zinc-400 dark:text-zinc-500 bg-zinc-50 dark:bg-zinc-950 border border-zinc-100 dark:border-zinc-800/30", children: item.type }) }) 2215 ] 2216 }, 2217 item.id 2218 )) }) }) 2219 ] }),
2220 /* @__PURE__ */ jsxRuntimeExports.jsx( 2221 "button", 2222 { 2223 type: "button", 2224 onClick: onMelodySearchClick, 2225 className: "h-10 w-10 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 border border-zinc-300 dark:border-zinc-700 rounded-md flex items-center justify-center transition-colors text-zinc-600 dark:text-zinc-300", 2226 title: t("header.melodySearch", "Search by Melody"), 2227 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 18 }) 2228 } 2229 ) 2230 ] }) }), 2231 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `items-center gap-2 ${isMobileSearchOpen ? "hidden md:flex" : "flex"}`, children: [ 2232 /* @__PURE__ */ jsxRuntimeExports.jsx( 2233 "button", 2234 { 2235 className: "md:hidden p-2 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors flex items-center gap-2", 2236 onClick: () => { 2237 setIsMobileSearchOpen(true); 2238 setTimeout(() => { 2239 var _a2; 2240 return (_a2 = searchInputRef.current) == null ? void 0 : _a2.focus(); 2241 }, 10); 2242 }, 2243 title: t("common.search", "Search"), 2244 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { size: 20 }) 2245 } 2246 ),
2247 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2248 "button", 2249 { 2250 className: "p-2 text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors flex items-center gap-2", 2251 onClick: onUploadClick, 2252 title: t("common.upload", "Upload"), 2253 children: [ 2254 /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 20 }), 2255 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden lg:inline text-sm font-medium", children: t("common.upload", "Upload") }) 2256 ] 2257 } 2258 ), 2259 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", ref: notificationRef, children: [ 2260 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2261 "button", 2262 { 2263 className: `p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md relative transition-colors text-zinc-600 dark:text-zinc-300 ${showNotifications ? "bg-zinc-100 dark:bg-zinc-800" : ""}`, 2264 onClick: () => { 2265 if (!currentUser) { 2266 setIsAuthModalOpen(true); 2267 } else { 2268 setShowNotifications(!showNotifications); 2269 } 2270 }, 2271 children: [
2272 /* @__PURE__ */ jsxRuntimeExports.jsx(Bell, { size: 20 }), 2273 currentUser && unreadCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0.5 right-0.5 min-w-[18px] h-[18px] px-1 bg-red-500 text-white text-[10px] font-bold rounded-full border-2 border-white dark:border-[#1a1a1a] flex items-center justify-center leading-none", children: unreadCount > 9 ? "9+" : unreadCount }) 2274 ] 2275 } 2276 ), 2277 showNotifications && /* @__PURE__ */ jsxRuntimeExports.jsx( 2278 NotificationsDropdown, 2279 { 2280 notifications, 2281 onMarkAsRead: onMarkNotificationRead, 2282 onMarkAllRead: onMarkAllNotificationsRead, 2283 onSeeAll: onSeeAllNotifications, 2284 onClose: () => setShowNotifications(false), 2285 onNavigate: onNavigateNotification 2286 } 2287 ) 2288 ] }), 2289 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-6 w-px bg-zinc-300 dark:bg-zinc-700 mx-2" }), 2290 currentUser ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", ref: userDropdownRef, children: [
2291 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2292 "button", 2293 { 2294 onClick: () => setShowUserDropdown(!showUserDropdown), 2295 className: "flex items-center gap-3 cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800/50 p-1.5 rounded-lg transition-colors border-0 bg-transparent text-left", 2296 children: [ 2297 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative shrink-0", children: [ 2298 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: currentUser.avatar, name: currentUser.username, size: 32, className: "rounded-md" }), 2299 isPro(currentUser) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute -bottom-1 -right-1 text-white text-[7px] font-bold px-1 rounded-full border border-white dark:border-zinc-900 uppercase tracking-tighter ${((_a = currentUser.subscriptionTier) == null ? void 0 : _a.toLowerCase()) === "premium" ? "bg-indigo-600" : ((_b = currentUser.subscriptionTier) == null ? void 0 : _b.toLowerCase()) === "gold" ? "bg-amber-500" : "bg-zinc-500"}`, children: currentUser.role === "ADMIN" ? "ADMIN" : currentUser.subscriptionTier || "PRO" }) 2300 ] }),
2301 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "hidden lg:block text-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-zinc-900 dark:text-white leading-none", children: currentUser.username }) }) 2302 ] 2303 } 2304 ), 2305 showUserDropdown && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute right-0 mt-2 w-56 bg-white dark:bg-zinc-900 rounded-xl shadow-xl border border-zinc-200 dark:border-zinc-800 py-1.5 z-50 animate-in fade-in slide-in-from-top-2 duration-150", children: [ 2306 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-2 border-b border-zinc-100 dark:border-zinc-800", children: [ 2307 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-bold text-zinc-900 dark:text-white truncate", children: currentUser.username }), 2308 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-500 dark:text-zinc-400 truncate", children: currentUser.email }) 2309 ] }),
2310 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3 border-b border-zinc-100 dark:border-zinc-800 bg-amber-50/30 dark:bg-amber-950/10", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 2311 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-amber-600 dark:text-amber-500", children: [ 2312 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 16 }), 2313 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold text-sm", children: [ 2314 currentUser.credits || 0, 2315 " Credits" 2316 ] }) 2317 ] }), 2318 /* @__PURE__ */ jsxRuntimeExports.jsx( 2319 "a", 2320 { 2321 href: "/billing", 2322 className: "text-xs font-semibold text-indigo-600 hover:text-indigo-700 dark:text-indigo-400 dark:hover:text-indigo-300 transition-colors", 2323 onClick: (e) => { 2324 if (!e.metaKey && !e.ctrlKey && !e.shiftKey && e.button !== 1) { 2325 e.preventDefault(); 2326 setShowUserDropdown(false); 2327 onNavigate == null ? void 0 : onNavigate("billing"); 2328 } 2329 }, 2330 children: "Details" 2331 } 2332 ) 2333 ] }) }),
2334 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2335 "a", 2336 { 2337 href: "/personal-space", 2338 onClick: (e) => { 2339 if (!e.metaKey && !e.ctrlKey && !e.shiftKey && e.button !== 1) { 2340 e.preventDefault(); 2341 setShowUserDropdown(false); 2342 onPersonalSpaceClick(); 2343 } 2344 }, 2345 className: "flex items-center gap-3 px-4 py-2.5 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors", 2346 children: [ 2347 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 16, className: "text-indigo-500" }), 2348 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "My Personal Space" }) 2349 ] 2350 } 2351 ), 2352 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2353 "a", 2354 { 2355 href: "/account", 2356 onClick: (e) => { 2357 if (!e.metaKey && !e.ctrlKey && !e.shiftKey && e.button !== 1) { 2358 e.preventDefault(); 2359 setShowUserDropdown(false); 2360 onAccountClick(); 2361 } 2362 }, 2363 className: "flex items-center gap-3 px-4 py-2.5 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors", 2364 children: [
2365 /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 16, className: "text-zinc-400 dark:text-zinc-500" }), 2366 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Account Settings" }) 2367 ] 2368 } 2369 ), 2370 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2371 "a", 2372 { 2373 href: "/billing", 2374 onClick: (e) => { 2375 if (!e.metaKey && !e.ctrlKey && !e.shiftKey && e.button !== 1) { 2376 e.preventDefault(); 2377 setShowUserDropdown(false); 2378 onNavigate == null ? void 0 : onNavigate("billing"); 2379 } 2380 }, 2381 className: "flex items-center gap-3 px-4 py-2.5 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-colors", 2382 children: [ 2383 /* @__PURE__ */ jsxRuntimeExports.jsx(CreditCard, { size: 16, className: "text-indigo-500" }),
2384 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Support Us / Billing" }) 2385 ] 2386 } 2387 ), 2388 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-100 dark:border-zinc-800 my-1" }), 2389 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2390 "button", 2391 { 2392 onClick: () => { 2393 setShowUserDropdown(false); 2394 onLogout == null ? void 0 : onLogout(); 2395 }, 2396 className: "w-full flex items-center gap-3 px-4 py-2.5 text-sm text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-950/20 transition-colors text-left border-0 bg-transparent", 2397 children: [ 2398 /* @__PURE__ */ jsxRuntimeExports.jsx(LogOut, { size: 16 }), 2399 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Sign Out" }) 2400 ] 2401 } 2402 ) 2403 ] }) 2404 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs( 2405 "button", 2406 { 2407 onClick: () => setIsAuthModalOpen(true),
2408 className: "flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md text-sm font-bold transition-all shadow-sm", 2409 children: [ 2410 /* @__PURE__ */ jsxRuntimeExports.jsx(UserPlus, { size: 18 }), 2411 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("common.signIn", "Sign In / Join") }) 2412 ] 2413 } 2414 ) 2415 ] }) 2416 ] }), 2417 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "banner-trigger-tab", title: "Show header", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-1 bg-zinc-400 dark:bg-zinc-600 rounded-full" }) }) 2418 ] 2419 } 2420 ); 2421} 2422function BackToTop() { 2423 const [isVisible, setIsVisible] = reactExports.useState(false); 2424 reactExports.useEffect(() => { 2425 const handleScroll = (e) => { 2426 const target = e.target; 2427 if (target && target.tagName === "MAIN" && typeof target.scrollTop === "number") { 2428 setIsVisible(target.scrollTop > 300); 2429 } 2430 }; 2431 window.addEventListener("scroll", handleScroll, true); 2432 return () => window.removeEventListener("scroll", handleScroll, true); 2433 }, []); 2434 const scrollToTop = () => { 2435 const main = document.getElementById("main-scroll-container"); 2436 if (main) { 2437 main.scrollTo({ top: 0, behavior: "smooth" }); 2438 } else { 2439 window.scrollTo({ top: 0, behavior: "smooth" }); 2440 } 2441 }; 2442 if (!isVisible) return null; 2443 return /* @__PURE__ */ jsxRuntimeExports.jsx( 2444 "button", 2445 { 2446 onClick: scrollToTop, 2447 className: "fixed bottom-[100px] md:bottom-10 right-6 z-[60] p-3 rounded-full bg-indigo-600 text-white shadow-lg shadow-indigo-600/30 hover:bg-indigo-700 hover:scale-110 transition-all duration-300 animate-in fade-in slide-in-from-bottom-4 group", 2448 title: "Back to top", 2449 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowUp, { size: 24, className: "group-hover:-translate-y-0.5 transition-transform" }) 2450 } 2451 ); 2452} 2453const LanguagePickerModal = ({ isOpen, onClose, availableCodes }) => { 2454 const { t } = useTranslation(); 2455 const { language, setLanguage } = useSettings(); 2456 const [search, setSearch] = reactExports.useState(""); 2457 const handleSelect = (code) => { 2458 setLanguage(code); 2459 onClose(); 2460 }; 2461 const filteredRegions = reactExports.useMemo(() => { 2462 const query = search.toLowerCase().trim(); 2463 if (!query) return LANGUAGE_REGIONS; 2464 const result = {}; 2465 for (const [region, codes] of Object.entries(LANGUAGE_REGIONS)) { 2466 const matchedCodes = codes.filter((c) => { 2467 if (!availableCodes.includes(c)) return false; 2468 const nativeName = NATIVE_LANGUAGE_NAMES[c] || c; 2469 return nativeName.toLowerCase().includes(query) || c.toLowerCase().includes(query); 2470 }); 2471 if (matchedCodes.length > 0) { 2472 result[region] = matchedCodes; 2473 } 2474 } 2475 return result; 2476 }, [search, availableCodes]); 2477 if (!isOpen) return null; 2478 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 w-full max-w-4xl max-h-[85vh] rounded-2xl shadow-2xl flex flex-col border border-zinc-200 dark:border-zinc-800", children: [ 2479 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-4 border-b border-zinc-100 dark:border-zinc-800", children: [
2480 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 2481 /* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { className: "text-zinc-500", size: 24 }), 2482 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-zinc-800 dark:text-zinc-100", children: t("language.selectLanguage", "Select Language") }) 2483 ] }), 2484 /* @__PURE__ */ jsxRuntimeExports.jsx( 2485 "button", 2486 { 2487 onClick: onClose, 2488 className: "p-2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-colors", 2489 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) 2490 } 2491 ) 2492 ] }), 2493 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 border-b border-zinc-100 dark:border-zinc-800", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 2494 /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400", size: 18 }),
2495 /* @__PURE__ */ jsxRuntimeExports.jsx( 2496 "input", 2497 { 2498 type: "text", 2499 placeholder: t("language.search", "Search languages..."), 2500 value: search, 2501 onChange: (e) => setSearch(e.target.value), 2502 className: "w-full pl-10 pr-4 py-3 bg-zinc-100 dark:bg-zinc-800 border-none rounded-xl text-zinc-800 dark:text-zinc-200 focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 outline-none", 2503 autoFocus: true 2504 } 2505 ) 2506 ] }) }), 2507 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-4 custom-scrollbar", children: [ 2508 Object.entries(filteredRegions).map(([region, codes]) => { 2509 const validCodes = codes.filter((c) => availableCodes.includes(c)); 2510 if (validCodes.length === 0) return null; 2511 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-8 last:mb-0", children: [ 2512 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-sm font-semibold text-zinc-400 dark:text-zinc-500 uppercase tracking-wider mb-3 px-2", children: region }),
2513 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-2", children: validCodes.map((code) => { 2514 const isSelected = language === code; 2515 const nativeName = NATIVE_LANGUAGE_NAMES[code] || code; 2516 return /* @__PURE__ */ jsxRuntimeExports.jsx( 2517 "button", 2518 { 2519 onClick: () => handleSelect(code), 2520 className: `flex flex-col items-start p-3 rounded-xl text-left transition-colors 2521 ${isSelected ? "bg-blue-50 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 border border-blue-200 dark:border-blue-800" : "hover:bg-zinc-100 dark:hover:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-transparent"}`, 2522 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-[15px]", children: nativeName }) 2523 }, 2524 code 2525 ); 2526 }) }) 2527 ] }, region); 2528 }), 2529 Object.keys(filteredRegions).length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "py-12 text-center text-zinc-500", children: t("language.noResults", "No languages found") }) 2530 ] }) 2531 ] }) }); 2532}; 2533const LanguageRegionSelector = () => { 2534 const { i18n, t } = useTranslation(); 2535 const { language, setLanguage } = useSettings(); 2536 const [locales, setLocales] = reactExports.useState([]); 2537 const [isModalOpen, setIsModalOpen] = reactExports.useState(false); 2538 reactExports.useEffect(() => { 2539 const fetchData = async () => { 2540 try { 2541 const localesData = await api.getLocales(); 2542 setLocales(localesData.filter((l) => l.isActive)); 2543 } catch (error) { 2544 console.error("Failed to fetch locales", error); 2545 } 2546 }; 2547 fetchData(); 2548 }, []); 2549 const handleLanguageChange = (val) => { 2550 if (val === "MORE") { 2551 setIsModalOpen(true); 2552 return; 2553 } 2554 setLanguage(val); 2555 }; 2556 const activeLocales = locales.length > 0 ? locales.map((l) => l.code) : Object.keys(NATIVE_LANGUAGE_NAMES); 2557 const topLanguages = ["en", "zh-CN", "zh-TW", "es", "fr", "de", "ja", "pt", "ru", "ko"]; 2558 const popularOptions = topLanguages.filter((code) => activeLocales.includes(code)); 2559 if (!popularOptions.includes(language) && activeLocales.includes(language)) { 2560 popularOptions.unshift(language); 2561 } 2562 return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 2563 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col md:flex-row gap-4 items-center text-sm text-zinc-600 dark:text-zinc-400", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 bg-zinc-100 dark:bg-zinc-800 px-3 py-1.5 rounded-full border border-zinc-200 dark:border-zinc-700", children: [
2564 /* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { size: 14, className: "text-zinc-400" }), 2565 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2566 "select", 2567 { 2568 value: language, 2569 onChange: (e) => handleLanguageChange(e.target.value), 2570 className: "bg-transparent border-none focus:ring-0 cursor-pointer outline-none font-medium text-zinc-800 dark:text-zinc-200", 2571 children: [ 2572 popularOptions.map((code) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: code, className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850", children: NATIVE_LANGUAGE_NAMES[code] || code }, code)), 2573 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "MORE", className: "text-zinc-800 dark:text-zinc-200 bg-white dark:bg-zinc-850 font-bold", children: t("language.more", "More languages...") }) 2574 ] 2575 } 2576 ) 2577 ] }) }), 2578 /* @__PURE__ */ jsxRuntimeExports.jsx( 2579 LanguagePickerModal, 2580 { 2581 isOpen: isModalOpen, 2582 onClose: () => setIsModalOpen(false), 2583 availableCodes: activeLocales 2584 } 2585 ) 2586 ] }); 2587}; 2588const Footer = () => { 2589 const { t } = useTranslation(); 2590 const currentYear = (/* @__PURE__ */ new Date()).getFullYear(); 2591 const [versionDate, setVersionDate] = reactExports.useState(null); 2592 reactExports.useEffect(() => { 2593 api.getAppVersionDate().then((data) => { 2594 if (data && data.versionDate) { 2595 const d = new Date(data.versionDate); 2596 if (!isNaN(d.getTime())) { 2597 const pad = (n) => String(n).padStart(2, "0"); 2598 const formatted = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; 2599 setVersionDate(formatted); 2600 } else { 2601 setVersionDate(data.versionDate); 2602 } 2603 } 2604 }).catch((err) => { 2605 console.warn("Failed to fetch version date:", err); 2606 }); 2607 }, []); 2608 return /* @__PURE__ */ jsxRuntimeExports.jsx("footer", { className: "bg-zinc-50 dark:bg-[#0a0a0a] border-t border-zinc-200 dark:border-zinc-800 py-8 px-6 mt-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-w-7xl mx-auto flex flex-col md:flex-row justify-between items-center gap-6", children: [ 2609 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2 text-center md:text-left max-w-md", children: [
2610 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-indigo-600 dark:text-indigo-400", children: "HamieNET" }), 2611 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] leading-relaxed text-zinc-500 dark:text-zinc-600 uppercase tracking-tight font-medium", children: [ 2612 "Copyright © 1999-", 2613 currentYear, 2614 " HamieNET.com. Certain Data Copyright © 2002-", 2615 currentYear, 2616 " Open Educational Music Library. For Educational Use Only. All Rights Reserved. Works, Contents, And Titles Are Property Of Their Respective Owners. Access or Use of This Site Signifies Your Acceptance of the ", 2617 /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/terms", className: "underline hover:text-indigo-500", children: "Terms and Conditions" }), 2618 " and ", 2619 /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/privacy", className: "underline hover:text-indigo-500", children: "Privacy Information" }), 2620 ". For DMCA or copyright concerns, please ",
2621 /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/dmca", className: "underline hover:text-indigo-500", children: "file an official takedown request" }), 2622 "." 2623 ] }) 2624 ] }), 2625 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center md:items-end gap-4", children: [ 2626 /* @__PURE__ */ jsxRuntimeExports.jsx(LanguageRegionSelector, {}), 2627 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-6 text-xs font-medium text-zinc-500 dark:text-zinc-500", children: [ 2628 /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/help", className: "hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors", children: t("footer.help", "Help") }), 2629 /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/terms", className: "hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors", children: t("footer.terms", "Terms") }), 2630 /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: "/privacy", className: "hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors", children: t("footer.privacy", "Privacy") }) 2631 ] }), 2632 versionDate && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] text-zinc-400 dark:text-zinc-600 font-mono tracking-tight text-center md:text-right mt-1", children: [ 2633 "Latest Edit: ", 2634 versionDate 2635 ] }) 2636 ] }) 2637 ] }) }); 2638}; 2639const GlobalSettingsModal = ({ isOpen, onClose, theme, toggleTheme, isAdmin }) => { 2640 const { t } = useTranslation(); 2641 const { 2642 language, 2643 setLanguage, 2644 currency, 2645 setCurrency, 2646 availableLanguages, 2647 uiConfig, 2648 autoplay, 2649 setAutoplay 2650 } = useSettings(); 2651 if (!isOpen) return null; 2652 const getThemeIcon = (t2) => { 2653 if (t2 === "dark") return /* @__PURE__ */ jsxRuntimeExports.jsx(Moon, { size: 18 }); 2654 if (t2 === "fun") return /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { size: 18 }); 2655 if (t2 === "retro") return /* @__PURE__ */ jsxRuntimeExports.jsx(Tv, { size: 18 }); 2656 return /* @__PURE__ */ jsxRuntimeExports.jsx(Sun, { size: 18 }); 2657 }; 2658 const activeModes = ["light", "dark", "fun"]; 2659 if (isAdmin) { 2660 activeModes.push("retro"); 2661 } 2662 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[200] flex items-center justify-center p-4", children: [ 2663 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/60 backdrop-blur-sm", onClick: onClose }), 2664 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-md bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-zinc-200 dark:border-zinc-800 flex flex-col overflow-hidden animate-in zoom-in-95", children: [
2665 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center bg-zinc-50 dark:bg-zinc-950", children: [ 2666 /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 2667 /* @__PURE__ */ jsxRuntimeExports.jsx(Monitor, { className: "text-indigo-500", size: 20 }), 2668 t("nav.settings", "Settings") 2669 ] }), 2670 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "p-2 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full text-zinc-500 dark:text-zinc-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) }) 2671 ] }), 2672 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 space-y-6 max-h-[70vh] overflow-y-auto custom-scrollbar", children: [ 2673 uiConfig.enableAppearanceSelector && /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { children: [ 2674 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xs font-bold text-zinc-500 uppercase mb-3 flex items-center gap-2", children: t("header.appearance", "Appearance") }),
2675 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `grid gap-2 ${activeModes.length === 4 ? "grid-cols-4" : "grid-cols-3"}`, children: activeModes.map((mode) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 2676 "button", 2677 { 2678 onClick: () => { 2679 if (theme !== mode) toggleTheme(mode); 2680 }, 2681 className: `flex flex-col items-center justify-center p-3 rounded-xl border-2 transition-all ${theme === mode ? "border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300" : "border-transparent bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-200 dark:hover:bg-zinc-700"}`, 2682 children: [ 2683 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-2", children: getThemeIcon(mode) }), 2684 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold capitalize", children: mode }) 2685 ] 2686 }, 2687 mode 2688 )) }) 2689 ] }),
2690 /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { children: [ 2691 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xs font-bold text-zinc-500 uppercase mb-3 flex items-center gap-2", children: t("player.settings", "Playback") }), 2692 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between p-3 bg-zinc-50 dark:bg-zinc-800/50 rounded-xl border border-zinc-100 dark:border-zinc-800", children: [ 2693 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 2694 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-2 bg-zinc-200 dark:bg-zinc-700 rounded-lg text-zinc-700 dark:text-zinc-300", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CirclePlay, { size: 18 }) }), 2695 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 2696 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-bold text-zinc-900 dark:text-white", children: t("player.autoplay", "Autoplay") }),
2697 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-500", children: "Auto-play videos on load" }) 2698 ] }) 2699 ] }), 2700 /* @__PURE__ */ jsxRuntimeExports.jsx( 2701 "button", 2702 { 2703 onClick: () => setAutoplay(!autoplay), 2704 className: `relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${autoplay ? "bg-indigo-600" : "bg-zinc-300 dark:bg-zinc-600"}`, 2705 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `inline-block h-4 w-4 transform rounded-full bg-white transition duration-200 ease-in-out ${autoplay ? "translate-x-6" : "translate-x-1"}` }) 2706 } 2707 ) 2708 ] }) 2709 ] }), 2710 uiConfig.enableLanguageSelector && /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { children: [ 2711 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-xs font-bold text-zinc-500 uppercase mb-3 flex items-center gap-2", children: [
2712 /* @__PURE__ */ jsxRuntimeExports.jsx(Globe, { size: 14 }), 2713 " ", 2714 t("header.language", "Language") 2715 ] }), 2716 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-2 gap-2", children: availableLanguages.map((lang) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 2717 "button", 2718 { 2719 onClick: () => setLanguage(lang.code), 2720 className: `flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium transition-colors ${language === lang.code ? "bg-indigo-600 text-white shadow-md" : "bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700"}`, 2721 children: [ 2722 lang.name, 2723 language === lang.code && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14 }) 2724 ] 2725 }, 2726 lang.code 2727 )) }) 2728 ] }), 2729 uiConfig.enableCurrencySelector && /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { children: [
2730 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-xs font-bold text-zinc-500 uppercase mb-3 flex items-center gap-2", children: [ 2731 /* @__PURE__ */ jsxRuntimeExports.jsx(Coins, { size: 14 }), 2732 " ", 2733 t("header.currency", "Currency") 2734 ] }), 2735 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "grid grid-cols-4 gap-2", children: ["USD", "JPY", "EUR", "GBP"].map((curr) => /* @__PURE__ */ jsxRuntimeExports.jsx( 2736 "button", 2737 { 2738 onClick: () => setCurrency(curr), 2739 className: `px-2 py-2 rounded-lg text-sm font-bold text-center transition-colors ${currency === curr ? "bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-700" : "bg-zinc-50 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-700"}`, 2740 children: curr 2741 }, 2742 curr 2743 )) }) 2744 ] }) 2745 ] }) 2746 ] }) 2747 ] }); 2748}; 2749function BugReportModal({ isOpen, onClose, currentUser }) { 2750 const { t } = useTranslation(); 2751 const [subject, setSubject] = reactExports.useState(""); 2752 const [description, setDescription] = reactExports.useState(""); 2753 const [isSubmitting, setIsSubmitting] = reactExports.useState(false); 2754 const [isSuccess, setIsSuccess] = reactExports.useState(false); 2755 const [error, setError] = reactExports.useState(""); 2756 const [honeypot, setHoneypot] = reactExports.useState(""); 2757 const [startTime] = reactExports.useState(Date.now()); 2758 const [hasInteracted, setHasInteracted] = reactExports.useState(false); 2759 if (!isOpen) return null; 2760 const handleSubmit = async (e) => { 2761 e.preventDefault(); 2762 setIsSubmitting(true); 2763 setError(""); 2764 if (honeypot) { 2765 console.warn("Spam detected via honeypot"); 2766 onClose(); 2767 return; 2768 } 2769 const duration = (Date.now() - startTime) / 1e3; 2770 if (duration < 4) { 2771 setError(t("bugReport.errorTooFast", "Please take a moment to fill out the form (Too fast).")); 2772 setIsSubmitting(false); 2773 return; 2774 } 2775 if (!hasInteracted) { 2776 setError(t("bugReport.errorInteract", "Please interact with the form before submitting.")); 2777 setIsSubmitting(false); 2778 return; 2779 } 2780 try { 2781 await api.createTicket( 2782 currentUser.id, 2783 `[Bug] ${subject}`, 2784 "bug_report", 2785 "high", 2786 description, 2787 honeypot 2788 ); 2789 setIsSuccess(true); 2790 setTimeout(() => { 2791 setIsSuccess(false); 2792 setSubject(""); 2793 setDescription(""); 2794 onClose(); 2795 }, 3e3); 2796 } catch (err) { 2797 setError(err.message || t("common.error", "An error occurred")); 2798 } finally { 2799 setIsSubmitting(false); 2800 } 2801 }; 2802 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[1000] flex items-center justify-center p-4", children: [ 2803 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/60 dark:bg-black/80 backdrop-blur-md transition-opacity animate-in fade-in duration-300", onClick: onClose }), 2804 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-lg bg-white/95 dark:bg-zinc-900/90 rounded-2xl shadow-2xl p-6 border border-zinc-200/50 dark:border-zinc-800/50 backdrop-blur-xl animate-in zoom-in-95 duration-200 overflow-hidden", children: [
2805 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -top-12 -left-12 w-32 h-32 bg-rose-500/10 dark:bg-rose-500/5 rounded-full blur-2xl pointer-events-none" }), 2806 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -bottom-12 -right-12 w-32 h-32 bg-orange-500/10 dark:bg-orange-500/5 rounded-full blur-2xl pointer-events-none" }), 2807 /* @__PURE__ */ jsxRuntimeExports.jsx( 2808 "button", 2809 { 2810 onClick: onClose, 2811 className: "absolute top-4 right-4 p-2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-all duration-200 hover:rotate-90", 2812 "aria-label": t("common.close", "Close"), 2813 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 18 }) 2814 } 2815 ), 2816 isSuccess ? ( 2817 /* Beautiful Success Screen with Animation */ 2818 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "py-8 flex flex-col items-center text-center animate-in fade-in zoom-in-95 duration-300", children: [
2819 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-16 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-full flex items-center justify-center text-emerald-500 dark:text-emerald-400 mb-6 shadow-lg shadow-emerald-500/10 animate-bounce", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheckBig, { size: 32 }) }), 2820 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-2xl font-black text-zinc-900 dark:text-white mb-2", children: t("bugReport.successTitle", "Securely Transmitted") }), 2821 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-600 dark:text-zinc-400 max-w-sm text-sm", children: t("bugReport.successMessage", "Thank you for helping us improve HamieNET! Your bug report has been securely registered in our system.") }) 2822 ] }) 2823 ) : ( 2824 /* Form Screen */ 2825 /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 2826 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3.5 mb-5", children: [
2827 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 bg-rose-50 dark:bg-rose-950/30 rounded-xl text-rose-500 dark:text-rose-400 border border-rose-100 dark:border-rose-900/20 shadow-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Bug, { size: 24, className: "animate-pulse" }) }), 2828 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 2829 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-black text-zinc-900 dark:text-white tracking-tight", children: t("bugReport.title", "Report a Bug") }), 2830 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400", children: t("bugReport.subtitle", "Submit system issue notification") }) 2831 ] }) 2832 ] }), 2833 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-5 p-3.5 bg-rose-500/10 dark:bg-rose-500/5 border border-rose-500/20 rounded-xl text-xs text-rose-700 dark:text-rose-300 leading-relaxed", children: [ 2834 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "font-bold mb-0.5 flex items-center gap-1.5 text-rose-800 dark:text-rose-400", children: [
2835 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 14 }), 2836 t("bugReport.disclaimerTitle", "Silent Notification Only") 2837 ] }), 2838 t("bugReport.disclaimer", "Bug reports are dispatched directly to the system administrators. Because these are recorded as background notifications to safeguard your dashboard, you will not receive confirmation emails or replies.") 2839 ] }), 2840 error && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center gap-2 text-xs text-red-700 dark:text-red-300 animate-shake", children: [ 2841 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 15 }), 2842 " ", 2843 error 2844 ] }), 2845 /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleSubmit, className: "space-y-4", children: [ 2846 /* @__PURE__ */ jsxRuntimeExports.jsx( 2847 "input", 2848 { 2849 type: "text", 2850 name: "form_honeypot_field", 2851 value: honeypot, 2852 onChange: (e) => setHoneypot(e.target.value), 2853 className: "hidden", 2854 autoComplete: "off", 2855 tabIndex: -1 2856 } 2857 ),
2858 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 2859 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-black text-zinc-600 dark:text-zinc-400 uppercase tracking-wider mb-1.5", children: t("bugReport.subjectLabel", "What went wrong?") }), 2860 /* @__PURE__ */ jsxRuntimeExports.jsx( 2861 "input", 2862 { 2863 required: true, 2864 type: "text", 2865 value: subject, 2866 onChange: (e) => { 2867 setSubject(e.target.value); 2868 setHasInteracted(true); 2869 }, 2870 onFocus: () => setHasInteracted(true), 2871 className: "w-full bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-rose-500/40 focus:border-rose-500 text-zinc-900 dark:text-white transition-all text-sm shadow-inner", 2872 placeholder: t("bugReport.subjectPlaceholder", "E.g., soundfont playback cuts off on track 3") 2873 } 2874 ) 2875 ] }),
2876 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 2877 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-black text-zinc-600 dark:text-zinc-400 uppercase tracking-wider mb-1.5", children: t("bugReport.descriptionLabel", "Steps to Reproduce / Description") }), 2878 /* @__PURE__ */ jsxRuntimeExports.jsx( 2879 "textarea", 2880 { 2881 required: true, 2882 value: description, 2883 onChange: (e) => { 2884 setDescription(e.target.value); 2885 setHasInteracted(true); 2886 }, 2887 onFocus: () => setHasInteracted(true), 2888 className: "w-full h-36 bg-zinc-50 dark:bg-zinc-950 border border-zinc-200 dark:border-zinc-800 rounded-xl px-4 py-3 outline-none focus:ring-2 focus:ring-rose-500/40 focus:border-rose-500 text-zinc-900 dark:text-white resize-none transition-all text-sm shadow-inner", 2889 placeholder: t("bugReport.descriptionPlaceholder", "Please describe the bug, how to reproduce it, or any details about your browser/device...") 2890 } 2891 ) 2892 ] }),
2893 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-3 pt-4 border-t border-zinc-200/50 dark:border-zinc-800/50 mt-2", children: [ 2894 /* @__PURE__ */ jsxRuntimeExports.jsx( 2895 "button", 2896 { 2897 type: "button", 2898 onClick: onClose, 2899 className: "px-5 py-2.5 rounded-xl font-bold text-sm text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors", 2900 children: t("common.cancel", "Cancel") 2901 } 2902 ), 2903 /* @__PURE__ */ jsxRuntimeExports.jsx( 2904 "button", 2905 { 2906 type: "submit", 2907 disabled: isSubmitting, 2908 className: "px-5 py-2.5 bg-gradient-to-r from-rose-500 to-rose-600 hover:from-rose-600 hover:to-rose-700 text-white rounded-xl font-bold text-sm flex items-center gap-2 transition-all shadow-lg shadow-rose-500/25 active:scale-95 disabled:opacity-50", 2909 children: isSubmitting ? t("bugReport.submitting", "Transmitting...") : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
2910 /* @__PURE__ */ jsxRuntimeExports.jsx(Send, { size: 15 }), 2911 t("bugReport.submit", "Transmit Bug Report") 2912 ] }) 2913 } 2914 ) 2915 ] }) 2916 ] }) 2917 ] }) 2918 ) 2919 ] }) 2920 ] }); 2921} 2922const ICON_MAP = { 2923 Home: House, 2924 Compass, 2925 Library, 2926 History, 2927 ListMusic, 2928 Clock, 2929 ThumbsUp, 2930 Shield, 2931 FileAudio: FileHeadphone, 2932 Layout: PanelsTopLeft, 2933 Star, 2934 Heart, 2935 Mail, 2936 Info, 2937 Tv, 2938 Hash, 2939 Monitor, 2940 Box, 2941 Settings, 2942 HelpCircle: CircleQuestionMark, 2943 MessageCircle, 2944 ChevronRight, 2945 Link, 2946 Music, 2947 Flask: FlaskConical 2948}; 2949const SidebarItem = ({ icon: Icon, label, active = false, onClick, badge, href, target, collapsed }) => { 2950 const content = /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 2951 /* @__PURE__ */ jsxRuntimeExports.jsx(Icon, { size: 20, className: `shrink-0 ${active ? "fill-current" : ""}` }), 2952 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-sm font-medium truncate transition-all duration-300 ease-in-out ${collapsed ? "w-0 opacity-0 hidden" : "w-auto opacity-100 flex-1 ml-3"}`, children: label }), 2953 !collapsed && badge && badge > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "bg-red-500 text-white text-[10px] font-bold px-2 py-0.5 rounded-full animate-in fade-in zoom-in duration-300", children: badge }), 2954 !collapsed && target === "_blank" && /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { size: 12, className: "opacity-50 ml-2" }) 2955 ] }); 2956 const className = `flex items-center px-4 py-3 cursor-pointer transition-all duration-200 border-l-4 group 2957 ${active ? "border-indigo-500 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-700 dark:text-indigo-300" : "border-transparent text-zinc-600 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800 hover:text-zinc-900 dark:hover:text-zinc-200"} 2958 ${collapsed ? "justify-center px-2" : ""} 2959 `; 2960 if (href) { 2961 return /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href, target, onClick: (e) => { 2962 if (target !== "_blank" && !e.metaKey && !e.ctrlKey && !e.shiftKey && e.button !== 1) { 2963 e.preventDefault(); 2964 onClick && onClick(); 2965 } 2966 }, className, title: collapsed ? label : void 0, children: content }); 2967 } 2968 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { onClick, className, title: collapsed ? label : void 0, children: content }); 2969}; 2970const DEFAULT_SECTIONS = [ 2971 { 2972 id: 1, 2973 title: "Main", 2974 order: 0, 2975 roles: null, 2976 isPublic: true, 2977 links: [ 2978 { id: 1, sectionId: 1, label: "Home", path: "/", icon: "Home", order: 0, roles: null, isPublic: true }, 2979 { id: 2, sectionId: 1, label: "Explore", path: "/explore", icon: "Compass", order: 1, roles: null, isPublic: true }, 2980 { id: 3, sectionId: 1, label: "Melody Search", path: "/melody-search", icon: "Music", order: 2, roles: null, isPublic: true }, 2981 { id: 4, sectionId: 1, label: "Subscriptions", path: "/subscriptions", icon: "Library", order: 3, roles: null, isPublic: true } 2982 ] 2983 }, 2984 { 2985 id: 2, 2986 title: "Library", 2987 order: 1, 2988 roles: null, 2989 isPublic: true, 2990 links: [ 2991 { id: 5, sectionId: 2, label: "History", path: "/history", icon: "History", order: 0, roles: null, isPublic: true }, 2992 { id: 6, sectionId: 2, label: "Playlists", path: "/playlists", icon: "ListMusic", order: 1, roles: null, isPublic: true }, 2993 { id: 7, sectionId: 2, label: "Watch Later", path: "/watchLater", icon: "Clock", order: 2, roles: null, isPublic: true }, 2994 { id: 8, sectionId: 2, label: "Liked Tracks", path: "/liked", icon: "ThumbsUp", order: 3, roles: null, isPublic: true }, 2995 { id: 9, sectionId: 2, label: "Lyrics", path: "/lyrics/search", icon: "Music", order: 4, roles: null, isPublic: true } 2996 ] 2997 }, 2998 { 2999 id: 3, 3000 title: "Tools", 3001 order: 2, 3002 roles: null, 3003 isPublic: true, 3004 links: [ 3005 { id: 10, sectionId: 3, label: "Hamie Lab", path: "/lab", icon: "Flask", order: 0, roles: null, isPublic: true }, 3006 { id: 11, sectionId: 3, label: "Help & Support", path: "/help", icon: "HelpCircle", order: 1, roles: null, isPublic: true } 3007 ] 3008 } 3009]; 3010const Sidebar = ({ 3011 isSidebarOpen, 3012 setIsSidebarOpen, 3013 activeView, 3014 setActiveView, 3015 activeTrack, 3016 setActiveTrack, 3017 currentUser, 3018 setIsAuthModalOpen, 3019 subscriptions, 3020 handleChannelSelect, 3021 handleGoHome, 3022 onAdminClick, 3023 siteModules = [], 3024 unreadMessageCount = 0, 3025 theme, 3026 toggleTheme, 3027 isMediaPage = false 3028}) => { 3029 var _a; 3030 const { t } = useTranslation(); 3031 const [isSettingsOpen, setIsSettingsOpen] = reactExports.useState(false); 3032 const [isBugReportOpen, setIsBugReportOpen] = reactExports.useState(false); 3033 const [navSections, setNavSections] = reactExports.useState(DEFAULT_SECTIONS); 3034 const { staffModeEnabled, isSuperAdmin, toggleStaffMode } = useAdmin(); 3035 const [isHovered, setIsHovered] = reactExports.useState(false); 3036 reactExports.useEffect(() => { 3037 api.getNavigation().then((sections) => { 3038 if (sections && sections.length > 0) { 3039 setNavSections(sections); 3040 } 3041 }).catch(() => { 3042 }); 3043 }, []); 3044 const isDesktopExpanded = isMediaPage ? true : isSidebarOpen || isHovered; 3045 const isCollapsed = !isDesktopExpanded; 3046 const handleMouseEnter = () => { 3047 if (!isMediaPage && !isSidebarOpen) { 3048 setIsHovered(true); 3049 } 3050 }; 3051 const handleMouseLeave = () => { 3052 if (!isMediaPage && !isSidebarOpen) { 3053 setIsHovered(false); 3054 } 3055 }; 3056 const canShow = (roles) => { 3057 if (!roles || roles.length === 0) return true; 3058 if (!currentUser) return roles.includes("GUEST"); 3059 return roles.includes(currentUser.role); 3060 }; 3061 const renderLink = (link) => {
3062 if (!canShow(link.roles)) return null; 3063 const Icon = link.icon && ICON_MAP[link.icon] ? ICON_MAP[link.icon] : Link; 3064 let isActive = false; 3065 if (link.path === "/") isActive = activeView === "home"; 3066 else isActive = activeView === link.path.substring(1); 3067 return /* @__PURE__ */ jsxRuntimeExports.jsx( 3068 SidebarItem, 3069 { 3070 icon: Icon, 3071 label: t(`nav.link.${link.label}`, link.label), 3072 active: isActive, 3073 onClick: () => { 3074 if (link.path === "/") { 3075 handleGoHome(); 3076 } else if (link.path.startsWith("/")) { 3077 const view = link.path.substring(1); 3078 setActiveView(view); 3079 } 3080 if (window.innerWidth < 1024 || isMediaPage) { 3081 setIsSidebarOpen(false); 3082 } 3083 }, 3084 href: link.path, 3085 target: link.target, 3086 collapsed: isCollapsed 3087 }, 3088 link.id 3089 ); 3090 }; 3091 return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 3092 /* @__PURE__ */ jsxRuntimeExports.jsx( 3093 GlobalSettingsModal, 3094 { 3095 isOpen: isSettingsOpen, 3096 onClose: () => setIsSettingsOpen(false), 3097 theme, 3098 toggleTheme, 3099 isAdmin: (currentUser == null ? void 0 : currentUser.role) === "ADMIN" 3100 } 3101 ), 3102 currentUser && /* @__PURE__ */ jsxRuntimeExports.jsx( 3103 BugReportModal, 3104 { 3105 isOpen: isBugReportOpen, 3106 onClose: () => setIsBugReportOpen(false), 3107 currentUser 3108 } 3109 ), 3110 isMediaPage && isSidebarOpen && /* @__PURE__ */ jsxRuntimeExports.jsx( 3111 "div", 3112 { 3113 className: "fixed inset-0 bg-black/50 z-30 lg:z-30 transition-opacity duration-300", 3114 onClick: () => setIsSidebarOpen(false) 3115 } 3116 ), 3117 /* @__PURE__ */ jsxRuntimeExports.jsx( 3118 "aside", 3119 { 3120 className: ` 3121 fixed inset-y-0 left-0 z-40 bg-white dark:bg-[#1a1a1a] border-r border-zinc-200 dark:border-zinc-800 3122 flex flex-col pt-16 overflow-hidden 3123 transition-all duration-300 ease-in-out 3124 ${isMediaPage ? ( 3125 // Media Page Styles (Overlay Drawer) 3126 (isSidebarOpen ? "translate-x-0 shadow-2xl" : "-translate-x-full") + " w-64" 3127 ) : ( 3128 // General Page Styles (Collapsible Sticky) 3129 `lg:sticky lg:top-0 lg:h-full lg:translate-x-0 3130 ${isDesktopExpanded ? "lg:w-64 lg:shadow-xl" : "lg:w-[72px] lg:shadow-none"} 3131 ${isSidebarOpen ? "translate-x-0" : "-translate-x-full"} 3132 w-64` 3133 )} 3134 `, 3135 onMouseEnter: handleMouseEnter, 3136 onMouseLeave: handleMouseLeave, 3137 children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 3138 "div", 3139 { 3140 className: "flex-1 overflow-y-auto custom-scrollbar py-4 overflow-x-hidden", 3141 style: { paddingBottom: activeTrack ? "96px" : "8px" }, 3142 children: [ 3143 navSections.map((section) => { 3144 if (!canShow(section.roles)) return null; 3145 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-6", children: [ 3146 section.title && section.title !== "Main" && /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: `px-6 mb-2 text-xs font-bold text-zinc-500 uppercase tracking-wider transition-opacity duration-300 ${isCollapsed ? "opacity-0 h-0 overflow-hidden mb-0" : "opacity-100"}`, children: t(`nav.section.${section.title}`, section.title) }), 3147 isCollapsed && section.title && section.title !== "Main" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mx-4 my-2 h-px bg-zinc-100 dark:bg-zinc-800" }), 3148 section.links.map(renderLink) 3149 ] }, section.id); 3150 }), 3151 navSections.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `px-6 py-4 text-sm text-zinc-500 ${isCollapsed ? "hidden" : ""}`, children: "Loading navigation..." }),
3152 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-6", children: [ 3153 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: `px-6 mb-2 text-xs font-bold text-zinc-500 uppercase tracking-wider transition-opacity duration-300 ${isCollapsed ? "opacity-0 h-0 overflow-hidden mb-0" : "opacity-100"}`, children: t("nav.subscriptions", "Subscriptions") }), 3154 isCollapsed && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mx-4 my-2 h-px bg-zinc-100 dark:bg-zinc-800" }), 3155 currentUser ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-1", children: Array.from(subscriptions).map((artist) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 3156 "a", 3157 { 3158 href: `/channel?c=${artist}`, 3159 onClick: (e) => { 3160 if (!e.metaKey && !e.ctrlKey && !e.shiftKey && e.button !== 1) { 3161 e.preventDefault(); 3162 handleChannelSelect(artist); 3163 } 3164 }, 3165 className: `flex items-center gap-3 px-4 py-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 cursor-pointer transition-colors ${isCollapsed ? "justify-center px-2" : "px-6"}`, 3166 title: isCollapsed ? artist : void 0, 3167 children: [
3168 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-6 h-6 rounded-full bg-indigo-500 flex items-center justify-center text-[10px] font-bold text-white shrink-0", children: artist.substring(0, 1) }), 3169 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-sm text-zinc-700 dark:text-zinc-300 truncate transition-all duration-300 ${isCollapsed ? "w-0 opacity-0 hidden" : "w-auto opacity-100 flex-1"}`, children: artist }) 3170 ] 3171 }, 3172 artist 3173 )) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `px-6 text-sm text-zinc-500 italic ${isCollapsed ? "hidden" : ""}`, children: t("sidebar.signInSubs", "Sign in to view") }) 3174 ] }), 3175 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 mx-2 rounded-lg bg-zinc-100/70 dark:bg-zinc-800/50 py-2", children: [ 3176 currentUser && (currentUser.role === "ADMIN" || currentUser.canMessage) && /* @__PURE__ */ jsxRuntimeExports.jsx( 3177 SidebarItem, 3178 { 3179 icon: MessageCircle, 3180 label: t("nav.internalComms", "Internal Comms"), 3181 active: activeView === "messages", 3182 onClick: () => setActiveView("messages"), 3183 badge: unreadMessageCount, 3184 href: "/messages", 3185 collapsed: isCollapsed 3186 } 3187 ), 3188 (currentUser == null ? void 0 : currentUser.role) === "ADMIN" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
3189 /* @__PURE__ */ jsxRuntimeExports.jsx( 3190 SidebarItem, 3191 { 3192 icon: Shield, 3193 label: t("nav.admin", "Admin Studio"), 3194 active: activeView === "admin", 3195 onClick: onAdminClick, 3196 href: ((_a = window.HAMIE_ENV) == null ? void 0 : _a.ADMIN_URL) ? `${window.HAMIE_ENV.ADMIN_URL}/admin` : "/admin", 3197 collapsed: isCollapsed 3198 } 3199 ), 3200 isSuperAdmin && /* @__PURE__ */ jsxRuntimeExports.jsx( 3201 SidebarItem, 3202 { 3203 icon: staffModeEnabled ? EyeOff : Eye, 3204 label: staffModeEnabled ? t("nav.disableStaffMode", "Disable Staff Mode") : t("nav.enableStaffMode", "Enable Staff Mode"), 3205 active: staffModeEnabled, 3206 onClick: toggleStaffMode, 3207 collapsed: isCollapsed 3208 } 3209 ),
3210 /* @__PURE__ */ jsxRuntimeExports.jsx( 3211 SidebarItem, 3212 { 3213 icon: ListMusic, 3214 label: "Soundfonts Beta", 3215 active: activeView === "soundfonts", 3216 onClick: () => setActiveView("soundfonts"), 3217 href: "/soundfonts", 3218 collapsed: isCollapsed 3219 } 3220 ) 3221 ] }), 3222 /* @__PURE__ */ jsxRuntimeExports.jsx( 3223 SidebarItem, 3224 { 3225 icon: Settings, 3226 label: t("nav.settings", "Settings"), 3227 active: false, 3228 onClick: () => setIsSettingsOpen(true), 3229 collapsed: isCollapsed 3230 } 3231 ), 3232 /* @__PURE__ */ jsxRuntimeExports.jsx( 3233 SidebarItem, 3234 { 3235 icon: Bug, 3236 label: t("nav.bugReport", "Report a Bug"), 3237 active: false, 3238 onClick: () => { 3239 if (!currentUser) { 3240 setIsAuthModalOpen(true); 3241 } else { 3242 setIsBugReportOpen(true); 3243 } 3244 }, 3245 collapsed: isCollapsed 3246 } 3247 ) 3248 ] }) 3249 ] 3250 } 3251 ) 3252 } 3253 ) 3254 ] }); 3255}; 3256const MelodyInput = ({ onSearch, onClose }) => { 3257 const { t } = useTranslation(); 3258 const [notes, setNotes] = reactExports.useState([]); 3259 const [isListening, setIsListening] = reactExports.useState(false); 3260 const [isPlayingSequence, setIsPlayingSequence] = reactExports.useState(false); 3261 const [clef, setClef] = reactExports.useState("treble"); 3262 const audioContextRef = reactExports.useRef(null); 3263 const analyserRef = reactExports.useRef(null); 3264 const micStreamRef = reactExports.useRef(null); 3265 const requestRef = reactExports.useRef(null); 3266 const c4KeyRef = reactExports.useRef(null); 3267 reactExports.useEffect(() => { 3268 const timer = setTimeout(() => { 3269 if (c4KeyRef.current) { 3270 c4KeyRef.current.scrollIntoView({ behavior: "auto", block: "nearest", inline: "center" }); 3271 } 3272 }, 150); 3273 return () => clearTimeout(timer); 3274 }, []); 3275 const startListening = async () => { 3276 try { 3277 const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); 3278 micStreamRef.current = stream; 3279 audioContextRef.current = new (window.AudioContext || window.webkitAudioContext)(); 3280 const source = audioContextRef.current.createMediaStreamSource(stream); 3281 analyserRef.current = audioContextRef.current.createAnalyser(); 3282 analyserRef.current.fftSize = 2048; 3283 source.connect(analyserRef.current); 3284 setIsListening(true); 3285 detectPitch(); 3286 } catch (e) { 3287 console.error("Mic Error", e); 3288 alert("Could not access microphone."); 3289 } 3290 }; 3291 const stopListening = () => { 3292 setIsListening(false); 3293 if (micStreamRef.current) { 3294 micStreamRef.current.getTracks().forEach((track) => track.stop()); 3295 } 3296 if (requestRef.current) cancelAnimationFrame(requestRef.current); 3297 }; 3298 const detectPitch = () => { 3299 var _a; 3300 if (!analyserRef.current) return; 3301 const buffer = new Float32Array(analyserRef.current.fftSize); 3302 analyserRef.current.getFloatTimeDomainData(buffer); 3303 let bestOffset = -1; 3304 let bestCorrelation = 0; 3305 let rms = 0; 3306 let foundGoodCorrelation = false; 3307 for (let i = 0; i < buffer.length; i++) { 3308 rms += buffer[i] * buffer[i]; 3309 } 3310 rms = Math.sqrt(rms / buffer.length); 3311 if (rms > 0.05) { 3312 for (let offset = 0; offset < buffer.length / 2; offset++) { 3313 let correlation = 0; 3314 for (let i = 0; i < buffer.length / 2; i++) { 3315 correlation += Math.abs(buffer[i] - buffer[i + offset]); 3316 } 3317 correlation = 1 - correlation / (buffer.length / 2); 3318 if (correlation > 0.9 && correlation > bestCorrelation) { 3319 foundGoodCorrelation = true; 3320 if (correlation > bestCorrelation) { 3321 bestCorrelation = correlation; 3322 bestOffset = offset; 3323 } 3324 } else if (foundGoodCorrelation) { 3325 const sampleRate = ((_a = audioContextRef.current) == null ? void 0 : _a.sampleRate) || 44100; 3326 const frequency = sampleRate / bestOffset; 3327 const midi = Math.round(69 + 12 * Math.log2(frequency / 440)); 3328 setNotes((prev) => { 3329 const last = prev[prev.length - 1]; 3330 if (last !== midi) { 3331 playNote(midi); 3332 return [...prev, midi]; 3333 } 3334 return prev; 3335 }); 3336 break; 3337 } 3338 } 3339 } 3340 requestRef.current = requestAnimationFrame(detectPitch); 3341 }; 3342 const playNote = (midi) => { 3343 const AudioContextClass = window.AudioContext || window.webkitAudioContext; 3344 if (!AudioContextClass) return; 3345 const ctx = new AudioContextClass(); 3346 if (ctx.state === "suspended") { 3347 ctx.resume().catch(() => { 3348 }); 3349 } 3350 const t2 = ctx.currentTime; 3351 const freq = 440 * Math.pow(2, (midi - 69) / 12); 3352 const masterGain = ctx.createGain(); 3353 masterGain.gain.setValueAtTime(0, t2); 3354 masterGain.gain.linearRampToValueAtTime(0.5, t2 + 0.015); 3355 masterGain.gain.exponentialRampToValueAtTime(0.01, t2 + 2); 3356 masterGain.connect(ctx.destination); 3357 const filter = ctx.createBiquadFilter(); 3358 filter.type = "lowpass"; 3359 filter.Q.value = 0.5; 3360 filter.frequency.setValueAtTime(freq * 6, t2); 3361 filter.frequency.exponentialRampToValueAtTime(freq * 1.5, t2 + 0.5); 3362 filter.connect(masterGain); 3363 const detunes = [0, -4, 4];
3364 detunes.forEach((detune) => { 3365 const osc = ctx.createOscillator(); 3366 osc.type = "sawtooth"; 3367 osc.frequency.value = freq; 3368 osc.detune.value = detune; 3369 osc.connect(filter); 3370 osc.start(t2); 3371 osc.stop(t2 + 2.5); 3372 }); 3373 const noiseOsc = ctx.createOscillator(); 3374 noiseOsc.type = "square"; 3375 noiseOsc.frequency.value = 80; 3376 const noiseGain = ctx.createGain(); 3377 noiseGain.gain.setValueAtTime(0.05, t2); 3378 noiseGain.gain.exponentialRampToValueAtTime(1e-3, t2 + 0.08); 3379 const noiseFilter = ctx.createBiquadFilter(); 3380 noiseFilter.type = "lowpass"; 3381 noiseFilter.frequency.value = 500; 3382 noiseOsc.connect(noiseFilter); 3383 noiseFilter.connect(noiseGain); 3384 noiseGain.connect(ctx.destination); 3385 noiseOsc.start(t2); 3386 noiseOsc.stop(t2 + 0.08); 3387 }; 3388 const playSequence = async () => { 3389 if (isPlayingSequence) return; 3390 setIsPlayingSequence(true); 3391 for (const note of notes) { 3392 playNote(note); 3393 await new Promise((r) => setTimeout(r, 400)); 3394 } 3395 setIsPlayingSequence(false); 3396 }; 3397 const handleKeyClick = (midi) => { 3398 playNote(midi); 3399 setNotes((prev) => [...prev, midi]); 3400 }; 3401 const handleDeleteNote = (index) => { 3402 setNotes((prev) => prev.filter((_, i) => i !== index)); 3403 }; 3404 const getDiatonicStep = (midi) => { 3405 const pc = midi % 12; 3406 const octave = Math.floor(midi / 12) - 1; 3407 const stepMap = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6]; 3408 return octave * 7 + stepMap[pc]; 3409 }; 3410 const getMidiFromDiatonicStep = (step) => { 3411 const octave = Math.floor(step / 7) + 1; 3412 const degree = step % 7; 3413 const scaleMap = [0, 2, 4, 5, 7, 9, 11]; 3414 return octave * 12 + scaleMap[degree]; 3415 }; 3416 const renderPiano = () => { 3417 const getNoteLabel = (midi) => { 3418 const pc = midi % 12; 3419 const octave = Math.floor(midi / 12) - 1; 3420 const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; 3421 return `${names[pc]}${octave}`; 3422 }; 3423 const startKey = 36; 3424 const endKey = 83; 3425 const allKeys = []; 3426 for (let i = startKey; i <= endKey; i++) { 3427 allKeys.push({ midi: i, isBlack: [1, 3, 6, 8, 10].includes(i % 12) }); 3428 } 3429 const whiteKeys = allKeys.filter((k) => !k.isBlack); 3430 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-start md:justify-center relative h-40 bg-zinc-100 dark:bg-zinc-800 rounded-b-xl overflow-x-auto overflow-y-hidden select-none border-t border-zinc-200 dark:border-zinc-700 custom-scrollbar", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex relative h-full px-4 md:px-0", children: whiteKeys.map((k) => { 3431 const isC4 = k.midi === 60; 3432 const hasBlackNoteAfter = allKeys.find((bk) => bk.midi === k.midi + 1 && bk.isBlack); 3433 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative shrink-0", children: [
3434 /* @__PURE__ */ jsxRuntimeExports.jsx( 3435 "div", 3436 { 3437 ref: isC4 ? c4KeyRef : null, 3438 onMouseDown: () => handleKeyClick(k.midi), 3439 className: `w-10 h-full border-r border-b border-zinc-300 dark:border-zinc-600 rounded-b-[4px] hover:bg-zinc-100 active:bg-zinc-200 cursor-pointer shadow-sm ${isC4 ? "bg-indigo-50/80 dark:bg-indigo-900/20" : "bg-white"} flex flex-col justify-end pb-2`, 3440 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[9px] text-center w-full pointer-events-none ${isC4 ? "text-indigo-600 font-bold" : "text-zinc-400 dark:text-zinc-500"}`, children: getNoteLabel(k.midi) }) 3441 } 3442 ), 3443 hasBlackNoteAfter && /* @__PURE__ */ jsxRuntimeExports.jsx( 3444 "div", 3445 { 3446 onMouseDown: (e) => { 3447 e.stopPropagation(); 3448 handleKeyClick(k.midi + 1); 3449 }, 3450 className: "absolute z-10 w-6 h-24 bg-black rounded-b-[3px] top-0 -right-3 cursor-pointer shadow-md hover:bg-zinc-800 active:bg-zinc-700 border-x border-b border-zinc-800 flex flex-col justify-end pb-1", 3451 style: { boxShadow: "2px 2px 4px rgba(0,0,0,0.4)" }, 3452 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[7px] text-center w-full text-zinc-400 pointer-events-none font-medium", children: getNoteLabel(k.midi + 1) }) 3453 } 3454 ) 3455 ] }, k.midi); 3456 }) }) }); 3457 }; 3458 const renderSheet = () => { 3459 const width = 600; 3460 const height = 240; 3461 const noteRadius = 6; 3462 const bottomLineY = 140; 3463 const stepSpacing = 5; 3464 let refStep = 30; 3465 if (clef === "bass") refStep = 18; 3466 if (clef === "alto") refStep = 24; 3467 const getYForNote = (midi) => { 3468 const step = getDiatonicStep(midi); 3469 return bottomLineY - (step - refStep) * stepSpacing; 3470 }; 3471 const getLedgerLines = (midi) => { 3472 const step = getDiatonicStep(midi); 3473 const lines = []; 3474 const relStep = step - refStep; 3475 if (relStep <= -2) { 3476 for (let s = -2; s >= relStep; s -= 2) { 3477 lines.push(bottomLineY - s * stepSpacing); 3478 } 3479 } 3480 if (relStep >= 10) { 3481 for (let s = 10; s <= relStep; s += 2) { 3482 lines.push(bottomLineY - s * stepSpacing); 3483 } 3484 } 3485 return lines; 3486 }; 3487 const getClefSymbol = () => { 3488 switch (clef) { 3489 case "treble": 3490 return { symbol: "ð", y: 130, size: 55 }; 3491 case "bass": 3492 return { symbol: "ð¢", y: 110, size: 45 }; 3493 case "alto": 3494 return { symbol: "ð¡", y: 120, size: 45 }; 3495 } 3496 }; 3497 const clefConfig = getClefSymbol(); 3498 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative bg-white rounded-t-lg border border-zinc-200 dark:border-zinc-700 overflow-hidden select-none", children: [
3499 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-2 left-4 z-10 flex gap-1 bg-zinc-100 rounded-lg p-1 border border-zinc-200 shadow-sm", children: [ 3500 /* @__PURE__ */ jsxRuntimeExports.jsx( 3501 "button", 3502 { 3503 onClick: () => setClef("treble"), 3504 className: `px-3 py-1 rounded-md text-xs font-bold transition-colors ${clef === "treble" ? "bg-white shadow text-black" : "text-zinc-500 hover:text-zinc-700"}`, 3505 children: t("melody.treble", "Treble") 3506 } 3507 ), 3508 /* @__PURE__ */ jsxRuntimeExports.jsx( 3509 "button", 3510 { 3511 onClick: () => setClef("alto"), 3512 className: `px-3 py-1 rounded-md text-xs font-bold transition-colors ${clef === "alto" ? "bg-white shadow text-black" : "text-zinc-500 hover:text-zinc-700"}`, 3513 children: t("melody.alto", "Alto") 3514 } 3515 ), 3516 /* @__PURE__ */ jsxRuntimeExports.jsx( 3517 "button", 3518 { 3519 onClick: () => setClef("bass"), 3520 className: `px-3 py-1 rounded-md text-xs font-bold transition-colors ${clef === "bass" ? "bg-white shadow text-black" : "text-zinc-500 hover:text-zinc-700"}`, 3521 children: t("melody.bass", "Bass") 3522 } 3523 ) 3524 ] }),
3525 /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: "100%", height, viewBox: `0 0 ${width} ${height}`, className: "cursor-crosshair bg-[#fffaf5]", onClick: (e) => { 3526 const rect = e.currentTarget.getBoundingClientRect(); 3527 const clickY = e.clientY - rect.top; 3528 const diffSteps = Math.round((bottomLineY - clickY) / stepSpacing); 3529 const targetStep = refStep + diffSteps; 3530 const pitch = getMidiFromDiatonicStep(targetStep); 3531 handleKeyClick(pitch); 3532 }, children: [ 3533 [0, 2, 4, 6, 8].map((stepOffset) => { 3534 const y = bottomLineY - stepOffset * stepSpacing; 3535 return /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "0", y1: y, x2: width, y2: y, stroke: "#999", strokeWidth: "1" }, y); 3536 }), 3537 /* @__PURE__ */ jsxRuntimeExports.jsx( 3538 "text", 3539 { 3540 x: "20", 3541 y: clefConfig.y, 3542 fontSize: clefConfig.size, 3543 fontFamily: "serif", 3544 fill: "#333", 3545 pointerEvents: "none", 3546 dominantBaseline: "central", 3547 className: "opacity-90", 3548 children: clefConfig.symbol 3549 } 3550 ), 3551 notes.map((n, i) => { 3552 const x = 80 + i * 35; 3553 const y = getYForNote(n); 3554 const ledgerLines = getLedgerLines(n); 3555 const isBlack = [1, 3, 6, 8, 10].includes(n % 12); 3556 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 3557 "g", 3558 { 3559 className: "hover:opacity-60 cursor-pointer group", 3560 onClick: (e) => { 3561 e.stopPropagation(); 3562 handleDeleteNote(i); 3563 }, 3564 children: [ 3565 ledgerLines.map((ly) => /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: x - 12, y1: ly, x2: x + 12, y2: ly, stroke: "black", strokeWidth: "1" }, ly)),
3566 /* @__PURE__ */ jsxRuntimeExports.jsx( 3567 "line", 3568 { 3569 x1: x + (y > 120 ? 6 : -6), 3570 y1: y, 3571 x2: x + (y > 120 ? 6 : -6), 3572 y2: y > 120 ? y - 35 : y + 35, 3573 stroke: "black", 3574 strokeWidth: "1.5", 3575 className: "group-hover:stroke-red-500" 3576 } 3577 ), 3578 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: x, cy: y, r: noteRadius, fill: "black", className: "group-hover:fill-red-500" }), 3579 isBlack && /* @__PURE__ */ jsxRuntimeExports.jsx("text", { x: x - 18, y: y + 5, fontSize: "16", fontWeight: "bold", children: "â¯" }), 3580 /* @__PURE__ */ jsxRuntimeExports.jsx( 3581 "text", 3582 { 3583 x, 3584 y: y > 120 ? y + 16 : y - 16, 3585 textAnchor: "middle", 3586 fontSize: "10", 3587 fontWeight: "bold", 3588 fill: "#4b5563", 3589 className: "pointer-events-none select-none", 3590 children: (() => { 3591 const pc = n % 12; 3592 const octave = Math.floor(n / 12) - 1; 3593 const names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; 3594 return `${names[pc]}${octave}`; 3595 })() 3596 } 3597 ),
3598 /* @__PURE__ */ jsxRuntimeExports.jsx("title", { children: t("melody.deleteNote", { midi: n, defaultValue: `MIDI ${n} (Click to delete)` }) }) 3599 ] 3600 }, 3601 i 3602 ); 3603 }) 3604 ] }), 3605 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-2 right-2 text-[10px] text-zinc-400 font-mono bg-white/50 px-2 rounded", children: t("melody.instructions", "Click staff to add note ⢠Click note to remove") }) 3606 ] }); 3607 }; 3608 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[2000] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 w-full max-w-2xl rounded-2xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95", children: [ 3609 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center bg-zinc-50 dark:bg-zinc-950", children: [
3610 /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-lg font-bold flex items-center gap-2 text-zinc-900 dark:text-white", children: [ 3611 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { className: "text-indigo-500" }), 3612 " ", 3613 t("melody.title", "Melody Search") 3614 ] }), 3615 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "p-2 hover:bg-zinc-200 dark:hover:bg-zinc-800 rounded-full text-zinc-500 dark:text-zinc-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) }) 3616 ] }), 3617 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6", children: [ 3618 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col rounded-xl shadow-lg border border-zinc-200 dark:border-zinc-700", children: [ 3619 renderSheet(), 3620 renderPiano() 3621 ] }), 3622 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mt-6", children: [ 3623 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4", children: [
3624 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3625 "button", 3626 { 3627 onClick: isListening ? stopListening : startListening, 3628 className: `px-4 py-2 rounded-full flex items-center gap-2 transition-all font-medium text-sm ${isListening ? "bg-red-500 text-white animate-pulse" : "bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700"}`, 3629 children: [ 3630 isListening ? /* @__PURE__ */ jsxRuntimeExports.jsx(CircleStop, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 18 }), 3631 isListening ? t("melody.listening", "Listening...") : t("melody.hum", "Hum Melody") 3632 ] 3633 } 3634 ), 3635 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3636 "button", 3637 { 3638 onClick: playSequence, 3639 disabled: notes.length === 0 || isPlayingSequence, 3640 className: `px-4 py-2 rounded-full flex items-center gap-2 transition-all font-medium text-sm ${isPlayingSequence ? "bg-emerald-500 text-white" : "bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700 disabled:opacity-50"}`, 3641 children: [ 3642 isPlayingSequence ? /* @__PURE__ */ jsxRuntimeExports.jsx(Volume2, { size: 18, className: "animate-pulse" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 18 }), 3643 t("melody.playback", "Playback") 3644 ] 3645 } 3646 ) 3647 ] }),
3648 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 3649 /* @__PURE__ */ jsxRuntimeExports.jsx( 3650 "button", 3651 { 3652 onClick: () => setNotes([]), 3653 className: "p-2 text-zinc-400 hover:text-red-500 transition-colors", 3654 title: t("melody.clearAll", "Clear All"), 3655 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 20 }) 3656 } 3657 ), 3658 /* @__PURE__ */ jsxRuntimeExports.jsx( 3659 "button", 3660 { 3661 onClick: () => { 3662 setNotes((prev) => prev.slice(0, -1)); 3663 }, 3664 className: "p-2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 transition-colors", 3665 title: t("melody.undo", "Undo Last"), 3666 children: /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCcw, { size: 20 }) 3667 } 3668 ) 3669 ] }) 3670 ] }) 3671 ] }), 3672 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 border-t border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 flex justify-end", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 3673 "button", 3674 { 3675 onClick: () => onSearch(notes), 3676 disabled: notes.length < 3, 3677 className: "px-6 py-2.5 bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg font-bold flex items-center gap-2 transition-all shadow-lg shadow-indigo-500/20", 3678 children: [
3679 /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { size: 18 }), 3680 " ", 3681 t("melody.findMatches", "Find Matching Tracks") 3682 ] 3683 } 3684 ) }) 3685 ] }) }); 3686}; 3687const Metronome = ({ bpm, isPlaying, isBuffering, currentTime, timeSignature = { numerator: 4, denominator: 4 } }) => { 3688 const { t } = useTranslation(); 3689 const [soundEnabled, setSoundEnabled] = reactExports.useState(false); 3690 const audioCtxRef = reactExports.useRef(null); 3691 const lastBeatRef = reactExports.useRef(-1); 3692 const [displayAngle, setDisplayAngle] = reactExports.useState(0); 3693 const requestRef = reactExports.useRef(); 3694 const startTimeRef = reactExports.useRef(0); 3695 const lastSyncTimeRef = reactExports.useRef(0); 3696 const initAudio = () => { 3697 if (!audioCtxRef.current) { 3698 audioCtxRef.current = new (window.AudioContext || window.webkitAudioContext)(); 3699 } 3700 if (audioCtxRef.current.state === "suspended") { 3701 audioCtxRef.current.resume().catch(() => { 3702 }); 3703 } 3704 }; 3705 const playTick = (isDownbeat) => { 3706 if (!audioCtxRef.current || !soundEnabled) return; 3707 const now = audioCtxRef.current.currentTime; 3708 const osc = audioCtxRef.current.createOscillator(); 3709 const gain = audioCtxRef.current.createGain(); 3710 const filter = audioCtxRef.current.createBiquadFilter(); 3711 osc.type = isDownbeat ? "sine" : "triangle"; 3712 osc.frequency.setValueAtTime(isDownbeat ? 2e3 : 800, now); 3713 filter.type = "lowpass"; 3714 filter.frequency.setValueAtTime(isDownbeat ? 4e3 : 2e3, now); 3715 gain.gain.setValueAtTime(0, now); 3716 gain.gain.linearRampToValueAtTime(isDownbeat ? 0.8 : 0.3, now + 2e-3); 3717 gain.gain.exponentialRampToValueAtTime(1e-3, now + (isDownbeat ? 0.15 : 0.06)); 3718 osc.connect(filter); 3719 filter.connect(gain); 3720 gain.connect(audioCtxRef.current.destination); 3721 osc.start(now); 3722 osc.stop(now + (isDownbeat ? 0.12 : 0.08)); 3723 setTimeout(() => { 3724 osc.disconnect(); 3725 gain.disconnect(); 3726 filter.disconnect(); 3727 }, 200); 3728 }; 3729 const animate = (time) => { 3730 if (isPlaying && !isBuffering) { 3731 const elapsedSinceSync = (time - lastSyncTimeRef.current) / 1e3; 3732 const estimatedTime = startTimeRef.current + elapsedSinceSync; 3733 const phase = Math.PI * bpm * estimatedTime / 60; 3734 const angle = 25 * Math.cos(phase); 3735 setDisplayAngle(angle); 3736 const beatDuration = 60 / bpm; 3737 const currentBeat = Math.floor(estimatedTime / beatDuration); 3738 if (currentBeat !== lastBeatRef.current) { 3739 if (lastBeatRef.current !== -1 && Math.abs(currentBeat - lastBeatRef.current) === 1) { 3740 const isDownbeat = currentBeat % timeSignature.numerator === 0; 3741 playTick(isDownbeat); 3742 } 3743 lastBeatRef.current = currentBeat; 3744 } 3745 } else { 3746 setDisplayAngle(0); 3747 lastBeatRef.current = -1; 3748 } 3749 requestRef.current = requestAnimationFrame(animate); 3750 }; 3751 reactExports.useEffect(() => { 3752 requestRef.current = requestAnimationFrame(animate); 3753 return () => { 3754 if (requestRef.current) cancelAnimationFrame(requestRef.current); 3755 }; 3756 }, [isPlaying, isBuffering, bpm, soundEnabled]); 3757 reactExports.useEffect(() => { 3758 lastSyncTimeRef.current = performance.now(); 3759 startTimeRef.current = currentTime; 3760 const beatDuration = 60 / bpm; 3761 lastBeatRef.current = Math.floor(currentTime / beatDuration); 3762 }, [currentTime, bpm]); 3763 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center group relative h-full py-1", children: [ 3764 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [
3765 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-8 h-9 shrink-0 flex items-end justify-center", children: [ 3766 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3767 "div", 3768 { 3769 className: "absolute inset-0 bg-[#5b3f2e] dark:bg-[#3d2b1f] shadow-inner", 3770 style: { clipPath: "polygon(20% 0%, 80% 0%, 100% 100%, 0% 100%)" }, 3771 children: [ 3772 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-gradient-to-b from-black/20 to-transparent pointer-events-none" }), 3773 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-2 left-1/2 -translate-x-1/2 w-14 h-14 border border-zinc-200/20 rounded-full opacity-40 pointer-events-none" }) 3774 ] 3775 } 3776 ), 3777 /* @__PURE__ */ jsxRuntimeExports.jsx( 3778 "div", 3779 { 3780 className: "absolute bottom-1.5 w-0.5 h-6 bg-zinc-300 dark:bg-zinc-300 origin-bottom will-change-transform z-10", 3781 style: { transform: `rotate(${displayAngle}deg)` }, 3782 children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 left-1/2 -translate-x-1/2 w-2.5 h-2 bg-gradient-to-br from-amber-200 to-amber-600 rounded-[1px] shadow-sm border border-amber-700/50" }) 3783 } 3784 ),
3785 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-0.5 w-1.5 h-1.5 bg-zinc-800 dark:bg-zinc-200 rounded-full z-20 border border-zinc-600 shadow-md translate-y-0.5" }) 3786 ] }), 3787 /* @__PURE__ */ jsxRuntimeExports.jsx( 3788 "button", 3789 { 3790 onClick: (e) => { 3791 e.stopPropagation(); 3792 initAudio(); 3793 setSoundEnabled(!soundEnabled); 3794 }, 3795 className: `absolute -bottom-2 -right-3 p-1 rounded-full transition-all border shadow-sm z-30 scale-90 ${soundEnabled ? "text-[#f50] bg-white dark:bg-[#1a1a1a] border-[#f50]/30" : "text-zinc-500 bg-white dark:bg-[#1a1a1a] border-zinc-200 dark:border-zinc-700 hover:text-zinc-700 dark:hover:text-zinc-300"}`, 3796 title: soundEnabled ? t("player.muteMetronome", "Mute Metronome") : t("player.unmuteMetronome", "Unmute Metronome"), 3797 children: soundEnabled ? /* @__PURE__ */ jsxRuntimeExports.jsx(Volume2, { size: 12 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 12 }) 3798 } 3799 ) 3800 ] }), 3801 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute -top-8 left-1/2 -translate-x-1/2 bg-zinc-800 text-white text-[10px] px-1.5 py-0.5 rounded opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap font-mono", children: [ 3802 Math.round(bpm), 3803 " BPM" 3804 ] }) 3805 ] }); 3806}; 3807const AutoScrollingText = ({ 3808 text, 3809 className = "", 3810 containerClassName = "", 3811 scrollDuration = 8, 3812 style, 3813 shrinkOnOverflow = false 3814}) => { 3815 const containerRef = reactExports.useRef(null); 3816 const textRef = reactExports.useRef(null); 3817 const [overflows, setOverflows] = reactExports.useState(false); 3818 const [shrunkFontSize, setShrunkFontSize] = reactExports.useState(null); 3819 reactExports.useEffect(() => { 3820 const checkOverflow = () => { 3821 if (containerRef.current && textRef.current) { 3822 const isOverflowing = textRef.current.offsetWidth > containerRef.current.offsetWidth; 3823 setOverflows(isOverflowing); 3824 if (shrinkOnOverflow && isOverflowing && containerRef.current) { 3825 const containerWidth = containerRef.current.offsetWidth; 3826 const textWidth = textRef.current.scrollWidth; 3827 const currentSize = parseFloat(getComputedStyle(textRef.current).fontSize); 3828 const ratio = containerWidth / textWidth; 3829 const newSize = Math.max(9, Math.floor(currentSize * ratio)); 3830 setShrunkFontSize(newSize); 3831 } else { 3832 setShrunkFontSize(null); 3833 } 3834 } 3835 }; 3836 checkOverflow(); 3837 const timer = setTimeout(checkOverflow, 150); 3838 window.addEventListener("resize", checkOverflow); 3839 return () => { 3840 clearTimeout(timer); 3841 window.removeEventListener("resize", checkOverflow); 3842 }; 3843 }, [text, shrinkOnOverflow]); 3844 if (shrinkOnOverflow && shrunkFontSize !== null) { 3845 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: containerRef, className: `w-full truncate flex items-center ${containerClassName}`, style, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { ref: textRef, className: `${className}`, style: { fontSize: `${shrunkFontSize}px` }, children: text }) }); 3846 } 3847 if (overflows && !shrinkOnOverflow) { 3848 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: containerRef, className: `overflow-hidden relative w-full flex items-center ${containerClassName}`, style, children: [
3849 /* @__PURE__ */ jsxRuntimeExports.jsxs( 3850 "div", 3851 { 3852 className: "animate-marquee-infinite flex w-max absolute inset-y-0 left-0 items-center", 3853 style: { "--scroll-dist": "-50%", "--animation-duration": `${scrollDuration}s` }, 3854 children: [ 3855 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-6 pr-6", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `whitespace-nowrap ${className}`, children: text }) }), 3856 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-6 pr-6", "aria-hidden": "true", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `whitespace-nowrap ${className}`, children: text }) }) 3857 ] 3858 } 3859 ), 3860 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `invisible whitespace-nowrap ${className}`, children: text }) 3861 ] }); 3862 } 3863 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: containerRef, className: `w-full truncate flex items-center ${containerClassName}`, style, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { ref: textRef, className: `${className}`, children: text }) }); 3864}; 3865const generateVerticalScrollCSS = (trackId, itemCount) => { 3866 const animationName = `slide-up-instruments-${trackId}`; 3867 const itemHeight = 20; 3868 let keyframes = `@keyframes ${animationName} { 3869`; 3870 const stepPercent = 100 / itemCount; 3871 const transitionPercent = 3.5; 3872 for (let i = 0; i <= itemCount; i++) { 3873 const yOffset = -(i * itemHeight); 3874 const startPct = i * stepPercent; 3875 const endPct = (i + 1) * stepPercent - transitionPercent; 3876 if (i === itemCount) { 3877 keyframes += ` 100% { transform: translateY(${yOffset}px); } 3878`; 3879 } else { 3880 keyframes += ` ${startPct.toFixed(1)}%, ${endPct.toFixed(1)}% { transform: translateY(${yOffset}px); } 3881`; 3882 } 3883 } 3884 keyframes += `}`; 3885 return { animationName, css: keyframes }; 3886}; 3887const MidiThumbnail = ({ 3888 track, 3889 isHovered, 3890 isActive, 3891 currentProgress, 3892 waveform, 3893 variant, 3894 isTitleOverflowing = false, 3895 titleContainerRef, 3896 titleRef, 3897 className = "", 3898 onClick 3899}) => { 3900 const displayArtist = track.artist && !["unknown", "unknown artist"].includes(track.artist.toLowerCase().trim()) ? track.artist : null; 3901 const hashColors = getColorsFromHash(track.title + (displayArtist || "")); 3902 const isCompact = variant === "player-thumbnail" || variant === "play-button" || variant === "small"; 3903 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 3904 "div", 3905 { 3906 onClick, 3907 style: { background: hashColors.gradient }, 3908 className: `relative overflow-hidden flex items-center justify-center select-none ${className}`, 3909 children: [ 3910 !isCompact && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-gradient-to-t from-black/55 via-black/15 to-black/35 z-10 pointer-events-none" }), 3911 variant === "standard" && displayArtist && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-3 left-3 z-20 pointer-events-none max-w-[65%] h-6 flex items-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3912 AutoScrollingText, 3913 { 3914 text: displayArtist, 3915 className: "text-[10px] font-bold text-zinc-300 drop-shadow-[0_1px_3px_rgba(0,0,0,0.85)] uppercase tracking-widest leading-none", 3916 containerClassName: "h-3.5 flex items-center" 3917 } 3918 ) }), 3919 variant === "standard" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-0 right-0 top-0 bottom-0 flex items-center justify-center z-11 opacity-65 group-hover:opacity-95 transition-opacity pointer-events-none select-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3920 Waveform, 3921 { 3922 data: waveform.length > 0 ? waveform : generatePseudoWaveform(String(track.id), 1e3), 3923 className: "w-full h-full", 3924 active: isActive, 3925 progress: currentProgress, 3926 colorBg: hashColors.waveformBg, 3927 colorFg: hashColors.waveformFg, 3928 colorBgActive: hashColors.waveformBgActive, 3929 colorFgActive: hashColors.waveformFgActive 3930 } 3931 ) }),
3932 variant === "standard" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute z-12 left-4 right-4 top-1/2 -translate-y-1/2 text-center pointer-events-none flex flex-col items-center justify-center w-[calc(100%-2rem)]", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 3933 AutoScrollingText, 3934 { 3935 text: track.title, 3936 className: "font-black text-sm sm:text-base text-white drop-shadow-[0_2px_10px_rgba(0,0,0,0.7)] leading-tight", 3937 containerClassName: "h-6 flex items-center justify-center" 3938 } 3939 ) }), 3940 (variant === "small" || isCompact) && /* @__PURE__ */ jsxRuntimeExports.jsx( 3941 Music, 3942 { 3943 size: variant === "small" ? 16 : 14, 3944 className: "midi-thumbnail-icon text-white/70 drop-shadow-[0_0_4px_rgba(255,255,255,0.2)] z-10 pointer-events-none" 3945 } 3946 ), 3947 variant === "standard" && track.instruments && track.instruments.length > 0 && (() => { 3948 const validInsts = track.instruments.filter((inst) => inst.notesCount > 0 && inst.midiChannel !== -1); 3949 const uniqueNames = Array.from(new Set(validInsts.map((inst) => getInstrumentName(inst.program, inst.midiChannel)))); 3950 if (uniqueNames.length === 0 && track.instruments.length > 0) { 3951 uniqueNames.push(getInstrumentName(track.instruments[0].program, track.instruments[0].midiChannel)); 3952 } 3953 if (uniqueNames.length === 0) return null; 3954 const shouldAnimate = uniqueNames.length > 1; 3955 const { animationName, css } = generateVerticalScrollCSS(String(track.id), uniqueNames.length); 3956 return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 3957 shouldAnimate && /* @__PURE__ */ jsxRuntimeExports.jsx("style", { dangerouslySetInnerHTML: { __html: css } }),
3958 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-0 left-0 right-0 h-5 overflow-hidden z-12 bg-[#18181b] border-t border-indigo-500/20", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 3959 "div", 3960 { 3961 className: "flex flex-col m-0 p-0 list-none", 3962 style: shouldAnimate ? { 3963 animation: `${animationName} ${uniqueNames.length * 3.5}s cubic-bezier(0.85, 0, 0.15, 1) infinite` 3964 } : void 0, 3965 children: [ 3966 uniqueNames.map((name, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-5 flex items-center justify-center shrink-0 select-none", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[8.5px] font-black text-zinc-100 dark:text-zinc-50 whitespace-nowrap uppercase tracking-widest flex items-center gap-1 drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]", children: [ 3967 "ð¹ ", 3968 name 3969 ] }) }, i)), 3970 shouldAnimate && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-5 flex items-center justify-center shrink-0 select-none", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[8.5px] font-black text-zinc-100 dark:text-zinc-50 whitespace-nowrap uppercase tracking-widest flex items-center gap-1 drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]", children: [ 3971 "ð¹ ", 3972 uniqueNames[0] 3973 ] }) }) 3974 ] 3975 } 3976 ) }) 3977 ] }); 3978 })() 3979 ] 3980 } 3981 ); 3982}; 3983const MidiCard = ({ 3984 track, 3985 onSelect, 3986 onToggleWatchLater, 3987 isWatchLater = false, 3988 onAddToQueue, 3989 onAddToPlaylist, 3990 onArtistSelect, 3991 variant = "standard", 3992 layout = "grid", 3993 className = "", 3994 sizeClass = "", 3995 playlist 3996}) => { 3997 var _a, _b, _c; 3998 const [isMenuOpen, setIsMenuOpen] = reactExports.useState(false); 3999 const menuRef = reactExports.useRef(null); 4000 const { t } = useTranslation(); 4001 const { activeTrack, isPlaying, playTrack, togglePlay, currentTime, duration, seekTo, isBuffering } = usePlayer(); 4002 const { staffModeEnabled } = useAdmin(); 4003 const [isHovered, setIsHovered] = reactExports.useState(false); 4004 const [isTitleOverflowing, setIsTitleOverflowing] = reactExports.useState(false); 4005 const titleContainerRef = reactExports.useRef(null); 4006 const titleRef = reactExports.useRef(null); 4007 const handleMouseEnter = () => { 4008 setIsHovered(true); 4009 if (titleContainerRef.current && titleRef.current) { 4010 setIsTitleOverflowing(titleRef.current.scrollWidth > titleContainerRef.current.clientWidth || titleRef.current.scrollHeight > titleContainerRef.current.clientHeight); 4011 } 4012 }; 4013 const [currentStatus, setCurrentStatus] = reactExports.useState(track.visibility); 4014 const [commentText, setCommentText] = reactExports.useState(""); 4015 const isActive = (activeTrack == null ? void 0 : activeTrack.id) === track.id; 4016 const isCommunityContributor = !track.uploader || track.uploader.id === -1 || track.uploader.name === "Community Contributor"; 4017 const currentProgress = isActive && duration > 0 ? currentTime / duration : 0; 4018 const displayArtist = track.artist && !["unknown", "unknown artist"].includes(track.artist.toLowerCase().trim()) ? track.artist : null; 4019 const hashColors = getColorsFromHash(track.title + (displayArtist || "")); 4020 const cardRef = reactExports.useRef(null); 4021 const [isInView, setIsInView] = reactExports.useState(false); 4022 reactExports.useEffect(() => { 4023 const observer = new IntersectionObserver((entries) => { 4024 if (entries[0].isIntersecting) { 4025 setIsInView(true); 4026 observer.disconnect(); 4027 } 4028 }, { rootMargin: "300px" }); 4029 if (cardRef.current) { 4030 observer.observe(cardRef.current); 4031 } 4032 return () => observer.disconnect(); 4033 }, []); 4034 const { waveform } = useWaveform(String(track.id), isInView ? track.waveformJsonPath : void 0, track.waveform); 4035 reactExports.useEffect(() => { 4036 const handleClickOutside = (event) => { 4037 if (menuRef.current && !menuRef.current.contains(event.target)) { 4038 setIsMenuOpen(false); 4039 } 4040 }; 4041 document.addEventListener("mousedown", handleClickOutside); 4042 return () => document.removeEventListener("mousedown", handleClickOutside); 4043 }, []); 4044 reactExports.useEffect(() => { 4045 const checkOverflow = () => { 4046 if (titleContainerRef.current && titleRef.current) { 4047 setIsTitleOverflowing( 4048 titleRef.current.scrollWidth > titleContainerRef.current.clientWidth 4049 ); 4050 } 4051 }; 4052 checkOverflow(); 4053 const timer = setTimeout(checkOverflow, 150); 4054 window.addEventListener("resize", checkOverflow); 4055 return () => { 4056 clearTimeout(timer); 4057 window.removeEventListener("resize", checkOverflow); 4058 }; 4059 }, [track.title, variant, layout]); 4060 const handleArtistClick = (e) => { 4061 e.preventDefault(); 4062 e.stopPropagation(); 4063 if (!displayArtist) return; 4064 const targetUrl = `/?q=${encodeURIComponent(displayArtist)}`; 4065 if (e.ctrlKey || e.metaKey || e.shiftKey || e.button === 1) { 4066 window.open(targetUrl, "_blank"); 4067 } else if (onArtistSelect) { 4068 onArtistSelect(displayArtist); 4069 } else { 4070 window.location.href = targetUrl; 4071 } 4072 }; 4073 const handleMenuClick = (e) => { 4074 e.stopPropagation(); 4075 e.preventDefault(); 4076 setIsMenuOpen(!isMenuOpen); 4077 }; 4078 const handleToggleVisibility = async (e) => { 4079 e.stopPropagation(); 4080 e.preventDefault(); 4081 try { 4082 const result = await api.toggleTrackVisibility(track.id); 4083 setCurrentStatus(result.status); 4084 } catch (e2) { 4085 console.error(e2); 4086 } 4087 }; 4088 const handleClick = (e) => { 4089 var _a2; 4090 if ((_a2 = window.getSelection()) == null ? void 0 : _a2.toString()) { 4091 return; 4092 } 4093 const targetElement = e.target; 4094 if (targetElement == null ? void 0 : targetElement.closest("button")) { 4095 e.preventDefault(); 4096 e.stopPropagation(); 4097 return; 4098 } 4099 if (e.ctrlKey || e.metaKey || e.shiftKey || e.button === 1) { 4100 return; 4101 } 4102 e.preventDefault(); 4103 e.stopPropagation(); 4104 onSelect(track); 4105 }; 4106 const renderStatusBadge = () => { 4107 if (!track.status || track.status === "completed") return null;
4108 let currentStatus2 = track.status; 4109 if (currentStatus2 === "pending") { 4110 currentStatus2 = "processing"; 4111 } else if (currentStatus2 !== "rejected" && currentStatus2 !== "suspended" && currentStatus2 !== "processing") { 4112 currentStatus2 = "processing"; 4113 } 4114 let config = { 4115 label: currentStatus2.toUpperCase(), 4116 className: "bg-zinc-500/90", 4117 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { size: 10 }) 4118 }; 4119 switch (currentStatus2) { 4120 case "processing": 4121 config = { 4122 label: t("status.processing", "Processing..."), 4123 className: "bg-amber-500/90", 4124 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Loader, { size: 10, className: "animate-spin" }) 4125 }; 4126 break; 4127 case "pending": 4128 case "paused": 4129 config = { 4130 label: t("status.pending", "Pending"), 4131 className: "bg-orange-500/90", 4132 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { size: 10 }) 4133 }; 4134 break; 4135 case "rejected": 4136 case "suspended": 4137 config = { 4138 label: currentStatus2.toUpperCase(), 4139 className: "bg-red-600/90", 4140 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 10 }) 4141 }; 4142 break; 4143 } 4144 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `absolute top-1.5 right-1.5 ${config.className} text-white text-[9px] font-bold px-1.5 py-0.5 rounded flex items-center gap-1 shadow-lg backdrop-blur-[2px] z-25`, children: [ 4145 config.icon, 4146 " ", 4147 config.label 4148 ] }); 4149 }; 4150 const renderStaffOverlay = () => { 4151 if (!staffModeEnabled) return null; 4152 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-1.5 left-1.5 z-30 flex items-center gap-1", children: [
4153 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4154 "button", 4155 { 4156 onClick: handleToggleVisibility, 4157 className: `p-1.5 rounded-lg shadow-lg backdrop-blur-md flex items-center gap-1.5 text-[10px] font-bold transition-all ${currentStatus === "suspended" ? "bg-red-600 text-white" : "bg-black/60 text-white hover:bg-black/80"}`, 4158 title: currentStatus === "suspended" ? "Suspended (Click to Unhide)" : "Visible (Click to Hide)", 4159 children: [ 4160 currentStatus === "suspended" ? /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { size: 12 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { size: 12 }), 4161 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: currentStatus === "suspended" ? "UNHIDE" : "HIDE" }) 4162 ] 4163 } 4164 ), 4165 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4166 "button", 4167 { 4168 onClick: (e) => { 4169 e.stopPropagation(); 4170 e.preventDefault(); 4171 window.open(`/admin/database/table/Midi?search=${track.id}`, "_blank"); 4172 }, 4173 className: "p-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg shadow-lg backdrop-blur-md flex items-center gap-1.5 text-[10px] font-bold transition-all", 4174 children: [
4175 /* @__PURE__ */ jsxRuntimeExports.jsx(EllipsisVertical, { size: 12 }), 4176 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "EDIT" }) 4177 ] 4178 } 4179 ) 4180 ] }); 4181 }; 4182 const menuDropdownContent = /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute right-0 top-6 w-48 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-lg shadow-xl z-50 overflow-hidden py-1", onClick: (e) => e.stopPropagation(), children: [ 4183 onAddToQueue && /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: (e) => { 4184 e.preventDefault(); 4185 onAddToQueue(track); 4186 setIsMenuOpen(false); 4187 }, className: "w-full text-left px-4 py-2 text-xs text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2", children: [ 4188 /* @__PURE__ */ jsxRuntimeExports.jsx(ListPlus, { size: 14 }), 4189 " ", 4190 t("player.addToQueue", "Add to queue") 4191 ] }), 4192 onToggleWatchLater && /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: (e) => { 4193 e.preventDefault(); 4194 onToggleWatchLater(track.id, e); 4195 setIsMenuOpen(false); 4196 }, className: "w-full text-left px-4 py-2 text-xs text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2", children: [
4197 /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { size: 14 }), 4198 " ", 4199 isWatchLater ? t("list.removeWatchLater", "Remove from Later") : t("nav.watchLater", "Watch Later") 4200 ] }), 4201 onAddToPlaylist && /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: (e) => { 4202 e.preventDefault(); 4203 onAddToPlaylist(track); 4204 setIsMenuOpen(false); 4205 }, className: "w-full text-left px-4 py-2 text-xs text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2", children: [ 4206 /* @__PURE__ */ jsxRuntimeExports.jsx(FolderPlus, { size: 14 }), 4207 " ", 4208 t("playlists.saveTo", "Save to playlist") 4209 ] }) 4210 ] }); 4211 if (variant === "player-thumbnail") { 4212 return /* @__PURE__ */ jsxRuntimeExports.jsx( 4213 "a", 4214 { 4215 ref: cardRef, 4216 href: generateTrackUrl(track), 4217 onClick: handleClick, 4218 className: "block", 4219 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 4220 MidiThumbnail, 4221 { 4222 track, 4223 isHovered, 4224 isActive, 4225 currentProgress, 4226 waveform, 4227 variant: "player-thumbnail", 4228 className: `${sizeClass || "w-10 h-10 rounded"} cursor-pointer hover:opacity-85 transition-opacity ${className}` 4229 } 4230 ) 4231 } 4232 ); 4233 } 4234 if (variant === "play-button") { 4235 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 4236 "a", 4237 { 4238 ref: cardRef, 4239 href: generateTrackUrl(track), 4240 onClick: handleClick, 4241 onMouseEnter: handleMouseEnter, 4242 onMouseLeave: () => setIsHovered(false), 4243 className: `flex items-center justify-between gap-3 bg-zinc-100 dark:bg-zinc-900/60 p-1 rounded-lg border border-zinc-200/50 dark:border-zinc-800/40 hover:border-zinc-300 dark:hover:border-zinc-700 hover:shadow-sm cursor-pointer group no-underline text-inherit ${className}`, 4244 children: [
4245 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [ 4246 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-10 h-10 rounded bg-zinc-800 overflow-hidden flex items-center justify-center shrink-0", children: [ 4247 /* @__PURE__ */ jsxRuntimeExports.jsx( 4248 MidiThumbnail, 4249 { 4250 track, 4251 isHovered, 4252 isActive, 4253 currentProgress, 4254 waveform, 4255 variant: "play-button", 4256 className: "w-full h-full" 4257 } 4258 ), 4259 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/40 flex items-center justify-center transition-all z-10 select-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 4260 "button", 4261 { 4262 onTouchStart: (e) => e.stopPropagation(), 4263 onTouchEnd: (e) => e.stopPropagation(), 4264 onClick: (e) => { 4265 e.preventDefault(); 4266 e.stopPropagation(); 4267 playTrack(track, playlist); 4268 }, 4269 className: "w-6 h-6 bg-indigo-600 text-white rounded-full flex items-center justify-center hover:scale-105", 4270 children: isActive && isPlaying ? isBuffering ? /* @__P
4270URE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 12, className: "animate-spin text-white" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 12, fill: "currentColor" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 12, fill: "currentColor", className: "ml-0.5" }) 4271 } 4272 ) }) 4273 ] }), 4274 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0 flex flex-col justify-center", children: [ 4275 /* @__PURE__ */ jsxRuntimeExports.jsx( 4276 AutoScrollingText, 4277 { 4278 text: track.title, 4279 className: "font-bold text-xs text-zinc-900 dark:text-white leading-tight", 4280 containerClassName: "h-5 flex items-center" 4281 } 4282 ), 4283 displayArtist && /* @__PURE__ */ jsxRuntimeExports.jsx( 4284 AutoScrollingText, 4285 { 4286 text: displayArtist, 4287 className: "text-[10px] text-zinc-500 leading-tight", 4288 containerClassName: "h-4 mt-0.5 flex items-center" 4289 } 4290 ) 4291 ] }) 4292 ] }),
4293 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 text-[10px] text-zinc-400 dark:text-zinc-500 font-medium shrink-0 pr-1", children: [ 4294 !isCommunityContributor && ((_a = track.uploader) == null ? void 0 : _a.name) ? /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "hidden sm:inline-flex items-center gap-1.5 w-28 shrink-0 min-w-0", children: [ 4295 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: track.uploader.avatar, name: track.uploader.name, size: 14 }), 4296 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-500 dark:text-zinc-400 font-semibold truncate", children: track.uploader.name }) 4297 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline-flex w-28 shrink-0" }), 4298 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden md:inline w-20 shrink-0", children: timeAgo(track.uploadedAt) }), 4299 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden lg:inline w-10 shrink-0 text-right", children: track.duration }) 4300 ] }) 4301 ] 4302 } 4303 ); 4304 } 4305 if (variant === "small") { 4306 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 4307 "a", 4308 { 4309 ref: cardRef, 4310 href: generateTrackUrl(track), 4311 onClick: handleClick, 4312 className: `flex flex-col group w-full bg-white dark:bg-zinc-900 p-1.5 rounded-xl transition-all relative border border-zinc-200/50 dark:border-zinc-800/40 hover:border-zinc-300 dark:hover:border-zinc-700 hover:shadow-sm cursor-pointer no-underline text-inherit ${className} midi-small-card-custom midi-card-container`, 4313 onMouseEnter: handleMouseEnter, 4314 onMouseLeave: () => setIsHovered(false), 4315 style: isHovered ? { boxShadow: hashColors.glow } : void 0, 4316 children: [
4317 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [ 4318 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-10 h-10 shrink-0 bg-zinc-100 dark:bg-zinc-800 rounded-lg overflow-hidden flex items-center justify-center", children: [ 4319 /* @__PURE__ */ jsxRuntimeExports.jsx( 4320 MidiThumbnail, 4321 { 4322 track, 4323 isHovered, 4324 isActive, 4325 currentProgress, 4326 waveform, 4327 variant: "small", 4328 className: "w-full h-full" 4329 } 4330 ), 4331 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute inset-0 bg-black/40 flex items-center justify-center transition-opacity z-10 select-none ${isHovered || isActive ? "opacity-100" : "opacity-0"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx( 4332 "button", 4333 { 4334 onTouchStart: (e) => e.stopPropagation(), 4335 onTouchEnd: (e) => e.stopPropagation(), 4336 onClick: (e) => { 4337 e.preventDefault(); 4338 e.stopPropagation(); 4339 playTrack(track, playlist); 4340 }, 4341 className: "w-6 h-6 bg-indigo-600 hover:bg-indigo-500 text-white rounded-full flex items-center justify-center transition-all transform scale-90 hover:scale-100", 4342 children: isActive && isPlaying ? isBuffering ? /* @__P
4342URE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 10, className: "animate-spin text-white" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { fill: "currentColor", size: 10 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { fill: "currentColor", size: 10, className: "ml-0.5" }) 4343 } 4344 ) }) 4345 ] }), 4346 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0 pr-6 flex flex-col justify-center", children: [ 4347 /* @__PURE__ */ jsxRuntimeExports.jsx( 4348 AutoScrollingText, 4349 { 4350 text: track.title, 4351 className: "font-bold text-xs text-zinc-900 dark:text-white group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors leading-tight", 4352 containerClassName: "h-5 flex items-center", 4353 shrinkOnOverflow: true 4354 } 4355 ), 4356 (() => { 4357 const validInstruments = (track.instruments || []).filter( 4358 (inst) => inst.notesCount === void 0 || inst.notesCount > 0 4359 ); 4360 const uniqueNames = Array.from(new Set(validInstruments.map((inst) => getInstrumentName(inst.program, inst.midiChannel)))); 4361 if (isHovered && uniqueNames.length > 0) { 4362 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap gap-1 w-full overflow-hidden mt-0.5 pointer-events-none", children: [ 4363 uniqueNames.slice(0, 2).map((name, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[7.5px] font-bold text-indigo-500 dark:text-indigo-400 whitespace-nowrap bg-indigo-500/10 dark:bg-indigo-500/20 px-1 py-0.5 rounded", children: [ 4364 "ð¹ ", 4365 name 4366 ] }, i)), 4367 uniqueNames.length > 2 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[7.5px] font-bold text-indigo-500 dark:text-indigo-400 whitespace-nowrap bg-indigo-500/10 dark:bg-indigo-500/20 px-1 py-0.5 rounded", children: [ 4368 "+", 4369 uniqueNames.length - 2 4370 ] }) 4371 ] }); 4372 } 4373 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] text-zinc-500 dark:text-zinc-400 leading-normal mt-0.5 w-full flex items-center gap-1.5 select-none font-medium truncate", children: [
4374 displayArtist ? /* @__PURE__ */ jsxRuntimeExports.jsx( 4375 "button", 4376 { 4377 type: "button", 4378 onClick: handleArtistClick, 4379 onAuxClick: handleArtistClick, 4380 className: "truncate max-w-[120px] hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline cursor-pointer transition-colors text-left font-medium", 4381 title: `Explore all tracks by ${displayArtist}`, 4382 children: displayArtist 4383 } 4384 ) : null, 4385 displayArtist ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-350 dark:text-zinc-700 select-none", children: "â¢" }) : null, 4386 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono", children: track.duration }), 4387 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-350 dark:text-zinc-700 select-none", children: "â¢" }),
4388 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-0.5 shrink-0", children: [ 4389 /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 8 }), 4390 " ", 4391 track.plays 4392 ] }) 4393 ] }); 4394 })() 4395 ] }) 4396 ] }), 4397 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute right-2 top-2.5 shrink-0 z-20", ref: menuRef, children: [ 4398 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleMenuClick, className: "text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsxRuntimeExports.jsx(EllipsisVertical, { size: 14 }) }), 4399 isMenuOpen && menuDropdownContent 4400 ] }) 4401 ] 4402 } 4403 ); 4404 } 4405 if (layout === "list" || variant === "list") { 4406 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 4407 "a", 4408 { 4409 href: generateTrackUrl(track), 4410 onClick: handleClick, 4411 className: `flex gap-4 group w-full bg-white dark:bg-[#18181b] p-2 rounded-xl transition-all relative border border-zinc-200/50 dark:border-zinc-800/40 hover:border-zinc-300 dark:hover:border-zinc-700 hover:shadow-lg no-underline text-inherit ${className}`, 4412 onMouseEnter: handleMouseEnter, 4413 onMouseLeave: () => setIsHovered(false), 4414 style: isHovered ? { boxShadow: hashColors.glow } : void 0, 4415 children: [
4416 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4417 "div", 4418 { 4419 className: "relative w-48 h-28 sm:w-56 sm:h-32 shrink-0 bg-zinc-950 rounded-lg overflow-hidden shadow-sm flex items-center justify-center cursor-pointer", 4420 onClick: (e) => { 4421 e.preventDefault(); 4422 e.stopPropagation(); 4423 playTrack(track, playlist); 4424 }, 4425 children: [ 4426 /* @__PURE__ */ jsxRuntimeExports.jsx( 4427 MidiThumbnail, 4428 { 4429 track, 4430 isHovered, 4431 isActive, 4432 currentProgress, 4433 waveform, 4434 variant: "standard", 4435 isTitleOverflowing: false, 4436 className: "w-full h-full" 4437 } 4438 ), 4439 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-1.5 right-1.5 bg-black/80 text-white text-[10px] font-bold px-1.5 py-0.5 rounded shadow-lg backdrop-blur-[2px] z-20", children: track.duration }), 4440 ((_b = track.midiMetadata) == null ? void 0 : _b.lyrics) && track.midiMetadata.lyrics.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-1.5 left-1.5 bg-indigo-600/90 text-white text-[9px] font-bold px-1.5 py-0.5 rounded flex items-center gap-1 shadow-lg z-20", children: [
4441 /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 10 }), 4442 " ", 4443 t("midi.karaoke", "Karaoke") 4444 ] }), 4445 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute inset-0 bg-black/40 flex items-center justify-center transition-opacity z-10 pointer-events-none ${isHovered || isActive ? "opacity-100" : "opacity-0"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-10 h-10 bg-indigo-600/90 text-white rounded-full flex items-center justify-center transition-transform transform scale-90 group-hover:scale-100 shadow-lg backdrop-blur-sm", children: isActive && isPlaying ? isBuffering ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 20, className: "animate-spin text-white" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { fill: "currentColor", size: 20 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { fill: "currentColor", size: 20, className: "ml-1" }) }) }), 4446 renderStatusBadge(), 4447 renderStaffOverlay() 4448 ] 4449 } 4450 ),
4451 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0 flex flex-col justify-between py-1", children: [ 4452 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-start gap-4", children: [ 4453 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [ 4454 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "block font-bold text-base text-zinc-900 dark:text-white hover:text-indigo-650 dark:hover:text-indigo-400 transition-colors w-full truncate", title: track.title, children: track.title }), 4455 displayArtist && /* @__PURE__ */ jsxRuntimeExports.jsx( 4456 "button", 4457 { 4458 type: "button", 4459 onClick: handleArtistClick, 4460 onAuxClick: handleArtistClick, 4461 className: "text-sm text-zinc-600 dark:text-zinc-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition-colors font-medium truncate mt-0.5 w-full text-left cursor-pointer", 4462 title: `Explore all tracks by ${displayArtist}`, 4463 children: displayArtist 4464 } 4465 ), 4466 ((_c = track.uploader) == null ? void 0 : _c.name) && /* @__P
4466URE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mt-1.5", children: [ 4467 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: track.uploader.avatar, name: track.uploader.name, size: 18 }), 4468 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] text-indigo-600 dark:text-indigo-400 font-bold truncate", children: track.uploader.name }) 4469 ] }) 4470 ] }), 4471 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative shrink-0", ref: menuRef, children: [ 4472 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleMenuClick, className: "text-zinc-500 p-1.5 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-full opacity-0 group-hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsxRuntimeExports.jsx(EllipsisVertical, { size: 16 }) }), 4473 isMenuOpen && menuDropdownContent 4474 ] }) 4475 ] }), 4476 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center gap-3 mt-2 w-full h-12", children: [
4477 /* @__PURE__ */ jsxRuntimeExports.jsx( 4478 "button", 4479 { 4480 onTouchStart: (e) => e.stopPropagation(), 4481 onTouchEnd: (e) => e.stopPropagation(), 4482 onClick: (e) => { 4483 e.preventDefault(); 4484 e.stopPropagation(); 4485 playTrack(track, playlist); 4486 }, 4487 className: `flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center transition-all ${isActive ? "bg-indigo-600 text-white shadow-md shadow-indigo-500/20" : "bg-indigo-50 hover:bg-indigo-400 text-white shadow-lg shadow-indigo-500/30"}`, 4488 children: isActive && isPlaying ? isBuffering ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 20, className: "animate-spin text-white" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { fill: "currentColor", size: 20 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { fill: "currentColor", size: 20, className: isActive && isPlaying ? "" : "ml-0.5" }) 4489 } 4490 ),
4491 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 relative h-full", children: [ 4492 /* @__PURE__ */ jsxRuntimeExports.jsx( 4493 Waveform, 4494 { 4495 data: waveform.length > 0 ? waveform : generatePseudoWaveform(String(track.id), 1e3), 4496 className: "h-full", 4497 active: isActive, 4498 progress: currentProgress, 4499 onSeek: (p) => { 4500 if (isActive && duration > 0) seekTo(p * duration); 4501 else { 4502 playTrack(track, playlist); 4503 setTimeout(() => seekTo(p * duration), 300); 4504 } 4505 }, 4506 colorBg: hashColors.waveformBg, 4507 colorFg: hashColors.waveformFg, 4508 colorBgActive: hashColors.waveformBgActive, 4509 colorFgActive: hashColors.waveformFgActive 4510 } 4511 ), 4512 isActive && isPlaying && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-indigo-500/5 mix-blend-overlay pointer-events-none pulse-animation" }) 4513 ] }) 4514 ] }),
4515 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mt-2 pt-2 border-t border-zinc-100 dark:border-zinc-800/60", children: [ 4516 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 4517 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "flex items-center gap-1.5 px-2 py-1 text-[10px] font-medium text-zinc-600 dark:text-zinc-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors", children: [ 4518 /* @__PURE__ */ jsxRuntimeExports.jsx(Heart, { size: 12 }), 4519 " ", 4520 track.likesCount || 0 4521 ] }), 4522 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "flex items-center gap-1.5 px-2 py-1 text-[10px] font-medium text-zinc-600 dark:text-zinc-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors", children: [
4523 /* @__PURE__ */ jsxRuntimeExports.jsx(Repeat, { size: 12 }), 4524 " ", 4525 track.repostsCount || 0 4526 ] }), 4527 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { className: "flex items-center gap-1.5 px-2 py-1 text-[10px] font-medium text-zinc-600 dark:text-zinc-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors", children: [ 4528 /* @__PURE__ */ jsxRuntimeExports.jsx(Share2, { size: 12 }), 4529 " ", 4530 t("actions.share", "Share") 4531 ] }), 4532 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: (e) => { 4533 navigator.clipboard.writeText(window.location.origin + generateTrackUrl(track)); 4534 }, className: "flex items-center gap-1.5 px-2 py-1 text-[10px] font-medium text-zinc-600 dark:text-zinc-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors", children: [
4535 /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { size: 12 }), 4536 " ", 4537 t("actions.copyLink", "Copy") 4538 ] }), 4539 onAddToQueue && /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: (e) => { 4540 e.preventDefault(); 4541 onAddToQueue(track); 4542 }, className: "flex items-center gap-1.5 px-2 py-1 text-[10px] font-medium text-zinc-600 dark:text-zinc-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors", children: [ 4543 /* @__PURE__ */ jsxRuntimeExports.jsx(ListPlus, { size: 12 }), 4544 " ", 4545 t("player.addToQueue", "Add to queue") 4546 ] }) 4547 ] }), 4548 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 text-[10px] text-zinc-500", children: [ 4549 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1", children: [
4550 /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 10, className: "fill-current" }), 4551 " ", 4552 track.plays, 4553 " views" 4554 ] }), 4555 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â¢" }), 4556 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: timeAgo(track.uploadedAt) }) 4557 ] }) 4558 ] }), 4559 isActive && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 flex items-center gap-2 animate-in slide-in-from-top-2 fade-in", children: [ 4560 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: void 0, name: "Current User", size: 24 }), 4561 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 relative", children: [ 4562 /* @__PURE__ */ jsxRuntimeExports.jsx( 4563 "input", 4564 { 4565 type: "text", 4566 placeholder: "Write a comment...", 4567 value: commentText, 4568 onChange: (e) => setCommentText(e.target.value), 4569 className: "w-full bg-zinc-100 dark:bg-zinc-800 border border-transparent focus:border-indigo-500 dark:focus:border-indigo-500 rounded-full px-4 py-1 text-[11px] text-zinc-900 dark:text-white outline-none transition-colors" 4570 } 4571 ), 4572 commentText.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setCommentText(""), className: "absolute right-2 top-1/2 -translate-y-1/2 text-indigo-600 dark:text-indigo-400 text-[10px] font-bold hover:text-indigo-700", children: "Post" }) 4573 ] }) 4574 ] }) 4575 ] }) 4576 ] 4577 } 4578 ); 4579 } 4580 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 4581 "a", 4582 { 4583 ref: cardRef, 4584 href: generateTrackUrl(track), 4585 className: `flex flex-col w-full h-full bg-white dark:bg-zinc-900 rounded-2xl border border-zinc-200 dark:border-zinc-800 overflow-hidden hover:shadow-xl hover:-translate-y-1 hover:border-zinc-300 dark:hover:border-zinc-700 transition-all duration-300 cursor-pointer group block relative no-underline text-inherit ${className} midi-card-custom midi-card-container`, 4586 onClick: handleClick, 4587 onMouseEnter: handleMouseEnter, 4588 onMouseLeave: () => setIsHovered(false), 4589 style: isHovered ? { boxShadow: hashColors.glow } : void 0, 4590 children: [
4591 /* @__PURE__ */ jsxRuntimeExports.jsxs( 4592 "div", 4593 { 4594 className: "relative aspect-video w-full overflow-hidden bg-zinc-950 flex items-center justify-center shrink-0 cursor-pointer", 4595 onClick: (e) => { 4596 e.preventDefault(); 4597 e.stopPropagation(); 4598 playTrack(track, playlist); 4599 }, 4600 children: [ 4601 /* @__PURE__ */ jsxRuntimeExports.jsx( 4602 MidiThumbnail, 4603 { 4604 track, 4605 isHovered, 4606 isActive, 4607 currentProgress, 4608 waveform, 4609 variant: "standard", 4610 isTitleOverflowing, 4611 titleContainerRef, 4612 titleRef, 4613 className: "w-full h-full transition-transform duration-500 group-hover:scale-105" 4614 } 4615 ), 4616 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute inset-0 bg-black/40 flex items-center justify-center transition-opacity z-10 pointer-events-none ${isHovered || isActive ? "opacity-100" : "opacity-0"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 bg-indigo-600/90 text-white rounded-full flex items-center justify-center transition-transform transform scale-90 group-hover:scale-100 shadow-lg backdrop-blur-sm", children: isActive && isPlaying ? isBuffering ? /* @__P
4616URE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 24, className: "animate-spin text-white" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { fill: "currentColor", size: 24 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { fill: "currentColor", size: 24, className: "ml-1" }) }) }), 4617 renderStatusBadge(), 4618 renderStaffOverlay() 4619 ] 4620 } 4621 ), 4622 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2.5 pb-2 flex flex-col gap-1 bg-white dark:bg-zinc-900 z-20 shrink-0", children: [ 4623 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-start gap-2", children: [ 4624 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [ 4625 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-black text-sm text-zinc-900 dark:text-white leading-tight hover:text-indigo-650 dark:hover:text-indigo-400 transition-colors w-full truncate", title: track.title, children: track.title }),
4626 displayArtist ? /* @__PURE__ */ jsxRuntimeExports.jsx( 4627 "button", 4628 { 4629 type: "button", 4630 onClick: handleArtistClick, 4631 onAuxClick: handleArtistClick, 4632 className: "text-[11px] text-zinc-500 dark:text-zinc-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition-colors font-semibold leading-normal w-full truncate mt-0.5 text-left cursor-pointer", 4633 title: `Explore all tracks by ${displayArtist}`, 4634 children: displayArtist 4635 } 4636 ) : null 4637 ] }), 4638 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative shrink-0", ref: menuRef, children: [ 4639 onToggleWatchLater && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleMenuClick, className: "text-zinc-500 hover:text-zinc-300 p-1 rounded-full hover:bg-zinc-800 opacity-0 group-hover:opacity-100 transition-opacity", children: /* @__PURE__ */ jsxRuntimeExports.jsx(EllipsisVertical, { size: 14 }) }), 4640 isMenuOpen && menuDropdownContent 4641 ] }) 4642 ] }),
4643 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-150 dark:bg-zinc-800/80 my-0.5" }), 4644 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-3 midi-card-bottom-row", children: [ 4645 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-2 min-w-0 flex-1 uploader-info", children: !isCommunityContributor ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 4646 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: track.uploader.avatar, name: track.uploader.name, size: 18 }), 4647 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] font-bold text-zinc-600 dark:text-zinc-400 truncate", title: track.uploader.name, children: track.uploader.name }) 4648 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] font-bold text-zinc-500", children: t("midi.community", "Community") }) }), 4649 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 shrink-0 stats-controls", children: [
4650 /* @__PURE__ */ jsxRuntimeExports.jsx( 4651 "button", 4652 { 4653 onTouchStart: (e) => e.stopPropagation(), 4654 onTouchEnd: (e) => e.stopPropagation(), 4655 onClick: (e) => { 4656 e.preventDefault(); 4657 e.stopPropagation(); 4658 playTrack(track, playlist); 4659 }, 4660 className: `flex items-center justify-center p-1.5 rounded-full transition-all shadow-sm ${isActive ? "bg-indigo-600 text-white" : "bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-white hover:bg-zinc-200 dark:hover:bg-zinc-700"}`, 4661 title: isActive && isPlaying ? t("player.pause", "Pause") : t("player.play", "Play"), 4662 children: isActive && isPlaying ? isBuffering ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 10, className: "animate-spin text-white" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 10, fill: "currentColor" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 10, className: "ml-0.5", fill: "currentColor" }) 4663 } 4664 ),
4665 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 font-medium font-mono", children: track.duration }), 4666 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] text-zinc-500 font-medium flex items-center gap-0.5", children: [ 4667 /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 9 }), 4668 " ", 4669 track.plays 4670 ] }) 4671 ] }) 4672 ] }) 4673 ] }) 4674 ] 4675 } 4676 ); 4677}; 4678const VideoCard = React.memo(MidiCard); 4679const parseDurationString = (str) => { 4680 if (!str) return 0; 4681 const parts = str.split(":").map(Number); 4682 if (parts.some(isNaN)) return 0; 4683 if (parts.length === 3) { 4684 return parts[0] * 3600 + parts[1] * 60 + parts[2]; 4685 } else if (parts.length === 2) { 4686 return parts[0] * 60 + parts[1]; 4687 } 4688 return parts[0] || 0; 4689}; 4690function PersistentPlayer({ 4691 track, 4692 isPlaying, 4693 setIsPlaying, 4694 onTimeUpdate, 4695 onNext, 4696 onPrev, 4697 toggleLike, 4698 isLiked, 4699 onTrackClick, 4700 onAddToPlaylist, 4701 queue = [], 4702 activePlaylist = null, 4703 playlistLabel = null, 4704 onClearPlaylist, 4705 onRemoveFromQueue, 4706 onPlayQueueTrack, 4707 onPlaybackRateChange, 4708 audioMode,
4709 setAudioMode, 4710 isBuffering = false, 4711 seekTo, 4712 isGameMode = false, 4713 currentUser, 4714 audioQuality, 4715 pitchSemitones, 4716 setPitchSemitones, 4717 activeView 4718}) { 4719 var _a, _b, _c, _d, _e, _f, _g, _h; 4720 const { trackStates, setTrackStates, isLiveSynthFallback, setIsLiveSynthFallback, isSynthReady, setIsBuffering, soundfont, setSoundfont, hasStartedLoading, setHasStartedLoading, engineOrder, setEngineOrder } = usePlayer(); 4721 const { t } = useTranslation(); 4722 const [sfMetadata, setSfMetadata] = reactExports.useState({}); 4723 const [unavailableModes, setUnavailableModes] = reactExports.useState({}); 4724 reactExports.useEffect(() => { 4725 var _a2; 4726 if (!track) return; 4727 setUnavailableModes({}); 4728 if (track.opusPath) { 4729 fetch(track.opusPath, { method: "HEAD" }).then((res) => { 4730 if (!res.ok) setUnavailableModes((prev) => ({ ...prev, singleopus: true })); 4731 }).catch(() => setUnavailableModes((prev) => ({ ...prev, singleopus: true }))); 4732 } else { 4733 setUnavailableModes((prev) => ({ ...prev, singleopus: true })); 4734 } 4735 const firstInst = (_a2 = track.instruments) == null ? void 0 : _a2.find((i) => !!(i.stemPathHigh || i.stemPath)); 4736 const firstStem = (firstInst == null ? void 0 : firstInst.stemPathHigh) || (firstInst == null ? void 0 : firstInst.stemPath); 4737 if (firstStem) { 4738 fetch(firstStem, { method: "HEAD" }).then((res) => { 4739 if (!res.ok) setUnavailableModes((prev) => ({ ...prev, opus: true })); 4740 }).catch(() => setUnavailableModes((prev) => ({ ...prev, opus: true }))); 4741 } else { 4742 setUnavailableModes((prev) => ({ ...prev, opus: true })); 4743 } 4744 const eventsSources = [ 4745 track.cleanMidiPath, 4746 track.midiPath 4747 ].filter(Boolean); 4748 if (eventsSources.length > 0) { 4749 Promise.all(eventsSources.map((src) => fetch(src, { method: "HEAD" }))).then((responses) => { 4750 if (!responses.some((res) => res.ok)) { 4751 setUnavailableModes((prev) => ({ 4752 ...prev, 4753 synth: true, 4754 webmidi: true, 4755 chiptune: true, 4756 wasmsynth: true, 4757 fmsynth: true 4758 })); 4759 } 4760 }).catch(() => { 4761 setUnavailableModes((prev) => ({ 4762 ...prev, 4763 synth: true, 4764 webmidi: true, 4765 chiptune: true, 4766 wasmsynth: true, 4767 fmsynth: true 4768 })); 4769 }); 4770 } else { 4771 setUnavailableModes((prev) => ({ 4772 ...prev, 4773 synth: true, 4774 webmidi: true, 4775 chiptune: true, 4776 wasmsynth: true, 4777 fmsynth: true 4778 })); 4779 } 4780 }, [track == null ? void 0 : track.id, track == null ? void 0 : track.opusPath, track == null ? void 0 : track.instruments, track == null ? void 0 : track.pianoRollPath, track == null ? void 0 : track.cleanMidiPath, track == null ? void 0 : track.midiPath]); 4781 reactExports.useEffect(() => { 4782 const loadInitData = async () => { 4783 try { 4784 const res = await fetch("/api/soundfonts"); 4785 if (res.ok) { 4786 const data = await res.json(); 4787 if (activeView !== "watch") { 4788 const activeSf = data.active || data.defaultLiveSf || "MusyngKite"; 4789 setSoundfont(activeSf); 4790 } 4791 if (data.engineOrder) { 4792 setEngineOrder(data.engineOrder); 4793 } 4794 if (data.metadata) { 4795 setSfMetadata(data.metadata); 4796 } 4797 if (data.defaultWasmSf) { 4798 synth.defaultWasmSoundfont = data.defaultWasmSf; 4799 } 4800 if (data.defaultJsSf) { 4801 synth.defaultJsSoundfont = data.defaultJsSf; 4802 } 4803 } 4804 } catch (e) { 4805 console.warn("[PersistentPlayer] Failed to load soundfont init data", e); 4806 } 4807 }; 4808 loadInitData(); 4809 }, [activeView, setSoundfont]); 4810 const [singleOpusProgress, setSingleOpusProgress] = reactExports.useState(0); 4811 const downloadProgress = reactExports.useMemo(() => { 4812 var _a2, _b2; 4813 if (audioMode === "singleopus") return singleOpusProgress; 4814 if (isSynthReady) return 100; 4815 if (!hasStartedLoading) return 0; 4816 if (isLiveSynthFallback) { 4817 const keys = Object.keys(trackStates); 4818 if (keys.length === 0) return 0; 4819 const total = keys.reduce((acc, key) => { 4820 var _a3; 4821 return acc + (((_a3 = trackStates[parseInt(key)]) == null ? void 0 : _a3.percent) || 0); 4822 }, 0); 4823 return total / keys.length; 4824 } 4825 const tracksToLoad = ((_a2 = track.instruments) == null ? void 0 : _a2.filter((inst) => !!inst.stemPath)) || []; 4826 if (tracksToLoad.length === 0) { 4827 if (trackStates[0]) return trackStates[0].percent; 4828 return 0; 4829 } 4830 let totalPercent = 0;
4831 let count = 0; 4832 (_b2 = track.instruments) == null ? void 0 : _b2.forEach((inst, idx) => { 4833 var _a3; 4834 if (inst.stemPath) { 4835 totalPercent += ((_a3 = trackStates[idx]) == null ? void 0 : _a3.percent) || 0; 4836 count++; 4837 } 4838 }); 4839 return count > 0 ? totalPercent / count : 0; 4840 }, [trackStates, track.instruments, isSynthReady, hasStartedLoading, isLiveSynthFallback, audioMode, singleOpusProgress]); 4841 const { country, autoplay, setAutoplay, setAudioQuality, soundfontVolume, setSoundfontVolume } = useSettings(); 4842 const getThumbnailElement = (targetTrack, sizeClass, onClick) => { 4843 return /* @__PURE__ */ jsxRuntimeExports.jsx( 4844 VideoCard, 4845 { 4846 track: targetTrack, 4847 variant: "player-thumbnail", 4848 sizeClass, 4849 onSelect: onClick ? () => onClick() : () => { 4850 } 4851 } 4852 ); 4853 }; 4854 const audioRef = reactExports.useRef(null); 4855 const progressContainerRef = reactExports.useRef(null); 4856 const volumeRef = reactExports.useRef(null); 4857 const volumeTimeoutRef = reactExports.useRef(null); 4858 const [volume, setVolume] = reactExports.useState(1); 4859 const [progress, setProgress] = reactExports.useState(0); 4860 const [duration, setDuration] = reactExports.useState( 4861 ((_a = track.midiMetadata) == null ? void 0 : _a.duration) || parseDurationString(track.duration) || 0 4862 ); 4863 const [currentTime, setCurrentTime] = reactExports.useState(0); 4864 const [isMuted, setIsMuted] = reactExports.useState(false); 4865 const [showQueue, setShowQueue] = reactExports.useState(false); 4866 const [showSettings, setShowSettings] = reactExports.useState(false); 4867 const [showEngineMenu, setShowEngineMenu] = reactExports.useState(false); 4868 const [showVolumeSlider, setShowVolumeSlider] = reactExports.useState(false); 4869 const [playbackError, setPlaybackError] = reactExports.useState(false); 4870 const [showTempoMenu, setShowTempoMenu] = reactExports.useState(false); 4871 const [showMoreMenu, setShowMoreMenu] = reactExports.useState(false); 4872 const [containerWidth, setContainerWidth] = reactExports.useState(0); 4873 const [midiOutputs, setMidiOutputs] = reactExports.useState([]); 4874 const [selectedMidiId, setSelectedMidiId] = reactExports.useState(""); 4875 const [lowPerfToggle, setLowPerfToggle] = reactExports.useState(() => { 4876 if (typeof window !== "undefined") { 4877 return localStorage.getItem("low_performance") === "true"; 4878 } 4879 return false; 4880 }); 4881 const [isDragging, setIsDragging] = reactExports.useState(false); 4882 const [fullscreenElement, setFullscreenElement] = reactExports.useState(null); 4883 const [showFullscreenControls, setShowFullscreenControls] = reactExports.useState(true); 4884 const [isPinned, setIsPinned] = reactExports.useState(false); 4885 const hideControlsTimer = reactExports.useRef(null); 4886 const isHoveringControls = reactExports.useRef(false); 4887 const getRetroLcdConfig = (mode) => { 4888 switch (mode) { 4889 case "opus": 4890 return { label: "STEMS", color: "#ff5500", bg: "bg-zinc-950", border: "border-[#ff5500]/50", text: "text-[#ff5500]", shadow: "shadow-[#ff5500]/10" }; 4891 case "singleopus": 4892 return { label: "OPUS", color: "#3b82f6", bg: "bg-zinc-950", border: "border-[#3b82f6]/50", text: "text-[#3b82f6]", shadow: "shadow-[#3b82f6]/10" }; 4893 case "synth": 4894 return { label: "JS", color: "#10b981", bg: "bg-zinc-950", border: "border-[#10b981]/50", text: "text-[#10b981]", shadow: "shadow-[#10b981]/10" }; 4895 case "wasmsynth": 4896 return { label: "SF", color: "#06b6d4", bg: "bg-zinc-950", border: "border-[#06b6d4]/50", text: "text-[#06b6d4]", shadow: "shadow-[#06b6d4]/10" }; 4897 case "fmsynth": 4898 return { label: "FM", color: "#8b5cf6", bg: "bg-zinc-950", border: "border-[#8b5cf6]/50", text: "text-[#8b5cf6]", shadow: "shadow-[#8b5cf6]/10" }; 4899 case "chiptune": 4900 return { label: "8BIT", color: "#f59e0b", bg: "bg-zinc-950", border: "border-[#f59e0b]/50", text: "text-[#f59e0b]", shadow: "shadow-[#f59e0b]/10" }; 4901 case "webmidi": 4902 return { label: "MIDI", color: "#6366f1", bg: "bg-zinc-950", border: "border-[#6366f1]/50", text: "text-[#6366f1]", shadow: "shadow-[#6366f1]/10" }; 4903 default: 4904 return { label: "SYNTH", color: "#10b981", bg: "bg-zinc-950", border: "border-[#10b981]/50", text: "text-[#10b981]", shadow: "shadow-[#10b981]/10" }; 4905 } 4906 }; 4907 const renderEngineOption = (mode, badge, color, title, desc, disabled, beforeSwitch) => { 4908 const isSelected = audioMode === mode; 4909 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 4910 "button", 4911 { 4912 disabled, 4913 type: "button", 4914 onClick: async () => { 4915 if (mode === "webmidi" && !synth.midiAccess) { 4916 await synth.initMidiOutput(); 4917 const outputs = synth.getMidiOutputs(); 4918 setMidiOutputs(outputs); 4919 setSelectedMidiId(synth.getActiveMidiOutputId() || ""); 4920 if (synth.midiAccess) { 4921 synth.midiAccess.onstatechange = () => { 4922 setMidiOutputs(synth.getMidiOutputs()); 4923 setSelectedMidiId(synth.getActiveMidiOutputId() || ""); 4924 }; 4925 } 4926 if (!synth.midiAccess) { 4927 setTrackStates((prev) => ({ ...prev })); 4928 return; 4929 } 4930 }
4931 setAudioMode(mode); 4932 if (typeof window !== "undefined") { 4933 localStorage.setItem("preferredAudioMode", mode); 4934 } 4935 setShowEngineMenu(false); 4936 }, 4937 className: `w-full flex items-center justify-between p-2 rounded-lg text-left transition-colors duration-150 relative ${isSelected ? "bg-zinc-100 dark:bg-zinc-800/80 border border-zinc-200 dark:border-zinc-700/50" : "hover:bg-zinc-50 dark:hover:bg-zinc-800/40 border border-transparent"} ${disabled ? "opacity-30 cursor-not-allowed hover:bg-transparent" : "hover:bg-zinc-100/50 dark:hover:bg-zinc-800/50"}`, 4938 children: [ 4939 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1", children: [ 4940 /* @__PURE__ */ jsxRuntimeExports.jsx( 4941 "div", 4942 { 4943 className: `font-mono text-[8px] font-black px-1.5 py-0.5 rounded border tracking-widest shrink-0 text-center min-w-[38px] bg-zinc-950 ${disabled ? "opacity-40 grayscale" : ""}`, 4944 style: { 4945 borderColor: disabled ? "rgba(113, 113, 122, 0.4)" : `${color}50`, 4946 color: disabled ? "#a1a1aa" : color, 4947 textShadow: disabled ? "none" : `0 0 3px ${color}` 4948 }, 4949 children: badge 4950 } 4951 ),
4952 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col min-w-0", children: [ 4953 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-xs font-semibold truncate ${disabled ? "text-zinc-400 dark:text-zinc-500" : "text-zinc-800 dark:text-zinc-200"}`, children: title }), 4954 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] text-zinc-500 dark:text-zinc-400 truncate", children: desc || (disabled ? "Not available for this track" : "") }) 4955 ] }) 4956 ] }), 4957 isSelected && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 12, className: "shrink-0", style: { color } }) 4958 ] 4959 } 4960 ); 4961 }; 4962 const renderRetroLcdIndicator = () => { 4963 var _a2, _b2; 4964 const config = getRetroLcdConfig(audioMode); 4965 const isMetadataOnly = track.status === "metadata_only"; 4966 const isCompleted = track.status === "completed"; 4967 const hasAudio = isCompleted && !isMetadataOnly && !!track.opusPath && ((_a2 = track.midiMetadata) == null ? void 0 : _a2.hasAudio) !== false; 4968 const hasStems = isCompleted && !isMetadataOnly && !!(track.instruments && track.instruments.some((i) => !!i.stemPath)); 4969 const isJsHidden = ((_b2 = sfMetadata[`${soundfont}_JS`]) == null ? void 0 : _b2.visible) === false; 4970 const isSynthDisabled = unavailableModes["synth"] || isJsHidden; 4971 const engineOptions = [ 4972 { id: "opus", label: "STEMS", color: "#ff5500", title: "Instrument Stems (Opus)", desc: "Multi-file high-fidelity audio", disabled: unavailableModes["opus"] || !hasStems }, 4973 { id: "singleopus", label: "OPUS", color: "#3b82f6", title: "Single-file Opus", desc: "Pre-mixed single file (low resource)", disabled: unavailableModes["singleopus"] || !hasAudio }, 4974 { id: "synth", label: "JS", color: "rgb(16, 185, 129)", title: "Web Audio JS SoundFont", desc: typeof navigator !== "undefined" && (/Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || "ontouchstart" in window) ? "Sampled Opus notes per instrument (heavy on mobile)" : "Sampled Opus notes per instrument (Web Audio)", disabled: isSynthDisabled }, 4975 { id: "wasmsynth", label: "SF", color: "#06b6d4", title: "Direct SoundFont Engine", desc: "Direct SF2/SF3 SoundFont rendering via WASM (High quality, low bandwidth)", disabled: unavailableModes["wasmsynth"] }, 4976 { id: "fmsynth", label: "FM", color: "#8b5cf6", title: "16-bit FM Synth", desc: "Retro arcade FM sound (Low CPU)", disabled: unavailableModes["fmsynth"] }, 4977 { id: "chiptune", label: "8BIT", color: "#f59e0b", title: "Chiptune Synth (8-bit)", desc: "Retro 8-bit sound (Ultra Low CPU)", disabled: unavailableModes["chiptune"] }, 4978 { id: "webmidi", label: "MIDI", color: "#6366f1", title: "Web MIDI Output", desc: getWebMidiStatusLabel() || "External hardware MIDI", disabled: unavailableModes["webmidi"] || !(typeof navigator !== "undefined" && !!navigator.requestMIDIAccess) || synth.midiAccessStatus === "denied" } 4979 ]; 4980 let list = null; 4981 if (engineOrder) { 4982 if (activeView === "watch") { 4983 list = isSlowMobile() ? engineOrder.watchMobile : engineOrder.watchDesktop; 4984 } else { 4985 list = engineOrder.miniPlayer; 4986 } 4987 } 4988 let sortedOptions = engineOptions; 4989 if (list && Array.isArray(list) && list.length > 0) { 4990 const sorted = []; 4991 for (const key of list) { 4992 const opt = engineOptions.find((o) => o.id === key); 4993 if (opt) sorted.push(opt); 4994 } 4995 sortedOptions = sorted; 4996 } 4997 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center", children: [
4998 /* @__PURE__ */ jsxRuntimeExports.jsx( 4999 "button", 5000 { 5001 onClick: () => setShowEngineMenu(!showEngineMenu), 5002 type: "button", 5003 className: `flex items-center font-mono text-[9px] font-black tracking-widest px-1.5 py-0.5 rounded border select-none transition-all duration-200 cursor-pointer shadow-sm hover:scale-105 active:scale-95 ${config.bg} ${config.border} ${config.text} ${config.shadow}`, 5004 style: { textShadow: `0 0 4px ${config.color}` }, 5005 children: config.label 5006 } 5007 ), 5008 showEngineMenu && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-8 right-0 w-64 bg-white dark:bg-[#1a1a1a] border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-2xl p-3 z-[110] backdrop-blur-md animate-in slide-in-from-bottom-3 text-zinc-900 dark:text-white", children: [ 5009 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-2 pb-2 mb-1 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-[#111] flex items-center justify-between rounded-t-xl -mx-3 -mt-3 p-3", children: [
5010 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider", children: "Playback Engine" }), 5011 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShowEngineMenu(false), className: "text-zinc-500 hover:text-zinc-900 dark:hover:text-white", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 12 }) }) 5012 ] }), 5013 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-1 max-h-[300px] overflow-y-auto pr-1 mt-2", children: sortedOptions.map((opt) => /* @__PURE__ */ jsxRuntimeExports.jsx(React.Fragment, { children: renderEngineOption(opt.id, opt.label, opt.color, opt.title, opt.desc, opt.disabled) }, opt.id)) }) 5014 ] }) 5015 ] }); 5016 }; 5017 const { waveform } = useWaveform(String(track.id), track.waveformJsonPath, track.waveform); 5018 const titleContainerRef = reactExports.useRef(null); 5019 const titleContentRef = reactExports.useRef(null); 5020 const [shouldScroll, setShouldScroll] = reactExports.useState(false); 5021 const [marqueeScrollDist, setMarqueeScrollDist] = reactExports.useState(0); 5022 const [marqueeDuration, setMarqueeDuration] = reactExports.useState("0s"); 5023 const activeAudioSrc = reactExports.useMemo(() => { 5024 if (audioMode === "singleopus" && track.opusPath) return track.opusPath; 5025 if (track.mp3PathHigh) return track.mp3PathHigh; 5026 return track.mp3Path || ""; 5027 }, [track.mp3Path, track.mp3PathHigh, track.opusPath, audioMode]); 5028 const [castAvailable, setCastAvailable] = reactExports.useState(false); 5029 const [castSession, setCastSession] = reactExports.useState(null); 5030 const originalTempo = ((_b = track.midiMetadata) == null ? void 0 : _b.tempo) || 120; 5031 const [currentTempo, setCurrentTempo] = reactExports.useState(originalTempo); 5032 const isBlocked = isContentBlocked(country, (_c = track.midiMetadata) == null ? void 0 : _c.blockedRegions); 5033 const visualWaveform = reactExports.useMemo(() => { 5034 return waveform.length > 0 ? [...waveform] : generatePseudoWaveform(track.id, 1e3); 5035 }, [track.id, waveform, containerWidth]); 5036 reactExports.useEffect(() => { 5037 if (!progressContainerRef.current) return; 5038 const target = progressContainerRef.current; 5039 const ro = new ResizeObserver((entries) => { 5040 for (const entry of entries) { 5041 setContainerWidth(entry.contentRect.width); 5042 } 5043 }); 5044 ro.observe(target); 5045 return () => ro.disconnect(); 5046 }, [fullscreenElement]); 5047 reactExports.useEffect(() => { 5048 if (!titleContainerRef.current || !titleContentRef.current) return; 5049 const container2 = titleContainerRef.current; 5050 const content = titleContentRef.current; 5051 const updateMarquee = () => { 5052 const isOverflowing = content.scrollWidth > container2.clientWidth; 5053 setShouldScroll(isOverflowing); 5054 if (isOverflowing) { 5055 const gap = 48; 5056 const dist = content.scrollWidth + gap; 5057 setMarqueeScrollDist(-dist); 5058 const minSpeed = 30; 5059 const scrollTime = dist / minSpeed; 5060 const totalTime = Math.max(15, scrollTime + 3); 5061 setMarqueeDuration(`${totalTime}s`); 5062 } 5063 }; 5064 const timer = setTimeout(updateMarquee, 100); 5065 const ro = new ResizeObserver(updateMarquee); 5066 ro.observe(container2); 5067 return () => { 5068 clearTimeout(timer); 5069 ro.disconnect(); 5070 }; 5071 }, [track.id, track.title, track.artist, containerWidth]); 5072 reactExports.useEffect(() => { 5073 const handleKeyDown = (e) => { 5074 var _a2; 5075 if (e.code === "Space") { 5076 const activeTag = (_a2 = document.activeElement) == null ? void 0 : _a2.tagName.toLowerCase(); 5077 if (activeTag === "input" || activeTag === "textarea") return; 5078 e.preventDefault(); 5079 setPlaybackError(false); 5080 if (!isPlaying && audioRef.current && duration > 0 && currentTime >= duration - 0.1) { 5081 audioRef.current.currentTime = 0; 5082 setCurrentTime(0); 5083 setProgress(0); 5084 onTimeUpdate(0, duration); 5085 if (["synth", "webmidi", "opus", "wasmsynth", "chiptune", "fmsynth"].includes(audioMode)) synth.seek(0); 5086 } 5087 const actx = synth.getAudioContext(); 5088 if (actx.state === "suspended") { 5089 actx.resume().catch(() => { 5090 }); 5091 } 5092 setIsPlaying(!isPlaying); 5093 } 5094 }; 5095 window.addEventListener("keydown", handleKeyDown); 5096 return () => window.removeEventListener("keydown", handleKeyDown); 5097 }, [isPlaying, setIsPlaying, currentTime, duration, audioMode, onTimeUpdate]); 5098 const loadMediaToCast = reactExports.useCallback((session) => { 5099 if (!session || !activeAudioSrc) return; 5100 const cast = window.cast; 5101 const mediaInfo = new cast.framework.messages.MediaInfo(activeAudioSrc, "audio/opus"); 5102 mediaInfo.metadata = new cast.framework.messages.MusicTrackMediaMetadata(); 5103 mediaInfo.metadata.title = track.title; 5104 mediaInfo.metadata.artist = track.artist && !["unknown", "unknown artist"].includes(track.artist.toLowerCase().trim()) ? track.artist : void 0; 5105 mediaInfo.metadata.images = [{ url: track.thumbnail }]; 5106 const request = new cast.framework.messages.LoadRequest(mediaInfo); 5107 session.loadMedia(request).then( 5108 () => { 5109 console.log("Cast media loaded"); 5110 }, 5111 // eslint-disable-next-line @typescript-eslint/no-explicit-any 5112 (errorCode) => { 5113 console.error("Cast load error", errorCode); 5114 } 5115 ); 5116 }, [activeAudioSrc, track.title, track.artist, track.thumbnail]); 5117 reactExports.useEffect(() => { 5118 window["__onGCastApiAvailable"] = (isAvailable) => { 5119 if (isAvailable) { 5120 setCastAvailable(true); 5121 const cast = window.cast; 5122 cast.framework.CastContext.getInstance().setOptions({ 5123 receiverApplicationId: cast.framework.CastContext.DEFAULT_MEDIA_RECEIVER_APP_ID, 5124 autoJoinPolicy: cast.framework.AutoJoinPolicy.ORIGIN_SCOPED 5125 }); 5126 const context = cast.framework.CastContext.getInstance(); 5127 context.addEventListener( 5128 cast.framework.CastContextEventType.SESSION_STATE_CHANGED, 5129 // eslint-disable-next-line @typescript-eslint/no-explicit-any 5130 (event) => { 5131 if (event.sessionState === cast.framework.SessionState.SESSION_STARTED) { 5132 setCastSession(event.session); 5133 loadMediaToCast(event.session); 5134 } else if (event.sessionState === cast.framework.SessionState.SESSION_ENDED) { 5135 setCastSession(null); 5136 } 5137 } 5138 ); 5139 } 5140 }; 5141 }, [loadMediaToCast]); 5142 reactExports.useEffect(() => { 5143 if (synth.midiAccess) { 5144 setMidiOutputs(synth.getMidiOutputs()); 5145 setSelectedMidiId(synth.getActiveMidiOutputId() || ""); 5146 synth.midiAccess.onstatechange = () => { 5147 setMidiOutputs(synth.getMidiOutputs()); 5148 setSelectedMidiId(synth.getActiveMidiOutputId() || ""); 5149 }; 5150 } 5151 }, []); 5152 reactExports.useEffect(() => { 5153 const handleFullscreenChange = () => { 5154 const el = document.fullscreenElement; 5155 setFullscreenElement(el);
5156 if (el) setShowFullscreenControls(true); 5157 }; 5158 document.addEventListener("fullscreenchange", handleFullscreenChange); 5159 return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); 5160 }, []); 5161 reactExports.useEffect(() => { 5162 if (!fullscreenElement) return; 5163 const handleMouseMove = () => { 5164 if (isPinned) { 5165 setShowFullscreenControls(true); 5166 if (hideControlsTimer.current) clearTimeout(hideControlsTimer.current); 5167 return; 5168 } 5169 setShowFullscreenControls(true); 5170 if (hideControlsTimer.current) clearTimeout(hideControlsTimer.current); 5171 hideControlsTimer.current = setTimeout(() => { 5172 if (!isHoveringControls.current) { 5173 setShowFullscreenControls(false); 5174 } 5175 }, 2500); 5176 }; 5177 fullscreenElement.addEventListener("mousemove", handleMouseMove); 5178 handleMouseMove(); 5179 return () => { 5180 fullscreenElement.removeEventListener("mousemove", handleMouseMove); 5181 if (hideControlsTimer.current) clearTimeout(hideControlsTimer.current); 5182 }; 5183 }, [fullscreenElement, isPinned]); 5184 reactExports.useEffect(() => { 5185 if (castSession) { 5186 loadMediaToCast(castSession); 5187 } 5188 }, [castSession, loadMediaToCast]); 5189 const handleCastClick = () => { 5190 if (castAvailable) { 5191 window.cast.framework.CastContext.getInstance().requestSession(); 5192 } else { 5193 console.warn("Cast API not available"); 5194 } 5195 }; 5196 const getWebMidiStatusLabel = () => { 5197 if (typeof navigator === "undefined") return ""; 5198 const hasSupport = !!navigator.requestMIDIAccess; 5199 if (!hasSupport) { 5200 if (window.location.protocol !== "https:" && window.location.hostname !== "localhost" && window.location.hostname !== "127.0.0.1") { 5201 return "Web MIDI requires HTTPS (secure context)."; 5202 } 5203 const ua = navigator.userAgent.toLowerCase(); 5204 if (ua.includes("firefox")) { 5205 return "Ensure dom.webmidi.enabled is true in Firefox about:config."; 5206 } 5207 if (ua.includes("ipad") || ua.includes("iphone") || ua.includes("macintosh") && navigator.maxTouchPoints > 0) { 5208 return "Not supported on iOS/iPadOS browsers."; 5209 } 5210 if (ua.includes("safari") && !ua.includes("chrome") && !ua.includes("chromium")) { 5211 return "Not supported by Safari. Try Chrome/Edge."; 5212 } 5213 return "Not supported by browser. Try Chrome/Edge."; 5214 } 5215 if (synth.midiAccessStatus === "denied") { 5216 return "MIDI permission denied. Check browser settings."; 5217 } 5218 if (!synth.midiAccess) { 5219 return "Click to enable Web MIDI"; 5220 } 5221 if (midiOutputs.length === 0) { 5222 return "No MIDI outputs found. Connect a MIDI device."; 5223 } 5224 return null; 5225 }; 5226 reactExports.useEffect(() => { 5227 if (audioRef.current && activeAudioSrc) { 5228 if (isPlaying && !isBlocked && (audioMode === "opus" || audioMode === "singleopus")) { 5229 const playPromise = audioRef.current.play(); 5230 if (playPromise !== void 0) { 5231 playPromise.catch((e) => { 5232 console.warn("Playback prevented:", e.message); 5233 }); 5234 } 5235 } else { 5236 audioRef.current.pause(); 5237 } 5238 } 5239 if (castSession) { 5240 const playerController = new window.cast.framework.RemotePlayerController(new window.cast.framework.RemotePlayer()); 5241 if (isPlaying && !isBuffering) playerController.playOrPause(); 5242 } 5243 }, [isPlaying, track.id, isBlocked, activeAudioSrc, isBuffering, castSession, audioMode]); 5244 reactExports.useEffect(() => { 5245 if (audioRef.current && (audioMode === "opus" || audioMode === "singleopus") && Math.abs(audioRef.current.currentTime - currentTime) > 0.5) { 5246 audioRef.current.currentTime = currentTime; 5247 } 5248 }, [audioMode]); 5249 reactExports.useEffect(() => { 5250 synth.audioMode = audioMode; 5251 }, [audioMode]); 5252 reactExports.useEffect(() => { 5253 if (!["synth", "webmidi", "chiptune", "wasmsynth", "fmsynth", "opus"].includes(audioMode)) { 5254 synth.stop(); 5255 } 5256 }, [audioMode]); 5257 reactExports.useEffect(() => { 5258 if (audioRef.current) { 5259 if (["synth", "webmidi", "chiptune", "wasmsynth", "fmsynth", "opus"].includes(audioMode)) { 5260 audioRef.current.volume = 0; 5261 audioRef.current.muted = true; 5262 if (["synth", "wasmsynth", "fmsynth", "chiptune", "opus"].includes(audioMode)) { 5263 synth.setMasterVolume(isMuted ? 0 : volume); 5264 } 5265 } else { 5266 audioRef.current.volume = volume; 5267 audioRef.current.muted = isMuted; 5268 } 5269 const rate = currentTempo / originalTempo; 5270 audioRef.current.playbackRate = Math.max(0.25, Math.min(4, rate)); 5271 if (["synth", "webmidi", "chiptune", "wasmsynth", "fmsynth", "opus"].includes(audioMode)) { 5272 synth.setPlaybackRate(Math.max(0.25, Math.min(4, rate))); 5273 if (["synth", "wasmsynth", "fmsynth", "chiptune", "opus"].includes(audioMode)) {
5274 synth.setPitchSemitones(pitchSemitones); 5275 } 5276 } 5277 onPlaybackRateChange == null ? void 0 : onPlaybackRateChange(rate); 5278 } 5279 }, [volume, isMuted, currentTempo, pitchSemitones, originalTempo, audioMode, onPlaybackRateChange, soundfontVolume]); 5280 reactExports.useEffect(() => { 5281 const handleSetPitch = (e) => { 5282 const customEvent = e; 5283 setPitchSemitones(customEvent.detail); 5284 }; 5285 window.addEventListener("setPitchSemitones", handleSetPitch); 5286 return () => window.removeEventListener("setPitchSemitones", handleSetPitch); 5287 }, [setPitchSemitones]); 5288 reactExports.useEffect(() => { 5289 const handleSetPlaybackRate = (e) => { 5290 const customEvent = e; 5291 setCurrentTempo(originalTempo * customEvent.detail); 5292 }; 5293 window.addEventListener("setPlaybackRate", handleSetPlaybackRate); 5294 return () => window.removeEventListener("setPlaybackRate", handleSetPlaybackRate); 5295 }, [originalTempo]); 5296 reactExports.useEffect(() => { 5297 const handleSetTrackDuration = (e) => { 5298 const customEvent = e; 5299 const newDuration = customEvent.detail; 5300 setDuration(newDuration); 5301 }; 5302 window.addEventListener("setTrackDuration", handleSetTrackDuration); 5303 return () => window.removeEventListener("setTrackDuration", handleSetTrackDuration); 5304 }, []); 5305 reactExports.useEffect(() => { 5306 var _a2; 5307 const metaDur = Number((_a2 = track == null ? void 0 : track.midiMetadata) == null ? void 0 : _a2.duration); 5308 const strDur = parseDurationString(track == null ? void 0 : track.duration); 5309 const initialDur = !isNaN(metaDur) && metaDur > 0 ? metaDur : strDur > 0 ? strDur : 0; 5310 if (initialDur > 0) { 5311 setDuration(initialDur); 5312 } 5313 }, [track == null ? void 0 : track.id, track == null ? void 0 : track.duration, (_d = track == null ? void 0 : track.midiMetadata) == null ? void 0 : _d.duration]); 5314 const lastSeekTimestamp = reactExports.useRef(0); 5315 reactExports.useEffect(() => { 5316 if (seekTo && audioRef.current && seekTo.timestamp !== lastSeekTimestamp.current) { 5317 lastSeekTimestamp.current = seekTo.timestamp; 5318 const newTime = seekTo.time; 5319 audioRef.current.currentTime = newTime; 5320 setCurrentTime(newTime); 5321 const dur = audioRef.current.duration || duration || 1; 5322 setProgress(newTime / dur * 100); 5323 onTimeUpdate(newTime, dur); 5324 if (["synth", "wasmsynth", "fmsynth", "chiptune", "webmidi", "opus"].includes(audioMode)) { 5325 synth.seek(newTime); 5326 } 5327 } 5328 }, [seekTo, audioMode, isPlaying, isBuffering, onTimeUpdate, duration]); 5329 const lastTrackChangeRef = reactExports.useRef(0); 5330 const [prevTrackId, setPrevTrackId] = reactExports.useState(track.id); 5331 if (track.id !== prevTrackId) { 5332 setPrevTrackId(track.id); 5333 lastTrackChangeRef.current = Date.now(); 5334 setProgress(0); 5335 setCurrentTime(0); 5336 setPlaybackError(false); 5337 setCurrentTempo(((_e = track.midiMetadata) == null ? void 0 : _e.tempo) || 120); 5338 setPitchSemitones(0); 5339 setHasStartedLoading(false); 5340 setTrackStates({}); 5341 setDuration(((_f = track.midiMetadata) == null ? void 0 : _f.duration) || parseDurationString(track.duration) || 0); 5342 } 5343 reactExports.useEffect(() => { 5344 if (audioRef.current) { 5345 audioRef.current.currentTime = 0; 5346 audioRef.current.playbackRate = 1; 5347 } 5348 }, [track.id]); 5349 const loadedAudioModeRef = reactExports.useRef(null); 5350 reactExports.useEffect(() => { 5351 var _a2, _b2; 5352 const isLiveSynthMode = audioMode === "synth" || audioMode === "webmidi" || audioMode === "chiptune" || audioMode === "wasmsynth" || audioMode === "fmsynth"; 5353 const isLiveOrOpus = isLiveSynthMode || audioMode === "opus"; 5354 if (activeView !== "watch" && isPlaying && isLiveOrOpus && (!hasStartedLoading || loadedAudioModeRef.current !== audioMode)) { 5355 console.log(`[PersistentPlayer] Non-watch page requested Play. Starting Download Sequence for mode: ${audioMode}...`); 5356 setHasStartedLoading(true); 5357 loadedAudioModeRef.current = audioMode; 5358 if (isLiveSynthMode) { 5359 setIsLiveSynthFallback(true); 5360 } 5361 const eventsSources = [ 5362 track.cleanMidiPath, 5363 track.midiPath
5364 ].filter(Boolean); 5365 if (eventsSources.length === 0) { 5366 console.error("[PersistentPlayer] Live synthesis error: No MIDI source path available."); 5367 setTrackStates({ 0: { ready: false, error: true, percent: 0 } }); 5368 setIsPlaying(false); 5369 return; 5370 } 5371 if (audioMode === "opus") { 5372 const tracksToLoad = track.instruments && track.instruments.length > 0 ? track.instruments : [{ 5373 name: "Master", 5374 program: 0, 5375 midiChannel: 0, 5376 notesCount: 0, 5377 stemPath: track.opusPath, 5378 trackId: 0 5379 }]; 5380 synth.audioMode = audioMode; 5381 synth.loadTracks( 5382 tracksToLoad, 5383 ((_a2 = track.midiMetadata) == null ? void 0 : _a2.tickDiv) || 480, 5384 ((_b2 = track.midiMetadata) == null ? void 0 : _b2.tempo) || 120, 5385 audioQuality, 5386 soundfont || "MusyngKite", 5387 track.pianoRollPath || track.cleanMidiPath || track.midiPath, 5388 (idx, status) => { 5389 setTrackStates((prev) => ({ ...prev, [idx]: status })); 5390 }, 5391 false, 5392 // forceLive 5393 "SoundBlasterOld.sf2", 5394 // defaultLiveSf 5395 false 5396 // isAdmin 5397 ).catch((err) => { 5398 console.error("[PersistentPlayer] Opus synthesis fallback error:", err); 5399 setTrackStates({ 0: { ready: false, error: true, percent: 0 } }); 5400 setIsPlaying(false); 5401 }); 5402 } else { 5403 parseMidiInstruments(eventsSources).then(({ tracks: liveFallbackTracks, duration: fallbackDuration, tempo: fallbackTempo, tickDiv: fallbackTickDiv }) => { 5404 var _a3, _b3, _c2; 5405 if (fallbackDuration > 0 && (!((_a3 = track.midiMetadata) == null ? void 0 : _a3.duration) || track.midiMetadata.duration === 0)) { 5406 window.dispatchEvent(new CustomEvent("setTrackDuration", { detail: fallbackDuration })); 5407 } 5408 window.dispatchEvent(new CustomEvent("liveFallbackTracksLoaded", { detail: liveFallbackTracks })); 5409 const tickDivToUse = ((_b3 = track.midiMetadata) == null ? void 0 : _b3.tickDiv) || fallbackTickDiv; 5410 const tempoToUse = ((_c2 = track.midiMetadata) == null ? void 0 : _c2.tempo) || fallbackTempo; 5411 let sfToLoad = soundfont || "MusyngKite"; 5412 if (lowPerfToggle) { 5413 const heavySfs = ["MusyngKite", "GeneralUser-GS", "FatBoy", "FluidR3_GM", "MuseScore_General", "Aspirin"]; 5414 if (heavySfs.includes(sfToLoad.replace(/\.h?sf[23]$/i, "").replace(/_JS$/i, ""))) { 5415 sfToLoad = "SoundBlasterOld.sf2"; 5416 } 5417 } 5418 synth.audioMode = audioMode; 5419 synth.loadTracks( 5420 liveFallbackTracks, 5421 tickDivToUse, 5422 tempoToUse, 5423 audioQuality, 5424 sfToLoad, 5425 eventsSources, 5426 (idx, status) => { 5427 setTrackStates((prev) => ({ ...prev, [idx]: status })); 5428 }, 5429 true, 5430 // forceLive 5431 "SoundBlasterOld.sf2", 5432 // defaultLiveSf 5433 false 5434 // isAdmin 5435 ); 5436 }).catch((err) => { 5437 console.error("[PersistentPlayer] Live synthesis fallback error:", err); 5438 setTrackStates({ 0: { ready: false, error: true, percent: 0 } }); 5439 setIsPlaying(false); 5440 }); 5441 } 5442 } 5443 }, [activeView, isPlaying, hasStartedLoading, audioMode, track, audioQuality, soundfont, setHasStartedLoading, setTrackStates, setIsPlaying, setIsLiveSynthFallback]); 5444 reactExports.useEffect(() => { 5445 if (activeView !== "watch" && (audioMode === "synth" || audioMode === "webmidi" || audioMode === "chiptune" || audioMode === "wasmsynth" || audioMode === "fmsynth" || audioMode === "opus")) { 5446 if (isPlaying && isSynthReady) { 5447 synth.play(currentTimeRef.current); 5448 } else if (!isPlaying) { 5449 synth.stop(); 5450 } 5451 } 5452 }, [isPlaying, audioMode, isSynthReady, activeView]); 5453 reactExports.useEffect(() => { 5454 if (activeView !== "watch" && (audioMode === "synth" || audioMode === "webmidi" || audioMode === "chiptune" || audioMode === "wasmsynth" || audioMode === "fmsynth" || audioMode === "opus")) { 5455 if (isPlaying && !isSynthReady) { 5456 setIsBuffering(true); 5457 } else { 5458 setIsBuffering(false); 5459 } 5460 } else if (activeView !== "watch" && audioMode !== "singleopus") { 5461 setIsBuffering(false); 5462 } 5463 }, [isPlaying, isSynthReady, audioMode, activeView, setIsBuffering]); 5464 const rafRef = reactExports.useRef(null); 5465 const onTimeUpdateRef = reactExports.useRef(onTimeUpdate); 5466 reactExports.useEffect(() => { 5467 onTimeUpdateRef.current = onTimeUpdate; 5468 }, [onTimeUpdate]); 5469 reactExports.useRef(0); 5470 reactExports.useRef(null); 5471 const isPlayingRef = reactExports.useRef(isPlaying); 5472 const isDraggingRef = reactExports.useRef(isDragging); 5473 const currentTimeRef = reactExports.useRef(currentTime); 5474 const durationRef = reactExports.useRef(duration); 5475 const audioModeRef = reactExports.useRef(audioMode); 5476 const previousModeRef = reactExports.useRef(audioMode); 5477 reactExports.useEffect(() => { 5478 audioModeRef.current = audioMode; 5479 if (previousModeRef.current !== audioMode) { 5480 setPlaybackError(false); 5481 setTrackStates({}); 5482 previousModeRef.current = audioMode; 5483 } 5484 }, [audioMode, setTrackStates]); 5485 reactExports.useEffect(() => { 5486 isPlayingRef.current = isPlaying; 5487 }, [isPlaying]); 5488 reactExports.useEffect(() => { 5489 isDraggingRef.current = isDragging; 5490 }, [isDragging]); 5491 reactExports.useEffect(() => { 5492 currentTimeRef.current = currentTime; 5493 }, [currentTime]); 5494 reactExports.useEffect(() => { 5495 durationRef.current = duration; 5496 }, [duration]); 5497 const updateTime = reactExports.useCallback(() => { 5498 if (audioRef.current && !isDraggingRef.current) { 5499 let curr = audioRef.current.currentTime; 5500 const currentMode = audioModeRef.current; 5501 const isSynthMode = ["synth", "webmidi", "opus", "wasmsynth", "chiptune", "fmsynth"].includes(currentMode); 5502 const dur = isSynthMode ? durationRef.current || audioRef.current.duration || 1 : audioRef.current.duration || durationRef.current || 1; 5503 if (isSynthMode) { 5504 const synthTime = synth.getCurrentTime(); 5505 if (synthTime === 0 && currentTimeRef.current > 0) { 5506 curr = currentTimeRef.current; 5507 } else { 5508 curr = synthTime; 5509 } 5510 if (dur > 1) { 5511 curr = Math.min(curr, dur); 5512 if (curr >= dur && isPlayingRef.current && Date.now() - lastTrackChangeRef.current > 1e3) { 5513 onNext(false); 5514 } 5515 } 5516 } 5517 setCurrentTime(curr); 5518 setProgress(curr / dur * 100); 5519 if (!isSynthMode) { 5520 setDuration(dur); 5521 } 5522 onTimeUpdateRef.current(curr, dur); 5523 } 5524 if (isPlayingRef.current) { 5525 rafRef.current = requestAnimationFrame(updateTime); 5526 } 5527 }, [setIsPlaying, onNext]); 5528 reactExports.useEffect(() => { 5529 if (isPlaying) { 5530 rafRef.current = requestAnimationFrame(updateTime); 5531 } else if (rafRef.current) { 5532 cancelAnimationFrame(rafRef.current); 5533 } 5534 return () => { 5535 if (rafRef.current) cancelAnimationFrame(rafRef.current); 5536 }; 5537 }, [isPlaying, updateTime]); 5538 const calculateProgressFromEvent = reactExports.useCallback((clientX) => { 5539 if (!progressContainerRef.current) return 0; 5540 const rect = progressContainerRef.current.getBoundingClientRect(); 5541 const x = clientX - rect.left; 5542 const width = rect.width; 5543 return Math.max(0, Math.min(100, x / width * 100)); 5544 }, []); 5545 const updateScrubPosition = reactExports.useCallback((newProgress) => { 5546 if (!audioRef.current) return; 5547 const trackDuration = audioRef.current.duration || duration || 0; 5548 const newTime = newProgress / 100 * trackDuration; 5549 if (isFinite(newTime) && trackDuration > 0) { 5550 audioRef.current.currentTime = newTime; 5551 setCurrentTime(newTime); 5552 setProgress(newProgress); 5553 onTimeUpdate(newTime, trackDuration); 5554 if (["synth", "wasmsynth", "fmsynth", "chiptune", "webmidi", "opus"].includes(audioMode)) { 5555 synth.seek(newTime); 5556 } 5557 } 5558 }, [onTimeUpdate, audioMode, duration]); 5559 const handleMouseDown = (e) => { 5560 setIsDragging(true); 5561 const newProgress = calculateProgressFromEvent(e.clientX); 5562 updateScrubPosition(newProgress); 5563 }; 5564 reactExports.useEffect(() => { 5565 const handleMouseMove = (e) => { 5566 if (isDragging) { 5567 e.preventDefault(); 5568 const newProgress = calculateProgressFromEvent(e.clientX); 5569 updateScrubPosition(newProgress); 5570 } 5571 }; 5572 const handleMouseUp = () => { 5573 if (isDragging) { 5574 setIsDragging(false); 5575 } 5576 }; 5577 if (isDragging) { 5578 window.addEventListener("mousemove", handleMouseMove); 5579 window.addEventListener("mouseup", handleMouseUp); 5580 } 5581 return () => { 5582 window.removeEventListener("mousemove", handleMouseMove); 5583 window.removeEventListener("mouseup", handleMouseUp); 5584 }; 5585 }, [isDragging, calculateProgressFromEvent, updateScrubPosition]); 5586 const handleVolumeChange = (e) => { 5587 if (!volumeRef.current) return; 5588 const rect = volumeRef.current.getBoundingClientRect(); 5589 const height = rect.height; 5590 const y = e.clientY - rect.top; 5591 let newVolume = (height - y) / height; 5592 newVolume = Math.max(0, Math.min(1, newVolume)); 5593 setVolume(newVolume); 5594 setIsMuted(newVolume === 0); 5595 }; 5596 const formatTime = (seconds) => { 5597 if (!isFinite(seconds)) return "0:00"; 5598 const m = Math.floor(seconds / 60); 5599 const s = Math.floor(seconds % 60); 5600 return `${m}:${s.toString().padStart(2, "0")}`; 5601 }; 5602 const playbackRate = currentTempo / originalTempo; 5603 const canvasRefBg = reactExports.useRef(null); 5604 const canvasRefFg = reactExports.useRef(null); 5605 const drawWaveform = reactExports.useCallback((canvas, isForeground) => { 5606 if (!canvas || containerWidth === 0) return; 5607 const ctx = canvas.getContext("2d"); 5608 if (!ctx) return; 5609 const dpr = window.devicePixelRatio || 1; 5610 const rect = canvas.getBoundingClientRect(); 5611 canvas.width = rect.width * dpr; 5612 canvas.height = rect.height * dpr; 5613 ctx.scale(dpr, dpr); 5614 ctx.clearRect(0, 0, rect.width, rect.height); 5615 const data = visualWaveform; 5616 const width = rect.width; 5617 const height = rect.height; 5618 const centerY = height / 2; 5619 const step = width / data.length; 5620 ctx.beginPath(); 5621 for (let i = 0; i < data.length; i++) { 5622 const val = data[i]; 5623 const x = i * step; 5624 const yAmplitude = val * height / 2; 5625 ctx.moveTo(x, centerY - yAmplitude); 5626 ctx.lineTo(x, centerY + yAmplitude); 5627 } 5628 ctx.lineWidth = 1; 5629 let fgColor = "#ffffff"; 5630 let bgColor = "#ffffff40"; 5631 if (audioMode === "synth") { 5632 fgColor = "#10b981"; 5633 bgColor = "#10b98140"; 5634 } else if (audioMode === "wasmsynth") { 5635 fgColor = "#06b6d4"; 5636 bgColor = "#06b6d440"; 5637 } else if (audioMode === "fmsynth") { 5638 fgColor = "#8b5cf6"; 5639 bgColor = "#8b5cf640"; 5640 } else if (audioMode === "webmidi") { 5641 fgColor = "#6366f1"; 5642 bgColor = "#6366f140"; 5643 } else if (audioMode === "opus") { 5644 fgColor = "#ff5500"; 5645 bgColor = "#ff550040"; 5646 } else if (audioMode === "chiptune") { 5647 fgColor = "#f59e0b"; 5648 bgColor = "#f59e0b40"; 5649 } else { 5650 fgColor = "#3b82f6"; 5651 bgColor = "#3b82f640"; 5652 } 5653 if (isForeground) { 5654 ctx.strokeStyle = fgColor; 5655 } else { 5656 ctx.strokeStyle = bgColor; 5657 } 5658 ctx.stroke(); 5659 }, [visualWaveform, containerWidth, fullscreenElement, audioMode]); 5660 reactExports.useEffect(() => { 5661 const timer = setTimeout(() => { 5662 drawWaveform(canvasRefBg.current, false); 5663 drawWaveform(canvasRefFg.current, true); 5664 }, 50); 5665 return () => clearTimeout(timer); 5666 }, [drawWaveform, fullscreenElement]); 5667 if (isBlocked) return null; 5668 const PlayerUI = /* @__PURE__ */ jsxRuntimeExports.jsx( 5669 "div", 5670 { 5671 className: `fixed bottom-0 left-0 right-0 border-t z-[100] shadow-[0_-4px_6px_-1px_rgba(0,0,0,0.1)] flex flex-col transition-all duration-300 ${fullscreenElement ? "bg-[var(--visualizer-bg,#050505)] border-white/10 text-white w-full" : "bg-white dark:bg-[#111] border-zinc-200 dark:border-zinc-800"}
5671 ${fullscreenElement && !showFullscreenControls ? "translate-y-full opacity-0" : "translate-y-0 opacity-100"}`, 5672 onMouseEnter: () => { 5673 isHoveringControls.current = true; 5674 if (hideControlsTimer.current) clearTimeout(hideControlsTimer.current); 5675 setShowFullscreenControls(true); 5676 }, 5677 onMouseLeave: () => { 5678 isHoveringControls.current = false; 5679 }, 5680 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex items-center justify-between px-2 sm:px-4 md:px-8 max-w-[1800px] mx-auto w-full gap-2 sm:gap-4 h-20", children: [ 5681 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 sm:gap-2 md:gap-4 shrink-0", children: [ 5682 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center gap-1 sm:gap-2 md:gap-4 ${isGameMode ? "opacity-50 pointer-events-none" : ""}`, children: [ 5683 /* @__PURE__ */ jsxRuntimeExports.jsx( 5684 "button", 5685 { 5686 className: `hidden lg:block transition-colors ${fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-400 hover:text-black dark:hover:text-white"}`, 5687 title: t("player.startOver", "Start from Beginning"), 5688 onClick: () => { 5689 if (audioRef.current) { 5690 audioRef.current.currentTime = 0; 5691 setCurrentTime(0); 5692 setProgress(0); 5693 onTimeUpdate(0, duration); 5694 if (["synth", "webmidi", "opus", "wasmsynth", "chiptune", "fmsynth"].includes(audioMode)) synth.seek(0); 5695 } 5696 }, 5697 children: /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 16 }) 5698 } 5699 ),
5700 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: `transition-colors ${fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-400 hover:text-black dark:hover:text-white"}`, title: t("player.prev", "Previous"), onClick: onPrev, children: /* @__PURE__ */ jsxRuntimeExports.jsx(SkipBack, { size: 18, fill: "currentColor" }) }), 5701 /* @__PURE__ */ jsxRuntimeExports.jsx( 5702 "button", 5703 { 5704 className: `hidden sm:block transition-colors ${fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-400 hover:text-black dark:hover:text-white"}`, 5705 title: t("player.rewind", "Fast backward"), 5706 onClick: () => { 5707 if (audioRef.current) { 5708 const newTime = Math.max(0, audioRef.current.currentTime - 10); 5709 audioRef.current.currentTime = newTime; 5710 setCurrentTime(newTime); 5711 setProgress(newTime / duration * 100); 5712 onTimeUpdate(newTime, duration); 5713 if (audioMode === "synth" || audioMode === "webmidi") synth.seek(newTime); 5714 } 5715 }, 5716 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
5717 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "11 19 2 12 11 5 11 19" }), 5718 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "22 19 13 12 22 5 22 19" }) 5719 ] }) 5720 } 5721 ) 5722 ] }), 5723 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 5724 "button", 5725 { 5726 onClick: () => { 5727 setPlaybackError(false); 5728 if (!isPlaying && audioRef.current && duration > 0 && currentTime >= duration - 0.1) { 5729 audioRef.current.currentTime = 0; 5730 setCurrentTime(0); 5731 setProgress(0); 5732 onTimeUpdate(0, duration); 5733 if (["synth", "webmidi", "opus", "wasmsynth", "chiptune", "fmsynth"].includes(audioMode)) synth.seek(0); 5734 } 5735 const actx = synth.getAudioContext(); 5736 if (actx.state === "suspended") { 5737 actx.resume().catch(() => { 5738 }); 5739 } 5740 setIsPlaying(!isPlaying); 5741 }, 5742 className: `hover:scale-105 transition-transform relative ${fullscreenElement ? "text-white" : "text-black dark:text-white"}`, 5743 title: isPlaying ? t("player.pause", "Pause") : t("player.play", "Play"), 5744 children: [
5745 isBuffering ? /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "animate-spin text-indigo-500", size: 30 }) : isPlaying ? /* @__PURE__ */ jsxRuntimeExports.jsx(Pause, { size: 30, fill: "currentColor" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 30, fill: "currentColor" }), 5746 playbackError && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full border border-white dark:border-[#111]", title: t("player.audioError", "Audio Error") }) 5747 ] 5748 } 5749 ) }), 5750 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center gap-1 sm:gap-2 md:gap-4 ${isGameMode ? "opacity-50 pointer-events-none" : ""}`, children: [ 5751 /* @__PURE__ */ jsxRuntimeExports.jsx( 5752 "button", 5753 { 5754 className: `hidden sm:block transition-colors ${fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-400 hover:text-black dark:hover:text-white"}`, 5755 title: t("player.fastForward", "Fast forward"), 5756 onClick: () => { 5757 if (audioRef.current) { 5758 const newTime = Math.min(duration, audioRef.current.currentTime + 10); 5759 audioRef.current.currentTime = newTime; 5760 setCurrentTime(newTime); 5761 setProgress(newTime / duration * 100); 5762 onTimeUpdate(newTime, duration); 5763 if (audioMode === "synth" || audioMode === "webmidi") synth.seek(newTime); 5764 } 5765 }, 5766 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "18", height: "18", viewBox: "0 0 24 24", fill: "currentColor", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
5767 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "13 19 22 12 13 5 13 19" }), 5768 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "2 19 11 12 2 5 2 19" }) 5769 ] }) 5770 } 5771 ), 5772 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: `transition-colors ${fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-400 hover:text-black dark:hover:text-white"}`, title: t("player.next", "Next"), onClick: () => onNext(true), children: /* @__PURE__ */ jsxRuntimeExports.jsx(SkipForward, { size: 18, fill: "currentColor" }) }), 5773 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "text-zinc-400 hover:text-[#f50] transition-colors hidden lg:block", title: t("player.shuffle", "Shuffle"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(Shuffle, { size: 16 }) }) 5774 ] }) 5775 ] }), 5776 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex items-center min-w-0 gap-2", children: [
5777 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "hidden sm:flex items-center gap-2 sm:gap-3 shrink-0 max-w-[120px] sm:max-w-[200px] lg:max-w-[300px] min-w-0", children: [ 5778 track.status === "completed" ? getThumbnailElement(track, `h-10 w-[60px] aspect-[1.5/1] bg-zinc-200 dark:bg-zinc-800 rounded shadow-sm shrink-0 ${onTrackClick ? "cursor-pointer" : "cursor-default"}`, onTrackClick) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-10 w-[60px] shrink-0" }), 5779 /* @__PURE__ */ jsxRuntimeExports.jsx( 5780 "div", 5781 { 5782 ref: titleContainerRef, 5783 className: "flex-1 flex flex-col justify-center min-w-0 hidden sm:flex h-full relative overflow-hidden", 5784 children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 5785 "div", 5786 { 5787 className: `flex min-w-max gap-12 ${shouldScroll ? "animate-marquee-infinite" : ""}`, 5788 style: { 5789 "--scroll-dist": `${marqueeScrollDist}px`, 5790 "--animation-duration": marqueeDuration 5791 }, 5792 children: [
5793 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: titleContentRef, className: "flex flex-col min-w-0", children: [ 5794 track.artist && !["unknown", "unknown artist"].includes(track.artist.toLowerCase().trim()) && /* @__PURE__ */ jsxRuntimeExports.jsx( 5795 "a", 5796 { 5797 href: `/?q=${encodeURIComponent(track.artist)}`, 5798 onClick: (e) => { 5799 if (!e.metaKey && !e.ctrlKey && !e.shiftKey && e.button !== 1) { 5800 e.preventDefault(); 5801 e.stopPropagation(); 5802 window.location.href = `/?q=${encodeURIComponent(track.artist)}`; 5803 } else { 5804 e.stopPropagation(); 5805 } 5806 }, 5807 className: `text-xs hover:underline cursor-pointer hover:text-indigo-400 transition-colors ${fullscreenElement ? "text-zinc-400" : "text-zinc-500"}`, 5808 title: `Explore all tracks by ${track.artist}`, 5809 children: track.artist 5810 } 5811 ),
5812 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { onClick: onTrackClick, className: `text-sm font-medium flex items-center gap-2 whitespace-nowrap ${onTrackClick ? "cursor-pointer hover:underline" : "cursor-default"} ${fullscreenElement ? "text-white" : "text-zinc-900 dark:text-white"}`, children: [ 5813 track.title, 5814 playbackError && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { title: t("player.audioError", "Audio Unavailable"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 12, className: "text-red-500" }) }) 5815 ] }) 5816 ] }), 5817 shouldScroll && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col min-w-0", children: [ 5818 track.artist && !["unknown", "unknown artist"].includes(track.artist.toLowerCase().trim()) && /* @__PURE__ */ jsxRuntimeExports.jsx( 5819 "a", 5820 { 5821 href: `/?q=${encodeURIComponent(track.artist)}`, 5822 onClick: (e) => { 5823 if (!e.metaKey && !e.ctrlKey && !e.shiftKey && e.button !== 1) { 5824 e.preventDefault(); 5825 e.stopPropagation(); 5826 window.location.href = `/?q=${encodeURIComponent(track.artist)}`; 5827 } else { 5828 e.stopPropagation(); 5829 } 5830 }, 5831 className: `text-xs hover:underline cursor-pointer hover:text-indigo-400 transition-colors ${fullscreenElement ? "text-zinc-400" : "text-zinc-500"}`, 5832 title: `Explore all tracks by ${track.artist}`, 5833 children: track.artist 5834 } 5835 ),
5836 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { onClick: onTrackClick, className: `text-sm font-medium flex items-center gap-2 whitespace-nowrap ${onTrackClick ? "cursor-pointer hover:underline" : "cursor-default"} ${fullscreenElement ? "text-white" : "text-zinc-900 dark:text-white"}`, children: [ 5837 track.title, 5838 playbackError && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { title: t("player.audioError", "Audio Unavailable"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 12, className: "text-red-500" }) }) 5839 ] }) 5840 ] }) 5841 ] 5842 } 5843 ) 5844 } 5845 ), 5846 /* @__PURE__ */ jsxRuntimeExports.jsx( 5847 "button", 5848 { 5849 onClick: () => toggleLike(track.id), 5850 className: `hidden sm:flex p-1.5 rounded-full border transition-all ${isLiked ? "border-[#f50] text-[#f50]" : "border-zinc-300 dark:border-zinc-700 text-zinc-400 hover:border-zinc-500"}`, 5851 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Heart, { size: 14, fill: isLiked ? "currentColor" : "none" }) 5852 } 5853 ) 5854 ] }),
5855 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 h-10 flex items-center gap-1 sm:gap-2 px-1 sm:px-4 min-w-0", children: [ 5856 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `text-xs font-mono shrink-0 min-w-[30px] sm:min-w-[35px] text-right ${fullscreenElement ? "text-zinc-400" : "text-zinc-500 dark:text-zinc-400"}`, children: formatTime(currentTime) }), 5857 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5858 "div", 5859 { 5860 ref: progressContainerRef, 5861 className: `flex-1 h-8 relative group select-none bg-black rounded overflow-hidden ${isGameMode ? "pointer-events-none" : "cursor-pointer"}`, 5862 onMouseDown: handleMouseDown, 5863 children: [ 5864 /* @__PURE__ */ jsxRuntimeExports.jsx( 5865 "canvas", 5866 { 5867 ref: canvasRefBg, 5868 className: "absolute inset-0 w-full h-full pointer-events-none" 5869 } 5870 ),
5871 /* @__PURE__ */ jsxRuntimeExports.jsx( 5872 "div", 5873 { 5874 className: "absolute inset-0 overflow-hidden border-r-2", 5875 style: { 5876 width: `${progress}%`, 5877 borderColor: audioMode === "synth" ? "#10b981" : audioMode === "wasmsynth" ? "#06b6d4" : audioMode === "fmsynth" ? "#8b5cf6" : audioMode === "webmidi" ? "#6366f1" : audioMode === "opus" ? "#ff5500" : audioMode === "chiptune" ? "#f59e0b" : "#3b82f6" 5878 }, 5879 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 5880 "canvas", 5881 { 5882 ref: canvasRefFg, 5883 className: "absolute inset-0 h-full pointer-events-none", 5884 style: { width: containerWidth ? `${containerWidth}px` : "100%" } 5885 } 5886 ) 5887 } 5888 ), 5889 /* @__PURE__ */ jsxRuntimeExports.jsx( 5890 "div", 5891 { 5892 className: `absolute top-0 bottom-0 w-2 bg-transparent -ml-1`, 5893 style: { left: `${progress}%` }, 5894 children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-4 h-4 bg-white shadow-md border border-zinc-300 rounded-full cursor-grab active:cursor-grabbing" }) 5895 } 5896 ), 5897 isBuffering && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/85 z-10 flex items-center justify-center backdrop-blur-[1px] transition-opacity duration-300", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-11/12 max-w-[280px] flex flex-col gap-1.5 px-3", children: [
5898 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center text-[11px] font-semibold tracking-wide text-zinc-300 uppercase", children: [ 5899 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5", children: [ 5900 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-indigo-500 animate-ping" }), 5901 "Downloading audio..." 5902 ] }), 5903 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-xs", children: [ 5904 Math.round(downloadProgress), 5905 "%" 5906 ] }) 5907 ] }), 5908 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-1 bg-zinc-800 rounded-full overflow-hidden border border-zinc-700/30", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 5909 "div", 5910 { 5911 className: "h-full bg-gradient-to-r from-indigo-500 to-purple-500 rounded-full transition-all duration-300 shadow-[0_0_8px_rgba(99,102,241,0.5)] animate-pulse", 5912 style: { width: `${downloadProgress}%` } 5913 } 5914 ) }) 5915 ] }) }) 5916 ] 5917 } 5918 ),
5919 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 shrink-0 select-none", children: [ 5920 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `text-xs font-mono min-w-[30px] sm:min-w-[35px] ${fullscreenElement ? "text-zinc-400" : "text-zinc-500 dark:text-zinc-400"}`, children: formatTime(duration) }), 5921 renderRetroLcdIndicator() 5922 ] }) 5923 ] }) 5924 ] }), 5925 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `hidden md:flex items-center gap-3 shrink-0 justify-end border-l border-zinc-200 dark:border-zinc-800 pl-4 ${isGameMode ? "opacity-50 pointer-events-none" : ""}`, children: [ 5926 /* @__PURE__ */ jsxRuntimeExports.jsx( 5927 Metronome, 5928 { 5929 bpm: currentTempo, 5930 isPlaying, 5931 isBuffering, 5932 currentTime, 5933 timeSignature: typeof ((_g = track.midiMetadata) == null ? void 0 : _g.timeSignature) === "string" ? { numerator: parseInt(track.midiMetadata.timeSignature.split("/")[0] || "4"), denominator: parseInt(track.midiMetadata.timeSignature.split("/")[1] || "4") } : (_h = track.midiMetadata) == null ? void 0 : _h.timeSignature 5934 } 5935 ),
5936 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 5937 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5938 "button", 5939 { 5940 onClick: () => setShowTempoMenu(!showTempoMenu), 5941 className: `text-[10px] sm:text-xs font-bold font-mono px-2 py-1 rounded transition-colors flex flex-col items-start pl-3 justify-center min-w-[70px] ${fullscreenElement ? "hover:bg-zinc-800" : "hover:bg-zinc-100 dark:hover:bg-zinc-800"} ${playbackRate !== 1 || pitchSemitones !== 0 ? "text-[#f50]" : fullscreenElement ? "text-zinc-400" : "text-zinc-500"}`, 5942 title: t("player.tempoControl", "Playback Settings"), 5943 children: [ 5944 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 5945 /* @__PURE__ */ jsxRuntimeExports.jsx(Timer, { size: 14 }), 5946 " ", 5947 playbackRate.toFixed(2), 5948 "x" 5949 ] }),
5950 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center gap-1 leading-none mt-1`, children: [ 5951 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[12px] font-bold font-sans", children: "ââ¯" }), 5952 " ", 5953 pitchSemitones > 0 ? "+" : pitchSemitones < 0 ? "" : "±", 5954 pitchSemitones 5955 ] }) 5956 ] 5957 } 5958 ), 5959 showTempoMenu && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-full mb-1 right-0 bg-white dark:bg-[#1a1a1a] border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-xl p-4 z-50 flex flex-col min-w-[200px] animate-in slide-in-from-bottom-2", children: [ 5960 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-2", children: [ 5961 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xs font-bold text-zinc-500 uppercase", children: t("player.playbackSpeed", "Playback Speed") }),
5962 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setCurrentTempo(originalTempo), className: "p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full text-zinc-400 hover:text-zinc-900 dark:hover:text-white", title: "Reset Tempo", children: /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 12 }) }) 5963 ] }), 5964 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 mb-3", children: [ 5965 /* @__PURE__ */ jsxRuntimeExports.jsx( 5966 "input", 5967 { 5968 type: "number", 5969 min: "10", 5970 max: "999", 5971 value: Math.round(currentTempo), 5972 onChange: (e) => setCurrentTempo(Math.max(10, Math.min(999, parseInt(e.target.value) || originalTempo))), 5973 className: "w-16 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded p-1 text-center font-mono text-sm text-zinc-900 dark:white outline-none focus:border-indigo-500" 5974 } 5975 ),
5976 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400", children: "BPM" }) 5977 ] }), 5978 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1 mb-4", children: [ 5979 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-[10px] text-zinc-400", children: [ 5980 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "0.25x" }), 5981 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "1.0x" }), 5982 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "1.75x" }) 5983 ] }), 5984 /* @__PURE__ */ jsxRuntimeExports.jsx( 5985 "input", 5986 { 5987 type: "range", 5988 min: "0.25", 5989 max: "1.75", 5990 step: "0.05", 5991 value: playbackRate, 5992 onChange: (e) => setCurrentTempo(originalTempo * parseFloat(e.target.value)), 5993 className: "w-full accent-[#f50] h-1 bg-zinc-200 dark:bg-zinc-700 rounded-lg appearance-none cursor-pointer" 5994 } 5995 ) 5996 ] }),
5997 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 5998 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-200 dark:border-zinc-800 my-2" }), 5999 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-2", children: [ 6000 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-xs font-bold text-zinc-500 uppercase flex items-center gap-1", children: [ 6001 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowUpDown, { size: 12 }), 6002 " ", 6003 t("player.keyChange", "Key Change") 6004 ] }), 6005 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setPitchSemitones(0), className: "p-1 hover:bg-zinc-100 dark:bg-zinc-800 rounded-full text-zinc-400 hover:text-zinc-900 dark:hover:text-white", title: t("player.resetPitch", "Reset Pitch"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 12 }) }) 6006 ] }),
6007 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between gap-1 mb-1", children: [ 6008 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [ 6009 /* @__PURE__ */ jsxRuntimeExports.jsx( 6010 "button", 6011 { 6012 onClick: () => setPitchSemitones((p) => Math.max(-24, p - 1)), 6013 className: "w-8 h-8 flex justify-center items-center rounded bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 text-zinc-600 dark:text-zinc-300 transition-colors", 6014 children: "-" 6015 } 6016 ), 6017 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-bold text-zinc-400", children: "â" }) 6018 ] }), 6019 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6020 "input", 6021 { 6022 type: "number", 6023 min: "-24", 6024 max: "24", 6025 value: pitchSemitones, 6026 onChange: (e) => setPitchSemitones(Math.max(-24, Math.min(24, parseInt(e.target.value) || 0))), 6027 className: "w-16 bg-zinc-100 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded p-1 text-center font-mono text-sm text-zinc-900 dark:text-white outline-none focus:border-indigo-500" 6028 } 6029 ) }),
6030 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [ 6031 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-bold text-zinc-400", children: "â¯" }), 6032 /* @__PURE__ */ jsxRuntimeExports.jsx( 6033 "button", 6034 { 6035 onClick: () => setPitchSemitones((p) => Math.min(24, p + 1)), 6036 className: "w-8 h-8 flex justify-center items-center rounded bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 text-zinc-600 dark:text-zinc-300 transition-colors", 6037 children: "+" 6038 } 6039 ) 6040 ] }) 6041 ] }) 6042 ] }) 6043 ] }) 6044 ] }), 6045 castAvailable && /* @__PURE__ */ jsxRuntimeExports.jsx( 6046 "button", 6047 { 6048 onClick: handleCastClick, 6049 className: `hidden lg:block transition-colors ${castSession ? "text-blue-500" : fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-500 hover:text-black dark:hover:text-white"}`, 6050 title: t("player.cast", "Cast to Device"), 6051 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Cast, { size: 18 }) 6052 } 6053 ), 6054 fullscreenElement && /* @__PURE__ */ jsxRuntimeExports.jsx( 6055 "button", 6056 { 6057 onClick: () => setIsPinned(!isPinned), 6058 className: `hidden lg:block transition-colors ${isPinned ? "text-[#f50] dark:text-[#f50]" : fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-500 hover:text-black dark:hover:text-white"}`, 6059 title: isPinned ? t("player.unpin", "Unpin Player") : t("player.pin", "Pin Player"), 6060 children: isPinned ? /* @__PURE__ */ jsxRuntimeExports.jsx(PinOff, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Pin, { size: 18 }) 6061 } 6062 ) 6063 ] }),
6064 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "hidden sm:flex items-center gap-3 shrink-0 justify-end pl-2 sm:pl-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 6065 "div", 6066 { 6067 className: "relative flex items-center h-full", 6068 onMouseEnter: () => { 6069 if (volumeTimeoutRef.current) clearTimeout(volumeTimeoutRef.current); 6070 setShowVolumeSlider(true); 6071 }, 6072 onMouseLeave: () => { 6073 volumeTimeoutRef.current = setTimeout(() => { 6074 setShowVolumeSlider(false); 6075 }, 300); 6076 }, 6077 children: [ 6078 /* @__PURE__ */ jsxRuntimeExports.jsx( 6079 "button", 6080 { 6081 className: `transition-colors ${fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-500 hover:text-black dark:hover:text-white"}`, 6082 onClick: () => { 6083 setIsMuted(!isMuted); 6084 }, 6085 children: isMuted || volume === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Volume2, { size: 18 }) 6086 } 6087 ), 6088 showVolumeSlider && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-full left-1/2 -translate-x-1/2 w-14 h-40 bg-white dark:bg-[#1a1a1a] border border-zinc-200 dark:border-zinc-800 rounded-lg shadow-xl flex items-center justify-center z-50 animate-in fade-in slide-in-from-bottom-2 duration-200 mb-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 6089 "div", 6090 { 6091 ref: volumeRef, 6092 className: "w-8 h-32 bg-zinc-200 dark:bg-zinc-850 rounded-full relative border border-zinc-300 dark:border-zinc-700 shadow-inner flex justify-center cursor-pointer", 6093 onClick: handleVolumeChange, 6094 onMouseDown: (e) => { 6095 if (e.buttons === 1) handleVolumeChange(e); 6096 const moveHandler = (ev) => handleVolumeChange(ev); 6097 const upHandler = () => { 6098 window.removeEventListener("mousemove", moveHandler); 6099 window.removeEventListener("mouseup", upHandler); 6100 }; 6101 window.addEventListener("mousemove", moveHandler); 6102 window.addEventListener("mouseup", upHandler); 6103 }, 6104 children: [
6105 /* @__PURE__ */ jsxRuntimeExports.jsx( 6106 "div", 6107 { 6108 className: "absolute bottom-0 left-0 w-full bg-[#f50] rounded-full pointer-events-none", 6109 style: { height: `${isMuted ? 0 : volume * 100}%` } 6110 } 6111 ), 6112 /* @__PURE__ */ jsxRuntimeExports.jsx( 6113 "div", 6114 { 6115 className: "absolute left-0 w-full h-4 bg-white dark:bg-zinc-300 rounded shadow border border-zinc-400 dark:border-zinc-600 pointer-events-none z-10", 6116 style: { bottom: `calc(${isMuted ? 0 : volume * 100}% - 8px)` }, 6117 children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-px bg-zinc-300 dark:bg-zinc-500 mt-[7px]" }) 6118 } 6119 ) 6120 ] 6121 } 6122 ) }) 6123 ] 6124 } 6125 ) }), 6126 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `hidden lg:flex items-center gap-3 shrink-0 justify-end pl-4 ${isGameMode ? "opacity-50 pointer-events-none" : ""}`, children: [ 6127 onAddToPlaylist && /* @__PURE__ */ jsxRuntimeExports.jsx( 6128 "button", 6129 { 6130 onClick: onAddToPlaylist, 6131 className: `transition-colors ${fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-500 hover:text-black dark:hover:text-white"}`, 6132 title: t("playlists.saveTo", "Save to Playlist"), 6133 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { size: 18 }) 6134 } 6135 ),
6136 /* @__PURE__ */ jsxRuntimeExports.jsx( 6137 "button", 6138 { 6139 onClick: () => setShowQueue(!showQueue), 6140 className: `transition-colors ${showQueue ? "text-[#f50] dark:text-[#f50]" : fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-500 hover:text-black dark:hover:text-white"}`, 6141 title: t("player.nextUp", "Next Up"), 6142 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ListMusic, { size: 18 }) 6143 } 6144 ), 6145 /* @__PURE__ */ jsxRuntimeExports.jsx( 6146 "button", 6147 { 6148 onClick: () => setShowSettings(!showSettings), 6149 className: `transition-colors ${showSettings ? "text-[#f50] dark:text-[#f50]" : fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-500 hover:text-black dark:hover:text-white"}`, 6150 title: t("player.settings", "Player Settings"), 6151 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 18 }) 6152 } 6153 ) 6154 ] }),
6155 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "lg:hidden flex items-center shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 6156 /* @__PURE__ */ jsxRuntimeExports.jsx( 6157 "button", 6158 { 6159 onClick: () => setShowMoreMenu(!showMoreMenu), 6160 className: `p-1.5 rounded-lg transition-colors ${showMoreMenu ? "text-[#f50] bg-zinc-100 dark:bg-zinc-800" : fullscreenElement ? "text-zinc-400 hover:text-white" : "text-zinc-500 hover:text-black dark:hover:text-white hover:bg-zinc-100 dark:hover:bg-zinc-800"}`, 6161 title: t("player.more", "More"), 6162 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Ellipsis, { size: 20 }) 6163 } 6164 ), 6165 showMoreMenu && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-full mb-1 right-0 bg-white dark:bg-[#1a1a1a] border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-2xl z-50 animate-in slide-in-from-bottom-2 min-w-[220px] overflow-hidden", children: [
6166 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-[#111] flex justify-between items-center px-3", children: [ 6167 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-500 uppercase", children: t("player.more", "More") }), 6168 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShowMoreMenu(false), children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 14, className: "text-zinc-500 hover:text-zinc-900 dark:hover:text-white" }) }) 6169 ] }), 6170 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-1", children: [ 6171 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6172 "button", 6173 { 6174 onClick: () => { 6175 toggleLike(track.id); 6176 setShowMoreMenu(false); 6177 }, 6178 className: "sm:hidden w-full flex items-center gap-3 px-3 py-2.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors text-left", 6179 children: [
6180 /* @__PURE__ */ jsxRuntimeExports.jsx(Heart, { size: 16, fill: isLiked ? "currentColor" : "none", className: isLiked ? "text-[#f50]" : "text-zinc-500" }), 6181 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: isLiked ? t("player.unlike", "Unlike") : t("player.like", "Like") }) 6182 ] 6183 } 6184 ), 6185 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6186 "button", 6187 { 6188 onClick: () => { 6189 setIsMuted(!isMuted); 6190 }, 6191 className: "sm:hidden w-full flex items-center gap-3 px-3 py-2.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors text-left", 6192 children: [ 6193 isMuted || volume === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 16, className: "text-zinc-500" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Volume2, { size: 16, className: "text-zinc-500" }),
6194 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: isMuted ? t("player.unmute", "Unmute") : t("player.mute", "Mute") }) 6195 ] 6196 } 6197 ), 6198 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "md:hidden", children: [ 6199 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full flex items-center gap-3 px-3 py-2.5", children: [ 6200 /* @__PURE__ */ jsxRuntimeExports.jsx(Timer, { size: 16, className: playbackRate !== 1 ? "text-[#f50]" : "text-zinc-500" }), 6201 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300 flex-1", children: "Tempo" }), 6202 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs font-mono text-zinc-500", children: [ 6203 playbackRate.toFixed(2), 6204 "x" 6205 ] }) 6206 ] }), 6207 /* @__P
6207URE__ */ jsxRuntimeExports.jsx("div", { className: "px-3 pb-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6208 "input", 6209 { 6210 type: "range", 6211 min: "0.25", 6212 max: "1.75", 6213 step: "0.05", 6214 value: playbackRate, 6215 onChange: (e) => setCurrentTempo(originalTempo * parseFloat(e.target.value)), 6216 className: "w-full accent-[#f50] h-1 bg-zinc-200 dark:bg-zinc-700 rounded-lg appearance-none cursor-pointer" 6217 } 6218 ) }) 6219 ] }), 6220 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "md:hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full flex items-center gap-3 px-3 py-2.5", children: [ 6221 /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowUpDown, { size: 16, className: pitchSemitones !== 0 ? "text-[#f50]" : "text-zinc-500" }), 6222 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300 flex-1", children: t("player.keyChange", "Key Change") }),
6223 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 6224 /* @__PURE__ */ jsxRuntimeExports.jsx( 6225 "button", 6226 { 6227 onClick: () => setPitchSemitones(pitchSemitones - 1), 6228 className: "w-6 h-6 flex items-center justify-center rounded bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-[#f50] hover:text-white transition-colors", 6229 children: "-" 6230 } 6231 ), 6232 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs font-mono text-zinc-500 w-10 text-center", children: [ 6233 pitchSemitones > 0 ? "+" : "", 6234 pitchSemitones 6235 ] }), 6236 /* @__PURE__ */ jsxRuntimeExports.jsx( 6237 "button", 6238 { 6239 onClick: () => setPitchSemitones(pitchSemitones + 1), 6240 className: "w-6 h-6 flex items-center justify-center rounded bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 hover:bg-[#f50] hover:text-white transition-colors", 6241 children: "+" 6242 } 6243 ) 6244 ] }) 6245 ] }) }),
6246 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6247 "button", 6248 { 6249 onClick: () => { 6250 if (audioRef.current) { 6251 audioRef.current.currentTime = 0; 6252 setCurrentTime(0); 6253 setProgress(0); 6254 onTimeUpdate(0, duration); 6255 if (["synth", "webmidi", "opus", "wasmsynth", "chiptune", "fmsynth"].includes(audioMode)) synth.seek(0); 6256 } 6257 setShowMoreMenu(false); 6258 }, 6259 className: "lg:hidden w-full flex items-center gap-3 px-3 py-2.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors text-left", 6260 children: [ 6261 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 16, className: "text-zinc-500" }), 6262 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: t("player.startOver", "Start Over") }) 6263 ] 6264 } 6265 ),
6266 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6267 "button", 6268 { 6269 className: "lg:hidden w-full flex items-center gap-3 px-3 py-2.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors text-left", 6270 children: [ 6271 /* @__PURE__ */ jsxRuntimeExports.jsx(Shuffle, { size: 16, className: "text-zinc-500" }), 6272 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: t("player.shuffle", "Shuffle") }) 6273 ] 6274 } 6275 ), 6276 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-200 dark:border-zinc-800 my-1" }), 6277 onAddToPlaylist && /* @__PURE__ */ jsxRuntimeExports.jsxs( 6278 "button", 6279 { 6280 onClick: () => { 6281 onAddToPlaylist(); 6282 setShowMoreMenu(false); 6283 }, 6284 className: "w-full flex items-center gap-3 px-3 py-2.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors text-left", 6285 children: [
6286 /* @__PURE__ */ jsxRuntimeExports.jsx(Save, { size: 16, className: "text-zinc-500" }), 6287 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: t("playlists.saveTo", "Save to Playlist") }) 6288 ] 6289 } 6290 ), 6291 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6292 "button", 6293 { 6294 onClick: () => { 6295 setShowQueue(!showQueue); 6296 setShowMoreMenu(false); 6297 }, 6298 className: "w-full flex items-center gap-3 px-3 py-2.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors text-left", 6299 children: [ 6300 /* @__PURE__ */ jsxRuntimeExports.jsx(ListMusic, { size: 16, className: showQueue ? "text-[#f50]" : "text-zinc-500" }), 6301 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: t("player.nextUp", "Next Up") }) 6302 ] 6303 } 6304 ),
6305 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6306 "button", 6307 { 6308 onClick: () => { 6309 setShowSettings(!showSettings); 6310 setShowMoreMenu(false); 6311 }, 6312 className: "w-full flex items-center gap-3 px-3 py-2.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors text-left", 6313 children: [ 6314 /* @__PURE__ */ jsxRuntimeExports.jsx(Settings, { size: 16, className: showSettings ? "text-[#f50]" : "text-zinc-500" }), 6315 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: t("player.settings", "Player Settings") }) 6316 ] 6317 } 6318 ), 6319 castAvailable && /* @__PURE__ */ jsxRuntimeExports.jsxs( 6320 "button", 6321 { 6322 onClick: () => { 6323 handleCastClick(); 6324 setShowMoreMenu(false); 6325 }, 6326 className: "w-full flex items-center gap-3 px-3 py-2.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg transition-colors text-left", 6327 children: [
6328 /* @__PURE__ */ jsxRuntimeExports.jsx(Cast, { size: 16, className: castSession ? "text-blue-500" : "text-zinc-500" }), 6329 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: t("player.cast", "Cast") }) 6330 ] 6331 } 6332 ) 6333 ] }) 6334 ] }) 6335 ] }) }) 6336 ] }) 6337 } 6338 ); 6339 return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6340 showSettings && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `fixed bottom-[81px] right-4 w-60 bg-white dark:bg-[#1a1a1a] border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-2xl z-[101] animate-in slide-in-from-bottom-5 ${fullscreenElement ? "right-auto left-1/2 -translate-x-1/2" : ""}`, children: [ 6341 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-[#111] rounded-t-xl flex justify-between items-center", children: [
6342 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-xs text-zinc-900 dark:text-white", children: t("player.settings", "Player Settings") }), 6343 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShowSettings(false), children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 14, className: "text-zinc-500 hover:text-zinc-900 dark:hover:text-white" }) }) 6344 ] }), 6345 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3", children: [ 6346 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6347 "button", 6348 { 6349 onClick: () => setAutoplay(!autoplay), 6350 className: "w-full flex items-center justify-between p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800/50 transition-colors", 6351 children: [ 6352 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: t("player.autoplay", "Autoplay on Load") }), 6353 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-4 h-4 rounded border flex items-center justify-center ${autoplay ? "bg-indigo-600 border-indigo-600" : "border-zinc-400 dark:border-zinc-600"}
6353`, children: autoplay && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 10, className: "text-white" }) }) 6354 ] 6355 } 6356 ), 6357 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6358 "button", 6359 { 6360 onClick: () => { 6361 const current = localStorage.getItem("low_performance") === "true"; 6362 const newVal = !current; 6363 localStorage.setItem("low_performance", newVal ? "true" : "false"); 6364 window.dispatchEvent(new Event("low_performance_change")); 6365 if (newVal) { 6366 const hasWebMidiSupport = typeof navigator !== "undefined" && !!navigator.requestMIDIAccess; 6367 if (hasWebMidiSupport) { 6368 setAudioMode("webmidi"); 6369 } else if (track.opusPath) { 6370 setAudioMode("singleopus"); 6371 } else if (activeAudioSrc) { 6372 setAudioMode("opus"); 6373 } 6374 } 6375 setLowPerfToggle(newVal); 6376 }, 6377 className: "w-full flex items-center justify-between p-2 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800/50 transition-colors text-left", 6378 children: [
6379 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [ 6380 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-700 dark:text-zinc-300", children: "Low-End Device Mode" }), 6381 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500", children: "Simplify UI & use fast audio engines" }) 6382 ] }), 6383 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-4 h-4 rounded border flex items-center justify-center ${lowPerfToggle ? "bg-indigo-600 border-indigo-600" : "border-zinc-400 dark:border-zinc-600"}`, children: lowPerfToggle && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 10, className: "text-white" }) }) 6384 ] 6385 } 6386 ), 6387 audioMode === "synth" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6388 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-200 dark:border-zinc-800 my-2" }),
6389 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-2 pb-1 flex justify-between items-center", children: [ 6390 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-500 uppercase tracking-wider", children: t("player.soundfontVolume", "Soundfont Volume") }), 6391 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-mono text-zinc-500 font-bold", children: [ 6392 soundfontVolume.toFixed(1), 6393 "x" 6394 ] }) 6395 ] }), 6396 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-2 py-1", children: [ 6397 /* @__PURE__ */ jsxRuntimeExports.jsx( 6398 "input", 6399 { 6400 type: "range", 6401 min: "1.0", 6402 max: "10.0", 6403 step: "0.5", 6404 value: soundfontVolume, 6405 onChange: (e) => setSoundfontVolume(parseFloat(e.target.value)), 6406 className: "w-full accent-indigo-600 h-1 bg-zinc-200 dark:bg-zinc-700 rounded-lg appearance-none cursor-pointer" 6407 } 6408 ),
6409 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-[9px] text-zinc-400 mt-1", children: [ 6410 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "1.0x (Quiet)" }), 6411 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "10.0x (Loud)" }) 6412 ] }) 6413 ] }) 6414 ] }), 6415 audioMode === "webmidi" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6416 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-zinc-200 dark:border-zinc-800 my-2" }), 6417 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-2 pb-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-bold text-zinc-500 uppercase tracking-wider", children: "MIDI Device" }) }), 6418 midiOutputs.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-2 py-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6419 "select", 6420 { 6421 value: selectedMidiId, 6422 onChange: (e) => { 6423 const id = e.target.value; 6424 synth.setMidiOutputById(id); 6425 setSelectedMidiId(id); 6426 }, 6427 className: "w-full bg-zinc-100 dark:bg-zinc-800 text-xs text-zinc-800 dark:text-zinc-200 border border-zinc-200 dark:border-zinc-700 rounded p-1.5 outline-none cursor-pointer", 6428 children: midiOutputs.map((out) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: out.id, children: out.name || `MIDI Port ${out.id}` }, out.id)) 6429 } 6430 ) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-2 py-1 text-[10px] text-red-500 leading-snug", children: "No MIDI outputs found. Connect a MIDI device or switch to Soundfont Synth." }) 6431 ] }) 6432 ] }) 6433 ] }), 6434 showQueue && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `fixed bottom-[81px] w-80 bg-white dark:bg-[#1a1a1a] border border-zinc-200 dark:border-zinc-800 rounded-t-xl shadow-2xl z-[101] animate-in slide-in-from-bottom-10 max-h-[60vh] flex flex-col ${fullscreenElement ? "right-4" : "right-16"}`, children: [
6435 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center bg-zinc-50 dark:bg-[#111] rounded-t-xl", children: [ 6436 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-bold text-sm text-zinc-900 dark:text-white", children: t("player.nextUp", "Next Up") }), 6437 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShowQueue(false), children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 16, className: "text-zinc-500 hover:text-zinc-900 dark:hover:text-white" }) }) 6438 ] }), 6439 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-2 custom-scrollbar", children: [ 6440 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 bg-indigo-50 dark:bg-indigo-900/20 rounded-lg flex items-center gap-3 mb-2 border border-indigo-100 dark:border-indigo-800", children: [ 6441 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 rounded bg-zinc-200 dark:bg-zinc-800 shrink-0 overflow-hidden", children: track.status === "completed" ? getThumbnailElement(track, "w-full h-full") : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-full" }) }),
6442 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0", children: [ 6443 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-indigo-600 dark:text-indigo-400 font-bold mb-0.5", children: t("player.nowPlaying", "Now Playing") }), 6444 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-bold text-zinc-900 dark:text-white truncate", children: track.title }) 6445 ] }), 6446 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ml-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-3 h-3 bg-indigo-500 rounded-full animate-pulse" }) }) 6447 ] }), 6448 activePlaylist && activePlaylist.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1 mt-3 mb-4", children: [ 6449 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between px-2 mb-1", children: [ 6450 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs font-bold text-indigo-600 dark:text-indigo-400 uppercase truncate", children: [ 6451 "ð¶ ", 6452 playlistLabel || t("player.activePlaylist", "Active Playlist"), 6453 " (", 6454 activePlaylist.length, 6455 ")" 6456 ] }),
6457 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6458 "button", 6459 { 6460 onClick: onClearPlaylist, 6461 className: "text-[10px] text-zinc-500 hover:text-red-500 transition-colors flex items-center gap-1 bg-zinc-100 dark:bg-zinc-800 px-2 py-0.5 rounded", 6462 children: [ 6463 /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 10 }), 6464 " Clear" 6465 ] 6466 } 6467 ) 6468 ] }), 6469 activePlaylist.map((pTrack, i) => { 6470 const isCurrent = pTrack._playlistItemId && track._playlistItemId ? pTrack._playlistItemId === track._playlistItemId : track.id === pTrack.id; 6471 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `group flex items-center gap-3 p-2 rounded-lg cursor-pointer transition-colors ${isCurrent ? "bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-100 dark:border-indigo-800" : "hover:bg-zinc-100 dark:hover:bg-zinc-800"}`, onClick: () => onPlayQueueTrack && onPlayQueueTrack(pTrack), children: [
6472 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-xs font-mono w-4 ${isCurrent ? "text-indigo-500 font-bold" : "text-zinc-400"}`, children: i + 1 }), 6473 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 rounded bg-zinc-200 dark:bg-zinc-800 shrink-0 overflow-hidden", children: pTrack.status === "completed" ? getThumbnailElement(pTrack, "w-full h-full") : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-full" }) }), 6474 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [ 6475 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `text-xs font-bold truncate ${isCurrent ? "text-indigo-700 dark:text-indigo-300" : "text-zinc-900 dark:text-white"}`, children: pTrack.title }), 6476 pTrack.artist && !["unknown", "unknown artist"].includes(pTrack.artist.toLowerCase().trim()) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500 truncate", children: pTrack.artist }) 6477 ] }), 6478 isCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "ml-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-2 h-2 bg-indigo-500 rounded-full animate-pulse" }) }) 6479 ] }, `pl-${i}`); 6480 }) 6481 ] }), 6482 queue.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-1 mt-3 border-t border-zinc-100 dark:border-zinc-800/50 pt-3", children: [
6483 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-bold text-zinc-500 uppercase px-2 mb-1", children: t("player.nextUp", "Next Up") }), 6484 queue.map((qTrack, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "group flex items-center gap-3 p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-lg cursor-pointer", onClick: () => onPlayQueueTrack && onPlayQueueTrack(qTrack), children: [ 6485 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400 font-mono w-4", children: i + 1 }), 6486 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 rounded bg-zinc-200 dark:bg-zinc-800 shrink-0 overflow-hidden", children: qTrack.status === "completed" ? getThumbnailElement(qTrack, "w-full h-full") : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-full" }) }), 6487 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", children: [ 6488 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs font-bold text-zinc-900 dark:text-white truncate", children: qTrack.title }), 6489 qTrack.artist && !["unknown", "unknown artist"].includes(qTrack.artist.toLowerCase().trim()) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-zinc-500 truncate", children: qTrack.artist }) 6490 ] }),
6491 /* @__PURE__ */ jsxRuntimeExports.jsx( 6492 "button", 6493 { 6494 onClick: (e) => { 6495 e.stopPropagation(); 6496 onRemoveFromQueue == null ? void 0 : onRemoveFromQueue(i); 6497 }, 6498 className: "opacity-0 group-hover:opacity-100 p-1 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded text-zinc-500 hover:text-red-500 transition-all", 6499 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Trash2, { size: 14 }) 6500 } 6501 ) 6502 ] }, `q-${i}`)) 6503 ] }) : (!activePlaylist || activePlaylist.length === 0) && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center py-8 text-zinc-500 text-xs", children: [ 6504 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 24, className: "mx-auto mb-2 opacity-50" }), 6505 t("player.queueEmpty", "Queue is empty") 6506 ] }) 6507 ] }) 6508 ] }), 6509 fullscreenElement ? ReactDOM.createPortal(PlayerUI, fullscreenElement) : PlayerUI, 6510 /* @__PURE__ */ jsxRuntimeExports.jsx( 6511 "audio", 6512 { 6513 id: "global-audio-element", 6514 ref: audioRef, 6515 ...activeAudioSrc ? { src: activeAudioSrc } : {}, 6516 preload: activeAudioSrc ? "metadata" : "none", 6517 crossOrigin: "anonymous", 6518 onLoadedMetadata: (e) => { 6519 if (duration === 0 || !["synth", "webmidi", "opus", "wasmsynth", "chiptune", "fmsynth"].includes(audioMode)) { 6520 setDuration(e.currentTarget.duration); 6521 } 6522 }, 6523 onWaiting: () => { 6524 if (audioMode === "singleopus") { 6525 setIsBuffering(true); 6526 } 6527 }, 6528 onPlaying: () => { 6529 if (audioMode === "singleopus") { 6530 setIsBuffering(false); 6531 } 6532 }, 6533 onSeeking: () => { 6534 if (audioMode === "singleopus") { 6535 setIsBuffering(true); 6536 } 6537 }, 6538 onSeeked: () => { 6539 if (audioMode === "singleopus") { 6540 setIsBuffering(false); 6541 } 6542 }, 6543 onProgress: (e) => { 6544 if (audioMode === "singleopus") { 6545 const audio = e.currentTarget; 6546 if (audio.duration > 0 && audio.buffered.length > 0) { 6547 const end = audio.buffered.end(audio.buffered.length - 1); 6548 setSingleOpusProgress(end / audio.duration * 100); 6549 } 6550 } 6551 }, 6552 onEnded: () => { 6553 const isSynthMode = ["synth", "webmidi", "opus", "wasmsynth", "chiptune", "fmsynth"].includes(audioMode); 6554 if (!isSynthMode) { 6555 onNext(false); 6556 } 6557 }, 6558 onError: (e) => { 6559 const errorMessage = e.currentTarget.error ? e.currentTarget.error.message : "Unknown Error"; 6560 const errorCode = e.currentTarget.error ? e.currentTarget.error.code : 0; 6561 console.warn(`Audio Playback Error [Code ${errorCode}]:`, errorMessage); 6562 if (audioMode === "opus" || audioMode === "singleopus") { 6563 setPlaybackError(true); 6564 setIsPlaying(false); 6565 } else { 6566 console.log("[PersistentPlayer] Suppressing audio error as we are in Synth mode"); 6567 } 6568 } 6569 } 6570 ) 6571 ] }); 6572} 6573const AddToPlaylistModal = ({ isOpen, onClose, track, currentUser }) => { 6574 const { t } = useTranslation(); 6575 const [playlists, setPlaylists] = reactExports.useState([]); 6576 const [newPlaylistName, setNewPlaylistName] = reactExports.useState(""); 6577 const [isPublic, setIsPublic] = reactExports.useState(false); 6578 const [isCreating, setIsCreating] = reactExports.useState(false); 6579 const [addedIds, setAddedIds] = reactExports.useState(/* @__PURE__ */ new Set()); 6580 reactExports.useEffect(() => { 6581 if (isOpen && currentUser) { 6582 api.getPlaylists(currentUser.id).then((pl) => { 6583 setPlaylists(pl); 6584 const existing = new Set(pl.filter((p) => p.trackIds.includes(track.id)).map((p) => p.id)); 6585 setAddedIds(existing); 6586 }); 6587 } 6588 }, [isOpen, currentUser, track.id]); 6589 const handleCreatePlaylist = async () => { 6590 if (!newPlaylistName.trim()) return; 6591 setIsCreating(true); 6592 const newPlaylist = await api.createPlaylist(currentUser.id, newPlaylistName, isPublic); 6593 setPlaylists((prev) => [...prev, newPlaylist]); 6594 setNewPlaylistName(""); 6595 setIsPublic(false); 6596 setIsCreating(false); 6597 handleAddTrack(newPlaylist.id); 6598 }; 6599 const handleAddTrack = async (playlistId) => { 6600 await api.addTrackToPlaylist(playlistId, track.id); 6601 setAddedIds((prev) => new Set(prev).add(playlistId)); 6602 }; 6603 if (!isOpen) return null; 6604 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[1000] flex items-center justify-center p-4", children: [
6605 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/80 backdrop-blur-sm", onClick: onClose }), 6606 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-sm bg-white dark:bg-zinc-900 rounded-xl shadow-2xl overflow-hidden animate-in zoom-in-95 flex flex-col max-h-[80vh]", children: [ 6607 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-b border-zinc-200 dark:border-zinc-800 flex justify-between items-center", children: [ 6608 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 6609 /* @__PURE__ */ jsxRuntimeExports.jsx(ListMusic, { size: 18, className: "text-indigo-500" }), 6610 t("playlists.addTo", "Add to Playlist") 6611 ] }), 6612 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20, className: "text-zinc-500 hover:text-zinc-900 dark:hover:text-white" }) }) 6613 ] }),
6614 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto p-4 space-y-2", children: [ 6615 playlists.map((pl) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 6616 "button", 6617 { 6618 onClick: () => !addedIds.has(pl.id) && handleAddTrack(pl.id), 6619 className: "w-full flex items-center justify-between p-3 rounded-lg hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors text-left", 6620 children: [ 6621 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-zinc-900 dark:text-white truncate", children: pl.title }), 6622 addedIds.has(pl.id) ? /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 18, className: "text-emerald-500" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-4 border-2 border-zinc-400 rounded-sm" }) 6623 ] 6624 }, 6625 pl.id 6626 )), 6627 playlists.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-center text-zinc-500 py-4 text-sm", children: t("playlists.noPlaylists", "No playlists found.") }) 6628 ] }),
6629 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 border-t border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [ 6630 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [ 6631 /* @__PURE__ */ jsxRuntimeExports.jsx( 6632 "input", 6633 { 6634 value: newPlaylistName, 6635 onChange: (e) => setNewPlaylistName(e.target.value), 6636 placeholder: t("playlists.newPlaylistPlaceholder", "New Playlist Name"), 6637 className: "flex-1 bg-white dark:bg-black border border-zinc-300 dark:border-zinc-700 rounded-lg px-3 py-2 text-sm outline-none focus:border-indigo-500 text-zinc-900 dark:text-white", 6638 onKeyDown: (e) => e.key === "Enter" && handleCreatePlaylist() 6639 } 6640 ), 6641 /* @__PURE__ */ jsxRuntimeExports.jsx( 6642 "button", 6643 { 6644 onClick: handleCreatePlaylist, 6645 disabled: !newPlaylistName || isCreating, 6646 className: "p-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg disabled:opacity-50", 6647 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { size: 20 }) 6648 } 6649 ) 6650 ] }),
6651 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 text-sm text-zinc-600 dark:text-zinc-400 cursor-pointer", children: [ 6652 /* @__PURE__ */ jsxRuntimeExports.jsx( 6653 "input", 6654 { 6655 type: "checkbox", 6656 checked: isPublic, 6657 onChange: (e) => setIsPublic(e.target.checked), 6658 className: "rounded border-zinc-300 text-indigo-600 focus:ring-indigo-500" 6659 } 6660 ), 6661 t("playlists.makePublic", "Make this playlist public") 6662 ] }) 6663 ] }) }) 6664 ] }) 6665 ] }); 6666}; 6667const StaffAdminBar = () => { 6668 var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m; 6669 const { staffModeEnabled, toggleStaffMode, isSuperAdmin } = useAdmin(); 6670 const { t } = useTranslation(); 6671 const [stats2, setStats] = reactExports.useState({ pendingReports: 0, activeUserCount: 0, system: { cpu: 0, memory: 0 } }); 6672 const [dbQueryTime, setDbQueryTime] = reactExports.useState(0); 6673 const [dbQueryCount, setDbQueryCount] = reactExports.useState(0); 6674 const [clientApiTime, setClientApiTime] = reactExports.useState(0); 6675 const [initialLoadTime, setInitialLoadTime] = reactExports.useState(null); 6676 const [showTelemetryDetails, setShowTelemetryDetails] = reactExports.useState(false); 6677 reactExports.useEffect(() => { 6678 if (!staffModeEnabled) return; 6679 const fetchStats = async () => { 6680 try { 6681 const data = await api.getActiveStaffStats(); 6682 setStats(data); 6683 } catch (e) { 6684 console.error("Failed to fetch staff stats", e); 6685 } 6686 }; 6687 fetchStats(); 6688 const interval = setInterval(fetchStats, 3e4); 6689 return () => clearInterval(interval); 6690 }, [staffModeEnabled]); 6691 reactExports.useEffect(() => { 6692 if (!staffModeEnabled) return; 6693 const measureLoadTime = () => { 6694 const [navigationEntry] = performance.getEntriesByType("navigation"); 6695 if (navigationEntry) { 6696 setInitialLoadTime(Math.round(navigationEntry.duration)); 6697 } else { 6698 const t2 = performance.timing; 6699 if (t2) { 6700 const loadTime = t2.loadEventEnd - t2.navigationStart; 6701 if (loadTime > 0) { 6702 setInitialLoadTime(loadTime); 6703 } 6704 } 6705 } 6706 }; 6707 if (document.readyState === "complete") { 6708 measureLoadTime(); 6709 } else { 6710 window.addEventListener("load", measureLoadTime); 6711 return () => window.removeEventListener("load", measureLoadTime); 6712 } 6713 }, [staffModeEnabled]); 6714 reactExports.useEffect(() => { 6715 if (!staffModeEnabled) return; 6716 const handleQueryTiming = (e) => { 6717 if (e.detail) { 6718 setDbQueryTime(Math.round(e.detail.queryTime)); 6719 setDbQueryCount(e.detail.queryCount); 6720 setClientApiTime(Math.round(e.detail.apiTime)); 6721 } 6722 }; 6723 window.addEventListener("page-query-timing", handleQueryTiming); 6724 return () => window.removeEventListener("page-query-timing", handleQueryTiming); 6725 }, [staffModeEnabled]); 6726 reactExports.useEffect(() => { 6727 if (!staffModeEnabled) return; 6728 setDbQueryTime(0); 6729 setDbQueryCount(0); 6730 setClientApiTime(0); 6731 }, [staffModeEnabled, window.location.pathname]); 6732 if (!staffModeEnabled) return null; 6733 const getLoadTimeColor = (ms) => { 6734 if (!ms) return "text-slate-300"; 6735 if (ms < 600) return "text-emerald-400"; 6736 if (ms < 1500) return "text-amber-400"; 6737 return "text-rose-400"; 6738 }; 6739 const getApiTimeColor = (ms) => { 6740 if (ms === 0) return "text-slate-300"; 6741 if (ms < 250) return "text-emerald-400"; 6742 if (ms < 800) return "text-amber-400"; 6743 return "text-rose-400"; 6744 }; 6745 const getDbTimeColor = (ms) => { 6746 if (ms === 0) return "text-slate-300"; 6747 if (ms < 50) return "text-emerald-400"; 6748 if (ms < 150) return "text-amber-400"; 6749 return "text-rose-400"; 6750 }; 6751 const getCpuColor = (percent) => { 6752 if (percent === void 0) return "text-slate-300"; 6753 if (percent < 30) return "text-emerald-400"; 6754 if (percent < 70) return "text-amber-400"; 6755 return "text-rose-400"; 6756 }; 6757 const getMemoryColor = (percent) => { 6758 if (percent === void 0) return "text-slate-300"; 6759 if (percent < 50) return "text-emerald-400"; 6760 if (percent < 85) return "text-amber-400"; 6761 return "text-rose-400"; 6762 }; 6763 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "staff-admin-bar bg-indigo-600 text-white py-1 px-4 flex items-center justify-between text-xs font-bold animate-in slide-in-from-top duration-300 sticky top-0 z-[60] shadow-lg", children: [
6764 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4", children: [ 6765 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 px-2 py-0.5 bg-white/20 rounded-full select-none", children: [ 6766 /* @__PURE__ */ jsxRuntimeExports.jsx(Shield, { size: 14, className: "fill-current" }), 6767 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "STAFF MODE ACTIVE" }) 6768 ] }), 6769 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "hidden md:flex items-center gap-4 opacity-90 select-none", children: [ 6770 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 6771 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { size: 14 }), 6772 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 6773 "Users: ", 6774 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-emerald-300", children: [ 6775 stats2.activeUserCount, 6776 " Active" 6777 ] }) 6778 ] }) 6779 ] }),
6780 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 border-l border-white/20 pl-4", children: [ 6781 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 14 }), 6782 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 6783 "Reports: ", 6784 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: stats2.pendingReports > 0 ? "text-rose-300" : "text-emerald-300", children: [ 6785 stats2.pendingReports, 6786 " ", 6787 stats2.pendingReports === 1 ? "Pending" : "Pending" 6788 ] }) 6789 ] }) 6790 ] }) 6791 ] }) 6792 ] }), 6793 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6794 "div", 6795 { 6796 className: "relative hidden lg:flex items-center gap-6 text-white/90 font-medium select-none cursor-pointer group py-1", 6797 onMouseEnter: () => setShowTelemetryDetails(true), 6798 onMouseLeave: () => setShowTelemetryDetails(false), 6799 children: [ 6800 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 hover:text-white transition-colors", children: [
6801 /* @__PURE__ */ jsxRuntimeExports.jsx(Gauge, { size: 13, className: "text-indigo-200 animate-pulse" }), 6802 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 6803 "Page Load: ", 6804 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getLoadTimeColor(initialLoadTime), children: initialLoadTime ? `${initialLoadTime}ms` : "Measuring..." }) 6805 ] }) 6806 ] }), 6807 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 border-l border-white/20 pl-4 hover:text-white transition-colors", children: [ 6808 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 13, className: "text-indigo-200" }), 6809 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 6810 "DB Queries: ", 6811 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getDbTimeColor(dbQueryTime), children: dbQueryTime > 0 ? `${dbQueryTime}ms` : "0ms" }) 6812 ] }) 6813 ] }), 6814 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 border-l border-white/20 pl-4 hover:text-white transition-colors", children: [
6815 /* @__PURE__ */ jsxRuntimeExports.jsx(Cpu, { size: 13, className: "text-indigo-200" }), 6816 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 6817 "CPU: ", 6818 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getCpuColor((_a = stats2.system) == null ? void 0 : _a.cpu), children: ((_b = stats2.system) == null ? void 0 : _b.cpu) !== void 0 ? `${stats2.system.cpu}%` : "0%" }) 6819 ] }) 6820 ] }), 6821 showTelemetryDetails && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-full left-1/2 -translate-x-1/2 mt-1 w-80 bg-slate-900/95 backdrop-blur-md border border-slate-700 text-slate-100 p-4 rounded-xl shadow-2xl z-[70] flex flex-col gap-3 font-normal text-xs animate-in fade-in slide-in-from-top-1 duration-200", children: [ 6822 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between border-b border-slate-800 pb-2", children: [ 6823 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-bold text-indigo-400 flex items-center gap-1", children: [
6824 /* @__PURE__ */ jsxRuntimeExports.jsx(Gauge, { size: 14 }), 6825 " SYSTEM PERFORMANCE TELEMETRY" 6826 ] }), 6827 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] bg-indigo-500/20 text-indigo-300 px-1.5 py-0.5 rounded-full font-bold", children: "STAFF MODE" }) 6828 ] }), 6829 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [ 6830 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-slate-400 font-bold tracking-wider uppercase", children: "Latency Breakdowns" }), 6831 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 6832 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5 text-slate-300", children: [ 6833 /* @__PURE__ */ jsxRuntimeExports.jsx(Timer, { size: 13, className: "text-slate-400" }), 6834 " Initial Hard Load" 6835 ] }),
6836 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `font-bold ${getLoadTimeColor(initialLoadTime)}`, children: initialLoadTime ? `${initialLoadTime}ms` : "Loading..." }) 6837 ] }), 6838 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 6839 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5 text-slate-300", children: [ 6840 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 13, className: "text-slate-400" }), 6841 " Current Page APIs" 6842 ] }), 6843 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `font-bold ${getApiTimeColor(clientApiTime)}`, children: clientApiTime > 0 ? `${clientApiTime}ms` : "0ms" }) 6844 ] }), 6845 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 6846 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5 text-slate-300", children: [
6847 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 13, className: "text-slate-400" }), 6848 " DB Execution (", 6849 dbQueryCount, 6850 " queries)" 6851 ] }), 6852 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `font-bold ${getDbTimeColor(dbQueryTime)}`, children: dbQueryTime > 0 ? `${dbQueryTime}ms` : "0ms" }) 6853 ] }), 6854 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pl-4 text-[11px] text-slate-400", children: [ 6855 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Express App Overhead" }), 6856 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-medium text-slate-300", children: clientApiTime > dbQueryTime ? `${clientApiTime - dbQueryTime}ms` : "0ms" }) 6857 ] }) 6858 ] }), 6859 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2 border-t border-slate-800 pt-2", children: [
6860 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-slate-400 font-bold tracking-wider uppercase", children: "Server Resources" }), 6861 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1", children: [ 6862 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between", children: [ 6863 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5 text-slate-300", children: [ 6864 /* @__PURE__ */ jsxRuntimeExports.jsx(Cpu, { size: 13, className: "text-slate-400" }), 6865 " Server CPU" 6866 ] }), 6867 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `font-bold ${getCpuColor((_c = stats2.system) == null ? void 0 : _c.cpu)}`, children: [ 6868 ((_d = stats2.system) == null ? void 0 : _d.cpu) ?? 0, 6869 "%" 6870 ] }) 6871 ] }), 6872 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-1.5 bg-slate-800 rounded-full overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6873 "div", 6874 { 6875 className: `h-full rounded-full transition-all duration-500 ${((_e = stats2.system) == null ? void 0 : _e.cpu) > 70 ? "bg-rose-500" : ((_f = stats2.system) == null ? void 0 : _f.cpu) > 30 ? "bg-amber-500" : "bg-emerald-500"}`, 6876 style: { width: `${((_g = stats2.system) == null ? void 0 : _g.cpu) ?? 0}%` } 6877 } 6878 ) }) 6879 ] }),
6880 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1", children: [ 6881 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between", children: [ 6882 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex items-center gap-1.5 text-slate-300", children: [ 6883 /* @__PURE__ */ jsxRuntimeExports.jsx(HardDrive, { size: 13, className: "text-slate-400" }), 6884 " Server Memory" 6885 ] }), 6886 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `font-bold ${getMemoryColor((_h = stats2.system) == null ? void 0 : _h.memory)}`, children: [ 6887 ((_i = stats2.system) == null ? void 0 : _i.memory) ?? 0, 6888 "%" 6889 ] }) 6890 ] }), 6891 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-1.5 bg-slate-800 rounded-full overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6892 "div", 6893 { 6894 className: `h-full rounded-full transition-all duration-500 ${((_j = stats2.system) == null ? void 0 : _j.memory) > 70 ? "bg-rose-500" : ((_k = stats2.system) == null ? void 0 : _k.memory) > 30 ? "bg-amber-500" : "bg-emerald-500"}`, 6895 style: { width: `${((_l = stats2.system) == null ? void 0 : _l.memory) ?? 0}%` } 6896 } 6897 ) }) 6898 ] }) 6899 ] }),
6900 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border-t border-slate-800 pt-2 text-[10px] text-slate-500 text-center italic", children: "Timings accumulate over the active page's lifetime and reset on route changes." }) 6901 ] }) 6902 ] 6903 } 6904 ), 6905 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 6906 isSuperAdmin && /* @__PURE__ */ jsxRuntimeExports.jsx( 6907 "button", 6908 { 6909 onClick: toggleStaffMode, 6910 className: "bg-white/10 hover:bg-white/20 px-3 py-1 rounded transition-colors flex items-center gap-1", 6911 children: "Disable Staff Mode" 6912 } 6913 ), 6914 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6915 "a", 6916 { 6917 href: ((_m = window.HAMIE_ENV) == null ? void 0 : _m.ADMIN_URL) ? `${window.HAMIE_ENV.ADMIN_URL}/admin` : "/admin", 6918 target: "_blank", 6919 className: "flex items-center gap-1 hover:underline", 6920 children: [ 6921 "Admin Studio ", 6922 /* @__P
6922URE__ */ jsxRuntimeExports.jsx(CircleCheckBig, { size: 12 }) 6923 ] 6924 } 6925 ) 6926 ] }) 6927 ] }); 6928}; 6929function formatRelativeTime(date) { 6930 const now = Date.now(); 6931 const then = new Date(date).getTime(); 6932 const diff = Math.floor((now - then) / 1e3); 6933 if (diff < 60) return "just now"; 6934 if (diff < 3600) return `${Math.floor(diff / 60)}m ago`; 6935 if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`; 6936 if (diff < 604800) return `${Math.floor(diff / 86400)}d ago`; 6937 return new Date(date).toLocaleDateString(); 6938} 6939function getIcon(type) { 6940 switch (type) { 6941 case "upload": 6942 return /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 14, className: "text-zinc-600 dark:text-white" }); 6943 case "like": 6944 return /* @__PURE__ */ jsxRuntimeExports.jsx(Heart, { size: 14, className: "text-red-500 fill-red-500" }); 6945 case "reply": 6946 return /* @__PURE__ */ jsxRuntimeExports.jsx(MessageSquare, { size: 14, className: "text-blue-500 dark:text-blue-400 fill-blue-500 dark:fill-blue-400" }); 6947 case "subscriber": 6948 return /* @__PURE__ */ jsxRuntimeExports.jsx(User, { size: 14, className: "text-emerald-500 dark:text-emerald-400" }); 6949 default: 6950 return /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { size: 14, className: "text-zinc-400" }); 6951 } 6952} 6953const NotificationsModal = ({ 6954 isOpen, 6955 onClose, 6956 previewNotifications, 6957 onMarkAsRead, 6958 onMarkAllRead, 6959 onNavigate 6960}) => { 6961 const { t } = useTranslation(); 6962 const [notifications, setNotifications] = reactExports.useState([]); 6963 const [hasMore, setHasMore] = reactExports.useState(false); 6964 const [nextCursor, setNextCursor] = reactExports.useState(null); 6965 const [isLoading, setIsLoading] = reactExports.useState(false); 6966 const [isInitialLoad, setIsInitialLoad] = reactExports.useState(true); 6967 const sentinelRef = reactExports.useRef(null); 6968 const scrollContainerRef = reactExports.useRef(null); 6969 reactExports.useEffect(() => { 6970 if (!isOpen) return; 6971 setNotifications(previewNotifications); 6972 setIsInitialLoad(true); 6973 setNextCursor(null); 6974 setHasMore(false); 6975 loadPage(void 0, true); 6976 }, [isOpen]); 6977 const loadPage = reactExports.useCallback(async (cursor, isFirst) => { 6978 setIsLoading(true); 6979 try { 6980 const result = await api.getNotificationPage(cursor); 6981 setNotifications((prev) => { 6982 if (isFirst) return result.items; 6983 const existingIds = new Set(prev.map((n) => n.id)); 6984 const newItems = result.items.filter((n) => !existingIds.has(n.id)); 6985 return [...prev, ...newItems]; 6986 }); 6987 setHasMore(result.hasMore); 6988 setNextCursor(result.nextCursor); 6989 } finally { 6990 setIsLoading(false); 6991 setIsInitialLoad(false); 6992 } 6993 }, []); 6994 reactExports.useEffect(() => { 6995 if (!sentinelRef.current || !hasMore || isLoading) return; 6996 const observer = new IntersectionObserver( 6997 (entries) => { 6998 if (entries[0].isIntersecting && hasMore && !isLoading) { 6999 loadPage(nextCursor ?? void 0); 7000 } 7001 }, 7002 { root: scrollContainerRef.current, rootMargin: "100px" } 7003 ); 7004 observer.observe(sentinelRef.current); 7005 return () => observer.disconnect(); 7006 }, [hasMore, isLoading, nextCursor, loadPage]); 7007 const handleClick = (note) => { 7008 if (!note.isRead) onMarkAsRead(note.id); 7009 setNotifications((prev) => prev.map((n) => n.id === note.id ? { ...n, isRead: true } : n)); 7010 if (note.link) { 7011 onNavigate(note.link); 7012 onClose(); 7013 } 7014 }; 7015 const handleMarkAllRead = () => { 7016 onMarkAllRead(); 7017 setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true }))); 7018 }; 7019 const unreadCount = notifications.filter((n) => !n.isRead).length; 7020 if (!isOpen) return null; 7021 return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 7022 /* @__PURE__ */ jsxRuntimeExports.jsx( 7023 "div", 7024 { 7025 className: "fixed inset-0 z-40 bg-black/30 backdrop-blur-sm", 7026 onClick: onClose 7027 } 7028 ), 7029 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed top-16 right-4 z-50 w-[420px] max-h-[calc(100vh-5rem)] flex flex-col bg-white dark:bg-[#1e1e1e] border border-zinc-200 dark:border-zinc-700 rounded-2xl shadow-2xl animate-in fade-in slide-in-from-top-3 duration-200", children: [
7030 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between px-5 py-4 border-b border-zinc-100 dark:border-zinc-700/60 shrink-0", children: [ 7031 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 7032 /* @__PURE__ */ jsxRuntimeExports.jsx(Bell, { size: 18, className: "text-indigo-500" }), 7033 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "font-semibold text-base text-zinc-900 dark:text-white", children: t("notifications.title", "Notifications") }), 7034 unreadCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 text-xs font-bold px-2 py-0.5 rounded-full", children: unreadCount }) 7035 ] }), 7036 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 7037 unreadCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs( 7038 "button", 7039 { 7040 onClick: handleMarkAllRead, 7041 className: "flex items-center gap-1.5 text-xs text-indigo-600 dark:text-indigo-400 hover:text-indigo-800 dark:hover:text-indigo-200 px-2.5 py-1.5 rounded-lg hover:bg-indigo-50 dark:hover:bg-indigo-900/30 transition-colors font-medium", 7042 title: t("notifications.markAllRead", "Mark all as read"), 7043 children: [
7044 /* @__PURE__ */ jsxRuntimeExports.jsx(CheckCheck, { size: 14 }), 7045 t("notifications.markAllRead", "Mark all read") 7046 ] 7047 } 7048 ), 7049 /* @__PURE__ */ jsxRuntimeExports.jsx( 7050 "button", 7051 { 7052 onClick: onClose, 7053 className: "p-1.5 rounded-lg text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 hover:bg-zinc-100 dark:hover:bg-zinc-700 transition-colors", 7054 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 18 }) 7055 } 7056 ) 7057 ] }) 7058 ] }), 7059 /* @__PURE__ */ jsxRuntimeExports.jsx( 7060 "div", 7061 { 7062 ref: scrollContainerRef, 7063 className: "flex-1 overflow-y-auto custom-scrollbar", 7064 children: isInitialLoad ? ( 7065 // Skeleton loading 7066 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-0", children: Array.from({ length: 6 }).map((_, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-3 px-5 py-3.5 border-b border-zinc-100 dark:border-zinc-800", children: [
7067 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-10 h-10 rounded-full bg-zinc-200 dark:bg-zinc-700 animate-pulse shrink-0" }), 7068 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 space-y-2 py-1", children: [ 7069 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-3 bg-zinc-200 dark:bg-zinc-700 rounded animate-pulse w-4/5" }), 7070 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-2.5 bg-zinc-100 dark:bg-zinc-800 rounded animate-pulse w-2/5" }) 7071 ] }) 7072 ] }, i)) }) 7073 ) : notifications.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center py-16 gap-3 text-center px-6", children: [ 7074 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-14 h-14 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Bell, { size: 24, className: "text-zinc-400" }) }),
7075 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 dark:text-zinc-400 text-sm leading-relaxed", children: t("notifications.empty", "No notifications here. When someone likes, comments, or subscribes, you'll see it here.") }) 7076 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [ 7077 notifications.map((note) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 7078 "div", 7079 { 7080 onClick: () => handleClick(note), 7081 className: `flex gap-3 px-5 py-3.5 border-b border-zinc-100 dark:border-zinc-800/60 cursor-pointer transition-colors relative group 7082 ${!note.isRead ? "bg-indigo-50/60 dark:bg-indigo-950/20 hover:bg-indigo-50 dark:hover:bg-indigo-950/30" : "hover:bg-zinc-50 dark:hover:bg-zinc-800/40"} 7083 ${note.link ? "cursor-pointer" : "cursor-default"} 7084 `, 7085 children: [ 7086 !note.isRead && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-2 top-1/2 -translate-y-1/2 w-1.5 h-1.5 bg-indigo-500 rounded-full" }),
7087 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative shrink-0", children: [ 7088 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: note.actor.avatar, name: note.actor.name, size: 40 }), 7089 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -bottom-1 -right-1 p-1 bg-white dark:bg-[#1e1e1e] rounded-full ring-2 ring-white dark:ring-[#1e1e1e]", children: getIcon(note.type) }) 7090 ] }), 7091 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [ 7092 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-sm leading-snug text-zinc-900 dark:text-white", children: [ 7093 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold mr-1", children: note.actor.name }), 7094 /* @__PURE__ */ jsxRuntimeExports.jsx( 7095 "span", 7096 { 7097 className: "text-zinc-600 dark:text-zinc-300", 7098 dangerouslySetInnerHTML: { __html: note.message } 7099 } 7100 ) 7101 ] }),
7102 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-400 mt-1 block", children: formatRelativeTime(note.timestamp) }) 7103 ] }), 7104 note.thumbnail ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 w-16 h-10 bg-zinc-200 dark:bg-zinc-800 rounded-md overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: note.thumbnail, className: "w-full h-full object-cover opacity-80", alt: "" }) }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-10 shrink-0" }), 7105 !note.isRead && /* @__PURE__ */ jsxRuntimeExports.jsx( 7106 "button", 7107 { 7108 onClick: (e) => { 7109 e.stopPropagation(); 7110 handleClick({ ...note, link: void 0 }); 7111 }, 7112 className: "absolute right-3 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 p-1 rounded-full bg-zinc-100 dark:bg-zinc-700 text-zinc-500 hover:text-indigo-600 dark:hover:text-indigo-400 transition-all", 7113 title: t("notifications.markRead", "Mark as read"), 7114 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 12 }) 7115 } 7116 ) 7117 ] 7118 }, 7119 note.id 7120 )),
7121 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: sentinelRef, className: "py-2 flex justify-center", children: [ 7122 isLoading && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1.5 items-center text-zinc-400 text-xs py-2", children: [ 7123 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 bg-zinc-400 rounded-full animate-bounce [animation-delay:-0.3s]" }), 7124 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 bg-zinc-400 rounded-full animate-bounce [animation-delay:-0.15s]" }), 7125 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-1.5 h-1.5 bg-zinc-400 rounded-full animate-bounce" }) 7126 ] }), 7127 !hasMore && !isLoading && notifications.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-400 dark:text-zinc-600 py-3", children: t("notifications.endOfList", "You're all caught up!") }) 7128 ] }) 7129 ] }) 7130 } 7131 ) 7132 ] }) 7133 ] }); 7134}; 7135function lazyWithRetry(componentImport) { 7136 return React.lazy(async () => { 7137 try { 7138 return await componentImport(); 7139 } catch (error) { 7140 console.error("Failed to fetch dynamically imported module. Attempting auto-reload...", error); 7141 const hasAutoReloaded = sessionStorage.getItem("has_auto_reloaded_from_error"); 7142 if (!hasAutoReloaded) { 7143 sessionStorage.setItem("has_auto_reloaded_from_error", "true"); 7144 window.location.reload(); 7145 return new Promise(() => { 7146 }); 7147 } 7148 throw error; 7149 } 7150 }); 7151} 7152class ErrorBoundary extends reactExports.Component { 7153 constructor() { 7154 super(...arguments); 7155 this.state = { 7156 hasError: false, 7157 error: null 7158 }; 7159 } 7160 static getDerivedStateFromError(error) { 7161 return { hasError: true, error }; 7162 } 7163 componentDidCatch(error, errorInfo) { 7164 console.error("Uncaught error:", error, errorInfo); 7165 fetch("/api/log-client-error", { 7166 method: "POST", 7167 headers: { "Content-Type": "application/json" }, 7168 body: JSON.stringify({ 7169 message: error.message, 7170 source: "React ErrorBoundary", 7171 lineno: 0, 7172 colno: 0, 7173 stack: error.stack + "\n" + errorInfo.componentStack, 7174 url: window.location.href 7175 }) 7176 }).catch((e) => console.error("Failed to send error log:", e)); 7177 try { 7178 const hasAutoReloaded = sessionStorage.getItem("has_auto_reloaded_from_error"); 7179 if (!hasAutoReloaded) { 7180 sessionStorage.setItem("has_auto_reloaded_from_error", "true"); 7181 window.location.reload(); 7182 } 7183 } catch (e) { 7184 console.error("Failed to handle auto-reload in ErrorBoundary:", e); 7185 } 7186 } 7187 render() { 7188 var _a, _b; 7189 if (this.state.hasError) { 7190 if (this.props.fallback) { 7191 return this.props.fallback; 7192 } 7193 const isAdmin = window.location.pathname.startsWith("/admin"); 7194 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center min-h-screen bg-zinc-50 dark:bg-zinc-900 text-zinc-900 dark:text-white p-6 text-center", children: [
7195 /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-2xl font-bold mb-4", children: isAdmin ? "Module Crash Detected" : "Something went wrong." }), 7196 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-600 dark:text-zinc-400 mb-6 max-w-md", children: isAdmin ? "An error occurred while rendering this module. Check the stack trace below." : "We've logged this error and will look into it. Please try refreshing the page." }), 7197 /* @__PURE__ */ jsxRuntimeExports.jsx( 7198 "button", 7199 { 7200 onClick: () => window.location.reload(), 7201 className: "px-6 py-2 bg-indigo-600 text-white rounded-lg font-bold hover:bg-indigo-700 transition-colors", 7202 children: "Refresh Page" 7203 } 7204 ), 7205 isAdmin && /* @__PURE__ */ jsxRuntimeExports.jsxs("pre", { className: "mt-8 p-4 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-left text-xs overflow-auto max-w-2xl whitespace-pre-wrap break-words", children: [ 7206 (_a = this.state.error) == null ? void 0 : _a.toString(), 7207 "\n\n", 7208 (_b = this.state.error) == null ? void 0 : _b.stack 7209 ] }), 7210 false 7211 ] }); 7212 } 7213 return this.props.children; 7214 } 7215} 7216const HomePage = lazyWithRetry(() => __vitePreload(() => import("./HomePage-q4aW8A7v.js"), true ? __vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22]) : void 0)); 7217const AdvancedSearchPage = lazyWithRetry(() => __vitePreload(() => import("./AdvancedSearchPage-tGa2266t.js"), true ? __vite__mapDeps([23,1,4,5,6,7,8,14,16,3,17,18,19,20,21,22,15,13,24]) : void 0)); 7218const MelodySearchPage = lazyWithRetry(() => __vitePreload(() => import("./MelodySearchPage-C8DfrA33.js"), true ? __vite__mapDeps([25,1,4,5,6,7,8,14,16,3,17,18,19,20,21,22,15,13]) : void 0)); 7219const WatchPage = lazyWithRetry(() => __vitePreload(() => import("./WatchPage-B8_07PT2.js"), true ? __vite__mapDeps([26,1,4,5,6,7,8,22,14,21,3,17,16,18,19,20,27,28,12,13,15]) : void 0)); 7220const ExplorePage = lazyWithRetry(() => __vitePreload(() => import("./ExplorePage-BWHkev4d.js"), true ? __vite__mapDeps([29,1,11,5,6,7,4,8,14,16,3,17,18,19,20,21,22,15,13]) : void 0)); 7221const AccountPage = lazyWithRetry(() => __vitePreload(() => import("./AccountPage-mZQDQlod.js"), true ? __vite__mapDeps([30,1,14,4,5,6,7,8,31,13,32,16,3,17,18,19,20,21,22,15]) : void 0)); 7222const ConverterPage = lazyWithRetry(() => __vitePreload(() => import("./ConverterPage-kt7PUb_r.js"), true ? __vite__mapDeps([33,16,1,6,5,7,4,8,34,21,22,14,35]) : void 0)); 7223const EditorPage = lazyWithRetry(() => __vitePreload(() => import("./EditorPage-Dsed0DcB.js"), true ? __vite__mapDeps([36,1,37,20,6,5,7,28,38,19,3,4,8,16,32]) : void 0)); 7224const HistoryPage = lazyWithRetry(() => __vitePreload(() => import("./HistoryPage-BYHoeFoZ.js"), true ? __vite__mapDeps([39,1,40,14,4,5,6,7,8,16,3,17,18,19,20,21,22,15,13]) : void 0)); 7225const LikedPage = lazyWithRetry(() => __vitePreload(() => import("./LikedPage-SwAUDqZq.js"), true ? __vite__mapDeps([41,1,40,14,4,5,6,7,8,16,3,17,18,19,20,21,22,15,13]) : void 0)); 7226const WatchLaterPage = lazyWithRetry(() => __vitePreload(() => import("./WatchLaterPage-32xxryAT.js"), true ? __vite__mapDeps([42,1,40,14,4,5,6,7,8,16,3,17,18,19,20,21,22,15,13]) : void 0)); 7227const SubscriptionsPage = lazyWithRetry(() => __vitePreload(() => import("./SubscriptionsPage-D5-uiNqs.js"), true ? __vite__mapDeps([43,1,40,14,4,5,6,7,8,16,3,17,18,19,20,21,22,15,13]) : void 0)); 7228const ChannelPage = lazyWithRetry(() => __vitePreload(() => import("./ChannelPage-DAFNmqL_.js"), true ? __vite__mapDeps([44,1,4,5,6,7,8,14,13,3,16,17,18,19,20,21,22,15]) : void 0)); 7229const TagPage = lazyWithRetry(() => __vitePreload(() => import("./TagPage-DI5S2d93.js"), true ? __vite__mapDeps([45,1,4,5,6,7,8,40,14,16,3,17,18,19,20,21,22,15,13]) : void 0)); 7230const PlaylistsPage = lazyWithRetry(() => __vitePreload(() => import("./PlaylistsPage-Br-5J847.js"), true ? __vite__mapDeps([31,1,4,5,6,7,8]) : void 0)); 7231const HelpPage = lazyWithRetry(() => __vitePreload(() => import("./HelpPage-DsiYZpvH.js"), true ? __vite__mapDeps([46,1,4,5,6,7,8,3,34]) : void 0)); 7232const MessengerPage = lazyWithRetry(() => __vitePreload(() =>
7232 import("./MessengerPage-D0_m_QNf.js"), true ? __vite__mapDeps([47,1,4,5,6,7,8,13]) : void 0)); 7233const LyricsPage = lazyWithRetry(() => __vitePreload(() => import("./LyricsPage-BWsBdPvc.js"), true ? __vite__mapDeps([48,1,4,5,6,7,8,14]) : void 0)); 7234const LyricsSearchPage = lazyWithRetry(() => __vitePreload(() => import("./LyricsSearchPage-CzRGUKfj.js"), true ? __vite__mapDeps([49,1,4,5,6,7,8]) : void 0)); 7235const AdminApp = lazyWithRetry(() => __vitePreload(() => import("./AdminApp-D1oXn0it.js"), true ? __vite__mapDeps([50,1,16,6,5,7,4,8]) : void 0)); 7236const PersonalSpacePage = lazyWithRetry(() => __vitePreload(() => import("./PersonalSpacePage-BQt1uXtk.js"), true ? __vite__mapDeps([51,1,4,5,6,7,8,3]) : void 0)); 7237const NewlyUploadedPage = lazyWithRetry(() => __vitePreload(() => import("./NewlyUploadedPage-CswaPEfP.js"), true ? __vite__mapDeps([52,1,14,4,5,6,7,8,16,3,17,18,19,20,21,22,15,13]) : void 0)); 7238const BillingPage = lazyWithRetry(() => __vitePreload(() => import("./BillingPage-B2Hb0PMo.js"), true ? __vite__mapDeps([53,1,4,5,6,7,8]) : void 0)); 7239const CollectionIndexPage = lazyWithRetry(() => __vitePreload(() => import("./CollectionIndexPage-B76qFKH2.js"), true ? __vite__mapDeps([54,1,4,5,6,7,8,3,9]) : void 0)); 7240const CollectionPage = lazyWithRetry(() => __vitePreload(() => import("./CollectionPage-DTEimYXF.js"), true ? __vite__mapDeps([55,1,4,5,6,7,8,2,3,9,10,16,17,18,19,20,21,22,15,13]) : void 0)); 7241const CollectionSectionPage = lazyWithRetry(() => __vitePreload(() => import("./CollectionSectionPage-B6Zl3jqi.js"), true ? __vite__mapDeps([56,1,4,5,6,7,8,10,3,9,16,17,18,19,20,21,22,15,13]) : void 0)); 7242const EmailVerification = lazyWithRetry(() => __vitePreload(() => import("./EmailVerification-D-JaNSfM.js"), true ? __vite__mapDeps([57,1,4,5,6,7,8]) : void 0)); 7243const ResetPassword = lazyWithRetry(() => __vitePreload(() => import("./ResetPassword-CpmlaEYr.js"), true ? __vite__mapDeps([58,1,4,5,6,7,8]) : void 0)); 7244const ScoreConverterPage = lazyWithRetry(() => __vitePreload(() => import("./ScoreConverterPage-BvnaneVe.js"), true ? __vite__mapDeps([59,1,35,5,6,7]) : void 0)); 7245const VocalProcessorPage = lazyWithRetry(() => __vitePreload(() => import("./VocalProcessorPage-BWIy7sro.js"), true ? __vite__mapDeps([60,1,6,5,7,17,16,18,19,20]) : void 0)); 7246const AlgorithmStudio = lazyWithRetry(() => __vitePreload(() => import("./AlgorithmStudio-BAKVJ64e.js"), true ? __vite__mapDeps([61,1,6,5,7]) : void 0)); 7247const AlgorithmEditor = lazyWithRetry(() => __vitePreload(() => import("./AlgorithmEditor-BuTd-7oq.js"), true ? __vite__mapDeps([62,1,63,6,5,7]) : void 0)); 7248const StagingArena = lazyWithRetry(() => __vitePreload(() => import("./StagingArena-C9ym5iYy.js"), true ? __vite__mapDeps([64,1,63,6,5,7]) : void 0)); 7249const TribunalPage = lazyWithRetry(() => __vitePreload(() => import("./TribunalPage-yYCBAk5S.js"), true ? __vite__mapDeps([65,1,4,5,6,7,8,13,16,3,17,18,19,20,21,22,15]) : void 0)); 7250const HamieLabPage = lazyWithRetry(() => __vitePreload(() => import("./HamieLabPage-KXwalnzA.js"), true ? __vite__mapDeps([66,1,4,5,6,7,8,15]) : void 0)); 7251const MidiVisualizerPage = lazyWithRetry(() => __vitePreload(() => import("./MidiVisualizerPage-B4L5oo98.js"), true ? __vite__mapDeps([67,16,1,6,5,7,20,4,8,3]) : void 0)); 7252const DMCAForm = lazyWithRetry(() => __vitePreload(() => import("./DMCAForm-DoA_m2iR.js"), true ? __vite__mapDeps([68,1,6,5,7]) : void 0)); 7253const SheetMusicDigitizerPage = lazyWithRetry(() => __vitePreload(() => import("./SheetMusicDigitizerPage-CGO_x0iu.js"), true ? __vite__mapDeps([69,1,4,5,6,7,8,34,37,20,28,38,19,3,35]) : void 0)); 7254const AudioDigitizerPage = lazyWithRetry(() => __vitePreload(() => import("./AudioDigitizerPage-DjRFGvnM.js"), true ? __vite__mapDeps([70,1,4,5,6,7,8,34]) : void 0)); 7255const ClientAudioDigitizerPage = lazyWithRetry(() => __vitePreload(() => import("./ClientAudioDigitizerPage-wnww40sz.js"), true ? __vite__mapDeps([71,16,1,6,5,7,72,20]) : void 0)); 7256const NotationEditor = lazyWithRetry(() => __vitePreload(() => import("./index--Kd7xwwe.js").then((n) => n.i), true ? __vite__mapDeps([73,16,1,6,5,7,20,4,8]) : void 0)); 7257const KeyboardEmulatorPage = lazyWithRetry(() => __vitePreload(() => import("./KeyboardEmulatorPage-3bHJ91O7.js"), true ? __vite__mapDeps([74,1,6,5,7,4,8,18,19,22,28]) : void 0)); 7258const SoundfontsPage = lazyWithRetry(() => __vitePreload(() => import("./SoundfontsPage-Kim5OuPr.js"), true ? __vite__mapDeps([75,1,4,5,6,7,8,14]) : void 0)); 7259const SoundfontDetailPage = lazyWithRetry(() => __vitePreload(() => import("./SoundfontDetailPage-R4ZkN9OB.js"), true ? __vite__mapDeps([76,1,4,5,6,7,8,27,16,28,18,19]) : void 0)); 7260const SoundfontUploadPage = lazyWithRetry(() => __vitePreload(() => import("./SoundfontUploadPage-C9Pu1znK.js"), true ? __vite__mapDeps([77,1,4,5,6,7,8]) : void 0)); 7261const DynamicPageRenderer = ({ module }) => { 7262 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-10 max-w-4xl mx-auto animate-in fade-in", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-yellow-50 dark:bg-yellow-900/10 border border-yellow-200 dark:border-yellow-800 rounded-xl p-6 mb-6", children: [
7263 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-yellow-800 dark:text-yellow-200 mb-2", children: "Dynamic Preview" }), 7264 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-yellow-700 dark:text-yellow-300 mb-4", children: "This page was created via Site Architect. In a production environment, this would be a server-rendered page or a dynamically compiled component." }), 7265 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-white dark:bg-black p-4 rounded-lg border border-zinc-200 dark:border-zinc-800 font-mono text-xs overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { children: module.content }) }) 7266 ] }) }); 7267}; 7268const LoadingFallback = () => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex h-full items-center justify-center min-h-[50vh]", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-8 h-8 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin" }) }); 7269const KNOWN_VIEWS = /* @__PURE__ */ new Set([ 7270 "home", 7271 "explore", 7272 "history", 7273 "melody-search", 7274 "liked", 7275 "watchlater", 7276 "subscriptions", 7277 "channel", 7278 "tag", 7279 "playlists", 7280 "help", 7281 "terms", 7282 "privacy", 7283 "messages", 7284 "account", 7285 "converter", 7286 "editor", 7287 "algorithm-studio", 7288 "algorithm-editor", 7289 "algorithm-arena", 7290 "tribunal", 7291 "lab", 7292 "visualizer", 7293 "dmca", 7294 "verify-email", 7295 "reset-password", 7296 "not-found", 7297 "admin", 7298 "lyrics", 7299 "lyrics-search", 7300 "sheet-digitizer", 7301 "audio-digitizer", 7302 "client-transcriber", 7303 "score-format-converter", 7304 "vocal-processor", 7305 "lab", 7306 "notation-editor", 7307 "personal-space", 7308 "newly-uploaded", 7309 "billing", 7310 "search", 7311 "collections", 7312 "collection", 7313 "collection-section", 7314 "keyboard-emulator", 7315 "soundfonts", 7316 "soundfont", 7317 "soundfonts-upload" 7318]); 7319function parseUrl(pathname, search) { 7320 const params = new URLSearchParams(search); 7321 const tagName = params.get("name") || null; 7322 const channelName = params.get("c") || null; 7323 const query = params.get("q") || ""; 7324 let view = "home"; 7325 let id = null; 7326 let category = params.get("cat") || "all"; 7327 const path = pathname; 7328 if (path === "/admin" || path.startsWith("/admin/")) { 7329 view = "admin"; 7330 } else { 7331 const lyricMatch = path.match(/^\/lyrics(\d+)\.html$/); 7332 const cleanLyricMatch = path.match(/^\/lyrics\/(\d+)$/); 7333 const scoreMatch = path.match(/^\/score(b)?(\d+)-(\d+)(?:-(\d+))?\.html$/); 7334 const sheetmusicMatch = path.match(/^\/(?:sheetmusic|gtab)(\d+)\.html$/); 7335 if (lyricMatch || cleanLyricMatch) { 7336 id = lyricMatch ? lyricMatch[1] : cleanLyricMatch[1]; 7337 view = "lyrics"; 7338 } else if (path === "/lyrics/search") { 7339 view = "lyrics-search"; 7340 } else if (scoreMatch || sheetmusicMatch) { 7341 id = scoreMatch ? scoreMatch[2] : sheetmusicMatch[1]; 7342 view = "watch"; 7343 } else { 7344 const match = path.match(/^\/midi(?:[a-zA-Z0-9_\-]+\/)?(\d+)_(.+)\.html$/); 7345 const trackMatch = path.match(/^\/track\/(\d+)$/); 7346 if (match) { 7347 id = match[1]; 7348 view = "watch"; 7349 } else if (trackMatch) { 7350 id = trackMatch[1]; 7351 view = "watch"; 7352 } else if (path === "/watch") { 7353 const vid = params.get("v"); 7354 if (vid) { 7355 id = vid; 7356 view = "watch"; 7357 } 7358 } else if (path.startsWith("/algorithm-editor/")) { 7359 id = path.split("/").pop() || null; 7360 view = "algorithm-editor"; 7361 } else if (path.startsWith("/algorithm-arena/")) { 7362 id = path.split("/").pop() || null; 7363 view = "algorithm-arena"; 7364 } else if (path.startsWith("/editor/")) { 7365 id = path.split("/").pop() || null; 7366 view = "editor"; 7367 } else if (path.startsWith("/help/")) { 7368 id = path.split("/").pop() || null; 7369 view = "help"; 7370 } else if (path.startsWith("/collections/")) { 7371 const parts = path.split("/"); 7372 if (parts.length >= 4) { 7373 const secIdWithSlug = parts.pop() || ""; 7374 const slug = parts.pop() || null; 7375 id = slug && secIdWithSlug ? `${slug}:${secIdWithSlug}` : null; 7376 view = "collection-section"; 7377 } else { 7378 id = parts.pop() || null; 7379 view = "collection"; 7380 } 7381 } else if (path.startsWith("/collection/")) { 7382 let slug = path.split("/").pop() || null; 7383 if (slug === "top-100-classical-composers") { 7384 slug = "classical-composers"; 7385 if (typeof window !== "undefined" && window.history) { 7386 window.history.replaceState(null, "", "/collection/classical-composers"); 7387 } 7388 } 7389 id = slug; 7390 view = "collection"; 7391 } else if (path.startsWith("/collection-section/")) { 7392 id = path.split("/").pop() || null; 7393 view = "collection-section"; 7394 } else if (path === "/soundfonts") { 7395 view = "soundfonts"; 7396 } else if (path === "/soundfonts/upload") { 7397 view = "soundfonts-upload"; 7398 } else if (path.startsWith("/soundfont/")) { 7399 id = path.split("/").pop() || null; 7400 view = "soundfont"; 7401 } else if (path === "/editor") { 7402 view = "editor"; 7403 id = null; 7404 } else if (path === "/" || path === "") { 7405 const vid = params.get("v"); 7406 if (vid) { 7407 id = vid; 7408 view = "watch"; 7409 } else { 7410 view = "home"; 7411 category = params.get("cat") || "all"; 7412 } 7413 } else if (path !== "/" && path !== "") { 7414 view = path.substring(1); 7415 const baseViewName = view.split("?")[0].split("#")[0].toLowerCase(); 7416 if (!KNOWN_VIEWS.has(baseViewName)) {
7417 category = baseViewName; 7418 } 7419 } 7420 } 7421 } 7422 return { view, id, tag: tagName, channel: channelName, query, category }; 7423} 7424const AppContent = () => { 7425 const { t, i18n } = useTranslation(); 7426 const { autoplay, audioQuality, adSettings } = useSettings(); 7427 const [currentUser, setCurrentUser] = reactExports.useState(() => { 7428 const urlParams = typeof window !== "undefined" ? new URLSearchParams(window.location.search) : null; 7429 const keyParam = urlParams ? urlParams.get("key") : null; 7430 const isBotKey = keyParam === "digitizer-test-2026-key" || typeof window !== "undefined" && sessionStorage.getItem("digitizer_test_key") === "digitizer-test-2026-key"; 7431 if (isBotKey) { 7432 if (typeof window !== "undefined" && keyParam) { 7433 sessionStorage.setItem("digitizer_test_key", keyParam); 7434 } 7435 return { 7436 id: 1, 7437 username: "Antigravity", 7438 email: "[email protected]", 7439 role: "USER", 7440 subscriptionTier: "premium", 7441 credits: 99999, 7442 createdAt: (/* @__PURE__ */ new Date()).toISOString(), 7443 updatedAt: (/* @__PURE__ */ new Date()).toISOString(), 7444 isBanned: false, 7445 showCountryBadge: false, 7446 uploadsCount: 0 7447 }; 7448 } 7449 const saved = localStorage.getItem("hamie_session"); 7450 if (saved) { 7451 try { 7452 return JSON.parse(saved); 7453 } catch { 7454 return null; 7455 } 7456 } 7457 return null; 7458 }); 7459 reactExports.useEffect(() => { 7460 const timer = setTimeout(() => { 7461 try { 7462 sessionStorage.removeItem("has_auto_reloaded_from_error"); 7463 } catch (e) { 7464 } 7465 }, 3e3); 7466 return () => clearTimeout(timer); 7467 }, []); 7468 reactExports.useEffect(() => { 7469 var _a; 7470 if (!adSettings || !adSettings.isEnabled || !adSettings.publisherId) { 7471 const existingScript2 = document.getElementById("adsense-global-script"); 7472 if (existingScript2) existingScript2.remove(); 7473 return; 7474 } 7475 const userRole = (currentUser == null ? void 0 : currentUser.role) || "GUEST"; 7476 const isExcluded = (_a = adSettings.excludeRoles) == null ? void 0 : _a.includes(userRole); 7477 if (isExcluded) { 7478 const existingScript2 = document.getElementById("adsense-global-script"); 7479 if (existingScript2) existingScript2.remove(); 7480 return; 7481 } 7482 const client = adSettings.publisherId.startsWith("ca-") ? adSettings.publisherId : `ca-${adSettings.publisherId}`; 7483 const isNonIntrusive = !!adSettings.enableNonIntrusiveOnly; 7484 isNonIntrusive ? false : adSettings.enableAutoAds; 7485 isNonIntrusive ? false : adSettings.enableVignettes; 7486 isNonIntrusive ? false : adSettings.enableAnchors; 7487 const existingScript = document.getElementById("adsense-global-script"); 7488 if (existingScript) existingScript.remove(); 7489 const script = document.createElement("script"); 7490 script.id = "adsense-global-script"; 7491 script.src = `https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=${client}`; 7492 script.async = true; 7493 script.crossOrigin = "anonymous"; 7494 script.onerror = () => { 7495 console.warn("Google AdSense script blocked or failed to load."); 7496 }; 7497 document.head.appendChild(script); 7498 try { 7499 window.adsbygoogle = window.adsbygoogle || []; 7500 window.__adsbygoogle_pushed = true; 7501 } catch (err) { 7502 console.warn("Failed to set adsense settings programmatically:", err); 7503 } 7504 }, [adSettings, currentUser]); 7505 reactExports.useEffect(() => { 7506 if (typeof window === "undefined" || typeof document === "undefined") return; 7507 const html = document.documentElement; 7508 const body = document.body; 7509 const checkAndFixGap = () => { 7510 const hasHtmlMargin = html.style.marginTop && html.style.marginTop !== "0px"; 7511 const hasHtmlPadding = html.style.paddingTop && html.style.paddingTop !== "0px"; 7512 const hasBodyMargin = body.style.marginTop && body.style.marginTop !== "0px"; 7513 const hasBodyPadding = body.style.paddingTop && body.style.paddingTop !== "0px"; 7514 if (hasHtmlMargin || hasHtmlPadding || hasBodyMargin || hasBodyPadding) { 7515 const fixedElements = Array.from(document.querySelectorAll("*")).filter((el) => { 7516 try { 7517 const style = window.getComputedStyle(el); 7518 return (style.position === "fixed" || style.position === "sticky") && (style.top === "0px" || style.top === "0") && el !== html && el !== body && el.id !== "root" && !el.closest("header") && el.tagName !== "HEADER"; 7519 } catch (e) { 7520 return false;
7521 } 7522 }); 7523 let topAdVisible = false; 7524 for (const el of fixedElements) { 7525 const hasGoogleAd = el.querySelector('ins.adsbygoogle, iframe[id*="google_ads"], iframe[src*="googleads"]') || el.classList.contains("adsbygoogle") || el.id.includes("google") || Array.from(el.attributes).some((attr) => attr.name.includes("google") || attr.value.includes("google")); 7526 if (hasGoogleAd) { 7527 const rect = el.getBoundingClientRect(); 7528 if (rect.height > 0 && window.getComputedStyle(el).display !== "none") { 7529 topAdVisible = true; 7530 break; 7531 } 7532 } 7533 } 7534 if (!topAdVisible) { 7535 if (hasHtmlMargin && html.style.getPropertyValue("margin-top") !== "0px") html.style.setProperty("margin-top", "0px", "important"); 7536 if (hasHtmlPadding && html.style.getPropertyValue("padding-top") !== "0px") html.style.setProperty("padding-top", "0px", "important"); 7537 if (hasBodyMargin && body.style.getPropertyValue("margin-top") !== "0px") body.style.setProperty("margin-top", "0px", "important"); 7538 if (hasBodyPadding && body.style.getPropertyValue("padding-top") !== "0px") body.style.setProperty("padding-top", "0px", "important"); 7539 } 7540 } 7541 }; 7542 const observer = new MutationObserver(() => { 7543 observer.disconnect(); 7544 checkAndFixGap(); 7545 observer.observe(html, { attributes: true, attributeFilter: ["style", "class"] }); 7546 observer.observe(body, { attributes: true, attributeFilter: ["style", "class"] }); 7547 }); 7548 observer.observe(html, { attributes: true, attributeFilter: ["style", "class"] }); 7549 observer.observe(body, { attributes: true, attributeFilter: ["style", "class"] }); 7550 const intervalId = setInterval(checkAndFixGap, 1e3); 7551 window.addEventListener("message", checkAndFixGap); 7552 window.addEventListener("resize", checkAndFixGap); 7553 checkAndFixGap(); 7554 return () => { 7555 observer.disconnect(); 7556 clearInterval(intervalId); 7557 window.removeEventListener("message", checkAndFixGap); 7558 window.removeEventListener("resize", checkAndFixGap); 7559 }; 7560 }, []); 7561 const [isSessionLoaded, setIsSessionLoaded] = reactExports.useState(true); 7562 const [isAuthModalOpen, setIsAuthModalOpen] = reactExports.useState(false); 7563 const [isUploadModalOpen, setIsUploadModalOpen] = reactExports.useState(false); 7564 const [uploadInitialFile, setUploadInitialFile] = reactExports.useState(null); 7565 const [uploadInitialMetadata, setUploadInitialMetadata] = reactExports.useState(null); 7566 const [isAddToPlaylistModalOpen, setIsAddToPlaylistModalOpen] = reactExports.useState(false); 7567 const [playlistTrack, setPlaylistTrack] = reactExports.useState(null); 7568 const [isNotificationsModalOpen, setIsNotificationsModalOpen] = reactExports.useState(false); 7569 const [currentLocation, setCurrentLocation] = reactExports.useState(() => ({ 7570 pathname: typeof window !== "undefined" ? window.location.pathname : "/", 7571 search: typeof window !== "undefined" ? window.location.search : "" 7572 })); 7573 const navigate = React.useCallback((url, options) => { 7574 try { 7575 const cleanUrl = url.startsWith("/") ? url : "/" + url; 7576 const currentUrl = window.location.pathname + window.location.search; 7577 if (cleanUrl === currentUrl && !(options == null ? void 0 : options.replace)) { 7578 return; 7579 } 7580 if (options == null ? void 0 : options.replace) { 7581 window.history.replaceState(null, "", cleanUrl); 7582 } else { 7583 window.history.pushState(null, "", cleanUrl); 7584 } 7585 setCurrentLocation({ 7586 pathname: window.location.pathname, 7587 search: window.location.search 7588 }); 7589 } catch (e) { 7590 console.warn("History navigation blocked or rate limited:", e); 7591 } 7592 }, []); 7593 const route = React.useMemo(() => parseUrl(currentLocation.pathname, currentLocation.search), [currentLocation]); 7594 const activeView = route.view; 7595 const activeViewId = route.id; 7596 const submittedQuery = route.query; 7597 const activeCategory = route.category; 7598 const setActiveView = React.useCallback((v) => { 7599 let target = v.startsWith("/") ? v : "/" + v; 7600 if (v === "home") target = "/"; 7601 navigate(target); 7602 }, [navigate]); 7603 const setActiveCategory = React.useCallback((cat) => { 7604 if (cat === "all") { 7605 navigate("/"); 7606 } else { 7607 navigate("/" + cat); 7608 } 7609 }, [navigate]); 7610 const [searchQuery, setSearchQuery] = reactExports.useState(() => { 7611 if (typeof window !== "undefined") { 7612 const params = new URLSearchParams(window.location.search); 7613 return params.get("q") || ""; 7614 } 7615 return ""; 7616 }); 7617 reactExports.useEffect(() => { 7618 setSearchQuery(submittedQuery || ""); 7619 }, [submittedQuery]); 7620 const { activeTrack, setActiveTrack, isPlaying, setIsPlaying, queue, setQueue, activePlaylist, setActivePlaylist, playlistLabel, setPlaylistLabel, seekRequest, setSeekRequest, currentTime, setCurrentTime, duration, setDuration, seekTo, audioMode, setAudioMo
7620de, isBuffering, setIsBuffering, setIsLiveSynthFallback, engineOrder } = usePlayer(); 7621 const [isSidebarOpen, setIsSidebarOpen] = reactExports.useState(() => { 7622 if (typeof window !== "undefined") { 7623 if (window.innerWidth < 1024) { 7624 return false; 7625 } 7626 const path = window.location.pathname; 7627 return path === "/" || path === ""; 7628 } 7629 return true; 7630 }); 7631 const [mutedChannels, setMutedChannels] = reactExports.useState([]); 7632 const [likedMidis, setLikedMidis] = reactExports.useState(/* @__PURE__ */ new Set()); 7633 const [playbackRate, setPlaybackRate] = reactExports.useState(1); 7634 const [pitchSemitones, setPitchSemitones] = reactExports.useState(0); 7635 const [tracks, setTracks] = reactExports.useState([]); 7636 const contextTracksRef = reactExports.useRef([]); 7637 const [siteModules, setSiteModules] = reactExports.useState([]); 7638 const [labConfig, setLabConfig] = reactExports.useState(null); 7639 const [isLoading, setIsLoading] = reactExports.useState(true); 7640 const [isTracksLoading, setIsTracksLoading] = reactExports.useState(false); 7641 const [lyricsResults, setLyricsResults] = reactExports.useState([]); 7642 const [isLyricsLoading, setIsLyricsLoading] = reactExports.useState(false); 7643 const [isMelodySearchOpen, setIsMelodySearchOpen] = reactExports.useState(false); 7644 const [melodyResults, setMelodyResults] = reactExports.useState(null); 7645 const [isSearchingMelody, setIsSearchingMelody] = reactExports.useState(false); 7646 const [isFullscreen, setIsFullscreen] = reactExports.useState(false); 7647 reactExports.useEffect(() => { 7648 const handleFullscreenChange = () => { 7649 setIsFullscreen(!!document.fullscreenElement); 7650 }; 7651 document.addEventListener("fullscreenchange", handleFullscreenChange); 7652 return () => { 7653 document.removeEventListener("fullscreenchange", handleFullscreenChange); 7654 }; 7655 }, []); 7656 const [notFoundError, setNotFoundError] = reactExports.useState(null); 7657 const [isGameMode, setIsGameMode] = reactExports.useState(false); 7658 reactExports.useEffect(() => { 7659 if (activeView !== "home") { 7660 setIsSidebarOpen(false); 7661 } else { 7662 if (window.innerWidth < 1024) { 7663 setIsSidebarOpen(false); 7664 } else { 7665 setIsSidebarOpen(true); 7666 } 7667 } 7668 }, [activeView]); 7669 reactExports.useEffect(() => { 7670 if (activeView === "lab") { 7671 document.title = "Hamie Lab | HamieNET"; 7672 } else if (activeView === "search") { 7673 if (submittedQuery) { 7674 document.title = `${submittedQuery} - ${t("search.title", "Search")} | HamieNET`; 7675 } else { 7676 document.title = `${t("search.advancedSearch", "Advanced Search")} | HamieNET`; 7677 } 7678 } else { 7679 const tool = DEFAULT_TOOLS.find((t2) => t2.route === activeView); 7680 if (tool) { 7681 document.title = `${tool.name} | HamieNET`; 7682 } else { 7683 const standardTitles = { 7684 "home": "HamieNET - MIDI Archive", 7685 "explore": "Explore | HamieNET", 7686 "history": "History | HamieNET", 7687 "liked": "Liked MIDIs | HamieNET", 7688 "watchLater": "Watch Later | HamieNET", 7689 "subscriptions": "Subscriptions | HamieNET", 7690 "playlists": "Playlists | HamieNET", 7691 "messages": "Messages | HamieNET", 7692 "account": "My Account | HamieNET", 7693 "personal-space": "Personal Space | HamieNET", 7694 "help": "Help Center | HamieNET", 7695 "terms": "Terms of Service | HamieNET", 7696 "privacy": "Privacy Policy | HamieNET", 7697 "billing": "Billing | HamieNET" 7698 }; 7699 if (standardTitles[activeView]) { 7700 document.title = standardTitles[activeView]; 7701 } 7702 } 7703 } 7704 }, [activeView, submittedQuery, t, i18n.language]); 7705 const [history2, setHistory] = reactExports.useState([]); 7706 const [watchLater, setWatchLater] = reactExports.useState(/* @__PURE__ */ new Set()); 7707 const [likedTracks, setLikedTracks] = reactExports.useState([]); 7708 const [watchLaterTracks, setWatchLaterTracks] = reactExports.useState([]); 7709 const [subscriptionTracks, setSubscriptionTracks] = reactExports.useState([]); 7710 const [subscriptions, setSubscriptions] = reactExports.useState(/* @__PURE__ */ new Set()); 7711 const selectedTag = route.tag; 7712 const selectedChannel = route.channel; 7713 const [notifications, setNotifications] = reactExports.useState([]); 7714 const [unreadMessageCount, setUnreadMessageCount] = reactExports.useState(0); 7715 const [theme, setTheme] = reactExports.useState(() => { 7716 const saved = localStorage.getItem("hamie_theme"); 7717 return saved || "light"; 7718 }); 7719 reactExports.useEffect(() => { 7720 const checkI18n = async () => { 7721 const getCookie = (name) => { 7722 var _a; 7723 const value = `; ${document.cookie}`; 7724 const parts = value.split(`; ${name}=`); 7725 if (parts.length === 2) return (_a = parts.pop()) == null ? void 0 : _a.split(";").shift(); 7726 return null; 7727 }; 7728 const savedSettings = localStorage.getItem("hamie_settings"); 7729 const hasManualLang = savedSettings && savedSettings.includes('"language"'); 7730 const hasLang = getCookie("i18nextLng") || localStorage.getItem("i18nextLng") || hasManualLang; 7731 const hasRegion = getCookie("hamie_region"); 7732 if (!hasLang || !hasRegion) { 7733 try { 7734 const { country } = await api.geolocate(); 7735 const expiry = /* @__PURE__ */ new Date(); 7736 expiry.setFullYear(expiry.getFullYear() + 1); 7737 if (!hasRegion) { 7738 document.cookie = `hamie_region=${country}; expires=${expiry.toUTCString()}; path=/; SameSite=Lax`; 7739 } 7740 if (!hasLang) { 7741 const countryToLang = { 7742 "JP": "ja", 7743 "FR": "fr", 7744 "DE": "de", 7745 "ES": "es", 7746 "IT": "it", 7747 "RU": "ru", 7748 "CN": "zh-CN", 7749 "TW": "zh-TW", 7750 "KR": "ko", 7751 "BR": "pt", 7752 "PT": "pt", 7753 "SA": "ar", 7754 "EG": "ar", 7755 "ID": "id" 7756 }; 7757 const targetLang = countryToLang[country] || "en"; 7758 __vitePreload(() => import("./index-D0Bmh-UX.js").then((n) => n.i), true ? __vite__mapDeps([4,1,5,6,7,8]) : void 0).then((module) => { 7759 module.default.changeLanguage(targetLang); 7760 }); 7761 document.cookie = `i18nextLng=${targetLang}; expires=${expiry.toUTCString()}; path=/; SameSite=Lax`; 7762 } 7763 } catch (e) { 7764 console.error("Smart i18n detection failed", e); 7765 } 7766 } 7767 }; 7768 checkI18n(); 7769 }, []); 7770 const [autoPlayCountdown, setAutoPlayCountdown] = reactExports.useState(null); 7771 const countdownIntervalRef = reactExports.useRef(null); 7772 const lastLoadingRef = reactExports.useRef(false); 7773 const loadStartRef = reactExports.useRef(0); 7774 const activeTrackIdRef = reactExports.useRef(null); 7775 const isPlayingRef = reactExports.useRef(isPlaying); 7776 reactExports.useEffect(() => { 7777 isPlayingRef.current = isPlaying; 7778 }, [isPlaying]); 7779 reactExports.useEffect(() => { 7780 activeTrackIdRef.current = activeTrack ? activeTrack.id : null; 7781 }, [activeTrack]); 7782 reactExports.useEffect(() => { 7783 if (activeTrack) { 7784 if (isPlayingRef.current && activeTrackIdRef.current === activeTrack.id) { 7785 return; 7786 } 7787 const hasWebMidiSupport = typeof navigator !== "undefined" && !!navigator.requestMIDIAccess; 7788 const midiOutputs = synth.getMidiOutputs() || []; 7789 const preferred = typeof window !== "undefined" ? localStorage.getItem("preferredAudioMode") : null;
7790 const best = getBestAudioMode(activeTrack, isSlowMobile(), hasWebMidiSupport, midiOutputs.length > 0, 0, activeView === "watch", engineOrder, preferred); 7791 console.log("[App.tsx] ð Sync Effect running, setting mode to", best.mode, "activeView:", activeView, "isWatchPage:", activeView === "watch", "hasStems:", !!(activeTrack.instruments && activeTrack.instruments.some((i) => i.stemPath)), "hasMasterAudio:", !!(activeTrack.status === "completed" && (activeTrack.opusPath || activeTrack.mp3PathHigh || activeTrack.mp3Path))); 7792 setAudioMode(best.mode); 7793 setIsLiveSynthFallback(best.isLiveSynthFallback); 7794 } 7795 }, [activeTrack == null ? void 0 : activeTrack.id, activeTrack == null ? void 0 : activeTrack.status, activeTrack == null ? void 0 : activeTrack.opusPath, activeTrack == null ? void 0 : activeTrack.mp3PathHigh, setAudioMode, setIsLiveSynthFallback, activeView, engineOrder]); 7796 const parseTimeParam = (tStr) => { 7797 if (!tStr) return null; 7798 if (tStr.includes(":")) { 7799 const parts = tStr.split(":"); 7800 if (parts.length === 2) { 7801 const m = parseInt(parts[0], 10); 7802 const s = parseInt(parts[1], 10); 7803 if (!isNaN(m) && !isNaN(s)) { 7804 return m * 60 + s; 7805 } 7806 } 7807 } 7808 const mMatch = tStr.match(/(\d+)m/); 7809 let seconds = 0; 7810 if (mMatch) { 7811 seconds += parseInt(mMatch[1], 10) * 60; 7812 const cleanStr = tStr.replace(mMatch[0], ""); 7813 const sOnlyMatch = cleanStr.match(/(\d+)/); 7814 if (sOnlyMatch) { 7815 seconds += parseInt(sOnlyMatch[1], 10); 7816 } 7817 return seconds; 7818 } 7819 const parsed = parseInt(tStr, 10); 7820 if (!isNaN(parsed)) return parsed; 7821 return null; 7822 }; 7823 const handleSelectTrack = React.useCallback((track, forcePlay, playlist, keepView) => { 7824 var _a; 7825 const isAlreadyActive = track && activeTrack && track.id === activeTrack.id; 7826 if (!isAlreadyActive) { 7827 setAutoPlayCountdown(null); 7828 setSeekRequest(null); 7829 if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); 7830 setCurrentTime(0); 7831 } 7832 if (track && track.id) { 7833 if (playlist !== void 0) { 7834 setActivePlaylist(playlist); 7835 } else if (!isAlreadyActive) { 7836 setActivePlaylist(null); 7837 contextTracksRef.current = tracks; 7838 } 7839 setHistory((prev) => [track, ...prev.filter((t2) => t2.id !== track.id)].slice(0, 100)); 7840 setActiveTrack(track); 7841 const metaDur = Number((_a = track.midiMetadata) == null ? void 0 : _a.duration); 7842 const strDur = parseTimeParam(track.duration); 7843 const initialDur = !isNaN(metaDur) && metaDur > 0 ? metaDur : strDur && strDur > 0 ? strDur : 0; 7844 if (initialDur > 0) { 7845 setDuration(initialDur); 7846 } 7847 if (!isAlreadyActive || forcePlay !== void 0) { 7848 setIsPlaying(forcePlay !== void 0 ? forcePlay : autoplay); 7849 } 7850 if (!isAlreadyActive || !isPlaying) { 7851 const hasWebMidiSupport = typeof navigator !== "undefined" && !!navigator.requestMIDIAccess; 7852 const midiOutputs = synth.getMidiOutputs() || []; 7853 const isWatchPage = !keepView || activeView === "watch"; 7854 const preferred = typeof window !== "undefined" ? localStorage.getItem("preferredAudioMode") : null; 7855 const best = getBestAudioMode(track, isSlowMobile(), hasWebMidiSupport, midiOutputs.length > 0, 0, isWatchPage, engineOrder, preferred); 7856 setAudioMode(best.mode); 7857 setIsLiveSynthFallback(best.isLiveSynthFallback); 7858 } 7859 if (!isAlreadyActive) { 7860 setPitchSemitones(0); 7861 } 7862 const searchParams = typeof window !== "undefined" ? window.location.search : ""; 7863 if (!keepView) { 7864 navigate(generateTrackUrl(track) + searchParams); 7865 } else if (activeView === "watch") { 7866 navigate(generateTrackUrl(track) + searchParams, { replace: true }); 7867 } 7868 if (!isAlreadyActive && typeof window !== "undefined") { 7869 const params = new URLSearchParams(window.location.search); 7870 const tVal = params.get("t") || params.get("time"); 7871 if (tVal) { 7872 const parsedSecs = parseTimeParam(tVal); 7873 if (parsedSecs !== null && parsedSecs > 0) { 7874 setTimeout(() => { 7875 seekTo(parsedSecs); 7876 }, 150); 7877 } 7878 } 7879 } 7880 if (currentUser) { 7881 api.recordHistory(track.id, currentUser.id); 7882 } 7883 } else {
7884 setActiveTrack(null); 7885 setIsPlaying(false); 7886 navigate("/"); 7887 } 7888 }, [autoplay, currentUser, setActiveTrack, setIsPlaying, setCurrentTime, setSeekRequest, setQueue, seekTo, setActivePlaylist, activeView, navigate]); 7889 const handleSelectTrackRef = reactExports.useRef(handleSelectTrack); 7890 reactExports.useEffect(() => { 7891 handleSelectTrackRef.current = handleSelectTrack; 7892 }, [handleSelectTrack]); 7893 reactExports.useEffect(() => { 7894 if (activeView === "watch" && activeViewId) { 7895 const isMismatch = !activeTrack || String(activeTrack.id) !== String(activeViewId); 7896 const isLight = !!(activeTrack && (activeTrack.isLight || activeTrack.pianoRollPath === void 0 || activeTrack.status === "completed" && activeTrack.pianoRollPath === null && activeTrack.scorePath === null)); 7897 if (isMismatch || isLight) { 7898 if (isMismatch) setIsLoading(true); 7899 api.getTrack(activeViewId).then((t2) => { 7900 if (t2) { 7901 handleSelectTrackRef.current(t2, isMismatch ? false : void 0, null, true); 7902 } else if (isMismatch) {
7903 setActiveView("home"); 7904 } 7905 }).catch(() => { 7906 if (isMismatch) setActiveView("home"); 7907 }).finally(() => { 7908 if (isMismatch) setIsLoading(false); 7909 }); 7910 } 7911 } 7912 }, [activeView, activeViewId, activeTrack]); 7913 reactExports.useEffect(() => { 7914 const handlePop = () => { 7915 setCurrentLocation({ 7916 pathname: window.location.pathname, 7917 search: window.location.search 7918 }); 7919 }; 7920 window.addEventListener("popstate", handlePop); 7921 return () => window.removeEventListener("popstate", handlePop); 7922 }, []); 7923 reactExports.useEffect(() => { 7924 if (typeof window !== "undefined") { 7925 const queryParams = new URLSearchParams(currentLocation.search); 7926 const urlKey = queryParams.get("key"); 7927 if (urlKey) { 7928 localStorage.setItem("digitizer_test_key", urlKey); 7929 } 7930 if (queryParams.get("login") === "true" || queryParams.get("auth") === "true") { 7931 setIsAuthModalOpen(true); 7932 } 7933 } 7934 }, [currentLocation.search]); 7935 reactExports.useEffect(() => { 7936 const path = currentLocation.pathname; 7937 const scoreMatch = path.match(/^\/score(b)?(\d+)-(\d+)(?:-(\d+))?\.html$/); 7938 if (scoreMatch) { 7939 const queryParams = new URLSearchParams(currentLocation.search); 7940 let changed = false; 7941 if (queryParams.get("mode") !== "score") { 7942 queryParams.set("mode", "score"); 7943 changed = true; 7944 } 7945 if (queryParams.get("track") !== scoreMatch[3]) { 7946 queryParams.set("track", scoreMatch[3]); 7947 changed = true; 7948 } 7949 if (scoreMatch[4] && queryParams.get("page") !== scoreMatch[4]) { 7950 queryParams.set("page", scoreMatch[4]); 7951 changed = true; 7952 } 7953 if (changed) { 7954 window.history.replaceState(null, "", `${path}?${queryParams.toString()}`); 7955 } 7956 } 7957 }, [currentLocation]); 7958 reactExports.useEffect(() => { 7959 const path = currentLocation.pathname; 7960 if (path.startsWith("/collection-section/")) { 7961 const id = path.split("/").pop() || ""; 7962 if (id.includes(":")) { 7963 const [slug, secIdWithSlug] = id.split(":"); 7964 navigate(`/collections/${slug}/${secIdWithSlug}`, { replace: true }); 7965 } 7966 } 7967 }, [currentLocation, navigate]); 7968 const handleNavigate = React.useCallback((v, id) => { 7969 const cleanedV = v.replace(/^\/+/, "").replace(/\?+/g, "?"); 7970 const [viewName, ...queryParts] = cleanedV.split("?"); 7971 const queryStr = queryParts.join("&"); 7972 let targetUrl = ""; 7973 if (viewName === "watch" && id) { 7974 targetUrl = `/watch?v=${id}`; 7975 } else if (viewName === "collection-section" && id && id.includes(":")) { 7976 const [slug, secIdWithSlug] = id.split(":"); 7977 targetUrl = `/collections/${slug}/${secIdWithSlug}`; 7978 } else { 7979 const actualView = viewName === "collection" ? "collections" : viewName; 7980 targetUrl = id ? `/${actualView}/${id}` : `/${actualView}`; 7981 } 7982 if (queryStr) { 7983 targetUrl += (targetUrl.includes("?") ? "&" : "?") + queryStr; 7984 } 7985 navigate(targetUrl); 7986 }, [navigate]); 7987 reactExports.useEffect(() => { 7988 const handleGlobalCaptureClick = (e) => { 7989 const target = e.target; 7990 const anchor = target.closest("a"); 7991 if (anchor && anchor.getAttribute("href")) { 7992 if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1) { 7993 e.stopPropagation(); 7994 } 7995 } 7996 }; 7997 const handleGlobalLinkClick = (e) => { 7998 const target = e.target; 7999 const anchor = target.closest("a"); 8000 if (!anchor) return; 8001 const href = anchor.getAttribute("href"); 8002 const targetAttr = anchor.getAttribute("target"); 8003 if (!href || targetAttr === "_blank") return; 8004 if (e.defaultPrevented) return; 8005 let isExternal = false; 8006 if (href.startsWith("http://") || href.startsWith("https://") || href.startsWith("//")) { 8007 try { 8008 const url = new URL(href, window.location.origin); 8009 if (url.origin !== window.location.origin) { 8010 isExternal = true; 8011 } 8012 } catch { 8013 isExternal = true; 8014 } 8015 } 8016 if (isExternal) return; 8017 if (e.metaKey || e.ctrlKey || e.shiftKey || e.button === 1 || e.button === 2) return; 8018 e.preventDefault(); 8019 let internalPath = href;
8020 try { 8021 const url = new URL(href, window.location.origin); 8022 internalPath = url.pathname + url.search + url.hash; 8023 } catch (err) { 8024 } 8025 navigate(internalPath); 8026 }; 8027 document.addEventListener("click", handleGlobalCaptureClick, { capture: true }); 8028 document.addEventListener("auxclick", handleGlobalCaptureClick, { capture: true }); 8029 document.addEventListener("click", handleGlobalLinkClick); 8030 return () => { 8031 document.removeEventListener("click", handleGlobalCaptureClick, { capture: true }); 8032 document.removeEventListener("auxclick", handleGlobalCaptureClick, { capture: true }); 8033 document.removeEventListener("click", handleGlobalLinkClick); 8034 }; 8035 }, [handleNavigate]); 8036 reactExports.useEffect(() => { 8037 if (currentUser) { 8038 api.getNotifications(currentUser.id).then(setNotifications); 8039 api.getSubscriptions(currentUser.id).then((subs) => setSubscriptions(new Set(subs))); 8040 api.getUserLikes(currentUser.id).then((ids) => setLikedMidis(new Set(ids))); 8041 api.getUserWatchLater(currentUser.id).then((ids) => setWatchLater(new Set(ids))); 8042 api.getUserHistory(currentUser.id).then(setHistory); 8043 } 8044 }, [currentUser == null ? void 0 : currentUser.id]); 8045 reactExports.useEffect(() => { 8046 if (!currentUser) return; 8047 const interval = setInterval(() => { 8048 api.getNotifications(currentUser.id).then(setNotifications).catch(() => { 8049 }); 8050 }, 6e4); 8051 return () => clearInterval(interval); 8052 }, [currentUser == null ? void 0 : currentUser.id]); 8053 reactExports.useEffect(() => { 8054 const fetchUnreadMessages = async () => { 8055 if (currentUser && (currentUser.role === "ADMIN" || currentUser.canMessage)) { 8056 try { 8057 const total = await api.getUnreadMessageCount(currentUser.id); 8058 setUnreadMessageCount(total); 8059 } catch { 8060 } 8061 } else { 8062 setUnreadMessageCount(0); 8063 } 8064 }; 8065 fetchUnreadMessages(); 8066 const interval = setInterval(fetchUnreadMessages, 6e4); 8067 const handleMessageUpdate = () => fetchUnreadMessages(); 8068 window.addEventListener("messages-update", handleMessageUpdate); 8069 return () => { 8070 clearInterval(interval); 8071 window.removeEventListener("messages-update", handleMessageUpdate); 8072 }; 8073 }, [currentUser == null ? void 0 : currentUser.id]); 8074 reactExports.useEffect(() => { 8075 const handleRequestUpload = (e) => { 8076 if (e.detail) { 8077 handleUploadClick(e.detail); 8078 } 8079 }; 8080 window.addEventListener("hamienet:request_upload", handleRequestUpload); 8081 return () => window.removeEventListener("hamienet:request_upload", handleRequestUpload); 8082 }, []); 8083 reactExports.useEffect(() => { 8084 if (activeView === "admin") return; 8085 const fetchContent = async () => { 8086 const isCategoryChange = activeView === "home" && !isLoading; 8087 if (isCategoryChange) { 8088 setIsTracksLoading(true); 8089 } else { 8090 setIsLoading(true); 8091 } 8092 try { 8093 const isExplore = activeView === "explore"; 8094 const isTrending = activeCategory === "trending"; 8095 const isRecommended = activeCategory === "recommended"; 8096 let tracksData; 8097 if (isRecommended) { 8098 tracksData = await api.getRecommendations(currentUser == null ? void 0 : currentUser.id); 8099 } else { 8100 const isHomeDefault = activeView === "home" && activeCategory === "all" && !submittedQuery; 8101 const isMine = activeView === "account"; 8102 tracksData = await api.getTracks( 8103 submittedQuery, 8104 activeCategory, 8105 isHomeDefault ? "random" : isTrending ? "popular" : "recent", 8106 isExplore ? 500 : 100, 8107 void 0, 8108 isMine, 8109 isMine ? currentUser == null ? void 0 : currentUser.username : void 0 8110 ); 8111 } 8112 if (submittedQuery) { 8113 setIsLyricsLoading(true); 8114 api.searchLyrics(submittedQuery, 1, 30).then((res) => setLyricsResults(res.results)).catch((err) => console.error("[Unified Lyrics Search Error]", err)).finally(() => setIsLyricsLoading(false)); 8115 } else { 8116 setLyricsResults([]); 8117 } 8118 const [modulesData, labConfigData] = await Promise.all([ 8119 api.getSitePages().catch(() => []),
8120 api.getLabConfig().catch(() => null) 8121 ]); 8122 setTracks(tracksData); 8123 setSiteModules(modulesData); 8124 setLabConfig(labConfigData); 8125 } catch (e) { 8126 console.error("Failed to load content", e); 8127 } finally { 8128 setIsLoading(false); 8129 setIsTracksLoading(false); 8130 } 8131 }; 8132 fetchContent(); 8133 }, [submittedQuery, activeCategory, activeView, currentUser == null ? void 0 : currentUser.id]); 8134 reactExports.useEffect(() => { 8135 const isNowLoading = isLoading || isTracksLoading; 8136 const wasLoading = lastLoadingRef.current; 8137 if (isNowLoading && !wasLoading) { 8138 loadStartRef.current = performance.now(); 8139 } else if (!isNowLoading && wasLoading) { 8140 const duration2 = performance.now() - loadStartRef.current; 8141 if (duration2 > 50) { 8142 uxTracker.logEvent("page_load", { 8143 url: window.location.pathname + window.location.search, 8144 duration: duration2, 8145 metadata: { 8146 view: activeView, 8147 category: activeCategory 8148 } 8149 }); 8150 } 8151 } 8152 lastLoadingRef.current = isNowLoading; 8153 }, [isLoading, isTracksLoading, activeView, activeCategory]); 8154 reactExports.useEffect(() => { 8155 if (theme === "dark") { 8156 document.documentElement.classList.add("dark"); 8157 } else { 8158 document.documentElement.classList.remove("dark"); 8159 } 8160 document.body.classList.remove("fun-mode"); 8161 document.body.classList.remove("retro-mode"); 8162 if (theme === "fun") { 8163 document.body.classList.add("fun-mode"); 8164 } else if (theme === "retro") { 8165 document.body.classList.add("retro-mode"); 8166 } 8167 }, [theme]); 8168 reactExports.useEffect(() => { 8169 if (!currentUser) return; 8170 if (activeView === "liked") { 8171 api.getLikedTracks(currentUser.id).then(setLikedTracks); 8172 } else if (activeView === "watchLater") { 8173 api.getWatchLaterTracks(currentUser.id).then(setWatchLaterTracks); 8174 } else if (activeView === "history") { 8175 api.getUserHistory(currentUser.id).then(setHistory); 8176 } else if (activeView === "subscriptions") { 8177 api.getSubscriptionTracks(currentUser.id).then(setSubscriptionTracks); 8178 } 8179 }, [activeView, currentUser]); 8180 const toggleTheme = (targetTheme) => { 8181 setTheme((prev) => { 8182 const next = targetTheme || (prev === "light" ? "dark" : prev === "dark" ? "fun" : prev === "fun" ? "retro" : "light"); 8183 localStorage.setItem("hamie_theme", next); 8184 return next; 8185 }); 8186 }; 8187 const handleUpdateUser = async (updates) => { 8188 if (!currentUser) return; 8189 try { 8190 const updatedUser = await api.updateUser(currentUser.id, updates); 8191 setCurrentUser(updatedUser); 8192 localStorage.setItem("hamie_session", JSON.stringify(updatedUser)); 8193 return updatedUser; 8194 } catch (e) { 8195 console.error("Failed to update user:", e); 8196 throw e; 8197 } 8198 }; 8199 const handleSearch = (e) => { 8200 if (e) e.preventDefault(); 8201 handleNavigate("search?q=" + encodeURIComponent(searchQuery)); 8202 setMelodyResults(null); 8203 window.scrollTo(0, 0); 8204 }; 8205 const handleMelodySearch = async (notes) => { 8206 setIsSearchingMelody(true); 8207 try { 8208 const results = await api.searchMelody(notes); 8209 setMelodyResults(results); 8210 setIsMelodySearchOpen(false); 8211 setActiveView("home"); 8212 } catch (e) { 8213 console.error(e); 8214 } finally { 8215 setIsSearchingMelody(false); 8216 } 8217 }; 8218 const toggleMute = (instr) => { 8219 setMutedChannels((prev) => prev.includes(instr) ? prev.filter((i) => i !== instr) : [...prev, instr]); 8220 }; 8221 const toggleLike = (id, e) => { 8222 if (e) e.stopPropagation(); 8223 if (!currentUser) { 8224 setIsAuthModalOpen(true); 8225 return; 8226 } 8227 setLikedMidis((prev) => { 8228 const next = new Set(prev); 8229 if (next.has(id)) next.delete(id); 8230 else next.add(id); 8231 return next; 8232 }); 8233 api.toggleLikeTrack(id, currentUser.id); 8234 }; 8235 const toggleWatchLater = (trackId, e) => { 8236 e.stopPropagation(); 8237 if (!currentUser) { 8238 setIsAuthModalOpen(true); 8239 return; 8240 } 8241 setWatchLater((prev) => { 8242 const next = new Set(prev); 8243 if (next.has(trackId)) next.delete(trackId); 8244 else next.add(trackId); 8245 return next; 8246 }); 8247 api.toggleWatchLaterTrack(trackId, currentUser.id); 8248 }; 8249 const toggleSubscription = async (artist, artistId) => { 8250 if (!currentUser) { 8251 setIsAuthModalOpen(true); 8252 return; 8253 } 8254 setSubscriptions((prev) => { 8255 const next = new Set(prev); 8256 if (next.has(artist)) next.delete(artist); 8257 else next.add(artist); 8258 return next; 8259 }); 8260 await api.toggleSubscription(currentUser.id, artistId); 8261 }; 8262 const handleAddToQueue = (track) => { 8263 setQueue((prev) => [...prev, track]); 8264 }; 8265 const handleRemoveFromQueue = (index) => { 8266 setQueue((prev) => prev.filter((_, i) => i !== index)); 8267 }; 8268 const handleNextTrack = (isManual) => { 8269 const nextPlayState = isManual ? true : isPlaying; 8270 if (queue.length > 0) { 8271 const next = queue[0]; 8272 setQueue((prev) => prev.slice(1)); 8273 handleSelectTrack(next, nextPlayState, void 0, true); 8274 return; 8275 } 8276 if (activePlaylist && activePlaylist.length > 0 && activeTrack) { 8277 const idx = activePlaylist.findIndex( 8278 (t2) => t2._playlistItemId && activeTrack._playlistItemId ? t2._playlistItemId === activeTrack._playlistItemId : String(t2.id) === String(activeTrack.id) 8279 ); 8280 if (idx !== -1) { 8281 const next = activePlaylist[(idx + 1) % activePlaylist.length]; 8282 handleSelectTrack(next, nextPlayState, activePlaylist, true); 8283 return; 8284 } 8285 } 8286 const currentFeedTracks = contextTracksRef.current.length > 0 ? contextTracksRef.current : tracks; 8287 if (!activeTrack || currentFeedTracks.length === 0) return; 8288 if (isManual) { 8289 const idx = currentFeedTracks.findIndex((t2) => String(t2.id) === String(activeTrack.id)); 8290 if (idx !== -1) { 8291 const next = currentFeedTracks[(idx + 1) % currentFeedTracks.length]; 8292 handleSelectTrack(next, true, void 0, true); 8293 } 8294 return; 8295 } 8296 if (autoplay) { 8297 if (autoPlayCountdown !== null) return; 8298 setAutoPlayCountdown(10); 8299 setIsPlaying(false); 8300 countdownIntervalRef.current = setInterval(() => { 8301 setAutoPlayCountdown((prev) => { 8302 if (prev !== null && prev <= 1) { 8303 clearInterval(countdownIntervalRef.current); 8304 const idx = currentFeedTracks.findIndex((t2) => String(t2.id) === String(activeTrack.id)); 8305 if (idx !== -1) { 8306 const next = currentFeedTracks[(idx + 1) % currentFeedTracks.length]; 8307 handleSelectTrack(next, true, void 0, true); 8308 } 8309 return null; 8310 } 8311 return prev !== null ? prev - 1 : null; 8312 }); 8313 }, 1e3); 8314 return; 8315 } 8316 setIsPlaying(false); 8317 }; 8318 const handleCancelAutoPlay = () => { 8319 if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); 8320 setAutoPlayCountdown(null); 8321 }; 8322 const handlePlayNextImmediate = () => { 8323 if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); 8324 if (activePlaylist && activePlaylist.length > 0 && activeTrack) { 8325 const idx2 = activePlaylist.findIndex( 8326 (t2) => t2._playlistItemId && activeTrack._playlistItemId ? t2._playlistItemId === activeTrack._playlistItemId : String(t2.id) === String(activeTrack.id) 8327 ); 8328 if (idx2 !== -1) { 8329 const next = activePlaylist[(idx2 + 1) % activePlaylist.length]; 8330 handleSelectTrack(next, true, activePlaylist, true); 8331 return; 8332 } 8333 } 8334 const currentFeedTracks = contextTracksRef.current.length > 0 ? contextTracksRef.current : tracks; 8335 const idx = currentFeedTracks.findIndex((t2) => String(t2.id) === String(activeTrack == null ? void 0 : activeTrack.id)); 8336 if (idx !== -1) { 8337 const next = currentFeedTracks[(idx + 1) % currentFeedTracks.length]; 8338 handleSelectTrack(next, true, void 0, true); 8339 } 8340 }; 8341 const handlePrevTrack = () => { 8342 const nextPlayState = isPlaying; 8343 if (activePlaylist && activePlaylist.length > 0 && activeTrack) { 8344 const idx2 = activePlaylist.findIndex( 8345 (t2) => t2._playlistItemId && activeTrack._playlistItemId ? t2._playlistItemId === activeTrack._playlistItemId : String(t2.id) === String(activeTrack.id) 8346 ); 8347 if (idx2 !== -1) { 8348 const prev = activePlaylist[(idx2 - 1 + activePlaylist.length) % activePlaylist.length]; 8349 handleSelectTrack(prev, nextPlayState, activePlaylist, true); 8350 return; 8351 } 8352 } 8353 const currentFeedTracks = contextTracksRef.current.length > 0 ? contextTracksRef.current : tracks; 8354 if (!activeTrack || currentFeedTracks.length === 0) return; 8355 const idx = currentFeedTracks.findIndex((t2) => String(t2.id) === String(activeTrack.id)); 8356 if (idx !== -1) { 8357 const prev = currentFeedTracks[(idx - 1 + currentFeedTracks.length) % currentFeedTracks.length]; 8358 handleSelectTrack(prev, nextPlayState, void 0, true); 8359 } 8360 }; 8361 const handleGoHome = () => { 8362 setSearchQuery(""); 8363 setMelodyResults(null); 8364 setSeekRequest(null); 8365 navigate("/"); 8366 }; 8367 const handleLogout = () => { 8368 localStorage.removeItem("hamie_session"); 8369 window.location.href = "/"; 8370 }; 8371 const handleChannelSelect = (artist) => { 8372 navigate(`/channel?c=${encodeURIComponent(artist)}`); 8373 }; 8374 const handleTagSelect = (tag) => { 8375 navigate(`/tag?name=${encodeURIComponent(tag)}`); 8376 }; 8377 const handleArtistSelect = (artist) => { 8378 navigate(`/?q=${encodeURIComponent(artist)}`); 8379 }; 8380 const handleCategorySelect = (category) => { 8381 navigate("/" + category); 8382 }; 8383 const handleMarkNotificationRead = async (id) => { 8384 await api.markNotificationRead(id); 8385 setNotifications((prev) => prev.map((n) => n.id === id ? { ...n, isRead: true } : n)); 8386 }; 8387 const handleMarkAllNotificationsRead = async () => { 8388 await api.markAllNotificationsRead(); 8389 setNotifications((prev) => prev.map((n) => ({ ...n, isRead: true }))); 8390 }; 8391 const handleNotificationNavigate = (link) => { 8392 try { 8393 const url = new URL(link, window.location.origin); 8394 const trackId = url.searchParams.get("id") || url.searchParams.get("trackId"); 8395 const search = url.search; 8396 const view = url.pathname.replace(/^\/+/, "") || "home"; 8397 if (trackId) { 8398 handleNavigate(view + search, trackId); 8399 } else { 8400 handleNavigate(view + search); 8401 } 8402 } catch { 8403 window.location.href = link; 8404 } 8405 }; 8406 const handleUploadClick = (fileOrMetadata, maybeMetadata) => { 8407 if (fileOrMetadata && fileOrMetadata instanceof File) { 8408 setUploadInitialFile(fileOrMetadata); 8409 setUploadInitialMetadata(maybeMetadata || null); 8410 } else if (fileOrMetadata && typeof fileOrMetadata === "object") { 8411 setUploadInitialFile(null); 8412 setUploadInitialMetadata(fileOrMetadata); 8413 } else { 8414 setUploadInitialFile(null); 8415 setUploadInitialMetadata(maybeMetadata || null); 8416 } 8417 setIsUploadModalOpen(true); 8418 }; 8419 const handleAddToPlaylistClick = (track) => { 8420 if (!currentUser) { 8421 setIsAuthModalOpen(true); 8422 } else { 8423 if (track) setPlaylistTrack(track); 8424 else if (activeTrack) setPlaylistTrack(activeTrack); 8425 setIsAddToPlaylistModalOpen(true); 8426 } 8427 }; 8428 if (activeView === "admin") { 8429 if (!isSessionLoaded) return /* @__PURE__ */ jsxRuntimeExports.jsx(LoadingFallback, {}); 8430 if (!currentUser || currentUser.role !== "ADMIN") { 8431 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-screen items-center justify-center bg-black text-white p-10 text-center animate-in fade-in", children: [
8432 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-20 h-20 bg-red-600/20 rounded-full flex items-center justify-center mb-6 animate-pulse", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 bg-red-600 rounded-full flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-2xl font-black", children: "!" }) }) }), 8433 /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-4xl font-black mb-4 tracking-tighter uppercase", children: "Clearance Required" }), 8434 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 mb-8 max-w-md leading-relaxed text-sm", children: "You have entered a restricted area. Access to the command center is limited to authenticated system administrators. Your attempt has been logged for audit." }), 8435 /* @__PURE__ */ jsxRuntimeExports.jsx( 8436 "button", 8437 { 8438 onClick: handleGoHome, 8439 className: "px-8 py-3 bg-white text-black rounded-full font-bold hover:bg-zinc-200 transition-all transform hover:scale-105", 8440 children: "Return to Safety" 8441 } 8442 ) 8443 ] }); 8444 } 8445 return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx(LoadingFallback, {}), children: /* @__PURE__ */ jsxRuntimeExports.jsx(AdminApp, { onExit: handleGoHome }) }); 8446 } 8447 const NotFoundView = ({ error, onBack }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center min-h-[60vh] text-center p-6 animate-in fade-in zoom-in-95", children: [ 8448 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-20 h-20 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center text-red-500 mb-6 shadow-lg", children: /* @__PURE__ */ jsxRuntimeExports.jsx(OctagonAlert, { size: 40 }) }), 8449 /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-3xl font-bold text-zinc-900 dark:text-white mb-3", children: "MIDI Not Found" }), 8450 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 dark:text-zinc-400 max-w-md mb-8", children: error || "We couldn't find the MIDI you were looking for. It may have been deleted, moved, or is temporarily unavailable." }), 8451 /* @__PURE__ */ jsxRuntimeExports.jsx( 8452 "button", 8453 { 8454 onClick: onBack, 8455 className: "px-8 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-bold shadow-lg shadow-indigo-500/30 transition-all flex items-center gap-2 active:scale-95", 8456 children: "Back to Home" 8457 } 8458 ) 8459 ] }); 8460 const AccessDeniedView = ({ title, message, onBack }) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center min-h-[60vh] text-center p-6 animate-in fade-in zoom-in-95", children: [ 8461 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-20 h-20 bg-amber-100 dark:bg-amber-900/30 rounded-full flex items-center justify-center text-amber-500 mb-6 shadow-lg", children: /* @__PURE__ */ jsxRuntimeExports.jsx(OctagonAlert, { size: 40 }) }), 8462 /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-3xl font-bold text-zinc-900 dark:text-white mb-3", children: title }), 8463 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 dark:text-zinc-400 max-w-md mb-8", children: message }), 8464 /* @__PURE__ */ jsxRuntimeExports.jsx( 8465 "button", 8466 { 8467 onClick: onBack, 8468 className: "px-8 py-3 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-bold shadow-lg shadow-indigo-500/30 transition-all flex items-center gap-2 active:scale-95", 8469 children: "Back to Home" 8470 } 8471 ) 8472 ] }); 8473 const isGoogleAGAgent = typeof navigator !== "undefined" && (navigator.userAgent.includes("Antigravity") || navigator.userAgent.includes("Google AG") || navigator.userAgent.includes("HeadlessChrome") || !!navigator.webdriver || localStorage.getItem("ag_agent") === "true" || new URLSearchParams(window.location.search).get("agent") === "true"); 8474 const isAdmin = (currentUser == null ? void 0 : currentUser.role) === "ADMIN" || isGoogleAGAgent; 8475 const COMPONENT_REGISTRY = { 8476 "home": /* @__PURE__ */ jsxRuntimeExports.jsx( 8477 HomePage, 8478 { 8479 currentUser, 8480 onNavigate: handleNavigate, 8481 activeCategory,
8482 setActiveCategory, 8483 tracks, 8484 onSelectTrack: handleSelectTrack, 8485 onToggleWatchLater: toggleWatchLater, 8486 watchLaterSet: watchLater, 8487 melodyResults, 8488 onClearMelodyResults: () => setMelodyResults(null), 8489 onAddToQueue: handleAddToQueue, 8490 onAddToPlaylist: handleAddToPlaylistClick, 8491 submittedQuery, 8492 isTracksLoading, 8493 lyricsResults, 8494 isLyricsLoading, 8495 activeView, 8496 setActiveView, 8497 onClearSearch: () => { 8498 setSearchQuery(""); 8499 navigate("/"); 8500 }, 8501 onChannelSelect: handleChannelSelect 8502 } 8503 ), 8504 "explore": /* @__PURE__ */ jsxRuntimeExports.jsx( 8505 ExplorePage, 8506 { 8507 tracks, 8508 onSelectTrack: handleSelectTrack, 8509 onToggleWatchLater: toggleWatchLater, 8510 watchLaterSet: watchLater, 8511 onCategorySelect: handleCategorySelect, 8512 onAddToQueue: handleAddToQueue, 8513 onAddToPlaylist: handleAddToPlaylistClick, 8514 onTagSelect: handleTagSelect 8515 } 8516 ), 8517 "history": /* @__PURE__ */ jsxRuntimeExports.jsx(HistoryPage, { history: history2, onSelectTrack: handleSelectTrack, onToggleWatchLater: toggleWatchLater, watchLaterSet: watchLater }), 8518 "melody-search": /* @__PURE__ */ jsxRuntimeExports.jsx( 8519 MelodySearchPage, 8520 { 8521 onSelectTrack: handleSelectTrack, 8522 onToggleWatchLater: toggleWatchLater, 8523 watchLaterSet: watchLater, 8524 onAddToQueue: handleAddToQueue, 8525 onAddToPlaylist: handleAddToPlaylistClick 8526 } 8527 ), 8528 "newly-uploaded": /* @__PURE__ */ jsxRuntimeExports.jsx( 8529 NewlyUploadedPage, 8530 { 8531 onSelectTrack: handleSelectTrack, 8532 onToggleWatchLater: toggleWatchLater, 8533 watchLaterSet: watchLater, 8534 onAddToQueue: handleAddToQueue, 8535 onAddToPlaylist: handleAddToPlaylistClick 8536 } 8537 ), 8538 "liked": /* @__PURE__ */ jsxRuntimeExports.jsx(LikedPage, { tracks: likedTracks, onSelectTrack: handleSelectTrack, onToggleWatchLater: toggleWatchLater, watchLaterSet: watchLater }), 8539 "watchLater": /* @__PURE__ */ jsxRuntimeExports.jsx(WatchLaterPage, { tracks: watchLaterTracks, onSelectTrack: handleSelectTrack, onToggleWatchLater: toggleWatchLater, watchLaterSet: watchLater }), 8540 "subscriptions": /* @__PURE__ */ jsxRuntimeExports.jsx(SubscriptionsPage, { tracks: subscriptionTracks, onSelectTrack: handleSelectTrack, onToggleWatchLater: toggleWatchLater, watchLaterSet: watchLater }), 8541 "channel": /* @__PURE__ */ jsxRuntimeExports.jsx( 8542 ChannelPage, 8543 { 8544 channelName: selectedChannel, 8545 onSelectTrack: handleSelectTrack, 8546 onToggleWatchLater: toggleWatchLater, 8547 watchLaterSet: watchLater, 8548 currentUser, 8549 onAuthRequested: () => setIsAuthModalOpen(true),
8550 onAddToQueue: handleAddToQueue, 8551 onAddToPlaylist: handleAddToPlaylistClick 8552 } 8553 ), 8554 "tag": /* @__PURE__ */ jsxRuntimeExports.jsx(TagPage, { tagName: selectedTag, tracks: tracks.filter((t2) => t2.tags.includes(selectedTag || "") || t2.artist === selectedTag || t2.title.includes(selectedTag || "")), onSelectTrack: handleSelectTrack, onToggleWatchLater: toggleWatchLater, watchLaterSet: watchLater }), 8555 "collections": /* @__PURE__ */ jsxRuntimeExports.jsx( 8556 CollectionIndexPage, 8557 { 8558 onNavigate: handleNavigate 8559 } 8560 ), 8561 "collection": /* @__PURE__ */ jsxRuntimeExports.jsx( 8562 CollectionPage, 8563 { 8564 slug: activeViewId, 8565 currentUser, 8566 onSelectTrack: handleSelectTrack, 8567 onToggleWatchLater: toggleWatchLater, 8568 watchLaterSet: watchLater, 8569 onAddToQueue: handleAddToQueue, 8570 onAddToPlaylist: handleAddToPlaylistClick, 8571 onNavigate: handleNavigate, 8572 onUploadRequested: handleUploadClick 8573 } 8574 ), 8575 "collection-section": /* @__PURE__ */ jsxRuntimeExports.jsx( 8576 CollectionSectionPage, 8577 { 8578 slug: activeViewId ? activeViewId.split(":")[0] : "", 8579 sectionId: activeViewId ? activeViewId.split(":")[1] : "", 8580 onSelectTrack: handleSelectTrack, 8581 onToggleWatchLater: toggleWatchLater, 8582 watchLaterSet: watchLater, 8583 onAddToQueue: handleAddToQueue, 8584 onAddToPlaylist: handleAddToPlaylistClick, 8585 onNavigate: handleNavigate, 8586 onUploadRequested: handleUploadClick, 8587 currentUser 8588 } 8589 ), 8590 "playlists": /* @__PURE__ */ jsxRuntimeExports.jsx(PlaylistsPage, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true) }), 8591 "help": /* @__PURE__ */ jsxRuntimeExports.jsx(HelpPage, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), onNavigate: handleNavigate, initialArticleId: activeViewId, category: activeCategory }), 8592 "terms": /* @__PURE__ */ jsxRuntimeExports.jsx(HelpPage, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), initialArticleId: "terms" }), 8593 "privacy": /* @__PURE__ */ jsxRuntimeExports.jsx(HelpPage, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), initialArticleId: "privacy" }), 8594 "messages": /* @__PURE__ */ jsxRuntimeExports.jsx(MessengerPage, { currentUser }), 8595 "account": /* @__PURE__ */ jsxRuntimeExports.jsx( 8596 AccountPage, 8597 { 8598 currentUser, 8599 onAuthRequested: () => setIsAuthModalOpen(true),
8600 onLogout: handleLogout, 8601 onUpdateUser: handleUpdateUser, 8602 tracks, 8603 onSelectTrack: handleSelectTrack, 8604 onToggleWatchLater: toggleWatchLater, 8605 watchLaterSet: watchLater, 8606 onUploadRequested: () => handleUploadClick(), 8607 onNavigate: handleNavigate 8608 } 8609 ), 8610 "converter": /* @__PURE__ */ jsxRuntimeExports.jsx(ConverterPage, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), onPublish: handleUploadClick }), 8611 "editor": /* @__PURE__ */ jsxRuntimeExports.jsx(EditorPage, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), id: activeViewId, onNavigate: handleNavigate }), 8612 "lyrics": /* @__PURE__ */ jsxRuntimeExports.jsx(LyricsPage, { id: activeViewId, onNavigate: handleNavigate }), 8613 "lyrics-search": /* @__PURE__ */ jsxRuntimeExports.jsx(LyricsSearchPage, { onNavigate: handleNavigate }), 8614 "search": /* @__PURE__ */ jsxRuntimeExports.jsx( 8615 AdvancedSearchPage, 8616 { 8617 searchQuery: submittedQuery, 8618 onSelectTrack: handleSelectTrack, 8619 onToggleWatchLater: toggleWatchLater, 8620 watchLaterSet: watchLater, 8621 onAddToQueue: handleAddToQueue, 8622 onAddToPlaylist: handleAddToPlaylistClick, 8623 theme 8624 } 8625 ), 8626 "algorithm-studio": /* @__PURE__ */ jsxRuntimeExports.jsx(AlgorithmStudio, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), onNavigate: handleNavigate }), 8627 "algorithm-editor": /* @__PURE__ */ jsxRuntimeExports.jsx(AlgorithmEditor, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), id: activeViewId, onNavigate: handleNavigate }), 8628 "algorithm-arena": /* @__PURE__ */ jsxRuntimeExports.jsx(StagingArena, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), id: activeViewId, onNavigate: handleNavigate }), 8629 "tribunal": /* @__PURE__ */ jsxRuntimeExports.jsx(TribunalPage, { currentUser, onNavigate: handleNavigate }), 8630 "lab": /* @__PURE__ */ jsxRuntimeExports.jsx(HamieLabPage, { currentUser, onNavigate: handleNavigate }), 8631 "visualizer": /* @__PURE__ */ jsxRuntimeExports.jsx(MidiVisualizerPage, { currentUser, onNavigate: handleNavigate, theme }), 8632 "keyboard-emulator": /* @__PURE__ */ jsxRuntimeExports.jsx(KeyboardEmulatorPage, { currentUser, onNavigate: handleNavigate, theme }), 8633 "sheet-digitizer": /* @__PURE__ */ jsxRuntimeExports.jsx(SheetMusicDigitizerPage, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), onNavigate: handleNavigate, initialProjectId: activeViewId }), 8634 "audio-digitizer": /* @__PURE__ */ jsxRuntimeExports.jsx(AudioDigitizerPage, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), onNavigate: handleNavigate }), 8635 "client-transcriber": /* @__PURE__ */ jsxRuntimeExports.jsx(ClientAudioDigitizerPage, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), onNavigate: handleNavigate }), 8636 "score-format-converter": /* @__PURE__ */ jsxRuntimeExports.jsx(ScoreConverterPage, { currentUser, onAuthRequested: () => setIsAuthModalOpen(true), onNavigate: handleNavigate }), 8637 "vocal-processor": /* @__PURE__ */ jsxRuntimeExports.jsx(VocalProcessorPage, { currentUser, onNavigate: handleNavigate }), 8638 "notation-editor": /* @__PURE__ */ jsxRuntimeExports.jsx( 8639 NotationEditor, 8640 { 8641 currentUser, 8642 initialProjectId: activeViewId, 8643 onNavigate: handleNavigate, 8644 theme 8645 } 8646 ), 8647 "billing": /* @__PURE__ */ jsxRuntimeExports.jsx(BillingPage, {}), 8648 "dmca": /* @__PURE__ */ jsxRuntimeExports.jsx(DMCAForm, {}), 8649 "verify-email": /* @__PURE__ */ jsxRuntimeExports.jsx(EmailVerification, { onAuthSuccess: (user) => setCurrentUser(user), onNavigate: handleNavigate }), 8650 "reset-password": /* @__PURE__ */ jsxRuntimeExports.jsx(ResetPassword, { onAuthSuccess: (user) => setCurrentUser(user), onAuthRequested: () => setIsAuthModalOpen(true), onNavigate: handleNavigate }), 8651 "soundfonts": /* @__PURE__ */ jsxRuntimeExports.jsx(SoundfontsPage, { onNavigate: handleNavigate, isAdmin }), 8652 "soundfont": /* @__PURE__ */ jsxRuntimeExports.jsx(SoundfontDetailPage, { isAdmin, slug: activeViewId, onNavigate: handleNavigate }), 8653 "soundfonts-upload": /* @__PURE__ */ jsxRuntimeExports.jsx(SoundfontUploadPage, { isAdmin, onNavigate: handleNavigate }),
8654 "personal-space": currentUser ? /* @__PURE__ */ jsxRuntimeExports.jsx( 8655 PersonalSpacePage, 8656 { 8657 currentUser, 8658 onNavigate: handleNavigate 8659 } 8660 ) : /* @__PURE__ */ jsxRuntimeExports.jsx( 8661 AccessDeniedView, 8662 { 8663 title: "Access Denied", 8664 message: "Please sign in or register to view your Personal Space.", 8665 onBack: handleGoHome 8666 } 8667 ), 8668 "not-found": /* @__PURE__ */ jsxRuntimeExports.jsx(NotFoundView, { error: notFoundError, onBack: () => { 8669 setNotFoundError(null); 8670 setActiveView("home"); 8671 } }) 8672 }; 8673 const renderContent = () => { 8674 var _a, _b; 8675 if (activeView === "watch" && activeTrack) { 8676 return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx(LoadingFallback, {}), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 8677 WatchPage, 8678 { 8679 theme, 8680 activeTrack, 8681 currentUser, 8682 isPlaying, 8683 setIsPlaying, 8684 toggleMute, 8685 mutedChannels, 8686 toggleLike, 8687 likedMidis, 8688 subscribe: () => toggleSubscription(activeTrack.uploader.name, activeTrack.uploader.id), 8689 isSubscribed: subscriptions.has(activeTrack.uploader.name), 8690 toggleSubscription, 8691 handleChannelSelect, 8692 handleSelectTrack, 8693 toggleWatchLater, 8694 watchLater, 8695 allTracks: tracks, 8696 onTagSelect: handleTagSelect, 8697 onArtistSelect: handleArtistSelect, 8698 currentTime, 8699 duration, 8700 onAddToQueue: handleAddToQueue, 8701 onAddToPlaylist: handleAddToPlaylistClick, 8702 onAuthRequested: () => setIsAuthModalOpen(true),
8703 audioMode, 8704 setAudioMode, 8705 isBuffering, 8706 setIsBuffering, 8707 autoPlayCountdown, 8708 onCancelAutoPlay: handleCancelAutoPlay, 8709 onPlayNext: handlePlayNextImmediate, 8710 nextTrack: queue.length > 0 ? queue[0] : activeTrack ? activePlaylist && activePlaylist.length > 0 && activePlaylist.some((p) => String(p.id) === String(activeTrack.id)) ? activePlaylist[(activePlaylist.findIndex((p) => String(p.id) === String(activeTrack.id)) + 1) % activePlaylist.length] : (contextTracksRef.current.length > 0 ? contextTracksRef.current : tracks)[((contextTracksRef.current.length > 0 ? contextTracksRef.current : tracks).findIndex((t2) => String(t2.id) === String(activeTrack.id)) + 1) % (contextTracksRef.current.length > 0 ? contextTracksRef.current : tracks).length] : void 0, 8711 onSeek: (time) => setSeekRequest({ time, timestamp: Date.now() }), 8712 playbackRate, 8713 isGameMode, 8714 setIsGameMode, 8715 audioQuality, 8716 pitchSemitones, 8717 setPitchSemitones, 8718 onUserUpdate: handleUpdateUser 8719 } 8720 ) }); 8721 } 8722 if (isLoading) { 8723 return /* @__PURE__ */ jsxRuntimeExports.jsx(LoadingFallback, {}); 8724 } 8725 if (isSearchingMelody) { 8726 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "py-20 text-center", children: [
8727 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin mx-auto mb-4" }), 8728 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500", children: "Analyzing melody contours..." }) 8729 ] }); 8730 } 8731 let isViewAllowed = true; 8732 let disabledModuleName = ""; 8733 const urlKey = new URLSearchParams(currentLocation.search).get("key"); 8734 const storedKey = localStorage.getItem("digitizer_test_key") || (typeof window !== "undefined" ? sessionStorage.getItem("digitizer_test_key") : null); 8735 const envTestKey = ((_a = window.HAMIE_ENV) == null ? void 0 : _a.DIGITIZER_TEST_KEY) || "digitizer-test-2026-key"; 8736 const adminKey = "2164dc28-6243-4840-903f-3d3c936f706c"; 8737 const hasValidTestKey = urlKey && (urlKey === envTestKey || urlKey === adminKey) || storedKey && (storedKey === envTestKey || storedKey === adminKey); 8738 if (!isAdmin) { 8739 let toolIdToCheck = null; 8740 if (activeView === "sheet-digitizer") { 8741 if (!hasValidTestKey) { 8742 toolIdToCheck = "sheet-digitizer"; 8743 } 8744 } else if (activeView === "audio-digitizer") toolIdToCheck = "audio-digitizer"; 8745 else if (activeView === "client-transcriber") toolIdToCheck = "client-audio-digitizer"; 8746 else if (activeView === "algorithm-studio" || activeView === "algorithm-editor" || activeView === "algorithm-arena") toolIdToCheck = "algorithm-studio"; 8747 else if (activeView === "tribunal") toolIdToCheck = "tribunal"; 8748 else if (activeView === "client-transcriber") toolIdToCheck = "client-transcriber"; 8749 else if (activeView === "score-format-converter") toolIdToCheck = "score-format-converter"; 8750 else if (activeView === "vocal-processor") toolIdToCheck = "vocal-processor"; 8751 else if (activeView === "converter") toolIdToCheck = "converter"; 8752 else if (activeView === "score-format-converter") toolIdToCheck = "score-format-converter"; 8753 else if (activeView === "melody-search") toolIdToCheck = "melody-search"; 8754 else if (activeView === "visualizer") toolIdToCheck = "midi-visualizer"; 8755 else if (activeView === "notation-editor") { 8756 if (!hasValidTestKey) { 8757 toolIdToCheck = "score-editor"; 8758 } 8759 } else if (activeView === "editor") { 8760 const params = new URLSearchParams(window.location.search); 8761 const mode = params.get("mode"); 8762 if (mode === "score") { 8763 if (!hasValidTestKey) { 8764 toolIdToCheck = "score-editor"; 8765 } 8766 } else { 8767 toolIdToCheck = "midi-editor"; 8768 } 8769 } 8770 if (toolIdToCheck) { 8771 const toolsList = (labConfig == null ? void 0 : labConfig.tools) || []; 8772 const dbTool = toolsList.find((t2) => t2.id === toolIdToCheck); 8773 const defTool = (_b = DEFAULT_TOOLS) == null ? void 0 : _b.find((t2) => t2.id === toolIdToCheck); 8774 const isEnabled = dbTool ? dbTool.enabled : defTool ? defTool.enabled : true; 8775 if (!isEnabled) { 8776 isViewAllowed = false; 8777 disabledModuleName = defTool ? defTool.name : activeView; 8778 } 8779 } 8780 } 8781 const dynamicModule = siteModules.find((m) => m.id === activeView); 8782 if (dynamicModule && !isAdmin && (!hasValidTestKey || activeView !== "notation-editor") && dynamicModule.status !== "active") { 8783 isViewAllowed = false; 8784 disabledModuleName = dynamicModule.name; 8785 } 8786 if (!isViewAllowed) { 8787 return /* @__PURE__ */ jsxRuntimeExports.jsx( 8788 AccessDeniedView, 8789 { 8790 title: "Module Disabled", 8791 message: `The "${disabledModuleName}" module has been disabled by the administrator. Direct access to this module is restricted.`, 8792 onBack: () => setActiveView("home") 8793 } 8794 ); 8795 } 8796 if (COMPONENT_REGISTRY[activeView]) { 8797 return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx(LoadingFallback, {}), children: COMPONENT_REGISTRY[activeView] }); 8798 } 8799 if (dynamicModule) { 8800 return /* @__PURE__ */ jsxRuntimeExports.jsx(DynamicPageRenderer, { module: dynamicModule }); 8801 } 8802 return /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx(LoadingFallback, {}), children: COMPONENT_REGISTRY["home"] }); 8803 };
8804 return /* @__PURE__ */ jsxRuntimeExports.jsx(AdminProvider, { currentUser, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-screen bg-white dark:bg-[#0f0f0f] text-zinc-900 dark:text-white flex flex-col font-sans selection:bg-indigo-500/30 transition-colors duration-200 overflow-hidden relative", children: [ 8805 /* @__PURE__ */ jsxRuntimeExports.jsx( 8806 AuthModal, 8807 { 8808 isOpen: isAuthModalOpen, 8809 onClose: () => setIsAuthModalOpen(false), 8810 onAuthSuccess: (user) => setCurrentUser(user) 8811 } 8812 ), 8813 /* @__PURE__ */ jsxRuntimeExports.jsx( 8814 UploadModal, 8815 { 8816 isOpen: isUploadModalOpen, 8817 onClose: () => { 8818 setIsUploadModalOpen(false); 8819 setUploadInitialFile(null); 8820 setUploadInitialMetadata(null); 8821 }, 8822 currentUser, 8823 initialFile: uploadInitialFile, 8824 initialMetadata: uploadInitialMetadata 8825 } 8826 ), 8827 isMelodySearchOpen && /* @__PURE__ */ jsxRuntimeExports.jsx( 8828 MelodyInput, 8829 { 8830 onSearch: handleMelodySearch, 8831 onClose: () => setIsMelodySearchOpen(false) 8832 } 8833 ),
8834 currentUser && playlistTrack && /* @__PURE__ */ jsxRuntimeExports.jsx( 8835 AddToPlaylistModal, 8836 { 8837 isOpen: isAddToPlaylistModalOpen, 8838 onClose: () => { 8839 setIsAddToPlaylistModalOpen(false); 8840 setPlaylistTrack(null); 8841 }, 8842 track: playlistTrack, 8843 currentUser 8844 } 8845 ), 8846 currentUser && /* @__PURE__ */ jsxRuntimeExports.jsx( 8847 NotificationsModal, 8848 { 8849 isOpen: isNotificationsModalOpen, 8850 onClose: () => setIsNotificationsModalOpen(false), 8851 previewNotifications: notifications, 8852 onMarkAsRead: handleMarkNotificationRead, 8853 onMarkAllRead: handleMarkAllNotificationsRead, 8854 onNavigate: handleNotificationNavigate 8855 } 8856 ), 8857 !(isFullscreen && activeView === "notation-editor") && /* @__PURE__ */ jsxRuntimeExports.jsx(StaffAdminBar, {}), 8858 !(isFullscreen && activeView === "notation-editor") && /* @__PURE__ */ jsxRuntimeExports.jsx( 8859 Header, 8860 { 8861 isSidebarOpen, 8862 setIsSidebarOpen, 8863 handleGoHome, 8864 searchQuery, 8865 setSearchQuery, 8866 handleSearch, 8867 currentUser, 8868 setIsAuthModalOpen, 8869 onAccountClick: () => {
8870 setActiveView("account"); 8871 }, 8872 onPersonalSpaceClick: () => { 8873 setActiveView("personal-space"); 8874 }, 8875 onLogout: handleLogout, 8876 notifications, 8877 onMarkNotificationRead: handleMarkNotificationRead, 8878 onMarkAllNotificationsRead: handleMarkAllNotificationsRead, 8879 onSeeAllNotifications: () => setIsNotificationsModalOpen(true), 8880 toggleTheme, 8881 theme, 8882 onUploadClick: handleUploadClick, 8883 onMelodySearchClick: () => setActiveView("melody-search"), 8884 onNavigateNotification: handleNotificationNavigate, 8885 onNavigate: handleNavigate 8886 } 8887 ), 8888 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-1 overflow-hidden relative", children: [ 8889 !(isFullscreen && activeView === "notation-editor") && /* @__PURE__ */ jsxRuntimeExports.jsx( 8890 Sidebar, 8891 { 8892 isSidebarOpen, 8893 setIsSidebarOpen, 8894 activeView, 8895 setActiveView, 8896 activeTrack, 8897 setActiveTrack: handleSelectTrack, 8898 currentUser, 8899 setIsAuthModalOpen, 8900 subscriptions, 8901 handleChannelSelect, 8902 handleGoHome, 8903 onAdminClick: () => { 8904 var _a; 8905 const adminUrl = (_a = window.HAMIE_ENV) == null ? void 0 : _a.ADMIN_URL; 8906 if (adminUrl) { 8907 window.open(adminUrl, "_blank"); 8908 } else {
8909 setActiveView("admin"); 8910 } 8911 }, 8912 siteModules, 8913 unreadMessageCount, 8914 theme, 8915 toggleTheme, 8916 isMediaPage: [ 8917 "watch", 8918 "editor", 8919 "lab", 8920 "sheet-digitizer", 8921 "audio-digitizer", 8922 "client-transcriber", 8923 "algorithm-studio", 8924 "algorithm-editor", 8925 "algorithm-arena", 8926 "tribunal", 8927 "converter", 8928 "melody-search", 8929 "visualizer", 8930 "notation-editor", 8931 "keyboard-emulator" 8932 ].includes(activeView) 8933 } 8934 ), 8935 /* @__PURE__ */ jsxRuntimeExports.jsxs("main", { id: "main-scroll-container", className: `flex-1 overflow-y-auto bg-white dark:bg-[#0f0f0f] custom-scrollbar transition-colors duration-200 ${activeTrack ? "pb-28" : ""} flex flex-col`, children: [ 8936 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `flex-1 ${!(isFullscreen && activeView === "notation-editor") ? "pt-16" : ""}`, children: renderContent() }), 8937 !(isFullscreen && activeView === "notation-editor") && /* @__P
8937URE__ */ jsxRuntimeExports.jsx(Footer, {}) 8938 ] }), 8939 /* @__PURE__ */ jsxRuntimeExports.jsx(BackToTop, {}) 8940 ] }), 8941 activeTrack && /* @__PURE__ */ jsxRuntimeExports.jsx( 8942 PersistentPlayer, 8943 { 8944 track: activeTrack, 8945 isPlaying, 8946 setIsPlaying, 8947 onTimeUpdate: (curr, dur) => { 8948 setCurrentTime(curr); 8949 setDuration(dur); 8950 }, 8951 onPlaybackRateChange: setPlaybackRate, 8952 onNext: handleNextTrack, 8953 onPrev: handlePrevTrack, 8954 toggleLike, 8955 isLiked: likedMidis.has(activeTrack.id), 8956 onTrackClick: activeView === "watch" && String(activeViewId) === String(activeTrack.id) ? void 0 : () => navigate(generateTrackUrl(activeTrack)), 8957 onAddToPlaylist: () => handleAddToPlaylistClick(activeTrack), 8958 queue, 8959 activePlaylist, 8960 playlistLabel, 8961 onClearPlaylist: () => { 8962 setActivePlaylist(null); 8963 setPlaylistLabel(null); 8964 }, 8965 onRemoveFromQueue: handleRemoveFromQueue, 8966 onPlayQueueTrack: (track) => { 8967 const idx = queue.findIndex((t2) => t2.id === track.id); 8968 if (idx !== -1) { 8969 const newQueue = [...queue]; 8970 newQueue.splice(idx, 1); 8971 setQueue(newQueue); 8972 } 8973 handleSelectTrack(track); 8974 }, 8975 activeView, 8976 audioMode, 8977 setAudioMode, 8978 isBuffering, 8979 seekTo: seekRequest, 8980 isGameMode, 8981 currentUser, 8982 audioQuality, 8983 pitchSemitones, 8984 setPitchSemitones 8985 } 8986 ) 8987 ] }) }); 8988}; 8989const App = () => /* @__PURE__ */ jsxRuntimeExports.jsx(SettingsProvider, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(PlayerProvider, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(ErrorBoundary, { children: /* @__PURE__ */ jsxRuntimeExports.jsx(AppContent, {}) }) }) }); 8990if (typeof StyleSheetList !== "undefined" && !StyleSheetList.prototype[Symbol.iterator]) { 8991 StyleSheetList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; 8992} 8993if (typeof window !== "undefined") { 8994 const urlParams = new URLSearchParams(window.location.search); 8995 const keyParam = urlParams.get("key"); 8996 if (keyParam === "digitizer-test-2026-key") { 8997 sessionStorage.setItem("digitizer_test_key", keyParam); 8998 } 8999 const originalFetch2 = window.fetch; 9000 window.fetch = function(input, init) { 9001 const testKey = sessionStorage.getItem("digitizer_test_key"); 9002 if (testKey) { 9003 init = init || {}; 9004 let headers = {}; 9005 if (init.headers) { 9006 if (init.headers instanceof Headers) { 9007 init.headers.forEach((value, key) => { 9008 headers[key] = value; 9009 }); 9010 } else if (Array.isArray(init.headers)) { 9011 init.headers.forEach(([key, value]) => { 9012 headers[key] = value; 9013 }); 9014 } else { 9015 headers = { ...init.headers }; 9016 } 9017 } 9018 headers["x-digitizer-test-key"] = testKey; 9019 init.headers = headers; 9020 } 9021 return originalFetch2.call(this, input, init); 9022 }; 9023} 9024const originalLog = console.log; 9025const originalInfo = console.info; 9026const originalDebug = console.debug; 9027const originalWarn = console.warn; 9028const originalError = console.error; 9029const isAdminUser = () => { 9030 try { 9031 const saved = localStorage.getItem("hamie_session"); 9032 if (saved) { 9033 const user = JSON.parse(saved); 9034 return (user == null ? void 0 : user.role) === "ADMIN"; 9035 } 9036 } catch { 9037 } 9038 return false; 9039}; 9040const isDebugMessage = (args) => { 9041 if (args.length === 0) return false; 9042 const firstArg = args[0]; 9043 if (typeof firstArg === "string") { 9044 const cleaned = firstArg.trim(); 9045 return cleaned.startsWith("[") || cleaned.startsWith("%c[") || cleaned.includes("[PersistentPlayer]") || cleaned.includes("[parseMidiInstruments]") || cleaned.includes("[HamieSynth]"); 9046 } 9047 return false; 9048}; 9049console.log = (...args) => { 9050 if (isAdminUser() || !isDebugMessage(args)) { 9051 originalLog.apply(console, args); 9052 } 9053}; 9054console.info = (...args) => { 9055 if (isAdminUser() || !isDebugMessage(args)) { 9056 originalInfo.apply(console, args); 9057 } 9058}; 9059console.debug = (...args) => {
9060 if (isAdminUser() || !isDebugMessage(args)) { 9061 originalDebug.apply(console, args); 9062 } 9063}; 9064console.warn = (...args) => { 9065 if (isAdminUser() || !isDebugMessage(args)) { 9066 originalWarn.apply(console, args); 9067 } 9068}; 9069console.error = (...args) => { 9070 if (isAdminUser() || !isDebugMessage(args)) { 9071 originalError.apply(console, args); 9072 } 9073}; 9074uxTracker.init(); 9075const logError = (message, source, lineno, colno, stack, url) => { 9076 console.error("Global Error Caught:", message); 9077 fetch("/api/log-client-error", { 9078 method: "POST", 9079 headers: { "Content-Type": "application/json" }, 9080 body: JSON.stringify({ 9081 message: message.toString(), 9082 source, 9083 lineno, 9084 colno, 9085 stack, 9086 url 9087 }) 9088 }).catch((e) => console.error("Failed to send error log:", e)); 9089 try { 9090 const msgStr = String(message).toLowerCase(); 9091 const isChunkError = msgStr.includes("chunk") || msgStr.includes("loading chunk") || msgStr.includes("failed to fetch dynamically imported module") || msgStr.includes("dynamic import"); 9092 if (isChunkError) { 9093 const hasAutoReloaded = sessionStorage.getItem("has_auto_reloaded_from_error"); 9094 if (!hasAutoReloaded) { 9095 sessionStorage.setItem("has_auto_reloaded_from_error", "true"); 9096 window.location.reload(); 9097 } 9098 } 9099 } catch (e) { 9100 console.error("Failed to handle auto-reload on global error:", e); 9101 } 9102}; 9103window.onerror = (message, source, lineno, colno, error) => { 9104 logError(message, source, lineno, colno, error == null ? void 0 : error.stack, window.location.href); 9105}; 9106window.onunhandledrejection = (event) => { 9107 var _a, _b; 9108 logError( 9109 ((_a = event.reason) == null ? void 0 : _a.message) || "Unhandled Rejection", 9110 "", 9111 0, 9112 0, 9113 ((_b = event.reason) == null ? void 0 : _b.stack) || JSON.stringify(event.reason), 9114 window.location.href 9115 ); 9116}; 9117const container = document.getElementById("root"); 9118if (container) { 9119 const root = createRoot(container); 9120 root.render( 9121 /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex h-screen items-center justify-center bg-[#0f0f0f] text-white", children: "Loading..." }), children: /* @__PURE__ */ jsxRuntimeExports.jsx(App, {}) }) 9122 ); 9123} else { 9124 console.error("DEBUG FATAL: Root container not found in DOM"); 9125} 9126export { 9127 VideoCard as V, 9128 useAdmin as a, 9129 usePlayer as u 9130};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.