navigation: tap back twice to exit
refactored selection dialogs
This commit is contained in:
parent
70018160c3
commit
67f873b3f5
29 changed files with 396 additions and 464 deletions
|
@ -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,6 +42,7 @@ class _AvesAppState extends State<AvesApp> {
|
|||
// place the settings provider above `MaterialApp`
|
||||
// so it can be used during navigation transitions
|
||||
return SettingsProvider(
|
||||
child: OverlaySupport(
|
||||
child: MaterialApp(
|
||||
title: 'Aves',
|
||||
theme: ThemeData(
|
||||
|
@ -71,6 +73,7 @@ class _AvesAppState extends State<AvesApp> {
|
|||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<void> 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();
|
||||
|
|
|
@ -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<Dependency> androidDependencies = [
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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<CollectionAppBar> with SingleTickerPr
|
|||
unawaited(_goToStats());
|
||||
break;
|
||||
case CollectionAction.group:
|
||||
final factor = await showDialog<EntryGroupFactor>(
|
||||
final value = await showDialog<EntryGroupFactor>(
|
||||
context: context,
|
||||
builder: (context) => CollectionGroupDialog(),
|
||||
builder: (context) => AvesSelectionDialog<EntryGroupFactor>(
|
||||
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<EntrySortFactor>(
|
||||
final value = await showDialog<EntrySortFactor>(
|
||||
context: context,
|
||||
builder: (context) => CollectionSortDialog(initialValue: settings.collectionSortFactor),
|
||||
builder: (context) => AvesSelectionDialog<EntrySortFactor>(
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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,8 +27,10 @@ class CollectionPage extends StatelessWidget {
|
|||
}
|
||||
return SynchronousFuture(true);
|
||||
},
|
||||
child: DoubleBackPopScope(
|
||||
child: ThumbnailCollection(),
|
||||
),
|
||||
),
|
||||
drawer: AppDrawer(
|
||||
source: collection.source,
|
||||
),
|
||||
|
|
|
@ -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<ChipSortDialog> {
|
||||
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<ChipSortFactor>(
|
||||
key: Key(value.toString()),
|
||||
value: value,
|
||||
groupValue: _selectedSort,
|
||||
onChanged: (sort) => setState(() => _selectedSort = sort),
|
||||
title: Text(
|
||||
title,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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<CollectionGroupDialog> {
|
||||
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<EntryGroupFactor>(
|
||||
key: Key(value.toString()),
|
||||
value: value,
|
||||
groupValue: _selectedGroup,
|
||||
onChanged: (group) => setState(() => _selectedGroup = group),
|
||||
title: Text(
|
||||
title,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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<CollectionSortDialog> {
|
||||
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<EntrySortFactor>(
|
||||
key: Key(value.toString()),
|
||||
value: value,
|
||||
groupValue: _selectedSort,
|
||||
onChanged: (sort) => setState(() => _selectedSort = sort),
|
||||
title: Text(
|
||||
title,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<MapStyleDialog> {
|
||||
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<EntryMapStyle>(
|
||||
value: style,
|
||||
groupValue: _selectedStyle,
|
||||
onChanged: (style) => setState(() => _selectedStyle = style),
|
||||
title: Text(
|
||||
title,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}
|
|
@ -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<bool> checkStoragePermission(BuildContext context, Iterable<ImageEntry> entries) {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
61
lib/widgets/common/aves_selection_dialog.dart
Normal file
61
lib/widgets/common/aves_selection_dialog.dart
Normal file
|
@ -0,0 +1,61 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'aves_dialog.dart';
|
||||
|
||||
class AvesSelectionDialog<T> extends StatefulWidget {
|
||||
final T initialValue;
|
||||
final Map<T, String> options;
|
||||
final String title;
|
||||
|
||||
const AvesSelectionDialog({
|
||||
@required this.initialValue,
|
||||
@required this.options,
|
||||
@required this.title,
|
||||
});
|
||||
|
||||
@override
|
||||
_AvesSelectionDialogState<T> createState() => _AvesSelectionDialogState<T>();
|
||||
}
|
||||
|
||||
class _AvesSelectionDialogState<T> extends State<AvesSelectionDialog> {
|
||||
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<T>(
|
||||
key: Key(value.toString()),
|
||||
value: value,
|
||||
groupValue: _selectedValue,
|
||||
onChanged: (v) => setState(() => _selectedValue = v),
|
||||
title: Text(
|
||||
title,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
maxLines: 1,
|
||||
),
|
||||
);
|
||||
}
|
54
lib/widgets/common/double_back_pop.dart
Normal file
54
lib/widgets/common/double_back_pop.dart
Normal file
|
@ -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<DoubleBackPopScope> 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();
|
||||
}
|
||||
}
|
43
lib/widgets/common/highlight_title.dart
Normal file
43
lib/widgets/common/highlight_title.dart
Normal file
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<ChipSortFactor>(
|
||||
context: context,
|
||||
builder: (context) => ChipSortDialog(initialValue: settings.albumSortFactor),
|
||||
builder: (context) => AvesSelectionDialog<ChipSortFactor>(
|
||||
initialValue: settings.albumSortFactor,
|
||||
options: {
|
||||
ChipSortFactor.date: 'By date',
|
||||
ChipSortFactor.name: 'By name',
|
||||
},
|
||||
title: 'Sort',
|
||||
),
|
||||
);
|
||||
if (factor != null) {
|
||||
settings.albumSortFactor = factor;
|
||||
|
|
|
@ -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<EntryMapStyle>(
|
||||
context: context,
|
||||
builder: (context) => MapStyleDialog(),
|
||||
builder: (context) => AvesSelectionDialog<EntryMapStyle>(
|
||||
initialValue: settings.infoMapStyle,
|
||||
options: Map.fromEntries(EntryMapStyle.values.map((v) => MapEntry(v, v.name))),
|
||||
title: 'Map Style',
|
||||
),
|
||||
);
|
||||
if (style != null) {
|
||||
settings.infoMapStyle = style;
|
||||
|
|
|
@ -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<MetadataSectionSliver> 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<MetadataSectionSliver> 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<String, String> tags;
|
||||
|
|
|
@ -103,11 +103,11 @@ class _HomePageState extends State<HomePage> {
|
|||
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,
|
||||
|
|
|
@ -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<CoordinateFormatSelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButton<CoordinateFormat>(
|
||||
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(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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<LaunchPageSelector> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DropdownButton<LaunchPage>(
|
||||
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(() {});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,39 +17,75 @@ class SettingsPage extends StatelessWidget {
|
|||
title: Text('Preferences'),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: ListView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Consumer<Settings>(
|
||||
builder: (context, settings, child) => ListView(
|
||||
padding: EdgeInsets.symmetric(vertical: 16),
|
||||
children: [
|
||||
Text('General', style: Constants.titleTextStyle),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Launch page:'),
|
||||
SizedBox(width: 8),
|
||||
Flexible(child: LaunchPageSelector()),
|
||||
],
|
||||
SectionTitle('Navigation'),
|
||||
ListTile(
|
||||
title: Text('Home'),
|
||||
subtitle: Text(settings.homePage.name),
|
||||
onTap: () async {
|
||||
final value = await showDialog<HomePageSetting>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<HomePageSetting>(
|
||||
initialValue: settings.homePage,
|
||||
options: Map.fromEntries(HomePageSetting.values.map((v) => MapEntry(v, v.name))),
|
||||
title: 'Home',
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('SVG background:'),
|
||||
SizedBox(width: 8),
|
||||
Flexible(child: SvgBackgroundSelector()),
|
||||
],
|
||||
);
|
||||
if (value != null) {
|
||||
settings.homePage = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Coordinate format:'),
|
||||
SizedBox(width: 8),
|
||||
Flexible(child: CoordinateFormatSelector()),
|
||||
],
|
||||
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<CoordinateFormat>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<CoordinateFormat>(
|
||||
initialValue: settings.coordinateFormat,
|
||||
options: Map.fromEntries(CoordinateFormat.values.map((v) => MapEntry(v, v.name))),
|
||||
title: 'Coordinate Format',
|
||||
),
|
||||
);
|
||||
if (value != null) {
|
||||
settings.coordinateFormat = value;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,9 +11,10 @@ class _SvgBackgroundSelectorState extends State<SvgBackgroundSelector> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const radius = 24.0;
|
||||
return DropdownButton<int>(
|
||||
return DropdownButtonHideUnderline(
|
||||
child: DropdownButton<int>(
|
||||
items: [0xFFFFFFFF, 0xFF000000, 0x00000000].map((selected) {
|
||||
return DropdownMenuItem(
|
||||
return DropdownMenuItem<int>(
|
||||
value: selected,
|
||||
child: Container(
|
||||
height: radius,
|
||||
|
@ -38,6 +39,7 @@ class _SvgBackgroundSelectorState extends State<SvgBackgroundSelector> {
|
|||
settings.svgBackground = selected;
|
||||
setState(() {});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -63,6 +63,7 @@ dependencies:
|
|||
intl:
|
||||
latlong: # for flutter_map
|
||||
outline_material_icons:
|
||||
overlay_support:
|
||||
package_info:
|
||||
palette_generator:
|
||||
pdf:
|
||||
|
|
Loading…
Reference in a new issue