Add CI/CD pipelines, NSIS installer, ES module bundling, and ruff linting
Lint & Test / test (push) Failing after 9s
Release / create-release (push) Successful in 1s
Release / build-windows (push) Successful in 59s

- 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:
2026-03-23 02:01:28 +03:00
parent be48318212
commit 5439af1955
41 changed files with 1702 additions and 310 deletions
+50 -38
View File
@@ -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;