mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 17:19:27 +01:00
259 lines
6.9 KiB
Dart
259 lines
6.9 KiB
Dart
import 'dart:async';
|
|
import 'dart:typed_data';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:palette_generator/palette_generator.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
import 'package:worker_manager/worker_manager.dart';
|
|
|
|
import '../cache/image_cache.dart';
|
|
import '../models/support.dart';
|
|
import '../services/cache_service.dart';
|
|
import 'audio.dart';
|
|
import 'music.dart';
|
|
|
|
part 'theme.g.dart';
|
|
|
|
const kDarkBackgroundValue = 0.28;
|
|
const kDarkerBackgroundLightness = 0.13;
|
|
const kOnDarkerBackgroundLightness = 0.6;
|
|
|
|
PaletteColor? _rankedByLuminance(
|
|
List<PaletteColor?> colors, [
|
|
double minLuminance = 0.0,
|
|
double maxLuminance = 1.0,
|
|
]) {
|
|
for (var color in colors) {
|
|
if (color == null) {
|
|
continue;
|
|
}
|
|
|
|
final luminance = color.color.computeLuminance();
|
|
if (luminance >= minLuminance && luminance <= maxLuminance) {
|
|
return color;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
PaletteColor? _rankedWithValue(double value, List<PaletteColor?> colors) {
|
|
for (var color in colors) {
|
|
if (color == null) {
|
|
continue;
|
|
}
|
|
|
|
final hsv = HSVColor.fromColor(color.color);
|
|
if (hsv.value > value) {
|
|
return PaletteColor(hsv.withValue(value).toColor(), 0);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@riverpod
|
|
ColorTheme _colorTheme(_ColorThemeRef ref, Palette palette) {
|
|
final base = ref.watch(baseThemeProvider);
|
|
|
|
final primary = _rankedByLuminance([
|
|
palette.dominantColor,
|
|
palette.vibrantColor,
|
|
palette.mutedColor,
|
|
palette.darkVibrantColor,
|
|
], 0.2);
|
|
final vibrant = _rankedByLuminance([
|
|
palette.vibrantColor,
|
|
palette.darkVibrantColor,
|
|
palette.dominantColor,
|
|
], 0.05);
|
|
final secondary = _rankedByLuminance([
|
|
palette.lightMutedColor,
|
|
palette.mutedColor,
|
|
palette.darkMutedColor,
|
|
]);
|
|
|
|
final background = _rankedWithValue(0.5, [
|
|
palette.vibrantColor,
|
|
palette.darkVibrantColor,
|
|
palette.darkMutedColor,
|
|
palette.dominantColor,
|
|
palette.mutedColor,
|
|
palette.lightVibrantColor,
|
|
palette.lightMutedColor,
|
|
]);
|
|
|
|
final colorScheme = ColorScheme.fromSeed(
|
|
brightness: Brightness.dark,
|
|
seedColor: background?.color ?? Colors.purple[800]!,
|
|
background: background?.color,
|
|
primaryContainer: primary?.color,
|
|
onPrimaryContainer: primary?.bodyTextColor,
|
|
secondaryContainer: secondary?.color,
|
|
onSecondaryContainer: secondary?.bodyTextColor,
|
|
surface: background?.color,
|
|
surfaceTint: vibrant?.color,
|
|
);
|
|
|
|
final hsv = HSVColor.fromColor(colorScheme.background);
|
|
final hsl = HSLColor.fromColor(colorScheme.background);
|
|
|
|
return base.copyWith(
|
|
theme: ThemeData(
|
|
colorScheme: colorScheme,
|
|
useMaterial3: base.theme.useMaterial3,
|
|
brightness: base.theme.brightness,
|
|
cardTheme: base.theme.cardTheme,
|
|
),
|
|
gradientHigh: colorScheme.background,
|
|
darkBackground: hsv.withValue(kDarkBackgroundValue).toColor(),
|
|
darkerBackground: hsl.withLightness(kDarkerBackgroundLightness).toColor(),
|
|
onDarkerBackground:
|
|
hsl.withLightness(kOnDarkerBackgroundLightness).toColor(),
|
|
);
|
|
}
|
|
|
|
@riverpod
|
|
ColorTheme baseTheme(BaseThemeRef ref) {
|
|
final theme = ThemeData(
|
|
useMaterial3: true,
|
|
colorSchemeSeed: Colors.purple[800],
|
|
brightness: Brightness.dark,
|
|
cardTheme: CardTheme(
|
|
clipBehavior: Clip.antiAlias,
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(2),
|
|
),
|
|
),
|
|
);
|
|
|
|
final hsv = HSVColor.fromColor(theme.colorScheme.background);
|
|
final hsl = HSLColor.fromColor(theme.colorScheme.background);
|
|
|
|
return ColorTheme(
|
|
theme: theme,
|
|
gradientHigh: theme.colorScheme.background,
|
|
gradientLow: HSLColor.fromColor(theme.colorScheme.background)
|
|
.withLightness(0.06)
|
|
.toColor(),
|
|
darkBackground: hsv.withValue(kDarkBackgroundValue).toColor(),
|
|
darkerBackground: hsl.withLightness(kDarkerBackgroundLightness).toColor(),
|
|
onDarkerBackground:
|
|
hsl.withLightness(kOnDarkerBackgroundLightness).toColor(),
|
|
);
|
|
}
|
|
|
|
@riverpod
|
|
FutureOr<Palette> albumArtPalette(
|
|
AlbumArtPaletteRef ref,
|
|
String id,
|
|
) async {
|
|
final album = await ref.watch(albumProvider(id).future);
|
|
final cache = ref.watch(cacheServiceProvider);
|
|
final info = cache.albumArt(album, thumbnail: true);
|
|
|
|
return Palette.fromPaletteGenerator(await _generateFromCache(info));
|
|
}
|
|
|
|
Future<PaletteGenerator> _generateFromCache(
|
|
UriCacheInfo cacheInfo,
|
|
) async {
|
|
var file = await cacheInfo.cacheManager.getSingleFile(
|
|
cacheInfo.uri.toString(),
|
|
key: cacheInfo.cacheKey,
|
|
);
|
|
|
|
final memoryImage = ResizeImage(
|
|
MemoryImage(await file.readAsBytes()),
|
|
height: 32,
|
|
width: 32,
|
|
);
|
|
final stream = memoryImage.resolve(const ImageConfiguration(
|
|
devicePixelRatio: 1.0,
|
|
));
|
|
|
|
Completer<ui.Image> imageCompleter = Completer<ui.Image>();
|
|
late ImageStreamListener listener;
|
|
listener = ImageStreamListener((ImageInfo info, bool synchronousCall) {
|
|
stream.removeListener(listener);
|
|
imageCompleter.complete(info.image);
|
|
});
|
|
|
|
stream.addListener(listener);
|
|
final image = await imageCompleter.future;
|
|
final byteData = (await image.toByteData())!;
|
|
final height = image.height;
|
|
final width = image.width;
|
|
|
|
return await Executor().execute(
|
|
arg1: byteData,
|
|
arg2: height,
|
|
arg3: width,
|
|
fun3: _computePalette,
|
|
);
|
|
}
|
|
|
|
Future<PaletteGenerator> _computePalette(
|
|
ByteData byteData,
|
|
int height,
|
|
int width,
|
|
TypeSendPort port,
|
|
) async {
|
|
return await PaletteGenerator.fromByteData(
|
|
EncodedImage(byteData, height: height, width: width),
|
|
);
|
|
}
|
|
|
|
@riverpod
|
|
FutureOr<Palette> playlistArtPalette(
|
|
PlaylistArtPaletteRef ref,
|
|
String id,
|
|
) async {
|
|
final playlist = await ref.watch(playlistProvider(id).future);
|
|
final cache = ref.watch(cacheServiceProvider);
|
|
final info = cache.playlistArt(playlist, thumbnail: true);
|
|
|
|
return Palette.fromPaletteGenerator(await _generateFromCache(info));
|
|
}
|
|
|
|
@riverpod
|
|
FutureOr<Palette> mediaItemPalette(MediaItemPaletteRef ref) async {
|
|
final item = ref.watch(mediaItemProvider).valueOrNull;
|
|
final itemData = ref.watch(mediaItemDataProvider);
|
|
final imageCache = ref.watch(imageCacheProvider);
|
|
|
|
if (item == null || item.artUri == null || itemData == null) {
|
|
return const Palette();
|
|
}
|
|
|
|
return Palette.fromPaletteGenerator(await _generateFromCache(
|
|
UriCacheInfo(
|
|
uri: item.artUri!,
|
|
cacheKey: itemData.artCache!.thumbnailArtCacheKey,
|
|
cacheManager: imageCache,
|
|
),
|
|
));
|
|
}
|
|
|
|
@riverpod
|
|
FutureOr<ColorTheme> mediaItemTheme(MediaItemThemeRef ref) async {
|
|
final palette = await ref.watch(mediaItemPaletteProvider.future);
|
|
return ref.watch(_colorThemeProvider(palette));
|
|
}
|
|
|
|
@riverpod
|
|
FutureOr<ColorTheme> albumArtTheme(AlbumArtThemeRef ref, String id) async {
|
|
final palette = await ref.watch(albumArtPaletteProvider(id).future);
|
|
return ref.watch(_colorThemeProvider(palette));
|
|
}
|
|
|
|
@riverpod
|
|
FutureOr<ColorTheme> playlistArtTheme(
|
|
PlaylistArtThemeRef ref,
|
|
String id,
|
|
) async {
|
|
final palette = await ref.watch(playlistArtPaletteProvider(id).future);
|
|
return ref.watch(_colorThemeProvider(palette));
|
|
}
|