1const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/SpectrumBarsVisualizer-D1eLSVtr.js","assets/vendor-react-Bay9Y7Lb.js","assets/SheetMusicViewer-CoFH8B_z.js","assets/vendor-react-three-MDLA5jog.js","assets/vendor-dVNaLJx_.js","assets/vendor-i18n-R-bMoYp_.js","assets/vendor-DqKRdbjz.css","assets/SharedKeyboard-B3r3Jaho.js","assets/useMidiKeyboard-DlyEIN8T.js","assets/wasm-synth-adapter-BrqgBX1y.js","assets/vendor-spessasynth-DH8n6o_V.js","assets/ParametricEQ-wkxmF0lG.js","assets/synth-BE1TSTD4.js","assets/vendor-audio-DH3w0BzW.js","assets/OrchestraVisualizer-Cwwo0xaQ.js","assets/StagePuppetEngine-BtPqoUG7.js","assets/IkMath-D6fi1ppd.js","assets/vendor-gsap-50Lx19ak.js","assets/vendor-framer-C8eEgkdL.js","assets/InstrumentMappings-UYUlFRMy.js","assets/CinematicVisualizer-BM6Yo-FE.js","assets/Musician3D-D56sI7cs.js","assets/SouthparkOrchestraVisualizer-BGIvyqzT.js","assets/SouthparkCharacter-BNwA9Wd0.js","assets/SouthparkVariants-DgWdjJ-T.js","assets/SouthparkStageBanner-BCxT9awg.js","assets/utils-BYCVoSTN.js","assets/index-D0Bmh-UX.js","assets/index-WEfYDNmt.css","assets/Taiko-BWRRYy3Y.js","assets/GlobeFlag-V1uaxVEK.js"])))=>i.map(i=>d[i]); 2import { r as reactExports, j as jsxRuntimeExports, R as React, a as reactDomExports } from "./vendor-react-Bay9Y7Lb.js"; 3import { a as api, u as useSettings } from "./index-D0Bmh-UX.js"; 4import { u as useTranslation } from "./vendor-i18n-R-bMoYp_.js"; 5import { cG as Tag, aW as X, bf as Search, ct as TrendingUp, cH as Disc, bg as Music, b1 as CircleAlert, cI as Flag, aY as TriangleAlert, cq as Scale, cJ as ShieldAlert, a_ as CircleCheck, b3 as FileHeadphone, b2 as Upload, cw as List, cK as Navigation, cL as RefreshCw, cM as Cable, cN as Gamepad2, cF as Keyboard, cO as GraduationCap, bS as Mic, cP as MicOff, cC as Drum, c7 as Timer, cQ as Minus, ce as Plus, cR as SquareSquare, cS as Route, cT as Type, cf as Activity, bs as Zap, bi as Sparkles, cs as BookOpen, cz as SlidersHorizontal, cU as RectangleHorizontal, cV as Minimize, cW as Maximize, b6 as LoaderCircle, cp as Settings2, cX as MicVocal, bU as Play, cY as ChevronLeft, bE as ChevronRight, cj as Layers, cZ as Minimize2, bQ as ExternalLink, cB as ChevronDown$1, bJ as PanelsTopLeft, c_ as Maximize2, c3 as RotateCcw, b7 as Check, cA as ChevronUp, bW as VolumeX, bT as Volume2, cD as Headphones, bz as EyeOff, bA as Eye, c$ as Download, a$ as Shield, d0 as UserCheck, d1 as UserMinus, d2 as Ghost, d3 as ChartColumn, d4 as ShieldCheck, ba as Info, aZ as Mail, c1 as Copy, cl as OctagonAlert, ch as Database, bK as ThumbsUp, d5 as ThumbsDown, c0 as Share2, cd as Ellipsis, d6 as SquarePen, bY as FolderPlus, d7 as Image, d8 as Trash, bL as Clock, d9 as Music2, bm as ArrowUp, da as ArrowDown, bH as Hash, b_ as EllipsisVertical, db as ListVideo } from "./vendor-dVNaLJx_.js"; 6import { A as AnimatePresence, m as motion } from "./vendor-framer-C8eEgkdL.js"; 7import { A as AdSlot } from "./AdSlot-CPW4Sb2o.js"; 8import { T as TagAutocomplete } from "./Waveform-MxtdejK7.js"; 9import { b as getInstrumentName, e as isSlowMobile, G as GM_PERCUSSION_MAP, j as getTrackColor, i as isPro, I as INSTRUMENT_CATEGORIES, k as GM_INSTRUMENTS, t as timeAgo, d as isContentBlocked, f as getBestAudioMode } from "./utils-BYCVoSTN.js"; 10import { s as synth, p as parseMidiInstruments } from "./synth-BE1TSTD4.js"; 11import { V as VideoCard, a as useAdmin, u as usePlayer } from "./main-CznOH3wZ.js"; 12import { _ as __vitePreload } from "./vendor-react-three-MDLA5jog.js"; 13import { u as useSoundfontKeyboard, g as getKeyGeometry, a as useKeyboardInput, b as getMidiFromKeyboardClick, C as CONFIG, c as getPcKeyLabel, r as renderSharedKeyboard } from "./SharedKeyboard-B3r3Jaho.js"; 14import { G as GlobeFlag } from "./GlobeFlag-V1uaxVEK.js"; 15import { U as UserAvatar } from "./UserAvatar-BQN4jlY-.js"; 16import "./wasm-synth-adapter-BrqgBX1y.js"; 17import "./vendor-spessasynth-DH8n6o_V.js"; 18import "./vendor-audio-DH3w0BzW.js"; 19import "./labTools-BATZiaPB.js"; 20import "./useMidiKeyboard-DlyEIN8T.js"; 21const TagModal = ({ isOpen, onClose, onSubmit }) => { 22 const { t, i18n } = useTranslation(); 23 const [tagName, setTagName] = reactExports.useState(""); 24 const [suggestions, setSuggestions] = reactExports.useState([]); 25 const [showSuggestions, setShowSuggestions] = reactExports.useState(false); 26 const [activeIndex, setActiveIndex] = reactExports.useState(-1); 27 const [isSubmitting, setIsSubmitting] = reactExports.useState(false); 28 const [isLoadingSuggestions, setIsLoadingSuggestions] = reactExports.useState(false); 29 const [error, setError] = reactExports.useState(null); 30 const containerRef = reactExports.useRef(null); 31 reactExports.useEffect(() => { 32 const trimmed = tagName.trim(); 33 if (trimmed.length < 2) { 34 setSuggestions([]); 35 setShowSuggestions(false); 36 return; 37 } 38 setIsLoadingSuggestions(true); 39 const timer = setTimeout(async () => {
40 try { 41 const results = await api.getSuggestedTags(trimmed); 42 setSuggestions(results); 43 setShowSuggestions(results.length > 0); 44 setActiveIndex(-1); 45 } catch (err) { 46 console.error("Error fetching suggestions:", err); 47 } finally { 48 setIsLoadingSuggestions(false); 49 } 50 }, 200); 51 return () => clearTimeout(timer); 52 }, [tagName]); 53 reactExports.useEffect(() => { 54 const handleClickOutside = (e) => { 55 if (containerRef.current && !containerRef.current.contains(e.target)) { 56 setShowSuggestions(false); 57 } 58 }; 59 document.addEventListener("mousedown", handleClickOutside); 60 return () => document.removeEventListener("mousedown", handleClickOutside); 61 }, []); 62 if (!isOpen) return null; 63 const handleSelectSuggestion = async (name) => { 64 setTagName(""); 65 setShowSuggestions(false); 66 setIsSubmitting(true); 67 setError(null); 68 try { 69 await onSubmit(name); 70 onClose(); 71 } catch (err) { 72 setError(err.message || "Failed to add tag"); 73 } finally { 74 setIsSubmitting(false); 75 } 76 }; 77 const handleSubmit = async (e) => { 78 e.preventDefault(); 79 if (!tagName.trim()) return; 80 if (showSuggestions && activeIndex >= 0 && suggestions[activeIndex]) { 81 handleSelectSuggestion(suggestions[activeIndex].name); 82 return; 83 } 84 setIsSubmitting(true); 85 setError(null); 86 try { 87 await onSubmit(tagName.trim()); 88 setTagName(""); 89 onClose(); 90 } catch (err) { 91 setError(err.message || "Failed to add tag"); 92 } finally { 93 setIsSubmitting(false); 94 } 95 }; 96 const handleKeyDown = (e) => { 97 if (e.key === "ArrowDown") { 98 e.preventDefault(); 99 if (showSuggestions && suggestions.length > 0) { 100 setActiveIndex((prev) => Math.min(prev + 1, suggestions.length - 1)); 101 } 102 } else if (e.key === "ArrowUp") { 103 e.preventDefault(); 104 if (showSuggestions && suggestions.length > 0) { 105 setActiveIndex((prev) => Math.max(prev - 1, -1)); 106 } 107 } else if (e.key === "Escape") { 108 if (showSuggestions) { 109 e.preventDefault(); 110 setShowSuggestions(false); 111 } else { 112 onClose(); 113 } 114 } 115 }; 116 const getLocalizedName = (s) => { 117 const lang = i18n.language; 118 if (s.translations && s.translations[lang]) return s.translations[lang]; 119 return s.name; 120 }; 121 const getTypeIcon = (type) => { 122 switch (type) { 123 case "COMPOSER": 124 return /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { className: "w-3.5 h-3.5" }); 125 case "SHOW": 126 return /* @__PURE__ */ jsxRuntimeExports.jsx(Disc, { className: "w-3.5 h-3.5" }); 127 case "INSTRUMENT": 128 return /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { className: "w-3.5 h-3.5" }); 129 default: 130 return /* @__PURE__ */ jsxRuntimeExports.jsx(Tag, { className: "w-3.5 h-3.5" }); 131 } 132 }; 133 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl w-full max-w-md overflow-hidden border border-zinc-200/50 dark:border-zinc-800/50 animate-in zoom-in-95 duration-200", children: [ 134 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-4 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50", children: [
135 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 136 /* @__PURE__ */ jsxRuntimeExports.jsx(Tag, { size: 18, className: "text-indigo-500 animate-pulse" }), 137 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Add Tag" }) 138 ] }), 139 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) }) 140 ] }), 141 /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleSubmit, className: "p-5", children: [ 142 error && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-4 p-3 bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-200/50 dark:border-red-800/30 rounded-xl text-sm", children: error }), 143 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4 relative", ref: containerRef, children: [
144 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-bold text-zinc-500 uppercase tracking-wider mb-2 dark:text-zinc-400", children: "Tag Name" }), 145 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 146 /* @__PURE__ */ jsxRuntimeExports.jsx( 147 "input", 148 { 149 type: "text", 150 value: tagName, 151 onChange: (e) => { 152 setTagName(e.target.value); 153 if (e.target.value.trim() === "") { 154 setShowSuggestions(false); 155 } 156 }, 157 onKeyDown: handleKeyDown, 158 onFocus: () => { 159 if (tagName.trim().length >= 2 && suggestions.length > 0) { 160 setShowSuggestions(true); 161 } 162 }, 163 placeholder: "e.g. piano, classical, fast", 164 className: "w-full p-3 pl-10 border border-zinc-200 dark:border-zinc-800 bg-zinc-50/50 dark:bg-zinc-800/50 rounded-xl focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-zinc-900 dark:text-white transition-all duration-200 font-medium placeholder:text-zinc-400 text-sm", 165 autoFocus: true, 166 disabled: isSubmitting 167 } 168 ),
169 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute left-3.5 top-3.5 text-zinc-400 dark:text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Search, { size: 16 }) }), 170 isLoadingSuggestions && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute right-3.5 top-3.5", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-4 h-4 border-2 border-indigo-500 border-t-transparent rounded-full animate-spin" }) }) 171 ] }), 172 /* @__PURE__ */ jsxRuntimeExports.jsx(AnimatePresence, { children: showSuggestions && suggestions.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx( 173 motion.div, 174 { 175 initial: { opacity: 0, y: -8, scale: 0.98 }, 176 animate: { opacity: 1, y: 0, scale: 1 }, 177 exit: { opacity: 0, y: -8, scale: 0.98 }, 178 transition: { duration: 0.15 }, 179 className: "absolute left-0 right-0 z-50 mt-1.5 bg-white/95 dark:bg-zinc-900/95 border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-2xl overflow-hidden backdrop-blur-md", 180 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "max-h-60 overflow-y-auto py-1.5 custom-scrollbar", children: [
181 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-3 py-1 text-[10px] font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-wider", children: "Similar Tags in Database" }), 182 suggestions.map((s, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 183 "button", 184 { 185 type: "button", 186 onClick: () => handleSelectSuggestion(s.name), 187 onMouseEnter: () => setActiveIndex(idx), 188 className: `w-full flex items-center justify-between px-3 py-2 text-left transition-all duration-150 ${idx === activeIndex ? "bg-indigo-500 text-white font-medium shadow-md shadow-indigo-500/10" : "hover:bg-zinc-50 dark:hover:bg-zinc-800/50 text-zinc-700 dark:text-zinc-300"}`, 189 children: [ 190 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2.5", children: [ 191 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `p-1.5 rounded-lg transition-colors ${idx === activeIndex ? "bg-white/20" : "bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400"}`, children: getTypeIcon(s.type) }),
192 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 193 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm font-semibold", children: getLocalizedName(s) }), 194 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `text-[9px] uppercase tracking-wider font-bold ${idx === activeIndex ? "text-white/70" : "text-zinc-400 dark:text-zinc-500"}`, children: s.type }) 195 ] }) 196 ] }), 197 s.score > 50 && /* @__PURE__ */ jsxRuntimeExports.jsx(TrendingUp, { className: `w-3.5 h-3.5 ${idx === activeIndex ? "text-white/75" : "text-zinc-400 dark:text-zinc-500"}` }) 198 ] 199 }, 200 s.id 201 )) 202 ] }) 203 } 204 ) }) 205 ] }), 206 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2.5 mt-8 border-t border-zinc-100 dark:border-zinc-800/50 pt-4", children: [ 207 /* @__PURE__ */ jsxRuntimeExports.jsx( 208 "button", 209 { 210 type: "button", 211 onClick: onClose, 212 className: "px-4 py-2.5 text-sm font-semibold text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-xl transition-all duration-200", 213 disabled: isSubmitting, 214 children: "Cancel" 215 } 216 ),
217 /* @__PURE__ */ jsxRuntimeExports.jsx( 218 "button", 219 { 220 type: "submit", 221 disabled: isSubmitting || !tagName.trim(), 222 className: "px-5 py-2.5 bg-indigo-600 text-white text-sm font-bold rounded-xl hover:bg-indigo-700 disabled:opacity-50 transition-all duration-200 shadow-lg shadow-indigo-500/15 hover:shadow-indigo-500/25 active:scale-98", 223 children: isSubmitting ? "Adding..." : "Add Tag" 224 } 225 ) 226 ] }) 227 ] }) 228 ] }) }); 229}; 230function EditTrackModal({ isOpen, onClose, track, currentUser, onSuccess }) { 231 const { t } = useTranslation(); 232 const [metadata, setMetadata] = reactExports.useState({ 233 title: track.title, 234 artist: track.artist, 235 description: track.description || "", 236 tags: track.tags.map((t2) => typeof t2 === "string" ? t2 : t2.name).join(", "), 237 genre: "" 238 }); 239 const [availableGenres, setAvailableGenres] = reactExports.useState([]); 240 const [saving, setSaving] = reactExports.useState(false); 241 const [validationErrors, setValidationErrors] = reactExports.useState({}); 242 const [isValidating, setIsValidating] = reactExports.useState(false); 243 const validationTimeoutRef = React.useRef(null); 244 const lastAutofilledFor = React.useRef({ artist: track.artist, title: track.title }); 245 reactExports.useEffect(() => { 246 if (isOpen) { 247 api.getTags({ type: "GENRE" }).then((tags) => { 248 const sorted = [...tags].sort((a, b) => a.name.localeCompare(b.name)); 249 setAvailableGenres(sorted); 250 }); 251 setMetadata({ 252 title: track.title, 253 artist: track.artist, 254 description: track.description || "", 255 tags: track.tags.map((t2) => typeof t2 === "string" ? t2 : t2.name).join(", "), 256 genre: "" 257 }); 258 } 259 }, [isOpen, track]); 260 reactExports.useEffect(() => { 261 if (isOpen) { 262 if (validationTimeoutRef.current) clearTimeout(validationTimeoutRef.current); 263 setIsValidating(true); 264 validationTimeoutRef.current = setTimeout(async () => { 265 try { 266 const res = await api.validateContent({ 267 title: metadata.title, 268 artist: metadata.artist, 269 description: metadata.description 270 }); 271 const errors = {}; 272 Object.entries(res.results).forEach(([key, val]) => { 273 if (!val.valid) errors[key] = val.error || "Invalid content"; 274 }); 275 setValidationErrors(errors); 276 } catch (e) { 277 console.error("Validation failed", e); 278 } finally { 279 setIsValidating(false); 280 } 281 }, 500); 282 } 283 return () => { 284 if (validationTimeoutRef.current) clearTimeout(validationTimeoutRef.current); 285 }; 286 }, [metadata.title, metadata.artist, metadata.description, isOpen]); 287 reactExports.useEffect(() => { 288 if (!isOpen) return; 289 const timeout = setTimeout(async () => { 290 const currentArtist = (metadata.artist || "").trim(); 291 const currentTitle = (metadata.title || "").trim(); 292 if (!currentArtist && !currentTitle) return; 293 if (lastAutofilledFor.current.artist === currentArtist && lastAutofilledFor.current.title === currentTitle) { 294 return; 295 } 296 try { 297 const autoTags = await api.autofillTags(currentArtist, currentTitle); 298 if (autoTags && autoTags.length > 0) { 299 setMetadata((prev) => { 300 const currentTagsList = prev.tags ? prev.tags.split(",").map((t2) => t2.trim()).filter(Boolean) : []; 301 let addedAny = false; 302 for (const t2 of autoTags) { 303 if (!currentTagsList.some((existing) => existing.toLowerCase() === t2.toLowerCase())) { 304 currentTagsList.push(t2); 305 addedAny = true; 306 } 307 } 308 if (addedAny) { 309 return { ...prev, tags: currentTagsList.join(", ") }; 310 } 311 return prev; 312 }); 313 } 314 } catch (err) { 315 console.error("Autofill failed", err); 316 } finally { 317 lastAutofilledFor.current = { artist: currentArtist, title: currentTitle }; 318 } 319 }, 1e3); 320 return () => clearTimeout(timeout); 321 }, [metadata.artist, metadata.title, isOpen]); 322 if (!isOpen) return null; 323 const handleSave = async () => { 324 setSaving(true); 325 let finalTags = metadata.tags; 326 if (metadata.genre) { 327 finalTags = metadata.genre + (finalTags ? "," + finalTags : ""); 328 } 329 try { 330 await api.editTrackWiki(track.id, { 331 title: metadata.title, 332 artist: metadata.artist, 333 description: metadata.description, 334 tags: finalTags, 335 userId: currentUser.id 336 }); 337 onSuccess(); 338 onClose(); 339 } catch (error) { 340 console.error(error); 341 alert(t("upload.failed", "Edit failed. Please try again.")); 342 } finally { 343 setSaving(false); 344 } 345 }; 346 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[1000] flex items-center justify-center p-4", children: [
347 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/80 backdrop-blur-sm", onClick: onClose }), 348 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-2xl bg-white dark:bg-zinc-900 rounded-xl shadow-2xl flex flex-col max-h-[90vh] overflow-hidden animate-in zoom-in-95", children: [ 349 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-4 border-b border-zinc-200 dark:border-zinc-800", children: [ 350 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-zinc-900 dark:text-white", children: "Edit MIDI Information" }), 351 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-md transition-colors text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 outline-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) }) 352 ] }), 353 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-8 flex-1 overflow-y-auto", children: [
354 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4 bg-emerald-50 dark:bg-emerald-900/20 border border-emerald-200 dark:border-emerald-800 rounded-lg p-4 flex gap-3 text-emerald-800 dark:text-emerald-200 text-sm", children: [ 355 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "shrink-0 mt-0.5", size: 18 }), 356 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 357 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-bold", children: "You are editing a public Wiki track." }), 358 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "mt-1 opacity-90", children: "Your edits will be visible to everyone and will be logged to your account. Helpful edits increase your Trust Score!" }) 359 ] }) 360 ] }), 361 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 362 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-4", children: [ 363 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
364 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-sm font-medium mb-1 text-zinc-900 dark:text-white", children: t("upload.fields.title", "Title") }), 365 /* @__PURE__ */ jsxRuntimeExports.jsx( 366 "input", 367 { 368 type: "text", 369 maxLength: 200, 370 value: metadata.title, 371 onChange: (e) => setMetadata({ ...metadata, title: e.target.value }), 372 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` 373 } 374 ), 375 validationErrors.title && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-red-500 mt-0.5", children: validationErrors.title }) 376 ] }), 377 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 378 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-sm font-medium mb-1 text-zinc-900 dark:text-white", children: t("upload.fields.artist", "Composer/Artist") }),
379 /* @__PURE__ */ jsxRuntimeExports.jsx( 380 TagAutocomplete, 381 { 382 value: metadata.artist, 383 onChange: (val) => setMetadata({ ...metadata, artist: val }), 384 onSelectSuggestion: (s) => { 385 if (s.source === "MusicBrainz" || s.type === "GENRE") { 386 const match = availableGenres.find((g) => g.name.toLowerCase() === s.name.toLowerCase()); 387 if (match) { 388 if (!metadata.genre) { 389 setMetadata((prev) => ({ ...prev, genre: match.name })); 390 } 391 } else { 392 setMetadata((prev) => { 393 const currentTags = prev.tags ? prev.tags.split(",").map((t2) => t2.trim()).filter(Boolean) : []; 394 if (!currentTags.some((t2) => t2.toLowerCase() === s.name.toLowerCase())) { 395 currentTags.push(s.name); 396 return { ...prev, tags: currentTags.join(", ") }; 397 } 398 return prev; 399 }); 400 } 401 } 402 }, 403 placeholder: "Andrew Lloyd Webber, Joe Hisaishi...", 404 maxLength: 100, 405 className: validationErrors.artist ? "border-red-500" : "" 406 } 407 ), 408 validationErrors.artist && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-red-500 mt-0.5", children: validationErrors.artist }) 409 ] }) 410 ] }),
411 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 412 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-sm font-medium mb-1 text-zinc-900 dark:text-white", children: t("upload.fields.genre", "Add Genre") }), 413 /* @__PURE__ */ jsxRuntimeExports.jsxs( 414 "select", 415 { 416 value: metadata.genre, 417 onChange: (e) => setMetadata({ ...metadata, genre: e.target.value }), 418 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", 419 children: [ 420 /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: t("upload.fields.selectGenre", "-- Select Genre --") }), 421 availableGenres.map((g) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: g.name, children: g.name }, g.id)) 422 ] 423 } 424 ) 425 ] }), 426 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
427 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-sm font-medium mb-1 text-zinc-900 dark:text-white", children: t("upload.fields.description", "Description") }), 428 /* @__PURE__ */ jsxRuntimeExports.jsx( 429 "textarea", 430 { 431 value: metadata.description, 432 onChange: (e) => setMetadata({ ...metadata, description: e.target.value }), 433 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` 434 } 435 ), 436 validationErrors.description && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-red-500 mt-0.5", children: validationErrors.description }) 437 ] }), 438 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 439 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-sm font-medium mb-1 text-zinc-900 dark:text-white", children: t("upload.fields.tags", "Tags") }),
440 /* @__PURE__ */ jsxRuntimeExports.jsx( 441 TagAutocomplete, 442 { 443 value: metadata.tags, 444 onChange: (val) => setMetadata({ ...metadata, tags: val }), 445 placeholder: "piano, virtuoso, 8-bit" 446 } 447 ) 448 ] }) 449 ] }) 450 ] }), 451 /* @__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: [ 452 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, 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.cancel", "Cancel") }), 453 /* @__PURE__ */ jsxRuntimeExports.jsx( 454 "button", 455 { 456 disabled: saving || isValidating || Object.keys(validationErrors).length > 0, 457 onClick: handleSave, 458 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", 459 children: saving ? "Saving..." : isValidating ? "Validating..." : "Save Edit" 460 } 461 ) 462 ] }) 463 ] }) 464 ] }); 465} 466const ReportModal = ({ isOpen, onClose, trackId, trackTitle, isUnowned, currentUser }) => { 467 const { t } = useTranslation(); 468 const [reason, setReason] = reactExports.useState(""); 469 const [details, setDetails] = reactExports.useState(""); 470 const [isSubmitting, setIsSubmitting] = reactExports.useState(false); 471 const [isSuccess, setIsSuccess] = reactExports.useState(false); 472 const [error, setError] = reactExports.useState(null); 473 if (!isOpen) return null; 474 const handleSubmit = async (e) => { 475 e.preventDefault(); 476 if (!currentUser) { 477 setError("You must be logged in to file a report."); 478 return; 479 } 480 if (!reason) { 481 setError("Please select a reason for reporting."); 482 return; 483 } 484 setIsSubmitting(true); 485 setError(null); 486 try { 487 const targetType = reason === "wiki_abuse" ? "wiki_edit" : "track"; 488 await api.reportContent(String(trackId), targetType, reason, details, currentUser.id); 489 setIsSuccess(true); 490 } catch (err) { 491 setError(err.message || "Failed to submit report. Please try again."); 492 } finally { 493 setIsSubmitting(false); 494 } 495 }; 496 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[1000] flex items-center justify-center p-4 bg-black/60 backdrop-blur-md", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-3xl shadow-2xl w-full max-w-lg overflow-hidden border border-zinc-200/50 dark:border-zinc-800/50 animate-in zoom-in-95 duration-200", children: [ 497 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-5 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-900/50", children: [
498 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-zinc-900 dark:text-white flex items-center gap-2.5", children: [ 499 /* @__PURE__ */ jsxRuntimeExports.jsx(Flag, { size: 20, className: "text-red-500 animate-pulse" }), 500 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Report Content" }) 501 ] }), 502 /* @__PURE__ */ jsxRuntimeExports.jsx( 503 "button", 504 { 505 onClick: onClose, 506 className: "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-colors focus:outline-none", 507 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) 508 } 509 ) 510 ] }), 511 !isSuccess ? /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleSubmit, className: "p-6", children: [ 512 error && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-5 p-3 bg-red-100 dark:bg-red-900/20 text-red-600 dark:text-red-400 border border-red-200/50 dark:border-red-800/30 rounded-2xl text-sm flex gap-2", children: [
513 /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 18, className: "shrink-0 mt-0.5" }), 514 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: error }) 515 ] }), 516 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-4", children: [ 517 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-black text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-3", children: "Select Reason for Report" }), 518 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [ 519 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: `flex items-start gap-3 p-3.5 rounded-2xl border cursor-pointer transition-all ${reason === "copyright" ? "bg-indigo-50/50 dark:bg-indigo-950/20 border-indigo-500 text-indigo-950 dark:text-indigo-200" : "bg-zinc-50/50 dark:bg-zinc-800/30 border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 text-zinc-700 dark:text-zinc-300"}`, children: [ 520 /* @__PURE__ */ jsxRuntimeExports.jsx( 521 "input", 522 { 523 type: "radio", 524 name: "reason", 525 value: "copyright", 526 checked: reason === "copyright", 527 onChange: () => setReason("copyright"), 528 className: "mt-1 accent-indigo-600" 529 } 530 ),
531 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 532 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-bold text-sm", children: "Copyright Infringement" }), 533 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: [ 534 "This MIDI contains intellectual property you own or represent without permission. Please use our official ", 535 /* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: `/dmca?trackId=${trackId}`, className: "text-indigo-600 dark:text-indigo-400 font-bold underline", onClick: (e) => e.stopPropagation(), children: "DMCA Takedown Portal" }), 536 " to submit an official claim." 537 ] }) 538 ] }) 539 ] }), 540 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: `flex items-start gap-3 p-3.5 rounded-2xl border cursor-pointer transition-all ${reason === "broken" ? "bg-indigo-50/50 dark:bg-indigo-950/20 border-indigo-500 text-indigo-950 dark:text-indigo-200" : "bg-zinc-50/50 dark:bg-zinc-800/30 border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 text-zinc-700 dark:text-zinc-300"}`, children: [
541 /* @__PURE__ */ jsxRuntimeExports.jsx( 542 "input", 543 { 544 type: "radio", 545 name: "reason", 546 value: "broken", 547 checked: reason === "broken", 548 onChange: () => setReason("broken"), 549 className: "mt-1 accent-indigo-600" 550 } 551 ), 552 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 553 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-bold text-sm", children: "Broken or Corrupted File" }), 554 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: "The track fails to load, has completely glitched playback, or was uploaded incorrectly." }) 555 ] }) 556 ] }), 557 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: `flex items-start gap-3 p-3.5 rounded-2xl border cursor-pointer transition-all ${reason === "duplicate" ? "bg-indigo-50/50 dark:bg-indigo-950/20 border-indigo-500 text-indigo-950 dark:text-indigo-200" : "bg-zinc-50/50 dark:bg-zinc-800/30 border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 text-zinc-700 dark:text-zinc-300"}`, children: [
558 /* @__PURE__ */ jsxRuntimeExports.jsx( 559 "input", 560 { 561 type: "radio", 562 name: "reason", 563 value: "duplicate", 564 checked: reason === "duplicate", 565 onChange: () => setReason("duplicate"), 566 className: "mt-1 accent-indigo-600" 567 } 568 ), 569 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 570 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "font-bold text-sm", children: "Duplicate Track" }), 571 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: "This arrangement is already available on the platform." }) 572 ] }) 573 ] }), 574 isUnowned && /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: `flex items-start gap-3 p-3.5 rounded-2xl border cursor-pointer transition-all ${reason === "wiki_abuse" ? "bg-amber-50/50 dark:bg-amber-950/20 border-amber-500 text-amber-950 dark:text-amber-200" : "bg-zinc-50/50 dark:bg-zinc-800/30 border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700 text-zinc-700 dark:text-zinc-300"}`, children: [
575 /* @__PURE__ */ jsxRuntimeExports.jsx( 576 "input", 577 { 578 type: "radio", 579 name: "reason", 580 value: "wiki_abuse", 581 checked: reason === "wiki_abuse", 582 onChange: () => setReason("wiki_abuse"), 583 className: "mt-1 accent-amber-600" 584 } 585 ), 586 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 587 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "font-bold text-sm flex items-center gap-1.5 text-amber-600 dark:text-amber-400", children: [ 588 /* @__PURE__ */ jsxRuntimeExports.jsx(Scale, { size: 14 }), 589 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Destructive Wiki Edits (Tribunal)" }) 590 ] }), 591 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5", children: "The recent wiki edits are destructive, malicious, or spammy." }) 592 ] }) 593 ] }) 594 ] }) 595 ] }),
596 /* @__PURE__ */ jsxRuntimeExports.jsx(AnimatePresence, { children: reason === "wiki_abuse" && /* @__PURE__ */ jsxRuntimeExports.jsx( 597 motion.div, 598 { 599 initial: { opacity: 0, height: 0 }, 600 animate: { opacity: 1, height: "auto" }, 601 exit: { opacity: 0, height: 0 }, 602 className: "mb-4 overflow-hidden", 603 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900/50 rounded-2xl text-xs text-amber-800 dark:text-amber-300 flex gap-2.5", children: [ 604 /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldAlert, { size: 18, className: "shrink-0 mt-0.5" }), 605 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 606 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-black", children: "Hamie Tribunal Escalation:" }), 607 " Reporting destructive wiki edits automatically starts a trial at the Hamie Tribunal. The editor will remain free during review. If found guilty by community votes, all of their recent edits will be automatically reverted." 608 ] }) 609 ] }) 610 } 611 ) }),
612 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-5", children: [ 613 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-xs font-black text-zinc-400 dark:text-zinc-500 uppercase tracking-widest mb-2", children: "Details / Supporting Evidence" }), 614 /* @__PURE__ */ jsxRuntimeExports.jsx( 615 "textarea", 616 { 617 value: details, 618 onChange: (e) => setDetails(e.target.value), 619 placeholder: reason === "wiki_abuse" ? "Specify exactly what was vandalized or incorrect in the recent wiki edit..." : "Provide any additional context or proof here...", 620 className: "w-full p-3.5 border border-zinc-200 dark:border-zinc-800 bg-zinc-50/50 dark:bg-zinc-800/50 rounded-2xl focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:border-indigo-500 text-zinc-900 dark:text-white transition-all text-sm h-24 placeholder:text-zinc-400 resize-none font-medium", 621 required: reason === "wiki_abuse" 622 } 623 ) 624 ] }),
625 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2.5 border-t border-zinc-100 dark:border-zinc-800/50 pt-4", children: [ 626 /* @__PURE__ */ jsxRuntimeExports.jsx( 627 "button", 628 { 629 type: "button", 630 onClick: onClose, 631 className: "px-4 py-2.5 text-sm font-bold text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-xl transition-all duration-200", 632 disabled: isSubmitting, 633 children: "Cancel" 634 } 635 ), 636 /* @__PURE__ */ jsxRuntimeExports.jsx( 637 "button", 638 { 639 type: "submit", 640 disabled: isSubmitting || !reason || reason === "wiki_abuse" && !details.trim(), 641 className: `px-5 py-2.5 text-sm font-bold rounded-xl transition-all duration-200 shadow-lg disabled:opacity-50 active:scale-98 ${reason === "wiki_abuse" ? "bg-amber-600 text-white hover:bg-amber-700 shadow-amber-500/15" : "bg-red-600 text-white hover:bg-red-700 shadow-red-500/15"}`, 642 children: isSubmitting ? "Submitting..." : reason === "wiki_abuse" ? "Escalate to Tribunal" : "Submit Report" 643 } 644 ) 645 ] }) 646 ] }) : ( 647 // Success View
648 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-8 text-center flex flex-col items-center", children: [ 649 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-16 bg-emerald-50 dark:bg-emerald-950/30 text-emerald-500 rounded-full flex items-center justify-center mb-4 border border-emerald-100 dark:border-emerald-900/40", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { size: 36, className: "animate-bounce" }) }), 650 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "text-lg font-bold text-zinc-900 dark:text-white mb-2", children: reason === "wiki_abuse" ? "Case Escalated Successfully!" : "Report Filed Successfully!" }), 651 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500 dark:text-zinc-400 max-w-sm mb-6 leading-relaxed", children: reason === "wiki_abuse" ? "Your report has successfully created an active case in the Hamie Tribunal. The editor will remain free while the community reviews the edits and votes on the case." : "Thank you for helping keep HamieNet clean. Moderation systems and administrators have been notified of this report." }),
652 /* @__PURE__ */ jsxRuntimeExports.jsx( 653 "button", 654 { 655 onClick: onClose, 656 className: "px-6 py-2.5 bg-zinc-900 dark:bg-zinc-100 text-white dark:text-zinc-900 font-bold text-sm rounded-xl hover:bg-zinc-800 dark:hover:bg-zinc-200 transition-all shadow-md active:scale-98", 657 children: "Done" 658 } 659 ) 660 ] }) 661 ) 662 ] }) }); 663}; 664function ReuploadModal({ isOpen, onClose, trackId, trackTitle, onSuccess }) { 665 const { t } = useTranslation(); 666 const [file, setFile] = reactExports.useState(null); 667 const [uploading, setUploading] = reactExports.useState(false); 668 const [error, setError] = reactExports.useState(null); 669 const [success, setSuccess] = reactExports.useState(false); 670 const fileInputRef = reactExports.useRef(null); 671 if (!isOpen) return null; 672 const handleFileSelect = (e) => { 673 var _a; 674 if (e.target.files && e.target.files[0]) { 675 const f = e.target.files[0]; 676 const ext = (_a = f.name.split(".").pop()) == null ? void 0 : _a.toLowerCase(); 677 if (!["mid", "midi", "kar"].includes(ext || "")) { 678 setError(t("upload.invalidType", "Please select a valid MIDI file (.mid, .midi, .kar)")); 679 setFile(null); 680 return; 681 } 682 setFile(f); 683 setError(null); 684 } 685 }; 686 const handleUpload = async () => { 687 if (!file) return; 688 setUploading(true); 689 setError(null); 690 try { 691 await api.reuploadMidi(trackId, file); 692 setSuccess(true); 693 if (onSuccess) { 694 onSuccess(); 695 } 696 } catch (e) { 697 setError(e.message || t("upload.failed", "Upload failed. Please try again.")); 698 } finally { 699 setUploading(false); 700 } 701 }; 702 const handleClose = () => { 703 setFile(null); 704 setError(null); 705 setSuccess(false); 706 onClose(); 707 }; 708 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-0 z-[1000] flex items-center justify-center p-4", children: [ 709 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/80 backdrop-blur-sm", onClick: handleClose }), 710 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-md bg-white dark:bg-zinc-900 rounded-xl shadow-2xl flex flex-col overflow-hidden animate-in zoom-in-95", children: [
711 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-4 border-b border-zinc-200 dark:border-zinc-800", children: [ 712 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-lg font-bold text-zinc-900 dark:text-white", children: t("upload.reuploadTitle", "Re-upload MIDI File") }), 713 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleClose, className: "p-1 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { className: "text-zinc-500", size: 18 }) }) 714 ] }), 715 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 space-y-4", children: [ 716 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-sm text-zinc-600 dark:text-zinc-400", children: [ 717 t("upload.reuploadWarning", 'The MIDI file for "'), 718 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-900 dark:text-white", children: trackTitle }), 719 t("upload.reuploadWarning2", '" is missing on the server. Please re-upload the original file.') 720 ] }), 721 error && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3 flex gap-2 text-red-800 dark:text-red-300 text-xs", children: [
722 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { className: "shrink-0", size: 16 }), 723 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: error }) 724 ] }), 725 success ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center py-6 space-y-3", children: [ 726 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleCheck, { size: 48, className: "mx-auto text-emerald-500 animate-bounce" }), 727 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-lg font-bold text-zinc-900 dark:text-white", children: t("upload.complete", "Re-upload Complete!") }), 728 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-zinc-500", children: t("upload.reprocessing", "Your MIDI is now queued for reprocessing. It will become public once rendering finishes.") }) 729 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: file ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 p-3 bg-zinc-50 dark:bg-zinc-800 rounded-lg border border-zinc-200 dark:border-zinc-700", children: [
730 /* @__PURE__ */ jsxRuntimeExports.jsx(FileHeadphone, { className: "text-indigo-500 shrink-0", size: 20 }), 731 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-xs flex-1 truncate text-zinc-800 dark:text-zinc-200", children: file.name }), 732 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setFile(null), className: "text-xs text-red-500 hover:underline", children: t("common.change", "Change") }) 733 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs( 734 "div", 735 { 736 className: "h-36 border-2 border-dashed border-zinc-300 dark:border-zinc-700 hover:border-indigo-500 dark:hover:border-indigo-400 rounded-xl flex flex-col items-center justify-center cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-800/30 transition-all", 737 onClick: () => { 738 var _a; 739 return (_a = fileInputRef.current) == null ? void 0 : _a.click(); 740 }, 741 children: [ 742 /* @__PURE__ */ jsxRuntimeExports.jsx(Upload, { size: 24, className: "text-zinc-400 mb-2" }),
743 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs font-semibold text-zinc-700 dark:text-zinc-300", children: t("upload.selectFile", "Select MIDI file") }), 744 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-zinc-400 mt-1", children: ".mid, .midi, .kar up to 5MB" }), 745 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "file", ref: fileInputRef, className: "hidden", accept: ".mid,.midi,.kar", onChange: handleFileSelect }) 746 ] 747 } 748 ) }) 749 ] }), 750 /* @__PURE__ */ jsxRuntimeExports.jsx("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: success ? /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleClose, className: "px-4 py-1.5 bg-zinc-200 dark:bg-zinc-800 hover:bg-zinc-300 dark:hover:bg-zinc-700 rounded-lg font-semibold text-xs text-zinc-900 dark:text-white", children: t("common.close", "Close") }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
751 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: handleClose, className: "px-4 py-1.5 bg-zinc-100 hover:bg-zinc-200 dark:bg-zinc-800 dark:hover:bg-zinc-700 text-zinc-700 dark:text-zinc-300 rounded-lg text-xs font-semibold", children: t("common.cancel", "Cancel") }), 752 /* @__PURE__ */ jsxRuntimeExports.jsx( 753 "button", 754 { 755 disabled: !file || uploading, 756 onClick: handleUpload, 757 className: "px-4 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-bold disabled:opacity-50 transition-opacity", 758 children: uploading ? t("upload.uploading", "Uploading...") : t("common.upload", "Upload") 759 } 760 ) 761 ] }) }) 762 ] }) 763 ] }); 764} 765const TrackEventsModal = ({ isOpen, onClose, instrument, currentTime = 0, tempo = 120, tickDiv = 480 }) => { 766 const { t } = useTranslation(); 767 const [events, setEvents] = reactExports.useState([]); 768 const [loading, setLoading] = reactExports.useState(false); 769 const [error, setError] = reactExports.useState(null); 770 const [autoScroll, setAutoScroll] = reactExports.useState(true); 771 const tableContainerRef = reactExports.useRef(null); 772 const activeRowRef = reactExports.useRef(null); 773 reactExports.useEffect(() => { 774 if (isOpen && instrument) { 775 if (instrument.eventsJson) { 776 setLoading(true); 777 setError(null); 778 fetch(instrument.eventsJson).then((res) => { 779 if (!res.ok) throw new Error("Failed to load events"); 780 return res.json(); 781 }).then((data) => { 782 let currentTick = 0; 783 let currentBpm = tempo; 784 const processedEvents = data.map((evt) => { 785 currentTick += evt.delta; 786 if (evt.event === "Tempo") { 787 const match = evt.data.match(/([\d.]+)\s*BPM/); 788 if (match && match[1]) { 789 currentBpm = parseFloat(match[1]); 790 } 791 } 792 const timeInSeconds = currentTick / tickDiv * (60 / currentBpm); 793 return { ...evt, absTime: timeInSeconds, absTick: currentTick }; 794 }); 795 setEvents(processedEvents); 796 setLoading(false); 797 }).catch((e) => { 798 console.error(e); 799 setError("Could not load event data."); 800 setLoading(false); 801 }); 802 } else { 803 setEvents([ 804 { id: 1, delta: 0, event: "Info", data: "No detailed event log available for this track.", absTime: 0 } 805 ]); 806 } 807 } else { 808 setEvents([]); 809 } 810 }, [isOpen, instrument, tempo, tickDiv]); 811 const activeEventIndex = reactExports.useMemo(() => { 812 if (!events.length || currentTime === 0) return -1; 813 for (let i = events.length - 1; i >= 0; i--) { 814 if (events[i].absTime !== void 0 && events[i].absTime <= currentTime) { 815 return i; 816 } 817 } 818 return 0; 819 }, [events, currentTime]); 820 const nextEventInfo = reactExports.useMemo(() => { 821 if (activeEventIndex === -1 || activeEventIndex >= events.length - 1) return null; 822 const currentEvent = events[activeEventIndex]; 823 const nextEvent = events[activeEventIndex + 1]; 824 if (nextEvent.absTime === void 0 || currentEvent.absTime === void 0 || nextEvent.absTick === void 0 || currentEvent.absTick === void 0) return null; 825 const timeRemaining = nextEvent.absTime - currentTime; 826 const timeFraction = (currentTime - currentEvent.absTime) / (nextEvent.absTime - currentEvent.absTime); 827 const tickDiff = nextEvent.absTick - currentEvent.absTick; 828 const currentTick = currentEvent.absTick + tickDiff * timeFraction; 829 const ticksRemaining = Math.max(0, Math.round(nextEvent.absTick - currentTick)); 830 return { 831 timeInSeconds: timeRemaining, 832 ticks: ticksRemaining 833 }; 834 }, [events, currentTime, activeEventIndex]); 835 reactExports.useEffect(() => { 836 if (autoScroll && activeRowRef.current && tableContainerRef.current) { 837 const container = tableContainerRef.current; 838 const row = activeRowRef.current; 839 const containerHeight = container.clientHeight; 840 const rowTop = row.offsetTop; 841 const rowHeight = row.clientHeight; 842 const scrollTop = rowTop - containerHeight / 2 + rowHeight / 2; 843 container.scrollTo({ 844 top: scrollTop, 845 behavior: "smooth" 846 }); 847 } 848 }, [activeEventIndex, autoScroll]); 849 if (!isOpen || !instrument) return null; 850 const instrumentName = instrument.name || getInstrumentName(instrument.program); 851 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "fixed inset-x-0 top-0 bottom-[84px] z-[1000] flex items-end justify-center", children: [
852 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/80 backdrop-blur-sm", onClick: onClose }), 853 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full max-w-4xl bg-white dark:bg-zinc-900 rounded-t-xl flex flex-col shadow-2xl animate-in zoom-in-95 border border-zinc-200 dark:border-zinc-800 border-b-0 max-h-[calc(100vh-8rem)] overflow-hidden", children: [ 854 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between px-6 py-4 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 shrink-0", children: [ 855 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 856 /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 857 /* @__PURE__ */ jsxRuntimeExports.jsx(List, { className: "text-indigo-500" }), 858 " ", 859 t("events.title", "Track Events") 860 ] }), 861 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-sm text-zinc-500 font-mono", children: [ 862 t("events.track", { channel: instrument.trackId + 1, name: instrumentName, count: instrument.notesCount, defaultValue: `Track ${instrument.trackId + 1}: ${instrumentName} (${instrument.notesCount} notes)` }), 863 instrument.midiChannel !== void 0 && instrument.midiChannel !== -1 && ` ⢠MIDI Channel: ${instrument.midiChannel + 1}` 864 ] }) 865 ] }),
866 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4", children: [ 867 /* @__PURE__ */ jsxRuntimeExports.jsxs( 868 "button", 869 { 870 onClick: () => setAutoScroll(!autoScroll), 871 className: `flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${autoScroll ? "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-400" : "bg-zinc-200 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400"}`, 872 children: [ 873 /* @__PURE__ */ jsxRuntimeExports.jsx(Navigation, { size: 14, className: autoScroll ? "animate-pulse" : "" }), 874 autoScroll ? "Auto-scroll On" : "Auto-scroll Off" 875 ] 876 } 877 ), 878 nextEventInfo !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-right", children: [ 879 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-400", children: "Next Event In" }),
880 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-lg font-bold font-mono text-indigo-500", children: [ 881 nextEventInfo.timeInSeconds.toFixed(1), 882 "s ", 883 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-sm text-zinc-400", children: [ 884 "(", 885 nextEventInfo.ticks, 886 " ticks)" 887 ] }) 888 ] }) 889 ] }), 890 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "p-2 hover:bg-zinc-200 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 }) }) 891 ] }) 892 ] }), 893 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: tableContainerRef, className: "flex-1 overflow-auto bg-white dark:bg-zinc-900 relative", children: [ 894 loading && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex items-center justify-center bg-white/50 dark:bg-black/50 z-10", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-2", children: [
895 /* @__PURE__ */ jsxRuntimeExports.jsx(RefreshCw, { className: "animate-spin text-indigo-500", size: 32 }), 896 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-bold text-zinc-500", children: t("events.loading", "Loading Events...") }) 897 ] }) }), 898 error ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-10 text-center text-red-500", children: t("events.error", "Could not load event data.") }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full text-left text-sm border-collapse", children: [ 899 /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "bg-zinc-100 dark:bg-zinc-800 sticky top-0 z-10 shadow-sm", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [ 900 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-6 py-3 font-bold text-zinc-600 dark:text-zinc-300 border-b border-zinc-200 dark:border-zinc-700 w-24", children: t("events.colId", "Item ID") }), 901 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-6 py-3 font-bold text-zinc-600 dark:text-zinc-300 border-b border-zinc-200 dark:border-zinc-700 w-32", children: t("events.colDelta", "Delta Time") }),
902 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-6 py-3 font-bold text-zinc-600 dark:text-zinc-300 border-b border-zinc-200 dark:border-zinc-700 w-48", children: t("events.colEvent", "Event") }), 903 /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-6 py-3 font-bold text-zinc-600 dark:text-zinc-300 border-b border-zinc-200 dark:border-zinc-700", children: t("events.colData", "Data (Note, Velocity)") }) 904 ] }) }), 905 /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { className: "divide-y divide-zinc-100 dark:divide-zinc-800", children: events.map((evt, idx) => { 906 const isActive = idx === activeEventIndex; 907 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 908 "tr", 909 { 910 ref: isActive ? activeRowRef : null, 911 className: `transition-colors font-mono text-xs ${isActive ? "bg-indigo-50 dark:bg-indigo-900/20 shadow-[inset_4px_0_0_0_rgba(99,102,241,1)]" : "hover:bg-zinc-50 dark:hover:bg-zinc-800/50"}`, 912 children: [
913 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: `px-6 py-2 ${isActive ? "text-indigo-600 dark:text-indigo-400 font-bold" : "text-zinc-500"}`, children: evt.id }), 914 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: `px-6 py-2 ${isActive ? "text-indigo-700 dark:text-indigo-300 font-bold" : "text-zinc-700 dark:text-zinc-300"}`, children: evt.delta }), 915 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-6 py-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `px-2 py-0.5 rounded text-[10px] font-bold uppercase ${evt.event === "Note on" ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400" : evt.event === "Note off" ? "bg-zinc-200 text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400" : "bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-400"}`, children: evt.event }) }), 916 /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: `px-6 py-2 font-medium ${isActive ? "text-indigo-900 dark:text-indigo-100" : "text-zinc-800 dark:text-zinc-200"}`, children: evt.data === "No detailed event log available for this track." ? t("events.noLog", "No detailed event log available for this track.") : evt.data }) 917 ] 918 }, 919 evt.id || idx 920 ); 921 }) }) 922 ] }) 923 ] }),
924 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 border-t border-zinc-200 dark:border-zinc-800 bg-zinc-50 dark:bg-zinc-950 shrink-0 flex justify-between items-center text-xs text-zinc-500", children: [ 925 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("events.showing", { count: events.length, defaultValue: `Showing ${events.length} events` }) }), 926 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "px-4 py-2 bg-zinc-200 dark:bg-zinc-800 hover:bg-zinc-300 dark:hover:bg-zinc-700 rounded-lg font-bold text-zinc-700 dark:text-zinc-300 transition-colors", children: t("events.close", "Close Viewer") }) 927 ] }) 928 ] }) 929 ] }); 930}; 931const PianoRollGame = ({ 932 data, 933 melodyData, 934 currentTime, 935 isPlaying, 936 colorMap, 937 selectedTrackIndex, 938 processedInstruments, 939 uniqueNames, 940 onPlayPause, 941 onSeek, 942 toggleGameMode, 943 onGameModeMute, 944 trackId, 945 userId, 946 userName, 947 onAuthRequested, 948 duration, 949 tempo, 950 timeSignature, 951 isLoadingAudio, 952 isAudioLoaded, 953 onSelectTrack, 954 track, 955 soundfont, 956 melodySource, 957 playbackRate 958}) => { 959 const containerRef = reactExports.useRef(null); 960 const canvasRef = reactExports.useRef(null); 961 const requestRef = reactExports.useRef(null); 962 const lastTimeRef = reactExports.useRef(currentTime); 963 const lastTickRef = reactExports.useRef(0); 964 const particlesRef = reactExports.useRef([]); 965 const prevActiveNotesRef = reactExports.useRef(/* @__PURE__ */ new Set()); 966 const starFieldRef = reactExports.useRef([]); 967 const targetStartTimeRef = reactExports.useRef(null); 968 reactExports.useEffect(() => { 969 lastTimeRef.current = currentTime; 970 lastTickRef.current = performance.now(); 971 }, [currentTime]); 972 const [gameCountdown, setGameCountdown] = reactExports.useState(null); 973 const countdownStartTimeRef = reactExports.useRef(null); 974 const countdownDurationRef = reactExports.useRef(0); 975 const hasCountedDownRef = reactExports.useRef(false); 976 const countdownIntervalRef = reactExports.useRef(null); 977 const [isGamePaused, setIsGamePaused] = reactExports.useState(false); 978 const [gameScore, setGameScore] = reactExports.useState(0); 979 const gameScoreRef = reactExports.useRef(0); 980 const [gameCombo, setGameCombo] = reactExports.useState(0); 981 const gameComboRef = reactExports.useRef(0); 982 const [maxCombo, setMaxCombo] = reactExports.useState(0); 983 const maxComboRef = reactExports.useRef(0); 984 const [totalNotes, setTotalNotes] = reactExports.useState(0); 985 const [hitNotesCount, setHitNotesCount] = reactExports.useState(0); 986 const hitNotesCountRef = reactExports.useRef(0); 987 const [showEndScreen, setShowEndScreen] = reactExports.useState(false); 988 const [leaderboard, setLeaderboard] = reactExports.useState([]); 989 const [, setIsSubmittingScore] = reactExports.useState(false); 990 const [melodyTrackNames, setMelodyTrackNames] = reactExports.useState(/* @__PURE__ */ new Set()); 991 const [melodyTrackIndices, setMelodyTrackIndices] = reactExports.useState(/* @__PURE__ */ new Set()); 992 const melodyTrackIndicesRef = reactExports.useRef(/* @__PURE__ */ new Set()); 993 const [currentMelodyTrackIdx, setCurrentMelodyTrackIdx] = reactExports.useState(-1); 994 const currentMelodyTrackRef = reactExports.useRef(-1); 995 reactExports.useEffect(() => { 996 melodyTrackIndicesRef.current = melodyTrackIndices; 997 }, [melodyTrackIndices]); 998 const getTrackIdxForName = reactExports.useCallback((name, noteTrackIdx) => { 999 if (noteTrackIdx !== void 0 && processedInstruments) { 1000 const musicalInstruments = processedInstruments.filter((inst) => inst.midiChannel !== -1); 1001 const numericIdx = Number(noteTrackIdx); 1002 if (numericIdx >= 0 && numericIdx < musicalInstruments.length) { 1003 const targetInst = musicalInstruments[numericIdx]; 1004 return processedInstruments.findIndex((inst) => inst.trackId === targetInst.trackId); 1005 } 1006 } 1007 if (!processedInstruments) return -1; 1008 const idx = processedInstruments.findIndex( 1009 (inst) => inst.originalName === name || inst.displayName === name || inst.name === name 1010 ); 1011 return idx; 1012 }, [processedInstruments]); 1013 reactExports.useEffect(() => { 1014 if (melodyData && melodyData.length > 0 && processedInstruments) { 1015 console.log("[DEBUG] --- PianoRollGame Melody Mapping Sample ---");
1016 melodyData.slice(0, 5).forEach((note, i) => { 1017 var _a; 1018 const finalIdx = getTrackIdxForName(note.i, note.trackIdx); 1019 const trackName = ((_a = processedInstruments[finalIdx]) == null ? void 0 : _a.displayName) || "Unknown"; 1020 console.log(`[DEBUG] Note ${i}: JSON Name[${note.i}] JSON Id[${note.trackIdx}] -> Mapped Index[${finalIdx}] (${trackName})`); 1021 }); 1022 console.log("----------------------------------------------------"); 1023 } 1024 }, [melodyData, processedInstruments, getTrackIdxForName]); 1025 const hitNotesRef = reactExports.useRef(/* @__PURE__ */ new Set()); 1026 const missedNotesRef = reactExports.useRef(/* @__PURE__ */ new Set()); 1027 const activeHoldsRef = reactExports.useRef(/* @__PURE__ */ new Map()); 1028 const dataRef = reactExports.useRef(data); 1029 const isPlayingRef = reactExports.useRef(isPlaying); 1030 const processedGameDataRef = reactExports.useRef([]); 1031 const [octaveTip, setOctaveTip] = reactExports.useState(null); 1032 const [gameSpeed, setGameSpeed] = reactExports.useState(1); 1033 const gameSpeedRef = reactExports.useRef(1); 1034 const prevMelodyDataRef = reactExports.useRef(melodyData); 1035 const prevMelodySourceRef = reactExports.useRef(melodySource); 1036 reactExports.useEffect(() => { 1037 dataRef.current = data; 1038 }, [data]); 1039 reactExports.useEffect(() => { 1040 if (isPlaying) { 1041 const elapsed = (performance.now() - lastTickRef.current) / 1e3; 1042 lastTimeRef.current += elapsed * gameSpeedRef.current; 1043 lastTickRef.current = performance.now(); 1044 } 1045 gameSpeedRef.current = gameSpeed; 1046 window.dispatchEvent(new CustomEvent("setPlaybackRate", { detail: gameSpeed })); 1047 }, [gameSpeed, isPlaying]); 1048 reactExports.useEffect(() => { 1049 if (isPlaying) { 1050 lastTickRef.current = performance.now(); 1051 } else { 1052 const elapsed = (performance.now() - lastTickRef.current) / 1e3; 1053 lastTimeRef.current += elapsed * gameSpeedRef.current; 1054 } 1055 }, [isPlaying]); 1056 reactExports.useEffect(() => { 1057 if (trackId) { 1058 api.getHighscores(trackId, "piano-roll", userId).then(setLeaderboard).catch(console.error); 1059 } 1060 }, [trackId, userId]); 1061 const [bgTheme, setBgTheme] = reactExports.useState({ color1: "#000000", color2: "#000000" }); 1062 const generateBgTheme = reactExports.useCallback(() => { 1063 const color = "#000000"; 1064 setBgTheme({ color1: color, color2: color }); 1065 }, [trackId]); 1066 reactExports.useEffect(() => { 1067 generateBgTheme(); 1068 }, [generateBgTheme]); 1069 const resetGame = reactExports.useCallback(() => { 1070 generateBgTheme(); 1071 setGameScore(0); 1072 gameScoreRef.current = 0; 1073 setGameCombo(0); 1074 gameComboRef.current = 0; 1075 setMaxCombo(0); 1076 maxComboRef.current = 0; 1077 setHitNotesCount(0); 1078 hitNotesCountRef.current = 0; 1079 setShowEndScreen(false); 1080 setIsGamePaused(false); 1081 hitNotesRef.current.clear(); 1082 missedNotesRef.current.clear(); 1083 activeHoldsRef.current.clear(); 1084 setOctaveTip(null); 1085 setGameSpeed(1); 1086 if (onSeek) onSeek(0); 1087 setGameCountdown(null); 1088 }, [onSeek, generateBgTheme]); 1089 reactExports.useEffect(() => { 1090 if (melodyData && melodyData.length > 0) { 1091 const mNames = /* @__PURE__ */ new Set(); 1092 const mIndices = /* @__PURE__ */ new Set(); 1093 console.log("[PianoRollGame] --- Melody Muting Sync ---"); 1094 melodyData.forEach((note) => { 1095 mNames.add(note.i); 1096 const idx = getTrackIdxForName(note.i, note.trackIdx); 1097 if (idx !== -1) { 1098 mIndices.add(idx); 1099 } 1100 }); 1101 console.log("[PianoRollGame] mNames (from melodyData):", Array.from(mNames)); 1102 if (processedInstruments) { 1103 if (mIndices.size === 0) { 1104 console.error("[PianoRollGame] FAILED to match any melody tracks for muting!"); 1105 console.log("[PianoRollGame] Instrument List:", processedInstruments.map((i) => i.originalName)); 1106 } else { 1107 console.log("[PianoRollGame] Final Mute Indices:", Array.from(mIndices)); 1108 } 1109 } 1110 setMelodyTrackNames(mNames); 1111 setMelodyTrackIndices(mIndices); 1112 if (onGameModeMute) { 1113 onGameModeMute(Array.from(mIndices)); 1114 } 1115 } else { 1116 const trackStats = {};
1117 const grouped = {}; 1118 const skylineCounts = {}; 1119 data.forEach((note) => { 1120 if (!trackStats[note.i]) { 1121 trackStats[note.i] = { notes: [], polyphonicCount: 0, minPitch: 128, maxPitch: -1, totalDuration: 0, name: note.i }; 1122 } 1123 trackStats[note.i].notes.push(note); 1124 trackStats[note.i].minPitch = Math.min(trackStats[note.i].minPitch, note.n); 1125 trackStats[note.i].maxPitch = Math.max(trackStats[note.i].maxPitch, note.n); 1126 trackStats[note.i].totalDuration += note.d; 1127 }); 1128 Object.values(trackStats).forEach((stats) => { 1129 stats.notes.sort((a, b) => a.t - b.t); 1130 let overlaps = 0; 1131 for (let i = 0; i < stats.notes.length - 1; i++) { 1132 if (stats.notes[i].t + stats.notes[i].d > stats.notes[i + 1].t + 0.05) { 1133 overlaps++; 1134 } 1135 } 1136 stats.polyphonicCount = overlaps; 1137 }); 1138 data.forEach((note) => { 1139 const nameLower = ((note == null ? void 0 : note.i) || "").toLowerCase(); 1140 if (nameLower.includes("drum") || nameLower.includes("perc") || nameLower.includes("kit") || nameLower.includes("cymbal")) return; 1141 if (note.d > 4) return; 1142 const timeKey = Math.round(note.t * 20); 1143 if (!grouped[timeKey]) grouped[timeKey] = []; 1144 grouped[timeKey].push(note); 1145 }); 1146 Object.values(grouped).forEach((group) => { 1147 let maxPitch = -1; 1148 let bestNote = null; 1149 group.forEach((n) => { 1150 if (n.n > maxPitch) { 1151 maxPitch = n.n; 1152 bestNote = n; 1153 } 1154 }); 1155 if (bestNote) { 1156 skylineCounts[bestNote.i] = (skylineCounts[bestNote.i] || 0) + 1; 1157 } 1158 }); 1159 const trackScores = {}; 1160 const maxSkyline = Math.max(1, ...Object.values(skylineCounts)); 1161 Object.values(trackStats).forEach((stats) => { 1162 const nameLower = ((stats == null ? void 0 : stats.name) || "").toLowerCase(); 1163 let score = (skylineCounts[stats.name] || 0) / maxSkyline * 100; 1164 if (nameLower.includes("melody") || nameLower.includes("lead") || nameLower.includes("vocal") || nameLower.includes("solo") || nameLower.includes("right")) { 1165 score += 50; 1166 } 1167 if (nameLower.includes("bass") || nameLower.includes("pad") || nameLower.includes("chord") || nameLower.includes("left")) { 1168 score -= 30; 1169 } 1170 if (nameLower.includes("drum") || nameLower.includes("perc") || nameLower.includes("kit") || nameLower.includes("cymbal")) { 1171 score -= 100; 1172 } 1173 const polyRatio = stats.notes.length > 0 ? stats.polyphonicCount / stats.notes.length : 0; 1174 if (polyRatio > 0.5) { 1175 score -= 40; 1176 } else if (polyRatio > 0.2) { 1177 score -= 20; 1178 } else { 1179 score += 10; 1180 } 1181 const range = stats.maxPitch - stats.minPitch; 1182 if (range < 7) { 1183 score -= 30; 1184 } else if (range > 48) { 1185 score -= 20; 1186 } 1187 trackScores[stats.name] = score; 1188 }); 1189 const melodyTracks = /* @__PURE__ */ new Set(); 1190 let bestTrack = null; 1191 let bestScore = -Infinity; 1192 for (const [t, score] of Object.entries(trackScores)) { 1193 if (score > bestScore) { 1194 bestScore = score; 1195 bestTrack = t; 1196 } 1197 if (score > 40) { 1198 melodyTracks.add(t); 1199 } 1200 } 1201 if (melodyTracks.size === 0 && bestTrack) { 1202 melodyTracks.add(bestTrack); 1203 } 1204 setMelodyTrackNames(melodyTracks); 1205 const mIndices = /* @__PURE__ */ new Set(); 1206 if (processedInstruments) { 1207 processedInstruments.forEach((inst, idx) => { 1208 if (melodyTracks.has(inst.originalName)) { 1209 mIndices.add(idx); 1210 } 1211 }); 1212 } 1213 setMelodyTrackIndices(mIndices); 1214 if (onGameModeMute) { 1215 onGameModeMute(Array.from(mIndices)); 1216 } 1217 } 1218 setGameScore(0); 1219 gameScoreRef.current = 0; 1220 setGameCombo(0); 1221 gameComboRef.current = 0; 1222 setMaxCombo(0); 1223 maxComboRef.current = 0; 1224 setHitNotesCount(0); 1225 hitNotesCountRef.current = 0; 1226 setShowEndScreen(false); 1227 setIsGamePaused(false); 1228 hitNotesRef.current.clear(); 1229 missedNotesRef.current.clear(); 1230 activeHoldsRef.current.clear(); 1231 setOctaveTip(null); 1232 setGameSpeed(1); 1233 if (isPlaying && onPlayPause) onPlayPause(false); 1234 const isFreshLoad = prevMelodyDataRef.current !== melodyData || prevMelodySourceRef.current !== melodySource; 1235 if (onSeek && isFreshLoad) { 1236 onSeek(0); 1237 lastTimeRef.current = 0; 1238 } 1239 prevMelodyDataRef.current = melodyData; 1240 prevMelodySourceRef.current = melodySource;
1241 setGameCountdown(null); 1242 const currentMissedNotes = missedNotesRef.current; 1243 return () => { 1244 if (userId && gameScoreRef.current > 0 && trackId) { 1245 const accuracy = hitNotesCountRef.current / (hitNotesCountRef.current + currentMissedNotes.size || 1); 1246 api.postHighscore(trackId, { 1247 userId, 1248 userName: "Me", 1249 score: gameScoreRef.current, 1250 accuracy, 1251 maxCombo: maxComboRef.current 1252 }).catch(console.error); 1253 } 1254 if (onGameModeMute) { 1255 onGameModeMute(null); 1256 } 1257 }; 1258 }, [melodyData, processedInstruments, uniqueNames]); 1259 reactExports.useEffect(() => { 1260 if (melodyData && melodyData.length > 0) { 1261 const rawMelodyNotes2 = [...melodyData].sort((a, b) => a.t - b.t); 1262 const finalData = rawMelodyNotes2.map((n, i) => { 1263 const resolvedIdx = getTrackIdxForName(n.i, n.trackIdx); 1264 const mappedI = resolvedIdx !== -1 && uniqueNames && uniqueNames[resolvedIdx] ? uniqueNames[resolvedIdx] : n.i; 1265 return { 1266 ...n, 1267 i: mappedI, 1268 trackIdx: resolvedIdx, 1269 id: `${n.t}-${n.n}-${i}` 1270 }; 1271 }); 1272 processedGameDataRef.current = finalData; 1273 return; 1274 } 1275 if (melodyTrackNames.size === 0) return; 1276 const grouped = {}; 1277 data.forEach((note) => { 1278 if (!melodyTrackNames.has(note.i)) return; 1279 const timeKey = Math.round(note.t * 20); 1280 if (!grouped[timeKey]) grouped[timeKey] = []; 1281 grouped[timeKey].push(note); 1282 }); 1283 const rawMelodyNotes = []; 1284 Object.values(grouped).forEach((group) => { 1285 let maxPitch = -1; 1286 let bestNote = null; 1287 group.forEach((n) => { 1288 if (n.n > maxPitch) { 1289 maxPitch = n.n; 1290 bestNote = n; 1291 } 1292 }); 1293 if (bestNote) { 1294 rawMelodyNotes.push(bestNote); 1295 } 1296 }); 1297 rawMelodyNotes.sort((a, b) => a.t - b.t); 1298 const filteredNotes = []; 1299 if (rawMelodyNotes.length > 0) { 1300 filteredNotes.push(rawMelodyNotes[0]); 1301 for (let i = 1; i < rawMelodyNotes.length; i++) { 1302 const current = rawMelodyNotes[i]; 1303 const prev = filteredNotes[filteredNotes.length - 1]; 1304 const next = i < rawMelodyNotes.length - 1 ? rawMelodyNotes[i + 1] : null; 1305 let isOutlier = false;
1306 if (prev && next) { 1307 const prevPitch = prev.n; 1308 const nextPitch = next.n; 1309 const currPitch = current.n; 1310 if (prevPitch - currPitch > 15 && nextPitch - currPitch > 15 && Math.abs(prevPitch - nextPitch) < 12) { 1311 isOutlier = true; 1312 } 1313 } 1314 if (!isOutlier) { 1315 filteredNotes.push(current); 1316 } 1317 } 1318 } 1319 const finalFilteredNotes = filteredNotes.map((n, i) => { 1320 const resolvedIdx = getTrackIdxForName(n.i, n.trackIdx); 1321 const mappedI = resolvedIdx !== -1 && uniqueNames && uniqueNames[resolvedIdx] ? uniqueNames[resolvedIdx] : n.i; 1322 return { 1323 ...n, 1324 i: mappedI, 1325 trackIdx: resolvedIdx, 1326 id: `${n.t}-${n.n}-${i}` 1327 }; 1328 }); 1329 processedGameDataRef.current = finalFilteredNotes; 1330 setTotalNotes(finalFilteredNotes.length); 1331 }, [data, melodyTrackNames, melodyData, getTrackIdxForName, uniqueNames]); 1332 const renderPodiumLeaderboard = (leaderboardData) => { 1333 if (leaderboardData.length === 0) { 1334 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-center py-8 text-white/20 text-xs italic", children: "No scores yet" }); 1335 } 1336 const top3 = leaderboardData.slice(0, 3); 1337 const rest = leaderboardData.slice(3); 1338 const podiumOrder = []; 1339 if (top3[1]) podiumOrder.push({ ...top3[1], rank: 2 }); 1340 if (top3[0]) podiumOrder.push({ ...top3[0], rank: 1 }); 1341 if (top3[2]) podiumOrder.push({ ...top3[2], rank: 3 }); 1342 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col w-full h-full", children: [
1343 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center items-end gap-2 sm:gap-4 mb-4 mt-6 shrink-0 px-2", children: podiumOrder.map((player) => { 1344 const isFirst = player.rank === 1; 1345 const isSecond = player.rank === 2; 1346 const heightClass = isFirst ? "h-28 sm:h-36" : isSecond ? "h-20 sm:h-28" : "h-16 sm:h-24"; 1347 const avatarSize = isFirst ? "w-14 h-14 sm:w-16 sm:h-16" : isSecond ? "w-10 h-10 sm:w-12 sm:h-12" : "w-8 h-8 sm:w-10 sm:h-10"; 1348 const medal = isFirst ? "ð¥" : isSecond ? "ð¥" : "ð¥"; 1349 const glowClass = isFirst ? "shadow-[0_0_30px_rgba(250,204,21,0.2)] border-yellow-400/50 bg-gradient-to-t from-yellow-500/20 to-transparent" : isSecond ? "shadow-[0_0_20px_rgba(161,161,170,0.15)] border-zinc-300/50 bg-gradient-to-t from-zinc-400/20 to-transparent" : "shadow-[0_0_20px_rgba(251,146,60,0.15)] border-orange-400/50 bg-gradient-to-t from-orange-500/20 to-transparent"; 1350 const nameSize = isFirst ? "text-base sm:text-lg font-black" : "text-sm sm:text-base font-bold"; 1351 const scoreSize = isFirst ? "text-lg sm:text-xl" : "text-base sm:text-lg"; 1352 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex flex-col items-center justify-end flex-1 max-w-[110px] ${isFirst ? "z-10 -mt-6" : "z-0"}`, children: [
1353 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative mb-2", children: [ 1354 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `${avatarSize} rounded-full bg-zinc-800 flex items-center justify-center border-2 ${isFirst ? "border-yellow-400" : isSecond ? "border-zinc-300" : "border-orange-400"} overflow-hidden shadow-lg`, children: player.avatarUrl ? /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: player.avatarUrl, alt: player.userName || "Player", className: "w-full h-full object-cover" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-white/50 text-lg font-bold", children: (player.userName || "P").charAt(0).toUpperCase() }) }), 1355 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute ${isFirst ? "-top-3 -right-3 text-3xl" : "-top-2 -right-2 text-2xl"} drop-shadow-md filter`, children: medal }) 1356 ] }), 1357 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `w-full ${heightClass} ${glowClass} rounded-t-lg border-t-2 border-x border-x-white/5 flex flex-col items-center pt-2 px-1 ${player.isCurrentUser ? "bg-indigo-500/20 border-indigo-400/50" : ""}`, children: [
1358 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 justify-center w-full px-1", children: [ 1359 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-white truncate ${nameSize}`, title: player.userName, children: player.userName }), 1360 player.countryCode && /* @__PURE__ */ jsxRuntimeExports.jsx(GlobeFlag, { countryCode: player.countryCode, size: 14 }) 1361 ] }), 1362 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `font-mono font-bold text-emerald-400 mt-0.5 ${scoreSize}`, children: player.score.toLocaleString() }) 1363 ] }) 1364 ] }, player.id); 1365 }) }), 1366 rest.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto custom-scrollbar min-h-0 bg-black/40 rounded-lg p-1.5 border border-white/5", children: rest.map((entry, idx) => { 1367 const rank = idx + 4; 1368 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center justify-between px-3 py-1.5 hover:bg-white/5 rounded border-b border-white/5 last:border-0 ${entry.isCurrentUser ? "bg-indigo-500/20 border-indigo-500/30" : ""}`, children: [
1369 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 1370 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-xs font-bold w-4 text-center ${entry.isCurrentUser ? "text-indigo-300" : "text-white/30"}`, children: entry.rank || rank }), 1371 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 max-w-[140px]", children: [ 1372 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-sm font-medium truncate ${entry.isCurrentUser ? "text-indigo-200" : "text-white/80"}`, children: entry.userName }), 1373 entry.countryCode && /* @__PURE__ */ jsxRuntimeExports.jsx(GlobeFlag, { countryCode: entry.countryCode, size: 12 }) 1374 ] }) 1375 ] }), 1376 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-sm font-mono font-bold ${entry.isCurrentUser ? "text-emerald-400" : "text-emerald-500/80"}`, children: entry.score.toLocaleString() }) 1377 ] }, entry.id); 1378 }) }) 1379 ] }); 1380 }; 1381 const [hasSubmittedGuestScore, setHasSubmittedGuestScore] = reactExports.useState(false); 1382 const handleGameEnd = reactExports.useCallback(async () => { 1383 if (!trackId) return; 1384 try { 1385 const scores = await api.getHighscores(trackId, "piano-roll", userId); 1386 setLeaderboard(scores); 1387 } catch (e) { 1388 console.error(e); 1389 } 1390 if (userId && gameScoreRef.current > 0) { 1391 setIsSubmittingScore(true);
1392 try { 1393 await api.postHighscore(trackId, { 1394 userId, 1395 userName: userName || "Me", 1396 score: gameScoreRef.current, 1397 accuracy: hitNotesCountRef.current / totalNotes, 1398 maxCombo: maxComboRef.current 1399 }); 1400 const scores = await api.getHighscores(trackId, "piano-roll", userId); 1401 setLeaderboard(scores); 1402 } catch (e) { 1403 console.error(e); 1404 } 1405 setIsSubmittingScore(false); 1406 } 1407 }, [trackId, userId, userName, totalNotes]); 1408 const submitGuestScore = async () => { 1409 if (!trackId || gameScoreRef.current <= 0 || hasSubmittedGuestScore) return; 1410 setIsSubmittingScore(true); 1411 try { 1412 await api.postHighscore(trackId, { 1413 userName: "Community Contributor", 1414 score: gameScoreRef.current, 1415 accuracy: hitNotesCountRef.current / totalNotes, 1416 maxCombo: maxComboRef.current 1417 }); 1418 const scores = await api.getHighscores(trackId, "piano-roll", userId); 1419 setLeaderboard(scores); 1420 setHasSubmittedGuestScore(true); 1421 } catch (e) { 1422 console.error(e); 1423 } 1424 setIsSubmittingScore(false); 1425 }; 1426 reactExports.useEffect(() => { 1427 if (isPlayingRef.current && !isPlaying && currentTime > duration - 1.5) { 1428 setShowEndScreen(true); 1429 handleGameEnd(); 1430 } 1431 isPlayingRef.current = isPlaying; 1432 }, [isPlaying, currentTime, duration, handleGameEnd]); 1433 reactExports.useEffect(() => { 1434 if (userId && showEndScreen && gameScoreRef.current > 0 && !hasSubmittedGuestScore) { 1435 const isAlreadyInLeaderboard = leaderboard.some((l) => l.isCurrentUser); 1436 if (!isAlreadyInLeaderboard) { 1437 handleGameEnd(); 1438 } 1439 } 1440 }, [userId, showEndScreen, hasSubmittedGuestScore, leaderboard, handleGameEnd]); 1441 reactExports.useEffect(() => { 1442 if (!onGameModeMute) return; 1443 const checkForMuteUpdates = () => { 1444 let time = lastTimeRef.current; 1445 if (isPlayingRef.current) { 1446 time += (performance.now() - lastTickRef.current) / 1e3 * gameSpeedRef.current; 1447 } 1448 const gameData = processedGameDataRef.current; 1449 if (!gameData || gameData.length === 0) return; 1450 const activeTracks = /* @__PURE__ */ new Set(); 1451 const nextTracks = /* @__PURE__ */ new Set(); 1452 let nextTime = Infinity; 1453 let bestIdx = -1; 1454 for (const note of gameData) { 1455 const idx = note.trackIdx; 1456 if (idx === void 0 || idx === -1) continue; 1457 if (note.t <= time && note.t + note.d > time) { 1458 activeTracks.add(idx); 1459 if (bestIdx === -1) bestIdx = idx; 1460 } 1461 if (note.t > time) { 1462 if (nextTime === Infinity) nextTime = note.t; 1463 if (note.t === nextTime) { 1464 nextTracks.add(idx); 1465 } 1466 } 1467 if (nextTime !== Infinity && note.t > nextTime) break; 1468 } 1469 const finalMuteTracks = activeTracks.size > 0 ? activeTracks : nextTracks; 1470 if (bestIdx === -1 && nextTracks.size > 0) { 1471 bestIdx = Array.from(nextTracks)[0]; 1472 } 1473 const bestTrackIdx = bestIdx; 1474 const currentMuted = Array.from(melodyTrackIndicesRef.current); 1475 const newMuted = Array.from(finalMuteTracks); 1476 currentMuted.sort(); 1477 newMuted.sort(); 1478 const isDifferent = currentMuted.length !== newMuted.length || !newMuted.every((v, i) => v === currentMuted[i]); 1479 if (isDifferent || bestTrackIdx !== currentMelodyTrackRef.current) { 1480 setMelodyTrackIndices(finalMuteTracks); 1481 currentMelodyTrackRef.current = bestTrackIdx; 1482 setCurrentMelodyTrackIdx(bestTrackIdx); 1483 if (onGameModeMute) { 1484 console.log(`[PianoRollGame] Muting Melody Tracks:`, newMuted); 1485 onGameModeMute(newMuted); 1486 } 1487 if (onSelectTrack && bestTrackIdx !== -1) { 1488 onSelectTrack(bestTrackIdx); 1489 } 1490 } 1491 }; 1492 const interval = setInterval(checkForMuteUpdates, 100); 1493 return () => clearInterval(interval); 1494 }, [onGameModeMute, onSelectTrack, processedInstruments]); 1495 reactExports.useEffect(() => { 1496 }, [gameSpeed]); 1497 const { audioContextRef, playNote: basePlayNote, stopNote: baseStopNote, stopAllNotes, previewActiveNotesRef } = useSoundfontKeyboard(selectedTrackIndex, processedInstruments, currentMelodyTrackIdx, soundfont); 1498 const handleGameStart = reactExports.useCallback((force = false, startFromBeat) => { 1499 var _a; 1500 synth.getAudioContext().resume().catch(() => { 1501 }); 1502 if (((_a = audioContextRef.current) == null ? void 0 : _a.state) === "suspended") { 1503 audioContextRef.current.resume().catch(() => { 1504 }); 1505 } 1506 if (!isAudioLoaded) { 1507 setWaitingForAudio(true); 1508 if (!isPlaying && onPlayPause) { 1509 onPlayPause(true); 1510 } 1511 return; 1512 } 1513 if (isLoadingAudio) { 1514 setWaitingForAudio(true); 1515 return; 1516 } 1517 if (onPlayPause && (!isPlaying || force)) { 1518 if (!isGamePaused) generateBgTheme(); 1519 const startTime = targetStartTimeRef.current !== null ? targetStartTimeRef.current : currentTime; 1520 targetStartTimeRef.current = null; 1521 lastTimeRef.current = startTime; 1522 lastTickRef.current = performance.now(); 1523 const currentSpeed = gameSpeedRef.current; 1524 const currentTempo = (tempo || 120) * currentSpeed; 1525 const beatsPerMeasure = timeSignature && timeSignature.length > 0 ? timeSignature[0] : 4; 1526 const totalBeats = startFromBeat !== void 0 ? startFromBeat : beatsPerMeasure * 2; 1527 const beatDurationMs = 60 / currentTempo * 1e3; 1528 const totalDurationMs = totalBeats * beatDurationMs;
1529 setGameCountdown(totalBeats); 1530 countdownStartTimeRef.current = performance.now(); 1531 countdownDurationRef.current = totalDurationMs; 1532 hasCountedDownRef.current = true; 1533 setIsGamePaused(false); 1534 let count = totalBeats; 1535 const playTick = (isFirstBeatOfMeasure) => { 1536 if (!audioContextRef.current) return; 1537 const ctx = audioContextRef.current; 1538 if (ctx.state === "suspended") { 1539 ctx.resume().catch(() => { 1540 }); 1541 } 1542 const osc = ctx.createOscillator(); 1543 const gain = ctx.createGain(); 1544 osc.type = "sine"; 1545 osc.frequency.setValueAtTime(isFirstBeatOfMeasure ? 880 : 440, ctx.currentTime); 1546 gain.gain.setValueAtTime(0, ctx.currentTime); 1547 gain.gain.linearRampToValueAtTime(0.5, ctx.currentTime + 0.01); 1548 gain.gain.exponentialRampToValueAtTime(1e-3, ctx.currentTime + 0.1); 1549 osc.connect(gain); 1550 gain.connect(ctx.destination); 1551 osc.start(); 1552 osc.stop(ctx.currentTime + 0.1); 1553 }; 1554 if (startFromBeat === void 0) playTick(true); 1555 if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); 1556 if (isPlaying) { 1557 if (onPlayPause) onPlayPause(false); 1558 } 1559 countdownIntervalRef.current = setInterval(() => { 1560 count--; 1561 if (count > 0) {
1562 setGameCountdown(count); 1563 playTick(count % beatsPerMeasure === 0); 1564 } else if (count === 0) { 1565 setGameCountdown("GO!"); 1566 playTick(true); 1567 } else { 1568 if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); 1569 setGameCountdown(null); 1570 countdownStartTimeRef.current = null; 1571 onPlayPause(true); 1572 } 1573 }, beatDurationMs); 1574 } 1575 }, [isAudioLoaded, isLoadingAudio, isPlaying, onPlayPause, tempo, timeSignature, audioContextRef, currentTime, generateBgTheme, isGamePaused]); 1576 const togglePause = reactExports.useCallback(() => { 1577 if (!isPlaying && gameCountdown === null && !showEndScreen && !isGamePaused) { 1578 return; 1579 } 1580 if (gameCountdown !== null) { 1581 if (isGamePaused) { 1582 if (typeof gameCountdown === "number") { 1583 handleGameStart(true, gameCountdown); 1584 } else if (gameCountdown === "GO!") { 1585 handleGameStart(true, 0); 1586 } 1587 } else { 1588 setIsGamePaused(true); 1589 if (countdownIntervalRef.current) clearInterval(countdownIntervalRef.current); 1590 } 1591 } else { 1592 if (isPlaying) { 1593 if (onPlayPause) onPlayPause(false); 1594 setIsGamePaused(true); 1595 } else { 1596 if (onPlayPause) onPlayPause(true); 1597 setIsGamePaused(false); 1598 } 1599 } 1600 }, [gameCountdown, isGamePaused, isPlaying, onPlayPause, showEndScreen, handleGameStart]); 1601 const [waitingForAudio, setWaitingForAudio] = reactExports.useState(false); 1602 reactExports.useEffect(() => { 1603 if (isPlayingRef.current && !isPlaying && currentTime > 0.1 && gameCountdown === null && !showEndScreen) { 1604 setIsGamePaused(true); 1605 } else if (isPlaying && isGamePaused) { 1606 setIsGamePaused(false); 1607 } 1608 isPlayingRef.current = isPlaying; 1609 }, [isPlaying, currentTime, gameCountdown, showEndScreen, isGamePaused]); 1610 reactExports.useEffect(() => { 1611 if (waitingForAudio && isAudioLoaded && !isLoadingAudio) { 1612 setWaitingForAudio(false); 1613 handleGameStart(true); 1614 } 1615 }, [waitingForAudio, isAudioLoaded, isLoadingAudio, handleGameStart]); 1616 reactExports.useEffect(() => { 1617 if (isPlaying && currentTime < 0.1 && !hasCountedDownRef.current) { 1618 handleGameStart(true); 1619 } else if (!isPlaying && currentTime < 0.1 && gameCountdown === null) { 1620 hasCountedDownRef.current = false; 1621 } 1622 }, [isPlaying, currentTime, gameCountdown, handleGameStart]); 1623 const playNote = reactExports.useCallback((midi2) => { 1624 basePlayNote(midi2); 1625 const currentT = lastTimeRef.current; 1626 const threshold = 0.2; 1627 const upcomingNotes = processedGameDataRef.current.filter( 1628 (n) => n.n === midi2 && Math.abs(n.t - currentT) <= threshold 1629 ); 1630 if (upcomingNotes.length > 0) { 1631 upcomingNotes.sort((a, b) => Math.abs(a.t - currentT) - Math.abs(b.t - currentT)); 1632 const targetNote = upcomingNotes[0]; 1633 const noteId = `${targetNote.i}-${targetNote.n}-${targetNote.t}`; 1634 if (!hitNotesRef.current.has(noteId)) { 1635 hitNotesRef.current.add(noteId); 1636 const diff = Math.abs(targetNote.t - currentT); 1637 const points = diff < 0.05 ? 100 : diff < 0.1 ? 50 : 10; 1638 const isPerfect = diff < 0.05; 1639 gameComboRef.current += 1; 1640 if (gameComboRef.current > maxComboRef.current) { 1641 maxComboRef.current = gameComboRef.current; 1642 setMaxCombo(maxComboRef.current); 1643 }
1644 setGameCombo(gameComboRef.current); 1645 const comboMultiplier = Math.min(4, 1 + Math.floor(gameComboRef.current / 10)); 1646 const speedMultiplier = gameSpeedRef.current; 1647 gameScoreRef.current += Math.round(points * comboMultiplier * speedMultiplier); 1648 setGameScore(gameScoreRef.current); 1649 hitNotesCountRef.current += 1; 1650 setHitNotesCount(hitNotesCountRef.current); 1651 if (targetNote.d > 0.1) { 1652 activeHoldsRef.current.set(midi2, targetNote); 1653 } 1654 if (isPerfect) { 1655 if (canvasRef.current) { 1656 const rect = canvasRef.current.getBoundingClientRect(); 1657 const w = rect.width; 1658 const h = rect.height; 1659 const keyHeight = h * 0.15; 1660 const keyTop = h - keyHeight; 1661 const whiteKeyCount = 52; 1662 const keyWidth = w / whiteKeyCount; 1663 const blackKeyWidth = keyWidth * 0.65; 1664 const { x, w: kw } = getKeyGeometry(midi2, keyWidth, blackKeyWidth); 1665 const color = colorMap[targetNote.i] || "#ffffff"; 1666 for (let i = 0; i < 15; i++) { 1667 particlesRef.current.push({ 1668 x: x + kw / 2, 1669 y: keyTop, 1670 vx: (Math.random() - 0.5) * 10, 1671 vy: -(Math.random() * 10 + 5), 1672 size: Math.random() * 4 + 2, 1673 color, 1674 life: 1, 1675 decay: 0.03 + Math.random() * 0.05 1676 }); 1677 } 1678 } 1679 } 1680 } 1681 } else { 1682 if (gameComboRef.current > 0) { 1683 gameComboRef.current = 0;
1684 setGameCombo(0); 1685 } 1686 } 1687 }, [colorMap, basePlayNote]); 1688 const stopNote = reactExports.useCallback((midi2) => { 1689 baseStopNote(midi2); 1690 const currentT = lastTimeRef.current; 1691 if (activeHoldsRef.current.has(midi2)) { 1692 const targetNote = activeHoldsRef.current.get(midi2); 1693 activeHoldsRef.current.delete(midi2); 1694 const noteEndTime = targetNote.t + targetNote.d; 1695 const diff = Math.abs(noteEndTime - currentT); 1696 if (diff < 0.2) { 1697 const points = diff < 0.05 ? 50 : 25; 1698 const comboMultiplier = Math.min(4, 1 + Math.floor(gameComboRef.current / 10)); 1699 const speedMultiplier = gameSpeedRef.current; 1700 gameScoreRef.current += Math.round(points * comboMultiplier * speedMultiplier); 1701 setGameScore(gameScoreRef.current); 1702 if (diff < 0.05 && canvasRef.current) { 1703 const rect = canvasRef.current.getBoundingClientRect(); 1704 const w = rect.width; 1705 const h = rect.height; 1706 const keyHeight = h * 0.15; 1707 const keyTop = h - keyHeight; 1708 const whiteKeyCount = 52; 1709 const keyWidth = w / whiteKeyCount; 1710 const blackKeyWidth = keyWidth * 0.65; 1711 const { x, w: kw } = getKeyGeometry(midi2, keyWidth, blackKeyWidth); 1712 const color = colorMap[targetNote.i] || "#ffffff"; 1713 for (let i = 0; i < 8; i++) { 1714 particlesRef.current.push({ 1715 x: x + kw / 2, 1716 y: keyTop, 1717 vx: (Math.random() - 0.5) * 6, 1718 vy: -(Math.random() * 6 + 3), 1719 size: Math.random() * 3 + 1, 1720 color, 1721 life: 1, 1722 decay: 0.04 + Math.random() * 0.05 1723 }); 1724 } 1725 } 1726 } else if (currentT < noteEndTime - 0.2 || currentT > noteEndTime + 0.2) { 1727 if (gameComboRef.current > 0) { 1728 gameComboRef.current = 0;
1729 setGameCombo(0); 1730 } 1731 } 1732 } 1733 }, [colorMap, baseStopNote]); 1734 const baseOctaveRef = reactExports.useRef(4); 1735 const [, setBaseOctave] = reactExports.useState(4); 1736 const activeKeysRef = reactExports.useRef(/* @__PURE__ */ new Map()); 1737 const [keyboardLabelMode, setKeyboardLabelMode] = reactExports.useState("pc"); 1738 const keyboardLabelModeRef = reactExports.useRef("pc"); 1739 reactExports.useEffect(() => { 1740 keyboardLabelModeRef.current = keyboardLabelMode; 1741 }, [keyboardLabelMode]); 1742 const midi = useKeyboardInput({ 1743 playNote, 1744 stopNote, 1745 baseOctaveRef, 1746 activeKeysRef, 1747 onOctaveChange: setBaseOctave 1748 }); 1749 const handlePointerDown = (e) => { 1750 if (!canvasRef.current) return; 1751 if (!isPlaying && gameCountdown === null && !showEndScreen && !isGamePaused) { 1752 return; 1753 } 1754 const rect = canvasRef.current.getBoundingClientRect(); 1755 const y = e.clientY - rect.top; 1756 const h = rect.height; 1757 const keyHeight = h * 0.15; 1758 const keyTop = h - keyHeight; 1759 if (y >= keyTop) { 1760 e.stopPropagation(); 1761 const x = e.clientX - rect.left; 1762 const w = rect.width; 1763 const midi2 = getMidiFromKeyboardClick(x, y - keyTop, w, keyHeight); 1764 if (midi2 !== null) { 1765 playNote(midi2); 1766 } 1767 } 1768 }; 1769 const handlePointerUp = () => { 1770 stopAllNotes(); 1771 }; 1772 const handlePointerLeave = () => { 1773 stopAllNotes(); 1774 }; 1775 const [uiScale, setUiScale] = reactExports.useState(1); 1776 reactExports.useEffect(() => { 1777 if (!containerRef.current || !canvasRef.current) return; 1778 const updateDim = () => { 1779 if (containerRef.current && canvasRef.current) { 1780 const { width, height } = containerRef.current.getBoundingClientRect(); 1781 const dpr = window.devicePixelRatio || 1; 1782 canvasRef.current.width = width * dpr; 1783 canvasRef.current.height = height * dpr; 1784 const ctx = canvasRef.current.getContext("2d"); 1785 if (ctx) ctx.scale(dpr, dpr); 1786 canvasRef.current.style.width = `${width}px`; 1787 canvasRef.current.style.height = `${height}px`; 1788 const baseW = 800; 1789 const baseH = 600; 1790 const scaleW = width / baseW; 1791 const scaleH = height / baseH; 1792 setUiScale(Math.min(1, Math.max(0.3, Math.min(scaleW, scaleH)))); 1793 } 1794 }; 1795 updateDim(); 1796 const ro = new ResizeObserver(updateDim); 1797 ro.observe(containerRef.current); 1798 return () => ro.disconnect(); 1799 }, []); 1800 const animateRef = reactExports.useRef(() => { 1801 }); 1802 const animate = () => { 1803 if (!canvasRef.current) return; 1804 const currentSpeed = gameSpeedRef.current; 1805 let visualTime = lastTimeRef.current; 1806 if (isPlaying) { 1807 visualTime += (performance.now() - lastTickRef.current) / 1e3 * currentSpeed; 1808 } else if (countdownStartTimeRef.current !== null && countdownDurationRef.current > 0) { 1809 const elapsed = performance.now() - countdownStartTimeRef.current; 1810 const remaining = Math.max(0, countdownDurationRef.current - elapsed); 1811 const targetTime = lastTimeRef.current; 1812 visualTime = Math.max(0, targetTime - remaining / 1e3 * currentSpeed); 1813 } else { 1814 visualTime = lastTimeRef.current; 1815 } 1816 const ctx = canvasRef.current.getContext("2d", { alpha: false }); 1817 if (ctx) { 1818 const { width, height } = canvasRef.current; 1819 renderScene(ctx, width, height, visualTime); 1820 } 1821 }; 1822 reactExports.useEffect(() => { 1823 animateRef.current = animate; 1824 }); 1825 reactExports.useEffect(() => { 1826 if (starFieldRef.current.length === 0) { 1827 for (let i = 0; i < 80; i++) { 1828 starFieldRef.current.push({ 1829 x: Math.random(), 1830 y: Math.random(), 1831 size: Math.random() * 1.5 + 0.5, 1832 speed: Math.random() * 0.05 + 0.01 1833 }); 1834 } 1835 } 1836 let lastFrameTime = 0; 1837 const isSlow = isSlowMobile(); 1838 const fpsInterval = isSlow ? 1e3 / 30 : 0; 1839 const loop = (timestamp) => { 1840 if (fpsInterval > 0) { 1841 const elapsed = timestamp - lastFrameTime; 1842 if (elapsed >= fpsInterval) { 1843 lastFrameTime = timestamp - elapsed % fpsInterval; 1844 animateRef.current(); 1845 } 1846 } else { 1847 animateRef.current(); 1848 } 1849 requestRef.current = requestAnimationFrame(loop); 1850 }; 1851 requestRef.current = requestAnimationFrame(loop); 1852 return () => { 1853 if (requestRef.current) cancelAnimationFrame(requestRef.current); 1854 }; 1855 }, []); 1856 const renderScene = (ctx, rawWidth, rawHeight, t) => { 1857 const dpr = window.devicePixelRatio || 1; 1858 const width = rawWidth / dpr; 1859 const fullHeight = rawHeight / dpr; 1860 const height = fullHeight; 1861 const effectiveTime = t + CONFIG.visualOffset;
1862 const keyHeight = height * 0.15; 1863 const keyTop = height - keyHeight; 1864 const whiteKeyCount = 52; 1865 const keyWidth = width / whiteKeyCount; 1866 const blackKeyWidth = keyWidth * 0.65; 1867 const gradient = ctx.createRadialGradient(width / 2, height / 2, 0, width / 2, height / 2, Math.max(width, height)); 1868 gradient.addColorStop(0, bgTheme.color2); 1869 gradient.addColorStop(1, bgTheme.color1); 1870 ctx.fillStyle = gradient; 1871 ctx.fillRect(0, 0, width, fullHeight); 1872 ctx.fillStyle = "rgba(255, 255, 255, 0.4)"; 1873 starFieldRef.current.forEach((star) => { 1874 star.y -= star.speed * (isPlaying ? 1 : 0) * gameSpeedRef.current; 1875 if (star.y < 0) star.y = 1; 1876 ctx.beginPath(); 1877 ctx.arc(star.x * width, star.y * height, star.size, 0, Math.PI * 2); 1878 ctx.fill(); 1879 }); 1880 ctx.strokeStyle = "rgba(255, 255, 255, 0.03)"; 1881 ctx.lineWidth = 1; 1882 for (let x = 0; x < width; x += 50) { 1883 ctx.beginPath(); 1884 ctx.moveTo(x, 0); 1885 ctx.lineTo(x, fullHeight); 1886 ctx.stroke(); 1887 } 1888 for (let y = 0; y < fullHeight; y += 50) { 1889 ctx.beginPath(); 1890 ctx.moveTo(0, y); 1891 ctx.lineTo(width, y); 1892 ctx.stroke(); 1893 } 1894 const activeNotes = /* @__PURE__ */ new Set(); 1895 previewActiveNotesRef.current.forEach((midi2) => activeNotes.add(midi2)); 1896 if (isPlaying) { 1897 activeNotes.forEach((midi2) => { 1898 if (!prevActiveNotesRef.current.has(midi2)) { 1899 const { x, w } = getKeyGeometry(midi2, keyWidth, blackKeyWidth); 1900 const color = "#ffffff"; 1901 for (let i = 0; i < 8; i++) { 1902 particlesRef.current.push({ 1903 x: x + w / 2 + (Math.random() - 0.5) * w, 1904 y: keyTop, 1905 vx: (Math.random() - 0.5) * 4, 1906 vy: -(Math.random() * 5 + 2), 1907 size: Math.random() * 3 + 1, 1908 color, 1909 life: 1, 1910 decay: 0.02 + Math.random() * 0.03 1911 }); 1912 } 1913 } 1914 }); 1915 } 1916 prevActiveNotesRef.current = activeNotes; 1917 for (let i = particlesRef.current.length - 1; i >= 0; i--) { 1918 const p = particlesRef.current[i]; 1919 p.x += p.vx; 1920 p.y += p.vy; 1921 p.life -= p.decay; 1922 p.vy += 0.1; 1923 if (p.life <= 0) { 1924 particlesRef.current.splice(i, 1); 1925 continue; 1926 } 1927 ctx.globalCompositeOperation = "lighter"; 1928 ctx.fillStyle = p.color; 1929 ctx.globalAlpha = p.life; 1930 ctx.beginPath(); 1931 ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); 1932 ctx.fill(); 1933 ctx.globalAlpha = 1; 1934 } 1935 const gameData = processedGameDataRef.current || []; 1936 const finalVisibleNotes = []; 1937 const activeGameNotes = /* @__PURE__ */ new Set(); 1938 let left = 0; 1939 let right = gameData.length - 1; 1940 let startIdx = 0; 1941 const searchTime = Math.max(0, effectiveTime - 10); 1942 while (left <= right) { 1943 const mid = Math.floor((left + right) / 2); 1944 if (gameData[mid].t < searchTime) { 1945 left = mid + 1; 1946 startIdx = left; 1947 } else { 1948 right = mid - 1; 1949 } 1950 } 1951 for (let i = startIdx; i < gameData.length; i++) { 1952 const note = gameData[i]; 1953 if (note.t > effectiveTime + CONFIG.lookAheadTime) break; 1954 const timeStart = note.t; 1955 const timeEnd = note.t + note.d; 1956 if (effectiveTime >= timeStart && effectiveTime <= timeEnd) activeGameNotes.add(note.n); 1957 if (timeEnd > effectiveTime) finalVisibleNotes.push(note); 1958 } 1959 const missThreshold = 0.2; 1960 const checkStart = effectiveTime - 1; 1961 const checkEnd = effectiveTime; 1962 let leftMiss = 0; 1963 let rightMiss = gameData.length - 1; 1964 let startMissIdx = 0; 1965 while (leftMiss <= rightMiss) { 1966 const mid = Math.floor((leftMiss + rightMiss) / 2); 1967 if (gameData[mid].t < checkStart) { 1968 leftMiss = mid + 1; 1969 startMissIdx = leftMiss; 1970 } else { 1971 rightMiss = mid - 1; 1972 } 1973 } 1974 for (let i = startMissIdx; i < gameData.length; i++) { 1975 const note = gameData[i]; 1976 if (note.t > checkEnd) break; 1977 const noteId = `${note.i}-${note.n}-${note.t}`; 1978 if (!hitNotesRef.current.has(noteId) && !missedNotesRef.current.has(noteId)) { 1979 if (effectiveTime > note.t + missThreshold) { 1980 missedNotesRef.current.add(noteId); 1981 if (gameComboRef.current > 0) { 1982 gameComboRef.current = 0;
1983 setGameCombo(0); 1984 } 1985 } 1986 } 1987 } 1988 for (const [midi2, note] of activeHoldsRef.current.entries()) { 1989 if (effectiveTime > note.t + note.d + missThreshold) { 1990 activeHoldsRef.current.delete(midi2); 1991 if (gameComboRef.current > 0) { 1992 gameComboRef.current = 0; 1993 setGameCombo(0); 1994 } 1995 } 1996 } 1997 let minPitch = 128; 1998 let maxPitch = 0; 1999 let hasUpcoming = false; 2000 for (const note of finalVisibleNotes) { 2001 if (note.t > effectiveTime && note.t < effectiveTime + 1.5) { 2002 if (note.n < minPitch) minPitch = note.n; 2003 if (note.n > maxPitch) maxPitch = note.n; 2004 hasUpcoming = true; 2005 } 2006 } 2007 if (hasUpcoming) { 2008 const baseOct = baseOctaveRef.current; 2009 const rangeStart = baseOct * 12; 2010 const rangeEnd = baseOct * 12 + 31; 2011 if (minPitch < rangeStart) { 2012 setOctaveTip("Press â or Z for Lower Octave"); 2013 } else if (maxPitch > rangeEnd) { 2014 setOctaveTip("Press â or X for Higher Octave"); 2015 } else { 2016 setOctaveTip(null); 2017 } 2018 } else { 2019 setOctaveTip(null); 2020 } 2021 ctx.save(); 2022 ctx.globalCompositeOperation = "source-over"; 2023 const horizonY = height * 0.1; 2024 const bottomY = keyTop; 2025 const centerX = width / 2; 2026 ctx.beginPath(); 2027 ctx.moveTo(centerX - width * 0.1, horizonY); 2028 ctx.lineTo(width, bottomY); 2029 ctx.lineTo(0, bottomY); 2030 ctx.lineTo(centerX + width * 0.1, horizonY); 2031 ctx.closePath(); 2032 ctx.strokeStyle = "rgba(255, 255, 255, 0.1)"; 2033 ctx.lineWidth = 1; 2034 const lanes = 12; 2035 for (let i = 0; i <= lanes; i++) { 2036 const t2 = i / lanes; 2037 const topX = centerX - width * 0.1 + width * 0.2 * t2; 2038 const bottomX = width * t2; 2039 ctx.beginPath(); 2040 ctx.moveTo(topX, horizonY); 2041 ctx.lineTo(bottomX, bottomY); 2042 ctx.stroke(); 2043 } 2044 const beatSpacing = 1; 2045 const timeOffset = effectiveTime % beatSpacing; 2046 for (let i = 0; i < 10; i++) { 2047 const lineTime = i * beatSpacing - timeOffset; 2048 if (lineTime < 0) continue; 2049 const progress = lineTime / CONFIG.lookAheadTime; 2050 if (progress > 1) continue; 2051 const y = bottomY - (bottomY - horizonY) * progress; 2052 const widthAtY = width * (1 - 0.8 * progress); 2053 const xLeft = (width - widthAtY) / 2; 2054 ctx.beginPath(); 2055 ctx.moveTo(xLeft, y); 2056 ctx.lineTo(xLeft + widthAtY, y); 2057 ctx.stroke(); 2058 } 2059 const getPerspective = (p) => { 2060 const clampedP = Math.max(0, Math.min(1, p)); 2061 const y = bottomY - (bottomY - horizonY) * clampedP; 2062 const scale = 1 - 0.7 * clampedP; 2063 return { y, scale }; 2064 }; 2065 const gameDataToRender = processedGameDataRef.current || []; 2066 let leftRender = 0; 2067 let rightRender = gameDataToRender.length - 1; 2068 let startRenderIdx = 0; 2069 const searchRenderTime = effectiveTime - 0.2; 2070 while (leftRender <= rightRender) { 2071 const mid = Math.floor((leftRender + rightRender) / 2); 2072 if (gameDataToRender[mid].t < searchRenderTime) { 2073 leftRender = mid + 1; 2074 startRenderIdx = leftRender; 2075 } else { 2076 rightRender = mid - 1; 2077 } 2078 } 2079 for (let idx = startRenderIdx; idx < gameDataToRender.length; idx++) { 2080 const note = gameDataToRender[idx]; 2081 const distFromPlayhead = note.t - effectiveTime; 2082 const progressStart = distFromPlayhead / CONFIG.lookAheadTime; 2083 if (progressStart > 1) break; 2084 const progressEnd = (distFromPlayhead + note.d) / CONFIG.lookAheadTime; 2085 const start = getPerspective(progressStart); 2086 const end = getPerspective(progressEnd); 2087 const { x: keyX, w: keyW } = getKeyGeometry(note.n, keyWidth, blackKeyWidth); 2088 const keyCenterX = keyX + keyW / 2; 2089 const distFromCenter = keyCenterX - width / 2; 2090 const x1 = width / 2 + distFromCenter * start.scale; 2091 const w1 = keyW * start.scale; 2092 const x2 = width / 2 + distFromCenter * end.scale; 2093 const w2 = keyW * end.scale; 2094 const isHeld = activeHoldsRef.current.has(note.n) && activeHoldsRef.current.get(note.n).t === note.t; 2095 const defaultColor = colorMap[note.i] || "#ffffff"; 2096 let baseColor = defaultColor; 2097 let sideColor = defaultColor; 2098 if (isHeld) { 2099 baseColor = "#ffffff"; 2100 sideColor = "#cccccc"; 2101 if (Math.random() < 0.3) { 2102 particlesRef.current.push({ 2103 x: x1 + (Math.random() - 0.5) * w1, 2104 y: start.y, 2105 vx: (Math.random() - 0.5) * 2, 2106 vy: -(Math.random() * 4 + 2), 2107 size: Math.random() * 2 + 1, 2108 color: baseColor, 2109 life: 1, 2110 decay: 0.05 + Math.random() * 0.05 2111 }); 2112 } 2113 } 2114 const thickness = 15 * start.scale; 2115 ctx.fillStyle = sideColor; 2116 ctx.beginPath(); 2117 ctx.moveTo(x1 - w1 / 2, start.y); 2118 ctx.lineTo(x1 + w1 / 2, start.y); 2119 ctx.lineTo(x1 + w1 / 2, start.y + thickness); 2120 ctx.lineTo(x1 - w1 / 2, start.y + thickness); 2121 ctx.closePath(); 2122 ctx.fill(); 2123 ctx.fillStyle = baseColor; 2124 ctx.shadowColor = baseColor; 2125 ctx.shadowBlur = isHeld ? 20 : 10; 2126 ctx.beginPath(); 2127 ctx.moveTo(x1 - w1 / 2, start.y); 2128 ctx.lineTo(x1 + w1 / 2, start.y); 2129 ctx.lineTo(x2 + w2 / 2, end.y); 2130 ctx.lineTo(x2 - w2 / 2, end.y); 2131 ctx.closePath(); 2132 ctx.fill(); 2133 ctx.shadowBlur = 0; 2134 ctx.globalAlpha = 1; 2135 ctx.strokeStyle = "rgba(255,255,255,0.3)"; 2136 ctx.lineWidth = 1; 2137 ctx.beginPath(); 2138 ctx.moveTo(x1 - w1 / 2, start.y); 2139 ctx.lineTo(x2 - w2 / 2, end.y); 2140 ctx.stroke(); 2141 const baseOct = baseOctaveRef.current; 2142 const label = getPcKeyLabel(note.n, baseOct); 2143 if (label && start.scale > 0.2) { 2144 const fontSize = Math.max(10, 24 * start.scale); 2145 ctx.font = `900 ${fontSize}px "Arial Black", sans-serif`; 2146 ctx.textAlign = "center"; 2147 ctx.textBaseline = "middle"; 2148 const labelY = start.y - (start.y - end.y) * 0.2; 2149 ctx.lineWidth = 6 * start.scale; 2150 ctx.lineJoin = "round"; 2151 ctx.strokeStyle = "#000000"; 2152 ctx.strokeText(label, x1, labelY); 2153 ctx.fillStyle = "#ffffff"; 2154 ctx.fillText(label, x1, labelY); 2155 } 2156 }
2157 ctx.restore(); 2158 ctx.globalCompositeOperation = "source-over"; 2159 renderSharedKeyboard(ctx, width, height, keyHeight, activeNotes, keyboardLabelModeRef.current, baseOctaveRef.current); 2160 }; 2161 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: containerRef, className: "w-full h-full bg-[#050505] relative @container", children: [ 2162 /* @__PURE__ */ jsxRuntimeExports.jsx( 2163 "canvas", 2164 { 2165 ref: canvasRef, 2166 className: "w-full h-full block touch-none cursor-pointer", 2167 onClick: (e) => { 2168 if (!isPlaying && gameCountdown === null && !showEndScreen && !isGamePaused) { 2169 e.stopPropagation(); 2170 } 2171 }, 2172 onPointerDown: (e) => { 2173 var _a; 2174 const rect = (_a = canvasRef.current) == null ? void 0 : _a.getBoundingClientRect(); 2175 if (rect) { 2176 const y = e.clientY - rect.top; 2177 const h = rect.height; 2178 const keyHeight = h * 0.15; 2179 const keyTop = h - keyHeight; 2180 if (y >= keyTop) { 2181 handlePointerDown(e); 2182 } else { 2183 togglePause(); 2184 } 2185 } 2186 }, 2187 onPointerUp: handlePointerUp, 2188 onPointerLeave: handlePointerLeave, 2189 onPointerCancel: handlePointerLeave 2190 } 2191 ), 2192 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2193 "div", 2194 { 2195 className: "absolute top-4 right-4 flex flex-col gap-2 pointer-events-none select-none z-40 items-end origin-top-right", 2196 style: { transform: `scale(${uiScale})` }, 2197 children: [
2198 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-black/60 backdrop-blur-md text-white px-4 py-2 rounded border border-white/10 text-right shrink-0", children: [ 2199 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-white/60 font-mono", children: "SCORE" }), 2200 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-2xl font-bold font-mono", children: gameScore.toLocaleString() }) 2201 ] }), 2202 gameCombo > 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-black/60 backdrop-blur-md text-white px-4 py-2 rounded border border-white/10 animate-bounce text-right shrink-0", children: [ 2203 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-emerald-400 font-bold font-mono", children: "COMBO" }), 2204 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-3xl font-black font-mono italic text-emerald-400", children: [ 2205 gameCombo, 2206 "x" 2207 ] }) 2208 ] }), 2209 gameCombo >
2209= 10 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-black/60 backdrop-blur-md text-white px-4 py-2 rounded border border-white/10 text-right shrink-0", children: [ 2210 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-yellow-400 font-mono", children: "MULTIPLIER" }), 2211 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xl font-bold text-yellow-400", children: [ 2212 Math.min(4, 1 + Math.floor(gameCombo / 10)), 2213 "x" 2214 ] }) 2215 ] }), 2216 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2217 "div", 2218 { 2219 className: "bg-black/60 backdrop-blur-md text-white p-2 rounded border border-white/10 pointer-events-auto flex flex-col items-center gap-2 mt-auto", 2220 onClick: (e) => e.stopPropagation(), 2221 children: [ 2222 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-white/60 font-mono uppercase", style: { writingMode: "vertical-rl", textOrientation: "mixed" }, children: "Speed" }),
2223 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-32 flex items-center justify-center py-2 relative w-6", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 2224 "input", 2225 { 2226 type: "range", 2227 min: "0.5", 2228 max: "1.2", 2229 step: "0.1", 2230 value: gameSpeed, 2231 onChange: (e) => setGameSpeed(parseFloat(e.target.value)), 2232 className: "absolute w-32 h-2 bg-white/20 rounded-full outline-none cursor-pointer", 2233 style: { 2234 transform: "rotate(-90deg)", 2235 transformOrigin: "center", 2236 top: "50%", 2237 left: "50%", 2238 marginTop: "-4px", 2239 marginLeft: "-64px" 2240 } 2241 } 2242 ) }), 2243 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs font-bold font-mono", children: [ 2244 gameSpeed.toFixed(1), 2245 "x" 2246 ] }),
2247 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[9px] text-emerald-400 font-mono", children: [ 2248 Math.round(gameSpeed * 100), 2249 "%" 2250 ] }) 2251 ] 2252 } 2253 ) 2254 ] 2255 } 2256 ), 2257 isGamePaused && gameCountdown !== null && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex items-center justify-center z-50 bg-black/40 pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 2258 "div", 2259 { 2260 className: "text-4xl font-black text-white tracking-widest drop-shadow-[0_0_20px_rgba(0,0,0,0.8)] animate-pulse", 2261 style: { transform: `scale(${uiScale})` }, 2262 children: "PAUSED" 2263 } 2264 ) }), 2265 (!isPlaying && gameCountdown === null && !showEndScreen && !isGamePaused || isGamePaused && gameCountdown === null) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex flex-col items-center justify-center z-30 bg-black/60 p-4", children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 2266 "div", 2267 { 2268 className: "flex flex-row items-stretch justify-center gap-8 md:gap-16 w-full max-w-6xl h-[70vh]", 2269 style: { transform: `scale(${uiScale})` }, 2270 children: [
2271 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pointer-events-auto bg-black/60 border border-white/10 rounded-xl p-6 w-1/2 overflow-hidden flex flex-col min-h-0 shadow-2xl", children: [ 2272 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-sm font-bold text-white/40 uppercase tracking-widest mb-4 text-center shrink-0", children: "Top Players" }), 2273 leaderboard.length > 0 ? renderPodiumLeaderboard(leaderboard) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex items-center justify-center text-white/20 italic text-sm", children: "No scores yet" }) 2274 ] }), 2275 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center gap-8 w-1/2 bg-white/5 border border-white/10 rounded-xl p-8 shadow-2xl", children: [ 2276 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-center", children: [ 2277 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-4xl font-black text-white mb-2 tracking-tighter", children: isGamePaused ? "GAME PAUSED" : "PIANO ROLL CHALLENGE" }),
2278 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-white/40 text-sm font-medium tracking-widest uppercase", children: isGamePaused ? "Ready to continue?" : "Test your skills" }) 2279 ] }), 2280 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-4", children: [ 2281 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2282 "button", 2283 { 2284 onClick: () => { 2285 if (isGamePaused) { 2286 handleGameStart(); 2287 } else { 2288 resetGame(); 2289 targetStartTimeRef.current = 0; 2290 handleGameStart(true); 2291 } 2292 }, 2293 disabled: waitingForAudio, 2294 className: `pointer-events-auto shrink-0 group relative px-16 py-8 ${waitingForAudio ? "bg-zinc-600" : "bg-emerald-500 hover:bg-emerald-400"} text-white font-black text-4xl tracking-wider rounded-2xl shadow-[0_0_50px_rgba(16,185,129,0.3)] hover:shadow-[0_0_80px_rgba(16,185,129,0.5)] transform hover:scale-105 transition-all border-2 ${waitingForAudio ? "border-zinc-500" : "border-emerald-300"}`, 2295 children: [
2296 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "drop-shadow-lg", children: waitingForAudio ? "LOADING..." : isGamePaused ? "RESUME" : "START" }), 2297 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 rounded-2xl ring-4 ring-white/20 group-hover:ring-white/40 transition-all" }) 2298 ] 2299 } 2300 ), 2301 isGamePaused && /* @__PURE__ */ jsxRuntimeExports.jsx( 2302 "button", 2303 { 2304 onClick: () => { 2305 resetGame(); 2306 targetStartTimeRef.current = 0; 2307 handleGameStart(true); 2308 }, 2309 className: "pointer-events-auto text-white/60 hover:text-white font-bold text-sm tracking-widest uppercase transition-colors", 2310 children: "Restart" 2311 } 2312 ), 2313 /* @__PURE__ */ jsxRuntimeExports.jsx( 2314 "button", 2315 { 2316 onClick: () => toggleGameMode(), 2317 className: "pointer-events-auto text-white/40 hover:text-white/80 font-medium text-xs tracking-widest uppercase transition-colors mt-2", 2318 children: "Back to playback mode" 2319 } 2320 ) 2321 ] }),
2322 !isGamePaused && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-2", children: [ 2323 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-white/30 text-[10px] font-bold tracking-[0.3em] uppercase", children: "Controls" }), 2324 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-4", children: [ 2325 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", children: [ 2326 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-white/10 px-2 py-1 rounded text-[10px] text-white font-mono mb-1", children: "SPACE" }), 2327 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[8px] text-white/40 uppercase", children: "Start/Pause" }) 2328 ] }), 2329 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center", children: [ 2330 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-white/10 px-2 py-1 rounded text-[10px] text-white font-mono mb-1", children: "Z/X or ARROWS" }),
2331 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[8px] text-white/40 uppercase", children: "Octave" }) 2332 ] }) 2333 ] }) 2334 ] }) 2335 ] }) 2336 ] 2337 } 2338 ) }), 2339 showEndScreen && /* @__PURE__ */ jsxRuntimeExports.jsx( 2340 "div", 2341 { 2342 className: "absolute inset-0 bg-black/80 backdrop-blur-xl z-50 flex items-center justify-center p-4", 2343 onClick: () => { 2344 setShowEndScreen(false); 2345 toggleGameMode(); 2346 }, 2347 children: /* @__PURE__ */ jsxRuntimeExports.jsxs( 2348 "div", 2349 { 2350 className: "bg-[#111] border border-white/10 rounded-2xl p-8 max-w-5xl w-full shadow-2xl transform transition-all flex flex-col", 2351 style: { 2352 maxHeight: "90vh", 2353 transform: `scale(${uiScale})` 2354 }, 2355 onClick: (e) => e.stopPropagation(), 2356 children: [ 2357 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-3xl font-black text-white mb-8 text-center shrink-0 tracking-tighter", children: "SONG COMPLETE" }),
2358 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-row gap-8 flex-1 min-h-0", children: [ 2359 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-1/2 flex flex-col min-h-0 bg-black/40 rounded-xl border border-white/5 p-6", children: [ 2360 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-xs font-bold text-white/40 uppercase tracking-widest mb-4 text-center shrink-0", children: "Leaderboard" }), 2361 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0", children: renderPodiumLeaderboard(leaderboard) }) 2362 ] }), 2363 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-1/2 flex flex-col justify-between", children: [ 2364 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 2365 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center mb-8", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `text-9xl leading-none font-black ${hitNotesCount / totalNotes > 0.95 ? "text-emerald-400 drop-shadow-[0_0_30px_rgba(52,211,153,0.6)]" : hitNotesCount / totalNotes > 0.9 ? "text-blue-400" : hitNotesCount / totalNotes > 0.8 ? "text-yellow-400" : hitNotesCount / totalNotes > 0.7 ? "text-orange-400" : "text-red-400"}`, children: hitNotesCount / totalNotes > 0.95 ? "S" : hitNotesCount / totalNotes > 0.9 ? "A" : hitNotesCount / totalNotes > 0.8 ? "B" : hitNotesCount / totalNotes > 0.7 ? "C" : "D" }) }),
2366 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-4 mb-8", children: [ 2367 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white/5 p-4 rounded-xl border border-white/5", children: [ 2368 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-white/50 uppercase font-bold tracking-wider mb-1", children: "Final Score" }), 2369 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-2xl font-black text-white font-mono", children: gameScore.toLocaleString() }) 2370 ] }), 2371 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white/5 p-4 rounded-xl border border-white/5", children: [ 2372 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-white/50 uppercase font-bold tracking-wider mb-1", children: "Max Combo" }), 2373 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-2xl font-black text-white font-mono", children: maxCombo }) 2374 ] }),
2375 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white/5 p-4 rounded-xl border border-white/5", children: [ 2376 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-white/50 uppercase font-bold tracking-wider mb-1", children: "Accuracy" }), 2377 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-2xl font-black text-white font-mono", children: [ 2378 totalNotes > 0 ? Math.round(hitNotesCount / totalNotes * 100) : 0, 2379 "%" 2380 ] }) 2381 ] }), 2382 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white/5 p-4 rounded-xl border border-white/5", children: [ 2383 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-white/50 uppercase font-bold tracking-wider mb-1", children: "Notes Hit" }), 2384 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-2xl font-black text-white font-mono", children: [ 2385 hitNotesCount, 2386 " ",
2387 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-white/40 text-sm font-bold", children: [ 2388 "/ ", 2389 totalNotes 2390 ] }) 2391 ] }) 2392 ] }) 2393 ] }), 2394 !userId && !hasSubmittedGuestScore && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-indigo-500/10 border border-indigo-500/20 rounded-xl p-4 mb-8 text-center", children: [ 2395 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-indigo-300 font-medium mb-3", children: "Log in to save your score and compete!" }), 2396 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-center gap-4", children: [ 2397 /* @__PURE__ */ jsxRuntimeExports.jsx( 2398 "button", 2399 { 2400 onClick: () => { 2401 if (onAuthRequested) onAuthRequested(); 2402 else window.location.href = "/login"; 2403 }, 2404 className: "px-4 py-2 bg-indigo-500 hover:bg-indigo-400 text-white text-xs font-bold rounded-lg transition-colors", 2405 children: "Login / Sign Up" 2406 } 2407 ),
2408 /* @__PURE__ */ jsxRuntimeExports.jsx( 2409 "button", 2410 { 2411 onClick: submitGuestScore, 2412 className: "px-4 py-2 bg-white/10 hover:bg-white/20 text-white text-xs font-bold rounded-lg transition-colors", 2413 children: "Save as Contributor" 2414 } 2415 ) 2416 ] }) 2417 ] }), 2418 !userId && hasSubmittedGuestScore && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-emerald-500/10 border border-emerald-500/20 rounded-xl p-4 mb-8 text-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-emerald-300 font-medium", children: "Score saved as Community Contributor!" }) }) 2419 ] }), 2420 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-4", children: [ 2421 /* @__PURE__ */ jsxRuntimeExports.jsx( 2422 "button", 2423 { 2424 onClick: () => { 2425 resetGame(); 2426 }, 2427 className: "flex-1 py-4 bg-emerald-500 text-white font-black rounded-xl hover:bg-emerald-400 transition-all shadow-lg shadow-emerald-500/20 hover:shadow-emerald-500/40 transform hover:-translate-y-1 text-base tracking-wider", 2428 children: "PLAY AGAIN" 2429 } 2430 ),
2431 /* @__PURE__ */ jsxRuntimeExports.jsx( 2432 "button", 2433 { 2434 onClick: () => { 2435 setShowEndScreen(false); 2436 toggleGameMode(); 2437 }, 2438 className: "flex-1 py-4 bg-white/5 text-white font-black rounded-xl hover:bg-white/10 transition-all border border-white/10 text-base tracking-wider", 2439 children: "CLOSE" 2440 } 2441 ) 2442 ] }) 2443 ] }) 2444 ] }) 2445 ] 2446 } 2447 ) 2448 } 2449 ), 2450 gameCountdown !== null && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none z-50", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 2451 "div", 2452 { 2453 className: "text-9xl font-bold text-white drop-shadow-[0_0_30px_rgba(255,255,255,0.8)] animate-pulse", 2454 style: { transform: `scale(${uiScale})` }, 2455 children: gameCountdown 2456 } 2457 ) }), 2458 octaveTip && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 pointer-events-none z-40", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 2459 "div", 2460 { 2461 className: "bg-red-500/80 backdrop-blur-md text-white px-6 py-3 rounded-full font-bold text-xl animate-bounce shadow-lg border border-white/20", 2462 style: { transform: `scale(${uiScale})` }, 2463 children: octaveTip 2464 } 2465 ) }), 2466 /* @__PURE__ */ jsxRuntimeExports.jsxs( 2467 "div", 2468 { 2469 className: "absolute bottom-[16%] right-4 flex flex-col gap-2 z-50 origin-bottom-right", 2470 style: { transform: `scale(${uiScale})` }, 2471 children: [
2472 /* @__PURE__ */ jsxRuntimeExports.jsx( 2473 "button", 2474 { 2475 onClick: async (e) => { 2476 e.stopPropagation(); 2477 if (!midi.isMidiConnected) { 2478 const success = await midi.connectMidi(); 2479 if (!success) { 2480 alert("Could not connect to MIDI devices. Please ensure your browser supports Web MIDI and permission is granted."); 2481 } 2482 } 2483 }, 2484 className: `p-2 rounded-full backdrop-blur-md border transition-all shadow-lg pointer-events-auto ${midi.isMidiConnected ? "bg-emerald-600/80 border-emerald-400/50 text-white opacity-100" : "bg-black/60 border-white/10 text-white/50 hover:opacity-100 hover:text-white/80"}`, 2485 title: midi.isMidiConnected ? "MIDI Connected" : "Connect MIDI Device", 2486 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Cable, { size: 16 }) 2487 } 2488 ), 2489 /* @__PURE__ */ jsxRuntimeExports.jsx( 2490 "button", 2491 { 2492 onClick: (e) => { 2493 e.stopPropagation(); 2494 toggleGameMode(); 2495 }, 2496 className: "p-2 rounded-full backdrop-blur-md border transition-colors pointer-events-auto bg-emerald-600/80 border-emerald-400/50 text-white", 2497 title: "Toggle Game Mode", 2498 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Gamepad2, { size: 16 }) 2499 } 2500 ),
2501 /* @__PURE__ */ jsxRuntimeExports.jsx( 2502 "button", 2503 { 2504 onClick: (e) => { 2505 e.stopPropagation(); 2506 setKeyboardLabelMode((prev) => prev === "pc" ? "notes" : prev === "notes" ? "off" : "pc"); 2507 }, 2508 className: `p-2 rounded-full backdrop-blur-md border transition-colors pointer-events-auto ${keyboardLabelMode !== "off" ? "bg-indigo-600/80 border-indigo-400/50 text-white" : "bg-black/60 border-white/10 text-white/50 hover:text-white/80"}`, 2509 title: "Toggle Keyboard Labels", 2510 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 16 }) 2511 } 2512 ) 2513 ] 2514 } 2515 ) 2516 ] }); 2517}; 2518const PianoRollVisualizer = ({ data, currentTime, isPlaying, hiddenTracks, colorMap, selectedTrackIndex, processedInstruments, uniqueNames, toggleGameMode, onPlayPause, onSeek, soundfont, theme = "dark", playbackRate = 1, trackId }) => { 2519 const containerRef = reactExports.useRef(null); 2520 const canvasRef = reactExports.useRef(null); 2521 const requestRef = reactExports.useRef(null); 2522 const lastTimeRef = reactExports.useRef(currentTime); 2523 const lastTickRef = reactExports.useRef(0); 2524 const particlesRef = reactExports.useRef([]); 2525 const prevActiveNotesRef = reactExports.useRef(/* @__PURE__ */ new Set()); 2526 const starFieldRef = reactExports.useRef([]); 2527 const isMobile = reactExports.useRef(typeof window !== "undefined" && (/Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || /Macintosh/i.test(navigator.userAgent) && navigator.maxTouchPoints > 0 || "ontouchstart" in window)).current; 2528 const [lowPerf, setLowPerf] = reactExports.useState(() => { 2529 if (typeof window === "undefined") return false; 2530 const saved = localStorage.getItem("low_performance"); 2531 if (saved !== null) return saved === "true"; 2532 return isMobile; 2533 }); 2534 reactExports.useEffect(() => { 2535 const handleLowPerfChange = () => { 2536 const saved = localStorage.getItem("low_performance"); 2537 if (saved !== null) { 2538 setLowPerf(saved === "true"); 2539 } else { 2540 setLowPerf(isMobile); 2541 } 2542 }; 2543 window.addEventListener("storage", handleLowPerfChange); 2544 window.addEventListener("low_performance_change", handleLowPerfChange); 2545 return () => { 2546 window.removeEventListener("storage", handleLowPerfChange); 2547 window.removeEventListener("low_performance_change", handleLowPerfChange); 2548 }; 2549 }, [isMobile]); 2550 reactExports.useEffect(() => { 2551 lastTimeRef.current = currentTime; 2552 lastTickRef.current = performance.now(); 2553 }, [currentTime]); 2554 const musicalInstruments = React.useMemo(() => { 2555 return (processedInstruments == null ? void 0 : processedInstruments.filter((inst) => inst.midiChannel !== -1)) || []; 2556 }, [processedInstruments]); 2557 const isPercussionNote = React.useCallback((note) => { 2558 if (!processedInstruments) return false; 2559 let inst = musicalInstruments[note.trackIdx]; 2560 if (!inst || inst.midiChannel !== 9) { 2561 inst = processedInstruments.find((i) => i.trackId === note.trackIdx); 2562 } 2563 if (!inst || inst.midiChannel !== 9) { 2564 inst = processedInstruments.find((i) => i.originalName === note.i || i.displayName === note.i || i.name === note.i); 2565 } 2566 if (inst) { 2567 return inst.midiChannel === 9; 2568 } 2569 const nameLower = (note.i || "").toLowerCase(); 2570 return nameLower.includes("drum") || nameLower.includes("perc") || nameLower.includes("kit") || nameLower.includes("cymbal"); 2571 }, [processedInstruments, musicalInstruments]); 2572 const getNoteKey = React.useCallback((note) => { 2573 if (!note) return ""; 2574 if (note.i && colorMap && colorMap[note.i]) { 2575 return note.i; 2576 } 2577 if (processedInstruments && note.trackIdx !== void 0 && musicalInstruments[note.trackIdx]) { 2578 const inst = musicalInstruments[note.trackIdx]; 2579 const foundIdx = processedInstruments.findIndex((i) => i.trackId === inst.trackId); 2580 if (foundIdx !== -1 && uniqueNames && uniqueNames[foundIdx]) { 2581 return uniqueNames[foundIdx]; 2582 } 2583 } 2584 return note.i || ""; 2585 }, [colorMap, processedInstruments, musicalInstruments, uniqueNames]); 2586 const { playNote, stopNote, stopAllNotes, previewActiveNotesRef } = useSoundfontKeyboard(selectedTrackIndex, processedInstruments, void 0, soundfont); 2587 const [practiceMode, setPracticeMode] = reactExports.useState(false); 2588 const lastSatisfiedTimeRef = reactExports.useRef(-1); 2589 const waitingNotesRef = reactExports.useRef(null); 2590 reactExports.useEffect(() => { 2591 lastSatisfiedTimeRef.current = -1; 2592 waitingNotesRef.current = null; 2593 }, [practiceMode, trackId]); 2594 const [micActive, setMicActive] = reactExports.useState(false); 2595 const micActiveNotesRef = reactExports.useRef(/* @__PURE__ */ new Map()); 2596 reactExports.useEffect(() => { 2597 let active = true; 2598 if (micActive) { 2599 const startMic = async () => { 2600 try { 2601 await synth.pitchTracker.start(); 2602 if (!active) { 2603 synth.pitchTracker.stop(); 2604 return; 2605 } 2606 const rmsThreshold = 0.015; 2607 let currentRms = 0; 2608 synth.pitchTracker.onVolumeUpdate = (volume) => { 2609 if (!active) return; 2610 currentRms = volume; 2611 }; 2612 synth.pitchTracker.onPitchDetected = (freq) => { 2613 if (!active) return; 2614 if (currentRms < rmsThreshold) return; 2615 const midi2 = Math.round(12 * Math.log2(freq / 440) + 69); 2616 if (midi2 >= CONFIG.keyRangeStart && midi2 <= CONFIG.keyRangeEnd) { 2617 micActiveNotesRef.current.set(midi2, Date.now()); 2618 previewActiveNotesRef.current.add(midi2); 2619 } 2620 }; 2621 } catch (err) { 2622 console.error("Failed to start pitch tracker:", err); 2623 if (active) setMicActive(false); 2624 } 2625 }; 2626 startMic(); 2627 } else { 2628 synth.pitchTracker.stop(); 2629 synth.pitchTracker.onVolumeUpdate = void 0; 2630 synth.pitchTracker.onPitchDetected = void 0; 2631 micActiveNotesRef.current.forEach((_, note) => { 2632 previewActiveNotesRef.current.delete(note); 2633 }); 2634 micActiveNotesRef.current.clear(); 2635 } 2636 return () => { 2637 active = false;
2638 synth.pitchTracker.stop(); 2639 synth.pitchTracker.onVolumeUpdate = void 0; 2640 synth.pitchTracker.onPitchDetected = void 0; 2641 }; 2642 }, [micActive, previewActiveNotesRef]); 2643 reactExports.useEffect(() => { 2644 if (Math.abs(currentTime - lastTimeRef.current) > 0.5) { 2645 lastSatisfiedTimeRef.current = -1; 2646 waitingNotesRef.current = null; 2647 } 2648 }, [currentTime]); 2649 const baseOctaveRef = reactExports.useRef(4); 2650 const [, setBaseOctave] = reactExports.useState(4); 2651 const activeKeysRef = reactExports.useRef(/* @__PURE__ */ new Map()); 2652 const [keyboardLabelMode, setKeyboardLabelMode] = reactExports.useState("pc"); 2653 const keyboardLabelModeRef = reactExports.useRef("pc"); 2654 reactExports.useEffect(() => { 2655 keyboardLabelModeRef.current = keyboardLabelMode; 2656 }, [keyboardLabelMode]); 2657 const midi = useKeyboardInput({ 2658 playNote, 2659 stopNote, 2660 baseOctaveRef, 2661 activeKeysRef, 2662 onOctaveChange: setBaseOctave 2663 }); 2664 const handlePointerDown = (e) => { 2665 if (!canvasRef.current) return; 2666 const rect = canvasRef.current.getBoundingClientRect(); 2667 const y = e.clientY - rect.top; 2668 const h = rect.height; 2669 const keyHeight = h * 0.15; 2670 const keyTop = h - keyHeight; 2671 if (y >= keyTop) { 2672 e.stopPropagation(); 2673 const x = e.clientX - rect.left; 2674 const w = rect.width; 2675 const midi2 = getMidiFromKeyboardClick(x, y - keyTop, w, keyHeight); 2676 if (midi2 !== null) { 2677 playNote(midi2); 2678 } 2679 } 2680 }; 2681 const handlePointerUp = () => { 2682 stopAllNotes(); 2683 }; 2684 const handlePointerLeave = () => { 2685 stopAllNotes(); 2686 }; 2687 reactExports.useEffect(() => { 2688 if (!containerRef.current || !canvasRef.current) return; 2689 const updateDim = () => { 2690 if (containerRef.current && canvasRef.current) { 2691 const { width, height } = containerRef.current.getBoundingClientRect(); 2692 let dpr = window.devicePixelRatio || 1; 2693 if (lowPerf) { 2694 dpr = Math.min(dpr, 1); 2695 } else if (isMobile) { 2696 dpr = Math.min(dpr, 1.5); 2697 } 2698 canvasRef.current.width = width * dpr; 2699 canvasRef.current.height = height * dpr; 2700 const ctx = canvasRef.current.getContext("2d"); 2701 if (ctx) ctx.scale(dpr, dpr); 2702 canvasRef.current.style.width = `${width}px`; 2703 canvasRef.current.style.height = `${height}px`; 2704 } 2705 }; 2706 updateDim(); 2707 const ro = new ResizeObserver(updateDim); 2708 ro.observe(containerRef.current); 2709 return () => ro.disconnect(); 2710 }, [lowPerf, isMobile]); 2711 const animateRef = reactExports.useRef(() => { 2712 }); 2713 const drawNotes = (ctx, notes, t, kw, bkw, yBase, pps, cmap, active = false, labelMode = "off") => { 2714 notes.forEach((note) => { 2715 const { x, w } = getKeyGeometry(note.n, kw, bkw); 2716 const distFromPlayhead = (note.t - t) * pps; 2717 const noteHeight = note.d * pps; 2718 const yBottom = yBase - distFromPlayhead; 2719 const yTop = yBottom - noteHeight; 2720 if (yBottom < -50) return; 2721 const color = cmap[getNoteKey(note)] || "#888"; 2722 if (active) { 2723 if (!isMobile) { 2724 ctx.shadowColor = color; 2725 } 2726 ctx.fillStyle = color; 2727 ctx.fillRect(x, yTop, w, noteHeight); 2728 } else { 2729 if (lowPerf) { 2730 ctx.fillStyle = color; 2731 ctx.fillRect(x, yTop, w - 1, noteHeight); 2732 } else if (isMobile) { 2733 ctx.fillStyle = color; 2734 ctx.beginPath(); 2735 ctx.roundRect(x, yTop, w - 1, noteHeight, 4); 2736 ctx.fill(); 2737 } else { 2738 const grad = ctx.createLinearGradient(x, yTop, x, yBottom); 2739 grad.addColorStop(0, "#222"); 2740 grad.addColorStop(1, color); 2741 ctx.fillStyle = grad; 2742 ctx.beginPath(); 2743 ctx.roundRect(x, yTop, w - 1, noteHeight, 4); 2744 ctx.fill(); 2745 ctx.strokeStyle = "rgba(255,255,255,0.2)"; 2746 ctx.lineWidth = 1; 2747 ctx.stroke(); 2748 } 2749 if (labelMode !== "off") { 2750 const baseOct = baseOctaveRef.current; 2751 const notesArr = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; 2752 const noteName = notesArr[note.n % 12]; 2753 const octave = Math.floor(note.n / 12) - 1; 2754 const noteLabel = `${noteName}${octave}`; 2755 const pcLabel = getPcKeyLabel(note.n, baseOct); 2756 let mainLabel = ""; 2757 if (labelMode === "notes") { 2758 mainLabel = noteLabel; 2759 } else if (labelMode === "pc") { 2760 if (pcLabel) { 2761 mainLabel = pcLabel; 2762 } else { 2763 mainLabel = noteLabel; 2764 } 2765 } 2766 if (mainLabel) { 2767 const centerX = x + w / 2; 2768 const labelY = yBottom - Math.min(noteHeight / 2, 20); 2769 ctx.textAlign = "center"; 2770 ctx.textBaseline = "middle"; 2771 ctx.fillStyle = "#fff"; 2772 ctx.strokeStyle = "#000000"; 2773 ctx.lineWidth = 3; 2774 ctx.lineJoin = "round"; 2775 ctx.miterLimit = 2; 2776 let fontSize = w * 0.8; 2777 fontSize = Math.max(12, Math.min(fontSize, 40)); 2778 ctx.font = `bold ${fontSize}px monospace`; 2779 ctx.strokeText(mainLabel, centerX, labelY); 2780 ctx.fillText(mainLabel, centerX, labelY); 2781 } 2782 } 2783 } 2784 }); 2785 }; 2786 const renderScene = (ctx, rawWidth, rawHeight, t) => { 2787 if (micActive) { 2788 const now = Date.now(); 2789 const decayTime = 1200; 2790 micActiveNotesRef.current.forEach((timestamp, note) => { 2791 if (now - timestamp > decayTime) { 2792 micActiveNotesRef.current.delete(note); 2793 previewActiveNotesRef.current.delete(note); 2794 } 2795 }); 2796 } 2797 let dpr = window.devicePixelRatio || 1; 2798 if (lowPerf) { 2799 dpr = Math.min(dpr, 1); 2800 } else if (isMobile) { 2801 dpr = Math.min(dpr, 1.5); 2802 } 2803 const width = rawWidth / dpr; 2804 const fullHeight = rawHeight / dpr; 2805 const height = fullHeight; 2806 const effectiveTime = t + CONFIG.visualOffset;
2807 const keyHeight = height * 0.15; 2808 const keyTop = height - keyHeight; 2809 const noteAreaHeight = keyTop; 2810 const pixelsPerSecond = noteAreaHeight / CONFIG.lookAheadTime; 2811 const whiteKeyCount = 52; 2812 const keyWidth = width / whiteKeyCount; 2813 const blackKeyWidth = keyWidth * 0.65; 2814 ctx.fillStyle = "#000000"; 2815 ctx.fillRect(0, 0, width, fullHeight); 2816 if (!lowPerf) { 2817 ctx.fillStyle = "rgba(255, 255, 255, 0.4)"; 2818 starFieldRef.current.forEach((star) => { 2819 star.y -= star.speed * (isPlaying ? 1 : 0) * (playbackRate || 1); 2820 if (star.y < 0) star.y = 1; 2821 if (isMobile) { 2822 ctx.fillRect(star.x * width - star.size, star.y * noteAreaHeight - star.size, star.size * 2, star.size * 2); 2823 } else { 2824 ctx.beginPath(); 2825 ctx.arc(star.x * width, star.y * noteAreaHeight, star.size, 0, Math.PI * 2); 2826 ctx.fill(); 2827 } 2828 }); 2829 } 2830 const noteData = data && Array.isArray(data) ? data : []; 2831 const visibleNotes = []; 2832 const activeNotes = /* @__PURE__ */ new Set(); 2833 let left = 0; 2834 let right = noteData.length - 1; 2835 let startIdx = 0; 2836 const searchTime = Math.max(0, effectiveTime - 10); 2837 while (left <= right) { 2838 const mid = Math.floor((left + right) / 2); 2839 if (noteData[mid].t < searchTime) { 2840 left = mid + 1; 2841 startIdx = left; 2842 } else { 2843 right = mid - 1; 2844 } 2845 } 2846 for (let i = startIdx; i < noteData.length; i++) { 2847 const note = noteData[i]; 2848 if (note.t > effectiveTime + CONFIG.lookAheadTime) break; 2849 if (hiddenTracks.has(getNoteKey(note))) continue; 2850 if (isPercussionNote(note)) continue; 2851 const timeStart = note.t; 2852 const timeEnd = note.t + note.d; 2853 if (effectiveTime >= timeStart && effectiveTime <= timeEnd) activeNotes.add(note.n); 2854 if (timeEnd > effectiveTime) visibleNotes.push(note); 2855 } 2856 if (practiceMode && noteData.length > 0) { 2857 if (isPlaying) { 2858 const upcomingNotes = []; 2859 for (let i = startIdx; i < noteData.length; i++) { 2860 const note = noteData[i]; 2861 if (hiddenTracks.has(getNoteKey(note))) continue; 2862 if (isPercussionNote(note)) continue; 2863 if (note.t <= effectiveTime + 0.05 && note.t > lastSatisfiedTimeRef.current + 0.02) { 2864 upcomingNotes.push(note); 2865 } 2866 if (note.t > effectiveTime + 0.2) break; 2867 } 2868 if (upcomingNotes.length > 0) { 2869 const chordTime = upcomingNotes[0].t; 2870 const chordNotes = upcomingNotes.filter((n) => Math.abs(n.t - chordTime) < 0.05); 2871 const targetNoteNumbers = new Set(chordNotes.map((n) => n.n)); 2872 if (targetNoteNumbers.size > 0) { 2873 let allPressed = true; 2874 for (const midiNum of targetNoteNumbers) { 2875 if (!previewActiveNotesRef.current.has(midiNum)) { 2876 allPressed = false; 2877 break; 2878 } 2879 } 2880 if (allPressed) { 2881 lastSatisfiedTimeRef.current = chordTime; 2882 waitingNotesRef.current = null; 2883 } else { 2884 waitingNotesRef.current = targetNoteNumbers; 2885 if (onPlayPause) onPlayPause(false); 2886 if (onSeek) onSeek(chordTime); 2887 } 2888 } 2889 } 2890 } else if (waitingNotesRef.current) { 2891 let allPressed = true; 2892 for (const midiNum of waitingNotesRef.current) { 2893 if (!previewActiveNotesRef.current.has(midiNum)) { 2894 allPressed = false; 2895 break; 2896 } 2897 } 2898 if (allPressed) { 2899 const satisfiedTime = Array.from(waitingNotesRef.current)[0]; 2900 const matchedNote = noteData.find((n) => n.n === satisfiedTime && !hiddenTracks.has(getNoteKey(n)) && !isPercussionNote(n) && Math.abs(n.t - effectiveTime) < 0.25); 2901 const satisfiedTimeSec = matchedNote ? matchedNote.t : effectiveTime; 2902 lastSatisfiedTimeRef.current = satisfiedTimeSec; 2903 waitingNotesRef.current = null; 2904 if (onPlayPause) onPlayPause(true); 2905 } 2906 } 2907 } else { 2908 waitingNotesRef.current = null; 2909 } 2910 previewActiveNotesRef.current.forEach((midi2) =>
2910 activeNotes.add(midi2)); 2911 if (isPlaying) { 2912 const spawnCount = lowPerf ? 0 : isMobile ? 1 : 8; 2913 activeNotes.forEach((midi2) => { 2914 if (!prevActiveNotesRef.current.has(midi2)) { 2915 const { x, w } = getKeyGeometry(midi2, keyWidth, blackKeyWidth); 2916 const activeNoteObj = visibleNotes.find((n) => n.n === midi2 && effectiveTime >= n.t && effectiveTime <= n.t + n.d); 2917 const color = activeNoteObj ? colorMap[getNoteKey(activeNoteObj)] || "#ffffff" : "#ffffff"; 2918 for (let i = 0; i < spawnCount; i++) { 2919 particlesRef.current.push({ 2920 x: x + w / 2 + (Math.random() - 0.5) * w, 2921 y: keyTop, 2922 vx: (Math.random() - 0.5) * 4, 2923 vy: -(Math.random() * 5 + 2), 2924 size: Math.random() * 3 + 1, 2925 color, 2926 life: 1, 2927 decay: 0.02 + Math.random() * 0.03 2928 }); 2929 } 2930 } 2931 }); 2932 } 2933 prevActiveNotesRef.current = activeNotes; 2934 for (let i = particlesRef.current.length - 1; i >= 0; i--) { 2935 const p = particlesRef.current[i]; 2936 p.x += p.vx; 2937 p.y += p.vy; 2938 p.life -= p.decay; 2939 p.vy += 0.1; 2940 if (p.life <= 0) { 2941 particlesRef.current.splice(i, 1); 2942 continue; 2943 } 2944 ctx.globalCompositeOperation = "lighter"; 2945 ctx.fillStyle = p.color; 2946 ctx.globalAlpha = p.life; 2947 if (isMobile) { 2948 ctx.fillRect(p.x - p.size, p.y - p.size, p.size * 2, p.size * 2); 2949 } else { 2950 ctx.beginPath(); 2951 ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); 2952 ctx.fill(); 2953 } 2954 ctx.globalAlpha = 1; 2955 } 2956 ctx.globalCompositeOperation = "source-over"; 2957 if (!isMobile && !lowPerf) { 2958 ctx.save(); 2959 ctx.translate(0, height + keyHeight); 2960 ctx.scale(1, -1); 2961 ctx.globalAlpha = 0.15; 2962 drawNotes(ctx, visibleNotes, effectiveTime, keyWidth, blackKeyWidth, keyTop, pixelsPerSecond, colorMap, false, "off"); 2963 ctx.restore(); 2964 } 2965 drawNotes(ctx, visibleNotes, effectiveTime, keyWidth, blackKeyWidth, keyTop, pixelsPerSecond, colorMap, false, keyboardLabelModeRef.current); 2966 ctx.globalCompositeOperation = "lighter"; 2967 if (!isMobile && !lowPerf) { 2968 ctx.shadowBlur = 20; 2969 } 2970 const hitNotes = visibleNotes.filter((n) => activeNotes.has(n.n) && effectiveTime >= n.t && effectiveTime <= n.t + n.d); 2971 drawNotes(ctx, hitNotes, effectiveTime, keyWidth, blackKeyWidth, keyTop, pixelsPerSecond, colorMap, true, "off"); 2972 if (!isMobile && !lowPerf) { 2973 ctx.shadowBlur = 0; 2974 } 2975 ctx.globalCompositeOperation = "source-over"; 2976 const micNotesSet = micActive ? new Set(micActiveNotesRef.current.keys()) : void 0; 2977 renderSharedKeyboard(ctx, width, height, keyHeight, activeNotes, keyboardLabelModeRef.current, baseOctaveRef.current, waitingNotesRef.current || void 0, micNotesSet); 2978 }; 2979 const animate = () => { 2980 if (!canvasRef.current) return; 2981 let visualTime = lastTimeRef.current; 2982 if (isPlaying) { 2983 visualTime += (performance.now() - lastTickRef.current) / 1e3; 2984 } 2985 const ctx = canvasRef.current.getContext("2d", { alpha: false }); 2986 if (ctx) { 2987 const { width, height } = canvasRef.current; 2988 renderScene(ctx, width, height, visualTime); 2989 } 2990 }; 2991 reactExports.useEffect(() => { 2992 animateRef.current = animate; 2993 }); 2994 reactExports.useEffect(() => { 2995 if (starFieldRef.current.length === 0) { 2996 for (let i = 0; i < 80; i++) { 2997 starFieldRef.current.push({ 2998 x: Math.random(), 2999 y: Math.random(), 3000 size: Math.random() * 1.5 + 0.5, 3001 speed: Math.random() * 0.05 + 0.01 3002 }); 3003 } 3004 } 3005 let lastFrameTime = 0; 3006 const isSlow = isSlowMobile(); 3007 const fpsInterval = isSlow ? 1e3 / 30 : 0; 3008 const loop = (timestamp) => { 3009 if (fpsInterval > 0) { 3010 const elapsed = timestamp - lastFrameTime; 3011 if (elapsed >= fpsInterval) { 3012 lastFrameTime = timestamp - elapsed % fpsInterval; 3013 animateRef.current(); 3014 } 3015 } else { 3016 animateRef.current(); 3017 } 3018 requestRef.current = requestAnimationFrame(loop); 3019 }; 3020 requestRef.current = requestAnimationFrame(loop); 3021 return () => { 3022 if (requestRef.current) cancelAnimationFrame(requestRef.current); 3023 }; 3024 }, []); 3025 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: containerRef, className: "w-full h-full relative", style: { backgroundColor: "#000000" }, children: [
3026 /* @__PURE__ */ jsxRuntimeExports.jsx( 3027 "canvas", 3028 { 3029 ref: canvasRef, 3030 className: "w-full h-full block touch-none cursor-pointer", 3031 onPointerDown: (e) => { 3032 var _a; 3033 const rect = (_a = canvasRef.current) == null ? void 0 : _a.getBoundingClientRect(); 3034 if (rect) { 3035 const y = e.clientY - rect.top; 3036 const h = rect.height; 3037 const keyHeight = h * 0.15; 3038 const keyTop = h - keyHeight; 3039 if (y >= keyTop) { 3040 handlePointerDown(e); 3041 } else { 3042 if (onPlayPause) onPlayPause(); 3043 } 3044 } 3045 }, 3046 onPointerUp: handlePointerUp, 3047 onPointerLeave: handlePointerLeave, 3048 onPointerCancel: handlePointerLeave 3049 } 3050 ), 3051 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-[16%] right-4 flex flex-col gap-2 z-50", children: [ 3052 /* @__PURE__ */ jsxRuntimeExports.jsx( 3053 "button", 3054 { 3055 onClick: async (e) => { 3056 e.stopPropagation(); 3057 if (!midi.isMidiConnected) { 3058 const success = await midi.connectMidi(); 3059 if (!success) { 3060 alert("Could not connect to MIDI devices. Please ensure your browser supports Web MIDI and permission is granted."); 3061 } 3062 } 3063 }, 3064 className: `p-2 rounded-full backdrop-blur-md border transition-all shadow-lg pointer-events-auto ${midi.isMidiConnected ? "bg-emerald-600/80 border-emerald-400/50 text-white opacity-100" : "bg-black/60 border-white/10 text-white/50 hover:opacity-100 hover:text-white/80"}`, 3065 title: midi.isMidiConnected ? "MIDI Connected" : "Connect MIDI Device", 3066 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Cable, { size: 16 }) 3067 } 3068 ),
3069 /* @__PURE__ */ jsxRuntimeExports.jsx( 3070 "button", 3071 { 3072 onClick: (e) => { 3073 e.stopPropagation(); 3074 toggleGameMode(); 3075 }, 3076 className: "p-2 rounded-full backdrop-blur-md border transition-colors pointer-events-auto bg-black/60 border-white/10 text-white/50 hover:text-white/80", 3077 title: "Toggle Game Mode", 3078 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Gamepad2, { size: 16 }) 3079 } 3080 ), 3081 /* @__PURE__ */ jsxRuntimeExports.jsx( 3082 "button", 3083 { 3084 onClick: (e) => { 3085 e.stopPropagation(); 3086 setPracticeMode((prev) => !prev); 3087 }, 3088 className: `p-2 rounded-full backdrop-blur-md border transition-colors pointer-events-auto ${practiceMode ? "bg-amber-600/80 border-amber-400/50 text-white shadow-lg" : "bg-black/60 border-white/10 text-white/50 hover:text-white/80"}`, 3089 title: practiceMode ? "Disable Practice Mode" : "Enable Practice Mode", 3090 children: /* @__PURE__ */ jsxRuntimeExports.jsx(GraduationCap, { size: 16 }) 3091 } 3092 ),
3093 /* @__PURE__ */ jsxRuntimeExports.jsx( 3094 "button", 3095 { 3096 onClick: (e) => { 3097 e.stopPropagation(); 3098 setMicActive((prev) => !prev); 3099 }, 3100 className: `p-2 rounded-full backdrop-blur-md border transition-colors pointer-events-auto ${micActive ? "bg-emerald-600/80 border-emerald-400/50 text-white shadow-lg" : "bg-black/60 border-white/10 text-white/50 hover:text-white/80"}`, 3101 title: micActive ? "Disable Microphone Input" : "Enable Microphone Input", 3102 children: micActive ? /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 16 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(MicOff, { size: 16 }) 3103 } 3104 ), 3105 /* @__PURE__ */ jsxRuntimeExports.jsx( 3106 "button", 3107 { 3108 onClick: (e) => { 3109 e.stopPropagation(); 3110 setKeyboardLabelMode((prev) => prev === "pc" ? "notes" : prev === "notes" ? "off" : "pc"); 3111 }, 3112 className: `p-2 rounded-full backdrop-blur-md border transition-colors pointer-events-auto ${keyboardLabelMode !== "off" ? "bg-indigo-600/80 border-indigo-400/50 text-white" : "bg-black/60 border-white/10 text-white/50 hover:text-white/80"}`, 3113 title: "Toggle Keyboard Labels", 3114 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 16 }) 3115 } 3116 ) 3117 ] }) 3118 ] }); 3119}; 3120const PianoRoll = (props) => { 3121 const [isGameMode, setIsGameMode] = reactExports.useState(props.isGameModeProp || false); 3122 reactExports.useEffect(() => { 3123 if (props.isGameModeProp !== void 0 && props.isGameModeProp !== isGameMode) { 3124 setIsGameMode(props.isGameModeProp); 3125 } 3126 }, [props.isGameModeProp]); 3127 const toggleGameMode = () => { 3128 const newMode = !isGameMode; 3129 if (props.setIsGameModeProp) { 3130 props.setIsGameModeProp(newMode); 3131 } else { 3132 setIsGameMode(newMode); 3133 } 3134 }; 3135 if (isGameMode) { 3136 return /* @__PURE__ */ jsxRuntimeExports.jsx( 3137 PianoRollGame, 3138 { 3139 ...props, 3140 toggleGameMode, 3141 soundfont: props.soundfont 3142 } 3143 ); 3144 } 3145 return /* @__PURE__ */ jsxRuntimeExports.jsx(PianoRollVisualizer, { ...props, toggleGameMode, soundfont: props.soundfont }); 3146}; 3147const PercussionRoll = ({ 3148 data, 3149 currentTime, 3150 isPlaying, 3151 hiddenTracks, 3152 colorMap, 3153 processedInstruments, 3154 uniqueNames, 3155 onPlayPause, 3156 playbackRate = 1, 3157 tempo = 120, 3158 timeSignature = [4, 4], 3159 soundfont = "MusyngKite" 3160}) => { 3161 const containerRef = reactExports.useRef(null); 3162 const canvasRef = reactExports.useRef(null); 3163 const requestRef = reactExports.useRef(null); 3164 const lastTimeRef = reactExports.useRef(currentTime); 3165 const lastTickRef = reactExports.useRef(0); 3166 const particlesRef = reactExports.useRef([]); 3167 const prevActivePitchesRef = reactExports.useRef(/* @__PURE__ */ new Set()); 3168 const starFieldRef = reactExports.useRef([]); 3169 const activeClicksRef = reactExports.useRef(/* @__PURE__ */ new Map()); 3170 const lastHitTimesRef = reactExports.useRef(/* @__PURE__ */ new Map()); 3171 const lastVisualTimeRef = reactExports.useRef(currentTime); 3172 const isMobile = reactExports.useRef(typeof window !== "undefined" && (/Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || /Macintosh/i.test(navigator.userAgent) && navigator.maxTouchPoints > 0 || "ontouchstart" in window)).current; 3173 reactExports.useEffect(() => { 3174 lastTimeRef.current = currentTime; 3175 lastVisualTimeRef.current = currentTime; 3176 lastTickRef.current = performance.now(); 3177 }, [currentTime]); 3178 const firstPercTrackIdx = processedInstruments == null ? void 0 : processedInstruments.findIndex((inst) => inst.midiChannel === 9); 3179 const { playNote, stopNote } = useSoundfontKeyboard( 3180 firstPercTrackIdx !== -1 ? firstPercTrackIdx : void 0, 3181 processedInstruments, 3182 void 0, 3183 soundfont 3184 ); 3185 const musicalInstruments = React.useMemo(() => { 3186 return (processedInstruments == null ? void 0 : processedInstruments.filter((inst) => inst.midiChannel !== -1)) || []; 3187 }, [processedInstruments]); 3188 const percussionNotes = React.useMemo(() => { 3189 return data.filter((note) => { 3190 if (hiddenTracks.has(note.i)) return false;
3191 let inst = musicalInstruments[note.trackIdx]; 3192 if (!inst || inst.midiChannel !== 9) { 3193 inst = processedInstruments == null ? void 0 : processedInstruments.find((i) => i.trackId === note.trackIdx); 3194 } 3195 if (!inst || inst.midiChannel !== 9) { 3196 inst = processedInstruments == null ? void 0 : processedInstruments.find((i) => i.originalName === note.i || i.displayName === note.i || i.name === note.i); 3197 } 3198 if (inst) { 3199 return inst.midiChannel === 9; 3200 } 3201 const nameLower = (note.i || "").toLowerCase(); 3202 return nameLower.includes("drum") || nameLower.includes("perc") || nameLower.includes("kit") || nameLower.includes("cymbal"); 3203 }); 3204 }, [data, hiddenTracks, processedInstruments, musicalInstruments]); 3205 const uniquePitches = React.useMemo(() => { 3206 return Array.from(new Set(percussionNotes.map((n) => n.n))).sort((a, b) => b - a); 3207 }, [percussionNotes]); 3208 reactExports.useEffect(() => { 3209 if (!containerRef.current || !canvasRef.current) return; 3210 const updateDim = () => { 3211 if (containerRef.current && canvasRef.current) { 3212 const { width, height } = containerRef.current.getBoundingClientRect(); 3213 const dpr = window.devicePixelRatio || 1; 3214 canvasRef.current.width = width * dpr; 3215 canvasRef.current.height = height * dpr; 3216 const ctx = canvasRef.current.getContext("2d"); 3217 if (ctx) ctx.scale(dpr, dpr); 3218 canvasRef.current.style.width = `${width}px`; 3219 canvasRef.current.style.height = `${height}px`; 3220 } 3221 }; 3222 updateDim(); 3223 const ro = new ResizeObserver(updateDim); 3224 ro.observe(containerRef.current); 3225 return () => ro.disconnect(); 3226 }, [uniquePitches.length]); 3227 const renderScene = (ctx, rawWidth, rawHeight, t) => { 3228 const prevT = lastVisualTimeRef.current; 3229 lastVisualTimeRef.current = t; 3230 const dpr = window.devicePixelRatio || 1; 3231 const width = rawWidth / dpr; 3232 const height = rawHeight / dpr; 3233 const labelWidth = 180; 3234 const playheadX = 220; 3235 const topMargin = 25; 3236 const bottomMargin = 25; 3237 const pixelsPerSecond = 140; 3238 const rowCount = Math.max(1, uniquePitches.length); 3239 const rowHeight = (height - topMargin - bottomMargin) / rowCount; 3240 ctx.fillStyle = "#06060c"; 3241 ctx.fillRect(0, 0, width, height); 3242 ctx.fillStyle = "rgba(255, 255, 255, 0.35)"; 3243 starFieldRef.current.forEach((star) => { 3244 star.x -= star.speed * (isPlaying ? 1 : 0) * (playbackRate || 1); 3245 if (star.x < 0) star.x = 1; 3246 if (isMobile) { 3247 ctx.fillRect(star.x * (width - labelWidth) + labelWidth - star.size, star.y * height - star.size, star.size * 2, star.size * 2); 3248 } else { 3249 ctx.beginPath(); 3250 ctx.arc(star.x * (width - labelWidth) + labelWidth, star.y * height, star.size, 0, Math.PI * 2); 3251 ctx.fill(); 3252 } 3253 }); 3254 ctx.strokeStyle = "rgba(255, 255, 255, 0.04)"; 3255 ctx.lineWidth = 1; 3256 for (let r = 0; r <= rowCount; r++) { 3257 const y = topMargin + r * rowHeight; 3258 ctx.beginPath(); 3259 ctx.moveTo(labelWidth, y); 3260 ctx.lineTo(width, y); 3261 ctx.stroke(); 3262 } 3263 const secondsPerBeat = 60 / (tempo || 120); 3264 const beatsPerMeasure = timeSignature[0] || 4; 3265 const windowStartTime = t - (playheadX - labelWidth) / pixelsPerSecond; 3266 const windowEndTime = t + (width - playheadX) / pixelsPerSecond; 3267 const startBeat = Math.floor(windowStartTime / secondsPerBeat); 3268 const endBeat = Math.ceil(windowEndTime / secondsPerBeat); 3269 for (let b = startBeat; b <= endBeat; b++) { 3270 const beatTime = b * secondsPerBeat; 3271 const bx = playheadX + (beatTime - t) * pixelsPerSecond; 3272 if (bx >= labelWidth && bx <= width) { 3273 const isMeasure = b % beatsPerMeasure === 0; 3274 ctx.strokeStyle = isMeasure ? "rgba(255, 255, 255, 0.12)" : "rgba(255, 255, 255, 0.03)"; 3275 ctx.lineWidth = isMeasure ? 1.5 : 1; 3276 ctx.beginPath(); 3277 ctx.moveTo(bx, topMargin); 3278 ctx.lineTo(bx, height - bottomMargin); 3279 ctx.stroke(); 3280 if (isMeasure && rowCount > 0) { 3281 ctx.fillStyle = "rgba(255, 255, 255, 0.25)"; 3282 ctx.font = "9px monospace"; 3283 ctx.textAlign = "center"; 3284 ctx.fillText(`${Math.floor(b / beatsPerMeasure) + 1}`, bx, topMargin - 8); 3285 } 3286 } 3287 } 3288 const activePitches = /* @__PURE__ */ new Set(); 3289 const nowMs = performance.now();
3290 percussionNotes.forEach((note) => { 3291 const isCrossing = t >= note.t && t <= note.t + note.d; 3292 const crossedOnset = isPlaying && t > prevT && t - prevT < 0.5 && note.t >= prevT && note.t <= t; 3293 if (isCrossing || crossedOnset) { 3294 activePitches.add(note.n); 3295 lastHitTimesRef.current.set(note.n, nowMs); 3296 } 3297 }); 3298 lastHitTimesRef.current.forEach((lastTime, pitch) => { 3299 if (nowMs - lastTime < 150) { 3300 activePitches.add(pitch); 3301 } else { 3302 lastHitTimesRef.current.delete(pitch); 3303 } 3304 }); 3305 activeClicksRef.current.forEach((_, pitch) => activePitches.add(pitch)); 3306 const getNoteColor = (noteName, noteTrackIdx, fallback) => { 3307 if (!processedInstruments || !uniqueNames) return fallback; 3308 let instIdx = processedInstruments.findIndex((i) => i.originalName === noteName || i.displayName === noteName || i.name === noteName); 3309 if (instIdx === -1) { 3310 const inst = musicalInstruments[noteTrackIdx] || processedInstruments.find((i) => i.trackId === noteTrackIdx); 3311 if (inst) instIdx = processedInstruments.indexOf(inst); 3312 } 3313 if (instIdx !== -1 && uniqueNames[instIdx]) { 3314 return colorMap[uniqueNames[instIdx]] || fallback; 3315 } 3316 return colorMap[noteName] || fallback; 3317 }; 3318 percussionNotes.forEach((note) => { 3319 const r = uniquePitches.indexOf(note.n); 3320 if (r === -1) return; 3321 const nx = playheadX + (note.t - t) * pixelsPerSecond; 3322 const nw = Math.max(12, note.d * pixelsPerSecond); 3323 const ny = topMargin + r * rowHeight + 4; 3324 const nh = rowHeight - 8; 3325 if (nx + nw < labelWidth || nx > width) return; 3326 const color = getNoteColor(note.i, note.trackIdx, "#a855f7"); 3327 const isActive = t >= note.t && t <= note.t + note.d; 3328 ctx.save(); 3329 if (isActive && !isMobile) { 3330 ctx.shadowColor = color; 3331 ctx.shadowBlur = 12; 3332 } 3333 if (isMobile) { 3334 ctx.fillStyle = color; 3335 ctx.beginPath(); 3336 ctx.roundRect(nx, ny, nw, nh, 4); 3337 ctx.fill(); 3338 } else { 3339 const grad = ctx.createLinearGradient(nx, ny, nx + nw, ny); 3340 grad.addColorStop(0, color); 3341 grad.addColorStop(1, "#1e1b4b"); 3342 ctx.fillStyle = grad; 3343 ctx.beginPath(); 3344 ctx.roundRect(nx, ny, nw, nh, 4); 3345 ctx.fill(); 3346 ctx.strokeStyle = isActive ? "#ffffff" : "rgba(255,255,255,0.2)"; 3347 ctx.lineWidth = isActive ? 1.5 : 0.8; 3348 ctx.stroke(); 3349 } 3350 ctx.restore(); 3351 }); 3352 if (isPlaying) { 3353 const spawnCount = isMobile ? 2 : 8; 3354 activePitches.forEach((pitch) => { 3355 if (!prevActivePitchesRef.current.has(pitch)) { 3356 const r = uniquePitches.indexOf(pitch); 3357 if (r !== -1) { 3358 const py = topMargin + r * rowHeight + rowHeight / 2; 3359 const noteObj = percussionNotes.find((n) => n.n === pitch && t >= n.t && t <= n.t + n.d); 3360 const color = noteObj ? getNoteColor(noteObj.i, noteObj.trackIdx, "#ffffff") : "#ffffff"; 3361 for (let i = 0; i < spawnCount; i++) { 3362 particlesRef.current.push({ 3363 x: playheadX, 3364 y: py, 3365 vx: -(Math.random() * 4 + 1), 3366 // burst leftwards 3367 vy: (Math.random() - 0.5) * 3, 3368 size: Math.random() * 2.5 + 1.2, 3369 color, 3370 life: 1, 3371 decay: 0.03 + Math.random() * 0.03 3372 }); 3373 } 3374 } 3375 } 3376 }); 3377 } 3378 prevActivePitchesRef.current = activePitches; 3379 for (let i = particlesRef.current.length - 1; i >= 0; i--) { 3380 const p = particlesRef.current[i]; 3381 p.x += p.vx; 3382 p.y += p.vy; 3383 p.life -= p.decay; 3384 if (p.life <= 0) { 3385 particlesRef.current.splice(i, 1); 3386 continue; 3387 } 3388 ctx.save(); 3389 ctx.globalCompositeOperation = "lighter"; 3390 ctx.fillStyle = p.color; 3391 ctx.globalAlpha = p.life; 3392 if (isMobile) { 3393 ctx.fillRect(p.x - p.size, p.y - p.size, p.size * 2, p.size * 2); 3394 } else { 3395 ctx.beginPath(); 3396 ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); 3397 ctx.fill(); 3398 }
3399 ctx.restore(); 3400 } 3401 ctx.save(); 3402 ctx.strokeStyle = "#818cf8"; 3403 ctx.lineWidth = 2; 3404 if (!isMobile) { 3405 ctx.shadowColor = "#4f46e5"; 3406 ctx.shadowBlur = 8; 3407 } 3408 ctx.beginPath(); 3409 ctx.moveTo(playheadX, topMargin); 3410 ctx.lineTo(playheadX, height - bottomMargin); 3411 ctx.stroke(); 3412 ctx.fillStyle = "#818cf8"; 3413 ctx.beginPath(); 3414 ctx.moveTo(playheadX - 6, topMargin); 3415 ctx.lineTo(playheadX + 6, topMargin); 3416 ctx.lineTo(playheadX, topMargin + 8); 3417 ctx.closePath(); 3418 ctx.fill(); 3419 ctx.restore(); 3420 ctx.fillStyle = "#08080f"; 3421 ctx.fillRect(0, 0, labelWidth, height); 3422 ctx.strokeStyle = "rgba(255, 255, 255, 0.12)"; 3423 ctx.lineWidth = 2; 3424 ctx.beginPath(); 3425 ctx.moveTo(labelWidth, 0); 3426 ctx.lineTo(labelWidth, height); 3427 ctx.stroke(); 3428 uniquePitches.forEach((pitch, r) => { 3429 const y = topMargin + r * rowHeight; 3430 const isActive = activePitches.has(pitch); 3431 const labelY = y + rowHeight / 2; 3432 ctx.font = "11px system-ui, sans-serif"; 3433 ctx.fillStyle = isActive ? "#c7d2fe" : "#71717a"; 3434 ctx.textAlign = "left"; 3435 ctx.textBaseline = "middle"; 3436 const instName = GM_PERCUSSION_MAP[pitch] || `Percussion (${pitch})`; 3437 const maxTextWidth = labelWidth - 35; 3438 let displayName = instName; 3439 if (ctx.measureText(instName).width > maxTextWidth) { 3440 while (ctx.measureText(displayName + "...").width > maxTextWidth && displayName.length > 0) { 3441 displayName = displayName.slice(0, -1); 3442 } 3443 displayName += "..."; 3444 } 3445 ctx.fillText(displayName, 15, labelY); 3446 if (isActive) { 3447 ctx.fillStyle = "#818cf8"; 3448 ctx.shadowColor = "#818cf8"; 3449 ctx.shadowBlur = 6; 3450 ctx.beginPath(); 3451 ctx.arc(labelWidth - 15, labelY, 4, 0, Math.PI * 2); 3452 ctx.fill(); 3453 ctx.shadowBlur = 0; 3454 } else { 3455 ctx.strokeStyle = "#27272a"; 3456 ctx.lineWidth = 1; 3457 ctx.beginPath(); 3458 ctx.arc(labelWidth - 15, labelY, 4, 0, Math.PI * 2); 3459 ctx.stroke(); 3460 } 3461 }); 3462 ctx.fillStyle = "#06060c"; 3463 ctx.fillRect(labelWidth, 0, width - labelWidth, topMargin); 3464 ctx.fillRect(labelWidth, height - bottomMargin, width - labelWidth, bottomMargin); 3465 ctx.fillStyle = "#3f3f46"; 3466 ctx.font = "bold 9px system-ui, sans-serif"; 3467 ctx.textAlign = "left"; 3468 ctx.fillText("PERCUSSION INSTRUMENT", 15, topMargin / 2 + 2); 3469 }; 3470 const animate = () => { 3471 if (!canvasRef.current) return; 3472 let visualTime = lastTimeRef.current; 3473 if (isPlaying) { 3474 visualTime += (performance.now() - lastTickRef.current) / 1e3; 3475 } 3476 const ctx = canvasRef.current.getContext("2d", { alpha: false }); 3477 if (ctx) { 3478 const { width, height } = canvasRef.current; 3479 renderScene(ctx, width, height, visualTime); 3480 } 3481 }; 3482 const animateRef = reactExports.useRef(() => { 3483 }); 3484 reactExports.useEffect(() => { 3485 animateRef.current = animate; 3486 }); 3487 reactExports.useEffect(() => { 3488 if (starFieldRef.current.length === 0) { 3489 for (let i = 0; i < 50; i++) { 3490 starFieldRef.current.push({ 3491 x: Math.random(), 3492 y: Math.random(), 3493 size: Math.random() * 1.2 + 0.4, 3494 speed: Math.random() * 3e-3 + 1e-3 3495 }); 3496 } 3497 } 3498 let lastFrameTime = 0; 3499 const isSlow = isSlowMobile(); 3500 const fpsInterval = isSlow ? 1e3 / 30 : 0; 3501 const loop = (timestamp) => { 3502 if (fpsInterval > 0) { 3503 const elapsed = timestamp - lastFrameTime; 3504 if (elapsed >= fpsInterval) { 3505 lastFrameTime = timestamp - elapsed % fpsInterval; 3506 animateRef.current(); 3507 } 3508 } else { 3509 animateRef.current(); 3510 } 3511 requestRef.current = requestAnimationFrame(loop); 3512 }; 3513 requestRef.current = requestAnimationFrame(loop); 3514 return () => { 3515 if (requestRef.current) cancelAnimationFrame(requestRef.current); 3516 }; 3517 }, []); 3518 const handlePointerDown = (e) => { 3519 if (!canvasRef.current) return; 3520 const rect = canvasRef.current.getBoundingClientRect(); 3521 const clickX = e.clientX - rect.left; 3522 const clickY = e.clientY - rect.top; 3523 const labelWidth = 180; 3524 const topMargin = 25; 3525 const bottomMargin = 25; 3526 if (clickX <= labelWidth) { 3527 e.stopPropagation(); 3528 const rowCount = Math.max(1, uniquePitches.length); 3529 const rowHeight = (rect.height - topMargin - bottomMargin) / rowCount; 3530 const clickedRow = Math.floor((clickY - topMargin) / rowHeight); 3531 if (clickedRow >= 0 && clickedRow < uniquePitches.length) { 3532 const pitch = uniquePitches[clickedRow]; 3533 if (activeClicksRef.current.has(pitch)) { 3534 clearTimeout(activeClicksRef.current.get(pitch)); 3535 } 3536 playNote(pitch, 9); 3537 const timeoutId = setTimeout(() => { 3538 stopNote(pitch, 9); 3539 activeClicksRef.current.delete(pitch); 3540 }, 250); 3541 activeClicksRef.current.set(pitch, timeoutId); 3542 } 3543 } else { 3544 if (onPlayPause) onPlayPause(); 3545 } 3546 }; 3547 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: containerRef, className: "w-full h-full relative overflow-hidden", style: { backgroundColor: "#06060c" }
3547, children: uniquePitches.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-full text-zinc-500 gap-2", children: [ 3548 /* @__PURE__ */ jsxRuntimeExports.jsx(Drum, { size: 32, className: "opacity-40 animate-pulse" }), 3549 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm", children: "No percussion notes found in this track." }) 3550 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx( 3551 "canvas", 3552 { 3553 ref: canvasRef, 3554 className: "w-full h-full block touch-none cursor-pointer", 3555 onPointerDown: handlePointerDown 3556 } 3557 ) }); 3558}; 3559const COMMON_WORDS_LIST = [ 3560 // Pronouns & Contractions 3561 "i", 3562 "me", 3563 "my", 3564 "myself", 3565 "we", 3566 "our", 3567 "ours", 3568 "ourselves", 3569 "you", 3570 "your", 3571 "yours", 3572 "yourself", 3573 "yourselves", 3574 "he", 3575 "him", 3576 "his", 3577 "himself", 3578 "she", 3579 "her", 3580 "hers", 3581 "herself", 3582 "it", 3583 "its", 3584 "itself", 3585 "they", 3586 "them", 3587 "their", 3588 "theirs", 3589 "themselves", 3590 "what", 3591 "which", 3592 "who", 3593 "whom", 3594 "this", 3595 "that", 3596 "these", 3597 "those", 3598 "am", 3599 "is", 3600 "are", 3601 "was", 3602 "were", 3603 "be", 3604 "been", 3605 "being", 3606 "have", 3607 "has", 3608 "had", 3609 "having", 3610 "do", 3611 "does", 3612 "did", 3613 "doing", 3614 "a", 3615 "an", 3616 "the", 3617 "and", 3618 "but", 3619 "if", 3620 "or", 3621 "because", 3622 "as", 3623 "until", 3624 "while", 3625 "of", 3626 "at", 3627 "by", 3628 "for", 3629 "with", 3630 "about", 3631 "against", 3632 "between", 3633 "into", 3634 "through", 3635 "during", 3636 "before", 3637 "after", 3638 "above", 3639 "below", 3640 "to", 3641 "from", 3642 "up", 3643 "down", 3644 "in", 3645 "out", 3646 "on", 3647 "off", 3648 "over", 3649 "under", 3650 "again", 3651 "further", 3652 "then", 3653 "once", 3654 "here", 3655 "there", 3656 "when", 3657 "where", 3658 "why", 3659 "how", 3660 "all", 3661 "any", 3662 "both", 3663 "each", 3664 "few", 3665 "more", 3666 "most", 3667 "other", 3668 "some", 3669 "such", 3670 "no", 3671 "nor", 3672 "not", 3673 "only", 3674 "own", 3675 "same", 3676 "so", 3677 "than", 3678 "too", 3679 "very", 3680 "s", 3681 "t", 3682 "can", 3683 "will", 3684 "just", 3685 "don", 3686 "should", 3687 "now", 3688 "im", 3689 "youre", 3690 "hes", 3691 "shes", 3692 "its", 3693 "were", 3694 "theyre", 3695 "ive", 3696 "youve", 3697 "weve", 3698 "theyve", 3699 "id", 3700 "youd", 3701 "hed", 3702 "shed", 3703 "wed", 3704 "theyd", 3705 "ill", 3706 "youll", 3707 "hell", 3708 "shell", 3709 "well", 3710 "theyll", 3711 "cant", 3712 "dont", 3713 "didnt", 3714 "wasnt", 3715 "werent", 3716 "havent", 3717 "hasnt", 3718 "hadnt", 3719 "isnt", 3720 "arent", 3721 "wouldnt", 3722 "shouldnt", 3723 "couldnt", 3724 "doesnt", 3725 // Common Verbs, Nouns, Adjectives, Adverbs in lyrics 3726 "love", 3727 "loved", 3728 "loves", 3729 "loving", 3730 "hate", 3731 "hated", 3732 "hating", 3733 "like", 3734 "liked", 3735 "likes", 3736 "liking", 3737 "want", 3738 "wants", 3739 "wanted", 3740 "wanting", 3741 "need", 3742 "needs", 3743 "needed", 3744 "needing", 3745 "feel", 3746 "feels", 3747 "felt", 3748 "feeling", 3749 "say", 3750 "says", 3751 "said", 3752 "saying", 3753 "tell", 3754 "tells", 3755 "told", 3756 "telling", 3757 "think", 3758 "thinks", 3759 "thought", 3760 "thinking", 3761 "know", 3762 "knows", 3763 "knew", 3764 "known", 3765 "knowing", 3766 "see", 3767 "sees", 3768 "saw", 3769 "seen", 3770 "seeing", 3771 "hear", 3772 "hears", 3773 "heard", 3774 "hearing", 3775 "listen", 3776 "listened", 3777 "listening", 3778 "look", 3779 "looks", 3780 "looked", 3781 "looking", 3782 "make", 3783 "makes", 3784 "made", 3785 "making", 3786 "take", 3787 "takes", 3788 "took", 3789 "taken", 3790 "taking", 3791 "give", 3792 "gives", 3793 "gave", 3794 "given", 3795 "giving", 3796 "go", 3797 "goes", 3798 "went", 3799 "gone", 3800 "going", 3801 "come", 3802 "comes", 3803 "came", 3804 "coming", 3805 "find", 3806 "finds", 3807 "found", 3808 "finding", 3809 "keep", 3810 "keeps", 3811 "kept", 3812 "keeping", 3813 "hold", 3814 "holds", 3815 "held", 3816 "holding", 3817 "turn", 3818 "turns", 3819 "turned", 3820 "turning", 3821 "walk", 3822 "walks", 3823 "walked", 3824 "walking", 3825 "run", 3826 "runs", 3827 "ran", 3828 "running", 3829 "sing", 3830 "sings", 3831 "sang", 3832 "sung", 3833 "singing", 3834 "dance", 3835 "dances", 3836 "danced", 3837 "dancing", 3838 "play", 3839 "plays", 3840 "played", 3841 "playing", 3842 "work", 3843 "works", 3844 "worked", 3845 "working", 3846 "live", 3847 "lives", 3848 "lived", 3849 "living", 3850 "die", 3851 "dies", 3852 "died", 3853 "dying", 3854 "dead", 3855 "death", 3856 "hope", 3857 "hopes", 3858 "hoped", 3859 "hoping", 3860 "wish", 3861 "wishes", 3862 "wished", 3863 "wishing", 3864 "dream", 3865 "dreams", 3866 "dreamed", 3867 "dreaming", 3868 "call", 3869 "calls", 3870 "called", 3871 "calling", 3872 "speak", 3873 "speaks", 3874 "spoke", 3875 "spoken", 3876 "speaking", 3877 "talk", 3878 "talks", 3879 "talked", 3880 "talking", 3881 "cry", 3882 "cries", 3883 "cried", 3884 "crying", 3885 "laugh", 3886 "laughs", 3887 "laughed", 3888 "laughing", 3889 "smile", 3890 "smiles", 3891 "smiled", 3892 "smiling", 3893 "wait", 3894 "waits", 3895 "waited", 3896 "waiting", 3897 "stay", 3898 "stays", 3899 "stayed", 3900 "staying", 3901 "leave", 3902 "leaves", 3903 "left", 3904 "leaving", 3905 "stop", 3906 "stops", 3907 "stopped", 3908 "stopping", 3909 "start", 3910 "starts", 3911 "started", 3912 "starting", 3913 "begin", 3914 "begins", 3915 "begun", 3916 "began", 3917 "beginning", 3918 "try", 3919 "tries", 3920 "tried", 3921 "trying", 3922 "help", 3923 "helps", 3924 "helped", 3925 "helping", 3926 "show", 3927 "shows", 3928 "showed", 3929 "shown", 3930 "showing", 3931 "bring", 3932 "brings", 3933 "brought", 3934 "bringing", 3935 "carry", 3936 "carries", 3937 "carried", 3938 "carrying", 3939 "fall", 3940 "falls", 3941 "fell", 3942 "fallen", 3943 "falling", 3944 "rise", 3945 "rises", 3946 "rose", 3947 "risen", 3948 "rising", 3949 "stand", 3950 "stands", 3951 "stood", 3952 "standing", 3953 "sit", 3954 "sits", 3955 "sat", 3956 "sitting", 3957 "lose", 3958 "loses", 3959 "lost", 3960 "losing", 3961 "win", 3962 "wins", 3963 "won", 3964 "winning", 3965 "break", 3966 "breaks", 3967 "broke", 3968 "broken", 3969 "breaking", 3970 "build", 3971 "builds", 3972 "built", 3973 "building", 3974 "burn", 3975 "burns", 3976 "burnt", 3977 "burned", 3978 "burning", 3979 "cut", 3980 "cuts", 3981 "cutting", 3982 "draw", 3983 "draws", 3984 "drew", 3985 "drawn", 3986 "drawing", 3987 "drive", 3988 "drives", 3989 "drove", 3990 "driven", 3991 "driving", 3992 "eat", 3993 "eats", 3994 "ate", 3995 "eaten", 3996 "eating", 3997 "drink", 3998 "drinks", 3999 "drank", 4000 "drunk", 4001 "drinking", 4002 "fly", 4003 "flies", 4004 "flew", 4005 "flown", 4006 "flying", 4007 "grow", 4008 "grows", 4009 "grew", 4010 "grown", 4011 "growing", 4012 "growl", 4013 "growls", 4014 "growled", 4015 "growling", 4016 "hide", 4017 "hides", 4018 "hid", 4019 "hidden", 4020 "hiding", 4021 "hit", 4022 "hits", 4023 "hitting", 4024 "hurt", 4025 "hurts", 4026 "hurting", 4027 "jump", 4028 "jumps", 4029 "jumped", 4030 "jumping", 4031 "lead", 4032 "leads", 4033 "led", 4034 "leading", 4035 "ride", 4036 "rides", 4037 "rode", 4038 "ridden", 4039 "riding", 4040 "shine", 4041 "shines", 4042 "shone", 4043 "shining", 4044 "shut", 4045 "shuts", 4046 "shutting", 4047 "sleep", 4048 "sleeps", 4049 "slept", 4050 "sleeping", 4051 "spend", 4052 "spends", 4053 "spent", 4054 "spending", 4055 "steal", 4056 "steals", 4057 "stole", 4058 "stolen", 4059 "stealing", 4060 "throw", 4061 "throws", 4062 "threw", 4063 "thrown", 4064 "throwing", 4065 "write", 4066 "writes", 4067 "wrote", 4068 "written", 4069 "writing", 4070 "speak", 4071 "speaks", 4072 "spoke", 4073 "spoken", 4074 "speaking", 4075 "apologize", 4076 "sorry", 4077 "excuse", 4078 "forgive", 4079 "forgiven", 4080 "forgiving", 4081 "please", 4082 "thanks", 4083 "thank", 4084 "welcome", 4085 // Nouns & Adjectives (Common in songs) 4086 "heart", 4087 "hearts", 4088 "mind", 4089 "minds", 4090 "soul", 4091 "souls", 4092 "body", 4093 "bodies", 4094 "life", 4095 "lives",
4096 "world", 4097 "worlds", 4098 "earth", 4099 "sky", 4100 "skies", 4101 "sun", 4102 "moon", 4103 "star", 4104 "stars", 4105 "cloud", 4106 "clouds", 4107 "rain", 4108 "wind", 4109 "snow", 4110 "storm", 4111 "fire", 4112 "fires", 4113 "water", 4114 "sea", 4115 "ocean", 4116 "river", 4117 "lake", 4118 "stone", 4119 "stones", 4120 "rock", 4121 "rocks", 4122 "sand", 4123 "dust", 4124 "light", 4125 "lights", 4126 "dark", 4127 "darkness", 4128 "shadow", 4129 "shadows", 4130 "night", 4131 "nights", 4132 "day", 4133 "days", 4134 "morning", 4135 "evening", 4136 "afternoon", 4137 "noon", 4138 "midnight", 4139 "time", 4140 "times", 4141 "year", 4142 "years", 4143 "month", 4144 "months", 4145 "week", 4146 "weeks", 4147 "hour", 4148 "hours", 4149 "minute", 4150 "minutes", 4151 "second", 4152 "seconds", 4153 "moment", 4154 "moments", 4155 "forever", 4156 "always", 4157 "never", 4158 "sometimes", 4159 "today", 4160 "yesterday", 4161 "tomorrow", 4162 "tonight", 4163 "home", 4164 "house", 4165 "room", 4166 "door", 4167 "window", 4168 "street", 4169 "road", 4170 "city", 4171 "town", 4172 "country", 4173 "place", 4174 "places", 4175 "space", 4176 "sky", 4177 "skies", 4178 "heaven", 4179 "hell", 4180 "angel", 4181 "angels", 4182 "devil", 4183 "devils", 4184 "god", 4185 "gods", 4186 "man", 4187 "men",
4188 "woman", 4189 "women", 4190 "boy", 4191 "boys", 4192 "girl", 4193 "girls", 4194 "child", 4195 "children", 4196 "baby", 4197 "babies", 4198 "friend", 4199 "friends", 4200 "enemy", 4201 "enemies", 4202 "family", 4203 "mother", 4204 "father", 4205 "brother", 4206 "sister", 4207 "son", 4208 "daughter", 4209 "king", 4210 "queen", 4211 "head", 4212 "face", 4213 "eye", 4214 "eyes", 4215 "ear", 4216 "ears", 4217 "nose", 4218 "mouth", 4219 "lip", 4220 "lips", 4221 "teeth", 4222 "tooth", 4223 "hair", 4224 "hand", 4225 "hands", 4226 "arm", 4227 "arms", 4228 "leg", 4229 "legs", 4230 "foot", 4231 "feet", 4232 "finger", 4233 "fingers", 4234 "toe", 4235 "toes", 4236 "back", 4237 "chest", 4238 "shoulder", 4239 "shoulders", 4240 "fire", 4241 "red", 4242 "blue", 4243 "green", 4244 "yellow", 4245 "orange", 4246 "purple", 4247 "pink", 4248 "brown", 4249 "black", 4250 "white", 4251 "gray", 4252 "grey", 4253 "silver", 4254 "gold", 4255 "golden", 4256 "bright", 4257 "clear", 4258 "cloudy", 4259 "sunny", 4260 "windy", 4261 "rainy", 4262 "stormy", 4263 "cold", 4264 "hot", 4265 "warm", 4266 "cool", 4267 "dry", 4268 "wet", 4269 "dirty", 4270 "clean", 4271 "soft", 4272 "hard", 4273 "loud", 4274 "quiet", 4275 "silent", 4276 "silence", 4277 "sound", 4278 "sounds", 4279 "voice", 4280 "voices", 4281 "noise", 4282 "noises", 4283 "sweet", 4284 "sour", 4285 "bitter", 4286 "salty", 4287 "happy", 4288 "sad", 4289 "angry", 4290 "scared", 4291 "afraid", 4292 "fear", 4293 "brave", 4294 "weak", 4295 "strong", 4296 "power", 4297 "heavy", 4298 "light", 4299 "fast", 4300 "slow", 4301 "quick", 4302 "rapid", 4303 "sudden", 4304 "suddenly", 4305 "slowly", 4306 "quickly", 4307 "high", 4308 "low", 4309 "deep", 4310 "shallow", 4311 "tall", 4312 "short", 4313 "long", 4314 "wide", 4315 "narrow", 4316 "thick", 4317 "thin", 4318 "fat", 4319 "slim", 4320 "skinny", 4321 "young", 4322 "old", 4323 "new", 4324 "ancient", 4325 "modern", 4326 "future", 4327 "past", 4328 "present", 4329 "true", 4330 "truth", 4331 "false", 4332 "lie", 4333 "lies", 4334 "beautiful", 4335 "pretty", 4336 "ugly", 4337 "cute", 4338 "nice", 4339 "kind", 4340 "mean", 4341 "cruel", 4342 "gentle", 4343 "rough", 4344 "smooth", 4345 "rope", 4346 "ropes", 4347 "ground", 4348 "pain", 4349 "hurt", 4350 "ache", 4351 "sickness", 4352 "healthy", 4353 "sick", 4354 "ill", 4355 "well", 4356 "fine", 4357 "good", 4358 "bad", 4359 "better", 4360 "best", 4361 "worse", 4362 "worst", 4363 "more", 4364 "most", 4365 "less", 4366 "least", 4367 "little", 4368 "few", 4369 "many", 4370 "much", 4371 "lot", 4372 "lots", 4373 "plenty", 4374 "some", 4375 "several", 4376 "all", 4377 "every", 4378 "each", 4379 "any", 4380 "no", 4381 "none", 4382 "nothing", 4383 "something", 4384 "anything", 4385 "everything", 4386 "someone", 4387 "somebody", 4388 "everyone", 4389 "everybody", 4390 "anyone", 4391 "anybody", 4392 "nobody", 4393 "noone", 4394 "nowhere", 4395 "somewhere", 4396 "everywhere", 4397 "anywhere", 4398 "here", 4399 "there", 4400 "where", 4401 "why", 4402 "how", 4403 "what", 4404 "which", 4405 "who", 4406 "whom", 4407 "whose", 4408 "when", 4409 "whenever", 4410 "wherever", 4411 "however", 4412 "another", 4413 "nother" 4414]; 4415const COMMON_WORDS = new Set(COMMON_WORDS_LIST.map((w) => w.toLowerCase())); 4416const memo = {}; 4417function segmentWord(word) { 4418 const w = word.toLowerCase().trim(); 4419 if (!w) return null; 4420 if (COMMON_WORDS.has(w)) { 4421 return [word]; 4422 } 4423 if (w.length <= 3) { 4424 return null; 4425 } 4426 if (memo[w] !== void 0) { 4427 return memo[w]; 4428 } 4429 let bestSplit = null; 4430 if (Object.keys(memo).length > 5e3) { 4431 for (const key in memo) { 4432 delete memo[key]; 4433 } 4434 } 4435 for (let i = 2; i <= w.length - 2; i++) { 4436 const leftLower = w.substring(0, i); 4437 const rightLower = w.substring(i); 4438 if (COMMON_WORDS.has(leftLower)) { 4439 const leftOrig = word.substring(0, i); 4440 const rightOrig = word.substring(i); 4441 let rightSplit = null; 4442 if (COMMON_WORDS.has(rightLower)) { 4443 rightSplit = [rightOrig]; 4444 } else { 4445 rightSplit = segmentWord(rightOrig); 4446 } 4447 if (rightSplit) { 4448 const currentSplit = [leftOrig, ...rightSplit]; 4449 if (!bestSplit || currentSplit.length < bestSplit.length) { 4450 bestSplit = currentSplit; 4451 } 4452 } 4453 } 4454 } 4455 memo[w] = bestSplit; 4456 return bestSplit; 4457} 4458function splitGluedWords(txt) { 4459 if (!txt) return txt; 4460 let s = txt; 4461 s = s.replace(/I'mHear/g, "I'm Hear"); 4462 s = s.replace(/imhear/gi, "I'm hear"); 4463 s = s.replace(/firered/gi, "fire red"); 4464 s = s.replace(/inblue/gi, "in blue"); 4465 s = s.replace(/yousay/gi, "you say"); 4466 s = s.replace(/yo u/gi, "you"); 4467 s = s.replace(/apologilt's/gi, "apologize It's"); 4468 s = s.replace(/apologiit's/gi, "apologize It's"); 4469 s = s.replace(/apologiIt's/g, "apologize It's"); 4470 s = s.replace(/I'm([A-Z])/g, "I'm $1"); 4471 s = s.replace(/it's([A-Z])/gi, "it's $1"); 4472 s = s.replace(/don't([A-Z])/gi, "don't $1"); 4473 s = s.replace(/didn't([A-Z])/gi, "didn't $1"); 4474 const wordRegex = /[A-Za-z]+('[A-Za-z]+)?/g; 4475 return s.replace(wordRegex, (match) => { 4476 if (match.includes("'")) { 4477 match.split("'"); 4478 return match; 4479 } 4480 const camelSplit = match.replace(/([a-z])([A-Z])/g, "$1 $2"); 4481 if (camelSplit !== match) { 4482 return camelSplit.split(" ").map((w) => { 4483 const segmented2 = segmentWord(w); 4484 return segmented2 ? segmented2.join(" ") : w; 4485 }).join(" "); 4486 } 4487 const segmented = segmentWord(match); 4488 if (segmented && segmented.length > 1) { 4489 return segmented.join(" "); 4490 } 4491 return match; 4492 }); 4493} 4494function shouldJoinSyllables(prev, curr) { 4495 const p = prev.toLowerCase().trim().replace(/['"âââ]/g, ""); 4496 const c = curr.toLowerCase().trim().replace(/['"âââ]/g, ""); 4497 if (!p || !c) return false;
4498 if (COMMON_WORDS.has(p + c)) { 4499 return true; 4500 } 4501 const commonCompleteWords = /^(i|im|id|ive|you|your|he|she|it|its|we|they|them|me|us|him|her|and|or|but|in|on|at|to|for|of|with|by|from|up|down|off|out|over|under|the|a|an|this|that|these|those|some|any|no|not|yes|ok|okay|too|very|ten|feet|fire|red|blue|say|says|said|hear|hears|heard|hearin|tell|tells|told|think|thinks|thought|sorry|apologize|another|nother)$/; 4502 if (commonCompleteWords.test(p)) return false; 4503 const prefixes = /^(hol|gro|hea|sor|di|apo|lo|gi|fi|an|be|de|re|con|pro|ex|im|un|pre|se|al|ar|as|at|ob|op|tra|trans|in|un|dis|mis|over|under|sub|super|inter|multi|wan|per|son|rev|o|lu|tion|ab|ac|ad|af|ag|al|an|ap|as|at|co|col|com|cor|dif|dis|ef|em|en|ep|es|ex|il|im|in|ir|ne|non|ob|oc|of|op|pe|poly|post|sub|suf|sug|sup|sur|sus|sym|syn|tele|tri|ultra|uni|vi|vo|shout|ech|whis|pers|con|ver|sa)$/; 4504 const suffixes = /^(ding|und|in'|ry|nd't|ze|re|gel|ven|y|ly|ing|ed|es|er|est|s|tion|sion|ment|ness|less|ful|able|ible|al|ic|ous|ive|ist|ism|ize|ise|ate|fy|en|some|ward|wise|na|per|son|rev|o|lu|tion|la|gy|gies|t|d|m|ve|ll|re|s|o|cho|pers|sa|ces|ross|g)$/; 4505 if (prefixes.test(p) || suffixes.test(c)) return true; 4506 return false; 4507} 4508const cleanGluedWords$3 = (txt) => { 4509 return splitGluedWords(txt); 4510}; 4511const LyricsHighway = ({ 4512 track, 4513 currentTime, 4514 melodyData, 4515 pianoRollData, 4516 isPlaying, 4517 isMicEnabled, 4518 setIsMicEnabled, 4519 score, 4520 maxScore, 4521 hitFlash, 4522 currentVolume, 4523 devices, 4524 selectedDevice, 4525 setSelectedDevice 4526}) => { 4527 var _a; 4528 const { t } = useTranslation(); 4529 const rawLyrics = ((_a = track.midiMetadata) == null ? void 0 : _a.lyrics) || []; 4530 const highwayData = reactExports.useMemo(() => { 4531 if (rawLyrics.length === 0) return []; 4532 let pitchSourceNotes = []; 4533 let bestTrackNotes = []; 4534 let maxAlignments = 0; 4535 if (pianoRollData && pianoRollData.length > 0) { 4536 const byTrack = /* @__PURE__ */ new Map(); 4537 pianoRollData.forEach((note) => { 4538 const tid = note.i ?? "default"; 4539 if (!byTrack.has(tid)) byTrack.set(tid, []); 4540 byTrack.get(tid).push(note); 4541 }); 4542 byTrack.forEach((notes) => { 4543 let alignments = 0; 4544 rawLyrics.forEach((lyric) => { 4545 const match = notes.find((n) => Math.abs(n.t - lyric.t) < 0.3); 4546 if (match) alignments++; 4547 }); 4548 if (alignments > maxAlignments) { 4549 maxAlignments = alignments; 4550 bestTrackNotes = notes.map((n) => ({ start: n.t, duration: n.d, midi: n.n })); 4551 } 4552 }); 4553 } 4554 if (maxAlignments > rawLyrics.length * 0.15) { 4555 pitchSourceNotes = bestTrackNotes; 4556 } else if (melodyData && melodyData.length > 0) { 4557 pitchSourceNotes = melodyData; 4558 } 4559 const blocks = []; 4560 let minMidi = 127; 4561 let maxMidi = 0; 4562 rawLyrics.forEach((syl, i) => { 4563 const nextSyl = rawLyrics[i + 1]; 4564 let duration = nextSyl ? Math.min(nextSyl.t - syl.t, 1) : 1; 4565 let midi = 60; 4566 const match = pitchSourceNotes.find((n) => n.start >= syl.t - 0.2 && n.start <= syl.t + 0.2); 4567 if (match) { 4568 midi = match.midi; 4569 duration = match.duration; 4570 } 4571 let txt = syl.txt.replace(/^[\/\\]/, "").replace(/[\n\r]/g, "").replace(/`/g, " ").trim(); 4572 txt = cleanGluedWords$3(txt); 4573 if (txt) { 4574 if (midi < minMidi) minMidi = midi; 4575 if (midi > maxMidi) maxMidi = midi; 4576 blocks.push({ 4577 id: i, 4578 txt, 4579 start: syl.t, 4580 duration, 4581 midi 4582 }); 4583 } 4584 }); 4585 if (maxMidi - minMidi < 12) { 4586 const mid = (maxMidi + minMidi) / 2; 4587 minMidi = mid - 6; 4588 maxMidi = mid + 6; 4589 } 4590 return blocks.map((b) => ({ 4591 ...b, 4592 normalizedPitch: Math.max(0, Math.min(1, (b.midi - minMidi) / (maxMidi - minMidi || 1))) 4593 })); 4594 }, [rawLyrics, melodyData, pianoRollData]); 4595 const SING_LINE_PCT = 20; 4596 const TIME_WINDOW_SEC = 10; 4597 const PIXELS_PER_SEC = 80 / TIME_WINDOW_SEC; 4598 if (highwayData.length === 0) { 4599 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full bg-[#111] text-zinc-500 font-serif italic text-lg", children: t("karaoke.noLyrics") }); 4600 } 4601 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-full bg-[#111] overflow-hidden relative w-full", children: [
4602 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-4 left-4 right-4 z-50 flex items-center justify-between pointer-events-none", children: isMicEnabled && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col items-end pointer-events-auto ml-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-3xl font-black font-mono text-white drop-shadow-[0_2px_10px_rgba(0,0,0,0.8)]", children: score.toLocaleString() }) }) }), 4603 hitFlash !== "none" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute top-20 left-1/2 -translate-x-1/2 z-50 text-4xl font-black italic uppercase tracking-wider animate-in fade-in zoom-in-50 duration-150 drop-shadow-[0_0_20px_rgba(0,0,0,0.8)] ${hitFlash === "perfect" ? "text-amber-400" : hitFlash === "good" ? "text-emerald-400" : hitFlash === "great" ? "text-blue-400" : "text-rose-500"}`, children: hitFlash }), 4604 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 left-0 right-0 h-2 bg-zinc-900 z-50", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-full bg-gradient-to-r from-emerald-400 to-indigo-500 transition-all", style: { width: `${maxScore > 0 ? score / (maxScore * 2) * 100 : 0}%` } }) }), 4605 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full h-[60%] border-y border-zinc-800 bg-[#0a0a0a] overflow-hidden", children: [
4606 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex flex-col justify-between opacity-10 pointer-events-none", children: [...Array(5)].map((_, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-px bg-white" }, i)) }), 4607 /* @__PURE__ */ jsxRuntimeExports.jsx( 4608 "div", 4609 { 4610 className: "absolute top-0 bottom-0 w-1 bg-gradient-to-b from-transparent via-amber-400 to-transparent z-40 drop-shadow-[0_0_8px_rgba(251,191,36,0.8)]", 4611 style: { left: `${SING_LINE_PCT}%` } 4612 } 4613 ), 4614 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0", children: highwayData.map((block) => { 4615 const timeDiff = block.start - currentTime; 4616 const leftPct = SING_LINE_PCT + timeDiff * PIXELS_PER_SEC; 4617 const widthPct = block.duration * PIXELS_PER_SEC; 4618 if (leftPct + widthPct < -10 || leftPct > 110) return null; 4619 const isSung = timeDiff + block.duration < 0; 4620 const isCurrent = timeDiff <= 0 && timeDiff + block.duration >= 0; 4621 const bottomPct = 10 + block.normalizedPitch * 70; 4622 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 4623 "div", 4624 { 4625 className: `absolute h-8 sm:h-10 rounded-full flex items-center justify-center font-bold px-3 transition-colors duration-200 overflow-hidden shadow-lg ${isSung ? "bg-zinc-700/50 text-zinc-500 border border-zinc-600" : isCurrent ? "bg-amber-500 text-black border-2 border-amber-300 drop-shadow-[0_0_15px_rgba(251,191,36,0.6)] scale-110 z-30" : "bg-indigo-600 text-white border border-indigo-400 drop-shadow-md z-20"}`, 4626 style: { 4627 left: `${leftPct}%`, 4628 width: `${Math.max(widthPct, 2)}%`, 4629 // Give a min width so very short notes don't disappear 4630 minWidth: "fit-content", 4631 bottom: `${bottomPct}%`, 4632 whiteSpace: "nowrap" 4633 }, 4634 children: [ 4635 isCurrent && /* @__PURE__ */ jsxRuntimeExports.jsx( 4636 "div", 4637 { 4638 className: "absolute left-0 top-0 bottom-0 bg-white/30", 4639 style: { width: `${Math.max(0, Math.min(100, -timeDiff / block.duration * 100))}%` } 4640 } 4641 ),
4642 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "relative z-10 truncate px-2", children: block.txt }) 4643 ] 4644 }, 4645 block.id 4646 ); 4647 }) }) 4648 ] }), 4649 isMicEnabled && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute right-6 top-1/2 -translate-y-1/2 w-4 h-64 bg-zinc-900 rounded-full border border-zinc-800 overflow-hidden flex flex-col justify-end z-50", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 4650 "div", 4651 { 4652 className: "w-full bg-gradient-to-t from-emerald-400 via-amber-400 to-rose-500 transition-all duration-75 ease-out", 4653 style: { height: `${Math.min(100, currentVolume * 200)}%` } 4654 } 4655 ) }), 4656 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-6 left-6 z-50 flex items-center gap-2 bg-black/80 backdrop-blur p-2 rounded-2xl border border-zinc-800 shadow-2xl", children: [ 4657 /* @__PURE__ */ jsxRuntimeExports.jsx( 4658 "button", 4659 { 4660 onClick: () => setIsMicEnabled(!isMicEnabled), 4661 className: `w-12 h-12 flex items-center justify-center rounded-xl transition-all ${isMicEnabled ? "bg-emerald-500/20 text-emerald-400" : "bg-zinc-800 text-zinc-400 hover:text-white"}`, 4662 children: isMicEnabled ? /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 24 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(MicOff, { size: 24 }) 4663 } 4664 ), 4665 isMicEnabled && devices.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col ml-2", children: [
4666 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] uppercase font-bold text-zinc-500 tracking-wider mb-1", children: "Input Device" }), 4667 /* @__PURE__ */ jsxRuntimeExports.jsx( 4668 "select", 4669 { 4670 value: selectedDevice, 4671 onChange: (e) => setSelectedDevice(e.target.value), 4672 className: "bg-zinc-900 border border-zinc-700 text-xs text-white rounded-lg px-2 py-1 max-w-[200px] outline-none", 4673 children: devices.map((d) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: d.deviceId, children: d.label || `Mic ${d.deviceId.slice(0, 4)}` }, d.deviceId)) 4674 } 4675 ) 4676 ] }) 4677 ] }) 4678 ] }); 4679}; 4680const cleanGluedWords$2 = (txt) => { 4681 return splitGluedWords(txt); 4682}; 4683const shouldAddSpace$2 = (prev, curr) => { 4684 if (!prev || !curr) return false; 4685 if (prev.endsWith(" ") || prev.endsWith("Â ") || curr.startsWith(" ") || curr.startsWith("Â ")) return false; 4686 if (shouldJoinSyllables(prev, curr)) return false; 4687 return true; 4688}; 4689const isCapitalizedStart$2 = (txt) => { 4690 const clean = txt.trim(); 4691 if (!/^[A-Z]/.test(clean)) return false; 4692 if (/^[I]($|'|m$)/.test(clean)) return true; 4693 if (clean.length === 1) return false; 4694 return true; 4695}; 4696const groupLyricsIntoLines$2 = (lyrics) => { 4697 const preprocessed = []; 4698 for (let i = 0; i < lyrics.length; i++) { 4699 const syl = lyrics[i]; 4700 const cleaned = cleanGluedWords$2(syl.txt); 4701 const parts = cleaned.split(" "); 4702 if (parts.length > 1) { 4703 parts.forEach((part, idx) => { 4704 preprocessed.push({ 4705 t: syl.t + idx * 0.01, 4706 txt: part 4707 }); 4708 }); 4709 } else { 4710 preprocessed.push({ 4711 t: syl.t, 4712 txt: cleaned 4713 }); 4714 } 4715 } 4716 const lines = []; 4717 let currentLine = []; 4718 for (let i = 0; i < preprocessed.length; i++) { 4719 const syl = preprocessed[i]; 4720 let txt = syl.txt; 4721 const isNewLine = txt.startsWith("/") || txt.startsWith("\\") || txt.includes("\n") || txt.includes("\r"); 4722 if (isNewLine && currentLine.length > 0) { 4723 lines.push({ 4724 startTime: currentLine[0].t, 4725 endTime: currentLine[currentLine.length - 1].t, 4726 syllables: currentLine 4727 }); 4728 currentLine = []; 4729 } 4730 txt = txt.replace(/^[\/\\]/, "").replace(/[\n\r]/g, "").replace(/`/g, " "); 4731 if (currentLine.length > 0) { 4732 const prevSyl = currentLine[currentLine.length - 1]; 4733 if (shouldAddSpace$2(prevSyl.txt, txt)) { 4734 txt = " " + txt; 4735 } 4736 } 4737 currentLine.push({ t: syl.t, txt }); 4738 if (i < preprocessed.length - 1) { 4739 const nextSyl = preprocessed[i + 1]; 4740 const cleanNextTxt = nextSyl.txt.replace(/^[\/\\]/, "").replace(/[\n\r]/g, "").replace(/`/g, " "); 4741 const hasGap = nextSyl.t - syl.t > 2.5; 4742 const isCapitalized = isCapitalizedStart$2(cleanNextTxt); 4743 const isJustI = /^[I]$/.test(cleanNextTxt.trim()); 4744 const shouldBreakOnCapital = isCapitalized && !isJustI && currentLine.length >= 1; 4745 const isWordBoundary = cleanNextTxt.startsWith(" ") || shouldAddSpace$2(txt, cleanNextTxt); 4746 const isTooLong = currentLine.length >= 28 && isWordBoundary; 4747 if ((hasGap || shouldBreakOnCapital || isTooLong) && currentLine.length > 0) { 4748 lines.push({ 4749 startTime: currentLine[0].t, 4750 endTime: currentLine[currentLine.length - 1].t, 4751 syllables: currentLine 4752 }); 4753 currentLine = []; 4754 } 4755 } 4756 } 4757 if (currentLine.length > 0) { 4758 lines.push({ 4759 startTime: currentLine[0].t, 4760 endTime: currentLine[currentLine.length - 1].t, 4761 syllables: currentLine 4762 }); 4763 } 4764 return lines; 4765}; 4766const SingThroughLyrics = ({ track, currentTime, melodyData, pianoRollData, isPlaying }) => { 4767 var _a; 4768 const { t } = useTranslation(); 4769 const rawLyrics = ((_a = track.midiMetadata) == null ? void 0 : _a.lyrics) || []; 4770 const lines = reactExports.useMemo(() => groupLyricsIntoLines$2(rawLyrics), [rawLyrics]); 4771 const syllableNotes = reactExports.useMemo(() => { 4772 let bestTrackNotes = []; 4773 let maxAlignments = 0; 4774 if (pianoRollData && pianoRollData.length > 0) { 4775 pianoRollData.forEach((pTrack) => { 4776 if (!pTrack.notes) return; 4777 let alignments = 0; 4778 rawLyrics.forEach((lyric) => { 4779 const match = pTrack.notes.find((n) => Math.abs(n.t - lyric.t) < 0.2); 4780 if (match) alignments++; 4781 }); 4782 if (alignments > maxAlignments) { 4783 maxAlignments = alignments; 4784 bestTrackNotes = pTrack.notes.map((n) => ({ start: n.t, duration: n.d, midi: n.midi, velocity: n.v })); 4785 } 4786 }); 4787 } 4788 let pitchSourceNotes = bestTrackNotes; 4789 if (maxAlignments < rawLyrics.length * 0.2 && melodyData && melodyData.length > 0) { 4790 pitchSourceNotes = melodyData; 4791 } 4792 const map = /* @__PURE__ */ new Map(); 4793 rawLyrics.forEach((syl) => { 4794 const match = pitchSourceNotes.find((n) => n.start >= syl.t - 0.2 && n.start <= syl.t + 0.2); 4795 if (match) { 4796 map.set(syl.t, match); 4797 } 4798 }); 4799 return map; 4800 }, [rawLyrics, melodyData, pianoRollData]); 4801 if (lines.length === 0) { 4802 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full bg-[#111] text-zinc-500 font-serif italic text-lg", children: t("karaoke.noLyrics") }); 4803 } 4804 let activeLineIdx = -1; 4805 for (let i = 0; i < lines.length; i++) { 4806 if (currentTime >= lines[i].startTime && currentTime <= lines[i].endTime) { 4807 activeLineIdx = i; 4808 break; 4809 } 4810 } 4811 if (activeLineIdx === -1) { 4812 for (let i = 0; i < lines.length; i++) { 4813 if (currentTime >= lines[i].startTime - 1.5 && currentTime <= lines[i].endTime + 1) { 4814 activeLineIdx = i; 4815 break; 4816 } 4817 } 4818 } 4819 if (activeLineIdx === -1) { 4820 for (let i = 0; i < lines.length; i++) { 4821 if (lines[i].startTime > currentTime) { 4822 activeLineIdx = i; 4823 break; 4824 } 4825 } 4826 } 4827 if (activeLineIdx === -1) activeLineIdx = lines.length - 1; 4828 const start = Math.max(0, activeLineIdx - 1); 4829 const end = Math.min(lines.length, activeLineIdx + 3); 4830 const visibleLines = lines.slice(start, end); 4831 let currentVelocity = 0; 4832 for (let i = 0; i < lines.length; i++) { 4833 if (currentTime >= lines[i].startTime - 1 && currentTime <= lines[i].endTime + 1) { 4834 for (let idx = 0; idx < lines[i].syllables.length; idx++) { 4835 const syl = lines[i].syllables[idx]; 4836 const note = syllableNotes.get(syl.t); 4837 let duration = note ? note.duration : 0.5; 4838 if (currentTime >= syl.t && currentTime <= syl.t + duration) { 4839 currentVelocity = (note == null ? void 0 : note.velocity) ?? 0.5; 4840 break; 4841 } 4842 } 4843 } 4844 } 4845 const normalizedVelocity = currentVelocity > 1 ? currentVelocity / 127 : currentVelocity; 4846 const pulseOpacity = Math.min(1, normalizedVelocity * 0.8); 4847 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-full bg-[#111] overflow-hidden relative w-full", children: [
4848 /* @__PURE__ */ jsxRuntimeExports.jsx( 4849 "div", 4850 { 4851 className: "absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-indigo-500/30 via-transparent to-transparent transition-opacity duration-100 ease-out", 4852 style: { opacity: isPlaying ? pulseOpacity : 0, transform: `scale(${1 + pulseOpacity * 0.1})` } 4853 } 4854 ), 4855 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-4 sm:space-y-12 text-center w-full max-w-5xl px-3 sm:px-8 z-10", children: visibleLines.map((line, i) => { 4856 const globalIndex = start + i; 4857 const isActiveLine = globalIndex === activeLineIdx; 4858 return /* @__PURE__ */ jsxRuntimeExports.jsx( 4859 "div", 4860 { 4861 className: `transition-all duration-500 leading-tight flex flex-wrap justify-center ${isActiveLine ? "scale-105 opacity-100" : "blur-[2px] scale-95 opacity-30"}`, 4862 children: line.syllables.map((syl, idx) => { 4863 const note = syllableNotes.get(syl.t); 4864 const nextLine = lines[globalIndex + 1]; 4865 const nextLineStart = nextLine ? nextLine.startTime : Infinity; 4866 const defaultNextTime = idx < line.syllables.length - 1 ? line.syllables[idx + 1].t : Math.min(line.endTime + 0.5, nextLineStart); 4867 const duration = note ? note.duration : defaultNextTime - syl.t; 4868 const noteEndTime = syl.t + duration; 4869 const isCurrentWord = currentTime >= syl.t && currentTime < noteEndTime; 4870 const isSung = currentTime >= noteEndTime; 4871 let fillPercentage = 0; 4872 if (isSung) { 4873 fillPercentage = 100; 4874 } else if (isCurrentWord) { 4875 fillPercentage = Math.max(0, Math.min(100, (currentTime - syl.t) / duration * 100)); 4876 } 4877 const isCJK = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\uFAFF\uFF66-\uFF9F]/.test(syl.txt); 4878 const hasSpacePrefix = syl.txt.startsWith(" "); 4879 const isContinuation = !hasSpacePrefix && !isCJK && idx > 0; 4880 const marginClass = isCJK ? "mx-[0.5px]" : isContinuation ? "ml-[0px] mr-[4px]" : "ml-[12px] mr-[4px]"; 4881 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 4882 "span", 4883 { 4884 className: `transition-transform duration-75 inline-block whitespace-nowrap ${marginClass} ${isActiveLine && isCurrentWord ? "scale-105 sm:scale-110 -translate-y-1 sm:-translate-y-2" : ""}`, 4885 style: { 4886 transformOrigin: "bottom center", 4887 position: "relative", 4888 paddingBottom: "8px" 4889 }, 4890 children: [ 4891 hasSpacePrefix && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-2 sm:w-3 inline-block" }),
4892 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "relative inline-block text-2xl sm:text-5xl md:text-7xl font-black tracking-tight", children: [ 4893 /* @__PURE__ */ jsxRuntimeExports.jsx( 4894 "span", 4895 { 4896 className: "opacity-50", 4897 style: { 4898 WebkitTextStroke: "2px rgba(255, 255, 255, 0.4)", 4899 color: "transparent" 4900 }, 4901 children: syl.txt.trim() 4902 } 4903 ), 4904 (fillPercentage > 0 || isSung) && /* @__PURE__ */ jsxRuntimeExports.jsx( 4905 "span", 4906 { 4907 className: `absolute top-0 left-0 bottom-0 overflow-hidden whitespace-nowrap text-white ${!isSung ? "drop-shadow-[0_0_15px_rgba(129,140,248,0.8)]" : "drop-shadow-none text-indigo-200"}`, 4908 style: { 4909 width: `${fillPercentage}%`, 4910 transition: "width 0.05s linear" 4911 }, 4912 children: syl.txt.trim() 4913 } 4914 ) 4915 ] }) 4916 ] 4917 }, 4918 idx 4919 ); 4920 }) 4921 }, 4922 globalIndex 4923 ); 4924 }) }) 4925 ] }); 4926}; 4927const KineticLine = ({ 4928 line, 4929 lineIdx, 4930 lines, 4931 currentTime, 4932 d, 4933 textScale 4934}) => { 4935 const rowRef = reactExports.useRef(null); 4936 const [scaleX, setScaleX] = reactExports.useState(1); 4937 reactExports.useEffect(() => { 4938 if (!rowRef.current) return; 4939 const observer = new ResizeObserver(() => { 4940 if (!rowRef.current || !rowRef.current.parentElement) return; 4941 const containerWidth = rowRef.current.parentElement.clientWidth * 0.95; 4942 const rowWidth = rowRef.current.scrollWidth; 4943 if (rowWidth > containerWidth) { 4944 setScaleX(Math.max(0.55, containerWidth / rowWidth)); 4945 } else { 4946 setScaleX(1); 4947 } 4948 }); 4949 observer.observe(rowRef.current); 4950 return () => observer.disconnect(); 4951 }, [textScale]); 4952 let translateY = 0, scale = 1, opacity = 0, zIndex = 10; 4953 if (d === -1) { 4954 translateY = -85; 4955 scale = 0.75; 4956 opacity = 0.35; 4957 zIndex = 20; 4958 } else if (d === 0) { 4959 translateY = 0; 4960 scale = 1.25; 4961 opacity = 1; 4962 zIndex = 30; 4963 } else if (d === 1) { 4964 translateY = 85; 4965 scale = 0.85; 4966 opacity = 0.45; 4967 zIndex = 20; 4968 } else if (d === -2) { 4969 translateY = -150; 4970 scale = 0.6; 4971 opacity = 0; 4972 zIndex = 10; 4973 } else if (d === 2) { 4974 translateY = 150; 4975 scale = 0.7; 4976 opacity = 0; 4977 zIndex = 10; 4978 } 4979 const baseFontSize = (d === 0 ? 1.5 : 1.1) * textScale; 4980 return /* @__PURE__ */ jsxRuntimeExports.jsx( 4981 "div", 4982 { 4983 className: "absolute left-[5%] right-[5%] w-[90%] flex items-center justify-center text-center px-2 sm:px-6 transition-all duration-700 ease-[cubic-bezier(0.34,1.56,0.64,1)] pointer-events-none", 4984 style: { 4985 transform: `translateY(${translateY}px) scale(${scale})`, 4986 opacity, 4987 zIndex, 4988 transformOrigin: "center center", 4989 willChange: "transform, opacity", 4990 fontSize: `${baseFontSize}rem` 4991 }, 4992 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 4993 "div", 4994 { 4995 ref: rowRef, 4996 className: "flex flex-wrap justify-center items-center", 4997 style: { transformOrigin: "center center", transform: `scaleX(${scaleX})` }, 4998 children: line.syllables.map((syl, idx) => { 4999 const nextLine = lines[lineIdx + 1]; 5000 const nextLineStart = nextLine ? nextLine.startTime : Infinity; 5001 const nextSylTime = idx < line.syllables.length - 1 ? line.syllables[idx + 1].t : Math.min(line.endTime + 0.5, nextLineStart); 5002 const isCurrentSyllable = currentTime >= syl.t && currentTime < nextSylTime; 5003 const isSung = currentTime >= nextSylTime; 5004 const duration = nextSylTime - syl.t; 5005 let fillPct = 0; 5006 if (isSung) fillPct = 100; 5007 else if (isCurrentSyllable) fillPct = Math.max(0, Math.min(100, (currentTime - syl.t) / duration * 100)); 5008 const isCJK = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\uFAFF\uFF66-\uFF9F]/.test(syl.txt); 5009 const hasSpacePrefix = syl.txt.startsWith(" "); 5010 const isContinuation = !hasSpacePrefix && !isCJK && idx > 0; 5011 const marginClass = isCJK ? "mx-[0.5px]" : isContinuation ? "ml-[0px] mr-[2px]" : "ml-[6px] mr-[2px]"; 5012 const glowStyle = d === 0 && isCurrentSyllable ? { textShadow: "0 0 15px rgba(52,211,153,0.8), 0 0 5px rgba(52,211,153,0.5), 0 0 30px rgba(255,255,255,0.25)" } : {}; 5013 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 5014 "span", 5015 { 5016 className: `inline-block whitespace-nowrap ${marginClass} transition-all duration-300 ease-out`, 5017 style: { 5018 transformOrigin: "center bottom", 5019 transform: isCurrentSyllable && d === 0 ? "scale(1.12) translateY(-2px)" : "scale(1)", 5020 willChange: "transform" 5021 }, 5022 children: [
5023 hasSpacePrefix && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-1.5 sm:w-2 inline-block" }), 5024 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "relative inline-block", style: glowStyle, children: [ 5025 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `${d === 0 ? isCurrentSyllable ? "text-emerald-400 font-extrabold" : isSung ? "text-indigo-400 font-bold" : "text-zinc-400 font-medium" : "text-zinc-500 font-normal"} transition-colors duration-200 ease-in-out`, children: syl.txt.trim() }), 5026 d === 0 && fillPct > 0 && !isSung && /* @__PURE__ */ jsxRuntimeExports.jsx( 5027 "span", 5028 { 5029 className: "absolute top-0 left-0 bottom-0 text-white overflow-hidden whitespace-nowrap font-extrabold", 5030 style: { width: `${fillPct}%`, transition: "width 0.05s linear" }, 5031 children: syl.txt.trim() 5032 } 5033 ) 5034 ] }) 5035 ] 5036 }, 5037 idx 5038 ); 5039 }) 5040 } 5041 ) 5042 } 5043 ); 5044}; 5045const LyricsKinetic = ({ lines, currentTime, activeLineIdx, textScale = 1 }) => { 5046 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full h-full flex flex-col items-center justify-center overflow-hidden bg-[#111]/90", children: [ 5047 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 pointer-events-none overflow-hidden", children: [
5048 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -top-40 left-1/4 w-[500px] h-[500px] rounded-full bg-indigo-500/10 blur-[120px] animate-pulse", style: { animationDuration: "8s" } }), 5049 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute -bottom-40 right-1/4 w-[500px] h-[500px] rounded-full bg-emerald-500/10 blur-[120px] animate-pulse", style: { animationDuration: "10s" } }) 5050 ] }), 5051 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "relative w-full max-w-5xl h-[320px] flex items-center justify-center", children: lines.map((line, i) => { 5052 const d = i - activeLineIdx; 5053 if (d < -2 || d > 2) return null; 5054 return /* @__PURE__ */ jsxRuntimeExports.jsx( 5055 KineticLine, 5056 { 5057 line, 5058 lineIdx: i, 5059 lines, 5060 currentTime, 5061 d, 5062 textScale 5063 }, 5064 i 5065 ); 5066 }) }) 5067 ] }); 5068}; 5069const BALL_ICONS = ["glow", "heart", "hamie", "star", "mic", "note", "fire", "diamond", "moon", "pixel"]; 5070const BallSVG = ({ icon, squish, size = 30 }) => { 5071 const scaleY = 1 - squish * 0.4; 5072 const scaleX = 1 + squish * 0.3; 5073 const style = { 5074 transform: `scaleY(${scaleY}) scaleX(${scaleX})`, 5075 display: "inline-block", 5076 transition: squish > 0.01 ? "none" : "transform 0.15s cubic-bezier(0.34,1.56,0.64,1)" 5077 }; 5078 if (icon === "glow") return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 30 30", children: [ 5079 /* @__PURE__ */ jsxRuntimeExports.jsxs("defs", { children: [
5080 /* @__PURE__ */ jsxRuntimeExports.jsxs("radialGradient", { id: "bg1", cx: "40%", cy: "35%", r: "60%", children: [ 5081 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#fff", stopOpacity: "0.9" }), 5082 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "50%", stopColor: "#a5f3fc" }), 5083 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#0ea5e9" }) 5084 ] }), 5085 /* @__PURE__ */ jsxRuntimeExports.jsxs("filter", { id: "gf1", x: "-50%", y: "-50%", width: "200%", height: "200%", children: [ 5086 /* @__PURE__ */ jsxRuntimeExports.jsx("feGaussianBlur", { stdDeviation: "2.5", result: "b" }), 5087 /* @__PURE__ */ jsxRuntimeExports.jsxs("feMerge", { children: [ 5088 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "b" }), 5089 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "SourceGraphic" }) 5090 ] }) 5091 ] }) 5092 ] }), 5093 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "15", cy: "15", r: "12", fill: "url(#bg1)", filter: "url(#gf1)" }), 5094 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "10", cy: "10", rx: "3.5", ry: "2.5", fill: "white", opacity: "0.5" }) 5095 ] }) }); 5096 if (icon === "heart") return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 30 30", children: [ 5097 /* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("filter", { id: "hf", children: [
5098 /* @__PURE__ */ jsxRuntimeExports.jsx("feGaussianBlur", { stdDeviation: "2", result: "b" }), 5099 /* @__PURE__ */ jsxRuntimeExports.jsxs("feMerge", { children: [ 5100 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "b" }), 5101 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "SourceGraphic" }) 5102 ] }) 5103 ] }) }), 5104 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M15 26s-10-7-10-13a5.5 5.5 0 0 1 10-3.1A5.5 5.5 0 0 1 25 13c0 6-10 13-10 13z", fill: "#f43f5e", filter: "url(#hf)" }), 5105 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M10.5 12a2.5 2.5 0 0 1 2.5-2.5", stroke: "white", strokeWidth: "1.2", strokeLinecap: "round", fill: "none", opacity: "0.55" }) 5106 ] }) }); 5107 if (icon === "hamie") return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 30 30", children: [ 5108 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "6", cy: "18", rx: "5.5", ry: "4.5", fill: "#f9a8d4", opacity: "0.8" }), 5109 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "24", cy: "18", rx: "5.5", ry: "4.5", fill: "#f9a8d4", opacity: "0.8" }), 5110 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "15", cy: "16", rx: "10", ry: "9", fill: "#fde68a" }), 5111 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "7.5", cy: "8", rx: "4.5", ry: "4", fill: "#fde68a" }), 5112 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "7.5", cy: "8", rx: "2.8", ry: "2.2", fill: "#fbcfe8" }), 5113 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "22.5", cy: "8", rx: "4.5", ry: "4", fill: "#fde68a" }), 5114 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "22.5", cy: "8", rx: "2.8", ry: "2.2", fill: "#fbcfe8" }), 5115 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "11", cy: "14", r: "2.2", fill: "#1c1917" }), 5116 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "19", cy: "14", r: "2.2", fill: "#1c1917" }), 5117 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "11.6", cy: "13.2", r: "0.7", fill: "white" }), 5118 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "19.6", cy: "13.2", r: "0.7", fill: "white" }), 5119 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "15", cy: "18.5", rx: "1.8", ry: "1.2", fill: "#f472b6" }), 5120 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12.5 20.5 Q15 22.5 17.5 20.5", stroke: "#92400e", strokeWidth: "0.9", fill: "none", strokeLinecap: "round" }) 5121 ] }) }); 5122 if (icon === "star") return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 30 30", children: [ 5123 /* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("filter", { id: "sf", children: [
5124 /* @__PURE__ */ jsxRuntimeExports.jsx("feGaussianBlur", { stdDeviation: "2.5", result: "b" }), 5125 /* @__PURE__ */ jsxRuntimeExports.jsxs("feMerge", { children: [ 5126 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "b" }), 5127 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "SourceGraphic" }) 5128 ] }) 5129 ] }) }), 5130 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "15,3 18.5,11.5 28,11.5 20.5,17.5 23.5,27 15,21.5 6.5,27 9.5,17.5 2,11.5 11.5,11.5", fill: "#fbbf24", filter: "url(#sf)" }), 5131 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "15,7 17,13 23,13 18,16.5 20,22 15,18.5 10,22 12,16.5 7,13 13,13", fill: "#fef08a", opacity: "0.55" }) 5132 ] }) }); 5133 if (icon === "mic") return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 30 30", children: [ 5134 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "11", y: "3", width: "8", height: "14", rx: "4", fill: "#a78bfa" }), 5135 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M6 15a9 9 0 0 0 18 0", stroke: "#7c3aed", strokeWidth: "2.2", fill: "none", strokeLinecap: "round" }), 5136 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "15", y1: "24", x2: "15", y2: "28", stroke: "#7c3aed", strokeWidth: "2", strokeLinecap: "round" }), 5137 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "11", y1: "28", x2: "19", y2: "28", stroke: "#7c3aed", strokeWidth: "2", strokeLinecap: "round" }), 5138 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "13", y: "6", width: "2", height: "4", rx: "1", fill: "white", opacity: "0.5" }) 5139 ] }) }); 5140 if (icon === "note") return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 30 30", children: [ 5141 /* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("filter", { id: "nf", children: [
5142 /* @__PURE__ */ jsxRuntimeExports.jsx("feGaussianBlur", { stdDeviation: "1.5", result: "b" }), 5143 /* @__PURE__ */ jsxRuntimeExports.jsxs("feMerge", { children: [ 5144 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "b" }), 5145 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "SourceGraphic" }) 5146 ] }) 5147 ] }) }), 5148 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { fill: "#34d399", filter: "url(#nf)", children: [ 5149 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "9", cy: "22", rx: "4.5", ry: "3" }), 5150 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "13", y: "5", width: "2.5", height: "17", rx: "1.25" }), 5151 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "13", y: "5", width: "10", height: "2.5", rx: "1.25" }), 5152 /* @__PURE__ */ jsxRuntimeExports.jsx("ellipse", { cx: "21.5", cy: "15", rx: "4.5", ry: "3" }), 5153 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "25.5", y: "5", width: "2.5", height: "10", rx: "1.25" }) 5154 ] }) 5155 ] }) }); 5156 if (icon === "fire") return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 30 30", children: [ 5157 /* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("filter", { id: "ff", children: [
5158 /* @__PURE__ */ jsxRuntimeExports.jsx("feGaussianBlur", { stdDeviation: "2", result: "b" }), 5159 /* @__PURE__ */ jsxRuntimeExports.jsxs("feMerge", { children: [ 5160 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "b" }), 5161 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "SourceGraphic" }) 5162 ] }) 5163 ] }) }), 5164 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M15 28C8.5 28 5 22.5 5 18c0-3.5 2.5-6 2.5-6s0 3.5 3.5 3.5C9.5 11 13 7 15 4c0 5 4.5 6 4.5 10.5C20.5 13 21 11 21 11c2.5 2.5 4 6 4 8.5 0 4.5-4 8.5-10 8.5z", fill: "#f97316", filter: "url(#ff)" }), 5165 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M15 26c-3.5 0-6-3-6-5.5 0-2 1.5-3.5 1.5-3.5s0 2 2 2c-.5-2.5 1.5-4.5 2.5-6 0 2.5 2.5 3 2.5 5.5.5-.5.5-2 .5-2 1.5 1.5 2.5 3 2.5 4.5 0 2.5-2.5 5-5.5 5z", fill: "#fef08a" }) 5166 ] }) }); 5167 if (icon === "diamond") return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 30 30", children: [ 5168 /* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("filter", { id: "df", children: [
5169 /* @__PURE__ */ jsxRuntimeExports.jsx("feGaussianBlur", { stdDeviation: "2.5", result: "b" }), 5170 /* @__PURE__ */ jsxRuntimeExports.jsxs("feMerge", { children: [ 5171 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "b" }), 5172 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "SourceGraphic" }) 5173 ] }) 5174 ] }) }), 5175 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "15,2 26,11 15,28 4,11", fill: "#67e8f9", filter: "url(#df)" }), 5176 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "15,2 26,11 15,16", fill: "#e0f2fe", opacity: "0.65" }), 5177 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "4,11 15,16 15,28", fill: "#0891b2", opacity: "0.55" }), 5178 /* @__PURE__ */ jsxRuntimeExports.jsx("polygon", { points: "15,2 21,11 15,16 9,11", fill: "white", opacity: "0.28" }) 5179 ] }) }); 5180 if (icon === "moon") return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 30 30", children: [ 5181 /* @__PURE__ */ jsxRuntimeExports.jsx("defs", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("filter", { id: "mf", children: [
5182 /* @__PURE__ */ jsxRuntimeExports.jsx("feGaussianBlur", { stdDeviation: "2", result: "b" }), 5183 /* @__PURE__ */ jsxRuntimeExports.jsxs("feMerge", { children: [ 5184 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "b" }), 5185 /* @__PURE__ */ jsxRuntimeExports.jsx("feMergeNode", { in: "SourceGraphic" }) 5186 ] }) 5187 ] }) }), 5188 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M22 15a9 9 0 1 1-9-9 7 7 0 0 0 9 9z", fill: "#fde68a", filter: "url(#mf)" }), 5189 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "20", cy: "9", r: "1.2", fill: "#fef9c3", opacity: "0.8" }), 5190 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "24", cy: "13", r: "0.8", fill: "#fef9c3", opacity: "0.6" }), 5191 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "22", cy: "18", r: "0.6", fill: "#fef9c3", opacity: "0.5" }) 5192 ] }) }); 5193 return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style, children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { width: size, height: size, viewBox: "0 0 30 30", shapeRendering: "crispEdges", children: [ 5194 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "11", y: "4", width: "8", height: "2", fill: "#4ade80" }), 5195 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "9", y: "6", width: "12", height: "2", fill: "#4ade80" }), 5196 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "7", y: "8", width: "16", height: "8", fill: "#4ade80" }), 5197 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "7", y: "8", width: "4", height: "4", fill: "#111" }), 5198 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "19", y: "8", width: "4", height: "4", fill: "#111" }), 5199 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "9", y: "9", width: "2", height: "2", fill: "white" }), 5200 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "19", y: "9", width: "2", height: "2", fill: "white" }), 5201 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "7", y: "16", width: "4", height: "4", fill: "#4ade80" }), 5202 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "19", y: "16", width: "4", height: "4", fill: "#4ade80" }), 5203 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "7", y: "20", width: "4", height: "4", fill: "#4ade80" }), 5204 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "19", y: "20", width: "4", height: "4", fill: "#4ade80" }) 5205 ] }) }); 5206}; 5207const LineRow = ({ 5208 line, 5209 lineIdx, 5210 isActiveLine, 5211 scaledFontSize, 5212 setWordRef, 5213 activeLineIdx, 5214 highlights 5215}) => { 5216 const rowRef = reactExports.useRef(null); 5217 const [scaleX, setScaleX] = reactExports.useState(1); 5218 reactExports.useEffect(() => { 5219 if (!rowRef.current) return; 5220 const observer = new ResizeObserver(() => { 5221 if (!rowRef.current || !rowRef.current.parentElement) return; 5222 const containerWidth = rowRef.current.parentElement.clientWidth * 0.9; 5223 const rowWidth = rowRef.current.scrollWidth; 5224 if (rowWidth > containerWidth) { 5225 setScaleX(Math.max(0.55, containerWidth / rowWidth)); 5226 } else { 5227 setScaleX(1); 5228 } 5229 }); 5230 observer.observe(rowRef.current); 5231 return () => observer.disconnect(); 5232 }, [scaledFontSize]); 5233 return /* @__PURE__ */ jsxRuntimeExports.jsx( 5234 "div", 5235 { 5236 ref: rowRef, 5237 className: "flex flex-wrap justify-center items-baseline leading-relaxed transition-opacity duration-300 w-full", 5238 style: { 5239 fontSize: `${scaledFontSize}rem`, 5240 transform: `scaleX(${scaleX})`, 5241 transformOrigin: "center center", 5242 opacity: isActiveLine ? 1 : 0.35, 5243 fontWeight: isActiveLine ? 800 : 500, 5244 paddingBottom: typeof window !== "undefined" && window.innerWidth < 640 ? "12px" : "40px" 5245 }, 5246 children: line.syllables.map((syl, idx) => { 5247 let fillPct = 0; 5248 if (lineIdx < activeLineIdx) { 5249 fillPct = 100; 5250 } else if (lineIdx > activeLineIdx) { 5251 fillPct = 0; 5252 } else { 5253 fillPct = highlights[`${lineIdx}-${idx}`] || 0; 5254 } 5255 const isCJK = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\uFAFF\uFF66-\uFF9F]/.test(syl.txt); 5256 const hasSpacePrefix = syl.txt.startsWith(" "); 5257 const isContinuation = !hasSpacePrefix && !isCJK && idx > 0; 5258 const marginClass = isCJK ? "mx-[0.5px]" : isContinuation ? "ml-[0px] mr-[2px]" : "ml-[6px] mr-[2px]"; 5259 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 5260 "span", 5261 { 5262 ref: (el) => setWordRef(lineIdx, idx, el), 5263 className: `inline-block whitespace-nowrap ${marginClass}`, 5264 style: { position: "relative", paddingBottom: "4px" }, 5265 children: [
5266 hasSpacePrefix && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "w-1.5 inline-block" }), 5267 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "relative inline-block", children: [ 5268 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: lineIdx === activeLineIdx ? "text-zinc-500" : lineIdx < activeLineIdx ? "text-indigo-400 font-extrabold" : "text-zinc-600", children: syl.txt.trim() }), 5269 lineIdx === activeLineIdx && fillPct > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx( 5270 "span", 5271 { 5272 className: "absolute top-0 left-0 bottom-0 text-indigo-400 font-extrabold overflow-hidden whitespace-nowrap transition-all duration-75", 5273 style: { width: `${fillPct}%` }, 5274 children: syl.txt.trim() 5275 } 5276 ) 5277 ] }) 5278 ] 5279 }, 5280 idx 5281 ); 5282 }) 5283 } 5284 ); 5285}; 5286const LyricsBouncing = ({ 5287 lines, 5288 currentTime, 5289 activeLineIdx, 5290 textScale = 1, 5291 ballIcon = "glow", 5292 onBallIconChange 5293}) => { 5294 const containerRef = reactExports.useRef(null); 5295 const wordRefs = reactExports.useRef(/* @__PURE__ */ new Map()); 5296 const [ballPos, setBallPos] = reactExports.useState({ 5297 x: 0, 5298 y: -60, 5299 opacity: 0, 5300 squish: 0 5301 }); 5302 const [sylHighlights, setSylHighlights] = reactExports.useState({}); 5303 const setWordRef = reactExports.useCallback((lineIdx, sylIdx, el) => { 5304 wordRefs.current.set(`${lineIdx}-${sylIdx}`, el); 5305 }, []); 5306 const flatSyllables = reactExports.useMemo(() => { 5307 const list = []; 5308 lines.forEach((line, lineIdx) => { 5309 line.syllables.forEach((syl, sylIdx) => { 5310 list.push({ 5311 lineIdx, 5312 sylIdx, 5313 t: syl.t, 5314 txt: syl.txt 5315 }); 5316 }); 5317 }); 5318 return list.sort((a, b) => a.t - b.t); 5319 }, [lines]); 5320 const flatSyllableIndices = reactExports.useMemo(() => { 5321 const map = /* @__PURE__ */ new Map(); 5322 flatSyllables.forEach((fs, index) => { 5323 map.set(`${fs.lineIdx}-${fs.sylIdx}`, index); 5324 }); 5325 return map; 5326 }, [flatSyllables]); 5327 const getBallInterpolation = reactExports.useCallback((time, cRect) => { 5328 if (flatSyllables.length === 0) { 5329 return { x: 0, y: 0, opacity: 0, squish: 0, activeLineIdx: -1, ballX: 0 }; 5330 } 5331 if (time < flatSyllables[0].t) { 5332 const first = flatSyllables[0]; 5333 const el = wordRefs.current.get(`${first.lineIdx}-${first.sylIdx}`); 5334 if (!el) return { x: 0, y: 0, opacity: 0, squish: 0, activeLineIdx: first.lineIdx, ballX: 0 }; 5335 const rect = el.getBoundingClientRect(); 5336 const x2 = rect.left + rect.width / 2 - cRect.left; 5337 const y2 = rect.top - cRect.top - 15; 5338 return { x: x2, y: y2, opacity: 1, squish: 0, activeLineIdx: first.lineIdx, ballX: x2 }; 5339 } 5340 if (time >= flatSyllables[flatSyllables.length - 1].t) { 5341 const last = flatSyllables[flatSyllables.length - 1]; 5342 const el = wordRefs.current.get(`${last.lineIdx}-${last.sylIdx}`); 5343 if (!el) return { x: 0, y: 0, opacity: 0, squish: 0, activeLineIdx: last.lineIdx, ballX: 0 }; 5344 const rect = el.getBoundingClientRect(); 5345 const x2 = rect.left + rect.width / 2 - cRect.left; 5346 const y2 = rect.top - cRect.top - 15; 5347 return { x: x2, y: y2, opacity: 1, squish: 0, activeLineIdx: last.lineIdx, ballX: x2 }; 5348 } 5349 let idx = 0; 5350 for (let i = 0; i < flatSyllables.length - 1; i++) { 5351 if (time >= flatSyllables[i].t && time < flatSyllables[i + 1].t) { 5352 idx = i; 5353 break; 5354 } 5355 } 5356 const sylN = flatSyllables[idx]; 5357 const sylNext = flatSyllables[idx + 1]; 5358 const elN = wordRefs.current.get(`${sylN.lineIdx}-${sylN.sylIdx}`); 5359 const elNext = wordRefs.current.get(`${sylNext.lineIdx}-${sylNext.sylIdx}`); 5360 if (!elN) return { x: 0, y: 0, opacity: 0, squish: 0, activeLineIdx: sylN.lineIdx, ballX: 0 }; 5361 const rectN = elN.getBoundingClientRect(); 5362 const startX = rectN.left + rectN.width / 2 - cRect.left; 5363 const startY = rectN.top - cRect.top - 15; 5364 if (!elNext) { 5365 return { x: startX, y: startY, opacity: 1, squish: 0, activeLineIdx: sylN.lineIdx, ballX: startX }; 5366 } 5367 const rectNext = elNext.getBoundingClientRect(); 5368 const endX = rectNext.left + rectNext.width / 2 - cRect.left; 5369 const endY = rectNext.top - cRect.top - 15; 5370 const p = (time - sylN.t) / (sylNext.t - sylN.t); 5371 const x = startX + (endX - startX) * p; 5372 const easeY = 1 - Math.pow(Math.abs(2 * p - 1), 3); 5373 const dx = Math.abs(endX - startX); 5374 const dy = Math.abs(endY - startY); 5375 const isCrossLine = sylNext.lineIdx !== sylN.lineIdx; 5376 let H = Math.max(35, dx * 0.22 + dy * 0.1); 5377 if (isCrossLine) { 5378 H = Math.max(90, dx * 0.15); 5379 } 5380 H = Math.min(120, H); 5381 const y = startY + (endY - startY) * p - H * easeY; 5382 const timeSinceLanding = time - sylN.t; 5383 const squish = timeSinceLanding < 0.12 ? (1 - timeSinceLanding / 0.12) * 0.35 : 0; 5384 return { x, y, opacity: 1, squish, activeLineIdx: sylN.lineIdx, ballX: x }; 5385 }, [flatSyllables]); 5386 reactExports.useEffect(() => { 5387 let raf; 5388 const update = () => { 5389 if (containerRef.current) { 5390 const cRect = containerRef.current.getBoundingClientRect(); 5391 const interpolation = getBallInterpolation(currentTime, c
5391Rect); 5392 setBallPos({ 5393 x: interpolation.x, 5394 y: interpolation.y, 5395 opacity: interpolation.opacity, 5396 squish: interpolation.squish 5397 }); 5398 const highlights = {}; 5399 if (flatSyllables.length > 0) { 5400 if (currentTime < flatSyllables[0].t) ; 5401 else if (currentTime >= flatSyllables[flatSyllables.length - 1].t) { 5402 lines.forEach((line, lIdx) => { 5403 line.syllables.forEach((_, sIdx) => { 5404 highlights[`${lIdx}-${sIdx}`] = 100; 5405 }); 5406 }); 5407 } else { 5408 let activeFlatIdx = 0; 5409 for (let i = 0; i < flatSyllables.length - 1; i++) { 5410 if (currentTime >= flatSyllables[i].t && currentTime < flatSyllables[i + 1].t) { 5411 activeFlatIdx = i; 5412 break; 5413 } 5414 } 5415 if (activeLineIdx >= 0 && activeLineIdx < lines.length) { 5416 const line = lines[activeLineIdx]; 5417 line.syllables.forEach((_, sylIdx) => { 5418 const flatIdx = flatSyllableIndices.get(`${activeLineIdx}-${sylIdx}`) ?? -1; 5419 if (flatIdx < activeFlatIdx) { 5420 highlights[`${activeLineIdx}-${sylIdx}`] = 100; 5421 } else if (flatIdx > activeFlatIdx) { 5422 highlights[`${activeLineIdx}-${sylIdx}`] = 0; 5423 } else { 5424 const el = wordRefs.current.get(`${activeLineIdx}-${sylIdx}`); 5425 if (el) { 5426 const rect = el.getBoundingClientRect(); 5427 const left = rect.left - cRect.left; 5428 const right = rect.right - cRect.left; 5429 let fillPct = 0; 5430 if (interpolation.ballX >= right) { 5431 fillPct = 100; 5432 } else if (interpolation.ballX <= left) { 5433 fillPct = 0; 5434 } else { 5435 fillPct = (interpolation.ballX - left) / (right - left) * 100; 5436 } 5437 highlights[`${activeLineIdx}-${sylIdx}`] = fillPct; 5438 } else { 5439 highlights[`${activeLineIdx}-${sylIdx}`] = 0; 5440 } 5441 } 5442 }); 5443 } 5444 } 5445 } 5446 setSylHighlights(highlights); 5447 } 5448 raf = requestAnimationFrame(update); 5449 }; 5450 raf = requestAnimationFrame(update); 5451 return () => cancelAnimationFrame(raf); 5452 }, [currentTime, activeLineIdx, lines, getBallInterpolation, flatSyllables, flatSyllableIndices]); 5453 const displayLineIdxs = []; 5454 if (activeLineIdx >= 0) { 5455 if (activeLineIdx > 0) displayLineIdxs.push(activeLineIdx - 1); 5456 displayLineIdxs.push(activeLineIdx); 5457 if (activeLineIdx + 1 < lines.length) displayLineIdxs.push(activeLineIdx + 1); 5458 } 5459 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { ref: containerRef, className: "relative w-full h-full flex flex-col items-center justify-center overflow-hidden bg-[#111] select-none", children: [ 5460 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col items-center gap-2 sm:gap-6 w-full max-w-5xl px-3 sm:px-8", children: displayLineIdxs.map((lineIdx) => { 5461 const line = lines[lineIdx]; 5462 const isActiveLine = lineIdx === activeLineIdx; 5463 const scaledFontSize = (isActiveLine ? 2.4 : 1.5) * textScale; 5464 return /* @__PURE__ */ jsxRuntimeExports.jsx( 5465 LineRow, 5466 { 5467 line, 5468 lineIdx, 5469 isActiveLine, 5470 scaledFontSize, 5471 setWordRef, 5472 activeLineIdx, 5473 highlights: sylHighlights 5474 }, 5475 lineIdx 5476 ); 5477 }) }), 5478 /* @__PURE__ */ jsxRuntimeExports.jsx( 5479 "div", 5480 { 5481 className: "absolute pointer-events-none z-50", 5482 style: { 5483 left: `${ballPos.x}px`, 5484 top: `${ballPos.y}px`, 5485 transform: "translate(-50%, -50%)", 5486 opacity: ballPos.opacity, 5487 transition: "opacity 0.2s ease" 5488 }, 5489 children: /* @__PURE__ */ jsxRuntimeExports.jsx(BallSVG, { icon: ballIcon, squish: ballPos.squish }) 5490 } 5491 ),
5492 onBallIconChange && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-2 sm:bottom-4 left-1/2 -translate-x-1/2 hidden sm:flex gap-1.5 bg-black/60 backdrop-blur border border-zinc-700/60 rounded-2xl px-3 py-2 shadow-xl z-40", children: BALL_ICONS.map((id) => /* @__PURE__ */ jsxRuntimeExports.jsx( 5493 "button", 5494 { 5495 onClick: () => onBallIconChange(id), 5496 className: `w-8 h-8 flex items-center justify-center rounded-lg transition-all ${ballIcon === id ? "bg-indigo-500/40 ring-2 ring-indigo-400 scale-110" : "hover:bg-zinc-700/60 hover:scale-105"}`, 5497 title: id, 5498 children: /* @__PURE__ */ jsxRuntimeExports.jsx(BallSVG, { icon: id, squish: 0, size: 22 }) 5499 }, 5500 id 5501 )) }) 5502 ] }); 5503}; 5504const DISPLAY_MODES = [ 5505 { value: "classic", label: "Classic", icon: /* @__PURE__ */ jsxRuntimeExports.jsx(SquareSquare, { size: 14 }) }, 5506 { value: "highway", label: "Highway", icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Route, { size: 14 }) }, 5507 { value: "kinetic", label: "Kinetic", icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Type, { size: 14 }) }, 5508 { value: "sing-through", label: "Sing-Through", icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { size: 14 }) } 5509]; 5510const DisplayModeDropdown = ({ 5511 displayMode, 5512 setDisplayMode 5513}) => { 5514 const current = DISPLAY_MODES.find((m) => m.value === displayMode) ?? DISPLAY_MODES[0]; 5515 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative group/modemenu", children: [ 5516 /* @__PURE__ */ jsxRuntimeExports.jsxs( 5517 "button", 5518 { 5519 className: "flex items-center gap-1.5 px-2.5 py-1.5 bg-zinc-800/80 hover:bg-zinc-700 backdrop-blur border border-zinc-700/50 rounded-lg text-zinc-300 transition-colors shadow-lg text-xs font-bold uppercase tracking-wider select-none", 5520 title: "Switch display mode", 5521 children: [ 5522 current.icon,
5523 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: current.label }), 5524 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown, { size: 12, className: "opacity-60" }) 5525 ] 5526 } 5527 ), 5528 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-full left-0 mt-1.5 min-w-[140px] bg-zinc-900/95 backdrop-blur-xl border border-zinc-700/60 rounded-xl shadow-2xl overflow-hidden z-[999] hidden group-hover/modemenu:flex flex-col py-1", children: DISPLAY_MODES.map((mode) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 5529 "button", 5530 { 5531 onClick: () => setDisplayMode(mode.value), 5532 className: `flex items-center gap-2 px-3 py-2 text-xs font-bold uppercase tracking-wider transition-colors ${mode.value === displayMode ? "bg-indigo-600/30 text-indigo-300" : "text-zinc-400 hover:bg-zinc-800 hover:text-white"}`, 5533 children: [ 5534 mode.icon, 5535 mode.label, 5536 mode.value === displayMode && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "ml-auto w-1.5 h-1.5 rounded-full bg-indigo-400" }) 5537 ] 5538 }, 5539 mode.value 5540 )) }) 5541 ] }); 5542}; 5543const cleanGluedWords$1 = (txt) => { 5544 return splitGluedWords(txt); 5545}; 5546const shouldAddSpace$1 = (prev, curr) => { 5547 if (!prev || !curr) return false; 5548 if (prev.endsWith(" ") || prev.endsWith("Â ") || curr.startsWith(" ") || curr.startsWith("Â ")) return false; 5549 if (shouldJoinSyllables(prev, curr)) return false; 5550 return true; 5551}; 5552const isCapitalizedStart$1 = (txt) => { 5553 const clean = txt.trim(); 5554 if (!/^[A-Z]/.test(clean)) return false; 5555 if (/^[I]($|'|m$)/.test(clean)) return true; 5556 if (clean.length === 1) return false; 5557 return true; 5558}; 5559const groupLyricsIntoLines$1 = (lyrics) => { 5560 const preprocessed = []; 5561 for (let i = 0; i < lyrics.length; i++) { 5562 const syl = lyrics[i]; 5563 const cleaned = cleanGluedWords$1(syl.txt); 5564 const parts = cleaned.split(" "); 5565 if (parts.length > 1) { 5566 parts.forEach((part, idx) => { 5567 preprocessed.push({ 5568 t: syl.t + idx * 0.01, 5569 txt: part 5570 }); 5571 }); 5572 } else { 5573 preprocessed.push({ 5574 t: syl.t, 5575 txt: cleaned 5576 }); 5577 } 5578 } 5579 const lines = []; 5580 let currentLine = []; 5581 for (let i = 0; i < preprocessed.length; i++) { 5582 const syl = preprocessed[i]; 5583 let txt = syl.txt; 5584 const isNewLine = txt.startsWith("/") || txt.startsWith("\\") || txt.includes("\n") || txt.includes("\r"); 5585 if (isNewLine && currentLine.length > 0) { 5586 lines.push({ 5587 startTime: currentLine[0].t, 5588 endTime: currentLine[currentLine.length - 1].t, 5589 syllables: currentLine 5590 }); 5591 currentLine = []; 5592 } 5593 txt = txt.replace(/^[\/\\]/, "").replace(/[\n\r]/g, "").replace(/`/g, " "); 5594 if (currentLine.length > 0) { 5595 const prevSyl = currentLine[currentLine.length - 1]; 5596 if (shouldAddSpace$1(prevSyl.txt, txt)) { 5597 txt = " " + txt; 5598 } 5599 } 5600 currentLine.push({ t: syl.t, txt }); 5601 if (i < preprocessed.length - 1) { 5602 const nextSyl = preprocessed[i + 1]; 5603 const cleanNextTxt = nextSyl.txt.replace(/^[\/\\]/, "").replace(/[\n\r]/g, "").replace(/`/g, " "); 5604 const hasGap = nextSyl.t - syl.t > 2.5; 5605 const isCapitalized = isCapitalizedStart$1(cleanNextTxt); 5606 const isJustI = /^[I]$/.test(cleanNextTxt.trim()); 5607 const shouldBreakOnCapital = isCapitalized && !isJustI && currentLine.length >= 1; 5608 const isWordBoundary = cleanNextTxt.startsWith(" ") || shouldAddSpace$1(txt, cleanNextTxt); 5609 const isTooLong = currentLine.length >= 28 && isWordBoundary; 5610 if ((hasGap || shouldBreakOnCapital || isTooLong) && currentLine.length > 0) { 5611 lines.push({ 5612 startTime: currentLine[0].t, 5613 endTime: currentLine[currentLine.length - 1].t, 5614 syllables: currentLine 5615 }); 5616 currentLine = []; 5617 } 5618 } 5619 } 5620 if (currentLine.length > 0) { 5621 lines.push({ 5622 startTime: currentLine[0].t, 5623 endTime: currentLine[currentLine.length - 1].t, 5624 syllables: currentLine 5625 }); 5626 } 5627 return lines; 5628}; 5629const Lyrics = ({ track, currentTime, melodyData, pianoRollData, isPlaying, playbackRate = 1, userId, trackId }) => { 5630 var _a; 5631 const { t } = useTranslation(); 5632 const rawLyrics = ((_a = track.midiMetadata) == null ? void 0 : _a.lyrics) || []; 5633 const [score, setScore] = reactExports.useState(0); 5634 const [maxScore, setMaxScore] = reactExports.useState(0); 5635 const [hitFlash, setHitFlash] = reactExports.useState("none"); 5636 const [isFinished, setIsFinished] = reactExports.useState(false); 5637 const [displayMode, setDisplayMode] = reactExports.useState("classic"); 5638 const [textScale, setTextScaleState] = reactExports.useState(() => { 5639 const session = sessionStorage.getItem("karaokeTextScale"); 5640 return session ? parseFloat(session) : 1; 5641 }); 5642 const textScaleSaveTimer = reactExports.useRef(null); 5643 const [ballIcon, setBallIconState] = reactExports.useState(() => { 5644 const session = sessionStorage.getItem("karaokeBallIcon"); 5645 return session || "glow"; 5646 }); 5647 const [isMicEnabled, setIsMicEnabled] = reactExports.useState(false); 5648 const [devices, setDevices] = reactExports.useState([]); 5649 const [selectedDevice, setSelectedDevice] = reactExports.useState(""); 5650 const [currentVolume, setCurrentVolume] = reactExports.useState(0); 5651 const [hasUsedMic, setHasUsedMic] = reactExports.useState(false); 5652 const [showScoreScreen, setShowScoreScreen] = reactExports.useState(false); 5653 const [leaderboard, setLeaderboard] = reactExports.useState([]); 5654 const [isSubmittingScore, setIsSubmittingScore] = reactExports.useState(false); 5655 const [hasSubmittedScore, setHasSubmittedScore] = reactExports.useState(false); 5656 const lines = reactExports.useMemo(() => groupLyricsIntoLines$1(rawLyrics), [rawLyrics]); 5657 const timeRef = reactExports.useRef(currentTime); 5658 reactExports.useEffect(() => { 5659 timeRef.current = currentTime; 5660 }, [currentTime]); 5661 reactExports.useEffect(() => { 5662 if (!userId) return; 5663 api.request(`/users/${userId}`).then((user) => { 5664 var _a2; 5665 const saved = (_a2 = user == null ? void 0 : user.preferences) == null ? void 0 : _a2.karaokeTextScale; 5666 if (saved !== void 0) { 5667 const val = parseFloat(saved); 5668 if (!isNaN(val) && val >= 0.4 && val <= 1.5) { 5669 setTextScaleState(val); 5670 sessionStorage.setItem("karaokeTextScale", String(val)); 5671 } 5672 } 5673 }).catch(() => { 5674 }); 5675 }, [userId]); 5676 reactExports.useEffect(() => { 5677 const checkSize = () => { 5678 const isSmallScreen = window.innerWidth < 640 || window.innerHeight < 500; 5679 const session = sessionStorage.getItem("karaokeTextScale"); 5680 if (!session && isSmallScreen) { 5681 setTextScaleState(0.65); 5682 } 5683 }; 5684 checkSize(); 5685 }, []); 5686 const setTextScale = reactExports.useCallback((newScale) => { 5687 const clamped = Math.max(0.4, Math.min(1.5, Math.round(newScale * 20) / 20)); 5688 setTextScaleState(clamped); 5689 sessionStorage.setItem("karaokeTextScale", String(clamped)); 5690 if (textScaleSaveTimer.current) clearTimeout(textScaleSaveTimer.current); 5691 textScaleSaveTimer.current = setTimeout(() => { 5692 if (!userId) return; 5693 api.request(`/users/${userId}`).then((user) => { 5694 return api.updateUser(userId, { 5695 preferences: { ...(user == null ? void 0 : user.preferences) || {}, karaokeTextScale: clamped } 5696 }); 5697 }).catch(() => { 5698 }); 5699 }, 500); 5700 }, [userId]); 5701 const handleBallIconChange = reactExports.useCallback((icon) => { 5702 setBallIconState(icon); 5703 sessionStorage.setItem("karaokeBallIcon", icon); 5704 }, []); 5705 const lastLogTimeRef = reactExports.useRef(0); 5706 reactExports.useEffect(() => { 5707 if (!isPlaying) { 5708 lastLogTimeRef.current = 0; 5709 return; 5710 } 5711 if (Math.abs(currentTime - lastLogTimeRef.current) >= 0.5) { 5712 lastLogTimeRef.current = currentTime; 5713 let activeLine = null; 5714 let activeSyl = null; 5715 let nextSylTimeVal = null; 5716 let fillPct = 0; 5717 let activeLineIdx2 = -1; 5718 for (let i = 0; i < lines.length; i++) { 5719 const line = lines[i]; 5720 if (currentTime >= line.startTime && currentTime <= line.endTime) { 5721 activeLineIdx2 = i; 5722 break; 5723 } 5724 } 5725 if (activeLineIdx2 === -1) { 5726 for (let i = 0; i < lines.length; i++) { 5727 const line = lines[i]; 5728 if (currentTime >= line.startTime - 1.5 && currentTime <= line.endTime + 1) { 5729 activeLineIdx2 = i; 5730 break; 5731 } 5732 } 5733 } 5734 if (activeLineIdx2 === -1) { 5735 for (let i = 0; i < lines.length; i++) { 5736 if (lines[i].startTime > currentTime) { 5737 activeLineIdx2 = i; 5738 break; 5739 } 5740 } 5741 } 5742 if (activeLineIdx2 === -1 && lines.length > 0) { 5743 activeLineIdx2 = lines.length - 1; 5744 } 5745 if (activeLineIdx2 !== -1) { 5746 activeLine = lines[activeLineIdx2]; 5747 } 5748 if (activeLine) { 5749 for (let idx = 0; idx < activeLine.syllables.length; idx++) { 5750 const syl = activeLine.syllables[idx]; 5751 const nextLine = lines[activeLineIdx2 + 1]; 5752 const nextLineStart = nextLine ? nextLine.startTime : Infinity; 5753 const nextSylT = idx < activeLine.syllables.length - 1 ? activeLine.syllables[idx + 1].t : Math.min(activeLine.endTime + 0.5, nextLineStart); 5754 if (currentTime >= syl.t && currentTime < nextSylT) { 5755 activeSyl = syl; 5756 nextSylTimeVal = nextSylT; 5757 const duration = nextSylT - syl.t; 5758 fillPct = Math.max(0, Math.min(100, (currentTime - syl.t) / duration * 100)); 5759 break; 5760 } 5761 } 5762 } 5763 console.log(`[KARAOKE_DEBUG]`, { 5764 currentTime, 5765 synthCurrentTime: synth.getCurrentTime(), 5766 audioContextTime: synth.getAudioContext().currentTime, 5767 isPlaying, 5768 activeSyllable: activeSyl ? { 5769 txt: activeSyl.txt, 5770 t: activeSyl.t, 5771 nextSylTime: nextSylTimeVal, 5772 fillPercentage: fillPct 5773 } : null 5774 }); 5775 } 5776 }, [currentTime, isPlaying, lines]); 5777 reactExports.useEffect(() => { 5778 if (isMicEnabled) { 5779 if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { 5780 alert(t("karaoke.micNotSupported", "Microphone is not supported in this browser or requires a secure HTTPS connection.")); 5781 setIsMicEnabled(false); 5782 return; 5783 } 5784 navigator.mediaDevices.getUserMedia({ audio: true }).then(() => { 5785 navigator.mediaDevices.enumerateDevices().then((devs) => { 5786 const audioInputs = devs.filter((d) => d.kind === "audioinput"); 5787 setDevices(audioInputs); 5788 if (audioInputs.length > 0 && !selectedDevice) { 5789 setSelectedDevice(audioInputs[0].deviceId); 5790 } 5791 }); 5792 }).catch((e) => { 5793 console.error("Mic permission denied", e); 5794 setIsMicEnabled(false); 5795 }); 5796 setHasUsedMic(true); 5797 } 5798 }, [isMicEnabled]); 5799 reactExports.useEffect(() => { 5800 if (isPlaying && isMicEnabled && synth.pitchTracker) { 5801 synth.pitchTracker.start(selectedDevice); 5802 synth.pitchTracker.onVolumeUpdate = (vol) => { 5803 setCurrentVolume(vol); 5804 }; 5805 synth.pitchTracker.onPitchDetected = (freq) => { 5806 if (!melodyData) return; 5807 const cTime = timeRef.current; 5808 const expected = melodyData.find((n) => cTime >= n.start && cTime <= n.start + n.duration); 5809 if (expected) { 5810 const targetFreq = 440 * Math.pow(2, (expected.midi - 69) / 12); 5811 const semitoneRatio = Math.pow(2, 1 / 12); 5812 const isPerfect = freq >= targetFreq / (semitoneRatio * 0.5) && freq <= targetFreq * (semitoneRatio * 0.5); 5813 const isGood = freq >= targetFreq / (semitoneRatio * 2) && freq <= targetFreq * (semitoneRatio * 2);
5814 setMaxScore((prev) => prev + 1); 5815 if (isPerfect) { 5816 setScore((prev) => prev + 2); 5817 setHitFlash("perfect"); 5818 } else if (isGood) { 5819 setScore((prev) => prev + 1); 5820 setHitFlash("good"); 5821 } else { 5822 setHitFlash("miss"); 5823 } 5824 setTimeout(() => setHitFlash("none"), 300); 5825 } 5826 }; 5827 } else if ((!isPlaying || !isMicEnabled) && synth.pitchTracker) { 5828 synth.pitchTracker.stop(); 5829 setCurrentVolume(0); 5830 } 5831 return () => { 5832 if (synth.pitchTracker) synth.pitchTracker.stop(); 5833 }; 5834 }, [isPlaying, isMicEnabled, selectedDevice, melodyData]); 5835 reactExports.useEffect(() => { 5836 if (rawLyrics.length > 0 && currentTime > rawLyrics[rawLyrics.length - 1].t + 2) { 5837 if (!isFinished) { 5838 setIsFinished(true); 5839 if (hasUsedMic) { 5840 setShowScoreScreen(true); 5841 loadLeaderboard(); 5842 } 5843 } 5844 } else { 5845 setIsFinished(false); 5846 setShowScoreScreen(false); 5847 } 5848 }, [currentTime, rawLyrics, hasUsedMic, isFinished]); 5849 const loadLeaderboard = async () => { 5850 if (trackId) { 5851 try { 5852 const scores = await api.getHighscores(trackId, "karaoke", userId); 5853 setLeaderboard(scores); 5854 } catch (e) { 5855 console.error("Failed to load karaoke leaderboard", e); 5856 } 5857 } 5858 }; 5859 const submitScore = async () => { 5860 if (!trackId || score <= 0 || hasSubmittedScore) return; 5861 setIsSubmittingScore(true); 5862 try { 5863 await api.postHighscore(trackId, { 5864 mode: "karaoke", 5865 score: Math.floor(score), 5866 accuracy: maxScore > 0 ? score / (maxScore * 2) * 100 : 0 5867 }); 5868 await loadLeaderboard(); 5869 setHasSubmittedScore(true); 5870 } catch (e) { 5871 console.error("Failed to submit karaoke score", e); 5872 } finally { 5873 setIsSubmittingScore(false); 5874 } 5875 }; 5876 let countdownData = null; 5877 if (isPlaying && lines.length > 0) { 5878 let nextLine = lines.find((l) => l.startTime > currentTime); 5879 if (nextLine) { 5880 const timeToNext = nextLine.startTime - currentTime; 5881 const prevLine = [...lines].reverse().find((l) => l.endTime <= currentTime); 5882 const gap = prevLine ? nextLine.startTime - prevLine.endTime : nextLine.startTime; 5883 const isLineActive = lines.some((l) => currentTime >= l.startTime && currentTime <= l.endTime); 5884 if (gap >= 4 && timeToNext <= 3 && timeToNext > 0 && !isLineActive) { 5885 countdownData = { 5886 timeToNext, 5887 percentage: (3 - timeToNext) / 3 5888 }; 5889 } 5890 } 5891 } 5892 let timeToNextSyllable = 0; 5893 if (lines.length > 0) { 5894 let nextSyllable = null; 5895 for (const line of lines) { 5896 const found = line.syllables.find((s) => s.t > currentTime); 5897 if (found) { 5898 nextSyllable = found; 5899 break; 5900 } 5901 } 5902 if (nextSyllable) { 5903 timeToNextSyllable = nextSyllable.t - currentTime; 5904 } 5905 } 5906 if (lines.length === 0) { 5907 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full bg-[#111] text-zinc-500 font-serif italic text-lg relative", children: t("karaoke.noLyrics") }); 5908 } 5909 if (displayMode === "highway") { 5910 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full h-full", children: [
5911 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-4 left-4 z-50", children: /* @__PURE__ */ jsxRuntimeExports.jsx(DisplayModeDropdown, { displayMode, setDisplayMode }) }), 5912 /* @__PURE__ */ jsxRuntimeExports.jsx( 5913 LyricsHighway, 5914 { 5915 track, 5916 currentTime, 5917 melodyData, 5918 pianoRollData, 5919 isPlaying, 5920 playbackRate, 5921 userId, 5922 trackId, 5923 isMicEnabled, 5924 setIsMicEnabled, 5925 score, 5926 maxScore, 5927 hitFlash, 5928 currentVolume, 5929 devices, 5930 selectedDevice, 5931 setSelectedDevice 5932 } 5933 ) 5934 ] }); 5935 } 5936 if (displayMode === "sing-through") { 5937 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full h-full", children: [ 5938 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-4 left-4 z-50", children: /* @__PURE__ */ jsxRuntimeExports.jsx(DisplayModeDropdown, { displayMode, setDisplayMode }) }),
5939 /* @__PURE__ */ jsxRuntimeExports.jsx( 5940 SingThroughLyrics, 5941 { 5942 track, 5943 currentTime, 5944 melodyData, 5945 pianoRollData, 5946 isPlaying, 5947 playbackRate, 5948 userId, 5949 trackId 5950 } 5951 ), 5952 isMicEnabled && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute right-6 top-1/2 -translate-y-1/2 w-4 h-64 bg-zinc-900 rounded-full border border-zinc-800 overflow-hidden flex flex-col justify-end pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 5953 "div", 5954 { 5955 className: "w-full bg-gradient-to-t from-emerald-400 via-amber-400 to-rose-500 transition-all duration-75 ease-out", 5956 style: { height: `${Math.min(100, currentVolume * 200)}%` } 5957 } 5958 ) }), 5959 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-3 left-3 sm:bottom-6 sm:left-6 z-50 flex items-center gap-2 bg-black/80 backdrop-blur p-1.5 sm:p-2 rounded-2xl border border-zinc-800 shadow-2xl", children: [
5960 /* @__PURE__ */ jsxRuntimeExports.jsx( 5961 "button", 5962 { 5963 onClick: () => setIsMicEnabled(!isMicEnabled), 5964 className: `w-9 h-9 sm:w-12 sm:h-12 flex items-center justify-center rounded-lg sm:rounded-xl transition-all ${isMicEnabled ? "bg-emerald-500/20 text-emerald-400" : "bg-zinc-800 text-zinc-400 hover:text-white"}`, 5965 children: isMicEnabled ? /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(MicOff, { size: 18 }) 5966 } 5967 ), 5968 isMicEnabled && devices.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col ml-2", children: [ 5969 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] uppercase font-bold text-zinc-500 tracking-wider mb-1", children: "Input Device" }), 5970 /* @__PURE__ */ jsxRuntimeExports.jsx( 5971 "select", 5972 { 5973 value: selectedDevice, 5974 onChange: (e) => setSelectedDevice(e.target.value), 5975 className: "bg-zinc-900 border border-zinc-700 text-xs text-white rounded-lg px-2 py-1 max-w-[200px] outline-none", 5976 children: devices.map((d) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: d.deviceId, children: d.label || `Mic ${d.deviceId.slice(0, 4)}` }, d.deviceId)) 5977 } 5978 ) 5979 ] }) 5980 ] }),
5981 isMicEnabled && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-4 right-4 z-50 flex flex-col items-end pointer-events-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-3xl font-black font-mono text-white drop-shadow-[0_2px_10px_rgba(0,0,0,0.8)]", children: score.toLocaleString() }) }) 5982 ] }); 5983 } 5984 let activeLineIdx = -1; 5985 for (let i = 0; i < lines.length; i++) { 5986 if (currentTime >= lines[i].startTime && currentTime <= lines[i].endTime) { 5987 activeLineIdx = i; 5988 break; 5989 } 5990 } 5991 if (activeLineIdx === -1) { 5992 for (let i = 0; i < lines.length; i++) { 5993 if (currentTime >= lines[i].startTime - 1.5 && currentTime <= lines[i].endTime + 1) { 5994 activeLineIdx = i; 5995 break; 5996 } 5997 } 5998 } 5999 if (activeLineIdx === -1) { 6000 for (let i = 0; i < lines.length; i++) { 6001 if (lines[i].startTime > currentTime) { 6002 activeLineIdx = i; 6003 break; 6004 } 6005 } 6006 } 6007 if (activeLineIdx === -1) activeLineIdx = lines.length - 1; 6008 const start = Math.max(0, activeLineIdx - 1); 6009 const end = Math.min(lines.length, activeLineIdx + 3); 6010 lines.slice(start, end); 6011 if (isFinished) { 6012 const accuracy = maxScore > 0 ? score / (maxScore * 2) * 100 : 0; 6013 let grade = "D"; 6014 if (accuracy >= 95) grade = "S"; 6015 else if (accuracy >= 85) grade = "A"; 6016 else if (accuracy >= 70) grade = "B"; 6017 else if (accuracy >= 50) grade = "C"; 6018 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-full bg-[#111] p-8 overflow-hidden relative overflow-y-auto", children: [
6019 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-zinc-900 border border-zinc-800 p-12 rounded-3xl text-center shadow-2xl mb-8", children: [ 6020 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-3xl font-bold text-zinc-400 uppercase tracking-widest mb-4", children: t("karaoke.finalGrade", "Final Grade") }), 6021 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `text-9xl font-black mb-6 ${grade === "S" ? "text-amber-400 drop-shadow-[0_0_20px_rgba(251,191,36,0.5)]" : grade === "A" ? "text-emerald-400 drop-shadow-[0_0_20px_rgba(52,211,153,0.5)]" : grade === "B" ? "text-blue-400 drop-shadow-[0_0_20px_rgba(96,165,250,0.5)]" : grade === "C" ? "text-orange-400 drop-shadow-[0_0_20px_rgba(251,146,60,0.5)]" : "text-rose-500 drop-shadow-[0_0_20px_rgba(244,63,94,0.5)]"}`, children: grade }), 6022 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xl text-zinc-300 font-mono bg-black/50 py-2 px-6 rounded-full inline-block mb-4", children: [ 6023 t("karaoke.accuracy", "Accuracy"), 6024 ": ", 6025 accuracy.toFixed(1), 6026 "%" 6027 ] }),
6028 hasUsedMic && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 6029 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-3xl text-white font-black mb-6", children: [ 6030 score.toLocaleString(), 6031 " PTS" 6032 ] }), 6033 !hasSubmittedScore ? /* @__PURE__ */ jsxRuntimeExports.jsx( 6034 "button", 6035 { 6036 onClick: submitScore, 6037 disabled: isSubmittingScore || !userId, 6038 className: "bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-3 px-8 rounded-full shadow-lg disabled:opacity-50 transition-all", 6039 children: isSubmittingScore ? "Submitting..." : userId ? "Submit Score" : "Log in to submit" 6040 } 6041 ) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-emerald-400 font-bold", children: "Score Submitted!" }) 6042 ] }) 6043 ] }), 6044 hasUsedMic && showScoreScreen && leaderboard.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full max-w-2xl bg-zinc-900 border border-zinc-800 rounded-3xl p-6 shadow-2xl", children: [
6045 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-xl font-bold text-white mb-4 flex items-center gap-2", children: [ 6046 /* @__PURE__ */ jsxRuntimeExports.jsx(Timer, { size: 20, className: "text-indigo-400" }), 6047 " Top Singers" 6048 ] }), 6049 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-2", children: leaderboard.map((entry, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center justify-between p-3 rounded-xl ${entry.isCurrentUser ? "bg-indigo-500/20 border border-indigo-500/50" : "bg-black/40"}`, children: [ 6050 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 6051 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-zinc-500 font-black w-6 text-center", children: [ 6052 "#", 6053 idx + 1 6054 ] }), 6055 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-white", children: entry.userName || "Guest" }) 6056 ] }),
6057 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-emerald-400 font-mono font-bold", children: entry.score.toLocaleString() }) 6058 ] }, idx)) }) 6059 ] }) 6060 ] }); 6061 } 6062 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-full bg-[#111] p-3 sm:p-8 overflow-hidden relative", children: [ 6063 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-2 left-2 right-2 sm:top-4 sm:left-4 sm:right-4 z-50 flex items-center justify-between pointer-events-none", children: [ 6064 timeToNextSyllable > 3 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 bg-black/60 backdrop-blur border border-zinc-800/80 px-3 py-1.5 rounded-full text-zinc-300 shadow-xl text-xs font-mono select-none animate-in fade-in slide-in-from-top-4", children: [ 6065 /* @__PURE__ */ jsxRuntimeExports.jsx(Timer, { size: 14, className: "text-indigo-400 animate-pulse" }), 6066 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("karaoke.nextLyricIn", { seconds: Math.ceil(timeToNextSyllable) }) }) 6067 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", {}),
6068 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pointer-events-auto ml-2 flex items-center gap-2", children: [ 6069 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 bg-zinc-800/80 backdrop-blur border border-zinc-700/50 rounded-lg px-2 py-1.5 shadow-lg", children: [ 6070 /* @__PURE__ */ jsxRuntimeExports.jsx( 6071 "button", 6072 { 6073 onClick: () => setTextScale(textScale - 0.05), 6074 className: "w-6 h-6 flex items-center justify-center text-zinc-400 hover:text-white transition-colors rounded", 6075 title: "Decrease text size", 6076 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Minus, { size: 12 }) 6077 } 6078 ), 6079 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-bold text-zinc-400 w-9 text-center tracking-wider", children: [ 6080 Math.round(textScale * 100), 6081 "%" 6082 ] }), 6083 /* @__PURE__ */ jsxRuntimeExports.jsx( 6084 "button", 6085 { 6086 onClick: () => setTextScale(textScale + 0.05), 6087 className: "w-6 h-6 flex items-center justify-center text-zinc-400 hover:text-white transition-colors rounded", 6088 title: "Increase text size", 6089 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Plus, { size: 12 }) 6090 } 6091 ) 6092 ] }),
6093 displayMode === "classic" ? /* @__PURE__ */ jsxRuntimeExports.jsxs( 6094 "button", 6095 { 6096 onClick: () => setDisplayMode("highway"), 6097 className: "p-2 bg-zinc-800/80 hover:bg-zinc-700 backdrop-blur border border-zinc-700/50 rounded-lg text-zinc-400 transition-colors shadow-lg flex items-center gap-2", 6098 title: "Switch to Highway Mode", 6099 children: [ 6100 /* @__PURE__ */ jsxRuntimeExports.jsx(SquareSquare, { size: 16 }), 6101 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold uppercase tracking-wider hidden sm:inline", children: "Classic" }) 6102 ] 6103 } 6104 ) : displayMode === "highway" ? /* @__PURE__ */ jsxRuntimeExports.jsxs( 6105 "button", 6106 { 6107 onClick: () => setDisplayMode("kinetic"), 6108 className: "p-2 bg-zinc-800/80 hover:bg-zinc-700 backdrop-blur border border-zinc-700/50 rounded-lg text-zinc-400 transition-colors shadow-lg flex items-center gap-2", 6109 title: "Switch to Kinetic Mode", 6110 children: [
6111 /* @__PURE__ */ jsxRuntimeExports.jsx(Route, { size: 16 }), 6112 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold uppercase tracking-wider hidden sm:inline", children: "Highway" }) 6113 ] 6114 } 6115 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs( 6116 "button", 6117 { 6118 onClick: () => setDisplayMode("classic"), 6119 className: "p-2 bg-zinc-800/80 hover:bg-zinc-700 backdrop-blur border border-zinc-700/50 rounded-lg text-zinc-400 transition-colors shadow-lg flex items-center gap-2", 6120 title: "Switch to Classic Mode", 6121 children: [ 6122 /* @__PURE__ */ jsxRuntimeExports.jsx(Type, { size: 16 }), 6123 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold uppercase tracking-wider hidden sm:inline", children: "Kinetic" }) 6124 ] 6125 } 6126 ) 6127 ] }), 6128 isMicEnabled && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col items-end pointer-events-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-3xl font-black font-mono text-white drop-shadow-[0_2px_10px_rgba(0,0,0,0.8)]", children: score.toLocaleString() }) }) 6129 ] }), 6130 hitFlash !== "none" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute top-20 left-1/2 -translate-x-1/2 z-50 text-4xl font-black italic uppercase tracking-wider animate-in fade-in zoom-in-50 duration-150 drop-shadow-[0_0_20px_rgba(0,0,0,0.8)] ${hitFlash === "perfect" ? "text-amber-400" : hitFlash === "good" ? "text-emerald-400" : hitFlash === "great" ? "text-blue-400" : "text-rose-500"}`, children: hitFlash }), 6131 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 left-0 right-0 h-2 bg-zinc-900", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-full bg-gradient-to-r from-emerald-400 to-indigo-500 transition-all", style: { width: `${maxScore > 0 ? score / (maxScore * 2) * 100 : 0}%` } }) }),
6132 countdownData && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute top-6 left-1/2 -translate-x-1/2 flex flex-col items-center z-50 animate-in fade-in zoom-in-95 duration-200", children: [ 6133 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-400 text-[10px] font-bold tracking-widest mb-1 uppercase drop-shadow-md", children: t("karaoke.getReady", "Get Ready") }), 6134 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-12 h-12 flex items-center justify-center", children: [ 6135 /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { className: "absolute inset-0 w-full h-full -rotate-90", viewBox: "0 0 64 64", children: [ 6136 /* @__PURE__ */ jsxRuntimeExports.jsx( 6137 "circle", 6138 { 6139 cx: "32", 6140 cy: "32", 6141 r: "26", 6142 fill: "transparent", 6143 stroke: "#27272a", 6144 strokeWidth: "4" 6145 } 6146 ), 6147 /* @__PURE__ */ jsxRuntimeExports.jsx( 6148 "circle", 6149 { 6150 cx: "32", 6151 cy: "32", 6152 r: "26", 6153 fill: "transparent", 6154 stroke: "#f50", 6155 strokeWidth: "4", 6156 strokeDasharray: 2 * Math.PI * 26, 6157 strokeDashoffset: 2 * Math.PI * 26 * (1 - countdownData.percentage), 6158 strokeLinecap: "round", 6159 className: "transition-all duration-75" 6160 } 6161 ) 6162 ] }),
6163 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-lg font-bold text-white drop-shadow-md z-10", children: Math.ceil(countdownData.timeToNext) }) 6164 ] }) 6165 ] }), 6166 displayMode === "kinetic" ? /* @__PURE__ */ jsxRuntimeExports.jsx( 6167 LyricsKinetic, 6168 { 6169 lines, 6170 currentTime, 6171 activeLineIdx, 6172 textScale 6173 } 6174 ) : /* @__PURE__ */ jsxRuntimeExports.jsx( 6175 LyricsBouncing, 6176 { 6177 lines, 6178 currentTime, 6179 activeLineIdx, 6180 textScale, 6181 ballIcon, 6182 onBallIconChange: handleBallIconChange 6183 } 6184 ), 6185 isMicEnabled && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute right-6 top-1/2 -translate-y-1/2 w-4 h-64 bg-zinc-900 rounded-full border border-zinc-800 overflow-hidden flex flex-col justify-end", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6186 "div", 6187 { 6188 className: "w-full bg-gradient-to-t from-emerald-400 via-amber-400 to-rose-500 transition-all duration-75 ease-out", 6189 style: { height: `${Math.min(100, currentVolume * 200)}%` } 6190 } 6191 ) }), 6192 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute bottom-3 left-3 sm:bottom-6 sm:left-6 z-50 flex items-center gap-2 bg-black/80 backdrop-blur p-1.5 sm:p-2 rounded-2xl border border-zinc-800 shadow-2xl", children: [
6193 /* @__PURE__ */ jsxRuntimeExports.jsx( 6194 "button", 6195 { 6196 onClick: () => setIsMicEnabled(!isMicEnabled), 6197 className: `w-9 h-9 sm:w-12 sm:h-12 flex items-center justify-center rounded-lg sm:rounded-xl transition-all ${isMicEnabled ? "bg-emerald-500/20 text-emerald-400" : "bg-zinc-800 text-zinc-400 hover:text-white"}`, 6198 children: isMicEnabled ? /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(MicOff, { size: 18 }) 6199 } 6200 ), 6201 isMicEnabled && devices.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col ml-2", children: [ 6202 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] uppercase font-bold text-zinc-500 tracking-wider mb-1", children: "Input Device" }), 6203 /* @__PURE__ */ jsxRuntimeExports.jsx( 6204 "select", 6205 { 6206 value: selectedDevice, 6207 onChange: (e) => setSelectedDevice(e.target.value), 6208 className: "bg-zinc-900 border border-zinc-700 text-xs text-white rounded-lg px-2 py-1 max-w-[200px] outline-none", 6209 children: devices.map((d) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: d.deviceId, children: d.label || `Mic ${d.deviceId.slice(0, 4)}` }, d.deviceId)) 6210 } 6211 ) 6212 ] }) 6213 ] }) 6214 ] }); 6215}; 6216function usePerformanceStats(isPlaying = false) { 6217 const [fps, setFps] = reactExports.useState(60); 6218 const [memUsedMB, setMemUsedMB] = reactExports.useState(null); 6219 const [memLimitMB, setMemLimitMB] = reactExports.useState(null); 6220 const [isLagging, setIsLagging] = reactExports.useState(false); 6221 const [lowPerf, setLowPerf] = reactExports.useState(() => { 6222 if (typeof window === "undefined") return false; 6223 return localStorage.getItem("low_performance") === "true"; 6224 }); 6225 const frameCountRef = reactExports.useRef(0); 6226 const lastFpsUpdateRef = reactExports.useRef(performance.now()); 6227 const consecutiveLowFpsCountRef = reactExports.useRef(0); 6228 const rafIdRef = reactExports.useRef(null); 6229 reactExports.useEffect(() => { 6230 const handleLowPerfChange = () => { 6231 setLowPerf(localStorage.getItem("low_performance") === "true"); 6232 }; 6233 window.addEventListener("storage", handleLowPerfChange); 6234 window.addEventListener("low_performance_change", handleLowPerfChange); 6235 return () => { 6236 window.removeEventListener("storage", handleLowPerfChange); 6237 window.removeEventListener("low_performance_change", handleLowPerfChange); 6238 }; 6239 }, []); 6240 const toggleLowPerf = () => { 6241 const next = !lowPerf; 6242 setLowPerf(next); 6243 localStorage.setItem("low_performance", next ? "true" : "false"); 6244 window.dispatchEvent(new Event("low_performance_change")); 6245 }; 6246 reactExports.useEffect(() => { 6247 let active = true; 6248 const loop = (now) => { 6249 if (!active) return; 6250 frameCountRef.current++; 6251 const elapsed = now - lastFpsUpdateRef.current; 6252 if (elapsed >= 500) { 6253 const currentFps = Math.round(frameCountRef.current * 1e3 / elapsed); 6254 setFps(currentFps); 6255 frameCountRef.current = 0; 6256 lastFpsUpdateRef.current = now; 6257 if (typeof window !== "undefined" && (performance == null ? void 0 : performance.memory)) { 6258 const mem = performance.memory; 6259 setMemUsedMB(Math.round(mem.usedJSHeapSize / (1024 * 1024))); 6260 setMemLimitMB(Math.round(mem.jsHeapSizeLimit / (1024 * 1024))); 6261 } 6262 if (isPlaying && currentFps < 28) { 6263 consecutiveLowFpsCountRef.current++; 6264 if (consecutiveLowFpsCountRef.current >= 6) { 6265 setIsLagging(true); 6266 } 6267 } else if (currentFps >= 45) { 6268 consecutiveLowFpsCountRef.current = 0; 6269 setIsLagging(false); 6270 } 6271 } 6272 rafIdRef.current = requestAnimationFrame(loop); 6273 }; 6274 rafIdRef.current = requestAnimationFrame(loop); 6275 return () => { 6276 active = false; 6277 if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current); 6278 }; 6279 }, [isPlaying]); 6280 return { 6281 fps, 6282 memUsedMB, 6283 memLimitMB, 6284 isLagging, 6285 lowPerf, 6286 toggleLowPerf 6287 }; 6288} 6289const PerformanceHud = ({ stats, onClose }) => { 6290 const { fps, memUsedMB, memLimitMB, lowPerf, toggleLowPerf } = stats; 6291 const fpsColor = fps >= 50 ? "bg-emerald-400" : fps >= 30 ? "bg-amber-400" : "bg-red-500 animate-pulse"; 6292 const fpsTextColor = fps >= 50 ? "text-emerald-400" : fps >= 30 ? "text-amber-400" : "text-red-400 font-bold"; 6293 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 6294 "div", 6295 { 6296 className: "absolute top-0 left-0 z-40 flex items-center gap-2 bg-black/75 hover:bg-black/90 backdrop-blur-md px-2.5 py-1 rounded-br-lg border-b border-r border-white/15 shadow-md text-[11px] font-mono select-none transition-all", 6297 onClick: (e) => e.stopPropagation(), 6298 onPointerDown: (e) => e.stopPropagation(), 6299 children: [
6300 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [ 6301 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `w-2 h-2 rounded-full ${fpsColor}` }), 6302 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: fpsTextColor, children: [ 6303 fps, 6304 " FPS" 6305 ] }) 6306 ] }), 6307 memUsedMB !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 6308 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600", children: "|" }), 6309 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 text-zinc-300", title: `JS Memory Heap: ${memUsedMB} MB / ${memLimitMB} MB`, children: [ 6310 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { size: 12, className: "text-cyan-400" }), 6311 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 6312 memUsedMB, 6313 " MB" 6314 ] }) 6315 ] }) 6316 ] }),
6317 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-600", children: "|" }), 6318 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6319 "button", 6320 { 6321 onClick: toggleLowPerf, 6322 className: `px-1.5 py-0.5 rounded text-[10px] font-sans font-medium transition-colors flex items-center gap-1 ${lowPerf ? "bg-emerald-600/80 text-white hover:bg-emerald-500" : "bg-zinc-800 text-zinc-400 hover:text-white hover:bg-zinc-700"}`, 6323 title: lowPerf ? "Low Performance Mode is Active (reduces particles/DPR)" : "Click to enable Low Performance Mode for smoother playback", 6324 children: [ 6325 /* @__PURE__ */ jsxRuntimeExports.jsx(Zap, { size: 10, className: lowPerf ? "fill-white" : "" }), 6326 lowPerf ? "Low-Perf ON" : "Low-Perf OFF" 6327 ] 6328 } 6329 ), 6330 /* @__PURE__ */ jsxRuntimeExports.jsx( 6331 "button", 6332 { 6333 onClick: onClose, 6334 className: "text-zinc-500 hover:text-zinc-200 transition-colors ml-0.5", 6335 title: "Hide Performance HUD", 6336 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 13 }) 6337 } 6338 ) 6339 ] 6340 } 6341 ); 6342}; 6343const PerformanceLagBanner = ({ fps, onEnableLowPerf, onDismiss }) => { 6344 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 6345 "div", 6346 { 6347 className: "absolute top-14 left-4 z-[70] flex items-center gap-3 bg-amber-950/95 border border-amber-500/60 backdrop-blur-md px-3.5 py-2 rounded-lg shadow-2xl text-xs text-amber-200 animate-in fade-in slide-in-from-top-2 max-w-[calc(100%-2rem)] sm:max-w-md md:max-w-lg", 6348 onClick: (e) => e.stopPropagation(), 6349 onPointerDown: (e) => e.stopPropagation(), 6350 children: [
6351 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 6352 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { size: 15, className: "text-amber-400 animate-pulse flex-shrink-0" }), 6353 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 6354 "Frame drop detected (", 6355 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono font-bold text-amber-300", children: [ 6356 fps, 6357 " FPS" 6358 ] }), 6359 "). Enable Low-Performance Mode for smoother visuals?" 6360 ] }) 6361 ] }), 6362 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 flex-shrink-0", children: [ 6363 /* @__PURE__ */ jsxRuntimeExports.jsx( 6364 "button", 6365 { 6366 onClick: onEnableLowPerf, 6367 className: "px-2 py-1 bg-amber-500 hover:bg-amber-400 text-black font-semibold rounded text-[11px] transition-colors", 6368 children: "Enable" 6369 } 6370 ),
6371 /* @__PURE__ */ jsxRuntimeExports.jsx( 6372 "button", 6373 { 6374 onClick: onDismiss, 6375 className: "p-1 hover:bg-white/10 rounded text-amber-300 hover:text-white transition-colors", 6376 title: "Dismiss", 6377 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 13 }) 6378 } 6379 ) 6380 ] }) 6381 ] 6382 } 6383 ); 6384}; 6385const SpectrumBarsVisualizer = reactExports.lazy(() => __vitePreload(() => import("./SpectrumBarsVisualizer-D1eLSVtr.js"), true ? __vite__mapDeps([0,1]) : void 0)); 6386const SheetMusicViewer = reactExports.lazy(() => __vitePreload(() => import("./SheetMusicViewer-CoFH8B_z.js"), true ? __vite__mapDeps([2,3,1,4,5,6,7,8,9,10]) : void 0)); 6387const ParametricEQ = reactExports.lazy(() => __vitePreload(() => import("./ParametricEQ-wkxmF0lG.js"), true ? __vite__mapDeps([11,3,1,4,5,6,12,9,10,13]) : void 0)); 6388const OrchestraVisualizer = reactExports.lazy(() => __vitePreload(() => import("./OrchestraVisualizer-Cwwo0xaQ.js"), true ? __vite__mapDeps([14,1,15,12,3,4,5,6,9,10,13,16,17,18,19]) : void 0)); 6389const CinematicVisualizer = reactExports.lazy(() => __vitePreload(() => import("./CinematicVisualizer-BM6Yo-FE.js"), true ? __vite__mapDeps([20,1,4,5,6,3,21]) : void 0)); 6390const SouthparkOrchestraVisualizer = reactExports.lazy(() => __vitePreload(() => import("./SouthparkOrchestraVisualizer-BGIvyqzT.js"), true ? __vite__mapDeps([22,1,12,3,4,5,6,9,10,13,23,16,18,24,15,17,25,26,27,28]) : void 0)); 6391const Taiko = reactExports.lazy(() => __vitePreload(() => import("./Taiko-BWRRYy3Y.js"), true ? __vite__mapDeps([29,1,23,16,18,4,5,6,24,27,28,30]) : void 0)); 6392const VisualizerWindow = ({ 6393 viewMode, 6394 setViewMode, 6395 track, 6396 currentTime, 6397 duration, 6398 isPlaying, 6399 onPlayPause, 6400 autoPlayCountdown, 6401 onCancelAutoPlay, 6402 onPlayNext, 6403 nextTrack, 6404 isTheaterMode, 6405 setIsTheaterMode, 6406 pianoRollData, 6407 melodyData, 6408 melodySource, 6409 setMelodySource, 6410 hiddenTracks, 6411 trackVisibility, 6412 colorMap, 6413 isLoadingAudio, 6414 isAudioLoaded, 6415 pitchSemitones, 6416 setPitchSemitones, 6417 currentTempo, 6418 setCurrentTempo, 6419 originalTempo, 6420 isBuffering = false, 6421 selectedTrackIndex, 6422 onSelectTrack, 6423 processedInstruments, 6424 uniqueNames, 6425 trackId, 6426 userId, 6427 userName, 6428 uploaderName, 6429 audioMode = "synth", 6430 onSeek, 6431 isGameMode, 6432 setIsGameMode, 6433 onGameModeMute, 6434 onAuthRequested, 6435 soloedIndex = null, 6436 onSolo = () => { 6437 }, 6438 mutedIndices = /* @__PURE__ */ new Set(), 6439 onMute = () => { 6440 }, 6441 onPanChange, 6442 onInstrumentChange, 6443 isAdmin = false, 6444 staffModeEnabled = false, 6445 soundfont, 6446 isAttached = false, 6447 theme = "light", 6448 playbackRate = 1 6449}) => { 6450 var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D; 6451 const isStaff = Boolean(staffModeEnabled || typeof window !== "undefined" && window.location.search.includes("ai_admin=1")); 6452 const isMusicPlaying = isPlaying && !isBuffering; 6453 const containerRef = reactExports.useRef(null); 6454 const hideControlsTimer = reactExports.useRef(null); 6455 const [isFullScreen, setIsFullScreen] = reactExports.useState(false); 6456 const [showControls, setShowControls] = reactExports.useState(true); 6457 const [hasOpenedEQ, setHasOpenedEQ] = reactExports.useState(viewMode === "eq"); 6458 const [showVocalControls, setShowVocalControls] = reactExports.useState(false); 6459 const [reverbAmount, setReverbAmount] = reactExports.useState(0); 6460 const [delayAmount, setDelayAmount] = reactExports.useState(0); 6461 const [vocalNormalize, setVocalNormalize] = reactExports.useState(true); 6462 const [autoVocal, setAutoVocal] = reactExports.useState(false); 6463 const [audioDevices, setAudioDevices] = reactExports.useState([]); 6464 const [selectedDeviceId, setSelectedDeviceId] = reactExports.useState(""); 6465 const [scoreExists, setScoreExists] = reactExports.useState(null); 6466 const [lowPerf, setLowPerf] = reactExports.useState(() => { 6467 if (typeof window === "undefined") return false; 6468 const saved = localStorage.getItem("low_performance"); 6469 if (saved !== null) return saved === "true"; 6470 return /Mobi|Android|iPhone|iPad/i.test(navigator.userAgent) || /Macintosh/i.test(navigator.userAgent) && navigator.maxTouchPoints > 0 || "ontouchstart" in window; 6471 }); 6472 const [showPerfHud, setShowPerfHud] = reactExports.useState(() => { 6473 if (typeof window === "undefined") return false;
6474 return localStorage.getItem("show_perf_hud") === "true"; 6475 }); 6476 const [dismissedLag, setDismissedLag] = reactExports.useState(false); 6477 const perfStats = usePerformanceStats(isMusicPlaying); 6478 const togglePerfHud = () => { 6479 const next = !showPerfHud; 6480 setShowPerfHud(next); 6481 localStorage.setItem("show_perf_hud", next ? "true" : "false"); 6482 }; 6483 reactExports.useEffect(() => { 6484 const qPaths = track.quantizedScorePaths; 6485 const url = (qPaths == null ? void 0 : qPaths.krn) || (qPaths == null ? void 0 : qPaths.studio) || (qPaths == null ? void 0 : qPaths.musescore) || track.scorePath || (qPaths == null ? void 0 : qPaths.q8) || (qPaths == null ? void 0 : qPaths.q16) || (qPaths == null ? void 0 : qPaths.q4) || (qPaths == null ? void 0 : qPaths.q32) || (qPaths == null ? void 0 : qPaths.melody); 6486 if (!url) { 6487 setScoreExists(false); 6488 return; 6489 } 6490 let active = true; 6491 setScoreExists(null); 6492 fetch(url, { method: "HEAD" }).then((res) => { 6493 if (!active) return; 6494 if (res.ok) { 6495 setScoreExists(true); 6496 } else { 6497 fetch(url).then((resGet) => { 6498 if (active) setScoreExists(resGet.ok); 6499 }).catch(() => { 6500 if (active) setScoreExists(false); 6501 }); 6502 } 6503 }).catch(() => { 6504 if (!active) return; 6505 fetch(url).then((resGet) => { 6506 if (active) setScoreExists(resGet.ok); 6507 }).catch(() => { 6508 if (active) setScoreExists(false); 6509 }); 6510 }); 6511 return () => { 6512 active = false; 6513 }; 6514 }, [ 6515 track.id, 6516 track.scorePath, 6517 (_a = track.quantizedScorePaths) == null ? void 0 : _a.krn, 6518 (_b = track.quantizedScorePaths) == null ? void 0 : _b.studio, 6519 (_c = track.quantizedScorePaths) == null ? void 0 : _c.musescore, 6520 (_d = track.quantizedScorePaths) == null ? void 0 : _d.q8, 6521 (_e = track.quantizedScorePaths) == null ? void 0 : _e.q16, 6522 (_f = track.quantizedScorePaths) == null ? void 0 : _f.q4, 6523 (_g = track.quantizedScorePaths) == null ? void 0 : _g.q32, 6524 (_h = track.quantizedScorePaths) == null ? void 0 : _h.melody 6525 ]); 6526 reactExports.useEffect(() => { 6527 if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) { 6528 navigator.mediaDevices.enumerateDevices().then((devices) => { 6529 const audioInputs = Array.isArray(devices) ? devices.filter((d) => d.kind === "audioinput") : []; 6530 setAudioDevices(audioInputs); 6531 if (audioInputs.length > 0 && !selectedDeviceId) { 6532 setSelectedDeviceId(audioInputs[0].deviceId); 6533 } 6534 }); 6535 } 6536 }, []); 6537 reactExports.useEffect(() => { 6538 if (synth && synth.vocalDSP) { 6539 synth.vocalDSP.setEffects(reverbAmount, delayAmount, vocalNormalize); 6540 synth.vocalDSP.setAutoVocalMode(autoVocal); 6541 synth.setGuideTrackIndex(0); 6542 } 6543 }, [reverbAmount, delayAmount, vocalNormalize, autoVocal]); 6544 reactExports.useEffect(() => { 6545 if (showVocalControls || autoVocal) { 6546 synth.startVocalMic(selectedDeviceId); 6547 } 6548 }, [showVocalControls, autoVocal, selectedDeviceId]); 6549 reactExports.useEffect(() => { 6550 if (viewMode === "eq" && !hasOpenedEQ) { 6551 const timer = setTimeout(() => setHasOpenedEQ(true), 0); 6552 return () => clearTimeout(timer); 6553 } 6554 }, [viewMode, hasOpenedEQ]); 6555 reactExports.useEffect(() => { 6556 const handleFS = () => setIsFullScreen(!!document.fullscreenElement); 6557 document.addEventListener("fullscreenchange", handleFS); 6558 return () => document.removeEventListener("fullscreenchange", handleFS); 6559 }, []); 6560 reactExports.useEffect(() => { 6561 const handleMouseMove = (e) => { 6562 const isNearTop = e.clientY <= 80; 6563 if (isNearTop) { 6564 setShowControls(true); 6565 if (hideControlsTimer.current) { 6566 clearTimeout(hideControlsTimer.current); 6567 hideControlsTimer.current = null; 6568 } 6569 } else { 6570 if (!hideControlsTimer.current) { 6571 hideControlsTimer.current = setTimeout(() => { 6572 setShowControls(false); 6573 hideControlsTimer.current = null; 6574 }, 1e3); 6575 } 6576 } 6577 }; 6578 if (!isFullScreen) { 6579 setShowControls(true); 6580 if (hideControlsTimer.current) { 6581 clearTimeout(hideControlsTimer.current); 6582 hideControlsTimer.current = null; 6583 } 6584 return; 6585 } 6586 setShowControls(false); 6587 window.addEventListener("mousemove", handleMouseMove); 6588 return () => { 6589 window.removeEventListener("mousemove", handleMouseMove); 6590 if (hideControlsTimer.current) { 6591 clearTimeout(hideControlsTimer.current); 6592 hideControlsTimer.current = null; 6593 } 6594 }; 6595 }, [isFullScreen]); 6596 const toggleFullScreen = () => { 6597 var _a2; 6598 if (!document.fullscreenElement) { 6599 (_a2 = containerRef.current) == null ? void 0 : _a2.requestFullscreen(); 6600 } else { 6601 document.exitFullscreen(); 6602 } 6603 }; 6604 const hasLyrics = ((_i = track == null ? void 0 : track.midiMetadata) == null ? void 0 : _i.lyrics) && track.midiMetadata.lyrics.length > 0; 6605 const hasPercussion = (_j = track == null ? void 0 : track.instruments) == null ? void 0 : _j.some((inst) => inst.midiChannel === 9 && (inst.notesCount > 0 || inst.notesCount === void 0)); 6606 const hasMelodicTracks = (_k = track == null ? void 0 : track.instruments) == null ? void 0 : _k.some((inst) => inst.midiChannel !== 9 && (inst.notesCount > 0 || inst.notesCount === void 0)); 6607 const isPianoRollLoading = (!!(track == null ? void 0 : track.isLight) || !!track.pianoRollPath) && pianoRollData === null; 6608 const hasPianoRollData = isPianoRollLoading || pianoRollData && pianoRollData.length > 0; 6609 const isScoreLoading = !!(track == null ? void 0 : track.isLight); 6610 const hasScore = (isScoreLoading || !!(track.scorePath || ((_l = track.quantizedScorePaths) == null ? void 0 : _l.studio) || ((_m = track.quantizedScorePaths) == null ? void 0 : _m.engraved) || ((_n = track.quantizedScorePaths) == null ? void 0 : _n.q8) || ((_o = track.quantizedScorePaths) == null ? void 0 : _o.q16) || ((_p = track.quantizedScorePaths) == null ? void 0 : _p.krn) || ((_q = track.quantizedScorePaths) == null ? void 0 : _q.q4) || ((_r = track.quantizedScorePaths) == null ? void 0 : _r.q32))) && scoreExists !== false; 6611 reactExports.useEffect(() => { 6612 if (track == null ? void 0 : track.isLight) return; 6613 if (!isStaff && (viewMode === "orchestra" || viewMode === "cinematic")) {
6614 setViewMode(hasPianoRollData ? "premium_orchestra" : "waveform"); 6615 return; 6616 } 6617 if ((!hasPianoRollData || !hasMelodicTracks) && viewMode === "piano") { 6618 setViewMode(hasPercussion ? "percussion" : "waveform"); 6619 } else if (!hasPianoRollData && (viewMode === "percussion" || viewMode === "orchestra" || viewMode === "premium_orchestra" || viewMode === "cinematic")) { 6620 setViewMode("waveform"); 6621 } else if (!hasScore && (viewMode === "score" || viewMode === "melody")) { 6622 setViewMode(hasPianoRollData && hasMelodicTracks ? "piano" : "waveform"); 6623 } 6624 }, [hasPianoRollData, hasMelodicTracks, hasPercussion, hasScore, viewMode, setViewMode, track == null ? void 0 : track.isLight, isStaff]); 6625 const getBgColor = () => { 6626 if (viewMode === "score" || viewMode === "melody") return "bg-[#fdfaf5]"; 6627 if (viewMode === "piano" || viewMode === "percussion" || viewMode === "orchestra" || viewMode === "cinematic" || viewMode === "premium_orchestra" || viewMode === "taiko") return "bg-[#050505]"; 6628 if (theme === "light") { 6629 if (viewMode === "waveform") return "bg-zinc-50"; 6630 if (viewMode === "karaoke") return "bg-zinc-100"; 6631 return "bg-zinc-50"; 6632 } 6633 if (viewMode === "waveform") return "bg-[#0a0a0a]"; 6634 if (viewMode === "karaoke") return "bg-[#111]"; 6635 return "bg-[#050505]"; 6636 }; 6637 const getTopBarClass = () => { 6638 if (theme === "light") return "bg-zinc-100 border-b border-zinc-200 text-zinc-800"; 6639 if (theme === "fun") return "bg-[#fff1f2] border-b border-pink-200 text-pink-900"; 6640 return "bg-[#050505] border-b border-white/10 text-white"; 6641 }; 6642 const getTabButtonClass = (isActive, isOrange = false, isDisabled = false) => { 6643 const base = `px-3 py-1.5 rounded-md text-xs font-bold flex items-center gap-2 transition-all ${isDisabled ? "opacity-40 cursor-not-allowed" : ""}`; 6644 if (isActive && !isDisabled) { 6645 if (theme === "fun") { 6646 return `${base} bg-gradient-to-r from-pink-500 to-indigo-500 text-white shadow-lg shadow-indigo-500/20`; 6647 } 6648 return `${base} ${isOrange ? "bg-orange-600" : "bg-indigo-600"} text-white shadow-lg ${isOrange ? "shadow-orange-500/20" : "shadow-indigo-500/20"}`; 6649 } 6650 if (theme === "light") return `${base} text-zinc-600 ${isDisabled ? "" : "hover:bg-zinc-200/60 hover:text-zinc-900"}`; 6651 if (theme === "fun") return `${base} text-pink-850 ${isDisabled ? "" : "hover:bg-pink-100/60 hover:text-pink-950"}`; 6652 return `${base} text-zinc-400 ${isDisabled ? "" : "hover:bg-white/5 hover:text-white"}`; 6653 }; 6654 const getControlIconClass = (isDisabled = false) => { 6655 const baseStyle = "p-1.5 rounded-md transition-colors"; 6656 if (isDisabled) return `${baseStyle} text-zinc-600 cursor-not-allowed`; 6657 if (theme === "light") return `${baseStyle} text-zinc-500 hover:text-zinc-900 hover:bg-zinc-200/60`; 6658 if (theme === "fun") return `${baseStyle} text-pink-700 hover:text-pink-950 hover:bg-pink-100/60`; 6659 return `${baseStyle} text-zinc-400 hover:text-white hover:bg-white/10`; 6660 }; 6661 const getSeparatorClass = () => { 6662 if (theme === "light") return "flex items-center gap-1 pl-4 border-l border-zinc-200 ml-2"; 6663 if (theme === "fun") return "flex items-center gap-1 pl-4 border-l border-pink-200 ml-2"; 6664 return "flex items-center gap-1 pl-4 border-l border-white/10 ml-2"; 6665 }; 6666 const getBottomBarClass = () => { 6667 const base = "w-full border-t py-2 px-4 flex items-center justify-center gap-4 z-30 shrink-0 rounded-b-xl"; 6668 if (theme === "light") return `${base} bg-zinc-100 border-zinc-200 text-zinc-800`; 6669 if (theme === "fun") return `${base} bg-[#fff1f2] border-t border-pink-200 text-pink-900`; 6670 return `${base} bg-[#050505] border-white/10 text-white`; 6671 }; 6672 const getBottomButtonClass = () => { 6673 if (theme === "light") return "p-1.5 hover:bg-zinc-200/60 disabled:opacity-30 rounded-full transition-colors text-zinc-700"; 6674 if (theme === "fun") return "p-1.5 hover:bg-pink-100/60 disabled:opacity-30 rounded-full transition-colors text-pink-800"; 6675 return "p-1.5 hover:bg-white/20 disabled:opacity-30 rounded-full transition-colors text-zinc-200"; 6676 }; 6677 const getBottomTextClass = () => { 6678 if (theme === "light") return "text-sm font-bold text-zinc-700 min-w-[80px] text-center drop-shadow-sm"; 6679 return "text-sm font-bold text-zinc-200 min-w-[80px] text-center drop-shadow-sm"; 6680 }; 6681 const [xmlData, setXmlData] = reactExports.useState(null); 6682 const [currentPage, setCurrentPage] = reactExports.useState(1);
6683 const [pageCount, setPageCount] = reactExports.useState(0); 6684 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 6685 "div", 6686 { 6687 id: "visualizer-container", 6688 ref: containerRef, 6689 onContextMenu: (e) => e.preventDefault(), 6690 className: `relative flex flex-col overflow-hidden transition-all duration-300 ${getBgColor()} ${isFullScreen ? "w-screen h-screen pb-10 rounded-none border-0" : `shadow-2xl w-full ${viewMode === "taiko" ? "min-h-[480px] sm:min-h-0 sm:aspect-video" : "aspect-video"} min-w-0 md:min-w-[600px] border-l border-b border-white/10 ${isAttached ? "rounded-l-xl rounded-r-none" : "rounded-xl border-t border-r"}`}`, 6691 style: !isFullScreen ? viewMode === "taiko" ? void 0 : { aspectRatio: "16/9" } : void 0, 6692 children: [ 6693 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center justify-between px-3 h-11 ${getTopBarClass()} z-30 transition-transform duration-300 ${isFullScreen ? "absolute top-0 left-0 right-0" : ""} ${isFullScreen && !showControls ? "-translate-y-full" : "translate-y-0"}`, children: [
6694 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 overflow-x-auto scrollbar-hide min-w-0 flex-1", children: [ 6695 hasLyrics && /* @__PURE__ */ jsxRuntimeExports.jsxs( 6696 "button", 6697 { 6698 onClick: () => { 6699 setViewMode("karaoke"); 6700 if (isGameMode && setIsGameMode) setIsGameMode(false); 6701 }, 6702 className: getTabButtonClass(viewMode === "karaoke"), 6703 title: "Karaoke / Lyrics", 6704 children: [ 6705 /* @__PURE__ */ jsxRuntimeExports.jsx(Mic, { size: 14 }), 6706 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Karaoke" }) 6707 ] 6708 } 6709 ), 6710 isStaff && /* @__PURE__ */ jsxRuntimeExports.jsxs( 6711 "button", 6712 { 6713 onClick: () => { 6714 if (!hasPianoRollData) return; 6715 setViewMode("orchestra"); 6716 if (isGameMode && setIsGameMode) setIsGameMode(false); 6717 }, 6718 className: getTabButtonClass(viewMode === "orchestra", false, !hasPianoRollData), 6719 title: !hasPianoRollData ? "Orchestra View not available (processing in progress)" : "Orchestra View (Legacy)", 6720 disabled: !hasPianoRollData, 6721 children: [
6722 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 14 }), 6723 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Orch" }) 6724 ] 6725 } 6726 ), 6727 isStaff && /* @__PURE__ */ jsxRuntimeExports.jsxs( 6728 "button", 6729 { 6730 onClick: () => { 6731 if (!hasPianoRollData) return; 6732 setViewMode("cinematic"); 6733 if (isGameMode && setIsGameMode) setIsGameMode(false); 6734 }, 6735 className: getTabButtonClass(viewMode === "cinematic", false, !hasPianoRollData), 6736 title: !hasPianoRollData ? "Cinematic View not available (processing in progress)" : "Cinematic (Beta)", 6737 disabled: !hasPianoRollData, 6738 children: [ 6739 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 14 }), 6740 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Cinematic" }) 6741 ] 6742 } 6743 ),
6744 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6745 "button", 6746 { 6747 onClick: () => { 6748 if (!hasPianoRollData) return; 6749 setViewMode("premium_orchestra"); 6750 if (isGameMode && setIsGameMode) setIsGameMode(false); 6751 }, 6752 className: getTabButtonClass(viewMode === "premium_orchestra", false, !hasPianoRollData), 6753 title: !hasPianoRollData ? "Showtime not available (processing in progress)" : "Showtime (Animated Orchestra Stage)", 6754 disabled: !hasPianoRollData, 6755 children: [ 6756 /* @__PURE__ */ jsxRuntimeExports.jsx(Sparkles, { size: 14 }), 6757 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Showtime" }) 6758 ] 6759 } 6760 ), 6761 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6762 "button", 6763 { 6764 onClick: () => { 6765 if (!hasPianoRollData || !hasMelodicTracks) return; 6766 setViewMode("piano"); 6767 if (isGameMode && setIsGameMode) setIsGameMode(false); 6768 }, 6769 className: getTabButtonClass(viewMode === "piano" && !isGameMode, false, !hasPianoRollData || !hasMelodicTracks), 6770 title: !hasPianoRollData ? "Piano Roll not available (processing in progress)" : !hasMelodicTracks ? "Piano Roll not available for percussion-only tracks" : "Piano Roll", 6771 disabled: !hasPianoRollData || !hasMelodicTracks, 6772 children: [
6773 /* @__PURE__ */ jsxRuntimeExports.jsx(Keyboard, { size: 14 }), 6774 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Piano" }) 6775 ] 6776 } 6777 ), 6778 /* @__PURE__ */ jsxRuntimeExports.jsx( 6779 "button", 6780 { 6781 onClick: () => { 6782 if (!hasPianoRollData || !hasMelodicTracks) return; 6783 setViewMode("piano"); 6784 if (setIsGameMode) setIsGameMode(true); 6785 }, 6786 className: getTabButtonClass(viewMode === "piano" && isGameMode, true, !hasPianoRollData || !hasMelodicTracks), 6787 title: !hasPianoRollData ? "Gamepad Play-Along not available (processing in progress)" : !hasMelodicTracks ? "Gamepad Play-Along not available for percussion-only tracks" : "Play-Along Game Mode", 6788 disabled: !hasPianoRollData || !hasMelodicTracks, 6789 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Gamepad2, { size: 14, className: viewMode === "piano" && isGameMode ? "animate-pulse" : "" }) 6790 } 6791 ),
6792 hasPercussion && /* @__PURE__ */ jsxRuntimeExports.jsxs( 6793 "button", 6794 { 6795 onClick: () => { 6796 if (!hasPianoRollData) return; 6797 setViewMode("percussion"); 6798 if (isGameMode && setIsGameMode) setIsGameMode(false); 6799 }, 6800 className: getTabButtonClass(viewMode === "percussion", false, !hasPianoRollData), 6801 title: !hasPianoRollData ? "Percussion Roll not available (processing in progress)" : "Percussion Roll View", 6802 disabled: !hasPianoRollData, 6803 children: [ 6804 /* @__PURE__ */ jsxRuntimeExports.jsx(Drum, { size: 14 }), 6805 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Perc" }) 6806 ] 6807 } 6808 ), 6809 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6810 "button", 6811 { 6812 onClick: () => { 6813 setViewMode("taiko"); 6814 if (isGameMode && setIsGameMode) setIsGameMode(false); 6815 }, 6816 className: getTabButtonClass(viewMode === "taiko", true, false), 6817 title: "Taiko no Hamie Rhythm Game", 6818 children: [
6819 /* @__PURE__ */ jsxRuntimeExports.jsx(Drum, { size: 14, className: viewMode === "taiko" ? "animate-pulse text-amber-300" : "" }), 6820 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Taiko" }) 6821 ] 6822 } 6823 ), 6824 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6825 "button", 6826 { 6827 onClick: () => { 6828 setViewMode("waveform"); 6829 if (isGameMode && setIsGameMode) setIsGameMode(false); 6830 }, 6831 className: getTabButtonClass(viewMode === "waveform"), 6832 title: "Waveform View", 6833 children: [ 6834 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { size: 14 }), 6835 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Wave" }) 6836 ] 6837 } 6838 ), 6839 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6840 "button", 6841 { 6842 onClick: () => { 6843 if (!hasScore) return; 6844 setViewMode(viewMode === "melody" ? "melody" : "score"); 6845 if (isGameMode && setIsGameMode) setIsGameMode(false); 6846 }, 6847 className: getTabButtonClass(viewMode === "score" || viewMode === "melody", false, !hasScore), 6848 title: !hasScore ? "Sheet Music Score not available (processing in progress)" : "Score", 6849 disabled: !hasScore, 6850 children: [
6851 /* @__PURE__ */ jsxRuntimeExports.jsx(BookOpen, { size: 14 }), 6852 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "Score" }) 6853 ] 6854 } 6855 ), 6856 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6857 "button", 6858 { 6859 onClick: () => { 6860 setViewMode("eq"); 6861 if (isGameMode && setIsGameMode) setIsGameMode(false); 6862 }, 6863 className: getTabButtonClass(viewMode === "eq"), 6864 title: "Equalizer Settings", 6865 children: [ 6866 /* @__PURE__ */ jsxRuntimeExports.jsx(SlidersHorizontal, { size: 14 }), 6867 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: "EQ" }) 6868 ] 6869 } 6870 ) 6871 ] }), 6872 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `${getSeparatorClass()} flex-shrink-0`, children: [
6873 /* @__PURE__ */ jsxRuntimeExports.jsx( 6874 "button", 6875 { 6876 onClick: togglePerfHud, 6877 className: `${getControlIconClass(false)} ${showPerfHud ? "text-cyan-400 bg-white/10" : ""}`, 6878 title: showPerfHud ? "Hide Device Performance Stats (FPS / RAM)" : "Show Device Performance Stats (FPS / RAM)", 6879 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { size: 18 }) 6880 } 6881 ), 6882 /* @__PURE__ */ jsxRuntimeExports.jsx( 6883 "button", 6884 { 6885 onClick: () => !isFullScreen && setIsTheaterMode(!isTheaterMode), 6886 className: getControlIconClass(isFullScreen), 6887 title: isFullScreen ? "Theater Mode disabled in Full Screen" : isTheaterMode ? "Exit Theater Mode" : "Theater Mode", 6888 disabled: isFullScreen, 6889 children: /* @__PURE__ */ jsxRuntimeExports.jsx(RectangleHorizontal, { size: 18 }) 6890 } 6891 ),
6892 /* @__PURE__ */ jsxRuntimeExports.jsx( 6893 "button", 6894 { 6895 onClick: toggleFullScreen, 6896 className: getControlIconClass(false), 6897 title: isFullScreen ? "Exit Full Screen" : "Full Screen", 6898 children: isFullScreen ? /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize, { size: 18 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Maximize, { size: 18 }) 6899 } 6900 ) 6901 ] }) 6902 ] }), 6903 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `relative flex-1 overflow-hidden group transition-all duration-300 ${isFullScreen ? getBgColor() : ""}`, children: [ 6904 /* @__PURE__ */ jsxRuntimeExports.jsxs( 6905 "div", 6906 { 6907 onClick: (e) => { 6908 const target = e.target; 6909 if (target.closest("button, select, input, textarea")) { 6910 return; 6911 } 6912 if (window.isPuppetDragging) { 6913 e.stopPropagation(); 6914 return; 6915 } 6916 if (viewMode === "eq" || viewMode === "score" || viewMode === "melody" || viewMode === "piano" || viewMode === "percussion" || isBuffering && isPlaying) return; 6917 if (!isPlaying && duration > 0 && currentTime >= duration - 0.1) { 6918 if (onSeek) onSeek(0); 6919 if (onPlayPause) onPlayPause(true); 6920 } else { 6921 if (onPlayPause) onPlayPause(); 6922 } 6923 }, 6924 className: `absolute inset-x-0 bottom-0 ${isFullScreen ? showControls ? "top-11" : "top-0" : "top-0"} transition-all duration-300 ${viewMode === "eq" || viewMode === "score" || viewMode === "melody" || viewMode === "percussion" ? "" : "cursor-pointer"} ${getBgColor()}`, 6925 children: [ 6926 viewMode === "orchestra" && isStaff && /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex items-center justify-center text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-8 h-8 animate-spin" }) }), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6927 OrchestraVisualizer, 6928 { 6929 track, 6930 currentTime, 6931 isPlaying: isMusicPlaying, 6932 pianoRollData, 6933 processedInstruments, 6934 uniqueNames, 6935 soloedIndex, 6936 onSolo, 6937 mutedIndices, 6938 onMute, 6939 onPanChange, 6940 hiddenTracks 6941 } 6942 ) }), 6943 viewMode === "premium_orchestra" && /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex items-center justify-center text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-8 h-8 animate-spin" }) }), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6944 SouthparkOrchestraVisualizer, 6945 { 6946 track, 6947 currentTime, 6948 isPlaying: isMusicPlaying, 6949 pianoRollData, 6950 processedInstruments, 6951 uniqueNames, 6952 soloedIndex, 6953 onSolo, 6954 mutedIndices, 6955 onMute, 6956 onPanChange, 6957 hiddenTracks, 6958 colorMap, 6959 selectedTrackIndex, 6960 onSelectTrack 6961 } 6962 ) }), 6963 viewMode === "cinematic" && isStaff && /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex items-center justify-center text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "w-8 h-8 animate-spin" }) }), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 6964 CinematicVisualizer, 6965 { 6966 track, 6967 currentTime, 6968 isPlaying: isMusicPlaying, 6969 pianoRollData, 6970 processedInstruments, 6971 uniqueNames, 6972 soloedIndex, 6973 onSolo, 6974 mutedIndices, 6975 onMute, 6976 onPanChange, 6977 hiddenTracks 6978 } 6979 ) }),
6980 viewMode === "piano" && /* @__PURE__ */ jsxRuntimeExports.jsx( 6981 PianoRoll, 6982 { 6983 data: pianoRollData, 6984 melodyData, 6985 currentTime, 6986 isPlaying: isMusicPlaying, 6987 isAudioLoaded, 6988 hiddenTracks, 6989 colorMap, 6990 selectedTrackIndex, 6991 processedInstruments, 6992 uniqueNames, 6993 onPlayPause, 6994 onSeek, 6995 isGameModeProp: isGameMode, 6996 setIsGameModeProp: setIsGameMode, 6997 onGameModeMute, 6998 onAuthRequested, 6999 trackId, 7000 playbackRate, 7001 userId, 7002 userName, 7003 duration, 7004 tempo: (_s = track == null ? void 0 : track.midiMetadata) == null ? void 0 : _s.tempo, 7005 timeSignature: ((_t = track == null ? void 0 : track.midiMetadata) == null ? void 0 : _t.timeSignature) ? track.midiMetadata.timeSignature.split("/").map(Number) : [4, 4], 7006 isLoadingAudio, 7007 onSelectTrack, 7008 soundfont, 7009 melodySource, 7010 theme 7011 } 7012 ), 7013 viewMode === "percussion" && /* @__PURE__ */ jsxRuntimeExports.jsx( 7014 PercussionRoll, 7015 { 7016 data: pianoRollData, 7017 currentTime, 7018 isPlaying: isMusicPlaying, 7019 hiddenTracks, 7020 colorMap, 7021 processedInstruments, 7022 uniqueNames, 7023 onPlayPause, 7024 onSeek, 7025 playbackRate, 7026 duration, 7027 tempo: (_u = track == null ? void 0 : track.midiMetadata) == null ? void 0 : _u.tempo, 7028 timeSignature: ((_v = track == null ? void 0 : track.midiMetadata) == null ? void 0 : _v.timeSignature) ? track.midiMetadata.timeSignature.split("/").map(Number) : [4, 4], 7029 soundfont, 7030 theme 7031 } 7032 ), 7033 viewMode === "taiko" && /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "animate-spin" }) }), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 7034 Taiko, 7035 { 7036 data: pianoRollData || [], 7037 melodyData, 7038 processedInstruments, 7039 midiMetadata: track == null ? void 0 : track.midiMetadata, 7040 currentTime, 7041 isPlaying: isMusicPlaying, 7042 duration, 7043 tempo: (_w = track == null ? void 0 : track.midiMetadata) == null ? void 0 : _w.tempo, 7044 timeSignature: ((_x = track == null ? void 0 : track.midiMetadata) == null ? void 0 : _x.timeSignature) ? track.midiMetadata.timeSignature.split("/").map(Number) : [4, 4], 7045 trackId, 7046 userId, 7047 userName, 7048 onPlayPause, 7049 onSeek, 7050 onGameModeMute, 7051 onAuthRequested, 7052 playbackRate, 7053 theme, 7054 isLoadingAudio, 7055 isAudioLoaded 7056 } 7057 ) }), 7058 viewMode === "waveform" && /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "animate-spin" }) }), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 7059 SpectrumBarsVisualizer, 7060 { 7061 isPlaying: isMusicPlaying, 7062 audioNode: audioMode !== "webmidi" && audioMode !== "singleopus" ? synth.getMasterGain() : void 0, 7063 audioContext: audioMode !== "webmidi" && audioMode !== "singleopus" ? synth.getAudioContext() : void 0 7064 }, 7065 track == null ? void 0 : track.id 7066 ) }), 7067 (viewMode === "score" || viewMode === "melody") && /* @__P
7067URE__ */ jsxRuntimeExports.jsx("div", { className: "h-full w-full bg-[#fdfaf5] overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-full gap-3 bg-[#fdfaf5]", children: [ 7068 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center justify-center w-12 h-12", children: [ 7069 /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 40, className: "text-indigo-600 animate-spin" }), 7070 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 18, className: "absolute text-indigo-600" }) 7071 ] }), 7072 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-zinc-600 tracking-wide", children: "Loading Sheet Music Engine..." }) 7073 ] }), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 7074 SheetMusicViewer, 7075 { 7076 xmlData, 7077 scorePath: track.scorePath, 7078 quantizedScorePaths: track.quantizedScorePaths, 7079 trackVisibility: trackVisibility || uniqueNames.map((name) => !hiddenTracks.has(name)), 7080 isPlaying: isMusicPlaying, 7081 currentTime, 7082 duration, 7083 processedInstruments, 7084 recommendedQuantization: (_y = track.midiMetadata) == null ? void 0 : _y.recommendedQuantization, 7085 onSeek, 7086 title: track.title, 7087 selectedTrackIndex, 7088 midiId: track.id, 7089 isMelodyMode: viewMode === "melody", 7090 onToggleMelody: () => setViewMode(viewMode === "melody" ? "score" : "melody"), 7091 melodySource, 7092 setMelodySource, 7093 melodyData, 7094 onGameModeMute, 7095 onPlayPause, 7096 uploaderName, 7097 audioLatency: audioMode === "synth" ? (synth.getAudioContext().outputLatency || 0) + (synth.getAudioContext().baseLatency || 0) : 0, 7098 sections: ((_z = track.midiMetadata) == null ? void 0 : _z.sections) || [], 7099 measureMap: ((_A = track.midiMetadata) == null ? void 0 : _A.measureMap) || [], 7100 soundfont, 7101 harmonyChordsUrl: (_B = track.midiMetadata) == null ? void 0 : _B.harmonyChordsXmlPath, 7102 harmonyRomanUrl: (_C = track.midiMetadata) == null ? void 0 : _C.harmonyRomanXmlPath, 7103 theme 7104 } 7105 ) }) }), 7106 viewMode === "karaoke" && /* @__PURE__ */ jsxRuntimeExports.jsxs(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "animate-spin" }) }), children: [
7107 hasLyrics && /* @__PURE__ */ jsxRuntimeExports.jsx( 7108 Lyrics, 7109 { 7110 track, 7111 currentTime, 7112 melodyData: (_D = track.midiMetadata) == null ? void 0 : _D.melodyData, 7113 pianoRollData, 7114 isPlaying: isMusicPlaying, 7115 playbackRate, 7116 userId, 7117 trackId 7118 } 7119 ), 7120 " ", 7121 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-4 right-4 flex items-center gap-2 z-50", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 7122 "button", 7123 { 7124 onClick: (e) => { 7125 e.stopPropagation(); 7126 setShowVocalControls(!showVocalControls); 7127 }, 7128 onPointerDown: (e) => e.stopPropagation(), 7129 className: "p-3 bg-zinc-800/80 hover:bg-zinc-700 backdrop-blur rounded-full text-zinc-300 transition-colors border border-zinc-700/50 shadow-xl", 7130 title: "Vocal & Audio Controls", 7131 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Settings2, { size: 20 }) 7132 } 7133 ) }), 7134 showVocalControls && /* @__PURE__ */ jsxRuntimeExports.jsxs( 7135 "div", 7136 { 7137 className: "absolute top-11 right-0 bottom-0 w-80 bg-zinc-900/95 backdrop-blur-xl border-l border-zinc-700/50 p-5 shadow-2xl z-50 overflow-y-auto animate-in slide-in-from-right", 7138 onClick: (e) => e.stopPropagation(), 7139 onPointerDown: (e) => e.stopPropagation(), 7140 children: [
7141 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center mb-4", children: [ 7142 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-white font-bold flex items-center gap-2", children: [ 7143 /* @__PURE__ */ jsxRuntimeExports.jsx(MicVocal, { size: 16, className: "text-indigo-400" }), 7144 " Vocal DSP Engine" 7145 ] }), 7146 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setShowVocalControls(false), className: "text-zinc-400 hover:text-white transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 16 }) }) 7147 ] }), 7148 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 7149 audioDevices.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 7150 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-zinc-400 mb-1", children: "Microphone" }),
7151 /* @__PURE__ */ jsxRuntimeExports.jsx( 7152 "select", 7153 { 7154 value: selectedDeviceId, 7155 onChange: (e) => setSelectedDeviceId(e.target.value), 7156 className: "w-full bg-zinc-800 text-sm text-zinc-300 border border-zinc-700 rounded-lg p-2 focus:border-indigo-500 outline-none", 7157 children: audioDevices.map((device) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: device.deviceId, children: device.label || `Microphone ${audioDevices.indexOf(device) + 1}` }, device.deviceId)) 7158 } 7159 ) 7160 ] }), 7161 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 7162 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs text-zinc-400 mb-1", children: [ 7163 /* @__P
7163URE__ */ jsxRuntimeExports.jsx("span", { children: "Reverb" }), 7164 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 7165 Math.round(reverbAmount * 100), 7166 "%" 7167 ] }) 7168 ] }), 7169 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "range", min: "0", max: "1", step: "0.05", value: reverbAmount, onChange: (e) => setReverbAmount(parseFloat(e.target.value)), className: "w-full accent-indigo-500 h-1 bg-zinc-700 rounded-lg appearance-none" }) 7170 ] }), 7171 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [ 7172 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs text-zinc-400 mb-1", children: [ 7173 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Echo / Delay" }), 7174 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 7175 Math.round(delayAmount * 100), 7176 "%" 7177 ] }) 7178 ] }),
7179 /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "range", min: "0", max: "1", step: "0.05", value: delayAmount, onChange: (e) => setDelayAmount(parseFloat(e.target.value)), className: "w-full accent-indigo-500 h-1 bg-zinc-700 rounded-lg appearance-none" }) 7180 ] }), 7181 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pt-2 border-t border-zinc-800", children: [ 7182 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-300", children: "Volume Normalization" }), 7183 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setVocalNormalize(!vocalNormalize), className: `w-10 h-5 rounded-full relative transition-colors ${vocalNormalize ? "bg-indigo-500" : "bg-zinc-700"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-3 h-3 bg-white rounded-full absolute top-1 transition-transform ${vocalNormalize ? "left-6" : "left-1"}` }) }) 7184 ] }),
7185 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pt-2 border-t border-zinc-800", children: [ 7186 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-300", title: "Mutes melody track when singing detected", children: "Auto-Vocal Mode" }), 7187 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setAutoVocal(!autoVocal), className: `w-10 h-5 rounded-full relative transition-colors ${autoVocal ? "bg-indigo-500" : "bg-zinc-700"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-3 h-3 bg-white rounded-full absolute top-1 transition-transform ${autoVocal ? "left-6" : "left-1"}` }) }) 7188 ] }), 7189 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pt-2 border-t border-zinc-800", children: [ 7190 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-300", title: "Reduces graphic quality and active particles for smoother performance", children: "Low Performance Mode" }),
7191 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => { 7192 perfStats.toggleLowPerf(); 7193 setLowPerf(!perfStats.lowPerf); 7194 }, className: `w-10 h-5 rounded-full relative transition-colors ${perfStats.lowPerf ? "bg-emerald-600" : "bg-zinc-700"}`, children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-3 h-3 bg-white rounded-full absolute top-1 transition-transform ${perfStats.lowPerf ? "left-6" : "left-1"}` }) }) 7195 ] }), 7196 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pt-2 border-t border-zinc-800/60 text-xs text-zinc-400", children: [ 7197 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Current Framerate" }), 7198 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `font-mono font-bold ${perfStats.fps >= 50 ? "text-emerald-400" : perfStats.fps >= 30 ? "text-amber-400" : "text-red-400"}`, children: [ 7199 perfStats.fps, 7200 " FPS" 7201 ] }) 7202 ] }), 7203 perfStats.memUsedMB !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs text-zinc-400", children: [
7204 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Browser Memory Heap" }), 7205 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "font-mono text-zinc-300", children: [ 7206 perfStats.memUsedMB, 7207 " MB / ", 7208 perfStats.memLimitMB, 7209 " MB" 7210 ] }) 7211 ] }), 7212 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between text-xs text-zinc-400 pt-1", children: [ 7213 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Performance HUD" }), 7214 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: togglePerfHud, className: `px-2 py-0.5 rounded text-[11px] font-medium transition-colors ${showPerfHud ? "bg-cyan-600 text-white" : "bg-zinc-800 text-zinc-400 hover:text-white"}`, children: showPerfHud ? "Visible" : "Hidden" }) 7215 ] }) 7216 ] }),
7217 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-white font-bold mt-6 mb-4 flex items-center gap-2", children: [ 7218 /* @__PURE__ */ jsxRuntimeExports.jsx(Settings2, { size: 16, className: "text-emerald-400" }), 7219 " Playback Controls" 7220 ] }), 7221 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [ 7222 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 7223 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-300", children: "Key Shift" }), 7224 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [ 7225 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setPitchSemitones && setPitchSemitones((pitchSemitones || 0) - 1), className: "w-6 h-6 flex items-center justify-center bg-zinc-800 rounded hover:bg-zinc-700 text-white", children: "-" }),
7226 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-sm font-mono text-zinc-300 w-6 text-center", children: [ 7227 (pitchSemitones || 0) > 0 ? "+" : "", 7228 pitchSemitones || 0 7229 ] }), 7230 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setPitchSemitones && setPitchSemitones((pitchSemitones || 0) + 1), className: "w-6 h-6 flex items-center justify-center bg-zinc-800 rounded hover:bg-zinc-700 text-white", children: "+" }) 7231 ] }) 7232 ] }), 7233 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [ 7234 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm text-zinc-300", children: "Tempo" }), 7235 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3", children: [
7236 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setCurrentTempo && setCurrentTempo((currentTempo || originalTempo || 120) * 0.95), className: "w-6 h-6 flex items-center justify-center bg-zinc-800 rounded hover:bg-zinc-700 text-white", children: "-" }), 7237 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs font-mono text-zinc-300 w-10 text-center", children: [ 7238 Math.round((currentTempo || originalTempo || 120) / (originalTempo || 120) * 100), 7239 "%" 7240 ] }), 7241 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setCurrentTempo && setCurrentTempo((currentTempo || originalTempo || 120) * 1.05), className: "w-6 h-6 flex items-center justify-center bg-zinc-800 rounded hover:bg-zinc-700 text-white", children: "+" }) 7242 ] }) 7243 ] }) 7244 ] }) 7245 ] 7246 } 7247 ) 7248 ] }) 7249 ] 7250 } 7251 ), 7252 hasOpenedEQ && /* @__PURE__ */ jsxRuntimeExports.jsx( 7253 "div", 7254 { 7255 style: { display: viewMode === "eq" ? "block" : "none" }, 7256 className: `absolute inset-x-0 bottom-0 ${isFullScreen ? showControls ? "top-11" : "top-0" : "top-0"} transition-all duration-300 z-10 bg-[#050505]`, 7257 children: /* @__PURE__ */ jsxRuntimeExports.jsx(reactExports.Suspense, { fallback: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full text-zinc-500", children: /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { className: "animate-spin" }) }), children: /* @__PURE__ */ jsxRuntimeExports.jsx( 7258 ParametricEQ, 7259 { 7260 inputNode: synth.getMasterGain(), 7261 availableNodes: synth.getChannels(), 7262 tracks: processedInstruments, 7263 selectedTrackIndex, 7264 onSelectTrack, 7265 trackId, 7266 userId 7267 } 7268 ) }) 7269 } 7270 ), 7271 autoPlayCountdown !== null && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-0 z-[60] bg-black/90 backdrop-blur-md flex flex-col items-center justify-center animate-in fade-in duration-500", children: [
7272 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-zinc-400 text-sm font-bold uppercase tracking-widest mb-4", children: [ 7273 "Up Next in ", 7274 autoPlayCountdown, 7275 "s" 7276 ] }), 7277 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-6 max-w-sm text-center", children: [ 7278 nextTrack && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "group relative cursor-pointer", onClick: onPlayNext, children: [ 7279 /* @__PURE__ */ jsxRuntimeExports.jsx( 7280 VideoCard, 7281 { 7282 track: nextTrack, 7283 variant: "player-thumbnail", 7284 sizeClass: "w-48 h-28 rounded-xl shadow-2xl ring-2 ring-zinc-800 group-hover:ring-indigo-500 transition-all", 7285 onSelect: onPlayNext 7286 } 7287 ), 7288 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4", children: [
7289 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "text-2xl font-bold text-white group-hover:text-indigo-400 transition-colors", children: nextTrack.title }), 7290 nextTrack.artist && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-500 text-sm", children: nextTrack.artist }) 7291 ] }) 7292 ] }), 7293 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-4 mt-2", children: [ 7294 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onCancelAutoPlay, className: "px-6 py-2 rounded-full bg-zinc-800 text-white font-bold hover:bg-zinc-700 transition-colors border border-zinc-700", children: "Cancel" }), 7295 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: onPlayNext, className: "px-6 py-2 rounded-full bg-white text-black font-bold hover:bg-zinc-200 transition-colors flex items-center gap-2", children: [ 7296 /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 18, fill: "currentColor" }), 7297 " Play Now" 7298 ] }) 7299 ] }) 7300 ] }) 7301 ] }), 7302 isLoadingAudio && isPlaying && viewMode !== "taiko" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex items-center justify-center z-40 bg-black/40 backdrop-blur-sm animate-in fade-in", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-3", children: [
7303 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-16 h-16 bg-black/60 backdrop-blur-sm rounded-full flex items-center justify-center border-2 border-white/10 animate-pulse", children: /* @__PURE__ */ jsxRuntimeExports.jsx(LoaderCircle, { size: 32, className: "text-white animate-spin" }) }), 7304 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-white font-bold text-sm bg-black/50 px-3 py-1 rounded-full backdrop-blur-sm shadow-lg", children: "Loading Audio..." }) 7305 ] }) }), 7306 !isPlaying && !autoPlayCountdown && viewMode !== "eq" && viewMode !== "score" && viewMode !== "melody" && viewMode !== "percussion" && viewMode !== "taiko" && (!isGameMode || viewMode !== "piano") && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 flex items-center justify-center pointer-events-none z-40", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 7307 "div", 7308 { 7309 className: "w-20 h-20 bg-black/40 backdrop-blur-sm rounded-full flex items-center justify-center border-2 border-white/20 animate-in zoom-in fade-in duration-300 pointer-events-auto cursor-pointer hover:bg-black/60 hover:scale-105 transition-all", 7310 onClick: (e) => { 7311 e.stopPropagation(); 7312 if (!isPlaying && duration > 0 && currentTime >= duration - 0.1) { 7313 if (onSeek) onSeek(0); 7314 if (onPlayPause) onPlayPause(true); 7315 } else { 7316 if (onPlayPause) onPlayPause(); 7317 } 7318 }, 7319 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Play, { size: 40, fill: "white", className: "text-white ml-2 opacity-80" }) 7320 } 7321 ) }),
7322 showPerfHud && /* @__PURE__ */ jsxRuntimeExports.jsx( 7323 PerformanceHud, 7324 { 7325 stats: perfStats, 7326 onClose: togglePerfHud 7327 } 7328 ), 7329 perfStats.isLagging && !perfStats.lowPerf && !dismissedLag && !showPerfHud && /* @__PURE__ */ jsxRuntimeExports.jsx( 7330 PerformanceLagBanner, 7331 { 7332 fps: perfStats.fps, 7333 onEnableLowPerf: perfStats.toggleLowPerf, 7334 onDismiss: () => setDismissedLag(true) 7335 } 7336 ) 7337 ] }), 7338 (viewMode === "score" || viewMode === "melody") && pageCount > 1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getBottomBarClass(), children: [ 7339 /* @__PURE__ */ jsxRuntimeExports.jsx( 7340 "button", 7341 { 7342 onClick: () => { 7343 const newPage = Math.max(1, currentPage - 1); 7344 if (onSeek) { 7345 setCurrentPage(newPage); 7346 } else { 7347 setCurrentPage(newPage); 7348 } 7349 }, 7350 disabled: currentPage === 1, 7351 className: getBottomButtonClass(), 7352 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronLeft, { size: 20 }) 7353 } 7354 ),
7355 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: getBottomTextClass(), children: [ 7356 "Page ", 7357 currentPage, 7358 " / ", 7359 pageCount 7360 ] }), 7361 /* @__PURE__ */ jsxRuntimeExports.jsx( 7362 "button", 7363 { 7364 onClick: () => { 7365 const newPage = Math.min(pageCount, currentPage + 1); 7366 if (onSeek) { 7367 setCurrentPage(newPage); 7368 } else { 7369 setCurrentPage(newPage); 7370 } 7371 }, 7372 disabled: currentPage === pageCount, 7373 className: getBottomButtonClass(), 7374 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronRight, { size: 20 }) 7375 } 7376 ) 7377 ] }) 7378 ] 7379 } 7380 ); 7381}; 7382const RotaryKnob = ({ 7383 value, 7384 onChange, 7385 min = -1, 7386 max = 1, 7387 label, 7388 accentColor = "#4ee2e2", 7389 minLabel = "L", 7390 maxLabel = "R" 7391}) => { 7392 const ratio = (value - min) / (max - min); 7393 const angle = -135 + ratio * 270; 7394 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center select-none relative group w-14", children: [ 7395 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[7.5px] font-extrabold text-zinc-400 tracking-wider mb-0.5 uppercase", children: label }), 7396 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-10 h-10 flex items-center justify-center cursor-pointer", children: [
7397 /* @__PURE__ */ jsxRuntimeExports.jsx( 7398 "div", 7399 { 7400 className: "absolute w-10 h-10 rounded-full border border-zinc-700 shadow-md", 7401 style: { 7402 background: "radial-gradient(circle, #a3a3a3 0%, #666666 100%)", 7403 boxShadow: "inset 0 1px 0 rgba(255,255,255,0.4), 0 2px 4px rgba(0,0,0,0.4)" 7404 } 7405 } 7406 ), 7407 /* @__PURE__ */ jsxRuntimeExports.jsx( 7408 "div", 7409 { 7410 className: "absolute w-7 h-7 rounded-full flex items-center justify-center border border-zinc-800", 7411 style: { 7412 transform: `rotate(${angle}deg)`, 7413 background: `radial-gradient(circle, ${accentColor} 0%, #112d2d 100%)`, 7414 boxShadow: "inset 0 1px 0 rgba(255,255,255,0.3), 0 1px 3px rgba(0,0,0,0.6)" 7415 }, 7416 children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-[1.5px] h-3 bg-white absolute top-0 rounded-full shadow-[0_0_2px_rgba(0,0,0,0.8)]" }) 7417 } 7418 ), 7419 /* @__PURE__ */ jsxRuntimeExports.jsx( 7420 "input", 7421 { 7422 type: "range", 7423 min, 7424 max, 7425 step: "0.05", 7426 value, 7427 onClick: (e) => e.stopPropagation(), 7428 onDoubleClick: (e) => { 7429 e.stopPropagation(); 7430 onChange(0); 7431 }, 7432 onChange: (e) => onChange(parseFloat(e.target.value)), 7433 className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer z-10" 7434 } 7435 ) 7436 ] }), 7437 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between w-11 text-[7px] font-bold text-zinc-400 mt-0.5 leading-none", children: [
7438 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: minLabel }), 7439 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: maxLabel }) 7440 ] }) 7441 ] }); 7442}; 7443const LedButton = ({ 7444 label, 7445 active, 7446 onClick, 7447 ledColor = "#fbbf24" 7448}) => { 7449 return /* @__PURE__ */ jsxRuntimeExports.jsx( 7450 "button", 7451 { 7452 onClick: (e) => { 7453 e.stopPropagation(); 7454 onClick(); 7455 }, 7456 className: "w-full h-5 flex items-center px-1.5 py-0.5 rounded border border-zinc-950 text-[8px] font-extrabold uppercase tracking-wider text-zinc-300 transition-all select-none", 7457 style: { 7458 background: "linear-gradient(to bottom, #444444 0%, #292929 100%)", 7459 boxShadow: active ? "inset 0 2px 3px rgba(0,0,0,0.9), 0 1px 0 rgba(255,255,255,0.05)" : "inset 0 1px 0 rgba(255,255,255,0.1), 0 2px 3px rgba(0,0,0,0.5)" 7460 }, 7461 children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 w-full", children: [ 7462 /* @__PURE__ */ jsxRuntimeExports.jsx( 7463 "div", 7464 { 7465 className: "w-2 h-2 rounded-full border border-zinc-950 transition-all", 7466 style: { 7467 backgroundColor: active ? ledColor : "#1a1a1a", 7468 boxShadow: active ? `0 0 5px 1px ${ledColor}, inset 0 1px 1px rgba(255,255,255,0.4)` : "inset 0 1px 1px rgba(0,0,0,0.8)" 7469 } 7470 } 7471 ),
7472 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "leading-none", children: label }) 7473 ] }) 7474 } 7475 ); 7476}; 7477const getVuSegmentClass = (segmentNum, lit) => { 7478 const base = "w-1.5 h-1.5 rounded-full transition-all duration-75"; 7479 if (lit) { 7480 if (segmentNum > 11) { 7481 return `${base} bg-rose-500 shadow-[0_0_5px_1px_rgba(244,63,94,0.8)]`; 7482 } else if (segmentNum > 9) { 7483 return `${base} bg-amber-400 shadow-[0_0_5px_1px_rgba(245,158,11,0.8)]`; 7484 } else { 7485 return `${base} bg-emerald-500 shadow-[0_0_5px_1px_rgba(16,185,129,0.8)]`; 7486 } 7487 } else { 7488 if (segmentNum > 11) { 7489 return `${base} bg-rose-950/45 border border-rose-900/10`; 7490 } else if (segmentNum > 9) { 7491 return `${base} bg-amber-950/45 border border-amber-900/10`; 7492 } else { 7493 return `${base} bg-emerald-950/45 border border-emerald-900/10`; 7494 } 7495 } 7496}; 7497const SegmentedVuMeter = ({ 7498 idx, 7499 vuMeterRefs 7500}) => { 7501 const segments = Array.from({ length: 14 }); 7502 return /* @__PURE__ */ jsxRuntimeExports.jsx( 7503 "div", 7504 { 7505 className: "w-4 h-56 bg-zinc-950 border border-zinc-900 rounded-full relative flex flex-col justify-between items-center py-2 px-[3px]", 7506 style: { 7507 boxShadow: "inset 0 1.5px 3px rgba(0,0,0,0.85)" 7508 }, 7509 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 7510 "div", 7511 { 7512 ref: (el) => { 7513 vuMeterRefs.current[idx] = el; 7514 }, 7515 className: "absolute inset-[2px] flex flex-col justify-between items-center origin-bottom h-[calc(100%-4px)]", 7516 children: segments.map((_, sIdx) => { 7517 const segmentNum = 13 - sIdx; 7518 return /* @__PURE__ */ jsxRuntimeExports.jsx( 7519 "div", 7520 { 7521 "data-segment": segmentNum, 7522 className: getVuSegmentClass(segmentNum, false) 7523 }, 7524 sIdx 7525 ); 7526 }) 7527 } 7528 ) 7529 } 7530 ); 7531}; 7532const MasterSegmentedVuMeter = ({ 7533 masterVuRef 7534}) => { 7535 const segments = Array.from({ length: 14 }); 7536 return /* @__PURE__ */ jsxRuntimeExports.jsx( 7537 "div", 7538 { 7539 className: "w-4 h-56 bg-zinc-950 border border-zinc-900 rounded-full relative flex flex-col justify-between items-center py-2 px-[3px]", 7540 style: { 7541 boxShadow: "inset 0 1.5px 3px rgba(0,0,0,0.85)" 7542 }, 7543 children: /* @__PURE__ */ jsxRuntimeExports.jsx( 7544 "div", 7545 { 7546 ref: masterVuRef, 7547 className: "absolute inset-[2px] flex flex-col justify-between items-center origin-bottom h-[calc(100%-4px)]", 7548 children: segments.map((_, sIdx) => { 7549 const segmentNum = 13 - sIdx; 7550 return /* @__PURE__ */ jsxRuntimeExports.jsx( 7551 "div", 7552 { 7553 "data-segment": segmentNum, 7554 className: getVuSegmentClass(segmentNum, false) 7555 }, 7556 sIdx 7557 ); 7558 }) 7559 } 7560 ) 7561 } 7562 ); 7563}; 7564const copyStyles = (sourceDoc, targetDoc) => { 7565 Array.from(sourceDoc.querySelectorAll('link[rel="stylesheet"]')).forEach((link) => { 7566 const newLink = targetDoc.createElement("link"); 7567 newLink.rel = "stylesheet"; 7568 newLink.href = link.href; 7569 targetDoc.head.appendChild(newLink); 7570 });
7571 Array.from(sourceDoc.querySelectorAll("style")).forEach((style) => { 7572 const newStyle = targetDoc.createElement("style"); 7573 newStyle.textContent = style.textContent; 7574 try { 7575 if (style.sheet) { 7576 const rules = Array.from(style.sheet.cssRules).map((r) => r.cssText).join("\n"); 7577 newStyle.textContent = rules; 7578 } 7579 } catch (e) { 7580 console.warn("Could not copy stylesheet rules", e); 7581 } 7582 targetDoc.head.appendChild(newStyle); 7583 }); 7584 Array.from(sourceDoc.styleSheets).forEach((sheet) => { 7585 try { 7586 if (sheet.cssRules && !Array.from(sourceDoc.querySelectorAll("style")).some((s) => s.sheet === sheet)) { 7587 const newStyle = targetDoc.createElement("style"); 7588 const rules = Array.from(sheet.cssRules).map((r) => r.cssText).join("\n"); 7589 newStyle.textContent = rules; 7590 targetDoc.head.appendChild(newStyle); 7591 } 7592 } catch (e) { 7593 if (sheet.href) { 7594 const newLink = targetDoc.createElement("link"); 7595 newLink.rel = "stylesheet"; 7596 newLink.href = sheet.href; 7597 targetDoc.head.appendChild(newLink); 7598 } 7599 } 7600 }); 7601}; 7602const InstrumentSelector = ({ 7603 currentProgram, 7604 channel, 7605 onChange, 7606 variant = "console", 7607 theme = "dark", 7608 size = "md" 7609}) => { 7610 const [isOpen, setIsOpen] = reactExports.useState(false); 7611 const [expandedGroup, setExpandedGroup] = reactExports.useState(null); 7612 const [menuStyle, setMenuStyle] = reactExports.useState({}); 7613 const buttonRef = reactExports.useRef(null); 7614 const menuRef = reactExports.useRef(null); 7615 const { t } = useTranslation(); 7616 const scrollToSelected = reactExports.useCallback(() => { 7617 if (!menuRef.current) return; 7618 const selectedEl = menuRef.current.querySelector('[data-selected="true"]'); 7619 if (selectedEl) { 7620 const menu = menuRef.current; 7621 const menuRect = menu.getBoundingClientRect(); 7622 const itemRect = selectedEl.getBoundingClientRect(); 7623 const offset = itemRect.top - menuRect.top - menuRect.height / 2 + itemRect.height / 2; 7624 menu.scrollTop += offset; 7625 } 7626 }, []); 7627 const updatePosition = reactExports.useCallback(() => { 7628 if (!buttonRef.current) return; 7629 const rect = buttonRef.current.getBoundingClientRect(); 7630 const spaceBelow = window.innerHeight - rect.bottom; 7631 const spaceAbove = rect.top; 7632 const menuWidth = Math.min(window.innerWidth - 20, Math.max(rect.width, 270)); 7633 let left = rect.left; 7634 if (left + menuWidth > window.innerWidth - 10) { 7635 left = Math.max(10, window.innerWidth - menuWidth - 10); 7636 } 7637 if (spaceBelow >= 200 || spaceBelow > spaceAbove) { 7638 setMenuStyle({ 7639 position: "fixed", 7640 top: rect.bottom + 4, 7641 left, 7642 width: menuWidth, 7643 maxHeight: Math.min(320, spaceBelow - 16), 7644 zIndex: 99999 7645 }); 7646 } else { 7647 setMenuStyle({ 7648 position: "fixed", 7649 bottom: window.innerHeight - rect.top + 4, 7650 left, 7651 width: menuWidth, 7652 maxHeight: Math.min(320, spaceAbove - 16), 7653 zIndex: 99999 7654 }); 7655 } 7656 }, []); 7657 reactExports.useEffect(() => { 7658 if (!isOpen) return; 7659 const raf = requestAnimationFrame(() => { 7660 scrollToSelected(); 7661 }); 7662 const timer = setTimeout(scrollToSelected, 50); 7663 const handleClickOutside = (event) => { 7664 const target = event.target; 7665 if (buttonRef.current && !buttonRef.current.contains(target) && (!menuRef.current || !menuRef.current.contains(target))) { 7666 setIsOpen(false); 7667 } 7668 }; 7669 const handleScroll = (e) => { 7670 if (menuRef.current && menuRef.current.contains(e.target)) { 7671 return; 7672 } 7673 updatePosition(); 7674 }; 7675 document.addEventListener("mousedown", handleClickOutside); 7676 window.addEventListener("scroll", handleScroll, true); 7677 window.addEventListener("resize", updatePosition); 7678 return () => { 7679 cancelAnimationFrame(raf); 7680 clearTimeout(timer); 7681 document.removeEventListener("mousedown", handleClickOutside); 7682 window.removeEventListener("scroll", handleScroll, true); 7683 window.removeEventListener("resize", updatePosition); 7684 }; 7685 }, [isOpen, expandedGroup, scrollToSelected, updatePosition]); 7686 const isPercussion = channel === 9; 7687 const bgClass = variant === "expand" ? "bg-[#3b0b0b] border-[#5f1313] hover:bg-[#4a0d0d] text-[#ff7b7b] shadow-[inset_0_1.5px_3px_rgba(0,0,0,0.8)]" : variant === "console" ? theme === "light" ? "bg-white border-zinc-200 hover:bg-zinc-50 text-zinc-700 shadow-sm" : theme === "retro" ? "bg-[#1a1a1a] border-[#3a3a3a] hover:bg-[#2a2a2a] text-[#ffd700]" : theme === "fun" ? "bg-white border-pink-200 hover:bg-pink-50 text-pink-800" : "bg-zinc-900/80 border-white/10 hover:bg-zinc-800 text-zinc-300" : theme === "light" ? "bg-transparent border-transparent hover:bg-zinc-100 text-zinc-800" : theme === "fun" ? "bg-transparent border-transparent hover:bg-pink-50 text-pink-900" : "bg-transparent border-transparent hover:bg-white/5 text-zinc-300"; 7688 const dropdownBgClass = variant === "expand" ? "bg-zinc-900 border-zinc-700 shadow-2xl text-zinc-100" : theme === "light" ? "bg-white border-zinc-200 shadow-2xl text-zinc-800" : theme === "retro" ? "bg-[#1a1a1a] border-[#3a3a3a] shadow-2xl text-[#ffd700]" : theme === "fun" ? "bg-white border-pink-200 shadow-2xl text-pink-900" : "bg-zinc-900 border-zinc-700 shadow-2xl text-zinc-100"; 7689 const itemHoverClass = variant === "expand" || theme === "dark" || theme === "retro" ? "hover:bg-indigo-600/30 hover:text-white" : theme === "light" ? "hover:bg-indigo-50 hover:text-indigo-900" : theme === "fun" ? "hover:bg-pink-100 hover:text-pink-950" : "hover:bg-indigo-600/30 hover:text-white"; 7690 const selectedTextClass = variant === "expand" || theme === "dark" || theme === "retro" ? "text-indigo-400 font-bold" : "text-indigo-600 font-bold"; 7691 const displayTitle = getInstrumentName(currentProgram, channel); 7692 const isDarkOrExpand = variant === "expand" || theme === "dark" || theme === "retro"; 7693 const textUnselectedClass = isDarkOrExpand ? "text-zinc-200" : theme === "fun" ? "text-pink-900" : "text-zinc-800"; 7694 const pitchNumberClass = isDarkOrExpand ? "text-zinc-400 font-mono" : "text-zinc-400 font-mono"; 7695 const categoryHeaderClass = isDarkOrExpand ? "border-zinc-800 text-zinc-200 bg-zinc-950/80 hover:bg-zinc-800/80" : theme === "fun" ? "border-pink-200 text-pink-900 bg-pink-50 hover:bg-pink-100" : "border-zinc-200 text-zinc-800 bg-zinc-100/80 hover:bg-zinc-200/80"; 7696 const paddingTextClass = size === "sm" ? "py-0.5 px-1.5 text-[11px]" : "py-1 px-2 text-xs"; 7697 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full min-w-0", children: [
7698 /* @__PURE__ */ jsxRuntimeExports.jsxs( 7699 "button", 7700 { 7701 ref: buttonRef, 7702 onClick: (e) => { 7703 e.stopPropagation(); 7704 if (isPercussion) return; 7705 if (!isOpen) { 7706 updatePosition(); 7707 const cat = INSTRUMENT_CATEGORIES.find((c) => currentProgram >= c.start && currentProgram <= c.end); 7708 setExpandedGroup(cat ? cat.name : null); 7709 } 7710 setIsOpen(!isOpen); 7711 }, 7712 disabled: isPercussion, 7713 className: `w-full flex items-center justify-between ${paddingTextClass} font-sans font-medium border rounded ${bgClass} transition-colors ${isPercussion ? "opacity-70 cursor-not-allowed" : "cursor-pointer"}`, 7714 title: isPercussion ? "Percussion channel cannot be changed" : displayTitle, 7715 children: [ 7716 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate mr-0.5 min-w-0 flex-1 text-left tracking-tight", children: displayTitle }), 7717 !isPercussion && /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown$1, { size: 10, className: "shrink-0 opacity-70 ml-1" }) 7718 ] 7719 } 7720 ), 7721 isOpen && !isPercussion && reactDomExports.createPortal(
7722 /* @__PURE__ */ jsxRuntimeExports.jsx(AnimatePresence, { children: /* @__PURE__ */ jsxRuntimeExports.jsx( 7723 motion.div, 7724 { 7725 ref: menuRef, 7726 initial: { opacity: 0, scale: 0.95 }, 7727 animate: { opacity: 1, scale: 1 }, 7728 exit: { opacity: 0, scale: 0.95 }, 7729 transition: { duration: 0.12 }, 7730 className: `overflow-y-auto border rounded-md custom-scrollbar ${dropdownBgClass}`, 7731 style: menuStyle, 7732 onClick: (e) => e.stopPropagation(), 7733 children: INSTRUMENT_CATEGORIES.map((category) => { 7734 const isExpanded = expandedGroup === category.name; 7735 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col", children: [ 7736 /* @__PURE__ */ jsxRuntimeExports.jsxs( 7737 "button", 7738 { 7739 className: `flex items-center justify-between w-full px-3 py-2 text-xs font-bold text-left border-b transition-colors ${categoryHeaderClass}`, 7740 onClick: (e) => { 7741 e.stopPropagation(); 7742 setExpandedGroup(isExpanded ? null : category.name); 7743 }, 7744 children: [
7745 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: category.name }), 7746 isExpanded ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 12, className: "opacity-70" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown$1, { size: 12, className: "opacity-70" }) 7747 ] 7748 } 7749 ), 7750 /* @__PURE__ */ jsxRuntimeExports.jsx(AnimatePresence, { children: isExpanded && /* @__PURE__ */ jsxRuntimeExports.jsx( 7751 motion.div, 7752 { 7753 initial: { height: 0, opacity: 0 }, 7754 animate: { height: "auto", opacity: 1 }, 7755 exit: { height: 0, opacity: 0 }, 7756 transition: { duration: 0.15 }, 7757 className: `overflow-hidden ${isDarkOrExpand ? "bg-zinc-950/40" : theme === "fun" ? "bg-pink-50/20" : "bg-zinc-50/50"}`, 7758 children: GM_INSTRUMENTS.slice(category.start, category.end + 1).map((instName, idx) => { 7759 const progNum = category.start + idx; 7760 const isSelected = progNum === currentProgram; 7761 const selectedBgClass = isDarkOrExpand ? "bg-indigo-600/25 font-semibold" : "bg-indigo-50 font-semibold"; 7762 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 7763 "button", 7764 { 7765 "data-selected": isSelected ? "true" : void 0, 7766 onClick: (e) => { 7767 e.stopPropagation(); 7768 onChange(progNum); 7769 setIsOpen(false); 7770 }, 7771 className: `flex items-center w-full px-3.5 py-1.5 text-xs text-left transition-colors ${isSelected ? selectedBgClass : ""} ${itemHoverClass}`, 7772 children: [
7773 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `w-7 font-mono text-[10px] shrink-0 ${isSelected ? selectedTextClass : pitchNumberClass}`, children: String(progNum).padStart(3, "0") }), 7774 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `truncate flex-1 ${isSelected ? selectedTextClass : textUnselectedClass}`, children: instName }), 7775 isSelected && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 12, className: `${selectedTextClass} shrink-0 ml-1.5` }) 7776 ] 7777 }, 7778 progNum 7779 ); 7780 }) 7781 } 7782 ) }) 7783 ] }, category.name); 7784 }) 7785 }, 7786 "instrument-portal-menu" 7787 ) }), 7788 document.body 7789 ) 7790 ] }); 7791}; 7792const MixerPanel = ({ 7793 tracks, 7794 uniqueNames, 7795 mutedIndices, 7796 playAlongMutedTracks = /* @__PURE__ */ new Set(), 7797 isMelodyMode = false, 7798 toggleMute, 7799 trackVolumes, 7800 onVolumeChange, 7801 trackPans, 7802 onPanChange, 7803 onSolo, 7804 soloedIndex, 7805 hiddenTracks, 7806 toggleVisibility, 7807 audioMode, 7808 mixerMode, 7809 setMixerMode, 7810 colorMap, 7811 onViewEvents, 7812 isPlaying, 7813 currentTime, 7814 duration, 7815 trackStates = {}, 7816 onResetMixer, 7817 selectedTrackIndex, 7818 onSelectTrack, 7819 isGameMode, 7820 midiTrack, 7821 soundfont, 7822 onSoundfontChange, 7823 libraries = [], 7824 currentUser, 7825 preRenderedSf, 7826 isAttached = false, 7827 theme = "dark", 7828 pianoRollData, 7829 onInstrumentChange 7830}) => { 7831 const { t } = useTranslation(); 7832 const notesByTrack = React.useMemo(() => { 7833 const map = {}; 7834 if (!pianoRollData) return map; 7835 for (const note of pianoRollData) { 7836 const trackIdx = note.trackIdx; 7837 if (trackIdx === void 0) continue; 7838 if (!map[trackIdx]) { 7839 map[trackIdx] = []; 7840 } 7841 map[trackIdx].push({ 7842 t: note.t, 7843 e: note.t + note.d 7844 }); 7845 } 7846 return map; 7847 }, [pianoRollData]); 7848 const isTrackActiveAtTime = (trackNotes, time) => { 7849 if (!trackNotes || trackNotes.length === 0) return false; 7850 let low = 0; 7851 let high = trackNotes.length; 7852 while (low < high) { 7853 const mid = low + high >> 1; 7854 if (trackNotes[mid].t > time) { 7855 high = mid; 7856 } else { 7857 low = mid + 1; 7858 } 7859 } 7860 for (let i = low - 1; i >= 0; i--) { 7861 const note = trackNotes[i]; 7862 if (note.t < time - 30) { 7863 break; 7864 } 7865 if (time >= note.t && time <= note.e) { 7866 return true; 7867 } 7868 } 7869 return false; 7870 }; 7871 const isLive = audioMode === "synth" || audioMode === "wasmsynth"; 7872 const modeLibraries = reactExports.useMemo(() => { 7873 const isWasmFormat = (id, isSf2) => { 7874 return isSf2 || /\.h?sf[23]$/i.test(id) || /_hsf3bank$/i.test(id); 7875 }; 7876 if (audioMode === "wasmsynth") { 7877 return libraries.filter((l) => isWasmFormat(l.id, l.isSf2)); 7878 } else if (audioMode === "synth") { 7879 return libraries.filter((l) => !isWasmFormat(l.id, l.isSf2)); 7880 } 7881 return libraries; 7882 }, [libraries, audioMode]); 7883 const [showInformational, setShowInformational] = reactExports.useState(false); 7884 const isInformationalTrack = reactExports.useCallback((instr, uName) => { 7885 const rawName = String((instr == null ? void 0 : instr.name) || (instr == null ? void 0 : instr.originalName) || (instr == null ? void 0 : instr.displayName) || "").toLowerCase().trim(); 7886 const u = String(uName || "").toLowerCase().trim(); 7887 const localized = t("mixer.informationalTrack", "Informational Track").toLowerCase().trim(); 7888 return rawName === "informational track" || u === "informational track" || u === localized || (instr == null ? void 0 : instr.midiChannel) === -1 || (instr == null ? void 0 : instr.isInformational) === true; 7889 }, [t]); 7890 reactExports.useMemo(() => { 7891 return tracks.filter((instr, idx) => { 7892 const uName = uniqueNames[idx] || instr.displayName; 7893 return isInformationalTrack(instr, uName); 7894 }).length; 7895 }, [tracks, uniqueNames, isInformationalTrack]); 7896 const [downloadingIndices, setDownloadingIndices] = reactExports.useState(/* @__PURE__ */ new Set()); 7897 const [isSfOpen, setIsSfOpen] = reactExports.useState(false); 7898 const sfRef = reactExports.useRef(null); 7899 const [isExpanded, setIsExpanded] = reactExports.useState(false); 7900 const [masterVolume, setMasterVolume] = reactExports.useState(1); 7901 const [masterPan, setMasterPan] = reactExports.useState(0);
7902 const [reverbLevel, setReverbLevel] = reactExports.useState(0.25); 7903 const [roomSize, setRoomSize] = reactExports.useState("Medium Hall"); 7904 const [isRoomSizeOpen, setIsRoomSizeOpen] = reactExports.useState(false); 7905 const [isFullscreen, setIsFullscreen] = reactExports.useState(false); 7906 const [popoutWindow, setPopoutWindow] = reactExports.useState(null); 7907 const containerRef = reactExports.useRef(null); 7908 const openPopout = () => { 7909 if (popoutWindow) { 7910 popoutWindow.focus(); 7911 return; 7912 } 7913 const width = 1200; 7914 const height = 800; 7915 const left = window.screenX + (window.outerWidth - width) / 2; 7916 const top = window.screenY + (window.outerHeight - height) / 2; 7917 const win = window.open( 7918 "", 7919 "studio-mixer-console", 7920 `width=${width},height=${height},left=${left},top=${top},menubar=no,status=no,toolbar=no,location=no` 7921 ); 7922 if (!win) { 7923 alert("Popup blocker prevented opening the mixer window. Please allow popups for this site."); 7924 return; 7925 } 7926 win.document.title = t("watch.mixerConsole", "Studio Console Mixer"); 7927 win.document.body.className = "bg-zinc-950 text-white overflow-hidden m-0 p-0 h-screen w-screen"; 7928 win.document.body.style.background = "radial-gradient(ellipse at center, #424347 0%, #1e1f22 100%)"; 7929 copyStyles(document, win.document); 7930 const handleUnload = () => { 7931 setPopoutWindow(null); 7932 }; 7933 win.addEventListener("beforeunload", handleUnload); 7934 setPopoutWindow(win); 7935 setIsExpanded(true); 7936 }; 7937 const closePopout = () => { 7938 if (popoutWindow) { 7939 popoutWindow.close(); 7940 setPopoutWindow(null); 7941 } 7942 }; 7943 reactExports.useEffect(() => { 7944 return () => { 7945 if (popoutWindow) { 7946 popoutWindow.close(); 7947 } 7948 }; 7949 }, [popoutWindow]); 7950 reactExports.useEffect(() => { 7951 const handleParentUnload = () => { 7952 if (popoutWindow) { 7953 popoutWindow.close(); 7954 } 7955 }; 7956 window.addEventListener("beforeunload", handleParentUnload); 7957 return () => { 7958 window.removeEventListener("beforeunload", handleParentUnload); 7959 }; 7960 }, [popoutWindow]); 7961 reactExports.useEffect(() => { 7962 if (synth && typeof synth.setMasterReverbLevel === "function") { 7963 synth.setMasterReverbLevel(reverbLevel); 7964 } 7965 }, [reverbLevel]); 7966 reactExports.useEffect(() => { 7967 if (synth && typeof synth.setMasterReverbRoomSize === "function") { 7968 synth.setMasterReverbRoomSize(roomSize); 7969 } 7970 }, [roomSize]); 7971 reactExports.useEffect(() => { 7972 const handleFullscreenChange = () => { 7973 setIsFullscreen(!!document.fullscreenElement); 7974 }; 7975 document.addEventListener("fullscreenchange", handleFullscreenChange); 7976 return () => document.removeEventListener("fullscreenchange", handleFullscreenChange); 7977 }, []); 7978 const analysersRef = reactExports.useRef([]); 7979 const masterAnalyserRef = reactExports.useRef(null); 7980 const vuMeterRefs = reactExports.useRef([]); 7981 const masterVuRef = reactExports.useRef(null); 7982 const toggleMuteAll = () => { 7983 const anyUnmuted = tracks.some((_, idx) => !mutedIndices.has(idx)); 7984 tracks.forEach((_, idx) => { 7985 if (anyUnmuted && !mutedIndices.has(idx)) { 7986 toggleMute(idx); 7987 } else if (!anyUnmuted && mutedIndices.has(idx)) { 7988 toggleMute(idx); 7989 } 7990 }); 7991 }; 7992 reactExports.useEffect(() => { 7993 const handleClickOutside = (e) => { 7994 if (sfRef.current && !sfRef.current.contains(e.target)) { 7995 setIsSfOpen(false); 7996 } 7997 }; 7998 document.addEventListener("mousedown", handleClickOutside); 7999 return () => document.removeEventListener("mousedown", handleClickOutside); 8000 }, []); 8001 reactExports.useEffect(() => { 8002 if (isExpanded && mixerMode === "console") { 8003 const originalOverflow = document.body.style.overflow; 8004 document.body.style.overflow = "hidden"; 8005 return () => { 8006 document.body.style.overflow = originalOverflow; 8007 }; 8008 } 8009 }, [isExpanded, mixerMode]); 8010 reactExports.useEffect(() => { 8011 if (!isExpanded) return; 8012 let animationFrameId; 8013 const audioCtx = synth.getAudioContext(); 8014 const setupAnalysers = () => { 8015 const channels = synth.getChannels(); 8016 if (analysersRef.current.length !== channels.length) { 8017 analysersRef.current.forEach((ana) => ana == null ? void 0 : ana.disconnect()); 8018 analysersRef.current = new Array(channels.length).fill(null); 8019 }
8020 channels.forEach((ch, idx) => { 8021 if (ch && ch.gainNode && !analysersRef.current[idx]) { 8022 try { 8023 const analyser = audioCtx.createAnalyser(); 8024 analyser.fftSize = 64; 8025 ch.gainNode.connect(analyser); 8026 analysersRef.current[idx] = analyser; 8027 } catch (e) { 8028 console.warn("Failed to connect analyser to channel", idx, e); 8029 } 8030 } 8031 }); 8032 const masterGain = synth.getMasterGain(); 8033 if (masterGain && !masterAnalyserRef.current) { 8034 try { 8035 const analyser = audioCtx.createAnalyser(); 8036 analyser.fftSize = 64; 8037 masterGain.connect(analyser); 8038 masterAnalyserRef.current = analyser; 8039 } catch (e) { 8040 console.warn("Failed to connect analyser to master gain", e); 8041 } 8042 } 8043 }; 8044 const updateMeters = () => { 8045 setupAnalysers(); 8046 const channels = synth.getChannels(); 8047 const dataArray = new Uint8Array(32); 8048 channels.forEach((ch, idx) => { 8049 const analyser = analysersRef.current[idx]; 8050 const element = vuMeterRefs.current[idx]; 8051 if (analyser && element) { 8052 analyser.getByteFrequencyData(dataArray); 8053 let sum = 0; 8054 for (let i = 0; i < dataArray.length; i++) { 8055 sum += dataArray[i]; 8056 } 8057 const average = sum / dataArray.length; 8058 const percent = Math.min(100, average / 128 * 100); 8059 const segmentElements = element.querySelectorAll("[data-segment]"); 8060 if (segmentElements.length > 0) { 8061 const litSegmentsCount = Math.round(percent / 100 * segmentElements.length); 8062 segmentElements.forEach((el) => { 8063 const segmentNum = parseInt(el.getAttribute("data-segment") || "0"); 8064 el.className = getVuSegmentClass(segmentNum, segmentNum < litSegmentsCount); 8065 }); 8066 } else { 8067 element.style.height = `${percent}%`; 8068 } 8069 } else if (element) { 8070 const segmentElements = element.querySelectorAll("[data-segment]"); 8071 if (segmentElements.length > 0) { 8072 segmentElements.forEach((el) => { 8073 const segmentNum = parseInt(el.getAttribute("data-segment") || "0"); 8074 el.className = getVuSegmentClass(segmentNum, false); 8075 }); 8076 } else { 8077 element.style.height = "0%"; 8078 } 8079 } 8080 }); 8081 const masterAnalyser = masterAnalyserRef.current; 8082 const masterElement = masterVuRef.current; 8083 if (masterAnalyser && masterElement) { 8084 masterAnalyser.getByteFrequencyData(dataArray); 8085 let sum = 0; 8086 for (let i = 0; i < dataArray.length; i++) { 8087 sum += dataArray[i]; 8088 } 8089 const average = sum / dataArray.length; 8090 const percent = Math.min(100, average / 128 * 100); 8091 const segmentElements = masterElement.querySelectorAll("[data-segment]"); 8092 if (segmentElements.length > 0) { 8093 const litSegmentsCount = Math.round(percent / 100 * segmentElements.length); 8094 segmentElements.forEach((el) => { 8095 const segmentNum = parseInt(el.getAttribute("data-segment") || "0"); 8096 el.className = getVuSegmentClass(segmentNum, segmentNum < litSegmentsCount); 8097 }); 8098 } else { 8099 masterElement.style.height = `${percent}%`; 8100 } 8101 } else if (masterElement) { 8102 const segmentElements = masterElement.querySelectorAll("[data-segment]"); 8103 if (segmentElements.length > 0) { 8104 segmentElements.forEach((el) => { 8105 const segmentNum = parseInt(el.getAttribute("data-segment") || "0"); 8106 el.className = getVuSegmentClass(segmentNum, false); 8107 }); 8108 } else { 8109 masterElement.style.height = "0%"; 8110 } 8111 } 8112 animationFrameId = requestAnimationFrame(updateMeters); 8113 }; 8114 animationFrameId = requestAnimationFrame(updateMeters); 8115 return () => { 8116 var _a; 8117 cancelAnimationFrame(animationFrameId); 8118 analysersRef.current.forEach((ana) => ana == null ? void 0 : ana.disconnect()); 8119 analysersRef.current = []; 8120 (_a = masterAnalyserRef.current) == null ? void 0 : _a.disconnect(); 8121 masterAnalyserRef.current = null; 8122 }; 8123 }, [isExpanded, tracks.length]); 8124 const getPanelBg = () => { 8125 if (theme === "light") { 8126 return isAttached ? "px-3 pb-3 bg-zinc-50 rounded-r-xl border-b border-r border-zinc-200/80 shadow-2xl text-zinc-800" : "flex-col gap-2 text-zinc-800"; 8127 } 8128 if (theme === "fun") { 8129 return isAttached ? "px-3 pb-3 bg-[#fff1f2] rounded-r-xl border-b border-r border-pink-200 shadow-2xl text-pink-900" : "flex-col gap-2 text-pink-900"; 8130 } 8131 return isAttached ? "px-3 pb-3 bg-[#050505] rounded-r-xl border-b border-r border-white/10 shadow-2xl text-white" : "flex-col gap-2 text-white"; 8132 }; 8133 const getHeaderBorder = () => { 8134 if (theme === "light") return "flex items-center justify-between border-b border-zinc-200 h-11 mb-4"; 8135 if (theme === "fun") return "flex items-center justify-between border-b border-pink-200 h-11 mb-4"; 8136 return "flex items-center justify-between border-b border-white/10 h-11 mb-4"; 8137 }; 8138 const getHeaderTextClass = () => { 8139 if (theme === "light") return "font-bold text-zinc-800 text-sm flex items-center gap-2"; 8140 if (theme === "fun") return "font-bold text-pink-900 text-sm flex items-center gap-2"; 8141 return "font-bold text-white text-sm flex items-center gap-2"; 8142 }; 8143 const getSubheaderSeparator = () => { 8144 if (theme === "light") return "h-4 w-px bg-zinc-200 mx-2"; 8145 if (theme === "fun") return "h-4 w-px bg-pink-200 mx-2"; 8146 return "h-4 w-px bg-white/10 mx-2"; 8147 }; 8148 const getHeaderButtonClass = () => { 8149 if (theme === "light") return "text-[10px] bg-zinc-200 px-2 py-1 rounded-md text-zinc-600 hover:text-zinc-900 hover:bg-zinc-300 transition-all flex items-center gap-1.5 font-bold uppercase tracking-wider"; 8150 if (theme === "fun") return "text-[10px] bg-pink-100 px-2 py-1 rounded-md text-pink-800 hover:text-pink-950 hover:bg-pink-200 transition-all flex items-center gap-1.5 font-bold uppercase tracking-wider border border-pink-200"; 8151 return "text-[10px] bg-white/5 px-2 py-1 rounded-md text-zinc-400 hover:text-white hover:bg-white/10 transition-all flex items-center gap-1.5 font-bold uppercase tracking-wider"; 8152 }; 8153 const getResetButtonClass = () => { 8154 if (theme === "light") return "text-[10px] text-zinc-400 hover:text-red-500 hover:bg-zinc-100 transition-all flex items-center gap-1 px-2 py-1 rounded font-bold uppercase tracking-wider"; 8155 if (theme === "fun") return "text-[10px] text-pink-400 hover:text-red-500 hover:bg-pink-100 transition-all flex items-center gap-1 px-2 py-1 rounded font-bold uppercase tracking-wider"; 8156 return "text-[10px] text-zinc-500 hover:text-red-400 hover:bg-white/5 transition-all flex items-center gap-1 px-2 py-1 rounded font-bold uppercase tracking-wider"; 8157 }; 8158 const getTrackButtonClass = (isMutedBtn, isActive, isPlayAlongMute) => { 8159 if (isMutedBtn) { 8160 if (isPlayAlongMute) return "w-full h-7 flex items-center justify-center rounded transition-all bg-orange-500/20 text-orange-400 border border-orange-500/40"; 8161 if (isActive) { 8162 if (theme === "light") return "w-full h-7 flex items-center justify-center rounded transition-all bg-red-100 text-red-600 border border-red-200"; 8163 return "w-full h-7 flex items-center justify-center rounded transition-all bg-red-500/20 text-red-400 border border-red-500/40"; 8164 } 8165 if (theme === "light") return "w-full h-7 flex items-center justify-center rounded transition-all bg-zinc-100 text-zinc-400 hover:bg-zinc-200 hover:text-zinc-800"; 8166 return "w-full h-7 flex items-center justify-center rounded transition-all bg-white/5 text-zinc-400 hover:bg-white/10 hover:text-white"; 8167 } 8168 if (isActive) { 8169 if (theme === "fun") return "w-full h-7 flex items-center justify-center rounded transition-all bg-pink-500 text-white shadow-lg shadow-pink-500/20"; 8170 return "w-full h-7 flex items-center justify-center rounded transition-all bg-amber-500 text-white shadow-lg shadow-amber-500/20"; 8171 } 8172 if (theme === "light") return "w-full h-7 flex items-center justify-center rounded transition-all bg-zinc-100 text-zinc-400 hover:bg-zinc-200 hover:text-zinc-800"; 8173 if (theme === "fun") return "w-full h-7 flex items-center justify-center rounded transition-all bg-pink-50 text-pink-500 hover:bg-pink-100 hover:text-pink-800 border border-pink-200"; 8174 return "w-full h-7 flex items-center justify-center rounded transition-all bg-white/5 text-zinc-400 hover:bg-white/10 hover:text-white"; 8175 };
8176 const getVisibilityButtonClass = (isHidden) => { 8177 if (isHidden) { 8178 if (theme === "light") return "w-full h-7 flex items-center justify-center rounded transition-all bg-zinc-200 text-zinc-400"; 8179 if (theme === "fun") return "w-full h-7 flex items-center justify-center rounded transition-all bg-pink-100 text-pink-400"; 8180 return "w-full h-7 flex items-center justify-center rounded transition-all bg-zinc-800/50 text-zinc-600"; 8181 } 8182 if (theme === "light") return "w-full h-7 flex items-center justify-center rounded transition-all bg-indigo-50 text-indigo-600 hover:bg-indigo-100"; 8183 if (theme === "fun") return "w-full h-7 flex items-center justify-center rounded transition-all bg-pink-50 text-pink-650 hover:bg-pink-100 border border-pink-200"; 8184 return "w-full h-7 flex items-center justify-center rounded transition-all bg-white/5 text-indigo-400 hover:bg-white/10 hover:text-indigo-300"; 8185 }; 8186 const getDownloadButtonClass = (isDownloading) => { 8187 if (theme === "light") return `w-full h-7 flex items-center justify-center rounded bg-zinc-100 text-zinc-400 hover:bg-zinc-200 hover:text-emerald-600 transition-all ${isDownloading ? "opacity-50 cursor-not-allowed" : ""}`; 8188 if (theme === "fun") return `w-full h-7 flex items-center justify-center rounded bg-pink-50 text-pink-500 hover:bg-pink-100 hover:text-emerald-600 transition-all ${isDownloading ? "opacity-50 cursor-not-allowed" : ""} border border-pink-200`; 8189 return `w-full h-7 flex items-center justify-center rounded bg-white/5 text-zinc-500 hover:bg-white/10 hover:text-emerald-400 transition-all ${isDownloading ? "opacity-50 cursor-not-allowed" : ""}`; 8190 }; 8191 const getListModeRowClass = (isSelected, isPlayAlongMute, isMuted) => { 8192 const base = "backdrop-blur-sm border rounded-lg overflow-hidden transition-all relative group cursor-pointer"; 8193 let opacityClass = isPlayAlongMute ? "opacity-90" : isMuted ? "opacity-40 grayscale" : "opacity-100"; 8194 let borderClass = ""; 8195 if (theme === "light") { 8196 borderClass = isSelected ? "border-indigo-500 bg-indigo-50/50 shadow-[0_0_15px_rgba(99,102,241,0.08)]" : isPlayAlongMute ? "border-orange-400 bg-orange-50/30 shadow-[0_0_10px_rgba(249,115,22,0.05)]" : "border-zinc-200 bg-white hover:border-zinc-300 hover:bg-zinc-50/50 text-zinc-700"; 8197 } else if (theme === "fun") { 8198 borderClass = isSelected ? "border-pink-500 bg-pink-50/40 shadow-[0_0_15px_rgba(236,72,153,0.15)] text-pink-900" : isPlayAlongMute ? "border-orange-500/30 bg-
8198orange-500/10 shadow-[0_0_10px_rgba(249,115,22,0.1)] text-pink-900" : "border-pink-200 bg-white/60 hover:border-pink-300 hover:bg-white/80 text-pink-900"; 8199 } else { 8200 borderClass = isSelected ? "border-indigo-500 bg-indigo-500/5 shadow-[0_0_15px_rgba(99,102,241,0.1)]" : isPlayAlongMute ? "border-orange-500/30 bg-orange-500/5 shadow-[0_0_10px_rgba(249,115,22,0.1)]" : "border-white/5 bg-white/5 hover:border-white/10 hover:bg-white/10"; 8201 } 8202 return `${base} ${borderClass} ${opacityClass}`; 8203 }; 8204 const getListModeMuteButtonClass = (isPlayAlongMute, isMuted) => { 8205 if (isPlayAlongMute) { 8206 if (theme === "light") return "p-1.5 rounded transition-all text-orange-600 bg-orange-50 border border-orange-200"; 8207 return "p-1.5 rounded transition-all text-orange-400 bg-orange-500/10 border border-orange-500/20"; 8208 } 8209 if (isGameMode || !isMixingSupported) return "p-1.5 rounded transition-all opacity-30 cursor-not-allowed"; 8210 if (isMuted) { 8211 if (theme === "light") return "p-1.5 rounded transition-all text-red-500 bg-red-50 hover:bg-red-100"; 8212 if (theme === "fun") return "p-1.5 rounded transition-all text-red-500 bg-red-50 hover:bg-red-100 border border-red-200"; 8213 return "p-1.5 rounded transition-all text-red-400 bg-red-400/10 hover:bg-red-400/20"; 8214 } 8215 if (theme === "light") return "p-1.5 rounded transition-all text-zinc-400 hover:text-zinc-800 hover:bg-zinc-100"; 8216 if (theme === "fun") return "p-1.5 rounded transition-all text-pink-500 hover:text-pink-900 hover:bg-pink-100"; 8217 return "p-1.5 rounded transition-all text-zinc-500 hover:text-zinc-200 hover:bg-white/5"; 8218 }; 8219 const getListModeSoloButtonClass = (isPlayAlongMute, isSoloed) => { 8220 if (isGameMode || !isMixingSupported || isPlayAlongMute) return "p-1.5 rounded transition-all opacity-30 cursor-not-allowed"; 8221 if (isSoloed) { 8222 if (theme === "fun") return "p-1.5 rounded transition-all bg-pink-500 text-white shadow-lg"; 8223 return "p-1.5 rounded transition-all bg-amber-500 text-white shadow-lg"; 8224 } 8225 if (theme === "light") return "p-1.5 rounded transition-all text-zinc-400 hover:text-zinc-800 hover:bg-zinc-100"; 8226 if (theme === "fun") return "p-1.5 rounded transition-all text-pink-500 hover:text-pink-900 hover:bg-pink-100"; 8227 return "p-1.5 rounded transition-all text-zinc-500 hover:text-amber-400 hover:bg-white/5"; 8228 }; 8229 const getListModeVisibilityButtonClass = (isHidden) => { 8230 if (theme === "light") { 8231 return `p-1.5 rounded transition-all ${isHidden ? "text-zinc-300 hover:text-zinc-500" : "text-indigo-600 bg-indigo-50 hover:bg-indigo-100"}`; 8232 } 8233 if (theme === "fun") { 8234 return `p-1.5 rounded transition-all ${isHidden ? "text-pink-300 hover:text-pink-500" : "text-pink-600 bg-pink-50 hover:bg-pink-100 border border-pink-200"}`; 8235 } 8236 return `p-1.5 rounded transition-all ${isHidden ? "text-zinc-600 hover:text-zinc-400" : "text-indigo-400 bg-indigo-400/10 hover:bg-indigo-400/20"}`; 8237 }; 8238 const getListModeEventsButtonClass = () => { 8239 if (theme === "light") return "p-1.5 text-zinc-400 hover:text-indigo-600 hover:bg-zinc-100 rounded transition-all"; 8240 if (theme === "fun") return "p-1.5 text-pink-500 hover:text-pink-900 hover:bg-pink-100 rounded transition-all"; 8241 return "p-1.5 text-zinc-500 hover:text-indigo-400 hover:bg-white/5 rounded transition-all"; 8242 }; 8243 const getListModeDownloadButtonClass = (isDownloading) => { 8244 if (theme === "light") return `p-1.5 text-zinc-400 hover:text-emerald-600 hover:bg-zinc-100 rounded transition-all ${isDownloading ? "opacity-50" : ""}`; 8245 if (theme === "fun") return `p-1.5 text-pink-500 hover:text-emerald-600 hover:bg-pink-100 rounded transition-all ${isDownloading ? "opacity-50" : ""}`; 8246 return `p-1.5 text-zinc-500 hover:text-emerald-400 hover:bg-white/5 rounded transition-all ${isDownloading ? "opacity-50" : ""}`; 8247 }; 8248 const getInformationalTracksBtnClass = () => { 8249 if (theme === "light") return "w-full py-2.5 flex items-center justify-center gap-2 text-[10px] font-bold uppercase tracking-widest text-zinc-500 hover:text-zinc-800 transition-all border border-dashed border-zinc-200 rounded-lg hover:border-zinc-300 bg-zinc-100/50"; 8250 return "w-full py-2.5 flex items-center justify-center gap-2 text-[10px] font-bold uppercase tracking-widest text-zinc-500 hover:text-zinc-300 transition-all border border-dashed border-white/10 rounded-lg hover:border-white/20 bg-white/5"; 8251 }; 8252 const getSoundfontPickerContainer = () => { 8253 if (theme === "light") return "flex flex-col gap-2 mt-4 p-3 rounded-xl bg-zinc-100/50 border border-zinc-200"; 8254 return "flex flex-col gap-2 mt-4 p-3 rounded-xl bg-white/5 border border-white/5"; 8255 }; 8256 const getSoundfontButtonClass = () => { 8257 const isSfChangeable = audioMode === "synth" || audioMode === "wasmsynth" || audioMode === "opus"; 8258 if (isGameMode || !isSfChangeable) return "opacity-40 cursor-not-allowed bg-black/20 border-white/5 text-zinc-500 w-full flex items-center justify-between border rounded-lg px-3 py-2 text-xs font-bold focus:ring-1 focus:ring-indigo-500 outline-none transition-all shadow-lg"; 8259 if (theme === "light") return "bg-white border-zinc-200 text-zinc-700 hover:bg-zinc-50 hover:border-zinc-300 w-full flex items-center justify-between border rounded-lg px-3 py-2 text-xs font-bold focus:ring-1 focus:ring-indigo-500 outline-none transition-all shadow-md"; 8260 return "bg-black/40 border-white/5 text-zinc-200 hover:bg-black/60 hover:border-white/10 w-full flex items-center justify-between border rounded-lg px-3 py-2 text-xs font-bold focus:ring-1 focus:ring-indigo-500 outline-none transition-all shadow-lg"; 8261 }; 8262 const getSoundfontDropdownBg = () => { 8263 if (theme === "light") return "absolute top-full left-0 right-0 mt-2 bg-white border border-zinc-200 rounded-xl shadow-2xl z-[60] overflow-hidden py-1"; 8264 return "absolute top-full left-0 right-0 mt-2 bg-zinc-900 border border-white/10 rounded-xl shadow-2xl z-[60] overflow-hidden py-1 backdrop-blur-xl"; 8265 }; 8266 const getSoundfontDropdownItemClass = (isSelected, restricted) => { 8267 const base = "w-full flex flex-col items-start px-4 py-2.5 text-left transition-all relative"; 8268 let state = ""; 8269 if (theme === "light") { 8270 state = isSelected ? "bg-indigo-50" : "hover:bg-zinc-50"; 8271 } else { 8272 state = isSelected ? "bg-indigo-500/10" : "hover:bg-white/5"; 8273 } 8274 const opacity = restricted ? "opacity-40 cursor-not-allowed grayscale" : ""; 8275 return `${base} ${state} ${opacity}`; 8276 }; 8277 const getSoundfontDropdownItemTitleClass = (isSelected) => { 8278 if (isSelected) return "text-xs font-bold text-indigo-600"; 8279 if (theme === "light") return "text-xs font-bold text-zinc-700"; 8280 return "text-xs font-bold text-zinc-200"; 8281 }; 8282 const currentSf = libraries.find((l) => l.id === soundfont) || libraries[0] || { name: "GeneralUser GS (Pre-rendered)" }; 8283 const isMixingSupported = audioMode === "synth" || audioMode === "wasmsynth" || audioMode === "opus"; 8284 const renderBlockedMixer = () => { 8285 let title = ""; 8286 let subtitle = ""; 8287 if (audioMode === "opus") { 8288 title = t("mixer.blockedOpusTitle", "Mixer Unavailable in Opus Mode"); 8289 subtitle = t("mixer.blockedOpusSub", "Switch to SF or JS Mode to mute, pan, and mix individual instruments."); 8290 } else if (audioMode === "singleopus") { 8291 title = t("mixer.blockedSingleOpusTitle", "Mixer Unavailable in Single Opus Mode"); 8292 subtitle = t("mixer.blockedSingleOpusSub", "Muting and soloing individual tracks is not supported for single pre-mixed files."); 8293 } else if (audioMode === "webmidi") { 8294 title = t("mixer.blockedMidiTitle", "Mixer Unavailable in Web MIDI Mode"); 8295 subtitle = t("mixer.blockedMidiSub", "Mixer controls are bypassed. Adjust levels directly on your external MIDI hardware."); 8296 } else if (audioMode === "chiptune") { 8297 title = t("mixer.blockedChiptuneTitle", "Mixer Unavailable in Chiptune Mode"); 8298 subtitle = t("mixer.blockedChiptuneSub", "Synthesized waves are output directly. Switch to SF or JS Mode to mix individual tracks."); 8299 } else if (audioMode === "fmsynth") { 8300 title = t("mixer.blockedFmTitle", "Mixer Unavailable in FM Synth Mode"); 8301 subtitle = t("mixer.blockedFmSub", "FM synthesis waves are output directly. Switch to SF or JS Mode to mix individual tracks."); 8302 } else { 8303 title = t("mixer.blockedTitle", "Mixer Unavailable"); 8304 subtitle = t("mixer.blockedSub", "Mixer is not supported in the current playback mode."); 8305 }
8306 let containerClass = ""; 8307 let iconClass = ""; 8308 let titleClass = ""; 8309 let subtitleClass = ""; 8310 if (theme === "fun") { 8311 containerClass = "flex flex-row items-center justify-start py-2 px-3 border border-dashed border-pink-200 dark:border-pink-900/40 rounded-lg bg-pink-50/50 dark:bg-pink-950/10 text-left mx-2 mb-2 shrink-0 gap-3"; 8312 iconClass = "text-pink-400 dark:text-pink-500 shrink-0"; 8313 titleClass = "text-[11px] font-bold text-pink-700 dark:text-pink-300 uppercase tracking-widest"; 8314 subtitleClass = "text-[9.5px] text-pink-600/90 dark:text-pink-400 leading-tight"; 8315 } else if (theme === "retro") { 8316 containerClass = "flex flex-row items-center justify-start py-2 px-3 border border-dashed border-amber-500/30 rounded-none bg-black/40 text-left mx-2 mb-2 font-mono shrink-0 gap-3"; 8317 iconClass = "text-amber-500 shrink-0"; 8318 titleClass = "text-[11px] font-bold text-amber-500 uppercase tracking-widest"; 8319 subtitleClass = "text-[9.5px] text-amber-600/90 leading-tight"; 8320 } else if (theme === "light") { 8321 containerClass = "flex flex-row items-center justify-start py-2 px-3 border border-dashed border-zinc-300 rounded-lg bg-zinc-50 text-left mx-2 mb-2 shrink-0 gap-3"; 8322 iconClass = "text-zinc-500 shrink-0"; 8323 titleClass = "text-[11px] font-bold text-zinc-700 uppercase tracking-widest"; 8324 subtitleClass = "text-[9.5px] text-zinc-600 leading-tight"; 8325 } else { 8326 containerClass = "flex flex-row items-center justify-start py-2 px-3 border border-dashed border-white/20 rounded-lg bg-white/5 text-left mx-2 mb-2 shrink-0 gap-3"; 8327 iconClass = "text-zinc-400 shrink-0"; 8328 titleClass = "text-[11px] font-bold text-zinc-200 uppercase tracking-widest"; 8329 subtitleClass = "text-[9.5px] text-zinc-400 leading-tight"; 8330 } 8331 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: containerClass, children: [
8332 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 20, className: iconClass }), 8333 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col min-w-0", children: [ 8334 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: titleClass, children: title }), 8335 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: subtitleClass, children: subtitle }) 8336 ] }) 8337 ] }); 8338 }; 8339 const handleStemDownload = async (idx, e) => { 8340 e.stopPropagation(); 8341 const instr = tracks[idx]; 8342 if (!instr.stemPath && !isLive) return; 8343 setDownloadingIndices((prev) => new Set(prev).add(idx)); 8344 try { 8345 let blob = synth.getTrackBlob(idx); 8346 let url; 8347 if (blob) { 8348 url = window.URL.createObjectURL(blob); 8349 } else { 8350 const stemUrl = instr.stemPathHigh || instr.stemPath; 8351 if (!stemUrl) throw new Error("No stem URL available for download"); 8352 const res = await fetch(stemUrl); 8353 blob = await res.blob(); 8354 url = window.URL.createObjectURL(blob); 8355 } 8356 const uniqueName = uniqueNames[idx] || instr.name; 8357 const filename = midiTrack ? `${midiTrack.artist} - ${midiTrack.title} - ${uniqueName}.opus` : `${uniqueName}.opus`; 8358 const a = document.createElement("a"); 8359 a.href = url; 8360 a.download = filename; 8361 document.body.appendChild(a); 8362 a.click(); 8363 window.URL.revokeObjectURL(url); 8364 document.body.removeChild(a); 8365 } catch (err) { 8366 console.error("Stem download failed", err); 8367 } finally { 8368 setDownloadingIndices((prev) => { 8369 const next = new Set(prev); 8370 next.delete(idx); 8371 return next; 8372 }); 8373 } 8374 }; 8375 const renderConsole = () => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col w-full h-full", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-2 pb-4 overflow-x-auto custom-scrollbar w-full h-full", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-2 min-w-max h-full", children: tracks.map((instr, idx) => { 8376 let uniqueName = uniqueNames[idx] || instr.displayName; 8377 if (!uniqueName) { 8378 uniqueName = instr.name === "Informational Track" ? t("mixer.informationalTrack", "Informational Track") : getInstrumentName(instr.program) || `${t("mixer.track", "Track")} ${idx + 1}`; 8379 } 8380 if (uniqueName === t("mixer.informationalTrack", "Informational Track") || instr.name === "Informational Track" || instr.midiChannel === -1 || instr.notesCount === 0) return null; 8381 const isMuted = mutedIndices.has(idx); 8382 const trackColor = colorMap[uniqueName] || getTrackColor(idx); 8383 const currentVol = trackVolumes[idx] !== void 0 ? trackVolumes[idx] : 1; 8384 const loadState = trackStates[idx]; 8385 const isLoading = loadState && !loadState.ready && !loadState.error && instr.stemPath; 8386 const isError = loadState && loadState.error; 8387 const trackDisplay = `${t("mixer.trk", "TRK")} ${idx + 1}`; 8388 const isHidden = hiddenTracks.has(uniqueName); 8389 const isSelected = selectedTrackIndex === idx; 8390 const isPlayAlongMute = playAlongMutedTracks.has(idx); 8391 isPlaying && isTrackActiveAtTime(notesByTrack[idx], currentTime); 8392 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 8393 "div", 8394 { 8395 onClick: () => onSelectTrack(idx), 8396 className: `w-[110px] shrink-0 flex flex-col h-full bg-gradient-to-b from-[#3a3b3f] to-[#242528] border-r border-zinc-950 relative select-none shadow-[inset_-1px_0_0_rgba(255,255,255,0.04)] ${isSelected ? "after:absolute after:inset-0 after:border-2 after:border-teal-500/30 after:pointer-events-none" : ""} ${isMuted ? "opacity-70" : ""}`, 8397 children: [ 8398 isLoading && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-30 flex flex-col justify-end pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-teal-500/40 transition-all duration-300 w-full backdrop-blur-sm border-t border-teal-300 shadow-[0_0_10px_rgba(20,184,166,0.3)]", style: { height: `${loadState.percent}%` } }) }),
8399 isError && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-red-950/60 z-30 flex items-center justify-center backdrop-blur-sm pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 20, className: "text-red-500 animate-pulse" }) }), 8400 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-7 bg-black/40 border-b border-zinc-950 flex flex-col items-center justify-center relative", children: [ 8401 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 left-0 w-full h-[3px]", style: { backgroundColor: trackColor } }), 8402 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-black text-zinc-400 tracking-wider", children: trackDisplay }) 8403 ] }), 8404 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mx-2 mt-2", onClick: (e) => { 8405 e.stopPropagation(); 8406 onSelectTrack(idx); 8407 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx( 8408 InstrumentSelector, 8409 { 8410 currentProgram: instr.program !== void 0 ? instr.program : 0, 8411 channel: instr.midiChannel ?? instr.channel, 8412 onChange: (prog) => onInstrumentChange && onInstrumentChange(idx, prog), 8413 variant: "expand", 8414 theme 8415 } 8416 ) }),
8417 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col items-center justify-between gap-2 py-3", children: [ 8418 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 8419 RotaryKnob, 8420 { 8421 value: trackPans && trackPans[idx] !== void 0 ? trackPans[idx] : 0, 8422 onChange: (val) => onPanChange && onPanChange(idx, val), 8423 label: "PAN", 8424 accentColor: "#2dd4bf" 8425 } 8426 ) }), 8427 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5 w-full px-2 shrink-0", children: [ 8428 /* @__PURE__ */ jsxRuntimeExports.jsx( 8429 "button", 8430 { 8431 onClick: (e) => { 8432 e.stopPropagation(); 8433 toggleMute(idx); 8434 }, 8435 title: isPlayAlongMute ? "Auto-muted for Play-Along" : isMuted ? t("mixer.unmute", "Unmute") : t("mixer.mute", "Mute"), 8436 className: getTrackButtonClass(true, isMuted, isPlayAlongMute), 8437 disabled: isGameMode || !isMixingSupported || isPlayAlongMute, 8438 children: isPlayAlongMute ? /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 13, className: "animate-pulse text-orange-400" }) : isMuted ? /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 13 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Volume2, { size: 13 }) 8439 } 8440 ),
8441 /* @__PURE__ */ jsxRuntimeExports.jsx( 8442 "button", 8443 { 8444 onClick: (e) => { 8445 e.stopPropagation(); 8446 onSolo(idx); 8447 }, 8448 title: isPlayAlongMute ? "Disabled during Play-Along" : t("mixer.solo", "Solo Track"), 8449 className: getTrackButtonClass(false, soloedIndex === idx, isPlayAlongMute), 8450 disabled: isGameMode || !isMixingSupported || isPlayAlongMute, 8451 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Headphones, { size: 13 }) 8452 } 8453 ), 8454 /* @__PURE__ */ jsxRuntimeExports.jsx( 8455 "button", 8456 { 8457 onClick: (e) => { 8458 e.stopPropagation(); 8459 toggleVisibility(idx); 8460 }, 8461 title: isMelodyMode ? "Cannot hide tracks in Melody Mode" : isHidden ? t("mixer.show", "Show") : t("mixer.hide", "Hide"), 8462 className: `${getVisibilityButtonClass(isHidden)} ${isMelodyMode ? "opacity-30" : ""}`, 8463 disabled: isMelodyMode, 8464 children: isHidden ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { size: 13 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { size: 13 }) 8465 } 8466 ),
8467 instr.stemPath && /* @__PURE__ */ jsxRuntimeExports.jsx( 8468 "button", 8469 { 8470 onClick: (e) => handleStemDownload(idx, e), 8471 disabled: downloadingIndices.has(idx), 8472 title: t("mixer.downloadTrack", "Download Track Opus"), 8473 className: getDownloadButtonClass(downloadingIndices.has(idx)), 8474 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { size: 13, className: downloadingIndices.has(idx) ? "animate-bounce" : "" }) 8475 } 8476 ) 8477 ] }), 8478 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex-1 w-full flex items-center justify-center gap-1.5 px-2 relative ${isGameMode || !isMixingSupported || isPlayAlongMute ? "opacity-30 cursor-not-allowed pointer-events-none" : ""}`, children: [ 8479 /* @__PURE__ */ jsxRuntimeExports.jsx(SegmentedVuMeter, { idx, vuMeterRefs }), 8480 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative h-56 w-11 flex items-center justify-center", children: [
8481 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute w-[3px] h-[216px] bg-zinc-950 rounded border-l border-zinc-900 border-r border-zinc-800 shadow-[inset_0_1px_2px_rgba(0,0,0,0.9)]" }), 8482 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-y-2 left-0.5 flex flex-col justify-between text-[5px] font-mono text-zinc-500/80 pointer-events-none select-none", children: [ 8483 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "+6" }), 8484 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "0" }), 8485 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-6" }), 8486 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-12" }), 8487 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-24" }), 8488 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-48" }), 8489 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-â" }) 8490 ] }),
8491 /* @__PURE__ */ jsxRuntimeExports.jsx( 8492 "input", 8493 { 8494 type: "range", 8495 min: "0", 8496 max: "1.2", 8497 step: "0.05", 8498 value: currentVol, 8499 onClick: (e) => e.stopPropagation(), 8500 onDoubleClick: (e) => { 8501 if (!isGameMode && isMixingSupported && !isPlayAlongMute) { 8502 e.stopPropagation(); 8503 onVolumeChange(idx, 1); 8504 } 8505 }, 8506 onChange: (e) => onVolumeChange(idx, parseFloat(e.target.value)), 8507 className: "absolute w-56 h-11 opacity-0 cursor-pointer z-20", 8508 style: { 8509 transform: "rotate(-90deg)", 8510 transformOrigin: "center" 8511 }, 8512 title: isPlayAlongMute ? "Auto-muted for Play-Along" : `${t("mixer.volume", "Volume")}: ${Math.round(currentVol * 100)}%`, 8513 disabled: isGameMode || !isMixingSupported || isPlayAlongMute 8514 } 8515 ),
8516 /* @__PURE__ */ jsxRuntimeExports.jsxs( 8517 "div", 8518 { 8519 className: "absolute w-9 h-7 rounded shadow-xl pointer-events-none transition-all duration-75 z-10 flex flex-col items-center justify-center cursor-grab active:cursor-grabbing border-b-2 border-r border-t border-l", 8520 style: { 8521 bottom: `calc(${Math.min(100, currentVol / 1.2 * 100)}% - 14px)`, 8522 background: isPlayAlongMute ? "linear-gradient(to bottom, #ea580c 0%, #c2410c 100%)" : "linear-gradient(to bottom, #e4e4e7 0%, #a1a1aa 100%)", 8523 borderColor: isPlayAlongMute ? "#7c2d12" : "#52525b", 8524 boxShadow: "0 4px 6px rgba(0,0,0,0.6), inset 0 1px 1px rgba(255,255,255,0.6)" 8525 }, 8526 children: [ 8527 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-[2px] bg-white shadow-[0_0_2px_rgba(0,0,0,0.3)] mb-0.5" }), 8528 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-px bg-zinc-950/20" }) 8529 ] 8530 } 8531 ) 8532 ] }) 8533 ] }) 8534 ] }) 8535 ] 8536 }, 8537 idx 8538 ); 8539 }) }) }) }); 8540 const renderList = () => { 8541 const informationalTracksCount2 = tracks.filter((instr, idx) => { 8542 let uniqueName = uniqueNames[idx] || instr.displayName; 8543 return isInformationalTrack(instr, uniqueName); 8544 }).length; 8545 const visibleTracksCount = tracks.length - (!showInformational ? informationalTracksCount2 : 0); 8546 const spaceYClass = visibleTracksCount >= 11 ? "space-y-0.5" : visibleTracksCount >= 9 ? "space-y-1" : visibleTracksCount >= 7 ? "space-y-1.5" : "space-y-2"; 8547 const rowPaddingClass = visibleTracksCount >= 11 ? "py-0.5" : visibleTracksCount >= 10 ? "py-1" : visibleTracksCount >= 8 ? "py-1.5" : visibleTracksCount >= 7 ? "py-2" : "py-2.5"; 8548 const indicatorHeightClass = visibleTracksCount >= 10 ? "h-4" : visibleTracksCount >= 8 ? "h-5" : visibleTracksCount >= 7 ? "h-6" : "h-8"; 8549 const instrumentSize = visibleTracksCount >= 8 ? "sm" : "md"; 8550 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `${spaceYClass} max-h-[600px] xl:max-h-none overflow-y-auto custom-scrollbar pr-1`, children: [ 8551 tracks.map((instr, idx) => { 8552 let uniqueName = uniqueNames[idx] || instr.displayName; 8553 if (!uniqueName) { 8554 uniqueName = instr.name === "Informational Track" ? t("mixer.informationalTrack", "Informational Track") : getInstrumentName(instr.program) || `${t("mixer.track", "Track")} ${idx + 1}`; 8555 } 8556 const isInformational = isInformationalTrack(instr, uniqueName); 8557 if (isInformational && !showInformational) return null; 8558 const isMuted = mutedIndices.has(idx); 8559 const trackColor = colorMap[uniqueName] || getTrackColor(idx); 8560 const isHidden = hiddenTracks.has(uniqueName); 8561 const loadState = trackStates[idx]; 8562 const isLoading = loadState && !loadState.ready && !loadState.error && instr.stemPath; 8563 const isSelected = selectedTrackIndex === idx; 8564 const isPlayAlongMute = playAlongMutedTracks.has(idx); 8565 const isNoteActive = isPlaying && isTrackActiveAtTime(notesByTrack[idx], currentTime); 8566 const unlitColor = theme === "light" ? "rgb(228, 228, 231)" : "rgb(39, 39, 42)"; 8567 const ledStyle = { 8568 backgroundColor: isNoteActive ? trackColor : unlitColor, 8569 boxShadow: isNoteActive ? `0 0 10px ${trackColor}, 0 0 4px ${trackColor}` : "none", 8570 transition: isNoteActive ? "none" : "background-color 450ms cubic-bezier(0.1, 0.8, 0.25, 1), box-shadow 450ms cubic-bezier(0.1, 0.8, 0.25, 1)" 8571 }; 8572 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 8573 "div", 8574 { 8575 onClick: () => !isGameMode && isMixingSupported && onSelectTrack(idx), 8576 className: getListModeRowClass(isSelected, isPlayAlongMute, isMuted), 8577 children: [ 8578 isLoading && !isInformational && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-black/60 z-30 flex items-center justify-center backdrop-blur-[2px]", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-3/4", children: [
8579 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-[10px] text-white font-bold mb-1 uppercase tracking-wider", children: [ 8580 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Loading" }), 8581 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 8582 Math.round(loadState.percent), 8583 "%" 8584 ] }) 8585 ] }), 8586 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-1 bg-white/10 rounded-full overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-full bg-indigo-500 transition-all duration-300", style: { width: `${loadState.percent}%` } }) }) 8587 ] }) }), 8588 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex items-center justify-between px-2.5 ${rowPaddingClass} min-w-0`, children: [ 8589 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0 flex-1 pr-1", children: [
8590 /* @__PURE__ */ jsxRuntimeExports.jsx( 8591 "div", 8592 { 8593 className: `w-1.5 ${indicatorHeightClass} rounded-full shrink-0 shadow-[0_0_8px_rgba(255,255,255,0.1)]`, 8594 style: { backgroundColor: isInformational ? "#4b5563" : isPlayAlongMute ? "#f97316" : trackColor }, 8595 title: isInformational ? t("mixer.informationalTrack", "Informational Track") : `Track Color: ${trackColor}` 8596 } 8597 ), 8598 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col min-w-0 flex-1", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 min-w-0 flex-1", children: [ 8599 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[10px] font-mono font-bold text-zinc-400 dark:text-zinc-500 whitespace-nowrap flex items-center gap-1 shrink-0", title: `${t("mixer.track", "Track")} ${idx + 1}`, children: [ 8600 "#", 8601 idx + 1,
8602 /* @__PURE__ */ jsxRuntimeExports.jsx( 8603 "div", 8604 { 8605 className: "w-1.5 h-1.5 rounded-full shrink-0", 8606 style: ledStyle, 8607 title: "Signal activity indicator" 8608 } 8609 ) 8610 ] }), 8611 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center min-w-0 flex-1", children: isInformational ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[12px] font-medium text-zinc-500 truncate min-w-0 font-sans ml-1`, children: t("mixer.informationalTrack", "Informational Track") }) : /* @__PURE__ */ jsxRuntimeExports.jsx( 8612 InstrumentSelector, 8613 { 8614 currentProgram: instr.program !== void 0 ? instr.program : 0, 8615 channel: instr.midiChannel, 8616 onChange: (prog) => onInstrumentChange && onInstrumentChange(idx, prog), 8617 variant: "list", 8618 theme, 8619 size: instrumentSize 8620 } 8621 ) }) 8622 ] }) }) 8623 ] }),
8624 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-0.5 shrink-0 ml-1", children: [ 8625 !isInformational && /* @__PURE__ */ jsxRuntimeExports.jsx( 8626 "button", 8627 { 8628 onClick: (e) => { 8629 e.stopPropagation(); 8630 toggleVisibility(idx); 8631 }, 8632 title: isMelodyMode ? "Cannot hide tracks in Melody Mode" : isHidden ? t("mixer.show", "Show") : t("mixer.hide", "Hide"), 8633 className: `${getListModeVisibilityButtonClass(isHidden)} ${isMelodyMode ? "opacity-30 cursor-not-allowed" : ""}`, 8634 disabled: isMelodyMode, 8635 children: isHidden ? /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { size: 14 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Eye, { size: 14 }) 8636 } 8637 ), 8638 /* @__PURE__ */ jsxRuntimeExports.jsx( 8639 "button", 8640 { 8641 onClick: (e) => { 8642 e.stopPropagation(); 8643 onViewEvents(instr); 8644 }, 8645 title: t("mixer.events", "View Events"), 8646 className: getListModeEventsButtonClass(), 8647 children: /* @__PURE__ */ jsxRuntimeExports.jsx(List, { size: 14 }) 8648 } 8649 ),
8650 instr.stemPath && /* @__PURE__ */ jsxRuntimeExports.jsx( 8651 "button", 8652 { 8653 onClick: (e) => handleStemDownload(idx, e), 8654 disabled: downloadingIndices.has(idx), 8655 title: t("mixer.downloadTrack", "Download Track Opus"), 8656 className: getListModeDownloadButtonClass(downloadingIndices.has(idx)), 8657 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { size: 14, className: downloadingIndices.has(idx) ? "animate-bounce" : "" }) 8658 } 8659 ), 8660 !isInformational && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 8661 /* @__PURE__ */ jsxRuntimeExports.jsx( 8662 "button", 8663 { 8664 onClick: (e) => { 8665 e.stopPropagation(); 8666 onSolo(idx); 8667 }, 8668 title: isPlayAlongMute ? "Disabled during Play-Along" : t("mixer.solo", "Solo Track"), 8669 className: getListModeSoloButtonClass(isPlayAlongMute, soloedIndex === idx), 8670 disabled: isGameMode || !isMixingSupported || isPlayAlongMute, 8671 children: /* @__PURE__ */ jsxRuntimeExports.jsx(Headphones, { size: 14 }) 8672 } 8673 ),
8674 /* @__PURE__ */ jsxRuntimeExports.jsx( 8675 "button", 8676 { 8677 onClick: (e) => { 8678 e.stopPropagation(); 8679 toggleMute(idx); 8680 }, 8681 title: isPlayAlongMute ? "Auto-muted for Play-Along" : isMuted ? t("mixer.unmute", "Unmute") : t("mixer.mute", "Mute"), 8682 className: getListModeMuteButtonClass(isPlayAlongMute, isMuted), 8683 disabled: isGameMode || !isMixingSupported || isPlayAlongMute, 8684 children: isPlayAlongMute ? /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 14, className: "animate-pulse text-orange-400" }) : isMuted ? /* @__PURE__ */ jsxRuntimeExports.jsx(VolumeX, { size: 14 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Volume2, { size: 14 }) 8685 } 8686 ) 8687 ] }) 8688 ] }) 8689 ] }) 8690 ] 8691 }, 8692 idx 8693 ); 8694 }), 8695 informationalTracksCount2 > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx( 8696 "button", 8697 { 8698 onClick: () => setShowInformational(!showInformational), 8699 className: getInformationalTracksBtnClass(), 8700 children: showInformational ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
8701 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 14 }), 8702 t("mixer.hideInformationalTracks", "Hide Informational Tracks") 8703 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 8704 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown$1, { size: 14 }), 8705 t("mixer.showInformationalTracks", { count: informationalTracksCount2, defaultValue: `Show ${informationalTracksCount2} Informational Track${informationalTracksCount2 !== 1 ? "s" : ""}` }) 8706 ] }) 8707 } 8708 ) 8709 ] }); 8710 }; 8711 return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 8712 isExpanded && mixerMode === "console" && typeof document !== "undefined" && reactDomExports.createPortal( 8713 /* @__PURE__ */ jsxRuntimeExports.jsxs( 8714 "div", 8715 { 8716 ref: containerRef, 8717 className: popoutWindow ? "fixed inset-0 z-[9999] flex flex-col text-white overflow-hidden" : `fixed top-0 left-0 right-0 ${isFullscreen ? "rounded-none" : "rounded-t-xl"} z-[9999] flex flex-col text-white animate-in fade-in zoom-in-95 overflow-hidden border border-zinc-800 shadow-2xl`, 8718 style: { 8719 background: "radial-gradient(ellipse at center, #424347 0%, #1e1f22 100%)", 8720 bottom: isFullscreen || popoutWindow ? "0px" : "5rem" 8721 }, 8722 children: [
8723 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `h-10 bg-gradient-to-r from-zinc-800 to-zinc-900 border-b border-zinc-950 flex items-center justify-between px-4 select-none shrink-0 ${isFullscreen || popoutWindow ? "" : "rounded-t-xl"}`, children: [ 8724 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 w-24", children: [ 8725 /* @__PURE__ */ jsxRuntimeExports.jsx( 8726 "button", 8727 { 8728 onClick: (e) => { 8729 e.stopPropagation(); 8730 if (popoutWindow) { 8731 closePopout(); 8732 } else { 8733 if (document.fullscreenElement && document.exitFullscreen) { 8734 document.exitFullscreen(); 8735 } 8736 setIsExpanded(false); 8737 } 8738 }, 8739 className: "w-3 h-3 rounded-full bg-rose-500 border border-rose-600/50 hover:bg-rose-600 transition-colors flex items-center justify-center group relative shadow-[inset_0_1px_1px_rgba(255,255,255,0.2)] cursor-pointer", 8740 title: popoutWindow ? t("mixer.closeWindow", "Close Window") : t("mixer.minimize", "Minimize"), 8741 children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden group-hover:block text-[8px] text-rose-950 font-bold leading-none -mt-[1px]", children: "Ã" }) 8742 } 8743 ),
8744 /* @__PURE__ */ jsxRuntimeExports.jsx( 8745 "button", 8746 { 8747 disabled: !isFullscreen && !popoutWindow, 8748 onClick: (e) => { 8749 e.stopPropagation(); 8750 if (popoutWindow) { 8751 closePopout(); 8752 } else if (isFullscreen && document.exitFullscreen) { 8753 document.exitFullscreen(); 8754 } 8755 }, 8756 className: `w-3 h-3 rounded-full bg-amber-500 border border-amber-600/50 shadow-[inset_0_1px_1px_rgba(255,255,255,0.2)] transition-all ${isFullscreen || popoutWindow ? "cursor-pointer hover:bg-amber-600" : "opacity-40 cursor-not-allowed"}`, 8757 title: popoutWindow ? t("mixer.reattach", "Reattach") : isFullscreen ? t("mixer.exitFullscreen", "Exit Fullscreen") : "" 8758 } 8759 ), 8760 /* @__PURE__ */ jsxRuntimeExports.jsx( 8761 "button", 8762 { 8763 onClick: (e) => { 8764 var _a, _b, _c; 8765 e.stopPropagation(); 8766 if (popoutWindow) { 8767 const doc = popoutWindow.document; 8768 const el = doc.body; 8769 if (!doc.fullscreenElement) { 8770 (_a = el.requestFullscreen) == null ? void 0 : _a.call(el); 8771 } else { 8772 (_b = doc.exitFullscreen) == null ? void 0 : _b.call(doc); 8773 } 8774 } else { 8775 if (!isFullscreen) { 8776 if ((_c = containerRef.current) == null ? void 0 : _c.requestFullscreen) { 8777 containerRef.current.requestFullscreen(); 8778 } 8779 } else { 8780 if (document.exitFullscreen) { 8781 document.exitFullscreen(); 8782 } 8783 } 8784 } 8785 }, 8786 className: "w-3 h-3 rounded-full bg-emerald-500 border border-emerald-600/50 hover:bg-emerald-600 transition-colors flex items-center justify-center cursor-pointer shadow-[inset_0_1px_1px_rgba(255,255,255,0.2)]", 8787 title: t("mixer.fullscreen", "Fullscreen") 8788 } 8789 ) 8790 ] }),
8791 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 8792 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 14, className: "text-teal-400 animate-pulse" }), 8793 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] font-extrabold uppercase tracking-[0.25em] text-zinc-300 drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)]", children: t("watch.mixerConsole", "Studio Console Mixer") }) 8794 ] }), 8795 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex gap-2 justify-end items-center shrink-0", children: popoutWindow ? /* @__PURE__ */ jsxRuntimeExports.jsxs( 8796 "button", 8797 { 8798 onClick: (e) => { 8799 e.stopPropagation(); 8800 closePopout(); 8801 }, 8802 className: "text-[9px] bg-zinc-950/40 border border-zinc-800 hover:border-zinc-700 px-2.5 py-1 rounded text-zinc-400 hover:text-white transition-all flex items-center gap-1 font-bold uppercase tracking-wider shadow-md", 8803 children: [
8804 /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize2, { size: 10 }), 8805 t("mixer.reattachShort", "Reattach") 8806 ] 8807 } 8808 ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 8809 /* @__PURE__ */ jsxRuntimeExports.jsxs( 8810 "button", 8811 { 8812 onClick: (e) => { 8813 e.stopPropagation(); 8814 openPopout(); 8815 }, 8816 className: "text-[9px] bg-zinc-950/40 border border-zinc-800 hover:border-zinc-700 px-2.5 py-1 rounded text-zinc-400 hover:text-white transition-all flex items-center gap-1 font-bold uppercase tracking-wider shadow-md", 8817 title: t("mixer.popout", "Show in separate window"), 8818 children: [ 8819 /* @__PURE__ */ jsxRuntimeExports.jsx(ExternalLink, { size: 10 }), 8820 t("mixer.popoutShort", "Pop Out") 8821 ] 8822 } 8823 ),
8824 /* @__PURE__ */ jsxRuntimeExports.jsxs( 8825 "button", 8826 { 8827 onClick: (e) => { 8828 e.stopPropagation(); 8829 if (document.fullscreenElement && document.exitFullscreen) { 8830 document.exitFullscreen(); 8831 } 8832 setIsExpanded(false); 8833 }, 8834 className: "text-[9px] bg-zinc-950/40 border border-zinc-800 hover:border-zinc-700 px-2.5 py-1 rounded text-zinc-400 hover:text-white transition-all flex items-center gap-1 font-bold uppercase tracking-wider shadow-md", 8835 children: [ 8836 /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize2, { size: 10 }), 8837 t("mixer.minimizeShort", "Minimize") 8838 ] 8839 } 8840 ) 8841 ] }) }) 8842 ] }), 8843 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex gap-0 min-h-0 overflow-hidden w-full items-stretch justify-start p-6", children: [
8844 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex gap-0 overflow-x-auto overflow-y-hidden custom-scrollbar pb-2 items-stretch bg-zinc-950/25 border border-zinc-900 rounded-lg shadow-[inset_0_2px_8px_rgba(0,0,0,0.8)]", children: tracks.map((instr, idx) => { 8845 let uniqueName = uniqueNames[idx] || instr.displayName; 8846 if (!uniqueName) { 8847 uniqueName = instr.name === "Informational Track" ? t("mixer.informationalTrack", "Informational Track") : getInstrumentName(instr.program) || `${t("mixer.track", "Track")} ${idx + 1}`; 8848 } 8849 if (uniqueName === t("mixer.informationalTrack", "Informational Track") || instr.name === "Informational Track" || instr.midiChannel === -1 || instr.notesCount === 0) return null; 8850 const isMuted = mutedIndices.has(idx); 8851 const trackColor = colorMap[uniqueName] || getTrackColor(idx); 8852 const currentVol = trackVolumes[idx] !== void 0 ? trackVolumes[idx] : 1; 8853 const loadState = trackStates[idx]; 8854 const isLoading = loadState && !loadState.ready && !loadState.error && instr.stemPath; 8855 const isError = loadState && loadState.error; 8856 const trackDisplay = `${t("mixer.trk", "TRK")} ${idx + 1}`; 8857 const isHidden = hiddenTracks.has(uniqueName); 8858 const isSelected = selectedTrackIndex === idx; 8859 const isPlayAlongMute = playAlongMutedTracks.has(idx); 8860 const faderBg = isPlayAlongMute ? "linear-gradient(to bottom, #f97316 0%, #ea580c 100%)" : "linear-gradient(to bottom, #2dd4bf 0%, #0d9488 100%)"; 8861 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 8862 "div", 8863 { 8864 onClick: () => onSelectTrack(idx), 8865 className: `w-[110px] shrink-0 flex flex-col h-full bg-gradient-to-b from-[#3a3b3f] to-[#242528] border-r border-zinc-950 relative select-none shadow-[inset_-1px_0_0_rgba(255,255,255,0.04)] ${isSelected ? "after:absolute after:inset-0 after:border-2 after:border-teal-500/30 after:pointer-events-none" : ""} ${isMuted ? "opacity-70" : ""}`, 8866 children: [ 8867 isLoading && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 z-30 flex flex-col justify-end pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-teal-500/40 transition-all duration-300 w-full backdrop-blur-sm border-t border-teal-300 shadow-[0_0_10px_rgba(20,184,166,0.3)]", style: { height: `${loadState.percent}%` } }) }), 8868 isError && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute inset-0 bg-red-950/60 z-30 flex items-center justify-center backdrop-blur-sm pointer-events-none", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 20, className: "text-red-500 animate-pulse" }) }),
8869 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-7 bg-black/40 border-b border-zinc-950 flex flex-col items-center justify-center relative", children: [ 8870 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 left-0 w-full h-[3px]", style: { backgroundColor: trackColor } }), 8871 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-black text-zinc-400 tracking-wider", children: trackDisplay }) 8872 ] }), 8873 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mx-2 mt-2", onClick: (e) => { 8874 e.stopPropagation(); 8875 onSelectTrack(idx); 8876 }, children: /* @__PURE__ */ jsxRuntimeExports.jsx( 8877 InstrumentSelector, 8878 { 8879 currentProgram: instr.program !== void 0 ? instr.program : 0, 8880 channel: instr.midiChannel ?? instr.channel, 8881 onChange: (prog) => onInstrumentChange && onInstrumentChange(idx, prog), 8882 variant: "expand", 8883 theme 8884 } 8885 ) }),
8886 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col items-center justify-between gap-2 py-3", children: [ 8887 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 8888 RotaryKnob, 8889 { 8890 value: trackPans && trackPans[idx] !== void 0 ? trackPans[idx] : 0, 8891 onChange: (val) => onPanChange && onPanChange(idx, val), 8892 label: "PAN", 8893 accentColor: "#2dd4bf" 8894 } 8895 ) }), 8896 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1 w-full px-2 shrink-0", children: [ 8897 /* @__PURE__ */ jsxRuntimeExports.jsx( 8898 LedButton, 8899 { 8900 label: "MUTE", 8901 active: isMuted, 8902 onClick: () => toggleMute(idx), 8903 ledColor: "#ef4444" 8904 } 8905 ),
8906 /* @__PURE__ */ jsxRuntimeExports.jsx( 8907 LedButton, 8908 { 8909 label: "SOLO", 8910 active: soloedIndex === idx, 8911 onClick: () => onSolo(idx), 8912 ledColor: "#fbbf24" 8913 } 8914 ), 8915 /* @__PURE__ */ jsxRuntimeExports.jsx( 8916 LedButton, 8917 { 8918 label: "BYP", 8919 active: isHidden, 8920 onClick: () => toggleVisibility(idx), 8921 ledColor: "#3b82f6" 8922 } 8923 ) 8924 ] }), 8925 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex-1 w-full flex items-center justify-center gap-1.5 px-2 relative ${isGameMode || !isMixingSupported || isPlayAlongMute ? "opacity-30 cursor-not-allowed pointer-events-none" : ""}`, children: [
8926 /* @__PURE__ */ jsxRuntimeExports.jsx(SegmentedVuMeter, { idx, vuMeterRefs }), 8927 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative h-56 w-11 flex items-center justify-center", children: [ 8928 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute w-[3px] h-[216px] bg-zinc-950 rounded border-l border-zinc-900 border-r border-zinc-800 shadow-[inset_0_1px_2px_rgba(0,0,0,0.9)]" }), 8929 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-y-2 left-0.5 flex flex-col justify-between text-[5px] font-mono text-zinc-500/80 pointer-events-none select-none", children: [ 8930 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "+6" }), 8931 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "0" }), 8932 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-6" }), 8933 /* @__P
8933URE__ */ jsxRuntimeExports.jsx("span", { children: "-12" }), 8934 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-24" }), 8935 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-oo" }) 8936 ] }), 8937 /* @__PURE__ */ jsxRuntimeExports.jsx( 8938 "input", 8939 { 8940 type: "range", 8941 min: "0", 8942 max: "1.2", 8943 step: "0.02", 8944 value: currentVol, 8945 onClick: (e) => e.stopPropagation(), 8946 onDoubleClick: (e) => { 8947 if (!isGameMode && isMixingSupported && !isPlayAlongMute) { 8948 e.stopPropagation(); 8949 onVolumeChange(idx, 1); 8950 } 8951 }, 8952 onChange: (e) => onVolumeChange(idx, parseFloat(e.target.value)), 8953 className: "absolute w-56 h-10 opacity-0 cursor-pointer z-20", 8954 style: { 8955 transform: "rotate(-90deg)", 8956 transformOrigin: "center" 8957 }, 8958 title: isPlayAlongMute ? "Auto-muted for Play-Along" : `${t("mixer.volume", "Volume")}: ${Math.round(currentVol * 100)}%`, 8959 disabled: isGameMode || !isMixingSupported || isPlayAlongMute 8960 } 8961 ),
8962 /* @__PURE__ */ jsxRuntimeExports.jsxs( 8963 "div", 8964 { 8965 className: "absolute w-[24px] h-[28px] border border-zinc-950 rounded flex flex-col justify-center items-center z-10 pointer-events-none transition-all duration-75", 8966 style: { 8967 background: faderBg, 8968 bottom: `calc(${Math.min(100, currentVol / 1.2 * 100)}% - 14px)`, 8969 left: "50%", 8970 transform: "translateX(-50%)", 8971 boxShadow: "inset 0 1.5px 0 rgba(255,255,255,0.45), inset 0 -1.5px 3px rgba(0,0,0,0.6), 0 3px 6px rgba(0,0,0,0.65)" 8972 }, 8973 children: [ 8974 /* @__PURE__ */ jsxRuntimeExports.jsx( 8975 "div", 8976 { 8977 className: "absolute inset-0 rounded pointer-events-none", 8978 style: { 8979 background: "linear-gradient(to bottom, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0) 30%, rgba(0,0,0,0.25) 45%, transparent 50%, rgba(0,0,0,0.25) 55%, rgba(255,255,255,0) 70%, rgba(255,255,255,0.15) 100%)" 8980 } 8981 } 8982 ),
8983 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-[3px] bg-black/95 border-y border-white/10 z-10 pointer-events-none" }) 8984 ] 8985 } 8986 ) 8987 ] }) 8988 ] }) 8989 ] }), 8990 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-10 border-t border-zinc-950 bg-black/30 flex flex-col items-center justify-center px-1.5 py-0.5 select-none text-center", children: [ 8991 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[8.5px] font-black text-zinc-400 uppercase tracking-wide truncate w-full", title: uniqueName, children: uniqueName }), 8992 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[6.5px] font-bold text-zinc-650 tracking-wider", children: instr.name === "Informational Track" ? "INFO" : `MIDI CH ${instr.midiChannel !== void 0 ? instr.midiChannel + 1 : idx + 1}` }) 8993 ] }) 8994 ] 8995 }, 8996 idx 8997 ); 8998 }) }),
8999 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 pl-6 flex items-stretch", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-32 shrink-0 flex flex-col h-full bg-gradient-to-b from-[#2e2f33] to-[#1a1b1d] border-l border-r border-zinc-950 shadow-[0_0_20px_rgba(0,0,0,0.5)] rounded-lg relative select-none", children: [ 9000 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-7 bg-black/60 border-b border-zinc-950 flex flex-col items-center justify-center relative rounded-t-lg", children: [ 9001 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 left-0 w-full h-[3px] bg-rose-500" }), 9002 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-black text-rose-500 uppercase tracking-widest mt-0.5", children: "Master" }) 9003 ] }), 9004 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col items-center justify-between gap-2 py-3 z-10", children: [
9005 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-center shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 9006 RotaryKnob, 9007 { 9008 value: reverbLevel, 9009 onChange: (val) => setReverbLevel(val), 9010 min: 0, 9011 max: 1, 9012 label: "REVERB", 9013 accentColor: "#3b82f6", 9014 minLabel: "DRY", 9015 maxLabel: "WET" 9016 } 9017 ) }), 9018 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative w-full px-2 shrink-0", children: [ 9019 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[7px] font-extrabold text-zinc-400 tracking-wider block text-center uppercase mb-0.5", children: "ROOM SIZE" }), 9020 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9021 "div", 9022 { 9023 className: "bg-[#3b0b0b] border border-[#5f1313] rounded-[3px] px-1.5 py-0.5 flex items-center justify-between text-rose-400 font-mono text-[8.5px] shadow-[inset_0_1.5px_3px_rgba(0,0,0,0.8)] cursor-pointer hover:bg-[#4a0e0e] transition-colors", 9024 onClick: (e) => { 9025 e.stopPropagation(); 9026 setIsRoomSizeOpen(!isRoomSizeOpen); 9027 }, 9028 children: [
9029 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate uppercase font-bold tracking-wide text-center w-full", children: roomSize }), 9030 /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown$1, { size: 8, className: "text-rose-500 shrink-0 ml-1" }) 9031 ] 9032 } 9033 ), 9034 isRoomSizeOpen && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute z-50 left-2 right-2 mt-1 bg-[#230606] border border-[#5f1313] rounded shadow-2xl py-1 text-[8px] font-mono text-rose-400", children: ["Small Room", "Medium Hall", "Large Hall", "Cathedral", "Plate"].map((size) => /* @__PURE__ */ jsxRuntimeExports.jsx( 9035 "button", 9036 { 9037 onClick: (e) => { 9038 e.stopPropagation(); 9039 setRoomSize(size); 9040 setIsRoomSizeOpen(false); 9041 }, 9042 className: `w-full text-left px-2 py-1 hover:bg-[#3b0b0b] hover:text-white uppercase ${roomSize === size ? "bg-[#4a0e0e] font-bold text-white" : ""}`, 9043 children: size 9044 }, 9045 size 9046 )) }) 9047 ] }),
9048 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1 w-full px-2 shrink-0", children: [ 9049 /* @__PURE__ */ jsxRuntimeExports.jsx( 9050 LedButton, 9051 { 9052 label: "MUTE ALL", 9053 active: tracks.every((_, i) => mutedIndices.has(i)), 9054 onClick: () => toggleMuteAll(), 9055 ledColor: "#ef4444" 9056 } 9057 ), 9058 /* @__PURE__ */ jsxRuntimeExports.jsx( 9059 LedButton, 9060 { 9061 label: "RESET ALL", 9062 active: false, 9063 onClick: () => { 9064 tracks.forEach((_, i) => { 9065 onVolumeChange(i, 1); 9066 if (onPanChange) onPanChange(i, 0); 9067 }); 9068 setMasterVolume(1); 9069 setMasterPan(0); 9070 setReverbLevel(0.25); 9071 setRoomSize("Medium Hall"); 9072 }, 9073 ledColor: "#fbbf24" 9074 } 9075 ) 9076 ] }),
9077 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 w-full flex items-center justify-center gap-1.5 px-2 relative", children: [ 9078 /* @__PURE__ */ jsxRuntimeExports.jsx(MasterSegmentedVuMeter, { masterVuRef }), 9079 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative h-56 w-11 flex items-center justify-center", children: [ 9080 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute w-[3px] h-[216px] bg-zinc-950 rounded border-l border-zinc-900 border-r border-zinc-800 shadow-[inset_0_1px_2px_rgba(0,0,0,0.9)]" }), 9081 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute inset-y-2 left-0.5 flex flex-col justify-between text-[5px] font-mono text-zinc-500/80 pointer-events-none select-none", children: [ 9082 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "+6" }), 9083 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "0" }),
9084 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-6" }), 9085 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-12" }), 9086 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-24" }), 9087 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "-oo" }) 9088 ] }), 9089 /* @__PURE__ */ jsxRuntimeExports.jsx( 9090 "input", 9091 { 9092 type: "range", 9093 min: "0", 9094 max: "1.2", 9095 step: "0.02", 9096 value: masterVolume, 9097 onClick: (e) => e.stopPropagation(), 9098 onDoubleClick: (e) => { 9099 e.stopPropagation(); 9100 setMasterVolume(1); 9101 tracks.forEach((_, i) => onVolumeChange(i, 1)); 9102 }, 9103 onChange: (e) => { 9104 const val = parseFloat(e.target.value); 9105 setMasterVolume(val); 9106 tracks.forEach((_, i) => onVolumeChange(i, val)); 9107 }, 9108 className: "absolute w-56 h-10 opacity-0 cursor-pointer z-20", 9109 style: { 9110 transform: "rotate(-90deg)", 9111 transformOrigin: "center" 9112 }, 9113 title: `Master Volume: ${Math.round(masterVolume * 100)}%` 9114 } 9115 ),
9116 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9117 "div", 9118 { 9119 className: "absolute w-[24px] h-[28px] border border-zinc-950 rounded flex flex-col justify-center items-center z-10 pointer-events-none transition-all duration-75", 9120 style: { 9121 background: "linear-gradient(to bottom, #ef4444 0%, #b91c1c 100%)", 9122 bottom: `calc(${Math.min(100, masterVolume / 1.2 * 100)}% - 14px)`, 9123 left: "50%", 9124 transform: "translateX(-50%)", 9125 boxShadow: "inset 0 1.5px 0 rgba(255,255,255,0.45), inset 0 -1.5px 3px rgba(0,0,0,0.6), 0 3px 6px rgba(0,0,0,0.65)" 9126 }, 9127 children: [ 9128 /* @__PURE__ */ jsxRuntimeExports.jsx( 9129 "div", 9130 { 9131 className: "absolute inset-0 rounded pointer-events-none", 9132 style: { 9133 background: "linear-gradient(to bottom, rgba(255,255,255,0.25) 0%, rgba(255,255,255,0) 30%, rgba(0,0,0,0.25) 45%, transparent 50%, rgba(0,0,0,0.25) 55%, rgba(255,255,255,0) 70%, rgba(255,255,255,0.15) 100%)" 9134 } 9135 } 9136 ),
9137 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-[3px] bg-black/95 border-y border-white/10 z-10 pointer-events-none" }) 9138 ] 9139 } 9140 ) 9141 ] }) 9142 ] }) 9143 ] }), 9144 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-10 border-t border-zinc-950 bg-black/60 flex flex-col items-center justify-center px-1.5 py-0.5 select-none text-center rounded-b-lg", children: [ 9145 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[9px] font-black text-rose-500 uppercase tracking-widest drop-shadow-[0_1px_1px_rgba(0,0,0,0.8)]", children: "MIDI MASTER" }), 9146 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[6px] font-bold text-zinc-600 tracking-wider", children: "MAIN OUT" }) 9147 ] }) 9148 ] }) }) 9149 ] }) 9150 ] 9151 } 9152 ), 9153 popoutWindow ? popoutWindow.document.body : document.body 9154 ), 9155 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex flex-col h-full animate-in fade-in slide-in-from-right-2 ${getPanelBg()}`, children: [
9156 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getHeaderBorder(), children: [ 9157 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 9158 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: getHeaderTextClass(), children: [ 9159 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 16, className: theme === "fun" ? "text-pink-400 animate-pulse" : "text-indigo-400" }), 9160 t("watch.mixer", "Mixer") 9161 ] }), 9162 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: getSubheaderSeparator() }), 9163 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9164 "button", 9165 { 9166 onClick: () => setMixerMode(mixerMode === "list" ? "console" : "list"), 9167 className: getHeaderButtonClass(), 9168 title: t("mixer.toggleView", "Toggle View"), 9169 children: [ 9170 mixerMode === "list" ? /* @__PURE__ */ jsxRuntimeExports.jsx(PanelsTopLeft, { size: 12 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(List, { size: 12 }), 9171 mixerMode === "list" ? t("mixer.console", "Console") : t("mixer.list", "List") 9172 ] 9173 } 9174 ), 9175 mixerMode === "console" && /* @__PURE__ */ jsxRuntimeExports.jsxs( 9176 "button", 9177 { 9178 onClick: () => setIsExpanded(!isExpanded), 9179 className: getHeaderButtonClass(), 9180 title: isExpanded ? t("mixer.minimize", "Minimize") : t("mixer.expand", "Expand"), 9181 children: [ 9182 isExpanded ? /* @__PURE__ */ jsxRuntimeExports.jsx(Minimize2, { size: 12 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Maximize2, { size: 12 }), 9183 isExpanded ? t("mixer.minimizeShort", "Minimize") : t("mixer.expandShort", "Expand") 9184 ] 9185 } 9186 ) 9187 ] }),
9188 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 9189 tracks.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs( 9190 "button", 9191 { 9192 onClick: onResetMixer, 9193 className: getResetButtonClass(), 9194 title: t("mixer.reset", "Reset All"), 9195 children: [ 9196 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 12 }), 9197 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "hidden sm:inline", children: t("mixer.resetAction", "Reset") }) 9198 ] 9199 } 9200 ), 9201 audioMode === "singleopus" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 text-[9px] font-bold uppercase text-amber-500 bg-amber-500/10 px-2 py-1 rounded border border-amber-500/20 cursor-help", title: t("mixer.opusMode", "Mixer is limited in Opus mode"), children: [ 9202 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 10 }), 9203 " No Stems" 9204 ] }) 9205 ] }) 9206 ] }),
9207 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-hidden min-h-0 flex flex-col justify-stretch", children: popoutWindow ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col items-center justify-center text-center p-6 bg-zinc-950/20 rounded-xl border border-dashed border-zinc-800 shadow-[inset_0_2px_4px_rgba(0,0,0,0.5)] my-2", children: [ 9208 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 36, className: "text-teal-400 mb-3 animate-pulse" }), 9209 /* @__PURE__ */ jsxRuntimeExports.jsx("h4", { className: "font-bold text-xs uppercase tracking-wider text-zinc-300 mb-1", children: "Pop-out Mode Active" }), 9210 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-zinc-500 mb-4 max-w-[220px] leading-relaxed", children: "The Studio Mixer Console is currently running in a separate window." }), 9211 /* @__PURE__ */ jsxRuntimeExports.jsx( 9212 "button", 9213 { 9214 onClick: closePopout, 9215 className: "text-[10px] bg-teal-500 hover:bg-teal-650 px-4 py-2 rounded text-white font-extrabold uppercase tracking-widest transition-all shadow-md shadow-teal-500/20 hover:scale-105 active:scale-95", 9216 children: "Reattach Mixer" 9217 } 9218 ) 9219 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col min-h-0", children: [ 9220 !isMixingSupported && renderBlockedMixer(), 9221 mixerMode === "console" ? renderConsole() : renderList() 9222 ] }) }),
9223 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: getSoundfontPickerContainer(), ref: sfRef, children: [ 9224 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-between", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 9225 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 12, className: theme === "light" ? "text-zinc-400" : "text-zinc-500" }), 9226 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: theme === "light" ? "text-[10px] font-bold uppercase tracking-widest text-zinc-500" : "text-[10px] font-bold uppercase tracking-widest text-zinc-400", children: "MIDI Soundfont" }) 9227 ] }) }), 9228 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [ 9229 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9230 "button", 9231 { 9232 onClick: () => (audioMode === "synth" || audioMode === "wasmsynth" || audioMode === "opus") && !isGameMode && setIsSfOpen(!isSfOpen), 9233 disabled: !(audioMode === "synth" || audioMode === "wasmsynth" || audioMode === "opus") || isGameMode, 9234 className: getSoundfontButtonClass(), 9235 children: [
9236 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "truncate pr-2", children: audioMode === "opus" ? t("watch.sfPrerendered", `${preRenderedSf ? preRenderedSf.replace(".sf2", "").replace(/_/g, " ") : "GeneralUser GS"} (Pre-rendered)`) : audioMode === "singleopus" ? "Pre-mixed Audio (N/A)" : audioMode === "wasmsynth" ? `${currentSf.name.replace(/\s*\(Pre-rendered\)/i, "")} (SF)` : audioMode === "fmsynth" ? "16-bit FM Synthesis (N/A)" : audioMode === "chiptune" ? "8-bit Chiptune (N/A)" : audioMode === "webmidi" ? "External Hardware MIDI (N/A)" : currentSf.name.replace(/\s*\(Pre-rendered\)/i, "") }), 9237 (audioMode === "synth" || audioMode === "wasmsynth" || audioMode === "opus") && /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown$1, { size: 14, className: "text-zinc-500 transition-transform duration-200", style: { transform: isSfOpen ? "rotate(180deg)" : "none" } }) 9238 ] 9239 } 9240 ), 9241 /* @__PURE__ */ jsxRuntimeExports.jsx(AnimatePresence, { children: isSfOpen && /* @__PURE__ */ jsxRuntimeExports.jsx( 9242 motion.div, 9243 { 9244 initial: { opacity: 0, y: -4, scale: 0.95 }, 9245 animate: { opacity: 1, y: 0, scale: 1 }, 9246 exit: { opacity: 0, y: -4, scale: 0.95 }, 9247 className: getSoundfontDropdownBg(), 9248 children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-h-64 overflow-y-auto custom-scrollbar", children: modeLibraries.length > 0 ? modeLibraries.map((lib) => { 9249 const restricted = lib.access === "pro" && !isPro(currentUser); 9250 const isSelected = lib.id === soundfont; 9251 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 9252 "button", 9253 { 9254 disabled: restricted, 9255 onClick: () => { 9256 onSoundfontChange == null ? void 0 : onSoundfontChange(lib.id); 9257 setIsSfOpen(false); 9258 }, 9259 className: getSoundfontDropdownItemClass(isSelected, restricted), 9260 children: [
9261 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between w-full gap-2", children: [ 9262 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: getSoundfontDropdownItemTitleClass(isSelected), children: audioMode !== "opus" ? lib.name.replace(/\s*\(Pre-rendered\)/i, "") : lib.name }), 9263 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 9264 lib.access === "pro" && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[8px] font-bold bg-amber-500 text-white px-1.5 py-0.5 rounded-sm uppercase tracking-tighter leading-none shadow-sm", children: "GOLD+" }), 9265 isSelected && /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14, className: "text-indigo-400" }) 9266 ] }) 9267 ] }), 9268 lib.desc && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-medium text-zinc-500 mt-0.5 leading-tight", children: audioMode !== "opus" && lib.isPreRendered ? "Standard quality live synthesis" : lib.desc }) 9269 ] 9270 }, 9271 lib.id 9272 ); 9273 }) : /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "w-full px-4 py-3 text-left text-xs text-zinc-500 font-medium", children: "GeneralUser GS (Pre-rendered)" }) }) 9274 } 9275 ) }) 9276 ] }) 9277 ] }) 9278 ] }) 9279 ] }); 9280}; 9281const GovernanceModal = ({ user, isOpen, onClose, onUpdate }) => { 9282 const [action, setAction] = reactExports.useState(""); 9283 const [duration, setDuration] = reactExports.useState(24); 9284 const [reason, setReason] = reactExports.useState(""); 9285 const [trustAdjustment, setTrustAdjustment] = reactExports.useState(0); 9286 const [isSubmitting, setIsSubmitting] = reactExports.useState(false); 9287 if (!isOpen) return null; 9288 const handleSubmit = async (e) => { 9289 e.preventDefault(); 9290 if (!action && trustAdjustment === 0 && !reason) return; 9291 setIsSubmitting(true); 9292 try { 9293 await api.governUser(user.id, { 9294 action, 9295 duration: action === "suspend" ? duration : void 0, 9296 reason, 9297 trustAdjustment: trustAdjustment !== 0 ? trustAdjustment : void 0 9298 }); 9299 onUpdate == null ? void 0 : onUpdate(); 9300 onClose(); 9301 } catch (e2) { 9302 console.error(e2); 9303 alert("Governance action failed"); 9304 } finally { 9305 setIsSubmitting(false); 9306 } 9307 }; 9308 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 w-full max-w-md rounded-2xl shadow-2xl overflow-hidden border border-zinc-200 dark:border-zinc-800 animate-in zoom-in-95 duration-200", children: [ 9309 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-indigo-600 p-4 flex items-center justify-between text-white", children: [
9310 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 9311 /* @__PURE__ */ jsxRuntimeExports.jsx(Shield, { size: 20 }), 9312 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "font-bold", children: "USER GOVERNANCE" }) 9313 ] }), 9314 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "p-1 hover:bg-white/20 rounded-lg transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) }) 9315 ] }), 9316 /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { onSubmit: handleSubmit, className: "p-6 space-y-6", children: [ 9317 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 p-3 bg-zinc-100 dark:bg-zinc-800/50 rounded-xl", children: [ 9318 /* @__PURE__ */ jsxRuntimeExports.jsx("img", { src: user.avatar || "/default-avatar.png", alt: user.username, className: "w-12 h-12 rounded-full border-2 border-white dark:border-zinc-700" }), 9319 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
9320 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "font-bold text-zinc-900 dark:text-white", children: user.username }), 9321 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-zinc-500", children: [ 9322 "ID: ", 9323 user.id, 9324 " ⢠Trust: ", 9325 user.trustScore, 9326 "%" 9327 ] }) 9328 ] }) 9329 ] }), 9330 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-2 gap-3", children: [ 9331 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9332 "button", 9333 { 9334 type: "button", 9335 onClick: () => setAction(user.isJailed ? "unjail" : "jail"), 9336 className: `flex flex-col items-center gap-2 p-3 rounded-xl border-2 transition-all ${action === "jail" || action === "unjail" ? "border-indigo-500 bg-indigo-500/5" : "border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700"}`, 9337 children: [ 9338 user.isJailed ? /* @__PURE__ */ jsxRuntimeExports.jsx(UserCheck, { className: "text-green-500" }) : /* @__PURE__ */ jsxRuntimeExports.jsx(UserMinus, { className: "text-red-500" }),
9339 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold", children: user.isJailed ? "Remove Jail" : "Send to Jail" }) 9340 ] 9341 } 9342 ), 9343 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9344 "button", 9345 { 9346 type: "button", 9347 onClick: () => setAction(user.suspendedUntil ? "unsuspend" : "suspend"), 9348 className: `flex flex-col items-center gap-2 p-3 rounded-xl border-2 transition-all ${action === "suspend" || action === "unsuspend" ? "border-indigo-500 bg-indigo-500/5" : "border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700"}`, 9349 children: [ 9350 /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { className: "text-amber-500" }), 9351 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold", children: user.suspendedUntil ? "Unsuspend" : "Suspend (Mute)" }) 9352 ] 9353 } 9354 ), 9355 /* @__PURE__ */ jsxRuntimeExports.jsxs( 9356 "button", 9357 { 9358 type: "button", 9359 onClick: () => setAction(user.isShadowBanned ? "unshadow-ban" : "shadow-ban"), 9360 className: `flex flex-col items-center gap-2 p-3 rounded-xl border-2 transition-all ${action === "shadow-ban" || action === "unshadow-ban" ? "border-indigo-500 bg-indigo-500/5" : "border-zinc-200 dark:border-zinc-800 hover:border-zinc-300 dark:hover:border-zinc-700"}`, 9361 children: [
9362 /* @__PURE__ */ jsxRuntimeExports.jsx(Ghost, { className: "text-zinc-500" }), 9363 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold", children: user.isShadowBanned ? "Un-Ghost" : "Shadow Ban" }) 9364 ] 9365 } 9366 ), 9367 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-2 p-3 rounded-xl border-2 border-zinc-200 dark:border-zinc-800", children: [ 9368 /* @__PURE__ */ jsxRuntimeExports.jsx(ChartColumn, { className: "text-blue-500" }), 9369 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 9370 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: () => setTrustAdjustment((prev) => prev - 10), className: "text-zinc-400 hover:text-red-500", children: "-" }), 9371 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: `text-xs font-bold ${trustAdjustment > 0 ? "text-green-500" : trustAdjustment < 0 ? "text-red-500" : ""}`, children: [ 9372 trustAdjustment > 0 ? "+" : "", 9373 trustAdjustment 9374 ] }),
9375 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: () => setTrustAdjustment((prev) => prev + 10), className: "text-zinc-400 hover:text-green-500", children: "+" }) 9376 ] }), 9377 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-zinc-500 uppercase font-bold tracking-wider", children: "Trust Score" }) 9378 ] }) 9379 ] }), 9380 action === "suspend" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2 animate-in slide-in-from-top-2", children: [ 9381 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 uppercase tracking-wider", children: "Suspension Duration (Hours)" }), 9382 /* @__PURE__ */ jsxRuntimeExports.jsx( 9383 "input", 9384 { 9385 type: "number", 9386 value: duration, 9387 onChange: (e) => setDuration(parseInt(e.target.value)), 9388 className: "w-full bg-zinc-100 dark:bg-zinc-800 border-none rounded-lg p-3 text-sm focus:ring-2 ring-indigo-500 outline-none" 9389 } 9390 ) 9391 ] }),
9392 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-2", children: [ 9393 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-500 uppercase tracking-wider", children: "Reason / Internal Note" }), 9394 /* @__PURE__ */ jsxRuntimeExports.jsx( 9395 "textarea", 9396 { 9397 value: reason, 9398 onChange: (e) => setReason(e.target.value), 9399 placeholder: "Why are you taking this action? (Internal only)", 9400 className: "w-full bg-zinc-100 dark:bg-zinc-800 border-none rounded-lg p-3 text-sm h-24 resize-none focus:ring-2 ring-indigo-500 outline-none" 9401 } 9402 ) 9403 ] }), 9404 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-3 pt-2", children: [ 9405 /* @__PURE__ */ jsxRuntimeExports.jsx( 9406 "button", 9407 { 9408 type: "button", 9409 onClick: onClose, 9410 className: "flex-1 px-4 py-3 rounded-xl font-bold text-sm text-zinc-600 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors", 9411 children: "Cancel" 9412 } 9413 ),
9414 /* @__PURE__ */ jsxRuntimeExports.jsx( 9415 "button", 9416 { 9417 type: "submit", 9418 disabled: isSubmitting || !action && trustAdjustment === 0 && !reason, 9419 className: "flex-[2] bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-indigo-500/20 disabled:opacity-50", 9420 children: isSubmitting ? "Applying..." : "Apply Governance" 9421 } 9422 ) 9423 ] }) 9424 ] }) 9425 ] }) }); 9426}; 9427const EducationalDisclaimerModal = ({ isOpen, onClose, onAccept }) => { 9428 const { t } = useTranslation(); 9429 const [checked, setChecked] = reactExports.useState(false); 9430 if (!isOpen) return null; 9431 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "bg-white dark:bg-zinc-900 w-full max-w-lg max-h-[90vh] flex flex-col rounded-3xl shadow-2xl border border-zinc-200 dark:border-zinc-800 overflow-hidden animate-in zoom-in-95 duration-200", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-5 md:p-8 overflow-y-auto custom-scrollbar flex-1", children: [ 9432 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-start mb-6", children: [
9433 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-12 h-12 rounded-2xl bg-indigo-50 dark:bg-indigo-900/30 flex items-center justify-center text-indigo-600 dark:text-indigo-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldCheck, { size: 28 }) }), 9434 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-colors text-zinc-400", children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) }) 9435 ] }), 9436 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-2xl font-black text-zinc-900 dark:text-white mb-4 tracking-tight", children: "Educational Use Acknowledgment" }), 9437 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4 text-zinc-600 dark:text-zinc-400 text-sm leading-relaxed mb-8", children: [ 9438 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [ 9439 "HamieNET is a ", 9440 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-zinc-900 dark:text-zinc-200", children: "non-profit digital archive" }), 9441 " dedicated to the preservation and musicological study of MIDI metadata and transcriptions." 9442 ] }),
9443 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-zinc-50 dark:bg-zinc-800/50 rounded-2xl border border-zinc-100 dark:border-zinc-800 flex gap-3", children: [ 9444 /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { size: 18, className: "text-indigo-500 shrink-0 mt-0.5" }), 9445 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [ 9446 "By downloading files from this archive, you acknowledge that the content is provided solely for", 9447 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-indigo-600 dark:text-indigo-400 mx-1", children: "educational, research, and non-commercial preservation purposes" }), 9448 "." 9449 ] }) 9450 ] }), 9451 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "You agree not to use this data for any commercial redistribution or performance that would violate local copyright laws." }) 9452 ] }), 9453 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-start gap-3 p-4 bg-indigo-50/50 dark:bg-indigo-900/10 rounded-2xl border border-indigo-100/50 dark:border-indigo-900/20 cursor-pointer group transition-all hover:bg-indigo-50 dark:hover:bg-indigo-900/20 mb-8", children: [
9454 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex items-center mt-1", children: [ 9455 /* @__PURE__ */ jsxRuntimeExports.jsx( 9456 "input", 9457 { 9458 type: "checkbox", 9459 checked, 9460 onChange: (e) => setChecked(e.target.checked), 9461 className: "peer h-5 w-5 cursor-pointer appearance-none rounded-md border border-zinc-300 dark:border-zinc-700 checked:bg-indigo-600 checked:border-transparent transition-all" 9462 } 9463 ), 9464 /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldCheck, { className: "absolute h-3.5 w-3.5 text-white opacity-0 peer-checked:opacity-100 left-0.5 top-0.5 pointer-events-none transition-opacity" }) 9465 ] }), 9466 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-sm font-bold text-zinc-700 dark:text-zinc-300 select-none", children: "I agree that this download is for educational and research purposes only." }) 9467 ] }), 9468 /* @__PURE__ */ jsxRuntimeExports.jsx( 9469 "button", 9470 { 9471 disabled: !checked, 9472 onClick: onAccept, 9473 className: "w-full py-4 bg-indigo-600 hover:bg-indigo-700 disabled:bg-zinc-200 dark:disabled:bg-zinc-800 disabled:text-zinc-400 dark:disabled:text-zinc-600 text-white font-black rounded-2xl transition-all shadow-lg shadow-indigo-600/20 disabled:shadow-none translate-y-0 active:translate-y-0.5", 9474 children: "Accept & Continue Download" 9475 } 9476 ) 9477 ] }) }) }); 9478}; 9479const ShareModal = ({ 9480 isOpen, 9481 onClose, 9482 currentTime, 9483 duration, 9484 trackTitle, 9485 trackId 9486}) => { 9487 const { t } = useTranslation(); 9488 const [copied, setCopied] = reactExports.useState(false); 9489 const [gameMidiCopied, setGameMidiCopied] = reactExports.useState(false); 9490 const [startAtChecked, setStartAtChecked] = reactExports.useState(false); 9491 reactExports.useEffect(() => { 9492 if (copied) { 9493 const timer = setTimeout(() => setCopied(false), 2e3); 9494 return () => clearTimeout(timer); 9495 } 9496 }, [copied]); 9497 reactExports.useEffect(() => { 9498 if (gameMidiCopied) { 9499 const timer = setTimeout(() => setGameMidiCopied(false), 2e3); 9500 return () => clearTimeout(timer); 9501 } 9502 }, [gameMidiCopied]); 9503 if (!isOpen) return null; 9504 const formatTime = (seconds) => { 9505 const mins = Math.floor(seconds / 60); 9506 const secs = Math.floor(seconds % 60); 9507 return `${mins}:${secs.toString().padStart(2, "0")}`; 9508 }; 9509 const getShareUrl = () => { 9510 if (typeof window === "undefined") return ""; 9511 const url = new URL(window.location.href); 9512 url.searchParams.delete("t"); 9513 url.searchParams.delete("time"); 9514 if (startAtChecked) { 9515 url.searchParams.set("t", Math.floor(currentTime).toString()); 9516 } 9517 return url.toString(); 9518 }; 9519 const shareUrl = getShareUrl(); 9520 const shareTitle = `${t("watch.shareTextPrefix", "Check out this song on HamieNET:")} ${trackTitle}`; 9521 const getGameMidiUrl = () => { 9522 if (typeof window === "undefined") return ""; 9523 const url = new URL(window.location.origin); 9524 url.pathname = `/media/midi/${trackId}.mid`; 9525 return url.toString(); 9526 }; 9527 const gameMidiUrl = getGameMidiUrl(); 9528 const handleCopy = async () => { 9529 try { 9530 await navigator.clipboard.writeText(shareUrl); 9531 setCopied(true); 9532 } catch (err) { 9533 console.error("Failed to copy text: ", err); 9534 } 9535 }; 9536 const handleCopyGameMidi = async () => { 9537 try { 9538 await navigator.clipboard.writeText(gameMidiUrl); 9539 setGameMidiCopied(true); 9540 } catch (err) { 9541 console.error("Failed to copy text: ", err); 9542 } 9543 }; 9544 const socialNetworks = [ 9545 { 9546 name: t("watch.shareFacebook", "Facebook"), 9547 url: `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(shareUrl)}`, 9548 icon: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "w-6 h-6 fill-current", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M9 8H7v3h2v9h3v-9h3.6l.4-3H12V6c0-.9.2-1.2 1-1.2h2V2h-3c-2.5 0-4 1.2-4 3.8V8z" }) }), 9549 bgClass: "bg-[#1877f2] hover:bg-[#166fe5]" 9550 }, 9551 { 9552 name: t("watch.shareTwitter", "X (Twitter)"), 9553 url: `https://twitter.com/intent/tweet?url=${encodeURIComponent(shareUrl)}&text=${encodeURIComponent(shareTitle)}`, 9554 icon: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "w-5 h-5 fill-current", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" }) }), 9555 bgClass: "bg-[#000000] hover:bg-[#1a1a1a] dark:bg-zinc-850 dar
9555k:hover:bg-zinc-700 border border-zinc-700/50" 9556 }, 9557 { 9558 name: t("watch.shareWhatsApp", "WhatsApp"), 9559 url: `https://api.whatsapp.com/send?text=${encodeURIComponent(shareTitle + " " + shareUrl)}`, 9560 icon: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "w-6 h-6 fill-current", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946C.06 5.348 5.397.01 12.008.01c3.202.001 6.212 1.246 8.477 3.514 2.266 2.268 3.507 5.28 3.505 8.484-.004 6.657-5.34 11.997-11.953 11.997-2.005-.001-3.973-.502-5.73-1.455L0 24zm6.59-4.846c1.6.95 3.188 1.449 4.825 1.451 5.436 0 9.86-4.37 9.864-9.799.002-2.63-1.023-5.101-2.885-6.968C16.59 1.97 14.12 1.01 11.49 1.01c-5.442 0-9.87 4.372-9.874 9.802-.001 1.782.485 3.524 1.407 5.11L1.932 20.17l4.715-1.016z" }) }), 9561 bgClass: "bg-[#25d366] hover:bg-[#20ba5a]" 9562 }, 9563 { 9564 name: t("watch.shareReddit", "Reddit"), 9565 url: `https://www.reddit.com/submit?url=${encodeURIComponent(shareUrl)}&title=${encodeURIComponent(shareTitle)}`, 9566 icon: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "w-6 h-6 fill-current", viewBox: "0 0 24 24", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-.532 1.023c.319.49.52 1.068.567 1.693.992.115 1.737.954 1.737 1.97 0 1.102-.898 2-2 2-.178 0-.348-.03-.508-.073-.418 1.488-1.745 2.593-3.376 2.766.115.347.18.718.18 1.103a4.01 4.01 0 0 1-4.004 4.004c-1.815 0-3.32-1.2-3.793-2.846a3.99 3.99 0 0 1-1.267.206c-2.206 0-4-1.794-4-4 0-1.408.73-2.646 1.83-3.344.02-.63.21-1.22.52-1.724a1.25 1.25 0 0 1-.58-.934c0-.688.562-1.25 1.25-1.25.513 0 .95.31 1.14.75a7.1 7.1 0 0 1 3.86-1.127l.812-2.553 2.18.497c-.015.08-.023.163-.023.248 0 .688.562 1.25 1.25 1.25.688 0 1.25-.562 1.25-1.25s-.562-1.25-1.25-1.25c-.562 0-1.026.376-1.185.889l-2.433-.555-.916 2.885c-1.826.047-3.473.493-4.7 1.176.155-.38.53-.655.975-.655a1.25 1.25 0 0 1 1.25 1.25c0 .356-.153.674-.395.898.375.52.61 1.157.65 1.848.914.156 1.597.94 1.597 1.892 0 .613-.29 1.156-.74 1.503a4.01 4.01 0 0 1-2.906 1.257c-.12 0-.24-.007-.358-.02a3.99 3.99 0 0 1-3.08-3.076c-.534-.14-1.022-.44-1.39-.844zm-7.66 4.316a.91.91 0 1 0 0 1.82.91.91 0 0 0 0-1.82zm5.3 0a.91.91 0 1 0 0 1.82.91.91 0 0 0 0-1.82z" }) }), 9567 bgClass: "bg-[#ff4500] hover:bg-[#e03d00]" 9568 }, 9569 { 9570 name: t("watch.shareEmail", "Email"), 9571 url: `mailto:?subject=${encodeURIComponent(shareTitle)}&body=${encodeURIComponent(t("watch.shareEmailBody", "Check this out on HamieNET: ") + shareUrl)}`, 9572 icon: /* @__PURE__ */ jsxRuntimeExports.jsx(Mail, { className: "w-5 h-5 text-white" }), 9573 bgClass: "bg-zinc-600 hover:bg-zinc-700" 9574 } 9575 ]; 9576 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-white dark:bg-zinc-900 rounded-3xl shadow-2xl w-full max-w-md overflow-hidden border border-zinc-200/50 dark:border-zinc-800/50 animate-in zoom-in-95 duration-200", children: [ 9577 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between items-center p-5 border-b border-zinc-100 dark:border-zinc-800", children: [
9578 /* @__PURE__ */ jsxRuntimeExports.jsx("h3", { className: "font-extrabold text-lg text-zinc-950 dark:text-white", children: t("watch.shareTitle", "Share") }), 9579 /* @__PURE__ */ jsxRuntimeExports.jsx( 9580 "button", 9581 { 9582 onClick: onClose, 9583 className: "text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300 p-1.5 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-colors", 9584 "aria-label": t("common.close", "Close"), 9585 children: /* @__PURE__ */ jsxRuntimeExports.jsx(X, { size: 20 }) 9586 } 9587 ) 9588 ] }), 9589 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-6 flex flex-col gap-6", children: [ 9590 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-4 overflow-x-auto py-2 px-2 -mx-2 -my-2 custom-scrollbar justify-start", children: socialNetworks.map((net) => /* @__PURE__ */ jsxRuntimeExports.jsxs( 9591 "a", 9592 { 9593 href: net.url, 9594 target: "_blank", 9595 rel: "noopener noreferrer", 9596 className: "flex flex-col items-center gap-2 group shrink-0", 9597 children: [
9598 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `w-12 h-12 rounded-full flex items-center justify-center text-white transition-all duration-200 transform group-hover:scale-110 shadow-md ${net.bgClass}`, children: net.icon }), 9599 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] font-medium text-zinc-600 dark:text-zinc-400 group-hover:text-zinc-900 dark:group-hover:text-zinc-200 transition-colors", children: net.name }) 9600 ] 9601 }, 9602 net.name 9603 )) }), 9604 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [ 9605 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-wider", children: t("watch.shareLink", "Page Link") }), 9606 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 bg-zinc-50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-1.5 pl-4 overflow-hidden", children: [
9607 /* @__PURE__ */ jsxRuntimeExports.jsx( 9608 "input", 9609 { 9610 type: "text", 9611 readOnly: true, 9612 value: shareUrl, 9613 onFocus: (e) => e.target.select(), 9614 className: "w-full bg-transparent border-none outline-none text-sm text-zinc-800 dark:text-zinc-200 font-medium" 9615 } 9616 ), 9617 /* @__PURE__ */ jsxRuntimeExports.jsx( 9618 "button", 9619 { 9620 onClick: handleCopy, 9621 className: `flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold transition-all ${copied ? "bg-emerald-500 text-white shadow-lg shadow-emerald-500/20" : "bg-indigo-600 hover:bg-indigo-700 text-white shadow-lg shadow-indigo-600/20"}`, 9622 children: copied ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 9623 /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14 }), 9624 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("watch.copied", "Copied") }) 9625 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
9626 /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { size: 14 }), 9627 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("watch.copy", "Copy") }) 9628 ] }) 9629 } 9630 ) 9631 ] }) 9632 ] }), 9633 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2 pt-2 border-t border-zinc-100 dark:border-zinc-800", children: [ 9634 /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-xs font-bold text-zinc-400 dark:text-zinc-500 uppercase tracking-wider", children: t("watch.midiLink", "MIDI Link") }), 9635 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 bg-zinc-50 dark:bg-zinc-800/50 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-1.5 pl-4 overflow-hidden", children: [ 9636 /* @__PURE__ */ jsxRuntimeExports.jsx( 9637 "input", 9638 { 9639 type: "text", 9640 readOnly: true, 9641 value: gameMidiUrl, 9642 onFocus: (e) => e.target.select(), 9643 className: "w-full bg-transparent border-none outline-none text-sm text-zinc-800 dark:text-zinc-200 font-medium" 9644 } 9645 ),
9646 /* @__PURE__ */ jsxRuntimeExports.jsx( 9647 "button", 9648 { 9649 onClick: handleCopyGameMidi, 9650 className: `flex items-center gap-1.5 px-4 py-2 rounded-xl text-xs font-bold transition-all ${gameMidiCopied ? "bg-emerald-500 text-white shadow-lg shadow-emerald-500/20" : "bg-indigo-600 hover:bg-indigo-700 text-white shadow-lg shadow-indigo-600/20"}`, 9651 children: gameMidiCopied ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 9652 /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 14 }), 9653 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("watch.copied", "Copied") }) 9654 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 9655 /* @__PURE__ */ jsxRuntimeExports.jsx(Copy, { size: 14 }), 9656 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("watch.copy", "Copy") }) 9657 ] }) 9658 } 9659 ) 9660 ] }) 9661 ] }), 9662 duration > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center gap-3 pt-2 border-t border-zinc-100 dark:border-zinc-800", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "relative flex items-center cursor-pointer select-none", children: [
9663 /* @__PURE__ */ jsxRuntimeExports.jsx( 9664 "input", 9665 { 9666 type: "checkbox", 9667 checked: startAtChecked, 9668 onChange: (e) => setStartAtChecked(e.target.checked), 9669 className: "sr-only peer" 9670 } 9671 ), 9672 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-5 h-5 bg-zinc-100 dark:bg-zinc-800 border-2 border-zinc-300 dark:border-zinc-700 rounded peer-checked:bg-indigo-600 peer-checked:border-indigo-600 transition-all flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Check, { size: 12, className: "text-white opacity-0 peer-checked:opacity-100 transition-opacity" }) }), 9673 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-3 text-sm font-semibold text-zinc-700 dark:text-zinc-300", children: [ 9674 t("watch.shareStartAt", "Start at"), 9675 " ", 9676 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-indigo-600 dark:text-indigo-400", children: formatTime(currentTime) }) 9677 ] }) 9678 ] }) }) 9679 ] }) 9680 ] }) }); 9681}; 9682const TRUST_LINK_THRESHOLD = 50; 9683const LinkifiedText = ({ text, trustScore = 0 }) => { 9684 if (!text) return null; 9685 if (trustScore < TRUST_LINK_THRESHOLD) { 9686 return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: text }); 9687 } 9688 const urlRegex = /(https?:\/\/[^\s]+)/g; 9689 const parts = text.split(urlRegex); 9690 return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: parts.map((part, i) => { 9691 if (part.match(urlRegex)) { 9692 return /* @__PURE__ */ jsxRuntimeExports.jsx( 9693 "a", 9694 { 9695 href: part, 9696 target: "_blank", 9697 rel: "noopener noreferrer nofollow ugc", 9698 className: "text-indigo-600 dark:text-indigo-400 hover:underline break-all", 9699 children: part 9700 }, 9701 i 9702 ); 9703 } 9704 return part; 9705 }) }); 9706}; 9707const VideoMetadata = ({ 9708 track, 9709 currentUser, 9710 isSubscribed, 9711 onSubscribe, 9712 onChannelSelect, 9713 onArtistSelect, 9714 likedMidis, 9715 toggleLike, 9716 tags, 9717 onTagSelect, 9718 onVoteTag, 9719 onOpenTagModal, 9720 onAddToPlaylist, 9721 onReport, 9722 onAuthRequested, 9723 onEditWiki,
9724 onCustomizeBackground, 9725 onRemoveBackground, 9726 currentTime = 0, 9727 duration = 0, 9728 isLiveSynthFallback = false, 9729 hasSynthError = false, 9730 onReuploadNeeded 9731}) => { 9732 var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l; 9733 const { t } = useTranslation(); 9734 const isCommunityContributor = !track.uploader || track.uploader.id === -1 || track.uploader.name === "Community Contributor"; 9735 const { formatNumber } = useSettings(); 9736 const { staffModeEnabled } = useAdmin(); 9737 const [currentStatus, setCurrentStatus] = reactExports.useState(track.visibility); 9738 const [isMenuOpen, setIsMenuOpen] = reactExports.useState(false); 9739 const [isShareModalOpen, setIsShareModalOpen] = reactExports.useState(false); 9740 const [governanceUser, setGovernanceUser] = reactExports.useState(null); 9741 const [isGovernModalOpen, setIsGovernModalOpen] = reactExports.useState(false); 9742 const [isDownloading, setIsDownloading] = reactExports.useState(false); 9743 const [isDisclaimerOpen, setIsDisclaimerOpen] = reactExports.useState(false); 9744 const [isStatusExpanded, setIsStatusExpanded] = reactExports.useState(false); 9745 const [pendingDownloadType, setPendingDownloadType] = reactExports.useState(null); 9746 const [hasAcceptedTerms, setHasAcceptedTerms] = reactExports.useState(() => { 9747 if (typeof window === "undefined") return false; 9748 return localStorage.getItem("hamie_edu_accepted") === "true"; 9749 }); 9750 const menuRef = reactExports.useRef(null); 9751 reactExports.useEffect(() => { 9752 const handleClickOutside = (event) => { 9753 if (menuRef.current && !menuRef.current.contains(event.target)) { 9754 setIsMenuOpen(false); 9755 } 9756 }; 9757 document.addEventListener("mousedown", handleClickOutside); 9758 return () => document.removeEventListener("mousedown", handleClickOutside); 9759 }, []); 9760 const handleSecureDownload = async (type, bypassTermsCheck = false) => { 9761 if (!bypassTermsCheck && !hasAcceptedTerms && (!currentUser || !currentUser.educationalAccepted)) { 9762 setPendingDownloadType(type); 9763 setIsDisclaimerOpen(true); 9764 return; 9765 } 9766 const userId = currentUser ? currentUser.id : -1; 9767 const displayType = type === "clean" ? "MIDI" : type.toUpperCase(); 9768 if (currentUser) { 9769 if (!isSubscribed && currentUser.role !== "ADMIN") { 9770 const confirmMsg = t("watch.downloadConfirmSubBenefit", "Download {{type}}? This will deduct credits from your account. (Subscribers download for FREE!)", { type: displayType }); 9771 if (!confirm(confirmMsg)) return; 9772 } else if (currentUser.role === "ADMIN") { 9773 const confirmMsg = t("watch.downloadConfirmAdmin", "Download {{type}}? (Admin: No credits deducted)", { type: displayType }); 9774 if (!confirm(confirmMsg)) return; 9775 } 9776 } else { 9777 const confirmMsg = t("watch.downloadConfirmGuest", "Download {{type}}? Unregistered users have a daily limit.", { type: displayType }); 9778 if (!confirm(confirmMsg)) return; 9779 } 9780 setIsDownloading(true); 9781 try { 9782 const response = await fetch(`/api/tracks/${track.id}/secure-download`, { 9783 method: "POST", 9784 headers: { "Content-Type": "application/json" }, 9785 body: JSON.stringify({ userId, type }) 9786 }); 9787 if (response.ok) { 9788 const blob = await response.blob(); 9789 const url = window.URL.createObjectURL(blob); 9790 const a = document.createElement("a"); 9791 a.href = url; 9792 const contentDisposition = response.headers.get("content-disposition"); 9793 let filename = `${track.title}.${type === "opus" ? "opus" : "mid"}`; 9794 if (contentDisposition) { 9795 const match = contentDisposition.match(/filename="(.+)"/); 9796 if (match) filename = match[1]; 9797 } 9798 a.download = filename; 9799 document.body.appendChild(a); 9800 const clickEvent = new MouseEvent("click", { 9801 bubbles: false, 9802 cancelable: true, 9803 view: window 9804 }); 9805 a.dispatchEvent(clickEvent); 9806 window.URL.revokeObjectURL(url); 9807 document.body.removeChild(a); 9808 } else { 9809 const err = await response.json(); 9810 alert(`Download failed: ${err.error}`); 9811 } 9812 } catch (e) { 9813 console.error(e); 9814 api.logError(e, "VideoMetadata:handleSecureDownload"); 9815 alert("Download error occurred."); 9816 } finally { 9817 setIsDownloading(false); 9818 setIsMenuOpen(false); 9819 } 9820 }; 9821 const handleToggleVisibility = async () => { 9822 try { 9823 const result = await api.toggleTrackVisibility(track.id); 9824 setCurrentStatus(result.status); 9825 } catch (e) { 9826 console.error(e); 9827 } 9828 }; 9829 const handleGovernUploader = async () => { 9830 try { 9831 const user = await api.getAdminUser(track.uploader.id); 9832 setGovernanceUser(user); 9833 setIsGovernModalOpen(true); 9834 } catch (e) { 9835 console.error(e); 9836 } 9837 }; 9838 const handleReprocess = async () => { 9839 if (!confirm(t("watch.reprocessConfirm", "Are you sure you want to re-process MIDI ID #{{id}}? This will reset its processing state and re-queue it.", { id: track.id }))) return; 9840 try { 9841 await api.post("/admin/tracks/reprocess", { ids: [track.id] }); 9842 alert(t("watch.reprocessSuccess", "Successfully queued MIDI ID #{{id}} for reprocessing.", { id: track.id })); 9843 } catch (e) { 9844 console.error(e); 9845 alert(t("watch.reprocessError", "Failed to queue MIDI: {{error}}", { error: e.message || e })); 9846 } 9847 };
9848 const [fitStage, setFitStage] = reactExports.useState(0); 9849 const containerRef = reactExports.useRef(null); 9850 const nameRef = reactExports.useRef(null); 9851 reactExports.useEffect(() => { 9852 const checkFit = () => { 9853 if (!nameRef.current || !containerRef.current) return; 9854 const nameEl = nameRef.current; 9855 const containerWidth = containerRef.current.clientWidth; 9856 const isOverflowing = nameEl.scrollWidth > containerWidth + 2; 9857 if (isOverflowing && fitStage < 3) { 9858 setFitStage((prev) => prev + 1); 9859 } 9860 }; 9861 const observer = new ResizeObserver(() => { 9862 setFitStage(0); 9863 }); 9864 if (containerRef.current) observer.observe(containerRef.current); 9865 checkFit(); 9866 return () => observer.disconnect(); 9867 }, [fitStage, track.uploader.name]); 9868 const getNameClasses = () => { 9869 const base = "font-bold text-zinc-900 dark:text-white cursor-pointer hover:underline transition-all duration-200"; 9870 switch (fitStage) { 9871 case 0: 9872 return `${base} text-base md:text-lg whitespace-nowrap overflow-visible`; 9873 case 1: 9874 return `${base} text-sm md:text-base whitespace-nowrap overflow-visible`; 9875 case 2: 9876 return `${base} text-sm line-clamp-2 leading-snug break-words`; 9877 case 3: 9878 return `${base} text-sm truncate leading-snug break-words`; 9879 default: 9880 return base; 9881 } 9882 }; 9883 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "", children: [ 9884 track.artist && !["unknown", "unknown artist"].includes(track.artist.toLowerCase().trim()) && /* @__PURE__ */ jsxRuntimeExports.jsx( 9885 "a", 9886 { 9887 href: `/?q=${encodeURIComponent(track.artist)}`, 9888 onClick: (e) => { 9889 if (!e.metaKey && !e.ctrlKey && !e.shiftKey && e.button !== 1) { 9890 e.preventDefault(); 9891 if (onArtistSelect) { 9892 onArtistSelect(track.artist); 9893 } else { 9894 window.location.href = `/?q=${encodeURIComponent(track.artist)}`; 9895 } 9896 } 9897 }, 9898 className: "inline-block text-xs md:text-sm font-semibold text-zinc-500 dark:text-zinc-400 hover:text-indigo-600 dark:hover:text-indigo-400 hover:underline transition-colors cursor-pointer leading-tight mb-1", 9899 title: `Explore all tracks by ${track.artist}`, 9900 children: track.artist 9901 } 9902 ), 9903 /* @__PURE__ */ jsxRuntimeExports.jsx("h1", { className: "text-xl md:text-2xl font-bold text-zinc-900 dark:text-white line-clamp-2 leading-tight", children: track.title }), 9904 (track.status === "pending" || track.status === "processing" || track.visibility === "missing" || hasSynthError || isLiveSynthFallback) && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 mb-2", children: [
9905 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap text-xs", children: [ 9906 (track.status === "pending" || track.status === "processing") && /* @__PURE__ */ jsxRuntimeExports.jsxs( 9907 "button", 9908 { 9909 onClick: () => setIsStatusExpanded(!isStatusExpanded), 9910 className: "group relative flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-amber-500/10 hover:bg-amber-500/20 border border-amber-500/30 text-amber-700 dark:text-amber-300 font-medium transition-all shadow-xs cursor-pointer select-none", 9911 title: t("watch.status.hoverTooltip", "Click to view processing details & status"), 9912 children: [ 9913 /* @__PURE__ */ jsxRuntimeExports.jsx(OctagonAlert, { size: 14, className: "text-amber-500 animate-pulse shrink-0" }), 9914 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: track.status === "pending" ? t("watch.status.pendingTitle", "MIDI Queued for Processing") : t("watch.status.processingTitle", "MIDI Processing in Progress") }),
9915 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-0.5 rounded-full hover:bg-amber-500/20 text-amber-600 dark:text-amber-400", children: isStatusExpanded ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 13 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown$1, { size: 13 }) }) 9916 ] 9917 } 9918 ), 9919 track.visibility === "missing" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 px-2.5 py-1 rounded-full bg-red-500/10 border border-red-500/30 text-red-700 dark:text-red-300 font-medium shadow-xs", children: [ 9920 /* @__PURE__ */ jsxRuntimeExports.jsx(TriangleAlert, { size: 14, className: "text-red-500 animate-pulse shrink-0" }), 9921 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("watch.status.missingTitle", "MIDI File Missing") }), 9922 onReuploadNeeded && /* @__PURE__ */ jsxRuntimeExports.jsx( 9923 "button", 9924 { 9925 onClick: onReuploadNeeded, 9926 className: "ml-1 px-2 py-0.5 bg-red-600 hover:bg-red-700 text-white rounded-md text-[11px] font-bold transition-all shadow-xs", 9927 children: t("watch.status.reuploadBtn", "Re-upload
9927Now") 9928 } 9929 ) 9930 ] }), 9931 hasSynthError && /* @__PURE__ */ jsxRuntimeExports.jsxs( 9932 "button", 9933 { 9934 onClick: () => setIsStatusExpanded(!isStatusExpanded), 9935 className: "flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-red-500/10 hover:bg-red-500/20 border border-red-500/30 text-red-700 dark:text-red-300 font-medium transition-all shadow-xs cursor-pointer select-none", 9936 title: t("watch.status.hoverTooltip", "Click to view processing details & status"), 9937 children: [ 9938 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 14, className: "text-red-500 shrink-0" }), 9939 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("watch.status.synthErrorTitle", "Audio Synthesizer Failed to Load") }), 9940 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-0.5 rounded-full hover:bg-red-500/20 text-red-600 dark:text-red-400", children: isStatusExpanded ? /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronUp, { size: 13 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(ChevronDown$1, { size: 13 }) }) 9941 ] 9942 } 9943 ), 9944 isLiveSynthFallback && track.status !== "pending" && track.status !== "processing" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-indigo-500/10 border border-indigo-500/30 text-indigo-700 dark:text-indigo-300 font-medium shadow-xs", title: t("watch.status.liveSynthDesc", "Playing directly from MIDI using a JS soundfont."), children: [
9945 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "ð¹" }), 9946 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("watch.status.liveSynthTitle", "Live Synthesis Active") }) 9947 ] }) 9948 ] }), 9949 isStatusExpanded && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 p-3.5 rounded-xl bg-amber-500/5 dark:bg-amber-950/20 border border-amber-500/20 text-zinc-800 dark:text-zinc-200 text-xs space-y-2 animate-in fade-in slide-in-from-top-1 duration-200 shadow-sm", children: [ 9950 (track.status === "pending" || track.status === "processing") && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 9951 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 font-bold text-amber-700 dark:text-amber-300", children: [ 9952 /* @__PURE__ */ jsxRuntimeExports.jsx(OctagonAlert, { size: 15 }), 9953 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: track.status === "pending" ? t("watch.status.pendingTitle", "MIDI Queued for Processing") : t("watch.status.processingTitle", "MIDI Processing in Progress") }) 9954 ] }),
9955 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "opacity-90 leading-relaxed", children: track.status === "pending" ? t("watch.status.pendingDesc", "This MIDI is currently in queue waiting to be processed.") : t("watch.status.processingDesc", "This MIDI is currently being analyzed and rendered.") }), 9956 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pt-1.5 border-t border-amber-500/15", children: [ 9957 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-semibold text-amber-600 dark:text-amber-400 block mb-1", children: t("watch.status.comingSoonHeader", "The following advanced interactive features are coming soon:") }), 9958 /* @__PURE__ */ jsxRuntimeExports.jsxs("ul", { className: "grid grid-cols-1 sm:grid-cols-2 gap-1 opacity-85 ml-1", children: [ 9959 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex items-center gap-1.5", children: [ 9960 "⢠", 9961 t("watch.status.feature3d", "Interactive 3D Piano Roll View") 9962 ] }),
9963 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex items-center gap-1.5", children: [ 9964 "⢠", 9965 t("watch.status.featurePercussion", "Percussion Roll Channel Analyzer") 9966 ] }), 9967 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex items-center gap-1.5", children: [ 9968 "⢠", 9969 t("watch.status.featureSheet", "Interactive Sheet Music Score Viewer") 9970 ] }), 9971 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex items-center gap-1.5", children: [ 9972 "⢠", 9973 t("watch.status.featureSynth", "Arrangement Orchestra Synthesizer") 9974 ] }), 9975 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex items-center gap-1.5", children: [ 9976 "⢠", 9977 t("watch.status.featureGame", "Gamepad Play-Along Game Mode") 9978 ] }), 9979 /* @__PURE__ */ jsxRuntimeExports.jsxs("li", { className: "flex items-center gap-1.5", children: [ 9980 "⢠", 9981 t("watch.status.featureStems", "High-fidelity Opus Audio Stems") 9982 ] }) 9983 ] }) 9984 ] }) 9985 ] }), 9986 hasSynthError && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "pt-1", children: [
9987 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 font-bold text-red-600 dark:text-red-400 mb-1", children: [ 9988 /* @__PURE__ */ jsxRuntimeExports.jsx(CircleAlert, { size: 15 }), 9989 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("watch.status.synthErrorTitle", "Audio Synthesizer Failed to Load") }) 9990 ] }), 9991 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "opacity-90 leading-relaxed", children: t("watch.status.synthErrorDesc", "Some audio tracks or the selected soundfont failed to load correctly. Please refresh the page or try switching to Pre-rendered Audio in the sidebar mixer panel.") }) 9992 ] }) 9993 ] }) 9994 ] }), 9995 staffModeEnabled && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 mb-3 p-3 bg-indigo-500/10 border border-indigo-500/20 rounded-xl flex items-center justify-between animate-in slide-in-from-top-2", children: [ 9996 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4", children: [
9997 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-indigo-600 dark:text-indigo-400 font-bold text-xs", children: [ 9998 /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldCheck, { size: 16 }), 9999 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "ADMIN CONTROL" }) 10000 ] }), 10001 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-4 w-px bg-indigo-500/20" }), 10002 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 10003 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10004 "button", 10005 { 10006 onClick: handleToggleVisibility, 10007 className: `px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-2 transition-all ${currentStatus === "suspended" ? "bg-red-600 text-white shadow-lg" : "bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-zinc-200 dark:border-zinc-700"}`, 10008 children: [ 10009 currentStatus === "suspended" ? /* @__P
10009URE__ */ jsxRuntimeExports.jsx(Eye, { size: 14 }) : /* @__PURE__ */ jsxRuntimeExports.jsx(EyeOff, { size: 14 }), 10010 currentStatus === "suspended" ? "UNHIDE TRACK" : "HIDE TRACK" 10011 ] 10012 } 10013 ), 10014 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10015 "a", 10016 { 10017 href: `${((_a = window.HAMIE_ENV) == null ? void 0 : _a.ADMIN_URL) || ""}/admin/media?id=${track.id}`, 10018 target: "_blank", 10019 className: "px-3 py-1.5 bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-zinc-200 dark:border-zinc-700 rounded-lg text-xs font-bold flex items-center gap-2 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-all", 10020 children: [ 10021 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 14 }), 10022 "DATABASE RECORD" 10023 ] 10024 } 10025 ), 10026 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10027 "button", 10028 { 10029 onClick: handleGovernUploader, 10030 className: "px-3 py-1.5 bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-zinc-200 dark:border-zinc-700 rounded-lg text-xs font-bold flex items-center gap-2 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-all", 10031 children: [
10032 /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldAlert, { size: 14, className: "text-indigo-500" }), 10033 "GOVERN UPLOADER" 10034 ] 10035 } 10036 ), 10037 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10038 "button", 10039 { 10040 onClick: handleReprocess, 10041 className: "px-3 py-1.5 bg-white dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 border border-zinc-200 dark:border-zinc-700 rounded-lg text-xs font-bold flex items-center gap-2 hover:bg-zinc-50 dark:hover:bg-zinc-700 transition-all", 10042 children: [ 10043 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 14, className: "text-indigo-500" }), 10044 "RE-PROCESS MIDI" 10045 ] 10046 } 10047 ) 10048 ] }) 10049 ] }), 10050 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-[10px] text-zinc-500 font-medium", children: [ 10051 "Last Modified: ", 10052 new Date(track.updatedAt || track.uploadedAt).toLocaleString() 10053 ] }) 10054 ] }), 10055 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col md:flex-row md:items-center justify-between gap-2 mt-0.5 pb-2 border-b border-zinc-200 dark:border-zinc-800", children: [
10056 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-3 md:gap-4 min-w-0 flex-1", children: [ 10057 !isCommunityContributor ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 10058 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative group cursor-pointer shrink-0", onClick: () => onChannelSelect(track.uploader.name), children: [ 10059 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: track.uploader.avatar, name: track.uploader.name, size: 40, className: "bg-zinc-200 shadow-sm shrink-0" }), 10060 isPro(track.uploader) && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `absolute -bottom-1 -right-1 text-white text-[8px] font-bold px-1.5 rounded-full border border-white dark:border-zinc-900 uppercase tracking-tighter shadow-sm ${((_b = track.uploader.subscriptionTier) == null ? void 0 : _b.toLowerCase()) === "premium" ? "bg-indigo-600" : "bg-amber-500"}`, children: ((_c = track.uploader.subscriptionTier) == null ? void 0 : _c.toLowerCase()) === "premium" ? "PREMIUM" : ((_d = track.uploader.subscriptionTier) == null ? void 0 : _d.toUpperCase()) || "PRO" }) 10061 ] }),
10062 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1", ref: containerRef, children: [ 10063 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 md:gap-2 min-w-0", children: [ 10064 /* @__PURE__ */ jsxRuntimeExports.jsx( 10065 "h3", 10066 { 10067 ref: nameRef, 10068 className: getNameClasses(), 10069 onClick: () => onChannelSelect(track.uploader.name), 10070 title: track.uploader.name, 10071 children: track.uploader.name 10072 } 10073 ), 10074 track.uploader.id === -1 && track.countryCode ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(GlobeFlag, { countryCode: track.countryCode, size: 16 }) }) : track.uploader.id !== -1 && track.uploader.showCountryBadge && track.uploader.country ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(GlobeFlag, { countryCode: track.uploader.country, size: 16 }) }) : null 10075 ] }),
10076 track.uploader.id !== -1 && /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] md:text-xs text-zinc-500 leading-normal", children: [ 10077 formatNumber(track.uploader.subscriberCount || 0), 10078 " ", 10079 t("common.subscribers", "subscribers") 10080 ] }) 10081 ] }) 10082 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "min-w-0 flex-1 flex items-center gap-1.5 py-2 relative group/tooltip", children: [ 10083 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5 cursor-help", children: [ 10084 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs md:text-sm italic text-zinc-500 dark:text-zinc-400 font-medium select-none", children: t("watch.fromCommunityContributor", "from a community contributor") }), 10085 /* @__PURE__ */ jsxRuntimeExports.jsx( 10086 Info, 10087 { 10088 size: 14, 10089 className: "text-zinc-400 hover:text-zinc-600 dark:text-zinc-500 dark:hover:text-zinc-300 transition-colors shrink-0" 10090 } 10091 ) 10092 ] }), 10093 track.countryCode && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 ml-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(GlobeFlag, { countryCode: track.countryCode, size: 14 }) }),
10094 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute left-0 top-full mt-2 hidden group-hover/tooltip:flex flex-col w-72 md:w-80 p-4 rounded-xl bg-zinc-900/95 dark:bg-zinc-800/95 backdrop-blur-md text-white text-xs leading-relaxed shadow-xl border border-white/10 dark:border-zinc-700/80 z-50 transition-all duration-300 animate-in fade-in slide-in-from-top-1", children: [ 10095 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-1.5 font-semibold text-amber-400", children: [ 10096 /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { size: 14, className: "shrink-0" }), 10097 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Community Contributor Status" }) 10098 ] }), 10099 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-300 font-normal leading-relaxed", children: t("watch.communityContributorTooltip", "Community Contributors are guest users who upload MIDI files without registering. These files are public and maintained by the community.") }),
10100 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute top-0 left-6 -mt-1.5 w-3 h-3 bg-zinc-900 dark:bg-zinc-800 border-t border-l border-white/10 dark:border-zinc-700/80 rotate-45" }) 10101 ] }) 10102 ] }), 10103 (!currentUser || currentUser.id !== track.uploader.id) && track.uploader.id !== -1 && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onSubscribe, className: `ml-2 px-4 py-2 rounded-full font-bold text-sm transition-all shrink-0 ${isSubscribed ? "bg-zinc-100 dark:bg-zinc-800 text-zinc-900 dark:text-white" : "bg-zinc-900 dark:bg-white text-white dark:text-black"}`, children: isSubscribed ? t("watch.subscribed", "Subscribed") : t("watch.subscribe", "Subscribe") }) 10104 ] }), 10105 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap justify-end", children: [ 10106 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center bg-zinc-100 dark:bg-zinc-800 rounded-full p-1", children: [ 10107 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: () => toggleLike(track.id), className: `flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium transition-colors ${likedMidis.has(track.id) ? "bg-zinc-200 dark:bg-zinc-700 text-black dark:text-white" : "hover:bg-zinc-200 dark:hover:bg-zinc-700 text-zinc-700 dark:text-zinc-300"}`, children: [
10108 /* @__PURE__ */ jsxRuntimeExports.jsx(ThumbsUp, { size: 18, className: likedMidis.has(track.id) ? "fill-current" : "" }), 10109 formatNumber(track.likesCount + (likedMidis.has(track.id) ? 1 : 0)) 10110 ] }), 10111 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-px h-6 bg-zinc-300 dark:bg-zinc-700 mx-1" }), 10112 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "px-3 py-1.5 rounded-full hover:bg-zinc-200 dark:hover:bg-zinc-700 text-zinc-700 dark:text-zinc-300 transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ThumbsDown, { size: 18 }) }) 10113 ] }), 10114 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10115 "button", 10116 { 10117 onClick: () => setIsShareModalOpen(true), 10118 className: "flex items-center gap-2 px-4 py-2.5 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-full text-sm font-bold text-zinc-700 dark:text-zinc-300 transition-colors", 10119 children: [ 10120 /* @__PURE__ */ jsxRuntimeExports.jsx(Share2, { size: 18 }), 10121 " ", 10122 t("watch.share", "Share") 10123 ] 10124 } 10125 ),
10126 /* @__PURE__ */ jsxRuntimeExports.jsx( 10127 ShareModal, 10128 { 10129 isOpen: isShareModalOpen, 10130 onClose: () => setIsShareModalOpen(false), 10131 currentTime, 10132 duration, 10133 trackTitle: track.title, 10134 trackId: track.id 10135 } 10136 ), 10137 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10138 "a", 10139 { 10140 href: `/media/opus/${track.id}.opus`, 10141 onClick: (e) => { 10142 e.preventDefault(); 10143 if (!isDownloading && track.opusPath) { 10144 e.stopPropagation(); 10145 handleSecureDownload("opus"); 10146 } 10147 }, 10148 className: `flex items-center gap-2 px-4 py-2.5 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-full text-sm font-bold text-zinc-700 dark:text-zinc-300 transition-colors ${isDownloading || !track.opusPath ? "opacity-50 cursor-not-allowed pointer-events-none" : ""}`, 10149 title: !track.opusPath ? t("watch.opusNotAvailable", "Opus audio not available") : "", 10150 children: [
10151 /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { size: 18 }), 10152 " ", 10153 isDownloading ? "..." : "Opus" 10154 ] 10155 } 10156 ), 10157 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10158 "a", 10159 { 10160 href: `/media/midi/${track.id}.mid`, 10161 onClick: (e) => { 10162 e.preventDefault(); 10163 if (!isDownloading) { 10164 e.stopPropagation(); 10165 handleSecureDownload("clean"); 10166 } 10167 }, 10168 className: `flex items-center gap-2 px-4 py-2.5 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-full text-sm font-bold text-zinc-700 dark:text-zinc-300 transition-colors ${isDownloading ? "opacity-50 cursor-not-allowed pointer-events-none" : ""}`, 10169 children: [ 10170 /* @__PURE__ */ jsxRuntimeExports.jsx(Download, { size: 18 }), 10171 " ", 10172 isDownloading ? "..." : t("common.midi", "MIDI") 10173 ] 10174 } 10175 ),
10176 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative ml-2", ref: menuRef, children: [ 10177 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setIsMenuOpen(!isMenuOpen), className: "p-2.5 bg-zinc-100 dark:bg-zinc-800 hover:bg-zinc-200 dark:hover:bg-zinc-700 rounded-full text-zinc-700 dark:text-zinc-300 transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(Ellipsis, { size: 18 }) }), 10178 isMenuOpen && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "absolute right-0 top-12 w-48 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-xl z-50 overflow-hidden py-1", onClick: (e) => e.stopPropagation(), children: [ 10179 /* @__PURE__ */ jsxRuntimeExports.jsxs("a", { href: `/editor/${track.id}`, className: "w-full text-left px-4 py-2.5 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2", children: [ 10180 /* @__PURE__ */ jsxRuntimeExports.jsx(SquarePen, { size: 16 }), 10181 " ", 10182 t("watch.editSong", "Edit this song") 10183 ] }),
10184 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-200 dark:bg-zinc-800 my-1" }), 10185 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: () => { 10186 setIsMenuOpen(false); 10187 onAddToPlaylist(); 10188 }, className: "w-full text-left px-4 py-2.5 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2", children: [ 10189 /* @__PURE__ */ jsxRuntimeExports.jsx(FolderPlus, { size: 16 }), 10190 " ", 10191 t("playlists.saveTo", "Save to Playlist") 10192 ] }), 10193 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-200 dark:bg-zinc-800 my-1" }), 10194 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: () => { 10195 setIsMenuOpen(false); 10196 onReport(); 10197 }, className: "w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2", children: [
10198 /* @__PURE__ */ jsxRuntimeExports.jsx(Flag, { size: 16 }), 10199 " ", 10200 t("watch.report", "Report") 10201 ] }), 10202 track.userId === -1 && currentUser && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 10203 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-200 dark:bg-zinc-800 my-1" }), 10204 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10205 "button", 10206 { 10207 onClick: () => { 10208 setIsMenuOpen(false); 10209 if (onEditWiki) onEditWiki(); 10210 }, 10211 className: "w-full text-left px-4 py-2.5 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2", 10212 children: [ 10213 /* @__PURE__ */ jsxRuntimeExports.jsx(SquarePen, { size: 16 }), 10214 " Edit Wiki Info" 10215 ] 10216 } 10217 ) 10218 ] }), 10219 currentUser && (currentUser.id === track.userId || currentUser.role === "ADMIN") && /* @__P
10219URE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 10220 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-200 dark:bg-zinc-800 my-1" }), 10221 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10222 "button", 10223 { 10224 onClick: () => { 10225 setIsMenuOpen(false); 10226 if (onCustomizeBackground) onCustomizeBackground(); 10227 }, 10228 className: "w-full text-left px-4 py-2.5 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2", 10229 children: [ 10230 /* @__PURE__ */ jsxRuntimeExports.jsx(Image, { size: 16 }), 10231 " Customize Background" 10232 ] 10233 } 10234 ), 10235 track.backgroundImageUrl && /* @__PURE__ */ jsxRuntimeExports.jsxs( 10236 "button", 10237 { 10238 onClick: () => { 10239 setIsMenuOpen(false); 10240 if (onRemoveBackground) onRemoveBackground(); 10241 }, 10242 className: "w-full text-left px-4 py-2.5 text-sm text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2", 10243 children: [
10244 /* @__PURE__ */ jsxRuntimeExports.jsx(RotateCcw, { size: 16 }), 10245 " Remove Background" 10246 ] 10247 } 10248 ) 10249 ] }), 10250 currentUser && currentUser.id === track.userId && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 10251 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-200 dark:bg-zinc-800 my-1" }), 10252 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10253 "button", 10254 { 10255 onClick: async () => { 10256 setIsMenuOpen(false); 10257 if (confirm(t("watch.confirmDelete", "Are you sure you want to delete this MIDI? It will be removed from your profile and search lists."))) { 10258 try { 10259 await api.deleteTrackAsUser(track.id, currentUser.id); 10260 alert(t("watch.deleteSuccess", "MIDI deleted successfully.")); 10261 window.location.href = "/"; 10262 } catch (err) { 10263 alert(err.message || "Deletion failed"); 10264 } 10265 } 10266 }, 10267 className: "w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2 font-bold", 10268 children: [
10269 /* @__PURE__ */ jsxRuntimeExports.jsx(Trash, { size: 16 }), 10270 " ", 10271 t("watch.deleteTrack", "Delete MIDI") 10272 ] 10273 } 10274 ) 10275 ] }), 10276 (currentUser == null ? void 0 : currentUser.role) === "ADMIN" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 10277 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-200 dark:bg-zinc-800 my-1" }), 10278 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10279 "a", 10280 { 10281 href: `${((_e = window.HAMIE_ENV) == null ? void 0 : _e.ADMIN_URL) || ""}/admin/media?id=${track.id}`, 10282 target: "_blank", 10283 rel: "noopener noreferrer", 10284 onClick: () => setIsMenuOpen(false), 10285 className: "w-full text-left px-4 py-2.5 text-sm text-indigo-600 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2 font-bold", 10286 children: [
10287 /* @__PURE__ */ jsxRuntimeExports.jsx(Database, { size: 16 }), 10288 " ", 10289 t("watch.mediaLibrary", "Media Library") 10290 ] 10291 } 10292 ) 10293 ] }) 10294 ] }) 10295 ] }) 10296 ] }), 10297 /* @__PURE__ */ jsxRuntimeExports.jsx( 10298 EducationalDisclaimerModal, 10299 { 10300 isOpen: isDisclaimerOpen, 10301 onClose: () => { 10302 setIsDisclaimerOpen(false); 10303 setPendingDownloadType(null); 10304 }, 10305 onAccept: async () => { 10306 setIsDisclaimerOpen(false); 10307 localStorage.setItem("hamie_edu_accepted", "true"); 10308 setHasAcceptedTerms(true); 10309 if (currentUser) { 10310 try { 10311 await fetch("/api/user/accept-educational", { method: "POST" }); 10312 } catch (e) { 10313 console.warn("Could not sync educational acceptance to DB", e); 10314 } 10315 } 10316 if (pendingDownloadType) { 10317 handleSecureDownload(pendingDownloadType, true); 10318 setPendingDownloadType(null); 10319 } 10320 } 10321 } 10322 ) 10323 ] }), 10324 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 bg-zinc-100 dark:bg-zinc-800/50 rounded-xl p-3 hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors cursor-text group/desc", children: [
10325 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap gap-2 text-xs font-bold text-zinc-900 dark:text-white mb-2 underline-offset-4", children: [ 10326 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10327 formatNumber(track.views || 0), 10328 " ", 10329 t("common.views", "views") 10330 ] }), 10331 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â¢" }), 10332 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10333 formatNumber(track.plays || 0), 10334 " ", 10335 t("common.plays", "plays") 10336 ] }), 10337 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â¢" }), 10338 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10339 formatNumber((track.midiDownloads || 0) + (track.audioDownloads || 0)), 10340 " ", 10341 t("common.downloads", "downloads") 10342 ] }), 10343 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "â¢" }), 10344 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: timeAgo(track.uploadedAt) }) 10345 ] }),
10346 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-sm text-zinc-700 dark:text-zinc-300 whitespace-pre-wrap font-medium", children: /* @__PURE__ */ jsxRuntimeExports.jsx(LinkifiedText, { text: track.description, trustScore: track.uploader.trustScore || 0 }) }), 10347 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2.5 pt-2.5 border-t border-zinc-200/50 dark:border-zinc-700/50 flex flex-wrap items-center gap-x-3 gap-y-1.5 text-xs text-zinc-500 dark:text-zinc-400", children: [ 10348 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 10349 /* @__PURE__ */ jsxRuntimeExports.jsx(Clock, { size: 13, className: "opacity-70" }), 10350 " ", 10351 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10352 t("watch.duration", "Duration"), 10353 ": ", 10354 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-800 dark:text-zinc-200 font-semibold", children: track.duration }) 10355 ] }) 10356 ] }),
10357 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 dark:text-zinc-700 select-none", children: "â¢" }), 10358 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 10359 /* @__PURE__ */ jsxRuntimeExports.jsx(Music2, { size: 13, className: "opacity-70" }), 10360 " ", 10361 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10362 t("watch.tempo", "Tempo"), 10363 ": ", 10364 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-800 dark:text-zinc-200 font-semibold", children: [ 10365 ((_f = track.midiMetadata) == null ? void 0 : _f.tempo) || 120, 10366 " ", 10367 t("watch.bpm", "BPM") 10368 ] }) 10369 ] }) 10370 ] }), 10371 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 dark:text-zinc-700 select-none", children: "â¢" }), 10372 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [
10373 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 13, className: "opacity-70" }), 10374 " ", 10375 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10376 t("watch.signature", "Signature"), 10377 ": ", 10378 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-800 dark:text-zinc-200 font-semibold", children: ((_g = track.midiMetadata) == null ? void 0 : _g.timeSignature) || "4/4" }) 10379 ] }) 10380 ] }), 10381 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 dark:text-zinc-700 select-none", children: "â¢" }), 10382 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 10383 /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { size: 13, className: "opacity-70" }), 10384 " ", 10385 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10386 t("watch.key", "Key"), 10387 ": ", 10388 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-800 dark:text-zinc-200 font-semibold", children: ((_h = track.midiMetadata) == null ? void 0 : _h.keySignature) || "C Major" }) 10389 ] }) 10390 ] }),
10391 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 dark:text-zinc-700 select-none", children: "â¢" }), 10392 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 10393 /* @__PURE__ */ jsxRuntimeExports.jsx(Layers, { size: 13, className: "opacity-70" }), 10394 " ", 10395 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10396 t("watch.tracks", "Tracks"), 10397 ": ", 10398 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-800 dark:text-zinc-200 font-semibold", children: ((_i = track.midiMetadata) == null ? void 0 : _i.tracksCount) || 1 }) 10399 ] }) 10400 ] }), 10401 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 dark:text-zinc-700 select-none", children: "â¢" }), 10402 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 10403 /* @__PURE__ */ jsxRuntimeExports.jsx(Activity, { size: 13, className: "opacity-70" }), 10404 " ",
10405 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10406 t("watch.resolution", "Resolution"), 10407 ": ", 10408 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-800 dark:text-zinc-200 font-semibold", children: [ 10409 ((_j = track.midiMetadata) == null ? void 0 : _j.tickDiv) || 480, 10410 " PPQ" 10411 ] }) 10412 ] }) 10413 ] }), 10414 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 dark:text-zinc-700 select-none", children: "â¢" }), 10415 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 10416 /* @__PURE__ */ jsxRuntimeExports.jsx(Info, { size: 13, className: "opacity-70" }), 10417 " ", 10418 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10419 t("watch.format", "Format"), 10420 ": ", 10421 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-zinc-800 dark:text-zinc-200 font-semibold", children: [ 10422 "SMF Type ", 10423 ((_k = track.midiMetadata) == null ? void 0 : _k.format) !== void 0 ? track.midiMetadata.format : 1 10424 ] }) 10425 ] }) 10426 ] }), 10427 ((_l = track.midiMetadata) == null ? void 0 : _l.karaokeFormat) && /* @__P
10427URE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 10428 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-300 dark:text-zinc-700 select-none", children: "â¢" }), 10429 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [ 10430 /* @__PURE__ */ jsxRuntimeExports.jsx(Music, { size: 13, className: "opacity-70" }), 10431 " ", 10432 /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [ 10433 t("watch.karaoke", "Lyrics Format"), 10434 ": ", 10435 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-zinc-800 dark:text-zinc-200 font-semibold", children: track.midiMetadata.karaokeFormat === "kar" ? "KAR" : "Standard" }) 10436 ] }) 10437 ] }) 10438 ] }) 10439 ] }), 10440 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-3 flex flex-wrap gap-1.5 items-center", children: [ 10441 tags.sort((a, b) => b.score - a.score).slice(0, 10).map((tag) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center bg-white dark:bg-zinc-900/50 rounded-full border border-zinc-200 dark:border-zinc-700 pl-2.5 pr-0.5 py-0 transition-all hover:border-zinc-300 dark:hover:border-zinc-600", children: [
10442 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: () => onTagSelect(tag.name), className: "text-xs font-medium text-zinc-600 dark:text-zinc-400 hover:text-indigo-600 dark:hover:text-indigo-300 transition-colors mr-2", children: [ 10443 "#", 10444 tag.name 10445 ] }), 10446 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-0", children: [ 10447 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => onVoteTag(tag.name, 1), className: "p-0.5 text-zinc-400 hover:text-emerald-500 hover:bg-emerald-50 dark:hover:bg-emerald-900/20 rounded transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowUp, { size: 11 }) }), 10448 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => onVoteTag(tag.name, -1), className: "p-0.5 text-zinc-400 hover:text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors", children: /* @__PURE__ */ jsxRuntimeExports.jsx(ArrowDown, { size: 11 }) }) 10449 ] }) 10450 ] }, tag.id)),
10451 /* @__PURE__ */ jsxRuntimeExports.jsxs("button", { onClick: onOpenTagModal, className: "text-xs text-indigo-600 dark:text-indigo-400 hover:underline font-bold px-2 flex items-center gap-1", children: [ 10452 /* @__PURE__ */ jsxRuntimeExports.jsx(Hash, { size: 10 }), 10453 " ", 10454 t("watch.addTags", "Add Tags") 10455 ] }) 10456 ] }) 10457 ] }), 10458 governanceUser && /* @__PURE__ */ jsxRuntimeExports.jsx( 10459 GovernanceModal, 10460 { 10461 user: governanceUser, 10462 isOpen: isGovernModalOpen, 10463 onClose: () => setIsGovernModalOpen(false) 10464 } 10465 ) 10466 ] }); 10467}; 10468const CommentItem = ({ comment, currentUser, onReport, onVote, onReply, t, isReply = false, onHide, onGovern }) => { 10469 const { staffModeEnabled } = useAdmin(); 10470 const [isMenuOpen, setIsMenuOpen] = reactExports.useState(false); 10471 const [isReplying, setIsReplying] = reactExports.useState(false); 10472 const [replyText, setReplyText] = reactExports.useState(""); 10473 const menuRef = reactExports.useRef(null); 10474 reactExports.useEffect(() => { 10475 const handleClickOutside = (event) => { 10476 if (menuRef.current && !menuRef.current.contains(event.target)) { 10477 setIsMenuOpen(false); 10478 } 10479 }; 10480 document.addEventListener("mousedown", handleClickOutside); 10481 return () => document.removeEventListener("mousedown", handleClickOutside); 10482 }, []); 10483 const handleVote = (value) => { 10484 if (!currentUser) return alert("Please sign in to vote"); 10485 onVote(comment.id, value); 10486 }; 10487 const handleReplySubmit = (e) => { 10488 e.preventDefault(); 10489 if (!replyText.trim() || !onReply) return; 10490 onReply(comment.id, replyText); 10491 setReplyText(""); 10492 setIsReplying(false); 10493 }; 10494 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex gap-4 group relative ${isReply ? "ml-12 scale-95" : ""}`, children: [ 10495 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: comment.avatar, name: comment.user, size: isReply ? 32 : 40 }), 10496 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-w-0", children: [
10497 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mb-1", children: [ 10498 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-xs text-zinc-900 dark:text-white", children: comment.user }), 10499 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-zinc-500", children: timeAgo(comment.timestamp) }) 10500 ] }), 10501 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-800 dark:text-zinc-200 leading-relaxed break-words", children: comment.text }), 10502 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 mt-2", children: [ 10503 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10504 "button", 10505 { 10506 onClick: () => handleVote(1), 10507 className: "flex items-center gap-1 text-xs text-zinc-500 hover:text-indigo-600 transition-colors", 10508 children: [ 10509 /* @__PURE__ */ jsxRuntimeExports.jsx(ThumbsUp, { size: 14 }), 10510 " ", 10511 comment.likes || 0 10512 ] 10513 } 10514 ),
10515 /* @__PURE__ */ jsxRuntimeExports.jsx( 10516 "button", 10517 { 10518 onClick: () => handleVote(-1), 10519 className: "flex items-center gap-1 text-xs text-zinc-500 hover:text-red-600 transition-colors", 10520 children: /* @__PURE__ */ jsxRuntimeExports.jsx(ThumbsDown, { size: 14 }) 10521 } 10522 ), 10523 !isReply && /* @__PURE__ */ jsxRuntimeExports.jsx( 10524 "button", 10525 { 10526 onClick: () => setIsReplying(!isReplying), 10527 className: "text-xs text-zinc-500 hover:text-zinc-800 dark:hover:text-zinc-200 font-bold transition-colors", 10528 children: t("watch.reply", "Reply") 10529 } 10530 ) 10531 ] }), 10532 isReplying && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 flex gap-3 animate-in fade-in slide-in-from-top-2", children: [ 10533 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: currentUser == null ? void 0 : currentUser.avatar, name: (currentUser == null ? void 0 : currentUser.username) || "", size: 24 }),
10534 /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { className: "flex-1", onSubmit: handleReplySubmit, children: [ 10535 /* @__PURE__ */ jsxRuntimeExports.jsx( 10536 "input", 10537 { 10538 autoFocus: true, 10539 type: "text", 10540 value: replyText, 10541 onChange: (e) => setReplyText(e.target.value), 10542 placeholder: t("watch.addReply", "Add a reply..."), 10543 className: "w-full bg-transparent border-b border-zinc-300 dark:border-zinc-700 py-1 text-sm focus:border-indigo-500 outline-none text-zinc-900 dark:text-white transition-colors" 10544 } 10545 ), 10546 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end mt-2 gap-2", children: [ 10547 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: () => setIsReplying(false), className: "px-3 py-1.5 rounded-full text-xs font-bold text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800", children: t("common.cancel", "Cancel") }),
10548 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "submit", disabled: !replyText.trim(), className: "px-3 py-1.5 rounded-full text-xs font-bold bg-indigo-600 hover:bg-indigo-700 text-white disabled:opacity-50", children: t("watch.replyBtn", "Reply") }) 10549 ] }) 10550 ] }) 10551 ] }) 10552 ] }), 10553 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", ref: menuRef, children: [ 10554 /* @__PURE__ */ jsxRuntimeExports.jsx( 10555 "button", 10556 { 10557 onClick: (e) => { 10558 e.stopPropagation(); 10559 setIsMenuOpen(!isMenuOpen); 10560 }, 10561 className: `p-2 text-zinc-500 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded-full transition-all self-start ${isMenuOpen ? "opacity-100 bg-zinc-100 dark:bg-zinc-800" : "opacity-0 group-hover:opacity-100"}`, 10562 children: /* @__PURE__ */ jsxRuntimeExports.jsx(EllipsisVertical, { size: 16 }) 10563 } 10564 ), 10565 isMenuOpen && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute right-0 top-8 w-40 bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-xl shadow-xl z-50 overflow-hidden animate-in fade-in zoom-in-95", onClick: (e) => e.stopPropagation(), children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "py-1", children: [
10566 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10567 "button", 10568 { 10569 onClick: () => { 10570 setIsMenuOpen(false); 10571 onReport(comment.id); 10572 }, 10573 className: "w-full text-left px-4 py-2.5 text-sm text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 flex items-center gap-2 transition-colors", 10574 children: [ 10575 /* @__PURE__ */ jsxRuntimeExports.jsx(Flag, { size: 14 }), 10576 " ", 10577 t("watch.report", "Report") 10578 ] 10579 } 10580 ), 10581 staffModeEnabled && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [ 10582 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-px bg-zinc-200 dark:bg-zinc-800 my-1" }), 10583 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10584 "button", 10585 { 10586 onClick: () => { 10587 setIsMenuOpen(false); 10588 onHide == null ? void 0 : onHide(comment.id); 10589 }, 10590 className: "w-full text-left px-4 py-2.5 text-sm text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 flex items-center gap-2 transition-colors font-bold", 10591 children: [
10592 /* @__PURE__ */ jsxRuntimeExports.jsx(Flag, { size: 14, className: "text-red-500" }), 10593 " ", 10594 t("watch.hide", "Hide Comment") 10595 ] 10596 } 10597 ), 10598 /* @__PURE__ */ jsxRuntimeExports.jsxs( 10599 "button", 10600 { 10601 onClick: () => { 10602 setIsMenuOpen(false); 10603 onGovern == null ? void 0 : onGovern(comment.userId); 10604 }, 10605 className: "w-full text-left px-4 py-2.5 text-sm text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 flex items-center gap-2 transition-colors font-bold", 10606 children: [ 10607 /* @__PURE__ */ jsxRuntimeExports.jsx(ShieldAlert, { size: 14, className: "text-indigo-500" }), 10608 " ", 10609 t("watch.governance", "Govern User") 10610 ] 10611 } 10612 ) 10613 ] }) 10614 ] }) }) 10615 ] }) 10616 ] }); 10617}; 10618const CommentSection = ({ trackId, comments, setComments, currentUser, onReport }) => { 10619 const { t } = useTranslation(); 10620 const { staffModeEnabled } = useAdmin(); 10621 const [newComment, setNewComment] = reactExports.useState(""); 10622 const [governanceUser, setGovernanceUser] = reactExports.useState(null); 10623 const [isGovernModalOpen, setIsGovernModalOpen] = reactExports.useState(false); 10624 const handleGovern = async (userId) => { 10625 try { 10626 const user = await api.getAdminUser(userId); 10627 setGovernanceUser(user); 10628 setIsGovernModalOpen(true); 10629 } catch (e) { 10630 console.error(e); 10631 alert("Failed to load user details"); 10632 } 10633 }; 10634 const handlePostComment = async (e) => { 10635 e.preventDefault(); 10636 if (!newComment.trim() || !currentUser) return; 10637 const comment = await api.postComment(trackId, currentUser.id, newComment); 10638 setComments([comment, ...comments]); 10639 setNewComment(""); 10640 }; 10641 const handleVote = async (commentId, value) => { 10642 if (!currentUser) return; 10643 try { 10644 const { likes } = await api.voteComment(commentId, currentUser.id, value); 10645 setComments(comments.map((c) => c.id === commentId ? { ...c, likes } : c)); 10646 } catch (e) { 10647 console.error("Failed to vote", e); 10648 } 10649 }; 10650 const handlePostReply = async (parentId, text) => { 10651 if (!currentUser) return; 10652 try { 10653 const reply = await api.postComment(trackId, currentUser.id, text, parentId); 10654 setComments([reply, ...comments]); 10655 } catch (e) { 10656 console.error("Failed to post reply", e); 10657 } 10658 }; 10659 const handleHideComment = async (commentId) => { 10660 if (!staffModeEnabled) return; 10661 if (!confirm("Are you sure you want to hide this comment? This will also penalize the user's trust score.")) return; 10662 try { 10663 await api.toggleCommentVisibility(commentId); 10664 setComments(comments.filter((c) => c.id !== commentId)); 10665 } catch (e) { 10666 console.error(e); 10667 alert("Failed to moderate comment"); 10668 } 10669 }; 10670 const parentComments = comments.filter((c) => !c.parentId); 10671 const getRepliesForParent = (parentId) => comments.filter((c) => c.parentId === parentId).sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()); 10672 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-8 pt-6 border-t border-zinc-200 dark:border-zinc-800", children: [ 10673 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-xl font-bold text-zinc-900 dark:text-white mb-4", children: [ 10674 comments.length, 10675 " ", 10676 t("watch.comments", "Comments") 10677 ] }), 10678 currentUser && currentUser.id !== -1 ? currentUser.isJailed || currentUser.suspendedUntil && new Date(currentUser.suspendedUntil) > /* @__PURE__ */ new Date() ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 bg-amber-50 dark:bg-amber-900/10 rounded-lg mb-8 border border-amber-200 dark:border-amber-800/30", children: [
10679 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-amber-700 dark:text-amber-400 font-bold mb-1", children: "âï¸ Account Restricted" }), 10680 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-amber-600 dark:text-amber-500", children: "Your account is currently under review by the Hamie Tribunal. You can browse but cannot post comments." }) 10681 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-4 mb-8", children: [ 10682 /* @__PURE__ */ jsxRuntimeExports.jsx(UserAvatar, { src: currentUser.avatar, name: currentUser.username, size: 40 }), 10683 /* @__PURE__ */ jsxRuntimeExports.jsxs("form", { className: "flex-1", onSubmit: handlePostComment, children: [ 10684 /* @__PURE__ */ jsxRuntimeExports.jsx( 10685 "input", 10686 { 10687 type: "text", 10688 value: newComment, 10689 onChange: (e) => setNewComment(e.target.value), 10690 placeholder: t("watch.addComment", "Add a comment..."), 10691 className: "w-full bg-transparent border-b border-zinc-300 dark:border-zinc-700 py-2 focus:border-indigo-500 outline-none text-zinc-900 dark:text-white transition-colors" 10692 } 10693 ),
10694 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end mt-2 gap-2", children: [ 10695 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "button", onClick: () => setNewComment(""), className: "px-4 py-2 rounded-full text-sm font-bold text-zinc-600 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800", children: t("common.cancel", "Cancel") }), 10696 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { type: "submit", disabled: !newComment, className: "px-4 py-2 rounded-full text-sm font-bold bg-indigo-600 hover:bg-indigo-700 text-white disabled:opacity-50", children: t("watch.commentBtn", "Comment") }) 10697 ] }) 10698 ] }) 10699 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 bg-zinc-100 dark:bg-zinc-800/50 rounded-lg text-center mb-8 border border-zinc-200 dark:border-zinc-700 border-dashed", children: /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500", children: t("watch.signInToComment", "Please sign in or register to join the conversation.") }) }), 10700 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-8", children: parentComments.map((comment) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-4", children: [
10701 /* @__PURE__ */ jsxRuntimeExports.jsx( 10702 CommentItem, 10703 { 10704 comment, 10705 currentUser, 10706 onReport, 10707 onVote: handleVote, 10708 onReply: handlePostReply, 10709 onHide: handleHideComment, 10710 onGovern: handleGovern, 10711 t 10712 } 10713 ), 10714 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "space-y-4", children: getRepliesForParent(comment.id).map((reply) => /* @__PURE__ */ jsxRuntimeExports.jsx( 10715 CommentItem, 10716 { 10717 comment: reply, 10718 currentUser, 10719 onReport, 10720 onVote: handleVote, 10721 onHide: handleHideComment, 10722 onGovern: handleGovern, 10723 t, 10724 isReply: true 10725 }, 10726 reply.id 10727 )) }) 10728 ] }, comment.id)) }), 10729 governanceUser && /* @__PURE__ */ jsxRuntimeExports.jsx( 10730 GovernanceModal, 10731 { 10732 user: governanceUser, 10733 isOpen: isGovernModalOpen, 10734 onClose: () => setIsGovernModalOpen(false) 10735 } 10736 ) 10737 ] }); 10738}; 10739const cleanGluedWords = (txt) => { 10740 return splitGluedWords(txt); 10741}; 10742const shouldAddSpace = (prev, curr) => { 10743 if (!prev || !curr) return false; 10744 if (prev.endsWith(" ") || prev.endsWith("Â ") || curr.startsWith(" ") || curr.startsWith("Â ")) return false; 10745 if (shouldJoinSyllables(prev, curr)) return false; 10746 return true; 10747}; 10748const isCapitalizedStart = (txt) => { 10749 const clean = txt.trim(); 10750 if (!/^[A-Z]/.test(clean)) return false; 10751 if (clean.length === 1) return false; 10752 if (clean.length === 2 && /^[A-Z][\.,!\?;:]/.test(clean)) return false; 10753 return true; 10754}; 10755const groupLyricsIntoLines = (lyrics) => { 10756 const preprocessed = []; 10757 for (let i = 0; i < lyrics.length; i++) { 10758 const syl = lyrics[i]; 10759 const cleaned = cleanGluedWords(syl.txt); 10760 const parts = cleaned.split(" "); 10761 if (parts.length > 1) { 10762 parts.forEach((part, idx) => { 10763 preprocessed.push({ 10764 t: syl.t + idx * 0.01, 10765 txt: part 10766 }); 10767 }); 10768 } else { 10769 preprocessed.push({ 10770 t: syl.t, 10771 txt: cleaned 10772 }); 10773 } 10774 } 10775 const lines = []; 10776 let currentLine = []; 10777 for (let i = 0; i < preprocessed.length; i++) { 10778 const syl = preprocessed[i]; 10779 let txt = syl.txt; 10780 const isNewLine = txt.startsWith("/") || txt.startsWith("\\") || txt.includes("\n") || txt.includes("\r"); 10781 if (isNewLine && currentLine.length > 0) { 10782 lines.push({ 10783 startTime: currentLine[0].t, 10784 endTime: currentLine[currentLine.length - 1].t, 10785 syllables: currentLine 10786 }); 10787 currentLine = []; 10788 } 10789 txt = txt.replace(/^[\/\\]/, "").replace(/[\n\r]/g, "").replace(/`/g, " "); 10790 if (currentLine.length > 0) { 10791 const prevSyl = currentLine[currentLine.length - 1]; 10792 if (shouldAddSpace(prevSyl.txt, txt)) { 10793 txt = " " + txt; 10794 } 10795 } 10796 currentLine.push({ t: syl.t, txt }); 10797 if (i < preprocessed.length - 1) { 10798 const nextSyl = preprocessed[i + 1]; 10799 const cleanNextTxt = nextSyl.txt.replace(/^[\/\\]/, "").replace(/[\n\r]/g, "").replace(/`/g, " "); 10800 const hasGap = nextSyl.t - syl.t > 2.5; 10801 const isCapitalized = isCapitalizedStart(cleanNextTxt); 10802 const isIWord = /^[iI]$/.test(cleanNextTxt.trim()); 10803 const shouldBreakOnCapital = isCapitalized && !isIWord && currentLine.length >= 2; 10804 const isWordBoundary = cleanNextTxt.startsWith(" ") || shouldAddSpace(txt, cleanNextTxt); 10805 const isTooLong = currentLine.length >= 28 && isWordBoundary; 10806 if ((hasGap || shouldBreakOnCapital || isTooLong) && currentLine.length > 0) { 10807 lines.push({ 10808 startTime: currentLine[0].t, 10809 endTime: currentLine[currentLine.length - 1].t, 10810 syllables: currentLine 10811 }); 10812 currentLine = []; 10813 } 10814 } 10815 } 10816 if (currentLine.length > 0) { 10817 lines.push({ 10818 startTime: currentLine[0].t, 10819 endTime: currentLine[currentLine.length - 1].t, 10820 syllables: currentLine 10821 }); 10822 } 10823 return lines; 10824}; 10825const WatchPage = ({ 10826 activeTrack, 10827 currentUser, 10828 isPlaying, 10829 setIsPlaying, 10830 toggleLike, 10831 likedMidis, 10832 subscribe, 10833 isSubscribed, 10834 handleChannelSelect, 10835 handleSelectTrack, 10836 toggleWatchLater, 10837 watchLater, 10838 allTracks, 10839 onTagSelect, 10840 onArtistSelect, 10841 currentTime, 10842 duration, 10843 onAddToQueue, 10844 onAddToPlaylist, 10845 onAuthRequested, 10846 audioMode,
10847 setAudioMode, 10848 autoPlayCountdown, 10849 onCancelAutoPlay, 10850 onPlayNext, 10851 nextTrack, 10852 isBuffering, 10853 setIsBuffering, 10854 onSeek, 10855 playbackRate, 10856 isGameMode, 10857 setIsGameMode, 10858 audioQuality, 10859 pitchSemitones, 10860 theme, 10861 onUserUpdate 10862}) => { 10863 var _a, _b, _c, _d, _e, _f; 10864 const { t, i18n } = useTranslation(); 10865 const { country } = useSettings(); 10866 const { staffModeEnabled } = useAdmin(); 10867 const isAdmin = (currentUser == null ? void 0 : currentUser.role) === "ADMIN"; 10868 const isEmbed = reactExports.useMemo(() => { 10869 if (typeof window !== "undefined") { 10870 return new URLSearchParams(window.location.search).get("embed") === "1"; 10871 } 10872 return false; 10873 }, []); 10874 const adminLog = reactExports.useCallback((...args) => { 10875 if (isAdmin) console.log(...args); 10876 }, [isAdmin]); 10877 const [rawLibraries, setRawLibraries] = reactExports.useState([]); 10878 const resolveSfToLoad = reactExports.useCallback((sfToLoad) => { 10879 var _a2; 10880 let finalSf = sfToLoad; 10881 const libEntry = rawLibraries.find((l) => l.id === sfToLoad || l.baseId === sfToLoad); 10882 if (libEntry && libEntry.autoRoute) { 10883 const uniqueCount = new Set((_a2 = activeTrack.instruments) == null ? void 0 : _a2.map((i) => i.program)).size || 1; 10884 const threshold = libEntry.autoRouteThreshold || 15; 10885 const sfBaseName = finalSf.replace(/\.h?sf[23]$/i, "").replace(/_JS$/i, ""); 10886 if (uniqueCount > threshold) { 10887 finalSf = sfBaseName + ".sf3"; 10888 console.log(`[WatchPage] Smart Auto-Routing: ${uniqueCount} insts > ${threshold} -> Selected ${finalSf}`); 10889 } else { 10890 finalSf = sfBaseName + "_HSF3Bank"; 10891 console.log(`[WatchPage] Smart Auto-Routing: ${uniqueCount} insts <= ${threshold} -> Selected ${finalSf}`); 10892 } 10893 } 10894 return finalSf; 10895 }, [rawLibraries, activeTrack.instruments]); 10896 const [eventsInstrument, setEventsInstrument] = reactExports.useState(null); 10897 const [instrumentOverrides, setInstrumentOverrides] = reactExports.useState({}); 10898 const [fallbackInstruments, setFallbackInstruments] = reactExports.useState([]); 10899 reactExports.useEffect(() => { 10900 const handler = (e) => setFallbackInstruments(e.detail); 10901 window.addEventListener("liveFallbackTracksLoaded", handler); 10902 return () => window.removeEventListener("liveFallbackTracksLoaded", handler); 10903 }, []); 10904 reactExports.useEffect(() => { 10905 if (!activeTrack) return; 10906 const sourceInstruments = activeTrack.instruments || []; 10907 const isMissingPrograms = sourceInstruments.length > 0 && sourceInstruments.every((i) => i.program === null || i.name === "Instrument None"); 10908 if (isMissingPrograms && fallbackInstruments.length === 0) { 10909 const eventsSources = [ 10910 activeTrack.cleanMidiPath, 10911 activeTrack.midiPath 10912 ].filter(Boolean); 10913 if (eventsSources.length > 0) { 10914 parseMidiInstruments(eventsSources).then(({ tracks, duration: duration2 }) => { 10915 var _a2; 10916 setFallbackInstruments(tracks); 10917 if (duration2 > 0 && (!((_a2 = activeTrack.midiMetadata) == null ? void 0 : _a2.duration) || activeTrack.midiMetadata.duration === 0)) { 10918 window.dispatchEvent(new CustomEvent("setTrackDuration", { detail: duration2 })); 10919 } 10920 }).catch((err) => { 10921 console.error("[WatchPage] Eager parseMidiInstruments failed:", err); 10922 }); 10923 } 10924 } 10925 }, [activeTrack, fallbackInstruments.length]); 10926 const [viewMode, setViewMode] = reactExports.useState(() => { 10927 var _a2, _b2, _c2; 10928 const hasPercussion = (_a2 = activeTrack == null ? void 0 : activeTrack.instruments) == null ? void 0 : _a2.some((inst) => inst.midiChannel === 9 && (inst.notesCount > 0 || inst.notesCount === void 0)); 10929 const hasMelodicTracks = (_b2 = activeTrack == null ? void 0 : activeTrack.instruments) == null ? void 0 : _b2.some((inst) => inst.midiChannel !== 9 && (inst.notesCount > 0 || inst.notesCount === void 0)); 10930 const isPercussionOnly = hasPercussion && !hasMelodicTracks; 10931 if (typeof window !== "undefined") { 10932 const params = new URLSearchParams(window.location.search); 10933 const mode = params.get("mode") || params.get("viewMode"); 10934 if (mode === "score" || mode === "melody" || mode === "piano" || mode === "waveform" || mode === "karaoke" || mode === "eq" || mode === "orchestra" || mode === "percussion" || mode === "cinematic" || mode === "premium_orchestra" || mode === "taiko") { 10935 if (mode === "piano" && isPercussionOnly) { 10936 return "percussion"; 10937 } 10938 const isStaff = staffModeEnabled || window.location.search.includes("ai_admin=1"); 10939 if (!isStaff && (mode === "orchestra" || mode === "cinematic")) { 10940 return "premium_orchestra"; 10941 } 10942 return mode; 10943 } 10944 } 10945 if (isPercussionOnly) return "percussion"; 10946 if (isSlowMobile()) return "waveform"; 10947 if (((_c2 = activeTrack == null ? void 0 : activeTrack.midiMetadata) == null ? void 0 : _c2.lyrics) && activeTrack.midiMetadata.lyrics.length > 0) return "karaoke"; 10948 const hasPianoRoll = !!(activeTrack == null ? void 0 : activeTrack.pianoRollPath) || (activeTrack == null ? void 0 : activeTrack.isLight); 10949 if (hasPianoRoll) return "piano"; 10950 return "waveform"; 10951 }); 10952 const [mixerMode, setMixerMode] = reactExports.useState("list"); 10953 const [comments, setComments] = reactExports.useState([]); 10954 const [rawPianoRollData, setRawPianoRollData] = reactExports.useState(null); 10955 const [melodyData, setMelodyData] = reactExports.useState(void 0); 10956 const [melodySource, setMelodySource] = reactExports.useState("standard");
10957 const [hiddenTracks, setHiddenTracks] = reactExports.useState(/* @__PURE__ */ new Set()); 10958 const [lyricsExpanded, setLyricsExpanded] = reactExports.useState(false); 10959 const [forceShowVisualizer, setForceShowVisualizer] = reactExports.useState(false); 10960 const [trackVolumes, setTrackVolumes] = reactExports.useState({}); 10961 const [trackPans, setTrackPans] = reactExports.useState({}); 10962 const [localMutedIndices, setLocalMutedIndices] = reactExports.useState(/* @__PURE__ */ new Set()); 10963 const [playAlongMutedTracks, setPlayAlongMutedTracks] = reactExports.useState(/* @__PURE__ */ new Set()); 10964 const [previousSoundfont, setPreviousSoundfont] = reactExports.useState(null); 10965 const [previousAudioMode, setPreviousAudioMode] = reactExports.useState(null); 10966 const [soloedIndex, setSoloedIndex] = reactExports.useState(null); 10967 const { 10968 setActiveTrack, 10969 trackStates, 10970 setTrackStates, 10971 isLiveSynthFallback, 10972 setIsLiveSynthFallback, 10973 isSynthReady, 10974 hasStartedLoading, 10975 setHasStartedLoading, 10976 soundfont, 10977 setSoundfont, 10978 engineOrder 10979 } = usePlayer(); 10980 const isPlayingRef = reactExports.useRef(isPlaying); 10981 const currentTimeRef = reactExports.useRef(currentTime); 10982 const lastLoadedConfigRef = reactExports.useRef(""); 10983 const lastSyncTrackIdRef = reactExports.useRef(null); 10984 reactExports.useEffect(() => { 10985 isPlayingRef.current = isPlaying; 10986 }, [isPlaying]); 10987 reactExports.useEffect(() => { 10988 currentTimeRef.current = currentTime; 10989 }, [currentTime]); 10990 const hasSynthError = reactExports.useMemo(() => { 10991 if (isLiveSynthFallback) { 10992 return Object.values(trackStates).some((status) => status && status.error); 10993 } 10994 if (!activeTrack.instruments || activeTrack.instruments.length === 0) return false; 10995 return activeTrack.instruments.some((inst, idx) => { 10996 if (!inst.stemPath) return false; 10997 const status = trackStates[idx]; 10998 return status && status.error; 10999 }); 11000 }, [trackStates, activeTrack.instruments, isLiveSynthFallback]); 11001 const [tags, setTags] = reactExports.useState([]); 11002 const [isTagModalOpen, setIsTagModalOpen] = reactExports.useState(false); 11003 const [isEditModalOpen, setIsEditModalOpen] = reactExports.useState(false); 11004 const [isReuploadModalOpen, setIsReuploadModalOpen] = reactExports.useState(false); 11005 const [isReportModalOpen, setIsReportModalOpen] = reactExports.useState(false); 11006 const [localBgUrl, setLocalBgUrl] = reactExports.useState(null); 11007 const [isBgModalOpen, setIsBgModalOpen] = reactExports.useState(false); 11008 const [bgUploadFile, setBgUploadFile] = reactExports.useState(null); 11009 const [bgUploadError, setBgUploadError] = reactExports.useState(null); 11010 const [bgUploadStatus, setBgUploadStatus] = reactExports.useState("idle"); 11011 const [hasAcceptedCopyright, setHasAcceptedCopyright] = reactExports.useState(false); 11012 reactExports.useEffect(() => { 11013 setLocalBgUrl((activeTrack == null ? void 0 : activeTrack.backgroundImageUrl) || null); 11014 setBgUploadStatus("idle"); 11015 setBgUploadFile(null); 11016 setBgUploadError(null); 11017 setHasAcceptedCopyright(false); 11018 }, [activeTrack]); 11019 const handleBackgroundUpload = async () => { 11020 if (!bgUploadFile) return; 11021 if (!hasAcceptedCopyright) { 11022 setBgUploadError("You must accept the copyright terms to upload."); 11023 return; 11024 } 11025 setBgUploadStatus("uploading"); 11026 setBgUploadError(null); 11027 const formData = new FormData(); 11028 formData.append("background", bgUploadFile); 11029 try { 11030 const response = await fetch(`/api/content/tracks/${activeTrack.id}/background`, { 11031 method: "POST", 11032 body: formData 11033 }); 11034 if (!response.ok) { 11035 const errData = await response.json(); 11036 throw new Error(errData.error || "Upload failed"); 11037 } 11038 const data = await response.json(); 11039 setLocalBgUrl(data.backgroundImageUrl); 11040 setBgUploadStatus("success"); 11041 setTimeout(() => { 11042 setIsBgModalOpen(false); 11043 }, 1e3); 11044 } catch (err) { 11045 setBgUploadStatus("idle"); 11046 setBgUploadError(err.message || "An error occurred during upload."); 11047 } 11048 };
11049 const handleBackgroundRemove = async () => { 11050 if (!confirm("Are you sure you want to remove the custom background and revert to defaults?")) return; 11051 try { 11052 const response = await fetch(`/api/content/tracks/${activeTrack.id}/background`, { 11053 method: "DELETE" 11054 }); 11055 if (!response.ok) { 11056 const errData = await response.json(); 11057 throw new Error(errData.error || "Failed to remove background"); 11058 } 11059 setLocalBgUrl(null); 11060 alert("Custom background removed successfully."); 11061 } catch (err) { 11062 alert(err.message || "Failed to remove background."); 11063 } 11064 }; 11065 const [isTheaterMode, setIsTheaterMode] = reactExports.useState(() => { 11066 if (typeof window === "undefined") return false; 11067 const pref = localStorage.getItem("theaterModePreference"); 11068 if (pref === "theater") return true; 11069 if (pref === "default") return false; 11070 return window.innerWidth > 1400; 11071 }); 11072 const handleSetTheaterMode = (isTheater) => { 11073 setIsTheaterMode(isTheater); 11074 localStorage.setItem("theaterModePreference", isTheater ? "theater" : "default"); 11075 }; 11076 const [selectedTrackIndex, setSelectedTrackIndex] = reactExports.useState("master"); 11077 const [preRenderedSf, setPreRenderedSf] = reactExports.useState("GeneralUser-GS"); 11078 const [defaultLiveSf, setDefaultLiveSf] = reactExports.useState("MusyngKite"); 11079 const [soundfontMetadata, setSoundfontMetadata] = reactExports.useState({}); 11080 reactExports.useEffect(() => { 11081 if (currentUser == null ? void 0 : currentUser.defaultSoundfont) { 11082 setSoundfont(currentUser.defaultSoundfont); 11083 } else { 11084 setSoundfont("GeneralUser-GS"); 11085 } 11086 }, [currentUser, setSoundfont]); 11087 const displayedLibraries = reactExports.useMemo(() => { 11088 const isProcessed = activeTrack.status === "completed"; 11089 const preSfBase = preRenderedSf.replace(/\.h?sf[23]$/, ""); 11090 if (!isProcessed) { 11091 const preSfLib = rawLibraries.find((l) => l.id === preRenderedSf || l.baseId === preSfBase); 11092 const otherLibs = rawLibraries.filter((l) => l.id !== preRenderedSf && l.baseId !== preSfBase); 11093 const livePreSfLib = preSfLib ? { 11094 ...preSfLib, 11095 name: preSfBase.replace(/_/g, " "), 11096 desc: "Balanced live soundfont", 11097 isPreRendered: false, 11098 access: "free" 11099 } : { 11100 id: preRenderedSf, 11101 baseId: preSfBase, 11102 name: preSfBase.replace(/_/g, " "), 11103 desc: "Balanced live soundfont", 11104 isPreRendered: false, 11105 access: "free" 11106 }; 11107 return [livePreSfLib, ...otherLibs]; 11108 } else { 11109 const otherLibs = rawLibraries.filter((l) => l.id !== preRenderedSf && l.baseId !== preSfBase); 11110 return [ 11111 { 11112 id: "pre-rendered", 11113 name: preSfBase.replace(/_/g, " ") + " (Pre-rendered)", 11114 desc: "High-performance pre-rendered Opus stems", 11115 isPreRendered: true, 11116 access: "free" 11117 }, 11118 ...otherLibs 11119 ]; 11120 } 11121 }, [rawLibraries, activeTrack.status, preRenderedSf]); 11122 const handleSoundfontChange = async (newSf) => { 11123 const selectedLib = displayedLibraries.find((l) => l.id === newSf); 11124 const isProLibrary = (selectedLib == null ? void 0 : selectedLib.access) === "pro"; 11125 if (isProLibrary && !isPro(currentUser)) { 11126 console.warn("[HamieSecurity] Access Denied: This soundfont is for Pro users only."); 11127 return; 11128 } 11129 const isSfBinaryFile = newSf.toLowerCase().endsWith(".sf2") || newSf.toLowerCase().endsWith(".sf3") || newSf.toLowerCase().endsWith(".hsf3") || newSf.toLowerCase().endsWith("_hsf3bank"); 11130 if (isSfBinaryFile && typeof AudioWorkletNode === "undefined") { 11131 alert( 11132 "â ï¸ Browser Security Limitation:\n\nYour browser blocks WebAssembly SoundFonts (.sf2/.sf3) when accessing the site over unsecure HTTP.\n\nTo resolve this, please do one of the following:\n1. Select a JS soundfont option, which works perfectly on plain HTTP.\n2. Access the site via a secure HTTPS connection.\n3. Acce
11132ss via 'http://localhost:3002' if you are running/testing locally (browsers treat localhost as secure)." 11133 ); 11134 } 11135 console.log("[WatchPage] handleSoundfontChange:", newSf); 11136 setSoundfont(newSf); 11137 if (audioMode !== "synth" && audioMode !== "wasmsynth") { 11138 setAudioMode(isSfBinaryFile ? "wasmsynth" : "synth"); 11139 } 11140 try { 11141 if (currentUser && currentUser.id !== -1) { 11142 if (onUserUpdate) { 11143 await onUserUpdate({ defaultSoundfont: newSf }); 11144 } else { 11145 await api.updateUser(currentUser.id, { defaultSoundfont: newSf }); 11146 } 11147 } 11148 } catch (e) { 11149 console.warn("Failed to save personal soundfont default", e); 11150 } 11151 }; 11152 reactExports.useEffect(() => { 11153 const loadDefaults = async () => { 11154 var _a2; 11155 try { 11156 const [sfData, preDef, metaSet, defaultLiveSet] = await Promise.all([ 11157 api.getSoundfonts(), 11158 api.getSystemSetting("PRE_RENDERED_SOUNDFONT"), 11159 api.getSystemSetting("SOUNDFONT_METADATA"), 11160 api.getSystemSetting("DEFAULT_LIVE_SOUNDFONT") 11161 ]); 11162 const meta = sfData.metadata || (metaSet == null ? void 0 : metaSet.value) || {}; 11163 setSoundfontMetadata(meta); 11164 const preSf = sfData.preRendered || (preDef == null ? void 0 : preDef.value) || "GeneralUser-GS"; 11165 const preSfBase = preSf.replace(/\.h?sf[23]$/, ""); 11166 setPreRenderedSf(preSf); 11167 let managerSf = ((_a2 = sfData.active) == null ? void 0 : _a2.filename) || sfData.active; 11168 if (managerSf && typeof managerSf === "string") { 11169 managerSf = managerSf.split("/").pop(); 11170 } 11171 const activeSf = managerSf || (defaultLiveSet == null ? void 0 : defaultLiveSet.value) || "MusyngKite"; 11172 setDefaultLiveSf(activeSf); 11173 const getBaseName = (name) => { 11174 return name.replace(/(_JS|_HSF3Bank|_SF3Bank)(?:_.*)?$/i, "").replace(/(?:_test_\d*)?\.h?sf[23]$/i, ""); 11175 }; 11176 const visibleLibs = (sfData.libraries || []).filter((lib) => { 11177 const libBase = getBaseName(lib.name); 11178 const formatMeta = meta[lib.name] || {}; 11179 const folderMeta = meta[libBase] || {}; 11180 const combinedMeta = { ...folderMeta, ...formatMeta }; 11181 return combinedMeta.visible !== false; 11182 }); 11183 const deduplicatedLibs = []; 11184 const seenAutoRoutes = /* @__PURE__ */ new Set(); 11185 visibleLibs.forEach((lib) => { 11186 const libBase = getBaseName(lib.name); 11187 const folderMeta = meta[libBase] || {}; 11188 if (folderMeta.autoRoute) { 11189 if (!seenAutoRoutes.has(libBase)) { 11190 seenAutoRoutes.add(libBase); 11191 deduplicatedLibs.push({ ...lib, isAutoRouted: true, idOverride: libBase }); 11192 } 11193 } else { 11194 deduplicatedLibs.push(lib); 11195 } 11196 }); 11197 const baseCounts = {}; 11198 deduplicatedLibs.forEach((lib) => { 11199 const libBase = getBaseName(lib.name); 11200 baseCounts[libBase] = (baseCounts[libBase] || 0) + 1; 11201 }); 11202 const processedLibs = deduplicatedLibs.map((lib) => { 11203 const libBase = getBaseName(lib.name); 11204 const isSf2 = lib.name.toLowerCase().endsWith(".sf2") || !!lib.isSf2; 11205 const isSf3 = lib.name.toLowerCase().endsWith(".sf3"); 11206 const isHsf3 = lib.name.toLowerCase().endsWith("_hsf3bank") || lib.name.toLowerCase().endsWith(".hsf3"); 11207 const isPre = lib.name === preSf || libBase === preSfBase; 11208 const formatMeta = meta[lib.name] || {}; 11209 const folderMeta = meta[libBase] || {}; 11210 const combinedMeta = { ...folderMeta, ...formatMeta }; 11211 let displayName = folderMeta.name || combinedMeta.name || libBase.replace(/_/g, " "); 11212 if (baseCounts[libBase] > 1 && !lib.isAutoRouted) { 11213 if (isSf2) displayName += " (SF2)"; 11214 else if (isSf3) displayName += " (SF3)"; 11215 else if (isHsf3) displayName += " (HSF3)"; 11216 else displayName += " (JS)"; 11217 } 11218 let threshold = 15; 11219 let autoRouteEnabled = !!folderMeta.autoRoute; 11220 if (autoRouteEnabled) { 11221 if (folderMeta.autoRouteThresholdOverride != null) { 11222 threshold = folderMeta.autoRouteThresholdOverride; 11223 } else { 11224 const allFormats = sfData.libraries || []; 11225 const sf3Lib = allFormats.find((f) => f.name.toLowerCase() === libBase.toLowerCase() + ".sf3"); 11226 const hsf3Lib = allFormats.find((f) => f.name.toLowerCase() === libBase.toLowerCase() + "_hsf3bank" || f.name.toLowerCase() === libBase.toLowerCase() + ".hsf3"); 11227 const sf3Size = (sf3Lib == null ? void 0 : sf3Lib.size) || 0; 11228 const hsf3TotalSize = (hsf3Lib == null ? void 0 : hsf3Lib.size) || 0; 11229 if (sf3Size > 0 && hsf3TotalSize > 0) { 11230 threshold = Math.floor(sf3Size / (hsf3TotalSize / 128)); 11231 } 11232 } 11233 } 11234 return { 11235 id: lib.idOverride || lib.name, 11236 baseId: libBase, 11237 name: displayName, 11238 desc: folderMeta.desc || combinedMeta.desc || (isPre ? "High-performance pre-rendered Opus stems" : ""), 11239 access: isPre ? "free" : folderMeta.access || combinedMeta.access || "free",
11240 isPreRendered: isPre, 11241 hasLive: !(isSf2 || isSf3 || isHsf3), 11242 autoRoute: autoRouteEnabled, 11243 autoRouteThreshold: threshold 11244 }; 11245 }); 11246 setRawLibraries(processedLibs); 11247 const hasStems = activeTrack.instruments && activeTrack.instruments.some((i) => i.stemPath); 11248 let initialSf = (currentUser == null ? void 0 : currentUser.defaultSoundfont) || (hasStems ? "pre-rendered" : activeSf); 11249 const preNorm = preSf.replace(/\.h?sf[23]$/, ""); 11250 const initialNorm = initialSf.replace(/\.h?sf[23]$/, ""); 11251 console.log("[WatchPage] Initial SF:", initialSf, "Pre-rendered SF:", preSf); 11252 if (initialNorm === preNorm || initialSf === "pre-rendered" || initialSf === preSf) { 11253 console.log("[WatchPage] Normalizing initial soundfont to 'pre-rendered' (Matches system pre-rendered SF:", preSf, ")"); 11254 initialSf = "pre-rendered"; 11255 } 11256 setSoundfont(initialSf); 11257 } catch (e) { 11258 console.warn("Failed to fetch soundfont defaults", e); 11259 } 11260 }; 11261 loadDefaults(); 11262 }, [currentUser]); 11263 reactExports.useEffect(() => { 11264 const resumeAudio = () => { 11265 const audioCtx = synth.getAudioContext(); 11266 if (audioCtx && audioCtx.state === "suspended") { 11267 audioCtx.resume().then(() => { 11268 console.log("[WatchPage] AudioContext resumed via user gesture"); 11269 window.removeEventListener("mousedown", resumeAudio); 11270 window.removeEventListener("keydown", resumeAudio); 11271 window.removeEventListener("touchstart", resumeAudio); 11272 }).catch(() => { 11273 }); 11274 } 11275 }; 11276 window.addEventListener("mousedown", resumeAudio); 11277 window.addEventListener("keydown", resumeAudio); 11278 window.addEventListener("touchstart", resumeAudio); 11279 return () => { 11280 window.removeEventListener("mousedown", resumeAudio); 11281 window.removeEventListener("keydown", resumeAudio); 11282 window.removeEventListener("touchstart", resumeAudio); 11283 }; 11284 }, []); 11285 const isBlocked = isContentBlocked(country, (_a = activeTrack.midiMetadata) == null ? void 0 : _a.blockedRegions); 11286 const { processedInstruments, uniqueNames, colorMap: derivedColorMap } = reactExports.useMemo(() => { 11287 const map = {}; 11288 let sourceInstruments = activeTrack.instruments || []; 11289 const isMissingPrograms = sourceInstruments.length > 0 && sourceInstruments.every((i) => i.program === null || i.name === "Instrument None"); 11290 const hasOnlyInformational = sourceInstruments.length > 0 && sourceInstruments.every((i) => i.name === "Informational Track" || i.notesCount === 0); 11291 if (isMissingPrograms && fallbackInstruments.length > 0) { 11292 sourceInstruments = fallbackInstruments; 11293 } else if ((sourceInstruments.length === 0 || hasOnlyInformational) && rawPianoRollData && rawPianoRollData.length > 0) { 11294 const trackMap = /* @__PURE__ */ new Map(); 11295 rawPianoRollData.forEach((note) => { 11296 const trackIdx = note.trackIdx !== void 0 ? note.trackIdx : note.i; 11297 if (!trackMap.has(trackIdx)) { 11298 trackMap.set(trackIdx, { 11299 trackId: trackIdx, 11300 name: typeof note.i === "string" ? note.i : `Track ${trackIdx}`, 11301 program: note.p || 0, 11302 midiChannel: note.isDrum ? 9 : 0, 11303 notesCount: 1, 11304 stemPath: null 11305 }); 11306 } else { 11307 trackMap.get(trackIdx).notesCount++; 11308 } 11309 }); 11310 sourceInstruments = Array.from(trackMap.values()); 11311 } 11312 const processed = sourceInstruments.map((inst, idx) => { 11313 let name = inst.name; 11314 const overriddenProgram = instrumentOverrides[idx] !== void 0 ? instrumentOverrides[idx] : inst.program; 11315 if (name !== "Informational Track") { 11316 name = getInstrumentName(overriddenProgram, inst.midiChannel); 11317 } 11318 const origProg = inst.program !== null && inst.program !== void 0 ? inst.program : overriddenProgram; 11319 const origFam = inst.midiChannel === 9 ? -1 : Math.floor(origProg / 8); 11320 return { 11321 ...inst, 11322 originalProgram: origProg, 11323 originalFamilyIndex: origFam, 11324 program: overriddenProgram, 11325 displayName: String(name), 11326 originalName: inst.name,
11327 index: idx 11328 }; 11329 }); 11330 const nameCounts = {}; 11331 const uNames = processed.map((inst, idx) => { 11332 const baseName = inst.displayName || getInstrumentName(inst.program, inst.midiChannel) || `Track ${idx + 1}`; 11333 nameCounts[baseName] = (nameCounts[baseName] || 0) + 1; 11334 const uniqueName = nameCounts[baseName] > 1 ? `${baseName} ${nameCounts[baseName]}` : baseName; 11335 return uniqueName; 11336 }); 11337 uNames.forEach((uName, idx) => { 11338 map[uName] = getTrackColor(idx); 11339 }); 11340 return { processedInstruments: processed, uniqueNames: uNames, colorMap: map }; 11341 }, [activeTrack.instruments, i18n.language, rawPianoRollData, instrumentOverrides, fallbackInstruments]); 11342 const pianoRollData = reactExports.useMemo(() => { 11343 if (!rawPianoRollData || !Array.isArray(rawPianoRollData) || rawPianoRollData.length === 0) { 11344 return rawPianoRollData; 11345 } 11346 const musicalInstruments = processedInstruments.filter((inst) => inst.midiChannel !== -1); 11347 const updated = rawPianoRollData.map((note) => { 11348 let mappedIdx = -1; 11349 if (note.trackIdx !== void 0 && note.trackIdx >= 0 && note.trackIdx < musicalInstruments.length) { 11350 const candidate = musicalInstruments[note.trackIdx]; 11351 if (candidate && candidate.midiChannel !== -1) { 11352 const rawName = (note.i || "").replace(/\s*\(\d+\)$/, "").trim().toLowerCase(); 11353 const candName = (candidate.displayName || candidate.name || "").toLowerCase(); 11354 if (musicalInstruments.length === 1 || !note.i || candName.includes(rawName) || rawName.includes(candName)) { 11355 mappedIdx = processedInstruments.findIndex((inst) => inst.trackId === candidate.trackId); 11356 } 11357 } 11358 } 11359 if (mappedIdx === -1 && note.trackIdx !== void 0) { 11360 const foundByTrackId = processedInstruments.findIndex( 11361 (inst) => inst.trackId === note.trackIdx && inst.midiChannel !== -1 11362 ); 11363 if (foundByTrackId !== -1) { 11364 mappedIdx = foundByTrackId; 11365 } 11366 } 11367 if (mappedIdx === -1 && note.i) { 11368 const match = note.i.match(/^(.*) \((\d+)\)$/); 11369 if (match) { 11370 const targetTrackId = parseInt(match[2], 10) - 1; 11371 const baseName = match[1].trim().toLowerCase(); 11372 const foundByTrackId = processedInstruments.findIndex( 11373 (inst) => inst.trackId === targetTrackId && inst.midiChannel !== -1 11374 ); 11375 if (foundByTrackId !== -1) { 11376 mappedIdx = foundByTrackId; 11377 } else { 11378 const directTrackId = parseInt(match[2], 10); 11379 const foundDirect = processedInstruments.findIndex( 11380 (inst) => inst.trackId === directTrackId && inst.midiChannel !== -1 11381 ); 11382 if (foundDirect !== -1) { 11383 mappedIdx = foundDirect; 11384 } else { 11385 const foundByName = musicalInstruments.findIndex( 11386 (inst) => (inst.displayName || inst.name || "").toLowerCase().includes(baseName) 11387 ); 11388 if (foundByName !== -1) { 11389 mappedIdx = processedInstruments.findIndex((inst) => inst.trackId === musicalInstruments[foundByName].trackId); 11390 } 11391 } 11392 } 11393 } 11394 } 11395 if (mappedIdx === -1 && note.i) { 11396 const trackMatch = note.i.match(/^Track\s+(\d+)$/i); 11397 let targetIdx = -1; 11398 if (trackMatch) { 11399 targetIdx = parseInt(trackMatch[1], 10) - 1; 11400 } else if (!isNaN(parseInt(note.i, 10))) { 11401 targetIdx = parseInt(note.i, 10); 11402 } 11403 if (targetIdx >= 0) { 11404 const foundByTrackId = processedInstruments.findIndex( 11405 (inst) => inst.trackId === targetIdx && inst.midiChannel !== -1 11406 ); 11407 if (foundByTrackId !== -1) { 11408 mappedIdx = foundByTrackId; 11409 } else if (targetIdx < musicalInstruments.length) { 11410 const matchedInst = musicalInstruments[targetIdx]; 11411 mappedIdx = processedInstruments.findIndex((inst) => inst.trackId === matchedInst.trackId); 11412 } 11413 } 11414 } 11415 if (mappedIdx === -1 && note.i) { 11416 const instIdx = processedInstruments.findIndex( 11417 (inst) => inst.midiChannel !== -1 && (inst.originalName === note.i || inst.displayName === note.i || inst.name === note.i) 11418 ); 11419 if (instIdx !== -1) { 11420 mappedIdx = instIdx; 11421 } 11422 } 11423 if (mappedIdx === -1 && note.trackIdx !== void 0 && note.trackIdx >= 0 && note.trackIdx < musicalInstruments.length) { 11424 const matchedInst = musicalInstruments[note.trackIdx]; 11425 mappedIdx = processedInstruments.findIndex((inst) => inst.trackId === matchedInst.trackId); 11426 } 11427 if (mappedIdx !== -1) { 11428 const musicalIdx = musicalInstruments.findIndex( 11429 (inst) => inst.trackId === processedInstruments[mappedIdx].trackId 11430 ); 11431 return { 11432 ...note, 11433 i: uniqueNames[mappedIdx] || note.i, 11434 trackIdx: musicalIdx !== -1 ? musicalIdx : mappedIdx !== -1 ? mappedIdx : note.trackIdx !== void 0 ? note.trackIdx : note.i 11435 }; 11436 } 11437 return note; 11438 }); 11439 return [...updated].sort((a, b) => (a.t || 0) - (b.t || 0)); 11440 }, [rawPianoRollData, processedInstruments, uniqueNames]); 11441 const trackVisibility = reactExports.useMemo(() => { 11442 if (viewMode === "melody") { 11443 const melodyTrackNames = new Set((melodyData == null ? void 0 : melodyData.map((note) => note.i)) || []); 11444 if (!melodyData || melodyData.length === 0) return processedInstruments.map(() => true); 11445 return processedInstruments.map((inst) => melodyTrackNames.has(inst.originalName)); 11446 } 11447 return uniqueNames.map((name) => !hiddenTracks.has(name)); 11448 }, [viewMode, melodyData, processedInstruments, uniqueNames, hiddenTracks
11448]); 11449 reactExports.useEffect(() => { 11450 if (!activeTrack.id || uniqueNames.length === 0) return; 11451 const params = new URLSearchParams(window.location.search); 11452 const trackParam = params.get("track") || params.get("trackId") || params.get("trackNumb"); 11453 if (trackParam) { 11454 const targetTrackId = parseInt(trackParam, 10); 11455 const foundInstr = processedInstruments.find( 11456 (inst) => inst.trackId === targetTrackId || inst.index === targetTrackId - 1 || inst.index === targetTrackId 11457 ); 11458 if (foundInstr && foundInstr.index !== void 0) { 11459 const uniqueName = uniqueNames[foundInstr.index]; 11460 const nextHidden = /* @__PURE__ */ new Set(); 11461 uniqueNames.forEach((name) => { 11462 if (name !== uniqueName) { 11463 nextHidden.add(name); 11464 } 11465 }); 11466 setHiddenTracks(nextHidden); 11467 } 11468 } 11469 }, [activeTrack.id, uniqueNames, processedInstruments]); 11470 const [relatedTracks, setRelatedTracks] = reactExports.useState([]); 11471 const [variations, setVariations] = reactExports.useState([]); 11472 const [hasCountedPlay, setHasCountedPlay] = reactExports.useState(false); 11473 reactExports.useEffect(() => { 11474 if (isPlaying && !hasCountedPlay) { 11475 api.recordPlay(activeTrack.id); 11476 setHasCountedPlay(true); 11477 } 11478 }, [isPlaying, hasCountedPlay, activeTrack.id]); 11479 const [hasProcessedClient, setHasProcessedClient] = reactExports.useState(false); 11480 reactExports.useEffect(() => { 11481 setHasProcessedClient(false); 11482 }, [activeTrack.id]); 11483 reactExports.useEffect(() => { 11484 if (activeTrack.status !== "pending" || hasProcessedClient) return; 11485 const runClientProcessing = async () => { 11486 try { 11487 console.log(`[WatchPage] Track #${activeTrack.id} is pending. Spawning client-side processor...`); 11488 setHasProcessedClient(true); 11489 const response = await fetch(`/api/tracks/${activeTrack.id}/midi`); 11490 if (!response.ok) return; 11491 const arrayBuffer = await response.arrayBuffer(); 11492 const worker = new Worker(new URL( 11493 /* @vite-ignore */ 11494 "/assets/midiProcessor.worker-DJEo_dd6.js", 11495 import.meta.url 11496 ), { type: "module" }); 11497 worker.onmessage = async (e) => { 11498 const { status, data, trackId, error } = e.data; 11499 if (status === "success" && trackId === activeTrack.id) { 11500 console.log("[WatchPage] Client processing complete! Hydrating UI and submitting to API..."); 11501 const updatedTrack = { ...activeTrack, instruments: data.instruments, duration: data.duration, midiMetadata: { ...activeTrack.midiMetadata, ...data } }; 11502 handleSelectTrack(updatedTrack); 11503 fetch("/api/worker/submit-client-data", { 11504 method: "POST", 11505 headers: { "Content-Type": "application/json" }, 11506 body: JSON.stringify({ 11507 trackId: activeTrack.id, 11508 ...data 11509 }) 11510 }).catch((err) => console.error("Failed to submit client data:", err)); 11511 } else if (status === "error") { 11512 console.error("[WatchPage] Client worker error:", error); 11513 } 11514 worker.terminate(); 11515 }; 11516 worker.postMessage({ trackId: activeTrack.id, midiBuffer: arrayBuffer }, [arrayBuffer]); 11517 } catch (e) { 11518 console.error("[WatchPage] Failed to run client processing:", e); 11519 } 11520 }; 11521 runClientProcessing(); 11522 }, [activeTrack.id, activeTrack.status, hasProcessedClient, handleSelectTrack]); 11523 reactExports.useEffect(() => { 11524 const isUnprocessed = activeTrack.status !== "completed" && activeTrack.status !== "failed"; 11525 if (!isUnprocessed) return; 11526 console.log(`[WatchPage] Track #${activeTrack.id} is unprocessed (status: ${activeTrack.status}). Starting polling for status updates...`); 11527 let timerId; 11528 const poll = () => { 11529 api.getTrack(String(activeTrack.id)).then((updated) => { 11530 var _a2, _b2; 11531 if (!updated) return; 11532 const hasNewInsts = updated.instruments && Array.isArray(updated.instruments) && updated.instruments.length > 0 && (!activeTrack.instruments || !Array.isArray(activeTrack.instruments) || activeTrack.instruments.length === 0); 11533 const hasNewPianoRoll = !!updated.pianoRollPath && !activeTrack.pianoRollPath; 11534 const hasNewMelody = !!updated.melodyPath && !activeTrack.melodyPath;
11535 const hasNewLyrics = ((_a2 = updated.midiMetadata) == null ? void 0 : _a2.lyrics) && updated.midiMetadata.lyrics.length > 0 && (!((_b2 = activeTrack.midiMetadata) == null ? void 0 : _b2.lyrics) || activeTrack.midiMetadata.lyrics.length === 0); 11536 const hasNewOpus = !!(updated.opusPath || updated.mp3PathHigh) && !(activeTrack.opusPath || activeTrack.mp3PathHigh); 11537 const statusChanged = updated.status !== activeTrack.status; 11538 if (hasNewInsts || hasNewPianoRoll || hasNewMelody || hasNewLyrics || hasNewOpus || statusChanged) { 11539 console.log(`[WatchPage] Track #${activeTrack.id} status/data updated (status: ${updated.status}). Updating track state...`); 11540 setActiveTrack((prev) => prev && prev.id === updated.id ? { ...prev, ...updated } : updated); 11541 } 11542 const isFinished = updated.status === "completed" || updated.status === "failed"; 11543 if (!isFinished) { 11544 timerId = setTimeout(poll, 3e3); 11545 } 11546 }).catch((err) => { 11547 console.error("[WatchPage] Polling error:", err); 11548 timerId = setTimeout(poll, 5e3); 11549 }); 11550 }; 11551 timerId = setTimeout(poll, 3e3); 11552 return () => { 11553 if (timerId) clearTimeout(timerId); 11554 }; 11555 }, [activeTrack.id, activeTrack.status, activeTrack.instruments, activeTrack.pianoRollPath, activeTrack.melodyPath, activeTrack.opusPath, activeTrack.mp3PathHigh, handleSelectTrack]); 11556 reactExports.useEffect(() => { 11557 var _a2, _b2, _c2; 11558 api.getComments(activeTrack.id).then(setComments); 11559 api.getRelatedTracks(activeTrack.id).then(setRelatedTracks); 11560 api.getVariations(activeTrack.id).then(setVariations); 11561 api.recordHistory(activeTrack.id, (currentUser == null ? void 0 : currentUser.id) || -1); 11562 const normalizedTags = activeTrack.tags.map( 11563 (t2) => typeof t2 === "string" ? { id: Math.random(), name: t2, slug: t2.toLowerCase(), type: "TAG", translations: {}, score: 10, source: "UPLOADER" } : t2 11564 ); 11565 Promise.resolve().then(() => setTags(normalizedTags)); 11566 setTrackStates({}); 11567 setHasStartedLoading(false); 11568 lastLoadedConfigRef.current = ""; 11569 setRawPianoRollData(null); 11570 synth.stop(); 11571 const vols = {}; 11572 (activeTrack.instruments || []).forEach((_, idx) => { 11573 vols[idx] = 1; 11574 }); 11575 Promise.resolve().then(() => { 11576 setTrackVolumes(vols); 11577 setHiddenTracks(/* @__PURE__ */ new Set()); 11578 setLocalMutedIndices(/* @__PURE__ */ new Set()); 11579 setPlayAlongMutedTracks(/* @__PURE__ */ new Set()); 11580 setHasCountedPlay(false); 11581 }); 11582 const hasLyrics = ((_a2 = activeTrack == null ? void 0 : activeTrack.midiMetadata) == null ? void 0 : _a2.lyrics) && activeTrack.midiMetadata.lyrics.length > 0; 11583 const hasPercussion = (_b2 = activeTrack == null ? void 0 : activeTrack.instruments) == null ? void 0 : _b2.some((inst) => inst.midiChannel === 9 && (inst.notesCount > 0 || inst.notesCount === void 0)); 11584 const hasMelodicTracks = (_c2 = activeTrack == null ? void 0 : activeTrack.instruments) == null ? void 0 : _c2.some((inst) => inst.midiChannel !== 9 && (inst.notesCount > 0 || inst.notesCount === void 0)); 11585 const isPercussionOnly = hasPercussion && !hasMelodicTracks; 11586 let initialMode = null; 11587 if (typeof window !== "undefined") { 11588 const params = new URLSearchParams(window.location.search); 11589 const mode = params.get("mode") || params.get("viewMode"); 11590 if (mode === "score" || mode === "melody" || mode === "piano" || mode === "waveform" || mode === "karaoke" || mode === "eq" || mode === "orchestra" || mode === "percussion" || mode === "cinematic" || mode === "premium_orchestra" || mode === "taiko") { 11591 const isStaff = staffModeEnabled || window.location.search.includes("ai_admin=1"); 11592 if (mode === "piano" && isPercussionOnly) { 11593 initialMode = "percussion"; 11594 } else if (!isStaff && (mode === "orchestra" || mode === "cinematic")) { 11595 initialMode = "premium_orchestra"; 11596 } else { 11597 initialMode = mode; 11598 } 11599 } 11600 } 11601 if (initialMode) { 11602 setViewMode(initialMode); 11603 } else if (isPercussionOnly) {
11604 setViewMode("percussion"); 11605 } else if (isSlowMobile()) { 11606 setViewMode("waveform"); 11607 } else { 11608 const hasPianoRoll = !!(activeTrack == null ? void 0 : activeTrack.pianoRollPath) || (activeTrack == null ? void 0 : activeTrack.isLight); 11609 setViewMode(hasLyrics ? "karaoke" : hasPianoRoll ? "piano" : "waveform"); 11610 } 11611 setForceShowVisualizer(false); 11612 return () => { 11613 synth.stop(); 11614 }; 11615 }, [activeTrack.id, staffModeEnabled]); 11616 reactExports.useEffect(() => { 11617 let isCurrent = true; 11618 const targetRollPath = activeTrack.pianoRollPath; 11619 if (targetRollPath) { 11620 fetch(targetRollPath).then((res) => { 11621 if (!res.ok) throw new Error("HTTP " + res.status); 11622 return res.json(); 11623 }).then((data) => { 11624 if (!isCurrent) return; 11625 setRawPianoRollData(data); 11626 }).catch((err) => { 11627 console.error("Failed to load piano roll data", err); 11628 if (isCurrent) setRawPianoRollData([]); 11629 }); 11630 } else if (!activeTrack.isLight) { 11631 Promise.resolve().then(() => { 11632 if (isCurrent) setRawPianoRollData([]); 11633 }); 11634 } 11635 return () => { 11636 isCurrent = false; 11637 }; 11638 }, [activeTrack.id, activeTrack.pianoRollPath, activeTrack.isLight]); 11639 reactExports.useEffect(() => { 11640 if (typeof window !== "undefined") { 11641 const params = new URLSearchParams(window.location.search); 11642 const currentMode = params.get("mode") || params.get("viewMode"); 11643 if (currentMode !== viewMode) { 11644 params.set("mode", viewMode); 11645 const newSearch = params.toString(); 11646 const newUrl = `${window.location.pathname}${newSearch ? `?${newSearch}` : ""}`; 11647 window.history.replaceState(window.history.state, "", newUrl); 11648 } 11649 } 11650 }, [viewMode]); 11651 reactExports.useEffect(() => { 11652 if (isPlayingRef.current && lastSyncTrackIdRef.current === activeTrack.id) { 11653 return; 11654 } 11655 lastSyncTrackIdRef.current = activeTrack.id; 11656 const hasWebMidiSupport = typeof navigator !== "undefined" && !!navigator.requestMIDIAccess; 11657 const midiOutputs = synth.getMidiOutputs() || []; 11658 const preferred = typeof window !== "undefined" ? localStorage.getItem("preferredAudioMode") : null; 11659 const best = getBestAudioMode(activeTrack, isSlowMobile(), hasWebMidiSupport, midiOutputs.length > 0, pitchSemitones, true, engineOrder, preferred); 11660 adminLog("%c[WatchPage] ð Audio Mode Sync", "color:#a78bfa;font-weight:bold"); 11661 adminLog(" trackId :", activeTrack.id); 11662 adminLog(" â MODE:", best.mode, "| fallback:", best.isLiveSynthFallback); 11663 setIsLiveSynthFallback(best.isLiveSynthFallback); 11664 setAudioMode(best.mode); 11665 }, [activeTrack.id, activeTrack.status, activeTrack.opusPath, activeTrack.mp3PathHigh, pitchSemitones, setAudioMode, setIsLiveSynthFallback, adminLog, engineOrder]); 11666 reactExports.useEffect(() => { 11667 var _a2; 11668 let isCurrent = true; 11669 let melodyPathToFetch = melodySource === "studio" ? ((_a2 = activeTrack.quantizedScorePaths) == null ? void 0 : _a2.studio_melody) || activeTrack.melodyPath : activeTrack.melodyPath; 11670 if (melodyPathToFetch) { 11671 fetch(melodyPathToFetch).then((res) => { 11672 if (!res.ok) throw new Error("HTTP " + res.status); 11673 return res.json(); 11674 }).then((data) => { 11675 if (!isCurrent) return; 11676 const updatedMelodyData = data.map((note) => { 11677 var _a3, _b2; 11678 const musicalInstruments = processedInstruments.filter((inst) => inst.midiChannel !== -1); 11679 let mappedIdx = -1; 11680 if (note.trackIdx !== void 0) { 11681 const targetIdx = note.trackIdx; 11682 if (targetIdx >= 0 && targetIdx < musicalInstruments.length) { 11683 const matchedInst = musicalInstruments[targetIdx]; 11684 mappedIdx = processedInstruments.findIndex((inst) => inst.trackId === matchedInst.trackId); 11685 } 11686 } 11687 if (mappedIdx === -1) { 11688 let targetIdx = -1; 11689 const match = (_a3 = note.i) == null ? void 0 : _a3.match(/^(.*) \((\d+)\)$/); 11690 if (match) { 11691 targetIdx = parseInt(match[2]) - 1; 11692 } else { 11693 const trackMatch = (_b2 = note.i) == null ? void 0 : _b2.match(/^Track\s+(\d+)$/i); 11694 if (trackMatch) { 11695 targetIdx = parseInt(trackMatch[1]) - 1; 11696 } else if (!isNaN(parseInt(note.i))) { 11697 targetIdx = parseInt(note.i); 11698 } 11699 } 11700 if (targetIdx >= 0) { 11701 const foundByTrackId = processedInstruments.findIndex((inst) => inst.trackId === targetIdx && inst.midiChannel !== -1); 11702 if (foundByTrackId !== -1) { 11703 mappedIdx = foundByTrackId; 11704 } else if (targetIdx < musicalInstruments.length) { 11705 const matchedInst = musicalInstruments[targetIdx]; 11706 mappedIdx = processedInstruments.findIndex((inst) => inst.trackId === matchedInst.trackId); 11707 } 11708 } 11709 } 11710 if (mappedIdx === -1) { 11711 const instIdx = processedInstruments.findIndex((inst) => inst.originalName === note.i); 11712 if (instIdx !== -1) { 11713 mappedIdx = instIdx; 11714 } 11715 } 11716 if (mappedIdx !== -1) { 11717 const musicalIdx = musicalInstruments.findIndex((inst) => inst.trackId === processedInstruments[mappedIdx].trackId); 11718 return { 11719 ...note, 11720 i: uniqueNames[mappedIdx], 11721 trackIdx: musicalIdx !== -1 ? musicalIdx : mappedIdx 11722 }; 11723 } 11724 return note; 11725 }); 11726 updatedMelodyData.sort((a, b) => a.t - b.t); 11727 setMelodyData(updatedMelodyData); 11728 }).catch((err) => { 11729 console.error("Failed to load melody data", err); 11730 if (isCurrent) setMelodyData([]); 11731 }); 11732 } else { 11733 setMelodyData(void 0); 11734 } 11735 return () => { 11736 isCurrent = false;
11737 }; 11738 }, [activeTrack.midiMetadata, melodySource]); 11739 reactExports.useEffect(() => { 11740 if (["synth", "wasmsynth", "chiptune", "fmsynth", "opus", "webmidi"].includes(audioMode)) { 11741 if (isPlaying && !isSynthReady) { 11742 setIsBuffering(true); 11743 } else { 11744 setIsBuffering(false); 11745 } 11746 } else if (audioMode !== "singleopus") { 11747 setIsBuffering(false); 11748 } 11749 }, [isPlaying, isSynthReady, audioMode, setIsBuffering]); 11750 reactExports.useEffect(() => { 11751 var _a2, _b2, _c2; 11752 const configKey = `${activeTrack.id}-${audioMode}-${soundfont}-${audioQuality}-${isGameMode}-${isLiveSynthFallback}`; 11753 const needsLoad = !hasStartedLoading || lastLoadedConfigRef.current !== configKey; 11754 const shouldStartLoad = (isPlaying || viewMode === "taiko" || isGameMode) && needsLoad; 11755 if (shouldStartLoad && (audioMode === "synth" || audioMode === "chiptune" || audioMode === "wasmsynth" || audioMode === "fmsynth" || audioMode === "opus" || audioMode === "webmidi")) { 11756 console.log("[WatchPage] User requested Play or Config changed. Starting Download Sequence..."); 11757 setHasStartedLoading(true); 11758 lastLoadedConfigRef.current = configKey; 11759 const isProUser = isPro(currentUser); 11760 ((_a2 = soundfontMetadata[soundfont]) == null ? void 0 : _a2.access) || "pro"; 11761 const isPlayAlongActive = isGameMode || playAlongMutedTracks && playAlongMutedTracks.size > 0; 11762 adminLog("%c[WatchPage] â¶ Load-on-Play triggered", "color:#34d399;font-weight:bold"); 11763 adminLog(" isLiveSynthFallback:", isLiveSynthFallback); 11764 adminLog(" soundfont :", soundfont); 11765 adminLog(" audioQuality :", audioQuality); 11766 adminLog(" isProUser :", isProUser); 11767 adminLog(" isPlayAlongActive :", isPlayAlongActive); 11768 if (isLiveSynthFallback || audioMode === "chiptune" || audioMode === "wasmsynth" || audioMode === "fmsynth") { 11769 console.log("[WatchPage] Live synthesis fallback active. Synthesizing from MIDI file."); 11770 const eventsSources = [ 11771 activeTrack.cleanMidiPath, 11772 activeTrack.midiPath 11773 ].filter(Boolean); 11774 adminLog("%c[WatchPage] ð¹ Live-synth fallback path resolution", "color:#f59e0b;font-weight:bold"); 11775 adminLog(" pianoRollPath :", activeTrack.pianoRollPath || "(missing)"); 11776 adminLog(" cleanMidiPath :", activeTrack.cleanMidiPath || "(missing)"); 11777 adminLog(" midiPath :", activeTrack.midiPath || "(missing)"); 11778 adminLog(" â eventsSources:", eventsSources); 11779 adminLog(" defaultLiveSf :", defaultLiveSf); 11780 if (eventsSources.length === 0) { 11781 console.error("[WatchPage] Live synthesis error: No MIDI source path available."); 11782 adminLog("%c[WatchPage] â No MIDI source â aborting live synth", "color:red;font-weight:bold"); 11783 setTrackStates({ 0: { ready: false, error: true, percent: 0 } }); 11784 setIsPlaying(false); 11785 return; 11786 } 11787 synth.audioMode = audioMode; 11788 parseMidiInstruments(eventsSources).then(({ tracks: liveFallbackTracks, duration: fallbackDuration, tempo: fallbackTempo, tickDiv: fallbackTickDiv }) => { 11789 var _a3, _b3, _c3; 11790 if (fallbackDuration > 0 && (!((_a3 = activeTrack.midiMetadata) == null ? void 0 : _a3.duration) || activeTrack.midiMetadata.duration === 0)) { 11791 console.log(`[WatchPage] Dispatching live fallback duration: ${fallbackDuration.toFixed(2)}s`); 11792 window.dispatchEvent(new CustomEvent("setTrackDuration", { detail: fallbackDuration })); 11793 } 11794 const tickDivToUse = ((_b3 = activeTrack.midiMetadata) == null ? void 0 : _b3.tickDiv) || fallbackTickDiv; 11795 const tempoToUse = ((_c3 = activeTrack.midiMetadata) == null ? void 0 : _c3.tempo) || fallbackTempo; 11796 let sfToLoad2 = soundfont === "pre-rendered" ? preRenderedSf : soundfont; 11797 if (soundfont === "pre-rendered" && isSlowMobile()) { 11798 const heavySfs = ["MusyngKite", "GeneralUser-GS", "FatBoy", "FluidR3_GM", "MuseScore_General", "Aspirin"]; 11799 if (heavySfs.includes(sfToLoad2.replace(/\.h?sf[23]$/i, "").replace(/_JS$/i, ""))) { 11800 sfToLoad2 = "SoundBlasterOld.sf2"; 11801 } 11802 } 11803 sfToLoad2 = resolveSfToLoad(sfToLoad2); 11804 synth.loadTracks( 11805 liveFallbackTracks, 11806 tickDivToUse, 11807 tempoToUse, 11808 audioQuality === "high" && isProUser ? "pro" : "free", 11809 sfToLoad2, 11810 eventsSources, 11811 (idx, status) => { 11812 setTrackStates((prev) => ({ ...prev, [idx]: status })); 11813 }, 11814 true, 11815 // forceLive 11816 defaultLiveSf, 11817 isAdmin 11818 ).then(() => { 11819 if (isPlayingRef.current) { 11820 synth.play(currentTimeRef.current); 11821 } 11822 }).catch((err) => { 11823 console.error("[WatchPage] Live synthesis load error:", err); 11824 setTrackStates({ 0: { ready: false, error: true, percent: 0 } }); 11825 setIsPlaying(false); 11826 }); 11827 }).catch((err) => { 11828 console.error("[WatchPage] Live synthesis parse error:", err); 11829 setTrackStates({ 0: { ready: false, error: true, percent: 0 } }); 11830 setIsPlaying(false); 11831 }); 11832 return; 11833 } 11834 const tracksToLoad = activeTrack.instruments && activeTrack.instruments.length > 0 ? activeTrack.instruments : [{ 11835 name: "Master", 11836 program: 0, 11837 midiChannel: 0, 11838 notesCount: 0, 11839 stemPath: activeTrack.opusPath || activeTrack.mp3PathHigh || activeTrack.mp3Path, 11840 stemPathHigh: activeTrack.opusPath || activeTrack.mp3PathHigh, 11841 trackId: 0 11842 }]; 11843 const isLive = audioMode === "synth" || audioMode === "chiptune" || audioMode === "wasmsynth" || audioMode === "fmsynth" || isPlayAlongActive || isLiveSynthFallback; 11844 let sfToLoad = soundfont === "pre-rendered" ? preRenderedSf || defaultLiveSf || "MusyngKite" : soundfont; 11845 if (soundfont === "pre-rendered" && isSlowMobile()) { 11846 const heavySfs = ["MusyngKite", "GeneralUser-GS", "FatBoy", "FluidR3_GM", "MuseScore_General", "Aspirin"]; 11847 if (heavySfs.includes(sfToLoad.replace(/\.h?sf[23]$/i, "").replace(/_JS$/i, ""))) { 11848 sfToLoad = "SoundBlasterOld.sf2"; 11849 } 11850 } 11851 sfToLoad = resolveSfToLoad(sfToLoad); 11852 synth.audioMode = audioMode; 11853 synth.loadTracks( 11854 tracksToLoad, 11855 (_b2 = activeTrack.midiMetadata) == null ? void 0 : _b2.tickDiv, 11856 (_c2 = activeTrack.midiMetadata) == null ? void 0 : _c2.tempo, 11857 audioQuality === "high" && isProUser ? "pro" : "free", 11858 sfToLoad, 11859 activeTrack.pianoRollPath || activeTrack.cleanMidiPath || activeTrack.midiPath || void 0, 11860 (idx, status) => { 11861 setTrackStates((prev) => ({ ...prev, [idx]: status })); 11862 }, 11863 isLive, 11864 defaultLiveSf, 11865 isAdmin 11866 ).then(() => { 11867 if (audioMode === "opus") { 11868 const stemDuration = synth.getDuration(); 11869 if (stemDuration > 0) { 11870 console.log(`[WatchPage] Stems loaded. Dispatching real stem duration: ${stemDuration.toFixed(2)}s`); 11871 window.dispatchEvent(new CustomEvent("setTrackDuration", { detail: stemDuration })); 11872 } 11873 } 11874 if (isPlayingRef.current) { 11875 synth.play(currentTimeRef.current); 11876 } 11877 }).catch((err) => { 11878 console.error("Synth load error:", err); 11879 }); 11880 } 11881 }, [isPlaying, hasStartedLoading, audioMode, activeTrack, audioQuality, soundfont, isGameMode, isLiveSynthFallback, viewMode]); 11882 reactExports.useEffect(() => { 11883 var _a2, _b2, _c2; 11884 const configKey = `${activeTrack.id}-${audioMode}-${soundfont}-${audioQuality}-${isGameMode}-${isLiveSynthFallback}`; 11885 if (hasStartedLoading && lastLoadedConfigRef.current && lastLoadedConfigRef.current !== configKey && (audioMode === "synth" || audioMode === "chiptune" || audioMode === "wasmsynth" || audioMode === "fmsynth")) { 11886 lastLoadedConfigRef.current = configKey; 11887 console.log("[WatchPage] Audio config change detected. Re-loading tracks..."); 11888 const isProUser = isPro(currentUser); 11889 ((_a2 = soundfontMetadata[soundfont]) == null ? void 0 : _a2.access) || "pro"; 11890 const isPlayAlongActive = isGameMode || playAlongMutedTracks && playAlongMutedTracks.size > 0; 11891 if (isLiveSynthFallback || audioMode === "chiptune" || audioMode === "wasmsynth" || audioMode === "fmsynth") { 11892 const eventsSources = [ 11893 activeTrack.cleanMidiPath, 11894 activeTrack.midiPath
11895 ].filter(Boolean); 11896 if (eventsSources.length === 0) { 11897 console.error("[WatchPage] Live synthesis config change error: No MIDI source path available."); 11898 setTrackStates({ 0: { ready: false, error: true, percent: 0 } }); 11899 setIsPlaying(false); 11900 return; 11901 } 11902 parseMidiInstruments(eventsSources).then(({ tracks: liveFallbackTracks, duration: fallbackDuration, tempo: fallbackTempo, tickDiv: fallbackTickDiv }) => { 11903 var _a3, _b3, _c3; 11904 if (fallbackDuration > 0 && (!((_a3 = activeTrack.midiMetadata) == null ? void 0 : _a3.duration) || activeTrack.midiMetadata.duration === 0)) { 11905 window.dispatchEvent(new CustomEvent("setTrackDuration", { detail: fallbackDuration })); 11906 } 11907 const tickDivToUse = ((_b3 = activeTrack.midiMetadata) == null ? void 0 : _b3.tickDiv) || fallbackTickDiv; 11908 const tempoToUse = ((_c3 = activeTrack.midiMetadata) == null ? void 0 : _c3.tempo) || fallbackTempo; 11909 let sfToLoad2 = soundfont === "pre-rendered" ? preRenderedSf : soundfont; 11910 if (soundfont === "pre-rendered" && isSlowMobile()) { 11911 const heavySfs = ["MusyngKite", "GeneralUser-GS", "FatBoy", "FluidR3_GM", "MuseScore_General", "Aspirin"]; 11912 if (heavySfs.includes(sfToLoad2.replace(/\.h?sf[23]$/i, "").replace(/_JS$/i, ""))) { 11913 sfToLoad2 = "SoundBlasterOld.sf2"; 11914 } 11915 } 11916 sfToLoad2 = resolveSfToLoad(sfToLoad2); 11917 synth.audioMode = audioMode; 11918 synth.loadTracks( 11919 liveFallbackTracks, 11920 tickDivToUse, 11921 tempoToUse, 11922 audioQuality === "high" && isProUser ? "pro" : "free", 11923 sfToLoad2, 11924 eventsSources, 11925 (idx, status) => { 11926 setTrackStates((prev) => ({ ...prev, [idx]: status })); 11927 }, 11928 true, 11929 // forceLive 11930 defaultLiveSf, 11931 isAdmin 11932 ).then(() => { 11933 if (isPlayingRef.current) synth.play(currentTimeRef.current); 11934 }); 11935 }).catch((err) => { 11936 console.error("[WatchPage] Live synthesis config-change error:", err); 11937 setTrackStates({ 0: { ready: false, error: true, percent: 0 } }); 11938 setIsPlaying(false); 11939 }); 11940 return; 11941 } 11942 const tracksToLoad = activeTrack.instruments && activeTrack.instruments.length > 0 ? activeTrack.instruments : [{ 11943 name: "Master", 11944 program: 0, 11945 midiChannel: 0, 11946 notesCount: 0, 11947 stemPath: activeTrack.opusPath || activeTrack.mp3PathHigh || activeTrack.mp3Path, 11948 stemPathHigh: activeTrack.opusPath || activeTrack.mp3PathHigh, 11949 trackId: 0 11950 }]; 11951 const isLive = audioMode === "synth" || audioMode === "chiptune" || audioMode === "wasmsynth" || audioMode === "fmsynth" || isPlayAlongActive || isLiveSynthFallback; 11952 let sfToLoad = soundfont === "pre-rendered" ? preRenderedSf || defaultLiveSf || "MusyngKite" : soundfont; 11953 if (soundfont === "pre-rendered" && isSlowMobile()) { 11954 const heavySfs = ["MusyngKite", "GeneralUser-GS", "FatBoy", "FluidR3_GM", "MuseScore_General", "Aspirin"]; 11955 if (heavySfs.includes(sfToLoad.replace(/\.h?sf[23]$/i, "").replace(/_JS$/i, ""))) { 11956 sfToLoad = "SoundBlasterOld.sf2"; 11957 } 11958 } 11959 sfToLoad = resolveSfToLoad(sfToLoad); 11960 synth.audioMode = audioMode; 11961 synth.loadTracks( 11962 tracksToLoad, 11963 (_b2 = activeTrack.midiMetadata) == null ? void 0 : _b2.tickDiv, 11964 (_c2 = activeTrack.midiMetadata) == null ? void 0 : _c2.tempo, 11965 audioQuality === "high" && isProUser ? "pro" : "free", 11966 sfToLoad, 11967 activeTrack.pianoRollPath || activeTrack.cleanMidiPath || activeTrack.midiPath || void 0, 11968 (idx, status) => { 11969 setTrackStates((prev) => ({ ...prev, [idx]: status })); 11970 }, 11971 isLive, 11972 defaultLiveSf, 11973 isAdmin 11974 ).then(() => { 11975 if (isPlayingRef.current) synth.play(currentTimeRef.current); 11976 }); 11977 } 11978 }, [audioQuality, soundfont, isGameMode, isLiveSynthFallback, audioMode, playAlongMutedTracks, preRenderedSf, activeTrack, currentUser, defaultLiveSf, isAdmin]); 11979 reactExports.useEffect(() => { 11980 if (audioMode === "synth" || audioMode === "webmidi" || audioMode === "chiptune" || audioMode === "wasmsynth" || audioMode === "fmsynth" || audioMode === "opus") { 11981 if (isPlaying && isSynthReady) { 11982 console.log(`[WatchPage] Playback Control: Playing Synth (${audioMode}) at`, currentTime); 11983 synth.play(currentTime); 11984 } else if (!isPlaying) { 11985 console.log(`[WatchPage] Playback Control: Stopping Synth (${audioMode}) (isPlaying:`, isPlaying, "isSynthReady:", isSynthReady, ")"); 11986 synth.stop(); 11987 } 11988 } else if (!isPlaying) { 11989 console.log("[WatchPage] Playback Control: Stopping Synth (Mode is Opu
11989s)"); 11990 synth.stop(); 11991 } 11992 }, [isPlaying, audioMode, isSynthReady]); 11993 reactExports.useEffect(() => { 11994 if (audioMode === "opus" && hasSynthError && isPlaying) { 11995 console.error("[WatchPage] Synth loading failed. Halting playback."); 11996 setIsPlaying(false); 11997 } 11998 }, [audioMode, hasSynthError, isPlaying, setIsPlaying]); 11999 reactExports.useEffect(() => { 12000 const isPlayAlongActive = isGameMode || playAlongMutedTracks && playAlongMutedTracks.size > 0; 12001 if (isPlayAlongActive) { 12002 if (previousSoundfont === null) { 12003 setPreviousSoundfont(soundfont); 12004 } 12005 if (previousAudioMode === null) { 12006 setPreviousAudioMode(audioMode); 12007 } 12008 const targetSf = defaultLiveSf || "MusyngKite"; 12009 const isNonLiveSf = soundfont === "pre-rendered" || soundfont === preRenderedSf && !isLiveSynthFallback; 12010 if (isNonLiveSf && soundfont !== targetSf) { 12011 console.log(`[WatchPage] Forcing live fallback soundfont (${targetSf}) for Play-Along Mode because current SF is non-live`); 12012 setSoundfont(targetSf); 12013 } 12014 if (audioMode === "opus" || audioMode === "singleopus") { 12015 const list = isSlowMobile() ? engineOrder == null ? void 0 : engineOrder.watchMobile : engineOrder == null ? void 0 : engineOrder.watchDesktop; 12016 const liveEngine = (list == null ? void 0 : list.find((e) => ["wasmsynth", "synth", "fmsynth", "chiptune"].includes(e))) || "wasmsynth"; 12017 console.log(`[WatchPage] Forcing ${liveEngine} audioMode (live MIDI synthesis) for Play-Along Mode`); 12018 setAudioMode(liveEngine); 12019 } 12020 } else { 12021 if (previousSoundfont !== null) { 12022 console.log(`[WatchPage] Reverting soundfont back to ${previousSoundfont}`); 12023 setSoundfont(previousSoundfont); 12024 setPreviousSoundfont(null); 12025 } 12026 if (previousAudioMode !== null) { 12027 console.log(`[WatchPage] Reverting audioMode back to ${previousAudioMode}`); 12028 setAudioMode(previousAudioMode); 12029 setPreviousAudioMode(null); 12030 } 12031 } 12032 }, [isGameMode, playAlongMutedTracks, soundfont, audioMode, defaultLiveSf, preRenderedSf, previousSoundfont, previousAudioMode, setAudioMo
12032de, engineOrder]); 12033 reactExports.useEffect(() => { 12034 console.log("[WatchPage] Applying Mutes to Synth:", Array.from(localMutedIndices)); 12035 processedInstruments.forEach((_, idx) => { 12036 synth.setMute(idx, localMutedIndices.has(idx)); 12037 }); 12038 }, [localMutedIndices, processedInstruments]); 12039 const handleInstrumentChange = reactExports.useCallback((trackIdx, newProgram) => { 12040 setInstrumentOverrides((prev) => ({ ...prev, [trackIdx]: newProgram })); 12041 synth.setTrackInstrument(trackIdx, newProgram); 12042 if (audioMode === "opus" || audioMode === "singleopus") { 12043 const list = isSlowMobile() ? engineOrder == null ? void 0 : engineOrder.watchMobile : engineOrder == null ? void 0 : engineOrder.watchDesktop; 12044 const liveEngine = (list == null ? void 0 : list.find((e) => ["wasmsynth", "synth", "fmsynth", "chiptune"].includes(e))) || "wasmsynth"; 12045 setAudioMode(liveEngine); 12046 } 12047 }, [audioMode, setAudioMode, engineOrder]); 12048 const handleInstrumentMute = (index) => { 12049 if (isGameMode) return; 12050 if (soloedIndex !== null) { 12051 setSoloedIndex(null); 12052 synth.setSolo(null); 12053 } 12054 setLocalMutedIndices((prev) => { 12055 const next = new Set(prev); 12056 const isMuted = next.has(index); 12057 if (isMuted) { 12058 next.delete(index); 12059 } else { 12060 next.add(index); 12061 } 12062 return next; 12063 }); 12064 }; 12065 const handleGameModeMute = reactExports.useCallback((melodyIndices) => { 12066 if (soloedIndex !== null) { 12067 setSoloedIndex(null); 12068 synth.setSolo(null); 12069 } 12070 if (melodyIndices === null) { 12071 console.log("[WatchPage] Resetting Mutes (Play-Along Ended)"); 12072 setLocalMutedIndices(/* @__PURE__ */ new Set()); 12073 setPlayAlongMutedTracks(/* @__PURE__ */ new Set()); 12074 } else { 12075 console.log("[WatchPage] Setting Play-Along Mutes:", melodyIndices); 12076 setLocalMutedIndices(new Set(melodyIndices)); 12077 setPlayAlongMutedTracks(new Set(melodyIndices)); 12078 } 12079 }, [soloedIndex]); 12080 const handleInstrumentSolo = (index) => { 12081 if (isGameMode) return; 12082 setSoloedIndex((prevSoloed) => { 12083 const newSoloed = prevSoloed === index ? null : index; 12084 synth.setSolo(newSoloed); 12085 return newSoloed; 12086 }); 12087 }; 12088 const handleVolumeChange = (index, val) => { 12089 setTrackVolumes((prev) => ({ ...prev, [index]: val })); 12090 synth.setTrackVolume(index, val); 12091 }; 12092 const handlePanChange = (index, val) => { 12093 setTrackPans((prev) => ({ ...prev, [index]: val })); 12094 synth.setTrackPan(index, val); 12095 }; 12096 const toggleTrackVisibility = (index) => { 12097 if (!activeTrack.instruments || !activeTrack.instruments[index] || !uniqueNames[index]) return; 12098 const uniqueName = uniqueNames[index]; 12099 setHiddenTracks((prev) => { 12100 const next = new Set(prev); 12101 if (next.has(uniqueName)) next.delete(uniqueName); 12102 else next.add(uniqueName); 12103 return next; 12104 }); 12105 }; 12106 const handleVoteTag = async (tagName, value) => { 12107 const userId = currentUser ? currentUser.id : null; 12108 await api.voteTag(activeTrack.id, tagName, value, userId); 12109 setTags((prev) => prev.map((t2) => { 12110 if (t2.name === tagName) { 12111 return { ...t2, score: t2.score + value * ((currentUser == null ? void 0 : currentUser.trustScore) && currentUser.trustScore > 50 ? 5 : 1) }; 12112 } 12113 return t2; 12114 }).sort((a, b) => b.score - a.score)); 12115 }; 12116 const handleProposeTag = async (tagName) => { 12117 if (!currentUser) { 12118 onAuthRequested(); 12119 setIsTagModalOpen(false); 12120 return; 12121 } 12122 const res = await api.proposeTag(activeTrack.id, tagName, currentUser.id); 12123 if (res && res.tag) { 12124 setTags((prev) => { 12125 if (prev.find((t2) => t2.id === res.tag.id)) return prev; 12126 const newTag = { ...res.tag, score: res.tag.score || 0 }; 12127 return [...prev, newTag].sort((a, b) => b.score - a.score); 12128 }); 12129 } 12130 }; 12131 const handleResetMixer = () => { 12132 setLocalMutedIndices(/* @__PURE__ */ new Set()); 12133 setPlayAlongMutedTracks(/* @__PURE__ */ new Set()); 12134 setTrackVolumes({}); 12135 setTrackPans({}); 12136 setHiddenTracks(/* @__PURE__ */ new Set()); 12137 setSoloedIndex(null); 12138 if (audioMode === "synth" || audioMode === "wasmsynth") { 12139 synth.resetMixer(); 12140 } 12141 }; 12142 const openReportModal = (id) => { 12143 setIsReportModalOpen(true); 12144 }; 12145 reactExports.useEffect(() => { 12146 if (activeTrack) { 12147 document.title = `${activeTrack.title} - ${activeTrack.artist} | HamieNet`; 12148 } 12149 }, [activeTrack]); 12150 if (isBlocked) { 12151 return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-[60vh] text-center p-6", children: [
12152 /* @__PURE__ */ jsxRuntimeExports.jsx(OctagonAlert, { size: 48, className: "text-red-500 mb-4" }), 12153 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-2xl font-bold text-zinc-900 dark:text-white mb-2", children: t("watch.contentUnavailable", "Content Unavailable") }), 12154 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-zinc-600 dark:text-zinc-400 max-w-md", children: t("watch.unavailableRegion", { region: country, defaultValue: `This content contains copyright-protected material that is not available in your region (${country}).` }) }) 12155 ] }); 12156 } 12157 const resolvedTrack = reactExports.useMemo(() => ({ 12158 ...activeTrack, 12159 backgroundImageUrl: localBgUrl 12160 }), [activeTrack, localBgUrl]); 12161 const showLoadingOverlay = isPlaying && ["synth", "wasmsynth", "chiptune", "fmsynth", "opus", "webmidi"].includes(audioMode) && !isSynthReady; 12162 const visualizerComponent = audioMode === "webmidi" && !forceShowVisualizer ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full aspect-video rounded-xl bg-zinc-950 border border-zinc-800 p-6 flex flex-col justify-center items-center text-center shadow-2xl", children: [ 12163 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-3 rounded-full bg-indigo-950 border border-indigo-500/30 text-indigo-400 mb-4 animate-pulse", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "32", height: "32", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
12164 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12 2v20" }), 12165 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m17 5-5-3-5 3" }), 12166 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m7 19 5 3 5-3" }), 12167 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M2 12h20" }), 12168 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m5 7 3 5-3 5" }), 12169 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "m19 17-3-5 3-5" }) 12170 ] }) }), 12171 /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-xl font-bold text-white mb-2", children: "Web MIDI Playback Active" }), 12172 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-400 max-w-md mb-6 leading-relaxed", children: "To save battery and CPU cycles on your device, audio is playing directly through your device's operating system synthesizer. Music visualizers are disabled to keep your browser running fast." }), 12173 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-wrap gap-4 justify-center", children: [
12174 /* @__PURE__ */ jsxRuntimeExports.jsx( 12175 "button", 12176 { 12177 onClick: () => setForceShowVisualizer(true), 12178 className: "px-4 py-2 text-xs font-bold rounded-lg border border-zinc-700 hover:border-zinc-500 text-zinc-300 hover:text-white transition-colors", 12179 children: "Open Visualizer Anyway" 12180 } 12181 ), 12182 /* @__PURE__ */ jsxRuntimeExports.jsx( 12183 "button", 12184 { 12185 onClick: () => { 12186 const list = isSlowMobile() ? engineOrder == null ? void 0 : engineOrder.watchMobile : engineOrder == null ? void 0 : engineOrder.watchDesktop; 12187 const liveEngine = (list == null ? void 0 : list.find((e) => ["wasmsynth", "synth", "fmsynth", "chiptune"].includes(e))) || "wasmsynth"; 12188 setAudioMode(liveEngine); 12189 setForceShowVisualizer(true); 12190 }, 12191 className: "px-4 py-2 text-xs font-bold rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white transition-all shadow-md shadow-indigo-600/20", 12192 children: "Switch to Soundfont Synth" 12193 } 12194 ) 12195 ] }) 12196 ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx( 12197 VisualizerWindow, 12198 { 12199 theme, 12200 viewMode, 12201 setViewMode, 12202 track: resolvedTrack, 12203 currentTime, 12204 duration, 12205 isPlaying, 12206 onPlayPause: (forcePlay) => { 12207 try { 12208 const ctx = synth.getAudioContext(); 12209 if (ctx && ctx.state === "suspended") { 12210 ctx.resume(); 12211 } 12212 } catch (err) { 12213 console.error("[WatchPage] Failed to resume AudioContext inside play click handler:", err); 12214 } 12215 if (forcePlay !== void 0) { 12216 setIsPlaying(forcePlay); 12217 } else { 12218 setIsPlaying(!isPlaying); 12219 } 12220 }, 12221 autoPlayCountdown, 12222 onCancelAutoPlay, 12223 onPlayNext, 12224 nextTrack, 12225 isTheaterMode, 12226 setIsTheaterMode: handleSetTheaterMode, 12227 pianoRollData, 12228 melodyData, 12229 melodySource, 12230 pitchSemitones, 12231 setPitchSemitones: (val) => window.dispatchEvent(new CustomEvent("setPitchSemitones", { detail: val })), 12232 currentTempo: (((_b = activeTrack.midiMetadata) == null ? void 0 : _b.tempo) || 120) * playbackRate, 12233 setCurrentTempo: (val) => { 12234 var _a2; 12235 return window.dispatchEvent(new CustomEvent("setPlaybackRate", { detail: val / (((_a2 = activeTrack.midiMetadata) == null ? void 0 : _a2.tempo) || 120) })); 12236 }, 12237 originalTempo: ((_c = activeTrack.midiMetadata) == null ? void 0 : _c.tempo) || 120, 12238 setMelodySource, 12239 hiddenTracks, 12240 trackVisibility, 12241 colorMap: derivedColorMap, 12242 isLoadingAudio: showLoadingOverlay, 12243 isAudioLoaded: ["synth", "wasmsynth", "chiptune", "fmsynth", "opus", "webmidi"].includes(audioMode) ? isSynthReady : true, 12244 isBuffering, 12245 selectedTrackIndex, 12246 onSelectTrack: setSelectedTrackIndex, 12247 processedInstruments, 12248 uniqueNames, 12249 trackId: activeTrack.id, 12250 userId: currentUser == null ? void 0 : currentUser.id, 12251 userName: currentUser == null ? void 0 : currentUser.username, 12252 uploaderName: activeTrack.uploader.name, 12253 isAdmin: (currentUser == null ? void 0 : currentUser.role) === "ADMIN", 12254 staffModeEnabled, 12255 audioMode, 12256 onSeek, 12257 playbackRate, 12258 isGameMode, 12259 setIsGameMode, 12260 onGameModeMute: handleGameModeMute, 12261 onAuthRequested, 12262 soloedIndex, 12263 onSolo: handleInstrumentSolo, 12264 mutedIndices: localMutedIndices, 12265 onMute: handleInstrumentMute, 12266 onPanChange: handlePanChange, 12267 soundfont, 12268 isAttached: true, 12269 onInstrumentChange: handleInstrumentChange 12270 } 12271 ); 12272 if (isEmbed) { 12273 return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full h-screen bg-black overflow-hidden flex flex-col justify-center items-center", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "w-full max-w-[1920px] aspect-video", children: visualizerComponent }) }); 12274 } 12275 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 12276 "div", 12277 { 12278 className: `mx-auto p-0 sm:p-4 animate-in fade-in w-full ${isTheaterMode ? "max-w-full" : "max-w-[1750px]"}`, 12279 children: [
12280 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col xl:flex-row justify-center items-start xl:gap-8 gap-6 mb-8 w-full", children: [ 12281 /* @__PURE__ */ jsxRuntimeExports.jsxs( 12282 "div", 12283 { 12284 className: "flex flex-col min-w-0 xl:min-w-[640px] w-full z-10", 12285 style: { 12286 flex: "1 1 auto", 12287 maxWidth: isTheaterMode ? "min(100%, calc((100vh - 190px) * 1.777))" : "min(100%, calc((100vh - 230px) * 1.777))" 12288 }, 12289 children: [ 12290 visualizerComponent, 12291 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-4 flex flex-col gap-6 px-4 sm:px-0 w-full", children: [ 12292 /* @__PURE__ */ jsxRuntimeExports.jsx( 12293 VideoMetadata, 12294 { 12295 track: resolvedTrack, 12296 currentUser, 12297 isSubscribed, 12298 onSubscribe: subscribe, 12299 onChannelSelect: handleChannelSelect,
12300 onCustomizeBackground: () => setIsBgModalOpen(true), 12301 onRemoveBackground: handleBackgroundRemove, 12302 likedMidis, 12303 toggleLike: (id) => toggleLike(id), 12304 tags, 12305 onTagSelect, 12306 onArtistSelect, 12307 onVoteTag: handleVoteTag, 12308 onOpenTagModal: () => setIsTagModalOpen(true), 12309 onAddToPlaylist: () => onAddToPlaylist(activeTrack), 12310 onReport: () => openReportModal(), 12311 onAuthRequested, 12312 onEditWiki: () => setIsEditModalOpen(true), 12313 currentTime, 12314 duration, 12315 isLiveSynthFallback, 12316 hasSynthError: (audioMode === "synth" || audioMode === "wasmsynth") && hasSynthError, 12317 onReuploadNeeded: () => setIsReuploadModalOpen(true) 12318 } 12319 ), 12320 /* @__P
12320URE__ */ jsxRuntimeExports.jsx(AdSlot, { type: "inArticleText", className: "!my-0 min-h-[90px]" }), 12321 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "xl:hidden w-full", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 12322 MixerPanel, 12323 { 12324 tracks: processedInstruments, 12325 uniqueNames, 12326 mutedIndices: localMutedIndices, 12327 playAlongMutedTracks, 12328 isMelodyMode: viewMode === "melody", 12329 toggleMute: handleInstrumentMute, 12330 trackVolumes, 12331 onVolumeChange: handleVolumeChange, 12332 trackPans, 12333 onPanChange: handlePanChange, 12334 onSolo: handleInstrumentSolo, 12335 soloedIndex, 12336 hiddenTracks, 12337 toggleVisibility: toggleTrackVisibility, 12338 audioMode, 12339 toggleAudioMode: () => { 12340 var _a2; 12341 if (audioMode === "opus" || audioMode === "singleopus") { 12342 const list = isSlowMobile() ? engineOrder == null ? void 0 : engineOrder.watchMobile : engineOrder == null ? void 0 : engineOrder.watchDesktop; 12343 const liveEngine = (list == null ? void 0 : list.find((e) => ["wasmsynth", "synth", "fmsynth", "chiptune", "webmidi"].includes(e))) || "wasmsynth"; 12344 setAudioMode(liveEngine); 12345 } else { 12346 const hasStems = (_a2 = activeTrack.instruments) == null ? void 0 : _a2.some((i) => i.stemPath); 12347 const hasAudio = !!activeTrack.opusPath; 12348 if (hasStems) setAudioMode("opus"); 12349 else if (hasAudio) setAudioMode("singleopus"); 12350 } 12351 }, 12352 mixerMode, 12353 setMixerMode, 12354 colorMap: derivedColorMap, 12355 onViewEvents: setEventsInstrument, 12356 isPlaying, 12357 currentTime, 12358 duration, 12359 trackStates, 12360 onResetMixer: handleResetMixer, 12361 selectedTrackIndex, 12362 onSelectTrack: (idx) => { 12363 setSelectedTrackIndex(idx); 12364 }, 12365 isGameMode, 12366 midiTrack: activeTrack, 12367 soundfont: soundfont === "pre-rendered" && activeTrack.status !== "completed" ? preRenderedSf : soundfont, 12368 onSoundfontChange: handleSoundfontChange, 12369 libraries: displayedLibraries, 12370 currentUser, 12371 preRenderedSf, 12372 isAttached: true, 12373 theme, 12374 pianoRollData, 12375 onInstrumentChange: handleInstrumentChange 12376 } 12377 ) }), 12378 ((_d = activeTrack.midiMetadata) == null ? void 0 : _d.lyrics) && activeTrack.midiMetadata.lyrics.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-5 shadow-sm mt-4", children: [
12379 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-3", children: [ 12380 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 12381 /* @__PURE__ */ jsxRuntimeExports.jsx(MicVocal, { size: 18, className: "text-indigo-500" }), 12382 t("karaoke.lyrics") 12383 ] }), 12384 /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setLyricsExpanded(!lyricsExpanded), className: "text-xs font-bold text-indigo-500 hover:text-indigo-600 bg-indigo-500/10 px-3 py-1.5 rounded-full transition-colors", children: lyricsExpanded ? t("karaoke.showLess") : t("karaoke.showFullLyrics") }) 12385 ] }), 12386 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `relative overflow-hidden transition-all duration-500 ${lyricsExpanded ? "max-h-[5000px]" : "max-h-[120px]"}`, children: [
12387 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-zinc-600 dark:text-zinc-400 leading-snug text-xs sm:text-sm font-medium whitespace-pre-wrap", children: (() => { 12388 const grouped = groupLyricsIntoLines(activeTrack.midiMetadata.lyrics); 12389 return grouped.map((line, lineIdx) => /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mb-1", children: line.syllables.map((syl, i) => /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: syl.txt.replace(/ /g, "Â ") }, i)) }, lineIdx)); 12390 })() }), 12391 !lyricsExpanded && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-zinc-50 dark:from-zinc-900 to-transparent pointer-events-none" }) 12392 ] }) 12393 ] }), 12394 /* @__PURE__ */ jsxRuntimeExports.jsx( 12395 CommentSection, 12396 { 12397 trackId: activeTrack.id, 12398 comments, 12399 setComments, 12400 currentUser, 12401 onReport: openReportModal 12402 } 12403 ) 12404 ] }) 12405 ] 12406 } 12407 ),
12408 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full xl:w-[380px] shrink-0 min-w-0 flex flex-col gap-6 px-4 sm:px-0", children: [ 12409 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "hidden xl:block w-full", children: /* @__PURE__ */ jsxRuntimeExports.jsx( 12410 MixerPanel, 12411 { 12412 tracks: processedInstruments, 12413 uniqueNames, 12414 mutedIndices: localMutedIndices, 12415 playAlongMutedTracks, 12416 isMelodyMode: viewMode === "melody", 12417 toggleMute: handleInstrumentMute, 12418 trackVolumes, 12419 onVolumeChange: handleVolumeChange, 12420 trackPans, 12421 onPanChange: handlePanChange, 12422 onSolo: handleInstrumentSolo, 12423 soloedIndex, 12424 hiddenTracks, 12425 toggleVisibility: toggleTrackVisibility, 12426 audioMode, 12427 toggleAudioMode: () => { 12428 var _a2; 12429 if (audioMode === "opus" || audioMode === "singleopus") { 12430 const list = isSlowMobile() ? engineOrder == null ? void 0 : engineOrder.watchMobile : engineOrder == null ? void 0 : engineOrder.watchDesktop; 12431 const liveEngine = (list == null ? void 0 : list.find((e) => ["wasmsynth", "synth", "fmsynth", "chiptune", "webmidi"].includes(e))) || "wasmsynth"; 12432 setAudioMode(liveEngine); 12433 } else { 12434 const hasStems = (_a2 = activeTrack.instruments) == null ? void 0 : _a2.some((i) => i.stemPath); 12435 const hasAudio = !!activeTrack.opusPath; 12436 if (hasStems) setAudioMode("opus"); 12437 else if (hasAudio) setAudioMode("singleopus"); 12438 } 12439 }, 12440 mixerMode, 12441 setMixerMode, 12442 colorMap: derivedColorMap, 12443 onViewEvents: setEventsInstrument, 12444 isPlaying, 12445 currentTime, 12446 duration, 12447 trackStates, 12448 onResetMixer: handleResetMixer, 12449 selectedTrackIndex, 12450 onSelectTrack: (idx) => { 12451 setSelectedTrackIndex(idx); 12452 }, 12453 isGameMode, 12454 midiTrack: activeTrack, 12455 soundfont: soundfont === "pre-rendered" && activeTrack.status !== "completed" ? preRenderedSf : soundfont, 12456 onSoundfontChange: handleSoundfontChange, 12457 libraries: displayedLibraries, 12458 currentUser, 12459 preRenderedSf, 12460 isAttached: true, 12461 theme, 12462 pianoRollData, 12463 onInstrumentChange: handleInstrumentChange 12464 } 12465 ) }),
12466 /* @__PURE__ */ jsxRuntimeExports.jsx(AdSlot, { type: "watchSidebar" }), 12467 variations.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-gradient-to-br from-indigo-50/50 via-white to-white dark:from-indigo-950/20 dark:via-zinc-900 dark:to-zinc-900 border border-indigo-100/50 dark:border-indigo-900/30 rounded-2xl p-5 shadow-sm w-full", children: [ 12468 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-zinc-900 dark:text-white mb-4 flex items-center gap-2", children: [ 12469 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "p-1.5 bg-indigo-500/10 rounded-lg text-indigo-500", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2.5", strokeLinecap: "round", strokeLinejoin: "round", className: "lucide lucide-git-fork", children: [ 12470 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "12", cy: "18", r: "3" }),
12471 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "6", cy: "6", r: "3" }), 12472 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "18", cy: "6", r: "3" }), 12473 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M18 9v2c0 .6-.4 1-1 1H7c-.6 0-1-.4-1-1V9" }), 12474 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12 12v3" }) 12475 ] }) }), 12476 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [ 12477 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: t("watch.variations", "Other Arrangements") }), 12478 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs bg-indigo-500/10 text-indigo-600 dark:text-indigo-400 font-semibold px-2 py-0.5 rounded-full border border-indigo-500/20", children: variations.length }) 12479 ] }) 12480 ] }), 12481 /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-3", children: variations.map((track) => /* @__PURE__ */ jsxRuntimeExports.jsx( 12482 VideoCard, 12483 { 12484 track, 12485 variant: "small", 12486 onSelect: handleSelectTrack, 12487 onToggleWatchLater: toggleWatchLater, 12488 isWatchLater: watchLater.has(track.id), 12489 onAddToQueue, 12490 onAddToPlaylist 12491 }, 12492 track.id 12493 )) }) 12494 ] }),
12495 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full", children: [ 12496 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "font-bold text-zinc-900 dark:text-white mb-4 flex items-center gap-2", children: [ 12497 /* @__PURE__ */ jsxRuntimeExports.jsx(ListVideo, { size: 18, className: "text-indigo-500" }), 12498 t("watch.upNext", "Up Next") 12499 ] }), 12500 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-4", children: [ 12501 nextTrack ? /* @__PURE__ */ jsxRuntimeExports.jsx( 12502 VideoCard, 12503 { 12504 track: nextTrack, 12505 onSelect: () => onPlayNext(), 12506 variant: "grid", 12507 isWatchLater: false, 12508 onToggleWatchLater: () => { 12509 } 12510 } 12511 ) : /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500", children: "No more tracks in queue." }), 12512 relatedTracks.filter((t2) => t2.id !== activeTrack.id && t2.id !== (nextTrack == null ? void 0 : nextTrack.id)).map((track, i) => /* @__PURE__ */ jsxRuntimeExports.jsx( 12513 VideoCard, 12514 { 12515 track, 12516 variant: "grid", 12517 onSelect: handleSelectTrack, 12518 onToggleWatchLater: toggleWatchLater, 12519 isWatchLater: watchLater.has(track.id), 12520 onAddToQueue, 12521 onAddToPlaylist 12522 }, 12523 track.id + i 12524 )), 12525 relatedTracks.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-zinc-500", children: "Finding related tracks..." }) 12526 ] }) 12527 ] }) 12528 ] }) 12529 ] }),
12530 /* @__PURE__ */ jsxRuntimeExports.jsx(AdSlot, { type: "watchBottom" }), 12531 /* @__PURE__ */ jsxRuntimeExports.jsx( 12532 TagModal, 12533 { 12534 isOpen: isTagModalOpen, 12535 onClose: () => setIsTagModalOpen(false), 12536 onSubmit: handleProposeTag 12537 } 12538 ), 12539 isEditModalOpen && currentUser && /* @__PURE__ */ jsxRuntimeExports.jsx( 12540 EditTrackModal, 12541 { 12542 isOpen: isEditModalOpen, 12543 onClose: () => setIsEditModalOpen(false), 12544 track: activeTrack, 12545 currentUser, 12546 onSuccess: () => { 12547 window.location.reload(); 12548 } 12549 } 12550 ), 12551 isReportModalOpen && /* @__PURE__ */ jsxRuntimeExports.jsx( 12552 ReportModal, 12553 { 12554 isOpen: isReportModalOpen, 12555 onClose: () => setIsReportModalOpen(false), 12556 trackId: activeTrack.id, 12557 trackTitle: activeTrack.title, 12558 isUnowned: !activeTrack.uploader || activeTrack.uploader.id === -1 || activeTrack.uploader.name === "Community Contributor", 12559 currentUser 12560 } 12561 ),
12562 /* @__PURE__ */ jsxRuntimeExports.jsx( 12563 TrackEventsModal, 12564 { 12565 isOpen: !!eventsInstrument, 12566 onClose: () => setEventsInstrument(null), 12567 instrument: eventsInstrument, 12568 currentTime, 12569 tempo: ((_e = activeTrack.midiMetadata) == null ? void 0 : _e.tempo) || 120, 12570 tickDiv: ((_f = activeTrack.midiMetadata) == null ? void 0 : _f.tickDiv) || 480 12571 } 12572 ), 12573 isReuploadModalOpen && /* @__PURE__ */ jsxRuntimeExports.jsx( 12574 ReuploadModal, 12575 { 12576 isOpen: isReuploadModalOpen, 12577 onClose: () => setIsReuploadModalOpen(false), 12578 trackId: activeTrack.id, 12579 trackTitle: activeTrack.title, 12580 onSuccess: () => { 12581 window.location.reload(); 12582 } 12583 } 12584 ), 12585 isBgModalOpen && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-full max-w-md bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 rounded-2xl p-6 shadow-2xl space-y-4", children: [
12586 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between pb-2 border-b border-zinc-100 dark:border-zinc-800", children: [ 12587 /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-lg font-bold text-zinc-900 dark:text-white flex items-center gap-2", children: [ 12588 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xl", children: "ð¨" }), 12589 " Customize Orchestra Background" 12590 ] }), 12591 /* @__PURE__ */ jsxRuntimeExports.jsx( 12592 "button", 12593 { 12594 onClick: () => setIsBgModalOpen(false), 12595 className: "text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-200 transition-colors text-xl font-bold", 12596 children: "Ã" 12597 } 12598 ) 12599 ] }), 12600 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-3", children: [ 12601 /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-xs text-zinc-500 dark:text-zinc-400 leading-relaxed font-sans", children: [ 12602 "Upload a custom landscape image (PNG, JPG, or WebP) to use as the background for this MIDI track in the Orchestra visualizer. Recommended size is ",
12603 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold", children: "1920x1080" }), 12604 "." 12605 ] }), 12606 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-3 bg-amber-50 dark:bg-amber-950/20 border border-amber-200 dark:border-amber-900/50 rounded-xl text-amber-800 dark:text-amber-300 space-y-1", children: [ 12607 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold uppercase tracking-wider block", children: "âï¸ Legal & Copyright Warning" }), 12608 /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] leading-relaxed", children: "Please ensure you own the copyright or have the necessary legal rights/licenses to upload and distribute this image. By uploading, you agree to our Terms of Service and accept all legal liability for copyright infringement." }) 12609 ] }), 12610 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-2 border-dashed border-zinc-200 dark:border-zinc-800 rounded-xl p-4 flex flex-col items-center justify-center gap-2 bg-zinc-50 dark:bg-zinc-950/50", children: [
12611 /* @__PURE__ */ jsxRuntimeExports.jsx( 12612 "input", 12613 { 12614 type: "file", 12615 accept: "image/png, image/jpeg, image/jpg, image/webp", 12616 onChange: (e) => { 12617 if (e.target.files && e.target.files[0]) { 12618 setBgUploadFile(e.target.files[0]); 12619 setBgUploadError(null); 12620 } 12621 }, 12622 className: "hidden", 12623 id: "bg-file-input" 12624 } 12625 ), 12626 /* @__PURE__ */ jsxRuntimeExports.jsx( 12627 "label", 12628 { 12629 htmlFor: "bg-file-input", 12630 className: "px-4 py-2 bg-indigo-600 hover:bg-indigo-700 text-white rounded-lg text-xs font-bold cursor-pointer transition-colors shadow-sm", 12631 children: "Select Image File" 12632 } 12633 ), 12634 bgUploadFile && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs font-medium text-zinc-700 dark:text-zinc-300 max-w-full truncate font-mono", children: [ 12635 "ð ", 12636 bgUploadFile.name, 12637 " (", 12638 (bgUploadFile.size / 1024 / 1024).toFixed(2), 12639 " MB)" 12640 ] }) 12641 ] }),
12642 /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-start gap-2.5 cursor-pointer p-1", children: [ 12643 /* @__PURE__ */ jsxRuntimeExports.jsx( 12644 "input", 12645 { 12646 type: "checkbox", 12647 checked: hasAcceptedCopyright, 12648 onChange: (e) => setHasAcceptedCopyright(e.target.checked), 12649 className: "mt-0.5 w-4 h-4 rounded border-zinc-300 text-indigo-600 focus:ring-indigo-500 cursor-pointer" 12650 } 12651 ), 12652 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] text-zinc-600 dark:text-zinc-400 select-none", children: "I confirm that I own this image or have full legal permission to upload and use it as a background." }) 12653 ] }), 12654 bgUploadError && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-xs text-red-500 font-medium", children: [ 12655 "â ï¸ ", 12656 bgUploadError 12657 ] }) 12658 ] }),
12659 /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-end gap-2 pt-2 border-t border-zinc-100 dark:border-zinc-800", children: [ 12660 /* @__PURE__ */ jsxRuntimeExports.jsx( 12661 "button", 12662 { 12663 onClick: () => setIsBgModalOpen(false), 12664 className: "px-4 py-2 text-xs font-semibold text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-200 transition-colors", 12665 disabled: bgUploadStatus === "uploading", 12666 children: "Cancel" 12667 } 12668 ), 12669 /* @__PURE__ */ jsxRuntimeExports.jsx( 12670 "button", 12671 { 12672 onClick: handleBackgroundUpload, 12673 disabled: !bgUploadFile || !hasAcceptedCopyright || bgUploadStatus === "uploading", 12674 className: "px-4 py-2 text-xs font-bold text-white bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg shadow transition-colors flex items-center gap-1.5", 12675 children: bgUploadStatus === "uploading" ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
12676 /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-block w-3.5 h-3.5 border-2 border-white border-t-transparent rounded-full animate-spin" }), 12677 "Uploading..." 12678 ] }) : bgUploadStatus === "success" ? "â Uploaded!" : "Upload Background" 12679 } 12680 ) 12681 ] }) 12682 ] }) }) 12683 ] 12684 } 12685 ); 12686}; 12687export { 12688 WatchPage as default 12689};
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.