diff --git a/lib/main.dart b/lib/main.dart index da0657f6a..7bbaa237c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:aves/model/settings.dart'; +import 'package:aves/widgets/common/data_providers/settings_provider.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/welcome_page.dart'; @@ -37,34 +38,38 @@ class _AvesAppState extends State { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Aves', - theme: ThemeData( - brightness: Brightness.dark, - accentColor: accentColor, - scaffoldBackgroundColor: Colors.grey[900], - buttonColor: accentColor, - toggleableActiveColor: accentColor, - tooltipTheme: TooltipThemeData( - verticalOffset: 32, - ), - appBarTheme: AppBarTheme( - textTheme: TextTheme( - headline6: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - fontFamily: 'Concourse Caps', + // place the settings provider above `MaterialApp` + // so it can be used during navigation transitions + return SettingsProvider( + child: MaterialApp( + title: 'Aves', + theme: ThemeData( + brightness: Brightness.dark, + accentColor: accentColor, + scaffoldBackgroundColor: Colors.grey[900], + buttonColor: accentColor, + toggleableActiveColor: accentColor, + tooltipTheme: TooltipThemeData( + verticalOffset: 32, + ), + appBarTheme: AppBarTheme( + textTheme: TextTheme( + headline6: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: 'Concourse Caps', + ), ), ), ), - ), - home: FutureBuilder( - future: _appSetup, - builder: (context, snapshot) { - if (snapshot.hasError) return Icon(AIcons.error); - if (snapshot.connectionState != ConnectionState.done) return Scaffold(); - return settings.hasAcceptedTerms ? HomePage() : WelcomePage(); - }, + home: FutureBuilder( + future: _appSetup, + builder: (context, snapshot) { + if (snapshot.hasError) return Icon(AIcons.error); + if (snapshot.connectionState != ConnectionState.done) return Scaffold(); + return settings.hasAcceptedTerms ? HomePage() : WelcomePage(); + }, + ), ), ); } diff --git a/lib/model/settings.dart b/lib/model/settings.dart index f6408fa3a..4f2056ac4 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -11,11 +11,9 @@ final Settings settings = Settings._private(); typedef SettingsCallback = void Function(String key, dynamic oldValue, dynamic newValue); -class Settings { +class Settings extends ChangeNotifier { static SharedPreferences _prefs; - final ObserverList _listeners = ObserverList(); - Settings._private(); // preferences @@ -28,6 +26,7 @@ class Settings { static const infoMapZoomKey = 'info_map_zoom'; static const launchPageKey = 'launch_page'; static const coordinateFormatKey = 'coordinates_format'; + static const svgBackgroundKey = 'svg_background'; Future init() async { _prefs = await SharedPreferences.getInstance(); @@ -37,26 +36,6 @@ class Settings { return _prefs.clear(); } - void addListener(SettingsCallback listener) => _listeners.add(listener); - - void removeListener(SettingsCallback listener) => _listeners.remove(listener); - - void notifyListeners(String key, dynamic oldValue, dynamic newValue) { - debugPrint('$runtimeType notifyListeners key=$key, old=$oldValue, new=$newValue'); - if (_listeners != null) { - final localListeners = _listeners.toList(); - for (final listener in localListeners) { - try { - if (_listeners.contains(listener)) { - listener(key, oldValue, newValue); - } - } catch (exception, stack) { - debugPrint('$runtimeType failed to notify listeners with exception=$exception\n$stack'); - } - } - } - } - String get catalogTimeZone => _prefs.getString(catalogTimeZoneKey) ?? ''; set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); @@ -93,6 +72,10 @@ class Settings { set coordinateFormat(CoordinateFormat newValue) => setAndNotify(coordinateFormatKey, newValue.toString()); + int get svgBackground => _prefs.getInt(svgBackgroundKey) ?? 0xFFFFFFFF; + + set svgBackground(int newValue) => setAndNotify(svgBackgroundKey, newValue); + // convenience methods // ignore: avoid_positional_boolean_parameters @@ -133,7 +116,7 @@ class Settings { _prefs.setBool(key, newValue); } if (oldValue != newValue) { - notifyListeners(key, oldValue, newValue); + notifyListeners(); } } } diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 902979c8e..32158651d 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -19,9 +19,6 @@ class Constants { ], ); - static const svgBackground = Colors.white; - static const svgColorFilter = ColorFilter.mode(svgBackground, BlendMode.dstOver); - static const List androidDependencies = [ Dependency( name: 'CWAC-Document', diff --git a/lib/widgets/album/grid/header_generic.dart b/lib/widgets/album/grid/header_generic.dart index 5dd1e3d44..19b92a58d 100644 --- a/lib/widgets/album/grid/header_generic.dart +++ b/lib/widgets/album/grid/header_generic.dart @@ -87,7 +87,8 @@ class SectionHeader extends StatelessWidget { // force a higher first line to match leading icon/selector dimension style: TextStyle(height: 2.3 * textScaleFactor), ), // 23 hair spaces match a width of 40.0 - if (hasTrailing) TextSpan(text: '\u200A' * 17), + if (hasTrailing) + TextSpan(text: '\u200A' * 17), TextSpan( text: text, style: Constants.titleTextStyle, diff --git a/lib/widgets/album/thumbnail/vector.dart b/lib/widgets/album/thumbnail/vector.dart index 02063b2ab..47a658fb2 100644 --- a/lib/widgets/album/thumbnail/vector.dart +++ b/lib/widgets/album/thumbnail/vector.dart @@ -1,8 +1,9 @@ import 'package:aves/model/image_entry.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/model/settings.dart'; import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; class ThumbnailVectorImage extends StatelessWidget { final ImageEntry entry; @@ -23,14 +24,20 @@ class ThumbnailVectorImage extends StatelessWidget { // so that `SvgPicture` doesn't get aligned by the `Stack` like the overlay icons width: extent, height: extent, - child: SvgPicture( - UriPicture( - uri: entry.uri, - mimeType: entry.mimeType, - colorFilter: Constants.svgColorFilter, - ), - width: extent, - height: extent, + child: Selector( + selector: (context, s) => s.svgBackground, + builder: (context, svgBackground, child) { + final colorFilter = ColorFilter.mode(Color(svgBackground), BlendMode.dstOver); + return SvgPicture( + UriPicture( + uri: entry.uri, + mimeType: entry.mimeType, + colorFilter: colorFilter, + ), + width: extent, + height: extent, + ); + }, ), ); return heroTag == null diff --git a/lib/widgets/common/borders.dart b/lib/widgets/common/borders.dart new file mode 100644 index 000000000..92c38bf78 --- /dev/null +++ b/lib/widgets/common/borders.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; + +class AvesCircleBorder { + static BoxBorder build(BuildContext context) { + final subPixel = MediaQuery.of(context).devicePixelRatio > 2; + return Border.all( + color: Colors.white30, + width: subPixel ? 0.5 : 1.0, + ); + } +} diff --git a/lib/widgets/common/data_providers/settings_provider.dart b/lib/widgets/common/data_providers/settings_provider.dart new file mode 100644 index 000000000..f5295c26c --- /dev/null +++ b/lib/widgets/common/data_providers/settings_provider.dart @@ -0,0 +1,17 @@ +import 'package:aves/model/settings.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class SettingsProvider extends StatelessWidget { + final Widget child; + + const SettingsProvider({@required this.child}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider.value( + value: settings, + child: child, + ); + } +} diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index cbd32dfea..eef223a91 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -1,5 +1,5 @@ import 'package:aves/model/image_entry.dart'; -import 'package:aves/utils/constants.dart'; +import 'package:aves/model/settings.dart'; import 'package:aves/widgets/album/empty.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart'; @@ -77,12 +77,13 @@ class ImageView extends StatelessWidget { Widget child; if (entry.isSvg) { + final colorFilter = ColorFilter.mode(Color(settings.svgBackground), BlendMode.dstOver); child = PhotoView.customChild( child: SvgPicture( UriPicture( uri: entry.uri, mimeType: entry.mimeType, - colorFilter: Constants.svgColorFilter, + colorFilter: colorFilter, ), placeholderBuilder: (context) => loadingBuilder(context, fastThumbnailProvider), ), diff --git a/lib/widgets/fullscreen/info/maps/common.dart b/lib/widgets/fullscreen/info/maps/common.dart index 107a74e34..3440a6665 100644 --- a/lib/widgets/fullscreen/info/maps/common.dart +++ b/lib/widgets/fullscreen/info/maps/common.dart @@ -1,6 +1,7 @@ import 'package:aves/model/settings.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/widgets/common/action_delegates/map_style_dialog.dart'; +import 'package:aves/widgets/common/borders.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart'; @@ -115,10 +116,10 @@ class MapOverlayButton extends StatelessWidget { return BlurredOval( child: Material( type: MaterialType.circle, - color: FullscreenOverlay.backgroundColor, + color: kOverlayBackgroundColor, child: Ink( decoration: BoxDecoration( - border: FullscreenOverlay.buildBorder(context), + border: AvesCircleBorder.build(context), shape: BoxShape.circle, ), child: IconButton( diff --git a/lib/widgets/fullscreen/overlay/bottom.dart b/lib/widgets/fullscreen/overlay/bottom.dart index 0cc39f525..506385831 100644 --- a/lib/widgets/fullscreen/overlay/bottom.dart +++ b/lib/widgets/fullscreen/overlay/bottom.dart @@ -80,7 +80,7 @@ class _FullscreenBottomOverlayState extends State { final overlayContentMaxWidth = mqWidth - viewPadding.horizontal - innerPadding.horizontal; return Container( - color: FullscreenOverlay.backgroundColor, + color: kOverlayBackgroundColor, padding: viewInsets + viewPadding.copyWith(top: 0), child: FutureBuilder( future: _detailLoader, diff --git a/lib/widgets/fullscreen/overlay/common.dart b/lib/widgets/fullscreen/overlay/common.dart index 38bf1c89c..c5830d2d5 100644 --- a/lib/widgets/fullscreen/overlay/common.dart +++ b/lib/widgets/fullscreen/overlay/common.dart @@ -1,17 +1,8 @@ +import 'package:aves/widgets/common/borders.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:flutter/material.dart'; -class FullscreenOverlay { - static const backgroundColor = Colors.black26; - - static BoxBorder buildBorder(BuildContext context) { - final subPixel = MediaQuery.of(context).devicePixelRatio > 2; - return Border.all( - color: Colors.white30, - width: subPixel ? 0.5 : 1.0, - ); - } -} +const kOverlayBackgroundColor = Colors.black26; class OverlayButton extends StatelessWidget { final Animation scale; @@ -26,10 +17,10 @@ class OverlayButton extends StatelessWidget { child: BlurredOval( child: Material( type: MaterialType.circle, - color: FullscreenOverlay.backgroundColor, + color: kOverlayBackgroundColor, child: Ink( decoration: BoxDecoration( - border: FullscreenOverlay.buildBorder(context), + border: AvesCircleBorder.build(context), shape: BoxShape.circle, ), child: child, diff --git a/lib/widgets/fullscreen/overlay/video.dart b/lib/widgets/fullscreen/overlay/video.dart index e8baef6ef..df66e30d9 100644 --- a/lib/widgets/fullscreen/overlay/video.dart +++ b/lib/widgets/fullscreen/overlay/video.dart @@ -4,6 +4,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/utils/time_utils.dart'; +import 'package:aves/widgets/common/borders.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; @@ -180,8 +181,8 @@ class VideoControlOverlayState extends State with SingleTic child: Container( padding: EdgeInsets.symmetric(vertical: 4, horizontal: 16) + EdgeInsets.only(bottom: 16), decoration: BoxDecoration( - color: FullscreenOverlay.backgroundColor, - border: FullscreenOverlay.buildBorder(context), + color: kOverlayBackgroundColor, + border: AvesCircleBorder.build(context), borderRadius: BorderRadius.circular(progressBarBorderRadius), ), child: Column( diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index f1f8f5891..2a32044c2 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -2,6 +2,7 @@ import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/settings/coordinate_format.dart'; import 'package:aves/widgets/settings/launch_page.dart'; +import 'package:aves/widgets/settings/svg_background.dart'; import 'package:flutter/material.dart'; class SettingsPage extends StatelessWidget { @@ -27,6 +28,14 @@ class SettingsPage extends StatelessWidget { Flexible(child: LaunchPageSelector()), ], ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('SVG background:'), + SizedBox(width: 8), + Flexible(child: SvgBackgroundSelector()), + ], + ), Row( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/widgets/settings/svg_background.dart b/lib/widgets/settings/svg_background.dart new file mode 100644 index 000000000..780ec1cb7 --- /dev/null +++ b/lib/widgets/settings/svg_background.dart @@ -0,0 +1,43 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/widgets/common/borders.dart'; +import 'package:flutter/material.dart'; + +class SvgBackgroundSelector extends StatefulWidget { + @override + _SvgBackgroundSelectorState createState() => _SvgBackgroundSelectorState(); +} + +class _SvgBackgroundSelectorState extends State { + @override + Widget build(BuildContext context) { + const radius = 24.0; + return DropdownButton( + items: [0xFFFFFFFF, 0xFF000000, 0x00000000].map((selected) { + return DropdownMenuItem( + value: selected, + child: Container( + height: radius, + width: radius, + decoration: BoxDecoration( + color: Color(selected), + border: AvesCircleBorder.build(context), + shape: BoxShape.circle, + ), + child: selected == 0 + ? Icon( + Icons.clear, + size: 20, + color: Colors.white30, + ) + : null, + ), + ); + }).toList(), + value: settings.svgBackground, + onChanged: (selected) { + settings.svgBackground = selected; + setState(() {}); + }, + ); + } +}