Compare commits

...

9 Commits

Author SHA1 Message Date
alexei.dolgolyov 2961f8eaec chore: release v0.1.2
Release / create-release (push) Successful in 4s
Lint & Test / test (push) Successful in 11s
Release / build-linux (push) Successful in 29s
Release / build-windows (push) Successful in 1m11s
2026-03-29 20:00:38 +03:00
alexei.dolgolyov c50a8f472c fix: make folder status visible with dot + text label
Lint & Test / test (push) Successful in 10s
Status dot was 8x8px with no text, nearly invisible in the table.
Now renders as a colored dot with an adjacent text label
(Available / Unavailable).
2026-03-29 15:07:46 +03:00
alexei.dolgolyov cad6e8a1fe feat: redesign media browser UI
Lint & Test / test (push) Successful in 9s
- Root folder cards with hero-style layout and SVG icons
- Full-width thumbnails with aspect-ratio grid items
- List view column headers (Name, Bitrate, Duration, Size)
- Modernized breadcrumb with pill segments and overflow handling
- Proper skeleton shimmer replacing emoji hourglass on thumbnails
- Pagination shows "Showing X-Y of Z" item count
- Refined hover effects, animations, and visual hierarchy
- Download button revealed on row hover in list view
- Type badges hidden by default, shown on hover
- Localized new keys in en.json and ru.json
2026-03-29 14:59:43 +03:00
alexei.dolgolyov c9ee41ad35 feat: add media folder management from WebUI
Lint & Test / test (push) Successful in 10s
- Add media_folders_management config flag (enabled by default)
- Guard folder CRUD endpoints with 403 when management disabled
- Wire up frontend folder add/edit/delete in Settings tab
- Add per-folder availability check (for network shares)
- Show unavailable badge on offline folders in browser view
- Expose management flag via /api/health endpoint
- Add EN/RU locale keys for folder management UI
2026-03-29 14:44:03 +03:00
alexei.dolgolyov 0256be816e chore: update release notes for v0.1.1
Release / create-release (push) Successful in 3s
Lint & Test / test (push) Successful in 9s
Release / build-linux (push) Successful in 32s
Release / build-windows (push) Successful in 1m10s
2026-03-28 18:53:13 +03:00
alexei.dolgolyov 5219263388 fix: port-in-use check and remove packaging dependency 2026-03-28 18:52:46 +03:00
alexei.dolgolyov 98163ea5a9 chore: update release notes and version for v0.1.1
Release / create-release (push) Successful in 5s
Lint & Test / test (push) Successful in 11s
Release / build-linux (push) Successful in 33s
Release / build-windows (push) Successful in 1m11s
2026-03-28 18:37:56 +03:00
alexei.dolgolyov 5e5e5036c0 fix: use custom icon for Windows shortcuts instead of python.exe
Lint & Test / test (push) Successful in 10s
2026-03-28 18:36:53 +03:00
alexei.dolgolyov 4f9e99e10b ci: add manual build workflow for testing artifacts
Lint & Test / test (push) Failing after 11m21s
workflow_dispatch-triggered build.yml that produces Windows
installer/portable and Linux tarball as CI artifacts without
creating a release. Trigger from Gitea UI → Actions → Run.
2026-03-27 23:44:21 +03:00
19 changed files with 803 additions and 283 deletions
+72
View File
@@ -0,0 +1,72 @@
name: Build Artifacts
on:
workflow_dispatch:
inputs:
version:
description: 'Version label (e.g. dev, 0.3.0-test)'
required: false
default: 'dev'
jobs:
build-windows:
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 "v${{ inputs.version }}"
- name: Build NSIS installer
run: makensis -DVERSION="${{ inputs.version }}" installer.nsi
- uses: actions/upload-artifact@v3
with:
name: MediaServer-${{ inputs.version }}-win-x64
path: |
build/MediaServer-*.zip
build/MediaServer-*-setup.exe
retention-days: 90
build-linux:
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: Build Linux distribution
run: |
chmod +x build-dist-linux.sh
./build-dist-linux.sh "v${{ inputs.version }}"
- uses: actions/upload-artifact@v3
with:
name: MediaServer-${{ inputs.version }}-linux-x64
path: build/MediaServer-*-linux-x64.tar.gz
retention-days: 90
+8 -135
View File
@@ -1,148 +1,21 @@
## v0.1.0 (2026-03-25)
Initial public release of Media Server — a standalone REST API server (FastAPI) for controlling system-wide media playback on Windows, Linux, macOS, and Android.
## v0.1.2 (2026-03-29)
### Features
- Remote media control: play, pause, stop, next, previous, volume, seek ([83acf5f](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/83acf5f))
- Built-in Web UI for media control and monitoring ([a0d138b](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/a0d138b))
- Media browser with grid/compact/list views and single-click playback ([e16674c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/e16674c))
- Low-latency volume control via WebSocket ([32b058c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/32b058c))
- Audio visualizer with spectrogram, beat-reactive art, and device selection ([0691e3d](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/0691e3d))
- Dynamic WebGL background with audio reactivity ([be48318](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/be48318))
- Runtime script management with Home Assistant integration ([d7c5994](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d7c5994))
- Typed script parameters with validation and icon-grid selector ([1410a8d](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/1410a8d))
- Callback management API/UI and theme support ([a0af855](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/a0af855))
- Multi-token authentication with client labels ([71a0a6e](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/71a0a6e))
- Optional authentication — no tokens = no auth ([4d1bb78](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4d1bb78))
- Internationalization (i18n) with English and Russian locales ([9bbb8e1](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/9bbb8e1))
- PWA support: installable standalone app with safe area handling ([a20812e](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/a20812e))
- System tray icon with Show UI, Restart, and Shutdown actions ([6500d6f](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/6500d6f), [3f14512](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/3f14512))
- Display brightness and power control ([a568608](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/a568608))
- Custom accent color picker and primary display indicator ([397d38a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/397d38a))
- 3D album art rotation and vinyl desaturation effect ([4112367](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4112367))
- Friendly media source names with brand icons ([73a6f38](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/73a6f38))
- Browser search/filter for media items ([5f474d6](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/5f474d6))
- Header quick links with CRUD management ([99dbbb1](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/99dbbb1))
- Swagger API docs button in header toolbar ([2b1e09d](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/2b1e09d))
- Update-available notification system ([795a15c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/795a15c))
- Persist audio capture device selection to config ([fb56e6c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/fb56e6c))
- UI animations: dialogs, tabs, settings, browser stagger, banner pulse ([3cfc437](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/3cfc437))
- Tabbed UI with browse caching and bottom mini player ([98a33bc](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/98a33bc))
- Slider tracks tinted with accent color ([1c0a011](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/1c0a011))
- Redesign media browser UI ([cad6e8a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/cad6e8a))
- Add media folder management from WebUI ([c9ee41a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c9ee41a))
### Bug Fixes
- Tray restart uses `python -m` for reliable process respawn ([415231f](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/415231f))
- Tray main-thread message loop, numpy <2.0 pin, installer config copy ([4021837](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4021837))
- Loopback device status showing 'Unavailable' after change ([0eca829](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/0eca829))
- Error handling for unavailable network shares ([d1ec27c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d1ec27c))
- HTTPException handling in folder endpoints ([c5f8c7a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c5f8c7a))
- FOUC (Flash of Untranslated Content) issues ([4f8f59d](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4f8f59d))
- Windows Task Scheduler auto-start ([8077181](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/8077181))
- Vinyl angle persistence on toggle ([00d313d](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/00d313d))
---
### Development / Internal
#### CI/Build
- CI/CD pipelines with Gitea Actions, NSIS installer, ES module bundling, ruff linting ([5439af1](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/5439af1))
- Linux build in release workflow ([ddd8788](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/ddd8788))
- NSIS installer with custom icon, launch-after-install, running-instance detection ([26b5f74](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/26b5f74))
- CI/build improvements and version detection ([4ef11c8](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4ef11c8))
- Warning annotation for existing release fallback ([d0830cb](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d0830cb))
#### Refactoring
- Modular frontend: refactor monolithic app.js into 8 modules ([92d6709](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/92d6709))
- Codebase audit: stability, performance, accessibility fixes ([9404b37](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/9404b37))
- Make folder status visible with dot + text label ([c50a8f4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c50a8f4))
---
<details>
<summary>All Commits (82)</summary>
<summary>All Commits</summary>
| Hash | Message | Author |
|------|---------|--------|
| [d0830cb](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d0830cb) | ci: use warning annotation for existing release fallback | alexei.dolgolyov |
| [4ef11c8](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4ef11c8) | chore: CI/build improvements and version detection | alexei.dolgolyov |
| [fb56e6c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/fb56e6c) | feat: persist audio capture device selection to config.yaml | alexei.dolgolyov |
| [ff67126](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/ff67126) | chore: bump version to 1.0.1 | alexei.dolgolyov |
| [795a15c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/795a15c) | feat: add update-available notification system | alexei.dolgolyov |
| [1410a8d](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/1410a8d) | feat: typed script parameters with validation and icon-grid selector | alexei.dolgolyov |
| [1c0a011](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/1c0a011) | feat: tint slider tracks with 15% accent color | alexei.dolgolyov |
| [2b1e09d](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/2b1e09d) | feat: add Swagger API docs button to header toolbar | alexei.dolgolyov |
| [415231f](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/415231f) | fix: tray restart uses python -m for reliable process respawn | alexei.dolgolyov |
| [32e2ff5](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/32e2ff5) | fix: add --only-binary to pip download fallback (CI compatibility) | alexei.dolgolyov |
| [309f547](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/309f547) | feat: add default MDI icons to example config scripts | alexei.dolgolyov |
| [4021837](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4021837) | fix: tray main-thread message loop, numpy <2.0 pin, installer config copy | alexei.dolgolyov |
| [d7e10b1](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d7e10b1) | fix: interpolate tag in release body template (f-string) | alexei.dolgolyov |
| [3f14512](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/3f14512) | feat: add Restart and Shutdown tray actions with confirmation dialogs | alexei.dolgolyov |
| [26b5f74](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/26b5f74) | feat: improve installer with custom icon, launch-after-install, and running-instance detection | alexei.dolgolyov |
| [1f6e4f6](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/1f6e4f6) | feat: add Launch option to installer finish page | alexei.dolgolyov |
| [6500d6f](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/6500d6f) | feat: add system tray icon with Show UI and Exit actions | alexei.dolgolyov |
| [4d1bb78](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4d1bb78) | feat: make authentication optional — no tokens = no auth | alexei.dolgolyov |
| [f80f6e9](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/f80f6e9) | fix: correct ._pth path in Windows build script | alexei.dolgolyov |
| [0216851](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/0216851) | docs: comprehensive README update with all API endpoints and features | alexei.dolgolyov |
| [c76ffb9](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c76ffb9) | fix: handle existing release in create-release job | alexei.dolgolyov |
| [ddd8788](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/ddd8788) | Add Linux build to release workflow, fix pytest exit code 5 | alexei.dolgolyov |
| [5439af1](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/5439af1) | Add CI/CD pipelines, NSIS installer, ES module bundling, and ruff linting | alexei.dolgolyov |
| [be48318](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/be48318) | Add dynamic WebGL background with audio reactivity | alexei.dolgolyov |
| [0eca829](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/0eca829) | Fix loopback device status showing 'Unavailable' after change | alexei.dolgolyov |
| [3cfc437](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/3cfc437) | Add UI animations: dialogs, tabs, settings, browser stagger, banner pulse | alexei.dolgolyov |
| [a20812e](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/a20812e) | Add PWA support: installable standalone app with safe area handling | alexei.dolgolyov |
| [652f10f](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/652f10f) | Reduce visualizer latency, tighten UI paddings, fix mobile browser toolbar | alexei.dolgolyov |
| [3846610](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/3846610) | On-demand audio visualizer capture + UI fixes | alexei.dolgolyov |
| [92d6709](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/92d6709) | Refactor monolithic app.js into 8 modular files | alexei.dolgolyov |
| [9404b37](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/9404b37) | Codebase audit fixes: stability, performance, accessibility | alexei.dolgolyov |
| [73a6f38](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/73a6f38) | Add friendly media source names with brand icons | alexei.dolgolyov |
| [b11edc2](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/b11edc2) | Redesign header as pill-shaped toolbar group | alexei.dolgolyov |
| [3d01d98](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/3d01d98) | Style audio device select, hide mini player volume on tablet | alexei.dolgolyov |
| [4112367](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4112367) | Add 3D album art rotation and vinyl desaturation effect | alexei.dolgolyov |
| [00d313d](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/00d313d) | Fix vinyl angle persistence on toggle, group player toggle buttons | alexei.dolgolyov |
| [0691e3d](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/0691e3d) | Add audio visualizer with spectrogram, beat-reactive art, and device selection | alexei.dolgolyov |
| [8a8f00f](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/8a8f00f) | Persist vinyl rotation angle across page reloads | alexei.dolgolyov |
| [397d38a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/397d38a) | Add primary display indicator, custom accent color picker, restart script | alexei.dolgolyov |
| [adf2d93](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/adf2d93) | Consolidate tabs, Quick Access links, mini player nav, link descriptions | alexei.dolgolyov |
| [99dbbb1](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/99dbbb1) | Add header quick links with CRUD management and icon enhancements | alexei.dolgolyov |
| [6f6a4e4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/6f6a4e4) | Improve slider track visibility in both themes | alexei.dolgolyov |
| [a568608](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/a568608) | Add display brightness and power control | alexei.dolgolyov |
| [03a1b30](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/03a1b30) | Comprehensive WebUI improvements: security, UX, accessibility, performance | alexei.dolgolyov |
| [ef1935c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/ef1935c) | Update README with current features | alexei.dolgolyov |
| [7ee0a60](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/7ee0a60) | Update media browser screenshot | alexei.dolgolyov |
| [7f28145](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/7f28145) | Update documentation screenshots | alexei.dolgolyov |
| [80d4dbc](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/80d4dbc) | Fix browser grid card sizing | alexei.dolgolyov |
| [caf24db](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/caf24db) | Compact browser grid cards | alexei.dolgolyov |
| [babdb61](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/babdb61) | Update media-server: Vinyl record mode and accent color picker | alexei.dolgolyov |
| [65b513c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/65b513c) | Update media-server: UI polish and bug fixes | alexei.dolgolyov |
| [84b985e](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/84b985e) | Backend optimizations, frontend optimizations, and UI design improvements | alexei.dolgolyov |
| [d1ec27c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d1ec27c) | Improve error handling for unavailable network shares | alexei.dolgolyov |
| [13df69a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/13df69a) | Show media title (Artist Title) instead of filename when available | alexei.dolgolyov |
| [4c13322](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4c13322) | Show bitrate in browser, remove type labels and Play All text | alexei.dolgolyov |
| [5f474d6](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/5f474d6) | Add browser search/filter for media items | alexei.dolgolyov |
| [98a33bc](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/98a33bc) | Tabbed UI, browse caching, and bottom mini player | alexei.dolgolyov |
| [8db40d3](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/8db40d3) | UI polish: refresh button, negative thumbnail cache, and style fixes | alexei.dolgolyov |
| [f275240](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/f275240) | Add Play All, home navigation, and UI improvements | alexei.dolgolyov |
| [e16674c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/e16674c) | Add media browser with grid/compact/list views and single-click playback | alexei.dolgolyov |
| [32b058c](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/32b058c) | Add low-latency volume control via WebSocket | alexei.dolgolyov |
| [c5f8c7a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c5f8c7a) | Fix HTTPException handling in folder endpoints and install script path | alexei.dolgolyov |
| [8d15a2a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/8d15a2a) | Update Web UI: Header redesign, thumbnail fix, and title fallback | alexei.dolgolyov |
| [1cb83ea](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/1cb83ea) | Add screenshots to README | alexei.dolgolyov |
| [62c42f7](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/62c42f7) | Move install_task_windows.ps1 to scripts folder | alexei.dolgolyov |
| [eb2aed4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/eb2aed4) | Update media browser UI with fade-in animations and improvements | alexei.dolgolyov |
| [7c631d0](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/7c631d0) | Add media browser feature with UI improvements | alexei.dolgolyov |
| [d5ec5c6](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d5ec5c6) | Update Web UI: Improve volume slider responsiveness | alexei.dolgolyov |
| [29e0618](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/29e0618) | Update Web UI: Add server management scripts and improve UX | alexei.dolgolyov |
| [4f8f59d](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4f8f59d) | Update media-server: Fix FOUC (Flash of Untranslated Content) issues | alexei.dolgolyov |
| [40c2c11](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/40c2c11) | Update media-server: Improve script/callback table layout and command editor UX | alexei.dolgolyov |
| [4635cac](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/4635cac) | Update media-server: Add execution timing and improve script/callback execution UI | alexei.dolgolyov |
| [957a177](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/957a177) | Update media-server: Add backdrop click-to-close for dialogs | alexei.dolgolyov |
| [8077181](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/8077181) | Fix Windows Task Scheduler auto-start | alexei.dolgolyov |
| [9bbb8e1](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/9bbb8e1) | Add internationalization (i18n) support with English and Russian locales | alexei.dolgolyov |
| [a0af855](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/a0af855) | Add callback management API/UI and theme support | alexei.dolgolyov |
| [d7c5994](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/d7c5994) | Add runtime script management with Home Assistant integration | alexei.dolgolyov |
| [71a0a6e](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/71a0a6e) | Add multi-token authentication with client labels | alexei.dolgolyov |
| [5342cff](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/5342cff) | Add script execution to Web UI | alexei.dolgolyov |
| [a0d138b](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/a0d138b) | Add built-in Web UI for media control and monitoring | alexei.dolgolyov |
| [1a1cfba](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/1a1cfba) | Add callbacks support for all media actions | alexei.dolgolyov |
| [83acf5f](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/83acf5f) | Initial commit: Media Server for remote media control | alexei.dolgolyov |
| [c50a8f4](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c50a8f4) | fix: make folder status visible with dot + text label | alexei.dolgolyov |
| [cad6e8a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/cad6e8a) | feat: redesign media browser UI | alexei.dolgolyov |
| [c9ee41a](https://git.dolgolyov-family.by/alexei.dolgolyov/media-player-server/commit/c9ee41a) | feat: add media folder management from WebUI | alexei.dolgolyov |
</details>
-1
View File
@@ -43,7 +43,6 @@ CORE_DEPS=(
"pyyaml>=6.0"
"mutagen>=1.47.0"
"pillow>=10.0.0"
"packaging>=23.0"
)
# Windows-specific dependencies
+5
View File
@@ -56,6 +56,11 @@ scripts:
timeout: 10
shell: true
# Media folder management from Web UI (default: true)
# When enabled, media folders can be added, edited, and deleted from the Settings tab.
# Set to false to disable folder management from the UI.
# media_folders_management: false
# Callback scripts (executed after media actions)
# All callbacks are optional - if not defined, the action runs without callback
callbacks:
+4 -4
View File
@@ -84,10 +84,10 @@ Section "!Core (required)" SecCore
CreateDirectory "$SMPROGRAMS\${APPNAME}"
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" \
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
"$INSTDIR\python\python.exe" 0
"$INSTDIR\app\media_server\static\icons\icon.ico" 0
CreateShortcut "$SMPROGRAMS\${APPNAME}\${APPNAME} (Console).lnk" \
"$INSTDIR\${EXENAME}" "" \
"$INSTDIR\python\python.exe" 0
"$INSTDIR\app\media_server\static\icons\icon.ico" 0
CreateShortcut "$SMPROGRAMS\${APPNAME}\Uninstall.lnk" \
"$INSTDIR\uninstall.exe"
@@ -117,14 +117,14 @@ SectionEnd
Section "Desktop shortcut" SecDesktop
CreateShortcut "$DESKTOP\${APPNAME}.lnk" \
"wscript.exe" '"$INSTDIR\scripts\${VBSNAME}"' \
"$INSTDIR\python\python.exe" 0
"$INSTDIR\app\media_server\static\icons\icon.ico" 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
"$INSTDIR\app\media_server\static\icons\icon.ico" 0
SectionEnd
; --- Section descriptions ---
+4
View File
@@ -124,6 +124,10 @@ class Settings(BaseSettings):
default_factory=dict,
description="Media folders available for browsing in the media browser",
)
media_folders_management: bool = Field(
default=True,
description="Allow adding, editing, and deleting media folders from the Web UI",
)
# Thumbnail settings
thumbnail_size: str = Field(
+14
View File
@@ -2,6 +2,7 @@
import argparse
import logging
import socket
import sys
from contextlib import asynccontextmanager
from pathlib import Path
@@ -259,6 +260,19 @@ def main():
print("\nAuthentication is DISABLED (no tokens configured)")
return
# Check if port is available before starting
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
try:
sock.bind((args.host if args.host != "0.0.0.0" else "127.0.0.1", args.port))
except OSError:
print(
f"ERROR: Port {args.port} is already in use. "
f"Another instance of Media Server may be running.\n"
f"Stop the other process or use --port to pick a different port.",
file=sys.stderr,
)
sys.exit(1)
from .tray import PYSTRAY_AVAILABLE, TrayManager
use_tray = PYSTRAY_AVAILABLE and not args.no_tray
+19 -2
View File
@@ -24,6 +24,15 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/browser", tags=["browser"])
def _require_folder_management() -> None:
"""Raise 403 if media folder management is disabled in config."""
if not settings.media_folders_management:
raise HTTPException(
status_code=403,
detail="Media folder management is disabled. Set media_folders_management: true in config.yaml to enable.",
)
async def _broadcast_after_open(controller, label: str, max_wait: float = 2.0) -> None:
"""Poll until media session registers, then broadcast status update.
@@ -83,17 +92,22 @@ async def list_folders(_: str = Depends(verify_token)):
"""List all configured media folders.
Returns:
Dictionary of folder configurations.
Dictionary with folder configurations and management flag.
"""
folders = {}
for folder_id, config in settings.media_folders.items():
folder_path = Path(config.path)
folders[folder_id] = {
"id": folder_id,
"label": config.label,
"path": config.path,
"enabled": config.enabled,
"available": folder_path.is_dir(),
}
return folders
return {
"folders": folders,
"management_enabled": settings.media_folders_management,
}
@router.post("/folders/create")
@@ -112,6 +126,7 @@ async def create_folder(
Raises:
HTTPException: If folder already exists or validation fails.
"""
_require_folder_management()
try:
# Validate folder_id format (alphanumeric and underscore only)
if not request.folder_id.replace("_", "").isalnum():
@@ -169,6 +184,7 @@ async def update_folder(
Raises:
HTTPException: If folder doesn't exist or validation fails.
"""
_require_folder_management()
try:
# Validate path exists
path = Path(request.path)
@@ -217,6 +233,7 @@ async def delete_folder(
Raises:
HTTPException: If folder doesn't exist.
"""
_require_folder_management()
try:
config_manager.delete_media_folder(folder_id)
+2
View File
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Request
from .. import __version__
from ..auth import auth_enabled
from ..config import settings
router = APIRouter(prefix="/api", tags=["health"])
@@ -23,6 +24,7 @@ async def health_check(request: Request) -> dict[str, Any]:
"platform": platform.system(),
"version": __version__,
"auth_required": auth_enabled(),
"media_folders_management": settings.media_folders_management,
}
# Include cached update info if available
+54 -11
View File
@@ -3,10 +3,9 @@
import asyncio
import logging
import re
from functools import total_ordering
from typing import Any, Optional
from packaging.version import Version
from .release_provider import ReleaseProvider
from .websocket_manager import ws_manager
@@ -15,23 +14,67 @@ logger = logging.getLogger(__name__)
_PRE_PATTERN = re.compile(
r"^(\d+\.\d+\.\d+)[-.]?(alpha|beta|rc)[.-]?(\d+)$", re.IGNORECASE
)
_PRE_MAP = {"alpha": "a", "beta": "b", "rc": "rc"}
_PRE_ORDER = {"alpha": 0, "beta": 1, "rc": 2}
def _parse_version(raw: str) -> Version:
"""Normalize a version tag to PEP 440 for correct comparison.
@total_ordering
class _Version:
"""Lightweight PEP 440-ish version for comparison without packaging dep.
Supports: X.Y.Z and X.Y.Z-{alpha,beta,rc}.N
Pre-releases sort before the corresponding stable release.
"""
__slots__ = ("_release", "_pre")
def __init__(self, release: tuple[int, ...], pre: Optional[tuple[int, int]]) -> None:
self._release = release
self._pre = pre
def __eq__(self, other: object) -> bool:
if not isinstance(other, _Version):
return NotImplemented
return self._release == other._release and self._pre == other._pre
def __lt__(self, other: object) -> bool:
if not isinstance(other, _Version):
return NotImplemented
if self._release != other._release:
return self._release < other._release
# No pre-release (stable) is greater than any pre-release
if self._pre is None and other._pre is None:
return False
if self._pre is not None and other._pre is None:
return True
if self._pre is None and other._pre is not None:
return False
return self._pre < other._pre # type: ignore[operator]
def __repr__(self) -> str:
v = ".".join(str(p) for p in self._release)
if self._pre is not None:
labels = {0: "alpha", 1: "beta", 2: "rc"}
v += f"-{labels[self._pre[0]]}.{self._pre[1]}"
return f"_Version('{v}')"
def _parse_version(raw: str) -> _Version:
"""Parse a version tag for comparison.
Examples:
v0.3.0-alpha.1 → 0.3.0a1 (pre-release, sorts below 0.3.0)
v0.3.0-rc.3 → 0.3.0rc3
v1.0.0 → 1.0.0
v0.3.0-alpha.1 → (0,3,0) pre=(0,1) (sorts below 0.3.0)
v0.3.0-rc.3 → (0,3,0) pre=(2,3)
v1.0.0 → (1,0,0) pre=None
"""
cleaned = raw.lstrip("v").strip()
m = _PRE_PATTERN.match(cleaned)
if m:
base, pre_label, pre_num = m.group(1), m.group(2).lower(), m.group(3)
cleaned = f"{base}{_PRE_MAP[pre_label]}{pre_num}"
return Version(cleaned)
base = tuple(int(x) for x in m.group(1).split("."))
pre_label = m.group(2).lower()
pre_num = int(m.group(3))
return _Version(base, (_PRE_ORDER[pre_label], pre_num))
release = tuple(int(x) for x in cleaned.split("."))
return _Version(release, None)
class UpdateChecker:
+297 -98
View File
@@ -192,17 +192,67 @@ h1 {
}
.status-dot {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.75rem;
color: var(--text-muted);
transition: color 0.3s;
}
.status-dot::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--error);
flex-shrink: 0;
transition: background 0.3s;
}
.status-dot.connected {
.status-dot.connected::before,
.status-dot.status-online::before {
background: var(--accent);
}
.status-dot.status-offline::before {
background: var(--error);
}
/* Folder management */
.folder-unavailable-badge,
.folder-disabled-badge {
font-size: 0.75rem;
padding: 1px 6px;
border-radius: 4px;
vertical-align: middle;
margin-left: 4px;
}
.folder-unavailable-badge {
background: color-mix(in srgb, var(--error) 20%, transparent);
color: var(--error);
}
.folder-disabled-badge {
background: color-mix(in srgb, var(--text-secondary) 20%, transparent);
color: var(--text-secondary);
}
.browser-item.unavailable,
.browser-list-item.unavailable {
opacity: 0.5;
cursor: default;
}
.path-cell {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-toolbar {
display: flex;
align-items: center;
@@ -2681,7 +2731,7 @@ footer .separator {
.browser-container {
background: var(--bg-secondary);
border-radius: 12px;
padding: 1rem;
padding: 1.25rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
@@ -2702,14 +2752,20 @@ footer .separator {
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
gap: 0.25rem;
margin-bottom: 1rem;
padding: 0.75rem;
padding: 0.5rem 0.75rem;
background: var(--bg-tertiary);
border-radius: 6px;
font-size: 0.875rem;
border-radius: 8px;
font-size: 0.813rem;
overflow-x: auto;
white-space: nowrap;
scrollbar-width: none;
border: 1px solid var(--border);
}
.breadcrumb::-webkit-scrollbar {
display: none;
}
.breadcrumb:empty {
@@ -2717,28 +2773,44 @@ footer .separator {
}
.breadcrumb-item {
color: var(--accent);
color: var(--text-secondary);
cursor: pointer;
transition: color 0.2s;
transition: all 0.2s;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-weight: 500;
}
.breadcrumb-item:hover {
color: var(--accent-hover);
text-decoration: underline;
color: var(--accent);
background: rgba(29, 185, 84, 0.08);
text-decoration: none;
}
.breadcrumb-item:last-child {
color: var(--text-primary);
font-weight: 600;
cursor: default;
pointer-events: none;
}
.breadcrumb-home {
display: flex;
align-items: center;
padding: 0.25rem;
color: var(--text-muted);
}
.breadcrumb-home:hover {
text-decoration: none;
color: var(--accent);
}
.breadcrumb-separator {
color: var(--text-muted);
margin: 0 0.25rem;
margin: 0;
opacity: 0.5;
font-size: 0.75rem;
}
/* Browser Toolbar */
@@ -2909,13 +2981,19 @@ footer .separator {
/* Browser Grid */
.browser-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 0.75rem;
margin-bottom: 1.5rem;
min-height: 200px;
align-items: stretch;
}
/* Root folder grid — wider cards */
.browser-grid.browser-root-grid {
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: 1rem;
}
/* Compact Grid */
.browser-grid.browser-grid-compact {
grid-template-columns: repeat(auto-fill, minmax(80px, 100px));
@@ -2952,41 +3030,66 @@ footer .separator {
.browser-list {
display: flex;
flex-direction: column;
gap: 2px;
gap: 1px;
margin-bottom: 1.5rem;
min-height: 200px;
}
/* List view column header */
.browser-list-header {
display: grid;
grid-template-columns: 40px 1fr auto auto auto auto;
align-items: center;
gap: 0.75rem;
padding: 0.4rem 0.75rem;
font-size: 0.688rem;
font-weight: 600;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
border-bottom: 1px solid var(--border);
margin-bottom: 0.25rem;
user-select: none;
}
.browser-list-header span:nth-child(n+3) {
text-align: right;
}
.browser-list-item {
display: grid;
grid-template-columns: 40px 1fr auto auto auto auto;
align-items: center;
gap: 0.75rem;
padding: 0.5rem 0.75rem;
background: var(--bg-tertiary);
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
animation: itemFadeIn 0.3s ease-out backwards;
animation-delay: calc(var(--item-index, 0) * 20ms);
animation-delay: calc(var(--item-index, 0) * 15ms);
}
.browser-list-item:hover {
background: var(--bg-tertiary);
border-color: var(--border);
}
.browser-list-item:active {
background: var(--border);
border-color: var(--accent);
}
.browser-list-icon {
width: 32px;
height: 32px;
width: 36px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.25rem;
border-radius: 4px;
background: var(--bg-primary);
border-radius: 6px;
background: var(--bg-tertiary);
flex-shrink: 0;
overflow: hidden;
position: relative;
@@ -2998,8 +3101,8 @@ footer .separator {
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.5);
border-radius: 4px;
background: rgba(0, 0, 0, 0.55);
border-radius: 6px;
opacity: 0;
transition: opacity 0.15s;
pointer-events: none;
@@ -3016,10 +3119,10 @@ footer .separator {
}
.browser-list-thumbnail {
width: 32px;
height: 32px;
width: 36px;
height: 36px;
object-fit: cover;
border-radius: 4px;
border-radius: 6px;
}
.browser-list-thumbnail.loading {
@@ -3046,6 +3149,7 @@ footer .separator {
white-space: nowrap;
min-width: 55px;
text-align: right;
font-variant-numeric: tabular-nums;
}
.browser-list-duration {
@@ -3063,6 +3167,7 @@ footer .separator {
white-space: nowrap;
min-width: 60px;
text-align: right;
font-variant-numeric: tabular-nums;
}
.browser-loading {
@@ -3087,85 +3192,114 @@ footer .separator {
.browser-item {
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 8px;
border: 1px solid transparent;
border-radius: 10px;
padding: 0.6rem;
cursor: pointer;
transition: all 0.2s;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
position: relative;
animation: itemFadeIn 0.3s ease-out backwards;
animation-delay: calc(var(--item-index, 0) * 30ms);
animation-delay: calc(var(--item-index, 0) * 25ms);
}
@keyframes itemFadeIn {
from { opacity: 0; transform: translateY(8px) scale(0.97); }
to { opacity: 1; transform: translateY(0) scale(1); }
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.browser-item:hover {
background: var(--border);
border-color: var(--accent);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
border-color: var(--border);
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25);
}
.browser-item:active {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
/* Root Folder Cards — distinctive hero style */
.browser-item.browser-root-folder {
padding: 1.25rem 1rem;
gap: 0.75rem;
border: 1px solid var(--border);
background: linear-gradient(135deg, var(--bg-tertiary) 0%, var(--bg-secondary) 100%);
min-height: 120px;
justify-content: center;
}
.browser-item.browser-root-folder .browser-thumb-wrapper {
width: auto;
height: auto;
}
.browser-item.browser-root-folder .browser-icon {
width: 56px;
height: 56px;
font-size: 1.75rem;
border-radius: 14px;
background: rgba(29, 185, 84, 0.1);
border: 1px solid rgba(29, 185, 84, 0.15);
transition: all 0.25s;
}
.browser-item.browser-root-folder:hover .browser-icon {
background: rgba(29, 185, 84, 0.18);
border-color: rgba(29, 185, 84, 0.3);
transform: scale(1.05);
}
.browser-item.browser-root-folder .browser-item-name {
font-size: 0.875rem;
font-weight: 600;
}
/* Unavailable root folder overlay */
.browser-item.browser-root-folder.unavailable .browser-icon {
background: rgba(231, 76, 60, 0.08);
border-color: rgba(231, 76, 60, 0.12);
opacity: 0.6;
}
/* Thumbnail Display */
.browser-thumbnail {
width: 90px;
height: 90px;
width: 100%;
aspect-ratio: 1;
object-fit: cover;
border-radius: 6px;
border-radius: 8px;
background: var(--bg-primary);
display: block;
}
.browser-thumbnail.loading {
background: linear-gradient(
90deg,
var(--bg-primary) 25%,
110deg,
var(--bg-primary) 30%,
var(--bg-tertiary) 50%,
var(--bg-primary) 75%
var(--bg-primary) 70%
);
background-size: 200% 100%;
animation: loading 1.5s infinite;
position: relative;
opacity: 0;
}
.browser-thumbnail.loading::after {
content: '⏳';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 2rem;
opacity: 0.6;
animation: pulse 1.5s infinite;
animation: shimmer 1.8s ease-in-out infinite;
opacity: 1;
}
.browser-thumbnail.loaded {
animation: fadeIn 0.5s ease-out forwards;
animation: fadeIn 0.4s ease-out forwards;
}
@keyframes loading {
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
@keyframes pulse {
0%, 100% { opacity: 0.4; }
50% { opacity: 0.8; }
}
@keyframes fadeIn {
from {
opacity: 0;
transform: scale(0.95);
transform: scale(0.97);
}
to {
opacity: 1;
@@ -3175,13 +3309,13 @@ footer .separator {
/* File/Folder Icons */
.browser-icon {
width: 90px;
height: 90px;
width: 100%;
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
border-radius: 6px;
font-size: 2.5rem;
border-radius: 8px;
background: var(--bg-primary);
}
@@ -3189,10 +3323,11 @@ footer .separator {
width: 100%;
text-align: center;
margin-top: auto;
padding: 0 0.15rem;
}
.browser-item-name {
font-size: 0.813rem;
font-size: 0.75rem;
font-weight: 500;
color: var(--text-primary);
word-break: break-word;
@@ -3201,12 +3336,14 @@ footer .separator {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
line-height: 1.3;
}
.browser-item-meta {
font-size: 0.75rem;
color: var(--text-secondary);
margin-top: 0.25rem;
font-size: 0.688rem;
color: var(--text-muted);
margin-top: 0.2rem;
line-height: 1.3;
}
.browser-item-type {
@@ -3222,6 +3359,11 @@ footer .separator {
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.2s;
}
.browser-item:hover .browser-item-type {
opacity: 0.85;
}
@@ -3236,9 +3378,11 @@ footer .separator {
/* Thumbnail Wrapper & Play Overlay */
.browser-thumb-wrapper {
position: relative;
width: 90px;
height: 90px;
width: 100%;
aspect-ratio: 1;
flex-shrink: 0;
border-radius: 8px;
overflow: hidden;
}
.browser-thumb-wrapper .browser-thumbnail,
@@ -3253,24 +3397,29 @@ footer .separator {
display: flex;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.45);
border-radius: 6px;
background: rgba(0, 0, 0, 0.5);
border-radius: 8px;
opacity: 0;
transition: opacity 0.2s;
pointer-events: none;
}
.browser-play-overlay svg {
width: 40px;
height: 40px;
width: 36px;
height: 36px;
color: #fff;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.4));
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.5));
transition: transform 0.15s;
}
.browser-item:hover .browser-play-overlay {
opacity: 1;
}
.browser-item:hover .browser-play-overlay svg {
transform: scale(1.1);
}
/* Compact grid overrides */
.browser-grid-compact .browser-thumb-wrapper {
width: 100%;
@@ -3287,7 +3436,7 @@ footer .separator {
background: transparent;
border: none;
border-radius: 4px;
padding: 0.2rem;
padding: 0.25rem;
color: var(--text-muted);
cursor: pointer;
transition: color 0.15s;
@@ -3297,6 +3446,11 @@ footer .separator {
justify-content: center;
width: auto;
height: auto;
opacity: 0;
}
.browser-list-item:hover .browser-list-download {
opacity: 1;
}
.browser-list-download:hover {
@@ -3308,7 +3462,7 @@ footer .separator {
/* Pagination */
.pagination {
display: flex;
justify-content: center;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding-top: 1rem;
@@ -3316,13 +3470,13 @@ footer .separator {
}
.pagination button {
padding: 0.5rem 1.5rem;
padding: 0.4rem 1.25rem;
border-radius: 6px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
color: var(--text-primary);
cursor: pointer;
font-size: 0.875rem;
font-size: 0.813rem;
font-weight: 600;
transition: all 0.2s;
width: auto;
@@ -3345,19 +3499,25 @@ footer .separator {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-size: 0.813rem;
color: var(--text-secondary);
}
.pagination-showing {
font-size: 0.75rem;
color: var(--text-muted);
white-space: nowrap;
}
.page-input {
width: 3.5rem;
padding: 0.3rem 0.4rem;
width: 3rem;
padding: 0.25rem 0.35rem;
text-align: center;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-size: 0.875rem;
font-size: 0.813rem;
-moz-appearance: textfield;
}
@@ -3374,8 +3534,17 @@ footer .separator {
/* Responsive Design */
@media (max-width: 600px) {
.browser-container {
padding: 0.75rem;
}
.browser-grid {
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 0.5rem;
}
.browser-grid.browser-root-grid {
grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
gap: 0.75rem;
}
@@ -3383,17 +3552,13 @@ footer .separator {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
}
.browser-thumb-wrapper {
width: 100px;
height: 100px;
}
.browser-icon {
font-size: 2.5rem;
}
.browser-item {
padding: 0.75rem;
padding: 0.5rem;
}
.browser-item.browser-root-folder {
padding: 1rem 0.75rem;
min-height: 100px;
}
.browser-header-section {
@@ -3429,12 +3594,27 @@ footer .separator {
display: none;
}
.browser-list-header {
grid-template-columns: 32px 1fr auto auto;
gap: 0.5rem;
padding: 0.35rem 0.5rem;
}
.browser-list-header span:nth-child(n+3):nth-child(-n+4) {
display: none;
}
.browser-list-item {
grid-template-columns: 32px 1fr auto auto;
gap: 0.5rem;
padding: 0.4rem 0.5rem;
}
.browser-list-icon {
width: 32px;
height: 32px;
}
.browser-list-duration {
display: none;
}
@@ -3447,6 +3627,17 @@ footer .separator {
display: none;
}
.pagination {
flex-wrap: wrap;
gap: 0.5rem;
}
.pagination-showing {
flex-basis: 100%;
text-align: center;
order: -1;
}
.album-art-glow {
width: 250px;
height: 250px;
@@ -3485,6 +3676,14 @@ footer .separator {
display: none;
}
.browser-list-header {
grid-template-columns: 40px 1fr auto auto auto;
}
.browser-list-header span:nth-child(3) {
display: none;
}
.browser-list-item {
grid-template-columns: 40px 1fr auto auto auto;
}
+34
View File
@@ -290,6 +290,7 @@
<span id="pageTotal">/ 1</span>
</div>
<button id="nextPage" onclick="nextPage()" data-i18n="browser.next">Next</button>
<span class="pagination-showing" id="paginationShowing"></span>
</div>
</div>
@@ -323,6 +324,39 @@
</div>
</details>
<details class="settings-section" open id="mediaFoldersSection" style="display: none;">
<summary data-i18n="settings.section.media_folders">Media Folders</summary>
<div class="settings-section-content">
<p class="settings-section-description" data-i18n="browser.folders_description">
Media folders available for browsing. Folders on network shares show availability status.
</p>
<table class="scripts-table">
<thead>
<tr>
<th data-i18n="browser.folders_table.id">ID</th>
<th data-i18n="browser.folders_table.label">Label</th>
<th data-i18n="browser.folders_table.path">Path</th>
<th data-i18n="browser.folders_table.status">Status</th>
<th data-i18n="browser.folders_table.actions">Actions</th>
</tr>
</thead>
<tbody id="foldersTableBody">
<tr>
<td colspan="5" class="empty-state">
<div class="empty-state-illustration">
<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>
<p data-i18n="browser.folders_empty">No media folders configured. Click "+" to add one.</p>
</div>
</td>
</tr>
</tbody>
</table>
<div class="add-card" onclick="showAddFolderDialog()">
<span class="add-card-icon">+</span>
</div>
</div>
</details>
<details class="settings-section" open>
<summary data-i18n="settings.section.scripts">Scripts</summary>
<div class="settings-section-content">
+21 -1
View File
@@ -57,6 +57,7 @@ import {
onBrowserSearch, clearBrowserSearch, onItemsPerPageChanged,
downloadFile, closeFolderDialog, saveFolder,
showManageFoldersDialog,
showAddFolderDialog, showEditFolderDialog, deleteFolderConfirm,
} from './browser.js';
import {
@@ -117,6 +118,7 @@ Object.assign(window, {
onBrowserSearch, clearBrowserSearch, onItemsPerPageChanged,
downloadFile, closeFolderDialog, saveFolder,
showManageFoldersDialog,
showAddFolderDialog, showEditFolderDialog, deleteFolderConfirm,
// Links
showAddLinkDialog, showEditLinkDialog, closeLinkDialog,
saveLink, deleteLinkConfirm,
@@ -323,6 +325,24 @@ window.addEventListener('DOMContentLoaded', async () => {
else if (action === 'delete') deleteCallbackConfirm(name);
});
// Folder dialog backdrop click to close
const folderDialog = document.getElementById('folderDialog');
folderDialog.addEventListener('click', (e) => {
if (e.target === folderDialog) {
closeFolderDialog();
}
});
// Delegated click handlers for folder table actions
document.getElementById('foldersTableBody').addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
const folderId = btn.dataset.folderId;
if (action === 'edit') showEditFolderDialog(folderId);
else if (action === 'delete') deleteFolderConfirm(folderId);
});
// Link dialog backdrop click to close
const linkDialog = document.getElementById('linkDialog');
linkDialog.addEventListener('click', (e) => {
@@ -352,7 +372,7 @@ window.addEventListener('DOMContentLoaded', async () => {
// Initialize browser toolbar and load folders
initBrowserToolbar();
if (token) {
if (!authReq || token) {
loadMediaFolders();
}
+213 -24
View File
@@ -3,7 +3,7 @@
// ============================================================
import {
t, showToast, escapeHtml, closeDialog,
t, showToast, showConfirm, escapeHtml, closeDialog,
SEARCH_DEBOUNCE_MS, EMPTY_SVG_FILE, EMPTY_SVG_FOLDER, emptyStateHtml,
getAuthHeaders, hasCredentials,
} from './core.js';
@@ -15,6 +15,7 @@ let currentOffset = 0;
let itemsPerPage = parseInt(localStorage.getItem('mediaBrowser.itemsPerPage')) || 100;
let totalItems = 0;
let mediaFolders = {};
let managementEnabled = false;
let viewMode = localStorage.getItem('mediaBrowser.viewMode') || 'grid';
let cachedItems = null;
let browserSearchTerm = '';
@@ -33,7 +34,20 @@ export async function loadMediaFolders() {
if (!response.ok) throw new Error('Failed to load folders');
mediaFolders = await response.json();
const data = await response.json();
mediaFolders = data.folders || {};
managementEnabled = data.management_enabled || false;
// Show/hide the media folders settings section
const section = document.getElementById('mediaFoldersSection');
if (section) {
section.style.display = managementEnabled ? '' : 'none';
}
// Render folders table in settings if management is enabled
if (managementEnabled) {
loadFoldersTable();
}
// Load last browsed path or show root folder list
loadLastBrowserPath();
@@ -69,41 +83,48 @@ function showRootFolders() {
revokeBlobUrls(container);
if (viewMode === 'list') {
container.className = 'browser-list';
} else if (viewMode === 'compact') {
container.className = 'browser-grid browser-grid-compact';
} else {
container.className = 'browser-grid';
container.className = 'browser-grid browser-root-grid';
}
container.innerHTML = '';
const folderSvg = '<svg viewBox="0 0 24 24" width="24" height="24"><path fill="currentColor" d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>';
Object.entries(mediaFolders).forEach(([id, folder]) => {
if (!folder.enabled) return;
const unavailable = folder.available === false;
const unavailableClass = unavailable ? ' unavailable' : '';
if (viewMode === 'list') {
const row = document.createElement('div');
row.className = 'browser-list-item';
row.onclick = () => {
currentFolderId = id;
browsePath(id, '');
};
row.className = 'browser-list-item' + unavailableClass;
if (!unavailable) {
row.onclick = () => {
currentFolderId = id;
browsePath(id, '');
};
}
row.innerHTML = `
<div class="browser-list-icon">\u{1F4C1}</div>
<div class="browser-list-name">${folder.label}</div>
<div class="browser-list-icon" style="color: var(--accent)">${folderSvg}</div>
<div class="browser-list-name">${escapeHtml(folder.label)}${unavailable ? ' <span class="folder-unavailable-badge">' + t('browser.unavailable') + '</span>' : ''}</div>
`;
container.appendChild(row);
} else {
const card = document.createElement('div');
card.className = 'browser-item';
card.onclick = () => {
currentFolderId = id;
browsePath(id, '');
};
card.className = 'browser-item browser-root-folder' + unavailableClass;
if (!unavailable) {
card.onclick = () => {
currentFolderId = id;
browsePath(id, '');
};
}
card.innerHTML = `
<div class="browser-thumb-wrapper">
<div class="browser-icon">\u{1F4C1}</div>
<div class="browser-icon" style="color: var(--accent)">${folderSvg}</div>
</div>
<div class="browser-item-info">
<div class="browser-item-name">${folder.label}</div>
<div class="browser-item-name">${escapeHtml(folder.label)}</div>
${unavailable ? '<div class="browser-item-meta folder-unavailable-badge">' + t('browser.unavailable') + '</div>' : ''}
</div>
`;
container.appendChild(card);
@@ -248,6 +269,19 @@ function renderBrowserList(items, container) {
return;
}
// Column header row
const header = document.createElement('div');
header.className = 'browser-list-header';
header.innerHTML = `
<span></span>
<span>${t('browser.list_header.name')}</span>
<span>${t('browser.list_header.bitrate')}</span>
<span>${t('browser.list_header.duration')}</span>
<span>${t('browser.list_header.size')}</span>
<span></span>
`;
container.appendChild(header);
items.forEach((item, idx) => {
const row = document.createElement('div');
row.className = 'browser-list-item';
@@ -662,6 +696,7 @@ function renderPagination() {
const nextBtn = document.getElementById('nextPage');
const pageInput = document.getElementById('pageInput');
const pageTotal = document.getElementById('pageTotal');
const showingEl = document.getElementById('paginationShowing');
const totalPages = Math.ceil(totalItems / itemsPerPage);
const currentPage = Math.floor(currentOffset / itemsPerPage) + 1;
@@ -676,6 +711,13 @@ function renderPagination() {
pageInput.max = totalPages;
pageTotal.textContent = `/ ${totalPages}`;
// "Showing X-Y of Z"
if (showingEl) {
const from = currentOffset + 1;
const to = Math.min(currentOffset + itemsPerPage, totalItems);
showingEl.textContent = t('browser.showing_items', { from, to, total: totalItems });
}
prevBtn.disabled = currentPage === 1;
nextBtn.disabled = currentPage === totalPages;
}
@@ -845,10 +887,72 @@ function loadLastBrowserPath() {
}
}
// Folder Management
export function showManageFoldersDialog() {
// TODO: Implement folder management UI
showToast(t('browser.manage_folders_hint'), 'info');
// Folder Management — Settings table
export function loadFoldersTable() {
const tbody = document.getElementById('foldersTableBody');
if (!tbody) return;
const entries = Object.entries(mediaFolders);
if (entries.length === 0) {
tbody.innerHTML = `<tr><td colspan="5" class="empty-state">
<div class="empty-state-illustration">
<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>
<p data-i18n="browser.folders_empty">${t('browser.folders_empty')}</p>
</div></td></tr>`;
return;
}
tbody.innerHTML = entries.map(([id, folder]) => {
const available = folder.available !== false;
const statusIcon = available
? '<span class="status-dot status-online">' + t('browser.folder_available') + '</span>'
: '<span class="status-dot status-offline">' + t('browser.folder_unavailable') + '</span>';
const enabledBadge = folder.enabled
? ''
: ' <span class="folder-disabled-badge">' + t('browser.folder_disabled') + '</span>';
return `<tr>
<td>${escapeHtml(id)}${enabledBadge}</td>
<td>${escapeHtml(folder.label)}</td>
<td class="path-cell" title="${escapeHtml(folder.path)}">${escapeHtml(folder.path)}</td>
<td>${statusIcon}</td>
<td class="actions-cell">
<button class="btn-icon" data-action="edit" data-folder-id="${escapeHtml(id)}" title="${t('browser.folder_edit')}">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>
</button>
<button class="btn-icon btn-danger-icon" data-action="delete" data-folder-id="${escapeHtml(id)}" title="${t('browser.folder_delete')}">
<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
</button>
</td>
</tr>`;
}).join('');
}
export function showAddFolderDialog() {
document.getElementById('folderDialogTitle').textContent = t('browser.folder_dialog.title_add');
document.getElementById('folderIsEdit').value = '';
document.getElementById('folderOriginalId').value = '';
document.getElementById('folderId').value = '';
document.getElementById('folderId').disabled = false;
document.getElementById('folderLabel').value = '';
document.getElementById('folderPath').value = '';
document.getElementById('folderEnabled').checked = true;
document.getElementById('folderDialog').showModal();
}
export function showEditFolderDialog(folderId) {
const folder = mediaFolders[folderId];
if (!folder) return;
document.getElementById('folderDialogTitle').textContent = t('browser.folder_dialog.title_edit');
document.getElementById('folderIsEdit').value = '1';
document.getElementById('folderOriginalId').value = folderId;
document.getElementById('folderId').value = folderId;
document.getElementById('folderId').disabled = true;
document.getElementById('folderLabel').value = folder.label;
document.getElementById('folderPath').value = folder.path;
document.getElementById('folderEnabled').checked = folder.enabled;
document.getElementById('folderDialog').showModal();
}
export function closeFolderDialog() {
@@ -857,5 +961,90 @@ export function closeFolderDialog() {
export async function saveFolder(event) {
event.preventDefault();
closeFolderDialog();
const isEdit = document.getElementById('folderIsEdit').value === '1';
const folderId = isEdit
? document.getElementById('folderOriginalId').value
: document.getElementById('folderId').value.trim();
const label = document.getElementById('folderLabel').value.trim();
const path = document.getElementById('folderPath').value.trim();
const enabled = document.getElementById('folderEnabled').checked;
if (!folderId || !label || !path) return;
const submitBtn = document.querySelector('#folderForm button[type="submit"]');
if (submitBtn) submitBtn.disabled = true;
try {
let response;
if (isEdit) {
response = await fetch(`/api/browser/folders/update/${encodeURIComponent(folderId)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
body: JSON.stringify({ label, path, enabled }),
});
} else {
response = await fetch('/api/browser/folders/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...getAuthHeaders() },
body: JSON.stringify({ folder_id: folderId, label, path, enabled }),
});
}
if (response.ok) {
closeFolderDialog();
showToast(t(isEdit ? 'browser.folder_updated' : 'browser.folder_created'), 'success');
await loadMediaFolders();
} else {
const result = await response.json().catch(() => ({}));
showToast(result.detail || t('browser.folder_save_error'), 'error');
}
} catch (error) {
console.error('Error saving folder:', error);
showToast(t('browser.folder_save_error'), 'error');
} finally {
if (submitBtn) submitBtn.disabled = false;
}
}
export async function deleteFolderConfirm(folderId) {
if (!await showConfirm(t('browser.folder_confirm_delete', { name: folderId }))) {
return;
}
try {
const response = await fetch(`/api/browser/folders/delete/${encodeURIComponent(folderId)}`, {
method: 'DELETE',
headers: getAuthHeaders(),
});
if (response.ok) {
showToast(t('browser.folder_deleted'), 'success');
await loadMediaFolders();
} else {
const result = await response.json().catch(() => ({}));
showToast(result.detail || t('browser.folder_delete_error'), 'error');
}
} catch (error) {
console.error('Error deleting folder:', error);
showToast(t('browser.folder_delete_error'), 'error');
}
}
// Legacy stub — now handled via settings table
export function showManageFoldersDialog() {
if (managementEnabled) {
// Switch to settings tab and scroll to the folders section
const switchTabFn = window.switchTab;
if (switchTabFn) switchTabFn('settings');
setTimeout(() => {
const section = document.getElementById('mediaFoldersSection');
if (section) {
section.setAttribute('open', '');
section.scrollIntoView({ behavior: 'smooth' });
}
}, 100);
} else {
showToast(t('browser.manage_folders_hint'), 'info');
}
}
+26 -1
View File
@@ -173,7 +173,27 @@
"browser.play_all_error": "Failed to play folder",
"browser.error_loading": "Error loading directory",
"browser.error_loading_folders": "Failed to load media folders",
"browser.manage_folders_hint": "Folder management coming soon! For now, edit config.yaml to add media folders.",
"browser.manage_folders_hint": "Folder management is disabled. Set media_folders_management: true in config.yaml to enable.",
"browser.unavailable": "Unavailable",
"browser.folder_available": "Available",
"browser.folder_unavailable": "Unavailable (path not reachable)",
"browser.folder_disabled": "disabled",
"browser.folder_edit": "Edit folder",
"browser.folder_delete": "Delete folder",
"browser.folder_created": "Media folder created successfully",
"browser.folder_updated": "Media folder updated successfully",
"browser.folder_deleted": "Media folder deleted successfully",
"browser.folder_save_error": "Failed to save media folder",
"browser.folder_delete_error": "Failed to delete media folder",
"browser.folder_confirm_delete": "Are you sure you want to delete the folder \"{name}\"?",
"browser.folders_description": "Media folders available for browsing. Folders on network shares show availability status.",
"browser.folders_empty": "No media folders configured. Click \"+\" to add one.",
"browser.folders_table.id": "ID",
"browser.folders_table.label": "Label",
"browser.folders_table.path": "Path",
"browser.folders_table.status": "Status",
"browser.folders_table.actions": "Actions",
"settings.section.media_folders": "Media Folders",
"browser.folder_dialog.title_add": "Add Media Folder",
"browser.folder_dialog.title_edit": "Edit Media Folder",
"browser.folder_dialog.folder_id": "Folder ID *",
@@ -185,6 +205,11 @@
"browser.folder_dialog.enabled": "Enabled",
"browser.folder_dialog.cancel": "Cancel",
"browser.folder_dialog.save": "Save",
"browser.list_header.name": "Name",
"browser.list_header.bitrate": "Bitrate",
"browser.list_header.duration": "Duration",
"browser.list_header.size": "Size",
"browser.showing_items": "Showing {from}\u2013{to} of {total}",
"browser.download_error": "Failed to download file",
"connection.reconnecting": "Connection lost. Reconnecting (attempt {attempt})...",
"connection.lost": "Connection lost. Server may be unavailable.",
+26 -1
View File
@@ -173,7 +173,27 @@
"browser.play_all_error": "Не удалось воспроизвести папку",
"browser.error_loading": "Ошибка загрузки каталога",
"browser.error_loading_folders": "Не удалось загрузить медиа папки",
"browser.manage_folders_hint": "Управление папками скоро появится! Пока редактируйте config.yaml для добавления медиа папок.",
"browser.manage_folders_hint": "Управление папками отключено. Установите media_folders_management: true в config.yaml для включения.",
"browser.unavailable": "Недоступна",
"browser.folder_available": "Доступна",
"browser.folder_unavailable": "Недоступна (путь не найден)",
"browser.folder_disabled": "отключена",
"browser.folder_edit": "Редактировать папку",
"browser.folder_delete": "Удалить папку",
"browser.folder_created": "Медиа папка успешно создана",
"browser.folder_updated": "Медиа папка успешно обновлена",
"browser.folder_deleted": "Медиа папка успешно удалена",
"browser.folder_save_error": "Не удалось сохранить медиа папку",
"browser.folder_delete_error": "Не удалось удалить медиа папку",
"browser.folder_confirm_delete": "Вы уверены, что хотите удалить папку \"{name}\"?",
"browser.folders_description": "Медиа папки для просмотра. Для сетевых ресурсов показан статус доступности.",
"browser.folders_empty": "Медиа папки не настроены. Нажмите \"+\" для добавления.",
"browser.folders_table.id": "ID",
"browser.folders_table.label": "Метка",
"browser.folders_table.path": "Путь",
"browser.folders_table.status": "Статус",
"browser.folders_table.actions": "Действия",
"settings.section.media_folders": "Медиа папки",
"browser.folder_dialog.title_add": "Добавить медиа папку",
"browser.folder_dialog.title_edit": "Редактировать медиа папку",
"browser.folder_dialog.folder_id": "ID папки *",
@@ -185,6 +205,11 @@
"browser.folder_dialog.enabled": "Включено",
"browser.folder_dialog.cancel": "Отмена",
"browser.folder_dialog.save": "Сохранить",
"browser.list_header.name": "Название",
"browser.list_header.bitrate": "Битрейт",
"browser.list_header.duration": "Длительность",
"browser.list_header.size": "Размер",
"browser.showing_items": "Показано {from}\u2013{to} из {total}",
"browser.download_error": "Не удалось скачать файл",
"connection.reconnecting": "Соединение потеряно. Переподключение (попытка {attempt})...",
"connection.lost": "Соединение потеряно. Сервер может быть недоступен.",
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "media-server-frontend",
"version": "1.0.0",
"version": "0.1.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "media-server-frontend",
"version": "1.0.0",
"version": "0.1.2",
"devDependencies": {
"esbuild": "^0.27.4"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "media-server-frontend",
"version": "1.0.0",
"version": "0.1.2",
"private": true,
"description": "Frontend build tooling for media server WebUI",
"scripts": {
+1 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "media-server"
version = "0.1.0"
version = "0.1.2"
description = "REST API server for controlling system-wide media playback"
readme = "README.md"
license = { text = "MIT" }
@@ -32,7 +32,6 @@ dependencies = [
"pyyaml>=6.0",
"mutagen>=1.47.0",
"pillow>=10.0.0",
"packaging>=23.0",
]
[project.optional-dependencies]