1import { r as reactExports, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2import { i as instance, B as Backend, a as Browser, b as initReactI18next } from "./vendor-i18n-BC7ZMXWQ.js"; 3(function polyfill() { 4 const relList = document.createElement("link").relList; 5 if (relList && relList.supports && relList.supports("modulepreload")) { 6 return; 7 } 8 for (const link of document.querySelectorAll('link[rel="modulepreload"]')) { 9 processPreload(link); 10 } 11 new MutationObserver((mutations) => { 12 for (const mutation of mutations) { 13 if (mutation.type !== "childList") { 14 continue; 15 } 16 for (const node of mutation.addedNodes) { 17 if (node.tagName === "LINK" && node.rel === "modulepreload") 18 processPreload(node); 19 } 20 } 21 }).observe(document, { childList: true, subtree: true }); 22 function getFetchOpts(link) { 23 const fetchOpts = {}; 24 if (link.integrity) fetchOpts.integrity = link.integrity; 25 if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy; 26 if (link.crossOrigin === "use-credentials") 27 fetchOpts.credentials = "include"; 28 else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit"; 29 else fetchOpts.credentials = "same-origin"; 30 return fetchOpts; 31 } 32 function processPreload(link) { 33 if (link.ep) 34 return; 35 link.ep = true; 36 const fetchOpts = getFetchOpts(link); 37 fetch(link.href, fetchOpts); 38 } 39})(); 40class MemoryStorage { 41 constructor() { 42 this.data = {}; 43 } 44 get length() { 45 return Object.keys(this.data).length; 46 } 47 clear() { 48 this.data = {}; 49 } 50 getItem(key) { 51 return Object.prototype.hasOwnProperty.call(this.data, key) ? this.data[key] : null; 52 } 53 key(index) { 54 const keys = Object.keys(this.data); 55 return keys[index] || null; 56 } 57 removeItem(key) { 58 delete this.data[key]; 59 } 60 setItem(key, value) { 61 this.data[key] = String(value); 62 } 63} 64const isStorageSupported = (type) => { 65 try { 66 const storage = window[type]; 67 if (!storage) return false; 68 const testKey = "__storage_test__"; 69 storage.setItem(testKey, testKey); 70 storage.removeItem(testKey); 71 return true; 72 } catch (e) { 73 return false; 74 } 75}; 76if (!isStorageSupported("localStorage")) { 77 try { 78 const mockLocalStorage = new MemoryStorage(); 79 Object.defineProperty(window, "localStorage", { 80 value: mockLocalStorage, 81 configurable: true, 82 writable: true, 83 enumerable: true 84 }); 85 console.warn("localStorage is blocked by browser settings; using in-memory fallback."); 86 } catch (e) { 87 console.error("Failed to shim localStorage:", e); 88 } 89} 90if (!isStorageSupported("sessionStorage")) { 91 try { 92 const mockSessionStorage = new MemoryStorage(); 93 Object.defineProperty(window, "sessionStorage", { 94 value: mockSessionStorage, 95 configurable: true, 96 writable: true, 97 enumerable: true 98 }); 99 console.warn("sessionStorage is blocked by browser settings; using in-memory fallback."); 100 } catch (e) { 101 console.error("Failed to shim sessionStorage:", e); 102 } 103} 104class ApiClient { 105 constructor(apiBase2) { 106 console.log("[API] Initializing LIVE mode...", apiBase2); 107 } 108 getDataSource() { 109 return "LIVE"; 110 } 111 async geolocate() { 112 return this.request("/system/geolocate"); 113 } 114 async heartbeat() { 115 return this.request("/system/heartbeat"); 116 } 117 async getAppVersionDate() { 118 return this.request("/system/version"); 119 } 120 setDataSource(dataSource) { 121 console.warn(`[API] DataSource switching to ${dataSource} is disabled. System is locked to LIVE mode.`); 122 } 123 async get(endpoint, options = {}) { 124 return this.request(endpoint, { 125 method: "GET", 126 ...options 127 }); 128 } 129 async post(endpoint, data = {}) { 130 return this.request(endpoint, { 131 method: "POST", 132 body: JSON.stringify(data) 133 }); 134 } 135 async put(endpoint, data = {}) { 136 return this.request(endpoint, { 137 method: "PUT", 138 body: JSON.stringify(data) 139 }); 140 } 141 async delete(endpoint, data) { 142 return this.request(endpoint, { 143 method: "DELETE", 144 ...data ? { body: JSON.stringify(data) } : {} 145 }); 146 } 147 async request(endpoint, options = {}) { 148 const cleanEndpoint = endpoint.startsWith("/api") ? endpoint.slice(4) : endpoint; 149 const normalizedEndpoint = cleanEndpoint.startsWith("/") ? cleanEndpoint : `/${cleanEndpoint}`; 150 const start = performance.now(); 151 const res = await fetch(`/api${normalizedEndpoint}`, { 152 ...options, 153 credentials: "include", 154 headers: { 155 "Content-Type": "application/json", 156 ...options.headers 157 } 158 }); 159 const duration = performance.now() - start; 160 if (typeof window !== "undefined") {
161 try { 162 const queryTimeHeader = res.headers.get("X-Query-Time"); 163 if (queryTimeHeader) { 164 const queryTime = parseFloat(queryTimeHeader); 165 if (!isNaN(queryTime)) { 166 const currentPath = window.location.pathname; 167 const globalCtx = window; 168 if (globalCtx.__lastPathname !== currentPath) { 169 globalCtx.__lastPathname = currentPath; 170 globalCtx.__pageQueryTime = 0; 171 globalCtx.__pageQueryCount = 0; 172 globalCtx.__pageApiTime = 0; 173 } 174 globalCtx.__pageQueryTime = (globalCtx.__pageQueryTime || 0) + queryTime; 175 globalCtx.__pageQueryCount = (globalCtx.__pageQueryCount || 0) + 1; 176 globalCtx.__pageApiTime = (globalCtx.__pageApiTime || 0) + duration; 177 window.dispatchEvent(new CustomEvent("page-query-timing", { 178 detail: { 179 queryTime: globalCtx.__pageQueryTime, 180 queryCount: globalCtx.__pageQueryCount, 181 apiTime: globalCtx.__pageApiTime 182 } 183 })); 184 } 185 } 186 } catch (err) { 187 } 188 } 189 if (!res.ok) { 190 const err = await res.json().catch(() => ({ error: res.statusText })); 191 throw new Error(err.error || `API Error: ${res.status} ${res.statusText}`); 192 } 193 return res.json(); 194 } 195 async resetDatabase(target) { 196 await this.post("/admin/database/reset", { target }); 197 } 198 // --- Internal Helpers --- 199 async purgeServerCache(trackId) { 200 await this.post("/internal/purge-cache", { trackId }).catch(() => { 201 }); 202 } 203 // --- Database Inspection --- 204 async getDatabaseSchema() { 205 return this.request("/admin/database/schema").catch(() => []); 206 } 207 async getTableData(table, params) { 208 var _a; 209 const query = new URLSearchParams({ 210 page: String(params.page || 1), 211 pageSize: String(params.pageSize || 10), 212 ...params.search && { search: params.search }, 213 ...params.sortBy && { sortBy: params.sortBy }, 214 ...params.sortDir && { sortDir: params.sortDir }, 215 ...((_a = params.columnFilters) == null ? void 0 : _a.length) && { columnFilters: JSON.stringify(params.columnFilters) } 216 }); 217 return this.request(`/admin/database/table/${table}?${query.toString()}`).catch(() => ({ data: [], total: 0 })); 218 } 219 // --- Staff Mode & Inline Moderation --- 220 async toggleStaffMode() { 221 return this.post("/auth/staff-mode/toggle"); 222 } 223 async getStaffModeStatus() { 224 return this.request("/auth/staff-mode/status"); 225 } 226 async toggleTrackVisibility(trackId) { 227 return this.post(`/admin/tracks/${trackId}/toggle-visibility`); 228 } 229 async toggleCommentVisibility(commentId) { 230 return this.post(`/admin/comments/${commentId}/toggle-visibility`); 231 } 232 async getActiveStaffStats() { 233 return this.request("/admin/reports/active"); 234 } 235 async resolveReportV2(reportId, action = "dismissed") { 236 await this.post(`/admin/reports/${reportId}/resolve`, { action }); 237 } 238 async governUser(userId, data) { 239 await this.post(`/admin/users/${userId}/govern`, data); 240 } 241 async updateDatabaseRecord(table, id, data) { 242 await this.put(`/admin/database/table/${table}/${id}`, data); 243 } 244 async deleteDatabaseRecord(table, id) { 245 await this.delete(`/admin/database/table/${table}/${id}`); 246 } 247 // --- User & Auth --- 248 async checkUserExists(email) { 249 return this.post("/auth/check", { email: email.toLowerCase().trim() }).then((data) => data.exists).catch(() => false); 250 } 251 async login(email, pass) { 252 return this.post("/auth/login", { email: email.toLowerCase().trim(), password: pass }); 253 } 254 async register(name, email, pass, avatarChoice, language) { 255 return this.post("/auth/register", { name, email, password: pass, avatarChoice, language }); 256 } 257 async reloadOAuthStrategies() { 258 await this.request("/auth/reload", { method: "POST" }); 259 } 260 async getMe() { 261 try { 262 const res = await this.request("/auth/me"); 263 return (res == null ? void 0 : res.user) || null; 264 } catch { 265 return null; 266 } 267 } 268 async verifyEmail(token) { 269 return this.request(`/auth/verify-email?token=${token}`).then(() =>
269 true).catch(() => false); 270 } 271 async resendVerification(email, language) { 272 await this.post("/auth/resend-verification", { email, language }); 273 } 274 async forgotPassword(email, language) { 275 return this.post("/auth/forgot-password", { email, language }); 276 } 277 async resetPassword(token, password) { 278 return this.post("/auth/reset-password", { token, password }); 279 } 280 async lookupHandle(query) { 281 return this.post("/auth/lookup-handle", { query }); 282 } 283 async updateEmail(email) { 284 return this.post("/auth/update-email", { email }); 285 } 286 async socialLogin(provider) { 287 const { url } = await this.request(`/auth/${provider}/url`); 288 const authWindow = window.open(url, "oauth_popup", "width=600,height=700"); 289 if (!authWindow) throw new Error("Please allow popups to connect your account."); 290 return new Promise((resolve, reject) => { 291 const handleMessage = (event) => { 292 var _a; 293 const origin = event.origin; 294 if (!origin.endsWith(".run.app") && !origin.includes("localhost") && !origin.includes("hamienet.com")) { 295 return; 296 } 297 if (((_a = event.data) == null ? void 0 : _a.type) === "OAUTH_AUTH_SUCCESS") { 298 window.removeEventListener("message", handleMessage); 299 this.request("/auth/status").then((data) => { 300 if (data.authenticated) { 301 resolve(data.user); 302 } else { 303 reject(new Error("Social login failed.")); 304 } 305 }).catch(reject); 306 } 307 }; 308 window.addEventListener("message", handleMessage); 309 const timer = setInterval(() => { 310 if (authWindow.closed) { 311 clearInterval(timer); 312 window.removeEventListener("message", handleMessage); 313 reject(new Error("Login window closed.")); 314 } 315 }, 1e3); 316 }); 317 } 318 async getAuthProviders() { 319 try { 320 return await this.request("/auth/providers"); 321 } catch (e) { 322 console.warn("Failed to fetch auth providers:", e); 323 return { google: false, facebook: false, apple: false }; 324 } 325 } 326 async getNotifications(userId) { 327 return this.request(`/notifications?userId=${userId}`).catch(() => []); 328 } 329 async getNotificationPage(cursor) { 330 const url = cursor ? `/notifications/page?cursor=${cursor}` : `/notifications/page`; 331 return this.request(url).catch(() => ({ items: [], hasMore: false, nextCursor: null })); 332 } 333 async markNotificationRead(id) { 334 await this.post(`/notifications/${id}/read`); 335 } 336 async markAllNotificationsRead() { 337 await this.post(`/notifications/read-all`); 338 } 339 async getSubscriptions(userId) { 340 return this.request(`/users/${userId}/subscriptions`).catch(() => []); 341 } 342 async toggleSubscription(userId, artistId) { 343 await this.post(`/users/${userId}/subscribe`, { artistId }); 344 } 345 async getChannelProfile(username) { 346 return this.request(`/users/profile/${encodeURIComponent(username)}`); 347 } 348 async getSubscribedTracks(userId) { 349 return this.request(`/tracks/subscriptions?userId=${userId}`); 350 } 351 async getSubscriptionTracks(userId) { 352 return this.request(`/tracks/subscriptions?userId=${userId}`).catch(() => []); 353 } 354 async updateUser(id, updates) { 355 return this.put(`/users/${id}`, updates); 356 } 357 async getUserLimits(userId) { 358 return this.request(`/users/${userId}/limits`); 359 } 360 // --- Content --- 361 async getContentStats() { 362 return this.request("/content/stats").catch(() => ({ totalTracks: 87e3, totalUsers: 103e3, totalArtists: 24e3 })); 363 } 364 async getContributors() { 365 const result = await this.request("/content/contributors").catch(() => []); 366 return Array.isArray(result) ? result : []; 367 } 368 async getTracks(query, category, sort, limit, skip, mine, username) { 369 const params = new URLSearchParams({ 370 q: query || "", 371 cat: category || "all", 372 ...sort && { sort }, 373 ...limit && { limit: limit.toString() }, 374 ...skip && { skip: skip.toString() }, 375 ...mine && { mine: "true" }, 376 ...username && { username } 377 }); 378 return this.request(`/tracks?${params.toString()}`).catch(() => []); 379 } 380 async advancedSearch(params) { 381 const queryParams = new URLSearchParams(); 382 for (const [key, val] of Object.entries(params)) { 383 if (val === void 0 || val === null || val === "") continue; 384 if (Array.isArray(val)) { 385 val.forEach((v) => queryParams.append(key, v)); 386 } else { 387 queryParams.append(key, String(val)); 388 } 389 } 390 return this.request(`/tracks/advanced-search?${queryParams.toString()}`); 391 } 392 async getSearchSuggestions(q) { 393 return this.request(`/search/suggestions?q=${encodeURIComponent(q)}`).catch(() => []); 394 } 395 async getRecommendations(userId) { 396 const url = userId ? `/tracks/recommendations?userId=${userId}` : "/tracks/recommendations"; 397 return this.request(url).catch(() => []); 398 } 399 async getRelatedTracks(trackId) { 400 return this.request(`/tracks/${trackId}/related`).catch(() => []); 401 } 402 async getVariations(trackId) { 403 return this.request(`/tracks/${trackId}/variations`).catch(() => []); 404 } 405 async getCategories(skip, limit) { 406 const params = new URLSearchParams(); 407 if (skip !== void 0) params.append("skip", skip.toString()); 408 if (limit !== void 0) params.append("limit", limit.toString()); 409 const query = params.toString() ? `?${params.toString()}` : ""; 410 return this.request(`/categories${query}`); 411 } 412 async getTrack(id) { 413 return this.request(`/tracks/${id}`); 414 } 415 async getAdminTrack(id) { 416 return this.getTrack(String(id)); 417 } 418 async deleteTrackAsUser(id, userId) { 419 return this.request(`/tracks/${id}`, { 420 method: "DELETE", 421 body: JSON.stringify({ userId }) 422 }); 423 } 424 async recordPlay(id) { 425 await this.post(`/tracks/${id}/play`).catch(() => { 426 }); 427 } 428 async uploadTrack(data) { 429 const res = await fetch("/api/upload", { 430 method: "POST", 431 body: data, 432 credentials: "include" 433 }); 434 if (!res.ok) { 435 const err = await res.json().catch(() => ({ error: res.statusText })); 436 const errorObj = new Error(err.error || err.message || "Upload failed"); 437 errorObj.duplicate = err.duplicate; 438 errorObj.existingTracks = err.existingTracks; 439 throw errorObj; 440 } 441 return await res.json(); 442 } 443 async reuploadMidi(trackId, file) { 444 const formData = new FormData(); 445 formData.append("midiFile", file); 446 const res = await fetch(`/api/tracks/${trackId}/reupload-midi`, { 447 method: "POST", 448 body: formData, 449 credentials: "include" 450 }); 451 if (!res.ok) { 452 const err = await res.json().catch(() => ({ error: "Re-upload failed" })); 453 throw new Error(err.error || "Re-upload failed"); 454 } 455 return await res.json(); 456 } 457 async processOMR(files, preprocess = false) { 458 const formData = new FormData();
459 files.forEach((f) => formData.append("pages", f)); 460 formData.append("preprocess", preprocess ? "true" : "false"); 461 const headers = {}; 462 if (typeof window !== "undefined") { 463 const testKey = localStorage.getItem("digitizer_test_key"); 464 if (testKey) { 465 headers["x-digitizer-test-key"] = testKey; 466 } 467 } 468 const res = await fetch("/api/omr/process", { 469 method: "POST", 470 body: formData, 471 headers, 472 credentials: "include" 473 }); 474 if (!res.ok) { 475 const err = await res.json().catch(() => ({ error: "OMR processing failed" })); 476 throw new Error(err.error || "OMR processing failed"); 477 } 478 return await res.json(); 479 } 480 async getOMRStatus(jobId) { 481 const res = await fetch(`/api/omr/status/${jobId}`, { 482 method: "GET", 483 credentials: "include" 484 }); 485 if (!res.ok) { 486 const err = await res.json().catch(() => ({ error: "Failed to fetch OMR status" })); 487 throw new Error(err.error || "Failed to fetch OMR status"); 488 } 489 return await res.json(); 490 } 491 async getAdminUser(id) { 492 return this.request(`/admin/users/${id}`); 493 } 494 async editTrackWiki(id, data) { 495 return this.put(`/tracks/${id}/wiki-edit`, data); 496 } 497 async validateContent(fields) { 498 return this.post("/content/validate", { fields }); 499 } 500 async getComments(trackId) { 501 return this.request(`/tracks/${trackId}/comments`).catch(() => []); 502 } 503 async postComment(trackId, userId, text, parentId) { 504 return this.post(`/tracks/${trackId}/comments`, { userId, text, parentId }); 505 } 506 async voteComment(commentId, userId, value) { 507 return this.post(`/comments/${commentId}/vote`, { userId, value }); 508 } 509 async searchMelody(notes, mode = "easy") { 510 return this.post("/tracks/search-melody", { notes, mode }).catch(() => []); 511 } 512 async getTags(params = {}) { 513 const query = params.type ? `?type=${params.type}` : ""; 514 return this.request(`/tags${query}`).catch(() => []); 515 } 516 async getSuggestedTags(q) { 517 return this.request(`/tags/suggest?q=${encodeURIComponent(q)}`).catch(() => []); 518 } 519 async suggestArtistsByTitle(title) { 520 return this.request(`/tags/suggest-artists-by-title?title=${encodeURIComponent(title)}`).catch(() => []); 521 } 522 async getCanonicalWorksByComposer(composer) { 523 return this.request(`/collections/works?composer=${encodeURIComponent(composer)}`).catch(() => []); 524 } 525 async autofillTags(artist, title) { 526 const params = new URLSearchParams(); 527 if (artist) params.append("artist", artist); 528 if (title) params.append("title", title); 529 if (!params.toString()) return []; 530 return this.request(`/tags/autofill?${params.toString()}`).catch(() => []); 531 } 532 async getTrendingTags() { 533 return this.request("/tags/trending").catch(() => []); 534 } 535 async proposeTag(trackId, tagName, userId) { 536 return this.post(`/tracks/${trackId}/tags/propose`, { tagName, userId }); 537 } 538 async voteTag(trackId, tagName, value, userId) { 539 await this.post(`/tracks/${trackId}/tags/vote`, { tagName, value, userId }); 540 } 541 async getNavSections() { 542 return this.request("/navigation").catch(() => []); 543 } 544 // Alias for compatibility 545 async getNavigation() { 546 return this.getNavSections(); 547 } 548 async createNavSection(data) { 549 return this.post("/admin/navigation/section", data); 550 } 551 async updateNavSection(id, data) { 552 return this.put(`/admin/navigation/section/${id}`, data); 553 } 554 async deleteNavSection(id) { 555 await this.delete(`/admin/navigation/section/${id}`); 556 } 557 async createNavLink(data) { 558 return this.post("/admin/navigation/link", data); 559 } 560 async updateNavLink(id, data) { 561 return this.put(`/admin/navigation/link/${id}`, data); 562 } 563 async deleteNavLink(id) { 564 await this.delete(`/admin/navigation/link/${id}`); 565 } 566 async reorderNavSections(items) { 567 await this.put("/admin/navigation/reorder/sections", { items }); 568 } 569 async reorderNavLinks(items) { 570 await this.put("/admin/navigation/reorder/links", { items }); 571 } 572 async seedNavigation() { 573 await this.post("/admin/navigation/seed"); 574 } 575 // --- Admin --- 576 async getSystemInfo() { 577 return this.request("/system/info"); 578 } 579 async getSystemStorage(refresh) { 580 return this.request(`/system/storage${refresh ? "?refresh=true" : ""}`); 581 } 582 async getAdminUsers({ page = 1, pageSize = 10, search = "", filters = {} }) { 583 const query = new URLSearchParams({ page: page.toString(), pageSize: pageSize.toString(), search, role: filters.role || "" }); 584 return this.request(`/admin/users?${query}`); 585 } 586 async getAdminTracks({ page = 1, pageSize = 10, search = "", filters = {}, sortBy = "", sortDir = "asc" }) { 587 const query = new URLSearchParams({ 588 page: page.toString(), 589 pageSize: pageSize.toString(), 590 search, 591 status: filters.status || "", 592 id: filters.id || "", 593 userId: filters.userId || "", 594 sortBy: sortBy || "", 595 sortDir: sortDir || "asc" 596 }); 597 return this.request(`/admin/tracks?${query}`); 598 } 599 async getTickets({ page = 1, pageSize = 10, search = "", filters = {}, sortBy = "", sortDir = "desc" } = {}) { 600 const query = new URLSearchParams({ 601 page: page.toString(), 602 pageSize: pageSize.toString(), 603 search: search || "", 604 category: filters.category || "", 605 status: filters.status || "", 606 ...sortBy && { sortBy }, 607 ...sortDir && { sortDir } 608 }); 609 return this.request(`/admin/tickets?${query.toString()}`); 610 } 611 async getActivityLogs({ page = 1, pageSize = 10, category, search, startDate, endDate, userId, actions }) { 612 const params = new URLSearchParams({ page: String(page), pageSize: String(pageSize) }); 613 if (category) params.append("category", category); 614 if (search) params.append("search", search); 615 if (startDate) params.append("startDate", startDate); 616 if (endDate) params.append("endDate", endDate); 617 if (userId) params.append("userId", String(userId)); 618 if (actions) params.append("actions", actions.join(",")); 619 return this.request(`/admin/logs?${params.toString()}`); 620 } 621 async clearActivityLogs({ category, beforeDate, source, userId, clearLegacy }) { 622 return this.delete("/admin/logs", { category, beforeDate, source, userId, clearLegacy }); 623 } 624 async deleteActivityLog(id) { 625 return this.delete(`/admin/logs/${id}`); 626 } 627 async getAdminTags({ page = 1, pageSize = 10, search = "", sortBy = "", sortDir = "asc" }) { 628 const query = new URLSearchParams({ 629 page: page.toString(), 630 pageSize: pageSize.toString(), 631 search, 632 sortBy: sortBy || "", 633 sortDir: sortDir || "asc" 634 }); 635 return this.request(`/admin/tags?${query}`); 636 } 637 async getAdminRegions(params) { 638 const queryString = new URLSearchParams(params).toString(); 639 return this.request(`/admin/regions?${queryString}`); 640 } 641 async getAdminRoles() { 642 return this.request("/admin/roles"); 643 } 644 // --- Pinned Modules --- 645 async getAdminPins() { 646 return this.request("/admin/pins"); 647 } 648 async saveAdminPins(pinnedModules) { 649 await this.post("/admin/pins", { pinnedModules }); 650 } 651 // --- Dynamic Modules --- 652 async getAdminModules() { 653 return this.request("/admin/modules"); 654 } 655 async getSitePages() { 656 return this.request("/pages").catch(() => []); 657 } 658 // --- Admin Mutations --- 659 async updateAdminModule(id, content, changeLog, author) { 660 await this.put(`/admin/modules/${id}
660`, { content, changeLog, author }); 661 } 662 async createAdminModule(name, path, description, content, author) { 663 await this.post("/admin/modules", { name, path, description, content, author }); 664 } 665 async getModuleHistory(id) { 666 return this.request(`/admin/modules/${id}/history`); 667 } 668 async revertModuleVersion(id, versionId) { 669 await this.post(`/admin/modules/${id}/revert`, { versionId }); 670 } 671 async createSitePage(name, path, description, content, author, visibility) { 672 await this.post("/admin/pages", { name, path, description, content, author, visibility }); 673 } 674 async updateSitePage(id, updates, changeLog, author) { 675 await this.put(`/admin/pages/${id}`, { ...updates, changeLog, author }); 676 } 677 async getSitePageHistory(id) { 678 return this.request(`/admin/pages/${id}/history`); 679 } 680 async revertSitePageVersion(id, versionId) { 681 await this.post(`/admin/pages/${id}/revert`, { versionId }); 682 } 683 // --- System Settings --- 684 async getSystemSettings() { 685 return this.request("/settings").catch(() => []); 686 } 687 async getGscStatus() { 688 return this.request("/admin/gsc/status"); 689 } 690 async generateGscSitemap() { 691 return this.post("/admin/gsc/generate-sitemap"); 692 } 693 async getSystemSetting(key) { 694 return this.request(`/settings/${key}`).catch(() => null); 695 } 696 async getAdminSystemSetting(key) { 697 return this.request(`/admin/settings/${key}`).catch(() => null); 698 } 699 async saveSystemSetting(key, value) { 700 await this.post(`/admin/settings/${key}`, { value }); 701 if (typeof window !== "undefined") window.dispatchEvent(new CustomEvent("settings-update", { detail: { key, value } })); 702 } 703 // --- UX & Performance Monitoring --- 704 async getPerformanceMonitoringStatus() { 705 return this.request("/system/monitoring-status").catch(() => ({ active: false })); 706 } 707 async startPerformanceMonitoring(durationMinutes) { 708 return this.post("/admin/performance/monitoring-session/start", { durationMinutes }); 709 } 710 async stopPerformanceMonitoring() { 711 return this.post("/admin/performance/monitoring-session/stop", {}); 712 } 713 async getPerformanceSessions() { 714 return this.request("/admin/performance/sessions").catch(() => []); 715 } 716 async getPerformanceSessionDetails(id) { 717 return this.request(`/admin/performance/sessions/${id}`); 718 } 719 async analyzePerformanceSession(id) { 720 return this.post(`/admin/performance/sessions/${id}/analyze`, {}); 721 } 722 async deletePerformanceSession(id) { 723 return this.delete(`/admin/performance/sessions/${id}`); 724 } 725 async getLabConfig() { 726 return this.request("/settings/lab_config").then((r) => (r == null ? void 0 : r.value) || null).catch(() => null); 727 } 728 async toggleLabTool(toolId, enabled) { 729 return this.put(`/settings/lab_config/${toolId}`, { enabled }); 730 } 731 // --- Localization --- 732 async getLocales() { 733 return this.request("/locales"); 734 } 735 async updateUserLanguage(language) { 736 return this.put("/user/language", { language }); 737 } 738 async getTranslation(lang, ns) { 739 return this.request(`/locales/${lang}/${ns}.json`).catch(() => ({})); 740 } 741 async createLocale(locale) { 742 await this.post("/admin/locales", locale); 743 } 744 async deleteLocale(code) { 745 await this.delete(`/admin/locales/${code}`); 746 } 747 async updateLocale(code, updates) { 748 await this.put(`/admin/locales/${code}`, updates); 749 } 750 async getRegions() { 751 return this.request("/regions"); 752 } 753 // --- Help Center --- 754 async getHelpCategories() { 755 return this.request("/help/categories").catch(() => []); 756 } 757 async getHelpArticles() { 758 return this.request("/help/articles").catch(() => []); 759 } 760 async saveHelpCategory(category) { 761 await this.post("/admin/help/categories", category); 762 } 763 async saveHelpArticle(article) { 764 await this.post("/admin/help/articles", article); 765 } 766 async deleteHelpCategory(id) { 767 await this.delete(`/admin/help/categories/${id}`); 768 } 769 async deleteHelpArticle(id) { 770 await this.delete(`/admin/help/articles/${id}`); 771 } 772 // --- Live Monitor --- 773 async getLiveMonitorSnapshot() { 774 return this.request("/admin/live-monitor/snapshot"); 775 } 776 async getLiveMonitorHistory(range) { 777 return this.request(`/admin/live-monitor/history?range=${range}`); 778 } 779 async createTicket(userId, subject, category, priority, description, honeypot) { 780 await this.post("/tickets", { userId, subject, category, priority, description, _hp: honeypot }); 781 } 782 async getMyTickets() { 783 return this.request("/my-tickets").catch(() => []); 784 } 785 async getTicketDetails(id) { 786 return this.request(`/tickets/${id}`); 787 } 788 async replyToTicket(ticketId, text, sendEmail = false) { 789 await this.post(`/tickets/${ticketId}/reply`, { text, sendEmail }); 790 } 791 async updateTicketStatus(ticketId, status) { 792 await this.post(`/tickets/${ticketId}/status`, { status }); 793 } 794 async resolveTicket(ticketId) { 795 await this.post(`/tickets/${ticketId}/resolve`); 796 } 797 async deleteTicketMessage(ticketId, msgId) { 798 await this.post(`/tickets/${ticketId}/messages/${msgId}/delete`); 799 } 800 async getFileSystem() { 801 return this.request("/admin/fs").catch(() => []); 802 } 803 async readDevAgentFile(paths) { 804 return this.post("/dev-agent/read", { paths }); 805 } 806 async commitDevAgentFile(changes, message) { 807 return this.post("/dev-agent/commit", { changes, message }); 808 } 809 async createRegion(data) { 810 await this.post("/admin/regions", data); 811 } 812 async updateRegion(code, data) { 813 await this.put(`/admin/regions/${code}`, data); 814 } 815 async deleteRegion(code) { 816 await this.delete(`/admin/regions/${code}`); 817 } 818 async createTag(name, slug, translations, type = "TAG") { 819 await this.post("/admin/tags", { name, slug, translations, type }); 820 } 821 async updateTag(id, name, slug, translations, type) { 822 await this.put(`/admin/tags/${id}`, { name, slug, translations, type }); 823 } 824 async deleteTag(id) { 825 await this.delete(`/admin/tags/${id}`); 826 } 827 async mergeTags(sourceId, targetId) { 828 await this.post("/admin/tags/merge", { sourceId, targetId }); 829 } 830 async previewTagCleanup() {
831 return this.post("/admin/tags/cleanup-preview"); 832 } 833 async executeTagCleanup(options) { 834 return this.post("/admin/tags/cleanup", options); 835 } 836 async getScanTagReferences() { 837 return this.request("/admin/scan-tag/references"); 838 } 839 async createScanTagReference(data) { 840 return this.post("/admin/scan-tag/references", data); 841 } 842 async updateScanTagReference(id, data) { 843 return this.put(`/admin/scan-tag/references/${id}`, data); 844 } 845 async deleteScanTagReference(id) { 846 return this.delete(`/admin/scan-tag/references/${id}`); 847 } 848 async getScanTagStats() { 849 return this.request("/admin/scan-tag/stats"); 850 } 851 async getScanTagHealth() { 852 return this.request("/admin/scan-tag/health"); 853 } 854 async runMelodyScan(options) { 855 return this.post("/admin/scan-tag/scan", options || {}); 856 } 857 async applyScanTagMatches(matches) { 858 return this.post("/admin/scan-tag/apply", { matches }); 859 } 860 async getMusicBrainzStatus() { 861 return this.request("/admin/musicbrainz/status"); 862 } 863 async musicbrainzTestLookup(artist, title) { 864 return this.post("/admin/musicbrainz/test-lookup", { artist, title }); 865 } 866 async triggerMusicBrainzSync() { 867 return this.post("/admin/musicbrainz/trigger-sync"); 868 } 869 async getMusicBrainzSyncLogs() { 870 return this.request("/admin/musicbrainz/sync-logs"); 871 } 872 async getTrustConfig() { 873 return this.request("/admin/trust/config").catch(() => ({ baseScore: 50, weights: { verifiedEmail: 10, hasAvatar: 5, hasBio: 5, accountAgeDays: 0.1, uploads: 5, comments: 1, likesReceived: 1, votesGiven: 0.5, reportsReceived: -10, dislikesReceived: -1 }, caps: { maxScore: 100, minScore: 0 }, thresholds: { trustedUser: 80, autoSuspend: 20 } })); 874 } 875 async saveTrustConfig(config) { 876 await this.post("/admin/trust/config", config); 877 } 878 async recalculateAllTrustScores() { 879 await this.post("/admin/trust/recalculate"); 880 } 881 calculateUserScoreDetails(user, config) { 882 var _a, _b; 883 const { weights, baseScore, caps } = config; 884 const breakdown = { 885 base: baseScore, 886 identity: { 887 email: user.emailVerified ? weights.verifiedEmail : 0, 888 avatar: user.avatar && !user.avatar.includes("default") ? weights.hasAvatar : 0, 889 bio: user.description && user.description.length > 5 ? weights.hasBio : 0 890 }, 891 age: { value: 0, points: 0 }, 892 uploads: { value: ((_a = user._count) == null ? void 0 : _a.midis) || user.legacyMidiCount || 0, points: 0 }, 893 comments: { value: ((_b = user._count) == null ? void 0 : _b.comments) || user.legacyPostCount || 0, points: 0 }, 894 likes: { value: user.likesReceivedCount || 0, points: 0 }, 895 votes: { value: user.votesGivenCount || 0, points: 0 }, 896 reports: { value: user.reportsReceivedCount || 0, points: 0 }, 897 dislikes: { value: 0, points: 0 } 898 }; 899 if (user.createdAt) { 900 const days = Math.floor((Date.now() - new Date(user.createdAt).getTime()) / (1e3 * 60 * 60 * 24)); 901 breakdown.age.value = days; 902 breakdown.age.points = Math.min(20, days * weights.accountAgeDays); 903 } 904 breakdown.uploads.points = breakdown.uploads.value * weights.uploads; 905 breakdown.comments.points = breakdown.comments.value * weights.comments; 906 breakdown.likes.points = breakdown.likes.value * weights.likesReceived; 907 breakdown.votes.points = breakdown.votes.value * weights.votesGiven; 908 breakdown.reports.points = breakdown.reports.value * weights.reportsReceived; 909 breakdown.dislikes.points = breakdown.dislikes.value * weights.dislikesReceived; 910 const totalIdentity = (breakdown.identity.email || 0) + (breakdown.identity.avatar || 0) + (breakdown.identity.bio || 0); 911 const totalActivity = breakdown.age.points + breakdown.uploads.points + breakdown.comments.points + breakdown.likes.points + breakdown.votes.points; 912 const totalNegative = breakdown.reports.points + breakdown.dislikes.points; 913 let total = baseScore + totalIdentity + totalActivity + totalNegative; 914 total = Math.max(caps.minScore, Math.min(caps.maxScore, total)); 915 return { 916 total: Math.round(total * 10) / 10, 917 breakdown 918 }; 919 } 920 async getTrustSafetySummary() { 921 return this.request("/admin/trust/summary").catch(() => ({ atRiskUsers: 0, flaggedTracks: 0, pendingItems: 0 })); 922 } 923 async getPendingContent() { 924 return this.request("/admin/trust/queue").catch(() => ({ tracks: [], comments: [] })); 925 } 926 async getFlaggedContent() {
927 return this.request("/admin/trust/flagged").catch(() => ({ users: [], tracks: [] })); 928 } 929 async getReports(status) { 930 return this.request(`/admin/trust/reports?status=${status}`).catch(() => []); 931 } 932 async reportContent(targetId, targetType, reason, details, reporterId) { 933 await this.post("/reports", { targetId, targetType, reason, details, reporterId }); 934 } 935 async resolveTrustItem(type, id, action) { 936 await this.post("/admin/moderation/resolve", { type, id, action }); 937 } 938 async resolveReport(reportId, action) { 939 await this.post("/admin/moderation/resolve-report", { reportId, action }); 940 } 941 async getAnalyticsDashboard() { 942 return this.request("/admin/analytics").catch(() => ({ cohorts: [], searchInsights: [], forecast: [], hotspots: [] })); 943 } 944 async getAnalyticsOverview(range, start, end) { 945 const params = new URLSearchParams({ range }); 946 if (start) params.set("start", start); 947 if (end) params.set("end", end); 948 return this.request(`/admin/analytics/overview?${params}`); 949 } 950 async getAnalyticsTraffic(range, start, end) { 951 const params = new URLSearchParams({ range }); 952 if (start) params.set("start", start); 953 if (end) params.set("end", end); 954 return this.request(`/admin/analytics/traffic?${params}`); 955 } 956 async getAnalyticsContent(range, start, end) { 957 const params = new URLSearchParams({ range }); 958 if (start) params.set("start", start); 959 if (end) params.set("end", end); 960 return this.request(`/admin/analytics/content?${params}`); 961 } 962 async getAnalyticsGames(range, start, end) { 963 const params = new URLSearchParams({ range }); 964 if (start) params.set("start", start); 965 if (end) params.set("end", end); 966 return this.request(`/admin/analytics/games?${params}`); 967 } 968 async getAnalyticsUsers(range, start, end) { 969 const params = new URLSearchParams({ range }); 970 if (start) params.set("start", start); 971 if (end) params.set("end", end); 972 return this.request(`/admin/analytics/users?${params}`); 973 } 974 async getAnalyticsBots(range, start, end) { 975 const params = new URLSearchParams({ range }); 976 if (start) params.set("start", start); 977 if (end) params.set("end", end); 978 return this.request(`/admin/analytics/bots?${params}`); 979 } 980 async getAnalyticsReferrers(range, start, end) { 981 const params = new URLSearchParams({ range }); 982 if (start) params.set("start", start); 983 if (end) params.set("end", end); 984 return this.request(`/admin/analytics/referrers?${params}`); 985 } 986 async getAnalyticsRealtime() { 987 return this.request("/admin/analytics/realtime"); 988 } 989 // Stripe & Supporter Payments API 990 async getPaymentStatus() { 991 return this.request("/payments/status"); 992 } 993 async createCheckoutSession(data) { 994 return this.post("/payments/checkout-session", data); 995 } 996 async createPortalSession() { 997 return this.post("/payments/portal-session", {}); 998 } 999 // Admin Subscriber Analytics & Override API 1000 async getAdminSubscribers(params) { 1001 const query = new URLSearchParams({ 1002 page: String(params.page), 1003 pageSize: String(params.pageSize), 1004 search: params.search || "" 1005 }); 1006 return this.request(`/payments/admin/subscribers?${query.toString()}`); 1007 } 1008 async adminOverrideSupporter(data) { 1009 return this.post("/payments/admin/override", data); 1010 } 1011 async saveEmailConfig(config) { 1012 await this.post("/admin/email/config", config); 1013 } 1014 async testEmailConfig(config, targetEmail) { 1015 return this.post("/admin/email/test", { config, targetEmail }); 1016 } 1017 async getEmailTemplates() { 1018 return this.request("/admin/email/templates"); 1019 } 1020 async saveEmailTemplateOverride(templateKey, language, subject, bodyHtml) { 1021 await this.post(`/admin/email/templates/${templateKey}`, { language, subject, bodyHtml }); 1022 } 1023 async resetEmailTemplateOverride(templateKey, language) { 1024 await this.post(`/admin/email/templates/${templateKey}/reset`, { language }); 1025 } 1026 async getEmailTemplateConfig() { 1027 return this.request("/admin/email/template-config"); 1028 } 1029 async saveEmailTemplateConfig(config) { 1030 await this.post("/admin/email/template-config", config); 1031 } 1032 async adminCreateUser(data) { 1033 return this.post("/admin/users/create", data); 1034 } 1035 async deleteAdminRole(id) { 1036 await this.delete(`/admin/roles/${id}`); 1037 } 1038 async updateAdminRole(id, data) { 1039 await this.put(`/admin/roles/${id}`, data); 1040 }
1041 async createAdminRole(data) { 1042 await this.post("/admin/roles", data); 1043 } 1044 async seedData(type, data) { 1045 await this.post("/admin/seed", { type, data }); 1046 } 1047 async toggleBan(userId, isBanned) { 1048 await this.post(`/admin/users/${userId}/ban`, { isBanned }); 1049 } 1050 async addModNote(userId, note) { 1051 await this.post(`/admin/users/${userId}/note`, { note }); 1052 } 1053 async assignRoleToUser(userId, roleId) { 1054 await this.post(`/admin/users/${userId}/role`, { roleId }); 1055 } 1056 // --- Avatar Management --- 1057 async getDefaultAvatars() { 1058 return this.request("/avatars/defaults").catch(() => []); 1059 } 1060 async uploadAvatar(userId, file) { 1061 const formData = new FormData(); 1062 formData.append("avatar", file); 1063 const res = await fetch(`/api/users/${userId}/avatar`, { method: "POST", body: formData, credentials: "include" }); 1064 if (!res.ok) throw new Error("Avatar upload failed"); 1065 return await res.json(); 1066 } 1067 async uploadDefaultAvatar(file, name) { 1068 const formData = new FormData(); 1069 formData.append("avatar", file); 1070 if (name) formData.append("name", name); 1071 const res = await fetch("/api/admin/avatars/defaults", { method: "POST", body: formData, credentials: "include" }); 1072 if (!res.ok) throw new Error("Default avatar upload failed"); 1073 return await res.json(); 1074 } 1075 async deleteDefaultAvatar(filename) { 1076 await this.delete(`/admin/avatars/defaults/${filename}`); 1077 } 1078 async bulkUpdateTrackMetadata(ids, updates) { 1079 await this.post("/admin/tracks/bulk", { ids, updates }); 1080 } 1081 async updateTrackMetadata(id, updates) { 1082 await this.put(`/admin/tracks/${id}`, updates); 1083 } 1084 async deleteTrack(id) { 1085 await this.delete(`/admin/tracks/${id}`); 1086 } 1087 async searchUsersForMessaging(query) { 1088 return this.request(`/admin/users?search=${encodeURIComponent(query)}`).then((r) => r.data || []).catch(() => []); 1089 } 1090 async getUnreadMessageCount(userId) { 1091 const res = await this.request(`/messages/unread-count?userId=${userId}`).catch(() => ({ count: 0 })); 1092 return res.count || 0; 1093 } 1094 async getConversations(userId) { 1095 return this.request(`/conversations?userId=${userId}`).catch(() => []); 1096 } 1097 async getMessages(userId, otherId) { 1098 return this.request(`/messages/${otherId}?userId=${userId}`).catch(() => []); 1099 } 1100 async sendMessage(senderId, receiverId, content) { 1101 return this.post("/messages", { senderId, receiverId, content }); 1102 } 1103 async markMessagesRead(userId, otherUserId) { 1104 await this.post("/messages/read", { userId, otherUserId }); 1105 } 1106 async getPlaylists(userId) { 1107 return this.request(`/users/${userId}/playlists`).catch(() => []); 1108 } 1109 async createPlaylist(userId, title, isPublic = false) { 1110 return this.post("/playlists", { userId, title, isPublic }); 1111 } 1112 async addTrackToPlaylist(playlistId, trackId) { 1113 await this.post(`/playlists/${playlistId}/tracks`, { trackId }); 1114 } 1115 async softDeleteTrack(trackId, userId) { 1116 await this.delete(`/tracks/${trackId}`, { userId }); 1117 } 1118 // --- Library Features --- 1119 async toggleLikeTrack(trackId, userId) { 1120 return this.post(`/tracks/${trackId}/like`, { userId }); 1121 } 1122 async getUserLikes(userId) { 1123 return this.request(`/users/${userId}/likes`).catch(() => []); 1124 } 1125 async getLikedTracks(userId) { 1126 return this.request(`/users/${userId}/liked-tracks`).catch(() => []); 1127 } 1128 async toggleWatchLaterTrack(trackId, userId) { 1129 return this.post(`/tracks/${trackId}/watch-later`, { userId }); 1130 } 1131 async getUserWatchLater(userId) { 1132 return this.request(`/users/${userId}/watch-later`).catch(() => []); 1133 } 1134 async getWatchLaterTracks(userId) { 1135 return this.request(`/users/${userId}/watch-later-tracks`).catch(() => []); 1136 } 1137 async recordHistory(trackId, userId) { 1138 await this.post(`/tracks/${trackId}/history`, { userId }).catch(() => { 1139 }); 1140 } 1141 async getUserHistory(userId) { 1142 return this.request(`/users/${userId}/history`).catch(() => []); 1143 } 1144 async verifyAdminPin(pin, remember = false) { 1145 try { 1146 return await this.post("/auth/admin-pin", { pin, remember }); 1147 } catch (err) { 1148 console.error("PIN Verification API Error:", err); 1149 throw err; 1150 } 1151 } 1152 async reverifyAdmin(token) { 1153 return this.post("/auth/admin-reverify", { token }); 1154 } 1155 async getAdminStatus() { 1156 return this.request("/auth/admin-status"); 1157 } 1158 // --- Soundfonts --- 1159 async getSoundfonts(options) { 1160 const query = (options == null ? void 0 : options.scan) ? "?scan=true" : ""; 1161 const res = await fetch(`/api/soundfonts${query}`); 1162 if (!res.ok) throw new Error("Failed to fetch soundfonts"); 1163 return res.json(); 1164 } 1165 async setActiveSoundfont(filename) { 1166 await this.post("/admin/soundfonts/active", { filename }); 1167 } 1168 async getSoundfontJobs() { 1169 return this.request("/admin/soundfonts/jobs"); 1170 } 1171 async startSoundfontJob(name, inputPath, jobType = "CONVERT") { 1172 return this.post("/admin/soundfonts/jobs", { name, inputPath, jobType }); 1173 } 1174 async deleteSoundfontJob(id) { 1175 await this.delete(`/admin/soundfonts/jobs/${id}`); 1176 } 1177 async deleteSoundfont(filename) { 1178 await this.delete(`/admin/soundfonts?filename=${encodeURIComponent(filename)}`); 1179 } 1180 async startSoundfontTest(sf2Path, params) { 1181 return this.post("/admin/soundfonts/test", { sf2Path, params }); 1182 } 1183 async autoLevelSoundfont(sf2Path, program, targetDb = -18) { 1184 return this.post("/admin/soundfonts/auto-level", { sf2Path, program, targetDb }); 1185 } 1186 async fetchSourceData(dbConfig, tableName, limit = 1e3, targetModel) { 1187 return this.post("/admin/migration/fetch-source", { dbConfig, tableName, limit, targetModel }); 1188 } 1189 async countCatchUp(dbConfig, sourceTable, targetModel, mapping, limit = 0, startId = 0) { 1190 return this.post("/admin/migration/count-catchup", { dbConfig, sourceTable, targetModel, mapping, limit, startId }); 1191 } 1192 async executeMigration(dbConfig, sourceTable, targetModel, mapping, limit = 0, startId = 0, overwrite = false, verbose = false) { 1193 return this.post("/admin/migration/execute", { dbConfig, sourceTable, targetModel, mapping, limit, startId, overwrite, verbose }); 1194 } 1195 async executeAvatarMigration() { 1196 return this.post("/admin/migration/avatars"); 1197 } 1198 async getMidiExtractionStatus(dbConfig) { 1199 return this.post("/admin/migration/midi-extraction-status", { dbConfig }); 1200 } 1201 async extractMidiFiles(dbConfig, startId, limit) { 1202 return this.post("/admin/migration/extract-midi-files", { dbConfig, startId, limit }); 1203 } 1204 async generateUserHandles() { 1205 return this.post("/admin/migration/generate-handles", {}); 1206 } 1207 async runSmartMigration(dbConfig) { 1208 return this.post("/admin/migration/smart-run", { dbConfig }); 1209 } 1210 async getSoundFonts() { 1211 return this.request("/soundfonts"); 1212 } 1213 async saveGeneratedAsset(data, name) { 1214 return this.post("/admin/assets/save-generated", { data, name }); 1215 } 1216 async getAvailableAssets() { 1217 return this.request("/admin/assets"); 1218 } 1219 async uploadSoundFont(file) { 1220 const formData = new FormData(); 1221 formData.append("soundfont", file); 1222 const res = await fetch("/api/admin/soundfonts", { method: "POST", body: formData, credentials: "include" }); 1223 if (!res.ok) throw new Error("SoundFont upload failed"); 1224 } 1225 async setActiveSoundFont(filename) { 1226 await this.post("/admin/soundfonts/active", { filename }); 1227 } 1228 async reprocessTracks(params) { 1229 await this.post("/admin/tracks/reprocess", params); 1230 } 1231 async analyzeTrackStructure(id) { 1232 await this.post(`/admin/tracks/${id}/analyze-structure`); 1233 } 1234 async getWorkerStatus() { 1235 return this.request("/admin/worker/status"); 1236 } 1237 async flushCache() { 1238 await this.post("/admin/system/flush-cache"); 1239 } 1240 async logError(error, context = "client") { 1241 try {
1242 const message = error instanceof Error ? error.message : String(error); 1243 const stack = error instanceof Error ? error.stack : ""; 1244 await this.post("/log-client-error", { message, stack, url: window.location.href, source: context }); 1245 } catch (err) { 1246 console.error("Failed to log error to server:", err); 1247 } 1248 } 1249 async logEvent(event, details = {}, userId) { 1250 try { 1251 await this.post("/analytics/event", { event, details, userId }); 1252 } catch (err) { 1253 console.error("Failed to log event to server:", err); 1254 } 1255 } 1256 // --- High Scores --- 1257 async getHighscores(trackId, gameKey = "piano-roll", userId) { 1258 let url = `/tracks/${trackId}/highscores?gameKey=${gameKey}`; 1259 if (userId) url += `&userId=${userId}`; 1260 return this.request(url).catch(() => []); 1261 } 1262 async postHighscore(trackId, data) { 1263 await this.post(`/tracks/${trackId}/highscores`, data); 1264 } 1265 async hideComment(commentId, adminId) { 1266 await this.request(`/comments/${commentId}`, { 1267 method: "DELETE", 1268 body: JSON.stringify({ userId: adminId }) 1269 }); 1270 } 1271 async resetStuckTasks() { 1272 return this.post("/admin/worker/reset_stuck"); 1273 } 1274 async clearQueueTasks() { 1275 return this.post("/admin/worker/clear_queue"); 1276 } 1277 async stopAllTasks() { 1278 return this.post("/admin/worker/stop_all"); 1279 } 1280 async clearFailureTasks() { 1281 return this.post("/admin/worker/clear_failures"); 1282 } 1283 async getWorkerLogs() { 1284 return this.request("/admin/worker/logs"); 1285 } 1286 async clearWorkerLogs() { 1287 return this.delete("/admin/worker/logs"); 1288 } 1289 async getWorkerPipelineSteps() { 1290 return this.request("/admin/worker/steps"); 1291 } 1292 async getWorkerInstances() { 1293 return this.request("/admin/workers/instances"); 1294 } 1295 async spawnWorkerInstance(count, scale) { 1296 return this.post("/admin/workers/instances/spawn", { count, scale }); 1297 } 1298 async closeIdleWorkerInstances() { 1299 return this.post("/admin/workers/instances/close-idle"); 1300 } 1301 async terminateWorkerInstance(pid) { 1302 return this.post(`/admin/workers/instances/${pid}/terminate`); 1303 } 1304 async runMIDIIntegrityCheck(startId, endId) { 1305 const params = {}; 1306 if (startId !== void 0 && startId !== null) params.startId = startId.toString(); 1307 if (endId !== void 0 && endId !== null) params.endId = endId.toString(); 1308 const queryStr = new URLSearchParams(params).toString(); 1309 return this.request(queryStr ? `/admin/workers/integrity-check?${queryStr}` : "/admin/workers/integrity-check"); 1310 } 1311 async getMIDIMap(startId, endId) { 1312 const params = {}; 1313 if (startId !== void 0 && startId !== null) params.startId = startId.toString(); 1314 if (endId !== void 0 && endId !== null) params.endId = endId.toString(); 1315 const queryStr = new URLSearchParams(params).toString(); 1316 return this.request(`/admin/workers/midi-map?${queryStr}`); 1317 } 1318 async reprocessUnhealthyMIDIs(filters, options) { 1319 return this.post("/admin/workers/reprocess-unhealthy", { filters, options }); 1320 } 1321 async requeueMissingFound() { 1322 return this.post("/admin/workers/requeue-missing-found"); 1323 } 1324 async getRequeueMissingStatus() { 1325 return this.request("/admin/workers/requeue-missing-status"); 1326 } 1327 async getQueueAllUnhealthyStatus() { 1328 return this.request("/admin/workers/queue-all-unhealthy-status"); 1329 } 1330 async reprocessMIDIRange(startId, endId, reprocessSteps, skipCompleted) { 1331 return this.post("/admin/workers/reprocess-range", { startId, endId, reprocessSteps, skipCompleted }); 1332 } 1333 async prioritizeMidi(midiId) { 1334 return this.post("/admin/workers/prioritize", { midiId }); 1335 } 1336 async replaceMidi(trackId, file) { 1337 const formData = new FormData(); 1338 formData.append("midi", file); 1339 const res = await fetch(`/api/admin/tracks/${trackId}/replace-midi`, { 1340 method: "POST", 1341 body: formData, 1342 credentials: "include" 1343 }); 1344 if (!res.ok) { 1345 const err = await res.json().catch(() => ({})); 1346 throw new Error(err.error || "MIDI replacement upload failed"); 1347 } 1348 return await res.json(); 1349 } 1350 async logActivity(data) { 1351 try { 1352 await this.post("/admin/logs", data); 1353 } catch (e) { 1354 console.error("Failed to report activity", e); 1355 } 1356 } 1357 // --- Hamie Tribunal --- 1358 async getTribunalCases(status) { 1359 const query = status ? `?status=${status}` : ""; 1360 return this.request(`/tribunal/cases${query}`).catch(() => []); 1361 } 1362 async getTribunalCase(caseId) { 1363 return this.request(`/tribunal/cases/${caseId}`); 1364 } 1365 async voteTribunalCase(caseId, verdict, sentenceId, reason) { 1366 return this.post(`/tribunal/cases/${caseId}/vote`, { verdict, sentenceId, reason }); 1367 } 1368 async getMyTribunalVotes() { 1369 return this.request("/tribunal/my-votes").catch(() => []); 1370 } 1371 async getJailCases(status) { 1372 const query = status ? `?status=${status}` : ""; 1373 return this.request(`/admin/jail/cases${query}`).catch(() => []); 1374 } 1375 async resolveJailCase(caseId, action, sentenceId) { 1376 await this.post(`/admin/jail/cases/${caseId}/resolve`, { action, sentenceId }); 1377 } 1378 async getSpamConfig() { 1379 return this.request("/admin/spam/config"); 1380 } 1381 async saveSpamConfig(config) { 1382 await this.post("/admin/spam/config", config); 1383 } 1384 async uploadArchiveBatch(data) { 1385 const res = await fetch("/api/admin/archive-import/upload-batch", { 1386 method: "POST", 1387 body: data, 1388 credentials: "include" 1389 }); 1390 if (!res.ok) { 1391 const err = await res.json().catch(() => ({ error: res.statusText })); 1392 throw new Error(err.error || `Batch Upload failed: ${res.status}`); 1393 } 1394 return await res.json(); 1395 } 1396 async getStagingStats() { 1397 const res = await fetch("/api/admin/archive-import/staging/stats", { credentials: "include" }); 1398 if (!res.ok) throw new Error(`Failed to fetch staging stats: ${res.statusText}`); 1399 return await res.json(); 1400 } 1401 async clearStaging() { 1402 const res = await fetch("/api/admin/archive-import/staging/clear", { 1403 method: "POST", 1404 credentials: "include" 1405 }); 1406 if (!res.ok) throw new Error(`Failed to clear staging folder: ${res.statusText}`); 1407 return await res.json(); 1408 } 1409 async uploadToStaging(data) { 1410 const res = await fetch("/api/admin/archive-import/staging/upload", { 1411 method: "POST", 1412 body: data, 1413 credentials: "include" 1414 }); 1415 if (!res.ok) { 1416 const err = await res.json().catch(() => ({ error: res.statusText })); 1417 throw new Error(err.error || `Staged Upload failed: ${res.status}`); 1418 } 1419 return await res.json(); 1420 } 1421 async getStagingFiles(page = 1, limit = 50, search = "") { 1422 const url = `/api/admin/archive-import/staging/list?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`; 1423 const res = await fetch(url, { credentials: "include" }); 1424 if (!res.ok) throw new Error(`Failed to fetch staging list: ${res.statusText}`); 1425 return await res.json(); 1426 }
1427 async importStagingBatch(items, overwrite) { 1428 const res = await fetch("/api/admin/archive-import/staging/import", { 1429 method: "POST", 1430 headers: { "Content-Type": "application/json" }, 1431 body: JSON.stringify({ items, overwrite }), 1432 credentials: "include" 1433 }); 1434 if (!res.ok) { 1435 const err = await res.json().catch(() => ({ error: res.statusText })); 1436 throw new Error(err.error || `Staged Import failed: ${res.status}`); 1437 } 1438 return await res.json(); 1439 } 1440 async getLakhStatus() { 1441 const res = await fetch("/api/admin/archive-import/lakh/status", { credentials: "include" }); 1442 if (!res.ok) throw new Error(`Failed to fetch Lakh status: ${res.statusText}`); 1443 return await res.json(); 1444 } 1445 async getLakhList(page = 1, limit = 10, search = "") { 1446 const url = `/api/admin/archive-import/lakh/list?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`; 1447 const res = await fetch(url, { credentials: "include" }); 1448 if (!res.ok) throw new Error(`Failed to fetch Lakh list: ${res.statusText}`); 1449 return await res.json(); 1450 } 1451 async setupLakhDemo() { 1452 const res = await fetch("/api/admin/archive-import/lakh/setup-demo", { 1453 method: "POST", 1454 credentials: "include" 1455 }); 1456 if (!res.ok) throw new Error(`Failed to setup Lakh demo seeds: ${res.statusText}`); 1457 return await res.json(); 1458 } 1459 async importLakhTracks(hashes, items, overwrite) { 1460 const res = await fetch("/api/admin/archive-import/lakh/import", { 1461 method: "POST", 1462 headers: { "Content-Type": "application/json" }, 1463 body: JSON.stringify({ hashes, items, overwrite }), 1464 credentials: "include" 1465 }); 1466 if (!res.ok) { 1467 const err = await res.json().catch(() => ({ error: res.statusText })); 1468 throw new Error(err.error || `Lakh Import failed: ${res.status}`); 1469 } 1470 return await res.json(); 1471 } 1472 async startLakhImportAll(overwrite) { 1473 const res = await fetch("/api/admin/archive-import/lakh/import-all", { 1474 method: "POST", 1475 headers: { "Content-Type": "application/json" }, 1476 body: JSON.stringify({ overwrite }), 1477 credentials: "include" 1478 }); 1479 if (!res.ok) { 1480 const err = await res.json().catch(() => ({ error: res.statusText })); 1481 throw new Error(err.error || `Lakh Import All failed: ${res.status}`); 1482 } 1483 return await res.json(); 1484 } 1485 async getLakhImportAllStatus() { 1486 const res = await fetch("/api/admin/archive-import/lakh/import-all-status", { 1487 method: "GET", 1488 credentials: "include" 1489 }); 1490 if (!res.ok) throw new Error(`Failed to get Lakh import-all status: ${res.statusText}`); 1491 return await res.json(); 1492 } 1493 async stopLakhImportAll() { 1494 const res = await fetch("/api/admin/archive-import/lakh/import-all-stop", { 1495 method: "POST", 1496 credentials: "include" 1497 }); 1498 if (!res.ok) throw new Error(`Failed to stop Lakh import-all: ${res.statusText}`); 1499 return await res.json(); 1500 } 1501 async getAriaStatus() { 1502 const res = await fetch("/api/admin/archive-import/aria/status", { credentials: "include" }); 1503 if (!res.ok) throw new Error(`Failed to fetch Aria status: ${res.statusText}`); 1504 return await res.json(); 1505 } 1506 async getAriaList(page = 1, limit = 10, search = "") { 1507 const url = `/api/admin/archive-import/aria/list?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}`; 1508 const res = await fetch(url, { credentials: "include" }); 1509 if (!res.ok) throw new Error(`Failed to fetch Aria list: ${res.statusText}`); 1510 return await res.json(); 1511 } 1512 async importAriaTracks(hashes, items, overwrite) { 1513 const res = await fetch("/api/admin/archive-import/aria/import", { 1514 method: "POST", 1515 headers: { "Content-Type": "application/json" }, 1516 body: JSON.stringify({ hashes, items, overwrite }), 1517 credentials: "include" 1518 }); 1519 if (!res.ok) { 1520 const err = await res.json().catch(() => ({ error: res.statusText })); 1521 throw new Error(err.error || `Aria Import failed: ${res.status}`); 1522 } 1523 return await res.json(); 1524 } 1525 async startAriaImportAll(overwrite) { 1526 const res = await fetch("/api/admin/archive-import/aria/import-all", { 1527 method: "POST", 1528 headers: { "Content-Type": "application/json" }, 1529 body: JSON.stringify({ overwrite }), 1530 credentials: "include" 1531 }); 1532 if (!res.ok) { 1533 const err = await res.json().catch(() => ({ error: res.statusText })); 1534 throw new Error(err.error || `Aria Import All failed: ${res.status}`); 1535 } 1536 return await res.json(); 1537 } 1538 async getAriaImportAllStatus() { 1539 const res = await fetch("/api/admin/archive-import/aria/import-all-status", { 1540 method: "GET", 1541 credentials: "include" 1542 }); 1543 if (!res.ok) throw new Error(`Failed to get Aria import-all status: ${res.statusText}`); 1544 return await res.json(); 1545 } 1546 async stopAriaImportAll() { 1547 const res = await fetch("/api/admin/archive-import/aria/import-all-stop", { 1548 method: "POST", 1549 credentials: "include" 1550 }); 1551 if (!res.ok) throw new Error(`Failed to stop Aria import-all: ${res.statusText}`); 1552 return await res.json(); 1553 } 1554 async getPianocoreStatus() { 1555 const res = await fetch("/api/admin/archive-import/pianocore/status", { credentials: "include" }); 1556 if (!res.ok) throw new Error(`Failed to fetch Pianocore status: ${res.statusText}`); 1557 return await res.json(); 1558 } 1559 async getPianocoreList(page = 1, limit = 10, search = "", refresh = false) { 1560 const url = `/api/admin/archive-import/pianocore/list?page=${page}&limit=${limit}&search=${encodeURIComponent(search)}${refresh ? "&refresh=true" : ""}`; 1561 const res = await fetch(url, { credentials: "include" }); 1562 if (!res.ok) throw new Error(`Failed to fetch Pianocore list: ${res.statusText}`); 1563 return await res.json(); 1564 }
1565 async importPianocoreTracks(hashes, items, overwrite) { 1566 const res = await fetch("/api/admin/archive-import/pianocore/import", { 1567 method: "POST", 1568 headers: { "Content-Type": "application/json" }, 1569 body: JSON.stringify({ hashes, items, overwrite }), 1570 credentials: "include" 1571 }); 1572 if (!res.ok) { 1573 const err = await res.json().catch(() => ({ error: res.statusText })); 1574 throw new Error(err.error || `Pianocore Import failed: ${res.status}`); 1575 } 1576 return await res.json(); 1577 } 1578 async startPianocoreImportAll(overwrite) { 1579 const res = await fetch("/api/admin/archive-import/pianocore/import-all", { 1580 method: "POST", 1581 headers: { "Content-Type": "application/json" }, 1582 body: JSON.stringify({ overwrite }), 1583 credentials: "include" 1584 }); 1585 if (!res.ok) { 1586 const err = await res.json().catch(() => ({ error: res.statusText })); 1587 throw new Error(err.error || `Pianocore Import All failed: ${res.status}`); 1588 } 1589 return await res.json(); 1590 } 1591 async getPianocoreImportAllStatus() { 1592 const res = await fetch("/api/admin/archive-import/pianocore/import-all-status", { 1593 method: "GET", 1594 credentials: "include" 1595 }); 1596 if (!res.ok) throw new Error(`Failed to get Pianocore import-all status: ${res.statusText}`); 1597 return await res.json(); 1598 } 1599 async stopPianocoreImportAll() { 1600 const res = await fetch("/api/admin/archive-import/pianocore/import-all-stop", { 1601 method: "POST", 1602 credentials: "include" 1603 }); 1604 if (!res.ok) throw new Error(`Failed to stop Pianocore import-all: ${res.statusText}`); 1605 return await res.json(); 1606 } 1607 async getClusterCredentials() { 1608 return this.request("/admin/cluster/load"); 1609 } 1610 async saveClusterCredentials(credentials) { 1611 return this.post("/admin/cluster/save", { credentials }); 1612 } 1613 async getClusterRsyncStatus() { 1614 return this.request("/admin/cluster/rsync/status"); 1615 } 1616 async controlClusterRsync(action, node, mode = "fast") { 1617 return this.post("/admin/cluster/rsync/control", { action, node, mode }); 1618 } 1619 // --- Cluster DB Status Response Contract --- 1620 // If the backend /api/admin/cluster/db/status response shape ever changes, 1621 // update this interface AND the parseRows calls in web/routes/admin/cluster.js. 1622 async getClusterDbStatus() { 1623 return this.request("/admin/cluster/db/status"); 1624 } 1625 async testOpenRouterAPI(prompt, model) { 1626 return this.post("/admin/test-openrouter", { prompt, model }); 1627 } 1628 // --- Keyboard Emulator Presets --- 1629 async getKeyboardEmulatorPresets() { 1630 return this.request("/v1/keyboard-emulator/presets"); 1631 } 1632 async createKeyboardEmulatorPreset(name, state) { 1633 return this.post("/v1/keyboard-emulator/presets", { name, state }); 1634 } 1635 async updateKeyboardEmulatorPreset(id, name, state) { 1636 return this.put(`/v1/keyboard-emulator/presets/${id}`, { name, state }); 1637 } 1638 async deleteKeyboardEmulatorPreset(id) { 1639 return this.delete(`/v1/keyboard-emulator/presets/${id}`); 1640 } 1641 async resetClusterDbSubscription(node) { 1642 return this.post("/admin/cluster/db/resync", { node }); 1643 } 1644 async controlClusterDbSubscription(action, node = "both") { 1645 return this.post("/admin/cluster/db/control", { action, node }); 1646 } 1647 async compareClusterDb(table = "Midi") { 1648 return this.request(`/admin/cluster/db/compare?table=${encodeURIComponent(table)}`); 1649 } 1650 async syncClusterDbRows(node, table, ids) { 1651 return this.post("/admin/cluster/db/sync-rows", { node, table, ids }); 1652 } 1653 async repairClusterPublication() { 1654 return this.post("/admin/cluster/db/repair-publication", {}); 1655 } 1656 async getClusterDbLogs() { 1657 return this.request("/admin/cluster/db/logs"); 1658 } 1659 async unblockClusterReplication() { 1660 return this.post("/admin/cluster/db/unblock", {}); 1661 } 1662 async getProjects() { 1663 return this.request("/projects").catch(() => []); 1664 } 1665 async deleteProject(id) { 1666 return this.delete(`/projects/${id}`); 1667 } 1668 // --- Lyrics Endpoints --- 1669 async getLyricsById(id) { 1670 return this.request(`/lyrics/${id}`); 1671 } 1672 async searchLyrics(q, page = 1, limit = 20) { 1673 const controller = typeof AbortController !== "undefined" ? new AbortController() : null; 1674 const timeoutId = controller ? setTimeout(() => controller.abort(), 8e3) : null; 1675 try { 1676 const res = await this.request(`/lyrics/search?q=${encodeURIComponent(q)}&page=${page}&limit=${limit}`, { 1677 signal: controller ? controller.signal : void 0 1678 }); 1679 if (timeoutId) clearTimeout(timeoutId); 1680 return res || { results: [], total: 0 }; 1681 } catch (e) { 1682 if (timeoutId) clearTimeout(timeoutId); 1683 return { results: [], total: 0 }; 1684 } 1685 } 1686 async submitLyrics(data) { 1687 return this.post("/lyrics", data); 1688 } 1689 async updateLyrics(id, data) { 1690 return this.request(`/lyrics/${id}`, { 1691 method: "PATCH", 1692 body: JSON.stringify(data) 1693 }); 1694 } 1695 async deleteLyrics(id) { 1696 return this.delete(`/lyrics/${id}`); 1697 } 1698 async getDigitizerProjects() { 1699 return this.request("/digitizer/projects").catch(() => []); 1700 } 1701 async getDigitizerProject(id) { 1702 return this.request(`/digitizer/projects/${id}`); 1703 } 1704 async deleteDigitizerProject(id) { 1705 return this.delete(`/digitizer/projects/${id}`); 1706 } 1707 async saveDigitizerProject(formData) { 1708 const res = await fetch(`/api/digitizer/projects/save`, { 1709 method: "POST", 1710 body: formData, 1711 credentials: "include" 1712 }); 1713 if (!res.ok) { 1714 const err = await res.json().catch(() => ({ error: "Failed to save digitizer project" })); 1715 throw new Error(err.error || "Failed to save digitizer project"); 1716 } 1717 return res.json(); 1718 } 1719 async processAudio(formData) { 1720 const res = await fetch("/api/audio-digitizer/process", { 1721 method: "POST", 1722 body: formData, 1723 credentials: "include" 1724 }); 1725 if (!res.ok) { 1726 const err = await res.json().catch(() => ({ error: "Audio transcription job failed" })); 1727 throw new Error(err.error || "Audio transcription job failed"); 1728 } 1729 return await res.json(); 1730 } 1731 async getAudioStatus(jobId) { 1732 return this.request(`/audio-digitizer/status/${jobId}`); 1733 } 1734 // --- Collections Endpoints --- 1735 async getCollections() { 1736 return this.request("/collections"); 1737 } 1738 async searchCollectionSections(query, limit = 24) { 1739 if (!query || query.trim().length < 2) return []; 1740 const res = await this.request(`/collections/search?q=${encodeURIComponent(query.trim())}&limit=${limit}`).catch(() => ({ sections: [] })); 1741 return (res == null ? void 0 : res.sections) || []; 1742 } 1743 async getTrackCollections(trackId) { 1744 return this.request(`/collections/by-track/${trackId}`).catch(() => []); 1745 } 1746 async getRandomCollectionSection() { 1747 return this.request("/collections/random-section"); 1748 } 1749 async getRandomBannerRow() { 1750 return this.request("/collections/random-banner-row"); 1751 } 1752 async getRandomBannerRows(count = 4) { 1753 const res = await this.request(`/collections/random-banner-rows?count=${count}`); 1754 return (res == null ? void 0 : res.rows) || []; 1755 } 1756 async getCollection(slug) { 1757 return this.request(`/collections/${slug}`); 1758 } 1759 async getSectionTracks(slug, sectionId, sort = "popular", limit = 20, skip = 0) { 1760 return this.request(`/collections/${slug}/sections/${sectionId}/tracks?sort=${sort}&limit=${limit}&skip=${skip}`); 1761 } 1762 async getSectionSongs(slug, sectionId) { 1763 return this.request(`/collections/${slug}/sections/${sectionId}/songs`); 1764 } 1765 async getSectionCounts(slug, sectionIds) { 1766 return this.post(`/collections/${slug}/section-counts`, { sectionIds }); 1767 } 1768 async scanSectionSongs(slug, sectionId) { 1769 return this.post(`/collections/${slug}/sections/${sectionId}/songs/scan`); 1770 } 1771 // --- Collections Admin Endpoints --- 1772 async getAdminCollections() { 1773 return this.request("/admin/collections"); 1774 } 1775 async getAdminCollection(id) { 1776 return this.request(`/admin/collections/${id}`); 1777 } 1778 async createAdminCollection(data) { 1779 return this.post("/admin/collections", data); 1780 } 1781 async updateAdminCollection(id, data) { 1782 return this.put(`/admin/collections/${id}`, data); 1783 } 1784 async deleteAdminCollection(id) { 1785 return this.delete(`/admin/collections/${id}`); 1786 } 1787 async createAdminSection(collectionId, data) { 1788 return this.post(`/admin/collections/${collectionId}/sections`, data); 1789 } 1790 async updateAdminSection(collectionId, sectionId, data) { 1791 return this.put(`/admin/collections/${collectionId}/sections/${sectionId}`, data); 1792 } 1793 async deleteAdminSection(collectionId, sectionId) { 1794 return this.delete(`/admin/collections/${collectionId}/sections/${sectionId}`); 1795 } 1796 async curateAdminSection(collectionId, sectionId, trackIds) { 1797 return this.post(`/admin/collections/${collectionId}/sections/${sectionId}/curate`, { trackIds }); 1798 } 1799 async reorderAdminSections(collectionId, sectionIds) { 1800 return this.post(`/admin/collections/${collectionId}/reorder`, { sectionIds }); 1801 } 1802 // Curated Works (Songs) Management 1803 async createSectionSong(collectionId, sectionId, data) { 1804 return this.post(`/admin/collections/${collectionId}/sections/${sectionId}/songs`, data); 1805 } 1806 async updateSectionSong(collectionId, sectionId, songId, data) { 1807 return this.put(`/admin/collections/${collectionId}/sections/${sectionId}/songs/${songId}`, data); 1808 } 1809 async deleteSectionSong(collectionId, sectionId, songId) { 1810 return this.delete(`/admin/collections/${collectionId}/sections/${sectionId}/songs/${songId}`); 1811 } 1812 async reorderSectionSongs(collectionId, sectionId, songIds) { 1813 return this.post(`/admin/collections/${collectionId}/sections/${sectionId}/songs/reorder`, { songIds }); 1814 } 1815 // --- Soundfonts API --- 1816 async getPublicSoundfonts(params) { 1817 const queryParams = new URLSearchParams(); 1818 if (params.page) queryParams.append("page", params.page.toString()); 1819 if (params.limit) queryParams.append("limit", params.limit.toString()); 1820 if (params.sort) queryParams.append("sort", params.sort); 1821 if (params.search) queryParams.append("search", params.search); 1822 if (params.tag) queryParams.append("tag", params.tag); 1823 return this.get(`/soundfonts?${queryParams.toString()}`); 1824 } 1825 async getSoundfont(slug) { 1826 return this.get(`/soundfonts/${slug}`); 1827 } 1828 async uploadSoundfont(formData) { 1829 return this.post("/soundfonts/upload", formData, { 1830 headers: { "Content-Type": "multipart/form-data" } 1831 }); 1832 } 1833 async rateSoundfont(id, rating, reviewText) { 1834 return this.post(`/soundfonts/${id}/rate`, { rating, reviewText }); 1835 } 1836} 1837const apiBase = "/api"; 1838const api = new ApiClient(apiBase); 1839class UxTracker { 1840 constructor() { 1841 this.eventQueue = []; 1842 this.flushInterval = null; 1843 this.initialized = false;
1844 this.activeSessionId = null; 1845 this.expiresAt = null; 1846 this.lastResourceTimestamp = 0; 1847 } 1848 init() { 1849 if (this.initialized || typeof window === "undefined") return; 1850 this.initialized = true; 1851 console.log("[UX Tracker] Initializing UX telemetry logger..."); 1852 this.checkMonitoringStatus(); 1853 this.setupFetchInterceptor(); 1854 this.setupClickInterceptor(); 1855 this.setupResourceTimingMonitor(); 1856 this.flushInterval = setInterval(() => this.flush(), 5e3); 1857 window.addEventListener("beforeunload", () => this.flushSync()); 1858 window.addEventListener("visibilitychange", () => { 1859 if (document.visibilityState === "hidden") { 1860 this.flushSync(); 1861 } 1862 }); 1863 } 1864 async checkMonitoringStatus() { 1865 try { 1866 const res = await window.fetch("/api/system/monitoring-status", { credentials: "include" }); 1867 if (res.ok) { 1868 const data = await res.json(); 1869 this.updateSessionState(data.active ? data.sessionId : null, data.expiresAt || null); 1870 } 1871 } catch (e) { 1872 console.error("[UX Tracker] Status check failed:", e); 1873 } 1874 } 1875 updateSessionState(sessionId, expiresAt) { 1876 this.activeSessionId = sessionId; 1877 this.expiresAt = expiresAt; 1878 window.__activeMonitoringSessionId = sessionId; 1879 if (sessionId) { 1880 console.log(`[UX Tracker] Interaction Logging ACTIVE for session: ${sessionId}`); 1881 } else { 1882 if (window.__activeMonitoringSessionId) { 1883 console.log("[UX Tracker] Interaction Logging INACTIVE."); 1884 } 1885 } 1886 } 1887 logEvent(eventType, data) { 1888 if (typeof window === "undefined") return; 1889 const sessionId = this.activeSessionId || window.__activeMonitoringSessionId; 1890 if (!sessionId) return; 1891 if (this.expiresAt && Date.now() > this.expiresAt) { 1892 this.updateSessionState(null, null); 1893 return; 1894 } 1895 const logEntry = { 1896 url: data.url || window.location.pathname + window.location.search, 1897 eventType, 1898 elementId: data.elementId || null, 1899 path: data.path || null, 1900 duration: data.duration !== void 0 ? Math.round(data.duration) : null, 1901 metadata: data.metadata || {} 1902 }; 1903 this.eventQueue.push(logEntry); 1904 if (this.eventQueue.length >= 15) { 1905 this.flush(); 1906 } 1907 } 1908 setupFetchInterceptor() { 1909 const originalFetch = window.fetch; 1910 const self = this; 1911 window.fetch = async function(input, init) { 1912 const start = performance.now(); 1913 let url = ""; 1914 if (typeof input === "string") { 1915 url = input; 1916 } else if (input instanceof Request) { 1917 url = input.url; 1918 } 1919 const isTelemetry = url.includes("/api/performance/logs") || url.includes("/api/system/monitoring-status");
1920 try { 1921 const response = await originalFetch(input, init); 1922 if (!isTelemetry) { 1923 const monitoringHeader = response.headers.get("X-Monitoring-Active"); 1924 const currentSession = self.activeSessionId; 1925 if (monitoringHeader && monitoringHeader !== currentSession) { 1926 self.updateSessionState(monitoringHeader, Date.now() + 30 * 60 * 1e3); 1927 } else if (monitoringHeader === "" && currentSession) { 1928 self.updateSessionState(null, null); 1929 } 1930 } 1931 const activeSession = self.activeSessionId || window.__activeMonitoringSessionId; 1932 if (activeSession && !isTelemetry) { 1933 const duration = performance.now() - start; 1934 self.logEvent("api_call", { 1935 url, 1936 duration, 1937 metadata: { 1938 method: (init == null ? void 0 : init.method) || "GET", 1939 status: response.status 1940 } 1941 }); 1942 const proxiedResponse = response.clone(); 1943 const wrapBodyMethod = (methodName) => { 1944 const originalMethod = response[methodName]; 1945 response[methodName] = async function() { 1946 const bodyStart = performance.now(); 1947 try { 1948 const result = await originalMethod.call(response); 1949 const bodyDuration = performance.now() - bodyStart; 1950 self.logEvent("download", { 1951 url, 1952 duration: duration + bodyDuration, 1953 // Headers roundtrip + download streaming time 1954 metadata: { 1955 method: (init == null ? void 0 : init.method) || "GET", 1956 status: response.status, 1957 type: methodName, 1958 size: result instanceof Blob ? result.size : void 0 1959 } 1960 }); 1961 return result; 1962 } catch (err) { 1963 throw err; 1964 } 1965 }; 1966 }; 1967 if (url.includes("/secure-download") || url.includes("/storage/tracks") || url.endsWith(".opus") || url.endsWith(".mp3")) { 1968 wrapBodyMethod("blob"); 1969 } 1970 } 1971 return response; 1972 } catch (err) { 1973 const activeSession = self.activeSessionId || window.__activeMonitoringSessionId; 1974 if (activeSession && !isTelemetry) { 1975 const duration = performance.now() - start; 1976 self.logEvent("error", { 1977 url, 1978 duration, 1979 metadata: { 1980 method: (init == null ? void 0 : init.method) || "GET", 1981 error: (err == null ? void 0 : err.message) || String(err) 1982 } 1983 }); 1984 } 1985 throw err; 1986 } 1987 }; 1988 } 1989 setupClickInterceptor() { 1990 document.addEventListener("click", (e) => { 1991 var _a; 1992 const activeSession = this.activeSessionId || window.__activeMonitoringSessionId; 1993 if (!activeSession) return; 1994 const target = e.target; 1995 const interactive = target.closest('button, a, input, select, [role="button"], .btn'); 1996 if (interactive) { 1997 const tag = interactive.tagName.toLowerCase(); 1998 const text = ((_a = interactive.innerText) == null ? void 0 : _a.substring(0, 30).trim()) || interactive.value || ""; 1999 const id = interactive.id || null; 2000 const className = interactive.className || ""; 2001 this.logEvent("click", { 2002 url: window.location.pathname + window.location.search, 2003 elementId: id, 2004 path: tag, 2005 metadata: { 2006 target: tag, 2007 text, 2008 className: className.substring(0, 50) 2009 } 2010 }); 2011 } 2012 }, true); 2013 } 2014 setupResourceTimingMonitor() { 2015 setInterval(() => { 2016 const activeSession = this.activeSessionId || window.__activeMonitoringSessionId; 2017 if (!activeSession) return; 2018 const entries = performance.getEntriesByType("resource"); 2019 const newEntries = entries.filter((entry) => entry.startTime > this.lastResourceTimestamp); 2020 for (const entry of newEntries) { 2021 const isSlow = entry.duration > 800; 2022 const isTargetAsset = entry.name.includes(".opus") || entry.name.includes(".mp3") || entry.name.includes("-opus.js") || entry.name.includes(".mid"); 2023 if (isSlow && isTargetAsset) { 2024 this.logEvent("resource", { 2025 url: entry.name, 2026 duration: entry.duration, 2027 metadata: { 2028 initiatorType: entry.initiatorType, 2029 transferSize: entry.transferSize, 2030 decodedBodySize: entry.decodedBodySize, 2031 encodedBodySize: entry.encodedBodySize 2032 } 2033 }); 2034 } 2035 } 2036 if (entries.length > 0) { 2037 this.lastResourceTimestamp = Math.max(...entries.map((e) => e.startTime)); 2038 } 2039 }, 1e4); 2040 } 2041 async flush() { 2042 if (this.eventQueue.length === 0) return; 2043 const sessionId = this.activeSessionId || window.__activeMonitoringSessionId; 2044 if (!sessionId) { 2045 this.eventQueue = []; 2046 return; 2047 } 2048 const logsToSend = [...this.eventQueue]; 2049 this.eventQueue = []; 2050 try { 2051 const res = await window.fetch("/api/performance/logs", { 2052 method: "POST", 2053 headers: { "Content-Type": "application/json" }, 2054 body: JSON.stringify({ sessionId, logs: logsToSend }), 2055 credentials: "include" 2056 }); 2057 if (!res.ok) { 2058 this.eventQueue = [...logsToSend, ...this.eventQueue].slice(0, 100); 2059 } 2060 } catch (e) { 2061 this.eventQueue = [...logsToSend, ...this.eventQueue].slice(0, 100); 2062 } 2063 } 2064 flushSync() { 2065 const sessionId = this.activeSessionId || window.__activeMonitoringSessionId; 2066 if (!sessionId || this.eventQueue.length === 0) return; 2067 const payload = JSON.stringify({ sessionId, logs: this.eventQueue }); 2068 this.eventQueue = []; 2069 if (navigator.sendBeacon) { 2070 navigator.sendBeacon("/api/performance/logs", new Blob([payload], { type: "application/json" })); 2071 } else {
2072 try { 2073 const xhr = new XMLHttpRequest(); 2074 xhr.open("POST", "/api/performance/logs", false); 2075 xhr.setRequestHeader("Content-Type", "application/json"); 2076 xhr.withCredentials = true; 2077 xhr.send(payload); 2078 } catch (e) { 2079 } 2080 } 2081 } 2082} 2083const uxTracker = new UxTracker(); 2084instance.use(Backend).use(Browser).use(initReactI18next).init({ 2085 fallbackLng: { 2086 "zh-Hant": ["zh-TW", "en"], 2087 "zh-Hans": ["zh-CN", "en"], 2088 "zh": ["zh-CN", "en"], 2089 "default": ["en"] 2090 }, 2091 load: "currentOnly", 2092 // Changed from 'languageOnly' to allow variants like zh-CN 2093 debug: false, 2094 // Backend Configuration 2095 backend: { 2096 loadPath: "/locales/{{lng}}/{{ns}}.json" 2097 }, 2098 react: { 2099 useSuspense: true 2100 // Enable suspense so the UI waits for translations 2101 }, 2102 interpolation: { 2103 escapeValue: false 2104 }, 2105 detection: { 2106 order: ["cookie", "localStorage", "navigator"], 2107 caches: ["cookie", "localStorage"], 2108 lookupCookie: "i18nextLng", 2109 lookupLocalStorage: "i18nextLng", 2110 cookieMinutes: 525600, 2111 // 1 year 2112 cookieOptions: { path: "/", sameSite: "lax" } 2113 } 2114}); 2115const reloadI18nResources = () => { 2116 instance.reloadResources(); 2117}; 2118const NATIVE_LANGUAGE_NAMES = { 2119 "en": "English", 2120 "zh-CN": "ç®ä½ä¸æ", 2121 "zh-TW": "ç¹é«ä¸æ", 2122 "es": "Español", 2123 "fr": "Français", 2124 "de": "Deutsch", 2125 "ja": "æ¥æ¬èª", 2126 "pt": "Português", 2127 "ru": "Ð ÑÑÑкий", 2128 "ko": "íêµì´", 2129 "it": "Italiano", 2130 "id": "Bahasa Indonesia", 2131 "ar": "Ø§ÙØ¹Ø±Ø¨ÙØ©", 2132 "hi": "हिनà¥à¤¦à¥", 2133 "bn": "বাà¦à¦²à¦¾", 2134 "tr": "Türkçe", 2135 "vi": "Tiếng Viá»t", 2136 "pl": "Polski", 2137 "uk": "УкÑаÑнÑÑка", 2138 "nl": "Nederlands", 2139 "th": "ภาษาà¹à¸à¸¢", 2140 "el": "Îλληνικά", 2141 "cs": "ÄeÅ¡tina", 2142 "ro": "RomânÄ", 2143 "sv": "Svenska", 2144 "hu": "Magyar", 2145 "da": "Dansk", 2146 "fi": "Suomi", 2147 "no": "Norsk", 2148 "sk": "SlovenÄina", 2149 "bg": "ÐÑлгаÑÑки", 2150 "hr": "Hrvatski", 2151 "sr": "СÑпÑки", 2152 "ms": "Bahasa Melayu", 2153 "he": "×¢×ר×ת", 2154 "fa": "ÙØ§Ø±Ø³Û", 2155 "ta": "தமிழà¯", 2156 "te": "à°¤à±à°²à±à°à±", 2157 "mr": "मराठà¥", 2158 "ur": "اردÙ", 2159 "gu": "àªà«àªàª°àª¾àª¤à«", 2160 "kn": "à²à²¨à³à²¨à²¡", 2161 "ml": "മലയാളà´", 2162 "pa": "ਪੰà¨à¨¾à¨¬à©", 2163 "fil": "Filipino", 2164 "sw": "Kiswahili", 2165 "jv": "Basa Jawa", 2166 "uz": "OÊ»zbekcha", 2167 "am": "á ááá", 2168 "ne": "नà¥à¤ªà¤¾à¤²à¥", 2169 "si": "à·à·à¶à·à¶½", 2170 "km": "áá¶áá¶ááááá", 2171 "my": "áá¼ááºáá¬á á¬", 2172 "tk": "Türkmen", 2173 "ug": "ئÛÙØºÛرÚÛ", 2174 "lo": "ລາວ", 2175 "ku": "Kurdî", 2176 "ps": "Ù¾ÚØªÙ", 2177 "cy": "Cymraeg", 2178 "ga": "Gaeilge", 2179 "mt": "Malti", 2180 "bs": "Bosanski", 2181 "mk": "ÐакедонÑки", 2182 "sq": "Shqip", 2183 "ka": "á¥áá áá£áá", 2184 "hy": "ÕÕ¡ÕµÕ¥ÖÕ¥Õ¶", 2185 "az": "AzÉrbaycan", 2186 "et": "Eesti", 2187 "lv": "LatvieÅ¡u", 2188 "lt": "Lietuvių", 2189 "sl": "SlovenÅ¡Äina", 2190 "is": "Ãslenska", 2191 "af": "Afrikaans", 2192 "gl": "Galego", 2193 "eu": "Euskara", 2194 "ca": "Català ", 2195 "mn": "Ðонгол", 2196 "la": "Latina", 2197 "eo": "Esperanto", 2198 "yi": "××Ö´××ש" 2199}; 2200const LANGUAGE_REGIONS = { 2201 "Popular": ["en", "zh-CN", "zh-TW", "es", "fr", "de", "ja", "pt", "ru", "ko"], 2202 "Asia": ["zh-CN", "zh-TW", "ja", "ko", "hi", "bn", "vi", "th", "ms", "id", "ta", "te", "mr", "ur", "gu", "kn", "ml", "pa", "fil", "jv", "uz", "ne", "si", "km", "my", "tk", "ug", "lo", "ps", "mn"], 2203 "Europe": ["en", "es", "fr", "de", "pt", "ru", "it", "tr", "pl", "uk", "nl", "el", "cs", "ro", "sv", "hu", "da", "fi", "no", "sk", "bg", "hr", "sr", "cy", "ga", "mt", "bs", "mk", "sq", "ka", "hy", "az", "et", "lv", "lt", "sl", "is", "gl", "eu", "ca", "la", "eo", "yi"], 2204 "Middle East & Africa": ["ar", "he", "fa", "sw", "am", "ku", "af"] 2205}; 2206const COUNTRY_TO_LANGUAGE = { 2207 "US": "en", 2208 "GB": "en", 2209 "CA": "en", 2210 "AU": "en", 2211 "NZ": "en", 2212 "CN": "zh-CN", 2213 "TW": "zh-TW", 2214 "HK": "zh-TW", 2215 "MO": "zh-TW", 2216 "ES": "es", 2217 "MX": "es", 2218 "AR": "es", 2219 "CO": "es", 2220 "PE": "es", 2221 "VE": "es", 2222 "CL": "es", 2223 "FR": "fr", 2224 "BE": "fr", 2225 "CH": "de", 2226 "DE": "de", 2227 "AT": "de", 2228 "JP": "ja", 2229 "KR": "ko", 2230 "BR": "pt", 2231 "PT": "pt", 2232 "RU": "ru", 2233 "BY": "ru", 2234 "KZ": "ru", 2235 "IT": "it", 2236 "ID": "id", 2237 "IN": "hi", 2238 "BD": "bn", 2239 "TR": "tr", 2240 "VN": "vi", 2241 "PL": "pl", 2242 "UA": "uk", 2243 "NL": "nl", 2244 "TH": "th", 2245 "GR": "el", 2246 "CZ": "cs", 2247 "RO": "ro", 2248 "SE": "sv", 2249 "HU": "hu", 2250 "DK": "da", 2251 "FI": "fi", 2252 "NO": "no", 2253 "SK": "sk", 2254 "BG": "bg", 2255 "HR": "hr", 2256 "RS": "sr", 2257 "MY": "ms", 2258 "IL": "he", 2259 "IR": "fa", 2260 "PK": "ur", 2261 "PH": "fil", 2262 "KE": "sw", 2263 "TZ": "sw", 2264 "ZA": "af", 2265 "EG": "ar", 2266 "SA": "ar", 2267 "AE": "ar", 2268 "IQ": "ar", 2269 "MA": "ar", 2270 "DZ": "ar" 2271}; 2272const i18n = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ 2273 __proto__: null, 2274 COUNTRY_TO_LANGUAGE, 2275 LANGUAGE_REGIONS, 2276 NATIVE_LANGUAGE_NAMES, 2277 default: instance, 2278 reloadI18nResources 2279}, Symbol.toStringTag, { value: "Module" })); 2280const DEFAULT_GRAPHICS = { 2281 mainLogo: "", 2282 favicon: "", 2283 defaultThumbnail: "", 2284 seasonalLogos: [], 2285 icons: {}, 2286 iconSets: [] 2287}; 2288const DEFAULT_UI_CONFIG = { 2289 enableLanguageSelector: true, 2290 enableCurrencySelector: true, 2291 enableAppearanceSelector: true 2292}; 2293const DEFAULT_AD_SETTINGS = { 2294 isEnabled: false, 2295 publisherId: "",
2296 enableAutoAds: false, 2297 enableVignettes: false, 2298 enableAnchors: false, 2299 enableNonIntrusiveOnly: false, 2300 adUnits: { 2301 homeTop: { enabled: false, slotId: "" }, 2302 homeBottom: { enabled: false, slotId: "" }, 2303 watchSidebar: { enabled: false, slotId: "" }, 2304 watchBottom: { enabled: false, slotId: "" }, 2305 exploreSidebar: { enabled: false, slotId: "" }, 2306 exploreList: { enabled: false, slotId: "" }, 2307 converterBottom: { enabled: false, slotId: "" } 2308 }, 2309 excludeRoles: ["ADMIN"] 2310}; 2311const SettingsContext = reactExports.createContext(void 0); 2312const SettingsProvider = ({ children }) => { 2313 const getSavedSettings = () => { 2314 const saved = localStorage.getItem("hamie_settings"); 2315 if (saved) { 2316 try { 2317 return JSON.parse(saved); 2318 } catch (e) { 2319 console.error("Failed to parse settings", e); 2320 } 2321 } 2322 return {}; 2323 }; 2324 const initialSettings = getSavedSettings(); 2325 const [language, setLanguageState] = reactExports.useState(initialSettings.language || instance.language || "en"); 2326 const [currency, setCurrencyState] = reactExports.useState(initialSettings.currency || "USD"); 2327 const [country, setCountryState] = reactExports.useState(initialSettings.country || "US"); 2328 const [autoplay, setAutoplayState] = reactExports.useState(initialSettings.autoplay ?? false); 2329 const [audioQuality, setAudioQualityState] = reactExports.useState(initialSettings.audioQuality || "low"); 2330 const [soundfontVolume, setSoundfontVolumeState] = reactExports.useState(() => { 2331 const val = initialSettings.soundfontVolume; 2332 if (val === void 0 || val === 4) { 2333 return 6; 2334 } 2335 return val; 2336 }); 2337 const [availableLanguages, setAvailableLanguages] = reactExports.useState([]); 2338 const [graphics, setGraphics] = reactExports.useState(DEFAULT_GRAPHICS); 2339 const [uiConfig, setUiConfig] = reactExports.useState(DEFAULT_UI_CONFIG); 2340 const [adSettings, setAdSettings] = reactExports.useState(DEFAULT_AD_SETTINGS); 2341 reactExports.useEffect(() => { 2342 if (initialSettings.language) { 2343 instance.changeLanguage(initialSettings.language); 2344 } 2345 const handleLanguageChanged = (lng) => { 2346 setLanguageState(lng); 2347 }; 2348 instance.on("languageChanged", handleLanguageChanged); 2349 api.getLocales().then((locales) => { 2350 setAvailableLanguages(locales.filter((l) => l.isActive).map((l) => ({ code: l.code, name: NATIVE_LANGUAGE_NAMES[l.code] || l.name }))); 2351 }).catch(console.error); 2352 const loadSystemSettings = async () => { 2353 try { 2354 const gfx = await api.getSystemSetting("site_graphics"); 2355 if (gfx && gfx.value) setGraphics(gfx.value); 2356 const ui = await api.getSystemSetting("ui_config"); 2357 if (ui && ui.value) setUiConfig(ui.value); 2358 const ads = await api.getSystemSetting("ad_settings"); 2359 if (ads && ads.value) setAdSettings(ads.value); 2360 } catch (error) { 2361 console.error("Failed to load system settings", error); 2362 } 2363 }; 2364 loadSystemSettings(); 2365 const handleSettingsUpdate = (event) => { 2366 const customEvent = event; 2367 const { key, value } = customEvent.detail; 2368 if (key === "site_graphics") { 2369 setGraphics((prev) => ({ ...prev, ...value })); 2370 } else if (key === "ui_config") { 2371 setUiConfig((prev) => ({ ...prev, ...value })); 2372 } else if (key === "ad_settings") { 2373 setAdSettings((prev) => ({ ...prev, ...value })); 2374 } 2375 }; 2376 window.addEventListener("settings-update", handleSettingsUpdate); 2377 return () => { 2378 window.removeEventListener("settings-update", handleSettingsUpdate); 2379 instance.off("languageChanged", handleLanguageChanged); 2380 }; 2381 }, []); 2382 reactExports.useEffect(() => { 2383 const rtlLanguages = ["ar", "he", "fa", "ur", "ku", "ps", "yi", "sd"]; 2384 const isRtl = rtlLanguages.includes(language); 2385 document.documentElement.dir = isRtl ? "rtl" : "ltr"; 2386 document.documentElement.lang = language; 2387 }, [language]); 2388 const setLanguage = (lang) => { 2389 setLanguageState(lang); 2390 instance.changeLanguage(lang); 2391 const current = JSON.parse(localStorage.getItem("hamie_settings") || "{}"); 2392 localStorage.setItem("hamie_settings", JSON.stringify({ ...current, language: lang })); 2393 api.updateUserLanguage(lang).catch((err) => { 2394 if ((err == null ? void 0 : err.status) !== 401) { 2395 console.error("Failed to save language to profile", err); 2396 } 2397 }); 2398 }; 2399 const setCurrency = (curr) => { 2400 setCurrencyState(curr); 2401 const current = JSON.parse(localStorage.getItem("hamie_settings") || "{}"); 2402 localStorage.setItem("hamie_settings", JSON.stringify({ ...current, currency: curr })); 2403 }; 2404 const setCountry = (c) => { 2405 setCountryState(c); 2406 const current = JSON.parse(localStorage.getItem("hamie_settings") || "{}"); 2407 localStorage.setItem("hamie_settings", JSON.stringify({ ...current, country: c })); 2408 }; 2409 const setAutoplay = (enabled) => {
2410 setAutoplayState(enabled); 2411 const current = JSON.parse(localStorage.getItem("hamie_settings") || "{}"); 2412 localStorage.setItem("hamie_settings", JSON.stringify({ ...current, autoplay: enabled })); 2413 }; 2414 const setAudioQuality = (q) => { 2415 setAudioQualityState(q); 2416 const current = JSON.parse(localStorage.getItem("hamie_settings") || "{}"); 2417 localStorage.setItem("hamie_settings", JSON.stringify({ ...current, audioQuality: q })); 2418 }; 2419 const setSoundfontVolume = (vol) => { 2420 setSoundfontVolumeState(vol); 2421 const current = JSON.parse(localStorage.getItem("hamie_settings") || "{}"); 2422 localStorage.setItem("hamie_settings", JSON.stringify({ ...current, soundfontVolume: vol })); 2423 }; 2424 const formatNumber = (num) => { 2425 return new Intl.NumberFormat(language, { notation: "compact", compactDisplay: "short" }).format(num); 2426 }; 2427 const formatCurrency = (amount) => { 2428 return new Intl.NumberFormat(language, { style: "currency", currency }).format(amount); 2429 }; 2430 return /* @__PURE__ */ jsxRuntimeExports.jsx(SettingsContext.Provider, { value: { 2431 language, 2432 setLanguage, 2433 currency, 2434 setCurrency, 2435 country, 2436 setCountry, 2437 autoplay, 2438 setAutoplay, 2439 audioQuality, 2440 setAudioQuality, 2441 soundfontVolume, 2442 setSoundfontVolume, 2443 formatNumber, 2444 formatCurrency, 2445 availableLanguages, 2446 graphics, 2447 uiConfig, 2448 adSettings 2449 }, children }); 2450}; 2451const useSettings = () => { 2452 const context = reactExports.useContext(SettingsContext); 2453 if (context === void 0) { 2454 throw new Error("useSettings must be used within a SettingsProvider"); 2455 } 2456 return context; 2457}; 2458export { 2459 LANGUAGE_REGIONS as L, 2460 NATIVE_LANGUAGE_NAMES as N, 2461 SettingsProvider as S, 2462 api as a, 2463 uxTracker as b, 2464 i18n as i, 2465 reloadI18nResources as r, 2466 useSettings as u 2467};
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.