Files
Ensemble/lib/theme/palette_helper.dart
Your NameandClaude Opus 4.5 1edb5a73c9 Improve logging system for user bug reports
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>
2025-12-10 20:45:32 +00:00

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;
}
}
}