Skip to content

Unify snackbars behind a coalescing queue and a shared theme - #319

Merged
ultraelectronica merged 4 commits into
mainfrom
versionF
Oct 17, 2026
Merged

ultraelectronica merged 4 commits into
mainfrom
versionF

Conversation

@ultraelectronica

Copy link
Copy Markdown
Member

Summary

Replaces ad-hoc ScaffoldMessenger.showSnackBar calls with one app-wide
snackbar surface: consistent styling, deduplication, queueing, and a
lifetime that respects the user's motion, accessibility, and app state.

Shared API and theme

  • Adds lib/widgets/common/flick_snackbar.dart: an extension on ScaffoldMessengerState with showFlickSnackBar(SnackBar, {Object? id}), hideFlickSnackBar({Object? id}), and clearFlickSnackBars(). Each messenger gets its own queue via an Expando, so state does not leak between hosts or tests.
  • Adds a shared SnackBarTheme (floating, surfaceLight surface, glass border, radiusMd, zero elevation, body text style, accent action text, close icon, 16/8/16/12 inset) to AppTheme.darkTheme.
  • Migrates every direct ScaffoldMessenger.showSnackBar call across 45 files (player sheets/controls/navigation, lyrics, playlists, favorites, settings, duplicate cleaner, logs, UAC2 prompts, ALAC conversion, and more). No direct calls remain in lib/.

Queue, coalescing, and identity

  • Routine notices (no action, not persistent, under one minute) are coalesced: an identical id + text + background while visible only refreshes the timer, and a rapid burst keeps only the latest routine message instead of stacking.
  • Actionable and long-running notices are protected: they queue behind each other and are never evicted by routine feedback. A note with a matching id may update itself without replaying a stale backlog.
  • id enables targeted dismissal. playback-desync and metadata-warmup in lib/app/app.dart are now hidden by id, so clearing one status no longer wipes unrelated Undo/Retry feedback.
  • The custom Undo row in songs_screen.dart becomes a real SnackBarAction, so it is protected and keeps its callback while routine toasts come and go.

Motion, accessibility, and lifetime

  • Keeps native placement, swipe dismissal, live-region semantics, and action handling, but animates the whole surface: fade + 12px rise over 280ms with easeOutCubic, exit over 200ms, instead of Material 3's narrow fade interval. Reduced motion or the app's zero-animation preference uses AnimationStyle.noAnimation.
  • Each notice owns a mounted-content timer rather than an app-wide poll. It pauses while the app is backgrounded, hovered, or focused, resumes when reading stops, and gives screen-reader users at least 8 seconds. Timers and pending notices are cancelled when the host is disposed, and the same notice can be mounted in two routes during a Hero transition without a duplicate GlobalKey.

Player bar clearance

  • MainShell wraps its subtree in a SnackBarTheme whose bottom insetPadding adds the nested bar clearance (150 + extraClearance + 12). The player/navigation bar is a body overlay, so Scaffold cannot infer that reserved space on its own.

Tests

  • Adds 15 widget tests in test/widgets/flick_snackbar_test.dart covering the 280/200ms surface animation, replacement during entry without an opaque flash, duplicate-timer refresh, burst coalescing, Undo survival and callback integrity, protected queueing, targeted dismissal, named-status replacement, swipe-to-advance, reduced motion, background pause/resume, screen-reader duration and semantics, keyboard/RTL/2x-text layout, route transitions, and dispose cleanup.

Related issue

none

Checklist

  • flutter analyze passes
  • flutter test passes
  • Rust changes: cargo test --manifest-path rust/Cargo.toml passes
  • Built and ran the app if the change touches a playback path (flutter run --flavor full)
  • Generated code regenerated and committed if bridge/drift/freezed models changed
  • CHANGELOG.md updated for user-facing changes
  • No secrets, keys, tokens, or machine-local files committed
  • One change per PR; no unrelated reformatting in the same diff
  • Hardware tested if the Rust audio engine or USB/DAC output changed

A mid-fade seek now lands in the incoming track and ends the fade,
retiring the outgoing source and publishing its completion so the Dart
handoff bookkeeping stays exact-once. The Dart completion handler skips
a stale TrackEnded when the engine is already on the expected track,
preventing a skipped song.
Introduce a shared SnackBarTheme and a showFlickSnackBar helper
so transient feedback is consistent and deduplicated by id. The
player bar is a body overlay that Scaffold cannot account for, so
reserve clearance via the snackbar inset padding.
Replace direct ScaffoldMessenger.showSnackBar calls with the
showFlickSnackBar extension across screens and widgets for consistent
snackbar behavior.
@ultraelectronica ultraelectronica self-assigned this Oct 17, 2026
@ultraelectronica
ultraelectronica merged commit 0988840 into main Oct 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant