cover art color scheme extraction (in background)

refactor text styles to use theme
port over part of album screen
This commit is contained in:
austinried 2025-12-03 13:22:14 +09:00
parent b9a094c1c4
commit 6609671ae2
13 changed files with 682 additions and 101 deletions

View File

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../images/images.dart';
import '../../sources/models.dart';
import '../ui/images.dart';
import '../util/clip.dart';
class AlbumGridTile extends HookConsumerWidget {
@ -25,7 +25,10 @@ class AlbumGridTile extends HookConsumerWidget {
margin: EdgeInsets.all(2),
child: ImageCard(
onTap: onTap,
child: CoverArtImage(coverArt: album.coverArt),
child: CoverArtImage(
coverArt: album.coverArt,
thumbnail: true,
),
),
);
}
@ -47,7 +50,10 @@ class ArtistListTile extends StatelessWidget {
Widget build(BuildContext context) {
return ListTile(
leading: CircleClip(
child: CoverArtImage(coverArt: artist.coverArt),
child: CoverArtImage(
coverArt: artist.coverArt,
thumbnail: true,
),
),
title: Text(artist.name),
subtitle: albumCount != null ? Text('$albumCount albums') : null,

View File

@ -1,8 +1,16 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'dart:async';
class AlbumScreen extends StatelessWidget {
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../l10n/generated/app_localizations.dart';
import '../state/database.dart';
import '../state/source.dart';
import '../ui/cover_art_theme.dart';
import '../ui/images.dart';
class AlbumScreen extends HookConsumerWidget {
const AlbumScreen({
super.key,
required this.id,
@ -11,27 +19,126 @@ class AlbumScreen extends StatelessWidget {
final String id;
@override
Widget build(BuildContext context) {
return Scaffold(
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context);
final db = ref.watch(databaseProvider);
final sourceId = ref.watch(sourceIdProvider);
final getAlbum = useMemoized(
() => db.libraryDao.getAlbum(sourceId, id).getSingle(),
);
final album = useFuture(getAlbum).data;
if (album == null) {
return Container();
}
return CoverArtTheme(
coverArt: album.coverArt,
child: Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Album $id!'),
TextButton(
onPressed: () {
context.push('/artist');
},
child: Text('Artist...'),
child: CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: SafeArea(
child: Padding(
padding: EdgeInsetsGeometry.symmetric(horizontal: 16),
child: _Header(
title: album.name,
subtitle: album.albumArtist,
coverArt: album.coverArt,
playText: l.resourcesAlbumActionsPlay,
onPlay: () {},
onMore: () {},
),
),
),
CachedNetworkImage(
imageUrl: 'https://placehold.net/400x400.png',
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
],
),
),
),
);
}
}
class _Header extends HookConsumerWidget {
const _Header({
required this.title,
this.subtitle,
this.coverArt,
this.playText,
this.onPlay,
this.onMore,
// required this.downloadActions,
});
final String title;
final String? subtitle;
final String? coverArt;
final String? playText;
final void Function()? onPlay;
final FutureOr<void> Function()? onMore;
// final List<DownloadAction> downloadActions;
@override
Widget build(BuildContext context, WidgetRef ref) {
// final inheritedStyle = DefaultTextStyle.of(context).style;
final theme = Theme.of(context);
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 16),
CoverArtImage(
height: 300,
thumbnail: false,
coverArt: coverArt,
fit: BoxFit.contain,
),
const SizedBox(height: 20),
Column(
children: [
Text(
title,
style: theme.textTheme.headlineMedium,
textAlign: TextAlign.center,
),
Text(
subtitle ?? '',
style: theme.textTheme.headlineSmall,
textAlign: TextAlign.center,
),
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.download_done_rounded),
),
if (onPlay != null)
FilledButton.icon(
onPressed: onPlay,
icon: const Icon(Icons.play_arrow_rounded),
label: Text(
playText ?? '',
// style: theme.textTheme.bodyLarge?.copyWith(
// color: theme.colorScheme.onPrimary,
// ),
),
),
if (onMore != null)
IconButton(
onPressed: onMore,
icon: const Icon(Icons.more_horiz),
),
],
),
],
);
}
}

