keybind rebinding
This commit is contained in:
parent
9054016ff2
commit
b29160385a
6 changed files with 599 additions and 71 deletions
25
index.html
25
index.html
|
|
@ -1124,6 +1124,27 @@
|
|||
<div class="shortcut-item"><kbd>L</kbd><span>Toggle lyrics</span></div>
|
||||
<div class="shortcut-item"><kbd>/</kbd><span>Focus search</span></div>
|
||||
<div class="shortcut-item"><kbd>Esc</kbd><span>Close modals</span></div>
|
||||
<div class="shortcut-item"><kbd>[</kbd><span>Previous visualizer preset</span></div>
|
||||
<div class="shortcut-item"><kbd>]</kbd><span>Next visualizer preset</span></div>
|
||||
<div class="shortcut-item"><kbd>\</kbd><span>Toggle visualizer auto-cycle</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="customize-shortcuts-modal" class="modal">
|
||||
<div class="modal-overlay"></div>
|
||||
<div class="modal-content medium">
|
||||
<div class="shortcuts-header">
|
||||
<h3>Customize Shortcuts</h3>
|
||||
<button class="close-customize-shortcuts">×</button>
|
||||
</div>
|
||||
<div class="customize-shortcuts-content">
|
||||
<p class="shortcut-hint">Click on a shortcut to rebind it. Press the new key combination.</p>
|
||||
<div id="shortcuts-list" class="shortcuts-list"></div>
|
||||
</div>
|
||||
<div class="customize-shortcuts-actions">
|
||||
<button id="reset-shortcuts-btn" class="btn-secondary">Reset to Defaults</button>
|
||||
<button id="close-customize-shortcuts-btn" class="btn-primary">Done</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -5001,9 +5022,9 @@
|
|||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Keyboard Shortcuts</span>
|
||||
<span class="description">View available keyboard shortcuts</span>
|
||||
<span class="description">View and customize keyboard shortcuts</span>
|
||||
</div>
|
||||
<button id="show-shortcuts-btn" class="btn-secondary">Show Shortcuts</button>
|
||||
<button id="customize-shortcuts-btn" class="btn-secondary">Customize</button>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
|
|
|
|||
333
js/app.js
333
js/app.js
|
|
@ -9,6 +9,7 @@ import {
|
|||
sidebarSettings,
|
||||
pwaUpdateSettings,
|
||||
modalSettings,
|
||||
keyboardShortcuts,
|
||||
} from './storage.js';
|
||||
import { UIRenderer } from './ui.js';
|
||||
import { Player } from './player.js';
|
||||
|
|
@ -160,74 +161,114 @@ function initializeCasting(audioPlayer, castBtn) {
|
|||
}
|
||||
|
||||
function initializeKeyboardShortcuts(player, audioPlayer) {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.matches('input, textarea')) return;
|
||||
const keyActionMap = {
|
||||
playPause: () => {
|
||||
trackKeyboardShortcut('Space');
|
||||
player.handlePlayPause();
|
||||
},
|
||||
seekForward: () => {
|
||||
trackKeyboardShortcut('Right');
|
||||
audioPlayer.currentTime = Math.min(audioPlayer.duration, audioPlayer.currentTime + 10);
|
||||
},
|
||||
seekBackward: () => {
|
||||
trackKeyboardShortcut('Left');
|
||||
audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 10);
|
||||
},
|
||||
nextTrack: () => {
|
||||
trackKeyboardShortcut('Shift+Right');
|
||||
player.playNext();
|
||||
},
|
||||
previousTrack: () => {
|
||||
trackKeyboardShortcut('Shift+Left');
|
||||
player.playPrev();
|
||||
},
|
||||
volumeUp: () => {
|
||||
trackKeyboardShortcut('Up');
|
||||
player.setVolume(player.userVolume + 0.1);
|
||||
},
|
||||
volumeDown: () => {
|
||||
trackKeyboardShortcut('Down');
|
||||
player.setVolume(player.userVolume - 0.1);
|
||||
},
|
||||
mute: () => {
|
||||
trackKeyboardShortcut('M');
|
||||
audioPlayer.muted = !audioPlayer.muted;
|
||||
},
|
||||
shuffle: () => {
|
||||
trackKeyboardShortcut('S');
|
||||
document.getElementById('shuffle-btn')?.click();
|
||||
},
|
||||
repeat: () => {
|
||||
trackKeyboardShortcut('R');
|
||||
document.getElementById('repeat-btn')?.click();
|
||||
},
|
||||
queue: () => {
|
||||
trackKeyboardShortcut('Q');
|
||||
document.getElementById('queue-btn')?.click();
|
||||
},
|
||||
lyrics: () => {
|
||||
trackKeyboardShortcut('L');
|
||||
document.querySelector('.now-playing-bar .cover')?.click();
|
||||
},
|
||||
search: () => {
|
||||
trackKeyboardShortcut('/');
|
||||
document.getElementById('search-input')?.focus();
|
||||
},
|
||||
escape: () => {
|
||||
trackKeyboardShortcut('Escape');
|
||||
document.getElementById('search-input')?.blur();
|
||||
sidePanelManager.close();
|
||||
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
|
||||
},
|
||||
visualizerNext: () => {
|
||||
trackKeyboardShortcut('VisualizerNext');
|
||||
const ui = window.monochromeUi;
|
||||
if (ui?.visualizer?.presets?.['butterchurn']) {
|
||||
ui.visualizer.presets['butterchurn'].nextPreset();
|
||||
}
|
||||
},
|
||||
visualizerPrev: () => {
|
||||
trackKeyboardShortcut('VisualizerPrev');
|
||||
const ui = window.monochromeUi;
|
||||
if (ui?.visualizer?.presets?.['butterchurn']) {
|
||||
ui.visualizer.presets['butterchurn'].prevPreset();
|
||||
}
|
||||
},
|
||||
visualizerCycle: () => {
|
||||
trackKeyboardShortcut('VisualizerCycle');
|
||||
const ui = window.monochromeUi;
|
||||
if (ui?.visualizer?.presets?.['butterchurn']) {
|
||||
ui.visualizer.presets['butterchurn'].toggleCycle();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
switch (e.key.toLowerCase()) {
|
||||
case ' ':
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.matches('input, textarea, [contenteditable="true"]')) return;
|
||||
|
||||
const shortcuts = keyboardShortcuts.getShortcuts();
|
||||
const pressedKey = e.key.toLowerCase();
|
||||
const hasShift = e.shiftKey;
|
||||
const hasCtrl = e.ctrlKey || e.metaKey;
|
||||
const hasAlt = e.altKey;
|
||||
|
||||
for (const [action, shortcut] of Object.entries(shortcuts)) {
|
||||
if (!shortcut?.key) continue;
|
||||
const shortcutKey = shortcut.key.toLowerCase();
|
||||
const matches =
|
||||
pressedKey === shortcutKey &&
|
||||
shortcut.shift === hasShift &&
|
||||
shortcut.ctrl === hasCtrl &&
|
||||
shortcut.alt === hasAlt;
|
||||
|
||||
if (matches) {
|
||||
e.preventDefault();
|
||||
trackKeyboardShortcut('Space');
|
||||
player.handlePlayPause();
|
||||
break;
|
||||
case 'arrowright':
|
||||
if (e.shiftKey) {
|
||||
trackKeyboardShortcut('Shift+Right');
|
||||
player.playNext();
|
||||
} else {
|
||||
trackKeyboardShortcut('Right');
|
||||
audioPlayer.currentTime = Math.min(audioPlayer.duration, audioPlayer.currentTime + 10);
|
||||
const actionFn = keyActionMap[action];
|
||||
if (actionFn) {
|
||||
actionFn();
|
||||
}
|
||||
break;
|
||||
case 'arrowleft':
|
||||
if (e.shiftKey) {
|
||||
trackKeyboardShortcut('Shift+Left');
|
||||
player.playPrev();
|
||||
} else {
|
||||
trackKeyboardShortcut('Left');
|
||||
audioPlayer.currentTime = Math.max(0, audioPlayer.currentTime - 10);
|
||||
}
|
||||
break;
|
||||
case 'arrowup':
|
||||
e.preventDefault();
|
||||
trackKeyboardShortcut('Up');
|
||||
player.setVolume(player.userVolume + 0.1);
|
||||
break;
|
||||
case 'arrowdown':
|
||||
e.preventDefault();
|
||||
trackKeyboardShortcut('Down');
|
||||
player.setVolume(player.userVolume - 0.1);
|
||||
break;
|
||||
case 'm':
|
||||
trackKeyboardShortcut('M');
|
||||
audioPlayer.muted = !audioPlayer.muted;
|
||||
break;
|
||||
case 's':
|
||||
trackKeyboardShortcut('S');
|
||||
document.getElementById('shuffle-btn')?.click();
|
||||
break;
|
||||
case 'r':
|
||||
trackKeyboardShortcut('R');
|
||||
document.getElementById('repeat-btn')?.click();
|
||||
break;
|
||||
case 'q':
|
||||
trackKeyboardShortcut('Q');
|
||||
document.getElementById('queue-btn')?.click();
|
||||
break;
|
||||
case '/':
|
||||
e.preventDefault();
|
||||
trackKeyboardShortcut('/');
|
||||
document.getElementById('search-input')?.focus();
|
||||
break;
|
||||
case 'escape':
|
||||
trackKeyboardShortcut('Escape');
|
||||
document.getElementById('search-input')?.blur();
|
||||
sidePanelManager.close();
|
||||
clearLyricsPanelSync(audioPlayer, sidePanelManager.panel);
|
||||
break;
|
||||
case 'l':
|
||||
trackKeyboardShortcut('L');
|
||||
document.querySelector('.now-playing-bar .cover')?.click();
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -378,6 +419,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
initializeCasting(audioPlayer, castBtn);
|
||||
|
||||
const ui = new UIRenderer(api, player);
|
||||
window.monochromeUi = ui;
|
||||
const scrobbler = new MultiScrobbler();
|
||||
const lyricsManager = new LyricsManager(api);
|
||||
|
||||
|
|
@ -2462,6 +2504,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
showKeyboardShortcuts();
|
||||
});
|
||||
|
||||
document.getElementById('customize-shortcuts-btn')?.addEventListener('click', () => {
|
||||
showCustomizeShortcutsModal();
|
||||
});
|
||||
|
||||
// Font Settings
|
||||
const fontSelect = document.getElementById('font-select');
|
||||
if (fontSelect) {
|
||||
|
|
@ -2846,3 +2892,160 @@ function showKeyboardShortcuts() {
|
|||
modal.addEventListener('click', handleClose);
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
function showCustomizeShortcutsModal() {
|
||||
const modal = document.getElementById('customize-shortcuts-modal');
|
||||
const shortcutsList = document.getElementById('shortcuts-list');
|
||||
let recordingAction = null;
|
||||
let recordingTimeout = null;
|
||||
|
||||
const shortcuts = keyboardShortcuts.getShortcuts();
|
||||
|
||||
const formatKey = (key) => {
|
||||
if (!key) return 'none';
|
||||
const keyMap = {
|
||||
' ': 'Space',
|
||||
arrowup: '↑',
|
||||
arrowdown: '↓',
|
||||
arrowleft: '←',
|
||||
arrowright: '→',
|
||||
escape: 'Esc',
|
||||
backspace: 'Backspace',
|
||||
delete: 'Delete',
|
||||
insert: 'Insert',
|
||||
home: 'Home',
|
||||
end: 'End',
|
||||
pageup: 'Page Up',
|
||||
pagedown: 'Page Down',
|
||||
'[': '[',
|
||||
']': ']',
|
||||
'\\': '\\',
|
||||
tab: 'Tab',
|
||||
enter: 'Enter',
|
||||
capslock: 'Caps Lock',
|
||||
shift: 'Shift',
|
||||
control: 'Ctrl',
|
||||
alt: 'Alt',
|
||||
meta: 'Meta',
|
||||
contextmenu: 'Context Menu',
|
||||
};
|
||||
return keyMap[key.toLowerCase()] || key.toUpperCase();
|
||||
};
|
||||
|
||||
const renderShortcuts = () => {
|
||||
shortcutsList.innerHTML = '';
|
||||
const currentShortcuts = keyboardShortcuts.getShortcuts();
|
||||
|
||||
for (const [action, shortcut] of Object.entries(currentShortcuts || {})) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'customize-shortcut-item';
|
||||
item.dataset.action = action;
|
||||
|
||||
const modifiers = [];
|
||||
if (shortcut?.shift) modifiers.push('Shift');
|
||||
if (shortcut?.ctrl) modifiers.push('Ctrl');
|
||||
if (shortcut?.alt) modifiers.push('Alt');
|
||||
|
||||
const keyDisplay = [...modifiers, formatKey(shortcut?.key)].join(' + ');
|
||||
|
||||
item.innerHTML = `
|
||||
<span class="shortcut-description">${shortcut?.description || 'Unknown'}</span>
|
||||
<div class="shortcut-key">
|
||||
<kbd class="${recordingAction === action ? 'recording' : ''}">${keyDisplay}</kbd>
|
||||
<button class="shortcut-btn" title="Reset to default">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
|
||||
<path d="M3 3v5h5"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const kbd = item.querySelector('kbd');
|
||||
kbd.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
if (recordingAction === action) {
|
||||
recordingAction = null;
|
||||
clearTimeout(recordingTimeout);
|
||||
} else {
|
||||
recordingAction = action;
|
||||
recordingTimeout = setTimeout(() => {
|
||||
keyboardShortcuts.setShortcut(action, {
|
||||
key: null,
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
description: shortcut?.description || 'Unknown',
|
||||
});
|
||||
recordingAction = null;
|
||||
renderShortcuts();
|
||||
}, 3000);
|
||||
}
|
||||
renderShortcuts();
|
||||
});
|
||||
|
||||
const resetBtn = item.querySelector('.shortcut-btn');
|
||||
resetBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const defaults = keyboardShortcuts.getDefaultShortcuts();
|
||||
keyboardShortcuts.setShortcut(action, defaults[action]);
|
||||
renderShortcuts();
|
||||
});
|
||||
|
||||
shortcutsList.appendChild(item);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (!recordingAction) return;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const key = e.key === ' ' ? ' ' : e.key;
|
||||
|
||||
if (['Control', 'Shift', 'Alt', 'Meta'].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
keyboardShortcuts.setShortcut(recordingAction, {
|
||||
key: key,
|
||||
shift: e.shiftKey,
|
||||
ctrl: e.ctrlKey || e.metaKey,
|
||||
alt: e.altKey,
|
||||
});
|
||||
|
||||
clearTimeout(recordingTimeout);
|
||||
recordingAction = null;
|
||||
renderShortcuts();
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
recordingAction = null;
|
||||
clearTimeout(recordingTimeout);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
modal.removeEventListener('click', handleClose);
|
||||
};
|
||||
|
||||
const handleClose = (e) => {
|
||||
if (
|
||||
e.target === modal ||
|
||||
e.target.classList.contains('close-customize-shortcuts') ||
|
||||
e.target.id === 'close-customize-shortcuts-btn' ||
|
||||
e.target.classList.contains('modal-overlay')
|
||||
) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('reset-shortcuts-btn')?.addEventListener('click', () => {
|
||||
keyboardShortcuts.resetShortcuts();
|
||||
renderShortcuts();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
modal.addEventListener('click', handleClose);
|
||||
renderShortcuts();
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import {
|
|||
musicProviderSettings,
|
||||
analyticsSettings,
|
||||
modalSettings,
|
||||
keyboardShortcuts,
|
||||
} from './storage.js';
|
||||
import { audioContextManager, EQ_PRESETS } from './audio-context.js';
|
||||
import { getButterchurnPresets } from './visualizers/butterchurn.js';
|
||||
|
|
|
|||
|
|
@ -2527,3 +2527,71 @@ export const contentBlockingSettings = {
|
|||
localStorage.removeItem(this.BLOCKED_ALBUMS_KEY);
|
||||
},
|
||||
};
|
||||
|
||||
export const keyboardShortcuts = {
|
||||
STORAGE_KEY: 'keyboard-shortcuts',
|
||||
|
||||
DEFAULT_SHORTCUTS: {
|
||||
playPause: { key: ' ', shift: false, ctrl: false, alt: false, description: 'Play / Pause' },
|
||||
seekForward: { key: 'arrowright', shift: false, ctrl: false, alt: false, description: 'Seek forward 10s' },
|
||||
seekBackward: { key: 'arrowleft', shift: false, ctrl: false, alt: false, description: 'Seek backward 10s' },
|
||||
nextTrack: { key: 'arrowright', shift: true, ctrl: false, alt: false, description: 'Next track' },
|
||||
previousTrack: { key: 'arrowleft', shift: true, ctrl: false, alt: false, description: 'Previous track' },
|
||||
volumeUp: { key: 'arrowup', shift: false, ctrl: false, alt: false, description: 'Volume up' },
|
||||
volumeDown: { key: 'arrowdown', shift: false, ctrl: false, alt: false, description: 'Volume down' },
|
||||
mute: { key: 'm', shift: false, ctrl: false, alt: false, description: 'Mute / Unmute' },
|
||||
shuffle: { key: 's', shift: false, ctrl: false, alt: false, description: 'Toggle shuffle' },
|
||||
repeat: { key: 'r', shift: false, ctrl: false, alt: false, description: 'Toggle repeat' },
|
||||
queue: { key: 'q', shift: false, ctrl: false, alt: false, description: 'Open queue' },
|
||||
lyrics: { key: 'l', shift: false, ctrl: false, alt: false, description: 'Toggle lyrics' },
|
||||
search: { key: '/', shift: false, ctrl: false, alt: false, description: 'Focus search' },
|
||||
escape: { key: 'escape', shift: false, ctrl: false, alt: false, description: 'Close modals' },
|
||||
visualizerNext: { key: ']', shift: false, ctrl: false, alt: false, description: 'Next visualizer preset' },
|
||||
visualizerPrev: { key: '[', shift: false, ctrl: false, alt: false, description: 'Previous visualizer preset' },
|
||||
visualizerCycle: {
|
||||
key: '\\',
|
||||
shift: false,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
description: 'Toggle visualizer auto-cycle',
|
||||
},
|
||||
},
|
||||
|
||||
getShortcuts() {
|
||||
try {
|
||||
const saved = localStorage.getItem(this.STORAGE_KEY);
|
||||
if (saved) {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (parsed && typeof parsed === 'object' && Object.keys(parsed).length > 0) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load keyboard shortcuts:', e);
|
||||
}
|
||||
return this.getDefaultShortcuts();
|
||||
},
|
||||
|
||||
getDefaultShortcuts() {
|
||||
return { ...this.DEFAULT_SHORTCUTS };
|
||||
},
|
||||
|
||||
setShortcut(action, shortcut) {
|
||||
const shortcuts = this.getShortcuts();
|
||||
const defaults = this.DEFAULT_SHORTCUTS;
|
||||
shortcuts[action] = {
|
||||
...(defaults[action] || {}),
|
||||
...shortcut,
|
||||
};
|
||||
localStorage.setItem(this.STORAGE_KEY, JSON.stringify(shortcuts));
|
||||
},
|
||||
|
||||
resetShortcuts() {
|
||||
localStorage.removeItem(this.STORAGE_KEY);
|
||||
},
|
||||
|
||||
getShortcutForAction(action) {
|
||||
const shortcuts = this.getShortcuts();
|
||||
return shortcuts[action] || this.DEFAULT_SHORTCUTS[action];
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -110,6 +110,15 @@ export class ButterchurnPreset {
|
|||
this.presets = cachedPresets || {};
|
||||
this.presetKeys = cachedPresetKeys || [];
|
||||
|
||||
// Shuffled queue for random mode
|
||||
this.shuffledQueue = [];
|
||||
this.shuffledIndex = 0;
|
||||
|
||||
// Generate shuffled queue if presets are already loaded
|
||||
if (this.presetKeys.length > 0) {
|
||||
this.generateShuffledQueue();
|
||||
}
|
||||
|
||||
// Transition settings
|
||||
this.blendProgress = 0;
|
||||
this.blendDuration = 2.7; // seconds for preset transitions
|
||||
|
|
@ -119,6 +128,7 @@ export class ButterchurnPreset {
|
|||
onButterchurnPresetsLoaded((presets, keys) => {
|
||||
this.presets = presets;
|
||||
this.presetKeys = keys;
|
||||
this.generateShuffledQueue();
|
||||
|
||||
// Notify system that presets are ready (for settings dropdown)
|
||||
window.dispatchEvent(new CustomEvent('butterchurn-presets-loaded'));
|
||||
|
|
@ -131,6 +141,44 @@ export class ButterchurnPreset {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a shuffled queue of preset indices
|
||||
*/
|
||||
generateShuffledQueue() {
|
||||
const indices = this.presetKeys.map((_, i) => i);
|
||||
// Fisher-Yates shuffle
|
||||
for (let i = indices.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[indices[i], indices[j]] = [indices[j], indices[i]];
|
||||
}
|
||||
this.shuffledQueue = indices;
|
||||
this.shuffledIndex = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current preset index based on mode
|
||||
*/
|
||||
getCurrentIndex() {
|
||||
const randomize = visualizerSettings.isButterchurnRandomizeEnabled();
|
||||
if (randomize && this.shuffledQueue.length > 0) {
|
||||
return this.shuffledQueue[this.shuffledIndex];
|
||||
}
|
||||
return this.currentPresetIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the current preset index based on mode
|
||||
*/
|
||||
setCurrentIndex(index) {
|
||||
const randomize = visualizerSettings.isButterchurnRandomizeEnabled();
|
||||
if (randomize && this.shuffledQueue.length > 0) {
|
||||
this.shuffledIndex = this.shuffledQueue.indexOf(index);
|
||||
if (this.shuffledIndex === -1) this.shuffledIndex = 0;
|
||||
} else {
|
||||
this.currentPresetIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preset cycle duration from settings (in milliseconds)
|
||||
*/
|
||||
|
|
@ -218,7 +266,16 @@ export class ButterchurnPreset {
|
|||
const randomize = visualizerSettings.isButterchurnRandomizeEnabled();
|
||||
|
||||
if (randomize) {
|
||||
this.currentPresetIndex = Math.floor(Math.random() * this.presetKeys.length);
|
||||
if (this.shuffledQueue.length === 0) {
|
||||
this.generateShuffledQueue();
|
||||
}
|
||||
this.shuffledIndex = (this.shuffledIndex + 1) % this.shuffledQueue.length;
|
||||
if (this.shuffledIndex === 0) {
|
||||
// Re-shuffle when we've gone through all presets
|
||||
this.generateShuffledQueue();
|
||||
this.shuffledIndex = 0;
|
||||
}
|
||||
this.currentPresetIndex = this.shuffledQueue[this.shuffledIndex];
|
||||
} else {
|
||||
this.currentPresetIndex = (this.currentPresetIndex + 1) % this.presetKeys.length;
|
||||
}
|
||||
|
|
@ -253,7 +310,7 @@ export class ButterchurnPreset {
|
|||
// Update current index if found
|
||||
const index = this.presetKeys.indexOf(presetName);
|
||||
if (index !== -1) {
|
||||
this.currentPresetIndex = index;
|
||||
this.setCurrentIndex(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -269,17 +326,85 @@ export class ButterchurnPreset {
|
|||
* Get current preset name
|
||||
*/
|
||||
getCurrentPresetName() {
|
||||
return this.presetKeys[this.currentPresetIndex] || 'Unknown';
|
||||
const index = this.getCurrentIndex();
|
||||
return this.presetKeys[index] || 'Unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip to next preset (manually triggered)
|
||||
* Uses shuffled queue in random mode, sequential in normal mode
|
||||
*/
|
||||
nextPreset() {
|
||||
this.loadNextPreset();
|
||||
if (!this.visualizer || this.presetKeys.length === 0) return;
|
||||
|
||||
const randomize = visualizerSettings.isButterchurnRandomizeEnabled();
|
||||
|
||||
if (randomize) {
|
||||
this.shuffledIndex = (this.shuffledIndex + 1) % this.shuffledQueue.length;
|
||||
if (this.shuffledIndex === 0) {
|
||||
// Re-shuffle when we've gone through all presets
|
||||
this.generateShuffledQueue();
|
||||
this.shuffledIndex = 0;
|
||||
}
|
||||
this.currentPresetIndex = this.shuffledQueue[this.shuffledIndex];
|
||||
} else {
|
||||
this.currentPresetIndex = (this.currentPresetIndex + 1) % this.presetKeys.length;
|
||||
}
|
||||
|
||||
const presetKey = this.presetKeys[this.currentPresetIndex];
|
||||
const preset = this.presets[presetKey];
|
||||
|
||||
if (preset) {
|
||||
try {
|
||||
this.visualizer.loadPreset(preset, this.blendDuration);
|
||||
} catch (error) {
|
||||
console.warn('[Butterchurn] Failed to load preset:', presetKey, error);
|
||||
if (this.presetKeys.length > 1) {
|
||||
this.nextPreset();
|
||||
}
|
||||
}
|
||||
}
|
||||
this.lastPresetChange = performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip to previous preset (manually triggered)
|
||||
* Uses shuffled queue in random mode, sequential in normal mode
|
||||
*/
|
||||
prevPreset() {
|
||||
if (!this.visualizer || this.presetKeys.length === 0) return;
|
||||
|
||||
const randomize = visualizerSettings.isButterchurnRandomizeEnabled();
|
||||
|
||||
if (randomize) {
|
||||
this.shuffledIndex = (this.shuffledIndex - 1 + this.shuffledQueue.length) % this.shuffledQueue.length;
|
||||
this.currentPresetIndex = this.shuffledQueue[this.shuffledIndex];
|
||||
} else {
|
||||
this.currentPresetIndex = (this.currentPresetIndex - 1 + this.presetKeys.length) % this.presetKeys.length;
|
||||
}
|
||||
|
||||
const presetKey = this.presetKeys[this.currentPresetIndex];
|
||||
const preset = this.presets[presetKey];
|
||||
|
||||
if (preset) {
|
||||
try {
|
||||
this.visualizer.loadPreset(preset, this.blendDuration);
|
||||
} catch (error) {
|
||||
console.warn('[Butterchurn] Failed to load preset:', presetKey, error);
|
||||
}
|
||||
}
|
||||
this.lastPresetChange = performance.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle auto-cycle on/off
|
||||
*/
|
||||
toggleCycle() {
|
||||
const current = visualizerSettings.isButterchurnCycleEnabled();
|
||||
visualizerSettings.setButterchurnCycleEnabled(!current);
|
||||
return !current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize handler
|
||||
*/
|
||||
|
|
|
|||
110
styles.css
110
styles.css
|
|
@ -4635,6 +4635,116 @@ input:checked + .slider::before {
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
.customize-shortcuts-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.shortcut-hint {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.shortcuts-list {
|
||||
/* list scrolls with modal */
|
||||
}
|
||||
|
||||
.customize-shortcut-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.customize-shortcut-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.customize-shortcut-item .shortcut-description {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.customize-shortcut-item .shortcut-key {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.customize-shortcut-item kbd {
|
||||
background: var(--secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
font-family: inherit;
|
||||
min-width: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.customize-shortcut-item kbd.recording {
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
border-color: var(--primary);
|
||||
animation: pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.customize-shortcut-item .shortcut-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
cursor: pointer;
|
||||
padding: 0.25rem;
|
||||
margin-left: 0.5rem;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition);
|
||||
}
|
||||
|
||||
.customize-shortcut-item:hover .shortcut-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.customize-shortcut-item .shortcut-btn:hover {
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.customize-shortcuts-actions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.close-customize-shortcuts {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.close-customize-shortcuts:hover {
|
||||
background: var(--secondary);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
#playlist-detail-description,
|
||||
#mix-detail-description {
|
||||
color: var(--foreground);
|
||||
|
|
|
|||
Loading…
Reference in a new issue