💄 Dynamic app color based on playing track
This commit is contained in:
173
lib/providers/theme_provider.dart
Normal file
173
lib/providers/theme_provider.dart
Normal 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));
|
||||
Reference in New Issue
Block a user