#902 widget: outline color options according to device theme

This commit is contained in:
Thibault Deckers 2024-02-13 00:03:25 +01:00
parent ed250f9ccf
commit 3cef268138
9 changed files with 166 additions and 75 deletions

View file

@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Viewer: prompt to show newly edited item - Viewer: prompt to show newly edited item
- Widget: outline color options according to device theme
- Catalan translation (thanks Marc Amorós) - Catalan translation (thanks Marc Amorós)
## <a id="v1.10.4"></a>[v1.10.4] - 2024-02-07 ## <a id="v1.10.4"></a>[v1.10.4] - 2024-02-07

View file

@ -87,6 +87,8 @@ class HomeWidgetProvider : AppWidgetProvider() {
val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo) val (widthPx, heightPx) = getWidgetSizePx(context, widgetInfo)
if (widthPx == 0 || heightPx == 0) return null 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) initFlutterEngine(context)
val messenger = flutterEngine!!.dartExecutor val messenger = flutterEngine!!.dartExecutor
val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL) val channel = MethodChannel(messenger, WIDGET_DRAW_CHANNEL)
@ -101,6 +103,7 @@ class HomeWidgetProvider : AppWidgetProvider() {
"devicePixelRatio" to getDevicePixelRatio(), "devicePixelRatio" to getDevicePixelRatio(),
"drawEntryImage" to drawEntryImage, "drawEntryImage" to drawEntryImage,
"reuseEntry" to reuseEntry, "reuseEntry" to reuseEntry,
"isSystemThemeDark" to isNightModeOn,
), object : MethodChannel.Result { ), object : MethodChannel.Result {
override fun success(result: Any?) { override fun success(result: Any?) {
cont.resume(result) cont.resume(result)

View file

@ -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?> 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);
}
}
}

View file

