Download Lyrics
diff --git a/js/app.js b/js/app.js
index 40feda1..2856fb5 100644
--- a/js/app.js
+++ b/js/app.js
@@ -1,7 +1,7 @@
//js/app.js
import { LosslessAPI } from './api.js';
-import { apiSettings, themeManager, nowPlayingSettings } from './storage.js';
+import { apiSettings, themeManager, nowPlayingSettings, trackListSettings } from './storage.js';
import { UIRenderer } from './ui.js';
import { Player } from './player.js';
import { LastFMScrobbler } from './lastfm.js';
@@ -202,6 +202,7 @@ document.addEventListener('DOMContentLoaded', async () => {
const currentTheme = themeManager.getTheme();
themeManager.setTheme(currentTheme);
+ trackListSettings.getMode();
initializeSettings(scrobbler, player, api, ui);
initializePlayerEvents(player, audioPlayer, scrobbler);
diff --git a/js/downloads.js b/js/downloads.js
index 39e84a6..ceb8a8f 100644
--- a/js/downloads.js
+++ b/js/downloads.js
@@ -24,6 +24,27 @@ function createDownloadNotification() {
return downloadNotificationContainer;
}
+export function showNotification(message) {
+ const container = createDownloadNotification();
+
+ const notifEl = document.createElement('div');
+ notifEl.className = 'download-task';
+
+ notifEl.innerHTML = `
+
+ ${message}
+
+ `;
+
+ container.appendChild(notifEl);
+
+ // Auto remove
+ setTimeout(() => {
+ notifEl.style.animation = 'slideOut 0.3s ease';
+ setTimeout(() => notifEl.remove(), 300);
+ }, 1500);
+}
+
export function addDownloadTask(trackId, track, filename, api) {
const container = createDownloadNotification();
diff --git a/js/events.js b/js/events.js
index 821aa2c..e11cd6a 100644
--- a/js/events.js
+++ b/js/events.js
@@ -1,7 +1,7 @@
//js/events.js
import { SVG_PLAY, SVG_PAUSE, SVG_VOLUME, SVG_MUTE, REPEAT_MODE, trackDataStore, RATE_LIMIT_ERROR_MESSAGE, buildTrackFilename } from './utils.js';
import { lastFMStorage } from './storage.js';
-import { addDownloadTask, updateDownloadProgress, completeDownloadTask, downloadTrackWithMetadata } from './downloads.js';
+import { addDownloadTask, updateDownloadProgress, completeDownloadTask, showNotification, downloadTrackWithMetadata } from './downloads.js';
import { lyricsSettings } from './storage.js';
import { updateTabTitle } from './router.js';
@@ -283,6 +283,29 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
let contextTrack = null;
mainContent.addEventListener('click', e => {
+ const actionBtn = e.target.closest('.track-action-btn');
+ if (actionBtn) {
+ e.stopPropagation();
+ const trackItem = actionBtn.closest('.track-item');
+ if (trackItem) {
+ const track = trackDataStore.get(trackItem);
+ const action = actionBtn.dataset.action;
+
+ if (action === 'add-to-queue' && track) {
+ player.addToQueue(track);
+ renderQueue(player);
+ showNotification(`Added to queue: ${track.title}`);
+ } else if (action === 'play-next' && track) {
+ player.addNextToQueue(track);
+ renderQueue(player);
+ showNotification(`Playing next: ${track.title}`);
+ } else if (action === 'download' && track) {
+ handleDownload(track, player, api);
+ }
+ }
+ return;
+ }
+
const menuBtn = e.target.closest('.track-menu-btn');
if (menuBtn) {
e.stopPropagation();
@@ -334,9 +357,14 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
e.stopPropagation();
const action = e.target.dataset.action;
- if (action === 'add-to-queue' && contextTrack) {
+ if (action === 'play-next' && contextTrack) {
+ player.addNextToQueue(contextTrack);
+ renderQueue(player);
+ showNotification(`Playing next: ${contextTrack.title}`);
+ } else if (action === 'add-to-queue' && contextTrack) {
player.addToQueue(contextTrack);
renderQueue(player);
+ showNotification(`Added to queue: ${contextTrack.title}`);
} else if (action === 'download' && contextTrack) {
await downloadTrackWithMetadata(contextTrack, player.quality, api, lyricsManager);
}
@@ -412,3 +440,33 @@ function positionMenu(menu, x, y, anchorRect = null) {
menu.style.left = `${left}px`;
menu.style.visibility = 'visible';
}
+
+async function handleDownload(track, player, api) {
+ const quality = player.quality;
+ const filename = buildTrackFilename(track, quality);
+
+ try {
+ const { taskEl, abortController } = addDownloadTask(
+ track.id,
+ track,
+ filename,
+ api
+ );
+
+ await api.downloadTrack(track.id, quality, filename, {
+ signal: abortController.signal,
+ onProgress: (progress) => {
+ updateDownloadProgress(track.id, progress);
+ }
+ });
+
+ completeDownloadTask(track.id, true);
+ } catch (error) {
+ if (error.name !== 'AbortError') {
+ const errorMsg = error.message === RATE_LIMIT_ERROR_MESSAGE
+ ? error.message
+ : 'Download failed. Please try again.';
+ completeDownloadTask(track.id, false, errorMsg);
+ }
+ }
+}
diff --git a/js/player.js b/js/player.js
index 8cb5090..8f17b4f 100644
--- a/js/player.js
+++ b/js/player.js
@@ -322,6 +322,26 @@ export class Player {
this.saveQueueState();
}
+ addNextToQueue(track) {
+ const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
+ const insertIndex = this.currentQueueIndex + 1;
+
+ // Insert after current track
+ currentQueue.splice(insertIndex, 0, track);
+
+ // If we are shuffling, we might want to also add it to the original queue for consistency,
+ // though syncing that is tricky. The standard logic often just appends to the active queue view.
+ if (this.shuffleActive) {
+ this.originalQueueBeforeShuffle.push(track); // Just append to end of main list? Or logic needed.
+ // Simplest is to just modify the active playing queue.
+ } else {
+ // In linear mode, `currentQueue` IS `this.queue`
+ }
+
+ this.saveQueueState();
+ this.preloadNextTracks(); // Update preload since next track changed
+ }
+
removeFromQueue(index) {
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
diff --git a/js/settings.js b/js/settings.js
index e8dd8ec..6c3aa96 100644
--- a/js/settings.js
+++ b/js/settings.js
@@ -1,5 +1,5 @@
//js/settings
-import { themeManager, lastFMStorage, nowPlayingSettings, lyricsSettings, backgroundSettings } from './storage.js';
+import { themeManager, lastFMStorage, nowPlayingSettings, lyricsSettings, backgroundSettings, trackListSettings } from './storage.js';
export function initializeSettings(scrobbler, player, api, ui) {
const lastfmConnectBtn = document.getElementById('lastfm-connect-btn');
@@ -176,6 +176,15 @@ export function initializeSettings(scrobbler, player, api, ui) {
});
}
+ // Track List Actions Mode
+ const trackListActionsMode = document.getElementById('track-list-actions-mode');
+ if (trackListActionsMode) {
+ trackListActionsMode.value = trackListSettings.getMode();
+ trackListActionsMode.addEventListener('change', (e) => {
+ trackListSettings.setMode(e.target.value);
+ });
+ }
+
// Download Lyrics Toggle
const downloadLyricsToggle = document.getElementById('download-lyrics-toggle');
if (downloadLyricsToggle) {
diff --git a/js/storage.js b/js/storage.js
index 1ceb671..ef58215 100644
--- a/js/storage.js
+++ b/js/storage.js
@@ -349,6 +349,25 @@ export const backgroundSettings = {
}
};
+export const trackListSettings = {
+ STORAGE_KEY: 'track-list-actions-mode',
+
+ getMode() {
+ try {
+ const mode = localStorage.getItem(this.STORAGE_KEY) || 'dropdown';
+ document.documentElement.setAttribute('data-track-actions-mode', mode);
+ return mode;
+ } catch (e) {
+ return 'dropdown';
+ }
+ },
+
+ setMode(mode) {
+ localStorage.setItem(this.STORAGE_KEY, mode);
+ document.documentElement.setAttribute('data-track-actions-mode', mode);
+ }
+};
+
export const queueManager = {
STORAGE_KEY: 'monochrome-queue',
diff --git a/js/ui.js b/js/ui.js
index 78c067c..603f1f4 100644
--- a/js/ui.js
+++ b/js/ui.js
@@ -1,6 +1,6 @@
//js/ui.js
import { formatTime, createPlaceholder, trackDataStore, hasExplicitContent, getTrackArtists, getTrackTitle, calculateTotalDuration, formatDuration } from './utils.js';
-import { recentActivityManager, backgroundSettings } from './storage.js';
+import { recentActivityManager, backgroundSettings, trackListSettings } from './storage.js';
export class UIRenderer {
constructor(api) {
@@ -85,6 +85,43 @@ export class UIRenderer {
}
}
+ const actionsHTML = `
+
+
+
+
+
+
+ `;
+
return `
${trackNumberHTML}
@@ -98,13 +135,9 @@ export class UIRenderer {