Refactor player UI#34
Conversation
…mand queue properly
…eck" This reverts commit 8b49d50.
There was a problem hiding this comment.
Code Review
This pull request introduces a significant UI refactor, including a new desktop lyrics feature, a simplified cassette component that focuses on album art, and a more compact player layout. It also improves audio file duration detection by using codec parameters for precise calculations instead of relying solely on file size estimates. However, the refactor has introduced several regressions: the playback progress slider and seeking functionality have been removed from the player UI, and the dynamic text truncation for track metadata in the cassette component was deleted, which will likely cause layout overflows. Additionally, the player panel width is now hardcoded, reducing UI responsiveness, and the new desktop lyrics toggle lacks visual feedback to indicate its state.
| // Tightly packed controls | ||
| ui.add_space(2.0); | ||
| ui.separator(); | ||
| ui.add_space(2.0); |
| // Draw title with truncation | ||
| let title = selected_track | ||
| .title() | ||
| .unwrap_or("Unknown Title".to_string()); | ||
| let title_galley = | ||
| ui.painter() | ||
| .layout_no_wrap(title.clone(), title_font.clone(), Color32::DARK_GRAY); | ||
|
|
||
| let truncated_title = if title_galley.rect.width() > max_width { | ||
| // Find appropriate truncation point | ||
| let mut truncated = title.clone(); | ||
| while truncated.len() > 3 { | ||
| // Keep at least 3 chars | ||
| truncated.pop(); | ||
| let test_galley = ui.painter().layout_no_wrap( | ||
| format!("{}...", truncated), | ||
| title_font.clone(), | ||
| Color32::DARK_GRAY, | ||
| ); | ||
| if test_galley.rect.width() <= max_width { | ||
| truncated.push_str("..."); | ||
| break; | ||
| } | ||
| } | ||
| truncated | ||
| } else { | ||
| title | ||
| }; | ||
|
|
||
| // Use basic text truncation | ||
| ui.painter().text( | ||
| title_pos, | ||
| eframe::egui::Align2::CENTER_CENTER, | ||
| truncated_title, | ||
| title, | ||
| title_font, | ||
| Color32::DARK_GRAY, | ||
| ui.visuals().text_color(), | ||
| ); | ||
|
|
||
| // Draw artist with truncation | ||
| let artist = selected_track | ||
| .artist() | ||
| .unwrap_or("Unknown Artist".to_string()); | ||
| let artist_galley = | ||
| ui.painter() | ||
| .layout_no_wrap(artist.clone(), artist_font.clone(), Color32::DARK_GRAY); | ||
|
|
||
| let truncated_artist = if artist_galley.rect.width() > max_width { | ||
| // Find appropriate truncation point | ||
| let mut truncated = artist.clone(); | ||
| while truncated.len() > 3 { | ||
| // Keep at least 3 chars | ||
| truncated.pop(); | ||
| let test_galley = ui.painter().layout_no_wrap( | ||
| format!("{}...", truncated), | ||
| artist_font.clone(), | ||
| Color32::DARK_GRAY, | ||
| ); | ||
| if test_galley.rect.width() <= max_width { | ||
| truncated.push_str("..."); | ||
| break; | ||
| } | ||
| } | ||
| truncated | ||
| } else { | ||
| artist | ||
| }; | ||
|
|
||
| ui.painter().text( | ||
| artist_pos, | ||
| eframe::egui::Align2::CENTER_CENTER, | ||
| truncated_artist, | ||
| artist, | ||
| artist_font, | ||
| Color32::DARK_GRAY, | ||
| ui.visuals().text_color(), | ||
| ); |
There was a problem hiding this comment.
The text truncation logic for the track title and artist has been removed in this refactor. For tracks with long metadata, the text will likely overflow the album art area or overlap with other UI elements. It is recommended to restore the truncation logic using ui.painter().layout_no_wrap to calculate the required width and append an ellipsis if the text exceeds the available space.
| min_width | ||
| }; | ||
| // Constrain middle pane width to make it a dense column | ||
| let panel_width = 320.0; |
There was a problem hiding this comment.
| let mode_btn = ui.add_enabled(has_selected_track, egui::Button::new(mode_icon).player_style()); | ||
|
|
||
| ui.add_space(8.0); | ||
| let lyrics_icon = if ctx.ui_state.desktop_lyrics_enabled { "词" } else { "词" }; |
There was a problem hiding this comment.
The lyrics_icon logic uses the same string "词" for both branches of the if statement. This provides no visual feedback to the user regarding whether desktop lyrics are enabled or disabled. Consider using distinct labels or colors to indicate the toggle state.
| let lyrics_icon = if ctx.ui_state.desktop_lyrics_enabled { "词" } else { "词" }; | |
| let lyrics_icon = if ctx.ui_state.desktop_lyrics_enabled { "词 (On)" } else { "词 (Off)" }; |
- Pump AudioEvent loop centrally in App::update via new App::pump_audio_events; PlayerComponent is now render-only. - Move 30s persistence-save throttle from a thread_local in PlayerComponent into UiState.last_persistence_save. - Replace string-keyed action dispatch in PlayerComponent with a type-checked PlayerAction enum. - Drop dead code: CASSETTE_WIDTH const, unused min_width / available_width vars, unused LyricsComponent import in MainShell, PlayerService::remove_current_track, UiState.show_library_and_playlist. - Fix clippy: deprecated allocate_ui_at_rect -> centered_and_justified, collapsed nested ifs, deduped identical lyrics_icon branches. - Remove per-frame eprintln! fps spam in App::update.
Centralise every spacing/sizing/radius/colour/icon literal that the player UI depends on into two new modules: - src/app/style/tokens.rs — single source of truth for spacing, radii, sizes, typography and palette. Brand colours (BRAND, BRAND_HOVER, BRAND_ACTIVE) are defined but unused; phase 2 will wire them into the egui visuals. - src/app/style/icons.rs — every emoji / glyph the UI prints today, ready to be swapped to an icon-font in phase 2. Existing call sites (player, cassette, lyrics, playlist table, footer, window chrome, main shell, desktop-lyrics overlay) now reference these constants instead of inline literals. Each value is preserved exactly, so this commit is a no-op visually — it just cleans the surface so the remaining UI redesign phases (button polish, progress bar, layout split, desktop-lyrics polish) can iterate on tokens rather than rewriting magic numbers.
- Adopt egui-phosphor 0.9 (regular variant) as the icon font; rewrite
src/app/style/icons.rs to point each constant at the corresponding
phosphor codepoint. Every icon in the player, footer, lyrics panel
and window chrome now renders from the same font, replacing the
previous mix of emoji and ASCII glyphs.
- Apply a brand-aware visuals tweak at boot via
apply_brand_visuals(): selection backgrounds and slider/select
strokes now pick up tokens::color::BRAND.
- Add a player_button(label, active) helper in style/mod.rs that
produces a brand-filled button when toggled on. Wire it through the
player control row so:
• play/pause turns brand-blue while audio is playing,
• the mode button highlights for any non-Normal playback mode,
• the desktop-lyrics toggle highlights while the floating viewport
is enabled.
- Convert the old static speaker label into a clickable mute toggle.
It stores the pre-mute volume in UiState::volume_before_mute (not
persisted) and restores it on the next click; manually dragging the
slider clears the sticky mute. Mute state also drives a speaker-x
glyph and the brand active fill.
…se 3) - Replace the four-line track-info block with a clearer hierarchy: title is rendered as the heading (tokens::text::LG, bold), artist uses default weight with a muted tint, and time + playlist collapse into a single small dim row separated by a middle dot. - Drop the redundant separator above the controls in favour of an always-visible thin timeline scrubber. Dragging the slider calls PlayerService::seek_to() and is automatically disabled when no track is selected or the duration is unknown. - Make CassetteComponent's cover responsive: it still tops out at tokens::size::ALBUM (180 px) but now shrinks with the available height/width down to 64 px, so the player remains usable in narrow or short layouts.
- Drop the in-player lyrics column. PlayerComponent is now strictly cover + track info + transport controls; the trailing separator and allocate_ui block that hosted LyricsComponent are gone. - Render lyrics in a dedicated egui::SidePanel::right (280 px default, 220 px min, user-resizable) shown by MainShell whenever ui_state.show_lyrics_panel is true. The existing View > Show/Hide Lyrics menu entry already drives that flag, and lyrics-fetch flows continue to flip it on automatically when new lyrics arrive. - The 词 transport button keeps its phase-2 behaviour: it toggles the borderless desktop lyrics overlay only, independent of the side panel.
…I phase 5) - Replace the click-anywhere-to-drag behaviour on the desktop lyrics overlay with a small 16x16 corner drag handle. The rest of the surface no longer intercepts pointer events, and the handle is hidden entirely when the overlay is locked. - Render the lyrics text via Painter::text with a four-direction black drop-shadow underneath the foreground glyphs, giving a cheap stroke effect that stays readable over busy desktops. - Read font size and color from the new UiState fields: desktop_lyrics_font_size (default 48), desktop_lyrics_color (default cyan), and desktop_lyrics_locked (default false). All three are persisted via UiSettings with serde defaults so older configs upgrade cleanly. - Add a 'View > Desktop Lyrics' submenu exposing the enabled/locked toggles, a 16-120 px font-size slider, and an sRGBA color picker. - Drop the now-unused DESKTOP_LYRICS_FG/BG tokens; defaults live with the runtime state instead.
- Route the minimize / maximize / close glyphs through phosphor regular via new icons::WINDOW_MINIMIZE, WINDOW_MAXIMIZE, WINDOW_RESTORE, and WINDOW_CLOSE constants. The maximize button now swaps to a CORNERS_IN glyph while the window is maximized so the action stays self-explanatory. - Replace the three ad-hoc Button widgets with a small chrome_button helper that strips strokes, enlarges the hit area to 32x22, and gives the close button a danger-red hover fill (Win/macOS standard) while the other two pick up the theme's normal hovered.weak_bg_fill. - Tighten the top chrome panel padding to 2 px vertical and SM/XS horizontal so the menu row no longer floats inside excessive gutter.
Bugs fixed:
- Chrome buttons now actually paint a hover background. Previously the
helper called Button::fill(Color32::TRANSPARENT) and Button::stroke(NONE),
but egui resolves those overrides for every interaction state, so the
red close-hover and the grey min/max-hover were both stomped flat.
Worse, the hovered fg_stroke I set to white turned the X glyph into
white-on-white. Drop the per-button overrides and drive every state
through WidgetVisuals (inactive transparent, hovered/active tinted),
so close goes red-on-white-glyph and the others get the theme grey.
- The timeline scrubber now seeks on click-on-rail too. It was using
Response::dragged(), which only fires while a drag is in progress.
Switch to Response::changed() so any value change (drag, click, or
keyboard) calls PlayerService::seek_to.
- Long song titles can no longer push the player panel taller. Both
the title and artist Labels are now built with .truncate(), so they
render as a single line that ellipsises on overflow.
Layout polish:
- Restructure PlayerComponent into three vertical bands:
1. cover + track-info row (cover keeps its responsive sizing,
info expands to fill the rest of the panel width),
2. full-width timeline scrubber,
3. transport row with prev/play/next/mode/lyrics on the left and
mute + volume hugging the right edge via a right-to-left
layout. Volume no longer floats in its own short stub row.
- Drop the now-unused tokens::size::PLAYER_PANEL since the player
panel sizes itself off ui.available_width() instead of a fixed
320 px column.
- Add icons::PLUS (phosphor regular plus) and icons::LYRICS_PRESENT
(phosphor music notes) so the rest of the UI can route through the
same icon set the player already uses.
- Library 'add folder' button now renders icons::PLUS in a frame-less
Button so it sits flush against the 'Music Files' label, matching the
visual weight of the new chrome buttons.
- Playlist tabs:
* font scaled from a hard-coded 12 px to tokens::text::MD (14 px),
* selected tab paints with tokens::color::BRAND + BRAND_ACTIVE
stroke and bold white text, mirroring the active player_button
treatment;
* unselected tabs are fully transparent so the tab strip reads as
a strip of links rather than a row of buttons,
* the trailing '+' uses icons::PLUS in a frame-less Button to match
the library affordance.
- Footer 'Search' button replaces its 🔍 emoji with icons::SEARCH; the
'No matches found' label switches from raw Color32::RED to the
shared tokens::color::LYRICS_FAILED warning red.
- Playlist table lyrics column drops the 🎵 / ❌ / ― emoji trio in
favour of icons::LYRICS_PRESENT, icons::CLOSE, and icons::LYRICS_NONE
so the column reads in the same phosphor weight as everything else.
- Playback menu's mode glyphs (➡ 🔁 🔂 🔀) are routed through
icons::MODE_* so the menu and the transport button always show the
same icon for the same mode.
The previous defaults made the player look more like a tablet hero card than a desktop music app — 180 px album art, 40 px transport buttons, and egui's stock 8 px item spacing meant every list / tab / control band burnt a generous chunk of vertical real estate. Pull everything in by ~25 % so the UI reads compactly without changing the visual language: - new style::apply_compact_spacing(): item_spacing 6×3, button_padding 6×2, menu_margin 4×3, indent 14 (was 18), interact_size.y 22. Wired in alongside apply_brand_visuals at boot. - tokens::size::ICON_BTN 40 → 32 (transport buttons, mute, lyrics toggle, etc. all auto-shrink with the player_style helper). - tokens::size::ALBUM 180 → 120 (cassette / album art card; cassette's responsive min/clamp logic keeps shrinking gracefully). - tokens::size::SLIDER_VOLUME 160 → 140 to match the tighter band. - Playlist Grid spacing 5×5 → tokens::spacing::MD × XS so rows hug each other vertically while keeping legible horizontal gutters. - Library: replace ad-hoc 'SM + 1.0' add_space() pairs with tokens::spacing::XS to take a few pixels off the heading and the folder list separator.
Iteration on the compact pass. The album art was still a touch larger than the info column, and three of the side-by-side panels (library, playlist tabs, lyrics) read as one continuous wall because there was nothing visually anchoring their headers to the body beneath: - tokens::size::ALBUM 120 \u2192 88. Closer to the natural height of the three-line info column (title / artist / time-and-playlist), so the cover stops dangling below the text baseline. - cassette_component: drop the .shrink(10) margin and re-anchor the cover rect to the available height's vertical centre instead of its top. With ui.horizontal()'s default Align::Center the info column centres too, so the cover and the text now share a midline. - library_component: add ui.separator() right under the 'Music Files' header. Mirrors the separator the lyrics panel already draws under its track header so the two side panels read consistently. - playlist_content: replace the post-tabs add_space(MD) with a XS-spaced ui.separator(), giving the tab strip its own underline and visually tying it to the table grid below.
The previous attempt computed the cover's y-position from available_rect_before_wrap().center().y. Inside ui.horizontal() that rect's height extends all the way to the bottom of the parent panel (it's the *available* slot, not the row's content height), so half of side was subtracted from a midpoint that lived well below the actual row \u2014 pushing the artwork up over the title bar and clipping the title line entirely. Replace the manual rect with allocate_exact_size: a horizontal strip defaults to Align::Center, so the layout itself vertically centres the cover against the info column on its right. No more guess-the-row-height math, and the title now sits where it belongs.
Three refinements that pull the visual language together: - style: introduce borderless_button_visuals(visuals) helper that flattens any button rendered inside the calling scope to transparent inactive / hover-tinted / no outline. Single function so we don't duplicate the WidgetVisuals dance from window_chrome's chrome_button. - window_chrome: apply the helper at the top of the menu strip so '\u6587\u4ef6 / \u64ad\u653e / \u89c6\u56fe / \u5e2e\u52a9' read with the same borderless aesthetic as the min / max / close buttons sitting next to them on the right edge. - lyrics_component: reskin the '\u4e0a\u4f20\u6b4c\u8bcd' button via the same helper so it stops looking like a chunky bordered chip in the otherwise light-touch lyrics header. - player_component: drop the two add_space(XS) gaps between cover/info, scrubber, and transport. Egui's compact item_spacing already gives each row breathing room; the explicit pads were stacking on top of it, leaving the band feeling airy. main_shell: trailing pad after the player drops from SM+1 to XS for the same reason.
A long playlist name was running into the elapsed/total counter on
the same line. Break the row into two:
- line 3: 'mm:ss / mm:ss' (truncated)
- line 4 (only when a playlist is actually selected): 'Playlist:
<name>' / '播放列表:<name>' so it's labelled rather than just
sitting there as a bare string.
i18n: add 'playlist_label' key (en: 'Playlist', zh: '播放列表').
Two visual unifications across the three side-by-side panels: - tokens::size::HEADER_HEIGHT (28 px). Library, playlist tabs, and the lyrics header all set_min_height to this value, so their bottom separators line up across the panel seams instead of stair-stepping. The playlist tab buttons used to push the tab strip taller than the library's plain heading; with a shared minimum row height the borderless library heading just stretches to match. - lyrics ScrollArea now uses auto_shrink([false, false]). The default shrink-to-content behaviour was making the scroll area only as wide as its longest line, which left the vertical scrollbar floating inside the panel rather than flush with the panel's right edge. Lyrics text now extends edge-to-edge and the scrollbar lives where it should. - lyrics: post-header pad standardised to spacing::XS to match the other two panels (was MD - XS = 6, off by 4 px).
The library / playlist tabs / lyrics headers all read with the same visual weight now \u2014 the previous inconsistencies came from each panel relying on a different default: - library_component: '\u97f3\u4e50\u5e93' was using egui's default body size (~14 px) with .strong(). It looked chunkier than the playlist tab text because the tabs go through tokens::text::MD which is the same number, but library was inheriting the system body font weight. Pinned explicitly to size(MD).strong() to match. - lyrics_component: track header was a plain ui.label() at body size, no weight \u2014 visually lighter than the other two panels. Bumped to RichText::size(MD).strong() and switched the separator from a hyphen to an em-dash for the title \u2014 artist split. - playlist_table/view: column headers were ui.strong() at body size (14 px), which made them read like a second tab strip stacked under the playlist tabs. They are *secondary* labels so demoted them to size(SM).strong() in weak_text_color() \u2014 still scannable but visually subordinate to the heading row above and consistent with the table's own SM body text.
…line separator
The three panel headers were still landing at slightly different y
positions because:
- library_component had its header rendered INSIDE ScrollArea::both(),
so the heading row + bottom separator scrolled with content. Even at
scrollTop=0 the area reserves a couple of pixels for the horizontal
scrollbar gutter, which pushed the library separator a few px below
the playlist tabs / lyrics separators. Hoisted the header
(ui.horizontal { music_label + add-folder button }) and its trailing
add_space(XS) + separator() OUT of the ScrollArea, then wrap only
the folder list in ScrollArea::both. Header now stays pinned and its
separator lands on the same horizontal seam as the other two.
- lyrics show_lyrics_type() was emitting an inline ui.separator()
after the type icon, which the other two panels don't have. The
vertical hairline made the lyrics header read as denser / taller
than 'jay 许嵩 南拳妈妈' and '音乐库'. Removed; the icon now sits next
to the title with the standard horizontal item_spacing.
Same root cause as the library header fix one commit ago: PlaylistTabs were rendered inside ScrollArea::horizontal(...), which reserves a few pixels of vertical space for the horizontal scrollbar gutter even when no scrollbar is currently visible. That gutter pushed the tab strip's bottom separator down, so the middle column's separator sat ~4 px below the library and lyrics separators on either side. Removed the wrapper — tab strip + add_space(XS) + separator() now render directly into the side-panel ui, identical structure to the library header. The three panel separators now share the same y, and visually the three columns line up. The trade-off: if a user creates enough playlists to overflow the panel width, tabs will clip rather than horizontally scroll. That's an acceptable trade for a desktop music player at this density (and the current 3-tab demo case fits comfortably); revisiting later with a clip-rect+wheel-scroll variant is preferable to keeping the gutter just for scroll.
…der aligns Real root cause of the misaligned middle separator across multiple attempts: the three columns use DIFFERENT panel types with different default frames. SidePanel::left and SidePanel::right both render with Frame::side_top_panel, whose inner_margin has a small top component. CentralPanel::default() uses Frame::central_panel, which has a LARGER top inner_margin (egui pads central panels more generously than sides). That extra top padding is what kept pushing the playlist tab strip — and therefore its bottom separator — a few pixels below the library and lyrics separators no matter what we did inside the header rows themselves. Fix: explicitly set the central panel's frame to side_top_panel so all three columns share the same outer chrome. Result: identical top inner_margin across the three side-by-side panels, and the three header separators land on the same horizontal seam.
The three side-by-side panels were rendering body text at three different sizes/weights: - library folder names: body size + .strong() - library track items: body size, no weight - playlist table number column: body size + .strong() - playlist table title/artist/album/genre: body size, no weight - lyrics current line: tokens::text::MD + .strong() - lyrics non-current synced lines: body size, no weight - lyrics plain-text and placeholder lines: body size Standardised everything that's not a panel header to tokens::text::SM (12 px) with no .strong(). Headers stay at MD strong so the three columns still have a visible heading hierarchy, but the body lists now read at a consistent, denser, calmer scale. Exceptions kept by design: - lyrics current line keeps .strong() + brand colour. Size matches surrounding lines (SM) so the highlight rides on weight + colour rather than scaling the type. - playlist tab text stays at MD because tabs ARE the panel header for the middle column; demoting them would visually lose the heading row. - playlist table column headers stay at SM strong + weak colour (subordinate to the tab header above).
Per user direction: panel headers should read at the same scale as their body lists, not as a louder heading row. - library label: MD strong \u2192 SM, no strong - lyrics 'title \u2014 artist': MD strong \u2192 SM, no strong - playlist tabs: MD + selected.strong() \u2192 SM, no strong (selected tab still distinguishes via brand fill + white text) - playlist table column headers: SM strong + weak \u2192 SM + weak Visual hierarchy now comes from layout (separators, selected-tab brand fill, current-row highlight) rather than type size/weight.
Per user direction: list headers should be more compact and the
horizontal rule under them should 'seal' to the panel edges instead
of stopping short on each side.
- New crate::app::style::panel_header(ui, contents) helper:
ui.horizontal { set_min_height(HEADER_HEIGHT); ... }
+ Separator::default().grow(8.0).spacing(0.0)
Separator::grow(8) cancels Frame::side_top_panel's 8 px horizontal
inner_margin so the rule extends fully to each panel border. All
three columns share Frame::side_top_panel since the central-panel
fix two commits ago, so the rules now meet at the panel seams and
read as one continuous horizontal line across the window.
- HEADER_HEIGHT 28 \u2192 22. The previous value was tuned around the
taller PlaylistTabs button row; with tabs now demoted to SM text
the header was reading too tall. 22 px gives a tight band that
still clears the borderless+ button comfortably.
- library_component / lyrics_component / playlist_content all switch
from the manual 'ui.horizontal + add_space(XS) + ui.separator()'
pattern to a single panel_header(ui, |ui| { ... }) call.
- playlist_tabs no longer wraps itself in ui.horizontal; the caller
(PlaylistContent via panel_header) now provides that scope, which
avoids a nested horizontal + double min_height inside the header.
Add design specification for playlist metadata fields (description, timestamps) and contextual playback information display in player UI. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add explicit conditions for when to show playlist vs single-track info. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… panel Detailed step-by-step plan covering: - Playlist metadata fields (description, timestamps) - Database schema migration to version 2 - Audio technical info extraction - PlaybackInfoPanel UI component - Integration into player component Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pdates Add current_timestamp_ms() and touch() helpers to Playlist, then replace the 6 inline timestamp+is_dirty patterns across new(), set_name(), set_description(), add(), remove(), reorder(), and load_from_db() with calls to these helpers.
Implements database migration from schema version 4 to 5, adding description, created_at, and updated_at columns to the playlists table. Updates save/load methods to persist and retrieve these new metadata fields. Migration is idempotent and sets current timestamps for existing playlists. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…els, codec) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace byte-based string slicing with character-based truncation to avoid panics when the 30th byte falls in the middle of a multi-byte UTF-8 character (e.g., Chinese, emoji). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add PlaybackInfoPanel to the player's top row layout, positioned on the right side. The layout now displays: cassette (left), track info (middle 60%), and playback info panel (right). The panel shows contextual information based on playback mode - playlist metadata when playing from a playlist, or album/technical details for single tracks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The progress bar would jump back to the original position after seeking because stale CurrentTimestamp events from the audio thread overwrote the seek target. Added a time-based guard (seeking_since) to suppress stale timestamps for 300ms after a seek. Also fixed a critical unit mismatch: the UI passes milliseconds but symphonia's SeekTo::TimeStamp expects timebase units (samples). Added ms-to-samples conversion in reader.rs. This also fixes the issue where playback position was not correctly restored on app restart. Additionally fixed the inverted timebase fallback calculation in loader.rs (numer/denom → denom/numer). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
No description provided.