mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
cover art color scheme extraction (in background)
refactor text styles to use theme port over part of album screen
This commit is contained in:
parent
b9a094c1c4
commit
6609671ae2
@ -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,
|
||||
|
||||
@ -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(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('Album $id!'),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.push('/artist');
|
||||
},
|
||||
child: Text('Artist...'),
|
||||
),
|
||||
CachedNetworkImage(
|
||||
imageUrl: 'https://placehold.net/400x400.png',
|
||||
placeholder: (context, url) => CircularProgressIndicator(),
|
||||
errorWidget: (context, url, error) => Icon(Icons.error),
|
||||
),
|
||||
],
|
||||
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: 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: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
56
lib/app/ui/cover_art_theme.dart
Normal file
56
lib/app/ui/cover_art_theme.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
@ -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
28
lib/app/ui/theme.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
431
lib/app/util/color_scheme.dart
Normal file
431
lib/app/util/color_scheme.dart
Normal 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,
|
||||
),
|
||||
};
|
||||
}
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user