remove dead apis, search in settings, playbar dragging

This commit is contained in:
Eduard Prigoana 2026-02-05 20:42:15 +00:00
parent bd45a8cac9
commit 60b60bd8fa
10 changed files with 653 additions and 167 deletions

View file

@ -2007,6 +2007,45 @@
<div id="page-settings" class="page">
<h2 class="section-title">Settings</h2>
<form
class="track-list-search-container settings-search-container"
onsubmit="return false;"
style="margin: 1rem 0"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="search-icon"
>
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input
type="search"
id="settings-search-input"
placeholder="Search settings..."
class="track-list-search-input"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
/>
<button
type="button"
class="search-clear-btn btn-icon"
title="Clear search"
style="display: none"
>
×
</button>
</form>
<div class="settings-tabs">
<button class="settings-tab active" data-tab="appearance">Appearance</button>
<button class="settings-tab" data-tab="interface">Interface</button>
@ -3729,7 +3768,6 @@
</div>
</footer>
</div>
<script src="https://unpkg.com/@studio-freight/lenis"></script>
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.21.3/dist/pocketbase.umd.js"></script>
<script type="module" src="/js/app.js"></script>
</body>

View file

@ -6,24 +6,48 @@ import { Player } from './player.js';
import { MultiScrobbler } from './multi-scrobbler.js';
import { LyricsManager, openLyricsPanel, clearLyricsPanelSync } from './lyrics.js';
import { createRouter, updateTabTitle, navigate } from './router.js';
import { initializeSettings } from './settings.js';
import { initializePlayerEvents, initializeTrackInteractions, handleTrackAction } from './events.js';
import { initializeUIInteractions } from './ui-interactions.js';
import {
downloadAlbumAsZip,
downloadDiscography,
downloadPlaylistAsZip,
downloadLikedTracks,
showNotification,
} from './downloads.js';
import { debounce, SVG_PLAY } from './utils.js';
import { sidePanelManager } from './side-panel.js';
import { db } from './db.js';
import { syncManager } from './accounts/pocketbase.js';
import { registerSW } from 'virtual:pwa-register';
import './smooth-scrolling.js';
import { readTrackMetadata } from './metadata.js';
import { initTracker } from './tracker.js';
// Lazy-loaded modules
let settingsModule = null;
let downloadsModule = null;
let trackerModule = null;
let metadataModule = null;
async function loadSettingsModule() {
if (!settingsModule) {
settingsModule = await import('./settings.js');
}
return settingsModule;
}
async function loadDownloadsModule() {
if (!downloadsModule) {
downloadsModule = await import('./downloads.js');
}
return downloadsModule;
}
async function loadTrackerModule() {
if (!trackerModule) {
trackerModule = await import('./tracker.js');
}
return trackerModule;
}
async function loadMetadataModule() {
if (!metadataModule) {
metadataModule = await import('./metadata.js');
}
return metadataModule;
}
function initializeCasting(audioPlayer, castBtn) {
if (!castBtn) return;
@ -305,10 +329,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}
}
// Pre-load Kuroshiro for romaji conversion in background (always load so it's ready instantly)
lyricsManager.loadKuroshiro().catch((err) => {
console.warn('Failed to pre-load Kuroshiro:', err);
});
// Kuroshiro is now loaded on-demand only when needed for Asian text with Romaji mode enabled
const currentTheme = themeManager.getTheme();
themeManager.setTheme(currentTheme);
@ -316,7 +337,10 @@ document.addEventListener('DOMContentLoaded', async () => {
// Restore sidebar state
sidebarSettings.restoreState();
// Load settings module and initialize
const { initializeSettings } = await loadSettingsModule();
initializeSettings(scrobbler, player, api, ui);
initializePlayerEvents(player, audioPlayer, scrobbler, ui);
initializeTrackInteractions(
player,
@ -330,6 +354,8 @@ document.addEventListener('DOMContentLoaded', async () => {
initializeUIInteractions(player, api, ui);
initializeKeyboardShortcuts(player, audioPlayer);
// Load tracker module
const { initTracker } = await loadTrackerModule();
initTracker(player);
const castBtn = document.getElementById('cast-btn');
@ -510,6 +536,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}
} catch (error) {
console.error('Failed to play album:', error);
const { showNotification } = await loadDownloadsModule();
showNotification('Failed to play album');
}
}
@ -533,10 +560,12 @@ document.addEventListener('DOMContentLoaded', async () => {
if (shuffleBtn) shuffleBtn.classList.remove('active');
player.shuffleActive = false;
player.playTrackFromQueue();
const { showNotification } = await loadDownloadsModule();
showNotification('Shuffling album');
}
} catch (error) {
console.error('Failed to shuffle album:', error);
const { showNotification } = await loadDownloadsModule();
showNotification('Failed to shuffle album');
}
}
@ -560,6 +589,7 @@ document.addEventListener('DOMContentLoaded', async () => {
try {
const { mix, tracks } = await api.getMix(mixId);
const { downloadPlaylistAsZip } = await loadDownloadsModule();
await downloadPlaylistAsZip(mix, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
} catch (error) {
console.error('Mix download failed:', error);
@ -603,6 +633,7 @@ document.addEventListener('DOMContentLoaded', async () => {
tracks = data.tracks;
}
const { downloadPlaylistAsZip } = await loadDownloadsModule();
await downloadPlaylistAsZip(playlist, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
} catch (error) {
console.error('Playlist download failed:', error);
@ -966,6 +997,7 @@ document.addEventListener('DOMContentLoaded', async () => {
try {
const { album, tracks } = await api.getAlbum(albumId);
const { downloadAlbumAsZip } = await loadDownloadsModule();
await downloadAlbumAsZip(album, tracks, api, downloadQualitySettings.getQuality(), lyricsManager);
} catch (error) {
console.error('Album download failed:', error);
@ -987,6 +1019,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const { tracks } = await api.getAlbum(albumId);
if (!tracks || tracks.length === 0) {
const { showNotification } = await loadDownloadsModule();
showNotification('No tracks found in this album.');
return;
}
@ -1044,10 +1077,12 @@ document.addEventListener('DOMContentLoaded', async () => {
try {
await db.addTracksToPlaylist(playlistId, tracks);
const { showNotification } = await loadDownloadsModule();
showNotification(`Added ${tracks.length} tracks to playlist.`);
closeModal();
} catch (err) {
console.error(err);
const { showNotification } = await loadDownloadsModule();
showNotification('Failed to add tracks.');
}
};
@ -1065,6 +1100,7 @@ document.addEventListener('DOMContentLoaded', async () => {
modal.classList.add('active');
} catch (error) {
console.error('Failed to prepare album for playlist:', error);
const { showNotification } = await loadDownloadsModule();
showNotification('Failed to load album tracks.');
}
}
@ -1176,6 +1212,7 @@ document.addEventListener('DOMContentLoaded', async () => {
alert('No liked tracks to download.');
return;
}
const { downloadLikedTracks } = await loadDownloadsModule();
await downloadLikedTracks(likedTracks, api, downloadQualitySettings.getQuality(), lyricsManager);
} catch (error) {
console.error('Liked tracks download failed:', error);
@ -1235,6 +1272,7 @@ document.addEventListener('DOMContentLoaded', async () => {
name.endsWith('.ogg')
) {
const file = await entry.getFile();
const { readTrackMetadata } = await loadMetadataModule();
const metadata = await readTrackMetadata(file);
metadata.id = `local-${idCounter++}-${file.name}`;
tracks.push(metadata);
@ -1342,6 +1380,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}
if (event && event.state && event.state.exitTrap) {
const { showNotification } = await loadDownloadsModule();
showNotification('Press back again to exit');
setTimeout(() => {
if (history.state && history.state.exitTrap) {
@ -1908,6 +1947,7 @@ function showDiscographyDownloadModal(artist, api, quality, lyricsManager, trigg
'<svg class="animate-spin" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle></svg><span>Downloading...</span>';
try {
const { downloadDiscography } = await loadDownloadsModule();
await downloadDiscography(artist, selectedReleases, api, quality, lyricsManager);
} catch (error) {
console.error('Discography download failed:', error);

View file

@ -417,6 +417,126 @@ async function bulkDownloadToZipStream(
}
}
// Generate ZIP as blob for browsers without File System Access API (iOS, etc.)
async function bulkDownloadToZipBlob(
tracks,
folderName,
api,
quality,
lyricsManager,
notification,
coverBlob = null,
type = 'playlist',
metadata = null
) {
const { abortController } = bulkDownloadTasks.get(notification);
const signal = abortController.signal;
const { downloadZip } = await loadClientZip();
async function* yieldFiles() {
// Add cover if available
if (coverBlob) {
yield { name: `${folderName}/cover.jpg`, lastModified: new Date(), input: coverBlob };
}
// Generate playlist files first
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
if (playlistSettings.shouldGenerateM3U()) {
const m3uContent = generateM3U(metadata || { title: folderName }, tracks, useRelativePaths);
yield {
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u`,
lastModified: new Date(),
input: m3uContent,
};
}
if (playlistSettings.shouldGenerateM3U8()) {
const m3u8Content = generateM3U8(metadata || { title: folderName }, tracks, useRelativePaths);
yield {
name: `${folderName}/${sanitizeForFilename(folderName)}.m3u8`,
lastModified: new Date(),
input: m3u8Content,
};
}
if (playlistSettings.shouldGenerateNFO()) {
const nfoContent = generateNFO(metadata || { title: folderName }, tracks, type);
yield {
name: `${folderName}/${sanitizeForFilename(folderName)}.nfo`,
lastModified: new Date(),
input: nfoContent,
};
}
if (playlistSettings.shouldGenerateJSON()) {
const jsonContent = generateJSON(metadata || { title: folderName }, tracks, type);
yield {
name: `${folderName}/${sanitizeForFilename(folderName)}.json`,
lastModified: new Date(),
input: jsonContent,
};
}
// For albums, generate CUE file
if (type === 'album' && playlistSettings.shouldGenerateCUE()) {
const audioFilename = `${sanitizeForFilename(folderName)}.flac`; // Assume FLAC for CUE
const cueContent = generateCUE(metadata, tracks, audioFilename);
yield {
name: `${folderName}/${sanitizeForFilename(folderName)}.cue`,
lastModified: new Date(),
input: cueContent,
};
}
// Download tracks
for (let i = 0; i < tracks.length; i++) {
if (signal.aborted) break;
const track = tracks[i];
const trackTitle = getTrackTitle(track);
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
try {
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
const filename = buildTrackFilename(track, quality, extension);
yield { name: `${folderName}/${filename}`, lastModified: new Date(), input: blob };
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
try {
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
if (lyricsData) {
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
if (lrcContent) {
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
yield {
name: `${folderName}/${lrcFilename}`,
lastModified: new Date(),
input: lrcContent,
};
}
}
} catch {
/* ignore */
}
}
} catch (err) {
if (err.name === 'AbortError') throw err;
console.error(`Failed to download track ${trackTitle}:`, err);
}
}
}
try {
const response = downloadZip(yieldFiles());
const blob = await response.blob();
triggerDownload(blob, `${folderName}.zip`);
} catch (error) {
if (error.name === 'AbortError') return;
throw error;
}
}
async function startBulkDownload(
tracks,
defaultName,
@ -431,9 +551,13 @@ async function startBulkDownload(
const notification = createBulkDownloadNotification(type, name, tracks.length);
try {
const useZip = window.showSaveFilePicker && !bulkDownloadSettings.shouldForceIndividual();
const hasFileSystemAccess =
'showSaveFilePicker' in window && 'createWritable' in FileSystemFileHandle.prototype;
const useZip = hasFileSystemAccess && !bulkDownloadSettings.shouldForceIndividual();
const useZipBlob = !hasFileSystemAccess && !bulkDownloadSettings.shouldForceIndividual();
if (useZip) {
// File System Access API available - use streaming
try {
const fileHandle = await window.showSaveFilePicker({
suggestedName: `${defaultName}.zip`,
@ -459,6 +583,20 @@ async function startBulkDownload(
}
throw err;
}
} else if (useZipBlob) {
// No File System Access API (iOS, etc.) - use blob-based ZIP
await bulkDownloadToZipBlob(
tracks,
defaultName,
api,
quality,
lyricsManager,
notification,
coverBlob,
type,
metadata
);
completeBulkDownload(notification, true);
} else {
// Fallback or Forced: Individual sequential downloads
await bulkDownloadSequentially(tracks, api, quality, lyricsManager, notification);
@ -521,10 +659,127 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
const { abortController } = bulkDownloadTasks.get(notification);
const signal = abortController.signal;
try {
const useZip = window.showSaveFilePicker && !bulkDownloadSettings.shouldForceIndividual();
const hasFileSystemAccess = 'showSaveFilePicker' in window && 'createWritable' in FileSystemFileHandle.prototype;
const useZip = hasFileSystemAccess && !bulkDownloadSettings.shouldForceIndividual();
const useZipBlob = !hasFileSystemAccess && !bulkDownloadSettings.shouldForceIndividual();
async function* yieldDiscography() {
for (let albumIndex = 0; albumIndex < selectedReleases.length; albumIndex++) {
if (signal.aborted) break;
const album = selectedReleases[albumIndex];
updateBulkDownloadProgress(notification, albumIndex, selectedReleases.length, album.title);
try {
const { album: fullAlbum, tracks } = await api.getAlbum(album.id);
const coverBlob = await getCoverBlob(api, fullAlbum.cover || album.cover);
const releaseDateStr =
fullAlbum.releaseDate ||
(tracks[0]?.streamStartDate ? tracks[0].streamStartDate.split('T')[0] : '');
const releaseDate = releaseDateStr ? new Date(releaseDateStr) : null;
const year = releaseDate && !isNaN(releaseDate.getTime()) ? releaseDate.getFullYear() : '';
const albumFolder = formatTemplate(
localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist}',
{
albumTitle: fullAlbum.title,
albumArtist: fullAlbum.artist?.name,
year: year,
}
);
const fullFolderPath = `${rootFolder}/${albumFolder}`;
if (coverBlob)
yield { name: `${fullFolderPath}/cover.jpg`, lastModified: new Date(), input: coverBlob };
// Generate playlist files for each album
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
if (playlistSettings.shouldGenerateM3U()) {
const m3uContent = generateM3U(fullAlbum, tracks, useRelativePaths);
yield {
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u`,
lastModified: new Date(),
input: m3uContent,
};
}
if (playlistSettings.shouldGenerateM3U8()) {
const m3u8Content = generateM3U8(fullAlbum, tracks, useRelativePaths);
yield {
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u8`,
lastModified: new Date(),
input: m3u8Content,
};
}
if (playlistSettings.shouldGenerateNFO()) {
const nfoContent = generateNFO(fullAlbum, tracks, 'album');
yield {
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.nfo`,
lastModified: new Date(),
input: nfoContent,
};
}
if (playlistSettings.shouldGenerateJSON()) {
const jsonContent = generateJSON(fullAlbum, tracks, 'album');
yield {
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.json`,
lastModified: new Date(),
input: jsonContent,
};
}
if (playlistSettings.shouldGenerateCUE()) {
const audioFilename = `${sanitizeForFilename(fullAlbum.title)}.flac`;
const cueContent = generateCUE(fullAlbum, tracks, audioFilename);
yield {
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.cue`,
lastModified: new Date(),
input: cueContent,
};
}
for (const track of tracks) {
if (signal.aborted) break;
try {
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
const filename = buildTrackFilename(track, quality, extension);
yield { name: `${fullFolderPath}/${filename}`, lastModified: new Date(), input: blob };
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
try {
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
if (lyricsData) {
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
if (lrcContent) {
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
yield {
name: `${fullFolderPath}/${lrcFilename}`,
lastModified: new Date(),
input: lrcContent,
};
}
}
} catch {
/* ignore */
}
}
} catch (err) {
if (err.name === 'AbortError') throw err;
console.error(`Failed to download track ${track.title}:`, err);
}
}
} catch (error) {
if (error.name === 'AbortError') throw error;
console.error(`Failed to download album ${album.title}:`, error);
}
}
}
try {
if (useZip) {
// File System Access API available - use streaming
const fileHandle = await window.showSaveFilePicker({
suggestedName: `${rootFolder}.zip`,
types: [{ description: 'ZIP Archive', accept: { 'application/zip': ['.zip'] } }],
@ -532,123 +787,16 @@ export async function downloadDiscography(artist, selectedReleases, api, quality
const writable = await fileHandle.createWritable();
const { downloadZip } = await loadClientZip();
async function* yieldDiscography() {
for (let albumIndex = 0; albumIndex < selectedReleases.length; albumIndex++) {
if (signal.aborted) break;
const album = selectedReleases[albumIndex];
updateBulkDownloadProgress(notification, albumIndex, selectedReleases.length, album.title);
try {
const { album: fullAlbum, tracks } = await api.getAlbum(album.id);
const coverBlob = await getCoverBlob(api, fullAlbum.cover || album.cover);
const releaseDateStr =
fullAlbum.releaseDate ||
(tracks[0]?.streamStartDate ? tracks[0].streamStartDate.split('T')[0] : '');
const releaseDate = releaseDateStr ? new Date(releaseDateStr) : null;
const year = releaseDate && !isNaN(releaseDate.getTime()) ? releaseDate.getFullYear() : '';
const albumFolder = formatTemplate(
localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist}',
{
albumTitle: fullAlbum.title,
albumArtist: fullAlbum.artist?.name,
year: year,
}
);
const fullFolderPath = `${rootFolder}/${albumFolder}`;
if (coverBlob)
yield { name: `${fullFolderPath}/cover.jpg`, lastModified: new Date(), input: coverBlob };
// Generate playlist files for each album
const useRelativePaths = playlistSettings.shouldUseRelativePaths();
if (playlistSettings.shouldGenerateM3U()) {
const m3uContent = generateM3U(fullAlbum, tracks, useRelativePaths);
yield {
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u`,
lastModified: new Date(),
input: m3uContent,
};
}
if (playlistSettings.shouldGenerateM3U8()) {
const m3u8Content = generateM3U8(fullAlbum, tracks, useRelativePaths);
yield {
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.m3u8`,
lastModified: new Date(),
input: m3u8Content,
};
}
if (playlistSettings.shouldGenerateNFO()) {
const nfoContent = generateNFO(fullAlbum, tracks, 'album');
yield {
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.nfo`,
lastModified: new Date(),
input: nfoContent,
};
}
if (playlistSettings.shouldGenerateJSON()) {
const jsonContent = generateJSON(fullAlbum, tracks, 'album');
yield {
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.json`,
lastModified: new Date(),
input: jsonContent,
};
}
if (playlistSettings.shouldGenerateCUE()) {
const audioFilename = `${sanitizeForFilename(fullAlbum.title)}.flac`;
const cueContent = generateCUE(fullAlbum, tracks, audioFilename);
yield {
name: `${fullFolderPath}/${sanitizeForFilename(fullAlbum.title)}.cue`,
lastModified: new Date(),
input: cueContent,
};
}
for (const track of tracks) {
if (signal.aborted) break;
try {
const { blob, extension } = await downloadTrackBlob(track, quality, api, null, signal);
const filename = buildTrackFilename(track, quality, extension);
yield { name: `${fullFolderPath}/${filename}`, lastModified: new Date(), input: blob };
if (lyricsManager && lyricsSettings.shouldDownloadLyrics()) {
try {
const lyricsData = await lyricsManager.fetchLyrics(track.id, track);
if (lyricsData) {
const lrcContent = lyricsManager.generateLRCContent(lyricsData, track);
if (lrcContent) {
const lrcFilename = filename.replace(/\.[^.]+$/, '.lrc');
yield {
name: `${fullFolderPath}/${lrcFilename}`,
lastModified: new Date(),
input: lrcContent,
};
}
}
} catch {
/* ignore */
}
}
} catch (err) {
if (err.name === 'AbortError') throw err;
console.error(`Failed to download track ${track.title}:`, err);
}
}
} catch (error) {
if (error.name === 'AbortError') throw error;
console.error(`Failed to download album ${album.title}:`, error);
}
}
}
const response = downloadZip(yieldDiscography());
await response.body.pipeTo(writable);
completeBulkDownload(notification, true);
} else if (useZipBlob) {
// No File System Access API (iOS, etc.) - use blob-based ZIP
const { downloadZip } = await loadClientZip();
const response = downloadZip(yieldDiscography());
const blob = await response.blob();
triggerDownload(blob, `${rootFolder}.zip`);
completeBulkDownload(notification, true);
} else {
// Sequential individual downloads for discography
for (let albumIndex = 0; albumIndex < selectedReleases.length; albumIndex++) {

View file

@ -3,6 +3,24 @@ import { getTrackTitle, getTrackArtists, buildTrackFilename, SVG_CLOSE } from '.
import { sidePanelManager } from './side-panel.js';
const SVG_GENIUS_ACTIVE = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none"><path d="M12 24c6.627 0 12-5.373 12-12S18.627 0 12 0 0 5.373 0 12s5.373 12 12 12z" fill="#ffff64"/><path d="M6.3 6.3h11.4v11.4H6.3z" fill="#000"/></svg>`;
// Check if text contains Japanese, Chinese, or Korean characters
function containsAsianText(text) {
if (!text) return false;
// Japanese: Hiragana (3040-309F), Katakana (30A0-30FF), Kanji (4E00-9FFF, 3400-4DBF)
// Chinese: CJK Unified Ideographs (4E00-9FFF, 3400-4DBF)
// Korean: Hangul (AC00-D7AF, 1100-11FF, 3130-318F)
const asianRegex = /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u3400-\u4DBF\uAC00-\uD7AF\u1100-\u11FF\u3130-\u318F]/;
return asianRegex.test(text);
}
// Check if track has Asian text in title or artist names
function trackHasAsianText(track) {
if (!track) return false;
const title = track.title || '';
const artist = getTrackArtists(track) || '';
return containsAsianText(title) || containsAsianText(artist);
}
const SVG_GENIUS_INACTIVE = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" style="opacity: 0.7;"><path d="M12 24c6.627 0 12-5.373 12-12S18.627 0 12 0 0 5.373 0 12s5.373 12 12 12z" /><path d="M6.3 6.3h11.4v11.4H6.3z" fill="var(--card)"/></svg>`;
class GeniusManager {
@ -289,6 +307,11 @@ export class LyricsManager {
return this.romajiTextCache.get(text);
}
// Only process if text contains Asian characters
if (!containsAsianText(text)) {
return text;
}
// Make sure Kuroshiro is loaded
if (!this.kuroshiroLoaded) {
const success = await this.loadKuroshiro();
@ -742,8 +765,9 @@ export class LyricsManager {
export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = false) {
const manager = lyricsManager || new LyricsManager();
// Load Kuroshiro in background if needed
if (!manager.kuroshiroLoaded && !manager.kuroshiroLoading) {
// Load Kuroshiro in background only if track has Asian text and Romaji mode is enabled
const isRomajiMode = manager.getRomajiMode();
if (isRomajiMode && trackHasAsianText(track) && !manager.kuroshiroLoaded && !manager.kuroshiroLoading) {
manager.loadKuroshiro().catch((err) => {
console.warn('Failed to load Kuroshiro for Romaji conversion:', err);
});
@ -944,8 +968,8 @@ async function renderLyricsComponent(container, track, audioPlayer, lyricsManage
// This is critical - observer must be running before lyrics arrive from LRCLIB
lyricsManager.setupLyricsObserver(amLyrics);
// If Romaji mode is enabled, ensure Kuroshiro is ready
if (lyricsManager.isRomajiMode && !lyricsManager.kuroshiroLoaded) {
// If Romaji mode is enabled and track has Asian text, ensure Kuroshiro is ready
if (lyricsManager.isRomajiMode && trackHasAsianText(track) && !lyricsManager.kuroshiroLoaded) {
await lyricsManager.loadKuroshiro();
}

View file

@ -1237,4 +1237,105 @@ export function initializeSettings(scrobbler, player, api, ui) {
}
});
}
// Settings Search functionality
setupSettingsSearch();
}
function setupSettingsSearch() {
const searchInput = document.getElementById('settings-search-input');
if (!searchInput) return;
// Setup clear button
const clearBtn = searchInput.parentElement.querySelector('.search-clear-btn');
if (clearBtn) {
clearBtn.addEventListener('click', () => {
searchInput.value = '';
searchInput.dispatchEvent(new Event('input'));
searchInput.focus();
});
}
// Show/hide clear button based on input
const updateClearButton = () => {
if (clearBtn) {
clearBtn.style.display = searchInput.value ? 'flex' : 'none';
}
};
searchInput.addEventListener('input', () => {
updateClearButton();
filterSettings(searchInput.value.toLowerCase().trim());
});
searchInput.addEventListener('focus', updateClearButton);
}
function filterSettings(query) {
const settingsPage = document.getElementById('page-settings');
if (!settingsPage) return;
const allTabContents = settingsPage.querySelectorAll('.settings-tab-content');
const allTabs = settingsPage.querySelectorAll('.settings-tab');
if (!query) {
// Reset: show active tab only
allTabContents.forEach((content) => {
content.classList.remove('active');
});
allTabs.forEach((tab) => {
tab.classList.remove('active');
});
// Restore first tab as active
const firstTab = allTabs[0];
const firstContent = allTabContents[0];
if (firstTab && firstContent) {
firstTab.classList.add('active');
firstContent.classList.add('active');
}
// Show all settings groups and items
const allGroups = settingsPage.querySelectorAll('.settings-group');
const allItems = settingsPage.querySelectorAll('.setting-item');
allGroups.forEach((group) => (group.style.display = ''));
allItems.forEach((item) => (item.style.display = ''));
return;
}
// When searching, show all tabs' content
allTabContents.forEach((content) => {
content.classList.add('active');
});
allTabs.forEach((tab) => {
tab.classList.remove('active');
});
// Search through all settings
const allGroups = settingsPage.querySelectorAll('.settings-group');
allGroups.forEach((group) => {
const items = group.querySelectorAll('.setting-item');
let hasMatch = false;
items.forEach((item) => {
const label = item.querySelector('.label');
const description = item.querySelector('.description');
const labelText = label?.textContent?.toLowerCase() || '';
const descriptionText = description?.textContent?.toLowerCase() || '';
const matches = labelText.includes(query) || descriptionText.includes(query);
if (matches) {
item.style.display = '';
hasMatch = true;
} else {
item.style.display = 'none';
}
});
// Show/hide group based on whether it has any visible items
group.style.display = hasMatch ? '' : 'none';
});
}

View file

@ -1,13 +1,53 @@
//js/smooth-scrolling.js
/* global Lenis */
import { smoothScrollingSettings } from './storage.js';
let lenis = null;
let lenisLoaded = false;
let lenisLoading = false;
function initializeSmoothScrolling() {
async function loadLenisScript() {
if (lenisLoaded) return true;
if (lenisLoading) {
return new Promise((resolve) => {
const checkLoaded = setInterval(() => {
if (!lenisLoading) {
clearInterval(checkLoaded);
resolve(lenisLoaded);
}
}, 100);
});
}
lenisLoading = true;
try {
await new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = 'https://unpkg.com/@studio-freight/lenis';
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
lenisLoaded = true;
lenisLoading = false;
console.log('✓ Lenis loaded successfully');
return true;
} catch (error) {
console.error('✗ Failed to load Lenis:', error);
lenisLoaded = false;
lenisLoading = false;
return false;
}
}
async function initializeSmoothScrolling() {
if (lenis) return; // Already initialized
lenis = new Lenis({
const loaded = await loadLenisScript();
if (!loaded) return;
lenis = new window.Lenis({
wrapper: document.querySelector('.main-content'),
content: document.querySelector('.main-content'),
lerp: 0.1,
@ -18,8 +58,10 @@ function initializeSmoothScrolling() {
});
function raf(time) {
lenis.raf(time);
requestAnimationFrame(raf);
if (lenis) {
lenis.raf(time);
requestAnimationFrame(raf);
}
}
requestAnimationFrame(raf);
@ -32,18 +74,18 @@ function destroySmoothScrolling() {
}
}
function setupSmoothScrolling() {
async function setupSmoothScrolling() {
// Check if smooth scrolling is enabled
const smoothScrollingEnabled = smoothScrollingSettings.isEnabled();
if (smoothScrollingEnabled) {
initializeSmoothScrolling();
await initializeSmoothScrolling();
}
// Listen for toggle changes
window.addEventListener('smooth-scrolling-toggle', function (e) {
window.addEventListener('smooth-scrolling-toggle', async function (e) {
if (e.detail.enabled) {
initializeSmoothScrolling();
await initializeSmoothScrolling();
} else {
destroySmoothScrolling();
}

View file

@ -54,11 +54,6 @@ export const apiSettings = {
console.error('Failed to load instances from GitHub:', error);
this.defaultInstances = {
api: [
'https://ayohh.monochrome.tf',
'https://seangreengoat.monochrome.tf',
'https://esdee.monochrome.tf',
'https://2jewish.monochrome.tf',
'https://ediddy.monochrome.tf',
'https://arran.monochrome.tf',
'https://api.monochrome.tf',
'https://triton.squid.wtf',
@ -73,11 +68,6 @@ export const apiSettings = {
'https://vogel.qqdl.site',
],
streaming: [
'https://ayohh.monochrome.tf',
'https://seangreengoat.monochrome.tf',
'https://esdee.monochrome.tf',
'https://2jewish.monochrome.tf',
'https://ediddy.monochrome.tf',
'https://arran.monochrome.tf',
'https://triton.squid.wtf',
'https://wolf.qqdl.site',

View file

@ -943,12 +943,91 @@ export class UIRenderer {
}
};
progressBar.onclick = (e) => {
const rect = progressBar.getBoundingClientRect();
const pos = (e.clientX - rect.left) / rect.width;
audioPlayer.currentTime = pos * audioPlayer.duration;
// Progress bar with drag support
let isFsSeeking = false;
let wasFsPlaying = false;
let lastFsSeekPosition = 0;
const updateFsSeekUI = (position) => {
if (!isNaN(audioPlayer.duration)) {
progressFill.style.width = `${position * 100}%`;
if (currentTimeEl) {
currentTimeEl.textContent = formatTime(position * audioPlayer.duration);
}
}
};
progressBar.addEventListener('mousedown', (e) => {
isFsSeeking = true;
wasFsPlaying = !audioPlayer.paused;
if (wasFsPlaying) audioPlayer.pause();
const rect = progressBar.getBoundingClientRect();
const pos = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
lastFsSeekPosition = pos;
updateFsSeekUI(pos);
});
progressBar.addEventListener(
'touchstart',
(e) => {
e.preventDefault();
isFsSeeking = true;
wasFsPlaying = !audioPlayer.paused;
if (wasFsPlaying) audioPlayer.pause();
const touch = e.touches[0];
const rect = progressBar.getBoundingClientRect();
const pos = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width));
lastFsSeekPosition = pos;
updateFsSeekUI(pos);
},
{ passive: false }
);
document.addEventListener('mousemove', (e) => {
if (isFsSeeking) {
const rect = progressBar.getBoundingClientRect();
const pos = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
lastFsSeekPosition = pos;
updateFsSeekUI(pos);
}
});
document.addEventListener(
'touchmove',
(e) => {
if (isFsSeeking) {
const touch = e.touches[0];
const rect = progressBar.getBoundingClientRect();
const pos = Math.max(0, Math.min(1, (touch.clientX - rect.left) / rect.width));
lastFsSeekPosition = pos;
updateFsSeekUI(pos);
}
},
{ passive: false }
);
document.addEventListener('mouseup', () => {
if (isFsSeeking) {
if (!isNaN(audioPlayer.duration)) {
audioPlayer.currentTime = lastFsSeekPosition * audioPlayer.duration;
if (wasFsPlaying) audioPlayer.play();
}
isFsSeeking = false;
}
});
document.addEventListener('touchend', () => {
if (isFsSeeking) {
if (!isNaN(audioPlayer.duration)) {
audioPlayer.currentTime = lastFsSeekPosition * audioPlayer.duration;
if (wasFsPlaying) audioPlayer.play();
}
isFsSeeking = false;
}
});
if (fsLikeBtn) {
fsLikeBtn.onclick = () => document.getElementById('now-playing-like-btn')?.click();
}
@ -1063,9 +1142,12 @@ export class UIRenderer {
const current = audioPlayer.currentTime || 0;
if (duration > 0) {
const percent = (current / duration) * 100;
progressFill.style.width = `${percent}%`;
currentTimeEl.textContent = formatTime(current);
// Only update progress if not currently seeking (user is dragging)
if (!isFsSeeking) {
const percent = (current / duration) * 100;
progressFill.style.width = `${percent}%`;
currentTimeEl.textContent = formatTime(current);
}
totalDurationEl.textContent = formatTime(duration);
}

View file

@ -1,10 +1,5 @@
{
"api": [
"https://ayohh.monochrome.tf",
"https://seangreengoat.monochrome.tf",
"https://esdee.monochrome.tf",
"https://2jewish.monochrome.tf",
"https://ediddy.monochrome.tf",
"https://arran.monochrome.tf",
"https://api.monochrome.tf/",
"https://tidal-api.binimum.org",

View file

@ -2418,6 +2418,11 @@ input:checked + .slider::before {
border-radius: 3px;
cursor: pointer;
position: relative;
transition: height 0.2s ease;
}
.fullscreen-progress-container .progress-bar:hover {
height: 8px;
}
.fullscreen-progress-container .progress-fill {
@ -2425,6 +2430,27 @@ input:checked + .slider::before {
background: var(--foreground);
border-radius: 3px;
width: 0%;
transition: width 0.1s ease;
position: relative;
pointer-events: none;
}
.fullscreen-progress-container .progress-bar:hover .progress-fill {
background: var(--highlight);
}
.fullscreen-progress-container .progress-bar:hover .progress-fill::after,
.fullscreen-progress-container .progress-bar:active .progress-fill::after {
content: '';
position: absolute;
right: -6px;
top: 50%;
transform: translateY(-50%);
width: 12px;
height: 12px;
background-color: var(--highlight);
border-radius: 50%;
box-shadow: 0 2px 4px rgb(0, 0, 0, 0.3);
}
.fullscreen-buttons {