Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CesarArellano/Music-Player-App/llms.txt

Use this file to discover all available pages before exploring further.

Musynth ships with a single, carefully hand-tuned dark theme built around a deep navy palette with amber accents. On top of this static foundation, the Now Playing screen dynamically recolors itself to reflect the dominant hue extracted from the current album’s artwork — updating the status bar brightness, text color, icon tints, and blurred background with each track change. This page covers the static theme constants, the customized sub-themes, the AppBackground/AppScaffold composition pattern, and the dynamic color pipeline in full.

AppTheme.darkTheme — Color Constants

All color decisions are centralized in lib/theme/app_theme.dart. The base constants are:
ConstantValueUsage
primaryColorColor(0xFF003D71)Bottom navigation bar background
backgroundBaseColor(0xFF0C1D30)Scaffold background (opaque, deep navy)
surfaceColorColor(0xFF112240)Cards, bottom sheets, list surfaces
accentColorColors.amberFAB, scrollbar thumb, secondary color scheme
lightTextColorColors.white54Subtitle text, unselected icons
// lib/theme/app_theme.dart
class AppTheme {
  static const Color primaryColor    = Color(0xFF003D71);
  static const Color backgroundBase  = Color(0xFF0C1D30);
  static const Color surfaceColor    = Color(0xFF112240);
  static const Color accentColor     = Colors.amber;
  static const Color lightTextColor  = Colors.white54;
}

Customized Sub-Themes

AppTheme.darkTheme is built with ThemeData.dark().copyWith(...). Every sub-theme is set explicitly so no screen depends on Flutter’s default dark theme fallbacks.
scrollbarTheme: ScrollbarThemeData(
  radius: Radius.circular(12),
  thickness: WidgetStateProperty.all(8),
  thumbColor: WidgetStateProperty.all(accentColor),   // amber
  trackColor: WidgetStateProperty.all(Colors.white12),
  thumbVisibility: WidgetStateProperty.all(true),
  trackVisibility: WidgetStateProperty.all(true),
),
The scrollbar is always visible on long lists (songs, albums, artists) and uses the amber accent to stay consistent with the FAB.
appBarTheme: const AppBarTheme(
  elevation: 0,
  scrolledUnderElevation: 0,
  backgroundColor: Colors.transparent,
  iconTheme: IconThemeData(color: lightTextColor),
  titleTextStyle: TextStyle(
    color: Colors.white,
    fontSize: 18,
    fontWeight: FontWeight.w600,
    letterSpacing: 0.2,
  ),
),
The transparent background lets AppBackground (the custom-painted gradient backdrop) show through the app bar region. scrolledUnderElevation: 0 prevents the “tint on scroll” elevation effect that would obscure the background.
cardTheme: CardThemeData(
  color: surfaceColor,        // Color(0xFF112240)
  elevation: 0,
  margin: EdgeInsets.zero,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.circular(artworkRadius), // 10.0
  ),
  clipBehavior: Clip.hardEdge,
),
Clip.hardEdge ensures artwork images inside cards are clipped to the rounded corners without an additional ClipRRect wrapper.
listTileTheme: const ListTileThemeData(
  contentPadding: EdgeInsets.symmetric(horizontal: 16),
  minLeadingWidth: 0,
  titleTextStyle: TextStyle(
    color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500,
  ),
  subtitleTextStyle: TextStyle(
    color: lightTextColor, fontSize: 12,
  ),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
  backgroundColor: primaryColor,      // Color(0xFF003D71)
  selectedItemColor: Colors.white,
  unselectedItemColor: lightTextColor,
  elevation: 0,
),
The bottom nav uses primaryColor (distinct from backgroundBase) to create a subtle depth separation between the content area and the navigation bar.
bottomSheetTheme: const BottomSheetThemeData(
  backgroundColor: surfaceColor,
  shape: RoundedRectangleBorder(
    borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
  ),
),
All bottom sheets (song options modals, playlist pickers) inherit this shape automatically — no per-sheet ShapeBorder required.
dividerTheme: const DividerThemeData(
  color: Colors.white12,
  thickness: 0.5,
),
Half-pixel hairline dividers at 12 % white opacity stay visible against both backgroundBase and surfaceColor without being distracting.

AppBackground and AppScaffold

Every screen in Musynth is wrapped in AppScaffold, a thin widget that passes AppBackground (a CustomPaint-based gradient blob painter) as the body of a standard Scaffold. AppBackground fills the entire screen by drawing a deep navy base color and six soft radial gradient blobs that create a depth illusion — no image asset is involved.
AppScaffold
  └── Scaffold (backgroundColor: Colors.transparent)
        └── AppBackground (CustomPaint — gradient blobs over backgroundBase)
              └── <screen content>
AppScaffold sets the inner Scaffold’s backgroundColor to Colors.transparent so AppBackground (painted as the Scaffold’s body) can fill the screen. AppBackground uses CustomPaint to draw an opaque deep-navy base color, making each screen fully opaque. During a route transition, the outgoing and incoming screens are both fully opaque — neither screen shows the route behind it. This prevents z-ordering artifacts during navigation.
Coding guideline: Theming must not bleed across routes. Each screen owns its own AppBackground layer, and every sub-theme override must be scoped inside that screen’s widget tree (e.g., via a local Theme widget). Never rely on a global background rendered outside the Navigator.

Dynamic Now Playing Colors

