diff --git a/RELEASE_NOTES_v0.5.3.md b/RELEASE_NOTES_v0.5.3.md deleted file mode 100644 index c534cb02..00000000 --- a/RELEASE_NOTES_v0.5.3.md +++ /dev/null @@ -1,173 +0,0 @@ -## OxiCloud v0.5.3 — Security, Stability & Kubernetes Ready - -A community-powered release with **42 commits** from **7 contributors**, touching **106 files** with nearly **2,000 lines of improvements**. This release focuses on **critical security hardening**, **memory & reliability optimizations**, **Kubernetes-native deployment via Helm**, and a wave of **UI/UX and WebDAV fixes** that dramatically improve the day-to-day experience. - ---- - -### Highlights - -- **Kubernetes Helm Chart** — First-class Kubernetes deployment with a full Helm chart, including optional WOPI integration -- **SQLx Migration System** — Replaced custom schema loader with `sqlx::migrate!()` for robust, versioned database migrations -- **Security Advisory Fix** — Patched RUSTSEC-2026-0037 (quinn-proto) to eliminate a known vulnerability -- **Folder Ownership Verification** — Files can no longer be moved to another user's folder, closing a critical access control gap -- **Drag & Drop into Breadcrumbs** — Drag files directly into breadcrumb folders for faster file organization -- **Download Progress for Large Files** — New progress bar for files >2GB with correct 64-bit math -- **Thumbnail Timeout Protection** — Large image processing now has configurable timeouts to prevent server hangs - ---- - -### Features - -- **SQL migration system using `sqlx::migrate!()`** — Automatic, versioned schema migrations on startup replace the previous manual schema loader (@jaredwolff — #191) -- **Helm chart for Kubernetes deployment** — Full Helm chart with configurable values, optional WOPI sidecar, and comprehensive documentation (@nk-designz — #198) -- **Drag & drop files into breadcrumb folders** — Move files by dragging them onto any breadcrumb folder in the navigation bar (@EdouardVanbelle — #238) -- **Download progress bar for files >2GB** — Inline viewer now shows real-time download progress with correct 64-bit float division, avoiding 32-bit overflow (@BillionClaw — #227) -- **Thumbnail generation timeout protection** — Configurable timeout (default 30s) prevents large image processing from hanging the server indefinitely (@DioCrafts — #242) -- **Add missing home icon** — Breadcrumb now displays the proper home icon for root folder navigation (@EdouardVanbelle — #235) - -### Security & Access Control - -- **Fix RUSTSEC-2026-0037** — Updated `quinn-proto` to 0.11.14 to patch a known security advisory (@jaredwolff — b6bcb7d) -- **Verify target folder ownership on file move** — Moving a file now validates that the caller owns the destination folder, preventing cross-user file injection (@BillionClaw — #224) -- **Enforce storage quota on WebDAV PUT uploads** — WebDAV uploads now check storage quota before persisting, returning 507 Insufficient Storage when exceeded — previously only REST and chunked uploads had this check (@BillionClaw — #220) -- **Cap admin initial quota to available disk space** — User creation no longer sets quotas exceeding actual available disk space (@BillionClaw — #226) -- **Resolve CSP blocking and session refresh loop** — Fixed Content Security Policy violations blocking inline styles and an infinite session refresh loop (@BillionClaw — #211) - -### Performance & Memory - -- **Drop encoded image data after decoding** — Explicitly frees the original encoded buffer after image decoding, reducing peak memory consumption during thumbnail generation by the original file size (@BillionClaw — #228) -- **Thumbnail generation timeout** — Wraps `spawn_blocking` in `tokio::time::timeout` so a single slow image can't block the thumbnail pipeline (@DioCrafts — #242) - -### Bug Fixes - -**Nextcloud Compatibility:** -- **Fix Nextcloud sync conflict** — Replaced static UUID-based ETags with content-hash ETags, resolving persistent sync conflicts in the Nextcloud desktop and mobile clients (@jaredwolff — #207) - -**WebDAV:** -- **Preserve correct status codes for rename/move failures** — AlreadyExists→409, NotFound→404, AccessDenied→403 instead of blanket 500 errors (@BillionClaw — #222) -- **Enforce storage quota on PUT uploads** — Closes a gap where WebDAV could bypass quota checks (@BillionClaw — #220) - -**Files & Storage:** -- **Batch folder deletion fails** — Added debug logging to diagnose and fix batch trash operation failures (@BillionClaw — #216) -- **Improve error messages for file/folder already exists** — More descriptive error messages when duplicate file/folder names are encountered (@BillionClaw — #225) -- **Correct shared link URL to include `/api` prefix** — Shared links previously generated 404 URLs missing the API path prefix (@BillionClaw — #223) - -**Calendar & Contacts:** -- **Change calendar `owner_id` from String to Uuid** — Aligns calendar ownership with the native UUID type used everywhere else, fixing lookup failures (@BillionClaw — #208) -- **Allow RGBA colors in calendar events** — Calendar color validation now accepts RGBA format in addition to RGB (@JVMerkle — #202) - -**Trash:** -- **Add missing display fields to `TrashedItemDto`** — Added `category`, `icon_class`, and `icon_special_class` fields so the trash view renders file type information correctly (@BillionClaw — #221) - -**UI/UX:** -- **Resolve broken menu navigation** — Fixed menu items not responding to clicks (@BillionClaw — #212) -- **Resolve dark mode toggle and file search errors** — Dark mode toggle now derives state from localStorage; empty folder_id no longer causes search errors (@BillionClaw — #218) -- **Photos view bleeding into trash view** — Fixed CSS isolation issue where photos grid styles leaked into the trash panel (@jaredwolff — #196) -- **Align size values in table view** — File sizes now use `tabular-nums` for proper column alignment (@BillionClaw — #219) -- **WOPI public base URL for Docker** — Added `OXICLOUD_WOPI_PUBLIC_BASE_URL` env var support so WOPI document editing works behind reverse proxies in Docker (@BillionClaw — #234) - -**Internationalization:** -- **Use translation keys for upload notification titles** — Replaced hardcoded English/Spanish strings with proper i18n lookup (@BillionClaw — #217) -- **Add missing `dialogs.share_folder` translation key** — Added to all 14 locale files, fixing share dialog failures for folders (@BillionClaw — #215) - -**Build, CI & Deployment:** -- **Add PostgreSQL service to Docker publish workflow** — The Docker Hub release CI job was failing because it lacked the PostgreSQL service required by tests, causing missing container images for v0.5.2 (@BillionClaw — #214) -- **Remove `target-cpu=native` from Dockerfile** — Ensures Docker images are portable across different CPU architectures (@jaredwolff — a0ee538) -- **ARMv7 32-bit compilation overflow** — Fixed integer overflow on 32-bit ARM targets (@BillionClaw — #209) -- **Resolve clippy warnings and rustfmt issues for CI compliance** — Cleaned up all remaining linting issues (@zjean — #188) -- **Update CI references from `db/schema.sql` to sqlx migrations** — Aligned CI pipelines with the new migration system (@jaredwolff — f6e2b30) -- **Add pre-commit checks to CLAUDE.md** — Documented required `cargo fmt` + `cargo clippy` checks (@jaredwolff — #192) - -**Migrations:** -- **Add ALTER TABLE fallback for `media_sort_date` column** — Handles pre-existing tables gracefully during migration (@jaredwolff — #195) - -### Documentation - -- **Add feature status table to README** — Clear overview of which features are stable, beta, or planned (@BillionClaw — #210) -- **Fix incorrect path in development guide** — Corrected branch path references in CONTRIBUTING.md (@BillionClaw — #213) -- **Helm chart documentation** — Comprehensive deployment guide for Kubernetes users (@nk-designz — #198) -- **Update README.md & example.env** — Improved documentation for remote access setup (@raenur — #197) - -### Developer Experience - -- **Dev-mode static assets without cache** — When `PROFILE=dev`, static assets are served directly from `/static` with no caching, enabling faster frontend iteration (@EdouardVanbelle — #236) -- **Remove duplicate breadcrumb home-folder code** — Refactored redundant logic in breadcrumb handling (@EdouardVanbelle — 276b9ff) -- **Apply rust format + fix clippy warning** — Code style cleanup (@EdouardVanbelle — #240) - ---- - -### Stats - -| Metric | Value | -|---|---| -| Commits | 42 | -| Contributors | 7 | -| Files changed | 106 | -| Insertions | +1,997 | -| Deletions | −882 | -| Issues closed | #82, #92, #101, #102, #104, #107, #108, #124, #189, #193, #230 | -| PRs merged | 35 | - ---- - -### 🙏 Contributor Acknowledgements - -This release would not have been possible without the incredible dedication and talent of every single contributor. The OxiCloud community continues to grow, and every contribution — from a one-line fix to a 22-commit marathon — makes this project stronger. - ---- - -#### @BillionClaw — 22 commits ⭐ MVP of this release - -An absolutely extraordinary contribution. **BillionClaw** single-handedly tackled the majority of this release, delivering a sweeping wave of fixes that touched every layer of OxiCloud — from **WebDAV quota enforcement** and **folder ownership security**, to **dark mode toggle fixes**, **i18n completeness**, **trash view rendering**, **shared link URLs**, **CI pipeline fixes**, and **ARMv7 compilation support**. The depth and breadth of these contributions is remarkable. Every fix came with clear commit messages, proper issue references, and thoughtful descriptions. BillionClaw didn't just fix bugs — they systematically audited and hardened OxiCloud's core functionality. The download progress bar for >2GB files and the thumbnail memory optimization show a keen eye for performance and user experience. **Thank you, BillionClaw, for this exceptional level of commitment to OxiCloud. You are a pillar of this community.** 🏆 - ---- - -#### @jaredwolff — 8 commits - -**Jared** continues to be one of OxiCloud's most impactful contributors. This release features his landmark **SQLx migration system** — a foundational infrastructure change that replaces the fragile custom schema loader with proper versioned migrations, ensuring rock-solid database upgrades for every deployment going forward. He also patched the critical **RUSTSEC-2026-0037 security advisory**, fixed the persistent **Nextcloud sync conflict** that plagued desktop and mobile clients, resolved the **photos-view-in-trash CSS leak**, removed the non-portable `target-cpu=native` from Docker builds, and aligned the entire CI pipeline with the new migration system. Jared's contributions consistently tackle the hardest, most impactful problems. **Thank you, Jared, for your continued engineering excellence and for making OxiCloud more reliable and secure with every release.** - ---- - -#### @EdouardVanbelle — 5 commits - -**Edouard** brought a beautiful **drag & drop into breadcrumbs** feature that makes file organization feel natural and intuitive. He also added the missing home icon, eliminated duplicate breadcrumb code, improved the developer experience with cache-free dev-mode static assets, and cleaned up code style. Every contribution shows a strong focus on user experience and code quality. **Thank you, Edouard, for bringing polish and elegance to OxiCloud's interface. Your UI contributions make a real difference in how people interact with the platform every day.** - ---- - -#### @nk-designz (Nico Kahlert) — 2 commits - -**Nico** opened the door to **enterprise Kubernetes deployment** by creating a complete Helm chart with configurable values, optional WOPI integration, and thorough documentation. This is a game-changer for teams looking to deploy OxiCloud in production Kubernetes clusters. **Thank you, Nico, for bringing OxiCloud to the cloud-native world. This Helm chart makes professional deployment accessible to an entirely new audience.** - ---- - -#### @raenur (Nathan Shepperd) — 2 commits - -**Nathan** contributed practical improvements to the **README** and **example.env** with documentation suggestions for accessing OxiCloud remotely after first install — exactly the kind of first-time-user perspective that makes onboarding smoother for everyone. **Thank you, Nathan, for thinking about the new user experience and making the first steps with OxiCloud clearer and more welcoming.** - ---- - -#### @JVMerkle (Julian Merkle) — 1 commit - -**Julian** fixed the calendar color validation to support **RGBA colors**, a small but important change that unblocks users who rely on RGBA color specs in their CalDAV clients. **Thank you, Julian, for this targeted and well-crafted fix. CalDAV compatibility improves with every contribution like this.** - ---- - -#### @zjean — 1 commit - -**zjean** resolved all remaining **clippy warnings and rustfmt issues** to bring the codebase into full CI compliance — a foundational cleanup that keeps the build green for everyone. **Thank you, zjean, for your dedication to code quality and for ensuring OxiCloud maintains a clean, warning-free codebase.** - ---- - -### New Contributors 🎉 - -A warm welcome to the contributors making their first contribution to OxiCloud in this release: - -* @BillionClaw made their first contribution in https://github.com/DioCrafts/OxiCloud/pull/208 -* @EdouardVanbelle made their first contribution in https://github.com/DioCrafts/OxiCloud/pull/235 -* @nk-designz made their first contribution in https://github.com/DioCrafts/OxiCloud/pull/198 -* @raenur made their first contribution in https://github.com/DioCrafts/OxiCloud/pull/197 -* @JVMerkle made their first contribution in https://github.com/DioCrafts/OxiCloud/pull/202 - ---- - -**Full Changelog**: https://github.com/DioCrafts/OxiCloud/compare/v0.5.2...v0.5.3 diff --git a/build.rs b/build.rs index 553952c4..56a9b798 100644 --- a/build.rs +++ b/build.rs @@ -35,6 +35,7 @@ const INDEX_VIEW_CSS: &[&str] = &[ "views/trash.css", "views/photos.css", "views/photosLightbox.css", + "views/music.css", ]; // ═══════════════════════════════════════════════════════════════════════════════ diff --git a/src/interfaces/api/routes.rs b/src/interfaces/api/routes.rs index 2b323019..ff343816 100644 --- a/src/interfaces/api/routes.rs +++ b/src/interfaces/api/routes.rs @@ -386,6 +386,10 @@ pub fn create_api_routes(app_state: &Arc) -> Router> { "/{playlist_id}/shares", get(music_handler::get_playlist_shares), ) + .route( + "/audio-metadata/{file_id}", + get(music_handler::get_audio_metadata), + ) .with_state(music_svc.clone()); router = router.nest("/playlists", music_router); diff --git a/static/css/views/music.css b/static/css/views/music.css index 8bb0c601..7e5c18d2 100644 --- a/static/css/views/music.css +++ b/static/css/views/music.css @@ -8,11 +8,52 @@ display: block; } -.music-toolbar { +/* ========= Full-width empty state (zero playlists) ========== */ +.music-empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 80px 40px; + min-height: calc(100vh - 260px); +} + +.music-empty-state-icon { + width: 96px; + height: 96px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; - justify-content: flex-end; - padding: 8px; + justify-content: center; + margin-bottom: 24px; + box-shadow: 0 8px 32px rgba(102, 126, 234, 0.25); +} + +.music-empty-state-icon i { + font-size: 40px; + color: #fff; +} + +.music-empty-state-title { + margin: 0 0 8px; + font-size: 20px; + font-weight: 600; + color: var(--color-text, #2d3748); +} + +.music-empty-state-desc { + margin: 0 0 28px; + font-size: 14px; + color: var(--color-text-muted, #718096); + max-width: 360px; + line-height: 1.5; +} + +/* ========= Toolbar (when playlists exist) ========== */ +.music-toolbar { + display: none; } .music-content { @@ -20,6 +61,11 @@ gap: 0; height: calc(100vh - 200px); min-height: 400px; + transition: height 0.3s ease; +} + +.music-player-active .music-content { + height: calc(100vh - 290px); } .music-sidebar { @@ -32,19 +78,43 @@ } .music-sidebar-header { - padding: 16px; + padding: 14px 16px; border-bottom: 1px solid var(--border-color, #e2e8f0); + display: flex; + align-items: center; + justify-content: space-between; } .music-sidebar-header h3 { margin: 0; - font-size: 14px; + font-size: 12px; font-weight: 600; color: var(--text-secondary, #64748b); text-transform: uppercase; letter-spacing: 0.5px; } +.music-sidebar-add-btn { + width: 28px; + height: 28px; + border-radius: 6px; + border: none; + background: var(--color-accent-gradient, linear-gradient(135deg, #ff5e3a, #ff2d55)); + color: #fff; + font-size: 13px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.15s, box-shadow 0.15s; + box-shadow: 0 2px 8px var(--color-accent-shadow, rgba(255, 94, 58, 0.25)); +} + +.music-sidebar-add-btn:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--color-accent-shadow, rgba(255, 94, 58, 0.35)); +} + .music-playlist-list { flex: 1; overflow-y: auto; @@ -70,15 +140,16 @@ } .music-playlist-icon { - width: 40px; - height: 40px; - border-radius: 6px; + width: 36px; + height: 36px; + border-radius: 8px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: #fff; - font-size: 16px; + font-size: 14px; + flex-shrink: 0; } .music-playlist-item-info { @@ -147,8 +218,9 @@ } .music-playlist-cover { - width: 180px; - height: 180px; + width: 140px; + height: 140px; + min-width: 140px; border-radius: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; @@ -178,9 +250,27 @@ .music-playlist-actions { display: flex; - gap: 12px; + gap: 8px; margin-bottom: 24px; flex-wrap: wrap; + align-items: center; +} + +.music-playlist-actions .btn { + padding: 8px 16px; + font-size: 13px; + border-radius: 8px; +} + +.music-playlist-actions .btn i { + font-size: 13px; +} + +/* Icon-only action buttons (no text label) */ +.music-playlist-actions .btn:not(:has(span)) { + padding: 8px 12px; + min-width: 36px; + justify-content: center; } .music-track-list { @@ -218,6 +308,10 @@ background: var(--hover-bg, #f1f5f9); } +.music-track.selected { + background: var(--active-bg, #dbeafe); +} + .music-track-col { padding: 0 8px; } @@ -596,10 +690,20 @@ display: flex; align-items: center; gap: 8px; - min-width: 140px; + min-width: 160px; justify-content: flex-end; } +.player-close-btn { + opacity: 0.5; + transition: opacity 0.15s ease; + margin-left: 4px; +} + +.player-close-btn:hover { + opacity: 1; +} + .player-volume-slider { width: 80px; display: flex; @@ -652,6 +756,20 @@ overflow: hidden; } +.player-queue::after { + content: ''; + position: absolute; + bottom: -8px; + right: 60px; + width: 14px; + height: 14px; + background: var(--bg-primary, #fff); + border-right: 1px solid var(--border-color, #e2e8f0); + border-bottom: 1px solid var(--border-color, #e2e8f0); + transform: rotate(45deg); + z-index: -1; +} + .player-queue.hidden { display: none; } @@ -775,6 +893,10 @@ background: var(--active-bg, #dbeafe); } +.music-track.playing.selected { + background: var(--active-bg, #dbeafe); +} + .music-track.playing .music-track-name { color: var(--primary-color, #667eea); } @@ -789,6 +911,10 @@ } /* Repeat one icon */ +.player-btn.repeat-one i { + position: relative; +} + .player-btn.repeat-one i::after { content: '1'; font-size: 8px; @@ -812,6 +938,11 @@ border-color: var(--border-color, #334155); } +[data-theme="dark"] .player-queue::after { + background: var(--bg-secondary, #1e293b); + border-color: var(--border-color, #334155); +} + [data-theme="dark"] .player-queue-header { border-color: var(--border-color, #334155); } @@ -840,6 +971,10 @@ gap: 8px; } + .music-player-active .music-content { + height: calc(100vh - 270px); + } + .player-track-info { min-width: auto; max-width: 120px; @@ -894,3 +1029,256 @@ width: auto; } } + +/* ========== Drag handle column ========== */ +.music-track-drag { + width: 28px; + flex: 0 0 28px; + cursor: grab; + color: var(--text-secondary, #888); + opacity: 0; + transition: opacity 0.15s; + display: flex; + align-items: center; + justify-content: center; +} +.music-track-header .music-track-drag { + cursor: default; +} +.music-track:hover .music-track-drag { + opacity: 0.6; +} +.music-track-drag:active { + cursor: grabbing; +} +.music-track.dragging { + opacity: 0.35; + background: var(--bg-hover, #f0f0f0); +} +.music-track.drag-over { + border-top: 2px solid var(--primary, #4a90d9); + margin-top: -2px; +} + +/* ========== Track actions column ========== */ +.music-track-actions { + width: 36px; + flex: 0 0 36px; + display: flex; + align-items: center; + justify-content: center; +} +.music-track-remove-btn { + background: none; + border: none; + color: var(--text-secondary, #888); + cursor: pointer; + padding: 4px 6px; + border-radius: 4px; + opacity: 0; + transition: opacity 0.15s, color 0.15s; + font-size: 0.85rem; +} +.music-track:hover .music-track-remove-btn { + opacity: 1; +} +.music-track-remove-btn:hover { + color: var(--danger, #e74c3c); + background: rgba(231, 76, 60, 0.08); +} + +/* ========== Cover art ========== */ +.music-playlist-cover { + cursor: pointer; + position: relative; + overflow: hidden; +} +.music-cover-img { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: inherit; +} +.music-cover-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0,0,0,0.4); + color: #fff; + font-size: 1.1rem; + opacity: 0; + transition: opacity 0.2s; + border-radius: inherit; +} +.music-playlist-cover:hover .music-cover-overlay { + opacity: 1; +} + +/* ========== Public badge ========== */ +.music-public-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + font-size: 0.72rem; + background: rgba(74, 144, 217, 0.12); + color: var(--primary, #4a90d9); + border-radius: 12px; + margin-top: 4px; +} +.music-public-badge.hidden { + display: none; +} +#music-toggle-public-btn.active { + color: var(--primary, #4a90d9); + background: rgba(74, 144, 217, 0.1); +} + +/* ========== Shares management dialog ========== */ +.music-shares-overlay { + position: fixed; + inset: 0; + background: rgba(0,0,0,0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; +} +.music-shares-panel { + background: var(--bg-primary, #fff); + border-radius: 12px; + width: 420px; + max-width: 90vw; + max-height: 80vh; + display: flex; + flex-direction: column; + box-shadow: 0 8px 32px rgba(0,0,0,0.18); +} +.music-shares-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 20px; + border-bottom: 1px solid var(--border-color, #eee); +} +.music-shares-header h3 { + margin: 0; + font-size: 0.95rem; + display: flex; + align-items: center; + gap: 8px; +} +.music-shares-close-btn { + background: none; + border: none; + cursor: pointer; + color: var(--text-secondary, #888); + padding: 4px; + font-size: 1rem; +} +.music-shares-close-btn:hover { + color: var(--text-primary, #333); +} +.music-shares-body { + flex: 1; + overflow-y: auto; + padding: 12px 20px; + min-height: 60px; +} +.music-shares-loading { + text-align: center; + padding: 20px; + color: var(--text-secondary, #888); +} +.music-shares-empty { + text-align: center; + color: var(--text-secondary, #888); + padding: 16px 0; + margin: 0; +} +.music-share-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid var(--border-color, #f0f0f0); +} +.music-share-item:last-child { + border-bottom: none; +} +.music-share-user { + flex: 1; + font-size: 0.88rem; + display: flex; + align-items: center; + gap: 6px; +} +.music-share-perm { + font-size: 0.78rem; + color: var(--text-secondary, #888); +} +.music-share-remove-btn { + background: none; + border: none; + cursor: pointer; + color: var(--text-secondary, #888); + padding: 4px; + transition: color 0.15s; +} +.music-share-remove-btn:hover { + color: var(--danger, #e74c3c); +} +.music-shares-add { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + border-top: 1px solid var(--border-color, #eee); + flex-wrap: wrap; +} +.music-shares-input { + flex: 1; + min-width: 120px; + padding: 6px 10px; + border: 1px solid var(--border-color, #ddd); + border-radius: 6px; + font-size: 0.88rem; + background: var(--bg-secondary, #f9f9f9); + color: var(--text-primary, #333); +} +.music-shares-input:focus { + outline: none; + border-color: var(--primary, #4a90d9); +} +.music-shares-write-label { + font-size: 0.78rem; + display: flex; + align-items: center; + gap: 4px; + color: var(--text-secondary, #666); + white-space: nowrap; +} + +/* ========== Dark mode ========== */ +[data-theme="dark"] .music-shares-panel { + background: var(--bg-primary, #1e1e1e); +} +[data-theme="dark"] .music-shares-input { + background: var(--bg-secondary, #2d2d2d); + border-color: var(--border-color, #444); + color: var(--text-primary, #e0e0e0); +} +[data-theme="dark"] .music-track.dragging { + background: var(--bg-hover, #2d2d2d); +} +[data-theme="dark"] .music-track-remove-btn:hover { + background: rgba(231, 76, 60, 0.15); +} +[data-theme="dark"] .music-empty-state-title { + color: var(--color-text, #e0e0e0); +} +[data-theme="dark"] .music-empty-state-desc { + color: var(--color-text-muted, #888); +} diff --git a/static/js/features/library/music.js b/static/js/features/library/music.js index c2f5bfb2..eb3687d5 100644 --- a/static/js/features/library/music.js +++ b/static/js/features/library/music.js @@ -82,26 +82,39 @@ const musicView = { return typeof i18n !== 'undefined' && i18n.t ? i18n.t(key) : fallback || key; }; + // Empty state: no playlists at all — show full-width centered onboarding + if (this.playlists.length === 0) { + this._container.innerHTML = ` +
+
+ +
+

${t('music.no_playlists', 'No playlists yet')}

+

${t('music.empty_hint', 'Create your first playlist to start organizing your music')}

+ +
+ `; + const createBtn = document.getElementById('music-create-playlist-btn'); + if (createBtn) { + createBtn.addEventListener('click', () => this._showCreatePlaylistDialog()); + } + return; + } + + // Normal layout: sidebar + main this._container.innerHTML = ` -
- -

${t('music.playlists', 'Playlists')}

+
-
- ${this.playlists.length === 0 ? ` -
- -

${t('music.no_playlists', 'No playlists yet')}

-
- ` : ''} -
+
@@ -111,12 +124,15 @@ const musicView = {