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:
@@ -0,0 +1,103 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# --- Create Gitea release ---
|
||||||
|
create-release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
release_id: ${{ steps.create.outputs.release_id }}
|
||||||
|
version: ${{ steps.create.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- name: Create Gitea release
|
||||||
|
id: create
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
TAG="${{ gitea.ref_name }}"
|
||||||
|
VERSION="${TAG#v}"
|
||||||
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||||
|
|
||||||
|
IS_PRE="false"
|
||||||
|
if echo "$TAG" | grep -qE '(alpha|beta|rc)'; then
|
||||||
|
IS_PRE="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY_JSON=$(python3 -c "
|
||||||
|
import json, textwrap
|
||||||
|
tag = '$TAG'
|
||||||
|
body = '''## Downloads
|
||||||
|
| Platform | File |
|
||||||
|
|----------|------|
|
||||||
|
| Windows (installer) | \`MediaServer-{tag}-setup.exe\` |
|
||||||
|
| Windows (portable) | \`MediaServer-{tag}-win-x64.zip\` |
|
||||||
|
'''
|
||||||
|
print(json.dumps(textwrap.dedent(body).strip()))
|
||||||
|
")
|
||||||
|
|
||||||
|
RELEASE=$(curl -s -X POST "$BASE_URL/releases" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{
|
||||||
|
\"tag_name\": \"$TAG\",
|
||||||
|
\"name\": \"Media Server $TAG\",
|
||||||
|
\"body\": $BODY_JSON,
|
||||||
|
\"draft\": false,
|
||||||
|
\"prerelease\": $IS_PRE
|
||||||
|
}")
|
||||||
|
|
||||||
|
RELEASE_ID=$(echo "$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
|
||||||
|
echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# --- Build Windows installer + portable ZIP ---
|
||||||
|
build-windows:
|
||||||
|
needs: create-release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: npm ci && npm run build
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install build tools
|
||||||
|
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends nsis zip
|
||||||
|
|
||||||
|
- name: Build Windows distribution
|
||||||
|
run: |
|
||||||
|
chmod +x build-dist-windows.sh
|
||||||
|
./build-dist-windows.sh "${{ gitea.ref_name }}"
|
||||||
|
|
||||||
|
- name: Build NSIS installer
|
||||||
|
run: |
|
||||||
|
VERSION="${{ needs.create-release.outputs.version }}"
|
||||||
|
makensis -DVERSION="${VERSION}" installer.nsi
|
||||||
|
|
||||||
|
- name: Upload assets to release
|
||||||
|
env:
|
||||||
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
|
run: |
|
||||||
|
RELEASE_ID="${{ needs.create-release.outputs.release_id }}"
|
||||||
|
BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}"
|
||||||
|
|
||||||
|
for FILE in build/MediaServer-*.zip build/MediaServer-*-setup.exe; do
|
||||||
|
[ -f "$FILE" ] || continue
|
||||||
|
echo "Uploading $(basename "$FILE")..."
|
||||||
|
curl -s -X POST \
|
||||||
|
"$BASE_URL/releases/$RELEASE_ID/assets?name=$(basename "$FILE")" \
|
||||||
|
-H "Authorization: token $GITEA_TOKEN" \
|
||||||
|
-H "Content-Type: application/octet-stream" \
|
||||||
|
--data-binary "@$FILE"
|
||||||
|
done
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: Lint & Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '20'
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: npm ci && npm run build
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -e ".[dev]"
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: ruff check media_server/
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: pytest --tb=short -q
|
||||||
@@ -49,3 +49,7 @@ Thumbs.db
|
|||||||
|
|
||||||
# Thumbnail cache
|
# Thumbnail cache
|
||||||
.cache/
|
.cache/
|
||||||
|
|
||||||
|
# Node.js / esbuild
|
||||||
|
node_modules/
|
||||||
|
media_server/static/dist/
|
||||||
|
|||||||
@@ -133,6 +133,48 @@ When releasing a new version, update both files with the same version string.
|
|||||||
|
|
||||||
**Important:** After making any changes, always ask the user if the version needs to be incremented.
|
**Important:** After making any changes, always ask the user if the version needs to be incremented.
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
Gitea Actions workflow at `.gitea/workflows/test.yml` runs on every push/PR to `master`:
|
||||||
|
|
||||||
|
1. **Lint** — `ruff check media_server/` (rules: E, F, I, W)
|
||||||
|
2. **Test** — `pytest --tb=short -q`
|
||||||
|
|
||||||
|
Release workflow at `.gitea/workflows/release.yml` triggers on `v*` tags:
|
||||||
|
|
||||||
|
1. **Create release** — Gitea release via REST API (detects pre-release from tag)
|
||||||
|
2. **Build Windows** — cross-builds on Linux using embedded Python + NSIS installer
|
||||||
|
3. **Upload assets** — portable ZIP + installer `.exe` attached to the release
|
||||||
|
|
||||||
|
### Releasing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Stable release
|
||||||
|
git tag v1.0.0 && git push origin v1.0.0
|
||||||
|
|
||||||
|
# Pre-release
|
||||||
|
git tag v1.1.0-alpha.1 && git push origin v1.1.0-alpha.1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Installer
|
||||||
|
|
||||||
|
The NSIS installer (`installer.nsi`) installs to `%LOCALAPPDATA%\Media Server` (no admin required) with optional:
|
||||||
|
- **Desktop shortcut**
|
||||||
|
- **Start with Windows** (Startup folder shortcut, runs hidden via VBS)
|
||||||
|
|
||||||
|
Uninstall preserves `config.yaml` (user data).
|
||||||
|
|
||||||
|
Reference: [gitea-python-ci-cd.md](https://git.dolgolyov-family.by/alexei.dolgolyov/claude-code-facts/src/branch/main/gitea-python-ci-cd.md)
|
||||||
|
|
||||||
|
### Before Pushing
|
||||||
|
|
||||||
|
Ensure CI will pass locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ruff check media_server/
|
||||||
|
pytest --tb=short -q
|
||||||
|
```
|
||||||
|
|
||||||
## Git Rules
|
## Git Rules
|
||||||
|
|
||||||
- **ALWAYS ask for user approval before committing and pushing changes.**
|
- **ALWAYS ask for user approval before committing and pushing changes.**
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Cross-build Windows distribution on Linux
|
||||||
|
# Usage: ./build-dist-windows.sh [VERSION]
|
||||||
|
|
||||||
|
# --- Version detection ---
|
||||||
|
VERSION="${1:-}"
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION=$(git describe --tags --exact-match 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$VERSION" ]; then
|
||||||
|
VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' \
|
||||||
|
media_server/__init__.py 2>/dev/null || echo "0.0.0")
|
||||||
|
fi
|
||||||
|
|
||||||
|
VERSION_CLEAN="${VERSION#v}"
|
||||||
|
echo "Building Media Server v${VERSION_CLEAN} for Windows"
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
PYTHON_VERSION="3.11.9"
|
||||||
|
PYTHON_SHORT="311"
|
||||||
|
DIST_DIR="dist/media-server"
|
||||||
|
WHEEL_DIR="build/win-wheels"
|
||||||
|
SITE_PACKAGES="${DIST_DIR}/python/Lib/site-packages"
|
||||||
|
BUILD_OUTPUT="build/MediaServer-v${VERSION_CLEAN}-win-x64"
|
||||||
|
|
||||||
|
rm -rf dist build
|
||||||
|
mkdir -p "${DIST_DIR}" "${WHEEL_DIR}" "${SITE_PACKAGES}"
|
||||||
|
|
||||||
|
# --- Download embedded Python ---
|
||||||
|
echo "Downloading embedded Python ${PYTHON_VERSION}..."
|
||||||
|
curl -sL "https://www.python.org/ftp/python/${PYTHON_VERSION}/python-${PYTHON_VERSION}-embed-amd64.zip" \
|
||||||
|
-o build/python-embed.zip
|
||||||
|
unzip -qo build/python-embed.zip -d "${DIST_DIR}/python"
|
||||||
|
|
||||||
|
# Patch ._pth to enable site-packages and app source
|
||||||
|
PTH_FILE=$(ls "${DIST_DIR}"/python/python*._pth | head -1)
|
||||||
|
sed -i 's/^#\s*import site/import site/' "$PTH_FILE"
|
||||||
|
echo 'Lib\site-packages' >> "$PTH_FILE"
|
||||||
|
echo '..\..\app' >> "$PTH_FILE"
|
||||||
|
|
||||||
|
# --- Download Windows wheels ---
|
||||||
|
echo "Downloading Windows wheels..."
|
||||||
|
|
||||||
|
# Core dependencies
|
||||||
|
CORE_DEPS=(
|
||||||
|
"fastapi>=0.109.0"
|
||||||
|
"uvicorn[standard]>=0.27.0"
|
||||||
|
"pydantic>=2.0"
|
||||||
|
"pydantic-settings>=2.0"
|
||||||
|
"pyyaml>=6.0"
|
||||||
|
"mutagen>=1.47.0"
|
||||||
|
"pillow>=10.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Windows-specific dependencies
|
||||||
|
WIN_DEPS=(
|
||||||
|
"winsdk>=1.0.0b10"
|
||||||
|
"pywin32>=306"
|
||||||
|
"comtypes>=1.2.0"
|
||||||
|
"pycaw>=20230407"
|
||||||
|
"screen-brightness-control>=0.20.0"
|
||||||
|
"monitorcontrol>=3.0.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Visualizer dependencies
|
||||||
|
VIS_DEPS=(
|
||||||
|
"soundcard>=0.4.0"
|
||||||
|
"numpy>=1.24.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
ALL_DEPS=("${CORE_DEPS[@]}" "${WIN_DEPS[@]}" "${VIS_DEPS[@]}")
|
||||||
|
|
||||||
|
for dep in "${ALL_DEPS[@]}"; do
|
||||||
|
pip download --quiet --dest "$WHEEL_DIR" \
|
||||||
|
--platform win_amd64 --python-version "${PYTHON_SHORT}" \
|
||||||
|
--implementation cp --only-binary :all: \
|
||||||
|
"$dep" 2>/dev/null || \
|
||||||
|
pip download --quiet --dest "$WHEEL_DIR" "$dep"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Install wheels into site-packages
|
||||||
|
echo "Installing wheels..."
|
||||||
|
for whl in "$WHEEL_DIR"/*.whl; do
|
||||||
|
unzip -qo "$whl" -d "$SITE_PACKAGES"
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Size optimization ---
|
||||||
|
echo "Optimizing size..."
|
||||||
|
find "$SITE_PACKAGES" -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find "$SITE_PACKAGES" -type d -name tests -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find "$SITE_PACKAGES" -type d -name "*.dist-info" -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
find "$SITE_PACKAGES" -name "*.pyi" -delete 2>/dev/null || true
|
||||||
|
rm -rf "$SITE_PACKAGES"/{pip,setuptools,pkg_resources}* 2>/dev/null || true
|
||||||
|
|
||||||
|
# Trim numpy if present
|
||||||
|
rm -rf "$SITE_PACKAGES"/numpy/{tests,f2py,typing} 2>/dev/null || true
|
||||||
|
|
||||||
|
# --- Verify frontend bundle ---
|
||||||
|
if [ ! -f "media_server/static/dist/app.bundle.js" ]; then
|
||||||
|
echo "ERROR: Frontend bundle not found. Run 'npm ci && npm run build' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Copy application ---
|
||||||
|
echo "Copying application files..."
|
||||||
|
mkdir -p "${DIST_DIR}/app"
|
||||||
|
cp -r media_server "${DIST_DIR}/app/"
|
||||||
|
|
||||||
|
# Remove source JS (bundle is in dist/)
|
||||||
|
rm -rf "${DIST_DIR}/app/media_server/static/js"
|
||||||
|
# Remove source maps from release
|
||||||
|
rm -f "${DIST_DIR}/app/media_server/static/dist/"*.map
|
||||||
|
|
||||||
|
# Copy config example
|
||||||
|
cp config.example.yaml "${DIST_DIR}/"
|
||||||
|
|
||||||
|
# Copy scripts needed for auto-start
|
||||||
|
mkdir -p "${DIST_DIR}/scripts"
|
||||||
|
cp scripts/start-hidden.vbs "${DIST_DIR}/scripts/"
|
||||||
|
|
||||||
|
# --- Write version ---
|
||||||
|
echo "$VERSION_CLEAN" > "${DIST_DIR}/VERSION"
|
||||||
|
|
||||||
|
# --- Create launcher ---
|
||||||
|
cat > "${DIST_DIR}/media-server.bat" << 'LAUNCHER'
|
||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
set "ROOT=%~dp0"
|
||||||
|
"%ROOT%python\python.exe" -m media_server.main %*
|
||||||
|
LAUNCHER
|
||||||
|
|
||||||
|
# --- Package ---
|
||||||
|
echo "Creating archives..."
|
||||||
|
mkdir -p build
|
||||||
|
|
||||||
|
# Portable ZIP
|
||||||
|
cp -r "${DIST_DIR}" "${BUILD_OUTPUT}"
|
||||||
|
cd build
|
||||||
|
zip -qr "MediaServer-v${VERSION_CLEAN}-win-x64.zip" "MediaServer-v${VERSION_CLEAN}-win-x64"
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
echo "Build complete: build/MediaServer-v${VERSION_CLEAN}-win-x64.zip"
|
||||||
|
echo "Dist directory ready for NSIS: ${DIST_DIR}"
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
import * as esbuild from 'esbuild';
|
||||||
|
|
||||||
|
const srcDir = 'media_server/static';
|
||||||
|
const outDir = `${srcDir}/dist`;
|
||||||
|
|
||||||
|
const watch = process.argv.includes('--watch');
|
||||||
|
|
||||||
|
/** @type {esbuild.BuildOptions} */
|
||||||
|
const jsOpts = {
|
||||||
|
entryPoints: [`${srcDir}/js/app.js`],
|
||||||
|
bundle: true,
|
||||||
|
format: 'iife',
|
||||||
|
outfile: `${outDir}/app.bundle.js`,
|
||||||
|
minify: true,
|
||||||
|
sourcemap: true,
|
||||||
|
target: ['es2020'],
|
||||||
|
logLevel: 'info',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (watch) {
|
||||||
|
const jsCtx = await esbuild.context(jsOpts);
|
||||||
|
await jsCtx.watch();
|
||||||
|
console.log('Watching for changes...');
|
||||||
|
} else {
|
||||||
|
await esbuild.build(jsOpts);
|
||||||
|
}
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
; Media Server NSIS Installer
|
||||||
|
; Cross-compilable: apt install nsis && makensis -DVERSION="1.0.0" installer.nsi
|
||||||
|
|
||||||
|
!include "MUI2.nsh"
|
||||||
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
|
; --- Configuration ---
|
||||||
|
!define APPNAME "Media Server"
|
||||||
|
!define EXENAME "media-server.bat"
|
||||||
|
!define VBSNAME "start-hidden.vbs"
|
||||||
|
!ifndef VERSION
|
||||||
|
!define VERSION "0.0.0"
|
||||||
|
!endif
|
||||||
|
|
||||||
|
Name "${APPNAME} ${VERSION}"
|
||||||
|
OutFile "build\MediaServer-v${VERSION}-setup.exe"
|
||||||
|
InstallDir "$LOCALAPPDATA\${APPNAME}"
|
||||||
|
RequestExecutionLevel user
|
||||||
|
|
||||||
|
; --- UI ---
|
||||||
|
; To use a custom icon, convert icon.svg to icon.ico and uncomment:
|
||||||
|
; !define MUI_ICON "media_server\static\icons\icon.ico"
|
||||||
|
; !define MUI_UNICON "media_server\static\icons\icon.ico"
|
||||||
|
!define MUI_ABORTWARNING
|
||||||
|
|
||||||
|
!insertmacro MUI_PAGE_WELCOME
|
||||||
|
!insertmacro MUI_PAGE_DIRECTORY
|
||||||
|
!insertmacro MUI_PAGE_COMPONENTS
|
||||||
|
!insertmacro MUI_PAGE_INSTFILES
|
||||||
|
!insertmacro MUI_PAGE_FINISH
|
||||||
|
|
||||||
|
!insertmacro MUI_UNPAGE_CONFIRM
|
||||||
|
!insertmacro MUI_UNPAGE_INSTFILES
|
||||||
|
|
||||||
|
!insertmacro MUI_LANGUAGE "English"
|
||||||
|
|
||||||
|
; --- Sections ---
|
||||||
|
Section "!Core (required)" SecCore
|
||||||
|
SectionIn RO
|
||||||
|
|
||||||
|
; Stop running instance if any
|
||||||
|
nsExec::ExecToLog 'taskkill /F /IM python.exe /FI "WINDOWTITLE eq media_server*"'
|
||||||
|
|
||||||
|
SetOutPath "$INSTDIR"
|
||||||
|
|
||||||
|
; Copy entire distribution
|
||||||
|
File /r "dist\media-server\*.*"
|
||||||
|
|
||||||
|
; Create uninstaller
|
||||||
|
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
; Start Menu shortcuts
|
||||||
|
CreateDirectory "$SMPROGRAMS\${APPNAME}"
|
||||||
|
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" \
|
||||||
|
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
|
||||||
|
"$INSTDIR\python\python.exe" 0
|
||||||
|
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME} (Console).lnk" \
|
||||||
|
"$INSTDIR\${EXENAME}" "" \
|
||||||
|
"$INSTDIR\python\python.exe" 0
|
||||||
|
CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" \
|
||||||
|
"$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
; Registry for Add/Remove Programs
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
|
"DisplayName" "${APPNAME}"
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
|
"DisplayVersion" "${VERSION}"
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
|
"UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
|
"InstallLocation" "$INSTDIR"
|
||||||
|
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
|
"Publisher" "Alexei Dolgolyov"
|
||||||
|
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
|
"NoModify" 1
|
||||||
|
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
|
"NoRepair" 1
|
||||||
|
|
||||||
|
; Calculate installed size
|
||||||
|
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||||
|
IntFmt $0 "0x%08X" $0
|
||||||
|
WriteRegDWORD HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}" \
|
||||||
|
"EstimatedSize" "$0"
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "Desktop shortcut" SecDesktop
|
||||||
|
CreateShortcut "$DESKTOP\${APPNAME}.lnk" \
|
||||||
|
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
|
||||||
|
"$INSTDIR\python\python.exe" 0
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
Section "Start with Windows" SecAutostart
|
||||||
|
; Create Startup folder shortcut (runs hidden via VBS)
|
||||||
|
CreateShortcut "$SMSTARTUP\${APPNAME}.lnk" \
|
||||||
|
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
|
||||||
|
"$INSTDIR\python\python.exe" 0
|
||||||
|
SectionEnd
|
||||||
|
|
||||||
|
; --- Section descriptions ---
|
||||||
|
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
|
||||||
|
!insertmacro MUI_DESCRIPTION_TEXT ${SecCore} \
|
||||||
|
"Core application files, embedded Python, and Start Menu shortcuts."
|
||||||
|
!insertmacro MUI_DESCRIPTION_TEXT ${SecDesktop} \
|
||||||
|
"Create a desktop shortcut to launch ${APPNAME}."
|
||||||
|
!insertmacro MUI_DESCRIPTION_TEXT ${SecAutostart} \
|
||||||
|
"Automatically start ${APPNAME} when you log in to Windows."
|
||||||
|
!insertmacro MUI_FUNCTION_DESCRIPTION_END
|
||||||
|
|
||||||
|
; --- Uninstaller ---
|
||||||
|
Section "Uninstall"
|
||||||
|
; Stop running instance
|
||||||
|
nsExec::ExecToLog 'taskkill /F /IM python.exe /FI "WINDOWTITLE eq media_server*"'
|
||||||
|
|
||||||
|
; Remove application files
|
||||||
|
RMDir /r "$INSTDIR\python"
|
||||||
|
RMDir /r "$INSTDIR\app"
|
||||||
|
RMDir /r "$INSTDIR\scripts"
|
||||||
|
Delete "$INSTDIR\${EXENAME}"
|
||||||
|
Delete "$INSTDIR\VERSION"
|
||||||
|
Delete "$INSTDIR\uninstall.exe"
|
||||||
|
|
||||||
|
; Preserve config.yaml (user data) — only remove the example
|
||||||
|
Delete "$INSTDIR\config.example.yaml"
|
||||||
|
|
||||||
|
; Remove shortcuts
|
||||||
|
Delete "$DESKTOP\${APPNAME}.lnk"
|
||||||
|
Delete "$SMSTARTUP\${APPNAME}.lnk"
|
||||||
|
RMDir /r "$SMPROGRAMS\${APPNAME}"
|
||||||
|
|
||||||
|
; Remove registry
|
||||||
|
DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APPNAME}"
|
||||||
|
|
||||||
|
; Remove install dir only if empty (config.yaml may remain)
|
||||||
|
RMDir "$INSTDIR"
|
||||||
|
SectionEnd
|
||||||
@@ -76,7 +76,10 @@ class Settings(BaseSettings):
|
|||||||
# Audio device settings
|
# Audio device settings
|
||||||
audio_device: Optional[str] = Field(
|
audio_device: Optional[str] = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Audio device name to control (None = default device). Use /api/audio/devices to list available devices.",
|
description=(
|
||||||
|
"Audio device name to control (None = default device)."
|
||||||
|
" Use /api/audio/devices to list available devices."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
+13
-4
@@ -15,8 +15,17 @@ from fastapi.staticfiles import StaticFiles
|
|||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .auth import get_token_label, token_label_var
|
from .auth import get_token_label, token_label_var
|
||||||
from .config import settings, generate_default_config, get_config_dir
|
from .config import generate_default_config, get_config_dir, settings
|
||||||
from .routes import audio_router, browser_router, callbacks_router, display_router, health_router, links_router, media_router, scripts_router
|
from .routes import (
|
||||||
|
audio_router,
|
||||||
|
browser_router,
|
||||||
|
callbacks_router,
|
||||||
|
display_router,
|
||||||
|
health_router,
|
||||||
|
links_router,
|
||||||
|
media_router,
|
||||||
|
scripts_router,
|
||||||
|
)
|
||||||
from .services import get_media_controller
|
from .services import get_media_controller
|
||||||
from .services.websocket_manager import ws_manager
|
from .services.websocket_manager import ws_manager
|
||||||
|
|
||||||
@@ -206,12 +215,12 @@ def main():
|
|||||||
if args.generate_config:
|
if args.generate_config:
|
||||||
config_path = generate_default_config()
|
config_path = generate_default_config()
|
||||||
print(f"Configuration file generated at: {config_path}")
|
print(f"Configuration file generated at: {config_path}")
|
||||||
print(f"API Token has been saved to the config file.")
|
print("API Token has been saved to the config file.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if args.show_token:
|
if args.show_token:
|
||||||
print(f"Config directory: {get_config_dir()}")
|
print(f"Config directory: {get_config_dir()}")
|
||||||
print(f"\nAPI Tokens:")
|
print("\nAPI Tokens:")
|
||||||
for label, token in settings.api_tokens.items():
|
for label, token in settings.api_tokens.items():
|
||||||
print(f" {label:20} {token}")
|
print(f" {label:20} {token}")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"""Pydantic models for the media server API."""
|
"""Pydantic models for the media server API."""
|
||||||
|
|
||||||
from .media import (
|
from .media import (
|
||||||
|
MediaInfo,
|
||||||
MediaState,
|
MediaState,
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
VolumeRequest,
|
|
||||||
SeekRequest,
|
SeekRequest,
|
||||||
MediaInfo,
|
VolumeRequest,
|
||||||
)
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
|||||||
@@ -9,4 +9,13 @@ from .links import router as links_router
|
|||||||
from .media import router as media_router
|
from .media import router as media_router
|
||||||
from .scripts import router as scripts_router
|
from .scripts import router as scripts_router
|
||||||
|
|
||||||
__all__ = ["audio_router", "browser_router", "callbacks_router", "display_router", "health_router", "links_router", "media_router", "scripts_router"]
|
__all__ = [
|
||||||
|
"audio_router",
|
||||||
|
"browser_router",
|
||||||
|
"callbacks_router",
|
||||||
|
"display_router",
|
||||||
|
"health_router",
|
||||||
|
"links_router",
|
||||||
|
"media_router",
|
||||||
|
"scripts_router",
|
||||||
|
]
|
||||||
|
|||||||
@@ -4,20 +4,19 @@ import asyncio
|
|||||||
import logging
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
from fastapi import APIRouter, Depends, HTTPException, Query, Response
|
||||||
from fastapi.responses import FileResponse, StreamingResponse
|
from fastapi.responses import FileResponse
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from ..auth import verify_token, verify_token_or_query
|
from ..auth import verify_token, verify_token_or_query
|
||||||
from ..config import MediaFolderConfig, settings
|
from ..config import MediaFolderConfig, settings
|
||||||
from ..config_manager import config_manager
|
from ..config_manager import config_manager
|
||||||
|
from ..services import get_media_controller
|
||||||
from ..services.browser_service import BrowserService
|
from ..services.browser_service import BrowserService
|
||||||
from ..services.metadata_service import MetadataService
|
from ..services.metadata_service import MetadataService
|
||||||
from ..services.thumbnail_service import ThumbnailService
|
from ..services.thumbnail_service import ThumbnailService
|
||||||
from ..services import get_media_controller
|
|
||||||
from ..services.websocket_manager import ws_manager
|
from ..services.websocket_manager import ws_manager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -281,7 +280,7 @@ async def browse(
|
|||||||
logger.warning(f"Folder temporarily unavailable: {e}")
|
logger.warning(f"Folder temporarily unavailable: {e}")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=503,
|
status_code=503,
|
||||||
detail=f"Folder is temporarily unavailable. It may be a network share that is not accessible at the moment."
|
detail="Folder is temporarily unavailable. It may be a network share that is not accessible at the moment."
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error browsing directory (type: {type(e).__name__}): {e}")
|
logger.error(f"Error browsing directory (type: {type(e).__name__}): {e}")
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
@@ -238,7 +237,10 @@ async def create_callback(
|
|||||||
if callback_name in settings.callbacks:
|
if callback_name in settings.callbacks:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
detail=f"Callback '{callback_name}' already exists. Use PUT /api/callbacks/update/{callback_name} to update it.",
|
detail=(
|
||||||
|
f"Callback '{callback_name}' already exists."
|
||||||
|
f" Use PUT /api/callbacks/update/{callback_name} to update it."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create callback config
|
# Create callback config
|
||||||
@@ -283,7 +285,10 @@ async def update_callback(
|
|||||||
if callback_name not in settings.callbacks:
|
if callback_name not in settings.callbacks:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
detail=f"Callback '{callback_name}' not found. Use POST /api/callbacks/create/{callback_name} to create it.",
|
detail=(
|
||||||
|
f"Callback '{callback_name}' not found."
|
||||||
|
f" Use POST /api/callbacks/create/{callback_name} to create it."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create updated callback config
|
# Create updated callback config
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ from pydantic import BaseModel, Field
|
|||||||
|
|
||||||
from ..auth import verify_token
|
from ..auth import verify_token
|
||||||
from ..services.display_service import (
|
from ..services.display_service import (
|
||||||
get_brightness,
|
|
||||||
list_monitors,
|
list_monitors,
|
||||||
set_brightness,
|
set_brightness,
|
||||||
set_power,
|
set_power,
|
||||||
|
|||||||
@@ -3,14 +3,13 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
|
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect, status
|
||||||
from fastapi import status
|
|
||||||
from fastapi.responses import Response
|
from fastapi.responses import Response
|
||||||
|
|
||||||
from ..auth import verify_token, verify_token_or_query
|
from ..auth import verify_token, verify_token_or_query
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
from ..models import MediaStatus, VolumeRequest, SeekRequest
|
from ..models import MediaStatus, SeekRequest, VolumeRequest
|
||||||
from ..services import get_media_controller, get_current_album_art
|
from ..services import get_current_album_art, get_media_controller
|
||||||
from ..services.websocket_manager import ws_manager
|
from ..services.websocket_manager import ws_manager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|||||||
@@ -13,15 +13,12 @@ Usage:
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import socket
|
|
||||||
import logging
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import win32serviceutil
|
|
||||||
import win32service
|
|
||||||
import win32event
|
|
||||||
import servicemanager
|
import servicemanager
|
||||||
import win32api
|
import win32event
|
||||||
|
import win32service
|
||||||
|
import win32serviceutil
|
||||||
|
|
||||||
WIN32_AVAILABLE = True
|
WIN32_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -64,8 +61,9 @@ class MediaServerService:
|
|||||||
def main(self):
|
def main(self):
|
||||||
"""Main service loop."""
|
"""Main service loop."""
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from media_server.main import app
|
|
||||||
from media_server.config import settings
|
from media_server.config import settings
|
||||||
|
from media_server.main import app
|
||||||
|
|
||||||
config = uvicorn.Config(
|
config = uvicorn.Config(
|
||||||
app,
|
app,
|
||||||
@@ -95,10 +93,9 @@ def install_service():
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Get the path to the Python executable
|
# Get the path to the Python executable
|
||||||
python_exe = sys.executable
|
|
||||||
|
|
||||||
# Get the path to this module
|
# Get the path to this module
|
||||||
module_path = os.path.abspath(__file__)
|
os.path.abspath(__file__)
|
||||||
|
|
||||||
win32serviceutil.InstallService(
|
win32serviceutil.InstallService(
|
||||||
MediaServerService._svc_name_,
|
MediaServerService._svc_name_,
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ def get_media_controller() -> "MediaController":
|
|||||||
system = platform.system()
|
system = platform.system()
|
||||||
|
|
||||||
if system == "Windows":
|
if system == "Windows":
|
||||||
from .windows_media import WindowsMediaController
|
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
|
from .windows_media import WindowsMediaController
|
||||||
|
|
||||||
_controller_instance = WindowsMediaController(audio_device=settings.audio_device)
|
_controller_instance = WindowsMediaController(audio_device=settings.audio_device)
|
||||||
elif system == "Linux":
|
elif system == "Linux":
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ Installation:
|
|||||||
4. Grant necessary permissions to Termux:API
|
4. Grant necessary permissions to Termux:API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from ..models import MediaState, MediaStatus
|
from ..models import MediaState, MediaStatus
|
||||||
from .media_controller import MediaController
|
from .media_controller import MediaController
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
"""Browser service for media file browsing and path validation."""
|
"""Browser service for media file browsing and path validation."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import stat as stat_module
|
import stat as stat_module
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from ..config import settings
|
from ..config import settings
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import logging
|
|||||||
import platform
|
import platform
|
||||||
import struct
|
import struct
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Optional, Any
|
from typing import Any, Optional
|
||||||
|
|
||||||
from ..models import MediaState, MediaStatus
|
from ..models import MediaState, MediaStatus
|
||||||
from .media_controller import MediaController
|
from .media_controller import MediaController
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
import json
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from ..models import MediaState, MediaStatus
|
from ..models import MediaState, MediaStatus
|
||||||
@@ -203,11 +202,6 @@ class MacOSMediaController(MediaController):
|
|||||||
async def play(self) -> bool:
|
async def play(self) -> bool:
|
||||||
"""Resume playback using media key simulation."""
|
"""Resume playback using media key simulation."""
|
||||||
# Use system media key
|
# Use system media key
|
||||||
script = '''
|
|
||||||
tell application "System Events"
|
|
||||||
key code 16 using {command down, option down}
|
|
||||||
end tell
|
|
||||||
'''
|
|
||||||
# Fallback: try specific app
|
# Fallback: try specific app
|
||||||
active_app = self._get_active_app()
|
active_app = self._get_active_app()
|
||||||
if active_app == "Spotify":
|
if active_app == "Spotify":
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -21,7 +20,6 @@ class MetadataService:
|
|||||||
Dictionary with audio metadata.
|
Dictionary with audio metadata.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import mutagen
|
|
||||||
from mutagen import File as MutagenFile
|
from mutagen import File as MutagenFile
|
||||||
|
|
||||||
audio = MutagenFile(str(file_path), easy=True)
|
audio = MutagenFile(str(file_path), easy=True)
|
||||||
@@ -68,7 +66,9 @@ class MetadataService:
|
|||||||
metadata["album"] = tags["album"][0] if isinstance(tags["album"], list) else tags["album"]
|
metadata["album"] = tags["album"][0] if isinstance(tags["album"], list) else tags["album"]
|
||||||
|
|
||||||
if "albumartist" in tags:
|
if "albumartist" in tags:
|
||||||
metadata["album_artist"] = tags["albumartist"][0] if isinstance(tags["albumartist"], list) else tags["albumartist"]
|
metadata["album_artist"] = (
|
||||||
|
tags["albumartist"][0] if isinstance(tags["albumartist"], list) else tags["albumartist"]
|
||||||
|
)
|
||||||
|
|
||||||
if "date" in tags:
|
if "date" in tags:
|
||||||
metadata["date"] = tags["date"][0] if isinstance(tags["date"], list) else tags["date"]
|
metadata["date"] = tags["date"][0] if isinstance(tags["date"], list) else tags["date"]
|
||||||
@@ -77,7 +77,9 @@ class MetadataService:
|
|||||||
metadata["genre"] = tags["genre"][0] if isinstance(tags["genre"], list) else tags["genre"]
|
metadata["genre"] = tags["genre"][0] if isinstance(tags["genre"], list) else tags["genre"]
|
||||||
|
|
||||||
if "tracknumber" in tags:
|
if "tracknumber" in tags:
|
||||||
metadata["track_number"] = tags["tracknumber"][0] if isinstance(tags["tracknumber"], list) else tags["tracknumber"]
|
metadata["track_number"] = (
|
||||||
|
tags["tracknumber"][0] if isinstance(tags["tracknumber"], list) else tags["tracknumber"]
|
||||||
|
)
|
||||||
|
|
||||||
# If no title tag, use filename
|
# If no title tag, use filename
|
||||||
if "title" not in metadata:
|
if "title" not in metadata:
|
||||||
@@ -110,7 +112,6 @@ class MetadataService:
|
|||||||
Dictionary with video metadata.
|
Dictionary with video metadata.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import mutagen
|
|
||||||
from mutagen import File as MutagenFile
|
from mutagen import File as MutagenFile
|
||||||
|
|
||||||
video = MutagenFile(str(file_path))
|
video = MutagenFile(str(file_path))
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
@@ -151,10 +149,10 @@ class ThumbnailService:
|
|||||||
Thumbnail bytes (JPEG) or None if no album art.
|
Thumbnail bytes (JPEG) or None if no album art.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
import mutagen
|
from io import BytesIO
|
||||||
|
|
||||||
from mutagen import File as MutagenFile
|
from mutagen import File as MutagenFile
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from io import BytesIO
|
|
||||||
|
|
||||||
audio = MutagenFile(str(file_path))
|
audio = MutagenFile(str(file_path))
|
||||||
if audio is None:
|
if audio is None:
|
||||||
@@ -232,9 +230,10 @@ class ThumbnailService:
|
|||||||
Thumbnail bytes (JPEG) or None if ffmpeg not available.
|
Thumbnail bytes (JPEG) or None if ffmpeg not available.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
# Check if ffmpeg is available
|
# Check if ffmpeg is available
|
||||||
if not shutil.which("ffmpeg"):
|
if not shutil.which("ffmpeg"):
|
||||||
logger.debug("ffmpeg not available, cannot generate video thumbnail")
|
logger.debug("ffmpeg not available, cannot generate video thumbnail")
|
||||||
@@ -247,7 +246,11 @@ class ThumbnailService:
|
|||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-i", str(file_path),
|
"-i", str(file_path),
|
||||||
"-vf", f"thumbnail,scale={target_size[0]}:{target_size[1]}:force_original_aspect_ratio=increase,crop={target_size[0]}:{target_size[1]}",
|
"-vf", (
|
||||||
|
f"thumbnail,scale={target_size[0]}:{target_size[1]}"
|
||||||
|
f":force_original_aspect_ratio=increase"
|
||||||
|
f",crop={target_size[0]}:{target_size[1]}"
|
||||||
|
),
|
||||||
"-frames:v", "1",
|
"-frames:v", "1",
|
||||||
"-f", "image2pipe",
|
"-f", "image2pipe",
|
||||||
"-vcodec", "mjpeg",
|
"-vcodec", "mjpeg",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import logging
|
|||||||
import threading
|
import threading
|
||||||
import time as _time
|
import time as _time
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
from typing import Optional, Any
|
from typing import Any
|
||||||
|
|
||||||
from ..models import MediaState, MediaStatus
|
from ..models import MediaState, MediaStatus
|
||||||
from .media_controller import MediaController
|
from .media_controller import MediaController
|
||||||
@@ -47,6 +47,8 @@ def get_current_album_art() -> bytes | None:
|
|||||||
try:
|
try:
|
||||||
from winsdk.windows.media.control import (
|
from winsdk.windows.media.control import (
|
||||||
GlobalSystemMediaTransportControlsSessionManager as MediaManager,
|
GlobalSystemMediaTransportControlsSessionManager as MediaManager,
|
||||||
|
)
|
||||||
|
from winsdk.windows.media.control import (
|
||||||
GlobalSystemMediaTransportControlsSessionPlaybackStatus as PlaybackStatus,
|
GlobalSystemMediaTransportControlsSessionPlaybackStatus as PlaybackStatus,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,11 +63,11 @@ _volume_control = None
|
|||||||
_configured_device_name: str | None = None
|
_configured_device_name: str | None = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from ctypes import cast, POINTER
|
|
||||||
from comtypes import CLSCTX_ALL, CoInitialize, CoUninitialize
|
|
||||||
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
|
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
|
from ctypes import POINTER, cast
|
||||||
|
|
||||||
|
from comtypes import CLSCTX_ALL
|
||||||
|
from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume
|
||||||
# Suppress pycaw warnings about missing device properties
|
# Suppress pycaw warnings about missing device properties
|
||||||
warnings.filterwarnings("ignore", category=UserWarning, module="pycaw")
|
warnings.filterwarnings("ignore", category=UserWarning, module="pycaw")
|
||||||
|
|
||||||
@@ -240,13 +242,18 @@ def _sync_get_media_status() -> dict[str, Any]:
|
|||||||
_track_skip_pending["stale_pos"] = -999 # Reset stale position tracking
|
_track_skip_pending["stale_pos"] = -999 # Reset stale position tracking
|
||||||
skip_just_completed = True
|
skip_just_completed = True
|
||||||
# Reset position cache for new track
|
# Reset position cache for new track
|
||||||
new_track_id = f"{current_title}:{result.get('artist', '')}:{result.get('duration', 0)}"
|
new_track_id = (
|
||||||
|
f"{current_title}:{result.get('artist', '')}:{result.get('duration', 0)}"
|
||||||
|
)
|
||||||
_position_cache["track_id"] = new_track_id
|
_position_cache["track_id"] = new_track_id
|
||||||
_position_cache["base_position"] = 0.0
|
_position_cache["base_position"] = 0.0
|
||||||
_position_cache["base_time"] = current_time
|
_position_cache["base_time"] = current_time
|
||||||
_position_cache["last_smtc_pos"] = -999 # Force fresh start
|
_position_cache["last_smtc_pos"] = -999 # Force fresh start
|
||||||
_position_cache["is_playing"] = is_playing
|
_position_cache["is_playing"] = is_playing
|
||||||
logger.debug(f"Track skip complete, new title: {current_title}, grace until: {_track_skip_pending['grace_until']}")
|
logger.debug(
|
||||||
|
f"Track skip complete, new title: {current_title},"
|
||||||
|
f" grace until: {_track_skip_pending['grace_until']}"
|
||||||
|
)
|
||||||
elif current_time - _track_skip_pending["skip_time"] > 5.0:
|
elif current_time - _track_skip_pending["skip_time"] > 5.0:
|
||||||
# Timeout after 5 seconds
|
# Timeout after 5 seconds
|
||||||
_track_skip_pending["active"] = False
|
_track_skip_pending["active"] = False
|
||||||
@@ -298,7 +305,10 @@ def _sync_get_media_status() -> dict[str, Any]:
|
|||||||
pos = smtc_pos
|
pos = smtc_pos
|
||||||
_track_skip_pending["grace_until"] = 0
|
_track_skip_pending["grace_until"] = 0
|
||||||
_track_skip_pending["stale_pos"] = -999
|
_track_skip_pending["stale_pos"] = -999
|
||||||
logger.debug(f"Grace period: accepting SMTC pos {smtc_pos} (low={smtc_pos < 10}, changed={smtc_changed})")
|
logger.debug(
|
||||||
|
f"Grace period: accepting SMTC pos {smtc_pos}"
|
||||||
|
f" (low={smtc_pos < 10}, changed={smtc_changed})"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# SMTC is stale - keep interpolating
|
# SMTC is stale - keep interpolating
|
||||||
pos = interpolated_pos
|
pos = interpolated_pos
|
||||||
@@ -307,7 +317,10 @@ def _sync_get_media_status() -> dict[str, Any]:
|
|||||||
_track_skip_pending["stale_pos"] = smtc_pos
|
_track_skip_pending["stale_pos"] = smtc_pos
|
||||||
# Keep grace period active indefinitely while SMTC is stale
|
# Keep grace period active indefinitely while SMTC is stale
|
||||||
_track_skip_pending["grace_until"] = current_time + 300.0
|
_track_skip_pending["grace_until"] = current_time + 300.0
|
||||||
logger.debug(f"Grace period: SMTC stale ({smtc_pos}), using interpolated {interpolated_pos}")
|
logger.debug(
|
||||||
|
f"Grace period: SMTC stale ({smtc_pos}),"
|
||||||
|
f" using interpolated {interpolated_pos}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# Normal position tracking
|
# Normal position tracking
|
||||||
# Create track ID from title + artist + duration
|
# Create track ID from title + artist + duration
|
||||||
@@ -335,7 +348,9 @@ def _sync_get_media_status() -> dict[str, Any]:
|
|||||||
|
|
||||||
# Update playing state
|
# Update playing state
|
||||||
if _position_cache.get("is_playing") != is_playing:
|
if _position_cache.get("is_playing") != is_playing:
|
||||||
_position_cache["base_position"] = pos if is_playing else _position_cache.get("base_position", smtc_pos)
|
_position_cache["base_position"] = (
|
||||||
|
pos if is_playing else _position_cache.get("base_position", smtc_pos)
|
||||||
|
)
|
||||||
_position_cache["base_time"] = current_time
|
_position_cache["base_time"] = current_time
|
||||||
_position_cache["is_playing"] = is_playing
|
_position_cache["is_playing"] = is_playing
|
||||||
|
|
||||||
|
|||||||
@@ -651,14 +651,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="/static/js/core.js"></script>
|
<script src="/static/dist/app.bundle.js"></script>
|
||||||
<script src="/static/js/player.js"></script>
|
|
||||||
<script src="/static/js/websocket.js"></script>
|
|
||||||
<script src="/static/js/scripts.js"></script>
|
|
||||||
<script src="/static/js/callbacks.js"></script>
|
|
||||||
<script src="/static/js/browser.js"></script>
|
|
||||||
<script src="/static/js/links.js"></script>
|
|
||||||
<script src="/static/js/background.js"></script>
|
|
||||||
<script src="/static/js/main.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,131 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Main: Initialization orchestrator (loaded last)
|
// App: Entry point — imports all modules, registers window globals,
|
||||||
|
// and orchestrates initialization (replaces main.js)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
// Layer 0: Core state & utilities
|
||||||
|
import {
|
||||||
|
cacheDom, dom, registerUpdateCallbacks,
|
||||||
|
initLocale, fetchVersion, formatTime, setupIconPreview,
|
||||||
|
isUserAdjustingVolume, setIsUserAdjustingVolume,
|
||||||
|
volumeUpdateTimer, setVolumeUpdateTimer,
|
||||||
|
currentDuration, currentPosition, setVolume, seek,
|
||||||
|
togglePlayPause, nextTrack, previousTrack, toggleMute,
|
||||||
|
VOLUME_THROTTLE_MS, VOLUME_RELEASE_DELAY_MS,
|
||||||
|
changeLocale, t,
|
||||||
|
} from './core.js';
|
||||||
|
|
||||||
|
// Layer 1: Player (tabs, theme, accent, vinyl, visualizer, UI)
|
||||||
|
import {
|
||||||
|
activeTab, switchTab, updateTabIndicator, setMiniPlayerVisible,
|
||||||
|
initTheme, toggleTheme, initAccentColor, applyAccentColor,
|
||||||
|
renderAccentSwatches, selectAccentColor, toggleAccentPicker, lightenColor,
|
||||||
|
toggleVinylMode, applyVinylMode,
|
||||||
|
visualizerEnabled, visualizerAvailable,
|
||||||
|
checkVisualizerAvailability, toggleVisualizer, applyVisualizerMode,
|
||||||
|
loadAudioDevices, onAudioDeviceChanged,
|
||||||
|
setupProgressDrag, updateUI, updatePlaybackState, stopPositionInterpolation,
|
||||||
|
} from './player.js';
|
||||||
|
|
||||||
|
// Layer 2: WebSocket
|
||||||
|
import {
|
||||||
|
connectWebSocket, showAuthForm, authenticate, clearToken,
|
||||||
|
manualReconnect, updateConnectionStatus,
|
||||||
|
} from './websocket.js';
|
||||||
|
|
||||||
|
// Layer 3: Features
|
||||||
|
import {
|
||||||
|
loadScripts, loadScriptsTable, displayQuickAccess,
|
||||||
|
showAddScriptDialog, showEditScriptDialog, closeScriptDialog, saveScript,
|
||||||
|
deleteScriptConfirm, executeScriptDebug, executeCallbackDebug,
|
||||||
|
closeExecutionDialog, scriptFormDirty, setScriptFormDirty,
|
||||||
|
} from './scripts.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
loadCallbacksTable,
|
||||||
|
showAddCallbackDialog, showEditCallbackDialog, closeCallbackDialog,
|
||||||
|
saveCallback, deleteCallbackConfirm,
|
||||||
|
callbackFormDirty, setCallbackFormDirty,
|
||||||
|
} from './callbacks.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
loadMediaFolders, initBrowserToolbar, thumbnailCache,
|
||||||
|
setViewMode, refreshBrowser, playAllFolder,
|
||||||
|
previousPage, nextPage, goToPage,
|
||||||
|
onBrowserSearch, clearBrowserSearch, onItemsPerPageChanged,
|
||||||
|
downloadFile, closeFolderDialog, saveFolder,
|
||||||
|
showManageFoldersDialog,
|
||||||
|
} from './browser.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
loadDisplayMonitors, onDisplayBrightnessInput, onDisplayBrightnessChange,
|
||||||
|
toggleDisplayPower, loadHeaderLinks, loadLinksTable,
|
||||||
|
showAddLinkDialog, showEditLinkDialog, closeLinkDialog, saveLink, deleteLinkConfirm,
|
||||||
|
linkFormDirty, setLinkFormDirty,
|
||||||
|
} from './links.js';
|
||||||
|
|
||||||
|
import {
|
||||||
|
toggleDynamicBackground, applyDynamicBackground, updateBackgroundColors,
|
||||||
|
} from './background.js';
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Register late-bound callbacks for core's updateAllText()
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
registerUpdateCallbacks({
|
||||||
|
updatePlaybackState,
|
||||||
|
updateConnectionStatus,
|
||||||
|
loadScriptsTable,
|
||||||
|
loadCallbacksTable,
|
||||||
|
loadLinksTable,
|
||||||
|
displayQuickAccess,
|
||||||
|
renderAccentSwatches,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Register all functions on window for HTML onclick handlers
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
Object.assign(window, {
|
||||||
|
// Player controls
|
||||||
|
togglePlayPause, nextTrack, previousTrack, toggleMute, seek,
|
||||||
|
// Tabs
|
||||||
|
switchTab,
|
||||||
|
// Theme & accent
|
||||||
|
toggleTheme, toggleAccentPicker, selectAccentColor, lightenColor,
|
||||||
|
// Vinyl & visualizer
|
||||||
|
toggleVinylMode, toggleVisualizer,
|
||||||
|
// Background
|
||||||
|
toggleDynamicBackground,
|
||||||
|
// Auth
|
||||||
|
authenticate, clearToken, manualReconnect,
|
||||||
|
// Locale
|
||||||
|
changeLocale,
|
||||||
|
// Scripts
|
||||||
|
showAddScriptDialog, showEditScriptDialog, closeScriptDialog, saveScript,
|
||||||
|
deleteScriptConfirm, executeScriptDebug, executeCallbackDebug,
|
||||||
|
closeExecutionDialog,
|
||||||
|
// Callbacks
|
||||||
|
showAddCallbackDialog, showEditCallbackDialog, closeCallbackDialog,
|
||||||
|
saveCallback, deleteCallbackConfirm,
|
||||||
|
// Browser
|
||||||
|
setViewMode, refreshBrowser, playAllFolder,
|
||||||
|
previousPage, nextPage, goToPage,
|
||||||
|
onBrowserSearch, clearBrowserSearch, onItemsPerPageChanged,
|
||||||
|
downloadFile, closeFolderDialog, saveFolder,
|
||||||
|
showManageFoldersDialog,
|
||||||
|
// Links
|
||||||
|
showAddLinkDialog, showEditLinkDialog, closeLinkDialog,
|
||||||
|
saveLink, deleteLinkConfirm,
|
||||||
|
// Display
|
||||||
|
loadDisplayMonitors, onDisplayBrightnessInput, onDisplayBrightnessChange,
|
||||||
|
toggleDisplayPower,
|
||||||
|
// Audio device
|
||||||
|
onAudioDeviceChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Initialization (DOMContentLoaded)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
window.addEventListener('DOMContentLoaded', async () => {
|
window.addEventListener('DOMContentLoaded', async () => {
|
||||||
@@ -50,7 +176,7 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
function setupVolumeSlider(sliderId) {
|
function setupVolumeSlider(sliderId) {
|
||||||
const slider = document.getElementById(sliderId);
|
const slider = document.getElementById(sliderId);
|
||||||
slider.addEventListener('input', (e) => {
|
slider.addEventListener('input', (e) => {
|
||||||
isUserAdjustingVolume = true;
|
setIsUserAdjustingVolume(true);
|
||||||
const volume = parseInt(e.target.value);
|
const volume = parseInt(e.target.value);
|
||||||
// Sync both sliders and displays
|
// Sync both sliders and displays
|
||||||
dom.volumeDisplay.textContent = `${volume}%`;
|
dom.volumeDisplay.textContent = `${volume}%`;
|
||||||
@@ -59,20 +185,20 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
dom.miniVolumeSlider.value = volume;
|
dom.miniVolumeSlider.value = volume;
|
||||||
|
|
||||||
if (volumeUpdateTimer) clearTimeout(volumeUpdateTimer);
|
if (volumeUpdateTimer) clearTimeout(volumeUpdateTimer);
|
||||||
volumeUpdateTimer = setTimeout(() => {
|
setVolumeUpdateTimer(setTimeout(() => {
|
||||||
setVolume(volume);
|
setVolume(volume);
|
||||||
volumeUpdateTimer = null;
|
setVolumeUpdateTimer(null);
|
||||||
}, VOLUME_THROTTLE_MS);
|
}, VOLUME_THROTTLE_MS));
|
||||||
});
|
});
|
||||||
|
|
||||||
slider.addEventListener('change', (e) => {
|
slider.addEventListener('change', (e) => {
|
||||||
if (volumeUpdateTimer) {
|
if (volumeUpdateTimer) {
|
||||||
clearTimeout(volumeUpdateTimer);
|
clearTimeout(volumeUpdateTimer);
|
||||||
volumeUpdateTimer = null;
|
setVolumeUpdateTimer(null);
|
||||||
}
|
}
|
||||||
const volume = parseInt(e.target.value);
|
const volume = parseInt(e.target.value);
|
||||||
setVolume(volume);
|
setVolume(volume);
|
||||||
setTimeout(() => { isUserAdjustingVolume = false; }, VOLUME_RELEASE_DELAY_MS);
|
setTimeout(() => { setIsUserAdjustingVolume(false); }, VOLUME_RELEASE_DELAY_MS);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,25 +250,24 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Script form dirty state tracking
|
// Script form dirty state tracking
|
||||||
const scriptForm = document.getElementById('scriptForm');
|
const scriptForm = document.getElementById('scriptForm');
|
||||||
scriptForm.addEventListener('input', () => {
|
scriptForm.addEventListener('input', () => {
|
||||||
scriptFormDirty = true;
|
setScriptFormDirty(true);
|
||||||
});
|
});
|
||||||
scriptForm.addEventListener('change', () => {
|
scriptForm.addEventListener('change', () => {
|
||||||
scriptFormDirty = true;
|
setScriptFormDirty(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Callback form dirty state tracking
|
// Callback form dirty state tracking
|
||||||
const callbackForm = document.getElementById('callbackForm');
|
const callbackForm = document.getElementById('callbackForm');
|
||||||
callbackForm.addEventListener('input', () => {
|
callbackForm.addEventListener('input', () => {
|
||||||
callbackFormDirty = true;
|
setCallbackFormDirty(true);
|
||||||
});
|
});
|
||||||
callbackForm.addEventListener('change', () => {
|
callbackForm.addEventListener('change', () => {
|
||||||
callbackFormDirty = true;
|
setCallbackFormDirty(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Script dialog backdrop click to close
|
// Script dialog backdrop click to close
|
||||||
const scriptDialog = document.getElementById('scriptDialog');
|
const scriptDialog = document.getElementById('scriptDialog');
|
||||||
scriptDialog.addEventListener('click', (e) => {
|
scriptDialog.addEventListener('click', (e) => {
|
||||||
// Check if click is on the backdrop (not the dialog content)
|
|
||||||
if (e.target === scriptDialog) {
|
if (e.target === scriptDialog) {
|
||||||
closeScriptDialog();
|
closeScriptDialog();
|
||||||
}
|
}
|
||||||
@@ -151,7 +276,6 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Callback dialog backdrop click to close
|
// Callback dialog backdrop click to close
|
||||||
const callbackDialog = document.getElementById('callbackDialog');
|
const callbackDialog = document.getElementById('callbackDialog');
|
||||||
callbackDialog.addEventListener('click', (e) => {
|
callbackDialog.addEventListener('click', (e) => {
|
||||||
// Check if click is on the backdrop (not the dialog content)
|
|
||||||
if (e.target === callbackDialog) {
|
if (e.target === callbackDialog) {
|
||||||
closeCallbackDialog();
|
closeCallbackDialog();
|
||||||
}
|
}
|
||||||
@@ -200,10 +324,10 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
// Track link form dirty state
|
// Track link form dirty state
|
||||||
const linkForm = document.getElementById('linkForm');
|
const linkForm = document.getElementById('linkForm');
|
||||||
linkForm.addEventListener('input', () => {
|
linkForm.addEventListener('input', () => {
|
||||||
linkFormDirty = true;
|
setLinkFormDirty(true);
|
||||||
});
|
});
|
||||||
linkForm.addEventListener('change', () => {
|
linkForm.addEventListener('change', () => {
|
||||||
linkFormDirty = true;
|
setLinkFormDirty(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize browser toolbar and load folders
|
// Initialize browser toolbar and load folders
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
// Background: WebGL shader-based dynamic background
|
// Background: WebGL shader-based dynamic background
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
import { frequencyData } from './player.js';
|
||||||
|
|
||||||
let bgCanvas = null;
|
let bgCanvas = null;
|
||||||
let bgGL = null;
|
let bgGL = null;
|
||||||
let bgProgram = null;
|
let bgProgram = null;
|
||||||
@@ -216,7 +218,7 @@ function resizeBackgroundCanvas() {
|
|||||||
|
|
||||||
// ---- Cached color/theme updates (called on accent or theme change, not per-frame) ----
|
// ---- Cached color/theme updates (called on accent or theme change, not per-frame) ----
|
||||||
|
|
||||||
function updateBackgroundColors() {
|
export function updateBackgroundColors() {
|
||||||
const style = getComputedStyle(document.documentElement);
|
const style = getComputedStyle(document.documentElement);
|
||||||
const accentHex = style.getPropertyValue('--accent').trim();
|
const accentHex = style.getPropertyValue('--accent').trim();
|
||||||
if (accentHex && accentHex.length >= 7) {
|
if (accentHex && accentHex.length >= 7) {
|
||||||
@@ -245,8 +247,8 @@ function renderBackgroundFrame() {
|
|||||||
|
|
||||||
const time = performance.now() / 1000 - bgStartTime;
|
const time = performance.now() / 1000 - bgStartTime;
|
||||||
|
|
||||||
// Smooth audio data from the global frequencyData (shared with visualizer)
|
// Smooth audio data from the imported frequencyData (shared with visualizer)
|
||||||
if (typeof frequencyData !== 'undefined' && frequencyData && frequencyData.frequencies) {
|
if (frequencyData && frequencyData.frequencies) {
|
||||||
const bins = frequencyData.frequencies;
|
const bins = frequencyData.frequencies;
|
||||||
const step = Math.max(1, Math.floor(bins.length / BG_BAND_COUNT));
|
const step = Math.max(1, Math.floor(bins.length / BG_BAND_COUNT));
|
||||||
for (let i = 0; i < BG_BAND_COUNT; i++) {
|
for (let i = 0; i < BG_BAND_COUNT; i++) {
|
||||||
@@ -296,13 +298,13 @@ function stopBackground() {
|
|||||||
|
|
||||||
// ---- Public API ----
|
// ---- Public API ----
|
||||||
|
|
||||||
function toggleDynamicBackground() {
|
export function toggleDynamicBackground() {
|
||||||
bgEnabled = !bgEnabled;
|
bgEnabled = !bgEnabled;
|
||||||
localStorage.setItem('dynamicBackground', bgEnabled);
|
localStorage.setItem('dynamicBackground', bgEnabled);
|
||||||
applyDynamicBackground();
|
applyDynamicBackground();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyDynamicBackground() {
|
export function applyDynamicBackground() {
|
||||||
const btn = document.getElementById('bgToggle');
|
const btn = document.getElementById('bgToggle');
|
||||||
if (bgEnabled) {
|
if (bgEnabled) {
|
||||||
startBackground();
|
startBackground();
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
// Media Browser: Navigation, rendering, search, pagination
|
// Media Browser: Navigation, rendering, search, pagination
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
import {
|
||||||
|
t, showToast, escapeHtml, closeDialog,
|
||||||
|
SEARCH_DEBOUNCE_MS, EMPTY_SVG_FILE, EMPTY_SVG_FOLDER, emptyStateHtml,
|
||||||
|
} from './core.js';
|
||||||
|
|
||||||
// Browser state
|
// Browser state
|
||||||
let currentFolderId = null;
|
let currentFolderId = null;
|
||||||
let currentPath = '';
|
let currentPath = '';
|
||||||
@@ -13,11 +18,11 @@ let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
|||||||
let cachedItems = null;
|
let cachedItems = null;
|
||||||
let browserSearchTerm = '';
|
let browserSearchTerm = '';
|
||||||
let browserSearchTimer = null;
|
let browserSearchTimer = null;
|
||||||
const thumbnailCache = new Map();
|
export const thumbnailCache = new Map();
|
||||||
const THUMBNAIL_CACHE_MAX = 200;
|
const THUMBNAIL_CACHE_MAX = 200;
|
||||||
|
|
||||||
// Load media folders on page load
|
// Load media folders on page load
|
||||||
async function loadMediaFolders() {
|
export async function loadMediaFolders() {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -169,11 +174,11 @@ async function browsePath(folderId, path, offset = 0, nocache = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBreadcrumbs(currentPath, parentPath) {
|
function renderBreadcrumbs(currentPathStr, parentPath) {
|
||||||
const breadcrumb = document.getElementById('breadcrumb');
|
const breadcrumb = document.getElementById('breadcrumb');
|
||||||
breadcrumb.innerHTML = '';
|
breadcrumb.innerHTML = '';
|
||||||
|
|
||||||
const parts = (currentPath || '').split('/').filter(p => p);
|
const parts = (currentPathStr || '').split('/').filter(p => p);
|
||||||
let path = '/';
|
let path = '/';
|
||||||
|
|
||||||
// Home link (back to folder list)
|
// Home link (back to folder list)
|
||||||
@@ -373,10 +378,10 @@ function renderBrowserGrid(items, container) {
|
|||||||
// Lazy load thumbnail
|
// Lazy load thumbnail
|
||||||
loadThumbnail(thumbnail, item.name);
|
loadThumbnail(thumbnail, item.name);
|
||||||
} else {
|
} else {
|
||||||
const icon = document.createElement('div');
|
const iconEl = document.createElement('div');
|
||||||
icon.className = 'browser-icon';
|
iconEl.className = 'browser-icon';
|
||||||
icon.textContent = getFileIcon(item.type);
|
iconEl.textContent = getFileIcon(item.type);
|
||||||
thumbWrapper.appendChild(icon);
|
thumbWrapper.appendChild(iconEl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play overlay for media files
|
// Play overlay for media files
|
||||||
@@ -527,11 +532,10 @@ async function loadThumbnail(imgElement, fileName) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Revoke previous blob URL if not managed by cache
|
// Revoke previous blob URL if not managed by cache
|
||||||
// (Cache is keyed by path, so check values)
|
|
||||||
if (imgElement.src && imgElement.src.startsWith('blob:')) {
|
if (imgElement.src && imgElement.src.startsWith('blob:')) {
|
||||||
let isCached = false;
|
let isCached = false;
|
||||||
for (const url of thumbnailCache.values()) {
|
for (const cachedUrl of thumbnailCache.values()) {
|
||||||
if (url === imgElement.src) { isCached = true; break; }
|
if (cachedUrl === imgElement.src) { isCached = true; break; }
|
||||||
}
|
}
|
||||||
if (!isCached) URL.revokeObjectURL(imgElement.src);
|
if (!isCached) URL.revokeObjectURL(imgElement.src);
|
||||||
}
|
}
|
||||||
@@ -544,10 +548,10 @@ async function loadThumbnail(imgElement, fileName) {
|
|||||||
if (isList) {
|
if (isList) {
|
||||||
parent.textContent = '\u{1F3B5}';
|
parent.textContent = '\u{1F3B5}';
|
||||||
} else {
|
} else {
|
||||||
const icon = document.createElement('div');
|
const iconEl = document.createElement('div');
|
||||||
icon.className = 'browser-icon';
|
iconEl.className = 'browser-icon';
|
||||||
icon.textContent = '\u{1F3B5}';
|
iconEl.textContent = '\u{1F3B5}';
|
||||||
parent.insertBefore(icon, parent.firstChild);
|
parent.insertBefore(iconEl, parent.firstChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -600,7 +604,7 @@ async function playMediaFile(fileName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function playAllFolder() {
|
export async function playAllFolder() {
|
||||||
if (playInProgress) return;
|
if (playInProgress) return;
|
||||||
playInProgress = true;
|
playInProgress = true;
|
||||||
const btn = document.getElementById('playAllBtn');
|
const btn = document.getElementById('playAllBtn');
|
||||||
@@ -634,7 +638,7 @@ async function playAllFolder() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadFile(fileName, event) {
|
export async function downloadFile(fileName, event) {
|
||||||
if (event) event.stopPropagation();
|
if (event) event.stopPropagation();
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
@@ -699,19 +703,19 @@ function renderPagination() {
|
|||||||
nextBtn.disabled = currentPage === totalPages;
|
nextBtn.disabled = currentPage === totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
function previousPage() {
|
export function previousPage() {
|
||||||
if (currentOffset >= itemsPerPage) {
|
if (currentOffset >= itemsPerPage) {
|
||||||
browsePath(currentFolderId, currentPath, currentOffset - itemsPerPage);
|
browsePath(currentFolderId, currentPath, currentOffset - itemsPerPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextPage() {
|
export function nextPage() {
|
||||||
if (currentOffset + itemsPerPage < totalItems) {
|
if (currentOffset + itemsPerPage < totalItems) {
|
||||||
browsePath(currentFolderId, currentPath, currentOffset + itemsPerPage);
|
browsePath(currentFolderId, currentPath, currentOffset + itemsPerPage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshBrowser() {
|
export function refreshBrowser() {
|
||||||
if (currentFolderId) {
|
if (currentFolderId) {
|
||||||
browsePath(currentFolderId, currentPath, currentOffset, true);
|
browsePath(currentFolderId, currentPath, currentOffset, true);
|
||||||
} else {
|
} else {
|
||||||
@@ -720,7 +724,7 @@ function refreshBrowser() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Browser search
|
// Browser search
|
||||||
function onBrowserSearch() {
|
export function onBrowserSearch() {
|
||||||
const input = document.getElementById('browserSearchInput');
|
const input = document.getElementById('browserSearchInput');
|
||||||
const clearBtn = document.getElementById('browserSearchClear');
|
const clearBtn = document.getElementById('browserSearchClear');
|
||||||
const term = input.value.trim();
|
const term = input.value.trim();
|
||||||
@@ -735,7 +739,7 @@ function onBrowserSearch() {
|
|||||||
}, SEARCH_DEBOUNCE_MS);
|
}, SEARCH_DEBOUNCE_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearBrowserSearch() {
|
export function clearBrowserSearch() {
|
||||||
const input = document.getElementById('browserSearchInput');
|
const input = document.getElementById('browserSearchInput');
|
||||||
input.value = '';
|
input.value = '';
|
||||||
document.getElementById('browserSearchClear').style.display = 'none';
|
document.getElementById('browserSearchClear').style.display = 'none';
|
||||||
@@ -768,7 +772,7 @@ function showBrowserSearch(visible) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setViewMode(mode) {
|
export function setViewMode(mode) {
|
||||||
if (mode === viewMode) return;
|
if (mode === viewMode) return;
|
||||||
viewMode = mode;
|
viewMode = mode;
|
||||||
localStorage.setItem('mediaBrowser.viewMode', mode);
|
localStorage.setItem('mediaBrowser.viewMode', mode);
|
||||||
@@ -786,7 +790,7 @@ function setViewMode(mode) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onItemsPerPageChanged() {
|
export function onItemsPerPageChanged() {
|
||||||
const select = document.getElementById('itemsPerPageSelect');
|
const select = document.getElementById('itemsPerPageSelect');
|
||||||
itemsPerPage = parseInt(select.value);
|
itemsPerPage = parseInt(select.value);
|
||||||
localStorage.setItem('mediaBrowser.itemsPerPage', itemsPerPage);
|
localStorage.setItem('mediaBrowser.itemsPerPage', itemsPerPage);
|
||||||
@@ -798,7 +802,7 @@ function onItemsPerPageChanged() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToPage() {
|
export function goToPage() {
|
||||||
const pageInput = document.getElementById('pageInput');
|
const pageInput = document.getElementById('pageInput');
|
||||||
const totalPages = Math.ceil(totalItems / itemsPerPage);
|
const totalPages = Math.ceil(totalItems / itemsPerPage);
|
||||||
let page = parseInt(pageInput.value);
|
let page = parseInt(pageInput.value);
|
||||||
@@ -813,7 +817,7 @@ function goToPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initBrowserToolbar() {
|
export function initBrowserToolbar() {
|
||||||
// Restore view mode
|
// Restore view mode
|
||||||
const savedViewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
const savedViewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
|
||||||
viewMode = savedViewMode;
|
viewMode = savedViewMode;
|
||||||
@@ -865,18 +869,16 @@ function loadLastBrowserPath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Folder Management
|
// Folder Management
|
||||||
function showManageFoldersDialog() {
|
export function showManageFoldersDialog() {
|
||||||
// TODO: Implement folder management UI
|
// TODO: Implement folder management UI
|
||||||
// For now, show a simple alert
|
|
||||||
showToast(t('browser.manage_folders_hint'), 'info');
|
showToast(t('browser.manage_folders_hint'), 'info');
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeFolderDialog() {
|
export function closeFolderDialog() {
|
||||||
closeDialog(document.getElementById('folderDialog'));
|
closeDialog(document.getElementById('folderDialog'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveFolder(event) {
|
export async function saveFolder(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// TODO: Implement folder save functionality
|
|
||||||
closeFolderDialog();
|
closeFolderDialog();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
// Callbacks: CRUD management
|
// Callbacks: CRUD management
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
let callbackFormDirty = false;
|
import { t, showToast, escapeHtml, closeDialog, showConfirm } from './core.js';
|
||||||
|
|
||||||
|
export let callbackFormDirty = false;
|
||||||
|
export function setCallbackFormDirty(value) { callbackFormDirty = value; }
|
||||||
|
|
||||||
let _loadCallbacksPromise = null;
|
let _loadCallbacksPromise = null;
|
||||||
async function loadCallbacksTable() {
|
export async function loadCallbacksTable() {
|
||||||
if (_loadCallbacksPromise) return _loadCallbacksPromise;
|
if (_loadCallbacksPromise) return _loadCallbacksPromise;
|
||||||
_loadCallbacksPromise = _loadCallbacksTableImpl();
|
_loadCallbacksPromise = _loadCallbacksTableImpl();
|
||||||
_loadCallbacksPromise.finally(() => { _loadCallbacksPromise = null; });
|
_loadCallbacksPromise.finally(() => { _loadCallbacksPromise = null; });
|
||||||
@@ -59,7 +62,7 @@ async function _loadCallbacksTableImpl() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAddCallbackDialog() {
|
export function showAddCallbackDialog() {
|
||||||
const dialog = document.getElementById('callbackDialog');
|
const dialog = document.getElementById('callbackDialog');
|
||||||
const form = document.getElementById('callbackForm');
|
const form = document.getElementById('callbackForm');
|
||||||
const title = document.getElementById('callbackDialogTitle');
|
const title = document.getElementById('callbackDialogTitle');
|
||||||
@@ -75,7 +78,7 @@ function showAddCallbackDialog() {
|
|||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showEditCallbackDialog(callbackName) {
|
export async function showEditCallbackDialog(callbackName) {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
const dialog = document.getElementById('callbackDialog');
|
const dialog = document.getElementById('callbackDialog');
|
||||||
const title = document.getElementById('callbackDialogTitle');
|
const title = document.getElementById('callbackDialogTitle');
|
||||||
@@ -115,7 +118,7 @@ async function showEditCallbackDialog(callbackName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeCallbackDialog() {
|
export async function closeCallbackDialog() {
|
||||||
if (callbackFormDirty) {
|
if (callbackFormDirty) {
|
||||||
if (!await showConfirm(t('callbacks.confirm.unsaved'))) {
|
if (!await showConfirm(t('callbacks.confirm.unsaved'))) {
|
||||||
return;
|
return;
|
||||||
@@ -128,7 +131,7 @@ async function closeCallbackDialog() {
|
|||||||
document.body.classList.remove('dialog-open');
|
document.body.classList.remove('dialog-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveCallback(event) {
|
export async function saveCallback(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||||
@@ -179,7 +182,7 @@ async function saveCallback(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteCallbackConfirm(callbackName) {
|
export async function deleteCallbackConfirm(callbackName) {
|
||||||
if (!await showConfirm(t('callbacks.confirm.delete').replace('{name}', callbackName))) {
|
if (!await showConfirm(t('callbacks.confirm.delete').replace('{name}', callbackName))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,22 +3,22 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// SVG path constants (avoid rebuilding innerHTML on every state update)
|
// SVG path constants (avoid rebuilding innerHTML on every state update)
|
||||||
const SVG_PLAY = '<path d="M8 5v14l11-7z"/>';
|
export const SVG_PLAY = '<path d="M8 5v14l11-7z"/>';
|
||||||
const SVG_PAUSE = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
|
export const SVG_PAUSE = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
|
||||||
const SVG_STOP = '<path d="M6 6h12v12H6z"/>';
|
export const SVG_STOP = '<path d="M6 6h12v12H6z"/>';
|
||||||
const SVG_IDLE = '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>';
|
export const SVG_IDLE = '<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 14.5v-9l6 4.5-6 4.5z"/>';
|
||||||
const SVG_MUTED = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
|
export const SVG_MUTED = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
|
||||||
const SVG_UNMUTED = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>';
|
export const SVG_UNMUTED = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>';
|
||||||
|
|
||||||
// Empty state illustration SVGs
|
// Empty state illustration SVGs
|
||||||
const EMPTY_SVG_FOLDER = '<svg viewBox="0 0 64 64"><path d="M8 16h20l4-6h20a4 4 0 014 4v36a4 4 0 01-4 4H8a4 4 0 01-4-4V20a4 4 0 014-4z"/><path d="M4 24h56" stroke-dasharray="4 3" opacity="0.4"/></svg>';
|
export const EMPTY_SVG_FOLDER = '<svg viewBox="0 0 64 64"><path d="M8 16h20l4-6h20a4 4 0 014 4v36a4 4 0 01-4 4H8a4 4 0 01-4-4V20a4 4 0 014-4z"/><path d="M4 24h56" stroke-dasharray="4 3" opacity="0.4"/></svg>';
|
||||||
const EMPTY_SVG_FILE = '<svg viewBox="0 0 64 64"><path d="M16 4h22l14 14v38a4 4 0 01-4 4H16a4 4 0 01-4-4V8a4 4 0 014-4z"/><path d="M38 4v14h14"/><path d="M22 32h20M22 40h14" opacity="0.5"/></svg>';
|
export const EMPTY_SVG_FILE = '<svg viewBox="0 0 64 64"><path d="M16 4h22l14 14v38a4 4 0 01-4 4H16a4 4 0 01-4-4V8a4 4 0 014-4z"/><path d="M38 4v14h14"/><path d="M22 32h20M22 40h14" opacity="0.5"/></svg>';
|
||||||
function emptyStateHtml(svgStr, text) {
|
export function emptyStateHtml(svgStr, text) {
|
||||||
return `<div class="empty-state-illustration">${svgStr}<p>${text}</p></div>`;
|
return `<div class="empty-state-illustration">${svgStr}<p>${text}</p></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Media source registry: substring key → { name, icon }
|
// Media source registry: substring key → { name, icon }
|
||||||
const MEDIA_SOURCES = {
|
export const MEDIA_SOURCES = {
|
||||||
'spotify': {
|
'spotify': {
|
||||||
name: 'Spotify',
|
name: 'Spotify',
|
||||||
icon: '<svg viewBox="0 0 24 24"><path fill="#1DB954" d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>'
|
icon: '<svg viewBox="0 0 24 24"><path fill="#1DB954" d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>'
|
||||||
@@ -89,7 +89,7 @@ const MEDIA_SOURCES = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function resolveMediaSource(raw) {
|
export function resolveMediaSource(raw) {
|
||||||
if (!raw) return null;
|
if (!raw) return null;
|
||||||
const lower = raw.toLowerCase();
|
const lower = raw.toLowerCase();
|
||||||
for (const [key, info] of Object.entries(MEDIA_SOURCES)) {
|
for (const [key, info] of Object.entries(MEDIA_SOURCES)) {
|
||||||
@@ -99,8 +99,8 @@ function resolveMediaSource(raw) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cached DOM references (populated once after DOMContentLoaded)
|
// Cached DOM references (populated once after DOMContentLoaded)
|
||||||
const dom = {};
|
export const dom = {};
|
||||||
function cacheDom() {
|
export function cacheDom() {
|
||||||
dom.trackTitle = document.getElementById('track-title');
|
dom.trackTitle = document.getElementById('track-title');
|
||||||
dom.artist = document.getElementById('artist');
|
dom.artist = document.getElementById('artist');
|
||||||
dom.album = document.getElementById('album');
|
dom.album = document.getElementById('album');
|
||||||
@@ -137,26 +137,35 @@ function cacheDom() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Timing constants
|
// Timing constants
|
||||||
const VOLUME_THROTTLE_MS = 16;
|
export const VOLUME_THROTTLE_MS = 16;
|
||||||
const POSITION_INTERPOLATION_MS = 100;
|
export const POSITION_INTERPOLATION_MS = 100;
|
||||||
const SEARCH_DEBOUNCE_MS = 200;
|
export const SEARCH_DEBOUNCE_MS = 200;
|
||||||
const TOAST_DURATION_MS = 3000;
|
export const TOAST_DURATION_MS = 3000;
|
||||||
const WS_BACKOFF_BASE_MS = 3000;
|
export const WS_BACKOFF_BASE_MS = 3000;
|
||||||
const WS_BACKOFF_MAX_MS = 30000;
|
export const WS_BACKOFF_MAX_MS = 30000;
|
||||||
const WS_MAX_RECONNECT_ATTEMPTS = 20;
|
export const WS_MAX_RECONNECT_ATTEMPTS = 20;
|
||||||
const WS_PING_INTERVAL_MS = 30000;
|
export const WS_PING_INTERVAL_MS = 30000;
|
||||||
const VOLUME_RELEASE_DELAY_MS = 500;
|
export const VOLUME_RELEASE_DELAY_MS = 500;
|
||||||
|
|
||||||
// Shared state (accessed across multiple modules)
|
// Shared state (accessed across multiple modules)
|
||||||
let ws = null;
|
export let ws = null;
|
||||||
let currentState = 'idle';
|
export function setWs(value) { ws = value; }
|
||||||
let currentDuration = 0;
|
export let currentState = 'idle';
|
||||||
let currentPosition = 0;
|
export function setCurrentState(value) { currentState = value; }
|
||||||
let isUserAdjustingVolume = false;
|
export let currentDuration = 0;
|
||||||
let volumeUpdateTimer = null;
|
export function setCurrentDuration(value) { currentDuration = value; }
|
||||||
let scripts = [];
|
export let currentPosition = 0;
|
||||||
let lastStatus = null;
|
export function setCurrentPosition(value) { currentPosition = value; }
|
||||||
let currentPlayState = 'idle';
|
export let isUserAdjustingVolume = false;
|
||||||
|
export function setIsUserAdjustingVolume(value) { isUserAdjustingVolume = value; }
|
||||||
|
export let volumeUpdateTimer = null;
|
||||||
|
export function setVolumeUpdateTimer(value) { volumeUpdateTimer = value; }
|
||||||
|
export let scripts = [];
|
||||||
|
export function setScripts(value) { scripts = value; }
|
||||||
|
export let lastStatus = null;
|
||||||
|
export function setLastStatus(value) { lastStatus = value; }
|
||||||
|
export let currentPlayState = 'idle';
|
||||||
|
export function setCurrentPlayState(value) { currentPlayState = value; }
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Internationalization (i18n)
|
// Internationalization (i18n)
|
||||||
@@ -178,7 +187,7 @@ const fallbackTranslations = {
|
|||||||
'player.status.disconnected': 'Disconnected'
|
'player.status.disconnected': 'Disconnected'
|
||||||
};
|
};
|
||||||
|
|
||||||
function t(key, params = {}) {
|
export function t(key, params = {}) {
|
||||||
let text = translations[key] || fallbackTranslations[key] || key;
|
let text = translations[key] || fallbackTranslations[key] || key;
|
||||||
Object.keys(params).forEach(param => {
|
Object.keys(params).forEach(param => {
|
||||||
text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]);
|
text = text.replace(new RegExp(`\\{${param}\\}`, 'g'), params[param]);
|
||||||
@@ -208,7 +217,7 @@ function detectBrowserLocale() {
|
|||||||
return supportedLocales[langCode] ? langCode : 'en';
|
return supportedLocales[langCode] ? langCode : 'en';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initLocale() {
|
export async function initLocale() {
|
||||||
const savedLocale = localStorage.getItem('locale') || detectBrowserLocale();
|
const savedLocale = localStorage.getItem('locale') || detectBrowserLocale();
|
||||||
await setLocale(savedLocale);
|
await setLocale(savedLocale);
|
||||||
}
|
}
|
||||||
@@ -228,7 +237,7 @@ async function setLocale(locale) {
|
|||||||
document.body.classList.add('translations-loaded');
|
document.body.classList.add('translations-loaded');
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeLocale() {
|
export function changeLocale() {
|
||||||
const select = document.getElementById('locale-select');
|
const select = document.getElementById('locale-select');
|
||||||
const newLocale = select.value;
|
const newLocale = select.value;
|
||||||
if (newLocale && newLocale !== currentLocale) {
|
if (newLocale && newLocale !== currentLocale) {
|
||||||
@@ -244,6 +253,26 @@ function updateLocaleSelect() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: updateAllText calls functions from other modules via late-bound references.
|
||||||
|
// These are set from app.js after all modules are loaded.
|
||||||
|
let _updatePlaybackState = null;
|
||||||
|
let _updateConnectionStatus = null;
|
||||||
|
let _loadScriptsTable = null;
|
||||||
|
let _loadCallbacksTable = null;
|
||||||
|
let _loadLinksTable = null;
|
||||||
|
let _displayQuickAccess = null;
|
||||||
|
let _renderAccentSwatches = null;
|
||||||
|
|
||||||
|
export function registerUpdateCallbacks(callbacks) {
|
||||||
|
_updatePlaybackState = callbacks.updatePlaybackState;
|
||||||
|
_updateConnectionStatus = callbacks.updateConnectionStatus;
|
||||||
|
_loadScriptsTable = callbacks.loadScriptsTable;
|
||||||
|
_loadCallbacksTable = callbacks.loadCallbacksTable;
|
||||||
|
_loadLinksTable = callbacks.loadLinksTable;
|
||||||
|
_displayQuickAccess = callbacks.displayQuickAccess;
|
||||||
|
_renderAccentSwatches = callbacks.renderAccentSwatches;
|
||||||
|
}
|
||||||
|
|
||||||
function updateAllText() {
|
function updateAllText() {
|
||||||
document.querySelectorAll('[data-i18n]').forEach(el => {
|
document.querySelectorAll('[data-i18n]').forEach(el => {
|
||||||
const key = el.getAttribute('data-i18n');
|
const key = el.getAttribute('data-i18n');
|
||||||
@@ -259,9 +288,9 @@ function updateAllText() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Re-apply dynamic content with new translations
|
// Re-apply dynamic content with new translations
|
||||||
updatePlaybackState(currentState);
|
if (_updatePlaybackState) _updatePlaybackState(currentState);
|
||||||
const connected = ws && ws.readyState === WebSocket.OPEN;
|
const connected = ws && ws.readyState === WebSocket.OPEN;
|
||||||
updateConnectionStatus(connected);
|
if (_updateConnectionStatus) _updateConnectionStatus(connected);
|
||||||
|
|
||||||
if (lastStatus) {
|
if (lastStatus) {
|
||||||
const fallbackTitle = lastStatus.state === 'idle' ? t('player.no_media') : t('player.title_unavailable');
|
const fallbackTitle = lastStatus.state === 'idle' ? t('player.no_media') : t('player.title_unavailable');
|
||||||
@@ -273,15 +302,15 @@ function updateAllText() {
|
|||||||
|
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
if (token) {
|
if (token) {
|
||||||
loadScriptsTable();
|
if (_loadScriptsTable) _loadScriptsTable();
|
||||||
loadCallbacksTable();
|
if (_loadCallbacksTable) _loadCallbacksTable();
|
||||||
loadLinksTable();
|
if (_loadLinksTable) _loadLinksTable();
|
||||||
displayQuickAccess();
|
if (_displayQuickAccess) _displayQuickAccess();
|
||||||
}
|
}
|
||||||
renderAccentSwatches();
|
if (_renderAccentSwatches) _renderAccentSwatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchVersion() {
|
export async function fetchVersion() {
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/health');
|
const response = await fetch('/api/health');
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -300,20 +329,20 @@ async function fetchVersion() {
|
|||||||
// Shared Utilities
|
// Shared Utilities
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function formatTime(seconds) {
|
export function formatTime(seconds) {
|
||||||
if (!seconds || seconds < 0) return '0:00';
|
if (!seconds || seconds < 0) return '0:00';
|
||||||
const mins = Math.floor(seconds / 60);
|
const mins = Math.floor(seconds / 60);
|
||||||
const secs = Math.floor(seconds % 60);
|
const secs = Math.floor(seconds % 60);
|
||||||
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapeHtml(text) {
|
export function escapeHtml(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.textContent = text;
|
div.textContent = text;
|
||||||
return div.innerHTML;
|
return div.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(message, type = 'success') {
|
export function showToast(message, type = 'success') {
|
||||||
const container = document.getElementById('toast-container');
|
const container = document.getElementById('toast-container');
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
toast.className = `toast ${type}`;
|
toast.className = `toast ${type}`;
|
||||||
@@ -331,7 +360,7 @@ function showToast(message, type = 'success') {
|
|||||||
}, TOAST_DURATION_MS);
|
}, TOAST_DURATION_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDialog(dialog) {
|
export function closeDialog(dialog) {
|
||||||
dialog.classList.add('dialog-closing');
|
dialog.classList.add('dialog-closing');
|
||||||
dialog.addEventListener('animationend', () => {
|
dialog.addEventListener('animationend', () => {
|
||||||
dialog.classList.remove('dialog-closing');
|
dialog.classList.remove('dialog-closing');
|
||||||
@@ -339,7 +368,7 @@ function closeDialog(dialog) {
|
|||||||
}, { once: true });
|
}, { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function showConfirm(message) {
|
export function showConfirm(message) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const dialog = document.getElementById('confirmDialog');
|
const dialog = document.getElementById('confirmDialog');
|
||||||
const msg = document.getElementById('confirmDialogMessage');
|
const msg = document.getElementById('confirmDialogMessage');
|
||||||
@@ -371,7 +400,7 @@ function showConfirm(message) {
|
|||||||
// API Commands
|
// API Commands
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function sendCommand(endpoint, body = null) {
|
export async function sendCommand(endpoint, body = null) {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
@@ -399,7 +428,7 @@ async function sendCommand(endpoint, body = null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePlayPause() {
|
export function togglePlayPause() {
|
||||||
if (currentState === 'playing') {
|
if (currentState === 'playing') {
|
||||||
sendCommand('pause');
|
sendCommand('pause');
|
||||||
} else {
|
} else {
|
||||||
@@ -407,16 +436,16 @@ function togglePlayPause() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextTrack() {
|
export function nextTrack() {
|
||||||
sendCommand('next');
|
sendCommand('next');
|
||||||
}
|
}
|
||||||
|
|
||||||
function previousTrack() {
|
export function previousTrack() {
|
||||||
sendCommand('previous');
|
sendCommand('previous');
|
||||||
}
|
}
|
||||||
|
|
||||||
let lastSentVolume = -1;
|
let lastSentVolume = -1;
|
||||||
function setVolume(volume) {
|
export function setVolume(volume) {
|
||||||
if (volume === lastSentVolume) return;
|
if (volume === lastSentVolume) return;
|
||||||
lastSentVolume = volume;
|
lastSentVolume = volume;
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
@@ -426,11 +455,11 @@ function setVolume(volume) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleMute() {
|
export function toggleMute() {
|
||||||
sendCommand('mute');
|
sendCommand('mute');
|
||||||
}
|
}
|
||||||
|
|
||||||
function seek(position) {
|
export function seek(position) {
|
||||||
sendCommand('seek', { position: position });
|
sendCommand('seek', { position: position });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +477,7 @@ function _persistMdiCache() {
|
|||||||
try { localStorage.setItem('mdiIconCache', JSON.stringify(mdiIconCache)); } catch {}
|
try { localStorage.setItem('mdiIconCache', JSON.stringify(mdiIconCache)); } catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMdiIcon(iconName) {
|
export async function fetchMdiIcon(iconName) {
|
||||||
const name = iconName.replace(/^mdi:/, '');
|
const name = iconName.replace(/^mdi:/, '');
|
||||||
if (mdiIconCache[name]) return mdiIconCache[name];
|
if (mdiIconCache[name]) return mdiIconCache[name];
|
||||||
|
|
||||||
@@ -467,7 +496,7 @@ async function fetchMdiIcon(iconName) {
|
|||||||
return '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>';
|
return '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/></svg>';
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveMdiIcons(container) {
|
export async function resolveMdiIcons(container) {
|
||||||
const els = container.querySelectorAll('[data-mdi-icon]');
|
const els = container.querySelectorAll('[data-mdi-icon]');
|
||||||
await Promise.all(Array.from(els).map(async (el) => {
|
await Promise.all(Array.from(els).map(async (el) => {
|
||||||
const icon = el.dataset.mdiIcon;
|
const icon = el.dataset.mdiIcon;
|
||||||
@@ -477,7 +506,7 @@ async function resolveMdiIcons(container) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupIconPreview(inputId, previewId) {
|
export function setupIconPreview(inputId, previewId) {
|
||||||
const input = document.getElementById(inputId);
|
const input = document.getElementById(inputId);
|
||||||
const preview = document.getElementById(previewId);
|
const preview = document.getElementById(previewId);
|
||||||
if (!input || !preview) return;
|
if (!input || !preview) return;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
// Display Brightness & Power Control
|
// Display Brightness & Power Control + Links Management
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
import { t, showToast, escapeHtml, closeDialog, showConfirm, resolveMdiIcons, fetchMdiIcon } from './core.js';
|
||||||
|
|
||||||
let displayBrightnessTimers = {};
|
let displayBrightnessTimers = {};
|
||||||
const DISPLAY_THROTTLE_MS = 50;
|
const DISPLAY_THROTTLE_MS = 50;
|
||||||
|
|
||||||
async function loadDisplayMonitors() {
|
export async function loadDisplayMonitors() {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
@@ -86,7 +88,7 @@ async function loadDisplayMonitors() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDisplayBrightnessInput(monitorId, value) {
|
export function onDisplayBrightnessInput(monitorId, value) {
|
||||||
const label = document.getElementById(`brightness-val-${monitorId}`);
|
const label = document.getElementById(`brightness-val-${monitorId}`);
|
||||||
if (label) label.textContent = `${value}%`;
|
if (label) label.textContent = `${value}%`;
|
||||||
|
|
||||||
@@ -97,7 +99,7 @@ function onDisplayBrightnessInput(monitorId, value) {
|
|||||||
}, DISPLAY_THROTTLE_MS);
|
}, DISPLAY_THROTTLE_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onDisplayBrightnessChange(monitorId, value) {
|
export function onDisplayBrightnessChange(monitorId, value) {
|
||||||
if (displayBrightnessTimers[monitorId]) {
|
if (displayBrightnessTimers[monitorId]) {
|
||||||
clearTimeout(displayBrightnessTimers[monitorId]);
|
clearTimeout(displayBrightnessTimers[monitorId]);
|
||||||
displayBrightnessTimers[monitorId] = null;
|
displayBrightnessTimers[monitorId] = null;
|
||||||
@@ -121,7 +123,7 @@ async function sendDisplayBrightness(monitorId, brightness) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleDisplayPower(monitorId, monitorName) {
|
export async function toggleDisplayPower(monitorId, monitorName) {
|
||||||
const btn = document.getElementById(`power-btn-${monitorId}`);
|
const btn = document.getElementById(`power-btn-${monitorId}`);
|
||||||
const isOn = btn && btn.classList.contains('on');
|
const isOn = btn && btn.classList.contains('on');
|
||||||
const newState = !isOn;
|
const newState = !isOn;
|
||||||
@@ -157,7 +159,7 @@ async function toggleDisplayPower(monitorId, monitorName) {
|
|||||||
// Header Quick Links
|
// Header Quick Links
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
async function loadHeaderLinks() {
|
export async function loadHeaderLinks() {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
if (!token) return;
|
if (!token) return;
|
||||||
|
|
||||||
@@ -197,9 +199,10 @@ async function loadHeaderLinks() {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
let _loadLinksPromise = null;
|
let _loadLinksPromise = null;
|
||||||
let linkFormDirty = false;
|
export let linkFormDirty = false;
|
||||||
|
export function setLinkFormDirty(value) { linkFormDirty = value; }
|
||||||
|
|
||||||
async function loadLinksTable() {
|
export async function loadLinksTable() {
|
||||||
if (_loadLinksPromise) return _loadLinksPromise;
|
if (_loadLinksPromise) return _loadLinksPromise;
|
||||||
_loadLinksPromise = _loadLinksTableImpl();
|
_loadLinksPromise = _loadLinksTableImpl();
|
||||||
_loadLinksPromise.finally(() => { _loadLinksPromise = null; });
|
_loadLinksPromise.finally(() => { _loadLinksPromise = null; });
|
||||||
@@ -251,7 +254,7 @@ async function _loadLinksTableImpl() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAddLinkDialog() {
|
export function showAddLinkDialog() {
|
||||||
const dialog = document.getElementById('linkDialog');
|
const dialog = document.getElementById('linkDialog');
|
||||||
const form = document.getElementById('linkForm');
|
const form = document.getElementById('linkForm');
|
||||||
const title = document.getElementById('linkDialogTitle');
|
const title = document.getElementById('linkDialogTitle');
|
||||||
@@ -269,7 +272,7 @@ function showAddLinkDialog() {
|
|||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showEditLinkDialog(linkName) {
|
export async function showEditLinkDialog(linkName) {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
const dialog = document.getElementById('linkDialog');
|
const dialog = document.getElementById('linkDialog');
|
||||||
const title = document.getElementById('linkDialogTitle');
|
const title = document.getElementById('linkDialogTitle');
|
||||||
@@ -320,7 +323,7 @@ async function showEditLinkDialog(linkName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeLinkDialog() {
|
export async function closeLinkDialog() {
|
||||||
if (linkFormDirty) {
|
if (linkFormDirty) {
|
||||||
if (!await showConfirm(t('links.confirm.unsaved'))) {
|
if (!await showConfirm(t('links.confirm.unsaved'))) {
|
||||||
return;
|
return;
|
||||||
@@ -333,7 +336,7 @@ async function closeLinkDialog() {
|
|||||||
document.body.classList.remove('dialog-open');
|
document.body.classList.remove('dialog-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveLink(event) {
|
export async function saveLink(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||||
@@ -385,7 +388,7 @@ async function saveLink(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteLinkConfirm(linkName) {
|
export async function deleteLinkConfirm(linkName) {
|
||||||
if (!await showConfirm(t('links.confirm.delete').replace('{name}', linkName))) {
|
if (!await showConfirm(t('links.confirm.delete').replace('{name}', linkName))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,21 @@
|
|||||||
// Player: Tabs, theme, accent, vinyl, visualizer, UI updates
|
// Player: Tabs, theme, accent, vinyl, visualizer, UI updates
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
// Tab management
|
import {
|
||||||
let activeTab = 'player';
|
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');
|
const miniPlayer = document.getElementById('mini-player');
|
||||||
if (visible) {
|
if (visible) {
|
||||||
miniPlayer.classList.remove('hidden');
|
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');
|
const indicator = document.getElementById('tabIndicator');
|
||||||
if (!indicator || !btn) return;
|
if (!indicator || !btn) return;
|
||||||
const tabBar = document.getElementById('tabBar');
|
const tabBar = document.getElementById('tabBar');
|
||||||
@@ -32,7 +43,7 @@ function updateTabIndicator(btn, animate = true) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchTab(tabName) {
|
export function switchTab(tabName) {
|
||||||
activeTab = tabName;
|
activeTab = tabName;
|
||||||
|
|
||||||
document.querySelectorAll('[data-tab-content]').forEach(el => {
|
document.querySelectorAll('[data-tab-content]').forEach(el => {
|
||||||
@@ -75,12 +86,12 @@ function switchTab(tabName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Theme management
|
// Theme management
|
||||||
function initTheme() {
|
export function initTheme() {
|
||||||
const savedTheme = localStorage.getItem('theme') || 'dark';
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
setTheme(savedTheme);
|
setTheme(savedTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTheme(theme) {
|
export function setTheme(theme) {
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
localStorage.setItem('theme', theme);
|
localStorage.setItem('theme', theme);
|
||||||
|
|
||||||
@@ -100,17 +111,17 @@ function setTheme(theme) {
|
|||||||
metaThemeColor.setAttribute('content', theme === 'light' ? '#ffffff' : '#121212');
|
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 currentTheme = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Accent color management
|
// Accent color management
|
||||||
const accentPresets = [
|
export const accentPresets = [
|
||||||
{ name: 'Green', color: '#1db954', hover: '#1ed760' },
|
{ name: 'Green', color: '#1db954', hover: '#1ed760' },
|
||||||
{ name: 'Blue', color: '#3b82f6', hover: '#60a5fa' },
|
{ name: 'Blue', color: '#3b82f6', hover: '#60a5fa' },
|
||||||
{ name: 'Purple', color: '#8b5cf6', hover: '#a78bfa' },
|
{ name: 'Purple', color: '#8b5cf6', hover: '#a78bfa' },
|
||||||
@@ -122,7 +133,7 @@ const accentPresets = [
|
|||||||
{ name: 'Yellow', color: '#eab308', hover: '#facc15' },
|
{ name: 'Yellow', color: '#eab308', hover: '#facc15' },
|
||||||
];
|
];
|
||||||
|
|
||||||
function lightenColor(hex, percent) {
|
export function lightenColor(hex, percent) {
|
||||||
const num = parseInt(hex.replace('#', ''), 16);
|
const num = parseInt(hex.replace('#', ''), 16);
|
||||||
const r = Math.min(255, (num >> 16) + Math.round(255 * percent / 100));
|
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));
|
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)}`;
|
return `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAccentColor() {
|
export function initAccentColor() {
|
||||||
const saved = localStorage.getItem('accentColor');
|
const saved = localStorage.getItem('accentColor');
|
||||||
if (saved) {
|
if (saved) {
|
||||||
const preset = accentPresets.find(p => p.color === saved);
|
const preset = accentPresets.find(p => p.color === saved);
|
||||||
@@ -143,16 +154,16 @@ function initAccentColor() {
|
|||||||
renderAccentSwatches();
|
renderAccentSwatches();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAccentColor(color, hover) {
|
export function applyAccentColor(color, hover) {
|
||||||
document.documentElement.style.setProperty('--accent', color);
|
document.documentElement.style.setProperty('--accent', color);
|
||||||
document.documentElement.style.setProperty('--accent-hover', hover);
|
document.documentElement.style.setProperty('--accent-hover', hover);
|
||||||
localStorage.setItem('accentColor', color);
|
localStorage.setItem('accentColor', color);
|
||||||
const dot = document.getElementById('accentDot');
|
const dot = document.getElementById('accentDot');
|
||||||
if (dot) dot.style.background = color;
|
if (dot) dot.style.background = color;
|
||||||
if (typeof updateBackgroundColors === 'function') updateBackgroundColors();
|
updateBackgroundColors();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderAccentSwatches() {
|
export function renderAccentSwatches() {
|
||||||
const dropdown = document.getElementById('accentDropdown');
|
const dropdown = document.getElementById('accentDropdown');
|
||||||
if (!dropdown) return;
|
if (!dropdown) return;
|
||||||
const current = localStorage.getItem('accentColor') || '#1db954';
|
const current = localStorage.getItem('accentColor') || '#1db954';
|
||||||
@@ -177,13 +188,13 @@ function renderAccentSwatches() {
|
|||||||
dropdown.innerHTML = swatches + customRow;
|
dropdown.innerHTML = swatches + customRow;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAccentColor(color, hover) {
|
export function selectAccentColor(color, hover) {
|
||||||
applyAccentColor(color, hover);
|
applyAccentColor(color, hover);
|
||||||
renderAccentSwatches();
|
renderAccentSwatches();
|
||||||
document.getElementById('accentDropdown').classList.remove('open');
|
document.getElementById('accentDropdown').classList.remove('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleAccentPicker() {
|
export function toggleAccentPicker() {
|
||||||
document.getElementById('accentDropdown').classList.toggle('open');
|
document.getElementById('accentDropdown').classList.toggle('open');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,14 +236,14 @@ function restoreVinylAngle() {
|
|||||||
setInterval(saveVinylAngle, 2000);
|
setInterval(saveVinylAngle, 2000);
|
||||||
window.addEventListener('beforeunload', saveVinylAngle);
|
window.addEventListener('beforeunload', saveVinylAngle);
|
||||||
|
|
||||||
function toggleVinylMode() {
|
export function toggleVinylMode() {
|
||||||
if (vinylMode) saveVinylAngle();
|
if (vinylMode) saveVinylAngle();
|
||||||
vinylMode = !vinylMode;
|
vinylMode = !vinylMode;
|
||||||
localStorage.setItem('vinylMode', vinylMode);
|
localStorage.setItem('vinylMode', vinylMode);
|
||||||
applyVinylMode();
|
applyVinylMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyVinylMode() {
|
export function applyVinylMode() {
|
||||||
const container = document.querySelector('.album-art-container');
|
const container = document.querySelector('.album-art-container');
|
||||||
const btn = document.getElementById('vinylToggle');
|
const btn = document.getElementById('vinylToggle');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -260,15 +271,16 @@ function updateVinylSpin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Audio Visualizer
|
// Audio Visualizer
|
||||||
let visualizerEnabled = localStorage.getItem('visualizerEnabled') === 'true';
|
export let visualizerEnabled = localStorage.getItem('visualizerEnabled') === 'true';
|
||||||
let visualizerAvailable = false;
|
export let visualizerAvailable = false;
|
||||||
let visualizerCtx = null;
|
let visualizerCtx = null;
|
||||||
let visualizerAnimFrame = null;
|
let visualizerAnimFrame = null;
|
||||||
let frequencyData = null;
|
export let frequencyData = null;
|
||||||
|
export function setFrequencyData(value) { frequencyData = value; }
|
||||||
let smoothedFrequencies = null;
|
let smoothedFrequencies = null;
|
||||||
const VISUALIZER_SMOOTHING = 0.15;
|
const VISUALIZER_SMOOTHING = 0.15;
|
||||||
|
|
||||||
async function checkVisualizerAvailability() {
|
export async function checkVisualizerAvailability() {
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
const resp = await fetch('/api/media/visualizer/status', {
|
const resp = await fetch('/api/media/visualizer/status', {
|
||||||
@@ -285,13 +297,13 @@ async function checkVisualizerAvailability() {
|
|||||||
if (btn) btn.style.display = visualizerAvailable ? '' : 'none';
|
if (btn) btn.style.display = visualizerAvailable ? '' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleVisualizer() {
|
export function toggleVisualizer() {
|
||||||
visualizerEnabled = !visualizerEnabled;
|
visualizerEnabled = !visualizerEnabled;
|
||||||
localStorage.setItem('visualizerEnabled', visualizerEnabled);
|
localStorage.setItem('visualizerEnabled', visualizerEnabled);
|
||||||
applyVisualizerMode();
|
applyVisualizerMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyVisualizerMode() {
|
export function applyVisualizerMode() {
|
||||||
const container = document.querySelector('.album-art-container');
|
const container = document.querySelector('.album-art-container');
|
||||||
const btn = document.getElementById('visualizerToggle');
|
const btn = document.getElementById('visualizerToggle');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -333,7 +345,7 @@ function startVisualizerRender() {
|
|||||||
renderVisualizerFrame();
|
renderVisualizerFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopVisualizerRender() {
|
export function stopVisualizerRender() {
|
||||||
if (visualizerAnimFrame) {
|
if (visualizerAnimFrame) {
|
||||||
cancelAnimationFrame(visualizerAnimFrame);
|
cancelAnimationFrame(visualizerAnimFrame);
|
||||||
visualizerAnimFrame = null;
|
visualizerAnimFrame = null;
|
||||||
@@ -410,7 +422,7 @@ function renderVisualizerFrame() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Audio device selection
|
// Audio device selection
|
||||||
async function loadAudioDevices() {
|
export async function loadAudioDevices() {
|
||||||
const section = document.getElementById('audioDeviceSection');
|
const section = document.getElementById('audioDeviceSection');
|
||||||
const select = document.getElementById('audioDeviceSelect');
|
const select = document.getElementById('audioDeviceSelect');
|
||||||
if (!section || !select) return;
|
if (!section || !select) return;
|
||||||
@@ -478,7 +490,7 @@ function updateAudioDeviceStatus(status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAudioDeviceChanged() {
|
export async function onAudioDeviceChanged() {
|
||||||
const select = document.getElementById('audioDeviceSelect');
|
const select = document.getElementById('audioDeviceSelect');
|
||||||
if (!select) return;
|
if (!select) return;
|
||||||
|
|
||||||
@@ -519,7 +531,7 @@ let lastPositionUpdate = 0;
|
|||||||
let lastPositionValue = 0;
|
let lastPositionValue = 0;
|
||||||
let interpolationInterval = null;
|
let interpolationInterval = null;
|
||||||
|
|
||||||
function setupProgressDrag(bar, fill) {
|
export function setupProgressDrag(bar, fill) {
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
|
|
||||||
function getPercent(clientX) {
|
function getPercent(clientX) {
|
||||||
@@ -571,8 +583,8 @@ function setupProgressDrag(bar, fill) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateUI(status) {
|
export function updateUI(status) {
|
||||||
lastStatus = status;
|
setLastStatus(status);
|
||||||
|
|
||||||
const fallbackTitle = status.state === 'idle' ? t('player.no_media') : t('player.title_unavailable');
|
const fallbackTitle = status.state === 'idle' ? t('player.no_media') : t('player.title_unavailable');
|
||||||
dom.trackTitle.textContent = status.title || fallbackTitle;
|
dom.trackTitle.textContent = status.title || fallbackTitle;
|
||||||
@@ -583,7 +595,7 @@ function updateUI(status) {
|
|||||||
dom.miniArtist.textContent = status.artist || '';
|
dom.miniArtist.textContent = status.artist || '';
|
||||||
|
|
||||||
const previousState = currentState;
|
const previousState = currentState;
|
||||||
currentState = status.state;
|
setCurrentState(status.state);
|
||||||
updatePlaybackState(status.state);
|
updatePlaybackState(status.state);
|
||||||
|
|
||||||
const altText = status.title && status.artist
|
const altText = status.title && status.artist
|
||||||
@@ -628,8 +640,8 @@ function updateUI(status) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (status.duration && status.position !== null) {
|
if (status.duration && status.position !== null) {
|
||||||
currentDuration = status.duration;
|
setCurrentDuration(status.duration);
|
||||||
currentPosition = status.position;
|
setCurrentPosition(status.position);
|
||||||
lastPositionUpdate = Date.now();
|
lastPositionUpdate = Date.now();
|
||||||
lastPositionValue = status.position;
|
lastPositionValue = status.position;
|
||||||
updateProgress(status.position, status.duration);
|
updateProgress(status.position, status.duration);
|
||||||
@@ -661,8 +673,8 @@ function updateUI(status) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePlaybackState(state) {
|
export function updatePlaybackState(state) {
|
||||||
currentPlayState = state;
|
setCurrentPlayState(state);
|
||||||
switch(state) {
|
switch(state) {
|
||||||
case 'playing':
|
case 'playing':
|
||||||
dom.playbackState.textContent = t('state.playing');
|
dom.playbackState.textContent = t('state.playing');
|
||||||
@@ -715,7 +727,7 @@ function updateProgress(position, duration) {
|
|||||||
miniBar.setAttribute('aria-valuemax', durRound);
|
miniBar.setAttribute('aria-valuemax', durRound);
|
||||||
}
|
}
|
||||||
|
|
||||||
function startPositionInterpolation() {
|
export function startPositionInterpolation() {
|
||||||
if (interpolationInterval) {
|
if (interpolationInterval) {
|
||||||
clearInterval(interpolationInterval);
|
clearInterval(interpolationInterval);
|
||||||
}
|
}
|
||||||
@@ -728,7 +740,7 @@ function startPositionInterpolation() {
|
|||||||
}, POSITION_INTERPOLATION_MS);
|
}, POSITION_INTERPOLATION_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopPositionInterpolation() {
|
export function stopPositionInterpolation() {
|
||||||
if (interpolationInterval) {
|
if (interpolationInterval) {
|
||||||
clearInterval(interpolationInterval);
|
clearInterval(interpolationInterval);
|
||||||
interpolationInterval = null;
|
interpolationInterval = null;
|
||||||
|
|||||||
@@ -2,9 +2,16 @@
|
|||||||
// Scripts: CRUD, quick access, execution dialog
|
// Scripts: CRUD, quick access, execution dialog
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
let scriptFormDirty = false;
|
import {
|
||||||
|
t, showToast, escapeHtml, closeDialog, showConfirm,
|
||||||
|
resolveMdiIcons, fetchMdiIcon,
|
||||||
|
scripts, setScripts,
|
||||||
|
} from './core.js';
|
||||||
|
|
||||||
async function loadScripts() {
|
export let scriptFormDirty = false;
|
||||||
|
export function setScriptFormDirty(value) { scriptFormDirty = value; }
|
||||||
|
|
||||||
|
export async function loadScripts() {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -15,7 +22,7 @@ async function loadScripts() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
scripts = await response.json();
|
setScripts(await response.json());
|
||||||
displayQuickAccess();
|
displayQuickAccess();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -24,7 +31,7 @@ async function loadScripts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _quickAccessGen = 0;
|
let _quickAccessGen = 0;
|
||||||
async function displayQuickAccess() {
|
export async function displayQuickAccess() {
|
||||||
const gen = ++_quickAccessGen;
|
const gen = ++_quickAccessGen;
|
||||||
const grid = document.getElementById('scripts-grid');
|
const grid = document.getElementById('scripts-grid');
|
||||||
|
|
||||||
@@ -150,7 +157,7 @@ async function executeScript(scriptName, buttonElement) {
|
|||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
let _loadScriptsPromise = null;
|
let _loadScriptsPromise = null;
|
||||||
async function loadScriptsTable() {
|
export async function loadScriptsTable() {
|
||||||
if (_loadScriptsPromise) return _loadScriptsPromise;
|
if (_loadScriptsPromise) return _loadScriptsPromise;
|
||||||
_loadScriptsPromise = _loadScriptsTableImpl();
|
_loadScriptsPromise = _loadScriptsTableImpl();
|
||||||
_loadScriptsPromise.finally(() => { _loadScriptsPromise = null; });
|
_loadScriptsPromise.finally(() => { _loadScriptsPromise = null; });
|
||||||
@@ -206,7 +213,7 @@ async function _loadScriptsTableImpl() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showAddScriptDialog() {
|
export function showAddScriptDialog() {
|
||||||
const dialog = document.getElementById('scriptDialog');
|
const dialog = document.getElementById('scriptDialog');
|
||||||
const form = document.getElementById('scriptForm');
|
const form = document.getElementById('scriptForm');
|
||||||
const title = document.getElementById('dialogTitle');
|
const title = document.getElementById('dialogTitle');
|
||||||
@@ -224,7 +231,7 @@ function showAddScriptDialog() {
|
|||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showEditScriptDialog(scriptName) {
|
export async function showEditScriptDialog(scriptName) {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
const dialog = document.getElementById('scriptDialog');
|
const dialog = document.getElementById('scriptDialog');
|
||||||
const title = document.getElementById('dialogTitle');
|
const title = document.getElementById('dialogTitle');
|
||||||
@@ -274,7 +281,7 @@ async function showEditScriptDialog(scriptName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function closeScriptDialog() {
|
export async function closeScriptDialog() {
|
||||||
if (scriptFormDirty) {
|
if (scriptFormDirty) {
|
||||||
if (!await showConfirm(t('scripts.confirm.unsaved'))) {
|
if (!await showConfirm(t('scripts.confirm.unsaved'))) {
|
||||||
return;
|
return;
|
||||||
@@ -287,7 +294,7 @@ async function closeScriptDialog() {
|
|||||||
document.body.classList.remove('dialog-open');
|
document.body.classList.remove('dialog-open');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveScript(event) {
|
export async function saveScript(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const submitBtn = event.target.querySelector('button[type="submit"]');
|
const submitBtn = event.target.querySelector('button[type="submit"]');
|
||||||
@@ -341,7 +348,7 @@ async function saveScript(event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteScriptConfirm(scriptName) {
|
export async function deleteScriptConfirm(scriptName) {
|
||||||
if (!await showConfirm(t('scripts.confirm.delete').replace('{name}', scriptName))) {
|
if (!await showConfirm(t('scripts.confirm.delete').replace('{name}', scriptName))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -373,7 +380,7 @@ async function deleteScriptConfirm(scriptName) {
|
|||||||
// Execution Result Dialog (shared by scripts and callbacks)
|
// Execution Result Dialog (shared by scripts and callbacks)
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function closeExecutionDialog() {
|
export function closeExecutionDialog() {
|
||||||
const dialog = document.getElementById('executionDialog');
|
const dialog = document.getElementById('executionDialog');
|
||||||
closeDialog(dialog);
|
closeDialog(dialog);
|
||||||
document.body.classList.remove('dialog-open');
|
document.body.classList.remove('dialog-open');
|
||||||
@@ -435,7 +442,7 @@ function showExecutionResult(name, result, type = 'script') {
|
|||||||
dialog.showModal();
|
dialog.showModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeScriptDebug(scriptName) {
|
export async function executeScriptDebug(scriptName) {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
const dialog = document.getElementById('executionDialog');
|
const dialog = document.getElementById('executionDialog');
|
||||||
const title = document.getElementById('executionDialogTitle');
|
const title = document.getElementById('executionDialogTitle');
|
||||||
@@ -486,7 +493,7 @@ async function executeScriptDebug(scriptName) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeCallbackDebug(callbackName) {
|
export async function executeCallbackDebug(callbackName) {
|
||||||
const token = localStorage.getItem('media_server_token');
|
const token = localStorage.getItem('media_server_token');
|
||||||
const dialog = document.getElementById('executionDialog');
|
const dialog = document.getElementById('executionDialog');
|
||||||
const title = document.getElementById('executionDialogTitle');
|
const title = document.getElementById('executionDialogTitle');
|
||||||
|
|||||||
@@ -2,11 +2,21 @@
|
|||||||
// WebSocket: Connection, reconnection, authentication
|
// WebSocket: Connection, reconnection, authentication
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
|
import {
|
||||||
|
dom, t, showToast, setWs,
|
||||||
|
WS_BACKOFF_BASE_MS, WS_BACKOFF_MAX_MS,
|
||||||
|
WS_MAX_RECONNECT_ATTEMPTS, WS_PING_INTERVAL_MS,
|
||||||
|
} from './core.js';
|
||||||
|
import { updateUI, visualizerEnabled, visualizerAvailable, setFrequencyData, stopPositionInterpolation, loadAudioDevices } from './player.js';
|
||||||
|
import { loadScripts, loadScriptsTable, displayQuickAccess } from './scripts.js';
|
||||||
|
import { loadCallbacksTable } from './callbacks.js';
|
||||||
|
import { loadHeaderLinks, loadLinksTable } from './links.js';
|
||||||
|
|
||||||
let reconnectTimeout = null;
|
let reconnectTimeout = null;
|
||||||
let pingInterval = null;
|
let pingInterval = null;
|
||||||
let wsReconnectAttempts = 0;
|
let wsReconnectAttempts = 0;
|
||||||
|
|
||||||
function showAuthForm(errorMessage = '') {
|
export function showAuthForm(errorMessage = '') {
|
||||||
const overlay = document.getElementById('auth-overlay');
|
const overlay = document.getElementById('auth-overlay');
|
||||||
overlay.classList.remove('hidden');
|
overlay.classList.remove('hidden');
|
||||||
|
|
||||||
@@ -23,7 +33,7 @@ function hideAuthForm() {
|
|||||||
document.getElementById('auth-overlay').classList.add('hidden');
|
document.getElementById('auth-overlay').classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function authenticate() {
|
export function authenticate() {
|
||||||
const token = document.getElementById('token-input').value.trim();
|
const token = document.getElementById('token-input').value.trim();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
showAuthForm(t('auth.required'));
|
showAuthForm(t('auth.required'));
|
||||||
@@ -34,15 +44,18 @@ function authenticate() {
|
|||||||
connectWebSocket(token);
|
connectWebSocket(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearToken() {
|
export function clearToken() {
|
||||||
localStorage.removeItem('media_server_token');
|
localStorage.removeItem('media_server_token');
|
||||||
if (ws) {
|
// Access ws via import
|
||||||
ws.close();
|
import('./core.js').then(core => {
|
||||||
|
if (core.ws) {
|
||||||
|
core.ws.close();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
showAuthForm(t('auth.cleared'));
|
showAuthForm(t('auth.cleared'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function connectWebSocket(token) {
|
export function connectWebSocket(token) {
|
||||||
if (pingInterval) {
|
if (pingInterval) {
|
||||||
clearInterval(pingInterval);
|
clearInterval(pingInterval);
|
||||||
pingInterval = null;
|
pingInterval = null;
|
||||||
@@ -51,9 +64,10 @@ function connectWebSocket(token) {
|
|||||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const wsUrl = `${protocol}//${window.location.host}/api/media/ws?token=${encodeURIComponent(token)}`;
|
const wsUrl = `${protocol}//${window.location.host}/api/media/ws?token=${encodeURIComponent(token)}`;
|
||||||
|
|
||||||
ws = new WebSocket(wsUrl);
|
const newWs = new WebSocket(wsUrl);
|
||||||
|
setWs(newWs);
|
||||||
|
|
||||||
ws.onopen = () => {
|
newWs.onopen = () => {
|
||||||
console.log('WebSocket connected');
|
console.log('WebSocket connected');
|
||||||
wsReconnectAttempts = 0;
|
wsReconnectAttempts = 0;
|
||||||
updateConnectionStatus(true);
|
updateConnectionStatus(true);
|
||||||
@@ -66,11 +80,11 @@ function connectWebSocket(token) {
|
|||||||
loadHeaderLinks();
|
loadHeaderLinks();
|
||||||
loadAudioDevices();
|
loadAudioDevices();
|
||||||
if (visualizerEnabled && visualizerAvailable) {
|
if (visualizerEnabled && visualizerAvailable) {
|
||||||
ws.send(JSON.stringify({ type: 'enable_visualizer' }));
|
newWs.send(JSON.stringify({ type: 'enable_visualizer' }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onmessage = (event) => {
|
newWs.onmessage = (event) => {
|
||||||
const msg = JSON.parse(event.data);
|
const msg = JSON.parse(event.data);
|
||||||
|
|
||||||
if (msg.type === 'status' || msg.type === 'status_update') {
|
if (msg.type === 'status' || msg.type === 'status_update') {
|
||||||
@@ -85,18 +99,18 @@ function connectWebSocket(token) {
|
|||||||
loadLinksTable();
|
loadLinksTable();
|
||||||
displayQuickAccess();
|
displayQuickAccess();
|
||||||
} else if (msg.type === 'audio_data') {
|
} else if (msg.type === 'audio_data') {
|
||||||
frequencyData = msg.data;
|
setFrequencyData(msg.data);
|
||||||
} else if (msg.type === 'error') {
|
} else if (msg.type === 'error') {
|
||||||
console.error('WebSocket error:', msg.message);
|
console.error('WebSocket error:', msg.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onerror = (error) => {
|
newWs.onerror = (error) => {
|
||||||
console.error('WebSocket error:', error);
|
console.error('WebSocket error:', error);
|
||||||
updateConnectionStatus(false);
|
updateConnectionStatus(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
ws.onclose = (event) => {
|
newWs.onclose = (event) => {
|
||||||
console.log('WebSocket closed:', event.code);
|
console.log('WebSocket closed:', event.code);
|
||||||
updateConnectionStatus(false);
|
updateConnectionStatus(false);
|
||||||
stopPositionInterpolation();
|
stopPositionInterpolation();
|
||||||
@@ -131,13 +145,13 @@ function connectWebSocket(token) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pingInterval = setInterval(() => {
|
pingInterval = setInterval(() => {
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
if (newWs && newWs.readyState === WebSocket.OPEN) {
|
||||||
ws.send(JSON.stringify({ type: 'ping' }));
|
newWs.send(JSON.stringify({ type: 'ping' }));
|
||||||
}
|
}
|
||||||
}, WS_PING_INTERVAL_MS);
|
}, WS_PING_INTERVAL_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateConnectionStatus(connected) {
|
export function updateConnectionStatus(connected) {
|
||||||
if (connected) {
|
if (connected) {
|
||||||
dom.statusDot.classList.add('connected');
|
dom.statusDot.classList.add('connected');
|
||||||
} else {
|
} else {
|
||||||
@@ -159,7 +173,7 @@ function hideConnectionBanner() {
|
|||||||
banner.classList.add('hidden');
|
banner.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
function manualReconnect() {
|
export function manualReconnect() {
|
||||||
const savedToken = localStorage.getItem('media_server_token');
|
const savedToken = localStorage.getItem('media_server_token');
|
||||||
if (savedToken) {
|
if (savedToken) {
|
||||||
wsReconnectAttempts = 0;
|
wsReconnectAttempts = 0;
|
||||||
|
|||||||
Generated
+690
@@ -0,0 +1,690 @@
|
|||||||
|
{
|
||||||
|
"name": "media-server-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 2,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "media-server-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.27.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.4",
|
||||||
|
"@esbuild/android-arm": "0.27.4",
|
||||||
|
"@esbuild/android-arm64": "0.27.4",
|
||||||
|
"@esbuild/android-x64": "0.27.4",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.4",
|
||||||
|
"@esbuild/darwin-x64": "0.27.4",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.4",
|
||||||
|
"@esbuild/linux-arm": "0.27.4",
|
||||||
|
"@esbuild/linux-arm64": "0.27.4",
|
||||||
|
"@esbuild/linux-ia32": "0.27.4",
|
||||||
|
"@esbuild/linux-loong64": "0.27.4",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.4",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.4",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.4",
|
||||||
|
"@esbuild/linux-s390x": "0.27.4",
|
||||||
|
"@esbuild/linux-x64": "0.27.4",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.4",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.4",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.4",
|
||||||
|
"@esbuild/sunos-x64": "0.27.4",
|
||||||
|
"@esbuild/win32-arm64": "0.27.4",
|
||||||
|
"@esbuild/win32-ia32": "0.27.4",
|
||||||
|
"@esbuild/win32-x64": "0.27.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/android-arm": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/android-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/android-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/darwin-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/linux-arm": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/linux-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/linux-ia32": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/linux-loong64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/linux-s390x": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/linux-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/sunos-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/win32-arm64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/win32-ia32": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@esbuild/win32-x64": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"esbuild": {
|
||||||
|
"version": "0.27.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
|
||||||
|
"integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@esbuild/aix-ppc64": "0.27.4",
|
||||||
|
"@esbuild/android-arm": "0.27.4",
|
||||||
|
"@esbuild/android-arm64": "0.27.4",
|
||||||
|
"@esbuild/android-x64": "0.27.4",
|
||||||
|
"@esbuild/darwin-arm64": "0.27.4",
|
||||||
|
"@esbuild/darwin-x64": "0.27.4",
|
||||||
|
"@esbuild/freebsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/freebsd-x64": "0.27.4",
|
||||||
|
"@esbuild/linux-arm": "0.27.4",
|
||||||
|
"@esbuild/linux-arm64": "0.27.4",
|
||||||
|
"@esbuild/linux-ia32": "0.27.4",
|
||||||
|
"@esbuild/linux-loong64": "0.27.4",
|
||||||
|
"@esbuild/linux-mips64el": "0.27.4",
|
||||||
|
"@esbuild/linux-ppc64": "0.27.4",
|
||||||
|
"@esbuild/linux-riscv64": "0.27.4",
|
||||||
|
"@esbuild/linux-s390x": "0.27.4",
|
||||||
|
"@esbuild/linux-x64": "0.27.4",
|
||||||
|
"@esbuild/netbsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/netbsd-x64": "0.27.4",
|
||||||
|
"@esbuild/openbsd-arm64": "0.27.4",
|
||||||
|
"@esbuild/openbsd-x64": "0.27.4",
|
||||||
|
"@esbuild/openharmony-arm64": "0.27.4",
|
||||||
|
"@esbuild/sunos-x64": "0.27.4",
|
||||||
|
"@esbuild/win32-arm64": "0.27.4",
|
||||||
|
"@esbuild/win32-ia32": "0.27.4",
|
||||||
|
"@esbuild/win32-x64": "0.27.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "media-server-frontend",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Frontend build tooling for media server WebUI",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node esbuild.mjs",
|
||||||
|
"watch": "node esbuild.mjs --watch"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.27.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -51,6 +51,7 @@ dev = [
|
|||||||
"pytest>=7.0",
|
"pytest>=7.0",
|
||||||
"pytest-asyncio>=0.21",
|
"pytest-asyncio>=0.21",
|
||||||
"httpx>=0.24",
|
"httpx>=0.24",
|
||||||
|
"ruff>=0.4.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
@@ -67,3 +68,18 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[tool.setuptools.packages.find]
|
[tool.setuptools.packages.find]
|
||||||
include = ["media_server*"]
|
include = ["media_server*"]
|
||||||
|
|
||||||
|
[tool.ruff]
|
||||||
|
target-version = "py310"
|
||||||
|
line-length = 120
|
||||||
|
|
||||||
|
[tool.ruff.lint]
|
||||||
|
select = ["E", "F", "I", "W"]
|
||||||
|
|
||||||
|
[tool.ruff.lint.per-file-ignores]
|
||||||
|
# AppleScript string literals contain long lines that cannot be broken
|
||||||
|
"media_server/services/macos_media.py" = ["E501"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
asyncio_mode = "auto"
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
Set WshShell = CreateObject("WScript.Shell")
|
|
||||||
Set FSO = CreateObject("Scripting.FileSystemObject")
|
|
||||||
' Get parent folder of scripts folder (media-server root)
|
|
||||||
WshShell.CurrentDirectory = FSO.GetParentFolderName(FSO.GetParentFolderName(WScript.ScriptFullName))
|
|
||||||
WshShell.Run "python -m media_server.main", 0, False
|
|
||||||
Set FSO = Nothing
|
|
||||||
Set WshShell = Nothing
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM Media Server Startup Script
|
|
||||||
REM This script starts the media server
|
|
||||||
|
|
||||||
echo Starting Media Server...
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM Change to the media-server directory (parent of scripts folder)
|
|
||||||
cd /d "%~dp0\.."
|
|
||||||
|
|
||||||
REM Start the media server
|
|
||||||
python -m media_server.main
|
|
||||||
|
|
||||||
REM If the server exits, pause to show any error messages
|
|
||||||
pause
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
@echo off
|
|
||||||
REM Media Server Stop Script
|
|
||||||
REM This script stops the running media server
|
|
||||||
|
|
||||||
echo Stopping Media Server...
|
|
||||||
echo.
|
|
||||||
|
|
||||||
REM Find and kill Python processes running media_server.main
|
|
||||||
for /f "tokens=2" %%i in ('tasklist /FI "IMAGENAME eq python.exe" /FO LIST ^| findstr /B "PID:"') do (
|
|
||||||
wmic process where "ProcessId=%%i" get CommandLine 2>nul | findstr /C:"media_server.main" >nul
|
|
||||||
if not errorlevel 1 (
|
|
||||||
taskkill /PID %%i /F
|
|
||||||
echo Media server process (PID %%i) terminated.
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Done! Media server stopped.
|
|
||||||
pause
|
|
||||||
Reference in New Issue
Block a user