The Now Playing screen (SongPlayedScreen) overrides the inherited dark theme with colors derived from the currently playing album’s dominant color. The pipeline has three stages: extraction → caching → application.

Stage 1 — Native Dominant Color Extraction

When PlaybackStateCubit emits a new song, UICubit subscribes to the stream and calls searchDominantColorByAlbumId:
// lib/cubits/ui/ui_cubit.dart
_songSub = playbackStateCubit.stream.listen((playbackState) {
  searchDominantColorByAlbumId(
    albumId: playbackState.songPlayed.albumId?.toString(),
  );
});
The dominant color is extracted natively on first visit to each album:
  • Android: androidx.palette — analyzes the decoded bitmap and returns the vibrant/dominant swatch
  • iOS: Core Image CIAreaAverage — averages pixel values over the artwork region
Subsequent visits are instant cache hits:
// Fast path: color already in the in-memory collection
if (state.dominantColorCollection.containsKey(albumId)) {
  final hex = state.dominantColorCollection[albumId];
  emit(state.copyWith(
    currentDominantColor: hex != null ? Color(int.parse(hex, radix: 16)) : null,
  ));
  return;
}
The collection is persisted to SharedPreferences (via SharedPreferencesRepository.dominantColorCollection) so the cache survives app restarts. See Isolates → dominantColorCollection setter for how the JSON encoding is offloaded.

Stage 2 — Computed Color Properties in UIState

UIState exposes three derived properties computed from currentDominantColor:
// lib/cubits/ui/ui_state.dart

// The dominant color, or black as a safe fallback
Color get dominantColor => currentDominantColor ?? Colors.black;

// White text on dark artwork; black text on light artwork
Color get songPlayedThemeColor =>
    (dominantColor.computeLuminance() < 0.4) ? Colors.white : Colors.black;

// Light status bar icons on dark bg; dark icons on light bg
Brightness get songPlayedBrightness =>
    (dominantColor.computeLuminance() < 0.4) ? Brightness.light : Brightness.dark;

// Typography system matched to luminance
TextTheme get songPlayedTypography =>
    (dominantColor.computeLuminance() < 0.4)
        ? Typography.whiteCupertino
        : Typography.blackCupertino;
The luminance threshold of 0.4 was chosen empirically — colors above this value are bright enough that dark text reads better against them; below it, white text is preferable.

Stage 3 — SongPlayedScreen Local Theme Override

SongPlayedScreen.build() wraps the entire screen in a local Theme widget that overrides only the color-sensitive properties, leaving the rest of AppTheme.darkTheme intact:
// lib/screens/song_played_screen.dart
Theme(
  data: AppTheme.darkTheme.copyWith(
    textTheme: uiState.songPlayedTypography,
    colorScheme: ColorScheme.dark(
      primary: songPlayedThemeColor,
      onSurface: songPlayedThemeColor,
    ),
    iconButtonTheme: IconButtonThemeData(
      style: ButtonStyle(
        iconColor: WidgetStatePropertyAll(songPlayedThemeColor),
      ),
    ),
    appBarTheme: AppBarTheme(
      iconTheme: IconThemeData(color: songPlayedThemeColor),
    ),
    floatingActionButtonTheme: FloatingActionButtonThemeData(
      foregroundColor: songPlayedThemeColor,
    ),
  ),
  child: Scaffold(/* ... */),
),
The AnnotatedRegion<SystemUiOverlayStyle> above this Theme sets the system status bar icon brightness to match:
AnnotatedRegion<SystemUiOverlayStyle>(
  value: SystemUiOverlayStyle(
    statusBarIconBrightness: songPlayedBrightness,
    systemNavigationBarColor: uiState.dominantColor,
  ),
  child: Theme(/* ... */),
),

Background Blur — ImageFiltered, Not BackdropFilter

The Now Playing background blurs the artwork image using ImageFiltered wrapping an Image.file, not BackdropFilter:
// lib/screens/song_played_screen.dart
ImageFiltered(
  imageFilter: ImageFilter.blur(sigmaX: 50.0, sigmaY: 50.0),
  child: Image.file(
    imageFile,
    fit: BoxFit.cover,
    gaplessPlayback: true,
    filterQuality: FilterQuality.low,
    cacheWidth: 200, // decode at reduced resolution — blur hides detail
  ),
),
BackdropFilter samples everything behind its layer, including whatever route is transitioning in or out. During the open/close slide animation this would blur the home screen behind the Now Playing screen — a visible artefact. ImageFiltered blurs only its own child (Image.file), so no route behind it is affected.
A Container with dominantColor.withValues(alpha: 0.8) is stacked on top of the blurred image, tinting the background with the dominant hue for a cohesive look.

Summary: Theming Decision Tree

App launch
  └── MaterialApp uses AppTheme.darkTheme globally
        └── Each screen wrapped in AppScaffold
              └── Scaffold (backgroundColor: transparent)
                    └── AppBackground (CustomPaint gradient blobs)
                          ├── Normal screens: inherit AppTheme.darkTheme
                          └── SongPlayedScreen: local Theme override
                                ├── textTheme   ← songPlayedTypography
                                ├── colorScheme ← songPlayedThemeColor
                                ├── iconButtonTheme ← songPlayedThemeColor
                                └── floatingActionButtonTheme ← songPlayedThemeColor

Build docs developers (and LLMs) love