fix(linting): fix js linting issues

This commit is contained in:
Daniel 2026-04-03 16:05:29 -05:00 committed by edideaur
parent ddc986bc52
commit 648e47e1d8
65 changed files with 1202 additions and 1037 deletions

View file

@ -1,5 +1,5 @@
export async function onRequest(context) {
const { request, env } = context;
const { request } = context;
const pageUrl = request.url;
const metaHtml = `

View file

@ -1,5 +1,5 @@
export async function onRequest(context) {
const { request, env } = context;
const { request } = context;
const pageUrl = request.url;
const metaHtml = `

View file

@ -1,5 +1,5 @@
export async function onRequest(context) {
const { request, env } = context;
const { request } = context;
const pageUrl = request.url;
const metaHtml = `

View file

@ -1,5 +1,5 @@
export async function onRequest(context) {
const { request, env } = context;
const { request } = context;
const pageUrl = request.url;
const metaHtml = `

View file

@ -50,7 +50,7 @@ export async function onRequest(context) {
const title = feed.title;
const author = feed.author || feed.ownerName || '';
const episodeCount = feed.episodeCount || 0;
const rawDescription = feed.description || '';
const _rawDescription = feed.description || '';
const description = author
? `Podcast by ${author}${episodeCount} Episodes\nListen on Monochrome`
: `Podcast • ${episodeCount} Episodes\nListen on Monochrome`;

View file

@ -1,5 +1,5 @@
export async function onRequest(context) {
const { request, env } = context;
const { request } = context;
const pageUrl = request.url;
const metaHtml = `

View file

@ -1,5 +1,5 @@
export async function onRequest(context) {
const { request, env } = context;
const { request } = context;
const pageUrl = request.url;
const metaHtml = `

View file

@ -1,7 +1,7 @@
// functions/unreleased/[sheetId]/[projectName].js
const ARTISTS_NDJSON_URL = 'https://assets.artistgrid.cx/artists.ndjson';
const ASSETS_BASE_URL = 'https://assets.artistgrid.cx';
const _ASSETS_BASE_URL = 'https://assets.artistgrid.cx';
const TRACKER_API_ENDPOINTS = [
'https://trackerapi-1.artistgrid.cx/get/',
'https://trackerapi-2.artistgrid.cx/get/',
@ -14,7 +14,7 @@ function getSheetId(url) {
return match ? match[1] : null;
}
function normalizeArtistName(name) {
function _normalizeArtistName(name) {
return name.toLowerCase().replace(/[^a-z0-9]/g, '');
}
@ -62,7 +62,7 @@ async function fetchTrackerData(sheetId) {
}
return data;
} catch (e) {
console.warn(`Failed to fetch from ${baseUrl}, trying next...`);
console.warn(`Failed to fetch from ${baseUrl}, trying next...`, e);
}
}
return null;

View file

@ -1,5 +1,5 @@
export async function onRequest(context) {
const { request, env } = context;
const { request } = context;
const pageUrl = request.url;
const metaHtml = `

View file

@ -1,11 +1,12 @@
import { expect, suite, test } from 'vitest';
import { expect, test } from 'vitest';
import { HiFiClient, TidalResponse } from './HiFi';
import type { Album, PlaybackInfo, Track } from './container-classes';
const ARTIST_ID = 3523908; // deadmau5
const ALBUM_ID = 433360012; // deadmau5 - 4x4=12
const ALBUM_ATMOS = 463900719; // Taylor Swift - The Life of a Showgirl
const _ALBUM_ATMOS = 463900719; // Taylor Swift - The Life of a Showgirl
const TRACK_ATMOS = 463900720; // Taylor Swift - The Fate of Ophelia
const TRACK_NO_LOSSLESS = 31097959; // deadmau5 - while(1<2)
const _TRACK_NO_LOSSLESS = 31097959; // deadmau5 - while(1<2)
const TRACK_VIDEO = 466464180; // Taylow Swift - The Fate of Ophelia
const TRACK_LOSSLESS = 31097949; // deadmau5 - Avaritia
const PLAYLIST_ID = '36ea71a8-445e-41a4-82ab-6628c581535d'; // Pop Hits
@ -19,25 +20,25 @@ function checkVersion({ version }: { version?: string }) {
expect(version).equals(HiFiClient.API_VERSION);
}
async function getJson(res: Response | Promise<Response>) {
async function _getJson(res: Response | Promise<Response>) {
res = await res;
expect(res).toBeInstanceOf(Response);
expect(res.ok).toBeTruthy();
return await res.json();
return (await res.json()) as object;
}
async function checkRoute(
route: string,
routeResult: () => Promise<any>,
checks: (data: any) => Promise<void>,
routeResult: () => Promise<Response>,
checks: (data: object) => Promise<void>,
mainKey: string | null = 'data'
) {
const routeData = await instance.query(route);
const routeRes = await routeResult();
const routeRes = (await routeResult()) as unknown;
expect(routeData).toBeInstanceOf(TidalResponse);
expect(routeData).toEqual(routeRes);
const json = await routeData.json();
const json = (await routeData.json()) as object;
checkVersion(json);
if (mainKey != null) {
@ -71,7 +72,7 @@ test('Fetch atmos track info', async () => {
await checkRoute(
`/info/?id=${TRACK_ATMOS}`,
() => instance.getInfo(TRACK_ATMOS),
async (info) => {
async (info: { data: Track }) => {
expect(info.data.audioModes).toContain('DOLBY_ATMOS');
}
);
@ -81,8 +82,8 @@ test('Fetch track', async () => {
await checkRoute(
`/track/?id=${TRACK_LOSSLESS}`,
() => instance.getTrack(TRACK_LOSSLESS),
async (track) => {
expect(track.data.trackId).toBe(TRACK_LOSSLESS);
async (track: { data: PlaybackInfo }) => {
expect(track?.data?.trackId).toBe(TRACK_LOSSLESS);
expect(track.data.assetPresentation).toBeTypeOf('string');
expect(track.data.audioQuality).toBeTypeOf('string');
expect(track.data.manifestMimeType).toBeTypeOf('string');
@ -102,7 +103,7 @@ test.skipIf(!instance.refreshToken)('Fetch recommendations', async () => {
await checkRoute(
`/recommendations/?id=${ARTIST_ID}`,
() => instance.getRecommendations(ARTIST_ID),
async (rec) => {}
async (_data) => {}
);
});
@ -110,7 +111,7 @@ test('Fetch similar artists', async () => {
await checkRoute(
`/artist/similar/?id=${ARTIST_ID}`,
() => instance.getSimilarArtists(ARTIST_ID),
async (rec) => {},
async (_data) => {},
'artists'
);
});
@ -119,7 +120,7 @@ test('Fetch similar albums', async () => {
await checkRoute(
`/album/similar/?id=${ALBUM_ID}`,
() => instance.getSimilarAlbums(ALBUM_ID),
async (rec) => {},
async (_data) => {},
'albums'
);
});
@ -128,7 +129,7 @@ test('Fetch artist info', async () => {
await checkRoute(
`/artist/?id=${ARTIST_ID}`,
() => instance.getArtist(ARTIST_ID),
async (info) => {
async (info: { cover: string }) => {
expect(info).toHaveProperty('cover');
expect(info.cover).not.toBeUndefined();
},
@ -144,7 +145,7 @@ test('Search', async () => {
instance.search({
q: query,
}),
async (res) => {}
async (_res) => {}
);
});
@ -152,7 +153,7 @@ test('Fetch album info', async () => {
await checkRoute(
`/album/?id=${ALBUM_ID}`,
() => instance.getAlbum(ALBUM_ID),
async (info) => {
async (info: { data: Album }) => {
expect(info.data).toHaveProperty('cover');
expect(info.data.cover).not.toBeUndefined();
}
@ -163,7 +164,7 @@ test('Fetch playlist info', async () => {
await checkRoute(
`/playlist/?id=${PLAYLIST_ID}`,
() => instance.getPlaylist(PLAYLIST_ID),
async (info) => {
async (info: { playlist: { image: string } }) => {
expect(info.playlist).toHaveProperty('image');
expect(info.playlist.image).not.toBeUndefined();
},
@ -175,7 +176,7 @@ test.skipIf(!instance.refreshToken)('Fetch lyrics ', async () => {
await checkRoute(
`/lyrics/?id=${TRACK_ATMOS}`,
() => instance.getLyrics(TRACK_ATMOS),
async (info) => {},
async (_info) => {},
'lyrics'
);
});
@ -184,7 +185,7 @@ test('Fetch video ', async () => {
await checkRoute(
`/video/?id=${TRACK_VIDEO}`,
() => instance.getVideo(TRACK_VIDEO),
async (info) => {},
async (_info) => {},
'video'
);
});
@ -193,7 +194,7 @@ test('Fetch track manifests ', async () => {
await checkRoute(
`/trackManifests/?id=${TRACK_LOSSLESS}`,
() => instance.getTrackManifest(TRACK_LOSSLESS),
async (info) => {},
async (_info) => {},
'data'
);
});

View file

@ -1,3 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { EventEmitter } from 'events';
type Params = Record<string, string | number | undefined | null>;
@ -170,7 +177,7 @@ class HiFiClient {
scope?: string;
signal?: AbortSignal;
force?: boolean;
}) {
}): Promise<string | null> {
if (!force && this.token && (this.appTokenExpiry < 0 || Date.now() < this.appTokenExpiry)) return this.token;
return await (this.#tokenPromise ??= (async () => {
@ -654,7 +661,7 @@ class HiFiClient {
};
const data = await this.#fetchJson(url, params, signal);
return HiFiClient.#jsonResponse({ version: API_VERSION, data: data });
return HiFiClient.#jsonResponse({ version: HiFiClient.API_VERSION, data: data });
}
#buildCoverEntry(cover_slug: string, name?: string | null, track_id?: number | null) {

View file

@ -12,9 +12,9 @@ import { db } from './db';
*
* @template C The accumulated shape of the settings object.
*/
class ModernSettings<C extends object = {}> {
class ModernSettings<C extends object = object> {
/** Internal map of pending async operations keyed by unique symbols. */
#pending: Record<symbol, Promise<any>> = {};
#pending: Record<symbol, Promise<void>> = {};
/** Whether new properties are prevented from being added. */
#finalized: boolean = false;
@ -51,7 +51,7 @@ class ModernSettings<C extends object = {}> {
* @param callback Function producing the promise to track.
* @returns The created promise.
*/
#addPending<C extends Promise<any>>(callback: () => C): C {
#addPending<C extends Promise<void>>(callback: () => C): C {
const sym = Symbol();
return (this.#pending[sym] = callback().finally(() => {
@ -145,14 +145,14 @@ class ModernSettings<C extends object = {}> {
const legacyValue = localStorage.getItem(legacy?.key ?? backingKey ?? key);
if (legacyValue !== null) {
db.saveSetting(backingKey ?? key, legacy.transformer!(legacyValue));
await db.saveSetting(backingKey ?? key, legacy.transformer(legacyValue));
localStorage.removeItem(legacy?.key ?? backingKey ?? key);
}
}
}
try {
value = (await db.getSetting(backingKey ?? key)) ?? defaultValue;
value = ((await db.getSetting(backingKey ?? key)) as T) ?? defaultValue;
} catch {
value = defaultValue;
}
@ -162,7 +162,7 @@ class ModernSettings<C extends object = {}> {
get: () => (getter ? getter(value, typed as ModernSettings<C> & C & Record<K, T>) : value),
set: (newValue: T) => {
value = setter ? setter(newValue, typed as ModernSettings<C> & C & Record<K, T>) : newValue;
this.#addPending(() => db.saveSetting(backingKey ?? key, value));
void this.#addPending(() => db.saveSetting(backingKey ?? key, value));
},
enumerable: true,
});

View file

@ -5,7 +5,7 @@ export class AuthManager {
constructor() {
this.user = null;
this.authListeners = [];
this.init();
this.init().catch(console.error);
}
async init() {

View file

@ -128,7 +128,7 @@ const syncManager = {
}
},
safeParseInternal(str, fieldName, fallback) {
safeParseInternal(str, _fieldName, fallback) {
if (!str) return fallback;
if (typeof str !== 'string') return str;
try {
@ -136,7 +136,7 @@ const syncManager = {
} catch {
try {
// Recovery attempt: replace illegal internal quotes in name/title fields
const recovered = str.replace(/(:\s*")(.+?)("(?=\s*[,}\n\r]))/g, (match, p1, p2, p3) => {
const recovered = str.replace(/(:\s*")(.+?)("(?=\s*[,}\n\r]))/g, (_match, p1, p2, p3) => {
const escapedContent = p2.replace(/(?<!\\)"/g, '\\"');
return p1 + escapedContent + p3;
});
@ -156,6 +156,8 @@ const syncManager = {
(jsFriendly.trim().startsWith('[') || jsFriendly.trim().startsWith('{')) &&
!jsFriendly.match(/function|=>|window|document|alert|eval/)
) {
// TODO: maybe this could be parsed as json5?
// eslint-disable-next-line @typescript-eslint/no-implied-eval
return new Function('return ' + jsFriendly)();
}
}
@ -565,11 +567,6 @@ const syncManager = {
if (cloudData) {
let database = db;
if (typeof database === 'function') {
database = await database();
} else {
database = await database;
}
const localData = {
tracks: (await database.getAll('favorites_tracks')) || [],

View file

@ -5,10 +5,7 @@ import {
delay,
isTrackUnavailable,
getExtensionFromBlob,
getTrackTitle,
getFullArtistString,
getTrackDiscNumber,
getMimeType,
} from './utils.js';
import { preferDolbyAtmosSettings, trackDateSettings } from './storage.js';
import { APICache } from './cache.js';
@ -36,7 +33,6 @@ import {
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
export { resolveDownloadTotalBytes };
const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25';
export class LosslessAPI {
constructor(settings) {
@ -48,8 +44,8 @@ export class LosslessAPI {
this.streamCache = new Map();
setInterval(
() => {
this.cache.clearExpired();
async () => {
await this.cache.clearExpired();
this.pruneStreamCache();
},
1000 * 60 * 5
@ -492,7 +488,7 @@ export class LosslessAPI {
await this.cache.set('search_all', query, results);
return results;
} catch (error) {
} catch (_error) {
// Fallback to individual searches if the backend proxy doesn't support ?q= or throws
const [tracks, videos, artists, albums, playlists] = await Promise.all([
this.searchTracks(query, options).catch(() => ({ items: [] })),
@ -1558,7 +1554,7 @@ export class LosslessAPI {
} else {
throw new Error('No URI in trackManifests response');
}
} catch (err) {
} catch (_err) {
// Fallback to /track endpoint
}

View file

@ -2,7 +2,6 @@ import { expect, test, suite, vi } from 'vitest';
import { apiSettings, preferDolbyAtmosSettings, losslessContainerSettings } from './storage.js';
import { MusicAPI } from './music-api.js';
import { LyricsManager } from './lyrics.js';
import type { LosslessAPI } from './api.js';
import { HiFiClient } from './HiFi.js';
import { FileRef } from '!/@dantheman827/taglib-ts/src/fileRef.js';
import { Mp4File } from '!/@dantheman827/taglib-ts/src/mp4/mp4File.js';
@ -13,6 +12,7 @@ import { ByteVector, StringType } from '!/@dantheman827/taglib-ts/src/byteVector
import { Mp4Codec } from '!/@dantheman827/taglib-ts/src/mp4/mp4Properties.js';
import { OggFile } from '!/@dantheman827/taglib-ts/src/ogg/oggFile.js';
import { ffmpeg } from './ffmpeg.js';
import type { Track } from './container-classes.js';
vi.mock(import('./storage.js'), async (importOriginal) => {
const mod = await importOriginal();
@ -46,26 +46,25 @@ vi.mock(import('./doTimed.js'), async (importOriginal) => {
return {
...mod,
doTimed: (label: string, fn: () => any) => {
return fn() as any;
doTimed: function <T>(_label: string, fn: () => T): T {
return fn();
},
doTimedAsync<T, R = T extends Promise<T> ? Promise<T> : T>(
message: string,
_message: string,
callback: () => R,
throwError: boolean = false
): R {
return new Promise<R>(async (resolve, reject) => {
try {
const ret = await callback();
resolve(ret);
} catch (err) {
if (throwError) {
reject(err);
return;
}
resolve(undefined);
}
return new Promise<R>((resolve, reject) => {
Promise.resolve()
.then(callback)
.then(resolve)
.catch((err) => {
if (throwError) {
reject(err as Error);
} else {
resolve(undefined);
}
});
}) as R;
},
} satisfies typeof import('./doTimed.js');
@ -99,15 +98,14 @@ suite('Track Downloads', async () => {
const TRACK_ATMOS = 463900720; // Taylor Swift - The Fate of Ophelia
const TRACK_NO_LOSSLESS = 31097959; // deadmau5 - while(1<2)
const { LosslessAPI } = await import('./api.js');
await MusicAPI.initialize(apiSettings);
await LyricsManager.initialize(apiSettings);
await HiFiClient.initialize();
const api: LosslessAPI = MusicAPI.instance.tidalAPI;
const api = MusicAPI.instance.tidalAPI;
async function downloadTrack(trackId: number, quality: string) {
const track = await (await HiFiClient.instance.getInfo(trackId)).json();
const track = (await (await HiFiClient.instance.getInfo(trackId)).json()) as { data: Track };
return await api.downloadTrack(trackId.toString(), quality, undefined, {
track: track.data,
triggerDownload: false,
@ -276,7 +274,9 @@ suite('Track Downloads', async () => {
ffmpegCalls: 1,
},
])('$display_quality', async ({ quality, container, preferDolbyAtmos, trackId, detection, ffmpegCalls }) => {
// eslint-disable-next-line @typescript-eslint/unbound-method
vi.mocked(preferDolbyAtmosSettings.isEnabled).mockReturnValue(preferDolbyAtmos);
// eslint-disable-next-line @typescript-eslint/unbound-method
vi.mocked(losslessContainerSettings.getContainer).mockReturnValue(container);
const blob = await downloadTrack(trackId, quality);
@ -286,7 +286,6 @@ suite('Track Downloads', async () => {
expect(file.isValid).toBe(true);
let trak: Mp4Atom | null = null;
let stsd: Mp4Atom | null = null;
let stsdData: ByteVector | null = null;
@ -313,13 +312,13 @@ suite('Track Downloads', async () => {
trak = null;
}
expect(trak).toBeInstanceOf(Mp4Atom);
stsd = trak!.find('mdia', 'minf', 'stbl', 'stsd');
stsd = trak.find('mdia', 'minf', 'stbl', 'stsd');
expect(stsd).toBeInstanceOf(Mp4Atom);
await stream.seek(stsd.offset);
stsdData = await stream.readBlock(stsd.length);
}
stream.seek(streamPosition);
await stream.seek(streamPosition);
switch (detection) {
case Detection.DolbyAtmos: {

165
js/app.js
View file

@ -33,7 +33,6 @@ import { authManager } from './accounts/auth.js';
import { registerSW } from 'virtual:pwa-register';
import { openEditProfile } from './profile.js';
import { ThemeStore } from './themeStore.js';
import { partyManager } from './listening-party.js';
import './commandPalette.js';
import { initTracker } from './tracker.js';
import {
@ -406,6 +405,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// Populate commit info
{
const repo = 'https://github.com/monochrome-music/monochrome';
// eslint-disable-next-line no-undef
const hash = typeof __COMMIT_HASH__ !== 'undefined' ? __COMMIT_HASH__ : 'dev';
const commitLink =
hash !== 'dev' && hash !== 'unknown'
@ -469,8 +469,7 @@ document.addEventListener('DOMContentLoaded', async () => {
await Player.initialize(audioPlayer, MusicAPI.instance, currentQuality);
// Initialize tracker
initTracker();
initTracker().catch(console.error);
const castBtn = document.getElementById('cast-btn');
initializeCasting(audioPlayer, castBtn);
@ -585,7 +584,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// checks for a saved handle and (in browser mode) requests read permission,
// so this is a silent no-op when no folder is configured or permission is not
// yet granted.
scanLocalMediaFolder();
scanLocalMediaFolder().catch(console.error);
const scrobbler = new MultiScrobbler();
window.monochromeScrobbler = scrobbler;
@ -995,9 +994,9 @@ document.addEventListener('DOMContentLoaded', async () => {
}
});
document.getElementById('download-current-btn')?.addEventListener('click', () => {
document.getElementById('download-current-btn')?.addEventListener('click', async () => {
if (Player.instance.currentTrack) {
handleTrackAction(
await handleTrackAction(
'download',
Player.instance.currentTrack,
Player.instance,
@ -1420,14 +1419,14 @@ document.addEventListener('DOMContentLoaded', async () => {
if (editingId) {
// Edit
const cover = document.getElementById('playlist-cover-input').value.trim();
db.getPlaylist(editingId).then(async (playlist) => {
await db.getPlaylist(editingId).then(async (playlist) => {
if (playlist) {
playlist.name = name;
playlist.cover = cover;
playlist.description = description;
await handlePublicStatus(playlist);
await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
syncManager.syncUserPlaylist(playlist, 'update');
await syncManager.syncUserPlaylist(playlist, 'update');
UIRenderer.instance.renderLibraryPage();
// Also update current page if we are on it
if (window.location.pathname === `/userplaylist/${editingId}`) {
@ -1948,7 +1947,7 @@ document.addEventListener('DOMContentLoaded', async () => {
console.log(`Added ${tracks.length} tracks (including pending)`);
}
db.createPlaylist(name, tracks, cover, description).then(async (playlist) => {
await db.createPlaylist(name, tracks, cover, description).then(async (playlist) => {
await handlePublicStatus(playlist);
// Update DB again with isPublic flag
await db.performTransaction('user_playlists', 'readwrite', (store) => store.put(playlist));
@ -1971,7 +1970,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (e.target.closest('.edit-playlist-btn')) {
const card = e.target.closest('.user-playlist');
const playlistId = card.dataset.userPlaylistId;
db.getPlaylist(playlistId).then(async (playlist) => {
await db.getPlaylist(playlistId).then(async (playlist) => {
if (playlist) {
const modal = document.getElementById('playlist-modal');
document.getElementById('playlist-modal-title').textContent = 'Edit Playlist';
@ -1991,7 +1990,10 @@ document.addEventListener('DOMContentLoaded', async () => {
shareBtn.style.display = playlist.isPublic ? 'flex' : 'none';
shareBtn.onclick = () => {
const url = getShareUrl(`/userplaylist/${playlist.id}`);
navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!'));
navigator.clipboard
.writeText(url)
.then(() => alert('Link copied to clipboard!'))
.catch(console.error);
};
}
@ -2030,73 +2032,77 @@ document.addEventListener('DOMContentLoaded', async () => {
const card = e.target.closest('.user-playlist');
const playlistId = card.dataset.userPlaylistId;
if (confirm('Are you sure you want to delete this playlist?')) {
db.deletePlaylist(playlistId).then(() => {
syncManager.syncUserPlaylist({ id: playlistId }, 'delete');
UIRenderer.instance.renderLibraryPage();
});
await db.deletePlaylist(playlistId);
await syncManager.syncUserPlaylist({ id: playlistId }, 'delete');
UIRenderer.instance.renderLibraryPage();
}
}
if (e.target.closest('#edit-playlist-btn')) {
const playlistId = window.location.pathname.split('/')[2];
db.getPlaylist(playlistId).then((playlist) => {
if (playlist) {
const modal = document.getElementById('playlist-modal');
document.getElementById('playlist-modal-title').textContent = 'Edit Playlist';
document.getElementById('playlist-name-input').value = playlist.name;
document.getElementById('playlist-cover-input').value = playlist.cover || '';
document.getElementById('playlist-description-input').value = playlist.description || '';
await db
.getPlaylist(playlistId)
.then((playlist) => {
if (playlist) {
const modal = document.getElementById('playlist-modal');
document.getElementById('playlist-modal-title').textContent = 'Edit Playlist';
document.getElementById('playlist-name-input').value = playlist.name;
document.getElementById('playlist-cover-input').value = playlist.cover || '';
document.getElementById('playlist-description-input').value = playlist.description || '';
const publicToggle = document.getElementById('playlist-public-toggle');
const shareBtn = document.getElementById('playlist-share-btn');
const publicToggle = document.getElementById('playlist-public-toggle');
const shareBtn = document.getElementById('playlist-share-btn');
if (publicToggle) publicToggle.checked = !!playlist.isPublic;
if (shareBtn) {
shareBtn.style.display = playlist.isPublic ? 'flex' : 'none';
shareBtn.onclick = () => {
const url = getShareUrl(`/userplaylist/${playlist.id}`);
navigator.clipboard.writeText(url).then(() => alert('Link copied to clipboard!'));
};
if (publicToggle) publicToggle.checked = !!playlist.isPublic;
if (shareBtn) {
shareBtn.style.display = playlist.isPublic ? 'flex' : 'none';
shareBtn.onclick = async () => {
const url = getShareUrl(`/userplaylist/${playlist.id}`);
await navigator.clipboard
.writeText(url)
.then(() => alert('Link copied to clipboard!'))
.catch(console.error);
};
}
// Set cover upload state - show URL input if there's an existing cover
const coverUploadBtn = document.getElementById('playlist-cover-upload-btn');
const coverUrlInput = document.getElementById('playlist-cover-input');
const coverToggleUrlBtn = document.getElementById('playlist-cover-toggle-url-btn');
if (playlist.cover) {
if (coverUploadBtn) coverUploadBtn.style.display = 'none';
if (coverUrlInput) coverUrlInput.style.display = 'block';
if (coverToggleUrlBtn) {
coverToggleUrlBtn.textContent = 'Upload';
coverToggleUrlBtn.title = 'Switch to file upload';
}
} else {
if (coverUploadBtn) {
coverUploadBtn.style.flex = '1';
coverUploadBtn.style.display = 'flex';
}
if (coverUrlInput) coverUrlInput.style.display = 'none';
if (coverToggleUrlBtn) {
coverToggleUrlBtn.textContent = 'or URL';
coverToggleUrlBtn.title = 'Switch to URL input';
}
}
modal.dataset.editingId = playlistId;
document.getElementById('import-section').style.display = 'none';
modal.classList.add('active');
document.getElementById('playlist-name-input').focus();
}
// Set cover upload state - show URL input if there's an existing cover
const coverUploadBtn = document.getElementById('playlist-cover-upload-btn');
const coverUrlInput = document.getElementById('playlist-cover-input');
const coverToggleUrlBtn = document.getElementById('playlist-cover-toggle-url-btn');
if (playlist.cover) {
if (coverUploadBtn) coverUploadBtn.style.display = 'none';
if (coverUrlInput) coverUrlInput.style.display = 'block';
if (coverToggleUrlBtn) {
coverToggleUrlBtn.textContent = 'Upload';
coverToggleUrlBtn.title = 'Switch to file upload';
}
} else {
if (coverUploadBtn) {
coverUploadBtn.style.flex = '1';
coverUploadBtn.style.display = 'flex';
}
if (coverUrlInput) coverUrlInput.style.display = 'none';
if (coverToggleUrlBtn) {
coverToggleUrlBtn.textContent = 'or URL';
coverToggleUrlBtn.title = 'Switch to URL input';
}
}
modal.dataset.editingId = playlistId;
document.getElementById('import-section').style.display = 'none';
modal.classList.add('active');
document.getElementById('playlist-name-input').focus();
}
});
})
.catch(console.error);
}
if (e.target.closest('#delete-playlist-btn')) {
const playlistId = window.location.pathname.split('/')[2];
if (confirm('Are you sure you want to delete this playlist?')) {
db.deletePlaylist(playlistId).then(() => {
syncManager.syncUserPlaylist({ id: playlistId }, 'delete');
navigate('/library');
});
await db.deletePlaylist(playlistId);
await syncManager.syncUserPlaylist({ id: playlistId }, 'delete');
navigate('/library');
}
}
@ -2105,7 +2111,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const btn = e.target.closest('.remove-from-playlist-btn');
const playlistId = window.location.pathname.split('/')[2];
db.getPlaylist(playlistId).then(async (playlist) => {
await db.getPlaylist(playlistId).then(async (playlist) => {
let trackId = null;
let trackType = null;
@ -2124,7 +2130,7 @@ document.addEventListener('DOMContentLoaded', async () => {
if (trackId) {
const updatedPlaylist = await db.removeTrackFromPlaylist(playlistId, trackId, trackType);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
await syncManager.syncUserPlaylist(updatedPlaylist, 'update');
const scrollTop = document.querySelector('.main-content').scrollTop;
await UIRenderer.instance.renderPlaylistPage(playlistId, 'user');
document.querySelector('.main-content').scrollTop = scrollTop;
@ -2645,7 +2651,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// PWA Update Logic
if (window.__AUTH_GATE__) {
disablePwaForAuthGate();
await disablePwaForAuthGate().catch(console.error);
} else {
const updateSW = registerSW({
onNeedRefresh() {
@ -2763,10 +2769,10 @@ document.addEventListener('DOMContentLoaded', async () => {
);
});
} else {
headerAccountBtn.addEventListener('click', (e) => {
headerAccountBtn.addEventListener('click', async (e) => {
e.stopPropagation();
headerAccountDropdown.classList.toggle('active');
updateAccountDropdown();
await updateAccountDropdown();
});
}
@ -2839,8 +2845,8 @@ document.addEventListener('DOMContentLoaded', async () => {
<button class="btn-primary" id="header-create-profile">Create Profile</button>
<button class="btn-secondary danger" id="header-sign-out">Sign Out</button>
`;
document.getElementById('header-create-profile').onclick = () => {
openEditProfile();
document.getElementById('header-create-profile').onclick = async () => {
openEditProfile().catch(console.error);
headerAccountDropdown.classList.remove('active');
};
}
@ -2928,7 +2934,7 @@ function showMissingTracksNotification(missingTracks, playlistName) {
const newCopyBtn = copyBtn.cloneNode(true);
copyBtn.parentNode.replaceChild(newCopyBtn, copyBtn);
newCopyBtn.addEventListener('click', () => {
newCopyBtn.addEventListener('click', async () => {
const header = `Missing songs from ${playlistName} import:\n\n`;
const textToCopy =
header +
@ -2940,11 +2946,14 @@ function showMissingTracksNotification(missingTracks, playlistName) {
})
.join('\n');
navigator.clipboard.writeText(textToCopy).then(() => {
const originalText = newCopyBtn.textContent;
newCopyBtn.textContent = 'Copied!';
setTimeout(() => (newCopyBtn.textContent = originalText), 2000);
});
await navigator.clipboard
.writeText(textToCopy)
.then(async () => {
const originalText = newCopyBtn.textContent;
newCopyBtn.textContent = 'Copied!';
setTimeout(() => (newCopyBtn.textContent = originalText), 2000);
})
.catch(console.error);
});
}

View file

@ -69,9 +69,7 @@ export class ZipStreamWriter implements IBulkDownloadWriter {
constructor(private readonly suggestedFilename: string) {}
async write(files: AsyncIterable<WriterEntry>): Promise<void> {
// showSaveFilePicker is part of the File System Access API (not yet in all TS DOM libs)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fileHandle = await (window as any).showSaveFilePicker({
const fileHandle = await window.showSaveFilePicker({
suggestedName: this.suggestedFilename,
types: [{ description: 'ZIP Archive', accept: { 'application/zip': ['.zip'] } }],
});
@ -134,8 +132,7 @@ export class FolderPickerWriter implements IBulkDownloadWriter {
// Try to re-use a saved handle first
if (savedHandle) {
try {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const permission = await (savedHandle as any).requestPermission({ mode: 'readwrite' });
const permission = await savedHandle.requestPermission({ mode: 'readwrite' });
if (permission === 'granted') {
return new FolderPickerWriter(savedHandle);
}
@ -145,9 +142,8 @@ export class FolderPickerWriter implements IBulkDownloadWriter {
}
// showDirectoryPicker is part of the File System Access API (not yet in all TS DOM libs)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
try {
const dirHandle: FileSystemDirectoryHandle = await (window as any).showDirectoryPicker({
const dirHandle: FileSystemDirectoryHandle = await window.showDirectoryPicker({
mode: 'readwrite',
});
return new FolderPickerWriter(dirHandle);

View file

@ -7,7 +7,7 @@ export class APICache {
this.dbName = 'monochrome-cache';
this.dbVersion = 1;
this.db = null;
this.initDB();
this.initDB().catch(console.error);
}
async initDB() {

View file

@ -338,9 +338,9 @@ class CommandPalette {
icon: 'trash',
label: 'Clear Queue',
keywords: ['wipe', 'clear', 'empty', 'queue'],
action: () => {
action: async () => {
Player.instance.wipeQueue();
this.notify('Queue cleared');
await this.notify('Queue cleared');
},
},
{
@ -674,7 +674,7 @@ class CommandPalette {
keywords: ['edit', 'profile', 'username', 'avatar', 'display name'],
action: async () => {
const { openEditProfile } = await import('./profile.js');
openEditProfile();
await openEditProfile();
},
},
{
@ -780,7 +780,7 @@ class CommandPalette {
this.updateSelection();
} else if (e.key === 'Enter') {
e.preventDefault();
this.executeSelected();
this.executeSelected().catch(console.error);
} else if (e.key === 'Escape') {
if (this.settingsMode) {
this.settingsMode = false;
@ -1036,9 +1036,9 @@ class CommandPalette {
el.innerHTML = `${iconHtml}<div class="cmdk-item-content"><span class="cmdk-item-label">${escapeHtml(item.label)}</span>${descHtml}</div>${shortcutHtml}`;
el.addEventListener('click', () => {
el.addEventListener('click', async () => {
this.selectedIndex = index;
this.executeSelected();
await this.executeSelected();
});
el.addEventListener('mouseenter', () => {
@ -1171,14 +1171,14 @@ class CommandPalette {
if (opt.dataset.theme === theme) opt.classList.add('active');
else opt.classList.remove('active');
});
this.notify(`Theme set to ${theme}`);
await this.notify(`Theme set to ${theme}`);
}
async toggleVisualizer() {
const { visualizerSettings } = await import('./storage.js');
const current = visualizerSettings.isEnabled();
visualizerSettings.setEnabled(!current);
this.notify(`Visualizer ${!current ? 'enabled' : 'disabled'}`);
await this.notify(`Visualizer ${!current ? 'enabled' : 'disabled'}`);
const overlay = document.getElementById('fullscreen-cover-overlay');
if (overlay && getComputedStyle(overlay).display !== 'none') {
@ -1192,7 +1192,7 @@ class CommandPalette {
if (UIRenderer.instance.visualizer) {
UIRenderer.instance.visualizer.setPreset(preset);
}
this.notify(`Visualizer preset: ${preset}`);
await this.notify(`Visualizer preset: ${preset}`);
}
async setQuality(quality) {
@ -1225,13 +1225,13 @@ class CommandPalette {
const downloadSelect = document.getElementById('download-quality-setting');
if (downloadSelect) downloadSelect.value = dlQuality;
this.notify(`Quality set to ${qualityNames[quality] || quality}`);
await this.notify(`Quality set to ${qualityNames[quality] || quality}`);
}
setSleepTimer(minutes) {
async setSleepTimer(minutes) {
if (Player.instance) {
Player.instance.setSleepTimer(minutes);
this.notify(`Sleep timer: ${minutes} minutes`);
await this.notify(`Sleep timer: ${minutes} minutes`);
}
}
@ -1242,7 +1242,7 @@ class CommandPalette {
const queue = player.getCurrentQueue();
if (queue.length === 0) {
this.notify('Queue is empty');
await this.notify('Queue is empty');
return;
}
@ -1250,7 +1250,7 @@ class CommandPalette {
const scrobbler = window.monochromeScrobbler;
let likedCount = 0;
this.notify('Liking all tracks in queue...');
await this.notify('Liking all tracks in queue...');
for (const track of queue) {
const isLiked = await db.isFavorite('track', track.id);
if (!isLiked) {
@ -1258,7 +1258,7 @@ class CommandPalette {
likedCount++;
}
}
this.notify(`Liked ${likedCount} new track(s)`);
await this.notify(`Liked ${likedCount} new track(s)`);
}
async downloadQueue() {
@ -1268,40 +1268,39 @@ class CommandPalette {
const queue = player.getCurrentQueue();
if (queue.length === 0) {
this.notify('Queue is empty');
await this.notify('Queue is empty');
return;
}
const { downloadTracks } = await import('./downloads.js');
const { downloadQualitySettings } = await import('./storage.js');
downloadTracks(queue, ui.api, downloadQualitySettings.getQuality(), ui.lyricsManager);
await downloadTracks(queue, ui.api, downloadQualitySettings.getQuality(), ui.lyricsManager);
}
async createPlaylist() {
const name = `New Playlist ${new Date().toLocaleDateString()}`;
await db.createPlaylist(name);
navigate('/library');
this.notify('Playlist created');
await this.notify('Playlist created');
}
async createFolder() {
const name = `New Folder ${new Date().toLocaleDateString()}`;
await db.createFolder(name);
navigate('/library');
this.notify('Folder created');
await this.notify('Folder created');
}
async clearCache() {
const api = UIRenderer.instance.api;
if (api) {
await api.clearCache();
this.notify('Cache cleared');
await this.notify('Cache cleared');
}
}
async notify(message) {
const { showNotification } = await import('./downloads.js');
showNotification(message);
await import('./downloads.js').then((m) => m.showNotification(message)).catch(console.error);
}
}

View file

@ -85,7 +85,7 @@ export class MediaMetadata extends BaseContainer {
}
export class Artist extends BaseContainer {
handle: any;
handle: unknown;
id: number;
name: string;
picture: string;
@ -99,6 +99,7 @@ export class Artist extends BaseContainer {
export class EnrichedTrack extends Track {
declare album: TrackAlbum | EnrichedAlbum;
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-redundant-type-constituents
declare replayGain: any | ReplayGain;
constructor(data: object) {

View file

@ -204,13 +204,13 @@ export class DashDownloader {
const resolveTemplate = (template: string, number: number, time: number): string => {
return template
.replace(/\$RepresentationID\$/g, repId ?? '')
.replace(/\$Number(?:%0([0-9]+)d)?\$/g, (_, width) => {
.replace(/\$Number(?:%0([0-9]+)d)?\$/g, (_, width: string) => {
if (width) {
return number.toString().padStart(parseInt(width), '0');
}
return number.toString();
})
.replace(/\$Time(?:%0([0-9]+)d)?\$/g, (_, width) => {
.replace(/\$Time(?:%0([0-9]+)d)?\$/g, (_, width: string) => {
if (width) {
return time.toString().padStart(parseInt(width), '0');
}

View file

@ -783,7 +783,6 @@ export class MusicDatabase {
}
// Return lightweight copy without tracks
// eslint-disable-next-line no-unused-vars
const { tracks, ...minified } = playlist;
return minified;
});

View file

@ -24,24 +24,30 @@ export function doTimedAsync<T, R = T extends Promise<T> ? Promise<T> : T>(
throwError: boolean = false
): R {
if (import.meta.env.DEV) {
return new Promise(async (resolve, reject) => {
const hiddenId = InvisibleCodec.encode(v7());
console.time(message + hiddenId);
try {
const output = await callback();
resolve(output);
} catch (err) {
console.error(`Error in timed operation "${message}":`, err);
if (throwError) {
reject(err);
} else {
resolve(undefined as R);
return new Promise((resolve, reject) => {
(async () => {
const hiddenId = InvisibleCodec.encode(v7());
console.time(message + hiddenId);
try {
const output = await callback();
resolve(output);
} catch (err) {
console.error(`Error in timed operation "${message}":`, err);
if (throwError) {
if (err instanceof Error) {
reject(err);
} else {
reject(new Error(String(err)));
}
} else {
resolve(undefined as R);
}
} finally {
console.timeEnd(message + hiddenId);
}
} finally {
console.timeEnd(message + hiddenId);
}
})().catch(reject);
}) as R;
} else {
return callback() as R;
return callback();
}
}

View file

@ -17,10 +17,10 @@ import { ZipStreamWriter, ZipBlobWriter, FolderPickerWriter, SequentialFileWrite
import { FfmpegProgress } from './ffmpeg.types.js';
import { DownloadProgress, ProgressMessage, SegmentedDownloadProgress } from './progressEvents.js';
import { db } from './db.js';
import { modernSettings } from './ModernSettings.js';
import { BulkDownloadMethod, modernSettings } from './ModernSettings.js';
import { SVG_CLOSE } from './icons.ts';
import { LyricsManager } from './lyrics.js';
import { MusicAPI } from './music-api.js';
import { LyricsManager } from './lyrics.js';
const downloadTasks = new Map();
const bulkDownloadTasks = new Map();
@ -167,7 +167,7 @@ export function showNotification(message) {
}, 1500);
}
export function addDownloadTask(trackId, track, filename, api, abortController) {
export function addDownloadTask(trackId, track, _filename, api, abortController) {
const container = createDownloadNotification();
const taskEl = document.createElement('div');
@ -508,7 +508,7 @@ async function createSingleTrackFolderWriter() {
const method = modernSettings.bulkDownloadMethod;
const hasFolderPicker = 'showDirectoryPicker' in window;
if (method === 'local') {
if (method === BulkDownloadMethod.LocalMedia) {
const localHandle = await db.getSetting('local_folder_handle');
if (hasFolderPicker && localHandle && typeof localHandle.requestPermission === 'function') {
try {
@ -521,7 +521,7 @@ async function createSingleTrackFolderWriter() {
return null;
}
if (method === 'folder' && hasFolderPicker) {
if (method === BulkDownloadMethod.Folder && hasFolderPicker) {
const rememberFolder = modernSettings.rememberBulkDownloadFolder;
const savedHandle = rememberFolder ? await db.getSetting('bulk_download_folder_handle') : null;
// Try to reuse the saved handle silently first.
@ -564,7 +564,7 @@ async function createBulkWriter(folderName) {
const hasFolderPicker = 'showDirectoryPicker' in window;
// ── Local Media Folder method ────────────────────────────────────────────
if (method === 'local') {
if (method === BulkDownloadMethod.LocalMedia) {
const localHandle = await db.getSetting('local_folder_handle');
if (hasFolderPicker) {
// Browser mode: try to reuse the stored handle with write permission
@ -594,7 +594,7 @@ async function createBulkWriter(folderName) {
}
// ── Folder Picker method ─────────────────────────────────────────────────
if (method === 'folder' && hasFolderPicker) {
if (method === BulkDownloadMethod.Folder && hasFolderPicker) {
const rememberFolder = modernSettings.rememberBulkDownloadFolder;
const savedHandle = rememberFolder ? await db.getSetting('bulk_download_folder_handle') : null;
try {
@ -614,7 +614,7 @@ async function createBulkWriter(folderName) {
}
}
if (method === 'individual') {
if (method === BulkDownloadMethod.Individual) {
return SequentialFileWriter;
}
// method === 'zip' (or folder picker unavailable as fallback)
@ -659,7 +659,7 @@ async function startBulkDownload({
completeBulkDownload(notification, true);
// If the download went to the local media folder, refresh the local library.
if (modernSettings.bulkDownloadMethod === 'local') {
if (modernSettings.bulkDownloadMethod === BulkDownloadMethod.LocalMedia) {
window.refreshLocalMediaFolder?.();
}
} catch (error) {
@ -672,7 +672,7 @@ async function startBulkDownload({
}
}
export async function downloadTracks(tracks, api, quality, lyricsManager = null) {
export async function downloadTracks(tracks, api, quality, _lyricsManager = null) {
const folderName = `Queue - ${new Date().toISOString().slice(0, 10)}`;
await startBulkDownload({
tracks,
@ -687,7 +687,7 @@ export async function downloadTracks(tracks, api, quality, lyricsManager = null)
});
}
export async function downloadAlbum(album, tracks, api, quality, lyricsManager = null) {
export async function downloadAlbum(album, tracks, api, quality, _lyricsManager = null) {
const releaseDateStr =
album.releaseDate || (tracks[0]?.streamStartDate ? tracks[0].streamStartDate.split('T')[0] : '');
const releaseDate = releaseDateStr ? new Date(releaseDateStr) : null;
@ -712,7 +712,7 @@ export async function downloadAlbum(album, tracks, api, quality, lyricsManager =
});
}
export async function downloadPlaylist(playlist, tracks, api, quality, lyricsManager = null) {
export async function downloadPlaylist(playlist, tracks, api, quality, _lyricsManager = null) {
const folderName = formatPathTemplate(modernSettings.folderTemplate, {
albumTitle: playlist.title,
albumArtist: 'Playlist',
@ -1120,7 +1120,7 @@ export async function downloadTrackWithMetadata(
// If the target is the local media folder, do a cheap partial update:
// pass the downloaded blob and base filename so only this one track's metadata
// is read and inserted into localFilesCache instead of re-walking the whole folder.
if (modernSettings.bulkDownloadMethod === 'local') {
if (modernSettings.bulkDownloadMethod === BulkDownloadMethod.LocalMedia) {
window.refreshLocalMediaFolder?.(blob, finalFilename);
}
@ -1136,7 +1136,7 @@ export async function downloadTrackWithMetadata(
}
}
export async function downloadLikedTracks(tracks, api, quality, lyricsManager = null) {
export async function downloadLikedTracks(tracks, api, quality, _lyricsManager = null) {
const folderName = `Liked Tracks - ${new Date().toISOString().slice(0, 10)}`;
await startBulkDownload({
tracks,

View file

@ -56,6 +56,9 @@ import {
} from './analytics.js';
import { SVG_BIN, SVG_MUTE, SVG_PAUSE, SVG_PLAY, SVG_VOLUME, SVG_CHECKBOX, SVG_CHECKBOX_CHECKED } from './icons.js';
import { partyManager } from './listening-party.js';
import { MusicAPI } from './music-api.js';
import { LyricsManager } from './lyrics.js';
import { Player } from './player.js';
let currentTrackIdForWaveform = null;
@ -78,21 +81,21 @@ function handleTrackTouchStart(e) {
isLongPress = false;
longPressTrackItem = trackItem;
longPressTimer = setTimeout(() => {
longPressTimer = setTimeout(async () => {
isLongPress = true;
toggleTrackSelection(trackItem, true, false);
hapticLongPress();
await hapticLongPress();
}, LONG_PRESS_DURATION);
}
function handleTrackTouchMove(e) {
function handleTrackTouchMove(_e) {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
}
}
function handleTrackTouchEnd(e) {
function handleTrackTouchEnd(_e) {
if (longPressTimer) {
clearTimeout(longPressTimer);
longPressTimer = null;
@ -204,7 +207,7 @@ function toggleTrackSelection(trackItem, ctrlHeld, shiftHeld) {
document.body.classList.toggle('multi-select-mode', trackSelection.isSelecting);
}
function showMultiSelectPlaylistModal(tracks) {
async function showMultiSelectPlaylistModal(tracks) {
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.style.cssText =
@ -237,7 +240,7 @@ function showMultiSelectPlaylistModal(tracks) {
document.body.appendChild(modal);
document.body.style.overflow = 'hidden';
db.getPlaylists(true).then((playlists) => {
await db.getPlaylists(true).then((playlists) => {
const listEl = modal.querySelector('.playlist-list');
if (playlists.length === 0) {
listEl.innerHTML = '<div style="padding: 12px; color: var(--muted-foreground);">No playlists yet</div>';
@ -260,17 +263,17 @@ function showMultiSelectPlaylistModal(tracks) {
for (const track of tracks) {
await db.addTrackToPlaylist(playlistId, track);
}
syncManager.syncUserPlaylist(await db.getPlaylist(playlistId), 'update');
await syncManager.syncUserPlaylist(await db.getPlaylist(playlistId), 'update');
showNotification(`Added ${tracks.length} tracks to playlist`);
closeModal();
});
});
});
modal.querySelector('.create-new-playlist').addEventListener('click', () => {
modal.querySelector('.create-new-playlist').addEventListener('click', async () => {
const name = prompt('Playlist name:');
if (name) {
db.createPlaylist(name, tracks).then((playlist) => {
await db.createPlaylist(name, tracks).then((_playlist) => {
showNotification(`Created playlist "${name}" with ${tracks.length} tracks`);
closeModal();
});
@ -278,127 +281,132 @@ function showMultiSelectPlaylistModal(tracks) {
});
}
export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
const playPauseBtn = document.querySelector('.now-playing-bar .play-pause-btn');
const nextBtn = document.getElementById('next-btn');
const prevBtn = document.getElementById('prev-btn');
const shuffleBtn = document.getElementById('shuffle-btn');
const repeatBtn = document.getElementById('repeat-btn');
const homeStartRadioBtn = document.getElementById('home-start-infinite-radio-btn');
const sleepTimerBtnDesktop = document.getElementById('sleep-timer-btn-desktop');
const playPauseBtn = document.querySelector('.now-playing-bar .play-pause-btn');
const nextBtn = document.getElementById('next-btn');
const prevBtn = document.getElementById('prev-btn');
const shuffleBtn = document.getElementById('shuffle-btn');
const repeatBtn = document.getElementById('repeat-btn');
const homeStartRadioBtn = document.getElementById('home-start-infinite-radio-btn');
const sleepTimerBtnDesktop = document.getElementById('sleep-timer-btn-desktop');
const volumeBar = document.getElementById('volume-bar');
const volumeFill = document.getElementById('volume-fill');
const volumeBtn = document.getElementById('volume-btn');
const _volumeBar = document.getElementById('volume-bar');
const volumeFill = document.getElementById('volume-fill');
const volumeBtn = document.getElementById('volume-btn');
const updateVolumeUI = () => {
const activeEl = player.activeElement;
const { muted } = activeEl;
const volume = player.userVolume;
volumeBtn.innerHTML = muted || volume === 0 ? SVG_MUTE(20) : SVG_VOLUME(20);
const effectiveVolume = muted ? 0 : volume * 100;
volumeFill.style.setProperty('--volume-level', `${effectiveVolume}%`);
volumeFill.style.width = `${effectiveVolume}%`;
};
const updateVolumeUI = () => {
const activeEl = Player.instance.activeElement;
const { muted } = activeEl;
const volume = Player.instance.userVolume;
volumeBtn.innerHTML = muted || volume === 0 ? SVG_MUTE(20) : SVG_VOLUME(20);
const effectiveVolume = muted ? 0 : volume * 100;
volumeFill.style.setProperty('--volume-level', `${effectiveVolume}%`);
volumeFill.style.width = `${effectiveVolume}%`;
};
function clearSelection() {
trackSelection.selectedIds.clear();
trackSelection.lastClickedId = null;
trackSelection.isSelecting = false;
document.body.classList.remove('multi-select-mode');
document.querySelectorAll('.track-item.selected').forEach((el) => {
el.classList.remove('selected');
});
document.querySelectorAll('.track-checkbox').forEach((checkbox) => {
checkbox.innerHTML = SVG_CHECKBOX(18);
checkbox.classList.remove('checked');
});
updateSelectionBar();
}
function clearSelection() {
trackSelection.selectedIds.clear();
trackSelection.lastClickedId = null;
trackSelection.isSelecting = false;
document.body.classList.remove('multi-select-mode');
document.querySelectorAll('.track-item.selected').forEach((el) => {
el.classList.remove('selected');
});
document.querySelectorAll('.track-checkbox').forEach((checkbox) => {
checkbox.innerHTML = SVG_CHECKBOX(18);
checkbox.classList.remove('checked');
});
updateSelectionBar();
}
function updateSelectionBar() {
let bar = document.getElementById('selection-bar');
if (!bar) {
bar = document.createElement('div');
bar.id = 'selection-bar';
bar.className = 'selection-bar';
bar.innerHTML = `
<span class="selection-count">0 selected</span>
<div class="selection-actions">
<button data-action="play-selected">Play</button>
<button data-action="add-to-queue-selected">Add to queue</button>
<button data-action="add-to-playlist-selected">Add to playlist</button>
<button data-action="download-selected">Download</button>
<button data-action="like-selected">Like</button>
</div>
<button data-action="clear-selection" style="margin-left: 8px;">Clear</button>
function updateSelectionBar() {
let bar = document.getElementById('selection-bar');
if (!bar) {
bar = document.createElement('div');
bar.id = 'selection-bar';
bar.className = 'selection-bar';
bar.innerHTML = `
<span class="selection-count">0 selected</span>
<div class="selection-actions">
<button data-action="play-selected">Play</button>
<button data-action="add-to-queue-selected">Add to queue</button>
<button data-action="add-to-playlist-selected">Add to playlist</button>
<button data-action="download-selected">Download</button>
<button data-action="like-selected">Like</button>
</div>
<button data-action="clear-selection" style="margin-left: 8px;">Clear</button>
`;
document.body.appendChild(bar);
document.body.appendChild(bar);
bar.querySelectorAll('button').forEach((btn) => {
btn.addEventListener('click', () => handleSelectionAction(btn.dataset.action));
});
}
const count = trackSelection.selectedIds.size;
bar.querySelector('.selection-count').textContent = `${count} selected`;
bar.classList.toggle('visible', count > 0);
}
function handleSelectionAction(action) {
const selectedIds = getSelectedTracks();
if (selectedIds.length === 0) return;
const mainContent = document.getElementById('main-content');
const selectedTracks = [];
mainContent.querySelectorAll('.track-item').forEach((item) => {
if (trackSelection.selectedIds.has(item.dataset.trackId)) {
const track = trackDataStore.get(item);
if (track) selectedTracks.push(track);
}
bar.querySelectorAll('button').forEach((btn) => {
btn.addEventListener('click', () => handleSelectionAction(btn.dataset.action));
});
switch (action) {
case 'play-selected':
if (selectedTracks.length > 0) {
player.setQueue(selectedTracks, 0);
document.getElementById('shuffle-btn').classList.remove('active');
player.playTrackFromQueue();
}
break;
case 'add-to-queue-selected':
if (selectedTracks.length > 0) {
player.addToQueue(selectedTracks);
if (window.renderQueueFunction) window.renderQueueFunction();
showNotification(`Added ${selectedTracks.length} tracks to queue`);
}
break;
case 'add-to-playlist-selected':
if (selectedTracks.length > 0) {
showMultiSelectPlaylistModal(selectedTracks);
}
break;
case 'download-selected':
if (selectedTracks.length > 0) {
selectedTracks.forEach((track) => {
downloadTrackWithMetadata(track, downloadQualitySettings.getQuality(), api, lyricsManager);
});
showNotification(`Downloading ${selectedTracks.length} tracks`);
}
break;
case 'like-selected':
selectedTracks.forEach(async (track) => {
const added = await db.toggleFavorite('track', track);
syncManager.syncLibraryItem('track', track, added);
});
showNotification(`Liked ${selectedTracks.length} tracks`);
break;
case 'clear-selection':
clearSelection();
break;
}
}
const count = trackSelection.selectedIds.size;
bar.querySelector('.selection-count').textContent = `${count} selected`;
bar.classList.toggle('visible', count > 0);
}
async function handleSelectionAction(action) {
const selectedIds = getSelectedTracks();
if (selectedIds.length === 0) return;
const mainContent = document.getElementById('main-content');
const selectedTracks = [];
mainContent.querySelectorAll('.track-item').forEach((item) => {
if (trackSelection.selectedIds.has(item.dataset.trackId)) {
const track = trackDataStore.get(item);
if (track) selectedTracks.push(track);
}
});
switch (action) {
case 'play-selected':
if (selectedTracks.length > 0) {
Player.instance.setQueue(selectedTracks, 0);
document.getElementById('shuffle-btn').classList.remove('active');
Player.instance.playTrackFromQueue();
}
break;
case 'add-to-queue-selected':
if (selectedTracks.length > 0) {
Player.instance.addToQueue(selectedTracks);
if (window.renderQueueFunction) await window.renderQueueFunction();
showNotification(`Added ${selectedTracks.length} tracks to queue`);
}
break;
case 'add-to-playlist-selected':
if (selectedTracks.length > 0) {
await showMultiSelectPlaylistModal(selectedTracks);
}
break;
case 'download-selected':
if (selectedTracks.length > 0) {
showNotification(`Downloading ${selectedTracks.length} tracks`);
for (const track of selectedTracks) {
await downloadTrackWithMetadata(
track,
downloadQualitySettings.getQuality(),
MusicAPI.instance.tidalAPI,
LyricsManager.instance
);
}
}
break;
case 'like-selected':
for (const track of selectedTracks) {
const added = await db.toggleFavorite('track', track);
await syncManager.syncLibraryItem('track', track, added);
}
showNotification(`Liked ${selectedTracks.length} tracks`);
break;
case 'clear-selection':
clearSelection();
break;
}
}
export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
if (homeStartRadioBtn) {
homeStartRadioBtn.addEventListener('click', async () => {
await player.enableRadio();
@ -417,14 +425,14 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
}
});
element.addEventListener('play', () => {
element.addEventListener('play', async () => {
if (player.activeElement !== element) return;
// Initialize audio context manager for EQ (only once)
if (!audioContextManager.isReady()) {
audioContextManager.init(element);
}
audioContextManager.resume();
await audioContextManager.resume();
if (player.currentTrack) {
// Track play event
@ -435,7 +443,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
scrobbler.updateNowPlaying(player.currentTrack);
}
updateWaveform();
await updateWaveform();
}
playPauseBtn.innerHTML = SVG_PAUSE(20);
@ -479,7 +487,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
if (currentTime >= 10 && player.currentTrack && player.currentTrack.id !== historyLoggedTrackId) {
historyLoggedTrackId = player.currentTrack.id;
const historyEntry = await db.addToHistory(player.currentTrack);
syncManager.syncHistoryItem(historyEntry);
await syncManager.syncHistoryItem(historyEntry);
if (window.location.hash === '#recent') {
ui.renderRecentPage();
@ -554,31 +562,31 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
setupMediaListeners(player.video);
}
playPauseBtn.addEventListener('click', () => {
hapticMedium();
playPauseBtn.addEventListener('click', async () => {
await hapticMedium();
player.handlePlayPause();
});
nextBtn.addEventListener('click', () => {
hapticMedium();
nextBtn.addEventListener('click', async () => {
await hapticMedium();
trackSkipTrack(player.currentTrack, 'next');
player.playNext();
});
prevBtn.addEventListener('click', () => {
hapticMedium();
prevBtn.addEventListener('click', async () => {
await hapticMedium();
trackSkipTrack(player.currentTrack, 'previous');
player.playPrev();
});
shuffleBtn.addEventListener('click', () => {
hapticLight();
shuffleBtn.addEventListener('click', async () => {
await hapticLight();
player.toggleShuffle();
trackToggleShuffle(player.shuffleActive);
shuffleBtn.classList.toggle('active', player.shuffleActive);
if (window.renderQueueFunction) window.renderQueueFunction();
if (window.renderQueueFunction) await window.renderQueueFunction();
});
repeatBtn.addEventListener('click', () => {
hapticLight();
repeatBtn.addEventListener('click', async () => {
await hapticLight();
const mode = player.toggleRepeat();
trackToggleRepeat(mode === REPEAT_MODE.OFF ? 'off' : mode === REPEAT_MODE.ALL ? 'all' : 'one');
repeatBtn.classList.toggle('active', mode !== REPEAT_MODE.OFF);
@ -709,7 +717,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
}
};
window.addEventListener('waveform-toggle', (e) => {
window.addEventListener('waveform-toggle', async (e) => {
if (!e.detail.enabled) {
const progressBar = document.getElementById('progress-bar');
const playerControls = document.querySelector('.player-controls');
@ -722,7 +730,7 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
playerControls.classList.remove('waveform-loaded');
}
}
updateWaveform();
await updateWaveform();
});
if (volumeBtn) {
@ -1102,7 +1110,7 @@ export async function showAddToPlaylistModal(track) {
e.stopPropagation();
await db.removeTrackFromPlaylist(playlistId, track.id);
const updatedPlaylist = await db.getPlaylist(playlistId);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
await syncManager.syncUserPlaylist(updatedPlaylist, 'update');
showNotification(`Removed from playlist: ${option.querySelector('span').textContent}`);
await renderModal();
} else {
@ -1110,7 +1118,7 @@ export async function showAddToPlaylistModal(track) {
await db.addTrackToPlaylist(playlistId, track);
const updatedPlaylist = await db.getPlaylist(playlistId);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
await syncManager.syncUserPlaylist(updatedPlaylist, 'update');
showNotification(`Added to playlist: ${option.querySelector('span').textContent}`);
closeModal();
}
@ -1272,14 +1280,14 @@ export async function handleTrackAction(
if (action === 'add-to-queue') {
player.addToQueue(tracks);
if (window.renderQueueFunction) window.renderQueueFunction();
if (window.renderQueueFunction) await window.renderQueueFunction();
showNotification(`Added ${tracks.length} tracks to queue`);
return;
}
if (action === 'play-next') {
player.addNextToQueue(tracks);
if (window.renderQueueFunction) window.renderQueueFunction();
if (window.renderQueueFunction) await window.renderQueueFunction();
showNotification(`Playing next: ${tracks.length} tracks`);
return;
}
@ -1345,12 +1353,12 @@ export async function handleTrackAction(
if (action === 'add-to-queue') {
trackAddToQueue(item, 'end');
player.addToQueue(item);
if (window.renderQueueFunction) window.renderQueueFunction();
if (window.renderQueueFunction) await window.renderQueueFunction();
showNotification(`Added to queue: ${item.title}`);
} else if (action === 'play-next') {
trackPlayNext(item);
player.addNextToQueue(item);
if (window.renderQueueFunction) window.renderQueueFunction();
if (window.renderQueueFunction) await window.renderQueueFunction();
showNotification(`Playing next: ${item.title}`);
} else if (action === 'play-card') {
player.setQueue([item], 0);
@ -1368,7 +1376,7 @@ export async function handleTrackAction(
await downloadTrackWithMetadata(item, downloadQualitySettings.getQuality(), api, lyricsManager);
} else if (action === 'toggle-like') {
const added = await db.toggleFavorite(type, item);
syncManager.syncLibraryItem(type, item, added);
await syncManager.syncLibraryItem(type, item, added);
// Track like/unlike
if (added) {
@ -1624,7 +1632,7 @@ export async function handleTrackAction(
e.stopPropagation();
await db.removeTrackFromPlaylist(playlistId, item.id);
const updatedPlaylist = await db.getPlaylist(playlistId);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
await syncManager.syncUserPlaylist(updatedPlaylist, 'update');
showNotification(`Removed from playlist: ${option.querySelector('span').textContent}`);
await renderModal();
} else {
@ -1632,7 +1640,7 @@ export async function handleTrackAction(
await db.addTrackToPlaylist(playlistId, item);
const updatedPlaylist = await db.getPlaylist(playlistId);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
await syncManager.syncUserPlaylist(updatedPlaylist, 'update');
showNotification(`Added to playlist: ${option.querySelector('span').textContent}`);
closeModal();
}
@ -1670,9 +1678,12 @@ export async function handleTrackAction(
const url = getShareUrl(storedHref ? storedHref : `/${typeForUrl}/${item.id || item.uuid}`);
trackCopyLink(type, item.id || item.uuid);
navigator.clipboard.writeText(url).then(() => {
showNotification('Link copied to clipboard!');
});
await navigator.clipboard
.writeText(url)
.then(() => {
showNotification('Link copied to clipboard!');
})
.catch(console.error);
} else if (action === 'open-in-new-tab') {
// Use stored href from card if available, otherwise construct URL
const contextMenu = document.getElementById('context-menu');
@ -2412,7 +2423,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
positionMenu(contextMenu, e.clientX, e.clientY);
});
document.addEventListener('click', (e) => {
document.addEventListener('click', async (e) => {
if (contextMenu.style.display === 'block') {
if (contextMenu._originalHTML) {
contextMenu.innerHTML = contextMenu._originalHTML;
@ -2432,7 +2443,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
}
});
document.addEventListener('keydown', (e) => {
document.addEventListener('keydown', async (e) => {
if (e.key === 'Escape' && trackSelection.isSelecting) {
clearSelection();
}
@ -2488,34 +2499,39 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
trackPlayNext(t);
player.addNextToQueue(t);
});
if (window.renderQueueFunction) window.renderQueueFunction();
if (window.renderQueueFunction) await window.renderQueueFunction();
showNotification(`Playing next: ${selectedTracks.length} tracks`);
clearSelection();
break;
case 'add-to-queue':
player.addToQueue(selectedTracks);
if (window.renderQueueFunction) window.renderQueueFunction();
if (window.renderQueueFunction) await window.renderQueueFunction();
showNotification(`Added ${selectedTracks.length} tracks to queue`);
clearSelection();
break;
case 'toggle-like':
selectedTracks.forEach(async (t) => {
const added = await db.toggleFavorite('track', t);
syncManager.syncLibraryItem('track', t, added);
await syncManager.syncLibraryItem('track', t, added);
});
showNotification(`Liked ${selectedTracks.length} tracks`);
clearSelection();
break;
case 'add-to-playlist':
showMultiSelectPlaylistModal(selectedTracks);
await showMultiSelectPlaylistModal(selectedTracks);
clearSelection();
break;
case 'download':
selectedTracks.forEach((t) => {
downloadTrackWithMetadata(t, downloadQualitySettings.getQuality(), api, lyricsManager);
});
showNotification(`Downloading ${selectedTracks.length} tracks`);
clearSelection();
for (const track of selectedTracks) {
await downloadTrackWithMetadata(
track,
downloadQualitySettings.getQuality(),
api,
lyricsManager
);
}
break;
default:
clearSelection();

View file

@ -114,7 +114,7 @@ async function ffmpegWorker(
reject(new FfmpegError('Worker failed: ' + error.message));
};
(async () => {
void (async () => {
const transferables = [];
if (audioData) transferables.push(audioData);
for (const f of extraFiles) {

View file

@ -1,9 +1,9 @@
import { expect, test, suite } from 'vitest';
import { expect, test } from 'vitest';
import { ffmpeg } from './ffmpeg';
test('Run `ffmpeg --help`', async () => {
const lines: string[] = [];
const info = await ffmpeg(null, {
await ffmpeg(null, {
rawArgs: ['--help'],
logConsole: false,
outputName: null,

View file

@ -183,6 +183,11 @@ export function getContainerFormat(internalName: string): ContainerFormat | unde
return containerFormats[internalName];
}
export interface ExtraFile {
name: string;
data: ArrayBuffer | Uint8Array;
}
/**
* Transcodes an audio blob using the specified custom format via ffmpeg.
* Throws if ffmpeg fails during transcoding.
@ -192,7 +197,7 @@ export async function transcodeWithCustomFormat(
format: CustomFormat,
onProgress: ((progress: ProgressEvent) => void) | null = null,
signal: AbortSignal | null = null,
extraFiles: any[] = []
extraFiles: ExtraFile[] = []
): Promise<Blob> {
return ffmpeg(audioBlob, {
args: format.ffmpegArgs,
@ -213,7 +218,7 @@ export async function transcodeWithContainerFormat(
format: ContainerFormat,
onProgress: ((progress: ProgressEvent) => void) | null = null,
signal: AbortSignal | null = null,
extraFiles: any[] = []
extraFiles: ExtraFile[] = []
): Promise<Blob> {
return ffmpeg(audioBlob, {
args: format.ffmpegArgs,

4
js/global.d.ts vendored
View file

@ -31,3 +31,7 @@ declare module 'https://cdn.jsdelivr.net/npm/client-zip@2.4.5/+esm' {
type WithRequiredKeys<T> = {
[K in keyof T]-?: T[K] | undefined;
};
declare global {
const __COMMIT_HASH__: string | undefined;
}

16
js/indexedIterator.ts Normal file
View file

@ -0,0 +1,16 @@
/**
* A generic iterator that yields the index, total count, and item for any finite iterable.
*
* @template T - The type of items in the iterable.
* @param iterable - The iterable to process.
* @returns A generator that yields an object with index, total, and item.
*/
export default function* indexedIterator<T>(
iterable: Iterable<T>
): Generator<{ index: number; total: number; item: T }> {
const array = Array.from(iterable); // Convert the iterable to an array
const total = array.length; // Get the total count of items
for (let index = 0; index < total; index++) {
yield { index, total, item: array[index] }; // Yield index, total, and item
}
}

View file

@ -284,8 +284,8 @@ export class LastFMScrobbler {
scheduleScrobble(delay) {
this.clearScrobbleTimer();
this.scrobbleTimer = setTimeout(() => {
this.scrobbleCurrentTrack();
this.scrobbleTimer = setTimeout(async () => {
await this.scrobbleCurrentTrack();
}, delay);
}
@ -350,9 +350,9 @@ export class LastFMScrobbler {
}
}
onTrackChange(track) {
async onTrackChange(track) {
if (!this.isAuthenticated()) return;
this.updateNowPlaying(track);
await this.updateNowPlaying(track);
}
onPlaybackStop() {

View file

@ -216,8 +216,8 @@ export class LibreFmScrobbler {
scheduleScrobble(delay) {
this.clearScrobbleTimer();
this.scrobbleTimer = setTimeout(() => {
this.scrobbleCurrentTrack();
this.scrobbleTimer = setTimeout(async () => {
await this.scrobbleCurrentTrack();
}, delay);
}
@ -282,9 +282,9 @@ export class LibreFmScrobbler {
}
}
onTrackChange(track) {
async onTrackChange(track) {
if (!this.isAuthenticated()) return;
this.updateNowPlaying(track);
await this.updateNowPlaying(track);
}
onPlaybackStop() {

View file

@ -209,8 +209,8 @@ export class ListenBrainzScrobbler {
scheduleScrobble(delay) {
this.clearScrobbleTimer();
this.scrobbleTimer = setTimeout(() => {
this.scrobbleCurrentTrack();
this.scrobbleTimer = setTimeout(async () => {
await this.scrobbleCurrentTrack();
}, delay);
}
@ -235,8 +235,8 @@ export class ListenBrainzScrobbler {
}
}
onTrackChange(track) {
this.updateNowPlaying(track);
async onTrackChange(track) {
await this.updateNowPlaying(track);
}
onPlaybackStop() {

View file

@ -96,7 +96,7 @@ export class ListeningPartyManager {
document.getElementById('copy-party-link-btn')?.addEventListener('click', () => this.copyInviteLink());
document.getElementById('party-chat-send-btn')?.addEventListener('click', () => this.sendChatMessage());
document.getElementById('party-chat-input')?.addEventListener('keypress', (e) => {
if (e.key === 'Enter') this.sendChatMessage();
if (e.key === 'Enter') this.sendChatMessage().catch(console.error);
});
}
@ -104,13 +104,13 @@ export class ListeningPartyManager {
const nameInput = document.getElementById('party-name-input');
const user = authManager.user;
if (!user) {
Modal.alert('Login Required', 'You must be logged in to host a listening party.');
await Modal.alert('Login Required', 'You must be logged in to host a listening party.');
return;
}
const pbUser = await syncManager._getUserRecord(user.$id);
if (!pbUser) {
Modal.alert('Sync Error', 'Failed to sync user data. Please try again.');
await Modal.alert('Sync Error', 'Failed to sync user data. Please try again.');
return;
}
@ -171,19 +171,19 @@ export class ListeningPartyManager {
this.setupSubscriptions(partyId);
this.startHeartbeat();
this.renderPartyUI();
this.loadInitialData(partyId);
await this.loadInitialData(partyId);
if (!this.isHost) {
this.lockControls();
this.setupGuestSyncInterception();
if (party.current_track) {
await audioContextManager.resume();
this.syncWithHost(party);
await this.syncWithHost(party);
}
}
} catch (error) {
console.error('Join error:', error);
Modal.alert('Error', 'Failed to join the party. It may have ended.');
await Modal.alert('Error', 'Failed to join the party. It may have ended.');
navigate('/parties');
} finally {
this.isJoining = false;
@ -199,7 +199,7 @@ export class ListeningPartyManager {
);
return confirmed ? { profile: null } : false;
} else {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
const cached = localStorage.getItem('party_guest_profile');
const defaultName = cached ? JSON.parse(cached).name : '';
@ -225,7 +225,9 @@ export class ListeningPartyManager {
},
{ label: 'Cancel', type: 'secondary', callback: () => false },
],
}).then(resolve);
})
.then(resolve)
.catch(reject);
});
}
}
@ -262,29 +264,31 @@ export class ListeningPartyManager {
pb.collection('parties')
.subscribe(
partyId,
(e) => {
async (e) => {
if (e.action === 'update') {
this.currentParty = e.record;
if (!this.isHost) this.syncWithHost(e.record);
if (!this.isHost) await this.syncWithHost(e.record);
this.updatePartyHeader();
} else if (e.action === 'delete') {
Modal.alert('Party Ended', 'The host has ended the listening party.');
this.leaveParty(false);
await Modal.alert('Party Ended', 'The host has ended the listening party.');
await this.leaveParty(false);
}
},
{ f_id }
)
.then((unsub) => this.unsubscribeFunctions.push(unsub));
.then((unsub) => this.unsubscribeFunctions.push(unsub))
.catch(console.error);
pb.collection('party_members')
.subscribe(
'*',
(e) => {
if (e.record.party === partyId) this.loadMembers();
async (e) => {
if (e.record.party === partyId) await this.loadMembers();
},
{ f_id }
)
.then((unsub) => this.unsubscribeFunctions.push(unsub));
.then((unsub) => this.unsubscribeFunctions.push(unsub))
.catch(console.error);
pb.collection('party_messages')
.subscribe(
@ -294,23 +298,25 @@ export class ListeningPartyManager {
},
{ f_id }
)
.then((unsub) => this.unsubscribeFunctions.push(unsub));
.then((unsub) => this.unsubscribeFunctions.push(unsub))
.catch(console.error);
pb.collection('party_requests')
.subscribe(
'*',
(e) => {
if (e.record.party === partyId) this.loadRequests();
async (e) => {
if (e.record.party === partyId) await this.loadRequests();
},
{ f_id }
)
.then((unsub) => this.unsubscribeFunctions.push(unsub));
.then((unsub) => this.unsubscribeFunctions.push(unsub))
.catch(console.error);
}
async loadInitialData(partyId) {
this.loadMembers();
this.loadMessages();
this.loadRequests();
async loadInitialData(_partyId) {
await this.loadMembers();
await this.loadMessages();
await this.loadRequests();
}
async loadMembers() {
@ -439,7 +445,7 @@ export class ListeningPartyManager {
</div>
${this.isHost ? `<button class="btn-primary btn-sm add-request-btn" data-req-id="${r.id}" style="padding: 0.4rem 1rem; font-size: 0.8rem; flex-shrink: 0; white-space: nowrap;">Add to Queue</button>` : ''}
</div>`;
} catch (e) {
} catch (_e) {
return '';
}
})
@ -510,7 +516,7 @@ export class ListeningPartyManager {
await pb
.collection('party_messages')
.create({ party: this.currentParty.id, sender_name: profile.name, content }, { f_id });
} catch (e) {}
} catch (_e) {}
}
async requestSong(track) {
@ -560,7 +566,7 @@ export class ListeningPartyManager {
if (party.is_playing) {
if (el.paused) {
const success = await player.safePlay(el);
const _success = await player.safePlay(el);
}
const latency = (Date.now() - party.playback_timestamp) / 1000;
const targetTime = party.is_playing ? party.playback_time + latency : party.playback_time;
@ -640,7 +646,7 @@ export class ListeningPartyManager {
},
{ f_id: authManager.user?.$id }
);
} catch (e) {}
} catch (_e) {}
};
['play', 'pause', 'seeked'].forEach((ev) => {
player.audio.addEventListener(ev, updateParty);
@ -667,7 +673,7 @@ export class ListeningPartyManager {
'danger'
);
if (!leave) return;
this.leaveParty();
await this.leaveParty();
}
return await originalPlayTrackFromQueue(...args);
};
@ -680,7 +686,7 @@ export class ListeningPartyManager {
await pb
.collection('party_members')
.update(this.memberId, { last_seen: Date.now() }, { f_id: authManager.user?.$id || 'guest' });
} catch (e) {}
} catch (_e) {}
}, 30000);
}
@ -705,11 +711,11 @@ export class ListeningPartyManager {
await cleanup('party_messages');
await cleanup('party_requests');
await pb.collection('parties').delete(this.currentParty.id, { f_id });
} catch (e) {}
} catch (_e) {}
} else if (this.memberId) {
try {
await pb.collection('party_members').delete(this.memberId, { f_id });
} catch (e) {}
} catch (_e) {}
}
this.restorePlayerMethods();
this.unlockControls();
@ -733,7 +739,7 @@ export class ListeningPartyManager {
}
copyInviteLink() {
navigator.clipboard.writeText(`${window.location.origin}/party/${this.currentParty.id}`);
navigator.clipboard.writeText(`${window.location.origin}/party/${this.currentParty.id}`).catch(console.error);
showNotification('Invite link copied!');
}

View file

@ -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').catch(console.error);
// Check if text contains Japanese, Chinese, or Korean characters
function containsAsianText(text) {
@ -246,6 +246,7 @@ export class LyricsManager {
// Monkey-patch XMLHttpRequest to redirect dictionary requests to CDN
// Kuromoji uses XHR, not fetch, for loading dictionary files
if (!window._originalXHROpen) {
// eslint-disable-next-line @typescript-eslint/unbound-method
window._originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
const urlStr = url.toString();
@ -264,7 +265,7 @@ export class LyricsManager {
if (!window._originalFetch) {
window._originalFetch = window.fetch;
window.fetch = async (url, options) => {
const urlStr = url.toString();
const urlStr = url instanceof URL ? url.toString() : url.url;
if (urlStr.includes('/dict/') && urlStr.includes('.dat.gz')) {
const filename = urlStr.split('/').pop();
const cdnUrl = `https://cdn.jsdelivr.net/npm/kuromoji@0.1.2/dict/${filename}`;
@ -527,7 +528,7 @@ export class LyricsManager {
}
// Setup MutationObserver to convert lyrics in am-lyrics component
setupLyricsObserver(amLyricsElement) {
async setupLyricsObserver(amLyricsElement) {
this.stopLyricsObserver();
if (!amLyricsElement) return;
@ -575,7 +576,7 @@ export class LyricsManager {
await this.convertLyricsContent(amLyricsElement);
}
if (this.isGeniusMode && this.currentGeniusData) {
this.applyGeniusAnnotations(amLyricsElement, this.currentGeniusData.referents);
await this.applyGeniusAnnotations(amLyricsElement, this.currentGeniusData.referents);
}
}, 100);
});
@ -591,10 +592,10 @@ export class LyricsManager {
// Initial conversion if Romaji mode is enabled - single attempt, no periodic polling
if (this.isRomajiMode) {
this.convertLyricsContent(amLyricsElement);
await this.convertLyricsContent(amLyricsElement);
}
if (this.isGeniusMode && this.currentGeniusData) {
this.applyGeniusAnnotations(amLyricsElement, this.currentGeniusData.referents);
await this.applyGeniusAnnotations(amLyricsElement, this.currentGeniusData.referents);
}
}
@ -692,7 +693,7 @@ export class LyricsManager {
if (amLyricsElement) {
if (this.isRomajiMode) {
// Turning ON: Setup observer and convert immediately
this.setupLyricsObserver(amLyricsElement);
await this.setupLyricsObserver(amLyricsElement);
await this.convertLyricsContent(amLyricsElement);
} else {
// Turning OFF: Stop observer
@ -1238,7 +1239,7 @@ export function clearFullscreenLyricsSync(container) {
}
}
export function clearLyricsPanelSync(audioPlayer, panel) {
export function clearLyricsPanelSync(_audioPlayer, panel) {
if (panel && panel.lyricsCleanup) {
panel.lyricsCleanup();
panel.lyricsCleanup = null;

View file

@ -135,8 +135,8 @@ export class MalojaScrobbler {
scheduleScrobble(delay) {
this.clearScrobbleTimer();
this.scrobbleTimer = setTimeout(() => {
this.scrobbleCurrentTrack();
this.scrobbleTimer = setTimeout(async () => {
await this.scrobbleCurrentTrack();
}, delay);
}
@ -161,8 +161,8 @@ export class MalojaScrobbler {
}
}
onTrackChange(track) {
this.updateNowPlaying(track);
async onTrackChange(track) {
await this.updateNowPlaying(track);
}
onPlaybackStop() {

View file

@ -37,7 +37,7 @@ export function prefetchMetadataObjects(track, api, coverBlob = null) {
* @param {string} quality - Audio quality
* @returns {Promise<Blob>} - Audio blob with embedded metadata
*/
export async function addMetadataToAudio(audioBlob, track, api, _quality, prefetchPromises) {
export async function addMetadataToAudio(audioBlob, track, _api, _quality, prefetchPromises) {
const { coverFetch, lyricsFetch } = prefetchPromises;
/**

View file

@ -546,9 +546,9 @@ export function createStringAtom(type, value, truncateType = true) {
export function createUserAtom(namespace, name, value) {
const encoder = new TextEncoder();
const dashBytes = encoder.encode('----'); // User-defined atom type
const _dashBytes = encoder.encode('----'); // User-defined atom type
const namespaceBytes = encoder.encode(namespace);
const meanBytes = encoder.encode('mean'); // Standard 'mean' atom for namespace
const _meanBytes = encoder.encode('mean'); // Standard 'mean' atom for namespace
const nameBytes = encoder.encode(name);
const valueBytes = encoder.encode('\x00\x00\x00\x01\x00\x00\x00\x00' + value);

View file

@ -32,18 +32,26 @@ export class MultiScrobbler {
);
}
updateNowPlaying(track) {
this.lastfm.updateNowPlaying(track);
this.listenbrainz.updateNowPlaying(track);
this.maloja.updateNowPlaying(track);
this.librefm.updateNowPlaying(track);
async updateNowPlaying(track) {
await Promise.allSettled(
[
this.lastfm.updateNowPlaying(track),
this.listenbrainz.updateNowPlaying(track),
this.maloja.updateNowPlaying(track),
this.librefm.updateNowPlaying(track),
].map((p) => p.catch(console.error))
);
}
onTrackChange(track) {
this.lastfm.onTrackChange(track);
this.listenbrainz.onTrackChange(track);
this.maloja.onTrackChange(track);
this.librefm.onTrackChange(track);
async onTrackChange(track) {
await Promise.allSettled(
[
this.lastfm.onTrackChange(track),
this.listenbrainz.onTrackChange(track),
this.maloja.onTrackChange(track),
this.librefm.onTrackChange(track),
].map((p) => p.catch(console.error))
);
}
onPlaybackStop() {
@ -55,9 +63,11 @@ export class MultiScrobbler {
// Love/Like tracks on all services that support it
async loveTrack(track) {
await this.lastfm.loveTrack(track);
await this.librefm.loveTrack(track);
await this.listenbrainz.loveTrack(track);
await Promise.allSettled(
[this.lastfm.loveTrack(track), this.librefm.loveTrack(track), this.listenbrainz.loveTrack(track)].map((p) =>
p.catch(console.error)
)
);
// Maloja feedback could be added here when supported
}
}

View file

@ -4,8 +4,45 @@ import { LosslessAPI } from './api.js';
import { PodcastsAPI } from './podcasts-api.js';
import { musicProviderSettings } from './storage.js';
/**
* MusicAPI - Singleton class that provides a unified interface for accessing music streaming services.
*
* Supports multiple providers (primarily Tidal) and includes functionality for searching,
* retrieving metadata, streaming, and managing playlists, artists, albums, tracks, and podcasts.
*
* @class MusicAPI
* @classdesc Manages API interactions with music providers and provides caching mechanisms
* for cover artwork and video metadata.
*
* @example
* // Initialize the MusicAPI
* await MusicAPI.initialize(settings);
*
* // Get the singleton instance
* const api = MusicAPI.instance;
*
* // Search for tracks
* const results = await api.search('query');
*
* // Get a specific track
* const track = await api.getTrack('track-id');
*
* // Get stream URL
* const streamUrl = await api.getStreamUrl('track-id', 'HIGH');
*
* @property {LosslessAPI} tidalAPI - The Tidal API instance
* @property {PodcastsAPI} podcastsAPI - The Podcasts API instance
* @property {Object} _settings - Configuration settings
* @property {Map} videoArtworkCache - Cache for video artwork data
*
* @throws {Error} Throws if instance is accessed before initialization
* @throws {Error} Throws if initialize is called more than once
*/
export class MusicAPI {
static #instance = null;
/**
* @type {MusicAPI}
*/
static get instance() {
if (!MusicAPI.#instance) {
throw new Error('MusicAPI not initialized. Call MusicAPI.initialize(settings) first.');
@ -35,7 +72,7 @@ export class MusicAPI {
}
// Get the appropriate API based on provider
getAPI(provider = null) {
getAPI() {
return this.tidalAPI;
}
@ -101,31 +138,31 @@ export class MusicAPI {
}
// Get methods
async getTrack(id, quality, provider = null) {
async getTrack(id, quality) {
const api = this.getAPI();
const cleanId = this.stripProviderPrefix(id);
return api.getTrack(cleanId, quality);
}
async getTrackMetadata(id, provider = null) {
async getTrackMetadata(id) {
const api = this.getAPI();
const cleanId = this.stripProviderPrefix(id);
return api.getTrackMetadata(cleanId);
}
async getAlbum(id, provider = null) {
async getAlbum(id) {
const api = this.getAPI();
const cleanId = this.stripProviderPrefix(id);
return api.getAlbum(cleanId);
}
async getArtist(id, provider = null) {
async getArtist(id) {
const api = this.getAPI();
const cleanId = this.stripProviderPrefix(id);
return api.getArtist(cleanId);
}
async getArtistBiography(id, provider = null) {
async getArtistBiography(id) {
const api = this.getAPI();
const cleanId = this.stripProviderPrefix(id);
if (typeof api.getArtistBiography === 'function') {
@ -134,13 +171,13 @@ export class MusicAPI {
return null;
}
async getVideo(id, provider = null) {
async getVideo(id) {
const api = this.getAPI();
const cleanId = this.stripProviderPrefix(id);
return api.getVideo(cleanId);
}
async getVideoStreamUrl(id, provider = null) {
async getVideoStreamUrl(id) {
const api = this.getAPI();
const cleanId = this.stripProviderPrefix(id);
if (typeof api.getVideoStreamUrl === 'function') {
@ -157,7 +194,7 @@ export class MusicAPI {
return this.tidalAPI.getPlaylist(id);
}
async getMix(id, _provider = null) {
async getMix(id) {
// Mixes are always Tidal for now
return this.tidalAPI.getMix(id);
}
@ -172,7 +209,7 @@ export class MusicAPI {
}
// Stream methods
async getStreamUrl(id, quality, provider = null) {
async getStreamUrl(id, quality) {
const api = this.getAPI();
const cleanId = this.stripProviderPrefix(id);
return api.getStreamUrl(cleanId, quality);

View file

@ -133,7 +133,7 @@ export class Player {
}
this.loadQueueState();
this.setupMediaSession();
await this.setupMediaSession();
this.radioEnabled = radioSettings.isEnabled();
this.radioSeeds = [];
@ -142,19 +142,19 @@ export class Player {
this.playbackSequence = 0;
window.addEventListener('beforeunload', () => {
this.saveQueueState();
window.addEventListener('beforeunload', async () => {
await this.saveQueueState();
});
// Handle visibility change for iOS - AudioContext gets suspended when screen locks
document.addEventListener('visibilitychange', () => {
document.addEventListener('visibilitychange', async () => {
const el = this.activeElement;
if (document.visibilityState === 'visible' && !el.paused) {
// Ensure audio context is resumed when user returns to the app
if (!audioContextManager.isReady()) {
audioContextManager.init(el);
}
audioContextManager.resume();
await audioContextManager.resume();
}
if (document.visibilityState === 'visible' && this.autoplayBlocked) {
this.autoplayBlocked = false;
@ -370,7 +370,7 @@ export class Player {
}
}
saveQueueState() {
async saveQueueState() {
queueManager.saveQueue({
queue: this.queue,
shuffledQueue: this.shuffledQueue,
@ -381,14 +381,14 @@ export class Player {
});
if (window.renderQueueFunction) {
window.renderQueueFunction();
await window.renderQueueFunction();
}
}
setupMediaSession() {
async setupMediaSession() {
if (!('mediaSession' in navigator)) return;
const setHandlers = () => {
const setHandlers = async () => {
navigator.mediaSession.setActionHandler('play', async () => {
const el = this.activeElement;
// Initialize and resume audio context first (required for iOS lock screen)
@ -404,7 +404,7 @@ export class Player {
} catch (e) {
console.error('MediaSession play failed:', e);
// If play fails, try to handle it like a regular play/pause
this.handlePlayPause();
await this.handlePlayPause();
}
});
@ -429,7 +429,7 @@ export class Player {
this.applyReplayGain();
}
await audioContextManager.resume();
this.playNext();
await this.playNext();
});
if (!this.isIOS) {
@ -465,7 +465,7 @@ export class Player {
this.video.addEventListener('playing', () => setHandlers(), { once: true });
}
} else {
setHandlers();
await setHandlers();
}
}
@ -542,7 +542,7 @@ export class Player {
video.play().catch(() => {});
await this.setupVideoQualitySelector();
});
this.hls.on(Hls.Events.ERROR, (event, data) => {
this.hls.on(Hls.Events.ERROR, (_event, data) => {
if (data.fatal) {
console.warn('HLS fatal error:', data.type);
if (fallbackImg) video.replaceWith(fallbackImg);
@ -578,7 +578,7 @@ export class Player {
const levels = this.hls.levels;
const qualityLabels = [
'Auto',
...levels.map((level, i) => {
...levels.map((level) => {
const height = level.height || 0;
const bandwidth = level.bitrate || 0;
if (height >= 1080) return '1080p';
@ -645,7 +645,7 @@ export class Player {
artist: video.artist || (video.artists && video.artists[0]) || 'Unknown Artist',
album: video.album || { title: 'Video', cover: video.image || video.cover },
};
this.setQueue([videoTrack], 0);
await this.setQueue([videoTrack], 0);
await this.playTrackFromQueue();
}
@ -663,7 +663,7 @@ export class Player {
const track = currentQueue[this.currentQueueIndex];
if (track.isUnavailable) {
console.warn(`Attempted to play unavailable track: ${track.title}. Skipping...`);
this.playNext();
await this.playNext();
return;
}
@ -671,7 +671,7 @@ export class Player {
const { contentBlockingSettings } = await import('./storage.js');
if (contentBlockingSettings.shouldHideTrack(track)) {
console.warn(`Attempted to play blocked track: ${track.title}. Skipping...`);
this.playNext();
await this.playNext();
return;
}
@ -694,15 +694,15 @@ export class Player {
this.currentQueueIndex >= currentQueue.length - 1
) {
console.log('[playTrackFromQueue] Fetching more tracks!');
this.fetchMoreArtistPopularTracks().then((newTracks) => {
await this.fetchMoreArtistPopularTracks().then(async (newTracks) => {
console.log('[playTrackFromQueue] Got tracks:', newTracks?.length);
if (newTracks && newTracks.length > 0) {
this.addToQueue(newTracks);
await this.addToQueue(newTracks);
}
});
}
this.saveQueueState();
await this.saveQueueState();
this.currentTrack = track;
@ -818,7 +818,7 @@ export class Player {
if (!streamUrl) {
console.warn(`Podcast episode ${trackTitle} audio URL is missing. Skipping.`);
track.isUnavailable = true;
this.playNext();
await this.playNext();
return;
}
@ -851,7 +851,7 @@ export class Player {
if (!streamUrl) {
console.warn(`Track ${trackTitle} audio URL is missing. Skipping.`);
track.isUnavailable = true;
this.playNext();
await this.playNext();
return;
}
@ -1018,7 +1018,7 @@ export class Player {
}
}
this.preloadNextTracks();
void this.preloadNextTracks().catch(console.error);
} catch (error) {
if (this.playbackSequence !== currentSequence) return;
if (error && (error.name === 'NotAllowedError' || error.name === 'AbortError')) {
@ -1041,6 +1041,8 @@ export class Player {
this.isFallbackRetry = false;
this.isFallbackInProgress = false;
}
return;
}
console.error(`Could not play track: ${trackTitle}`, error);
@ -1051,33 +1053,33 @@ export class Player {
}
}
playAtIndex(index) {
async playAtIndex(index) {
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
if (index >= 0 && index < currentQueue.length) {
this.currentQueueIndex = index;
this.playTrackFromQueue(0, 0);
await this.playTrackFromQueue(0, 0);
}
}
playNext(recursiveCount = 0) {
async playNext(recursiveCount = 0) {
const currentQueue = this.getCurrentQueue();
const isLastTrack = this.currentQueueIndex >= currentQueue.length - 1;
if (recursiveCount > currentQueue.length) {
if (this.radioEnabled && isLastTrack) {
this.fetchRadioRecommendations().then(() => {
this.fetchRadioRecommendations().then(async () => {
const updatedQueue = this.getCurrentQueue();
if (this.currentQueueIndex < updatedQueue.length - 1) {
this.playNext(0);
await this.playNext(0);
}
});
return;
}
if (this.artistPopularTracksState.artistId && this.artistPopularTracksState.hasMore) {
this.fetchMoreArtistPopularTracks().then((newTracks) => {
await this.fetchMoreArtistPopularTracks().then(async (newTracks) => {
if (newTracks && newTracks.length > 0) {
this.addToQueue(newTracks);
this.playNext(0);
await this.addToQueue(newTracks);
await this.playNext(0);
} else {
this.activeElement.pause();
}
@ -1088,52 +1090,54 @@ export class Player {
return;
}
import('./storage.js').then(({ contentBlockingSettings }) => {
if (
this.repeatMode === REPEAT_MODE.ONE &&
!currentQueue[this.currentQueueIndex]?.isUnavailable &&
!contentBlockingSettings.shouldHideTrack(currentQueue[this.currentQueueIndex])
) {
this.playTrackFromQueue(0, recursiveCount);
return;
}
if (!isLastTrack) {
this.currentQueueIndex++;
const track = currentQueue[this.currentQueueIndex];
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
return this.playNext(recursiveCount + 1);
import('./storage.js')
.then(async ({ contentBlockingSettings }) => {
if (
this.repeatMode === REPEAT_MODE.ONE &&
!currentQueue[this.currentQueueIndex]?.isUnavailable &&
!contentBlockingSettings.shouldHideTrack(currentQueue[this.currentQueueIndex])
) {
await this.playTrackFromQueue(0, recursiveCount);
return;
}
} else if (this.radioEnabled) {
this.fetchRadioRecommendations().then(() => {
const updatedQueue = this.getCurrentQueue();
if (this.currentQueueIndex < updatedQueue.length - 1) {
this.playNext(0);
}
});
return;
} else if (this.artistPopularTracksState.artistId && this.artistPopularTracksState.hasMore) {
this.fetchMoreArtistPopularTracks().then((newTracks) => {
if (newTracks && newTracks.length > 0) {
this.addToQueue(newTracks);
}
// Now play the next track (which is now at currentQueueIndex + 1 if tracks were added)
if (!isLastTrack) {
this.currentQueueIndex++;
this.playTrackFromQueue(0, recursiveCount);
});
return;
} else if (this.repeatMode === REPEAT_MODE.ALL) {
this.currentQueueIndex = 0;
const track = currentQueue[this.currentQueueIndex];
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
return this.playNext(recursiveCount + 1);
const track = currentQueue[this.currentQueueIndex];
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
return this.playNext(recursiveCount + 1);
}
} else if (this.radioEnabled) {
this.fetchRadioRecommendations().then(async () => {
const updatedQueue = this.getCurrentQueue();
if (this.currentQueueIndex < updatedQueue.length - 1) {
await this.playNext(0);
}
});
return;
} else if (this.artistPopularTracksState.artistId && this.artistPopularTracksState.hasMore) {
await this.fetchMoreArtistPopularTracks().then(async (newTracks) => {
if (newTracks && newTracks.length > 0) {
await this.addToQueue(newTracks);
}
// Now play the next track (which is now at currentQueueIndex + 1 if tracks were added)
this.currentQueueIndex++;
await this.playTrackFromQueue(0, recursiveCount);
});
return;
} else if (this.repeatMode === REPEAT_MODE.ALL) {
this.currentQueueIndex = 0;
const track = currentQueue[this.currentQueueIndex];
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
return this.playNext(recursiveCount + 1);
}
} else {
return;
}
} else {
return;
}
this.playTrackFromQueue(0, recursiveCount);
});
await this.playTrackFromQueue(0, recursiveCount);
})
.catch(console.error);
}
async enableRadio(seeds = []) {
@ -1141,20 +1145,20 @@ export class Player {
radioSettings.setEnabled(true);
if (seeds.length === 0) {
this.wipeQueue();
await this.wipeQueue();
const pickedSeeds = await this.pickRadioSeeds();
if (pickedSeeds.length > 0) {
this.radioSeeds = pickedSeeds;
const initialQueue = [...pickedSeeds].sort(() => 0.5 - Math.random()).slice(0, 5);
this.setQueue(initialQueue, 0, true);
this.playAtIndex(0);
await this.setQueue(initialQueue, 0, true);
await this.playAtIndex(0);
}
} else {
this.radioSeeds = Array.isArray(seeds) ? seeds : [seeds];
this.wipeQueue();
await this.wipeQueue();
const initialQueue = Array.isArray(seeds) ? seeds.slice(0, 5) : [seeds];
this.setQueue(initialQueue, 0, true);
this.playAtIndex(0);
await this.setQueue(initialQueue, 0, true);
await this.playAtIndex(0);
}
const currentQueue = this.getCurrentQueue();
@ -1217,7 +1221,7 @@ export class Player {
if (newTracks.length > 0) {
const tracksToAdd = newTracks.sort(() => 0.5 - Math.random()).slice(0, 5);
this.addToQueue(tracksToAdd);
await this.addToQueue(tracksToAdd);
}
}
} catch (error) {
@ -1304,13 +1308,15 @@ export class Player {
return;
}
import('./storage.js').then(({ contentBlockingSettings }) => {
const track = currentQueue[this.currentQueueIndex];
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
return this.playPrev(recursiveCount + 1);
}
this.playTrackFromQueue(0, recursiveCount);
});
import('./storage.js')
.then(async ({ contentBlockingSettings }) => {
const track = currentQueue[this.currentQueueIndex];
if (track?.isUnavailable || contentBlockingSettings.shouldHideTrack(track)) {
return this.playPrev(recursiveCount + 1);
}
await this.playTrackFromQueue(0, recursiveCount);
})
.catch(console.error);
}
}
@ -1318,28 +1324,28 @@ export class Player {
return this.currentTrack?.type === 'video' ? this.video : this.audio;
}
handlePlayPause() {
async handlePlayPause() {
const el = this.activeElement;
const hasSource = el.src || el.currentSrc || el.srcObject || this.shakaInitialized;
if (!hasSource || el.error) {
if (this.currentTrack) {
this.playTrackFromQueue(0, 0);
await this.playTrackFromQueue(0, 0);
}
return;
}
if (el.paused) {
this.safePlay(el).catch((e) => {
this.safePlay(el).catch(async (e) => {
if (e.name === 'NotAllowedError' || e.name === 'AbortError') return;
console.error('Play failed, reloading track:', e);
if (this.currentTrack) {
this.playTrackFromQueue(0, 0);
await this.playTrackFromQueue(0, 0);
}
});
} else {
el.pause();
this.saveQueueState();
await this.saveQueueState();
}
}
@ -1358,7 +1364,7 @@ export class Player {
this.updateMediaSessionPositionState();
}
toggleShuffle() {
async toggleShuffle() {
this.shuffleActive = !this.shuffleActive;
if (this.shuffleActive) {
@ -1389,17 +1395,17 @@ export class Player {
}
this.preloadCache.clear();
this.preloadNextTracks();
this.saveQueueState();
void this.preloadNextTracks().catch(console.error);
await this.saveQueueState();
}
toggleRepeat() {
async toggleRepeat() {
this.repeatMode = (this.repeatMode + 1) % 3;
this.saveQueueState();
await this.saveQueueState();
return this.repeatMode;
}
setQueue(tracks, startIndex = 0, isRadio = false) {
async setQueue(tracks, startIndex = 0, isRadio = false) {
if (!isRadio) {
this.disableRadio();
}
@ -1407,7 +1413,7 @@ export class Player {
this.currentQueueIndex = startIndex;
this.shuffleActive = false;
this.preloadCache.clear();
this.saveQueueState();
await this.saveQueueState();
}
setArtistPopularTracksContext(artistId, initialTracks, offset = 15, hasMore = true) {
@ -1474,7 +1480,7 @@ export class Player {
}
}
addToQueue(trackOrTracks) {
async addToQueue(trackOrTracks) {
const tracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks];
this.queue.push(...tracks);
@ -1485,12 +1491,12 @@ export class Player {
if (!this.currentTrack || this.currentQueueIndex === -1) {
this.currentQueueIndex = this.getCurrentQueue().length - tracks.length;
this.playTrackFromQueue(0, 0);
await this.playTrackFromQueue(0, 0);
}
this.saveQueueState();
await this.saveQueueState();
}
addNextToQueue(trackOrTracks) {
async addNextToQueue(trackOrTracks) {
const tracks = Array.isArray(trackOrTracks) ? trackOrTracks : [trackOrTracks];
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
const insertIndex = this.currentQueueIndex + 1;
@ -1504,11 +1510,11 @@ export class Player {
this.originalQueueBeforeShuffle.push(...tracks); // Sync original queue
}
this.saveQueueState();
this.preloadNextTracks(); // Update preload since next track changed
await this.saveQueueState();
void this.preloadNextTracks().catch(console.error); // Update preload since next track changed
}
removeFromQueue(index) {
async removeFromQueue(index) {
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
// If removing current track
@ -1532,11 +1538,11 @@ export class Player {
}
}
this.saveQueueState();
this.preloadNextTracks();
await this.saveQueueState();
void this.preloadNextTracks().catch(console.error);
}
clearQueue() {
async clearQueue() {
if (this.currentTrack) {
this.queue = [this.currentTrack];
@ -1556,10 +1562,10 @@ export class Player {
}
this.preloadCache.clear();
this.saveQueueState();
await this.saveQueueState();
}
wipeQueue() {
async wipeQueue() {
const el = this.activeElement;
el.pause();
el.src = '';
@ -1568,16 +1574,16 @@ export class Player {
this.shuffledQueue = [];
this.originalQueueBeforeShuffle = [];
this.currentQueueIndex = -1;
this.saveQueueState();
await this.saveQueueState();
if (UIRenderer.instance) {
UIRenderer.instance.setCurrentTrack(null);
}
if (window.renderQueueFunction) {
window.renderQueueFunction();
await window.renderQueueFunction();
}
}
moveInQueue(fromIndex, toIndex) {
async moveInQueue(fromIndex, toIndex) {
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
if (fromIndex < 0 || fromIndex >= currentQueue.length) return;
@ -1593,7 +1599,7 @@ export class Player {
} else if (fromIndex > this.currentQueueIndex && toIndex <= this.currentQueueIndex) {
this.currentQueueIndex++;
}
this.saveQueueState();
await this.saveQueueState();
}
getCurrentQueue() {

View file

@ -41,11 +41,11 @@ function getTrackArtists(track) {
/**
* Generates CSV playlist export
* @param {Object} playlist - Playlist metadata
* @param {Object} _playlist - Playlist metadata
* @param {Array} tracks - Array of track objects
* @returns {string} CSV content
*/
export function generateCSV(playlist, tracks) {
export function generateCSV(_playlist, tracks) {
const headers = ['Track Name', 'Artist Name(s)', 'Album', 'Duration'];
let content = headers.map((h) => `"${h}"`).join(',') + '\n';

View file

@ -248,30 +248,31 @@ export async function loadProfile(username) {
}
if (profile.lastfm_username && profile.privacy?.lastfm !== 'private') {
fetchLastFmRecentTracks(profile.lastfm_username).then(async (tracks) => {
if (tracks.length > 0) {
recentSection.style.display = 'block';
recentContainer.innerHTML = tracks
.map((track, index) => {
const isNowPlaying = track['@attr']?.nowplaying === 'true';
let image = getLastFmImage(track.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
fetchLastFmRecentTracks(profile.lastfm_username)
.then(async (tracks) => {
if (tracks.length > 0) {
recentSection.style.display = 'block';
recentContainer.innerHTML = tracks
.map((track, index) => {
const isNowPlaying = track['@attr']?.nowplaying === 'true';
let image = getLastFmImage(track.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
track._imgId = `scrobble-img-${index}`;
track._needsCover = !hasImage;
track._imgId = `scrobble-img-${index}`;
track._needsCover = !hasImage;
let dateDisplay = '';
if (isNowPlaying) dateDisplay = 'Scrobbling now';
else if (track.date) {
const date = new Date(track.date.uts * 1000);
dateDisplay =
date.toLocaleDateString() +
' ' +
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
let dateDisplay = '';
if (isNowPlaying) dateDisplay = 'Scrobbling now';
else if (track.date) {
const date = new Date(track.date.uts * 1000);
dateDisplay =
date.toLocaleDateString() +
' ' +
date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
return `
return `
<div class="track-item lastfm-track" data-title="${escapeHtml(track.name)}" data-artist="${escapeHtml(track.artist?.['#text'] || track.artist?.name || '')}" style="grid-template-columns: 40px 1fr auto; cursor: pointer;">
<img id="${track._imgId}" src="${image}" class="track-item-cover" style="width: 40px; height: 40px; border-radius: 4px;" loading="lazy" onerror="this.src='/assets/appicon.png'">
<div class="track-item-info">
@ -283,39 +284,45 @@ export async function loadProfile(username) {
<div class="track-item-duration" style="font-size: 0.8rem; min-width: auto;">${dateDisplay}</div>
</div>
`;
})
.join('');
})
.join('');
recentContainer.querySelectorAll('.track-item').forEach((item) => {
item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist));
item.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
recentContainer.querySelectorAll('.track-item').forEach((item) => {
item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist));
item.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
});
});
for (const track of tracks) {
if (track._needsCover) {
fetchFallbackCover(track.name, track.artist?.['#text'] || track.artist?.name, track._imgId);
for (const track of tracks) {
if (track._needsCover) {
await fetchFallbackCover(
track.name,
track.artist?.['#text'] || track.artist?.name,
track._imgId
);
}
}
}
}
});
})
.catch(console.error);
fetchLastFmTopArtists(profile.lastfm_username).then(async (artists) => {
if (artists.length > 0 && topArtistsSection && topArtistsContainer) {
topArtistsSection.style.display = 'block';
topArtistsContainer.innerHTML = artists
.map((artist, index) => {
let image = getLastFmImage(artist.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
fetchLastFmTopArtists(profile.lastfm_username)
.then(async (artists) => {
if (artists.length > 0 && topArtistsSection && topArtistsContainer) {
topArtistsSection.style.display = 'block';
topArtistsContainer.innerHTML = artists
.map((artist, index) => {
let image = getLastFmImage(artist.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
const imgId = `top-artist-img-${index}`;
artist._imgId = imgId;
artist._needsCover = !hasImage;
const imgId = `top-artist-img-${index}`;
artist._imgId = imgId;
artist._needsCover = !hasImage;
return `
return `
<div class="card artist lastfm-card" data-name="${escapeHtml(artist.name)}" style="cursor: pointer;">
<div class="card-image-wrapper">
<img id="${imgId}" src="${image}" class="card-image" loading="lazy" onerror="this.src='/assets/appicon.png'">
@ -326,45 +333,47 @@ export async function loadProfile(username) {
</div>
</div>
`;
})
.join('');
})
.join('');
topArtistsContainer.querySelectorAll('.card').forEach((card) => {
card.addEventListener('click', () => handleArtistClick(card.dataset.name));
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
topArtistsContainer.querySelectorAll('.card').forEach((card) => {
card.addEventListener('click', () => handleArtistClick(card.dataset.name));
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
});
});
for (const artist of artists) {
if (artist._needsCover) {
fetchFallbackArtistImage(artist.name, artist._imgId);
for (const artist of artists) {
if (artist._needsCover) {
await fetchFallbackArtistImage(artist.name, artist._imgId);
}
}
}
}
});
})
.catch(console.error);
fetchLastFmTopAlbums(profile.lastfm_username).then(async (albums) => {
if (albums.length > 0 && topAlbumsSection && topAlbumsContainer) {
topAlbumsSection.style.display = 'block';
topAlbumsContainer.innerHTML = albums
.map((album, index) => {
let image = getLastFmImage(album.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
fetchLastFmTopAlbums(profile.lastfm_username)
.then(async (albums) => {
if (albums.length > 0 && topAlbumsSection && topAlbumsContainer) {
topAlbumsSection.style.display = 'block';
topAlbumsContainer.innerHTML = albums
.map((album, index) => {
let image = getLastFmImage(album.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
const imgId = `top-album-img-${index}`;
album._imgId = imgId;
album._needsCover = !hasImage;
const imgId = `top-album-img-${index}`;
album._imgId = imgId;
album._needsCover = !hasImage;
const artistName =
album.artist?.name ||
album.artist?.['#text'] ||
(typeof album.artist === 'string' ? album.artist : 'Unknown Artist');
album._artistName = artistName;
const artistName =
album.artist?.name ||
album.artist?.['#text'] ||
(typeof album.artist === 'string' ? album.artist : 'Unknown Artist');
album._artistName = artistName;
return `
return `
<div class="card lastfm-card" data-name="${escapeHtml(album.name)}" data-artist="${escapeHtml(artistName)}" style="cursor: pointer;">
<div class="card-image-wrapper">
<img id="${imgId}" src="${image}" class="card-image" loading="lazy" onerror="this.src='/assets/appicon.png'">
@ -375,45 +384,47 @@ export async function loadProfile(username) {
</div>
</div>
`;
})
.join('');
})
.join('');
topAlbumsContainer.querySelectorAll('.card').forEach((card) => {
card.addEventListener('click', () => handleAlbumClick(card.dataset.name, card.dataset.artist));
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
topAlbumsContainer.querySelectorAll('.card').forEach((card) => {
card.addEventListener('click', () => handleAlbumClick(card.dataset.name, card.dataset.artist));
card.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
});
});
for (const album of albums) {
if (album._needsCover) {
fetchFallbackAlbumCover(album.name, album._artistName, album._imgId);
for (const album of albums) {
if (album._needsCover) {
await fetchFallbackAlbumCover(album.name, album._artistName, album._imgId);
}
}
}
}
});
})
.catch(console.error);
fetchLastFmTopTracks(profile.lastfm_username).then(async (tracks) => {
if (tracks.length > 0 && topTracksSection && topTracksContainer) {
topTracksSection.style.display = 'block';
topTracksContainer.innerHTML = tracks
.map((track, index) => {
let image = getLastFmImage(track.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
fetchLastFmTopTracks(profile.lastfm_username)
.then(async (tracks) => {
if (tracks.length > 0 && topTracksSection && topTracksContainer) {
topTracksSection.style.display = 'block';
topTracksContainer.innerHTML = tracks
.map((track, index) => {
let image = getLastFmImage(track.image);
const hasImage = !!image;
if (!image) image = '/assets/appicon.png';
const imgId = `top-track-img-${index}`;
track._imgId = imgId;
track._needsCover = !hasImage;
const imgId = `top-track-img-${index}`;
track._imgId = imgId;
track._needsCover = !hasImage;
const artistName =
track.artist?.name ||
track.artist?.['#text'] ||
(typeof track.artist === 'string' ? track.artist : 'Unknown Artist');
track._artistName = artistName;
const artistName =
track.artist?.name ||
track.artist?.['#text'] ||
(typeof track.artist === 'string' ? track.artist : 'Unknown Artist');
track._artistName = artistName;
return `
return `
<div class="track-item lastfm-track" data-title="${escapeHtml(track.name)}" data-artist="${escapeHtml(artistName)}" style="grid-template-columns: 40px 1fr auto; cursor: pointer;">
<img id="${imgId}" src="${image}" class="track-item-cover" style="width: 40px; height: 40px; border-radius: 4px;" loading="lazy" onerror="this.src='/assets/appicon.png'">
<div class="track-item-info">
@ -425,24 +436,25 @@ export async function loadProfile(username) {
<div class="track-item-duration" style="font-size: 0.8rem; min-width: auto;">${parseInt(track.playcount).toLocaleString()} plays</div>
</div>
`;
})
.join('');
})
.join('');
topTracksContainer.querySelectorAll('.track-item').forEach((item) => {
item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist));
item.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
topTracksContainer.querySelectorAll('.track-item').forEach((item) => {
item.addEventListener('click', () => handleTrackClick(item.dataset.title, item.dataset.artist));
item.addEventListener('contextmenu', (e) => {
e.preventDefault();
return false;
});
});
});
for (const track of tracks) {
if (track._needsCover) {
fetchFallbackCover(track.name, track._artistName, track._imgId);
for (const track of tracks) {
if (track._needsCover) {
await fetchFallbackCover(track.name, track._artistName, track._imgId);
}
}
}
}
});
})
.catch(console.error);
}
const currentUser = await syncManager.getUserData();
@ -483,8 +495,8 @@ export async function loadProfile(username) {
}
}
export function openEditProfile() {
syncManager.getUserData().then((data) => {
export async function openEditProfile() {
await syncManager.getUserData().then((data) => {
if (!data || !data.profile) return;
const p = data.profile;
@ -566,7 +578,7 @@ async function saveProfile() {
try {
await syncManager.updateProfile(data);
editProfileModal.classList.remove('active');
loadProfile(newUsername);
await loadProfile(newUsername);
if (window.location.pathname.includes('/user/@')) {
window.history.replaceState(null, '', `/user/@${newUsername}`);
@ -589,7 +601,7 @@ viewMyProfileBtn.addEventListener('click', async () => {
if (data && data.profile && data.profile.username) {
navigate(`/user/@${data.profile.username}`);
} else {
openEditProfile();
await openEditProfile();
}
});

View file

@ -1,9 +1,9 @@
declare global {
type MonochromeProgress<T = {}> = {
type MonochromeProgress<T = object> = {
stage: string;
} & T;
type MonochromeProgressMessage<T = MonochromeProgress> = {
type MonochromeProgressMessage<_T = MonochromeProgress> = {
message: string;
};

View file

@ -48,7 +48,7 @@ import { db } from './db.js';
import { authManager } from './accounts/auth.js';
import { syncManager } from './accounts/pocketbase.js';
import { containerFormats, customFormats } from './ffmpegFormats.ts';
import { modernSettings } from './ModernSettings.js';
import { BulkDownloadMethod, modernSettings } from './ModernSettings.js';
async function getButterchurnPresets(...args) {
const butterchurnModule = await import('./visualizers/butterchurn.js');
@ -943,10 +943,10 @@ export async function initializeSettings(scrobbler, player, api, ui) {
const showQualityBadgesToggle = document.getElementById('show-quality-badges-toggle');
if (showQualityBadgesToggle) {
showQualityBadgesToggle.checked = qualityBadgeSettings.isEnabled();
showQualityBadgesToggle.addEventListener('change', (e) => {
showQualityBadgesToggle.addEventListener('change', async (e) => {
qualityBadgeSettings.setEnabled(e.target.checked);
// Re-render queue if available, but don't force navigation to library
if (window.renderQueueFunction) window.renderQueueFunction();
if (window.renderQueueFunction) await window.renderQueueFunction();
});
}
@ -979,15 +979,15 @@ export async function initializeSettings(scrobbler, player, api, ui) {
if (!forceZipBlobSettingItem) return;
const method = modernSettings.bulkDownloadMethod;
// Only relevant when zip method is selected and the browser supports streaming
const visible = method === 'zip' && hasFileSystemAccess;
const visible = method === BulkDownloadMethod.Zip && hasFileSystemAccess;
forceZipBlobSettingItem.style.display = visible ? '' : 'none';
}
/** Shows/hides folder-picker-specific and folder-method settings */
async function updateFolderMethodVisibility() {
const method = modernSettings.bulkDownloadMethod;
const isFolderMethod = method === 'folder';
const isFolderOrLocal = isFolderMethod || method === 'local';
const isFolderMethod = method === BulkDownloadMethod.Folder;
const isFolderOrLocal = isFolderMethod || method === BulkDownloadMethod.LocalMedia;
if (rememberFolderSetting) {
rememberFolderSetting.style.display = isFolderMethod && hasFolderPicker ? '' : 'none';
@ -1022,8 +1022,8 @@ export async function initializeSettings(scrobbler, player, api, ui) {
}
// If the stored method is 'folder' or 'local' without native support, fall back to 'zip'
const currentMethod = modernSettings.bulkDownloadMethod;
if (currentMethod === 'folder' || currentMethod === 'local') {
modernSettings.bulkDownloadMethod = 'zip';
if (currentMethod === BulkDownloadMethod.Folder || currentMethod === BulkDownloadMethod.LocalMedia) {
modernSettings.bulkDownloadMethod = BulkDownloadMethod.Zip;
}
}
bulkDownloadMethod.value = modernSettings.bulkDownloadMethod;
@ -1033,7 +1033,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
modernSettings.bulkDownloadMethod = newMethod;
// When switching to 'local', prompt to select the local media folder if not yet configured
if (newMethod === 'local') {
if (newMethod === BulkDownloadMethod.LocalMedia) {
const existingHandle = await db.getSetting('local_folder_handle');
if (!existingHandle) {
let picked = false;
@ -1329,12 +1329,12 @@ export async function initializeSettings(scrobbler, player, api, ui) {
autoeqHeadphoneSelect.appendChild(optgroup);
// When user picks a popular headphone from the dropdown, load it
autoeqHeadphoneSelect.addEventListener('change', () => {
autoeqHeadphoneSelect.addEventListener('change', async () => {
const selected = autoeqHeadphoneSelect.value;
if (!selected) return;
const popularEntry = POPULAR_HEADPHONES.find((hp) => hp.name === selected);
if (popularEntry && (!autoeqSelectedEntry || autoeqSelectedEntry.name !== selected)) {
loadHeadphoneEntry(popularEntry);
await loadHeadphoneEntry(popularEntry);
}
});
}
@ -2433,7 +2433,12 @@ export async function initializeSettings(scrobbler, player, api, ui) {
}
const x = freqToX(f, pw);
const y = mid - (Math.max(-dbRange, Math.min(dbRange, total)) / dbRange) * mid * 0.9;
first ? (ctx.moveTo(x, y), (first = false)) : ctx.lineTo(x, y);
if (first) {
ctx.moveTo(x, y);
first = false;
} else {
ctx.lineTo(x, y);
}
}
ctx.strokeStyle = 'rgba(255,255,255,0.9)';
ctx.lineWidth = 2;
@ -2650,7 +2655,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
modelMap.get(baseName).push(entry);
});
modelMap.forEach((variants, name) => {
modelMap.forEach(async (variants, name) => {
const wrapper = document.createElement('div');
const rawFirstChar = name[0]?.toUpperCase() || '#';
const firstLetter = /^[A-Z]$/.test(rawFirstChar) ? rawFirstChar : '#';
@ -2676,19 +2681,19 @@ export async function initializeSettings(scrobbler, player, api, ui) {
const subList = document.createElement('div');
subList.className = 'autoeq-db-sub-list';
variants.forEach((entry) => {
for (const entry of variants) {
const subItem = document.createElement('div');
subItem.className = 'autoeq-db-sub-item';
// Extract source from parentheses
const sourceMatch = entry.name.match(/\(([^)]+)\)\s*$/);
const sourceMatch = await entry.name.match(/\(([^)]+)\)\s*$/);
const source = sourceMatch ? sourceMatch[1] : entry.type;
subItem.innerHTML = `<span>${entry.name}</span><span class="sub-source">${source}</span>`;
subItem.addEventListener('click', (e) => {
subItem.addEventListener('click', async (e) => {
e.stopPropagation();
loadHeadphoneEntry(entry);
await loadHeadphoneEntry(entry);
});
subList.appendChild(subItem);
});
}
wrapper.appendChild(subList);
@ -4409,7 +4414,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
if (initParaProfiles) initParaProfiles.style.display = 'none';
// Auto-load headphone database
loadFullDatabase();
await loadFullDatabase();
// Auto-load default popular headphone if no saved profile is active
const activeProfileId = equalizerSettings.getActiveAutoEQProfile();
@ -4432,7 +4437,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
if (autoeqRunBtn) autoeqRunBtn.disabled = false;
requestAnimationFrame(drawAutoEQGraph);
} else if (POPULAR_HEADPHONES.length > 0) {
loadHeadphoneEntry(POPULAR_HEADPHONES[0]);
await loadHeadphoneEntry(POPULAR_HEADPHONES[0]);
}
}
@ -4990,7 +4995,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
const currentSource = homePageSettings.getEditorsPicksSource();
editorsPicksSourceSelect.value = currentSource;
}
populateEditorsPicksSource();
await populateEditorsPicksSource();
editorsPicksSourceSelect.addEventListener('change', (e) => {
homePageSettings.setEditorsPicksSource(e.target.value);
@ -5365,7 +5370,7 @@ export async function initializeSettings(scrobbler, player, api, ui) {
try {
await syncManager.clearCloudData();
alert('Cloud data cleared successfully.');
authManager.signOut();
await authManager.signOut();
} catch (error) {
console.error('Failed to clear cloud data:', error);
alert('Failed to clear cloud data: ' + error.message);
@ -5716,7 +5721,7 @@ function initializeFontSettings() {
});
// Google Fonts apply
fontGoogleApply.addEventListener('click', () => {
fontGoogleApply.addEventListener('click', async () => {
const input = fontGoogleInput.value.trim();
if (!input) return;
@ -5735,16 +5740,16 @@ function initializeFontSettings() {
// Not a URL, treat as font name
}
fontSettings.loadGoogleFont(fontName);
await fontSettings.loadGoogleFont(fontName);
});
// URL font apply
fontUrlApply.addEventListener('click', () => {
fontUrlApply.addEventListener('click', async () => {
const url = fontUrlInput.value.trim();
const name = fontUrlName.value.trim();
if (!url) return;
fontSettings.loadFontFromUrl(url, name || 'CustomFont');
await fontSettings.loadFontFromUrl(url, name || 'CustomFont');
});
// File upload

View file

@ -118,25 +118,25 @@ export class SidePanelManager {
return this.currentView === view && this.panel.classList.contains('active');
}
refresh(view, renderControlsCallback, renderContentCallback, options = {}) {
async refresh(view, renderControlsCallback, renderContentCallback, options = {}) {
if (this.isActive(view)) {
if (renderControlsCallback) {
this.controlsElement.innerHTML = '';
renderControlsCallback(this.controlsElement);
await renderControlsCallback(this.controlsElement);
}
if (renderContentCallback) {
if (!options.noClear) {
this.contentElement.innerHTML = '';
}
renderContentCallback(this.contentElement);
await renderContentCallback(this.contentElement);
}
}
}
updateContent(view, renderContentCallback) {
async updateContent(view, renderContentCallback) {
if (this.isActive(view)) {
this.contentElement.innerHTML = '';
renderContentCallback(this.contentElement);
await renderContentCallback(this.contentElement);
}
}
}

View file

@ -2653,18 +2653,18 @@ export const fontSettings = {
document.documentElement.style.setProperty('--font-family', "'SF Pro Display', sans-serif");
},
applyFont() {
async applyFont() {
const config = this.getConfig();
switch (config.type) {
case 'google':
this.loadGoogleFont(config.family);
await this.loadGoogleFont(config.family);
break;
case 'url':
this.loadFontFromUrl(config.url, config.family);
await this.loadFontFromUrl(config.url, config.family);
break;
case 'uploaded':
this.loadUploadedFont(config.fontId);
await this.loadUploadedFont(config.fontId);
break;
case 'preset':
default:

View file

@ -22,7 +22,11 @@ export async function withTimeout<T>(callback: () => Promise<T>, timeout: number
})
.catch((err) => {
clearTimeout(timer);
reject(err);
if (err instanceof Error) {
reject(err);
} else {
reject(new Error(String(err)));
}
});
});
}
@ -33,7 +37,7 @@ function toUint8Array(audioData: ArrayBufferLike | Uint8Array) {
}
return doTimed(
`Converting audio data (${(audioData as any)?.constructor?.name}) to Uint8Array`,
`Converting audio data (${(audioData as object)?.constructor?.name}) to Uint8Array`,
() => new Uint8Array(audioData)
);
}
@ -60,7 +64,7 @@ async function convertInputToTaglib<R = TagLibReadTypes>(
return (await doTimedAsync('Reading File from FileSystemHandle as Uint8Array', async () => {
const file = await audioData.getFile();
const arrayBuffer = await file.arrayBuffer();
return await toUint8Array(arrayBuffer);
return toUint8Array(arrayBuffer);
})) as R;
} else if (
!(audioData instanceof Uint8Array) &&
@ -69,7 +73,7 @@ async function convertInputToTaglib<R = TagLibReadTypes>(
!('FileSystemFileEntry' in globalThis && audioData instanceof FileSystemFileEntry) &&
!('FileSystemFileHandle' in globalThis && audioData instanceof FileSystemFileHandle)
) {
return toUint8Array(audioData as any) as R;
return toUint8Array(audioData as unknown as ArrayBufferLike) as R;
}
return audioData as R;
@ -114,19 +118,19 @@ export async function addMetadataWithTagLib(
if (error) {
reject(new Error(error));
} else {
resolve(data!);
resolve(data);
}
};
worker.onerror = reject;
worker.onmessageerror = reject;
const transferables: Transferable[] = [];
if ((audioData as any)?.buffer instanceof ArrayBuffer) {
transferables.push((audioData as any).buffer);
if ((audioData as Uint8Array)?.buffer instanceof ArrayBuffer) {
transferables.push((audioData as Uint8Array).buffer);
}
if ((data as any).cover?.data?.buffer instanceof ArrayBuffer) {
transferables.push((data as any).cover.data.buffer);
if (data.cover?.data?.buffer instanceof ArrayBuffer) {
transferables.push(data.cover.data.buffer);
}
worker.postMessage({ ...data, type: 'Add', audioData, filename }, transferables);
@ -168,15 +172,15 @@ export async function getMetadataWithTagLib(
if (error) {
reject(new Error(error));
} else {
resolve(data!);
resolve(data);
}
};
worker.onerror = reject;
worker.onmessageerror = reject;
const transferables: Transferable[] = [];
if ((audioData as any)?.buffer instanceof ArrayBuffer) {
transferables.push((audioData as any).buffer);
if ((audioData as Uint8Array)?.buffer instanceof ArrayBuffer) {
transferables.push((audioData as Uint8Array).buffer);
}
worker.postMessage({ type: 'Get', audioData, filename }, transferables);
}),

View file

@ -52,7 +52,6 @@ export enum Mp4Stik {
WhackedBookmark = 5,
MusicVideo = 6,
Movie = 9,
ShortFilm = 9,
TVShow = 10,
Booklet = 11,
}

View file

@ -1,8 +1,8 @@
// filepath: /workspaces/monochrome/js/taglib.worker.ts
declare var self: DedicatedWorkerGlobalScope;
declare let self: DedicatedWorkerGlobalScope;
import { ByteVector } from '!/@dantheman827/taglib-ts/src/byteVector.js';
import { Mp4Tag, Mp4Item } from '!/@dantheman827/taglib-ts/src/mp4/mp4Tag.js';
import { Mp4Item } from '!/@dantheman827/taglib-ts/src/mp4/mp4Tag.js';
import { Variant } from '!/@dantheman827/taglib-ts/src/toolkit/variant.js';
import { doTimed, doTimedAsync } from './doTimed';
import {
@ -10,7 +10,6 @@ import {
type _AddMetadataMessage,
type _GetMetadataMessage,
type AddMetadataMessage,
type GetMetadataMessage,
type TagLibFileResponse,
type TagLibMetadata,
type TagLibMetadataResponse,
@ -18,6 +17,7 @@ import {
type TagLibWorkerMessage,
type TagLibWorkerResponse,
} from './taglib.types';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { File as TagLibFile } from '!/@dantheman827/taglib-ts/src/file.js';
import { FileRef } from '!/@dantheman827/taglib-ts/src/fileRef.js';
import { ChunkedByteVectorStream } from '!/@dantheman827/taglib-ts/src/toolkit/chunkedByteVectorStream.js';
@ -38,7 +38,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
const {
audioData,
audioRef,
filename,
filename: _filename,
title,
artist,
writeArtistsSeparately = false,
@ -79,7 +79,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
const isMp4 = underlying instanceof Mp4File;
const isMpeg = underlying instanceof MpegFile;
const isOgg = underlying instanceof OggVorbisFile;
const isWav = underlying instanceof WavFile;
const _isWav = underlying instanceof WavFile;
const needsCombinedTrackDisc = isMp4 || isMpeg;
@ -137,7 +137,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
if (copyright) props.replace('COPYRIGHT', [copyright]);
if (isrc) props.replace('ISRC', [isrc]);
if (isrc && isMp4) {
const mp4Tag = (underlying as Mp4File).tag() as Mp4Tag;
const mp4Tag = underlying.tag();
mp4Tag.setItem('xid ', Mp4Item.fromStringList([`:isrc:${isrc}`]));
}
if (upc) props.replace('UPC', [upc]);
@ -145,8 +145,8 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
if (explicit !== undefined) {
if (isMp4) {
// rtng is a byte item - must be set directly on the Mp4Tag
const mp4Tag = (underlying as Mp4File).tag() as Mp4Tag;
// rtng is a byte item must be set directly on the Mp4Tag
const mp4Tag = underlying.tag();
mp4Tag.setItem('rtng', Mp4Item.fromByte(explicit ? 1 : 0));
} else {
props.replace('ITUNESADVISORY', [explicit ? '1' : '0']);
@ -154,7 +154,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
}
if (stik != null && isMp4) {
const mp4Tag = (underlying as Mp4File).tag() as Mp4Tag;
const mp4Tag = underlying.tag();
mp4Tag.setItem('stik', Mp4Item.fromByte(stik));
}
@ -177,7 +177,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
await ref.save();
});
const file = ref.file() as TagLibFile;
const file = ref.file();
if (!file) return audioData;
const stream = file.stream();
@ -207,7 +207,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
}
export async function getMetadataFromAudio(message: _GetMetadataMessage): Promise<TagLibReadMetadata> {
const { audioData, audioRef, filename } = message;
const { audioData, audioRef } = message;
const data: TagLibReadMetadata = { duration: 0 };
const ref =
@ -263,7 +263,7 @@ export async function getMetadataFromAudio(message: _GetMetadataMessage): Promis
data.isrc = props.get('ISRC')?.[0] || undefined;
if (isMp4) {
const mp4Tag = (underlying as Mp4File).tag() as Mp4Tag;
const mp4Tag = underlying.tag();
data.explicit = mp4Tag.item('rtng')?.toByte() === 1;
} else {
data.explicit = props.get('ITUNESADVISORY')?.[0] === '1';

View file

@ -48,9 +48,9 @@ export class ThemeStore {
}
init() {
document.getElementById('open-theme-store-btn')?.addEventListener('click', () => {
document.getElementById('open-theme-store-btn')?.addEventListener('click', async () => {
this.modal.classList.add('active');
this.loadThemes();
await this.loadThemes();
});
this.modal?.querySelector('.close-modal-btn')?.addEventListener('click', () => {
@ -59,14 +59,14 @@ export class ThemeStore {
const tabs = this.modal?.querySelectorAll('.search-tab');
tabs?.forEach((tab) => {
tab.addEventListener('click', () => {
tab.addEventListener('click', async () => {
tabs.forEach((t) => t.classList.remove('active'));
this.modal.querySelectorAll('.search-tab-content').forEach((c) => c.classList.remove('active'));
tab.classList.add('active');
const contentId = tab.dataset.tab === 'browse' ? 'theme-store-browse' : 'theme-store-upload';
document.getElementById(contentId)?.classList.add('active');
if (tab.dataset.tab === 'upload') {
this.checkAuth();
await this.checkAuth();
} else {
this.resetEditState();
}
@ -82,9 +82,9 @@ export class ThemeStore {
this.uploadForm?.addEventListener('submit', (e) => this.handleUpload(e));
if (authManager) {
authManager.onAuthStateChanged(() => {
authManager.onAuthStateChanged(async () => {
if (this.modal.classList.contains('active')) {
this.checkAuth();
await this.checkAuth();
}
});
}
@ -231,10 +231,10 @@ export class ThemeStore {
</div>
`;
div.addEventListener('click', (e) => {
div.addEventListener('click', async (e) => {
if (e.target.closest('.delete-theme-btn')) {
e.stopPropagation();
this.deleteTheme(theme.id);
await this.deleteTheme(theme.id);
return;
}
if (e.target.closest('.edit-theme-btn')) {
@ -266,7 +266,7 @@ export class ThemeStore {
await this.pb.collection('themes').delete(themeId, { f_id: fbUser.$id });
alert('Theme deleted successfully.');
this.loadThemes();
await this.loadThemes();
} catch (err) {
console.error('Failed to delete theme:', err);
alert('Failed to delete theme. You might not have permission.');
@ -467,6 +467,7 @@ export class ThemeStore {
// Force reflow to ensure theme changes are applied immediately
document.documentElement.style.display = 'none';
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
document.documentElement.offsetHeight;
document.documentElement.style.display = '';
@ -572,7 +573,7 @@ export class ThemeStore {
}
this.modal.querySelector('[data-tab="browse"]').click();
this.loadThemes();
await this.loadThemes();
} catch (err) {
console.error('Upload failed:', err);
console.error('Response data:', err.data);

View file

@ -280,7 +280,7 @@ function renderTrackerTracks(container, tracks) {
}
// Create project card HTML - EXACTLY like album cards
export function createProjectCardHTML(era, artist, sheetId, trackCount) {
export function createProjectCardHTML(era, _artist, sheetId, trackCount) {
const playBtnHTML = `
<button class="play-btn card-play-btn" data-action="play-card" data-type="tracker-project" data-id="${encodeURIComponent(era.name)}" title="Play">
${SVG_PLAY(20)}

View file

@ -66,7 +66,7 @@ export function initializeUIInteractions(player, api, ui) {
if (playlistId && folderId) {
const updatedFolder = await db.addPlaylistToFolder(folderId, playlistId);
syncManager.syncUserFolder(updatedFolder, 'update');
await syncManager.syncUserFolder(updatedFolder, 'update');
const subtitle = folderCard.querySelector('.card-subtitle');
if (subtitle) {
subtitle.textContent = `${updatedFolder.playlists.length} playlists`;
@ -112,7 +112,7 @@ export function initializeUIInteractions(player, api, ui) {
});
// Queue panel
const renderQueueControls = (container) => {
const renderQueueControls = async (container) => {
const currentQueue = player.getCurrentQueue();
const showActionBtns = currentQueue.length > 0;
@ -141,7 +141,7 @@ export function initializeUIInteractions(player, api, ui) {
const downloadBtn = container.querySelector('#download-queue-btn');
if (downloadBtn) {
downloadBtn.addEventListener('click', async () => {
downloadTracks(currentQueue, api, downloadQualitySettings.getQuality());
await downloadTracks(currentQueue, api, downloadQualitySettings.getQuality());
});
}
@ -152,7 +152,7 @@ export function initializeUIInteractions(player, api, ui) {
for (const track of currentQueue) {
const wasAdded = await db.toggleFavorite('track', track);
if (wasAdded) {
syncManager.syncLibraryItem('track', track, true);
await syncManager.syncLibraryItem('track', track, true);
addedCount++;
}
}
@ -163,7 +163,7 @@ export function initializeUIInteractions(player, api, ui) {
showNotification('All tracks in queue are already liked');
}
refreshQueuePanel();
await refreshQueuePanel();
});
}
@ -222,7 +222,7 @@ export function initializeUIInteractions(player, api, ui) {
}
const updatedPlaylist = await db.getPlaylist(playlistId);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
await syncManager.syncUserPlaylist(updatedPlaylist, 'update');
showNotification(`Added ${addedCount} tracks to playlist: ${playlistName}`);
} catch (error) {
@ -238,9 +238,9 @@ export function initializeUIInteractions(player, api, ui) {
const clearBtn = container.querySelector('#clear-queue-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
clearBtn.addEventListener('click', async () => {
player.clearQueue();
refreshQueuePanel();
await refreshQueuePanel();
});
}
};
@ -283,7 +283,7 @@ export function initializeUIInteractions(player, api, ui) {
`;
};
const attachQueueListeners = (container) => {
const attachQueueListeners = async (container) => {
if (container._queueListenersAttached) return;
container.addEventListener('click', async (e) => {
@ -295,7 +295,7 @@ export function initializeUIInteractions(player, api, ui) {
if (removeBtn) {
e.stopPropagation();
player.removeFromQueue(index);
refreshQueuePanel();
await refreshQueuePanel();
return;
}
@ -305,12 +305,12 @@ export function initializeUIInteractions(player, api, ui) {
const track = player.getCurrentQueue()[index];
if (track) {
const added = await db.toggleFavorite('track', track);
syncManager.syncLibraryItem('track', track, added);
await syncManager.syncLibraryItem('track', track, added);
likeBtn.classList.toggle('active', added);
likeBtn.innerHTML = added ? SVG_HEART_FILLED(20) : SVG_HEART(20);
hapticSuccess();
await hapticSuccess();
showNotification(added ? `Added to Liked: ${track.title}` : `Removed from Liked: ${track.title}`);
}
return;
@ -319,7 +319,7 @@ export function initializeUIInteractions(player, api, ui) {
if (item.classList.contains('blocked')) return;
player.playAtIndex(index);
refreshQueuePanel();
await refreshQueuePanel();
});
container.addEventListener('contextmenu', async (e) => {
@ -369,14 +369,14 @@ export function initializeUIInteractions(player, api, ui) {
e.preventDefault();
});
container.addEventListener('drop', (e) => {
container.addEventListener('drop', async (e) => {
e.preventDefault();
const item = e.target.closest('.queue-track-item');
if (item && draggedQueueIndex !== null) {
const index = parseInt(item.dataset.queueIndex);
if (draggedQueueIndex !== index) {
player.moveInQueue(draggedQueueIndex, index);
refreshQueuePanel();
await refreshQueuePanel();
}
}
});
@ -384,7 +384,7 @@ export function initializeUIInteractions(player, api, ui) {
container._queueListenersAttached = true;
};
const renderQueueContent = (container, isUpdate = false) => {
const renderQueueContent = async (container, isUpdate = false) => {
const currentQueue = player.getCurrentQueue();
if (currentQueue.length === 0) {
@ -395,7 +395,7 @@ export function initializeUIInteractions(player, api, ui) {
}
isQueueRendering = true;
attachQueueListeners(container);
await attachQueueListeners(container);
if (currentQueue.length > QUEUE_VIRTUALIZATION_THRESHOLD) {
if (!isUpdate) {
@ -422,26 +422,26 @@ export function initializeUIInteractions(player, api, ui) {
if (bottomObserver) bottomObserver.disconnect();
bottomObserver = new IntersectionObserver(
(entries) => {
async (entries) => {
if (entries[0].isIntersecting && !isQueueRendering && queueEndIndex < currentQueue.length) {
queueEndIndex = Math.min(currentQueue.length, queueEndIndex + QUEUE_CHUNK_SIZE);
if (queueEndIndex - queueStartIndex > QUEUE_MAX_RENDERED) {
queueStartIndex += QUEUE_CHUNK_SIZE;
}
renderQueueContent(container, true);
await renderQueueContent(container, true);
}
},
{ root: container, rootMargin: '200px' }
);
topObserver = new IntersectionObserver(
(entries) => {
async (entries) => {
if (entries[0].isIntersecting && !isQueueRendering && queueStartIndex > 0) {
queueStartIndex = Math.max(0, queueStartIndex - QUEUE_CHUNK_SIZE);
if (queueEndIndex - queueStartIndex > QUEUE_MAX_RENDERED) {
queueEndIndex -= QUEUE_CHUNK_SIZE;
}
renderQueueContent(container, true);
await renderQueueContent(container, true);
}
},
{ root: container, rootMargin: '200px' }
@ -469,8 +469,8 @@ export function initializeUIInteractions(player, api, ui) {
isQueueRendering = false;
};
const refreshQueuePanel = () => {
sidePanelManager.refresh('queue', renderQueueControls, renderQueueContent, { noClear: true });
const refreshQueuePanel = async () => {
await sidePanelManager.refresh('queue', renderQueueControls, renderQueueContent, { noClear: true });
};
const openQueuePanel = () => {
@ -489,9 +489,9 @@ export function initializeUIInteractions(player, api, ui) {
queueBtn.addEventListener('click', openQueuePanel);
// Expose renderQueue for external updates (e.g. shuffle, add to queue)
window.renderQueueFunction = () => {
window.renderQueueFunction = async () => {
if (sidePanelManager.isActive('queue')) {
refreshQueuePanel();
await refreshQueuePanel();
}
const overlay = document.getElementById('fullscreen-cover-overlay');
@ -519,7 +519,7 @@ export function initializeUIInteractions(player, api, ui) {
if (playlistId && folderId) {
try {
const updatedFolder = await db.addPlaylistToFolder(folderId, playlistId);
syncManager.syncUserFolder(updatedFolder, 'update');
await syncManager.syncUserFolder(updatedFolder, 'update');
window.dispatchEvent(new HashChangeEvent('hashchange'));
showNotification('Playlist added to folder');
} catch (error) {
@ -562,9 +562,11 @@ export function initializeUIInteractions(player, api, ui) {
document.getElementById(contentId)?.classList.add('active');
// Save active tab
import('./storage.js').then(({ settingsUiState }) => {
settingsUiState.setActiveTab(tab.dataset.tab);
});
import('./storage.js')
.then(({ settingsUiState }) => {
settingsUiState.setActiveTab(tab.dataset.tab);
})
.catch(console.error);
});
});

421
js/ui.js

File diff suppressed because it is too large Load diff

View file

@ -664,7 +664,7 @@ export function fetchBlob(url) {
}
export async function fetchBlobURL(url) {
return await URL.createObjectURL(await fetchBlob(url));
return URL.createObjectURL(await fetchBlob(url));
}
export function getMimeType(data) {

View file

@ -133,14 +133,14 @@ export class Visualizer {
this._currentContextType = type;
}
start() {
async start() {
if (this.isActive) return;
if (!this.ctx) {
this.initContext();
}
if (!this.audioContext) {
this.init();
await this.init();
}
if (!this.analyser) {

View file

@ -92,7 +92,7 @@ export function onButterchurnPresetsLoaded(callback) {
}
// Start loading presets immediately when module is imported (lazy loaded)
loadPresetsModule();
loadPresetsModule().catch(console.error);
export class ButterchurnPreset {
constructor() {
@ -191,7 +191,7 @@ export class ButterchurnPreset {
/**
* Initialize Butterchurn with the given WebGL context
*/
init(canvas, gl, audioContext, sourceNode) {
init(canvas, _gl, audioContext, sourceNode) {
if (this.isInitialized) return;
try {
@ -418,7 +418,7 @@ export class ButterchurnPreset {
/**
* Main draw function called each animation frame
*/
draw(ctx, canvas, analyser, dataArray, params) {
draw(_ctx, canvas, _analyser, _dataArray, params) {
if (!this.isInitialized) {
return;
}

View file

@ -125,7 +125,7 @@ export class KawarpPreset {
if (this.kawarp) this.kawarp.resize();
}
draw(ctx, canvas, analyser, dataArray, stats) {
draw(_ctx, canvas, analyser, _dataArray, stats) {
if (!this.kawarp || !this.isInitialized) return;
this._ensureStarted();

View file

@ -14,7 +14,7 @@ export class ParticlesPreset {
// No cleanup needed
}
draw(ctx, canvas, analyser, dataArray, params) {
draw(ctx, canvas, _analyser, _dataArray, params) {
const { width, height } = canvas;
const { kick, intensity, primaryColor, mode } = params;
const sensitivity = params.sensitivity || 1.0;

View file

@ -5,4 +5,4 @@ async function test() {
const json = await res.json();
console.log(JSON.stringify(json.data || {}));
}
test();
void test();

View file

@ -1,6 +1,5 @@
import { loadEnv } from 'vite';
import cookieSession from 'cookie-session';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { readFileSync, existsSync } from 'fs';
import { join, extname } from 'path';

View file

@ -110,7 +110,7 @@ export default function getBlobUrl() {
chunk.code = chunk.code.replace(
/"__BLOB_ASSET_(.*?)__"/g,
(_, refId) => `"${this.getFileName(refId)}"`
(_, refId: string) => `"${this.getFileName(refId)}"`
);
}
},

View file

@ -1,4 +1,4 @@
import { normalizePath, Plugin } from 'vite';
import { normalizePath, type Plugin, type ResolvedConfig } from 'vite';
import path from 'path';
import fs from 'fs';
import { optimize } from 'svgo';
@ -30,7 +30,7 @@ function parseAttrs(str: string): Record<string, string> {
* Merge attributes into root <svg>
*/
function mergeSvgAttributes(svg: string, attrs: Record<string, string>) {
return svg.replace(/<svg([^>]*)>/i, (match, existingAttrs) => {
return svg.replace(/<svg([^>]*)>/i, (_match, existingAttrs: string | undefined) => {
// Size is shorthand for setting both width and height to the same value
if (attrs['size']) {
attrs['width'] = attrs['size'];
@ -40,7 +40,7 @@ function mergeSvgAttributes(svg: string, attrs: Record<string, string>) {
const map = new Map<string, string>();
for (const [, name, value] of existingAttrs.matchAll(ATTR_REGEX)) {
for (const [, name, value] of (existingAttrs ?? '').matchAll(ATTR_REGEX)) {
map.set(name, value);
}
@ -104,7 +104,7 @@ function loadSvg<S extends boolean = true, T = S extends true ? string : Promise
* Main plugin
*/
export default function viteSvgUsePlugin(): Plugin {
let config: any;
let config: ResolvedConfig;
const watched = new Set<string>();
/**
@ -117,10 +117,8 @@ export default function viteSvgUsePlugin(): Plugin {
}
// Check for alias
if (config && config.resolve && config.resolve.alias) {
for (const [_, { find, replacement }] of Object.entries<{ find: string; replacement: string }>(
config.resolve.alias
)) {
if (src.startsWith(find)) {
for (const [_, { find, replacement }] of config.resolve.alias.entries()) {
if (typeof find === 'string' ? src.startsWith(find) : find.test(src)) {
// Remove alias prefix and resolve
const aliasedPath = src.replace(find, replacement);
return normalizePath(path.resolve(root, aliasedPath.replace(/^\//, '')));
@ -144,23 +142,26 @@ export default function viteSvgUsePlugin(): Plugin {
transformIndexHtml: {
order: 'pre',
async handler(html, ctx) {
return html.replace(SVG_USE_REGEX, (full, before, src, after) => {
const attrs = {
...parseAttrs(before || ''),
...parseAttrs(after || ''),
};
return html.replace(
SVG_USE_REGEX,
(_full, before: string | undefined, src: string | undefined, after: string | undefined) => {
const attrs = {
...parseAttrs(before || ''),
...parseAttrs(after || ''),
};
delete attrs['use'];
delete attrs['use'];
const filePath = resolveSvg(config.root, ctx.filename || '', src);
const filePath = resolveSvg(config.root, ctx.filename || '', src);
watched.add(filePath);
watched.add(filePath);
let svg = loadSvg(filePath);
svg = mergeSvgAttributes(optimize(svg).data, attrs);
let svg = loadSvg(filePath);
svg = mergeSvgAttributes(optimize(svg).data, attrs);
return svg;
});
return svg;
}
);
},
},

View file

@ -16,7 +16,7 @@ function getGitCommitHash() {
}
}
export default defineConfig(({ mode }) => {
export default defineConfig((_options) => {
const commitHash = getGitCommitHash();
return {