💄 Dynamic app color based on playing track

This commit is contained in:
2025-12-16 23:24:26 +08:00
parent 29edbfbb8a
commit 5edc9cf2ca
9 changed files with 482 additions and 35 deletions

View File

@@ -1,3 +1,4 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:groovybox/logic/audio_handler.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

View File

@@ -48,4 +48,4 @@ final class AudioHandlerProvider
}
}
String _$audioHandlerHash() => r'd2864a90812b2c615afb327e5a5504558097c945';
String _$audioHandlerHash() => r'65fbd92e049fe4f3a0763516f1e68e1614f7630f';

View File

@@ -0,0 +1,173 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:palette_generator/palette_generator.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'theme_provider.g.dart';
// Default seed color
const Color defaultSeedColor = Colors.deepPurple;
// State class for theme data
class ThemeState {
final ThemeMode themeMode;
final Color seedColor;
const ThemeState({required this.themeMode, required this.seedColor});
ThemeState copyWith({ThemeMode? themeMode, Color? seedColor}) {
return ThemeState(
themeMode: themeMode ?? this.themeMode,
seedColor: seedColor ?? this.seedColor,
);
}
}
// Light theme definition with dynamic seed color
ThemeData createLightTheme(Color seedColor) => ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.light,
),
useMaterial3: true,
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
),
);
// Dark theme definition with dynamic seed color
ThemeData createDarkTheme(Color seedColor) => ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: seedColor,
brightness: Brightness.dark,
),
useMaterial3: true,
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
),
);
@Riverpod(keepAlive: true)
class ThemeNotifier extends _$ThemeNotifier {
@override
ThemeMode build() {
return ThemeMode.system; // Default to system theme
}
void setThemeMode(ThemeMode themeMode) {
state = themeMode;
}
void toggleTheme() {
switch (state) {
case ThemeMode.light:
state = ThemeMode.dark;
break;
case ThemeMode.dark:
state = ThemeMode.light;
break;
case ThemeMode.system:
// If system, default to light
state = ThemeMode.light;
break;
}
}
void setLightTheme() => state = ThemeMode.light;
void setDarkTheme() => state = ThemeMode.dark;
void setSystemTheme() => state = ThemeMode.system;
}
@Riverpod(keepAlive: true)
class SeedColorNotifier extends _$SeedColorNotifier {
@override
Color build() {
return defaultSeedColor;
}
void setSeedColor(Color color) {
state = color;
}
void updateFromAlbumArt(String? imagePath) async {
if (imagePath == null || imagePath.isEmpty) {
// Reset to default color if no album art
state = defaultSeedColor;
return;
}
try {
// Validate that the file exists before attempting to load it
final file = File(imagePath);
if (!await file.exists()) {
// File doesn't exist, reset to default color
state = defaultSeedColor;
return;
}
// Additional validation: check if file is readable and not empty
final fileStat = await file.stat();
if (fileStat.size == 0) {
// Empty file, reset to default color
state = defaultSeedColor;
return;
}
final paletteGenerator = await PaletteGenerator.fromImageProvider(
FileImage(file),
size: const Size(200, 200),
maximumColorCount: 20, // Increase color count for better extraction
);
// Use dominant color with better fallback hierarchy
Color? extractedColor;
if (paletteGenerator.dominantColor != null) {
extractedColor = paletteGenerator.dominantColor!.color;
} else if (paletteGenerator.vibrantColor != null) {
extractedColor = paletteGenerator.vibrantColor!.color;
} else if (paletteGenerator.mutedColor != null) {
extractedColor = paletteGenerator.mutedColor!.color;
} else if (paletteGenerator.paletteColors.isNotEmpty) {
// Fallback to the first available color
extractedColor = paletteGenerator.paletteColors.first.color;
}
// Ensure we have a valid color, otherwise use default
state = extractedColor ?? defaultSeedColor;
} catch (e) {
// Log the error for debugging (in a real app, you'd use proper logging)
// debugPrint('Failed to extract color from album art: $e');
// If color extraction fails, reset to default color
state = defaultSeedColor;
}
}
void resetToDefault() {
state = defaultSeedColor;
}
}
@Riverpod(keepAlive: true)
ThemeData currentTheme(Ref ref) {
final themeMode = ref.watch(themeProvider);
final seedColor = ref.watch(seedColorProvider);
final brightness = themeMode == ThemeMode.system
? WidgetsBinding.instance.platformDispatcher.platformBrightness
: (themeMode == ThemeMode.dark ? Brightness.dark : Brightness.light);
return brightness == Brightness.dark
? createDarkTheme(seedColor)
: createLightTheme(seedColor);
}
// Legacy providers for backward compatibility
@Riverpod(keepAlive: true)
ThemeData lightTheme(Ref ref) => createLightTheme(ref.watch(seedColorProvider));
@Riverpod(keepAlive: true)
ThemeData darkTheme(Ref ref) => createDarkTheme(ref.watch(seedColorProvider));

