diff --git a/bun.lock b/bun.lock index e9356fd..a32b58e 100644 --- a/bun.lock +++ b/bun.lock @@ -11,13 +11,15 @@ "@ffmpeg/util": "^0.12.2", "@kawarp/core": "^1.1.1", "@neutralinojs/lib": "^6.5.0", + "@svta/common-media-library": "^0.18.1", "@uimaxbai/am-lyrics": "^1.1.4", "appwrite": "^23.0.0", "butterchurn": "^2.6.7", "butterchurn-presets": "^2.4.7", "client-zip": "^2.5.0", "cookie-session": "^2.1.1", - "dashjs": "^5.1.1", + "dashjs": "https://github.com/Dash-Industry-Forum/dash.js/archive/refs/tags/v5.1.1.tar.gz", + "eventemitter3": "^5.0.4", "fuse.js": "^7.1.0", "hls.js": "^1.6.15", "jose": "^6.2.0", @@ -27,6 +29,7 @@ "pocketbase": "^0.26.8", "simple-icons": "^16.12.0", "svgo": "^4.0.1", + "url-toolkit": "^2.2.5", "uuid": "^13.0.0", }, "devDependencies": { @@ -537,6 +540,8 @@ "@svta/cml-xml": ["@svta/cml-xml@1.0.1", "", { "peerDependencies": { "@svta/cml-utils": "1.0.1" } }, "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g=="], + "@svta/common-media-library": ["@svta/common-media-library@0.18.1", "", {}, "sha512-VMj1jI8OWphurcozF+dezABUm9Mht6iAsSiKsFUKVT35fddOowvLoGz23Gx6lEHaAHkDy9o/aVi5s9DSp3K15Q=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], @@ -687,7 +692,7 @@ "d": ["d@1.0.2", "", { "dependencies": { "es5-ext": "^0.10.64", "type": "^2.7.2" } }, "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw=="], - "dashjs": ["dashjs@5.1.1", "", { "dependencies": { "@svta/cml-608": "1.0.1", "@svta/cml-cmcd": "1.0.1", "@svta/cml-cmsd": "1.0.1", "@svta/cml-dash": "1.0.1", "@svta/cml-id3": "1.0.1", "@svta/cml-request": "1.0.1", "@svta/cml-xml": "1.0.1", "bcp-47-match": "^2.0.3", "bcp-47-normalize": "^2.3.0", "codem-isoboxer": "0.3.10", "fast-deep-equal": "3.1.3", "html-entities": "^2.5.2", "imsc": "^1.1.5", "localforage": "^1.10.0", "path-browserify": "^1.0.1", "ua-parser-js": "^1.0.37" } }, "sha512-BzNXlUgzEjhuZ5M5hlSp1qIyQHZ7NpXAR0loP9DAAFVZj/ntL1DHeZ7qp/L3bvI4rq50X5indkAZQ3zEHWJoCA=="], + "dashjs": ["dashjs@https://github.com/Dash-Industry-Forum/dash.js/archive/refs/tags/v5.1.1.tar.gz", { "dependencies": { "@svta/cml-608": "1.0.1", "@svta/cml-cmcd": "1.0.1", "@svta/cml-cmsd": "1.0.1", "@svta/cml-dash": "1.0.1", "@svta/cml-id3": "1.0.1", "@svta/cml-request": "1.0.1", "@svta/cml-xml": "1.0.1", "bcp-47-match": "^2.0.3", "bcp-47-normalize": "^2.3.0", "codem-isoboxer": "0.3.10", "fast-deep-equal": "3.1.3", "html-entities": "^2.5.2", "imsc": "^1.1.5", "localforage": "^1.10.0", "path-browserify": "^1.0.1", "ua-parser-js": "^1.0.37" } }, "sha512-lhD1tvEe4PO6t086flm6WfO2Jt1EOIolDQ17F3vLomMthaL1RH96h8peIQTvrDvfSJTRXeisL+CwPj4oud5e9g=="], "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], @@ -791,6 +796,8 @@ "event-emitter": ["event-emitter@0.3.5", "", { "dependencies": { "d": "1", "es5-ext": "~0.10.14" } }, "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA=="], + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + "ext": ["ext@1.7.0", "", { "dependencies": { "type": "^2.7.2" } }, "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw=="], "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], @@ -1401,6 +1408,8 @@ "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "url-toolkit": ["url-toolkit@2.2.5", "", {}, "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg=="], + "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], diff --git a/js/app.js b/js/app.js index d85473e..479f60d 100644 --- a/js/app.js +++ b/js/app.js @@ -416,6 +416,7 @@ document.addEventListener('DOMContentLoaded', async () => { const currentQuality = localStorage.getItem('playback-quality') || 'HI_RES_LOSSLESS'; const player = new Player(audioPlayer, api, currentQuality); + await player.init(); window.monochromePlayer = player; // Initialize tracker @@ -1136,7 +1137,7 @@ document.addEventListener('DOMContentLoaded', async () => { const shuffleBtn = document.getElementById('shuffle-btn'); if (shuffleBtn) shuffleBtn.classList.remove('active'); player.shuffleActive = false; - player.playTrackFromQueue(); + await player.playTrackFromQueue(); } } catch (error) { console.error('Failed to play album:', error); @@ -1167,7 +1168,7 @@ document.addEventListener('DOMContentLoaded', async () => { const shuffleBtn = document.getElementById('shuffle-btn'); if (shuffleBtn) shuffleBtn.classList.remove('active'); player.shuffleActive = false; - player.playTrackFromQueue(); + await player.playTrackFromQueue(); const { showNotification } = await loadDownloadsModule(); showNotification('Shuffling album'); @@ -1235,7 +1236,7 @@ document.addEventListener('DOMContentLoaded', async () => { const shuffleBtn = document.getElementById('shuffle-btn'); if (shuffleBtn) shuffleBtn.classList.remove('active'); player.shuffleActive = false; - player.playTrackFromQueue(); + await player.playTrackFromQueue(); const { showNotification } = await loadDownloadsModule(); showNotification('Shuffling artist discography'); @@ -2183,7 +2184,7 @@ document.addEventListener('DOMContentLoaded', async () => { if (tracks.length > 0) { player.setQueue(tracks, 0); document.getElementById('shuffle-btn').classList.remove('active'); - player.playTrackFromQueue(); + await player.playTrackFromQueue(); } } catch (error) { console.error('Failed to play playlist:', error); @@ -2369,7 +2370,7 @@ document.addEventListener('DOMContentLoaded', async () => { } player.setQueue(allTracks, 0); - player.playTrackFromQueue(); + await player.playTrackFromQueue(); } else { throw new Error('No tracks found across all albums'); } @@ -2398,7 +2399,7 @@ document.addEventListener('DOMContentLoaded', async () => { } player.setQueue(likedTracks, 0); document.getElementById('shuffle-btn').classList.remove('active'); - player.playTrackFromQueue(); + await player.playTrackFromQueue(); } } catch (error) { console.error('Failed to shuffle liked tracks:', error); diff --git a/js/commandPalette.js b/js/commandPalette.js index b9bae51..154f477 100644 --- a/js/commandPalette.js +++ b/js/commandPalette.js @@ -1,6 +1,7 @@ import { debounce } from './utils.js'; import { db } from './db.js'; import Fuse from 'fuse.js'; +import { navigate } from './router.js'; class CommandPalette { constructor() { @@ -66,15 +67,15 @@ class CommandPalette { } init() { - document.addEventListener('keydown', (e) => { + document.addEventListener('keydown', async (e) => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); - this.toggle(); + await this.toggle(); } }); this.input.addEventListener('input', () => this.handleInput()); - this.input.addEventListener('keydown', (e) => this.handleKeydown(e)); + this.input.addEventListener('keydown', async (e) => await this.handleKeydown(e)); this.overlay.addEventListener('click', (e) => { if (e.target === this.overlay) this.close(); @@ -83,17 +84,17 @@ class CommandPalette { this.cacheAllSettings(); } - toggle() { + async toggle() { if (this.isOpen) this.close(); - else this.open(); + else await this.open(); } - open() { + async open() { this.isOpen = true; this.overlay.style.display = 'flex'; this.input.value = '>'; this.input.focus(); - this.handleInput(); + await this.handleInput(); } close() { @@ -101,18 +102,18 @@ class CommandPalette { this.overlay.style.display = 'none'; } - handleInput() { + async handleInput() { const value = this.input.value; this.selectedIndex = 0; if (!value.startsWith('>')) { - this.renderResults([ + await this.renderResults([ { name: 'Type > to use commands', description: 'e.g. >theme White, >play The Whole World Is Free', - action: () => { + action: async () => { this.input.value = '>'; - this.handleInput(); + await this.handleInput(); }, type: 'hint', }, @@ -124,7 +125,7 @@ class CommandPalette { const match = fullQuery.match(/^(\S+)(?:\s+(.*))?$/); if (!match) { - this.renderDefaultCommands(); + await this.renderDefaultCommands(); return; } @@ -140,7 +141,7 @@ class CommandPalette { return; } - this.renderResults([ + await this.renderResults([ { name: `Execute: ${command.name} ${args}`, description: args ? `Run ${command.name} for "${args}"` : command.description, @@ -153,11 +154,11 @@ class CommandPalette { this.debouncedSearch(cmdName, args.trim()); } } else { - this.renderDefaultCommands(cmdName); + await this.renderDefaultCommands(cmdName); } } - handleKeydown(e) { + async handleKeydown(e) { if (e.key === 'ArrowDown') { e.preventDefault(); this.selectedIndex = Math.min(this.selectedIndex + 1, this.results.length - 1); @@ -168,13 +169,13 @@ class CommandPalette { this.updateSelection(); } else if (e.key === 'Enter') { e.preventDefault(); - this.executeSelected(); + await this.executeSelected(); } else if (e.key === 'Escape') { this.close(); } } - renderDefaultCommands(filter = '') { + async renderDefaultCommands(filter = '') { let cmds = this.commands; if (filter) { if (Fuse) { @@ -185,20 +186,20 @@ class CommandPalette { } } - this.renderResults( + await this.renderResults( cmds.map((c) => ({ name: c.name, description: c.description, - action: () => { + action: async () => { this.input.value = `>${c.name} `; - this.handleInput(); + await this.handleInput(); }, type: 'command', })) ); } - renderResults(results) { + async renderResults(results) { this.results = results; this.resultsContainer.innerHTML = ''; @@ -222,9 +223,9 @@ class CommandPalette {
${result.name}${result.description || ''}
`; - div.addEventListener('click', () => { + div.addEventListener('click', async () => { this.selectedIndex = index; - this.executeSelected(); + await this.executeSelected(); }); this.resultsContainer.appendChild(div); }); @@ -242,16 +243,16 @@ class CommandPalette { }); } - executeSelected() { + async executeSelected() { const result = this.results[this.selectedIndex]; if (result && result.action) { - result.action(); + await result.action(); if (result.type !== 'hint') { this.close(); } } else if (result && result.type === 'command') { this.input.value = `>${result.name} `; - this.handleInput(); + await this.handleInput(); } } @@ -273,7 +274,7 @@ class CommandPalette { showNotification(message); } - handleQueue(args) { + async handleQueue(args) { const player = window.monochromePlayer; const ui = window.monochromeUi; @@ -283,7 +284,7 @@ class CommandPalette { } if (!args || !args.trim()) { - this.renderResults( + await this.renderResults( [ { name: '>queue wipe', description: 'Clear the queue and stop playback' }, { name: '>queue like all', description: 'Like all tracks in the current queue' }, @@ -291,9 +292,9 @@ class CommandPalette { ].map((c) => ({ ...c, type: 'command', - action: () => { + action: async () => { this.input.value = c.name; - this.handleInput(); + await this.handleInput(); }, })) ); @@ -358,17 +359,17 @@ class CommandPalette { this.close(); } - handleNavigation(args) { + async handleNavigation(args) { const validPages = ['home', 'library', 'recent', 'settings', 'unreleased', 'about', 'download']; if (!args || !args.trim()) { - this.renderResults( + await this.renderResults( validPages.map((p) => ({ name: `>go ${p}`, description: `Navigate to ${p}`, action: () => { this.close(); - import('./router.js').then((m) => m.navigate(p === 'home' ? '/' : `/${p}`)); + navigate(p === 'home' ? '/' : `/${p}`); }, type: 'command', })) @@ -380,15 +381,15 @@ class CommandPalette { if (validPages.includes(page)) { this.close(); - import('./router.js').then((m) => m.navigate(page === 'home' ? '/' : `/${page}`)); + navigate(page === 'home' ? '/' : `/${page}`); } else { this.showNotification(`Unknown page: ${page}`); } } - handleSleepTimer(args) { + async handleSleepTimer(args) { if (!args || !args.trim()) { - this.renderResults( + await this.renderResults( [15, 30, 45, 60, 120].map((m) => ({ name: `>sleep ${m}`, description: `Set sleep timer for ${m} minutes`, @@ -418,7 +419,7 @@ class CommandPalette { } } - handleQuality(args) { + async handleQuality(args) { const qualityMap = { low: 'LOW', high: 'HIGH', @@ -452,7 +453,7 @@ class CommandPalette { action: () => {}, type: 'hint', }); - this.renderResults(results); + await this.renderResults(results); return; } @@ -524,7 +525,7 @@ class CommandPalette { async handleVisualizer(args) { if (!args || !args.trim()) { - this.renderResults( + await this.renderResults( [ { name: '>visualizer toggle', description: 'Toggle visualizer on/off', cmd: 'toggle' }, { name: '>visualizer butterchurn', description: 'Set preset to Butterchurn', cmd: 'butterchurn' }, @@ -630,7 +631,7 @@ class CommandPalette { const query = args.trim().toLowerCase(); if (!query) { - this.renderResults( + await this.renderResults( this.allSettings.map((setting) => ({ name: setting.label, description: `[${setting.tab}] ${setting.description}`, @@ -659,7 +660,7 @@ class CommandPalette { return; } - this.renderResults( + await this.renderResults( results.map((setting) => ({ name: setting.label, description: `[${setting.tab}] ${setting.description}`, @@ -673,8 +674,7 @@ class CommandPalette { } async navigateToSetting(setting) { - const router = await import('./router.js'); - router.navigate('/settings'); + navigate('/settings'); await new Promise((resolve) => setTimeout(resolve, 100)); @@ -713,9 +713,9 @@ class CommandPalette { name: track.title, description: `${track.artist?.name || 'Unknown'} • ${track.album?.title || 'Unknown'}`, image: api.getCoverUrl(track.album?.cover, 80), - action: () => { + action: async () => { window.monochromePlayer.setQueue([track], 0); - window.monochromePlayer.playTrackFromQueue(); + await window.monochromePlayer.playTrackFromQueue(); this.close(); }, type: 'result', @@ -769,7 +769,7 @@ class CommandPalette { } if (this.isOpen && results.length > 0) { - this.renderResults(results); + await this.renderResults(results); } } @@ -782,7 +782,7 @@ class CommandPalette { if (results.items.length > 0) { const track = results.items[0]; window.monochromePlayer.setQueue([track], 0); - window.monochromePlayer.playTrackFromQueue(); + await window.monochromePlayer.playTrackFromQueue(); this.close(); } } diff --git a/js/dash-media-player.ts b/js/dash-media-player.ts new file mode 100644 index 0000000..7c165ec --- /dev/null +++ b/js/dash-media-player.ts @@ -0,0 +1 @@ +export { default as MediaPlayer } from '!/dashjs/src/streaming/MediaPlayer.js'; diff --git a/js/lyrics.js b/js/lyrics.js index dfbc81a..b5d5378 100644 --- a/js/lyrics.js +++ b/js/lyrics.js @@ -10,7 +10,7 @@ import { SVG_GLOBE, } from './icons.js'; import { sidePanelManager } from './side-panel.js'; -import '@uimaxbai/am-lyrics/am-lyrics.js'; +import('@uimaxbai/am-lyrics/am-lyrics.js'); // Check if text contains Japanese, Chinese, or Korean characters function containsAsianText(text) { diff --git a/js/player.js b/js/player.js index adad1e0..9879ba5 100644 --- a/js/player.js +++ b/js/player.js @@ -1,5 +1,4 @@ //js/player.js -import { MediaPlayer } from 'dashjs'; import { REPEAT_MODE, formatTime, @@ -20,7 +19,8 @@ import { } from './storage.js'; import { audioContextManager } from './audio-context.js'; import { db } from './db.js'; -import Hls from 'hls.js'; + +import('./dash-media-player.js'); import { SVG_CLOCK } from './icons.js'; export class Player { constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') { @@ -52,7 +52,9 @@ export class Player { this.sleepTimer = null; this.sleepTimerEndTime = null; this.sleepTimerInterval = null; + } + async init() { // Apply audio effects when track is ready this.audio.addEventListener('canplay', () => { this.applyAudioEffects(); @@ -64,6 +66,7 @@ export class Player { } // Initialize dash.js player + const { MediaPlayer } = await import('./dash-media-player.js'); this.dashPlayer = MediaPlayer().create(); this.dashPlayer.updateSettings({ streaming: { @@ -452,8 +455,9 @@ export class Player { } } - setupHlsVideo(video, result, fallbackImg) { + async setupHlsVideo(video, result, fallbackImg) { const url = result.videoUrl || result.hlsUrl || result; + const Hls = (await import('hls.js')).default; if (!url) return; if (this.hls) { @@ -471,9 +475,9 @@ export class Player { this.hls = new Hls(); this.hls.loadSource(url); this.hls.attachMedia(video); - this.hls.on(Hls.Events.MANIFEST_PARSED, () => { + this.hls.on(Hls.Events.MANIFEST_PARSED, async () => { video.play().catch(() => {}); - this.setupVideoQualitySelector(); + await this.setupVideoQualitySelector(); }); this.hls.on(Hls.Events.ERROR, (event, data) => { if (data.fatal) { @@ -490,9 +494,9 @@ export class Player { } } else { video.src = url; - video.onerror = () => { + video.onerror = async () => { if (result && result.hlsUrl) { - this.setupHlsVideo(video, { videoUrl: null, hlsUrl: result.hlsUrl }, fallbackImg); + await this.setupHlsVideo(video, { videoUrl: null, hlsUrl: result.hlsUrl }, fallbackImg); } else if (fallbackImg) { video.replaceWith(fallbackImg); } @@ -500,8 +504,9 @@ export class Player { } } - setupVideoQualitySelector() { + async setupVideoQualitySelector() { if (!this.hls || !this.hls.levels || this.hls.levels.length === 0) return; + const Hls = (await import('hls.js')).default; const qualityBtn = document.getElementById('fs-quality-btn'); const qualityMenu = document.getElementById('fs-quality-menu'); @@ -802,7 +807,7 @@ export class Player { if (this.playbackSequence !== currentSequence) return; if (streamUrl.includes('.m3u8') || streamUrl.includes('application/vnd.apple.mpegurl')) { - this.setupHlsVideo(activeElement, streamUrl, null); + await this.setupHlsVideo(activeElement, streamUrl, null); } else if (streamUrl.startsWith('blob:') || streamUrl.includes('.mpd')) { this.dashPlayer.initialize(activeElement, streamUrl, false); this.dashInitialized = true; diff --git a/js/settings.js b/js/settings.js index 41cac78..e43c0fa 100644 --- a/js/settings.js +++ b/js/settings.js @@ -37,12 +37,16 @@ import { modalSettings, } from './storage.js'; import { audioContextManager, EQ_PRESETS } from './audio-context.js'; -import { getButterchurnPresets } from './visualizers/butterchurn.js'; import { db } from './db.js'; import { authManager } from './accounts/auth.js'; import { syncManager } from './accounts/pocketbase.js'; import { containerFormats, customFormats } from './ffmpegFormats.ts'; +async function getButterchurnPresets(...args) { + const butterchurnModule = await import('./visualizers/butterchurn.js'); + return butterchurnModule.getButterchurnPresets(...args); +} + export function initializeSettings(scrobbler, player, api, ui) { // Restore last active settings tab const savedTab = settingsUiState.getActiveTab(); @@ -2311,7 +2315,7 @@ export function initializeSettings(scrobbler, player, api, ui) { const butterchurnDurationInput = document.getElementById('butterchurn-duration-input'); const butterchurnRandomizeToggle = document.getElementById('butterchurn-randomize-toggle'); - const updateButterchurnSettingsVisibility = () => { + const updateButterchurnSettingsVisibility = async () => { const isEnabled = visualizerEnabledToggle ? visualizerEnabledToggle.checked : false; const isButterchurn = visualizerPresetSelect ? visualizerPresetSelect.value === 'butterchurn' : false; const show = isEnabled && isButterchurn; @@ -2327,7 +2331,7 @@ export function initializeSettings(scrobbler, player, api, ui) { if (butterchurnRandomizeSetting) butterchurnRandomizeSetting.style.display = showSubSettings ? 'flex' : 'none'; // Populate preset list using module-level cache (works even before visualizer initializes) - const { keys: presetNames } = getButterchurnPresets(); + const { keys: presetNames } = await getButterchurnPresets(); const select = butterchurnSpecificPresetSelect; if (select && presetNames.length > 0) { @@ -2362,7 +2366,7 @@ export function initializeSettings(scrobbler, player, api, ui) { } }; - const updateVisualizerSettingsVisibility = (enabled) => { + const updateVisualizerSettingsVisibility = async (enabled) => { const display = enabled ? 'flex' : 'none'; if (visualizerModeSetting) visualizerModeSetting.style.display = display; if (visualizerSmartIntensitySetting) visualizerSmartIntensitySetting.style.display = display; @@ -2370,7 +2374,7 @@ export function initializeSettings(scrobbler, player, api, ui) { if (visualizerPresetSetting) visualizerPresetSetting.style.display = display; // Also update Butterchurn specific visibility - updateButterchurnSettingsVisibility(); + await updateButterchurnSettingsVisibility(); }; // Initialize preset select value early so visibility logic works correctly on load @@ -2381,24 +2385,24 @@ export function initializeSettings(scrobbler, player, api, ui) { if (visualizerEnabledToggle) { visualizerEnabledToggle.checked = visualizerSettings.isEnabled(); - updateVisualizerSettingsVisibility(visualizerEnabledToggle.checked); + await updateVisualizerSettingsVisibility(visualizerEnabledToggle.checked); - visualizerEnabledToggle.addEventListener('change', (e) => { + visualizerEnabledToggle.addEventListener('change', async (e) => { visualizerSettings.setEnabled(e.target.checked); - updateVisualizerSettingsVisibility(e.target.checked); + await updateVisualizerSettingsVisibility(e.target.checked); }); } // Visualizer Preset Select if (visualizerPresetSelect) { // value set above - visualizerPresetSelect.addEventListener('change', (e) => { + visualizerPresetSelect.addEventListener('change', async (e) => { const val = e.target.value; visualizerSettings.setPreset(val); if (ui && ui.visualizer) { ui.visualizer.setPreset(val); } - updateButterchurnSettingsVisibility(); + await updateButterchurnSettingsVisibility(); //Since changing the preset breaks the visualizer, a location.reload() is added to make sure that it works window.location.reload(); @@ -2407,9 +2411,9 @@ export function initializeSettings(scrobbler, player, api, ui) { if (butterchurnCycleToggle) { butterchurnCycleToggle.checked = visualizerSettings.isButterchurnCycleEnabled(); - butterchurnCycleToggle.addEventListener('change', (e) => { + butterchurnCycleToggle.addEventListener('change', async (e) => { visualizerSettings.setButterchurnCycleEnabled(e.target.checked); - updateButterchurnSettingsVisibility(); + await updateButterchurnSettingsVisibility(); }); } @@ -2441,30 +2445,30 @@ export function initializeSettings(scrobbler, player, api, ui) { } // Refresh settings when presets are loaded asynchronously - window.addEventListener('butterchurn-presets-loaded', () => { + window.addEventListener('butterchurn-presets-loaded', async () => { console.log('[Settings] Butterchurn presets loaded event received'); - updateButterchurnSettingsVisibility(); + await updateButterchurnSettingsVisibility(); }); // Check if presets already cached and update immediately - const { keys: cachedKeys } = getButterchurnPresets(); + const { keys: cachedKeys } = await getButterchurnPresets(); if (cachedKeys.length > 0) { console.log('[Settings] Presets already cached, updating dropdown immediately'); - updateButterchurnSettingsVisibility(); + await updateButterchurnSettingsVisibility(); } // Watch for appearance tab becoming active and refresh presets const appearanceTabContent = document.getElementById('settings-tab-appearance'); if (appearanceTabContent) { - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { + const observer = new MutationObserver(async (mutations) => { + for (const mutation of mutations) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { if (appearanceTabContent.classList.contains('active')) { console.log('[Settings] Appearance tab became active, refreshing presets'); - updateButterchurnSettingsVisibility(); + await updateButterchurnSettingsVisibility(); } } - }); + } }); observer.observe(appearanceTabContent, { attributes: true }); } diff --git a/js/ui.js b/js/ui.js index 80236e4..0f625ab 100644 --- a/js/ui.js +++ b/js/ui.js @@ -44,7 +44,6 @@ import { createTrackFromSong, } from './tracker.js'; import { trackSearch, trackChangeSort } from './analytics.js'; -import Hls from 'hls.js'; fontSettings.applyFont(); fontSettings.applyFontSize(); @@ -1127,7 +1126,7 @@ export class UIRenderer { overlay.style.display = 'flex'; - const startVisualizer = () => { + const startVisualizer = async () => { if (!visualizerSettings.isEnabled()) { if (this.visualizer) this.visualizer.stop(); return; @@ -1137,6 +1136,7 @@ export class UIRenderer { const canvas = document.getElementById('visualizer-canvas'); if (canvas) { this.visualizer = new Visualizer(canvas, activeElement); + await this.visualizer.initPresets(); } } if (this.visualizer) { @@ -1151,7 +1151,7 @@ export class UIRenderer { this.setupUIToggleButton(overlay); if (localStorage.getItem('epilepsy-warning-dismissed') === 'true') { - startVisualizer(); + await startVisualizer(); } else { const modal = document.getElementById('epilepsy-warning-modal'); if (modal) { @@ -1160,17 +1160,17 @@ export class UIRenderer { const acceptBtn = document.getElementById('epilepsy-accept-btn'); const cancelBtn = document.getElementById('epilepsy-cancel-btn'); - acceptBtn.onclick = () => { + acceptBtn.onclick = async () => { modal.classList.remove('active'); localStorage.setItem('epilepsy-warning-dismissed', 'true'); - startVisualizer(); + await startVisualizer(); }; cancelBtn.onclick = () => { modal.classList.remove('active'); this.closeFullscreenCover(); }; } else { - startVisualizer(); + await startVisualizer(); } } } @@ -2652,12 +2652,13 @@ export class UIRenderer { return items.filter((item) => !favoriteIds.has(item.id)); } - setupHlsVideo(video, result, fallbackImg) { + async setupHlsVideo(video, result, fallbackImg) { if (!result) return; const url = typeof result === 'string' ? result : result.videoUrl || result.hlsUrl; if (!url) return; if (url.endsWith('.m3u8')) { + const Hls = (await import('hls.js')).default; if (Hls.isSupported()) { const hls = new Hls(); video._hls = hls; @@ -2692,17 +2693,17 @@ export class UIRenderer { video.play().catch(() => {}); }); } - video.onerror = () => { + video.onerror = async () => { if (result.hlsUrl) { // HLS fallback (for some reason alot of animated covers js dont work on MP4 lol) - this.setupHlsVideo(video, { videoUrl: null, hlsUrl: result.hlsUrl }, fallbackImg); + await this.setupHlsVideo(video, { videoUrl: null, hlsUrl: result.hlsUrl }, fallbackImg); } else { video.replaceWith(fallbackImg); } }; } - replaceVideoArtwork(container, type, id, result) { + async replaceVideoArtwork(container, type, id, result) { const url = result.videoUrl || result.hlsUrl; if (!url) return; @@ -2722,9 +2723,9 @@ export class UIRenderer { video.poster = img.src; - video.onerror = () => { + video.onerror = async () => { if (video.src === result.videoUrl && result.hlsUrl) { - this.setupHlsVideo(video, { videoUrl: null, hlsUrl: result.hlsUrl }, img); + await this.setupHlsVideo(video, { videoUrl: null, hlsUrl: result.hlsUrl }, img); return; } video.replaceWith(img); @@ -2732,9 +2733,9 @@ export class UIRenderer { video.addEventListener( 'error', - (e) => { + async (e) => { if (video.src === result.videoUrl && result.hlsUrl) { - this.setupHlsVideo(video, { videoUrl: null, hlsUrl: result.hlsUrl }, img); + await this.setupHlsVideo(video, { videoUrl: null, hlsUrl: result.hlsUrl }, img); return; } console.warn('Video decoding error:', e); @@ -2745,7 +2746,7 @@ export class UIRenderer { img.replaceWith(video); - this.setupHlsVideo(video, result, img); + await this.setupHlsVideo(video, result, img); } } @@ -2999,7 +3000,7 @@ export class UIRenderer { if (!videoCoverUrl && tracks.length > 0) { const firstTrack = tracks[0]; - this.api.getVideoArtwork(firstTrack.title, getTrackArtists(firstTrack)).then((result) => { + this.api.getVideoArtwork(firstTrack.title, getTrackArtists(firstTrack)).then(async (result) => { if (result && this.currentPage === 'album' && this.currentAlbumId === albumId) { const url = result.videoUrl || result.hlsUrl; if (!url) return; @@ -3017,7 +3018,7 @@ export class UIRenderer { video.style.opacity = '1'; video.poster = currentImageEl.src; - this.setupHlsVideo(video, result, currentImageEl); + await this.setupHlsVideo(video, result, currentImageEl); currentImageEl.replaceWith(video); } } @@ -3036,10 +3037,10 @@ export class UIRenderer { video.preload = 'auto'; video.className = imageEl.className; video.id = imageEl.id; - this.setupHlsVideo(video, videoCoverUrl, imageEl); + await this.setupHlsVideo(video, videoCoverUrl, imageEl); imageEl.replaceWith(video); } else { - this.setupHlsVideo(imageEl, videoCoverUrl, null); + await this.setupHlsVideo(imageEl, videoCoverUrl, null); } } else { if (imageEl.tagName === 'VIDEO') { @@ -3788,30 +3789,32 @@ export class UIRenderer { if (!videoCoverUrl && (firstTrack.album || firstTrack.type === 'video')) { const fetchArtwork = () => { - this.api.getVideoArtwork(firstTrack.title, getTrackArtists(firstTrack)).then((result) => { - if (result && this.currentPage === 'mix' && this.currentMixId === mixId) { - const url = result.videoUrl || result.hlsUrl; - if (!url) return; - firstTrack.album = firstTrack.album || {}; - firstTrack.album.videoCoverUrl = url; - const currentImageEl = document.getElementById('mix-detail-image'); - if (currentImageEl && currentImageEl.tagName !== 'VIDEO') { - const video = document.createElement('video'); - video.autoplay = true; - video.loop = true; - video.muted = true; - video.playsInline = true; - video.preload = 'auto'; - video.className = currentImageEl.className; - video.id = currentImageEl.id; - video.style.opacity = '1'; - video.poster = currentImageEl.src; + this.api + .getVideoArtwork(firstTrack.title, getTrackArtists(firstTrack)) + .then(async (result) => { + if (result && this.currentPage === 'mix' && this.currentMixId === mixId) { + const url = result.videoUrl || result.hlsUrl; + if (!url) return; + firstTrack.album = firstTrack.album || {}; + firstTrack.album.videoCoverUrl = url; + const currentImageEl = document.getElementById('mix-detail-image'); + if (currentImageEl && currentImageEl.tagName !== 'VIDEO') { + const video = document.createElement('video'); + video.autoplay = true; + video.loop = true; + video.muted = true; + video.playsInline = true; + video.preload = 'auto'; + video.className = currentImageEl.className; + video.id = currentImageEl.id; + video.style.opacity = '1'; + video.poster = currentImageEl.src; - this.setupHlsVideo(video, result, currentImageEl); - currentImageEl.replaceWith(video); + await this.setupHlsVideo(video, result, currentImageEl); + currentImageEl.replaceWith(video); + } } - } - }); + }); }; if (firstTrack.type === 'video') { @@ -5141,7 +5144,7 @@ export class UIRenderer { if (!videoCoverUrl && (track.album || track.type === 'video')) { const fetchArtwork = () => { - this.api.getVideoArtwork(track.title, getTrackArtists(track)).then((result) => { + this.api.getVideoArtwork(track.title, getTrackArtists(track)).then(async (result) => { if (result && this.currentPage === 'track' && this.currentTrackPageId === track.id) { const url = result.videoUrl || result.hlsUrl; if (!url) return; @@ -5160,7 +5163,7 @@ export class UIRenderer { video.style.opacity = '1'; video.poster = currentImageEl.src; - this.setupHlsVideo(video, result, currentImageEl); + await this.setupHlsVideo(video, result, currentImageEl); currentImageEl.replaceWith(video); } } @@ -5196,10 +5199,10 @@ export class UIRenderer { video.preload = 'auto'; video.className = imageEl.className; video.id = imageEl.id; - this.setupHlsVideo(video, videoCoverUrl, imageEl); + await this.setupHlsVideo(video, videoCoverUrl, imageEl); imageEl.replaceWith(video); } else { - this.setupHlsVideo(imageEl, videoCoverUrl, null); + await this.setupHlsVideo(imageEl, videoCoverUrl, null); } } else { if (imageEl.tagName === 'VIDEO') { diff --git a/js/visualizer.js b/js/visualizer.js index 2e58dae..5cfdc2f 100644 --- a/js/visualizer.js +++ b/js/visualizer.js @@ -1,10 +1,5 @@ // js/visualizer.js import { visualizerSettings } from './storage.js'; -import { LCDPreset } from './visualizers/lcd.js'; -import { ParticlesPreset } from './visualizers/particles.js'; -import { UnknownPleasuresWebGL } from './visualizers/unknown_pleasures_webgl.js'; -import { ButterchurnPreset } from './visualizers/butterchurn.js'; -import { KawarpPreset } from './visualizers/kawarp.js'; import { audioContextManager } from './audio-context.js'; export class Visualizer { @@ -12,21 +7,10 @@ export class Visualizer { this.canvas = canvas; this.ctx = null; this.audio = audio; - this.audioContext = null; this.analyser = null; - this.isActive = false; this.animationId = null; - - this.presets = { - lcd: new LCDPreset(), - particles: new ParticlesPreset(), - 'unknown-pleasures': new UnknownPleasuresWebGL(), - butterchurn: new ButterchurnPreset(), - kawarp: new KawarpPreset(), - }; - this.activePresetKey = visualizerSettings.getPreset(); // ---- AUDIO BUFFERS (REUSED) ---- @@ -51,6 +35,19 @@ export class Visualizer { this._resizeBound = () => this.resize(); } + /** + * Must be called after class is constructed! + */ + async initPresets() { + this.presets = { + lcd: new (await import('./visualizers/lcd.js')).LCDPreset(), + particles: new (await import('./visualizers/particles.js')).ParticlesPreset(), + 'unknown-pleasures': new (await import('./visualizers/unknown_pleasures_webgl.js')).UnknownPleasuresWebGL(), + butterchurn: new (await import('./visualizers/butterchurn.js')).ButterchurnPreset(), + kawarp: new (await import('./visualizers/kawarp.js')).KawarpPreset(), + }; + } + updateDimming() { if (!this.canvas || !this.canvas.parentElement) return; const dimAmount = visualizerSettings.getDimAmount(); @@ -61,7 +58,7 @@ export class Visualizer { return this.presets[this.activePresetKey] || this.presets['lcd']; } - init() { + async init() { // Ensure shared audio context is initialized if (!audioContextManager.isReady()) { audioContextManager.init(this.audio); diff --git a/package-lock.json b/package-lock.json index 2297a39..d1092c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,15 @@ "@ffmpeg/util": "^0.12.2", "@kawarp/core": "^1.1.1", "@neutralinojs/lib": "^6.5.0", + "@svta/common-media-library": "^0.18.1", "@uimaxbai/am-lyrics": "^1.1.4", "appwrite": "^23.0.0", "butterchurn": "^2.6.7", "butterchurn-presets": "^2.4.7", "client-zip": "^2.5.0", "cookie-session": "^2.1.1", - "dashjs": "^5.1.1", + "dashjs": "https://github.com/Dash-Industry-Forum/dash.js/archive/refs/tags/v5.1.1.tar.gz", + "eventemitter3": "^5.0.4", "fuse.js": "^7.1.0", "hls.js": "^1.6.15", "jose": "^6.2.0", @@ -31,6 +33,7 @@ "pocketbase": "^0.26.8", "simple-icons": "^16.12.0", "svgo": "^4.0.1", + "url-toolkit": "^2.2.5", "uuid": "^13.0.0" }, "devDependencies": { @@ -3774,6 +3777,13 @@ "@svta/cml-utils": "1.0.1" } }, + "node_modules/@svta/common-media-library": { + "version": "0.18.1", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "dev": true, @@ -4598,6 +4608,8 @@ }, "node_modules/dashjs": { "version": "5.1.1", + "resolved": "https://github.com/Dash-Industry-Forum/dash.js/archive/refs/tags/v5.1.1.tar.gz", + "integrity": "sha512-lhD1tvEe4PO6t086flm6WfO2Jt1EOIolDQ17F3vLomMthaL1RH96h8peIQTvrDvfSJTRXeisL+CwPj4oud5e9g==", "license": "BSD-3-Clause", "dependencies": { "@svta/cml-608": "1.0.1", @@ -5327,6 +5339,10 @@ "es5-ext": "~0.10.14" } }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "license": "MIT" + }, "node_modules/ext": { "version": "1.7.0", "dev": true, @@ -5518,11 +5534,8 @@ }, "node_modules/flatted": { "version": "3.4.2", -<<<<<<< svg-refactor -======= "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", ->>>>>>> main "dev": true, "license": "ISC" }, @@ -10813,6 +10826,10 @@ "punycode": "^2.1.0" } }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "license": "Apache-2.0" + }, "node_modules/utf-8-validate": { "version": "5.0.10", "dev": true, diff --git a/package.json b/package.json index 86f321d..999cf2d 100644 --- a/package.json +++ b/package.json @@ -57,13 +57,15 @@ "@ffmpeg/util": "^0.12.2", "@kawarp/core": "^1.1.1", "@neutralinojs/lib": "^6.5.0", + "@svta/common-media-library": "^0.18.1", "@uimaxbai/am-lyrics": "^1.1.4", "appwrite": "^23.0.0", "butterchurn": "^2.6.7", "butterchurn-presets": "^2.4.7", "client-zip": "^2.5.0", "cookie-session": "^2.1.1", - "dashjs": "^5.1.1", + "dashjs": "https://github.com/Dash-Industry-Forum/dash.js/archive/refs/tags/v5.1.1.tar.gz", + "eventemitter3": "^5.0.4", "fuse.js": "^7.1.0", "hls.js": "^1.6.15", "jose": "^6.2.0", @@ -73,6 +75,7 @@ "pocketbase": "^0.26.8", "simple-icons": "^16.12.0", "svgo": "^4.0.1", + "url-toolkit": "^2.2.5", "uuid": "^13.0.0" } } diff --git a/stream-stub.js b/stream-stub.js new file mode 100644 index 0000000..f5265c0 --- /dev/null +++ b/stream-stub.js @@ -0,0 +1 @@ +export function Stream() {} diff --git a/vite.config.ts b/vite.config.ts index d7887f9..7211e14 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,7 +5,6 @@ import authGatePlugin from './vite-plugin-auth-gate.js'; import path from 'path'; import uploadPlugin from './vite-plugin-upload.js'; import blobAssetPlugin from './vite-plugin-blob.js'; -import injectHTML from 'vite-plugin-html-inject'; import svgUse from './vite-plugin-svg-use.js'; export default defineConfig(({ mode }) => { @@ -23,6 +22,7 @@ export default defineConfig(({ mode }) => { '!': '/node_modules', pocketbase: '/node_modules/pocketbase/dist/pocketbase.es.js', + stream: path.resolve(__dirname, 'stream-stub.js'), // Stub for stream module }, }, optimizeDeps: { @@ -50,7 +50,6 @@ export default defineConfig(({ mode }) => { uploadPlugin(), blobAssetPlugin(), svgUse(), - injectHTML(), VitePWA({ registerType: 'prompt', workbox: {