diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bfcd0e5f5..fda262d88 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -177,6 +177,10 @@ "accessibilityAnimationsRemove": "Prevent screen effects", "accessibilityAnimationsKeep": "Keep screen effects", + "themeBrightnessLight": "Light", + "themeBrightnessDark": "Dark", + "themeBrightnessBlack": "Black", + "albumTierNew": "New", "albumTierPinned": "Pinned", "albumTierSpecial": "Common", @@ -671,6 +675,10 @@ "settingsTimeToTakeActionTile": "Time to take action", "settingsTimeToTakeActionTitle": "Time to Take Action", + "settingsSectionDisplay": "Display", + "settingsThemeBrightness": "Theme", + "settingsThemeColorful": "Colorful", + "settingsSectionLanguage": "Language & Formats", "settingsLanguage": "Language", "settingsCoordinateFormatTile": "Coordinate format", diff --git a/lib/model/actions/entry_actions.dart b/lib/model/actions/entry_actions.dart index b9282b429..5b21dfa55 100644 --- a/lib/model/actions/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -177,7 +177,8 @@ extension ExtraEntryAction on EntryAction { switch (this) { case EntryAction.debug: return ShaderMask( - shaderCallback: AColors.debugGradient.createShader, + shaderCallback: AvesColorsData.debugGradient.createShader, + blendMode: BlendMode.srcIn, child: child, ); default: diff --git a/lib/model/actions/entry_info_actions.dart b/lib/model/actions/entry_info_actions.dart index 1498c8066..313bdb2e1 100644 --- a/lib/model/actions/entry_info_actions.dart +++ b/lib/model/actions/entry_info_actions.dart @@ -55,7 +55,8 @@ extension ExtraEntryInfoAction on EntryInfoAction { switch (this) { case EntryInfoAction.debug: return ShaderMask( - shaderCallback: AColors.debugGradient.createShader, + shaderCallback: AvesColorsData.debugGradient.createShader, + blendMode: BlendMode.srcIn, child: child, ); default: diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 48fb78ab5..ffe4cd293 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -1,4 +1,3 @@ -import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/colors.dart'; @@ -7,13 +6,11 @@ import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:palette_generator/palette_generator.dart'; +import 'package:provider/provider.dart'; class AlbumFilter extends CollectionFilter { static const type = 'album'; - static final Map _appColors = {}; - final String album; final String? displayName; @@ -56,6 +53,7 @@ class AlbumFilter extends CollectionFilter { @override Future color(BuildContext context) { + final colors = context.watch(); // do not use async/await and rely on `SynchronousFuture` // to prevent rebuilding of the `FutureBuilder` listening on this future final albumType = androidFileUtils.getAlbumType(album); @@ -63,31 +61,19 @@ class AlbumFilter extends CollectionFilter { case AlbumType.regular: break; case AlbumType.app: - if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!); - - final packageName = androidFileUtils.getAlbumAppPackageName(album); - if (packageName != null) { - return PaletteGenerator.fromImageProvider( - AppIconImage(packageName: packageName, size: 24), - ).then((palette) async { - // `dominantColor` is most representative but can have low contrast with a dark background - // `vibrantColor` is usually representative and has good contrast with a dark background - final color = palette.vibrantColor?.color ?? (await super.color(context)); - _appColors[album] = color; - return color; - }); - } + final appColor = colors.appColor(album); + if (appColor != null) return appColor; break; case AlbumType.camera: - return SynchronousFuture(AColors.albumCamera); + return SynchronousFuture(colors.albumCamera); case AlbumType.download: - return SynchronousFuture(AColors.albumDownload); + return SynchronousFuture(colors.albumDownload); case AlbumType.screenRecordings: - return SynchronousFuture(AColors.albumScreenRecordings); + return SynchronousFuture(colors.albumScreenRecordings); case AlbumType.screenshots: - return SynchronousFuture(AColors.albumScreenshots); + return SynchronousFuture(colors.albumScreenshots); case AlbumType.videoCaptures: - return SynchronousFuture(AColors.albumVideoCaptures); + return SynchronousFuture(colors.albumVideoCaptures); } return super.color(context); } diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index afa6116a6..964992f8c 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -4,6 +4,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class FavouriteFilter extends CollectionFilter { static const type = 'favourite'; @@ -33,7 +34,10 @@ class FavouriteFilter extends CollectionFilter { Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.favourite, size: size); @override - Future color(BuildContext context) => SynchronousFuture(AColors.favourite); + Future color(BuildContext context) { + final colors = context.watch(); + return SynchronousFuture(colors.favourite); + } @override String get category => type; diff --git a/lib/model/filters/filters.dart b/lib/model/filters/filters.dart index 2c9da7615..567d45a62 100644 --- a/lib/model/filters/filters.dart +++ b/lib/model/filters/filters.dart @@ -12,11 +12,12 @@ import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/trash.dart'; import 'package:aves/model/filters/type.dart'; -import 'package:aves/utils/color_utils.dart'; +import 'package:aves/theme/colors.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; @immutable abstract class CollectionFilter extends Equatable implements Comparable { @@ -93,7 +94,10 @@ abstract class CollectionFilter extends Equatable implements Comparable null; - Future color(BuildContext context) => SynchronousFuture(stringToColor(getLabel(context))); + Future color(BuildContext context) { + final colors = context.watch(); + return SynchronousFuture(colors.fromString(getLabel(context))); + } String get category; diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 9bdc1c753..9e81157cc 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -1,12 +1,14 @@ +import 'dart:async'; + import 'package:aves/model/filters/filters.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; class MimeFilter extends CollectionFilter { static const type = 'mime'; @@ -15,7 +17,6 @@ class MimeFilter extends CollectionFilter { late final EntryFilter _test; late final String _label; late final IconData _icon; - late final Color _color; static final image = MimeFilter(MimeTypes.anyImage); static final video = MimeFilter(MimeTypes.anyVideo); @@ -25,7 +26,6 @@ class MimeFilter extends CollectionFilter { MimeFilter(this.mime) { IconData? icon; - Color? color; var lowMime = mime.toLowerCase(); if (lowMime.endsWith('/*')) { lowMime = lowMime.substring(0, lowMime.length - 2); @@ -33,17 +33,14 @@ class MimeFilter extends CollectionFilter { _label = lowMime.toUpperCase(); if (mime == MimeTypes.anyImage) { icon = AIcons.image; - color = AColors.image; } else if (mime == MimeTypes.anyVideo) { icon = AIcons.video; - color = AColors.video; } } else { _test = (entry) => entry.mimeType == lowMime; _label = MimeUtils.displayType(lowMime); } _icon = icon ?? AIcons.vector; - _color = color ?? stringToColor(_label); } MimeFilter.fromMap(Map json) @@ -79,7 +76,17 @@ class MimeFilter extends CollectionFilter { Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); @override - Future color(BuildContext context) => SynchronousFuture(_color); + Future color(BuildContext context) { + final colors = context.watch(); + switch (mime) { + case MimeTypes.anyImage: + return SynchronousFuture(colors.image); + case MimeTypes.anyVideo: + return SynchronousFuture(colors.video); + default: + return SynchronousFuture(colors.fromString(_label)); + } + } @override String get category => type; diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index b7c5e210d..e8fe793ea 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -1,10 +1,11 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; class QueryFilter extends CollectionFilter { static const type = 'query'; @@ -67,7 +68,14 @@ class QueryFilter extends CollectionFilter { Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(AIcons.text, size: size); @override - Future color(BuildContext context) => colorful ? super.color(context) : SynchronousFuture(AvesFilterChip.defaultOutlineColor); + Future color(BuildContext context) { + if (colorful) { + return super.color(context); + } + + final colors = context.watch(); + return SynchronousFuture(colors.neutral); + } @override String get category => type; diff --git a/lib/model/filters/type.dart b/lib/model/filters/type.dart index 1d2b986f0..e4565a671 100644 --- a/lib/model/filters/type.dart +++ b/lib/model/filters/type.dart @@ -4,6 +4,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; class TypeFilter extends CollectionFilter { static const type = 'type'; @@ -18,7 +19,6 @@ class TypeFilter extends CollectionFilter { final String itemType; late final EntryFilter _test; late final IconData _icon; - late final Color _color; static final animated = TypeFilter._private(_animated); static final geotiff = TypeFilter._private(_geotiff); @@ -35,32 +35,26 @@ class TypeFilter extends CollectionFilter { case _animated: _test = (entry) => entry.isAnimated; _icon = AIcons.animated; - _color = AColors.animated; break; case _geotiff: _test = (entry) => entry.isGeotiff; _icon = AIcons.geo; - _color = AColors.geotiff; break; case _motionPhoto: _test = (entry) => entry.isMotionPhoto; _icon = AIcons.motionPhoto; - _color = AColors.motionPhoto; break; case _panorama: _test = (entry) => entry.isImage && entry.is360; _icon = AIcons.threeSixty; - _color = AColors.panorama; break; case _raw: _test = (entry) => entry.isRaw; _icon = AIcons.raw; - _color = AColors.raw; break; case _sphericalVideo: _test = (entry) => entry.isVideo && entry.is360; _icon = AIcons.threeSixty; - _color = AColors.sphericalVideo; break; } } @@ -106,7 +100,24 @@ class TypeFilter extends CollectionFilter { Widget iconBuilder(BuildContext context, double size, {bool showGenericIcon = true}) => Icon(_icon, size: size); @override - Future color(BuildContext context) => SynchronousFuture(_color); + Future color(BuildContext context) { + final colors = context.watch(); + switch (itemType) { + case _animated: + return SynchronousFuture(colors.animated); + case _geotiff: + return SynchronousFuture(colors.geotiff); + case _motionPhoto: + return SynchronousFuture(colors.motionPhoto); + case _panorama: + return SynchronousFuture(colors.panorama); + case _raw: + return SynchronousFuture(colors.raw); + case _sphericalVideo: + return SynchronousFuture(colors.sphericalVideo); + } + return super.color(context); + } @override String get category => type; diff --git a/lib/model/settings/defaults.dart b/lib/model/settings/defaults.dart index afa1f219f..dd6c75106 100644 --- a/lib/model/settings/defaults.dart +++ b/lib/model/settings/defaults.dart @@ -16,6 +16,8 @@ class SettingsDefaults { static const isInstalledAppAccessAllowed = false; static const isErrorReportingAllowed = false; static const tileLayout = TileLayout.grid; + static const themeBrightness = AvesThemeBrightness.system; + static const themeColorMode = AvesThemeColorMode.polychrome; // navigation static const mustBackTwiceToExit = true; diff --git a/lib/model/settings/enums/enums.dart b/lib/model/settings/enums/enums.dart index f5100f413..2c73ccec7 100644 --- a/lib/model/settings/enums/enums.dart +++ b/lib/model/settings/enums/enums.dart @@ -2,6 +2,10 @@ enum AccessibilityAnimations { system, disabled, enabled } enum AccessibilityTimeout { system, appDefault, s10, s30, s60, s120 } +enum AvesThemeColorMode { monochrome, polychrome } + +enum AvesThemeBrightness { system, light, dark, black } + enum ConfirmationDialog { delete, moveToBin } enum CoordinateFormat { dms, decimal } diff --git a/lib/model/settings/enums/theme_brightness.dart b/lib/model/settings/enums/theme_brightness.dart new file mode 100644 index 000000000..effdeefc4 --- /dev/null +++ b/lib/model/settings/enums/theme_brightness.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import 'enums.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; + +extension ExtraAvesThemeBrightness on AvesThemeBrightness { + String getName(BuildContext context) { + switch (this) { + case AvesThemeBrightness.system: + return context.l10n.settingsSystemDefault; + case AvesThemeBrightness.light: + return context.l10n.themeBrightnessLight; + case AvesThemeBrightness.dark: + return context.l10n.themeBrightnessDark; + case AvesThemeBrightness.black: + return context.l10n.themeBrightnessBlack; + } + } + ThemeMode get appThemeMode { + switch (this) { + case AvesThemeBrightness.system: + return ThemeMode.system; + case AvesThemeBrightness.light: + return ThemeMode.light; + case AvesThemeBrightness.dark: + case AvesThemeBrightness.black: + return ThemeMode.dark; + } + } +} diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 67b8f7e0f..443db7e31 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -41,6 +41,8 @@ class Settings extends ChangeNotifier { static const isInstalledAppAccessAllowedKey = 'is_installed_app_access_allowed'; static const isErrorReportingAllowedKey = 'is_crashlytics_enabled'; static const localeKey = 'locale'; + static const themeBrightnessKey = 'theme_brightness'; + static const themeColorModeKey = 'theme_color_mode'; static const catalogTimeZoneKey = 'catalog_time_zone'; static const tileExtentPrefixKey = 'tile_extent_'; static const tileLayoutPrefixKey = 'tile_layout_'; @@ -239,6 +241,14 @@ class Settings extends ChangeNotifier { return _appliedLocale!; } + AvesThemeBrightness get themeBrightness => getEnumOrDefault(themeBrightnessKey, SettingsDefaults.themeBrightness, AvesThemeBrightness.values); + + set themeBrightness(AvesThemeBrightness newValue) => setAndNotify(themeBrightnessKey, newValue.toString()); + + AvesThemeColorMode get themeColorMode => getEnumOrDefault(themeColorModeKey, SettingsDefaults.themeColorMode, AvesThemeColorMode.values); + + set themeColorMode(AvesThemeColorMode newValue) => setAndNotify(themeColorModeKey, newValue.toString()); + String get catalogTimeZone => getString(catalogTimeZoneKey) ?? ''; set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); @@ -675,6 +685,8 @@ class Settings extends ChangeNotifier { } break; case localeKey: + case themeBrightnessKey: + case themeColorModeKey: case keepScreenOnKey: case homePageKey: case collectionGroupFactorKey: diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index 83cc962d8..cda295720 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -325,4 +325,7 @@ class CollectionLens with ChangeNotifier { sections = Map.unmodifiable(Map.fromEntries(sections.entries.where((kv) => kv.value.isNotEmpty))); notifyListeners(); } + + @override + String toString() => '$runtimeType#${shortHash(this)}{id=$id, source=$source, filters=$filters, entryCount=$entryCount}'; } diff --git a/lib/theme/colors.dart b/lib/theme/colors.dart index e430be7a0..bb17bd746 100644 --- a/lib/theme/colors.dart +++ b/lib/theme/colors.dart @@ -1,37 +1,125 @@ -import 'package:aves/utils/color_utils.dart'; +import 'package:aves/image_providers/app_icon_image_provider.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/utils/android_file_utils.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:palette_generator/palette_generator.dart'; +import 'package:provider/provider.dart'; + +class AvesColorsProvider extends StatelessWidget { + final Widget child; + + const AvesColorsProvider({ + Key? key, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ProxyProvider( + update: (context, settings, __) { + final isDark = Theme.of(context).brightness == Brightness.dark; + switch (settings.themeColorMode) { + case AvesThemeColorMode.monochrome: + return isDark ? _MonochromeOnDark() : _MonochromeOnLight(); + case AvesThemeColorMode.polychrome: + return isDark ? NeonOnDark() : PastelOnLight(); + } + }, + child: child, + ); + } +} + +abstract class AvesColorsData { + Color get neutral; + + Color fromHue(double hue); + + Color? fromBrandColor(Color? color); + + final Map _stringColors = {}, _appColors = {}; + + Color fromString(String string) { + var color = _stringColors[string]; + if (color == null) { + final hash = string.codeUnits.fold(0, (prev, el) => prev = el + ((prev << 5) - prev)); + final hue = (hash % 360).toDouble(); + color = fromHue(hue); + _stringColors[string] = color; + } + return color; + } + + Future? appColor(String album) { + if (_appColors.containsKey(album)) return SynchronousFuture(_appColors[album]!); + + final packageName = androidFileUtils.getAlbumAppPackageName(album); + if (packageName == null) return null; + + return PaletteGenerator.fromImageProvider( + AppIconImage(packageName: packageName, size: 24), + ).then((palette) async { + // `dominantColor` is most representative but can have low contrast with a dark background + // `vibrantColor` is usually representative and has good contrast with a dark background + final color = palette.vibrantColor?.color ?? fromString(album); + _appColors[album] = color; + return color; + }); + } + + static const Color _neutralOnDark = Colors.white; + static const Color _neutralOnLight = Color(0xAA000000); -class AColors { // mime - static final image = stringToColor('Image'); - static final video = stringToColor('Video'); + Color get image => fromHue(243); + + Color get video => fromHue(323); // type - static const favourite = Colors.red; - static final animated = stringToColor('Animated'); - static final geotiff = stringToColor('GeoTIFF'); - static final motionPhoto = stringToColor('Motion Photo'); - static final panorama = stringToColor('Panorama'); - static final raw = stringToColor('Raw'); - static final sphericalVideo = stringToColor('360° Video'); + Color get favourite => fromHue(0); + + Color get animated => fromHue(83); + + Color get geotiff => fromHue(70); + + Color get motionPhoto => fromHue(104); + + Color get panorama => fromHue(5); + + Color get raw => fromHue(208); + + Color get sphericalVideo => fromHue(174); // albums - static final albumCamera = stringToColor('Camera'); - static final albumDownload = stringToColor('Download'); - static final albumScreenshots = stringToColor('Screenshots'); - static final albumScreenRecordings = stringToColor('Screen recordings'); - static final albumVideoCaptures = stringToColor('Video Captures'); + Color get albumCamera => fromHue(165); + + Color get albumDownload => fromHue(104); + + Color get albumScreenshots => fromHue(149); + + Color get albumScreenRecordings => fromHue(222); + + Color get albumVideoCaptures => fromHue(266); // info - static final xmp = stringToColor('XMP'); + Color get xmp => fromHue(275); // settings - static final accessibility = stringToColor('Accessibility'); - static final language = stringToColor('Language'); - static final navigation = stringToColor('Navigation'); - static final privacy = stringToColor('Privacy'); - static final thumbnails = stringToColor('Thumbnails'); + Color get accessibility => fromHue(134); + Color get display => fromHue(50); + + Color get language => fromHue(264); + + Color get navigation => fromHue(140); + + Color get privacy => fromHue(344); + + Color get thumbnails => fromHue(87); + + // debug static const debugGradient = LinearGradient( begin: Alignment.bottomCenter, end: Alignment.topCenter, @@ -41,3 +129,51 @@ class AColors { ], ); } + +abstract class _Monochrome extends AvesColorsData { + @override + Color fromHue(double hue) => neutral; + + @override + Color? fromBrandColor(Color? color) => neutral; + + @override + Color fromString(String string) => neutral; + + @override + Future? appColor(String album) => SynchronousFuture(neutral); +} + +class _MonochromeOnDark extends _Monochrome { + @override + Color get neutral => AvesColorsData._neutralOnDark; +} + +class _MonochromeOnLight extends _Monochrome { + @override + Color get neutral => AvesColorsData._neutralOnLight; +} + +class NeonOnDark extends AvesColorsData { + @override + Color get neutral => AvesColorsData._neutralOnDark; + + @override + Color fromHue(double hue) => HSLColor.fromAHSL(1.0, hue, .8, .6).toColor(); + + @override + Color? fromBrandColor(Color? color) => color; +} + +class PastelOnLight extends AvesColorsData { + @override + Color get neutral => AvesColorsData._neutralOnLight; + + @override + Color fromHue(double hue) => _pastellize(HSLColor.fromAHSL(1.0, hue, .8, .6).toColor()); + + @override + Color? fromBrandColor(Color? color) => color != null ? _pastellize(color) : null; + + Color _pastellize(Color color) => Color.lerp(color, Colors.white, .5)!; +} diff --git a/lib/theme/durations.dart b/lib/theme/durations.dart index da4010ca9..c13392c3c 100644 --- a/lib/theme/durations.dart +++ b/lib/theme/durations.dart @@ -47,6 +47,7 @@ class Durations { static const tagEditorTransition = Duration(milliseconds: 200); // settings animations + static const themeColorModeAnimation = Duration(milliseconds: 400); static const quickActionListAnimation = Duration(milliseconds: 200); static const quickActionHighlightAnimation = Duration(milliseconds: 200); diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 8c02d5913..0f0c3ee28 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -14,6 +14,7 @@ class AIcons { static const IconData checked = Icons.done_outlined; static const IconData date = Icons.calendar_today_outlined; static const IconData disc = Icons.fiber_manual_record; + static const IconData display = Icons.light_mode_outlined; static const IconData error = Icons.error_outline; static const IconData folder = Icons.folder_outlined; static const IconData grid = Icons.grid_on_outlined; diff --git a/lib/theme/themes.dart b/lib/theme/themes.dart index 795c00371..034874289 100644 --- a/lib/theme/themes.dart +++ b/lib/theme/themes.dart @@ -1,50 +1,181 @@ import 'dart:ui'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; class Themes { static const _accentColor = Colors.indigoAccent; - static final darkTheme = ThemeData( - brightness: Brightness.dark, - // canvas color is used as background for the drawer and popups - // when using a popup menu on a dialog, lighten the background via `PopupMenuTheme` - canvasColor: Colors.grey[850], - scaffoldBackgroundColor: Colors.grey.shade900, - dialogBackgroundColor: Colors.grey[850], - indicatorColor: _accentColor, - toggleableActiveColor: _accentColor, - tooltipTheme: const TooltipThemeData( - verticalOffset: 32, - ), - appBarTheme: AppBarTheme( - backgroundColor: Colors.grey.shade900, - titleTextStyle: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.normal, - fontFeatures: [FontFeature.enable('smcp')], - ), - ), - colorScheme: ColorScheme.dark( + static const _tooltipTheme = TooltipThemeData( + verticalOffset: 32, + ); + + static const _appBarTitleTextStyle = TextStyle( + fontSize: 20, + fontWeight: FontWeight.normal, + fontFeatures: [FontFeature.enable('smcp')], + ); + + static const _snackBarTheme = SnackBarThemeData( + actionTextColor: _accentColor, + behavior: SnackBarBehavior.floating, + ); + + static final _typography = Typography.material2018(platform: TargetPlatform.android); + + static final _lightThemeTypo = _typography.black; + static final _lightTitleColor = _lightThemeTypo.titleMedium!.color!; + static final _lightBodyColor = _lightThemeTypo.bodyMedium!.color!; + static final _lightLabelColor = _lightThemeTypo.labelMedium!.color!; + static const _lightActionIconColor = Color(0xAA000000); + static const _lightFirstLayer = Color(0xFFFAFAFA); // aka `Colors.grey[50]` + static const _lightSecondLayer = Color(0xFFF5F5F5); // aka `Colors.grey[100]` + static const _lightThirdLayer = Color(0xFFEEEEEE); // aka `Colors.grey[200]` + + static final lightTheme = ThemeData( + colorScheme: ColorScheme.light( primary: _accentColor, secondary: _accentColor, - // surface color is used as background for the date picker header - surface: Colors.grey.shade800, - onPrimary: Colors.white, - onSecondary: Colors.white, + onPrimary: _lightBodyColor, + onSecondary: _lightBodyColor, ), - snackBarTheme: SnackBarThemeData( - backgroundColor: Colors.grey.shade800, - actionTextColor: _accentColor, - contentTextStyle: const TextStyle( - color: Colors.white, - ), - behavior: SnackBarBehavior.floating, + brightness: Brightness.light, + // `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard` + canvasColor: _lightSecondLayer, + scaffoldBackgroundColor: _lightFirstLayer, + // `cardColor` is used by `ExpansionPanel` + cardColor: _lightSecondLayer, + dialogBackgroundColor: _lightSecondLayer, + indicatorColor: _accentColor, + toggleableActiveColor: _accentColor, + typography: _typography, + appBarTheme: AppBarTheme( + backgroundColor: _lightFirstLayer, + // `foregroundColor` is used by icons + foregroundColor: _lightActionIconColor, + // `titleTextStyle.color` is used by text + titleTextStyle: _appBarTitleTextStyle.copyWith(color: _lightTitleColor), + systemOverlayStyle: SystemUiOverlayStyle.dark, + ), + listTileTheme: const ListTileThemeData( + iconColor: _lightActionIconColor, + ), + popupMenuTheme: const PopupMenuThemeData( + color: _lightSecondLayer, + ), + snackBarTheme: _snackBarTheme, + tabBarTheme: TabBarTheme( + labelColor: _lightTitleColor, + unselectedLabelColor: Colors.black54, ), textButtonTheme: TextButtonThemeData( style: TextButton.styleFrom( - primary: Colors.white, + primary: _lightLabelColor, ), ), + tooltipTheme: _tooltipTheme, ); + + static final _darkThemeTypo = _typography.white; + static final _darkTitleColor = _darkThemeTypo.titleMedium!.color!; + static final _darkBodyColor = _darkThemeTypo.bodyMedium!.color!; + static final _darkLabelColor = _darkThemeTypo.labelMedium!.color!; + static const _darkFirstLayer = Color(0xFF212121); // aka `Colors.grey[900]` + static const _darkSecondLayer = Color(0xFF363636); + static const _darkThirdLayer = Color(0xFF424242); // aka `Colors.grey[800]` + + static final darkTheme = ThemeData( + colorScheme: ColorScheme.dark( + primary: _accentColor, + secondary: _accentColor, + // surface color is used by the date/time pickers + surface: Colors.grey.shade800, + onPrimary: _darkBodyColor, + onSecondary: _darkBodyColor, + ), + brightness: Brightness.dark, + // `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard` + canvasColor: _darkSecondLayer, + scaffoldBackgroundColor: _darkFirstLayer, + // `cardColor` is used by `ExpansionPanel` + cardColor: _darkSecondLayer, + dialogBackgroundColor: _darkSecondLayer, + indicatorColor: _accentColor, + toggleableActiveColor: _accentColor, + typography: _typography, + appBarTheme: AppBarTheme( + backgroundColor: _darkFirstLayer, + // `foregroundColor` is used by icons + foregroundColor: _darkTitleColor, + // `titleTextStyle.color` is used by text + titleTextStyle: _appBarTitleTextStyle.copyWith(color: _darkTitleColor), + systemOverlayStyle: SystemUiOverlayStyle.light, + ), + popupMenuTheme: const PopupMenuThemeData( + color: _darkSecondLayer, + ), + snackBarTheme: _snackBarTheme.copyWith( + backgroundColor: Colors.grey.shade800, + contentTextStyle: TextStyle( + color: _darkBodyColor, + ), + ), + tabBarTheme: TabBarTheme( + labelColor: _darkTitleColor, + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + primary: _darkLabelColor, + ), + ), + tooltipTheme: _tooltipTheme, + ); + + static const _blackFirstLayer = Colors.black; + static const _blackSecondLayer = Color(0xFF212121); // aka `Colors.grey[900]` + static const _blackThirdLayer = Color(0xFF303030); // aka `Colors.grey[850]` + + static final blackTheme = darkTheme.copyWith( + // `canvasColor` is used by `Drawer`, `DropdownButton` and `ExpansionTileCard` + canvasColor: _blackSecondLayer, + scaffoldBackgroundColor: _blackFirstLayer, + // `cardColor` is used by `ExpansionPanel` + cardColor: _blackSecondLayer, + dialogBackgroundColor: _blackSecondLayer, + appBarTheme: darkTheme.appBarTheme.copyWith( + backgroundColor: _blackFirstLayer, + ), + popupMenuTheme: darkTheme.popupMenuTheme.copyWith( + color: _blackSecondLayer, + ), + ); + + static Color overlayBackgroundColor({ + required Brightness brightness, + required bool blurred, + }) { + switch (brightness) { + case Brightness.dark: + return blurred ? Colors.black26 : Colors.black45; + case Brightness.light: + return blurred ? Colors.white54 : const Color(0xCCFFFFFF); + } + } + + static Color thirdLayerColor(BuildContext context) { + final isBlack = context.select((v) => v.themeBrightness == AvesThemeBrightness.black); + if (isBlack) { + return _blackThirdLayer; + } else { + switch (Theme.of(context).brightness) { + case Brightness.dark: + return _darkThirdLayer; + case Brightness.light: + return _lightThirdLayer; + } + } + } } diff --git a/lib/utils/color_utils.dart b/lib/utils/color_utils.dart deleted file mode 100644 index d9842ead6..000000000 --- a/lib/utils/color_utils.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:flutter/material.dart'; - -final Map _stringColors = {}; - -Color stringToColor(String string, {double saturation = .8, double lightness = .6}) { - var color = _stringColors[string]; - if (color == null) { - final hash = string.codeUnits.fold(0, (prev, el) => prev = el + ((prev << 5) - prev)); - final hue = (hash % 360).toDouble(); - color = HSLColor.fromAHSL(1.0, hue, saturation, lightness).toColor(); - _stringColors[string] = color; - } - return color; -} diff --git a/lib/widgets/about/app_ref.dart b/lib/widgets/about/app_ref.dart index 2f401185e..b7d1f253e 100644 --- a/lib/widgets/about/app_ref.dart +++ b/lib/widgets/about/app_ref.dart @@ -53,7 +53,7 @@ class _AppReferenceState extends State { mainAxisSize: MainAxisSize.min, children: [ AvesLogo( - size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.25, + size: style.fontSize! * MediaQuery.textScaleFactorOf(context) * 1.3, ), const SizedBox(width: 8), Text( diff --git a/lib/widgets/about/bug_report.dart b/lib/widgets/about/bug_report.dart index 592486d78..4b77272c3 100644 --- a/lib/widgets/about/bug_report.dart +++ b/lib/widgets/about/bug_report.dart @@ -4,9 +4,11 @@ import 'dart:typed_data'; import 'package:aves/app_flavor.dart'; import 'package:aves/flutter_version.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; @@ -72,16 +74,19 @@ class _BugReportState extends State with FeedbackMixin { builder: (context, snapshot) { final info = snapshot.data; if (info == null) return const SizedBox(); + + final theme = Theme.of(context); return Container( decoration: BoxDecoration( - color: Colors.grey.shade800, + color: theme.cardColor, border: Border.all( - color: Colors.white, + color: theme.colorScheme.onPrimary, ), borderRadius: const BorderRadius.all(Radius.circular(8)), ), constraints: const BoxConstraints(maxHeight: 100), margin: const EdgeInsets.symmetric(vertical: 8), + clipBehavior: Clip.antiAlias, child: Theme( data: Theme.of(context).copyWith( scrollbarTheme: const ScrollbarThemeData( @@ -115,13 +120,14 @@ class _BugReportState extends State with FeedbackMixin { ), isExpanded: _showInstructions, canTapOnHeader: true, - backgroundColor: Theme.of(context).scaffoldBackgroundColor, + backgroundColor: Colors.transparent, ), ], ); } Widget _buildStep(int step, String text, String buttonText, VoidCallback onPressed) { + final isMonochrome = settings.themeColorMode == AvesThemeColorMode.monochrome; return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( @@ -130,7 +136,7 @@ class _BugReportState extends State with FeedbackMixin { padding: const EdgeInsets.all(12), decoration: BoxDecoration( border: Border.fromBorderSide(BorderSide( - color: Theme.of(context).colorScheme.secondary, + color: isMonochrome ? context.select((v) => v.neutral) : Theme.of(context).colorScheme.secondary, width: AvesFilterChip.outlineWidth, )), shape: BoxShape.circle, @@ -162,7 +168,7 @@ class _BugReportState extends State with FeedbackMixin { 'Device: ${androidInfo.manufacturer} ${androidInfo.model}', 'Google Play services: ${hasPlayServices ? 'ready' : 'not available'}', 'System locales: ${WidgetsBinding.instance!.window.locales.join(', ')}', - 'Aves locale: ${settings.locale} -> ${settings.appliedLocale}', + 'Aves locale: ${settings.locale ?? 'system'} -> ${settings.appliedLocale}', ].join('\n'); } diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index 18e593d42..103d8c90d 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -1,5 +1,6 @@ import 'package:aves/app_flavor.dart'; import 'package:aves/ref/brand_colors.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/basic/link_chip.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -40,6 +41,7 @@ class _LicensesState extends State { @override Widget build(BuildContext context) { + final colors = context.watch(); return SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 8), sliver: SliverList( @@ -49,25 +51,25 @@ class _LicensesState extends State { const SizedBox(height: 16), AvesExpansionTile( title: context.l10n.aboutLicensesAndroidLibraries, - color: BrandColors.android, + highlightColor: colors.fromBrandColor(BrandColors.android), expandedNotifier: _expandedNotifier, children: _platform.map((package) => LicenseRow(package: package)).toList(), ), AvesExpansionTile( title: context.l10n.aboutLicensesFlutterPlugins, - color: BrandColors.flutter, + highlightColor: colors.fromBrandColor(BrandColors.flutter), expandedNotifier: _expandedNotifier, children: _flutterPlugins.map((package) => LicenseRow(package: package)).toList(), ), AvesExpansionTile( title: context.l10n.aboutLicensesFlutterPackages, - color: BrandColors.flutter, + highlightColor: colors.fromBrandColor(BrandColors.flutter), expandedNotifier: _expandedNotifier, children: _flutterPackages.map((package) => LicenseRow(package: package)).toList(), ), AvesExpansionTile( title: context.l10n.aboutLicensesDartPackages, - color: BrandColors.flutter, + highlightColor: colors.fromBrandColor(BrandColors.flutter), expandedNotifier: _expandedNotifier, children: _dartPackages.map((package) => LicenseRow(package: package)).toList(), ), diff --git a/lib/widgets/aves_app.dart b/lib/widgets/aves_app.dart index a1fcd40d9..d310b4728 100644 --- a/lib/widgets/aves_app.dart +++ b/lib/widgets/aves_app.dart @@ -6,13 +6,16 @@ import 'package:aves/app_mode.dart'; import 'package:aves/l10n/l10n.dart'; import 'package:aves/model/device.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/screen_on.dart'; +import 'package:aves/model/settings/enums/theme_brightness.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/services/accessibility_service.dart'; import 'package:aves/services/common/services.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/theme/themes.dart'; @@ -101,11 +104,16 @@ class _AvesAppState extends State with WidgetsBindingObserver { : Scaffold( body: snapshot.hasError ? _buildError(snapshot.error!) : const SizedBox(), ); - return Selector>( - selector: (context, s) => Tuple2(s.locale, s.initialized ? s.accessibilityAnimations.animate : true), + return Selector>( + selector: (context, s) => Tuple3( + s.locale, + s.initialized ? s.accessibilityAnimations.animate : true, + s.initialized ? s.themeBrightness : AvesThemeBrightness.system, + ), builder: (context, s, child) { final settingsLocale = s.item1; final areAnimationsEnabled = s.item2; + final themeBrightness = s.item3; return MaterialApp( navigatorKey: _navigatorKey, home: home, @@ -126,11 +134,15 @@ class _AvesAppState extends State with WidgetsBindingObserver { child: child!, ); } - return child!; + return AvesColorsProvider( + child: child!, + ); + // return child!; }, onGenerateTitle: (context) => context.l10n.appName, - darkTheme: Themes.darkTheme, - themeMode: ThemeMode.dark, + theme: Themes.lightTheme, + darkTheme: themeBrightness == AvesThemeBrightness.black ? Themes.blackTheme : Themes.darkTheme, + themeMode: themeBrightness.appThemeMode, locale: settingsLocale, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 463053562..405fd7daa 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,11 +1,15 @@ import 'dart:async'; import 'package:aves/model/entry.dart'; +import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/trash.dart'; +import 'package:aves/model/highlight.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/collection_grid.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; @@ -21,11 +25,15 @@ import 'package:provider/provider.dart'; class CollectionPage extends StatefulWidget { static const routeName = '/collection'; - final CollectionLens collection; + final CollectionSource source; + final Set? filters; + final bool Function(AvesEntry element)? highlightTest; const CollectionPage({ Key? key, - required this.collection, + required this.source, + required this.filters, + this.highlightTest, }) : super(key: key); @override @@ -34,17 +42,23 @@ class CollectionPage extends StatefulWidget { class _CollectionPageState extends State { final List _subscriptions = []; - - CollectionLens get collection => widget.collection; + late CollectionLens _collection; @override void initState() { + // do not seed this widget with the collection, but control its lifecycle here instead, + // as the collection properties may change and they should not be reset by a widget update (e.g. with theme change) + _collection = CollectionLens( + source: widget.source, + filters: widget.filters, + ); super.initState(); _subscriptions.add(settings.updateStream.where((event) => event.key == Settings.enableBinKey).listen((_) { if (!settings.enableBin) { - collection.removeFilter(TrashFilter.instance); + _collection.removeFilter(TrashFilter.instance); } })); + WidgetsBinding.instance!.addPostFrameCallback((_) => _checkInitHighlight()); } @override @@ -52,13 +66,13 @@ class _CollectionPageState extends State { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); - collection.dispose(); + _collection.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - final liveFilter = collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; + final liveFilter = _collection.filters.firstWhereOrNull((v) => v is QueryFilter && v.live) as QueryFilter?; return MediaQueryDataProvider( child: Scaffold( body: SelectionProvider( @@ -79,7 +93,7 @@ class _CollectionPageState extends State { child: SafeArea( bottom: false, child: ChangeNotifierProvider.value( - value: collection, + value: _collection, child: const CollectionGrid( // key is expected by test driver key: Key('collection-grid'), @@ -93,9 +107,21 @@ class _CollectionPageState extends State { ), ), ), - drawer: AppDrawer(currentCollection: collection), + drawer: AppDrawer(currentCollection: _collection), resizeToAvoidBottomInset: false, ), ); } + + Future _checkInitHighlight() async { + final highlightTest = widget.highlightTest; + if (highlightTest == null) return; + + final delayDuration = context.read().staggeredAnimationPageTarget; + await Future.delayed(delayDuration + Durations.highlightScrollInitDelay); + final targetEntry = _collection.sortedEntries.firstWhereOrNull(highlightTest); + if (targetEntry != null) { + context.read().trackItem(targetEntry, highlightItem: targetEntry); + } + } } diff --git a/lib/widgets/collection/grid/headers/album.dart b/lib/widgets/collection/grid/headers/album.dart index fbce99fd0..7cb21b09b 100644 --- a/lib/widgets/collection/grid/headers/album.dart +++ b/lib/widgets/collection/grid/headers/album.dart @@ -20,25 +20,20 @@ class AlbumSectionHeader extends StatelessWidget { @override Widget build(BuildContext context) { Widget? albumIcon; - if (directory != null) { - albumIcon = IconUtils.getAlbumIcon(context: context, albumPath: directory!); + final _directory = directory; + if (_directory != null) { + albumIcon = IconUtils.getAlbumIcon(context: context, albumPath: _directory); if (albumIcon != null) { albumIcon = RepaintBoundary( - child: Material( - type: MaterialType.circle, - elevation: 3, - color: Colors.transparent, - shadowColor: Colors.black, - child: albumIcon, - ), + child: albumIcon, ); } } return SectionHeader( - sectionKey: EntryAlbumSectionKey(directory), + sectionKey: EntryAlbumSectionKey(_directory), leading: albumIcon, title: albumName ?? context.l10n.sectionUnknown, - trailing: directory != null && androidFileUtils.isOnRemovableStorage(directory!) + trailing: _directory != null && androidFileUtils.isOnRemovableStorage(_directory) ? const Icon( AIcons.removableStorage, size: 16, diff --git a/lib/widgets/collection/grid/list_details.dart b/lib/widgets/collection/grid/list_details.dart index 98d2a6636..608c9ea11 100644 --- a/lib/widgets/collection/grid/list_details.dart +++ b/lib/widgets/collection/grid/list_details.dart @@ -25,7 +25,7 @@ class EntryListDetails extends StatelessWidget { return Container( padding: EntryListDetailsTheme.contentPadding, foregroundDecoration: BoxDecoration( - border: Border(top: AvesBorder.straightSide), + border: Border(top: AvesBorder.straightSide(context)), ), margin: EntryListDetailsTheme.contentMargin, child: IconTheme.merge( diff --git a/lib/widgets/common/action_mixins/entry_storage.dart b/lib/widgets/common/action_mixins/entry_storage.dart index 702a2d9c6..3cec4cea3 100644 --- a/lib/widgets/common/action_mixins/entry_storage.dart +++ b/lib/widgets/common/action_mixins/entry_storage.dart @@ -150,43 +150,38 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { action = SnackBarAction( label: l10n.showButtonLabel, onPressed: () async { - late CollectionLens targetCollection; + final newUris = movedOps.map((v) => v.newFields['uri'] as String?).toSet(); + bool highlightTest(AvesEntry entry) => newUris.contains(entry.uri); - final highlightInfo = context.read(); final collection = context.read(); - if (collection != null) { - targetCollection = collection; - } if (collection == null || collection.filters.any((f) => f is AlbumFilter || f is TrashFilter)) { - targetCollection = CollectionLens( - source: source, - filters: collection?.filters.where((f) => f != TrashFilter.instance).toSet(), - ); + final targetFilters = collection?.filters.where((f) => f != TrashFilter.instance).toSet() ?? {}; // we could simply add the filter to the current collection // but navigating makes the change less jarring if (destinationAlbums.length == 1) { final destinationAlbum = destinationAlbums.single; - final filter = AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum)); - targetCollection.addFilter(filter); + targetFilters.removeWhere((f) => f is AlbumFilter); + targetFilters.add(AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))); } unawaited(Navigator.pushAndRemoveUntil( context, MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( - collection: targetCollection, + source: source, + filters: targetFilters, + highlightTest: highlightTest, ), ), (route) => false, )); - final delayDuration = context.read().staggeredAnimationPageTarget; - await Future.delayed(delayDuration); - } - await Future.delayed(Durations.highlightScrollInitDelay); - final newUris = movedOps.map((v) => v.newFields['uri'] as String?).toSet(); - final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => newUris.contains(entry.uri)); - if (targetEntry != null) { - highlightInfo.trackItem(targetEntry, highlightItem: targetEntry); + } else { + // track in current page, without navigation + await Future.delayed(Durations.highlightScrollInitDelay); + final targetEntry = collection.sortedEntries.firstWhereOrNull(highlightTest); + if (targetEntry != null) { + context.read().trackItem(targetEntry, highlightItem: targetEntry); + } } }, ); diff --git a/lib/widgets/common/action_mixins/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart index d1c9f9e54..1b81ebd50 100644 --- a/lib/widgets/common/action_mixins/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -171,8 +171,8 @@ class _ReportOverlayState extends State> with SingleTickerPr Container( width: diameter + 2, height: diameter + 2, - decoration: const BoxDecoration( - color: Color(0xBB000000), + decoration: BoxDecoration( + color: Theme.of(context).brightness == Brightness.dark ? const Color(0xBB000000) : const Color(0xEEFFFFFF), shape: BoxShape.circle, ), ), @@ -190,7 +190,7 @@ class _ReportOverlayState extends State> with SingleTickerPr percent: percent, lineWidth: strokeWidth, radius: diameter / 2, - backgroundColor: Colors.white24, + backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(.2), progressColor: progressColor, animation: animate, center: Text( @@ -270,6 +270,8 @@ class _FeedbackMessageState extends State<_FeedbackMessage> { Widget build(BuildContext context) { final text = Text(widget.message); final duration = widget.duration; + final theme = Theme.of(context); + final contentTextStyle = theme.snackBarTheme.contentTextStyle ?? ThemeData(brightness: theme.brightness).textTheme.subtitle1; return duration == null ? text : Row( @@ -286,7 +288,10 @@ class _FeedbackMessageState extends State<_FeedbackMessage> { progressColor: Colors.grey, animation: true, animationDuration: duration.inMilliseconds, - center: Text('$_remainingSecs'), + center: Text( + '$_remainingSecs', + style: contentTextStyle, + ), animateFromLastPercent: true, reverse: true, ), diff --git a/lib/widgets/common/app_bar_subtitle.dart b/lib/widgets/common/app_bar_subtitle.dart index 7975f088e..9871ca7e8 100644 --- a/lib/widgets/common/app_bar_subtitle.dart +++ b/lib/widgets/common/app_bar_subtitle.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/events.dart'; @@ -62,25 +64,28 @@ class SourceStateSubtitle extends StatelessWidget { final subtitle = sourceState.getName(context.l10n); if (subtitle == null) return const SizedBox(); - final subtitleStyle = Theme.of(context).textTheme.caption!; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text(subtitle, style: subtitleStyle), - ValueListenableBuilder( - valueListenable: source.progressNotifier, - builder: (context, progress, snapshot) { - if (progress.total == 0 || sourceState == SourceState.locatingCountries) return const SizedBox(); - return Padding( - padding: const EdgeInsetsDirectional.only(start: 8), - child: Text( - '${progress.done}/${progress.total}', - style: subtitleStyle.copyWith(color: Colors.white30), - ), - ); - }, - ), - ], + final theme = Theme.of(context); + return DefaultTextStyle.merge( + style: theme.textTheme.caption!.copyWith(fontFeatures: const [FontFeature.disable('smcp')]), + child: ValueListenableBuilder( + valueListenable: source.progressNotifier, + builder: (context, progress, snapshot) { + return Text.rich( + TextSpan( + children: [ + TextSpan(text: subtitle), + if (progress.total != 0 && sourceState != SourceState.locatingCountries) ...[ + const WidgetSpan(child: SizedBox(width: 8)), + TextSpan( + text: '${progress.done}/${progress.total}', + style: TextStyle(color: theme.brightness == Brightness.dark ? Colors.white30 : Colors.black26), + ), + ] + ], + ), + ); + }, + ), ); } } diff --git a/lib/widgets/common/basic/color_list_tile.dart b/lib/widgets/common/basic/color_list_tile.dart index 92216f957..b03085c7f 100644 --- a/lib/widgets/common/basic/color_list_tile.dart +++ b/lib/widgets/common/basic/color_list_tile.dart @@ -27,7 +27,7 @@ class ColorListTile extends StatelessWidget { width: radius * 2, decoration: BoxDecoration( color: value, - border: AvesBorder.border, + border: AvesBorder.border(context), shape: BoxShape.circle, ), ), diff --git a/lib/widgets/common/basic/markdown_container.dart b/lib/widgets/common/basic/markdown_container.dart index 76110e64f..43a37964f 100644 --- a/lib/widgets/common/basic/markdown_container.dart +++ b/lib/widgets/common/basic/markdown_container.dart @@ -1,3 +1,4 @@ +import 'package:aves/widgets/common/fx/borders.dart'; import 'package:flutter/material.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -18,9 +19,10 @@ class MarkdownContainer extends StatelessWidget { Widget build(BuildContext context) { return Container( margin: const EdgeInsets.symmetric(horizontal: 8), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(16)), - color: Colors.white10, + decoration: BoxDecoration( + color: Theme.of(context).canvasColor, + border: Border.all(color: Theme.of(context).dividerColor, width: AvesBorder.curvedBorderWidth), + borderRadius: const BorderRadius.all(Radius.circular(16)), ), constraints: const BoxConstraints(maxWidth: maxWidth), child: ClipRRect( diff --git a/lib/widgets/common/basic/menu.dart b/lib/widgets/common/basic/menu.dart index a4984026f..8c568a77a 100644 --- a/lib/widgets/common/basic/menu.dart +++ b/lib/widgets/common/basic/menu.dart @@ -18,8 +18,13 @@ class MenuRow extends StatelessWidget { children: [ if (icon != null) Padding( - padding: const EdgeInsetsDirectional.only(end: 8), - child: icon, + padding: const EdgeInsetsDirectional.only(end: 12), + child: IconTheme.merge( + data: IconThemeData( + color: ListTileTheme.of(context).iconColor, + ), + child: icon!, + ), ), Expanded(child: Text(text)), ], @@ -110,6 +115,7 @@ class _PopupMenuItemExpansionPanelState extends State entries; @@ -73,7 +74,10 @@ class _FavouriteTogglerState extends State { ), Sweeper( key: ValueKey(entries.length == 1 ? entries.first : entries.length), - builder: (context) => const Icon(AIcons.favourite, color: AColors.favourite), + builder: (context) => Icon( + AIcons.favourite, + color: context.select((v) => v.favourite), + ), toggledNotifier: isFavouriteNotifier, ), ], diff --git a/lib/widgets/common/fx/borders.dart b/lib/widgets/common/fx/borders.dart index 766cd8671..2e2fe275e 100644 --- a/lib/widgets/common/fx/borders.dart +++ b/lib/widgets/common/fx/borders.dart @@ -3,7 +3,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; class AvesBorder { - static const borderColor = Colors.white30; + static Color _borderColor(BuildContext context) => Theme.of(context).brightness == Brightness.dark ? Colors.white30 : Colors.black26; // directly uses `devicePixelRatio` as it never changes, to avoid visiting ancestors via `MediaQuery` @@ -13,15 +13,15 @@ class AvesBorder { // 1 device pixel for curves is too thin static double get curvedBorderWidth => window.devicePixelRatio > 2 ? 0.5 : 1.0; - static BorderSide get straightSide => BorderSide( - color: borderColor, + static BorderSide straightSide(BuildContext context) => BorderSide( + color: _borderColor(context), width: straightBorderWidth, ); - static BorderSide get curvedSide => BorderSide( - color: borderColor, + static BorderSide curvedSide(BuildContext context) => BorderSide( + color: _borderColor(context), width: curvedBorderWidth, ); - static Border get border => Border.fromBorderSide(curvedSide); + static Border border(BuildContext context) => Border.fromBorderSide(curvedSide(context)); } diff --git a/lib/widgets/common/fx/colors.dart b/lib/widgets/common/fx/colors.dart new file mode 100644 index 000000000..0b7646f68 --- /dev/null +++ b/lib/widgets/common/fx/colors.dart @@ -0,0 +1,26 @@ +import 'package:flutter/painting.dart'; + +class MatrixColorFilters { + static const ColorFilter greyscale = ColorFilter.matrix([ + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ]); +} diff --git a/lib/widgets/common/grid/overlay.dart b/lib/widgets/common/grid/overlay.dart index c9c111df4..0bae6317b 100644 --- a/lib/widgets/common/grid/overlay.dart +++ b/lib/widgets/common/grid/overlay.dart @@ -44,13 +44,13 @@ class GridItemSelectionOverlay extends StatelessWidget { child: child, ); child = AnimatedContainer( - duration: duration, alignment: AlignmentDirectional.topEnd, padding: padding, decoration: BoxDecoration( color: isSelected ? Colors.black54 : Colors.transparent, borderRadius: borderRadius, ), + duration: duration, child: child, ); return child; diff --git a/lib/widgets/common/grid/scaling.dart b/lib/widgets/common/grid/scaling.dart index 7d99d31f2..53c0e67a9 100644 --- a/lib/widgets/common/grid/scaling.dart +++ b/lib/widgets/common/grid/scaling.dart @@ -245,6 +245,10 @@ class _ScaleOverlayState extends State<_ScaleOverlay> { double get gridWidth => widget.viewportWidth; + // `Color(0x00FFFFFF)` is different from `Color(0x00000000)` (or `Colors.transparent`) + // when used in gradients or lerping to it + static const transparentWhite = Color(0x00FFFFFF); + @override void initState() { super.initState(); @@ -309,24 +313,35 @@ class _ScaleOverlayState extends State<_ScaleOverlay> { break; } + final isDark = Theme.of(context).brightness == Brightness.dark; return _init ? BoxDecoration( gradient: RadialGradient( center: FractionalOffset.fromOffsetAndSize(gradientCenter, context.select((mq) => mq.size)), radius: 1, - colors: const [ - Colors.black, - Colors.black54, - ], + colors: isDark + ? const [ + Colors.black, + Colors.black54, + ] + : const [ + Colors.white, + Colors.white38, + ], ), ) - : const BoxDecoration( + : BoxDecoration( // provide dummy gradient to lerp to the other one during animation gradient: RadialGradient( - colors: [ - Colors.transparent, - Colors.transparent, - ], + colors: isDark + ? const [ + Colors.transparent, + Colors.transparent, + ] + : const [ + transparentWhite, + transparentWhite, + ], ), ); } diff --git a/lib/widgets/common/identity/aves_expansion_tile.dart b/lib/widgets/common/identity/aves_expansion_tile.dart index f1fac2a67..34c4e019d 100644 --- a/lib/widgets/common/identity/aves_expansion_tile.dart +++ b/lib/widgets/common/identity/aves_expansion_tile.dart @@ -8,7 +8,7 @@ class AvesExpansionTile extends StatelessWidget { final String value; final Widget? leading; final String title; - final Color? color; + final Color? highlightColor; final ValueNotifier? expandedNotifier; final bool initiallyExpanded, showHighlight; final List children; @@ -18,7 +18,7 @@ class AvesExpansionTile extends StatelessWidget { String? value, this.leading, required this.title, - this.color, + this.highlightColor, this.expandedNotifier, this.initiallyExpanded = false, this.showHighlight = true, @@ -31,7 +31,7 @@ class AvesExpansionTile extends StatelessWidget { final enabled = children.isNotEmpty == true; Widget titleChild = HighlightTitle( title: title, - color: color, + color: highlightColor, enabled: enabled, showHighlight: showHighlight, ); @@ -63,8 +63,8 @@ class AvesExpansionTile extends StatelessWidget { expandable: enabled, initiallyExpanded: initiallyExpanded, finalPadding: const EdgeInsets.symmetric(vertical: 6.0), - baseColor: Colors.grey.shade900, - expandedColor: Colors.grey[850], + baseColor: theme.scaffoldBackgroundColor, + expandedColor: theme.canvasColor, duration: animationDuration, shadowColor: theme.shadowColor, child: Column( diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index e362d4ce7..7473c4ecb 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -6,6 +6,7 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/basic/menu.dart'; @@ -46,7 +47,6 @@ class AvesFilterChip extends StatefulWidget { final FilterCallback? onTap; final OffsetFilterCallback? onLongPress; - static const Color defaultOutlineColor = Colors.white; static const double defaultRadius = 32; static const double outlineWidth = 2; static const double minChipHeight = kMinInteractiveDimension; @@ -156,7 +156,7 @@ class _AvesFilterChipState extends State { // the existing widget FutureBuilder cycles again from the start, with a frame in `waiting` state and no data. // So we save the result of the Future to a local variable because of this specific case. _colorFuture = filter.color(context); - _outlineColor = AvesFilterChip.defaultOutlineColor; + _outlineColor = context.read().neutral; } @override @@ -270,7 +270,7 @@ class _AvesFilterChipState extends State { return DecoratedBox( decoration: BoxDecoration( border: Border.fromBorderSide(BorderSide( - color: widget.useFilterColor ? _outlineColor : AvesFilterChip.defaultOutlineColor, + color: widget.useFilterColor ? _outlineColor : context.select((v) => v.neutral), width: AvesFilterChip.outlineWidth, )), borderRadius: borderRadius, diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index f259495e2..d6780b12c 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -27,7 +27,6 @@ class VideoIcon extends StatelessWidget { if (showDuration) { child = DefaultTextStyle( style: TextStyle( - color: Colors.grey.shade200, fontSize: gridTheme.fontSize, ), child: child, @@ -146,7 +145,6 @@ class MultiPageIcon extends StatelessWidget { ); return DefaultTextStyle( style: TextStyle( - color: Colors.grey.shade200, fontSize: context.select((t) => t.fontSize), ), child: child, @@ -167,7 +165,6 @@ class RatingIcon extends StatelessWidget { final gridTheme = context.watch(); return DefaultTextStyle( style: TextStyle( - color: Colors.grey.shade200, fontSize: gridTheme.fontSize, ), child: OverlayIcon( @@ -195,7 +192,6 @@ class TrashIcon extends StatelessWidget { return DefaultTextStyle( style: TextStyle( - color: Colors.grey.shade200, fontSize: context.select((t) => t.fontSize), ), child: child, @@ -224,8 +220,6 @@ class OverlayIcon extends StatelessWidget { final iconChild = Icon( icon, size: size, - // consistent with the color used for the text next to it - color: DefaultTextStyle.of(context).style.color, ); final iconBox = SizedBox( width: size, @@ -243,7 +237,7 @@ class OverlayIcon extends StatelessWidget { margin: margin, padding: text != null ? EdgeInsetsDirectional.only(end: size / 4) : null, decoration: BoxDecoration( - color: const Color(0xBB000000), + color: Theme.of(context).brightness == Brightness.dark ? const Color(0xAA000000) : const Color(0xCCFFFFFF), borderRadius: BorderRadius.all(Radius.circular(size)), ), child: text == null @@ -254,7 +248,11 @@ class OverlayIcon extends StatelessWidget { children: [ iconBox, const SizedBox(width: 2), - Text(text!), + Text( + text!, + // consistent with the color used for the icon next to it + style: TextStyle(color: IconTheme.of(context).color), + ), ], ), ); diff --git a/lib/widgets/common/identity/aves_logo.dart b/lib/widgets/common/identity/aves_logo.dart index b61fefa70..23fb7a04e 100644 --- a/lib/widgets/common/identity/aves_logo.dart +++ b/lib/widgets/common/identity/aves_logo.dart @@ -1,4 +1,9 @@ +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves/widgets/common/fx/colors.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class AvesLogo extends StatelessWidget { final double size; @@ -10,14 +15,32 @@ class AvesLogo extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + + Widget child = CustomPaint( + size: Size(size / 1.4, size / 1.4), + painter: AvesLogoPainter(), + ); + if (context.select((v) => v.themeColorMode == AvesThemeColorMode.monochrome)) { + final tint = Color.lerp(theme.colorScheme.secondary, Colors.white, .5)!; + child = ColorFiltered( + colorFilter: ColorFilter.mode(tint, BlendMode.modulate), + child: ColorFiltered( + colorFilter: MatrixColorFilters.greyscale, + child: child, + ), + ); + } + return CircleAvatar( - backgroundColor: Colors.white, + backgroundColor: theme.dividerColor, radius: size / 2, - child: Padding( - padding: EdgeInsets.only(top: size / 15), - child: CustomPaint( - size: Size(size / 1.4, size / 1.4), - painter: AvesLogoPainter(), + child: CircleAvatar( + backgroundColor: Colors.white, + radius: size / 2 - AvesBorder.curvedBorderWidth, + child: Padding( + padding: EdgeInsets.only(top: size / 15), + child: child, ), ), ); diff --git a/lib/widgets/common/identity/buttons.dart b/lib/widgets/common/identity/buttons.dart index 797026f83..3897ee2d6 100644 --- a/lib/widgets/common/identity/buttons.dart +++ b/lib/widgets/common/identity/buttons.dart @@ -22,7 +22,7 @@ class AvesOutlinedButton extends StatelessWidget { ); }), foregroundColor: MaterialStateProperty.resolveWith((states) { - return states.contains(MaterialState.disabled) ? theme.disabledColor : Colors.white; + return states.contains(MaterialState.disabled) ? theme.disabledColor : theme.colorScheme.onSecondary; }), ); return icon != null diff --git a/lib/widgets/common/identity/highlight_title.dart b/lib/widgets/common/identity/highlight_title.dart index 7993734e5..a8976cb8d 100644 --- a/lib/widgets/common/identity/highlight_title.dart +++ b/lib/widgets/common/identity/highlight_title.dart @@ -1,8 +1,11 @@ import 'dart:ui'; -import 'package:aves/utils/color_utils.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/widgets/common/fx/highlight_decoration.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class HighlightTitle extends StatelessWidget { final String title; @@ -34,18 +37,19 @@ class HighlightTitle extends StatelessWidget { @override Widget build(BuildContext context) { final style = TextStyle( - shadows: shadows, + shadows: Theme.of(context).brightness == Brightness.dark ? shadows : null, fontSize: fontSize, letterSpacing: 1.0, fontFeatures: const [FontFeature.enable('smcp')], ); + final colors = context.watch(); return Align( alignment: AlignmentDirectional.centerStart, child: Container( - decoration: showHighlight + decoration: showHighlight && context.select((v) => v.themeColorMode == AvesThemeColorMode.polychrome) ? HighlightDecoration( - color: enabled ? color ?? stringToColor(title) : disabledColor, + color: enabled ? color ?? colors.fromString(title) : disabledColor, ) : null, margin: const EdgeInsets.symmetric(vertical: 4.0), diff --git a/lib/widgets/common/map/attribution.dart b/lib/widgets/common/map/attribution.dart index afeb94a2b..9488b38cb 100644 --- a/lib/widgets/common/map/attribution.dart +++ b/lib/widgets/common/map/attribution.dart @@ -22,19 +22,20 @@ class Attribution extends StatelessWidget { case EntryMapStyle.stamenWatercolor: return _buildAttributionMarkdown(context, context.l10n.mapAttributionStamen); default: - return const SizedBox.shrink(); + return const SizedBox(); } } Widget _buildAttributionMarkdown(BuildContext context, String data) { + final theme = Theme.of(context); return Padding( padding: const EdgeInsets.only(top: 4), child: MarkdownBody( data: data, selectable: true, styleSheet: MarkdownStyleSheet( - a: TextStyle(color: Theme.of(context).colorScheme.secondary), - p: const TextStyle(color: Colors.white70, fontSize: InfoRowGroup.fontSize), + a: TextStyle(color: theme.colorScheme.secondary), + p: theme.textTheme.caption!.merge(const TextStyle(fontSize: InfoRowGroup.fontSize)), ), onTapLink: (text, href, title) async { if (href != null && await canLaunch(href)) { diff --git a/lib/widgets/common/map/buttons.dart b/lib/widgets/common/map/buttons.dart index e470a0b96..68595fd76 100644 --- a/lib/widgets/common/map/buttons.dart +++ b/lib/widgets/common/map/buttons.dart @@ -5,6 +5,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/themes.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; @@ -15,7 +16,6 @@ import 'package:aves/widgets/common/map/theme.dart'; import 'package:aves/widgets/common/map/zoomed_bounds.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/viewer/info/notifications.dart'; -import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/material.dart'; import 'package:latlong2/latlong.dart'; import 'package:provider/provider.dart'; @@ -207,10 +207,10 @@ class MapOverlayButton extends StatelessWidget { enabled: blurred, child: Material( type: MaterialType.circle, - color: overlayBackgroundColor(blurred: blurred), + color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), child: Ink( decoration: BoxDecoration( - border: AvesBorder.border, + border: AvesBorder.border(context), shape: BoxShape.circle, ), child: Selector( @@ -278,9 +278,10 @@ class _OverlayCoordinateFilterChipState extends State<_OverlayCoordinateFilterCh @override Widget build(BuildContext context) { final blurred = settings.enableOverlayBlurEffect; + final theme = Theme.of(context); return Theme( - data: Theme.of(context).copyWith( - scaffoldBackgroundColor: overlayBackgroundColor(blurred: blurred), + data: theme.copyWith( + scaffoldBackgroundColor: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred), ), child: Align( alignment: Alignment.topLeft, diff --git a/lib/widgets/common/map/decorator.dart b/lib/widgets/common/map/decorator.dart index d3f6dc7b8..cafee9b0e 100644 --- a/lib/widgets/common/map/decorator.dart +++ b/lib/widgets/common/map/decorator.dart @@ -1,3 +1,4 @@ +import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/map/theme.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -28,6 +29,10 @@ class MapDecorator extends StatelessWidget { borderRadius: mapBorderRadius, child: Container( color: mapBackground, + foregroundDecoration: BoxDecoration( + border: AvesBorder.border(context), + borderRadius: mapBorderRadius, + ), child: Stack( children: [ const GridPaper( diff --git a/lib/widgets/common/map/marker.dart b/lib/widgets/common/map/marker.dart index f8238f888..ddbfb9cad 100644 --- a/lib/widgets/common/map/marker.dart +++ b/lib/widgets/common/map/marker.dart @@ -14,12 +14,14 @@ class ImageMarker extends StatelessWidget { static const double outerBorderRadiusDim = 8; static const double outerBorderWidth = 1.5; static const double innerBorderWidth = 2; - static const outerBorderColor = Colors.white30; - static const innerBorderColor = Color(0xFF212121); static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim)); static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth); static const innerBorderRadius = BorderRadius.all(innerRadius); + static Color themedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26; + + static Color themedInnerBorderColor(bool isDark) => isDark ? const Color(0xFF212121) : Colors.white; + const ImageMarker({ Key? key, required this.entry, @@ -46,7 +48,12 @@ class ImageMarker extends StatelessWidget { child: child, ); - const outerDecoration = BoxDecoration( + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final outerBorderColor = themedOuterBorderColor(isDark); + final innerBorderColor = themedInnerBorderColor(isDark); + + final outerDecoration = BoxDecoration( border: Border.fromBorderSide(BorderSide( color: outerBorderColor, width: outerBorderWidth, @@ -54,7 +61,7 @@ class ImageMarker extends StatelessWidget { borderRadius: outerBorderRadius, ); - const innerDecoration = BoxDecoration( + final innerDecoration = BoxDecoration( border: Border.fromBorderSide(BorderSide( color: innerBorderColor, width: innerBorderWidth, @@ -72,7 +79,7 @@ class ImageMarker extends StatelessWidget { ); if (count != null) { - const borderSide = BorderSide( + final borderSide = BorderSide( color: innerBorderColor, width: innerBorderWidth, ); @@ -82,28 +89,28 @@ class ImageMarker extends StatelessWidget { Container( padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 2), decoration: ShapeDecoration( - color: Theme.of(context).colorScheme.secondary, + color: theme.colorScheme.secondary, shape: context.isRtl - ? const CustomRoundedRectangleBorder( + ? CustomRoundedRectangleBorder( leftSide: borderSide, rightSide: borderSide, topSide: borderSide, bottomSide: borderSide, topRightCornerSide: borderSide, bottomLeftCornerSide: borderSide, - borderRadius: BorderRadius.only( + borderRadius: const BorderRadius.only( topRight: innerRadius, bottomLeft: innerRadius, ), ) - : const CustomRoundedRectangleBorder( + : CustomRoundedRectangleBorder( leftSide: borderSide, rightSide: borderSide, topSide: borderSide, bottomSide: borderSide, topLeftCornerSide: borderSide, bottomRightCornerSide: borderSide, - borderRadius: BorderRadius.only( + borderRadius: const BorderRadius.only( topLeft: innerRadius, bottomRight: innerRadius, ), @@ -111,7 +118,10 @@ class ImageMarker extends StatelessWidget { ), child: Text( '$count', - style: const TextStyle(fontSize: 12), + style: const TextStyle( + fontSize: 12, + color: Colors.white, + ), ), ), ], @@ -190,17 +200,22 @@ class DotMarker extends StatelessWidget { @override Widget build(BuildContext context) { - const outerDecoration = BoxDecoration( + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final outerBorderColor = ImageMarker.themedOuterBorderColor(isDark); + final innerBorderColor = ImageMarker.themedInnerBorderColor(isDark); + + final outerDecoration = BoxDecoration( border: Border.fromBorderSide(BorderSide( - color: ImageMarker.outerBorderColor, + color: outerBorderColor, width: ImageMarker.outerBorderWidth, )), borderRadius: outerBorderRadius, ); - const innerDecoration = BoxDecoration( + final innerDecoration = BoxDecoration( border: Border.fromBorderSide(BorderSide( - color: ImageMarker.innerBorderColor, + color: innerBorderColor, width: ImageMarker.innerBorderWidth, )), borderRadius: innerBorderRadius, @@ -216,7 +231,7 @@ class DotMarker extends StatelessWidget { child: Container( width: diameter, height: diameter, - color: Theme.of(context).colorScheme.secondary, + color: theme.colorScheme.secondary, ), ), ), diff --git a/lib/widgets/common/sliver_app_bar_title.dart b/lib/widgets/common/sliver_app_bar_title.dart index 0ed370ba2..5d4f786eb 100644 --- a/lib/widgets/common/sliver_app_bar_title.dart +++ b/lib/widgets/common/sliver_app_bar_title.dart @@ -14,7 +14,7 @@ class SliverAppBarTitleWrapper extends StatelessWidget { @override Widget build(BuildContext context) { final toolbarOpacity = context.dependOnInheritedWidgetOfExactType()!.toolbarOpacity; - final baseColor = (DefaultTextStyle.of(context).style.color ?? Theme.of(context).primaryTextTheme.headline6!.color!); + final baseColor = (DefaultTextStyle.of(context).style.color ?? Theme.of(context).textTheme.headline6!.color!); return DefaultTextStyle.merge( style: TextStyle(color: baseColor.withOpacity(toolbarOpacity)), child: child, diff --git a/lib/widgets/common/thumbnail/image.dart b/lib/widgets/common/thumbnail/image.dart index f3ae1a39a..558ae4153 100644 --- a/lib/widgets/common/thumbnail/image.dart +++ b/lib/widgets/common/thumbnail/image.dart @@ -179,9 +179,12 @@ class _ThumbnailImageState extends State { Color? _loadingBackgroundColor; - Color get loadingBackgroundColor { + Color loadingBackgroundColor(BuildContext context) { if (_loadingBackgroundColor == null) { - final rgb = 0x30 + entry.uri.hashCode % 0x20; + var rgb = 0x30 + entry.uri.hashCode % 0x20; + if (Theme.of(context).brightness == Brightness.light) { + rgb = 0xFF - rgb; + } _loadingBackgroundColor = Color.fromARGB(0xFF, rgb, rgb, rgb); } return _loadingBackgroundColor!; @@ -201,7 +204,7 @@ class _ThumbnailImageState extends State { final imageInfo = _lastImageInfo; Widget image = imageInfo == null ? Container( - color: widget.showLoadingBackground ? loadingBackgroundColor : Colors.transparent, + color: widget.showLoadingBackground ? loadingBackgroundColor(context) : Colors.transparent, width: extent, height: extent, ) diff --git a/lib/widgets/debug/android_apps.dart b/lib/widgets/debug/android_apps.dart index 1aa035e91..fb6180883 100644 --- a/lib/widgets/debug/android_apps.dart +++ b/lib/widgets/debug/android_apps.dart @@ -70,7 +70,7 @@ class _DebugAndroidAppSectionState extends State with Au ), TextSpan( text: ' ${package.packageName}\n', - style: InfoRowGroup.keyStyle, + style: InfoRowGroup.keyStyle(context), ), WidgetSpan( alignment: PlaceholderAlignment.middle, @@ -94,7 +94,7 @@ class _DebugAndroidAppSectionState extends State with Au ), TextSpan( text: ' ${package.potentialDirs.join(', ')}\n', - style: InfoRowGroup.baseStyle, + style: InfoRowGroup.valueStyle, ), ], ), diff --git a/lib/widgets/dialogs/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart index e14416b85..6316dfa31 100644 --- a/lib/widgets/dialogs/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -2,6 +2,7 @@ import 'package:aves/model/covers.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; import 'package:aves/widgets/dialogs/item_pick_dialog.dart'; @@ -105,14 +106,20 @@ class _AddShortcutDialogState extends State { Widget _buildCover(AvesEntry entry, double extent) { return GestureDetector( onTap: _pickEntry, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(32)), - child: SizedBox( - width: extent, - height: extent, - child: ThumbnailImage( - entry: entry, - extent: extent, + child: Container( + decoration: BoxDecoration( + border: AvesBorder.border(context), + borderRadius: const BorderRadius.all(Radius.circular(32)), + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(32)), + child: SizedBox( + width: extent, + height: extent, + child: ThumbnailImage( + entry: entry, + extent: extent, + ), ), ), ), diff --git a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart index 28112801c..6c3418078 100644 --- a/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/edit_date_dialog.dart @@ -5,6 +5,7 @@ import 'package:aves/model/metadata/fields.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/basic/wheel.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; @@ -33,11 +34,6 @@ class _EditEntryDateDialogState extends State { bool _showOptions = false; final Set _fields = {...DateModifier.writableDateFields}; - // use a different shade to avoid having the same background - // on the dialog (using the theme `dialogBackgroundColor`) - // and on the dropdown (using the theme `canvasColor`) - static final dropdownColor = Colors.grey.shade800; - @override void initState() { super.initState(); @@ -81,7 +77,7 @@ class _EditEntryDateDialogState extends State { value: _action, onChanged: (v) => setState(() => _action = v!), isExpanded: true, - dropdownColor: dropdownColor, + dropdownColor: Themes.thirdLayerColor(context), ), ), AnimatedSwitcher( @@ -169,7 +165,7 @@ class _EditEntryDateDialogState extends State { value: _copyFieldSource, onChanged: (v) => setState(() => _copyFieldSource = v!), isExpanded: true, - dropdownColor: dropdownColor, + dropdownColor: Themes.thirdLayerColor(context), ), ); } diff --git a/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart b/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart index c7aeb4e52..ec9c74360 100644 --- a/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart +++ b/lib/widgets/dialogs/entry_editors/remove_metadata_dialog.dart @@ -1,7 +1,9 @@ import 'package:aves/model/metadata/enums.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/ref/brand_colors.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/highlight_decoration.dart'; import 'package:aves/widgets/common/identity/highlight_title.dart'; @@ -97,6 +99,22 @@ class _RemoveEntryMetadataDialogState extends State { Widget _toTile(MetadataType type) { final text = type.getText(); + Widget child = Text( + text, + style: TextStyle( + shadows: Theme.of(context).brightness == Brightness.dark ? HighlightTitle.shadows : null, + ), + ); + if (context.select((v) => v.themeColorMode == AvesThemeColorMode.polychrome)) { + final colors = context.watch(); + child = DecoratedBox( + decoration: HighlightDecoration( + color: colors.fromBrandColor(BrandColors.get(text)) ?? colors.fromString(text), + ), + child: child, + ); + } + return SwitchListTile( value: _types.contains(type), onChanged: (selected) { @@ -106,17 +124,7 @@ class _RemoveEntryMetadataDialogState extends State { }, title: Align( alignment: Alignment.centerLeft, - child: DecoratedBox( - decoration: HighlightDecoration( - color: BrandColors.get(text) ?? stringToColor(text), - ), - child: Text( - text, - style: const TextStyle( - shadows: HighlightTitle.shadows, - ), - ), - ), + child: child, ), ); } diff --git a/lib/widgets/dialogs/tile_view_dialog.dart b/lib/widgets/dialogs/tile_view_dialog.dart index 61c1a03c1..eb957e3d2 100644 --- a/lib/widgets/dialogs/tile_view_dialog.dart +++ b/lib/widgets/dialogs/tile_view_dialog.dart @@ -78,7 +78,12 @@ class _TileViewDialogState extends State> with final tabs = >[ if (sortOptions.isNotEmpty) Tuple2( - _buildTab(context, const Key('tab-sort'), AIcons.sort, l10n.viewDialogTabSort), + _buildTab( + context, + const Key('tab-sort'), + AIcons.sort, + l10n.viewDialogTabSort, + ), Column( children: sortOptions.entries .map((kv) => _buildRadioListTile( @@ -92,7 +97,13 @@ class _TileViewDialogState extends State> with ), if (groupOptions.isNotEmpty) Tuple2( - _buildTab(context, const Key('tab-group'), AIcons.group, l10n.viewDialogTabGroup, color: canGroup ? null : Theme.of(context).disabledColor), + _buildTab( + context, + const Key('tab-group'), + AIcons.group, + l10n.viewDialogTabGroup, + color: canGroup ? null : Theme.of(context).disabledColor, + ), Column( children: groupOptions.entries .map((kv) => _buildRadioListTile( @@ -106,7 +117,12 @@ class _TileViewDialogState extends State> with ), if (layoutOptions.isNotEmpty) Tuple2( - _buildTab(context, const Key('tab-layout'), AIcons.layout, l10n.viewDialogTabLayout), + _buildTab( + context, + const Key('tab-layout'), + AIcons.layout, + l10n.viewDialogTabLayout, + ), Column( children: layoutOptions.entries .map((kv) => _buildRadioListTile( @@ -209,7 +225,13 @@ class _TileViewDialogState extends State> with ); } - Tab _buildTab(BuildContext context, Key key, IconData icon, String text, {Color? color}) { + Tab _buildTab( + BuildContext context, + Key key, + IconData icon, + String text, { + Color? color, + }) { // cannot use `IconTheme` over `TabBar` to change size, // because `TabBar` does so internally final textScaleFactor = MediaQuery.textScaleFactorOf(context); diff --git a/lib/widgets/dialogs/video_stream_selection_dialog.dart b/lib/widgets/dialogs/video_stream_selection_dialog.dart index e9fa19a0f..106f00ae5 100644 --- a/lib/widgets/dialogs/video_stream_selection_dialog.dart +++ b/lib/widgets/dialogs/video_stream_selection_dialog.dart @@ -1,6 +1,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/ref/languages.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:collection/collection.dart'; @@ -167,10 +168,7 @@ class _VideoStreamSelectionDialogState extends State value: current, onChanged: streams.length > 1 ? (newValue) => setState(() => setter(newValue)) : null, isExpanded: true, - // use a different shade to avoid having the same background - // on the dialog (using the theme `dialogBackgroundColor`) - // and on the dropdown (using the theme `canvasColor`) - dropdownColor: Colors.grey.shade800, + dropdownColor: Themes.thirdLayerColor(context), ), ), ]; diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index 2d08bb60a..0be0f2e81 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -121,6 +121,7 @@ class AppDrawer extends StatelessWidget { Text( context.l10n.appName, style: const TextStyle( + color: Colors.white, fontSize: 44, fontWeight: FontWeight.w300, letterSpacing: 1.0, @@ -136,6 +137,7 @@ class AppDrawer extends StatelessWidget { style: ButtonStyle( foregroundColor: MaterialStateProperty.all(Colors.white), overlayColor: MaterialStateProperty.all(Colors.white24), + side: MaterialStateProperty.all(BorderSide(width: 1, color: Colors.white.withOpacity(0.12))), ), ), child: Wrap( diff --git a/lib/widgets/drawer/collection_nav_tile.dart b/lib/widgets/drawer/collection_nav_tile.dart index 8d264f276..bfe6f41f5 100644 --- a/lib/widgets/drawer/collection_nav_tile.dart +++ b/lib/widgets/drawer/collection_nav_tile.dart @@ -65,10 +65,8 @@ class CollectionNavTile extends StatelessWidget { MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( - collection: CollectionLens( - source: context.read(), - filters: {filter}, - ), + source: context.read(), + filters: {filter}, ), ), (route) => false, diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart index fd4c97d8d..fa59ff537 100644 --- a/lib/widgets/drawer/tile.dart +++ b/lib/widgets/drawer/tile.dart @@ -77,7 +77,8 @@ class DrawerPageIcon extends StatelessWidget { return const Icon(AIcons.tag); case AppDebugPage.routeName: return ShaderMask( - shaderCallback: AColors.debugGradient.createShader, + shaderCallback: AvesColorsData.debugGradient.createShader, + blendMode: BlendMode.srcIn, child: const Icon(AIcons.debug), ); default: diff --git a/lib/widgets/filter_grids/common/covered_filter_chip.dart b/lib/widgets/filter_grids/common/covered_filter_chip.dart index 8e2769952..30b6ad5e3 100644 --- a/lib/widgets/filter_grids/common/covered_filter_chip.dart +++ b/lib/widgets/filter_grids/common/covered_filter_chip.dart @@ -14,7 +14,6 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/thumbnail/image.dart'; -import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -151,7 +150,7 @@ class CoveredFilterChip extends StatelessWidget { radius: radius(extent), ), banner: banner, - details: showText ? _buildDetails(source, filter) : null, + details: showText ? _buildDetails(context, source, filter) : null, padding: titlePadding, heroType: heroType, onTap: onTap, @@ -159,7 +158,9 @@ class CoveredFilterChip extends StatelessWidget { ); } - Widget _buildDetails(CollectionSource source, T filter) { + Color _detailColor(BuildContext context) => Theme.of(context).textTheme.caption!.color!; + + Widget _buildDetails(BuildContext context, CollectionSource source, T filter) { final padding = min(8.0, extent / 16); final iconSize = detailIconSize(extent); final fontSize = detailFontSize(extent); @@ -172,7 +173,7 @@ class CoveredFilterChip extends StatelessWidget { duration: Durations.chipDecorationAnimation, child: Icon( AIcons.pin, - color: FilterGridPage.detailColor, + color: _detailColor(context), size: iconSize, ), ), @@ -182,14 +183,14 @@ class CoveredFilterChip extends StatelessWidget { duration: Durations.chipDecorationAnimation, child: Icon( AIcons.removableStorage, - color: FilterGridPage.detailColor, + color: _detailColor(context), size: iconSize, ), ), Text( '${source.count(filter)}', style: TextStyle( - color: FilterGridPage.detailColor, + color: _detailColor(context), fontSize: fontSize, ), ), diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 50679bac9..fcf4526b7 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -67,8 +67,6 @@ class FilterGridPage extends StatelessWidget { required this.heroType, }) : super(key: key); - static const Color detailColor = Color(0xFFE0E0E0); - @override Widget build(BuildContext context) { return MediaQueryDataProvider( diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index 5a289f21b..9746f1c1a 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -52,7 +52,7 @@ class FilterNavigationPage extends StatelessWidget { emptyBuilder: () => ValueListenableBuilder( valueListenable: source.stateNotifier, builder: (context, sourceState, child) { - return sourceState != SourceState.loading ? emptyBuilder() : const SizedBox.shrink(); + return sourceState != SourceState.loading ? emptyBuilder() : const SizedBox(); }, ), // do not always enable hero, otherwise unwanted hero gets triggered diff --git a/lib/widgets/filter_grids/common/filter_tile.dart b/lib/widgets/filter_grids/common/filter_tile.dart index b7850d69a..b7d246092 100644 --- a/lib/widgets/filter_grids/common/filter_tile.dart +++ b/lib/widgets/filter_grids/common/filter_tile.dart @@ -2,7 +2,6 @@ import 'package:aves/app_mode.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/selection.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/widgets/collection/collection_page.dart'; @@ -95,10 +94,8 @@ class _InteractiveFilterTileState extends State CollectionPage( - collection: CollectionLens( - source: context.read(), - filters: {filter}, - ), + source: context.read(), + filters: {filter}, ), ), ); diff --git a/lib/widgets/filter_grids/common/list_details.dart b/lib/widgets/filter_grids/common/list_details.dart index f638d7847..f7605a8ec 100644 --- a/lib/widgets/filter_grids/common/list_details.dart +++ b/lib/widgets/filter_grids/common/list_details.dart @@ -37,7 +37,7 @@ class FilterListDetails extends StatelessWidget { return Container( padding: FilterListDetailsTheme.contentPadding, foregroundDecoration: BoxDecoration( - border: Border(top: AvesBorder.straightSide), + border: Border(top: AvesBorder.straightSide(context)), ), margin: FilterListDetailsTheme.contentMargin, child: Column( diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index f1ccbce77..34fd98f52 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -252,10 +252,8 @@ class _HomePageState extends State { return DirectMaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (_) => CollectionPage( - collection: CollectionLens( - source: source, - filters: filters, - ), + source: source, + filters: filters, ), ); } diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 4dfeb4a2f..fb9092f53 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -382,10 +382,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) { return CollectionPage( - collection: CollectionLens( - source: openingCollection.source, - filters: openingCollection.filters, - )..addFilter(filter), + source: openingCollection.source, + filters: {...openingCollection.filters, filter}, ); }, ), diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index e426f5799..b002b4f42 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -266,10 +266,8 @@ class CollectionSearchDelegate { MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( - collection: CollectionLens( - source: source, - filters: {filter}, - ), + source: source, + filters: {filter}, ), ), (route) => false, diff --git a/lib/widgets/search/search_page.dart b/lib/widgets/search/search_page.dart index fc730e171..801e132a4 100644 --- a/lib/widgets/search/search_page.dart +++ b/lib/widgets/search/search_page.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -109,16 +111,19 @@ class _SearchPageState extends State { return Scaffold( appBar: AppBar( leading: widget.delegate.buildLeading(context), - title: TextField( - controller: widget.delegate.queryTextController, - focusNode: _focusNode, - style: theme.textTheme.headline6, - textInputAction: TextInputAction.search, - onSubmitted: (_) => widget.delegate.showResults(context), - decoration: InputDecoration( - border: InputBorder.none, - hintText: context.l10n.searchCollectionFieldHint, - hintStyle: theme.inputDecorationTheme.hintStyle, + title: DefaultTextStyle.merge( + style: const TextStyle(fontFeatures: [FontFeature.disable('smcp')]), + child: TextField( + controller: widget.delegate.queryTextController, + focusNode: _focusNode, + style: theme.textTheme.headline6, + textInputAction: TextInputAction.search, + onSubmitted: (_) => widget.delegate.showResults(context), + decoration: InputDecoration( + border: InputBorder.none, + hintText: context.l10n.searchCollectionFieldHint, + hintStyle: theme.inputDecorationTheme.hintStyle, + ), ), ), actions: widget.delegate.buildActions(context), diff --git a/lib/widgets/settings/accessibility/accessibility.dart b/lib/widgets/settings/accessibility/accessibility.dart index e4fd0aadb..5d44b6853 100644 --- a/lib/widgets/settings/accessibility/accessibility.dart +++ b/lib/widgets/settings/accessibility/accessibility.dart @@ -1,11 +1,15 @@ +import 'package:aves/model/settings/enums/accessibility_animations.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; -import 'package:aves/widgets/settings/accessibility/remove_animations.dart'; import 'package:aves/widgets/settings/accessibility/time_to_take_action.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class AccessibilitySection extends StatelessWidget { final ValueNotifier expandedNotifier; @@ -20,14 +24,21 @@ class AccessibilitySection extends StatelessWidget { return AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.accessibility, - color: AColors.accessibility, + color: context.select((v) => v.accessibility), ), title: context.l10n.settingsSectionAccessibility, expandedNotifier: expandedNotifier, showHighlight: false, - children: const [ - RemoveAnimationsTile(), - TimeToTakeActionTile(), + children: [ + SettingsSelectionListTile( + values: AccessibilityAnimations.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.accessibilityAnimations, + onSelection: (v) => settings.accessibilityAnimations = v, + tileTitle: context.l10n.settingsRemoveAnimationsTile, + dialogTitle: context.l10n.settingsRemoveAnimationsTitle, + ), + const TimeToTakeActionTile(), ], ); } diff --git a/lib/widgets/settings/accessibility/remove_animations.dart b/lib/widgets/settings/accessibility/remove_animations.dart deleted file mode 100644 index 62ea40095..000000000 --- a/lib/widgets/settings/accessibility/remove_animations.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:aves/model/settings/enums/accessibility_animations.dart'; -import 'package:aves/model/settings/enums/enums.dart'; -import 'package:aves/model/settings/settings.dart'; -import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class RemoveAnimationsTile extends StatelessWidget { - const RemoveAnimationsTile({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - final currentAnimations = context.select((s) => s.accessibilityAnimations); - - return ListTile( - title: Text(context.l10n.settingsRemoveAnimationsTile), - subtitle: Text(currentAnimations.getName(context)), - onTap: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentAnimations, - options: Map.fromEntries(AccessibilityAnimations.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsRemoveAnimationsTitle, - ), - onSelection: (v) => settings.accessibilityAnimations = v, - ), - ); - } -} diff --git a/lib/widgets/settings/accessibility/time_to_take_action.dart b/lib/widgets/settings/accessibility/time_to_take_action.dart index d1777b134..79db20731 100644 --- a/lib/widgets/settings/accessibility/time_to_take_action.dart +++ b/lib/widgets/settings/accessibility/time_to_take_action.dart @@ -3,9 +3,8 @@ import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/accessibility_service.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class TimeToTakeActionTile extends StatefulWidget { const TimeToTakeActionTile({Key? key}) : super(key: key); @@ -25,26 +24,20 @@ class _TimeToTakeActionTileState extends State { @override Widget build(BuildContext context) { - final currentTimeToTakeAction = context.select((s) => s.timeToTakeAction); - return FutureBuilder( future: _hasSystemOptionLoader, builder: (context, snapshot) { - if (snapshot.hasError || !snapshot.hasData) return const SizedBox.shrink(); + if (snapshot.hasError || !snapshot.hasData) return const SizedBox(); final hasSystemOption = snapshot.data!; final optionValues = hasSystemOption ? AccessibilityTimeout.values : AccessibilityTimeout.values.where((v) => v != AccessibilityTimeout.system).toList(); - return ListTile( - title: Text(context.l10n.settingsTimeToTakeActionTile), - subtitle: Text(currentTimeToTakeAction.getName(context)), - onTap: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentTimeToTakeAction, - options: Map.fromEntries(optionValues.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsTimeToTakeActionTitle, - ), - onSelection: (v) => settings.timeToTakeAction = v, - ), + + return SettingsSelectionListTile( + values: optionValues, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.timeToTakeAction, + onSelection: (v) => settings.timeToTakeAction = v, + tileTitle: context.l10n.settingsTimeToTakeActionTile, + dialogTitle: context.l10n.settingsTimeToTakeActionTitle, ); }, ); diff --git a/lib/widgets/settings/common/tile_leading.dart b/lib/widgets/settings/common/tile_leading.dart index 4f503f818..45f1140cd 100644 --- a/lib/widgets/settings/common/tile_leading.dart +++ b/lib/widgets/settings/common/tile_leading.dart @@ -1,3 +1,4 @@ +import 'package:aves/theme/durations.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:decorated_icon/decorated_icon.dart'; @@ -15,7 +16,7 @@ class SettingsTileLeading extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return AnimatedContainer( padding: const EdgeInsets.all(6), decoration: BoxDecoration( border: Border.fromBorderSide(BorderSide( @@ -24,9 +25,10 @@ class SettingsTileLeading extends StatelessWidget { )), shape: BoxShape.circle, ), + duration: Durations.themeColorModeAnimation, child: DecoratedIcon( icon, - shadows: Constants.embossShadows, + shadows: Theme.of(context).brightness == Brightness.dark ? Constants.embossShadows : null, size: 18, ), ); diff --git a/lib/widgets/settings/common/tiles.dart b/lib/widgets/settings/common/tiles.dart new file mode 100644 index 000000000..eee450281 --- /dev/null +++ b/lib/widgets/settings/common/tiles.dart @@ -0,0 +1,91 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class SettingsSwitchListTile extends StatelessWidget { + final bool Function(BuildContext, Settings) selector; + final ValueChanged onChanged; + final String title; + final String? subtitle; + final Widget? trailing; + + const SettingsSwitchListTile({ + Key? key, + required this.selector, + required this.onChanged, + required this.title, + this.subtitle, + this.trailing, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Selector( + selector: selector, + builder: (context, current, child) { + Widget titleWidget = Text(title); + if (trailing != null) { + titleWidget = Row( + children: [ + Expanded(child: titleWidget), + AnimatedOpacity( + opacity: current ? 1 : .2, + duration: Durations.toggleableTransitionAnimation, + child: trailing, + ), + ], + ); + } + return SwitchListTile( + value: current, + onChanged: onChanged, + title: titleWidget, + subtitle: subtitle != null ? Text(subtitle!) : null, + ); + }, + ); + } +} + +class SettingsSelectionListTile extends StatelessWidget { + final List values; + final String Function(BuildContext, T) getName; + final T Function(BuildContext, Settings) selector; + final ValueChanged onSelection; + final String tileTitle, dialogTitle; + final TextBuilder? optionSubtitleBuilder; + + const SettingsSelectionListTile({ + Key? key, + required this.values, + required this.getName, + required this.selector, + required this.onSelection, + required this.tileTitle, + required this.dialogTitle, + this.optionSubtitleBuilder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Selector( + selector: selector, + builder: (context, current, child) => ListTile( + title: Text(tileTitle), + subtitle: Text(getName(context, current)), + onTap: () => showSelectionDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: current, + options: Map.fromEntries(values.map((v) => MapEntry(v, getName(context, v)))), + optionSubtitleBuilder: optionSubtitleBuilder, + title: dialogTitle, + ), + onSelection: onSelection, + ), + ), + ); + } +} diff --git a/lib/widgets/settings/display/display.dart b/lib/widgets/settings/display/display.dart new file mode 100644 index 000000000..87203a87c --- /dev/null +++ b/lib/widgets/settings/display/display.dart @@ -0,0 +1,48 @@ +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/enums/theme_brightness.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/colors.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class DisplaySection extends StatelessWidget { + final ValueNotifier expandedNotifier; + + const DisplaySection({ + Key? key, + required this.expandedNotifier, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return AvesExpansionTile( + leading: SettingsTileLeading( + icon: AIcons.display, + color: context.select((v) => v.display), + ), + title: context.l10n.settingsSectionDisplay, + expandedNotifier: expandedNotifier, + showHighlight: false, + children: [ + SettingsSelectionListTile( + values: AvesThemeBrightness.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.themeBrightness, + onSelection: (v) => settings.themeBrightness = v, + tileTitle: context.l10n.settingsThemeBrightness, + dialogTitle: context.l10n.settingsThemeBrightness, + ), + SettingsSwitchListTile( + selector: (context, s) => s.themeColorMode == AvesThemeColorMode.polychrome, + onChanged: (v) => settings.themeColorMode = v ? AvesThemeColorMode.polychrome : AvesThemeColorMode.monochrome, + title: context.l10n.settingsThemeColorful, + ), + ], + ); + } +} diff --git a/lib/widgets/settings/language/language.dart b/lib/widgets/settings/language/language.dart index adcd05e2d..549766333 100644 --- a/lib/widgets/settings/language/language.dart +++ b/lib/widgets/settings/language/language.dart @@ -7,8 +7,8 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/language/locale.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -24,9 +24,6 @@ class LanguageSection extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = context.l10n; - final currentCoordinateFormat = context.select((s) => s.coordinateFormat); - final currentUnitSystem = context.select((s) => s.unitSystem); - return AvesExpansionTile( // key is expected by test driver key: const Key('section-language'), @@ -35,39 +32,29 @@ class LanguageSection extends StatelessWidget { value: 'language', leading: SettingsTileLeading( icon: AIcons.language, - color: AColors.language, + color: context.select((v) => v.language), ), title: l10n.settingsSectionLanguage, expandedNotifier: expandedNotifier, showHighlight: false, children: [ const LocaleTile(), - ListTile( - title: Text(l10n.settingsCoordinateFormatTile), - subtitle: Text(currentCoordinateFormat.getName(context)), - onTap: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentCoordinateFormat, - options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.getName(context)))), - optionSubtitleBuilder: (value) => value.format(l10n, Constants.pointNemo), - title: l10n.settingsCoordinateFormatTitle, - ), - onSelection: (v) => settings.coordinateFormat = v, - ), + SettingsSelectionListTile( + values: CoordinateFormat.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.coordinateFormat, + onSelection: (v) => settings.coordinateFormat = v, + tileTitle: l10n.settingsCoordinateFormatTile, + dialogTitle: l10n.settingsCoordinateFormatTitle, + optionSubtitleBuilder: (value) => value.format(l10n, Constants.pointNemo), ), - ListTile( - title: Text(l10n.settingsUnitSystemTile), - subtitle: Text(currentUnitSystem.getName(context)), - onTap: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentUnitSystem, - options: Map.fromEntries(UnitSystem.values.map((v) => MapEntry(v, v.getName(context)))), - title: l10n.settingsUnitSystemTitle, - ), - onSelection: (v) => settings.unitSystem = v, - ), + SettingsSelectionListTile( + values: UnitSystem.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.unitSystem, + onSelection: (v) => settings.unitSystem = v, + tileTitle: l10n.settingsUnitSystemTile, + dialogTitle: l10n.settingsUnitSystemTitle, ), ], ); diff --git a/lib/widgets/settings/navigation/navigation.dart b/lib/widgets/settings/navigation/navigation.dart index 0737957ca..9602fb286 100644 --- a/lib/widgets/settings/navigation/navigation.dart +++ b/lib/widgets/settings/navigation/navigation.dart @@ -6,8 +6,8 @@ import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/navigation/confirmation_dialogs.dart'; import 'package:aves/widgets/settings/navigation/drawer.dart'; import 'package:flutter/material.dart'; @@ -23,51 +23,37 @@ class NavigationSection extends StatelessWidget { @override Widget build(BuildContext context) { - final currentHomePage = context.select((s) => s.homePage); - final currentKeepScreenOn = context.select((s) => s.keepScreenOn); - final currentMustBackTwiceToExit = context.select((s) => s.mustBackTwiceToExit); - return AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.home, - color: AColors.navigation, + color: context.select((v) => v.navigation), ), title: context.l10n.settingsSectionNavigation, expandedNotifier: expandedNotifier, showHighlight: false, children: [ - ListTile( - title: Text(context.l10n.settingsHome), - subtitle: Text(currentHomePage.getName(context)), - onTap: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentHomePage, - options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsHome, - ), - onSelection: (v) => settings.homePage = v, - ), + SettingsSelectionListTile( + values: HomePageSetting.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.homePage, + onSelection: (v) => settings.homePage = v, + tileTitle: context.l10n.settingsHome, + dialogTitle: context.l10n.settingsHome, ), const NavigationDrawerTile(), const ConfirmationDialogTile(), - ListTile( - title: Text(context.l10n.settingsKeepScreenOnTile), - subtitle: Text(currentKeepScreenOn.getName(context)), - onTap: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: currentKeepScreenOn, - options: Map.fromEntries(KeepScreenOn.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsKeepScreenOnTitle, - ), - onSelection: (v) => settings.keepScreenOn = v, - ), + SettingsSelectionListTile( + values: KeepScreenOn.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => s.keepScreenOn, + onSelection: (v) => settings.keepScreenOn = v, + tileTitle: context.l10n.settingsKeepScreenOnTile, + dialogTitle: context.l10n.settingsKeepScreenOnTitle, ), - SwitchListTile( - value: currentMustBackTwiceToExit, + SettingsSwitchListTile( + selector: (context, s) => s.mustBackTwiceToExit, onChanged: (v) => settings.mustBackTwiceToExit = v, - title: Text(context.l10n.settingsDoubleBackExit), + title: context.l10n.settingsDoubleBackExit, ), ], ); diff --git a/lib/widgets/settings/privacy/privacy.dart b/lib/widgets/settings/privacy/privacy.dart index 327326d67..b37bc1abb 100644 --- a/lib/widgets/settings/privacy/privacy.dart +++ b/lib/widgets/settings/privacy/privacy.dart @@ -6,6 +6,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/privacy/access_grants.dart'; import 'package:aves/widgets/settings/privacy/hidden_items.dart'; import 'package:flutter/material.dart'; @@ -26,56 +27,44 @@ class PrivacySection extends StatelessWidget { return AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.privacy, - color: AColors.privacy, + color: context.select((v) => v.privacy), ), title: context.l10n.settingsSectionPrivacy, expandedNotifier: expandedNotifier, showHighlight: false, children: [ - Selector( + SettingsSwitchListTile( selector: (context, s) => s.isInstalledAppAccessAllowed, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.isInstalledAppAccessAllowed = v, - title: Text(context.l10n.settingsAllowInstalledAppAccess), - subtitle: Text(context.l10n.settingsAllowInstalledAppAccessSubtitle), - ), + onChanged: (v) => settings.isInstalledAppAccessAllowed = v, + title: context.l10n.settingsAllowInstalledAppAccess, + subtitle: context.l10n.settingsAllowInstalledAppAccessSubtitle, ), if (canEnableErrorReporting) - Selector( + SettingsSwitchListTile( selector: (context, s) => s.isErrorReportingAllowed, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.isErrorReportingAllowed = v, - title: Text(context.l10n.settingsAllowErrorReporting), - ), + onChanged: (v) => settings.isErrorReportingAllowed = v, + title: context.l10n.settingsAllowErrorReporting, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.saveSearchHistory, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) { - settings.saveSearchHistory = v; - if (!v) { - settings.searchHistory = []; - } - }, - title: Text(context.l10n.settingsSaveSearchHistory), - ), + onChanged: (v) { + settings.saveSearchHistory = v; + if (!v) { + settings.searchHistory = []; + } + }, + title: context.l10n.settingsSaveSearchHistory, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.enableBin, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) { - settings.enableBin = v; - if (!v) { - settings.searchHistory = []; - } - }, - title: Text(context.l10n.settingsEnableBin), - subtitle: Text(context.l10n.settingsEnableBinSubtitle), - ), + onChanged: (v) { + settings.enableBin = v; + if (!v) { + settings.searchHistory = []; + } + }, + title: context.l10n.settingsEnableBin, + subtitle: context.l10n.settingsEnableBinSubtitle, ), const HiddenItemsTile(), if (device.canGrantDirectoryAccess) const StorageAccessTile(), diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 0e217194d..989888248 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -14,6 +14,7 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/settings/accessibility/accessibility.dart'; import 'package:aves/widgets/settings/app_export/items.dart'; import 'package:aves/widgets/settings/app_export/selection_dialog.dart'; +import 'package:aves/widgets/settings/display/display.dart'; import 'package:aves/widgets/settings/language/language.dart'; import 'package:aves/widgets/settings/navigation/navigation.dart'; import 'package:aves/widgets/settings/privacy/privacy.dart'; @@ -98,6 +99,7 @@ class _SettingsPageState extends State with FeedbackMixin { VideoSection(expandedNotifier: _expandedNotifier), PrivacySection(expandedNotifier: _expandedNotifier), AccessibilitySection(expandedNotifier: _expandedNotifier), + DisplaySection(expandedNotifier: _expandedNotifier), LanguageSection(expandedNotifier: _expandedNotifier), ], ), diff --git a/lib/widgets/settings/thumbnails/thumbnails.dart b/lib/widgets/settings/thumbnails/thumbnails.dart index 4e5f00d4c..d12a14d4f 100644 --- a/lib/widgets/settings/thumbnails/thumbnails.dart +++ b/lib/widgets/settings/thumbnails/thumbnails.dart @@ -1,11 +1,11 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/colors.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/thumbnails/collection_actions_editor.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -21,131 +21,77 @@ class ThumbnailsSection extends StatelessWidget { @override Widget build(BuildContext context) { final iconSize = IconTheme.of(context).size! * MediaQuery.textScaleFactorOf(context); - double opacityFor(bool enabled) => enabled ? 1 : .2; - + final iconColor = context.select((v) => v.neutral); return AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.grid, - color: AColors.thumbnails, + color: context.select((v) => v.thumbnails), ), title: context.l10n.settingsSectionThumbnails, expandedNotifier: expandedNotifier, showHighlight: false, children: [ const CollectionActionsTile(), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.showThumbnailFavourite, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.showThumbnailFavourite = v, - title: Row( - children: [ - Expanded(child: Text(context.l10n.settingsThumbnailShowFavouriteIcon)), - AnimatedOpacity( - opacity: opacityFor(current), - duration: Durations.toggleableTransitionAnimation, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - FavouriteIcon.scale) / 2), - child: Icon( - AIcons.favourite, - size: iconSize * FavouriteIcon.scale, - ), - ), - ), - ], + onChanged: (v) => settings.showThumbnailFavourite = v, + title: context.l10n.settingsThumbnailShowFavouriteIcon, + trailing: Padding( + padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - FavouriteIcon.scale) / 2), + child: Icon( + AIcons.favourite, + size: iconSize * FavouriteIcon.scale, + color: iconColor, ), ), ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.showThumbnailLocation, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.showThumbnailLocation = v, - title: Row( - children: [ - Expanded(child: Text(context.l10n.settingsThumbnailShowLocationIcon)), - AnimatedOpacity( - opacity: opacityFor(current), - duration: Durations.toggleableTransitionAnimation, - child: Icon( - AIcons.location, - size: iconSize, - ), - ), - ], - ), + onChanged: (v) => settings.showThumbnailLocation = v, + title: context.l10n.settingsThumbnailShowLocationIcon, + trailing: Icon( + AIcons.location, + size: iconSize, + color: iconColor, ), ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.showThumbnailMotionPhoto, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.showThumbnailMotionPhoto = v, - title: Row( - children: [ - Expanded(child: Text(context.l10n.settingsThumbnailShowMotionPhotoIcon)), - AnimatedOpacity( - opacity: opacityFor(current), - duration: Durations.toggleableTransitionAnimation, - child: Padding( - padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - MotionPhotoIcon.scale) / 2), - child: Icon( - AIcons.motionPhoto, - size: iconSize * MotionPhotoIcon.scale, - ), - ), - ), - ], + onChanged: (v) => settings.showThumbnailMotionPhoto = v, + title: context.l10n.settingsThumbnailShowMotionPhotoIcon, + trailing: Padding( + padding: EdgeInsets.symmetric(horizontal: iconSize * (1 - MotionPhotoIcon.scale) / 2), + child: Icon( + AIcons.motionPhoto, + size: iconSize * MotionPhotoIcon.scale, + color: iconColor, ), ), ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.showThumbnailRating, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.showThumbnailRating = v, - title: Row( - children: [ - Expanded(child: Text(context.l10n.settingsThumbnailShowRating)), - AnimatedOpacity( - opacity: opacityFor(current), - duration: Durations.toggleableTransitionAnimation, - child: Icon( - AIcons.rating, - size: iconSize, - ), - ), - ], - ), + onChanged: (v) => settings.showThumbnailRating = v, + title: context.l10n.settingsThumbnailShowRating, + trailing: Icon( + AIcons.rating, + size: iconSize, + color: iconColor, ), ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.showThumbnailRaw, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.showThumbnailRaw = v, - title: Row( - children: [ - Expanded(child: Text(context.l10n.settingsThumbnailShowRawIcon)), - AnimatedOpacity( - opacity: opacityFor(current), - duration: Durations.toggleableTransitionAnimation, - child: Icon( - AIcons.raw, - size: iconSize, - ), - ), - ], - ), + onChanged: (v) => settings.showThumbnailRaw = v, + title: context.l10n.settingsThumbnailShowRawIcon, + trailing: Icon( + AIcons.raw, + size: iconSize, + color: iconColor, ), ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.showThumbnailVideoDuration, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.showThumbnailVideoDuration = v, - title: Text(context.l10n.settingsThumbnailShowVideoDuration), - ), + onChanged: (v) => settings.showThumbnailVideoDuration = v, + title: context.l10n.settingsThumbnailShowVideoDuration, ), ], ); diff --git a/lib/widgets/settings/video/controls.dart b/lib/widgets/settings/video/controls.dart index 895148960..1263508bb 100644 --- a/lib/widgets/settings/video/controls.dart +++ b/lib/widgets/settings/video/controls.dart @@ -2,9 +2,8 @@ import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/enums/video_controls.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; class VideoControlsTile extends StatelessWidget { const VideoControlsTile({Key? key}) : super(key: key); @@ -40,37 +39,23 @@ class VideoControlsPage extends StatelessWidget { body: SafeArea( child: ListView( children: [ - Selector( + SettingsSelectionListTile( + values: VideoControls.values, + getName: (context, v) => v.getName(context), selector: (context, s) => s.videoControls, - builder: (context, current, child) => ListTile( - title: Text(context.l10n.settingsVideoButtonsTile), - subtitle: Text(current.getName(context)), - onTap: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: current, - options: Map.fromEntries(VideoControls.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsVideoButtonsTitle, - ), - onSelection: (v) => settings.videoControls = v, - ), - ), + onSelection: (v) => settings.videoControls = v, + tileTitle: context.l10n.settingsVideoButtonsTile, + dialogTitle: context.l10n.settingsVideoButtonsTitle, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.videoGestureDoubleTapTogglePlay, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.videoGestureDoubleTapTogglePlay = v, - title: Text(context.l10n.settingsVideoGestureDoubleTapTogglePlay), - ), + onChanged: (v) => settings.videoGestureDoubleTapTogglePlay = v, + title: context.l10n.settingsVideoGestureDoubleTapTogglePlay, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.videoGestureSideDoubleTapSeek, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v, - title: Text(context.l10n.settingsVideoGestureSideDoubleTapSeek), - ), + onChanged: (v) => settings.videoGestureSideDoubleTapSeek = v, + title: context.l10n.settingsVideoGestureSideDoubleTapSeek, ), ], ), diff --git a/lib/widgets/settings/video/subtitle_sample.dart b/lib/widgets/settings/video/subtitle_sample.dart index 40917395f..208f9eea6 100644 --- a/lib/widgets/settings/video/subtitle_sample.dart +++ b/lib/widgets/settings/video/subtitle_sample.dart @@ -32,7 +32,7 @@ class SubtitleSample extends StatelessWidget { Color(0xffeaecc6), ], ), - border: AvesBorder.border, + border: AvesBorder.border(context), borderRadius: const BorderRadius.all(Radius.circular(24)), ), height: 128, diff --git a/lib/widgets/settings/video/subtitle_theme.dart b/lib/widgets/settings/video/subtitle_theme.dart index 31f2b6ba7..444c65456 100644 --- a/lib/widgets/settings/video/subtitle_theme.dart +++ b/lib/widgets/settings/video/subtitle_theme.dart @@ -2,7 +2,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/basic/color_list_tile.dart'; import 'package:aves/widgets/common/basic/slider_list_tile.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/video/subtitle_sample.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -30,8 +30,6 @@ class SubtitleThemeTile extends StatelessWidget { class SubtitleThemePage extends StatelessWidget { static const routeName = '/settings/video/subtitle_theme'; - static const textAlignOptions = [TextAlign.left, TextAlign.center, TextAlign.right]; - const SubtitleThemePage({Key? key}) : super(key: key); @override @@ -54,18 +52,13 @@ class SubtitleThemePage extends StatelessWidget { Expanded( child: ListView( children: [ - ListTile( - title: Text(context.l10n.settingsSubtitleThemeTextAlignmentTile), - subtitle: Text(_getTextAlignName(context, settings.subtitleTextAlignment)), - onTap: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: settings.subtitleTextAlignment, - options: Map.fromEntries(textAlignOptions.map((v) => MapEntry(v, _getTextAlignName(context, v)))), - title: context.l10n.settingsSubtitleThemeTextAlignmentTitle, - ), - onSelection: (v) => settings.subtitleTextAlignment = v, - ), + SettingsSelectionListTile( + values: const [TextAlign.left, TextAlign.center, TextAlign.right], + getName: _getTextAlignName, + selector: (context, s) => s.subtitleTextAlignment, + onSelection: (v) => settings.subtitleTextAlignment = v, + tileTitle: context.l10n.settingsSubtitleThemeTextAlignmentTile, + dialogTitle: context.l10n.settingsSubtitleThemeTextAlignmentTitle, ), SliderListTile( title: context.l10n.settingsSubtitleThemeTextSize, @@ -95,10 +88,10 @@ class SubtitleThemePage extends StatelessWidget { value: settings.subtitleBackgroundColor.opacity, onChanged: (v) => settings.subtitleBackgroundColor = settings.subtitleBackgroundColor.withOpacity(v), ), - SwitchListTile( - value: settings.subtitleShowOutline, + SettingsSwitchListTile( + selector: (context, s) => s.subtitleShowOutline, onChanged: (v) => settings.subtitleShowOutline = v, - title: Text(context.l10n.settingsSubtitleThemeShowOutline), + title: context.l10n.settingsSubtitleThemeShowOutline, ), ], ), diff --git a/lib/widgets/settings/video/video.dart b/lib/widgets/settings/video/video.dart index 9162582e3..4db331bc1 100644 --- a/lib/widgets/settings/video/video.dart +++ b/lib/widgets/settings/video/video.dart @@ -7,8 +7,8 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/video/controls.dart'; import 'package:aves/widgets/settings/video/subtitle_theme.dart'; import 'package:flutter/material.dart'; @@ -28,45 +28,28 @@ class VideoSection extends StatelessWidget { Widget build(BuildContext context) { final children = [ if (!standalonePage) - Selector( + SettingsSwitchListTile( selector: (context, s) => !s.hiddenFilters.contains(MimeFilter.video), - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.changeFilterVisibility({MimeFilter.video}, v), - title: Text(context.l10n.settingsVideoShowVideos), - ), + onChanged: (v) => settings.changeFilterVisibility({MimeFilter.video}, v), + title: context.l10n.settingsVideoShowVideos, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.enableVideoHardwareAcceleration, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.enableVideoHardwareAcceleration = v, - title: Text(context.l10n.settingsVideoEnableHardwareAcceleration), - ), + onChanged: (v) => settings.enableVideoHardwareAcceleration = v, + title: context.l10n.settingsVideoEnableHardwareAcceleration, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.enableVideoAutoPlay, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.enableVideoAutoPlay = v, - title: Text(context.l10n.settingsVideoEnableAutoPlay), - ), + onChanged: (v) => settings.enableVideoAutoPlay = v, + title: context.l10n.settingsVideoEnableAutoPlay, ), - Selector( + SettingsSelectionListTile( + values: VideoLoopMode.values, + getName: (context, v) => v.getName(context), selector: (context, s) => s.videoLoopMode, - builder: (context, current, child) => ListTile( - title: Text(context.l10n.settingsVideoLoopModeTile), - subtitle: Text(current.getName(context)), - onTap: () => showSelectionDialog( - context: context, - builder: (context) => AvesSelectionDialog( - initialValue: current, - options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))), - title: context.l10n.settingsVideoLoopModeTitle, - ), - onSelection: (v) => settings.videoLoopMode = v, - ), - ), + onSelection: (v) => settings.videoLoopMode = v, + tileTitle: context.l10n.settingsVideoLoopModeTile, + dialogTitle: context.l10n.settingsVideoLoopModeTitle, ), const VideoControlsTile(), const SubtitleThemeTile(), @@ -79,7 +62,7 @@ class VideoSection extends StatelessWidget { : AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.video, - color: AColors.video, + color: context.select((v) => v.video), ), title: context.l10n.settingsSectionVideo, expandedNotifier: expandedNotifier, diff --git a/lib/widgets/settings/viewer/entry_background.dart b/lib/widgets/settings/viewer/entry_background.dart index 64fdf6b28..4d808a33d 100644 --- a/lib/widgets/settings/viewer/entry_background.dart +++ b/lib/widgets/settings/viewer/entry_background.dart @@ -49,7 +49,7 @@ class _EntryBackgroundSelectorState extends State { width: radius * 2, decoration: BoxDecoration( color: selected.isColor ? selected.color : null, - border: AvesBorder.border, + border: AvesBorder.border(context), shape: BoxShape.circle, ), child: selected == EntryBackground.checkered diff --git a/lib/widgets/settings/viewer/overlay.dart b/lib/widgets/settings/viewer/overlay.dart index d85832c83..463fa97e9 100644 --- a/lib/widgets/settings/viewer/overlay.dart +++ b/lib/widgets/settings/viewer/overlay.dart @@ -1,5 +1,6 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -38,22 +39,16 @@ class ViewerOverlayPage extends StatelessWidget { body: SafeArea( child: ListView( children: [ - Selector( + SettingsSwitchListTile( selector: (context, s) => s.showOverlayOnOpening, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.showOverlayOnOpening = v, - title: Text(context.l10n.settingsViewerShowOverlayOnOpening), - ), + onChanged: (v) => settings.showOverlayOnOpening = v, + title: context.l10n.settingsViewerShowOverlayOnOpening, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.showOverlayInfo, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.showOverlayInfo = v, - title: Text(context.l10n.settingsViewerShowInformation), - subtitle: Text(context.l10n.settingsViewerShowInformationSubtitle), - ), + onChanged: (v) => settings.showOverlayInfo = v, + title: context.l10n.settingsViewerShowInformation, + subtitle: context.l10n.settingsViewerShowInformationSubtitle, ), Selector>( selector: (context, s) => Tuple2(s.showOverlayInfo, s.showOverlayShootingDetails), @@ -67,29 +62,20 @@ class ViewerOverlayPage extends StatelessWidget { ); }, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.showOverlayMinimap, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.showOverlayMinimap = v, - title: Text(context.l10n.settingsViewerShowMinimap), - ), + onChanged: (v) => settings.showOverlayMinimap = v, + title: context.l10n.settingsViewerShowMinimap, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.showOverlayThumbnailPreview, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.showOverlayThumbnailPreview = v, - title: Text(context.l10n.settingsViewerShowOverlayThumbnails), - ), + onChanged: (v) => settings.showOverlayThumbnailPreview = v, + title: context.l10n.settingsViewerShowOverlayThumbnails, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.enableOverlayBlurEffect, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.enableOverlayBlurEffect = v, - title: Text(context.l10n.settingsViewerEnableOverlayBlurEffect), - ), + onChanged: (v) => settings.enableOverlayBlurEffect = v, + title: context.l10n.settingsViewerEnableOverlayBlurEffect, ), ], ), diff --git a/lib/widgets/settings/viewer/viewer.dart b/lib/widgets/settings/viewer/viewer.dart index 794d64bd5..5f993816a 100644 --- a/lib/widgets/settings/viewer/viewer.dart +++ b/lib/widgets/settings/viewer/viewer.dart @@ -6,6 +6,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/settings/common/tile_leading.dart'; +import 'package:aves/widgets/settings/common/tiles.dart'; import 'package:aves/widgets/settings/viewer/entry_background.dart'; import 'package:aves/widgets/settings/viewer/overlay.dart'; import 'package:aves/widgets/settings/viewer/viewer_actions_editor.dart'; @@ -25,7 +26,7 @@ class ViewerSection extends StatelessWidget { return AvesExpansionTile( leading: SettingsTileLeading( icon: AIcons.image, - color: AColors.image, + color: context.select((v) => v.image), ), title: context.l10n.settingsSectionViewer, expandedNotifier: expandedNotifier, @@ -34,21 +35,15 @@ class ViewerSection extends StatelessWidget { const ViewerActionsTile(), const ViewerOverlayTile(), const _CutoutModeSwitch(), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.viewerMaxBrightness, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.viewerMaxBrightness = v, - title: Text(context.l10n.settingsViewerMaximumBrightness), - ), + onChanged: (v) => settings.viewerMaxBrightness = v, + title: context.l10n.settingsViewerMaximumBrightness, ), - Selector( + SettingsSwitchListTile( selector: (context, s) => s.enableMotionPhotoAutoPlay, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.enableMotionPhotoAutoPlay = v, - title: Text(context.l10n.settingsMotionPhotoAutoPlay), - ), + onChanged: (v) => settings.enableMotionPhotoAutoPlay = v, + title: context.l10n.settingsMotionPhotoAutoPlay, ), Selector( selector: (context, s) => s.imageBackground, @@ -87,13 +82,10 @@ class _CutoutModeSwitchState extends State<_CutoutModeSwitch> { future: _canSet, builder: (context, snapshot) { if (snapshot.hasData && snapshot.data!) { - return Selector( + return SettingsSwitchListTile( selector: (context, s) => s.viewerUseCutout, - builder: (context, current, child) => SwitchListTile( - value: current, - onChanged: (v) => settings.viewerUseCutout = v, - title: Text(context.l10n.settingsViewerUseCutout), - ), + onChanged: (v) => settings.viewerUseCutout = v, + title: context.l10n.settingsViewerUseCutout, ); } return const SizedBox.shrink(); diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index 635b91e05..700b2d0c0 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -1,4 +1,6 @@ import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/settings/enums/enums.dart'; +import 'package:aves/model/settings/settings.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; @@ -49,6 +51,8 @@ class FilterTable extends StatelessWidget { builder: (context, constraints) { final showPercentIndicator = constraints.maxWidth - (chipWidth + countWidth) > percentIndicatorMinWidth; final displayedEntries = maxRowCount != null ? sortedEntries.take(maxRowCount!) : sortedEntries; + final theme = Theme.of(context); + final isMonochrome = settings.themeColorMode == AvesThemeColorMode.monochrome; return Table( children: displayedEntries.map((kv) { final filter = filterBuilder(kv.key); @@ -81,14 +85,16 @@ class FilterTable extends StatelessWidget { return LinearPercentIndicator( percent: percent, lineHeight: lineHeight, - backgroundColor: Colors.white24, - progressColor: color, + backgroundColor: theme.colorScheme.onPrimary.withOpacity(.1), + progressColor: isMonochrome ? theme.colorScheme.secondary : color, animation: true, isRTL: isRtl, barRadius: barRadius, center: Text( intl.NumberFormat.percentPattern().format(percent), - style: const TextStyle(shadows: Constants.embossShadows), + style: TextStyle( + shadows: theme.brightness == Brightness.dark ? Constants.embossShadows : null, + ), ), padding: EdgeInsets.zero, ); @@ -98,7 +104,9 @@ class FilterTable extends StatelessWidget { ), Text( '$count', - style: const TextStyle(color: Colors.white70), + style: TextStyle( + color: theme.textTheme.caption!.color, + ), textAlign: TextAlign.end, ), ], diff --git a/lib/widgets/stats/stats_page.dart b/lib/widgets/stats/stats_page.dart index 32db49dae..24a594ca3 100644 --- a/lib/widgets/stats/stats_page.dart +++ b/lib/widgets/stats/stats_page.dart @@ -10,8 +10,8 @@ import 'package:aves/model/settings/enums/accessibility_animations.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; @@ -22,7 +22,7 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/stats/filter_table.dart'; import 'package:charts_flutter/flutter.dart' as charts; import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; +import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart' as intl; import 'package:percent_indicator/linear_percent_indicator.dart'; @@ -77,17 +77,26 @@ class StatsPage extends StatelessWidget { text: context.l10n.collectionEmptyImages, ); } else { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; final animate = context.select((v) => v.accessibilityAnimations.animate); final byMimeTypes = groupBy(entries, (entry) => entry.mimeType).map((k, v) => MapEntry(k, v.length)); final imagesByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('image'))); final videoByMimeTypes = Map.fromEntries(byMimeTypes.entries.where((kv) => kv.key.startsWith('video'))); - final mimeDonuts = Wrap( - alignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - _buildMimeDonut(context, AIcons.image, imagesByMimeTypes, animate), - _buildMimeDonut(context, AIcons.video, videoByMimeTypes, animate), - ], + final mimeDonuts = Provider.value( + value: isDark ? NeonOnDark() : PastelOnLight(), + child: Builder( + builder: (context) { + return Wrap( + alignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + _buildMimeDonut(context, AIcons.image, imagesByMimeTypes, animate), + _buildMimeDonut(context, AIcons.video, videoByMimeTypes, animate), + ], + ); + }, + ), ); final catalogued = entries.where((entry) => entry.isCatalogued); @@ -114,14 +123,16 @@ class StatsPage extends StatelessWidget { child: LinearPercentIndicator( percent: withGpsPercent, lineHeight: lineHeight, - backgroundColor: Colors.white24, - progressColor: Theme.of(context).colorScheme.secondary, + backgroundColor: theme.colorScheme.onPrimary.withOpacity(.1), + progressColor: theme.colorScheme.secondary, animation: animate, isRTL: context.isRtl, barRadius: barRadius, center: Text( intl.NumberFormat.percentPattern().format(withGpsPercent), - style: const TextStyle(shadows: Constants.embossShadows), + style: TextStyle( + shadows: isDark ? Constants.embossShadows : null, + ), ), padding: EdgeInsets.zero, ), @@ -176,7 +187,17 @@ class StatsPage extends StatelessWidget { final sum = byMimeTypes.values.fold(0, (prev, v) => prev + v); - final seriesData = byMimeTypes.entries.map((kv) => EntryByMimeDatum(mimeType: kv.key, entryCount: kv.value)).toList(); + final colors = context.watch(); + final seriesData = byMimeTypes.entries.map((kv) { + final mimeType = kv.key; + final displayText = MimeUtils.displayType(mimeType); + return EntryByMimeDatum( + mimeType: mimeType, + displayText: displayText, + color: colors.fromString(displayText), + entryCount: kv.value, + ); + }).toList(); seriesData.sort((d1, d2) { final c = d2.entryCount.compareTo(d1.entryCount); return c != 0 ? c : compareAsciiUpperCase(d1.displayText, d2.displayText); @@ -250,7 +271,9 @@ class StatsPage extends StatelessWidget { const SizedBox(width: 8), Text( '${d.entryCount}', - style: const TextStyle(color: Colors.white70), + style: TextStyle( + color: Theme.of(context).textTheme.caption!.color, + ), ), ], ), @@ -327,10 +350,8 @@ class StatsPage extends StatelessWidget { MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( - collection: CollectionLens( - source: source, - filters: {filter}, - ), + source: source, + filters: {filter}, ), ), (route) => false, @@ -338,18 +359,19 @@ class StatsPage extends StatelessWidget { } } -class EntryByMimeDatum { - final String mimeType; - final String displayText; +@immutable +class EntryByMimeDatum extends Equatable { + final String mimeType, displayText; + final Color color; final int entryCount; - EntryByMimeDatum({ - required this.mimeType, - required this.entryCount, - }) : displayText = MimeUtils.displayType(mimeType); - - Color get color => stringToColor(displayText); - @override - String toString() => '$runtimeType#${shortHash(this)}{mimeType=$mimeType, displayText=$displayText, entryCount=$entryCount}'; + List get props => [mimeType, displayText, color, entryCount]; + + const EntryByMimeDatum({ + required this.mimeType, + required this.displayText, + required this.color, + required this.entryCount, + }); } diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 8f093ba5b..5434ee6c1 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -8,16 +8,13 @@ import 'package:aves/model/device.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/filters/album.dart'; -import 'package:aves/model/highlight.dart'; import 'package:aves/model/settings/enums/enums.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/common/image_op_events.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; import 'package:aves/services/media/media_file_service.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/action_mixins/entry_storage.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; @@ -262,28 +259,19 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final showAction = isMainMode && newUris.isNotEmpty ? SnackBarAction( label: context.l10n.showButtonLabel, - onPressed: () async { - final highlightInfo = context.read(); - final targetCollection = CollectionLens( - source: source, - filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))}, - ); - unawaited(Navigator.pushAndRemoveUntil( + onPressed: () { + Navigator.pushAndRemoveUntil( context, MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( - collection: targetCollection, + source: source, + filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))}, + highlightTest: (entry) => newUris.contains(entry.uri), ), ), (route) => false, - )); - final delayDuration = context.read().staggeredAnimationPageTarget; - await Future.delayed(delayDuration + Durations.highlightScrollInitDelay); - final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => newUris.contains(entry.uri)); - if (targetEntry != null) { - highlightInfo.trackItem(targetEntry, highlightItem: targetEntry); - } + ); }, ) : null; diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index ed28f6d7a..59866e2cf 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -39,7 +39,7 @@ class ViewerVerticalPageView extends StatefulWidget { } class _ViewerVerticalPageViewState extends State { - final ValueNotifier _backgroundColorNotifier = ValueNotifier(Colors.black); + final ValueNotifier _backgroundOpacityNotifier = ValueNotifier(1); final ValueNotifier _isVerticallyScrollingNotifier = ValueNotifier(false); Timer? _verticalScrollMonitoringTimer; AvesEntry? _oldEntry; @@ -122,12 +122,15 @@ class _ViewerVerticalPageViewState extends State { imagePage, infoPage, ]; - return ValueListenableBuilder( - valueListenable: _backgroundColorNotifier, - builder: (context, backgroundColor, child) => Container( - color: backgroundColor, - child: child, - ), + return ValueListenableBuilder( + valueListenable: _backgroundOpacityNotifier, + builder: (context, backgroundOpacity, child) { + final background = Theme.of(context).brightness == Brightness.dark ? Colors.black : Colors.white; + return Container( + color: background.withOpacity(backgroundOpacity), + child: child, + ); + }, child: PageView( // key is expected by test driver key: const Key('vertical-pageview'), @@ -196,7 +199,7 @@ class _ViewerVerticalPageViewState extends State { final page = widget.verticalPager.page!; final opacity = min(1.0, page); - _backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity); + _backgroundOpacityNotifier.value = opacity * opacity; if (page <= 1 && settings.viewerMaxBrightness) { _systemBrightness?.then((system) { diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index 674f55963..16b8f7d31 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -35,7 +35,11 @@ class EntryViewerPage extends StatelessWidget { ), ), ), - backgroundColor: Navigator.canPop(context) ? Colors.transparent : Colors.black, + backgroundColor: Navigator.canPop(context) + ? Colors.transparent + : Theme.of(context).brightness == Brightness.dark + ? Colors.black + : Colors.white, resizeToAvoidBottomInset: false, ), ); diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index 3307af7b7..3773c9e4e 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -414,10 +414,8 @@ class _EntryViewerStackState extends State with FeedbackMixin, settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) { return CollectionPage( - collection: CollectionLens( - source: baseCollection.source, - filters: baseCollection.filters, - )..addFilter(filter), + source: baseCollection.source, + filters: {...baseCollection.filters, filter}, ); }, ), diff --git a/lib/widgets/viewer/info/basic_section.dart b/lib/widgets/viewer/info/basic_section.dart index 91b3a97a3..6027073fb 100644 --- a/lib/widgets/viewer/info/basic_section.dart +++ b/lib/widgets/viewer/info/basic_section.dart @@ -8,6 +8,7 @@ import 'package:aves/model/filters/rating.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/type.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; @@ -156,7 +157,7 @@ class BasicSection extends StatelessWidget { DecoratedBox( decoration: BoxDecoration( border: Border.fromBorderSide(BorderSide( - color: isEditing ? Theme.of(context).disabledColor : AvesFilterChip.defaultOutlineColor, + color: isEditing ? Theme.of(context).disabledColor : context.select((v) => v.neutral), width: AvesFilterChip.outlineWidth, )), borderRadius: const BorderRadius.all(Radius.circular(AvesFilterChip.defaultRadius)), diff --git a/lib/widgets/viewer/info/common.dart b/lib/widgets/viewer/info/common.dart index f1c568c5c..f6e72a77d 100644 --- a/lib/widgets/viewer/info/common.dart +++ b/lib/widgets/viewer/info/common.dart @@ -21,7 +21,6 @@ class SectionRow extends StatelessWidget { width: dim, child: Divider( thickness: AvesFilterChip.outlineWidth, - color: Colors.white70, ), ); return Row( @@ -47,11 +46,11 @@ class InfoRowGroup extends StatefulWidget { final Map? linkHandlers; static const keyValuePadding = 16; - static const linkColor = Colors.blue; static const fontSize = 13.0; - static const baseStyle = TextStyle(fontSize: fontSize); - static final keyStyle = baseStyle.copyWith(color: Colors.white70, height: 2.0); - static final linkStyle = baseStyle.copyWith(color: linkColor, decoration: TextDecoration.underline); + static const valueStyle = TextStyle(fontSize: fontSize); + static final _keyStyle = valueStyle.copyWith(height: 2.0); + + static TextStyle keyStyle(BuildContext context) => Theme.of(context).textTheme.caption!.merge(_keyStyle); const InfoRowGroup({ Key? key, @@ -77,9 +76,11 @@ class _InfoRowGroupState extends State { Widget build(BuildContext context) { if (keyValues.isEmpty) return const SizedBox.shrink(); + final _keyStyle = InfoRowGroup.keyStyle(context); + // compute the size of keys and space in order to align values final textScaleFactor = MediaQuery.textScaleFactorOf(context); - final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: key, style: InfoRowGroup.keyStyle), textScaleFactor)))); + final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: key, style: _keyStyle), textScaleFactor)))); final lastKey = keyValues.keys.last; return LayoutBuilder( @@ -102,7 +103,10 @@ class _InfoRowGroupState extends State { value = handler.linkText(context); // open link on tap recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context); - style = InfoRowGroup.linkStyle; + // `colorScheme.secondary` is overridden upstream as an `ExpansionTileCard` theming workaround, + // so we use `colorScheme.primary` instead + final linkColor = Theme.of(context).colorScheme.primary; + style = InfoRowGroup.valueStyle.copyWith(color: linkColor, decoration: TextDecoration.underline); } else { value = kv.value; // long values are clipped, and made expandable by tapping them @@ -125,14 +129,14 @@ class _InfoRowGroupState extends State { // (e.g. keys on the right for RTL locale, whatever the key intrinsic directionality) // and each span respects the directionality of its inner text only return [ - TextSpan(text: '${Constants.fsi}$key${Constants.pdi}', style: InfoRowGroup.keyStyle), + TextSpan(text: '${Constants.fsi}$key${Constants.pdi}', style: _keyStyle), WidgetSpan(child: SizedBox(width: thisSpaceSize)), TextSpan(text: '${Constants.fsi}$value${Constants.pdi}', style: style, recognizer: recognizer), ]; }, ).toList(), ), - style: InfoRowGroup.baseStyle, + style: InfoRowGroup.valueStyle, ); }, ); diff --git a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart index 9caaa55ba..839e1dd67 100644 --- a/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart +++ b/lib/widgets/viewer/info/metadata/metadata_dir_tile.dart @@ -3,7 +3,7 @@ import 'dart:collection'; import 'package:aves/model/entry.dart'; import 'package:aves/ref/brand_colors.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart'; -import 'package:aves/utils/color_utils.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; @@ -15,6 +15,7 @@ import 'package:aves/widgets/viewer/info/metadata/xmp_tile.dart'; import 'package:aves/widgets/viewer/source_viewer_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class MetadataDirTile extends StatelessWidget { final AvesEntry entry; @@ -58,9 +59,10 @@ class MetadataDirTile extends StatelessWidget { break; } + final colors = context.watch(); return AvesExpansionTile( title: title, - color: dir.color ?? BrandColors.get(dirName) ?? stringToColor(dirName), + highlightColor: dir.color ?? colors.fromBrandColor(BrandColors.get(dirName)) ?? colors.fromString(dirName), expandedNotifier: expandedDirectoryNotifier, initiallyExpanded: initiallyExpanded, children: [ diff --git a/lib/widgets/viewer/info/metadata/metadata_section.dart b/lib/widgets/viewer/info/metadata/metadata_section.dart index 72201bc3d..bfe18d098 100644 --- a/lib/widgets/viewer/info/metadata/metadata_section.dart +++ b/lib/widgets/viewer/info/metadata/metadata_section.dart @@ -7,9 +7,9 @@ import 'package:aves/model/video/metadata.dart'; import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/metadata/svg_metadata_service.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/utils/color_utils.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart'; import 'package:collection/collection.dart'; @@ -218,13 +218,14 @@ class _MetadataSectionSliverState extends State { if (knownStreams.isNotEmpty) { final indexDigits = knownStreams.length.toString().length; + final colors = context.read(); for (final stream in knownStreams) { final index = (stream[Keys.index] ?? 0) + 1; final typeText = getTypeText(stream); final dirName = 'Stream ${index.toString().padLeft(indexDigits, '0')} • $typeText'; final formattedStreamTags = VideoMetadataFormatter.formatInfo(stream); if (formattedStreamTags.isNotEmpty) { - final color = stringToColor(typeText); + final color = colors.fromString(typeText); directories.add(MetadataDirectory(dirName, _toSortedTags(formattedStreamTags), color: color)); } } diff --git a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart index 4bf210b72..0fbbc0f5b 100644 --- a/lib/widgets/viewer/info/metadata/xmp_namespaces.dart +++ b/lib/widgets/viewer/info/metadata/xmp_namespaces.dart @@ -1,4 +1,5 @@ import 'package:aves/ref/brand_colors.dart'; +import 'package:aves/theme/colors.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/string_utils.dart'; import 'package:aves/utils/xmp_utils.dart'; @@ -19,6 +20,7 @@ import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; @immutable class XmpNamespace extends Equatable { @@ -98,7 +100,10 @@ class XmpNamespace extends Equatable { 'Iptc4xmpCore': 'IPTC Core', 'Iptc4xmpExt': 'IPTC Extension', 'lr': 'Lightroom', - 'MicrosoftPhoto': 'Microsoft Photo', + 'mediapro': 'MediaPro', + 'MicrosoftPhoto': 'Microsoft Photo 1.0', + 'MP1': 'Microsoft Photo 1.1', + 'MP': 'Microsoft Photo 1.2', 'mwg-rs': 'Regions', 'nga': 'National Gallery of Art', 'panorama': 'Panorama', @@ -122,7 +127,7 @@ class XmpNamespace extends Equatable { Map get buildProps => rawProps; - List buildNamespaceSection() { + List buildNamespaceSection(BuildContext context) { final props = buildProps.entries .map((kv) { final prop = XmpProp(kv.key, kv.value); @@ -149,7 +154,7 @@ class XmpNamespace extends Equatable { padding: const EdgeInsets.only(top: 8), child: HighlightTitle( title: displayTitle, - color: BrandColors.get(displayTitle), + color: context.select((v) => v.fromBrandColor(BrandColors.get(displayTitle))), selectable: true, ), ), diff --git a/lib/widgets/viewer/info/metadata/xmp_tile.dart b/lib/widgets/viewer/info/metadata/xmp_tile.dart index 74cc82ce2..aa7d2c79d 100644 --- a/lib/widgets/viewer/info/metadata/xmp_tile.dart +++ b/lib/widgets/viewer/info/metadata/xmp_tile.dart @@ -7,6 +7,7 @@ import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class XmpDirTile extends StatefulWidget { final AvesEntry entry; @@ -43,7 +44,7 @@ class _XmpDirTileState extends State { return AvesExpansionTile( // title may contain parent to distinguish multiple XMP directories title: widget.title, - color: AColors.xmp, + highlightColor: context.select((v) => v.xmp), expandedNotifier: widget.expandedNotifier, initiallyExpanded: widget.initiallyExpanded, children: [ @@ -51,7 +52,7 @@ class _XmpDirTileState extends State { padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: sections.expand((section) => section.buildNamespaceSection()).toList(), + children: sections.expand((section) => section.buildNamespaceSection(context)).toList(), ), ), ], diff --git a/lib/widgets/viewer/info/owner.dart b/lib/widgets/viewer/info/owner.dart index fe01690cd..302516c51 100644 --- a/lib/widgets/viewer/info/owner.dart +++ b/lib/widgets/viewer/info/owner.dart @@ -63,7 +63,7 @@ class _OwnerPropState extends State { children: [ TextSpan( text: context.l10n.viewerInfoLabelOwner, - style: InfoRowGroup.keyStyle, + style: InfoRowGroup.keyStyle(context), ), WidgetSpan( alignment: PlaceholderAlignment.middle, @@ -81,7 +81,7 @@ class _OwnerPropState extends State { ), TextSpan( text: appName, - style: InfoRowGroup.baseStyle, + style: InfoRowGroup.valueStyle, ), ], ), diff --git a/lib/widgets/viewer/overlay/common.dart b/lib/widgets/viewer/overlay/common.dart index 4aa7331d1..0c94ca5a5 100644 --- a/lib/widgets/viewer/overlay/common.dart +++ b/lib/widgets/viewer/overlay/common.dart @@ -1,10 +1,9 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:flutter/material.dart'; -Color overlayBackgroundColor({required bool blurred}) => blurred ? Colors.black26 : Colors.black38; - class OverlayButton extends StatelessWidget { final Animation scale; final BorderRadius? borderRadius; @@ -19,6 +18,7 @@ class OverlayButton extends StatelessWidget { @override Widget build(BuildContext context) { + final brightness = Theme.of(context).brightness; final blurred = settings.enableOverlayBlurEffect; return ScaleTransition( scale: scale, @@ -29,10 +29,10 @@ class OverlayButton extends StatelessWidget { child: Material( type: MaterialType.button, borderRadius: borderRadius, - color: overlayBackgroundColor(blurred: blurred), + color: Themes.overlayBackgroundColor(brightness: brightness, blurred: blurred), child: Ink( decoration: BoxDecoration( - border: AvesBorder.border, + border: AvesBorder.border(context), borderRadius: borderRadius, ), child: child, @@ -43,10 +43,10 @@ class OverlayButton extends StatelessWidget { enabled: blurred, child: Material( type: MaterialType.circle, - color: overlayBackgroundColor(blurred: blurred), + color: Themes.overlayBackgroundColor(brightness: brightness, blurred: blurred), child: Ink( decoration: BoxDecoration( - border: AvesBorder.border, + border: AvesBorder.border(context), shape: BoxShape.circle, ), child: child, @@ -78,6 +78,7 @@ class OverlayTextButton extends StatelessWidget { @override Widget build(BuildContext context) { final blurred = settings.enableOverlayBlurEffect; + final theme = Theme.of(context); return SizeTransition( sizeFactor: scale, child: BlurredRRect.all( @@ -86,11 +87,11 @@ class OverlayTextButton extends StatelessWidget { child: OutlinedButton( onPressed: onPressed, style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(overlayBackgroundColor(blurred: blurred)), - foregroundColor: MaterialStateProperty.all(Colors.white), - overlayColor: MaterialStateProperty.all(Colors.white.withOpacity(0.12)), + backgroundColor: MaterialStateProperty.all(Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred)), + foregroundColor: MaterialStateProperty.all(theme.colorScheme.onSurface), + overlayColor: theme.brightness == Brightness.dark ? MaterialStateProperty.all(Colors.white.withOpacity(0.12)) : null, minimumSize: _minSize, - side: MaterialStateProperty.all(AvesBorder.curvedSide), + side: MaterialStateProperty.all(AvesBorder.curvedSide(context)), shape: MaterialStateProperty.all(const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(_borderRadius)), )), diff --git a/lib/widgets/viewer/overlay/details.dart b/lib/widgets/viewer/overlay/details.dart index e0c8bb44b..cfc166590 100644 --- a/lib/widgets/viewer/overlay/details.dart +++ b/lib/widgets/viewer/overlay/details.dart @@ -24,6 +24,8 @@ const double _iconSize = 16.0; const double _interRowPadding = 2.0; const double _subRowMinWidth = 300.0; +List? _shadows(BuildContext context) => Theme.of(context).brightness == Brightness.dark ? Constants.embossShadows : null; + class ViewerDetailOverlay extends StatefulWidget { final List entries; final int index; @@ -142,7 +144,7 @@ class ViewerDetailOverlayContent extends StatelessWidget { return DefaultTextStyle( style: Theme.of(context).textTheme.bodyText2!.copyWith( - shadows: Constants.embossShadows, + shadows: _shadows(context), ), softWrap: false, overflow: TextOverflow.fade, @@ -271,7 +273,7 @@ class _LocationRow extends AnimatedWidget { } return Row( children: [ - const DecoratedIcon(AIcons.location, shadows: Constants.embossShadows, size: _iconSize), + DecoratedIcon(AIcons.location, shadows: _shadows(context), size: _iconSize), const SizedBox(width: _iconPadding), Expanded(child: Text(location, strutStyle: Constants.overflowStrutStyle)), ], @@ -352,7 +354,7 @@ class _DateRow extends StatelessWidget { return Row( children: [ - const DecoratedIcon(AIcons.date, shadows: Constants.embossShadows, size: _iconSize), + DecoratedIcon(AIcons.date, shadows: _shadows(context), size: _iconSize), const SizedBox(width: _iconPadding), Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)), Expanded(flex: 2, child: Text(resolutionText, strutStyle: Constants.overflowStrutStyle)), @@ -381,7 +383,7 @@ class _ShootingRow extends StatelessWidget { return Row( children: [ - const DecoratedIcon(AIcons.shooting, shadows: Constants.embossShadows, size: _iconSize), + DecoratedIcon(AIcons.shooting, shadows: _shadows(context), size: _iconSize), const SizedBox(width: _iconPadding), Expanded(child: Text(apertureText, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(details.exposureTime ?? Constants.overlayUnknown, strutStyle: Constants.overflowStrutStyle)), diff --git a/lib/widgets/viewer/overlay/top.dart b/lib/widgets/viewer/overlay/top.dart index fdebc1f4b..b7075e7bd 100644 --- a/lib/widgets/viewer/overlay/top.dart +++ b/lib/widgets/viewer/overlay/top.dart @@ -1,9 +1,9 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/themes.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/viewer/multipage/conductor.dart'; import 'package:aves/widgets/viewer/multipage/controller.dart'; -import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/overlay/details.dart'; import 'package:aves/widgets/viewer/overlay/minimap.dart'; import 'package:aves/widgets/viewer/page_entry_builder.dart'; @@ -74,7 +74,7 @@ class ViewerTopOverlay extends StatelessWidget { BlurredRect( enabled: blurred, child: Container( - color: overlayBackgroundColor(blurred: blurred), + color: Themes.overlayBackgroundColor(brightness: Theme.of(context).brightness, blurred: blurred), child: SafeArea( minimum: EdgeInsets.only(top: (viewInsets?.top ?? 0) + (viewPadding?.top ?? 0)), bottom: false, diff --git a/lib/widgets/viewer/overlay/video/progress_bar.dart b/lib/widgets/viewer/overlay/video/progress_bar.dart index 6138b37f2..aa9a1774c 100644 --- a/lib/widgets/viewer/overlay/video/progress_bar.dart +++ b/lib/widgets/viewer/overlay/video/progress_bar.dart @@ -2,10 +2,10 @@ import 'dart:async'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/theme/format.dart'; +import 'package:aves/theme/themes.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; -import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:aves/widgets/viewer/video/controller.dart'; import 'package:flutter/material.dart'; @@ -38,7 +38,10 @@ class _VideoProgressBarState extends State { @override Widget build(BuildContext context) { final blurred = settings.enableOverlayBlurEffect; - const textStyle = TextStyle(shadows: Constants.embossShadows); + final brightness = Theme.of(context).brightness; + final textStyle = TextStyle( + shadows: brightness == Brightness.dark ? Constants.embossShadows : null, + ); return SizeTransition( sizeFactor: widget.scale, child: BlurredRRect.all( @@ -66,8 +69,8 @@ class _VideoProgressBarState extends State { alignment: Alignment.center, padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), decoration: BoxDecoration( - color: overlayBackgroundColor(blurred: blurred), - border: AvesBorder.border, + color: Themes.overlayBackgroundColor(brightness: brightness, blurred: blurred), + border: AvesBorder.border(context), borderRadius: const BorderRadius.all(Radius.circular(radius)), ), child: Column( @@ -106,12 +109,12 @@ class _VideoProgressBarState extends State { if (!progress.isFinite) progress = 0.0; return LinearProgressIndicator( value: progress, - backgroundColor: Colors.grey.shade700, + backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(.2), ); }), ), ), - const Text( + Text( // fake text below to match the height of the text above and center the whole thing '', style: textStyle, diff --git a/lib/widgets/viewer/overlay/viewer_button_row.dart b/lib/widgets/viewer/overlay/viewer_button_row.dart index 3b8585b77..0461daefc 100644 --- a/lib/widgets/viewer/overlay/viewer_button_row.dart +++ b/lib/widgets/viewer/overlay/viewer_button_row.dart @@ -362,16 +362,21 @@ class ViewerButtonRowContent extends StatelessWidget { ); return PopupMenuItem( - child: Row( - children: [ - buildDivider(), - buildItem(EntryAction.rotateCCW), - buildDivider(), - buildItem(EntryAction.rotateCW), - buildDivider(), - buildItem(EntryAction.flip), - buildDivider(), - ], + child: IconTheme.merge( + data: IconThemeData( + color: ListTileTheme.of(context).iconColor, + ), + child: Row( + children: [ + buildDivider(), + buildItem(EntryAction.rotateCCW), + buildDivider(), + buildItem(EntryAction.rotateCW), + buildDivider(), + buildItem(EntryAction.flip), + buildDivider(), + ], + ), ), ); } diff --git a/lib/widgets/viewer/video_action_delegate.dart b/lib/widgets/viewer/video_action_delegate.dart index da920b545..0873f0ef0 100644 --- a/lib/widgets/viewer/video_action_delegate.dart +++ b/lib/widgets/viewer/video_action_delegate.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/filters/album.dart'; -import 'package:aves/model/highlight.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/services/media/enums.dart'; @@ -113,30 +112,21 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix final showAction = _collection != null ? SnackBarAction( label: context.l10n.showButtonLabel, - onPressed: () async { - final highlightInfo = context.read(); + onPressed: () { final source = _collection.source; - final targetCollection = CollectionLens( - source: source, - filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))}, - ); - unawaited(Navigator.pushAndRemoveUntil( + final newUri = newFields['uri'] as String?; + Navigator.pushAndRemoveUntil( context, MaterialPageRoute( settings: const RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage( - collection: targetCollection, + source: source, + filters: {AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum))}, + highlightTest: (entry) => entry.uri == newUri, ), ), (route) => false, - )); - final delayDuration = context.read().staggeredAnimationPageTarget; - await Future.delayed(delayDuration + Durations.highlightScrollInitDelay); - final newUri = newFields['uri'] as String?; - final targetEntry = targetCollection.sortedEntries.firstWhereOrNull((entry) => entry.uri == newUri); - if (targetEntry != null) { - highlightInfo.trackItem(targetEntry, highlightItem: targetEntry); - } + ); }, ) : null; diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index f89ff0c79..5d74b5c9a 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -209,6 +209,7 @@ class _EntryPageViewState extends State { _actionFeedbackChildNotifier.value = DecoratedIcon( icon?.call() ?? action.getIconData(), size: 48, + color: Colors.white, shadows: const [ Shadow( color: Colors.black, diff --git a/untranslated.json b/untranslated.json index 30b7b78cf..112e127e1 100644 --- a/untranslated.json +++ b/untranslated.json @@ -1,7 +1,29 @@ { + "de": [ + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", + "settingsSectionDisplay", + "settingsThemeBrightness", + "settingsThemeColorful" + ], + "es": [ - "videoActionMute", - "videoActionUnmute" + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", + "settingsSectionDisplay", + "settingsThemeBrightness", + "settingsThemeColorful" + ], + + "fr": [ + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", + "settingsSectionDisplay", + "settingsThemeBrightness", + "settingsThemeColorful" ], "id": [ @@ -11,12 +33,54 @@ "videoControlsPlaySeek", "videoControlsPlayOutside", "videoControlsNone", + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", "settingsViewerShowOverlayThumbnails", "settingsVideoControlsTile", "settingsVideoControlsTitle", "settingsVideoButtonsTile", "settingsVideoButtonsTitle", "settingsVideoGestureDoubleTapTogglePlay", - "settingsVideoGestureSideDoubleTapSeek" + "settingsVideoGestureSideDoubleTapSeek", + "settingsSectionDisplay", + "settingsThemeBrightness", + "settingsThemeColorful" + ], + + "ja": [ + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", + "settingsSectionDisplay", + "settingsThemeBrightness", + "settingsThemeColorful" + ], + + "ko": [ + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", + "settingsSectionDisplay", + "settingsThemeBrightness", + "settingsThemeColorful" + ], + + "pt": [ + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", + "settingsSectionDisplay", + "settingsThemeBrightness", + "settingsThemeColorful" + ], + + "ru": [ + "themeBrightnessLight", + "themeBrightnessDark", + "themeBrightnessBlack", + "settingsSectionDisplay", + "settingsThemeBrightness", + "settingsThemeColorful" ] }