@ -274,12 +274,9 @@ class Settings with ChangeNotifier, SettingsAccess, AppSettings, DisplaySettings
// widget // widget
Color? getWidgetOutline(int widgetId) { WidgetOutline getWidgetOutline(int widgetId) => getEnumOrDefault('${SettingKeys.widgetOutlinePrefixKey}$widgetId', WidgetOutline.none, WidgetOutline.values);
final value = getInt('${SettingKeys.widgetOutlinePrefixKey}$widgetId');
return value != null ? Color(value) : null;
}
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); WidgetShape getWidgetShape(int widgetId) => getEnumOrDefault('${SettingKeys.widgetShapePrefixKey}$widgetId', SettingsDefaults.widgetShape, WidgetShape.values);

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/app_flavor.dart'; import 'package:aves/app_flavor.dart';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/sort.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/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/model/source/media_store_source.dart';
@ -20,6 +21,7 @@ void widgetMainCommon(AppFlavor flavor) async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
initPlatformServices(); initPlatformServices();
await settings.init(monitorPlatformSettings: false); await settings.init(monitorPlatformSettings: false);
await reportService.init();
_widgetDrawChannel.setMethodCallHandler((call) async { _widgetDrawChannel.setMethodCallHandler((call) async {
// widget settings may be modified in a different process after channel setup // widget settings may be modified in a different process after channel setup
@ -41,6 +43,10 @@ Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
final devicePixelRatio = args['devicePixelRatio'] as double; final devicePixelRatio = args['devicePixelRatio'] as double;
final drawEntryImage = args['drawEntryImage'] as bool; final drawEntryImage = args['drawEntryImage'] as bool;
final reuseEntry = args['reuseEntry'] 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 entry = drawEntryImage ? await _getWidgetEntry(widgetId, reuseEntry) : null;
final painter = HomeWidgetPainter( final painter = HomeWidgetPainter(
@ -50,7 +56,7 @@ Future<Map<String, dynamic>> _drawWidget(dynamic args) async {
final bytes = await painter.drawWidget( final bytes = await painter.drawWidget(
widthPx: widthPx, widthPx: widthPx,
heightPx: heightPx, heightPx: heightPx,
outline: settings.getWidgetOutline(widgetId), outline: outline,
shape: settings.getWidgetShape(widgetId), shape: settings.getWidgetShape(widgetId),
); );
return { return {

View file

@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
class ColorIndicator extends StatelessWidget { class ColorIndicator extends StatelessWidget {
final Color? value; final Color? value;
final Color? alternate;
final Widget? child; final Widget? child;
static const double radius = 16; static const double radius = 16;
@ -10,18 +11,33 @@ class ColorIndicator extends StatelessWidget {
const ColorIndicator({ const ColorIndicator({
super.key, super.key,
required this.value, required this.value,
this.alternate,
this.child, this.child,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const dimension = radius * 2; 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( return Container(
height: dimension, height: dimension,
width: dimension, width: dimension,
decoration: BoxDecoration( decoration: BoxDecoration(
color: value, color: _value,
border: AvesBorder.border(context), border: AvesBorder.border(context),
gradient: gradient,
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: child, child: child,

View file

@ -14,9 +14,10 @@ class HomeWidgetPainter {
final AvesEntry? entry; final AvesEntry? entry;
final double devicePixelRatio; final double devicePixelRatio;
// do not use `AlignmentDirectional` as there is no `TextDirection` in context
static const backgroundGradient = LinearGradient( static const backgroundGradient = LinearGradient(
begin: AlignmentDirectional.bottomStart, begin: Alignment.bottomLeft,
end: AlignmentDirectional.topEnd, end: Alignment.topRight,
colors: AColors.boraBoraGradient, colors: AColors.boraBoraGradient,
); );

View file

@ -1,4 +1,6 @@
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/filters.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/enums/widget_shape.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/widget_service.dart'; import 'package:aves/services/widget_service.dart';
@ -33,10 +35,11 @@ class HomeWidgetSettingsPage extends StatefulWidget {
class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> { class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
late WidgetShape _shape; late WidgetShape _shape;
late Color? _outline; late WidgetOutline _outline;
late WidgetOpenPage _openPage; late WidgetOpenPage _openPage;
late WidgetDisplayedItem _displayedItem; late WidgetDisplayedItem _displayedItem;
late Set<CollectionFilter> _collectionFilters; late Set<CollectionFilter> _collectionFilters;
Future<Map<Brightness, Map<WidgetOutline, Color?>>> _outlineColorsByBrightness = Future.value({});
int get widgetId => widget.widgetId; int get widgetId => widget.widgetId;
@ -61,6 +64,24 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
_openPage = settings.getWidgetOpenPage(widgetId); _openPage = settings.getWidgetOpenPage(widgetId);
_displayedItem = settings.getWidgetDisplayedItem(widgetId); _displayedItem = settings.getWidgetDisplayedItem(widgetId);
_collectionFilters = settings.getWidgetCollectionFilters(widgetId); _collectionFilters = settings.getWidgetCollectionFilters(widgetId);
WidgetsBinding.instance.addPostFrameCallback((_) => _updateOutlineColors());
}
void _updateOutlineColors() {
_outlineColorsByBrightness = _loadOutlineColors();
setState(() {});
}
Future<Map<Brightness, Map<WidgetOutline, Color?>>> _loadOutlineColors() async {
final byBrightness = <Brightness, Map<WidgetOutline, Color?>>{};
await Future.forEach(Brightness.values, (brightness) async {
final byOutline = <WidgetOutline, Color?>{};
await Future.forEach(WidgetOutline.values, (outline) async {
byOutline[outline] = await outline.color(brightness);
});
byBrightness[brightness] = byOutline;
});
return byBrightness;
} }
@override @override
@ -71,58 +92,70 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
title: Text(l10n.settingsWidgetPageTitle), title: Text(l10n.settingsWidgetPageTitle),
), ),
body: SafeArea( body: SafeArea(
child: Column( child: FutureBuilder<Map<Brightness, Map<WidgetOutline, Color?>>>(
children: [ future: _outlineColorsByBrightness,
Expanded( builder: (context, snapshot) {
child: ListView( final outlineColorsByBrightness = snapshot.data;
children: [ if (outlineColorsByBrightness == null) return const SizedBox();
_buildShapeSelector(),
ListTile( final effectiveOutlineColors = outlineColorsByBrightness[Theme.of(context).brightness];
title: Text(l10n.settingsWidgetShowOutline), if (effectiveOutlineColors == null) return const SizedBox();
trailing: HomeWidgetOutlineSelector(
getter: () => _outline, return Column(
setter: (v) => setState(() => _outline = v), children: [
), Expanded(
child: ListView(
children: [
_buildShapeSelector(effectiveOutlineColors),
ListTile(
title: Text(l10n.settingsWidgetShowOutline),
trailing: HomeWidgetOutlineSelector(
getter: () => _outline,
setter: (v) => setState(() => _outline = v),
outlineColorsByBrightness: outlineColorsByBrightness,
),
),
SettingsSelectionListTile<WidgetOpenPage>(
values: WidgetOpenPage.values,
getName: (context, v) => v.getName(context),
selector: (context, s) => _openPage,
onSelection: (v) => setState(() => _openPage = v),
tileTitle: l10n.settingsWidgetOpenPage,
),
SettingsSelectionListTile<WidgetDisplayedItem>(
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<WidgetOpenPage>( ),
values: WidgetOpenPage.values, const Divider(height: 0),
getName: (context, v) => v.getName(context), Padding(
selector: (context, s) => _openPage, padding: const EdgeInsets.all(8),
onSelection: (v) => setState(() => _openPage = v), child: AvesOutlinedButton(
tileTitle: l10n.settingsWidgetOpenPage, label: l10n.saveTooltip,
onPressed: () {
_saveSettings();
WidgetService.configure();
},
), ),
SettingsSelectionListTile<WidgetDisplayedItem>( ),
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<WidgetOutline, Color?> outlineColors) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
child: Wrap( child: Wrap(
@ -143,7 +176,7 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
height: 124, height: 124,
decoration: ShapeDecoration( decoration: ShapeDecoration(
gradient: selected ? gradient : deselectedGradient, gradient: selected ? gradient : deselectedGradient,
shape: _WidgetShapeBorder(_outline, shape), shape: _WidgetShapeBorder(_outline, shape, outlineColors),
), ),
), ),
), ),
@ -169,12 +202,13 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
} }
class _WidgetShapeBorder extends ShapeBorder { class _WidgetShapeBorder extends ShapeBorder {
final Color? outline; final WidgetOutline outline;
final WidgetShape shape; final WidgetShape shape;
final Map<WidgetOutline, Color?> outlineColors;
static const _devicePixelRatio = 1.0; static const _devicePixelRatio = 1.0;
const _WidgetShapeBorder(this.outline, this.shape); const _WidgetShapeBorder(this.outline, this.shape, this.outlineColors);
@override @override
EdgeInsetsGeometry get dimensions => EdgeInsets.zero; EdgeInsetsGeometry get dimensions => EdgeInsets.zero;
@ -191,10 +225,11 @@ class _WidgetShapeBorder extends ShapeBorder {
@override @override
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) { 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); final path = shape.path(rect.size, _devicePixelRatio);
canvas.clipPath(path); 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 { class HomeWidgetOutlineSelector extends StatefulWidget {
final ValueGetter<Color?> getter; final ValueGetter<WidgetOutline> getter;
final ValueSetter<Color?> setter; final ValueSetter<WidgetOutline> setter;
final Map<Brightness, Map<WidgetOutline, Color?>> outlineColorsByBrightness;
const HomeWidgetOutlineSelector({ const HomeWidgetOutlineSelector({
super.key, super.key,
required this.getter, required this.getter,
required this.setter, required this.setter,
required this.outlineColorsByBrightness,
}); });
@override @override
@ -217,35 +254,40 @@ class HomeWidgetOutlineSelector extends StatefulWidget {
} }
class _HomeWidgetOutlineSelectorState extends State<HomeWidgetOutlineSelector> { class _HomeWidgetOutlineSelectorState extends State<HomeWidgetOutlineSelector> {
static const List<Color?> options = [
null,
Colors.black,
Colors.white,
];
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return DropdownButtonHideUnderline( return DropdownButtonHideUnderline(
child: DropdownButton<Color?>( child: DropdownButton<WidgetOutline>(
items: _buildItems(context), items: _buildItems(context),
value: widget.getter(), value: widget.getter(),
onChanged: (selected) { onChanged: (selected) {
widget.setter(selected); widget.setter(selected ?? WidgetOutline.none);
setState(() {}); setState(() {});
}, },
), ),
); );
} }
List<DropdownMenuItem<Color?>> _buildItems(BuildContext context) { List<DropdownMenuItem<WidgetOutline>> _buildItems(BuildContext context) {
return options.map((selected) { return supportedWidgetOutlines.map((selected) {
return DropdownMenuItem<Color?>( final lightColors = widget.outlineColorsByBrightness[Brightness.light];
final darkColors = widget.outlineColorsByBrightness[Brightness.dark];
return DropdownMenuItem<WidgetOutline>(
value: selected, value: selected,
child: ColorIndicator( child: ColorIndicator(
value: selected, value: lightColors?[selected],
child: selected == null ? const Icon(AIcons.clear) : null, alternate: darkColors?[selected],
child: lightColors?[selected] == null ? const Icon(AIcons.clear) : null,
), ),
); );
}).toList(); }).toList();
} }
List<WidgetOutline> get supportedWidgetOutlines => [
WidgetOutline.none,
WidgetOutline.black,
WidgetOutline.white,
WidgetOutline.systemBlackAndWhite,
if (device.isDynamicColorAvailable) WidgetOutline.systemDynamic,
];
} }

View file

@ -48,4 +48,6 @@ enum WidgetDisplayedItem { random, mostRecent }
enum WidgetOpenPage { home, collection, viewer, updateWidget } enum WidgetOpenPage { home, collection, viewer, updateWidget }
enum WidgetOutline { none, black, white, systemBlackAndWhite, systemDynamic }
enum WidgetShape { rrect, circle, heart } enum WidgetShape { rrect, circle, heart }