View File

@ -8,7 +8,6 @@ import '../../l10n/generated/app_localizations.dart';
import '../lists/albums_grid.dart';
import '../lists/artists_list.dart';
import '../state/services.dart';
import '../ui/text.dart';
import '../util/custom_scroll_fix.dart';
const kIconSize = 26.0;
@ -162,6 +161,7 @@ class TabTitleText extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context);
final text = TextTheme.of(context);
String tabLocalization(LibraryTab tab) => switch (tab) {
LibraryTab.albums => l.navigationTabsAlbums,
@ -180,7 +180,7 @@ class TabTitleText extends HookConsumerWidget {
return;
}, [tabName]);
return TextH1(tabText.value);
return Text(tabText.value, style: text.headlineLarge);
}
}

View File

@ -5,7 +5,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../../l10n/generated/app_localizations.dart';
import '../state/database.dart';
import '../state/source.dart';
import '../ui/text.dart';
const kHorizontalPadding = 18.0;
@ -15,10 +14,11 @@ class SettingsScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final l = AppLocalizations.of(context);
final text = TextTheme.of(context);
return Scaffold(
appBar: AppBar(
title: TextH1(l.navigationTabsSettings),
title: Text(l.navigationTabsSettings, style: text.headlineLarge),
),
body: ListView(
children: [
@ -59,6 +59,8 @@ class _SectionHeader extends StatelessWidget {
@override
Widget build(BuildContext context) {
final text = TextTheme.of(context);
return Column(
children: [
const SizedBox(height: 16),
@ -66,7 +68,7 @@ class _SectionHeader extends StatelessWidget {
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: kHorizontalPadding),
child: TextH2(title),
child: Text(title, style: text.headlineMedium),
),
),
],

View File

@ -0,0 +1,56 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import '../state/source.dart';
import '../util/color_scheme.dart';
import 'theme.dart';
class CoverArtTheme extends HookConsumerWidget {
const CoverArtTheme({
super.key,
required this.coverArt,
required this.child,
});
final String? coverArt;
final Widget child;
@override
Widget build(BuildContext context, WidgetRef ref) {
final source = ref.watch(sourceProvider);
final sourceId = ref.watch(sourceIdProvider);
final getColorScheme = useMemoized(
() async {
try {
return await colorSchemefromImageProvider(
brightness: Brightness.dark,
provider: CachedNetworkImageProvider(
coverArt != null
? source.coverArtUri(coverArt!, thumbnail: true).toString()
: 'https://placehold.net/400x400.png',
cacheKey: coverArt != null
? '$sourceId$coverArt${true}'
: 'https://placehold.net/400x400.png',
),
);
} catch (err) {
print(err);
return null;
}
},
[source, sourceId, coverArt],
);
final colorScheme = useFuture(getColorScheme).data;
return colorScheme != null
? Theme(
data: subtracksTheme(colorScheme),
child: child,
)
: child;
}
}

View File

@ -3,17 +3,23 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../app/state/source.dart';
import '../state/source.dart';
class CoverArtImage extends HookConsumerWidget {
const CoverArtImage({
super.key,
this.coverArt,
this.thumbnail = false,
this.thumbnail = true,
this.fit,
this.height,
this.width,
});
final String? coverArt;
final bool thumbnail;
final BoxFit? fit;
final double? height;
final double? width;
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -24,31 +30,11 @@ class CoverArtImage extends HookConsumerWidget {
? source.coverArtUri(coverArt!, thumbnail: thumbnail).toString()
: 'https://placehold.net/400x400.png';
return BaseImage(
imageUrl: imageUrl,
// can't use the URL because of token auth, which is a cache-buster
cacheKey: '$sourceId$coverArt$thumbnail',
);
}
}
class BaseImage extends HookConsumerWidget {
const BaseImage({
super.key,
required this.imageUrl,
this.cacheKey,
this.fit = BoxFit.cover,
});
final String imageUrl;
final String? cacheKey;
final BoxFit fit;
@override
Widget build(BuildContext context, WidgetRef ref) {
return CachedNetworkImage(
height: height,
width: width,
imageUrl: imageUrl,
cacheKey: cacheKey,
cacheKey: '$sourceId$coverArt$thumbnail',
placeholder: (context, url) => Icon(Symbols.cached_rounded),
errorWidget: (context, url, error) => Icon(Icons.error),
fit: BoxFit.cover,

View File

@ -1,43 +0,0 @@
import 'package:flutter/material.dart';
class TextH1 extends StatelessWidget {
const TextH1(
this.data, {
super.key,
});
final String data;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Text(
data,
style: theme.textTheme.headlineLarge?.copyWith(
fontWeight: FontWeight.w800,
),
);
}
}
class TextH2 extends StatelessWidget {
const TextH2(
this.data, {
super.key,
});
final String data;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Text(
data,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w700,
),
);
}
}

28
lib/app/ui/theme.dart Normal file
View File

@ -0,0 +1,28 @@
import 'package:flutter/material.dart';
ThemeData subtracksTheme([ColorScheme? colorScheme]) {
final theme = ThemeData.from(
colorScheme:
colorScheme ??
ColorScheme.fromSeed(
seedColor: Colors.purple.shade800,
brightness: Brightness.dark,
),
useMaterial3: true,
);
final text = theme.textTheme;
return theme.copyWith(
textTheme: text.copyWith(
headlineLarge: text.headlineLarge?.copyWith(
fontWeight: FontWeight.w800,
),
headlineMedium: text.headlineMedium?.copyWith(
fontWeight: FontWeight.w700,
),
headlineSmall: text.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
);
}

View File

@ -0,0 +1,431 @@
/// This file is a fork of the built-in [ColorScheme.fromImageProvider] function
/// with the following changes:
///
/// 1. [_extractColorsFromImageProvider] now runs the Quantizer to extract colors
/// in an isolate to prevent jank on the UI thread (especially noticable during
/// transitions).
///
/// 2. [_imageProviderToScaled] has its hard-coded image resize max dimensions
/// to 32x32 pixels.
library;
import 'dart:async';
import 'dart:isolate';
import 'dart:typed_data';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:material_color_utilities/material_color_utilities.dart';
/// Generate a [ColorScheme] derived from the given `imageProvider`.
///
/// Material Color Utilities extracts the dominant color from the
/// supplied [ImageProvider]. Using this color, a [ColorScheme] is generated
/// with harmonious colors that meet contrast requirements for accessibility.
///
/// If any of the optional color parameters are non-null, they will be
/// used in place of the generated colors for that field in the resulting
/// [ColorScheme]. This allows apps to override specific colors for their
/// needs.
///
/// Given the nature of the algorithm, the most dominant color of the
/// `imageProvider` may not wind up as one of the [ColorScheme] colors.
///
/// The provided image will be scaled down to a maximum size of 112x112 pixels
/// during color extraction.
///
/// {@tool dartpad}
/// This sample shows how to use [ColorScheme.fromImageProvider] to create
/// content-based dynamic color schemes.
///
/// ** See code in examples/api/lib/material/color_scheme/dynamic_content_color.0.dart **
/// {@end-tool}
///
/// See also:
///
/// * [M3 Guidelines: Dynamic color from content](https://m3.material.io/styles/color/dynamic-color/user-generated-color#8af550b9-a19e-4e9f-bb0a-7f611fed5d0f)
/// * <https://pub.dev/packages/dynamic_color>, a package to create
/// [ColorScheme]s based on a platform's implementation of dynamic color.
/// * <https://m3.material.io/styles/color/the-color-system/color-roles>, the
/// Material 3 Color system specification.
/// * <https://pub.dev/packages/material_color_utilities>, the package
/// used to algorithmically determine the dominant color and to generate
/// the [ColorScheme].
Future<ColorScheme> colorSchemefromImageProvider({
required ImageProvider provider,
Brightness brightness = Brightness.light,
DynamicSchemeVariant dynamicSchemeVariant = DynamicSchemeVariant.tonalSpot,
double contrastLevel = 0.0,
Color? primary,
Color? onPrimary,
Color? primaryContainer,
Color? onPrimaryContainer,
Color? primaryFixed,
Color? primaryFixedDim,
Color? onPrimaryFixed,
Color? onPrimaryFixedVariant,
Color? secondary,
Color? onSecondary,
Color? secondaryContainer,
Color? onSecondaryContainer,
Color? secondaryFixed,
Color? secondaryFixedDim,
Color? onSecondaryFixed,
Color? onSecondaryFixedVariant,
Color? tertiary,
Color? onTertiary,
Color? tertiaryContainer,
Color? onTertiaryContainer,
Color? tertiaryFixed,
Color? tertiaryFixedDim,
Color? onTertiaryFixed,
Color? onTertiaryFixedVariant,
Color? error,
Color? onError,
Color? errorContainer,
Color? onErrorContainer,
Color? outline,
Color? outlineVariant,
Color? surface,
Color? onSurface,
Color? surfaceDim,
Color? surfaceBright,
Color? surfaceContainerLowest,
Color? surfaceContainerLow,
Color? surfaceContainer,
Color? surfaceContainerHigh,
Color? surfaceContainerHighest,
Color? onSurfaceVariant,
Color? inverseSurface,
Color? onInverseSurface,
Color? inversePrimary,
Color? shadow,
Color? scrim,
Color? surfaceTint,
@Deprecated(
'Use surface instead. '
'This feature was deprecated after v3.18.0-0.1.pre.',
)
Color? background,
@Deprecated(
'Use onSurface instead. '
'This feature was deprecated after v3.18.0-0.1.pre.',
)
Color? onBackground,
@Deprecated(
'Use surfaceContainerHighest instead. '
'This feature was deprecated after v3.18.0-0.1.pre.',
)
Color? surfaceVariant,
}) async {
// Extract dominant colors from image.
final QuantizerResult quantizerResult = await _extractColorsFromImageProvider(
provider,
);
final Map<int, int> colorToCount = quantizerResult.colorToCount.map(
(int key, int value) => MapEntry<int, int>(_getArgbFromAbgr(key), value),
);
// Score colors for color scheme suitability.
final List<int> scoredResults = Score.score(colorToCount, desired: 1);
final ui.Color baseColor = Color(scoredResults.first);
final DynamicScheme scheme = _buildDynamicScheme(
brightness,
baseColor,
dynamicSchemeVariant,
contrastLevel,
);
return ColorScheme(
primary: primary ?? Color(MaterialDynamicColors.primary.getArgb(scheme)),
onPrimary:
onPrimary ?? Color(MaterialDynamicColors.onPrimary.getArgb(scheme)),
primaryContainer:
primaryContainer ??
Color(MaterialDynamicColors.primaryContainer.getArgb(scheme)),
onPrimaryContainer:
onPrimaryContainer ??
Color(MaterialDynamicColors.onPrimaryContainer.getArgb(scheme)),
primaryFixed:
primaryFixed ??
Color(MaterialDynamicColors.primaryFixed.getArgb(scheme)),
primaryFixedDim:
primaryFixedDim ??
Color(MaterialDynamicColors.primaryFixedDim.getArgb(scheme)),
onPrimaryFixed:
onPrimaryFixed ??
Color(MaterialDynamicColors.onPrimaryFixed.getArgb(scheme)),
onPrimaryFixedVariant:
onPrimaryFixedVariant ??
Color(MaterialDynamicColors.onPrimaryFixedVariant.getArgb(scheme)),
secondary:
secondary ?? Color(MaterialDynamicColors.secondary.getArgb(scheme)),
onSecondary:
onSecondary ?? Color(MaterialDynamicColors.onSecondary.getArgb(scheme)),
secondaryContainer:
secondaryContainer ??
Color(MaterialDynamicColors.secondaryContainer.getArgb(scheme)),
onSecondaryContainer:
onSecondaryContainer ??
Color(MaterialDynamicColors.onSecondaryContainer.getArgb(scheme)),
secondaryFixed:
secondaryFixed ??
Color(MaterialDynamicColors.secondaryFixed.getArgb(scheme)),
secondaryFixedDim:
secondaryFixedDim ??
Color(MaterialDynamicColors.secondaryFixedDim.getArgb(scheme)),
onSecondaryFixed:
onSecondaryFixed ??
Color(MaterialDynamicColors.onSecondaryFixed.getArgb(scheme)),
onSecondaryFixedVariant:
onSecondaryFixedVariant ??
Color(MaterialDynamicColors.onSecondaryFixedVariant.getArgb(scheme)),
tertiary: tertiary ?? Color(MaterialDynamicColors.tertiary.getArgb(scheme)),
onTertiary:
onTertiary ?? Color(MaterialDynamicColors.onTertiary.getArgb(scheme)),
tertiaryContainer:
tertiaryContainer ??
Color(MaterialDynamicColors.tertiaryContainer.getArgb(scheme)),
onTertiaryContainer:
onTertiaryContainer ??
Color(MaterialDynamicColors.onTertiaryContainer.getArgb(scheme)),
tertiaryFixed:
tertiaryFixed ??
Color(MaterialDynamicColors.tertiaryFixed.getArgb(scheme)),
tertiaryFixedDim:
tertiaryFixedDim ??
Color(MaterialDynamicColors.tertiaryFixedDim.getArgb(scheme)),
onTertiaryFixed:
onTertiaryFixed ??
Color(MaterialDynamicColors.onTertiaryFixed.getArgb(scheme)),
onTertiaryFixedVariant:
onTertiaryFixedVariant ??
Color(MaterialDynamicColors.onTertiaryFixedVariant.getArgb(scheme)),
error: error ?? Color(MaterialDynamicColors.error.getArgb(scheme)),
onError: onError ?? Color(MaterialDynamicColors.onError.getArgb(scheme)),
errorContainer:
errorContainer ??
Color(MaterialDynamicColors.errorContainer.getArgb(scheme)),
onErrorContainer:
onErrorContainer ??
Color(MaterialDynamicColors.onErrorContainer.getArgb(scheme)),
outline: outline ?? Color(MaterialDynamicColors.outline.getArgb(scheme)),
outlineVariant:
outlineVariant ??
Color(MaterialDynamicColors.outlineVariant.getArgb(scheme)),
surface: surface ?? Color(MaterialDynamicColors.surface.getArgb(scheme)),
surfaceDim:
surfaceDim ?? Color(MaterialDynamicColors.surfaceDim.getArgb(scheme)),
surfaceBright:
surfaceBright ??
Color(MaterialDynamicColors.surfaceBright.getArgb(scheme)),
surfaceContainerLowest:
surfaceContainerLowest ??
Color(MaterialDynamicColors.surfaceContainerLowest.getArgb(scheme)),
surfaceContainerLow:
surfaceContainerLow ??
Color(MaterialDynamicColors.surfaceContainerLow.getArgb(scheme)),
surfaceContainer:
surfaceContainer ??
Color(MaterialDynamicColors.surfaceContainer.getArgb(scheme)),
surfaceContainerHigh:
surfaceContainerHigh ??
Color(MaterialDynamicColors.surfaceContainerHigh.getArgb(scheme)),
surfaceContainerHighest:
surfaceContainerHighest ??
Color(MaterialDynamicColors.surfaceContainerHighest.getArgb(scheme)),
onSurface:
onSurface ?? Color(MaterialDynamicColors.onSurface.getArgb(scheme)),
onSurfaceVariant:
onSurfaceVariant ??
Color(MaterialDynamicColors.onSurfaceVariant.getArgb(scheme)),
inverseSurface:
inverseSurface ??
Color(MaterialDynamicColors.inverseSurface.getArgb(scheme)),
onInverseSurface:
onInverseSurface ??
Color(MaterialDynamicColors.inverseOnSurface.getArgb(scheme)),
inversePrimary:
inversePrimary ??
Color(MaterialDynamicColors.inversePrimary.getArgb(scheme)),
shadow: shadow ?? Color(MaterialDynamicColors.shadow.getArgb(scheme)),
scrim: scrim ?? Color(MaterialDynamicColors.scrim.getArgb(scheme)),
surfaceTint:
surfaceTint ?? Color(MaterialDynamicColors.primary.getArgb(scheme)),
brightness: brightness,
// DEPRECATED (newest deprecations at the bottom)
// ignore: deprecated_member_use
background:
background ?? Color(MaterialDynamicColors.background.getArgb(scheme)),
// ignore: deprecated_member_use
onBackground:
onBackground ??
Color(MaterialDynamicColors.onBackground.getArgb(scheme)),
// ignore: deprecated_member_use
surfaceVariant:
surfaceVariant ??
Color(MaterialDynamicColors.surfaceVariant.getArgb(scheme)),
);
}
// ColorScheme.fromImageProvider() utilities.
/// Extracts bytes from an [ImageProvider] and returns a [QuantizerResult]
/// containing the most dominant colors.
Future<QuantizerResult> _extractColorsFromImageProvider(
ImageProvider imageProvider,
) async {
final ui.Image scaledImage = await _imageProviderToScaled(imageProvider);
final ByteData? imageBytes = await scaledImage.toByteData();
return Isolate.run(
() => QuantizerCelebi().quantize(
imageBytes!.buffer.asUint32List(),
128,
returnInputPixelToClusterPixel: true,
),
);
}
/// Scale image size down to reduce computation time of color extraction.
Future<ui.Image> _imageProviderToScaled(ImageProvider imageProvider) async {
const double maxDimension = 32.0;
final ImageStream stream = imageProvider.resolve(
const ImageConfiguration(size: Size(maxDimension, maxDimension)),
);
final Completer<ui.Image> imageCompleter = Completer<ui.Image>();
late ImageStreamListener listener;
late ui.Image scaledImage;
Timer? loadFailureTimeout;
listener = ImageStreamListener(
(ImageInfo info, bool sync) async {
loadFailureTimeout?.cancel();
stream.removeListener(listener);
final ui.Image image = info.image;
final int width = image.width;
final int height = image.height;
double paintWidth = width.toDouble();
double paintHeight = height.toDouble();
assert(width > 0 && height > 0);
final bool rescale = width > maxDimension || height > maxDimension;
if (rescale) {
paintWidth = (width > height)
? maxDimension
: (maxDimension / height) * width;
paintHeight = (height > width)
? maxDimension
: (maxDimension / width) * height;
}
final ui.PictureRecorder pictureRecorder = ui.PictureRecorder();
final Canvas canvas = Canvas(pictureRecorder);
paintImage(
canvas: canvas,
rect: Rect.fromLTRB(0, 0, paintWidth, paintHeight),
image: image,
filterQuality: FilterQuality.none,
);
final ui.Picture picture = pictureRecorder.endRecording();
scaledImage = await picture.toImage(
paintWidth.toInt(),
paintHeight.toInt(),
);
imageCompleter.complete(info.image);
},
onError: (Object exception, StackTrace? stackTrace) {
loadFailureTimeout?.cancel();
stream.removeListener(listener);
imageCompleter.completeError(
Exception('Failed to render image: $exception'),
stackTrace,
);
},
);
loadFailureTimeout = Timer(const Duration(seconds: 5), () {
stream.removeListener(listener);
imageCompleter.completeError(
TimeoutException('Timeout occurred trying to load image'),
);
});
stream.addListener(listener);
await imageCompleter.future;
return scaledImage;
}
/// Converts AABBGGRR color int to AARRGGBB format.
int _getArgbFromAbgr(int abgr) {
const int exceptRMask = 0xFF00FFFF;
const int onlyRMask = ~exceptRMask;
const int exceptBMask = 0xFFFFFF00;
const int onlyBMask = ~exceptBMask;
final int r = (abgr & onlyRMask) >> 16;
final int b = abgr & onlyBMask;
return (abgr & exceptRMask & exceptBMask) | (b << 16) | r;
}
DynamicScheme _buildDynamicScheme(
Brightness brightness,
Color seedColor,
DynamicSchemeVariant schemeVariant,
double contrastLevel,
) {
assert(
contrastLevel >= -1.0 && contrastLevel <= 1.0,
'contrastLevel must be between -1.0 and 1.0 inclusive.',
);
final bool isDark = brightness == Brightness.dark;
// ignore: deprecated_member_use
final Hct sourceColor = Hct.fromInt(seedColor.value);
return switch (schemeVariant) {
DynamicSchemeVariant.tonalSpot => SchemeTonalSpot(
sourceColorHct: sourceColor,
isDark: isDark,
contrastLevel: contrastLevel,
),
DynamicSchemeVariant.fidelity => SchemeFidelity(
sourceColorHct: sourceColor,
isDark: isDark,
contrastLevel: contrastLevel,
),
DynamicSchemeVariant.content => SchemeContent(
sourceColorHct: sourceColor,
isDark: isDark,
contrastLevel: contrastLevel,
),
DynamicSchemeVariant.monochrome => SchemeMonochrome(
sourceColorHct: sourceColor,
isDark: isDark,
contrastLevel: contrastLevel,
),
DynamicSchemeVariant.neutral => SchemeNeutral(
sourceColorHct: sourceColor,
isDark: isDark,
contrastLevel: contrastLevel,
),
DynamicSchemeVariant.vibrant => SchemeVibrant(
sourceColorHct: sourceColor,
isDark: isDark,
contrastLevel: contrastLevel,
),
DynamicSchemeVariant.expressive => SchemeExpressive(
sourceColorHct: sourceColor,
isDark: isDark,
contrastLevel: contrastLevel,
),
DynamicSchemeVariant.rainbow => SchemeRainbow(
sourceColorHct: sourceColor,
isDark: isDark,
contrastLevel: contrastLevel,
),
DynamicSchemeVariant.fruitSalad => SchemeFruitSalad(
sourceColorHct: sourceColor,
isDark: isDark,
contrastLevel: contrastLevel,
),
};
}

View File

@ -58,4 +58,10 @@ class LibraryDao extends DatabaseAccessor<SubtracksDatabase>
)
.toList();
}
Selectable<models.Album> getAlbum(int sourceId, String id) {
return db.managers.albums.filter(
(f) => f.sourceId.equals(sourceId) & f.id.equals(id),
);
}
}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'app/router.dart';
import 'app/ui/theme.dart';
import 'l10n/generated/app_localizations.dart';
void main() async {
@ -17,7 +18,7 @@ class MainApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp.router(
themeMode: ThemeMode.dark,
darkTheme: ThemeData.dark(useMaterial3: true),
darkTheme: subtracksTheme(),
debugShowCheckedModeBanner: false,
routerConfig: router,
localizationsDelegates: AppLocalizations.localizationsDelegates,

View File

@ -582,7 +582,7 @@ packages:
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
dependency: "direct main"
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec

View File

@ -26,6 +26,7 @@ dependencies:
infinite_scroll_pagination: ^5.1.1
intl: any
json_annotation: ^4.9.0
material_color_utilities: ^0.11.1
material_symbols_icons: ^4.2874.0
octo_image: ^2.1.0
package_info_plus: ^9.0.0