Merge pull request #380 from DanTheMan827/singleton
Convert classes to singleton pattern
This commit is contained in:
commit
7e16dc23c2
11 changed files with 366 additions and 198 deletions
60
js/HiFi.ts
60
js/HiFi.ts
|
|
@ -18,7 +18,23 @@ export class TidalResponse extends Response {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HiFiConstructorOptions {
|
||||||
|
clientId?: string;
|
||||||
|
clientSecret?: string;
|
||||||
|
countryCode?: string;
|
||||||
|
token?: string;
|
||||||
|
tokenExpiry?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class HiFiClient {
|
export class HiFiClient {
|
||||||
|
static #instance: HiFiClient | null = null;
|
||||||
|
static get instance() {
|
||||||
|
if (!HiFiClient.#instance) {
|
||||||
|
throw new Error('HiFiClient is not initialized. Call HiFiClient.initialize(options) first.');
|
||||||
|
}
|
||||||
|
return HiFiClient.#instance;
|
||||||
|
}
|
||||||
|
|
||||||
private static tokenPromise: Promise<string> | null = null;
|
private static tokenPromise: Promise<string> | null = null;
|
||||||
private static albumTracksMax = 20;
|
private static albumTracksMax = 20;
|
||||||
private static albumTracksActive = 0;
|
private static albumTracksActive = 0;
|
||||||
|
|
@ -90,15 +106,7 @@ export class HiFiClient {
|
||||||
return u.toString();
|
return u.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static encodeBasic(id: string, secret: string) {
|
static setToken(token: string, expiry: number = -1) {
|
||||||
if (typeof globalThis.btoa === 'function') {
|
|
||||||
return btoa(`${id}:${secret}`);
|
|
||||||
}
|
|
||||||
// Node fallback
|
|
||||||
return Buffer.from(`${id}:${secret}`).toString('base64');
|
|
||||||
}
|
|
||||||
|
|
||||||
static setToken(token: string, expiry: number = Date.now() + 60000) {
|
|
||||||
HiFiClient.token = token;
|
HiFiClient.token = token;
|
||||||
HiFiClient.appTokenExpiry = expiry;
|
HiFiClient.appTokenExpiry = expiry;
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +116,8 @@ export class HiFiClient {
|
||||||
clientId: string,
|
clientId: string,
|
||||||
clientSecret: string
|
clientSecret: string
|
||||||
) {
|
) {
|
||||||
if (HiFiClient.token && Date.now() < HiFiClient.appTokenExpiry) return HiFiClient.token;
|
if (HiFiClient.token && (HiFiClient.appTokenExpiry < 0 || Date.now() < HiFiClient.appTokenExpiry))
|
||||||
|
return HiFiClient.token;
|
||||||
|
|
||||||
return await (HiFiClient.tokenPromise ??= (async () => {
|
return await (HiFiClient.tokenPromise ??= (async () => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -116,7 +125,6 @@ export class HiFiClient {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'content-type': 'application/x-www-form-urlencoded',
|
'content-type': 'application/x-www-form-urlencoded',
|
||||||
authorization: `Basic ${this.encodeBasic(clientId, clientSecret)}`,
|
|
||||||
},
|
},
|
||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
grant_type: 'client_credentials',
|
grant_type: 'client_credentials',
|
||||||
|
|
@ -144,10 +152,38 @@ export class HiFiClient {
|
||||||
})());
|
})());
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(clientId = BROWSER_CLIENT_ID, clientSecret = BROWSER_CLIENT_SECRET, countryCode = 'US') {
|
private static getOptions({
|
||||||
|
clientId = BROWSER_CLIENT_ID,
|
||||||
|
clientSecret = BROWSER_CLIENT_SECRET,
|
||||||
|
countryCode = 'US',
|
||||||
|
token,
|
||||||
|
tokenExpiry,
|
||||||
|
}: HiFiConstructorOptions = {}) {
|
||||||
|
return { clientId, clientSecret, countryCode, token, tokenExpiry };
|
||||||
|
}
|
||||||
|
|
||||||
|
private constructor(options: HiFiConstructorOptions = {}) {
|
||||||
|
const { clientId, clientSecret, countryCode, token, tokenExpiry } = HiFiClient.getOptions(options);
|
||||||
this.countryCode = countryCode;
|
this.countryCode = countryCode;
|
||||||
this.clientId = clientId;
|
this.clientId = clientId;
|
||||||
this.clientSecret = clientSecret;
|
this.clientSecret = clientSecret;
|
||||||
|
if (token) {
|
||||||
|
HiFiClient.setToken(token, tokenExpiry ?? -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async initialize(options: HiFiConstructorOptions = {}) {
|
||||||
|
if (HiFiClient.#instance) {
|
||||||
|
throw new Error('HiFiClient is already initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const instance = (HiFiClient.#instance = new HiFiClient(options));
|
||||||
|
|
||||||
|
if (!options.token && !options.clientId && !options.clientSecret) {
|
||||||
|
await HiFiClient.fetchAppToken(new AbortController().signal, instance.clientId, instance.clientSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (HiFiClient.#instance = instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async fetchJson(url: string, params?: Params, signal: AbortSignal = new AbortController().signal) {
|
private async fetchJson(url: string, params?: Params, signal: AbortSignal = new AbortController().signal) {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ import { HiFiClient, TidalResponse } from './HiFi.ts';
|
||||||
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
|
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
|
||||||
export { resolveDownloadTotalBytes };
|
export { resolveDownloadTotalBytes };
|
||||||
const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25';
|
const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25';
|
||||||
const client = new HiFiClient();
|
|
||||||
|
|
||||||
export class LosslessAPI {
|
export class LosslessAPI {
|
||||||
constructor(settings) {
|
constructor(settings) {
|
||||||
|
|
@ -64,7 +63,7 @@ export class LosslessAPI {
|
||||||
console.log(relativePath);
|
console.log(relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return await client.queryResponse(relativePath);
|
return await HiFiClient.instance.queryResponse(relativePath);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`Direct fetch failed for ${relativePath}. Falling back to configured API instances...`,
|
`Direct fetch failed for ${relativePath}. Falling back to configured API instances...`,
|
||||||
|
|
|
||||||
318
js/app.js
318
js/app.js
|
|
@ -71,6 +71,7 @@ import {
|
||||||
SVG_CLOSE,
|
SVG_CLOSE,
|
||||||
SVG_RESET,
|
SVG_RESET,
|
||||||
} from './icons.js';
|
} from './icons.js';
|
||||||
|
import { HiFiClient } from './HiFi.js';
|
||||||
|
|
||||||
// Capture real iOS state before spoofing (needed for background audio)
|
// Capture real iOS state before spoofing (needed for background audio)
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
|
|
@ -106,8 +107,6 @@ let settingsModule = null;
|
||||||
let downloadsModule = null;
|
let downloadsModule = null;
|
||||||
let metadataModule = null;
|
let metadataModule = null;
|
||||||
|
|
||||||
export const managers = {};
|
|
||||||
|
|
||||||
async function loadSettingsModule() {
|
async function loadSettingsModule() {
|
||||||
if (!settingsModule) {
|
if (!settingsModule) {
|
||||||
settingsModule = await import('./settings.js');
|
settingsModule = await import('./settings.js');
|
||||||
|
|
@ -265,23 +264,20 @@ function initializeKeyboardShortcuts(player, _audioPlayer) {
|
||||||
},
|
},
|
||||||
visualizerNext: () => {
|
visualizerNext: () => {
|
||||||
trackKeyboardShortcut('VisualizerNext');
|
trackKeyboardShortcut('VisualizerNext');
|
||||||
const ui = window.monochromeUi;
|
if (UIRenderer.instance.visualizer?.presets?.['butterchurn']) {
|
||||||
if (ui?.visualizer?.presets?.['butterchurn']) {
|
UIRenderer.instance.visualizer.presets['butterchurn'].nextPreset();
|
||||||
ui.visualizer.presets['butterchurn'].nextPreset();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
visualizerPrev: () => {
|
visualizerPrev: () => {
|
||||||
trackKeyboardShortcut('VisualizerPrev');
|
trackKeyboardShortcut('VisualizerPrev');
|
||||||
const ui = window.monochromeUi;
|
if (UIRenderer.instance.visualizer?.presets?.['butterchurn']) {
|
||||||
if (ui?.visualizer?.presets?.['butterchurn']) {
|
UIRenderer.instance.visualizer.presets['butterchurn'].prevPreset();
|
||||||
ui.visualizer.presets['butterchurn'].prevPreset();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
visualizerCycle: () => {
|
visualizerCycle: () => {
|
||||||
trackKeyboardShortcut('VisualizerCycle');
|
trackKeyboardShortcut('VisualizerCycle');
|
||||||
const ui = window.monochromeUi;
|
if (UIRenderer.instance.visualizer?.presets?.['butterchurn']) {
|
||||||
if (ui?.visualizer?.presets?.['butterchurn']) {
|
UIRenderer.instance.visualizer.presets['butterchurn'].toggleCycle();
|
||||||
ui.visualizer.presets['butterchurn'].toggleCycle();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -385,12 +381,23 @@ async function uploadCoverImage(file) {
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await modernSettings.waitPending();
|
await modernSettings.waitPending();
|
||||||
|
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
window.monochrome = {
|
||||||
|
HiFiClient,
|
||||||
|
LyricsManager,
|
||||||
|
MusicAPI,
|
||||||
|
Player,
|
||||||
|
UIRenderer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize analytics
|
// Initialize analytics
|
||||||
initAnalytics();
|
initAnalytics();
|
||||||
|
|
||||||
new ThemeStore();
|
new ThemeStore();
|
||||||
|
await HiFiClient.initialize();
|
||||||
|
await MusicAPI.initialize(apiSettings);
|
||||||
|
|
||||||
const api = new MusicAPI(apiSettings);
|
|
||||||
const audioPlayer = document.getElementById('audio-player');
|
const audioPlayer = document.getElementById('audio-player');
|
||||||
|
|
||||||
// i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only playback
|
// i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only playback
|
||||||
|
|
@ -418,21 +425,19 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentQuality = localStorage.getItem('playback-quality') || 'HI_RES_LOSSLESS';
|
const currentQuality = localStorage.getItem('playback-quality') || 'HI_RES_LOSSLESS';
|
||||||
const player = new Player(audioPlayer, api, currentQuality);
|
await Player.initialize(audioPlayer, MusicAPI.instance, currentQuality);
|
||||||
await player.init();
|
|
||||||
window.monochromePlayer = player;
|
|
||||||
|
|
||||||
// Initialize tracker
|
// Initialize tracker
|
||||||
initTracker(player);
|
initTracker();
|
||||||
|
|
||||||
// Linux Media Keys Fix
|
// Linux Media Keys Fix
|
||||||
if (window.NL_MODE) {
|
if (window.NL_MODE) {
|
||||||
import('./desktop/neutralino-bridge.js').then(({ events }) => {
|
import('./desktop/neutralino-bridge.js').then(({ events }) => {
|
||||||
events.on('mediaNext', () => player.playNext());
|
events.on('mediaNext', () => Player.instance.playNext());
|
||||||
events.on('mediaPrevious', () => player.playPrev());
|
events.on('mediaPrevious', () => Player.instance.playPrev());
|
||||||
events.on('mediaPlayPause', () => player.handlePlayPause());
|
events.on('mediaPlayPause', () => Player.instance.handlePlayPause());
|
||||||
events.on('mediaStop', () => {
|
events.on('mediaStop', () => {
|
||||||
const el = player.activeElement;
|
const el = Player.instance.activeElement;
|
||||||
el.pause();
|
el.pause();
|
||||||
el.currentTime = 0;
|
el.currentTime = 0;
|
||||||
});
|
});
|
||||||
|
|
@ -450,7 +455,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
window.NL_MODE = true;
|
window.NL_MODE = true;
|
||||||
try {
|
try {
|
||||||
const desktopModule = await import('./desktop/desktop.js');
|
const desktopModule = await import('./desktop/desktop.js');
|
||||||
await desktopModule.initDesktop(player);
|
await desktopModule.initDesktop(Player.instance);
|
||||||
|
|
||||||
import('./desktop/neutralino-bridge.js').then(({ updater }) => {
|
import('./desktop/neutralino-bridge.js').then(({ updater }) => {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
|
@ -499,8 +504,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const castBtn = document.getElementById('cast-btn');
|
const castBtn = document.getElementById('cast-btn');
|
||||||
initializeCasting(audioPlayer, castBtn);
|
initializeCasting(audioPlayer, castBtn);
|
||||||
|
|
||||||
const ui = new UIRenderer(api, player);
|
await UIRenderer.initialize(MusicAPI.instance, Player.instance);
|
||||||
window.monochromeUi = ui;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scans the configured local media folder and refreshes `window.localFilesCache`.
|
* Scans the configured local media folder and refreshes `window.localFilesCache`.
|
||||||
|
|
@ -552,7 +556,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const metadata = await readTrackMetadata(file);
|
const metadata = await readTrackMetadata(file);
|
||||||
metadata.id = `local-${idCounter++}-${entry.entry}`;
|
metadata.id = `local-${idCounter++}-${entry.entry}`;
|
||||||
tracks.push(metadata);
|
tracks.push(metadata);
|
||||||
window.monochromeUi?.renderLocalFiles(
|
UIRenderer.instance.renderLocalFiles(
|
||||||
document.getElementById('library-local-container')
|
document.getElementById('library-local-container')
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -589,7 +593,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const metadata = await readTrackMetadata(file);
|
const metadata = await readTrackMetadata(file);
|
||||||
metadata.id = `local-${idCounter++}-${file.name}`;
|
metadata.id = `local-${idCounter++}-${file.name}`;
|
||||||
tracks.push(metadata);
|
tracks.push(metadata);
|
||||||
window.monochromeUi?.renderLocalFiles(
|
UIRenderer.instance.renderLocalFiles(
|
||||||
document.getElementById('library-local-container')
|
document.getElementById('library-local-container')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -603,7 +607,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
tracks.sort((a, b) => (a.artist.name || '').localeCompare(b.artist.name || ''));
|
tracks.sort((a, b) => (a.artist.name || '').localeCompare(b.artist.name || ''));
|
||||||
// Update only the local-files section without navigating to the library page.
|
// Update only the local-files section without navigating to the library page.
|
||||||
window.monochromeUi?.renderLocalFiles(document.getElementById('library-local-container'));
|
UIRenderer.instance.renderLocalFiles(document.getElementById('library-local-container'));
|
||||||
} finally {
|
} finally {
|
||||||
window.localFilesScanInProgress = false;
|
window.localFilesScanInProgress = false;
|
||||||
}
|
}
|
||||||
|
|
@ -636,7 +640,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
window.localFilesCache = [...existing, metadata].sort((a, b) =>
|
window.localFilesCache = [...existing, metadata].sort((a, b) =>
|
||||||
(a.artist.name || '').localeCompare(b.artist.name || '')
|
(a.artist.name || '').localeCompare(b.artist.name || '')
|
||||||
);
|
);
|
||||||
window.monochromeUi?.renderLocalFiles(document.getElementById('library-local-container'));
|
UIRenderer.instance.renderLocalFiles(document.getElementById('library-local-container'));
|
||||||
} catch {
|
} catch {
|
||||||
// Fall back to a full rescan if metadata extraction fails.
|
// Fall back to a full rescan if metadata extraction fails.
|
||||||
await scanLocalMediaFolder(true);
|
await scanLocalMediaFolder(true);
|
||||||
|
|
@ -656,9 +660,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const scrobbler = new MultiScrobbler();
|
const scrobbler = new MultiScrobbler();
|
||||||
window.monochromeScrobbler = scrobbler;
|
window.monochromeScrobbler = scrobbler;
|
||||||
const lyricsManager = new LyricsManager(api);
|
|
||||||
ui.lyricsManager = lyricsManager;
|
const lyricsManager = await LyricsManager.initialize(MusicAPI.instance);
|
||||||
managers.lyricsManager = lyricsManager;
|
UIRenderer.instance.lyricsManager = lyricsManager;
|
||||||
|
|
||||||
// Check browser support for local files
|
// Check browser support for local files
|
||||||
const selectLocalBtn = document.getElementById('select-local-folder-btn');
|
const selectLocalBtn = document.getElementById('select-local-folder-btn');
|
||||||
|
|
@ -691,11 +695,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
sidebarSettings.restoreState();
|
sidebarSettings.restoreState();
|
||||||
|
|
||||||
// Render pinned items
|
// Render pinned items
|
||||||
await ui.renderPinnedItems();
|
await UIRenderer.instance.renderPinnedItems();
|
||||||
|
|
||||||
// Load settings module and initialize
|
// Load settings module and initialize
|
||||||
const { initializeSettings } = await loadSettingsModule();
|
const { initializeSettings } = await loadSettingsModule();
|
||||||
await initializeSettings(scrobbler, player, api, ui);
|
await initializeSettings(scrobbler, Player.instance, MusicAPI.instance, UIRenderer.instance);
|
||||||
|
|
||||||
// Track sidebar navigation clicks
|
// Track sidebar navigation clicks
|
||||||
document.querySelectorAll('.sidebar-nav a').forEach((link) => {
|
document.querySelectorAll('.sidebar-nav a').forEach((link) => {
|
||||||
|
|
@ -708,28 +712,28 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
initializePlayerEvents(player, audioPlayer, scrobbler, ui);
|
initializePlayerEvents(Player.instance, audioPlayer, scrobbler, UIRenderer.instance);
|
||||||
initializeTrackInteractions(
|
initializeTrackInteractions(
|
||||||
player,
|
Player.instance,
|
||||||
api,
|
MusicAPI.instance,
|
||||||
document.querySelector('.main-content'),
|
document.querySelector('.main-content'),
|
||||||
document.getElementById('context-menu'),
|
document.getElementById('context-menu'),
|
||||||
lyricsManager,
|
lyricsManager,
|
||||||
ui,
|
UIRenderer.instance,
|
||||||
scrobbler
|
scrobbler
|
||||||
);
|
);
|
||||||
initializeUIInteractions(player, api, ui);
|
initializeUIInteractions(Player.instance, MusicAPI.instance, UIRenderer.instance);
|
||||||
initializeKeyboardShortcuts(player, audioPlayer);
|
initializeKeyboardShortcuts(Player.instance, audioPlayer);
|
||||||
|
|
||||||
// Restore UI state for the current track (like button, theme)
|
// Restore UI state for the current track (like button, theme)
|
||||||
if (player.currentTrack) {
|
if (Player.instance.currentTrack) {
|
||||||
ui.setCurrentTrack(player.currentTrack);
|
UIRenderer.instance.setCurrentTrack(Player.instance.currentTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.querySelector('.now-playing-bar').addEventListener('click', async (e) => {
|
document.querySelector('.now-playing-bar').addEventListener('click', async (e) => {
|
||||||
if (!e.target.closest('.cover')) return;
|
if (!e.target.closest('.cover')) return;
|
||||||
|
|
||||||
if (!player.currentTrack) {
|
if (!Player.instance.currentTrack) {
|
||||||
alert('No track is currently playing');
|
alert('No track is currently playing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -740,16 +744,16 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const isActive = sidePanelManager.isActive('lyrics');
|
const isActive = sidePanelManager.isActive('lyrics');
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
trackCloseLyrics(player.currentTrack);
|
trackCloseLyrics(Player.instance.currentTrack);
|
||||||
} else {
|
} else {
|
||||||
trackOpenLyrics(player.currentTrack);
|
trackOpenLyrics(Player.instance.currentTrack);
|
||||||
}
|
}
|
||||||
} else if (mode === 'cover') {
|
} else if (mode === 'cover') {
|
||||||
const overlay = document.getElementById('fullscreen-cover-overlay');
|
const overlay = document.getElementById('fullscreen-cover-overlay');
|
||||||
if (overlay && overlay.style.display === 'flex') {
|
if (overlay && overlay.style.display === 'flex') {
|
||||||
trackCloseFullscreenCover();
|
trackCloseFullscreenCover();
|
||||||
} else {
|
} else {
|
||||||
trackOpenFullscreenCover(player.currentTrack);
|
trackOpenFullscreenCover(Player.instance.currentTrack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -758,9 +762,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
sidePanelManager.close();
|
sidePanelManager.close();
|
||||||
clearLyricsPanelSync(player.activeElement, sidePanelManager.panel);
|
clearLyricsPanelSync(Player.instance.activeElement, sidePanelManager.panel);
|
||||||
} else {
|
} else {
|
||||||
openLyricsPanel(player.currentTrack, player.activeElement, lyricsManager);
|
openLyricsPanel(Player.instance.currentTrack, Player.instance.activeElement, lyricsManager);
|
||||||
}
|
}
|
||||||
} else if (mode === 'cover') {
|
} else if (mode === 'cover') {
|
||||||
const overlay = document.getElementById('fullscreen-cover-overlay');
|
const overlay = document.getElementById('fullscreen-cover-overlay');
|
||||||
|
|
@ -768,16 +772,21 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (window.location.hash === '#fullscreen') {
|
if (window.location.hash === '#fullscreen') {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
} else {
|
} else {
|
||||||
ui.closeFullscreenCover();
|
UIRenderer.instance.closeFullscreenCover();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const nextTrack = player.getNextTrack();
|
const nextTrack = Player.instance.getNextTrack();
|
||||||
ui.showFullscreenCover(player.currentTrack, nextTrack, lyricsManager, player.activeElement);
|
UIRenderer.instance.showFullscreenCover(
|
||||||
|
Player.instance.currentTrack,
|
||||||
|
nextTrack,
|
||||||
|
lyricsManager,
|
||||||
|
Player.instance.activeElement
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Default to 'album' mode - navigate to album
|
// Default to 'album' mode - navigate to album
|
||||||
if (player.currentTrack.album?.id) {
|
if (Player.instance.currentTrack.album?.id) {
|
||||||
navigate(`/album/${player.currentTrack.album.id}`);
|
navigate(`/album/${Player.instance.currentTrack.album.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -793,7 +802,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (window.location.hash === '#fullscreen') {
|
if (window.location.hash === '#fullscreen') {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
} else {
|
} else {
|
||||||
ui.closeFullscreenCover();
|
UIRenderer.instance.closeFullscreenCover();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -805,14 +814,14 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const action = fullscreenCoverClickSettings.getAction();
|
const action = fullscreenCoverClickSettings.getAction();
|
||||||
const overlay = document.getElementById('fullscreen-cover-overlay');
|
const overlay = document.getElementById('fullscreen-cover-overlay');
|
||||||
const playerInstance = window.monochromePlayer;
|
const playerInstance = Player.instance;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'exit':
|
case 'exit':
|
||||||
if (window.location.hash === '#fullscreen') {
|
if (window.location.hash === '#fullscreen') {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
} else {
|
} else {
|
||||||
ui.closeFullscreenCover();
|
UIRenderer.instance.closeFullscreenCover();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'hide-ui':
|
case 'hide-ui':
|
||||||
|
|
@ -835,11 +844,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
toggleBtn.title = 'Show UI';
|
toggleBtn.title = 'Show UI';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (ui && typeof ui.setupUIToggleButton === 'function') {
|
if (UIRenderer.instance && typeof UIRenderer.instance.setupUIToggleButton === 'function') {
|
||||||
if (ui.uiToggleCleanup) {
|
if (UIRenderer.instance.uiToggleCleanup) {
|
||||||
ui.uiToggleCleanup();
|
UIRenderer.instance.uiToggleCleanup();
|
||||||
}
|
}
|
||||||
ui.setupUIToggleButton(overlay);
|
UIRenderer.instance.setupUIToggleButton(overlay);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
@ -858,7 +867,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (window.location.hash === '#fullscreen') {
|
if (window.location.hash === '#fullscreen') {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
} else {
|
} else {
|
||||||
ui.closeFullscreenCover();
|
UIRenderer.instance.closeFullscreenCover();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -1049,7 +1058,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
document.getElementById('toggle-lyrics-btn')?.addEventListener('click', async (e) => {
|
document.getElementById('toggle-lyrics-btn')?.addEventListener('click', async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (!player.currentTrack) {
|
if (!Player.instance.currentTrack) {
|
||||||
alert('No track is currently playing');
|
alert('No track is currently playing');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -1058,44 +1067,57 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
sidePanelManager.close();
|
sidePanelManager.close();
|
||||||
clearLyricsPanelSync(player.activeElement, sidePanelManager.panel);
|
clearLyricsPanelSync(Player.instance.activeElement, sidePanelManager.panel);
|
||||||
} else {
|
} else {
|
||||||
openLyricsPanel(player.currentTrack, player.activeElement, lyricsManager);
|
openLyricsPanel(Player.instance.currentTrack, Player.instance.activeElement, lyricsManager);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById('download-current-btn')?.addEventListener('click', () => {
|
document.getElementById('download-current-btn')?.addEventListener('click', () => {
|
||||||
if (player.currentTrack) {
|
if (Player.instance.currentTrack) {
|
||||||
handleTrackAction('download', player.currentTrack, player, api, lyricsManager, 'track', ui);
|
handleTrackAction(
|
||||||
|
'download',
|
||||||
|
Player.instance.currentTrack,
|
||||||
|
Player.instance,
|
||||||
|
MusicAPI.instance,
|
||||||
|
lyricsManager,
|
||||||
|
'track',
|
||||||
|
UIRenderer.instance
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-update lyrics when track changes
|
// Auto-update lyrics when track changes
|
||||||
let previousTrackId = null;
|
let previousTrackId = null;
|
||||||
audioPlayer.addEventListener('play', async () => {
|
audioPlayer.addEventListener('play', async () => {
|
||||||
if (!player.currentTrack) return;
|
if (!Player.instance.currentTrack) return;
|
||||||
|
|
||||||
// Update UI with current track info for theme
|
// Update UI with current track info for theme
|
||||||
ui.setCurrentTrack(player.currentTrack);
|
UIRenderer.instance.setCurrentTrack(Player.instance.currentTrack);
|
||||||
|
|
||||||
// Update Media Session with new track
|
// Update Media Session with new track
|
||||||
player.updateMediaSession(player.currentTrack);
|
Player.instance.updateMediaSession(Player.instance.currentTrack);
|
||||||
|
|
||||||
const currentTrackId = player.currentTrack.id;
|
const currentTrackId = Player.instance.currentTrack.id;
|
||||||
if (currentTrackId === previousTrackId) return;
|
if (currentTrackId === previousTrackId) return;
|
||||||
previousTrackId = currentTrackId;
|
previousTrackId = currentTrackId;
|
||||||
|
|
||||||
// Update lyrics panel if it's open
|
// Update lyrics panel if it's open
|
||||||
if (sidePanelManager.isActive('lyrics')) {
|
if (sidePanelManager.isActive('lyrics')) {
|
||||||
// Re-open forces update/refresh of content and sync
|
// Re-open forces update/refresh of content and sync
|
||||||
openLyricsPanel(player.currentTrack, player.activeElement, lyricsManager, true);
|
openLyricsPanel(Player.instance.currentTrack, Player.instance.activeElement, lyricsManager, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Fullscreen if it's open
|
// Update Fullscreen if it's open
|
||||||
const fullscreenOverlay = document.getElementById('fullscreen-cover-overlay');
|
const fullscreenOverlay = document.getElementById('fullscreen-cover-overlay');
|
||||||
if (fullscreenOverlay && getComputedStyle(fullscreenOverlay).display !== 'none') {
|
if (fullscreenOverlay && getComputedStyle(fullscreenOverlay).display !== 'none') {
|
||||||
const nextTrack = player.getNextTrack();
|
const nextTrack = Player.instance.getNextTrack();
|
||||||
ui.showFullscreenCover(player.currentTrack, nextTrack, lyricsManager, player.activeElement);
|
UIRenderer.instance.showFullscreenCover(
|
||||||
|
Player.instance.currentTrack,
|
||||||
|
nextTrack,
|
||||||
|
lyricsManager,
|
||||||
|
Player.instance.activeElement
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEV: Auto-open fullscreen mode if ?fullscreen=1 in URL
|
// DEV: Auto-open fullscreen mode if ?fullscreen=1 in URL
|
||||||
|
|
@ -1105,8 +1127,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
fullscreenOverlay &&
|
fullscreenOverlay &&
|
||||||
getComputedStyle(fullscreenOverlay).display === 'none'
|
getComputedStyle(fullscreenOverlay).display === 'none'
|
||||||
) {
|
) {
|
||||||
const nextTrack = player.getNextTrack();
|
const nextTrack = Player.instance.getNextTrack();
|
||||||
ui.showFullscreenCover(player.currentTrack, nextTrack, lyricsManager, player.activeElement);
|
UIRenderer.instance.showFullscreenCover(
|
||||||
|
Player.instance.currentTrack,
|
||||||
|
nextTrack,
|
||||||
|
lyricsManager,
|
||||||
|
Player.instance.activeElement
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1126,7 +1153,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (!albumId) return;
|
if (!albumId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { tracks } = await api.getAlbum(albumId);
|
const { tracks } = await MusicAPI.instance.getAlbum(albumId);
|
||||||
if (tracks && tracks.length > 0) {
|
if (tracks && tracks.length > 0) {
|
||||||
// Sort tracks by disc and track number for consistent playback
|
// Sort tracks by disc and track number for consistent playback
|
||||||
const sortedTracks = [...tracks].sort((a, b) => {
|
const sortedTracks = [...tracks].sort((a, b) => {
|
||||||
|
|
@ -1136,11 +1163,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
return a.trackNumber - b.trackNumber;
|
return a.trackNumber - b.trackNumber;
|
||||||
});
|
});
|
||||||
|
|
||||||
player.setQueue(sortedTracks, 0);
|
Player.instance.setQueue(sortedTracks, 0);
|
||||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||||
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
||||||
player.shuffleActive = false;
|
Player.instance.shuffleActive = false;
|
||||||
await player.playTrackFromQueue();
|
await Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to play album:', error);
|
console.error('Failed to play album:', error);
|
||||||
|
|
@ -1164,14 +1191,14 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (!albumId) return;
|
if (!albumId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { tracks } = await api.getAlbum(albumId);
|
const { tracks } = await MusicAPI.instance.getAlbum(albumId);
|
||||||
if (tracks && tracks.length > 0) {
|
if (tracks && tracks.length > 0) {
|
||||||
const shuffledTracks = [...tracks].sort(() => Math.random() - 0.5);
|
const shuffledTracks = [...tracks].sort(() => Math.random() - 0.5);
|
||||||
player.setQueue(shuffledTracks, 0);
|
Player.instance.setQueue(shuffledTracks, 0);
|
||||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||||
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
||||||
player.shuffleActive = false;
|
Player.instance.shuffleActive = false;
|
||||||
await player.playTrackFromQueue();
|
await Player.instance.playTrackFromQueue();
|
||||||
|
|
||||||
const { showNotification } = await loadDownloadsModule();
|
const { showNotification } = await loadDownloadsModule();
|
||||||
showNotification('Shuffling album');
|
showNotification('Shuffling album');
|
||||||
|
|
@ -1194,7 +1221,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
btn.innerHTML = `${SVG_ANIMATE_SPIN(18)}<span>Shuffling...</span>`;
|
btn.innerHTML = `${SVG_ANIMATE_SPIN(18)}<span>Shuffling...</span>`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const artist = await api.getArtist(artistId);
|
const artist = await MusicAPI.instance.getArtist(artistId);
|
||||||
const allReleases = [...(artist.albums || []), ...(artist.eps || [])];
|
const allReleases = [...(artist.albums || []), ...(artist.eps || [])];
|
||||||
const trackSet = new Set();
|
const trackSet = new Set();
|
||||||
const allTracks = [];
|
const allTracks = [];
|
||||||
|
|
@ -1206,7 +1233,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
chunk.map(async (album) => {
|
chunk.map(async (album) => {
|
||||||
try {
|
try {
|
||||||
const { tracks } = await api.getAlbum(album.id);
|
const { tracks } = await MusicAPI.instance.getAlbum(album.id);
|
||||||
tracks.forEach((track) => {
|
tracks.forEach((track) => {
|
||||||
if (!trackSet.has(track.id)) {
|
if (!trackSet.has(track.id)) {
|
||||||
trackSet.add(track.id);
|
trackSet.add(track.id);
|
||||||
|
|
@ -1235,11 +1262,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const shuffledTracks = [...allTracks].sort(() => Math.random() - 0.5);
|
const shuffledTracks = [...allTracks].sort(() => Math.random() - 0.5);
|
||||||
player.setQueue(shuffledTracks, 0);
|
Player.instance.setQueue(shuffledTracks, 0);
|
||||||
const shuffleBtn = document.getElementById('shuffle-btn');
|
const shuffleBtn = document.getElementById('shuffle-btn');
|
||||||
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
if (shuffleBtn) shuffleBtn.classList.remove('active');
|
||||||
player.shuffleActive = false;
|
Player.instance.shuffleActive = false;
|
||||||
await player.playTrackFromQueue();
|
await Player.instance.playTrackFromQueue();
|
||||||
|
|
||||||
const { showNotification } = await loadDownloadsModule();
|
const { showNotification } = await loadDownloadsModule();
|
||||||
showNotification('Shuffling artist discography');
|
showNotification('Shuffling artist discography');
|
||||||
|
|
@ -1266,9 +1293,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
btn.innerHTML = `${SVG_ANIMATE_SPIN(20)}<span>Downloading...</span>`;
|
btn.innerHTML = `${SVG_ANIMATE_SPIN(20)}<span>Downloading...</span>`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { mix, tracks } = await api.getMix(mixId);
|
const { mix, tracks } = await MusicAPI.instance.getMix(mixId);
|
||||||
const { downloadPlaylistAsZip } = await loadDownloadsModule();
|
const { downloadPlaylistAsZip } = await loadDownloadsModule();
|
||||||
await downloadPlaylistAsZip(mix, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
|
await downloadPlaylistAsZip(
|
||||||
|
mix,
|
||||||
|
tracks,
|
||||||
|
MusicAPI.instance,
|
||||||
|
downloadQualitySettings.getQuality(),
|
||||||
|
lyricsManager
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Mix download failed:', error);
|
console.error('Mix download failed:', error);
|
||||||
alert('Failed to download mix: ' + error.message);
|
alert('Failed to download mix: ' + error.message);
|
||||||
|
|
@ -1305,13 +1338,19 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
playlist = { ...userPlaylist, title: userPlaylist.name || userPlaylist.title };
|
playlist = { ...userPlaylist, title: userPlaylist.name || userPlaylist.title };
|
||||||
tracks = userPlaylist.tracks || [];
|
tracks = userPlaylist.tracks || [];
|
||||||
} else {
|
} else {
|
||||||
const data = await api.getPlaylist(playlistId);
|
const data = await MusicAPI.instance.getPlaylist(playlistId);
|
||||||
playlist = data.playlist;
|
playlist = data.playlist;
|
||||||
tracks = data.tracks;
|
tracks = data.tracks;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { downloadPlaylistAsZip } = await loadDownloadsModule();
|
const { downloadPlaylistAsZip } = await loadDownloadsModule();
|
||||||
await downloadPlaylistAsZip(playlist, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
|
await downloadPlaylistAsZip(
|
||||||
|
playlist,
|
||||||
|
tracks,
|
||||||
|
MusicAPI.instance,
|
||||||
|
downloadQualitySettings.getQuality(),
|
||||||
|
lyricsManager
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Playlist download failed:', error);
|
console.error('Playlist download failed:', error);
|
||||||
alert('Failed to download playlist: ' + error.message);
|
alert('Failed to download playlist: ' + error.message);
|
||||||
|
|
@ -1392,7 +1431,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const folder = await db.createFolder(name, cover);
|
const folder = await db.createFolder(name, cover);
|
||||||
trackCreateFolder(folder);
|
trackCreateFolder(folder);
|
||||||
await syncManager.syncUserFolder(folder, 'create');
|
await syncManager.syncUserFolder(folder, 'create');
|
||||||
ui.renderLibraryPage();
|
UIRenderer.instance.renderLibraryPage();
|
||||||
document.getElementById('folder-modal').classList.remove('active');
|
document.getElementById('folder-modal').classList.remove('active');
|
||||||
trackCloseModal('Create Folder');
|
trackCloseModal('Create Folder');
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -1454,10 +1493,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await handlePublicStatus(playlist);
|
await handlePublicStatus(playlist);
|
||||||
await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
|
await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
|
||||||
syncManager.syncUserPlaylist(playlist, 'update');
|
syncManager.syncUserPlaylist(playlist, 'update');
|
||||||
ui.renderLibraryPage();
|
UIRenderer.instance.renderLibraryPage();
|
||||||
// Also update current page if we are on it
|
// Also update current page if we are on it
|
||||||
if (window.location.pathname === `/userplaylist/${editingId}`) {
|
if (window.location.pathname === `/userplaylist/${editingId}`) {
|
||||||
ui.renderPlaylistPage(editingId, 'user');
|
UIRenderer.instance.renderPlaylistPage(editingId, 'user');
|
||||||
}
|
}
|
||||||
modal.classList.remove('active');
|
modal.classList.remove('active');
|
||||||
delete modal.dataset.editingId;
|
delete modal.dataset.editingId;
|
||||||
|
|
@ -1548,7 +1587,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const result = await parseCSV(
|
const result = await parseCSV(
|
||||||
csvText,
|
csvText,
|
||||||
api,
|
MusicAPI.instance,
|
||||||
(progress) => {
|
(progress) => {
|
||||||
const percentage = totalTracks > 0 ? (progress.current / totalTracks) * 100 : 0;
|
const percentage = totalTracks > 0 ? (progress.current / totalTracks) * 100 : 0;
|
||||||
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
||||||
|
|
@ -1609,7 +1648,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const jspfText = await file.text();
|
const jspfText = await file.text();
|
||||||
|
|
||||||
const result = await parseJSPF(jspfText, api, (progress) => {
|
const result = await parseJSPF(jspfText, MusicAPI.instance, (progress) => {
|
||||||
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
|
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
|
||||||
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
||||||
progressCurrent.textContent = progress.current.toString();
|
progressCurrent.textContent = progress.current.toString();
|
||||||
|
|
@ -1697,7 +1736,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const result = await parseDynamicCSV(
|
const result = await parseDynamicCSV(
|
||||||
csvText,
|
csvText,
|
||||||
api,
|
MusicAPI.instance,
|
||||||
(progress) => {
|
(progress) => {
|
||||||
const percentage = totalItems > 0 ? (progress.current / totalItems) * 100 : 0;
|
const percentage = totalItems > 0 ? (progress.current / totalItems) * 100 : 0;
|
||||||
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
||||||
|
|
@ -1808,7 +1847,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const xspfText = await file.text();
|
const xspfText = await file.text();
|
||||||
|
|
||||||
const result = await parseXSPF(xspfText, api, (progress) => {
|
const result = await parseXSPF(xspfText, MusicAPI.instance, (progress) => {
|
||||||
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
|
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
|
||||||
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
||||||
progressCurrent.textContent = progress.current.toString();
|
progressCurrent.textContent = progress.current.toString();
|
||||||
|
|
@ -1867,7 +1906,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const xmlText = await file.text();
|
const xmlText = await file.text();
|
||||||
|
|
||||||
const result = await parseXML(xmlText, api, (progress) => {
|
const result = await parseXML(xmlText, MusicAPI.instance, (progress) => {
|
||||||
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
|
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
|
||||||
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
||||||
progressCurrent.textContent = progress.current.toString();
|
progressCurrent.textContent = progress.current.toString();
|
||||||
|
|
@ -1926,7 +1965,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const m3uText = await file.text();
|
const m3uText = await file.text();
|
||||||
|
|
||||||
const result = await parseM3U(m3uText, api, (progress) => {
|
const result = await parseM3U(m3uText, MusicAPI.instance, (progress) => {
|
||||||
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
|
const percentage = progress.total > 0 ? (progress.current / progress.total) * 100 : 0;
|
||||||
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
||||||
progressCurrent.textContent = progress.current.toString();
|
progressCurrent.textContent = progress.current.toString();
|
||||||
|
|
@ -1980,7 +2019,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
|
await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
|
||||||
await syncManager.syncUserPlaylist(playlist, 'create');
|
await syncManager.syncUserPlaylist(playlist, 'create');
|
||||||
trackCreatePlaylist(playlist, importSource);
|
trackCreatePlaylist(playlist, importSource);
|
||||||
ui.renderLibraryPage();
|
UIRenderer.instance.renderLibraryPage();
|
||||||
modal.classList.remove('active');
|
modal.classList.remove('active');
|
||||||
trackCloseModal('Create Playlist');
|
trackCloseModal('Create Playlist');
|
||||||
});
|
});
|
||||||
|
|
@ -2058,7 +2097,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (confirm('Are you sure you want to delete this playlist?')) {
|
if (confirm('Are you sure you want to delete this playlist?')) {
|
||||||
db.deletePlaylist(playlistId).then(() => {
|
db.deletePlaylist(playlistId).then(() => {
|
||||||
syncManager.syncUserPlaylist({ id: playlistId }, 'delete');
|
syncManager.syncUserPlaylist({ id: playlistId }, 'delete');
|
||||||
ui.renderLibraryPage();
|
UIRenderer.instance.renderLibraryPage();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2152,7 +2191,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const updatedPlaylist = await db.removeTrackFromPlaylist(playlistId, trackId, trackType);
|
const updatedPlaylist = await db.removeTrackFromPlaylist(playlistId, trackId, trackType);
|
||||||
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
|
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
|
||||||
const scrollTop = document.querySelector('.main-content').scrollTop;
|
const scrollTop = document.querySelector('.main-content').scrollTop;
|
||||||
await ui.renderPlaylistPage(playlistId, 'user');
|
await UIRenderer.instance.renderPlaylistPage(playlistId, 'user');
|
||||||
document.querySelector('.main-content').scrollTop = scrollTop;
|
document.querySelector('.main-content').scrollTop = scrollTop;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -2173,7 +2212,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
} else {
|
} else {
|
||||||
// Try API, if fail, try Public Pocketbase
|
// Try API, if fail, try Public Pocketbase
|
||||||
try {
|
try {
|
||||||
const { tracks: apiTracks } = await api.getPlaylist(playlistId);
|
const { tracks: apiTracks } = await MusicAPI.instance.getPlaylist(playlistId);
|
||||||
tracks = apiTracks;
|
tracks = apiTracks;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const publicPlaylist = await syncManager.getPublicPlaylist(playlistId);
|
const publicPlaylist = await syncManager.getPublicPlaylist(playlistId);
|
||||||
|
|
@ -2185,9 +2224,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tracks.length > 0) {
|
if (tracks.length > 0) {
|
||||||
player.setQueue(tracks, 0);
|
Player.instance.setQueue(tracks, 0);
|
||||||
document.getElementById('shuffle-btn').classList.remove('active');
|
document.getElementById('shuffle-btn').classList.remove('active');
|
||||||
await player.playTrackFromQueue();
|
await Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to play playlist:', error);
|
console.error('Failed to play playlist:', error);
|
||||||
|
|
@ -2207,9 +2246,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
btn.innerHTML = `${SVG_ANIMATE_SPIN(20)}<span>Downloading...</span>`;
|
btn.innerHTML = `${SVG_ANIMATE_SPIN(20)}<span>Downloading...</span>`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { album, tracks } = await api.getAlbum(albumId);
|
const { album, tracks } = await MusicAPI.instance.getAlbum(albumId);
|
||||||
const { downloadAlbumAsZip } = await loadDownloadsModule();
|
const { downloadAlbumAsZip } = await loadDownloadsModule();
|
||||||
await downloadAlbumAsZip(album, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
|
await downloadAlbumAsZip(
|
||||||
|
album,
|
||||||
|
tracks,
|
||||||
|
MusicAPI.instance,
|
||||||
|
downloadQualitySettings.getQuality(),
|
||||||
|
lyricsManager
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Album download failed:', error);
|
console.error('Album download failed:', error);
|
||||||
alert('Failed to download album: ' + error.message);
|
alert('Failed to download album: ' + error.message);
|
||||||
|
|
@ -2227,7 +2272,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (!albumId) return;
|
if (!albumId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { tracks } = await api.getAlbum(albumId);
|
const { tracks } = await MusicAPI.instance.getAlbum(albumId);
|
||||||
|
|
||||||
if (!tracks || tracks.length === 0) {
|
if (!tracks || tracks.length === 0) {
|
||||||
const { showNotification } = await loadDownloadsModule();
|
const { showNotification } = await loadDownloadsModule();
|
||||||
|
|
@ -2330,7 +2375,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
btn.innerHTML = `${SVG_ANIMATE_SPIN(20)}<span>Loading...</span>`;
|
btn.innerHTML = `${SVG_ANIMATE_SPIN(20)}<span>Loading...</span>`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const artist = await api.getArtist(artistId);
|
const artist = await MusicAPI.instance.getArtist(artistId);
|
||||||
|
|
||||||
const allReleases = [...(artist.albums || []), ...(artist.eps || [])];
|
const allReleases = [...(artist.albums || []), ...(artist.eps || [])];
|
||||||
if (allReleases.length === 0) {
|
if (allReleases.length === 0) {
|
||||||
|
|
@ -2352,7 +2397,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
chunk.map(async (album) => {
|
chunk.map(async (album) => {
|
||||||
try {
|
try {
|
||||||
const { tracks } = await api.getAlbum(album.id);
|
const { tracks } = await MusicAPI.instance.getAlbum(album.id);
|
||||||
tracks.forEach((track) => {
|
tracks.forEach((track) => {
|
||||||
if (!trackSet.has(track.id)) {
|
if (!trackSet.has(track.id)) {
|
||||||
trackSet.add(track.id);
|
trackSet.add(track.id);
|
||||||
|
|
@ -2372,8 +2417,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
[allTracks[i], allTracks[j]] = [allTracks[j], allTracks[i]];
|
[allTracks[i], allTracks[j]] = [allTracks[j], allTracks[i]];
|
||||||
}
|
}
|
||||||
|
|
||||||
player.setQueue(allTracks, 0);
|
Player.instance.setQueue(allTracks, 0);
|
||||||
await player.playTrackFromQueue();
|
await Player.instance.playTrackFromQueue();
|
||||||
} else {
|
} else {
|
||||||
throw new Error('No tracks found across all albums');
|
throw new Error('No tracks found across all albums');
|
||||||
}
|
}
|
||||||
|
|
@ -2400,9 +2445,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
[likedTracks[i], likedTracks[j]] = [likedTracks[j], likedTracks[i]];
|
[likedTracks[i], likedTracks[j]] = [likedTracks[j], likedTracks[i]];
|
||||||
}
|
}
|
||||||
player.setQueue(likedTracks, 0);
|
Player.instance.setQueue(likedTracks, 0);
|
||||||
document.getElementById('shuffle-btn').classList.remove('active');
|
document.getElementById('shuffle-btn').classList.remove('active');
|
||||||
await player.playTrackFromQueue();
|
await Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to shuffle liked tracks:', error);
|
console.error('Failed to shuffle liked tracks:', error);
|
||||||
|
|
@ -2424,7 +2469,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { downloadLikedTracks } = await loadDownloadsModule();
|
const { downloadLikedTracks } = await loadDownloadsModule();
|
||||||
await downloadLikedTracks(likedTracks, api, downloadQualitySettings.getQuality(), lyricsManager);
|
await downloadLikedTracks(
|
||||||
|
likedTracks,
|
||||||
|
MusicAPI.instance,
|
||||||
|
downloadQualitySettings.getQuality(),
|
||||||
|
lyricsManager
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Liked tracks download failed:', error);
|
console.error('Liked tracks download failed:', error);
|
||||||
alert('Failed to download liked tracks: ' + error.message);
|
alert('Failed to download liked tracks: ' + error.message);
|
||||||
|
|
@ -2442,8 +2492,14 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (!artistId) return;
|
if (!artistId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const artist = await api.getArtist(artistId);
|
const artist = await MusicAPI.instance.getArtist(artistId);
|
||||||
showDiscographyDownloadModal(artist, api, downloadQualitySettings.getQuality(), lyricsManager, btn);
|
showDiscographyDownloadModal(
|
||||||
|
artist,
|
||||||
|
MusicAPI.instance,
|
||||||
|
downloadQualitySettings.getQuality(),
|
||||||
|
lyricsManager,
|
||||||
|
btn
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load artist for discography download:', error);
|
console.error('Failed to load artist for discography download:', error);
|
||||||
alert('Failed to load artist: ' + error.message);
|
alert('Failed to load artist: ' + error.message);
|
||||||
|
|
@ -2486,7 +2542,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
const tracks = scanLocalMediaFolder(true);
|
const tracks = scanLocalMediaFolder(true);
|
||||||
trackSelectLocalFolder(tracks?.length ?? 0);
|
trackSelectLocalFolder(tracks?.length ?? 0);
|
||||||
ui.renderLibraryPage();
|
UIRenderer.instance.renderLibraryPage();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.name !== 'AbortError') {
|
if (err.name !== 'AbortError') {
|
||||||
console.error('Error selecting folder:', err);
|
console.error('Error selecting folder:', err);
|
||||||
|
|
@ -2506,7 +2562,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const searchForm = document.getElementById('search-form');
|
const searchForm = document.getElementById('search-form');
|
||||||
const searchInput = document.getElementById('search-input');
|
const searchInput = document.getElementById('search-input');
|
||||||
|
|
||||||
ui.setupSearchClearButton(searchInput);
|
UIRenderer.instance.setupSearchClearButton(searchInput);
|
||||||
|
|
||||||
const performSearch = (query) => {
|
const performSearch = (query) => {
|
||||||
if (query) {
|
if (query) {
|
||||||
|
|
@ -2559,16 +2615,16 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
searchInput.addEventListener('change', (e) => {
|
searchInput.addEventListener('change', (e) => {
|
||||||
const query = e.target.value.trim();
|
const query = e.target.value.trim();
|
||||||
if (query) {
|
if (query) {
|
||||||
ui.addToSearchHistory(query);
|
UIRenderer.instance.addToSearchHistory(query);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
searchInput.addEventListener('focus', () => {
|
searchInput.addEventListener('focus', () => {
|
||||||
ui.renderSearchHistory();
|
UIRenderer.instance.renderSearchHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
searchInput.addEventListener('click', () => {
|
searchInput.addEventListener('click', () => {
|
||||||
ui.renderSearchHistory();
|
UIRenderer.instance.renderSearchHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
|
|
@ -2584,7 +2640,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
if (!query) return;
|
if (!query) return;
|
||||||
|
|
||||||
if (!handleExternalLink(query)) {
|
if (!handleExternalLink(query)) {
|
||||||
ui.addToSearchHistory(query);
|
UIRenderer.instance.addToSearchHistory(query);
|
||||||
performSearch(query);
|
performSearch(query);
|
||||||
const historyEl = document.getElementById('search-history');
|
const historyEl = document.getElementById('search-history');
|
||||||
if (historyEl) historyEl.style.display = 'none';
|
if (historyEl) historyEl.style.display = 'none';
|
||||||
|
|
@ -2603,14 +2659,14 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
|
||||||
document.querySelector('.now-playing-bar .play-pause-btn').innerHTML = SVG_PLAY(20);
|
document.querySelector('.now-playing-bar .play-pause-btn').innerHTML = SVG_PLAY(20);
|
||||||
|
|
||||||
const router = createRouter(ui);
|
const router = createRouter(UIRenderer.instance);
|
||||||
|
|
||||||
const handleRouteChange = async (event) => {
|
const handleRouteChange = async (event) => {
|
||||||
const overlay = document.getElementById('fullscreen-cover-overlay');
|
const overlay = document.getElementById('fullscreen-cover-overlay');
|
||||||
const isFullscreenOpen = overlay && getComputedStyle(overlay).display === 'flex';
|
const isFullscreenOpen = overlay && getComputedStyle(overlay).display === 'flex';
|
||||||
|
|
||||||
if (isFullscreenOpen && window.location.hash !== '#fullscreen') {
|
if (isFullscreenOpen && window.location.hash !== '#fullscreen') {
|
||||||
ui.closeFullscreenCover();
|
UIRenderer.instance.closeFullscreenCover();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event && event.state && event.state.exitTrap) {
|
if (event && event.state && event.state.exitTrap) {
|
||||||
|
|
@ -2639,7 +2695,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await router();
|
await router();
|
||||||
updateTabTitle(player);
|
updateTabTitle(Player.instance);
|
||||||
};
|
};
|
||||||
|
|
||||||
await handleRouteChange();
|
await handleRouteChange();
|
||||||
|
|
@ -2661,7 +2717,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
audioPlayer.addEventListener('play', () => {
|
audioPlayer.addEventListener('play', () => {
|
||||||
updateTabTitle(player);
|
updateTabTitle(Player.instance);
|
||||||
});
|
});
|
||||||
|
|
||||||
// PWA Update Logic
|
// PWA Update Logic
|
||||||
|
|
@ -2714,14 +2770,14 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
window.addEventListener('library-changed', () => {
|
window.addEventListener('library-changed', () => {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
if (path === '/library') {
|
if (path === '/library') {
|
||||||
ui.renderLibraryPage();
|
UIRenderer.instance.renderLibraryPage();
|
||||||
} else if (path === '/' || path === '/home') {
|
} else if (path === '/' || path === '/home') {
|
||||||
ui.renderHomePage();
|
UIRenderer.instance.renderHomePage();
|
||||||
} else if (path.startsWith('/userplaylist/')) {
|
} else if (path.startsWith('/userplaylist/')) {
|
||||||
const playlistId = path.split('/')[2];
|
const playlistId = path.split('/')[2];
|
||||||
const content = document.querySelector('.main-content');
|
const content = document.querySelector('.main-content');
|
||||||
const scroll = content ? content.scrollTop : 0;
|
const scroll = content ? content.scrollTop : 0;
|
||||||
ui.renderPlaylistPage(playlistId, 'user').then(() => {
|
UIRenderer.instance.renderPlaylistPage(playlistId, 'user').then(() => {
|
||||||
if (content) content.scrollTop = scroll;
|
if (content) content.scrollTop = scroll;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -2729,7 +2785,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
window.addEventListener('history-changed', () => {
|
window.addEventListener('history-changed', () => {
|
||||||
const path = window.location.pathname;
|
const path = window.location.pathname;
|
||||||
if (path === '/recent') {
|
if (path === '/recent') {
|
||||||
ui.renderRecentPage();
|
UIRenderer.instance.renderRecentPage();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ import {
|
||||||
SVG_MIC,
|
SVG_MIC,
|
||||||
SVG_RADIO,
|
SVG_RADIO,
|
||||||
} from './icons.js';
|
} from './icons.js';
|
||||||
|
import { Player } from './player.js';
|
||||||
|
import { UIRenderer } from './ui.js';
|
||||||
|
|
||||||
const ICON_SIZE = 16;
|
const ICON_SIZE = 16;
|
||||||
|
|
||||||
|
|
@ -214,7 +216,7 @@ class CommandPalette {
|
||||||
keywords: ['play', 'pause', 'toggle', 'resume', 'stop'],
|
keywords: ['play', 'pause', 'toggle', 'resume', 'stop'],
|
||||||
shortcut: 'Space',
|
shortcut: 'Space',
|
||||||
action: () => {
|
action: () => {
|
||||||
window.monochromePlayer?.handlePlayPause();
|
Player.instance.handlePlayPause();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -225,7 +227,7 @@ class CommandPalette {
|
||||||
keywords: ['next', 'skip', 'forward'],
|
keywords: ['next', 'skip', 'forward'],
|
||||||
shortcut: 'Shift+\u2192',
|
shortcut: 'Shift+\u2192',
|
||||||
action: () => {
|
action: () => {
|
||||||
window.monochromePlayer?.playNext();
|
Player.instance.playNext();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -236,7 +238,7 @@ class CommandPalette {
|
||||||
keywords: ['previous', 'back', 'rewind'],
|
keywords: ['previous', 'back', 'rewind'],
|
||||||
shortcut: 'Shift+\u2190',
|
shortcut: 'Shift+\u2190',
|
||||||
action: () => {
|
action: () => {
|
||||||
window.monochromePlayer?.playPrev();
|
Player.instance.playPrev();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -269,7 +271,7 @@ class CommandPalette {
|
||||||
keywords: ['mute', 'unmute', 'sound', 'volume', 'silent'],
|
keywords: ['mute', 'unmute', 'sound', 'volume', 'silent'],
|
||||||
shortcut: 'M',
|
shortcut: 'M',
|
||||||
action: () => {
|
action: () => {
|
||||||
const el = window.monochromePlayer?.activeElement;
|
const el = Player.instance.activeElement;
|
||||||
if (el) el.muted = !el.muted;
|
if (el) el.muted = !el.muted;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -281,7 +283,7 @@ class CommandPalette {
|
||||||
keywords: ['volume', 'louder'],
|
keywords: ['volume', 'louder'],
|
||||||
shortcut: '\u2191',
|
shortcut: '\u2191',
|
||||||
action: () => {
|
action: () => {
|
||||||
const p = window.monochromePlayer;
|
const p = Player.instance;
|
||||||
if (p) p.setVolume(p.userVolume + 0.1);
|
if (p) p.setVolume(p.userVolume + 0.1);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -293,7 +295,7 @@ class CommandPalette {
|
||||||
keywords: ['volume', 'quieter', 'softer'],
|
keywords: ['volume', 'quieter', 'softer'],
|
||||||
shortcut: '\u2193',
|
shortcut: '\u2193',
|
||||||
action: () => {
|
action: () => {
|
||||||
const p = window.monochromePlayer;
|
const p = Player.instance;
|
||||||
if (p) p.setVolume(p.userVolume - 0.1);
|
if (p) p.setVolume(p.userVolume - 0.1);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -337,7 +339,7 @@ class CommandPalette {
|
||||||
label: 'Clear Queue',
|
label: 'Clear Queue',
|
||||||
keywords: ['wipe', 'clear', 'empty', 'queue'],
|
keywords: ['wipe', 'clear', 'empty', 'queue'],
|
||||||
action: () => {
|
action: () => {
|
||||||
window.monochromePlayer?.wipeQueue();
|
Player.instance.wipeQueue();
|
||||||
this.notify('Queue cleared');
|
this.notify('Queue cleared');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -827,7 +829,7 @@ class CommandPalette {
|
||||||
async searchMusic(query) {
|
async searchMusic(query) {
|
||||||
if (!query || query.length < 2) return;
|
if (!query || query.length < 2) return;
|
||||||
|
|
||||||
const api = window.monochromeUi?.api;
|
const api = UIRenderer.instance.api;
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
|
|
||||||
this.cancelMusicSearch();
|
this.cancelMusicSearch();
|
||||||
|
|
@ -856,8 +858,8 @@ class CommandPalette {
|
||||||
label: track.title,
|
label: track.title,
|
||||||
description: `${track.artist?.name || 'Unknown'} \u2022 ${track.album?.title || ''}`,
|
description: `${track.artist?.name || 'Unknown'} \u2022 ${track.album?.title || ''}`,
|
||||||
action: async () => {
|
action: async () => {
|
||||||
window.monochromePlayer.setQueue([track], 0);
|
Player.instance.setQueue([track], 0);
|
||||||
await window.monochromePlayer.playTrackFromQueue();
|
await Player.instance.playTrackFromQueue();
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
@ -1173,15 +1175,15 @@ class CommandPalette {
|
||||||
|
|
||||||
const overlay = document.getElementById('fullscreen-cover-overlay');
|
const overlay = document.getElementById('fullscreen-cover-overlay');
|
||||||
if (overlay && getComputedStyle(overlay).display !== 'none') {
|
if (overlay && getComputedStyle(overlay).display !== 'none') {
|
||||||
window.monochromeUi?.closeFullscreenCover();
|
UIRenderer.instance.closeFullscreenCover();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async setVisualizerPreset(preset) {
|
async setVisualizerPreset(preset) {
|
||||||
const { visualizerSettings } = await import('./storage.js');
|
const { visualizerSettings } = await import('./storage.js');
|
||||||
visualizerSettings.setPreset(preset);
|
visualizerSettings.setPreset(preset);
|
||||||
if (window.monochromeUi?.visualizer) {
|
if (UIRenderer.instance.visualizer) {
|
||||||
window.monochromeUi.visualizer.setPreset(preset);
|
UIRenderer.instance.visualizer.setPreset(preset);
|
||||||
}
|
}
|
||||||
this.notify(`Visualizer preset: ${preset}`);
|
this.notify(`Visualizer preset: ${preset}`);
|
||||||
}
|
}
|
||||||
|
|
@ -1189,8 +1191,8 @@ class CommandPalette {
|
||||||
async setQuality(quality) {
|
async setQuality(quality) {
|
||||||
const qualityNames = { LOW: 'Low', HIGH: 'High', LOSSLESS: 'Lossless', HI_RES_LOSSLESS: 'Hi-Res' };
|
const qualityNames = { LOW: 'Low', HIGH: 'High', LOSSLESS: 'Lossless', HI_RES_LOSSLESS: 'Hi-Res' };
|
||||||
|
|
||||||
if (window.monochromePlayer) {
|
if (Player.instance) {
|
||||||
window.monochromePlayer.setQuality(quality);
|
Player.instance.setQuality(quality);
|
||||||
localStorage.setItem('playback-quality', quality);
|
localStorage.setItem('playback-quality', quality);
|
||||||
const streamingSelect = document.getElementById('streaming-quality-setting');
|
const streamingSelect = document.getElementById('streaming-quality-setting');
|
||||||
if (streamingSelect) streamingSelect.value = quality;
|
if (streamingSelect) streamingSelect.value = quality;
|
||||||
|
|
@ -1205,15 +1207,15 @@ class CommandPalette {
|
||||||
}
|
}
|
||||||
|
|
||||||
setSleepTimer(minutes) {
|
setSleepTimer(minutes) {
|
||||||
if (window.monochromePlayer) {
|
if (Player.instance) {
|
||||||
window.monochromePlayer.setSleepTimer(minutes);
|
Player.instance.setSleepTimer(minutes);
|
||||||
this.notify(`Sleep timer: ${minutes} minutes`);
|
this.notify(`Sleep timer: ${minutes} minutes`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async likeAllInQueue() {
|
async likeAllInQueue() {
|
||||||
const player = window.monochromePlayer;
|
const player = Player.instance;
|
||||||
const ui = window.monochromeUi;
|
const ui = UIRenderer.instance;
|
||||||
if (!player || !ui) return;
|
if (!player || !ui) return;
|
||||||
|
|
||||||
const queue = player.getCurrentQueue();
|
const queue = player.getCurrentQueue();
|
||||||
|
|
@ -1238,8 +1240,8 @@ class CommandPalette {
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadQueue() {
|
async downloadQueue() {
|
||||||
const player = window.monochromePlayer;
|
const player = Player.instance;
|
||||||
const ui = window.monochromeUi;
|
const ui = UIRenderer.instance;
|
||||||
if (!player || !ui) return;
|
if (!player || !ui) return;
|
||||||
|
|
||||||
const queue = player.getCurrentQueue();
|
const queue = player.getCurrentQueue();
|
||||||
|
|
@ -1268,7 +1270,7 @@ class CommandPalette {
|
||||||
}
|
}
|
||||||
|
|
||||||
async clearCache() {
|
async clearCache() {
|
||||||
const api = window.monochromeUi?.api;
|
const api = UIRenderer.instance.api;
|
||||||
if (api) {
|
if (api) {
|
||||||
await api.clearCache();
|
await api.clearCache();
|
||||||
this.notify('Cache cleared');
|
this.notify('Cache cleared');
|
||||||
|
|
|
||||||
17
js/lyrics.js
17
js/lyrics.js
|
|
@ -151,6 +151,16 @@ class GeniusManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LyricsManager {
|
export class LyricsManager {
|
||||||
|
static #instance = null;
|
||||||
|
|
||||||
|
static get instance() {
|
||||||
|
if (!LyricsManager.#instance) {
|
||||||
|
throw new Error('LyricsManager is not initialized. Call LyricsManager.initialize() first.');
|
||||||
|
}
|
||||||
|
return LyricsManager.#instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
constructor(api) {
|
constructor(api) {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
this.currentLyrics = null;
|
this.currentLyrics = null;
|
||||||
|
|
@ -174,6 +184,13 @@ export class LyricsManager {
|
||||||
this.timingOffset = 0; // Offset in milliseconds (positive = delay lyrics, negative = advance lyrics)
|
this.timingOffset = 0; // Offset in milliseconds (positive = delay lyrics, negative = advance lyrics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async initialize(api) {
|
||||||
|
if (LyricsManager.#instance) {
|
||||||
|
throw new Error('LyricsManager is already initialized');
|
||||||
|
}
|
||||||
|
return (LyricsManager.#instance = new LyricsManager(api));
|
||||||
|
}
|
||||||
|
|
||||||
// Get timing offset for current track
|
// Get timing offset for current track
|
||||||
getTimingOffset(trackId) {
|
getTimingOffset(trackId) {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { getCoverBlob, getTrackTitle, getFullArtistString, getMimeType, getTrackCoverId } from './utils.js';
|
import { getCoverBlob, getTrackTitle, getFullArtistString, getMimeType, getTrackCoverId } from './utils.js';
|
||||||
import { addMetadataWithTagLib, getMetadataWithTagLib } from './taglib.ts';
|
import { addMetadataWithTagLib, getMetadataWithTagLib } from './taglib.ts';
|
||||||
import { doTimed, doTimedAsync } from './doTimed.ts';
|
import { doTimed, doTimedAsync } from './doTimed.ts';
|
||||||
import { managers } from './app.js';
|
import { LyricsManager } from './lyrics.js';
|
||||||
|
|
||||||
export function prefetchMetadataObjects(track, api, coverBlob = null) {
|
export function prefetchMetadataObjects(track, api, coverBlob = null) {
|
||||||
const coverId = getTrackCoverId(track);
|
const coverId = getTrackCoverId(track);
|
||||||
|
|
@ -10,7 +10,7 @@ export function prefetchMetadataObjects(track, api, coverBlob = null) {
|
||||||
: coverId
|
: coverId
|
||||||
? getCoverBlob(api, coverId).catch(console.error)
|
? getCoverBlob(api, coverId).catch(console.error)
|
||||||
: Promise.resolve(null);
|
: Promise.resolve(null);
|
||||||
const lyricsFetch = managers?.lyricsManager?.fetchLyrics?.(track.id, track)?.catch(console.error);
|
const lyricsFetch = LyricsManager.initialize.fetchLyrics?.(track.id, track)?.catch(console.error);
|
||||||
|
|
||||||
return { coverFetch, lyricsFetch };
|
return { coverFetch, lyricsFetch };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,15 @@ import { QobuzAPI } from './qobuz-api.js';
|
||||||
import { musicProviderSettings } from './storage.js';
|
import { musicProviderSettings } from './storage.js';
|
||||||
|
|
||||||
export class MusicAPI {
|
export class MusicAPI {
|
||||||
|
static #instance = null;
|
||||||
|
static get instance() {
|
||||||
|
if (!MusicAPI.#instance) {
|
||||||
|
throw new Error('MusicAPI not initialized. Call MusicAPI.initialize(settings) first.');
|
||||||
|
}
|
||||||
|
return MusicAPI.#instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
constructor(settings) {
|
constructor(settings) {
|
||||||
this.tidalAPI = new LosslessAPI(settings);
|
this.tidalAPI = new LosslessAPI(settings);
|
||||||
this.qobuzAPI = new QobuzAPI();
|
this.qobuzAPI = new QobuzAPI();
|
||||||
|
|
@ -13,6 +22,15 @@ export class MusicAPI {
|
||||||
this.videoArtworkCache = new Map();
|
this.videoArtworkCache = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async initialize(settings) {
|
||||||
|
if (MusicAPI.#instance) {
|
||||||
|
throw new Error('MusicAPI is already initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const api = new MusicAPI(settings);
|
||||||
|
return (MusicAPI.#instance = api);
|
||||||
|
}
|
||||||
|
|
||||||
getCurrentProvider() {
|
getCurrentProvider() {
|
||||||
return musicProviderSettings.getProvider();
|
return musicProviderSettings.getProvider();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
33
js/player.js
33
js/player.js
|
|
@ -23,7 +23,19 @@ import { db } from './db.js';
|
||||||
|
|
||||||
import('./dash-media-player.js');
|
import('./dash-media-player.js');
|
||||||
import { SVG_CLOCK } from './icons.js';
|
import { SVG_CLOCK } from './icons.js';
|
||||||
|
import { UIRenderer } from './ui.js';
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
|
static #instance = null;
|
||||||
|
|
||||||
|
static get instance() {
|
||||||
|
if (!Player.#instance) {
|
||||||
|
throw new Error('Player is not initialized. Call Player.initialize(audioElement, api) first.');
|
||||||
|
}
|
||||||
|
return Player.#instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') {
|
constructor(audioElement, api, quality = 'HI_RES_LOSSLESS') {
|
||||||
this.audio = audioElement;
|
this.audio = audioElement;
|
||||||
this.video = document.getElementById('video-player');
|
this.video = document.getElementById('video-player');
|
||||||
|
|
@ -63,6 +75,17 @@ export class Player {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async initialize(audioElement, api, quality) {
|
||||||
|
if (Player.#instance) {
|
||||||
|
throw new Error('Player is already initialized');
|
||||||
|
}
|
||||||
|
|
||||||
|
const player = new Player(audioElement, api, quality);
|
||||||
|
await player.init();
|
||||||
|
Player.#instance = player;
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
// Apply audio effects when track is ready
|
// Apply audio effects when track is ready
|
||||||
this.audio.addEventListener('canplay', () => {
|
this.audio.addEventListener('canplay', () => {
|
||||||
|
|
@ -825,12 +848,12 @@ export class Player {
|
||||||
const played = await this.safePlay(activeElement);
|
const played = await this.safePlay(activeElement);
|
||||||
if (!played) return;
|
if (!played) return;
|
||||||
} else if (track.type === 'video') {
|
} else if (track.type === 'video') {
|
||||||
if (window.monochromeUi) {
|
if (UIRenderer.instance) {
|
||||||
const isInFullscreen =
|
const isInFullscreen =
|
||||||
document.getElementById('fullscreen-cover-overlay')?.style.display === 'flex';
|
document.getElementById('fullscreen-cover-overlay')?.style.display === 'flex';
|
||||||
if (!isInFullscreen) {
|
if (!isInFullscreen) {
|
||||||
const lyricsManager = window.monochromeUi.lyricsManager;
|
const lyricsManager = UIRenderer.instance.lyricsManager;
|
||||||
window.monochromeUi.showFullscreenCover(
|
UIRenderer.instance.showFullscreenCover(
|
||||||
track,
|
track,
|
||||||
this.getNextTrack(),
|
this.getNextTrack(),
|
||||||
lyricsManager,
|
lyricsManager,
|
||||||
|
|
@ -1483,8 +1506,8 @@ export class Player {
|
||||||
this.originalQueueBeforeShuffle = [];
|
this.originalQueueBeforeShuffle = [];
|
||||||
this.currentQueueIndex = -1;
|
this.currentQueueIndex = -1;
|
||||||
this.saveQueueState();
|
this.saveQueueState();
|
||||||
if (window.monochromeUi) {
|
if (UIRenderer.instance) {
|
||||||
window.monochromeUi.setCurrentTrack(null);
|
UIRenderer.instance.setCurrentTrack(null);
|
||||||
}
|
}
|
||||||
if (window.renderQueueFunction) {
|
if (window.renderQueueFunction) {
|
||||||
window.renderQueueFunction();
|
window.renderQueueFunction();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { navigate } from './router.js';
|
||||||
import { MusicAPI } from './music-api.js';
|
import { MusicAPI } from './music-api.js';
|
||||||
import { apiSettings } from './storage.js';
|
import { apiSettings } from './storage.js';
|
||||||
import { debounce, escapeHtml } from './utils.js';
|
import { debounce, escapeHtml } from './utils.js';
|
||||||
|
import { Player } from './player.js';
|
||||||
|
|
||||||
// objects execution february 29th 2027
|
// objects execution february 29th 2027
|
||||||
|
|
||||||
|
|
@ -826,9 +827,9 @@ async function handleTrackClick(title, artist) {
|
||||||
const results = await api.searchTracks(query, { limit: 1 });
|
const results = await api.searchTracks(query, { limit: 1 });
|
||||||
if (results.items.length > 0) {
|
if (results.items.length > 0) {
|
||||||
const track = results.items[0];
|
const track = results.items[0];
|
||||||
if (window.monochromePlayer) {
|
if (Player.instance) {
|
||||||
window.monochromePlayer.setQueue([track], 0);
|
Player.instance.setQueue([track], 0);
|
||||||
window.monochromePlayer.playTrackFromQueue();
|
Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
alert('Track not found');
|
alert('Track not found');
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,10 @@
|
||||||
import { escapeHtml, trackDataStore, formatTime } from './utils.js';
|
import { escapeHtml, trackDataStore, formatTime } from './utils.js';
|
||||||
import { navigate } from './router.js';
|
import { navigate } from './router.js';
|
||||||
import { SVG_MENU, SVG_PLAY, SVG_HEART } from './icons.js';
|
import { SVG_MENU, SVG_PLAY, SVG_HEART } from './icons.js';
|
||||||
|
import { Player } from './player.js';
|
||||||
|
|
||||||
let artistsData = [];
|
let artistsData = [];
|
||||||
let artistsPopularity = new Map(); // name -> popularity score
|
let artistsPopularity = new Map(); // name -> popularity score
|
||||||
let globalPlayer = null;
|
|
||||||
|
|
||||||
// Map to store artist info keyed by sheetId for quick lookup
|
// Map to store artist info keyed by sheetId for quick lookup
|
||||||
const artistBySheetId = new Map();
|
const artistBySheetId = new Map();
|
||||||
|
|
@ -372,8 +372,8 @@ export async function renderTrackerArtistPage(sheetId, container) {
|
||||||
const availableTracks = allTracks.filter((t) => !t.unavailable);
|
const availableTracks = allTracks.filter((t) => !t.unavailable);
|
||||||
if (availableTracks.length > 0) {
|
if (availableTracks.length > 0) {
|
||||||
const shuffled = [...availableTracks].sort(() => Math.random() - 0.5);
|
const shuffled = [...availableTracks].sort(() => Math.random() - 0.5);
|
||||||
globalPlayer.setQueue(shuffled, 0);
|
Player.instance.setQueue(shuffled, 0);
|
||||||
globalPlayer.playTrackFromQueue();
|
Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -450,8 +450,8 @@ export async function renderTrackerArtistPage(sheetId, container) {
|
||||||
}
|
}
|
||||||
const availableTracks = eraTracks.filter((t) => !t.unavailable);
|
const availableTracks = eraTracks.filter((t) => !t.unavailable);
|
||||||
if (availableTracks.length > 0) {
|
if (availableTracks.length > 0) {
|
||||||
globalPlayer.setQueue(availableTracks, 0);
|
Player.instance.setQueue(availableTracks, 0);
|
||||||
globalPlayer.playTrackFromQueue();
|
Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
} else if (e.target.closest('.card-menu-btn')) {
|
} else if (e.target.closest('.card-menu-btn')) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -521,8 +521,8 @@ export async function renderTrackerArtistPage(sheetId, container) {
|
||||||
const availableTracks = searchTracks.filter((t) => !t.unavailable);
|
const availableTracks = searchTracks.filter((t) => !t.unavailable);
|
||||||
const trackIndex = availableTracks.findIndex((t) => t.id === track.id);
|
const trackIndex = availableTracks.findIndex((t) => t.id === track.id);
|
||||||
if (trackIndex >= 0 && availableTracks.length > 0) {
|
if (trackIndex >= 0 && availableTracks.length > 0) {
|
||||||
globalPlayer.setQueue(availableTracks, trackIndex);
|
Player.instance.setQueue(availableTracks, trackIndex);
|
||||||
globalPlayer.playTrackFromQueue();
|
Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -601,8 +601,8 @@ export async function renderTrackerProjectPage(sheetId, projectName, container,
|
||||||
playBtn.onclick = () => {
|
playBtn.onclick = () => {
|
||||||
const availableTracks = eraTracks.filter((t) => !t.unavailable);
|
const availableTracks = eraTracks.filter((t) => !t.unavailable);
|
||||||
if (availableTracks.length > 0) {
|
if (availableTracks.length > 0) {
|
||||||
globalPlayer.setQueue(availableTracks, 0);
|
Player.instance.setQueue(availableTracks, 0);
|
||||||
globalPlayer.playTrackFromQueue();
|
Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -612,8 +612,8 @@ export async function renderTrackerProjectPage(sheetId, projectName, container,
|
||||||
const availableTracks = eraTracks.filter((t) => !t.unavailable);
|
const availableTracks = eraTracks.filter((t) => !t.unavailable);
|
||||||
if (availableTracks.length > 0) {
|
if (availableTracks.length > 0) {
|
||||||
const shuffled = [...availableTracks].sort(() => Math.random() - 0.5);
|
const shuffled = [...availableTracks].sort(() => Math.random() - 0.5);
|
||||||
globalPlayer.setQueue(shuffled, 0);
|
Player.instance.setQueue(shuffled, 0);
|
||||||
globalPlayer.playTrackFromQueue();
|
Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -643,8 +643,8 @@ export async function renderTrackerProjectPage(sheetId, projectName, container,
|
||||||
const availableTracks = eraTracks.filter((t) => !t.unavailable);
|
const availableTracks = eraTracks.filter((t) => !t.unavailable);
|
||||||
const trackIndex = availableTracks.findIndex((t) => t.id === track.id);
|
const trackIndex = availableTracks.findIndex((t) => t.id === track.id);
|
||||||
if (trackIndex >= 0 && availableTracks.length > 0) {
|
if (trackIndex >= 0 && availableTracks.length > 0) {
|
||||||
globalPlayer.setQueue(availableTracks, trackIndex);
|
Player.instance.setQueue(availableTracks, trackIndex);
|
||||||
globalPlayer.playTrackFromQueue();
|
Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
@ -702,8 +702,8 @@ export async function renderTrackerProjectPage(sheetId, projectName, container,
|
||||||
}
|
}
|
||||||
const availableTracks = otherEraTracks.filter((t) => !t.unavailable);
|
const availableTracks = otherEraTracks.filter((t) => !t.unavailable);
|
||||||
if (availableTracks.length > 0) {
|
if (availableTracks.length > 0) {
|
||||||
globalPlayer.setQueue(availableTracks, 0);
|
Player.instance.setQueue(availableTracks, 0);
|
||||||
globalPlayer.playTrackFromQueue();
|
Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
} else if (e.target.closest('.card-menu-btn')) {
|
} else if (e.target.closest('.card-menu-btn')) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
@ -907,8 +907,8 @@ export async function renderTrackerTrackPage(trackId, container, _ui) {
|
||||||
const availableTracks = allTracks.filter((t) => !t.unavailable);
|
const availableTracks = allTracks.filter((t) => !t.unavailable);
|
||||||
const trackPos = availableTracks.findIndex((t) => t.id === currentTrack.id);
|
const trackPos = availableTracks.findIndex((t) => t.id === currentTrack.id);
|
||||||
if (trackPos >= 0 && availableTracks.length > 0) {
|
if (trackPos >= 0 && availableTracks.length > 0) {
|
||||||
globalPlayer.setQueue(availableTracks, trackPos);
|
Player.instance.setQueue(availableTracks, trackPos);
|
||||||
globalPlayer.playTrackFromQueue();
|
Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -938,8 +938,8 @@ export async function renderTrackerTrackPage(trackId, container, _ui) {
|
||||||
const availableTracks = allTracks.filter((t) => !t.unavailable);
|
const availableTracks = allTracks.filter((t) => !t.unavailable);
|
||||||
const trackPos = availableTracks.findIndex((t) => t.id === currentTrack.id);
|
const trackPos = availableTracks.findIndex((t) => t.id === currentTrack.id);
|
||||||
if (trackPos >= 0 && availableTracks.length > 0) {
|
if (trackPos >= 0 && availableTracks.length > 0) {
|
||||||
globalPlayer.setQueue(availableTracks, trackPos);
|
Player.instance.setQueue(availableTracks, trackPos);
|
||||||
globalPlayer.playTrackFromQueue();
|
Player.instance.playTrackFromQueue();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -982,8 +982,7 @@ export async function renderTrackerTrackPage(trackId, container, _ui) {
|
||||||
document.title = `${currentTrack.title} - ${artist.name}`;
|
document.title = `${currentTrack.title} - ${artist.name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initTracker(player) {
|
export async function initTracker() {
|
||||||
globalPlayer = player;
|
|
||||||
await Promise.all([loadArtistsPopularity(), loadArtistsData()]);
|
await Promise.all([loadArtistsPopularity(), loadArtistsData()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
17
js/ui.js
17
js/ui.js
|
|
@ -117,6 +117,16 @@ function sortTracks(tracks, sortType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UIRenderer {
|
export class UIRenderer {
|
||||||
|
static #instance = null;
|
||||||
|
|
||||||
|
static get instance() {
|
||||||
|
if (!UIRenderer.#instance) {
|
||||||
|
throw new Error('UIRenderer is not initialized. Call UIRenderer.initialize(api, player) first.');
|
||||||
|
}
|
||||||
|
return UIRenderer.#instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @private */
|
||||||
constructor(api, player) {
|
constructor(api, player) {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
this.player = player;
|
this.player = player;
|
||||||
|
|
@ -145,6 +155,13 @@ export class UIRenderer {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async initialize(api, player) {
|
||||||
|
if (UIRenderer.#instance) {
|
||||||
|
throw new Error('UIRenderer is already initialized');
|
||||||
|
}
|
||||||
|
return (UIRenderer.#instance = new UIRenderer(api, player));
|
||||||
|
}
|
||||||
|
|
||||||
// Helper for Heart Icon
|
// Helper for Heart Icon
|
||||||
createHeartIcon(filled = false) {
|
createHeartIcon(filled = false) {
|
||||||
if (filled) {
|
if (filled) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue