next
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b1acc5b5b3 |
Add German, Portuguese, and Indonesian translations (#415)
* Add German, Portuguese, and Indonesian translations; fix i18n coverage gaps Extends the existing English/Polish/Japanese/Simplified Chinese i18n system to seven languages total. Also fixes several pre-existing i18n coverage gaps found while auditing: the recent-tracks list, search placeholder, and trash empty-state were hardcoding English text instead of using the translation system; a presence-panel legend lacked data-i18n attributes; upload/job/ playlist error toasts were untranslated; and library list content did not refresh on a live language switch. Widened the settings dropdown to fit the longest new language name and the device-select to stop truncating longer translated values. Bumps the Unraid template pin to 0.13.0. * Remove unused plural import in job.js Flagged in PR review: job.js imports plural from i18n.js but never calls it, only t(). * Native-speaker QA pass on all translations Fixes real mistranslations (German "schleifen" for loop, "Skala" for musical scale, Indonesian countdown/count-in mixup, Chinese Alpha badge), grammar bugs (Polish aria-labels requiring an unavailable grammatical case, singular/plural adjective agreement in playlist skip messages), inconsistent terminology within each language, and a stray three-dot ellipsis instead of the single character used everywhere else. Converts playlist.skip.* from t() to plural() with proper singular/plural forms across all seven languages, since Portuguese and Polish adjectives don't inflect correctly as flat strings. --------- Co-authored-by: Thales <> |
||
|
|
fea4fcf145 |
Count-in, and a transport footer rebuilt around the studio's column grid (#369)
* feat(playback): count-in before playback and exports, redesign transport footer Count-in (#269): one bar of click count-in leads into playback and into audio exports, independent of the running click track (a clean backing track can still get a count-in). The lead-in math is defined once and mirrored between metronome.js and click_render.py, pinned by parity tests on both sides. - Playback: audioEngine schedules stem playback on a future ctx-time start so the count-in clicks land in the silent gap before the song begins; the metronome schedules them through the same clock mapping the running click already uses. - Export: stems are delayed via ffmpeg's adelay and the click WAV is rendered in output coordinates when a count-in is requested, so it isn't re-trimmed by the region -ss like a plain click. Also rebuilds the transport footer around labelled control groups (Transport, Position, Speed, Click Track) instead of a right-click popover: playback speed collapses to three practice presets (0.25x / 0.5x / 1x), the click track gets an on/off toggle and a count-in switch, and the track-info block collapses from four stacked detail rows to one compact line. * fix(ui): hide click-track panel by default before any track is loaded The panel lost its default "hidden" class when it changed from a right-click popover to always-inline (#269 follow-up) -- on a fresh page load, before any track was ever picked, nothing forced it hidden, so "Ready to import a track" showed a full set of live- looking click controls for a track that didn't exist. * polish(ui): footer wave time labels, orphan dividers, visible click-volume readout - Time labels above the footer's mini waveform, matching the main ruler. - Divider marks between control clusters in the footer's controls row, hidden via ResizeObserver when wrapping strands one at the end of a line with nothing after it to separate. - Click volume percentage shown next to the slider again instead of screen-reader-only -- a level you can only learn by hovering isn't one you can reliably match between sessions. - Count-in switched from a checkbox to a press-to-toggle button, matching the click on/off control beside it (both answer "is this on for the next play?", so they read as the same kind of control now). * fix(playback): count-in never armed on the chunked audio engine The chunked engine is the default playback path (engineMode() falls back to "chunked" unless a debug localStorage flag forces "fulldecode") -- but count-in support (play(leadIn), supportsCountIn, a clamped getCurrentTime during the lead-in) was only ever added to audioEngine.js, the full-decode path. Since _armCountIn() bails out whenever eng.supportsCountIn is falsy, count-in silently never armed for any track played through the engine essentially everyone actually uses, and playback started immediately regardless of the toggle. Mirrors the same fix in chunkedAudioEngine.js: play() accepts a leadIn and schedules the first chunk that far in the future (falling back to the existing 10ms/50ms margins when there is no count-in), and getCurrentTime() clamps to the start offset during that gap instead of reading negative. Verified directly against the running engine clock (not just DOM text, which rounds to whole seconds): the position holds at the start offset for the full lead-in and then advances normally, pausing mid-count-in stops cleanly with no phantom scheduled audio, and replaying re-arms a fresh count-in. * polish(ui): align the footer with the lane column, move track info into it The footer's waveform strip ran the full width of the window while the lane waveforms above it start after the 300px stems/mixer panel, so the same position sat at two different x positions in the two strips and neither ruler's ticks lined up with the other's. The footer is now two columns on the studio's own grid. Everything time-related -- the control clusters, the waveform, its ruler and the detection note -- sits in the right column and starts exactly where the lane waveforms start, running flush to the window edge like they do. The track identity (art, title, meta, favourite, Export Mix) moves into the left column under the mixer panel and shares its width and 14px padding, so titles, stem names and the "Mixer" heading share one left edge down the page. That also drops a whole row from the footer: 255px tall where the three stacked tiers were 318px. - The 300px is now --daw-col-w, read by the stems panel, the label cell above it and the footer, instead of being hardcoded in each. - The waveform strip is full-bleed with top/bottom rules rather than a rounded inset panel: a side border would have offset the canvas by its own width, which is exactly the misalignment being fixed. - Both rulers share tickStep(), so a time is labelled at the same x in each. - The export menu opens up and to the right; right-aligned from the left column it would have hung over the sidebar. Grid becomes a press-to-toggle button matching the click and count-in buttons beside it -- click opens the editor and lights it, click again closes it. Its lit state is synced inside toggleBeatGridEditor, the one place every open and close runs through, so Done, Escape and losing the beat grid all leave the button correct. The G shortcut is gone: the button says what it does now, and a single letter bound to a modal editor is easy to hit by accident. * polish(ui): close the footer waveform strip's open left edge The strip carries only top and bottom rules -- side borders were dropped so the canvas would land exactly on the lane waveforms' left edge -- which left its left end open, the two rules stopping in mid-air. Drawn as an outset box-shadow rather than a border-left: a border sits inside the box and would push the canvas a pixel off the alignment it exists to keep. The line falls on the same x as the stems panel's right border, so that seam now runs unbroken from the top of the mixer to the bottom of the strip. * fix(ui): ticking an export option no longer closes the export menu Every interactive element in the export menu called stopPropagation so the document-level dismiss handler would not fire, but the two option checkboxes had no click handler at all -- so ticking one bubbled out and closed the menu under the pointer. That was survivable with one checkbox. This branch adds a second ("Add count-in"), and wanting both is the normal case for practising to a click: the first tick closed the menu, and the second needed it reopened. Guard the panel itself rather than adding a third per-element stopPropagation that the next option added would forget: a click inside a menu is not a click away from it. Nothing depended on the bubble to close the menu -- the export actions close it themselves through enterBusy() -> closePanel(). --------- Co-authored-by: Thales <> |
||
|
|
04c0b4dee1 |
feat(ui-refactor): DAW redesign, library overhaul, sections, analysis, and transport (#67)
* feat(ui): redesign frontend to flat dark DAW aesthetic Replace the glassmorphism panel layout with a flat, dark DAW-style UI matching the Stemdeck-static.html reference design. - Rewrite index.html: new topbar with composer pill (URL zone + stem chips + Process button), sidebar rail, track header with energy bars and Key/BPM/LUFS analysis, section ribbon, waveform ruler, horizontal mixer lanes, and transport footer - Add daw.css: complete new design system using .daw prefix; flat solid surfaces, stem color-mix() chips, horizontal mixer lane rows styled for the JS-built .lane-header.mx-row elements - Update variables.css: new palette tokens (--bg, --bg-2, --panel, --border, stem colours, --accent) with legacy compat aliases for waves.css All 65 JS-required IDs and hooks (.app, .url-wrap, .stem-choice, .stem-list, .mixer-column, .energy-row, etc.) are preserved. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ui): topbar and composer cleanup, notification panel, layout tweaks - Remove duplicate SVG logo; keep text-only wordmark - Add "All" toggle before stem chips in composer pill - Expand composer pill max-width 920→1080px - Replace topbar "new release available" text with notification panel under bell button (dropdown card, outside-click close, version relay via MutationObserver from brandVersion element) - Track header grid: info card 320→380px, energy panel fixed 200px - Hide .daw-version from topbar (content surfaced in notif panel) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ui): expand composer pill to full topbar width Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ui): restore waveform artstyle and wire All stem toggle - Remove daw.css overrides for .wave-scroll/.wave-canvas/.waves-grid/.loop-region that shadowed waves.css's dark background, golden scrollbar, and loop marker visual styles; only keep layout wrapper (.daw-wave-panel flex column) - Add .daw .wave-scroll flex:1 override so wave fills full panel height (zoom toolbar now lives in .daw-wave-header outside .wave-editor) - Add wireAllButton() in main.js: toggles all stems on/off via selectedStems, syncs aria-pressed state when individual chips are clicked - Remove data-stem="all" from All button to avoid main.js stem handler picking it up Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ui): remove waves.css wave-editor padding/radius in flat layout waves.css applies padding:12px 14px 14px and border-radius:12px to any .wave-editor element. Our .daw-wave-panel.wave-editor was inheriting those, creating a visible gap/frame around the waveform canvas instead of a flush edge-to-edge dark panel. Strip padding, min-height, and border-radius via !important overrides so the wave-scroll fills the panel completely with its dark background and golden scrollbar as intended by waves.css. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ui): restore SVG waveform layer — fix height chain and remove 48px offsets waves.css hides #multitrack-container (opacity:0) and shows the custom stem-waveform-layer (SVG min/max peaks) in the active state. Two issues prevented this from working in our new layout: 1. Height chain broken: waves-column used height:calc(100%-34px) but wave-canvas had no explicit height, so the percentage never resolved. Fix: .daw .wave-canvas { height: 100% }. 2. 48px inner stem strip: the original layout had a 48px icon strip inside the wave panel, so waves.css offsets multitrack-container and stem-waveform-layer by left:48px. Our mixer is a separate 300px panel, so those offsets must be zeroed out. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ui): show WaveSurfer native bar rendering, hide SVG waveform layer waves.css hides #multitrack-container (opacity:0, position:absolute) in favour of the custom SVG stem-waveform-layer. User wants the WaveSurfer bar style instead (vertical bars with gaps on dark background). - Force #multitrack-container opacity:1 and position:relative so it is visible and contributes to waves-column height - Hide .stem-waveform-layer entirely - Set waves-column height:auto so it sizes from WaveSurfer's injected height - Remove now-redundant inset:0 override (only needed for absolute positioning) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ui): align mixer rows with waveform, fix transport buttons, remove zoom toolbar - Transport footer: add display:flex !important to override waves.css grid - Wave/mixer gap: zero border on .daw .wave-scroll and the 48px inset/margin-left offsets (waves-grid, lanes-ruler, multitrack-container) that assumed an icon strip - Double playhead: set WaveSurfer global cursorWidth:0 — CSS .playhead-marker is the only active playhead now - Loop region position: loopOverlayParent() returns rulerTime first so the loop overlay lives in the same coordinate space as the time→percent calculation - Zoom toolbar: removed +/−/slider controls from wave header; zoom is mouse-scroll only; Fit button in footer retained; dropped dead zoomInBtn/zoomOutBtn/zoomTrack refs that were causing a ReferenceError aborting main.js init (breaking the split stems button and all subsequent wiring) - Mixer row height: 64px → 66px (64 wave + 2px separator) to match WaveSurfer lane pitch and eliminate cumulative row drift Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ui): show loop region as full-height waveform overlay Parent #loop-region to .waves-column instead of rulerTime so the yellow overlay spans all stem lanes like a real DAW. The % position calculation remains correct at any zoom level since waves-column width represents the full timeline extent. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ui): remove all zoom capability Drop zoom controls entirely — Fit button, Ctrl+wheel zoom, zoom state, and all zoom helper functions. Keep the ResizeObserver (recalculates --wave-playhead-h on resize) and the plain vertical wheel pan handler. applyWaveZoom() now only sets --wave-playhead-h and syncs WaveSurfer pxPerSec to fit-to-viewport (zoom=1 always). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ui): audio playback, sidebar navigation, and catalog deduplication - mixer.js: revert to audioEl.volume for WKWebView (MediaElementSource → GainNode does not reliably pass audio in Safari); remove GainNode path - player.js: remove attachAnalysers() import and call — VU meters use pre-computed envelope data, not live Web Audio analysis; calling createMediaElementSource on canplay was disconnecting Safari's native audio output path - index.html: add rail-library class to Library button so catalog.js selectors (.rail-library) actually find it - main.js: Library button click in trash view switches back to library instead of collapsing the sidebar - catalog.js: normalize YouTube URLs to yt:<videoId> in normalizeSource to deduplicate youtu.be/xxx vs youtube.com/watch?v=xxx and variants with &t= / ?si= query params; re-importing a trashed track removes it from trash and places new import in library - daw.css: add Empty trash button (clear-bin-bar/btn), hide lib-header in trash view, show clear-bin-bar only when sidebar has trash-view class Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(library): subfolders, folder drag-reorder, sidebar fixes, and trash purge - Subfolders: folders now support parentId; drag a folder onto the middle zone of another to nest it, or use the subfolder button on hover; delete cascades to children - Folder drag-to-reorder: grip handle on each folder header; three-zone drop target (top/bottom = reorder, middle = reparent as subfolder); circular-nesting guard prevents invalid trees - Sidebar never collapses from within: library button and search input no longer trigger collapse; catalogToggle is expand-only - Unsorted count shows only truly unsorted tracks (not total library) - Trash purge fix: markJobsDeleted() persists a denylist in localStorage before clearing trash; syncWithServer() skips trashed and hard-deleted IDs so purged tracks never reappear on reload; DELETE /api/jobs/{id} fired for each purged track to remove server files and registry entry - New folder button redesigned as a labeled chip (icon + text) - Sidebar width increased to 390px Closes #38, #39, #40, #41, #42, #43, #44 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(ui): stem presence cards panel (Part A) Replaces the energy bar panel with per-stem presence cards. Backend computes mean RMS per extracted stem after collect(), normalizes 0-100, stores in metadata.json and surfaces in the API. Frontend renders one card per stem in STEM_NAMES order: extracted stems show a colored fill bar + percentage, non-extracted stems render grayed with "—". * feat(ui): redesign track info panel to 2-row card layout Row 1: track card + KEY / BPM / LUFS / DURATION / SCALE metadata cards. Row 2: 6 stem presence cards (VOCAL PRESENCE, DRUM INTENSITY, BASS DEPTH, GUITAR PRESENCE, PIANO PRESENCE, OTHER) with large colored percentage values. Matches the reference design — replaces the old 3-column grid with a full-width card grid closer to the screenshot. * feat(analysis): add dynamic range and tempo stability metrics Dynamic range = peak_db - integrated LUFS (dB), classified as Compressed / Moderate / High / Wide. Tempo stability = 0-100% from beat interval coefficient of variation; shown green when >= 80%. Both stored in metadata.json, surfaced in the API, and displayed as metadata cards in the track info panel row. * fix(ui): gray out stem presence cards with 0% value * feat(ui): add favorites heart, Extracted/Source/Quality to track card Heart button toggles favorite per track (stored in catalog localStorage). Extracted date derived from created_at (now surfaced in API). Source and Quality derived from source_url (YouTube vs local file + format). * fix(ui): All button only active when all stems selected * fix(ui): gray out non-extracted stems in mixer and waveform * fix(ui): align waveform rows with mixer by using fixed TRACK_NAMES positions Multitrack.create() now receives all TRACK_NAMES (7 entries) with url:null for non-extracted stems. Previously only extracted stems were passed, so WaveSurfer placed Drums at row 1 (Vocals position), Piano at row 2 (Drums position), etc. Fixed indices mean each stem always occupies the same row regardless of which subset was extracted. * feat(library): redesign sidebar with Recent, Stem Collections, Tags, and Favorites - Rail: add Favorites button (heart), keep Library + Trash, remove Projects/Storage - Library view: three sections — Recent (last 3 tracks by date), Stem Collections (folders), Tags (chips from track.tags; click chip to filter) - Favorites view: filtered list of heart-marked tracks, accessible via rail button - Tags: #tag search prefix filters tracks by tag; clicking active chip clears filter - Unsorted folder now renders as a standard collapsible folder in Stem Collections - daw-lib-header simplified to New folder button only (label removed) - favorites-view hides lib-header and clear-bin-bar via CSS class on sidebar * feat(sections): interactive sections bar with drag, resize, rename, and persist - Backend: Job.sections field, PATCH /api/jobs/{id}/sections endpoint with pydantic validation (id, name, start/end range, color hex check), loads from metadata.json on registry recovery - sections.js: new module — section blocks positioned by %-time over the waveform, drag-to-move with no-overlap clamping, left/right resize handles, double-click rename inline input, delete button on hover, + button to add section (auto-opens rename), debounced PATCH save, 8-color cycling palette - UI: sections bar is the existing daw-section-ribbon row (label changed to Sections); each block has colored border + colored text, 36px height - Wired into player.destroyPlayer (destroySections), catalog.loadTrackIntoStudio, and job.applyState (initSections on done) * fix(sections): move Add button into Sections label, add Mixer label to wave header - Add section button moved from floating inside timeline to the Sections label column (HTML static button, wired by initSections); shows as 'Add' with + icon - Wave header left column now shows 'Mixer' title + 'Drag fader · M/S' sublabel (previously empty box) * fix(sections): persist sections correctly when switching tracks Two bugs caused sections to vanish on track reload: 1. destroySections() was cancelling the debounced save timer, dropping all unsaved changes when the user switched to another track before 600ms elapsed. Fix: flush the save immediately before clearing state (JSON.stringify runs synchronously before the first await, so the body is captured correctly). 2. loadTrackIntoStudio skipped the API fetch when a track had cached audio/analysis data in localStorage, so server-written sections were never loaded. Fix: always fetch fresh state from /api/jobs/{id} on every track load. * feat(sections): saving indicator — spinner while PATCH in-flight, Saved ✓ on success * fix(sections): persist sections across backend restarts and fix save indicator - Call registry_persist() after writing sections to metadata.json so registry.json stays in sync; sections were lost on backend restart because jobs already in registry.json skip the metadata.json recovery path - Check res.ok in _save() before showing "Saved ✓" to surface HTTP errors - Silence spurious audio errors for null-URL placeholder waveform tracks - Update help dialog: correct GitHub org URL and add stemdeck.app link * feat(library): extract YouTube tags and show in library sidebar Extract tags and categories from yt-dlp info after download, lowercase and deduplicate, cap at 8 entries. Stored on Job.tags, written to metadata.json, served via to_state(). The frontend tag chips and #tag search filter were already wired to track.tags. * fix(ui): align mixer rows with waveform by moving original to bottom STEM_NAMES always occupy WaveSurfer rows 0-5 so mixer lanes stay aligned. "original" is appended at row 6 only when it has a URL — omitting it when absent eliminates the phantom 70px gap that shifted all mixer rows down. Also reorders mixer DOM and overview waveform CSS order to match, and fixes renderAllMiniWaves to use trackIndex for stem→wavesurfer mapping. * fix(ui): move original track to top when present, not bottom When original.wav exists the user expects it at the top (A/B comparison). Prepend it to orderedNames only when present; STEM_NAMES follow. Omitting it when absent still prevents the phantom 70px gap from the previous bug. * fix(ui): show all 6 stem rows when original track is present The mixer fill loop was capped at STEM_NAMES.length (6), so when original occupied one slot only 5 stems were shown, leaving a phantom WaveSurfer row at the bottom with no corresponding mixer row. Cap now scales to 7 when original is present, matching orderedNames in wireUpAudio. * fix(ui): guarantee waveform loading overlay is visible for at least 900ms If canplay fires before a browser repaint (cache hit or instant null-URL resolution), the pulsing glow animation flashed and disappeared in under one frame. Now the overlay stays visible for a minimum of 900ms so the user always sees the loading state when opening a track. * feat(ui): show waveform loading overlay during entire pipeline + load Previously the pulsing glow only appeared during WaveSurfer decode. Now it shows immediately when Extract Stems is clicked and stays on through the full pipeline (download → analyze → separate → mix) until canplay fires after WaveSurfer finishes loading the stems. Hidden on error and cancel. * feat(transport): add time display and export buttons to footer bar * feat(transport): redesign footer bar with track info, scrub, speed dropdown, and MP3 export * fix(transport): center-align footer buttons and add visible stop/loop backgrounds * feat(footer): move track info panel to footer and add placeholder waveform - Relocate full track card (art, title, time/stems, fav, detail rows) from top transport header into the footer left section; all element IDs preserved so no JS logic changes needed - Title extracted as a full-width row above the art+detail body so it left-aligns with the thumbnail edge - Footer waveform renders a dim organic placeholder (sum-of-sines) when no track is loaded; replaced by real waveform on track load - Fix waveform bar background/border so it blends into the footer seamlessly - Reduce bar count 300→150, pixel-snap positions, tighten gap and amplitude for smoother bar rendering - Footer height set to 200px to accommodate full track card * feat(ui): topbar sizing, footer heart placement, waveform coverage fixes - Move favorite heart button next to song title (remove margin-left: auto) - Topbar height +10% (70→77px); scale up composer pill, URL input, stem chips, and Split Stems button to fill the extra space - Footer waveform: 300 bars edge-to-edge (last bar reaches canvas.width), matching bar computation in placeholder wave - Footer track info panel title left-aligns with thumbnail via full-width title row above the art+details body row * fix(ui): fill mixer and waveform vertical space dynamically Compute lane height from available panel height at load time so stems fill the full area between header and footer with no black gap. Deferred with rAF in renderEmptyShell so clientHeight is measured after layout. * fix(export): make WAV/MP3 export buttons work Three bugs prevented exports from working: - _triggerDownload used <a download> which WKWebView silently ignores; now uses open_url Tauri command so the system browser handles the save - downloadCurrentMix looked for "original" stem which is absent when all 6 stems are selected; _exportMixUrl now prefers mix_url (the actual selected-stems mix) with "original" as fallback - mix_url from the job API was never forwarded through stateMetadataToTrack or wireUpAudio; now mapped and passed from both job.js and catalog.js --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> |
||
|
|
fcdde67063 | chore: initial commit |