diff --git a/lib/main.dart b/lib/main.dart index 7bbaa237c..ed07ea752 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'package:aves/widgets/welcome_page.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:overlay_support/overlay_support.dart'; void main() { // HttpClient.enableTimelineLogging = true; // enable network traffic logging @@ -41,34 +42,36 @@ class _AvesAppState extends State { // 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', + child: OverlaySupport( + 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 f99b3e77c..97b2116e2 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -16,18 +16,27 @@ class Settings extends ChangeNotifier { Settings._private(); - // preferences + // app + static const hasAcceptedTermsKey = 'has_accepted_terms'; + static const mustBackTwiceToExitKey = 'must_back_twice_to_exit'; + static const homePageKey = 'home_page'; static const catalogTimeZoneKey = 'catalog_time_zone'; + + // collection static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionSortFactorKey = 'collection_sort_factor'; static const collectionTileExtentKey = 'collection_tile_extent'; - static const hasAcceptedTermsKey = 'has_accepted_terms'; + + // filter grids + static const albumSortFactorKey = 'album_sort_factor'; + + // info static const infoMapStyleKey = 'info_map_style'; static const infoMapZoomKey = 'info_map_zoom'; - static const launchPageKey = 'launch_page'; static const coordinateFormatKey = 'coordinates_format'; + + // rendering static const svgBackgroundKey = 'svg_background'; - static const albumSortFactorKey = 'album_sort_factor'; Future init() async { _prefs = await SharedPreferences.getInstance(); @@ -37,10 +46,26 @@ class Settings extends ChangeNotifier { return _prefs.clear(); } + // app + + bool get hasAcceptedTerms => getBoolOrDefault(hasAcceptedTermsKey, false); + + set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue); + + bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true); + + set mustBackTwiceToExit(bool newValue) => setAndNotify(mustBackTwiceToExitKey, newValue); + + HomePageSetting get homePage => getEnumOrDefault(homePageKey, HomePageSetting.collection, HomePageSetting.values); + + set homePage(HomePageSetting newValue) => setAndNotify(homePageKey, newValue.toString()); + String get catalogTimeZone => _prefs.getString(catalogTimeZoneKey) ?? ''; set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); + // collection + EntryGroupFactor get collectionGroupFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values); set collectionGroupFactor(EntryGroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString()); @@ -53,6 +78,14 @@ class Settings extends ChangeNotifier { set collectionTileExtent(double newValue) => setAndNotify(collectionTileExtentKey, newValue); + // filter grids + + ChipSortFactor get albumSortFactor => getEnumOrDefault(albumSortFactorKey, ChipSortFactor.name, ChipSortFactor.values); + + set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString()); + + // info + EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); @@ -61,26 +94,16 @@ class Settings extends ChangeNotifier { set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); - bool get hasAcceptedTerms => getBoolOrDefault(hasAcceptedTermsKey, false); - - set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue); - - LaunchPage get launchPage => getEnumOrDefault(launchPageKey, LaunchPage.collection, LaunchPage.values); - - set launchPage(LaunchPage newValue) => setAndNotify(launchPageKey, newValue.toString()); - CoordinateFormat get coordinateFormat => getEnumOrDefault(coordinateFormatKey, CoordinateFormat.dms, CoordinateFormat.values); set coordinateFormat(CoordinateFormat newValue) => setAndNotify(coordinateFormatKey, newValue.toString()); + // rendering + int get svgBackground => _prefs.getInt(svgBackgroundKey) ?? 0xFFFFFFFF; set svgBackground(int newValue) => setAndNotify(svgBackgroundKey, newValue); - ChipSortFactor get albumSortFactor => getEnumOrDefault(albumSortFactorKey, ChipSortFactor.date, ChipSortFactor.values); - - set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString()); - // convenience methods // ignore: avoid_positional_boolean_parameters @@ -126,14 +149,14 @@ class Settings extends ChangeNotifier { } } -enum LaunchPage { collection, albums } +enum HomePageSetting { collection, albums } -extension ExtraLaunchPage on LaunchPage { +extension ExtraHomePageSetting on HomePageSetting { String get name { switch (this) { - case LaunchPage.collection: + case HomePageSetting.collection: return 'All Media'; - case LaunchPage.albums: + case HomePageSetting.albums: return 'Albums'; default: return toString(); diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 32158651d..658ff6763 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -10,13 +10,6 @@ class Constants { color: Color(0xFFEEEEEE), fontSize: 20, fontFamily: 'Concourse Caps', - shadows: [ - Shadow( - offset: Offset(0, 2), - blurRadius: 3, - color: Color(0xFF212121), - ), - ], ); static const List androidDependencies = [ diff --git a/lib/utils/durations.dart b/lib/utils/durations.dart index 3bb1e1a4d..f4d7c2a26 100644 --- a/lib/utils/durations.dart +++ b/lib/utils/durations.dart @@ -31,4 +31,5 @@ class Durations { static const collectionScalingCompleteNotificationDelay = Duration(milliseconds: 300); static const videoProgressTimerInterval = Duration(milliseconds: 300); static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation; + static const doubleBackTimerDelay = Duration(milliseconds: 1000); } diff --git a/lib/widgets/album/app_bar.dart b/lib/widgets/album/app_bar.dart index fc370dfcc..bb7d9fade 100644 --- a/lib/widgets/album/app_bar.dart +++ b/lib/widgets/album/app_bar.dart @@ -7,10 +7,9 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/album/filter_bar.dart'; import 'package:aves/widgets/album/search/search_delegate.dart'; -import 'package:aves/widgets/common/action_delegates/collection_group_dialog.dart'; -import 'package:aves/widgets/common/action_delegates/collection_sort_dialog.dart'; import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart'; +import 'package:aves/widgets/common/aves_selection_dialog.dart'; import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/icons.dart'; @@ -296,23 +295,40 @@ class _CollectionAppBarState extends State with SingleTickerPr unawaited(_goToStats()); break; case CollectionAction.group: - final factor = await showDialog( + final value = await showDialog( context: context, - builder: (context) => CollectionGroupDialog(), + builder: (context) => AvesSelectionDialog( + initialValue: settings.collectionGroupFactor, + options: { + EntryGroupFactor.album: 'By album', + EntryGroupFactor.month: 'By month', + EntryGroupFactor.day: 'By day', + EntryGroupFactor.none: 'Do not group', + }, + title: 'Group', + ), ); - if (factor != null) { - settings.collectionGroupFactor = factor; - collection.group(factor); + if (value != null) { + settings.collectionGroupFactor = value; + collection.group(value); } break; case CollectionAction.sort: - final factor = await showDialog( + final value = await showDialog( context: context, - builder: (context) => CollectionSortDialog(initialValue: settings.collectionSortFactor), + builder: (context) => AvesSelectionDialog( + initialValue: settings.collectionSortFactor, + options: { + EntrySortFactor.date: 'By date', + EntrySortFactor.size: 'By size', + EntrySortFactor.name: 'By album & file name', + }, + title: 'Sort', + ), ); - if (factor != null) { - settings.collectionSortFactor = factor; - collection.sort(factor); + if (value != null) { + settings.collectionSortFactor = value; + collection.sort(value); } break; } diff --git a/lib/widgets/album/collection_page.dart b/lib/widgets/album/collection_page.dart index d1c942e0a..7349dc8ef 100644 --- a/lib/widgets/album/collection_page.dart +++ b/lib/widgets/album/collection_page.dart @@ -2,6 +2,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/app_drawer.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/double_back_pop.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -26,7 +27,9 @@ class CollectionPage extends StatelessWidget { } return SynchronousFuture(true); }, - child: ThumbnailCollection(), + child: DoubleBackPopScope( + child: ThumbnailCollection(), + ), ), drawer: AppDrawer( source: collection.source, diff --git a/lib/widgets/common/action_delegates/chip_sort_dialog.dart b/lib/widgets/common/action_delegates/chip_sort_dialog.dart deleted file mode 100644 index 9308343b4..000000000 --- a/lib/widgets/common/action_delegates/chip_sort_dialog.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'package:aves/model/source/enums.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../dialog.dart'; - -class ChipSortDialog extends StatefulWidget { - final ChipSortFactor initialValue; - - const ChipSortDialog({@required this.initialValue}); - - @override - _ChipSortDialogState createState() => _ChipSortDialogState(); -} - -class _ChipSortDialogState extends State { - ChipSortFactor _selectedSort; - - @override - void initState() { - super.initState(); - _selectedSort = widget.initialValue; - } - - @override - Widget build(BuildContext context) { - return AvesDialog( - title: 'Sort', - scrollableContent: [ - _buildRadioListTile(ChipSortFactor.date, 'By date'), - _buildRadioListTile(ChipSortFactor.name, 'By name'), - ], - actions: [ - FlatButton( - onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), - ), - FlatButton( - key: Key('apply-button'), - onPressed: () => Navigator.pop(context, _selectedSort), - child: Text('Apply'.toUpperCase()), - ), - ], - ); - } - - Widget _buildRadioListTile(ChipSortFactor value, String title) => RadioListTile( - key: Key(value.toString()), - value: value, - groupValue: _selectedSort, - onChanged: (sort) => setState(() => _selectedSort = sort), - title: Text( - title, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - ); -} diff --git a/lib/widgets/common/action_delegates/collection_group_dialog.dart b/lib/widgets/common/action_delegates/collection_group_dialog.dart deleted file mode 100644 index 66f898734..000000000 --- a/lib/widgets/common/action_delegates/collection_group_dialog.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:aves/model/settings.dart'; -import 'package:aves/model/source/enums.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../dialog.dart'; - -class CollectionGroupDialog extends StatefulWidget { - @override - _CollectionGroupDialogState createState() => _CollectionGroupDialogState(); -} - -class _CollectionGroupDialogState extends State { - EntryGroupFactor _selectedGroup; - - @override - void initState() { - super.initState(); - _selectedGroup = settings.collectionGroupFactor; - } - - @override - Widget build(BuildContext context) { - return AvesDialog( - title: 'Group', - scrollableContent: [ - _buildRadioListTile(EntryGroupFactor.album, 'By album'), - _buildRadioListTile(EntryGroupFactor.month, 'By month'), - _buildRadioListTile(EntryGroupFactor.day, 'By day'), - _buildRadioListTile(EntryGroupFactor.none, 'Do not group'), - ], - actions: [ - FlatButton( - onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), - ), - FlatButton( - key: Key('apply-button'), - onPressed: () => Navigator.pop(context, _selectedGroup), - child: Text('Apply'.toUpperCase()), - ), - ], - ); - } - - Widget _buildRadioListTile(EntryGroupFactor value, String title) => RadioListTile( - key: Key(value.toString()), - value: value, - groupValue: _selectedGroup, - onChanged: (group) => setState(() => _selectedGroup = group), - title: Text( - title, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - ); -} diff --git a/lib/widgets/common/action_delegates/collection_sort_dialog.dart b/lib/widgets/common/action_delegates/collection_sort_dialog.dart deleted file mode 100644 index 302ef203b..000000000 --- a/lib/widgets/common/action_delegates/collection_sort_dialog.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:aves/model/source/enums.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../dialog.dart'; - -class CollectionSortDialog extends StatefulWidget { - final EntrySortFactor initialValue; - - const CollectionSortDialog({@required this.initialValue}); - - @override - _CollectionSortDialogState createState() => _CollectionSortDialogState(); -} - -class _CollectionSortDialogState extends State { - EntrySortFactor _selectedSort; - - @override - void initState() { - super.initState(); - _selectedSort = widget.initialValue; - } - - @override - Widget build(BuildContext context) { - return AvesDialog( - title: 'Sort', - scrollableContent: [ - _buildRadioListTile(EntrySortFactor.date, 'By date'), - _buildRadioListTile(EntrySortFactor.size, 'By size'), - _buildRadioListTile(EntrySortFactor.name, 'By album & file name'), - ], - actions: [ - FlatButton( - onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), - ), - FlatButton( - key: Key('apply-button'), - onPressed: () => Navigator.pop(context, _selectedSort), - child: Text('Apply'.toUpperCase()), - ), - ], - ); - } - - Widget _buildRadioListTile(EntrySortFactor value, String title) => RadioListTile( - key: Key(value.toString()), - value: value, - groupValue: _selectedSort, - onChanged: (sort) => setState(() => _selectedSort = sort), - title: Text( - title, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - ); -} diff --git a/lib/widgets/common/action_delegates/create_album_dialog.dart b/lib/widgets/common/action_delegates/create_album_dialog.dart index a32dfcf53..801255497 100644 --- a/lib/widgets/common/action_delegates/create_album_dialog.dart +++ b/lib/widgets/common/action_delegates/create_album_dialog.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:path/path.dart'; -import '../dialog.dart'; +import '../aves_dialog.dart'; class CreateAlbumDialog extends StatefulWidget { @override diff --git a/lib/widgets/common/action_delegates/entry_action_delegate.dart b/lib/widgets/common/action_delegates/entry_action_delegate.dart index ad52a6441..368acd591 100644 --- a/lib/widgets/common/action_delegates/entry_action_delegate.dart +++ b/lib/widgets/common/action_delegates/entry_action_delegate.dart @@ -7,7 +7,7 @@ import 'package:aves/services/image_file_service.dart'; import 'package:aves/widgets/common/action_delegates/feedback.dart'; import 'package:aves/widgets/common/action_delegates/permission_aware.dart'; import 'package:aves/widgets/common/action_delegates/rename_entry_dialog.dart'; -import 'package:aves/widgets/common/dialog.dart'; +import 'package:aves/widgets/common/aves_dialog.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/image_providers/uri_image_provider.dart'; import 'package:aves/widgets/fullscreen/debug.dart'; diff --git a/lib/widgets/common/action_delegates/map_style_dialog.dart b/lib/widgets/common/action_delegates/map_style_dialog.dart deleted file mode 100644 index a9b0fd3d2..000000000 --- a/lib/widgets/common/action_delegates/map_style_dialog.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:aves/model/settings.dart'; -import 'package:aves/widgets/fullscreen/info/location_section.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; - -import '../dialog.dart'; - -class MapStyleDialog extends StatefulWidget { - @override - _MapStyleDialogState createState() => _MapStyleDialogState(); -} - -class _MapStyleDialogState extends State { - EntryMapStyle _selectedStyle; - - @override - void initState() { - super.initState(); - _selectedStyle = settings.infoMapStyle; - } - - @override - Widget build(BuildContext context) { - return AvesDialog( - title: 'Map Style', - scrollableContent: EntryMapStyle.values.map((style) => _buildRadioListTile(style, style.name)).toList(), - actions: [ - FlatButton( - onPressed: () => Navigator.pop(context), - child: Text('Cancel'.toUpperCase()), - ), - FlatButton( - onPressed: () => Navigator.pop(context, _selectedStyle), - child: Text('Apply'.toUpperCase()), - ), - ], - ); - } - - Widget _buildRadioListTile(EntryMapStyle style, String title) => RadioListTile( - value: style, - groupValue: _selectedStyle, - onChanged: (style) => setState(() => _selectedStyle = style), - title: Text( - title, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - ); -} diff --git a/lib/widgets/common/action_delegates/permission_aware.dart b/lib/widgets/common/action_delegates/permission_aware.dart index d1df692b5..b020c627e 100644 --- a/lib/widgets/common/action_delegates/permission_aware.dart +++ b/lib/widgets/common/action_delegates/permission_aware.dart @@ -2,7 +2,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:flutter/material.dart'; -import '../dialog.dart'; +import '../aves_dialog.dart'; mixin PermissionAwareMixin { Future checkStoragePermission(BuildContext context, Iterable entries) { diff --git a/lib/widgets/common/action_delegates/rename_entry_dialog.dart b/lib/widgets/common/action_delegates/rename_entry_dialog.dart index 33cee0309..4e3106a47 100644 --- a/lib/widgets/common/action_delegates/rename_entry_dialog.dart +++ b/lib/widgets/common/action_delegates/rename_entry_dialog.dart @@ -1,7 +1,8 @@ import 'package:aves/model/image_entry.dart'; -import 'package:aves/widgets/common/dialog.dart'; import 'package:flutter/material.dart'; +import '../aves_dialog.dart'; + class RenameEntryDialog extends StatefulWidget { final ImageEntry entry; diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 24faeadee..b3bf36570 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -14,7 +14,7 @@ import 'package:aves/widgets/album/empty.dart'; import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart'; import 'package:aves/widgets/common/action_delegates/feedback.dart'; import 'package:aves/widgets/common/action_delegates/permission_aware.dart'; -import 'package:aves/widgets/common/dialog.dart'; +import 'package:aves/widgets/common/aves_dialog.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/filter_grids/filter_grid_page.dart'; diff --git a/lib/widgets/common/dialog.dart b/lib/widgets/common/aves_dialog.dart similarity index 100% rename from lib/widgets/common/dialog.dart rename to lib/widgets/common/aves_dialog.dart diff --git a/lib/widgets/common/aves_selection_dialog.dart b/lib/widgets/common/aves_selection_dialog.dart new file mode 100644 index 000000000..4165183ba --- /dev/null +++ b/lib/widgets/common/aves_selection_dialog.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import 'aves_dialog.dart'; + +class AvesSelectionDialog extends StatefulWidget { + final T initialValue; + final Map options; + final String title; + + const AvesSelectionDialog({ + @required this.initialValue, + @required this.options, + @required this.title, + }); + + @override + _AvesSelectionDialogState createState() => _AvesSelectionDialogState(); +} + +class _AvesSelectionDialogState extends State { + T _selectedValue; + + @override + void initState() { + super.initState(); + _selectedValue = widget.initialValue; + } + + @override + Widget build(BuildContext context) { + return AvesDialog( + title: widget.title, + scrollableContent: widget.options.entries.map((kv) => _buildRadioListTile(kv.key, kv.value)).toList(), + actions: [ + FlatButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancel'.toUpperCase()), + ), + FlatButton( + key: Key('apply-button'), + onPressed: () => Navigator.pop(context, _selectedValue), + child: Text('Apply'.toUpperCase()), + ), + ], + ); + } + + Widget _buildRadioListTile(T value, String title) => RadioListTile( + key: Key(value.toString()), + value: value, + groupValue: _selectedValue, + onChanged: (v) => setState(() => _selectedValue = v), + title: Text( + title, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + ); +} diff --git a/lib/widgets/common/double_back_pop.dart b/lib/widgets/common/double_back_pop.dart new file mode 100644 index 000000000..583a9563f --- /dev/null +++ b/lib/widgets/common/double_back_pop.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'package:aves/model/settings.dart'; +import 'package:aves/utils/durations.dart'; +import 'package:aves/widgets/common/action_delegates/feedback.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:overlay_support/overlay_support.dart'; + +class DoubleBackPopScope extends StatefulWidget { + final Widget child; + + const DoubleBackPopScope({ + @required this.child, + }); + + @override + _DoubleBackPopScopeState createState() => _DoubleBackPopScopeState(); +} + +class _DoubleBackPopScopeState extends State with FeedbackMixin { + bool _backOnce = false; + Timer _backTimer; + + @override + void dispose() { + _stopBackTimer(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: () { + if (!Navigator.of(context).canPop() && settings.mustBackTwiceToExit && !_backOnce) { + _backOnce = true; + _stopBackTimer(); + _backTimer = Timer(Durations.doubleBackTimerDelay, () => _backOnce = false); + toast( + 'Tap “back” again to exit.', + duration: Durations.doubleBackTimerDelay, + ); + return SynchronousFuture(false); + } + return SynchronousFuture(true); + }, + child: widget.child, + ); + } + + void _stopBackTimer() { + _backTimer?.cancel(); + } +} diff --git a/lib/widgets/common/highlight_title.dart b/lib/widgets/common/highlight_title.dart new file mode 100644 index 000000000..1fbb5cab7 --- /dev/null +++ b/lib/widgets/common/highlight_title.dart @@ -0,0 +1,43 @@ +import 'package:aves/utils/color_utils.dart'; +import 'package:aves/widgets/common/fx/highlight_decoration.dart'; +import 'package:flutter/material.dart'; + +class HighlightTitle extends StatelessWidget { + final String name; + final double fontSize; + + const HighlightTitle( + this.name, { + this.fontSize = 20, + }); + + @override + Widget build(BuildContext context) { + return Align( + alignment: AlignmentDirectional.centerStart, + child: Container( + decoration: HighlightDecoration( + color: stringToColor(name), + ), + margin: EdgeInsets.symmetric(vertical: 4.0), + child: Text( + name, + style: TextStyle( + shadows: [ + Shadow( + color: Colors.black, + offset: Offset(1, 1), + blurRadius: 2, + ) + ], + fontSize: fontSize, + fontFamily: 'Concourse Caps', + ), + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + ), + ); + } +} diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index 67078e0d8..b696941e3 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -7,7 +7,7 @@ import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/album/empty.dart'; -import 'package:aves/widgets/common/action_delegates/chip_sort_dialog.dart'; +import 'package:aves/widgets/common/aves_selection_dialog.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/filter_grids/filter_grid_page.dart'; @@ -117,7 +117,14 @@ class AlbumListPage extends StatelessWidget { case ChipAction.sort: final factor = await showDialog( context: context, - builder: (context) => ChipSortDialog(initialValue: settings.albumSortFactor), + builder: (context) => AvesSelectionDialog( + initialValue: settings.albumSortFactor, + options: { + ChipSortFactor.date: 'By date', + ChipSortFactor.name: 'By name', + }, + title: 'Sort', + ), ); if (factor != null) { settings.albumSortFactor = factor; diff --git a/lib/widgets/fullscreen/info/maps/common.dart b/lib/widgets/fullscreen/info/maps/common.dart index 3440a6665..7cbb51131 100644 --- a/lib/widgets/fullscreen/info/maps/common.dart +++ b/lib/widgets/fullscreen/info/maps/common.dart @@ -1,6 +1,6 @@ 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/aves_selection_dialog.dart'; import 'package:aves/widgets/common/borders.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/icons.dart'; @@ -70,7 +70,11 @@ class MapButtonPanel extends StatelessWidget { onPressed: () async { final style = await showDialog( context: context, - builder: (context) => MapStyleDialog(), + builder: (context) => AvesSelectionDialog( + initialValue: settings.infoMapStyle, + options: Map.fromEntries(EntryMapStyle.values.map((v) => MapEntry(v, v.name))), + title: 'Map Style', + ), ); if (style != null) { settings.infoMapStyle = style; diff --git a/lib/widgets/fullscreen/info/metadata_section.dart b/lib/widgets/fullscreen/info/metadata_section.dart index fb3cf769e..11bfda26d 100644 --- a/lib/widgets/fullscreen/info/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata_section.dart @@ -3,8 +3,7 @@ import 'dart:collection'; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/metadata_service.dart'; -import 'package:aves/utils/color_utils.dart'; -import 'package:aves/widgets/common/fx/highlight_decoration.dart'; +import 'package:aves/widgets/common/highlight_title.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:expansion_tile_card/expansion_tile_card.dart'; @@ -90,7 +89,10 @@ class _MetadataSectionSliverState extends State with Auto key: Key('tilecard-${dir.name}'), value: dir.name, expandedNotifier: _expandedDirectoryNotifier, - title: _DirectoryTitle(dir.name), + title: HighlightTitle( + dir.name, + fontSize: 18, + ), children: [ Divider(thickness: 1.0, height: 1.0), Container( @@ -138,42 +140,6 @@ class _MetadataSectionSliverState extends State with Auto bool get wantKeepAlive => true; } -class _DirectoryTitle extends StatelessWidget { - final String name; - - const _DirectoryTitle(this.name); - - @override - Widget build(BuildContext context) { - return Align( - alignment: Alignment.centerLeft, - child: Container( - decoration: HighlightDecoration( - color: stringToColor(name), - ), - margin: EdgeInsets.symmetric(vertical: 4.0), - child: Text( - name, - style: TextStyle( - shadows: [ - Shadow( - color: Colors.black, - offset: Offset(1, 1), - blurRadius: 2, - ) - ], - fontSize: 18, - fontFamily: 'Concourse Caps', - ), - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - ), - ); - } -} - class _MetadataDirectory { final String name; final SplayTreeMap tags; diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 05931e7c0..732aa78a0 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -103,11 +103,11 @@ class _HomePageState extends State { return SingleFullscreenPage(entry: _viewerEntry); } if (_mediaStore != null) { - switch (settings.launchPage) { - case LaunchPage.albums: + switch (settings.homePage) { + case HomePageSetting.albums: return AlbumListPage(source: _mediaStore); break; - case LaunchPage.collection: + case HomePageSetting.collection: return CollectionPage(CollectionLens( source: _mediaStore, groupFactor: settings.collectionGroupFactor, diff --git a/lib/widgets/settings/coordinate_format.dart b/lib/widgets/settings/coordinate_format.dart deleted file mode 100644 index 47b0d93a2..000000000 --- a/lib/widgets/settings/coordinate_format.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:aves/model/settings.dart'; -import 'package:flutter/material.dart'; - -class CoordinateFormatSelector extends StatefulWidget { - @override - _CoordinateFormatSelectorState createState() => _CoordinateFormatSelectorState(); -} - -class _CoordinateFormatSelectorState extends State { - @override - Widget build(BuildContext context) { - return DropdownButton( - items: CoordinateFormat.values - .map((selected) => DropdownMenuItem( - value: selected, - child: Text( - selected.name, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - )) - .toList(), - value: settings.coordinateFormat, - onChanged: (selected) { - settings.coordinateFormat = selected; - setState(() {}); - }, - ); - } -} diff --git a/lib/widgets/settings/launch_page.dart b/lib/widgets/settings/launch_page.dart deleted file mode 100644 index 321f4bbbc..000000000 --- a/lib/widgets/settings/launch_page.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:aves/model/settings.dart'; -import 'package:flutter/material.dart'; - -class LaunchPageSelector extends StatefulWidget { - @override - _LaunchPageSelectorState createState() => _LaunchPageSelectorState(); -} - -class _LaunchPageSelectorState extends State { - @override - Widget build(BuildContext context) { - return DropdownButton( - items: LaunchPage.values - .map((selected) => DropdownMenuItem( - value: selected, - child: Text( - selected.name, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - )) - .toList(), - value: settings.launchPage, - onChanged: (selected) { - settings.launchPage = selected; - setState(() {}); - }, - ); - } -} diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 2a32044c2..0866c6cae 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -1,9 +1,10 @@ -import 'package:aves/utils/constants.dart'; +import 'package:aves/model/settings.dart'; +import 'package:aves/widgets/common/aves_selection_dialog.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/common/highlight_title.dart'; import 'package:aves/widgets/settings/svg_background.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class SettingsPage extends StatelessWidget { @override @@ -16,35 +17,57 @@ class SettingsPage extends StatelessWidget { title: Text('Preferences'), ), body: SafeArea( - child: ListView( - padding: EdgeInsets.all(16), - children: [ - Text('General', style: Constants.titleTextStyle), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Launch page:'), - SizedBox(width: 8), - Flexible(child: LaunchPageSelector()), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('SVG background:'), - SizedBox(width: 8), - Flexible(child: SvgBackgroundSelector()), - ], - ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Coordinate format:'), - SizedBox(width: 8), - Flexible(child: CoordinateFormatSelector()), - ], - ), - ], + child: Consumer( + builder: (context, settings, child) => ListView( + padding: EdgeInsets.symmetric(vertical: 16), + children: [ + SectionTitle('Navigation'), + ListTile( + title: Text('Home'), + subtitle: Text(settings.homePage.name), + onTap: () async { + final value = await showDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: settings.homePage, + options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.name))), + title: 'Home', + ), + ); + if (value != null) { + settings.homePage = value; + } + }, + ), + SwitchListTile( + value: settings.mustBackTwiceToExit, + onChanged: (v) => settings.mustBackTwiceToExit = v, + title: Text('Tap “back” twice to exit'), + ), + SectionTitle('Display'), + ListTile( + title: Text('SVG background'), + trailing: SvgBackgroundSelector(), + ), + ListTile( + title: Text('Coordinate format'), + subtitle: Text(settings.coordinateFormat.name), + onTap: () async { + final value = await showDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: settings.coordinateFormat, + options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.name))), + title: 'Coordinate Format', + ), + ); + if (value != null) { + settings.coordinateFormat = value; + } + }, + ), + ], + ), ), ), ), @@ -52,3 +75,17 @@ class SettingsPage extends StatelessWidget { ); } } + +class SectionTitle extends StatelessWidget { + final String text; + + const SectionTitle(this.text); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only(left: 16, top: 6, right: 16, bottom: 12), + child: HighlightTitle(text), + ); + } +} diff --git a/lib/widgets/settings/svg_background.dart b/lib/widgets/settings/svg_background.dart index 780ec1cb7..fc2497b3d 100644 --- a/lib/widgets/settings/svg_background.dart +++ b/lib/widgets/settings/svg_background.dart @@ -11,33 +11,35 @@ 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, + return DropdownButtonHideUnderline( + child: 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, ), - child: selected == 0 - ? Icon( - Icons.clear, - size: 20, - color: Colors.white30, - ) - : null, - ), - ); - }).toList(), - value: settings.svgBackground, - onChanged: (selected) { - settings.svgBackground = selected; - setState(() {}); - }, + ); + }).toList(), + value: settings.svgBackground, + onChanged: (selected) { + settings.svgBackground = selected; + setState(() {}); + }, + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index 10e5d7d0b..dccce0a00 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -466,6 +466,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.1" + overlay_support: + dependency: "direct main" + description: + name: overlay_support + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e1ca0fab9..14a2061d3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: intl: latlong: # for flutter_map outline_material_icons: + overlay_support: package_info: palette_generator: pdf: