From 3cef268138e6b115db225587f882b620435adf7f Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 13 Feb 2024 00:03:25 +0100 Subject: [PATCH] #902 widget: outline color options according to device theme --- CHANGELOG.md | 1 + .../thibault/aves/HomeWidgetProvider.kt | 3 + lib/model/settings/enums/widget_outline.dart | 23 +++ lib/model/settings/settings.dart | 7 +- lib/widget_common.dart | 8 +- lib/widgets/common/basic/color_indicator.dart | 18 +- lib/widgets/home_widget.dart | 5 +- .../settings/home_widget_settings_page.dart | 174 +++++++++++------- .../aves_model/lib/src/settings/enums.dart | 2 + 9 files changed, 166 insertions(+), 75 deletions(-) create mode 100644 lib/model/settings/enums/widget_outline.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc550140..267423d61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file. ### Added - Viewer: prompt to show newly edited item +- Widget: outline color options according to device theme - Catalan translation (thanks Marc AmorĂ³s) ## [v1.10.4] - 2024-02-07 diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt index c65609042..81933f251 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/HomeWidgetProvider.kt @@ -87,6 +87,8 @@ class HomeWidgetProvider : AppWidgetProvider() { val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo) if (widthPx == 0 || heightPx == 0) return null + val isNightModeOn = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + initFlutterEngine(context) val messenger = flutterEngine!!.dartExecutor val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL) @@ -101,6 +103,7 @@ class HomeWidgetProvider : AppWidgetProvider() { "devicePixelRatio" to getDevicePixelRatio(), "drawEntryImage" to drawEntryImage, "reuseEntry" to reuseEntry, + "isSystemThemeDark" to isNightModeOn, ), object : MethodChannel.Result { override fun success(result: Any?) { cont.resume(result) diff --git a/lib/model/settings/enums/widget_outline.dart b/lib/model/settings/enums/widget_outline.dart new file mode 100644 index 000000000..0b4f0e13f --- /dev/null +++ b/lib/model/settings/enums/widget_outline.dart @@ -0,0 +1,23 @@ +import 'package:aves_model/aves_model.dart'; +import 'package:dynamic_color/dynamic_color.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +extension ExtraWidgetOutline on WidgetOutline { + Future color(Brightness brightness) async { + switch (this) { + case WidgetOutline.none: + return SynchronousFuture(null); + case WidgetOutline.black: + return SynchronousFuture(Colors.black); + case WidgetOutline.white: + return SynchronousFuture(Colors.white); + case WidgetOutline.systemBlackAndWhite: + return SynchronousFuture(brightness == Brightness.dark ? Colors.black : Colors.white); + case WidgetOutline.systemDynamic: + final corePalette = await DynamicColorPlugin.getCorePalette(); + final scheme = corePalette?.toColorScheme(brightness: brightness); + return scheme?.primary ?? await WidgetOutline.systemBlackAndWhite.color(brightness); + } + } +} diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 693f8d9e7..f8a7325d8 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -274,12 +274,9 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings // widget - Color? getWidgetOutline(int widgetId) { - final value = getInt('${SettingKeys.widgetOutlinePrefixKey}$widgetId'); - return value != null ? Color(value) : null; - } + WidgetOutline getWidgetOutline(int widgetId) => getEnumOrDefault('${SettingKeys.widgetOutlinePrefixKey}$widgetId', WidgetOutline.none, WidgetOutline.values); - void setWidgetOutline(int widgetId, Color? newValue) => set('${SettingKeys.widgetOutlinePrefixKey}$widgetId', newValue?.value); + void setWidgetOutline(int widgetId, WidgetOutline newValue) => set('${SettingKeys.widgetOutlinePrefixKey}$widgetId', newValue.toString()); WidgetShape getWidgetShape(int widgetId) => getEnumOrDefault('${SettingKeys.widgetShapePrefixKey}$widgetId', SettingsDefaults.widgetShape, WidgetShape.values); diff --git a/lib/widget_common.dart b/lib/widget_common.dart index caca41659..fd20ac7eb 100644 --- a/lib/widget_common.dart +++ b/lib/widget_common.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:aves/app_flavor.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/sort.dart'; +import 'package:aves/model/settings/enums/widget_outline.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/media_store_source.dart'; @@ -20,6 +21,7 @@ void widgetMainCommon(AppFlavor flavor) async { WidgetsFlutterBinding.ensureInitialized(); initPlatformServices(); await settings.init(monitorPlatformSettings: false); + await reportService.init(); _widgetDrawChannel.setMethodCallHandler((call) async { // widget settings may be modified in a different process after channel setup @@ -41,6 +43,10 @@ Future> _drawWidget(dynamic args) async { final devicePixelRatio = args['devicePixelRatio'] as double; final drawEntryImage = args['drawEntryImage'] as bool; final reuseEntry = args['reuseEntry'] as bool; + final isSystemThemeDark = args['isSystemThemeDark'] as bool; + + final brightness = isSystemThemeDark ? Brightness.dark : Brightness.light; + final outline = await settings.getWidgetOutline(widgetId).color(brightness); final entry = drawEntryImage ? await _getWidgetEntry(widgetId, reuseEntry) : null; final painter = HomeWidgetPainter( @@ -50,7 +56,7 @@ Future> _drawWidget(dynamic args) async { final bytes = await painter.drawWidget( widthPx: widthPx, heightPx: heightPx, - outline: settings.getWidgetOutline(widgetId), + outline: outline, shape: settings.getWidgetShape(widgetId), ); return { diff --git a/lib/widgets/common/basic/color_indicator.dart b/lib/widgets/common/basic/color_indicator.dart index 34f31bcd4..85b5da001 100644 --- a/lib/widgets/common/basic/color_indicator.dart +++ b/lib/widgets/common/basic/color_indicator.dart @@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart'; class ColorIndicator extends StatelessWidget { final Color? value; + final Color? alternate; final Widget? child; static const double radius = 16; @@ -10,18 +11,33 @@ class ColorIndicator extends StatelessWidget { const ColorIndicator({ super.key, required this.value, + this.alternate, this.child, }); @override Widget build(BuildContext context) { const dimension = radius * 2; + + Gradient? gradient; + final _value = value; + final _alternate = alternate; + if (_value != null && _alternate != null && _alternate != _value) { + gradient = LinearGradient( + begin: AlignmentDirectional.topStart, + end: AlignmentDirectional.bottomEnd, + colors: [_value, _value, _alternate, _alternate], + stops: const [0, .5, .5, 1], + ); + } + return Container( height: dimension, width: dimension, decoration: BoxDecoration( - color: value, + color: _value, border: AvesBorder.border(context), + gradient: gradient, shape: BoxShape.circle, ), child: child, diff --git a/lib/widgets/home_widget.dart b/lib/widgets/home_widget.dart index 76e1288ba..38f6dca7f 100644 --- a/lib/widgets/home_widget.dart +++ b/lib/widgets/home_widget.dart @@ -14,9 +14,10 @@ class HomeWidgetPainter { final AvesEntry? entry; final double devicePixelRatio; + // do not use `AlignmentDirectional` as there is no `TextDirection` in context static const backgroundGradient = LinearGradient( - begin: AlignmentDirectional.bottomStart, - end: AlignmentDirectional.topEnd, + begin: Alignment.bottomLeft, + end: Alignment.topRight, colors: AColors.boraBoraGradient, ); diff --git a/lib/widgets/settings/home_widget_settings_page.dart b/lib/widgets/settings/home_widget_settings_page.dart index a2e49fe95..ca9225a6e 100644 --- a/lib/widgets/settings/home_widget_settings_page.dart +++ b/lib/widgets/settings/home_widget_settings_page.dart @@ -1,4 +1,6 @@ +import 'package:aves/model/device.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/settings/enums/widget_outline.dart'; import 'package:aves/model/settings/enums/widget_shape.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/widget_service.dart'; @@ -33,10 +35,11 @@ class HomeWidgetSettingsPage extends StatefulWidget { class _HomeWidgetSettingsPageState extends State { late WidgetShape _shape; - late Color? _outline; + late WidgetOutline _outline; late WidgetOpenPage _openPage; late WidgetDisplayedItem _displayedItem; late Set _collectionFilters; + Future>> _outlineColorsByBrightness = Future.value({}); int get widgetId => widget.widgetId; @@ -61,6 +64,24 @@ class _HomeWidgetSettingsPageState extends State { _openPage = settings.getWidgetOpenPage(widgetId); _displayedItem = settings.getWidgetDisplayedItem(widgetId); _collectionFilters = settings.getWidgetCollectionFilters(widgetId); + WidgetsBinding.instance.addPostFrameCallback((_) => _updateOutlineColors()); + } + + void _updateOutlineColors() { + _outlineColorsByBrightness = _loadOutlineColors(); + setState(() {}); + } + + Future>> _loadOutlineColors() async { + final byBrightness = >{}; + await Future.forEach(Brightness.values, (brightness) async { + final byOutline = {}; + await Future.forEach(WidgetOutline.values, (outline) async { + byOutline[outline] = await outline.color(brightness); + }); + byBrightness[brightness] = byOutline; + }); + return byBrightness; } @override @@ -71,58 +92,70 @@ class _HomeWidgetSettingsPageState extends State { title: Text(l10n.settingsWidgetPageTitle), ), body: SafeArea( - child: Column( - children: [ - Expanded( - child: ListView( - children: [ - _buildShapeSelector(), - ListTile( - title: Text(l10n.settingsWidgetShowOutline), - trailing: HomeWidgetOutlineSelector( - getter: () => _outline, - setter: (v) => setState(() => _outline = v), - ), + child: FutureBuilder>>( + future: _outlineColorsByBrightness, + builder: (context, snapshot) { + final outlineColorsByBrightness = snapshot.data; + if (outlineColorsByBrightness == null) return const SizedBox(); + + final effectiveOutlineColors = outlineColorsByBrightness[Theme.of(context).brightness]; + if (effectiveOutlineColors == null) return const SizedBox(); + + return Column( + children: [ + Expanded( + child: ListView( + children: [ + _buildShapeSelector(effectiveOutlineColors), + ListTile( + title: Text(l10n.settingsWidgetShowOutline), + trailing: HomeWidgetOutlineSelector( + getter: () => _outline, + setter: (v) => setState(() => _outline = v), + outlineColorsByBrightness: outlineColorsByBrightness, + ), + ), + SettingsSelectionListTile( + values: WidgetOpenPage.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => _openPage, + onSelection: (v) => setState(() => _openPage = v), + tileTitle: l10n.settingsWidgetOpenPage, + ), + SettingsSelectionListTile( + values: WidgetDisplayedItem.values, + getName: (context, v) => v.getName(context), + selector: (context, s) => _displayedItem, + onSelection: (v) => setState(() => _displayedItem = v), + tileTitle: l10n.settingsWidgetDisplayedItem, + ), + SettingsCollectionTile( + filters: _collectionFilters, + onSelection: (v) => setState(() => _collectionFilters = v), + ), + ], ), - SettingsSelectionListTile( - values: WidgetOpenPage.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => _openPage, - onSelection: (v) => setState(() => _openPage = v), - tileTitle: l10n.settingsWidgetOpenPage, + ), + const Divider(height: 0), + Padding( + padding: const EdgeInsets.all(8), + child: AvesOutlinedButton( + label: l10n.saveTooltip, + onPressed: () { + _saveSettings(); + WidgetService.configure(); + }, ), - SettingsSelectionListTile( - values: WidgetDisplayedItem.values, - getName: (context, v) => v.getName(context), - selector: (context, s) => _displayedItem, - onSelection: (v) => setState(() => _displayedItem = v), - tileTitle: l10n.settingsWidgetDisplayedItem, - ), - SettingsCollectionTile( - filters: _collectionFilters, - onSelection: (v) => setState(() => _collectionFilters = v), - ), - ], - ), - ), - const Divider(height: 0), - Padding( - padding: const EdgeInsets.all(8), - child: AvesOutlinedButton( - label: l10n.saveTooltip, - onPressed: () { - _saveSettings(); - WidgetService.configure(); - }, - ), - ), - ], + ), + ], + ); + }, ), ), ); } - Widget _buildShapeSelector() { + Widget _buildShapeSelector(Map outlineColors) { return Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), child: Wrap( @@ -143,7 +176,7 @@ class _HomeWidgetSettingsPageState extends State { height: 124, decoration: ShapeDecoration( gradient: selected ? gradient : deselectedGradient, - shape: _WidgetShapeBorder(_outline, shape), + shape: _WidgetShapeBorder(_outline, shape, outlineColors), ), ), ), @@ -169,12 +202,13 @@ class _HomeWidgetSettingsPageState extends State { } class _WidgetShapeBorder extends ShapeBorder { - final Color? outline; + final WidgetOutline outline; final WidgetShape shape; + final Map outlineColors; static const _devicePixelRatio = 1.0; - const _WidgetShapeBorder(this.outline, this.shape); + const _WidgetShapeBorder(this.outline, this.shape, this.outlineColors); @override EdgeInsetsGeometry get dimensions => EdgeInsets.zero; @@ -191,10 +225,11 @@ class _WidgetShapeBorder extends ShapeBorder { @override void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { - if (outline != null) { + final outlineColor = outlineColors[outline]; + if (outlineColor != null) { final path = shape.path(rect.size, _devicePixelRatio); canvas.clipPath(path); - HomeWidgetPainter.drawOutline(canvas, path, _devicePixelRatio, outline!); + HomeWidgetPainter.drawOutline(canvas, path, _devicePixelRatio, outlineColor); } } @@ -203,13 +238,15 @@ class _WidgetShapeBorder extends ShapeBorder { } class HomeWidgetOutlineSelector extends StatefulWidget { - final ValueGetter getter; - final ValueSetter setter; + final ValueGetter getter; + final ValueSetter setter; + final Map> outlineColorsByBrightness; const HomeWidgetOutlineSelector({ super.key, required this.getter, required this.setter, + required this.outlineColorsByBrightness, }); @override @@ -217,35 +254,40 @@ class HomeWidgetOutlineSelector extends StatefulWidget { } class _HomeWidgetOutlineSelectorState extends State { - static const List options = [ - null, - Colors.black, - Colors.white, - ]; - @override Widget build(BuildContext context) { return DropdownButtonHideUnderline( - child: DropdownButton( + child: DropdownButton( items: _buildItems(context), value: widget.getter(), onChanged: (selected) { - widget.setter(selected); + widget.setter(selected ?? WidgetOutline.none); setState(() {}); }, ), ); } - List> _buildItems(BuildContext context) { - return options.map((selected) { - return DropdownMenuItem( + List> _buildItems(BuildContext context) { + return supportedWidgetOutlines.map((selected) { + final lightColors = widget.outlineColorsByBrightness[Brightness.light]; + final darkColors = widget.outlineColorsByBrightness[Brightness.dark]; + return DropdownMenuItem( value: selected, child: ColorIndicator( - value: selected, - child: selected == null ? const Icon(AIcons.clear) : null, + value: lightColors?[selected], + alternate: darkColors?[selected], + child: lightColors?[selected] == null ? const Icon(AIcons.clear) : null, ), ); }).toList(); } + + List get supportedWidgetOutlines => [ + WidgetOutline.none, + WidgetOutline.black, + WidgetOutline.white, + WidgetOutline.systemBlackAndWhite, + if (device.isDynamicColorAvailable) WidgetOutline.systemDynamic, + ]; } diff --git a/plugins/aves_model/lib/src/settings/enums.dart b/plugins/aves_model/lib/src/settings/enums.dart index fecfac6ca..e8088d359 100644 --- a/plugins/aves_model/lib/src/settings/enums.dart +++ b/plugins/aves_model/lib/src/settings/enums.dart @@ -48,4 +48,6 @@ enum WidgetDisplayedItem { random, mostRecent } enum WidgetOpenPage { home, collection, viewer, updateWidget } +enum WidgetOutline { none, black, white, systemBlackAndWhite, systemDynamic } + enum WidgetShape { rrect, circle, heart }