#902 widget: outline color options according to device theme
This commit is contained in:
parent
ed250f9ccf
commit
3cef268138
9 changed files with 166 additions and 75 deletions
|
@ -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)
|
||||
|
||||
## <a id="v1.10.4"></a>[v1.10.4] - 2024-02-07
|
||||
|
|
|
@ -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)
|
||||
|
|
23
lib/model/settings/enums/widget_outline.dart
Normal file
23
lib/model/settings/enums/widget_outline.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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<Map<String, dynamic>> _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<Map<String, dynamic>> _drawWidget(dynamic args) async {
|
|||
final bytes = await painter.drawWidget(
|
||||
widthPx: widthPx,
|
||||
heightPx: heightPx,
|
||||
outline: settings.getWidgetOutline(widgetId),
|
||||
outline: outline,
|
||||
shape: settings.getWidgetShape(widgetId),
|
||||
);
|
||||
return {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
||||
|
|
|
@ -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<HomeWidgetSettingsPage> {
|
||||
late WidgetShape _shape;
|
||||
late Color? _outline;
|
||||
late WidgetOutline _outline;
|
||||
late WidgetOpenPage _openPage;
|
||||
late WidgetDisplayedItem _displayedItem;
|
||||
late Set<CollectionFilter> _collectionFilters;
|
||||
Future<Map<Brightness, Map<WidgetOutline, Color?>>> _outlineColorsByBrightness = Future.value({});
|
||||
|
||||
int get widgetId => widget.widgetId;
|
||||
|
||||
|
@ -61,6 +64,24 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
|||
_openPage = settings.getWidgetOpenPage(widgetId);
|
||||
_displayedItem = settings.getWidgetDisplayedItem(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
|
||||
|
@ -71,17 +92,27 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
|||
title: Text(l10n.settingsWidgetPageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
child: FutureBuilder<Map<Brightness, Map<WidgetOutline, Color?>>>(
|
||||
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(),
|
||||
_buildShapeSelector(effectiveOutlineColors),
|
||||
ListTile(
|
||||
title: Text(l10n.settingsWidgetShowOutline),
|
||||
trailing: HomeWidgetOutlineSelector(
|
||||
getter: () => _outline,
|
||||
setter: (v) => setState(() => _outline = v),
|
||||
outlineColorsByBrightness: outlineColorsByBrightness,
|
||||
),
|
||||
),
|
||||
SettingsSelectionListTile<WidgetOpenPage>(
|
||||
|
@ -117,12 +148,14 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
|||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShapeSelector() {
|
||||
Widget _buildShapeSelector(Map<WidgetOutline, Color?> outlineColors) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 8),
|
||||
child: Wrap(
|
||||
|
@ -143,7 +176,7 @@ class _HomeWidgetSettingsPageState extends State<HomeWidgetSettingsPage> {
|
|||
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<HomeWidgetSettingsPage> {
|
|||
}
|
||||
|
||||
class _WidgetShapeBorder extends ShapeBorder {
|
||||
final Color? outline;
|
||||
final WidgetOutline outline;
|
||||
final WidgetShape shape;
|
||||
final Map<WidgetOutline, Color?> 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<Color?> getter;
|
||||
final ValueSetter<Color?> setter;
|
||||
final ValueGetter<WidgetOutline> getter;
|
||||
final ValueSetter<WidgetOutline> setter;
|
||||
final Map<Brightness, Map<WidgetOutline, Color?>> 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<HomeWidgetOutlineSelector> {
|
||||
static const List<Color?> options = [
|
||||
null,
|
||||
Colors.black,
|
||||
Colors.white,
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton<Color?>(
|
||||
child: DropdownButton<WidgetOutline>(
|
||||
items: _buildItems(context),
|
||||
value: widget.getter(),
|
||||
onChanged: (selected) {
|
||||
widget.setter(selected);
|
||||
widget.setter(selected ?? WidgetOutline.none);
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<DropdownMenuItem<Color?>> _buildItems(BuildContext context) {
|
||||
return options.map((selected) {
|
||||
return DropdownMenuItem<Color?>(
|
||||
List<DropdownMenuItem<WidgetOutline>> _buildItems(BuildContext context) {
|
||||
return supportedWidgetOutlines.map((selected) {
|
||||
final lightColors = widget.outlineColorsByBrightness[Brightness.light];
|
||||
final darkColors = widget.outlineColorsByBrightness[Brightness.dark];
|
||||
return DropdownMenuItem<WidgetOutline>(
|
||||
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<WidgetOutline> get supportedWidgetOutlines => [
|
||||
WidgetOutline.none,
|
||||
WidgetOutline.black,
|
||||
WidgetOutline.white,
|
||||
WidgetOutline.systemBlackAndWhite,
|
||||
if (device.isDynamicColorAvailable) WidgetOutline.systemDynamic,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
Loading…
Reference in a new issue