fix(linting): fix js linting issues
This commit is contained in:
parent
ddc986bc52
commit
648e47e1d8
65 changed files with 1202 additions and 1037 deletions
|
|
@ -1,5 +1,5 @@
|
|||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
const { request } = context;
|
||||
const pageUrl = request.url;
|
||||
|
||||
const metaHtml = `
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
const { request } = context;
|
||||
const pageUrl = request.url;
|
||||
|
||||
const metaHtml = `
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
const { request } = context;
|
||||
const pageUrl = request.url;
|
||||
|
||||
const metaHtml = `
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
const { request } = context;
|
||||
const pageUrl = request.url;
|
||||
|
||||
const metaHtml = `
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
const { request } = context;
|
||||
const pageUrl = request.url;
|
||||
|
||||
const metaHtml = `
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
const { request } = context;
|
||||
const pageUrl = request.url;
|
||||
|
||||
const metaHtml = `
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export async function onRequest(context) {
|
||||
const { request, env } = context;
|
||||
const { request } = context;
|
||||
const pageUrl = request.url;
|
||||
|
||||
const metaHtml = `
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
|
|
|
|||
11
js/HiFi.ts
11
js/HiFi.ts
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ export class AuthManager {
|
|||
constructor() {
|
||||
this.user = null;
|
||||
this.authListeners = [];
|
||||
this.init();
|
||||
this.init().catch(console.error);
|
||||
}
|
||||
|
||||
async init() {
|
||||
|
|
|
|||
|
|
@ -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')) || [],
|
||||
|
|
|
|||
12
js/api.js
12
js/api.js
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
165
js/app.js
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
1
js/db.js
1
js/db.js
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
334
js/events.js
334
js/events.js
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
4
js/global.d.ts
vendored
|
|
@ -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
16
js/indexedIterator.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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!');
|
||||
}
|
||||
|
||||
|
|
|
|||
17
js/lyrics.js
17
js/lyrics.js
|
|
@ -10,7 +10,7 @@ import {
|
|||
SVG_GLOBE,
|
||||
} from './icons.js';
|
||||
import { sidePanelManager } from './side-panel.js';
|
||||
import('@uimaxbai/am-lyrics/am-lyrics.js');
|
||||
import('@uimaxbai/am-lyrics/am-lyrics.js').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;
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
236
js/player.js
236
js/player.js
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
258
js/profile.js
258
js/profile.js
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
28
js/taglib.ts
28
js/taglib.ts
|
|
@ -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);
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -52,7 +52,6 @@ export enum Mp4Stik {
|
|||
WhackedBookmark = 5,
|
||||
MusicVideo = 6,
|
||||
Movie = 9,
|
||||
ShortFilm = 9,
|
||||
TVShow = 10,
|
||||
Booklet = 11,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ async function test() {
|
|||
const json = await res.json();
|
||||
console.log(JSON.stringify(json.data || {}));
|
||||
}
|
||||
test();
|
||||
void test();
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}"`
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ function getGitCommitHash() {
|
|||
}
|
||||
}
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
export default defineConfig((_options) => {
|
||||
const commitHash = getGitCommitHash();
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Reference in a new issue