View File

@@ -0,0 +1,239 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'theme_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(ThemeNotifier)
const themeProvider = ThemeNotifierProvider._();
final class ThemeNotifierProvider
extends $NotifierProvider<ThemeNotifier, ThemeMode> {
const ThemeNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'themeProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$themeNotifierHash();
@$internal
@override
ThemeNotifier create() => ThemeNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ThemeMode value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ThemeMode>(value),
);
}
}
String _$themeNotifierHash() => r'5ba92ae22271e01ef165260fc7dc1cce6d1d72e0';
abstract class _$ThemeNotifier extends $Notifier<ThemeMode> {
ThemeMode build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<ThemeMode, ThemeMode>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ThemeMode, ThemeMode>,
ThemeMode,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(SeedColorNotifier)
const seedColorProvider = SeedColorNotifierProvider._();
final class SeedColorNotifierProvider
extends $NotifierProvider<SeedColorNotifier, Color> {
const SeedColorNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'seedColorProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$seedColorNotifierHash();
@$internal
@override
SeedColorNotifier create() => SeedColorNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Color value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Color>(value),
);
}
}
String _$seedColorNotifierHash() => r'7c7893af5be42f3771a268159e7a5e2597bebc4e';
abstract class _$SeedColorNotifier extends $Notifier<Color> {
Color build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<Color, Color>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Color, Color>,
Color,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(currentTheme)
const currentThemeProvider = CurrentThemeProvider._();
final class CurrentThemeProvider
extends $FunctionalProvider<ThemeData, ThemeData, ThemeData>
with $Provider<ThemeData> {
const CurrentThemeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'currentThemeProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$currentThemeHash();
@$internal
@override
$ProviderElement<ThemeData> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
ThemeData create(Ref ref) {
return currentTheme(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ThemeData value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ThemeData>(value),
);
}
}
String _$currentThemeHash() => r'29c9080ae24ba144ebb6e0aac60b16bebcc8a919';
@ProviderFor(lightTheme)
const lightThemeProvider = LightThemeProvider._();
final class LightThemeProvider
extends $FunctionalProvider<ThemeData, ThemeData, ThemeData>
with $Provider<ThemeData> {
const LightThemeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'lightThemeProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$lightThemeHash();
@$internal
@override
$ProviderElement<ThemeData> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
ThemeData create(Ref ref) {
return lightTheme(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ThemeData value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ThemeData>(value),
);
}
}
String _$lightThemeHash() => r'be4e02c30ddc60a134ed2a1f7124caf162894889';
@ProviderFor(darkTheme)
const darkThemeProvider = DarkThemeProvider._();
final class DarkThemeProvider
extends $FunctionalProvider<ThemeData, ThemeData, ThemeData>
with $Provider<ThemeData> {
const DarkThemeProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'darkThemeProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$darkThemeHash();
@$internal
@override
$ProviderElement<ThemeData> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
ThemeData create(Ref ref) {
return darkTheme(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ThemeData value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ThemeData>(value),
);
}
}
String _$darkThemeHash() => r'edc0e042521cc60d1d1bb026100ac46094825f7c';