Add CI/CD pipelines, NSIS installer, ES module bundling, and ruff linting
- Add Gitea Actions workflows: test.yml (lint + test on push/PR) and release.yml (build + NSIS installer + upload on v* tags) - Add NSIS installer with optional desktop shortcut and auto-start - Add esbuild bundler: ES module migration with IIFE bundle output - Add build-dist-windows.sh for cross-building Windows distribution - Fix all ruff lint errors (import sorting, unused imports, line length) - Remove redundant scripts (start-server.bat, stop-server.bat, start-server-background.vbs) - Update CLAUDE.md with CI/CD and release documentation
This commit is contained in:
@@ -2,10 +2,21 @@
|
||||
// Player: Tabs, theme, accent, vinyl, visualizer, UI updates
|
||||
// ============================================================
|
||||
|
||||
// Tab management
|
||||
let activeTab = 'player';
|
||||
import {
|
||||
dom, t, formatTime, showToast, resolveMediaSource,
|
||||
SVG_PLAY, SVG_PAUSE, SVG_STOP, SVG_IDLE, SVG_MUTED, SVG_UNMUTED,
|
||||
ws, currentState, setCurrentState, currentDuration, setCurrentDuration,
|
||||
currentPosition, setCurrentPosition, isUserAdjustingVolume,
|
||||
lastStatus, setLastStatus, currentPlayState, setCurrentPlayState,
|
||||
POSITION_INTERPOLATION_MS, seek,
|
||||
} from './core.js';
|
||||
import { updateBackgroundColors } from './background.js';
|
||||
import { loadDisplayMonitors } from './links.js';
|
||||
|
||||
function setMiniPlayerVisible(visible) {
|
||||
// Tab management
|
||||
export let activeTab = 'player';
|
||||
|
||||
export function setMiniPlayerVisible(visible) {
|
||||
const miniPlayer = document.getElementById('mini-player');
|
||||
if (visible) {
|
||||
miniPlayer.classList.remove('hidden');
|
||||
@@ -16,7 +27,7 @@ function setMiniPlayerVisible(visible) {
|
||||
}
|
||||
}
|
||||
|
||||
function updateTabIndicator(btn, animate = true) {
|
||||
export function updateTabIndicator(btn, animate = true) {
|
||||
const indicator = document.getElementById('tabIndicator');
|
||||
if (!indicator || !btn) return;
|
||||
const tabBar = document.getElementById('tabBar');
|
||||
@@ -32,7 +43,7 @@ function updateTabIndicator(btn, animate = true) {
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(tabName) {
|
||||
export function switchTab(tabName) {
|
||||
activeTab = tabName;
|
||||
|
||||
document.querySelectorAll('[data-tab-content]').forEach(el => {
|
||||
@@ -75,12 +86,12 @@ function switchTab(tabName) {
|
||||
}
|
||||
|
||||
// Theme management
|
||||
function initTheme() {
|
||||
export function initTheme() {
|
||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||
setTheme(savedTheme);
|
||||
}
|
||||
|
||||
function setTheme(theme) {
|
||||
export function setTheme(theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
|
||||
@@ -100,17 +111,17 @@ function setTheme(theme) {
|
||||
metaThemeColor.setAttribute('content', theme === 'light' ? '#ffffff' : '#121212');
|
||||
}
|
||||
|
||||
if (typeof updateBackgroundColors === 'function') updateBackgroundColors();
|
||||
updateBackgroundColors();
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
export function toggleTheme() {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
setTheme(newTheme);
|
||||
}
|
||||
|
||||
// Accent color management
|
||||
const accentPresets = [
|
||||
export const accentPresets = [
|
||||
{ name: 'Green', color: '#1db954', hover: '#1ed760' },
|
||||
{ name: 'Blue', color: '#3b82f6', hover: '#60a5fa' },
|
||||
{ name: 'Purple', color: '#8b5cf6', hover: '#a78bfa' },
|
||||
@@ -122,7 +133,7 @@ const accentPresets = [
|
||||
{ name: 'Yellow', color: '#eab308', hover: '#facc15' },
|
||||
];
|
||||
|
||||
function lightenColor(hex, percent) {
|
||||
export function lightenColor(hex, percent) {
|
||||
const num = parseInt(hex.replace('#', ''), 16);
|
||||
const r = Math.min(255, (num >> 16) + Math.round(255 * percent / 100));
|
||||
const g = Math.min(255, ((num >> 8) & 0xff) + Math.round(255 * percent / 100));
|
||||
@@ -130,7 +141,7 @@ function lightenColor(hex, percent) {
|
||||
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||
}
|
||||
|
||||
function initAccentColor() {
|
||||
export function initAccentColor() {
|
||||
const saved = localStorage.getItem('accentColor');
|
||||
if (saved) {
|
||||
const preset = accentPresets.find(p => p.color === saved);
|
||||
@@ -143,16 +154,16 @@ function initAccentColor() {
|
||||
renderAccentSwatches();
|
||||
}
|
||||
|
||||
function applyAccentColor(color, hover) {
|
||||
export function applyAccentColor(color, hover) {
|
||||
document.documentElement.style.setProperty('--accent', color);
|
||||
document.documentElement.style.setProperty('--accent-hover', hover);
|
||||
localStorage.setItem('accentColor', color);
|
||||
const dot = document.getElementById('accentDot');
|
||||
if (dot) dot.style.background = color;
|
||||
if (typeof updateBackgroundColors === 'function') updateBackgroundColors();
|
||||
updateBackgroundColors();
|
||||
}
|
||||
|
||||
function renderAccentSwatches() {
|
||||
export function renderAccentSwatches() {
|
||||
const dropdown = document.getElementById('accentDropdown');
|
||||
if (!dropdown) return;
|
||||
const current = localStorage.getItem('accentColor') || '#1db954';
|
||||
@@ -177,13 +188,13 @@ function renderAccentSwatches() {
|
||||
dropdown.innerHTML = swatches + customRow;
|
||||
}
|
||||
|
||||
function selectAccentColor(color, hover) {
|
||||
export function selectAccentColor(color, hover) {
|
||||
applyAccentColor(color, hover);
|
||||
renderAccentSwatches();
|
||||
document.getElementById('accentDropdown').classList.remove('open');
|
||||
}
|
||||
|
||||
function toggleAccentPicker() {
|
||||
export function toggleAccentPicker() {
|
||||
document.getElementById('accentDropdown').classList.toggle('open');
|
||||
}
|
||||
|
||||
@@ -225,14 +236,14 @@ function restoreVinylAngle() {
|
||||
setInterval(saveVinylAngle, 2000);
|
||||
window.addEventListener('beforeunload', saveVinylAngle);
|
||||
|
||||
function toggleVinylMode() {
|
||||
export function toggleVinylMode() {
|
||||
if (vinylMode) saveVinylAngle();
|
||||
vinylMode = !vinylMode;
|
||||
localStorage.setItem('vinylMode', vinylMode);
|
||||
applyVinylMode();
|
||||
}
|
||||
|
||||
function applyVinylMode() {
|
||||
export function applyVinylMode() {
|
||||
const container = document.querySelector('.album-art-container');
|
||||
const btn = document.getElementById('vinylToggle');
|
||||
if (!container) return;
|
||||
@@ -260,15 +271,16 @@ function updateVinylSpin() {
|
||||
}
|
||||
|
||||
// Audio Visualizer
|
||||
let visualizerEnabled = localStorage.getItem('visualizerEnabled') === 'true';
|
||||
let visualizerAvailable = false;
|
||||
export let visualizerEnabled = localStorage.getItem('visualizerEnabled') === 'true';
|
||||
export let visualizerAvailable = false;
|
||||
let visualizerCtx = null;
|
||||
let visualizerAnimFrame = null;
|
||||
let frequencyData = null;
|
||||
export let frequencyData = null;
|
||||
export function setFrequencyData(value) { frequencyData = value; }
|
||||
let smoothedFrequencies = null;
|
||||
const VISUALIZER_SMOOTHING = 0.15;
|
||||
|
||||
async function checkVisualizerAvailability() {
|
||||
export async function checkVisualizerAvailability() {
|
||||
try {
|
||||
const token = localStorage.getItem('media_server_token');
|
||||
const resp = await fetch('/api/media/visualizer/status', {
|
||||
@@ -285,13 +297,13 @@ async function checkVisualizerAvailability() {
|
||||
if (btn) btn.style.display = visualizerAvailable ? '' : 'none';
|
||||
}
|
||||
|
||||
function toggleVisualizer() {
|
||||
export function toggleVisualizer() {
|
||||
visualizerEnabled = !visualizerEnabled;
|
||||
localStorage.setItem('visualizerEnabled', visualizerEnabled);
|
||||
applyVisualizerMode();
|
||||
}
|
||||
|
||||
function applyVisualizerMode() {
|
||||
export function applyVisualizerMode() {
|
||||
const container = document.querySelector('.album-art-container');
|
||||
const btn = document.getElementById('visualizerToggle');
|
||||
if (!container) return;
|
||||
@@ -333,7 +345,7 @@ function startVisualizerRender() {
|
||||
renderVisualizerFrame();
|
||||
}
|
||||
|
||||
function stopVisualizerRender() {
|
||||
export function stopVisualizerRender() {
|
||||
if (visualizerAnimFrame) {
|
||||
cancelAnimationFrame(visualizerAnimFrame);
|
||||
visualizerAnimFrame = null;
|
||||
@@ -410,7 +422,7 @@ function renderVisualizerFrame() {
|
||||
}
|
||||
|
||||
// Audio device selection
|
||||
async function loadAudioDevices() {
|
||||
export async function loadAudioDevices() {
|
||||
const section = document.getElementById('audioDeviceSection');
|
||||
const select = document.getElementById('audioDeviceSelect');
|
||||
if (!section || !select) return;
|
||||
@@ -478,7 +490,7 @@ function updateAudioDeviceStatus(status) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onAudioDeviceChanged() {
|
||||
export async function onAudioDeviceChanged() {
|
||||
const select = document.getElementById('audioDeviceSelect');
|
||||
if (!select) return;
|
||||
|
||||
@@ -519,7 +531,7 @@ let lastPositionUpdate = 0;
|
||||
let lastPositionValue = 0;
|
||||
let interpolationInterval = null;
|
||||
|
||||
function setupProgressDrag(bar, fill) {
|
||||
export function setupProgressDrag(bar, fill) {
|
||||
let dragging = false;
|
||||
|
||||
function getPercent(clientX) {
|
||||
@@ -571,8 +583,8 @@ function setupProgressDrag(bar, fill) {
|
||||
});
|
||||
}
|
||||
|
||||
function updateUI(status) {
|
||||
lastStatus = status;
|
||||
export function updateUI(status) {
|
||||
setLastStatus(status);
|
||||
|
||||
const fallbackTitle = status.state === 'idle' ? t('player.no_media') : t('player.title_unavailable');
|
||||
dom.trackTitle.textContent = status.title || fallbackTitle;
|
||||
@@ -583,7 +595,7 @@ function updateUI(status) {
|
||||
dom.miniArtist.textContent = status.artist || '';
|
||||
|
||||
const previousState = currentState;
|
||||
currentState = status.state;
|
||||
setCurrentState(status.state);
|
||||
updatePlaybackState(status.state);
|
||||
|
||||
const altText = status.title && status.artist
|
||||
@@ -628,8 +640,8 @@ function updateUI(status) {
|
||||
}
|
||||
|
||||
if (status.duration && status.position !== null) {
|
||||
currentDuration = status.duration;
|
||||
currentPosition = status.position;
|
||||
setCurrentDuration(status.duration);
|
||||
setCurrentPosition(status.position);
|
||||
lastPositionUpdate = Date.now();
|
||||
lastPositionValue = status.position;
|
||||
updateProgress(status.position, status.duration);
|
||||
@@ -661,8 +673,8 @@ function updateUI(status) {
|
||||
}
|
||||
}
|
||||
|
||||
function updatePlaybackState(state) {
|
||||
currentPlayState = state;
|
||||
export function updatePlaybackState(state) {
|
||||
setCurrentPlayState(state);
|
||||
switch(state) {
|
||||
case 'playing':
|
||||
dom.playbackState.textContent = t('state.playing');
|
||||
@@ -715,7 +727,7 @@ function updateProgress(position, duration) {
|
||||
miniBar.setAttribute('aria-valuemax', durRound);
|
||||
}
|
||||
|
||||
function startPositionInterpolation() {
|
||||
export function startPositionInterpolation() {
|
||||
if (interpolationInterval) {
|
||||
clearInterval(interpolationInterval);
|
||||
}
|
||||
@@ -728,7 +740,7 @@ function startPositionInterpolation() {
|
||||
}, POSITION_INTERPOLATION_MS);
|
||||
}
|
||||
|
||||
function stopPositionInterpolation() {
|
||||
export function stopPositionInterpolation() {
|
||||
if (interpolationInterval) {
|
||||
clearInterval(interpolationInterval);
|
||||
interpolationInterval = null;
|
||||
|
||||
Reference in New Issue
Block a user