Upgraded DebugLogger with: - Log levels (debug, info, warning, error) for better filtering - Context parameter for categorizing log sources - generateBugReport() with device info, app version, and log stats - Increased log retention to 1000 entries Updated debug log screen: - Added share button to generate and share bug reports - Added level filter chips (All, Info+, Warnings, Errors) - Color-coded log entries by level - Shows filtered entry count Replaced raw print() statements with proper logging: - metadata_service.dart: API errors as warnings - artist_card.dart: Fallback image lookups as debug - palette_helper.dart: Palette extraction errors as warnings - theme_provider.dart: Color extraction as debug - system_theme_helper.dart: System color errors as warnings - playlist_details_screen.dart: Playlist operations as info/error Added dependencies: - package_info_plus: App version info for reports - device_info_plus: Device details for reports - share_plus: Native share functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
205 lines
7.3 KiB
Dart
205 lines
7.3 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:palette_generator/palette_generator.dart';
|
|
import '../services/debug_logger.dart';
|
|
|
|
final _logger = DebugLogger();
|
|
|
|
/// Extracted adaptive colors from album art
|
|
class AdaptiveColors {
|
|
final Color primary; // Main accent color (vibrant)
|
|
final Color surface; // Background color
|
|
final Color onSurface; // Text color on background
|
|
final Color miniPlayer; // Darker version for mini player
|
|
|
|
const AdaptiveColors({
|
|
required this.primary,
|
|
required this.surface,
|
|
required this.onSurface,
|
|
required this.miniPlayer,
|
|
});
|
|
|
|
static const fallback = AdaptiveColors(
|
|
primary: Color(0xFF604CEC),
|
|
surface: Color(0xFF121212),
|
|
onSurface: Colors.white,
|
|
miniPlayer: Color(0xFF1a1a1a),
|
|
);
|
|
}
|
|
|
|
/// Helper to get contrasting text color based on background luminance
|
|
Color getContrastingTextColor(Color backgroundColor) {
|
|
// Use relative luminance to determine if we need light or dark text
|
|
// Standard threshold is 0.179 based on WCAG guidelines
|
|
return backgroundColor.computeLuminance() > 0.4 ? Colors.black : Colors.white;
|
|
}
|
|
|
|
class PaletteHelper {
|
|
/// Extract a color palette from an image with higher color count for better variety
|
|
static Future<PaletteGenerator?> extractPalette(ImageProvider imageProvider) async {
|
|
try {
|
|
final palette = await PaletteGenerator.fromImageProvider(
|
|
imageProvider,
|
|
maximumColorCount: 32, // Increased from 20 for more color variety
|
|
);
|
|
return palette;
|
|
} catch (e) {
|
|
_logger.warning('Failed to extract palette: $e', context: 'Palette');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// Extract actual colors from the palette (not seed-based)
|
|
static AdaptiveColors? extractAdaptiveColors(PaletteGenerator? palette, {required bool isDark}) {
|
|
if (palette == null) return null;
|
|
|
|
// Get primary color - prefer vibrant, then dominant
|
|
final Color primary = palette.vibrantColor?.color ??
|
|
palette.lightVibrantColor?.color ??
|
|
palette.dominantColor?.color ??
|
|
const Color(0xFF604CEC);
|
|
|
|
// Ensure primary has enough saturation to be visually distinct
|
|
// AND enough lightness for white text to be readable on it
|
|
final HSLColor hslPrimary = HSLColor.fromColor(primary);
|
|
Color adjustedPrimary = primary;
|
|
|
|
// Adjust saturation if too low
|
|
if (hslPrimary.saturation < 0.3) {
|
|
adjustedPrimary = hslPrimary.withSaturation(0.5).toColor();
|
|
}
|
|
|
|
// Ensure minimum lightness for contrast with white text
|
|
// If primary is too dark (luminance < 0.15), lighten it
|
|
if (adjustedPrimary.computeLuminance() < 0.15) {
|
|
final hsl = HSLColor.fromColor(adjustedPrimary);
|
|
adjustedPrimary = hsl.withLightness((hsl.lightness + 0.25).clamp(0.3, 0.6)).toColor();
|
|
}
|
|
|
|
if (isDark) {
|
|
// Dark mode: use dark muted colors for background
|
|
final Color surfaceBase = palette.darkMutedColor?.color ??
|
|
palette.mutedColor?.color ??
|
|
const Color(0xFF121212);
|
|
|
|
// Darken the surface color significantly for the expanded background
|
|
final HSLColor hslSurface = HSLColor.fromColor(surfaceBase);
|
|
final Color surface = hslSurface
|
|
.withLightness((hslSurface.lightness * 0.3).clamp(0.05, 0.15))
|
|
.toColor();
|
|
|
|
// Mini player should be medium brightness - noticeably tinted but readable
|
|
// Use the muted color but at medium lightness (0.25-0.35 range)
|
|
final Color miniPlayer = hslSurface
|
|
.withLightness(0.3.clamp(0.25, 0.38))
|
|
.withSaturation((hslSurface.saturation * 1.2).clamp(0.15, 0.5))
|
|
.toColor();
|
|
|
|
return AdaptiveColors(
|
|
primary: adjustedPrimary,
|
|
surface: surface,
|
|
onSurface: Colors.white,
|
|
miniPlayer: miniPlayer,
|
|
);
|
|
} else {
|
|
// Light mode: use light muted colors
|
|
final Color surfaceBase = palette.lightMutedColor?.color ??
|
|
palette.mutedColor?.color ??
|
|
Colors.white;
|
|
|
|
final HSLColor hslSurface = HSLColor.fromColor(surfaceBase);
|
|
final Color surface = hslSurface
|
|
.withLightness((hslSurface.lightness).clamp(0.92, 0.98))
|
|
.toColor();
|
|
|
|
// Mini player in light mode - medium tinted
|
|
final Color miniPlayer = hslSurface
|
|
.withLightness(0.75.clamp(0.65, 0.8))
|
|
.withSaturation((hslSurface.saturation * 1.2).clamp(0.15, 0.5))
|
|
.toColor();
|
|
|
|
return AdaptiveColors(
|
|
primary: adjustedPrimary,
|
|
surface: surface,
|
|
onSurface: Colors.black87,
|
|
miniPlayer: miniPlayer,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Generate color schemes from a palette (kept for backward compatibility)
|
|
static (ColorScheme, ColorScheme)? generateColorSchemes(PaletteGenerator? palette) {
|
|
if (palette == null) return null;
|
|
|
|
final lightColors = extractAdaptiveColors(palette, isDark: false);
|
|
final darkColors = extractAdaptiveColors(palette, isDark: true);
|
|
|
|
if (lightColors == null || darkColors == null) return null;
|
|
|
|
// Determine contrasting text colors based on primary color luminance
|
|
final lightOnPrimary = getContrastingTextColor(lightColors.primary);
|
|
final darkOnPrimary = getContrastingTextColor(darkColors.primary);
|
|
final lightOnMiniPlayer = getContrastingTextColor(lightColors.miniPlayer);
|
|
final darkOnMiniPlayer = getContrastingTextColor(darkColors.miniPlayer);
|
|
|
|
final lightScheme = ColorScheme(
|
|
brightness: Brightness.light,
|
|
primary: lightColors.primary,
|
|
onPrimary: lightOnPrimary,
|
|
secondary: lightColors.primary.withOpacity(0.8),
|
|
onSecondary: lightOnPrimary,
|
|
error: Colors.red,
|
|
onError: Colors.white,
|
|
surface: lightColors.surface,
|
|
onSurface: lightColors.onSurface,
|
|
primaryContainer: lightColors.miniPlayer,
|
|
onPrimaryContainer: lightOnMiniPlayer,
|
|
);
|
|
|
|
final darkScheme = ColorScheme(
|
|
brightness: Brightness.dark,
|
|
primary: darkColors.primary,
|
|
onPrimary: darkOnPrimary,
|
|
secondary: darkColors.primary.withOpacity(0.8),
|
|
onSecondary: darkOnPrimary,
|
|
error: Colors.redAccent,
|
|
onError: Colors.black,
|
|
surface: darkColors.surface,
|
|
onSurface: darkColors.onSurface,
|
|
primaryContainer: darkColors.miniPlayer,
|
|
onPrimaryContainer: darkOnMiniPlayer,
|
|
);
|
|
|
|
return (lightScheme, darkScheme);
|
|
}
|
|
|
|
/// Extract color schemes from an image provider in one call
|
|
static Future<(ColorScheme, ColorScheme)?> extractColorSchemes(ImageProvider imageProvider) async {
|
|
final palette = await extractPalette(imageProvider);
|
|
return generateColorSchemes(palette);
|
|
}
|
|
|
|
/// Get primary color for use in UI elements
|
|
static Color? getPrimaryColor(PaletteGenerator? palette) {
|
|
if (palette == null) return null;
|
|
|
|
return palette.vibrantColor?.color ??
|
|
palette.dominantColor?.color ??
|
|
palette.lightVibrantColor?.color;
|
|
}
|
|
|
|
/// Get background color for use in UI
|
|
static Color? getBackgroundColor(PaletteGenerator? palette, {required bool isDark}) {
|
|
if (palette == null) return null;
|
|
|
|
if (isDark) {
|
|
return palette.darkMutedColor?.color ??
|
|
palette.mutedColor?.color?.withOpacity(0.3) ??
|
|
const Color(0xFF1a1a1a);
|
|
} else {
|
|
return palette.lightMutedColor?.color ??
|
|
palette.mutedColor?.color?.withOpacity(0.9) ??
|
|
Colors.white;
|
|
}
|
|
}
|
|
}
|