Merge branch 'develop'
This commit is contained in:
commit
dc907a0d51
64 changed files with 1052 additions and 991 deletions
2
.github/workflows/check.yml
vendored
2
.github/workflows/check.yml
vendored
|
@ -5,6 +5,8 @@ on:
|
||||||
branches:
|
branches:
|
||||||
- develop
|
- develop
|
||||||
|
|
||||||
|
# TODO TLAD run `flutter format -l 1000 .` and fail if any
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Check code quality.
|
name: Check code quality.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/common/data_providers/settings_provider.dart';
|
import 'package:aves/widgets/common/data_providers/settings_provider.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
import 'package:aves/widgets/home_page.dart';
|
import 'package:aves/widgets/home_page.dart';
|
||||||
|
@ -6,6 +6,7 @@ import 'package:aves/widgets/welcome_page.dart';
|
||||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:overlay_support/overlay_support.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
// HttpClient.enableTimelineLogging = true; // enable network traffic logging
|
// HttpClient.enableTimelineLogging = true; // enable network traffic logging
|
||||||
|
@ -41,6 +42,7 @@ class _AvesAppState extends State<AvesApp> {
|
||||||
// place the settings provider above `MaterialApp`
|
// place the settings provider above `MaterialApp`
|
||||||
// so it can be used during navigation transitions
|
// so it can be used during navigation transitions
|
||||||
return SettingsProvider(
|
return SettingsProvider(
|
||||||
|
child: OverlaySupport(
|
||||||
child: MaterialApp(
|
child: MaterialApp(
|
||||||
title: 'Aves',
|
title: 'Aves',
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
|
@ -71,6 +73,7 @@ class _AvesAppState extends State<AvesApp> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
28
lib/model/settings/coordinate_format.dart
Normal file
28
lib/model/settings/coordinate_format.dart
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import 'package:aves/utils/geo_utils.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
|
enum CoordinateFormat { dms, decimal }
|
||||||
|
|
||||||
|
extension ExtraCoordinateFormat on CoordinateFormat {
|
||||||
|
String get name {
|
||||||
|
switch (this) {
|
||||||
|
case CoordinateFormat.dms:
|
||||||
|
return 'DMS';
|
||||||
|
case CoordinateFormat.decimal:
|
||||||
|
return 'Decimal degrees';
|
||||||
|
default:
|
||||||
|
return toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String format(Tuple2<double, double> latLng) {
|
||||||
|
switch (this) {
|
||||||
|
case CoordinateFormat.dms:
|
||||||
|
return toDMS(latLng).join(', ');
|
||||||
|
case CoordinateFormat.decimal:
|
||||||
|
return [latLng.item1, latLng.item2].map((n) => n.toStringAsFixed(6)).join(', ');
|
||||||
|
default:
|
||||||
|
return toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
lib/model/settings/home_page.dart
Normal file
28
lib/model/settings/home_page.dart
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import 'package:aves/widgets/album/collection_page.dart';
|
||||||
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
|
|
||||||
|
enum HomePageSetting { collection, albums }
|
||||||
|
|
||||||
|
extension ExtraHomePageSetting on HomePageSetting {
|
||||||
|
String get name {
|
||||||
|
switch (this) {
|
||||||
|
case HomePageSetting.collection:
|
||||||
|
return 'Collection';
|
||||||
|
case HomePageSetting.albums:
|
||||||
|
return 'Albums';
|
||||||
|
default:
|
||||||
|
return toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get routeName {
|
||||||
|
switch (this) {
|
||||||
|
case HomePageSetting.collection:
|
||||||
|
return CollectionPage.routeName;
|
||||||
|
case HomePageSetting.albums:
|
||||||
|
return AlbumListPage.routeName;
|
||||||
|
default:
|
||||||
|
return toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,11 @@
|
||||||
import 'package:aves/utils/geo_utils.dart';
|
import 'package:aves/model/settings/coordinate_format.dart';
|
||||||
|
import 'package:aves/model/settings/home_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/location_section.dart';
|
import 'package:aves/widgets/fullscreen/info/location_section.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
|
|
||||||
import 'source/enums.dart';
|
import '../source/enums.dart';
|
||||||
|
|
||||||
final Settings settings = Settings._private();
|
final Settings settings = Settings._private();
|
||||||
|
|
||||||
|
@ -16,18 +16,27 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
Settings._private();
|
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';
|
static const catalogTimeZoneKey = 'catalog_time_zone';
|
||||||
|
|
||||||
|
// collection
|
||||||
static const collectionGroupFactorKey = 'collection_group_factor';
|
static const collectionGroupFactorKey = 'collection_group_factor';
|
||||||
static const collectionSortFactorKey = 'collection_sort_factor';
|
static const collectionSortFactorKey = 'collection_sort_factor';
|
||||||
static const collectionTileExtentKey = 'collection_tile_extent';
|
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 infoMapStyleKey = 'info_map_style';
|
||||||
static const infoMapZoomKey = 'info_map_zoom';
|
static const infoMapZoomKey = 'info_map_zoom';
|
||||||
static const launchPageKey = 'launch_page';
|
|
||||||
static const coordinateFormatKey = 'coordinates_format';
|
static const coordinateFormatKey = 'coordinates_format';
|
||||||
|
|
||||||
|
// rendering
|
||||||
static const svgBackgroundKey = 'svg_background';
|
static const svgBackgroundKey = 'svg_background';
|
||||||
static const albumSortFactorKey = 'album_sort_factor';
|
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
_prefs = await SharedPreferences.getInstance();
|
_prefs = await SharedPreferences.getInstance();
|
||||||
|
@ -37,10 +46,26 @@ class Settings extends ChangeNotifier {
|
||||||
return _prefs.clear();
|
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) ?? '';
|
String get catalogTimeZone => _prefs.getString(catalogTimeZoneKey) ?? '';
|
||||||
|
|
||||||
set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue);
|
set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue);
|
||||||
|
|
||||||
|
// collection
|
||||||
|
|
||||||
EntryGroupFactor get collectionGroupFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values);
|
EntryGroupFactor get collectionGroupFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values);
|
||||||
|
|
||||||
set collectionGroupFactor(EntryGroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString());
|
set collectionGroupFactor(EntryGroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString());
|
||||||
|
@ -53,6 +78,14 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set collectionTileExtent(double newValue) => setAndNotify(collectionTileExtentKey, newValue);
|
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);
|
EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values);
|
||||||
|
|
||||||
set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString());
|
set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString());
|
||||||
|
@ -61,25 +94,23 @@ class Settings extends ChangeNotifier {
|
||||||
|
|
||||||
set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue);
|
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);
|
CoordinateFormat get coordinateFormat => getEnumOrDefault(coordinateFormatKey, CoordinateFormat.dms, CoordinateFormat.values);
|
||||||
|
|
||||||
set coordinateFormat(CoordinateFormat newValue) => setAndNotify(coordinateFormatKey, newValue.toString());
|
set coordinateFormat(CoordinateFormat newValue) => setAndNotify(coordinateFormatKey, newValue.toString());
|
||||||
|
|
||||||
|
// rendering
|
||||||
|
|
||||||
int get svgBackground => _prefs.getInt(svgBackgroundKey) ?? 0xFFFFFFFF;
|
int get svgBackground => _prefs.getInt(svgBackgroundKey) ?? 0xFFFFFFFF;
|
||||||
|
|
||||||
set svgBackground(int newValue) => setAndNotify(svgBackgroundKey, newValue);
|
set svgBackground(int newValue) => setAndNotify(svgBackgroundKey, newValue);
|
||||||
|
|
||||||
ChipSortFactor get albumSortFactor => getEnumOrDefault(albumSortFactorKey, ChipSortFactor.date, ChipSortFactor.values);
|
// utils
|
||||||
|
|
||||||
set albumSortFactor(ChipSortFactor newValue) => setAndNotify(albumSortFactorKey, newValue.toString());
|
// `RoutePredicate`
|
||||||
|
RoutePredicate navRemoveRoutePredicate(String pushedRouteName) {
|
||||||
|
final home = homePage.routeName;
|
||||||
|
return (route) => pushedRouteName != home && route.settings?.name == home;
|
||||||
|
}
|
||||||
|
|
||||||
// convenience methods
|
// convenience methods
|
||||||
|
|
||||||
|
@ -125,44 +156,3 @@ class Settings extends ChangeNotifier {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LaunchPage { collection, albums }
|
|
||||||
|
|
||||||
extension ExtraLaunchPage on LaunchPage {
|
|
||||||
String get name {
|
|
||||||
switch (this) {
|
|
||||||
case LaunchPage.collection:
|
|
||||||
return 'All Media';
|
|
||||||
case LaunchPage.albums:
|
|
||||||
return 'Albums';
|
|
||||||
default:
|
|
||||||
return toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CoordinateFormat { dms, decimal }
|
|
||||||
|
|
||||||
extension ExtraCoordinateFormat on CoordinateFormat {
|
|
||||||
String get name {
|
|
||||||
switch (this) {
|
|
||||||
case CoordinateFormat.dms:
|
|
||||||
return 'DMS';
|
|
||||||
case CoordinateFormat.decimal:
|
|
||||||
return 'Decimal degrees';
|
|
||||||
default:
|
|
||||||
return toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String format(Tuple2<double, double> latLng) {
|
|
||||||
switch (this) {
|
|
||||||
case CoordinateFormat.dms:
|
|
||||||
return toDMS(latLng).join(', ');
|
|
||||||
case CoordinateFormat.decimal:
|
|
||||||
return [latLng.item1, latLng.item2].map((n) => n.toStringAsFixed(6)).join(', ');
|
|
||||||
default:
|
|
||||||
return toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import 'dart:collection';
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/tag.dart';
|
import 'package:aves/model/source/tag.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
|
|
@ -10,13 +10,6 @@ class Constants {
|
||||||
color: Color(0xFFEEEEEE),
|
color: Color(0xFFEEEEEE),
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontFamily: 'Concourse Caps',
|
fontFamily: 'Concourse Caps',
|
||||||
shadows: [
|
|
||||||
Shadow(
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
blurRadius: 3,
|
|
||||||
color: Color(0xFF212121),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
static const List<Dependency> androidDependencies = [
|
static const List<Dependency> androidDependencies = [
|
||||||
|
|
|
@ -31,4 +31,5 @@ class Durations {
|
||||||
static const collectionScalingCompleteNotificationDelay = Duration(milliseconds: 300);
|
static const collectionScalingCompleteNotificationDelay = Duration(milliseconds: 300);
|
||||||
static const videoProgressTimerInterval = Duration(milliseconds: 300);
|
static const videoProgressTimerInterval = Duration(milliseconds: 300);
|
||||||
static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation;
|
static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation;
|
||||||
|
static const doubleBackTimerDelay = Duration(milliseconds: 1000);
|
||||||
}
|
}
|
||||||
|
|
5
lib/utils/flutter_utils.dart
Normal file
5
lib/utils/flutter_utils.dart
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
extension ExtraContext on BuildContext {
|
||||||
|
String get currentRouteName => ModalRoute.of(this)?.settings?.name;
|
||||||
|
}
|
|
@ -7,6 +7,8 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
import 'package:package_info/package_info.dart';
|
import 'package:package_info/package_info.dart';
|
||||||
|
|
||||||
class AboutPage extends StatelessWidget {
|
class AboutPage extends StatelessWidget {
|
||||||
|
static const routeName = '/about';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/main.dart';
|
import 'package:aves/main.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/utils/durations.dart';
|
import 'package:aves/utils/durations.dart';
|
||||||
import 'package:aves/widgets/album/filter_bar.dart';
|
import 'package:aves/widgets/album/filter_bar.dart';
|
||||||
import 'package:aves/widgets/album/search/search_delegate.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/action_delegates/selection_action_delegate.dart';
|
||||||
import 'package:aves/widgets/common/app_bar_subtitle.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/data_providers/media_store_collection_provider.dart';
|
||||||
import 'package:aves/widgets/common/entry_actions.dart';
|
import 'package:aves/widgets/common/entry_actions.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
|
@ -135,7 +134,10 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
|
|
||||||
Widget _buildAppBarTitle() {
|
Widget _buildAppBarTitle() {
|
||||||
if (collection.isBrowsing) {
|
if (collection.isBrowsing) {
|
||||||
Widget title = Text(AvesApp.mode == AppMode.pick ? 'Select' : 'Aves', key: Key('appbar-title'));
|
Widget title = Text(
|
||||||
|
AvesApp.mode == AppMode.pick ? 'Select' : 'Collection',
|
||||||
|
key: Key('appbar-title'),
|
||||||
|
);
|
||||||
if (AvesApp.mode == AppMode.main) {
|
if (AvesApp.mode == AppMode.main) {
|
||||||
title = SourceStateAwareAppBarTitle(
|
title = SourceStateAwareAppBarTitle(
|
||||||
title: title,
|
title: title,
|
||||||
|
@ -296,23 +298,40 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
unawaited(_goToStats());
|
unawaited(_goToStats());
|
||||||
break;
|
break;
|
||||||
case CollectionAction.group:
|
case CollectionAction.group:
|
||||||
final factor = await showDialog<EntryGroupFactor>(
|
final value = await showDialog<EntryGroupFactor>(
|
||||||
context: context,
|
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) {
|
if (value != null) {
|
||||||
settings.collectionGroupFactor = factor;
|
settings.collectionGroupFactor = value;
|
||||||
collection.group(factor);
|
collection.group(value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case CollectionAction.sort:
|
case CollectionAction.sort:
|
||||||
final factor = await showDialog<EntrySortFactor>(
|
final value = await showDialog<EntrySortFactor>(
|
||||||
context: context,
|
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) {
|
if (value != null) {
|
||||||
settings.collectionSortFactor = factor;
|
settings.collectionSortFactor = value;
|
||||||
collection.sort(factor);
|
collection.sort(value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -329,6 +348,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
return Navigator.push(
|
return Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: StatsPage.routeName),
|
||||||
builder: (context) => StatsPage(
|
builder: (context) => StatsPage(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/album/thumbnail_collection.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/data_providers/media_query_data_provider.dart';
|
||||||
|
import 'package:aves/widgets/common/double_back_pop.dart';
|
||||||
|
import 'package:aves/widgets/drawer/app_drawer.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class CollectionPage extends StatelessWidget {
|
class CollectionPage extends StatelessWidget {
|
||||||
|
static const routeName = '/collection';
|
||||||
|
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
|
|
||||||
const CollectionPage(this.collection);
|
const CollectionPage(this.collection);
|
||||||
|
@ -26,8 +29,10 @@ class CollectionPage extends StatelessWidget {
|
||||||
}
|
}
|
||||||
return SynchronousFuture(true);
|
return SynchronousFuture(true);
|
||||||
},
|
},
|
||||||
|
child: DoubleBackPopScope(
|
||||||
child: ThumbnailCollection(),
|
child: ThumbnailCollection(),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
drawer: AppDrawer(
|
drawer: AppDrawer(
|
||||||
source: collection.source,
|
source: collection.source,
|
||||||
),
|
),
|
||||||
|
|
|
@ -5,7 +5,7 @@ import 'package:aves/services/viewer_service.dart';
|
||||||
import 'package:aves/widgets/album/grid/list_known_extent.dart';
|
import 'package:aves/widgets/album/grid/list_known_extent.dart';
|
||||||
import 'package:aves/widgets/album/grid/list_section_layout.dart';
|
import 'package:aves/widgets/album/grid/list_section_layout.dart';
|
||||||
import 'package:aves/widgets/album/thumbnail/decorated.dart';
|
import 'package:aves/widgets/album/thumbnail/decorated.dart';
|
||||||
import 'package:aves/widgets/common/transparent_material_page_route.dart';
|
import 'package:aves/widgets/common/routes.dart';
|
||||||
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -85,6 +85,7 @@ class GridThumbnail extends StatelessWidget {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
TransparentMaterialPageRoute(
|
TransparentMaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: MultiFullscreenPage.routeName),
|
||||||
pageBuilder: (c, a, sa) => MultiFullscreenPage(
|
pageBuilder: (c, a, sa) => MultiFullscreenPage(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
initialEntry: entry,
|
initialEntry: entry,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
class TileExtentManager {
|
class TileExtentManager {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
|
|
@ -1,319 +0,0 @@
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:aves/model/filters/album.dart';
|
|
||||||
import 'package:aves/model/filters/favourite.dart';
|
|
||||||
import 'package:aves/model/filters/filters.dart';
|
|
||||||
import 'package:aves/model/filters/mime.dart';
|
|
||||||
import 'package:aves/model/mime_types.dart';
|
|
||||||
import 'package:aves/model/settings.dart';
|
|
||||||
import 'package:aves/model/source/album.dart';
|
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
|
||||||
import 'package:aves/model/source/location.dart';
|
|
||||||
import 'package:aves/model/source/tag.dart';
|
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
|
||||||
import 'package:aves/widgets/about/about_page.dart';
|
|
||||||
import 'package:aves/widgets/album/collection_page.dart';
|
|
||||||
import 'package:aves/widgets/common/aves_logo.dart';
|
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
|
||||||
import 'package:aves/widgets/debug_page.dart';
|
|
||||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
|
||||||
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
|
||||||
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
|
||||||
import 'package:aves/widgets/settings/settings_page.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class AppDrawer extends StatefulWidget {
|
|
||||||
final CollectionSource source;
|
|
||||||
|
|
||||||
const AppDrawer({@required this.source});
|
|
||||||
|
|
||||||
@override
|
|
||||||
_AppDrawerState createState() => _AppDrawerState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AppDrawerState extends State<AppDrawer> {
|
|
||||||
CollectionSource get source => widget.source;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final header = Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border(
|
|
||||||
bottom: Divider.createBorderSide(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Container(
|
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
color: Theme.of(context).accentColor,
|
|
||||||
child: SafeArea(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Align(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 16,
|
|
||||||
crossAxisAlignment: WrapCrossAlignment.center,
|
|
||||||
children: [
|
|
||||||
AvesLogo(size: 64),
|
|
||||||
Text(
|
|
||||||
'Aves',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 44,
|
|
||||||
fontFamily: 'Concourse Caps',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final allMediaEntry = _FilteredCollectionNavTile(
|
|
||||||
source: source,
|
|
||||||
leading: Icon(AIcons.allMedia),
|
|
||||||
title: 'All media',
|
|
||||||
filter: null,
|
|
||||||
);
|
|
||||||
final videoEntry = _FilteredCollectionNavTile(
|
|
||||||
source: source,
|
|
||||||
leading: Icon(AIcons.video),
|
|
||||||
title: 'Videos',
|
|
||||||
filter: MimeFilter(MimeTypes.anyVideo),
|
|
||||||
);
|
|
||||||
final favouriteEntry = _FilteredCollectionNavTile(
|
|
||||||
source: source,
|
|
||||||
leading: Icon(AIcons.favourite),
|
|
||||||
title: 'Favourites',
|
|
||||||
filter: FavouriteFilter(),
|
|
||||||
);
|
|
||||||
final settingsEntry = SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: ListTile(
|
|
||||||
leading: Icon(AIcons.settings),
|
|
||||||
title: Text('Preferences'),
|
|
||||||
onTap: () => _goTo((_) => SettingsPage()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final aboutEntry = SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: ListTile(
|
|
||||||
leading: Icon(AIcons.info),
|
|
||||||
title: Text('About'),
|
|
||||||
onTap: () => _goTo((_) => AboutPage()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final drawerItems = <Widget>[
|
|
||||||
header,
|
|
||||||
allMediaEntry,
|
|
||||||
videoEntry,
|
|
||||||
favouriteEntry,
|
|
||||||
_buildSpecialAlbumSection(),
|
|
||||||
Divider(),
|
|
||||||
_buildRegularAlbumSection(),
|
|
||||||
_buildCountrySection(),
|
|
||||||
_buildTagSection(),
|
|
||||||
Divider(),
|
|
||||||
settingsEntry,
|
|
||||||
aboutEntry,
|
|
||||||
if (kDebugMode) ...[
|
|
||||||
Divider(),
|
|
||||||
SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: ListTile(
|
|
||||||
leading: Icon(AIcons.debug),
|
|
||||||
title: Text('Debug'),
|
|
||||||
onTap: () => _goTo((_) => DebugPage(source: source)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
return Drawer(
|
|
||||||
child: Selector<MediaQueryData, double>(
|
|
||||||
selector: (c, mq) => mq.viewInsets.bottom,
|
|
||||||
builder: (c, mqViewInsetsBottom, child) {
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: EdgeInsets.only(bottom: mqViewInsetsBottom),
|
|
||||||
child: Theme(
|
|
||||||
data: Theme.of(context).copyWith(
|
|
||||||
// color used by `ExpansionTile` for leading icon
|
|
||||||
unselectedWidgetColor: Colors.white,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: drawerItems,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAlbumEntry(String album, {bool dense = true}) {
|
|
||||||
final uniqueName = source.getUniqueAlbumName(album);
|
|
||||||
return _FilteredCollectionNavTile(
|
|
||||||
source: source,
|
|
||||||
leading: IconUtils.getAlbumIcon(context: context, album: album),
|
|
||||||
title: uniqueName,
|
|
||||||
trailing: androidFileUtils.isOnRemovableStorage(album)
|
|
||||||
? Icon(
|
|
||||||
AIcons.removableStorage,
|
|
||||||
size: 16,
|
|
||||||
color: Colors.grey,
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
dense: dense,
|
|
||||||
filter: AlbumFilter(album, uniqueName),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSpecialAlbumSection() {
|
|
||||||
return StreamBuilder(
|
|
||||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final specialAlbums = source.sortedAlbums.where((album) {
|
|
||||||
final type = androidFileUtils.getAlbumType(album);
|
|
||||||
return type != AlbumType.regular && type != AlbumType.app;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (specialAlbums.isEmpty) return SizedBox.shrink();
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
Divider(),
|
|
||||||
...specialAlbums.map((album) => _buildAlbumEntry(album, dense: false)),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRegularAlbumSection() {
|
|
||||||
return SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: ListTile(
|
|
||||||
key: Key('albums-tile'),
|
|
||||||
leading: Icon(AIcons.album),
|
|
||||||
title: Text('Albums'),
|
|
||||||
trailing: StreamBuilder(
|
|
||||||
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
return Text(
|
|
||||||
'${source.sortedAlbums.length}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white.withOpacity(.6),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
onTap: () => _goTo((_) => AlbumListPage(source: source)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCountrySection() {
|
|
||||||
return SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: ListTile(
|
|
||||||
leading: Icon(AIcons.location),
|
|
||||||
title: Text('Countries'),
|
|
||||||
trailing: StreamBuilder(
|
|
||||||
stream: source.eventBus.on<LocationsChangedEvent>(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
return Text(
|
|
||||||
'${source.sortedCountries.length}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white.withOpacity(.6),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
onTap: () => _goTo((_) => CountryListPage(source: source)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTagSection() {
|
|
||||||
return SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: ListTile(
|
|
||||||
leading: Icon(AIcons.tag),
|
|
||||||
title: Text('Tags'),
|
|
||||||
trailing: StreamBuilder(
|
|
||||||
stream: source.eventBus.on<TagsChangedEvent>(),
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
return Text(
|
|
||||||
'${source.sortedTags.length}',
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.white.withOpacity(.6),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
onTap: () => _goTo((_) => TagListPage(source: source)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _goTo(WidgetBuilder builder) {
|
|
||||||
Navigator.pop(context);
|
|
||||||
Navigator.push(context, MaterialPageRoute(builder: builder));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _FilteredCollectionNavTile extends StatelessWidget {
|
|
||||||
final CollectionSource source;
|
|
||||||
final Widget leading;
|
|
||||||
final String title;
|
|
||||||
final Widget trailing;
|
|
||||||
final bool dense;
|
|
||||||
final CollectionFilter filter;
|
|
||||||
|
|
||||||
const _FilteredCollectionNavTile({
|
|
||||||
@required this.source,
|
|
||||||
@required this.leading,
|
|
||||||
@required this.title,
|
|
||||||
this.trailing,
|
|
||||||
bool dense,
|
|
||||||
@required this.filter,
|
|
||||||
}) : dense = dense ?? false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SafeArea(
|
|
||||||
top: false,
|
|
||||||
bottom: false,
|
|
||||||
child: ListTile(
|
|
||||||
leading: leading,
|
|
||||||
title: Text(title),
|
|
||||||
trailing: trailing,
|
|
||||||
dense: dense,
|
|
||||||
onTap: () => _goToCollection(context),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _goToCollection(BuildContext context) {
|
|
||||||
Navigator.pushAndRemoveUntil(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
builder: (context) => CollectionPage(CollectionLens(
|
|
||||||
source: source,
|
|
||||||
filters: [filter],
|
|
||||||
groupFactor: settings.collectionGroupFactor,
|
|
||||||
sortFactor: settings.collectionSortFactor,
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
(route) => false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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:flutter/widgets.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
import '../dialog.dart';
|
import '../aves_dialog.dart';
|
||||||
|
|
||||||
class CreateAlbumDialog extends StatefulWidget {
|
class CreateAlbumDialog extends StatefulWidget {
|
||||||
@override
|
@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/feedback.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/permission_aware.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/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/entry_actions.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||||
import 'package:aves/widgets/fullscreen/debug.dart';
|
import 'package:aves/widgets/fullscreen/debug.dart';
|
||||||
|
@ -174,6 +174,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: FullscreenDebugPage.routeName),
|
||||||
builder: (context) => FullscreenDebugPage(entry: entry),
|
builder: (context) => FullscreenDebugPage(entry: entry),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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:aves/services/android_file_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../dialog.dart';
|
import '../aves_dialog.dart';
|
||||||
|
|
||||||
mixin PermissionAwareMixin {
|
mixin PermissionAwareMixin {
|
||||||
Future<bool> checkStoragePermission(BuildContext context, Iterable<ImageEntry> entries) {
|
Future<bool> checkStoragePermission(BuildContext context, Iterable<ImageEntry> entries) {
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/widgets/common/dialog.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../aves_dialog.dart';
|
||||||
|
|
||||||
class RenameEntryDialog extends StatefulWidget {
|
class RenameEntryDialog extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,10 @@ 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/create_album_dialog.dart';
|
||||||
import 'package:aves/widgets/common/action_delegates/feedback.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/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/entry_actions.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -89,7 +90,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin {
|
||||||
],
|
],
|
||||||
floating: true,
|
floating: true,
|
||||||
),
|
),
|
||||||
filterEntries: source.getAlbumEntries(),
|
filterEntries: AlbumListPage.getAlbumEntries(source),
|
||||||
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
||||||
emptyBuilder: () => EmptyContent(
|
emptyBuilder: () => EmptyContent(
|
||||||
icon: AIcons.album,
|
icon: AIcons.album,
|
||||||
|
|
60
lib/widgets/common/aves_selection_dialog.dart
Normal file
60
lib/widgets/common/aves_selection_dialog.dart
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
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()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildRadioListTile(T value, String title) => RadioListTile<T>(
|
||||||
|
key: Key(value.toString()),
|
||||||
|
value: value,
|
||||||
|
groupValue: _selectedValue,
|
||||||
|
onChanged: (v) {
|
||||||
|
_selectedValue = v;
|
||||||
|
Navigator.pop(context, _selectedValue);
|
||||||
|
setState(() {});
|
||||||
|
},
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
softWrap: false,
|
||||||
|
overflow: TextOverflow.fade,
|
||||||
|
maxLines: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import 'dart:math';
|
||||||
import 'package:aves/model/favourite_repo.dart';
|
import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
|
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/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.canPop(context) && 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:flutter/material.dart';
|
||||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
class AIcons {
|
class AIcons {
|
||||||
static const IconData allMedia = OMIcons.collections;
|
static const IconData allCollection = OMIcons.collections;
|
||||||
static const IconData image = OMIcons.photo;
|
static const IconData image = OMIcons.photo;
|
||||||
static const IconData video = OMIcons.movie;
|
static const IconData video = OMIcons.movie;
|
||||||
static const IconData vector = OMIcons.code;
|
static const IconData vector = OMIcons.code;
|
||||||
|
|
33
lib/widgets/common/routes.dart
Normal file
33
lib/widgets/common/routes.dart
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class DirectMaterialPageRoute<T> extends PageRouteBuilder<T> {
|
||||||
|
DirectMaterialPageRoute({
|
||||||
|
RouteSettings settings,
|
||||||
|
@required WidgetBuilder builder,
|
||||||
|
}) : super(
|
||||||
|
settings: settings,
|
||||||
|
transitionDuration: Duration.zero,
|
||||||
|
pageBuilder: (c, a, sa) => builder(c),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TransparentMaterialPageRoute<T> extends PageRouteBuilder<T> {
|
||||||
|
TransparentMaterialPageRoute({
|
||||||
|
RouteSettings settings,
|
||||||
|
@required RoutePageBuilder pageBuilder,
|
||||||
|
}) : super(settings: settings, pageBuilder: pageBuilder);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get opaque => false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||||
|
final theme = Theme.of(context).pageTransitionsTheme;
|
||||||
|
return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,16 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class TransparentMaterialPageRoute<T> extends PageRouteBuilder<T> {
|
|
||||||
TransparentMaterialPageRoute({
|
|
||||||
@required RoutePageBuilder pageBuilder,
|
|
||||||
}) : super(pageBuilder: pageBuilder);
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool get opaque => false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
|
||||||
final theme = Theme.of(context).pageTransitionsTheme;
|
|
||||||
return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import 'package:aves/model/favourite_repo.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
import 'package:aves/model/metadata_db.dart';
|
import 'package:aves/model/metadata_db.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
|
@ -18,6 +18,8 @@ import 'package:flutter_svg/flutter_svg.dart';
|
||||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
class DebugPage extends StatefulWidget {
|
class DebugPage extends StatefulWidget {
|
||||||
|
static const routeName = '/debug';
|
||||||
|
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
|
|
||||||
const DebugPage({this.source});
|
const DebugPage({this.source});
|
||||||
|
|
228
lib/widgets/drawer/app_drawer.dart
Normal file
228
lib/widgets/drawer/app_drawer.dart
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:aves/model/filters/album.dart';
|
||||||
|
import 'package:aves/model/filters/favourite.dart';
|
||||||
|
import 'package:aves/model/filters/mime.dart';
|
||||||
|
import 'package:aves/model/mime_types.dart';
|
||||||
|
import 'package:aves/model/source/album.dart';
|
||||||
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
import 'package:aves/model/source/location.dart';
|
||||||
|
import 'package:aves/model/source/tag.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
|
import 'package:aves/widgets/about/about_page.dart';
|
||||||
|
import 'package:aves/widgets/common/aves_logo.dart';
|
||||||
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
|
import 'package:aves/widgets/debug_page.dart';
|
||||||
|
import 'package:aves/widgets/drawer/collection_tile.dart';
|
||||||
|
import 'package:aves/widgets/drawer/tile.dart';
|
||||||
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
|
import 'package:aves/widgets/filter_grids/countries_page.dart';
|
||||||
|
import 'package:aves/widgets/filter_grids/tags_page.dart';
|
||||||
|
import 'package:aves/widgets/settings/settings_page.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class AppDrawer extends StatefulWidget {
|
||||||
|
final CollectionSource source;
|
||||||
|
|
||||||
|
const AppDrawer({@required this.source});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_AppDrawerState createState() => _AppDrawerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppDrawerState extends State<AppDrawer> {
|
||||||
|
CollectionSource get source => widget.source;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final header = Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border(
|
||||||
|
bottom: Divider.createBorderSide(context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
color: Theme.of(context).accentColor,
|
||||||
|
child: SafeArea(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
children: [
|
||||||
|
AvesLogo(size: 64),
|
||||||
|
Text(
|
||||||
|
'Aves',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 44,
|
||||||
|
fontFamily: 'Concourse Caps',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final drawerItems = <Widget>[
|
||||||
|
header,
|
||||||
|
allCollectionTile,
|
||||||
|
videoTile,
|
||||||
|
favouriteTile,
|
||||||
|
_buildSpecialAlbumSection(),
|
||||||
|
Divider(),
|
||||||
|
albumListTile,
|
||||||
|
countryListTile,
|
||||||
|
tagListTile,
|
||||||
|
Divider(),
|
||||||
|
settingsTile,
|
||||||
|
aboutTile,
|
||||||
|
if (kDebugMode) ...[
|
||||||
|
Divider(),
|
||||||
|
debugTile,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return Drawer(
|
||||||
|
child: Selector<MediaQueryData, double>(
|
||||||
|
selector: (c, mq) => mq.viewInsets.bottom,
|
||||||
|
builder: (c, mqViewInsetsBottom, child) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: EdgeInsets.only(bottom: mqViewInsetsBottom),
|
||||||
|
child: Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
// color used by `ExpansionTile` for leading icon
|
||||||
|
unselectedWidgetColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: drawerItems,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildAlbumTile(String album) {
|
||||||
|
final uniqueName = source.getUniqueAlbumName(album);
|
||||||
|
return CollectionNavTile(
|
||||||
|
source: source,
|
||||||
|
leading: IconUtils.getAlbumIcon(context: context, album: album),
|
||||||
|
title: uniqueName,
|
||||||
|
trailing: androidFileUtils.isOnRemovableStorage(album)
|
||||||
|
? Icon(
|
||||||
|
AIcons.removableStorage,
|
||||||
|
size: 16,
|
||||||
|
color: Colors.grey,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
filter: AlbumFilter(album, uniqueName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSpecialAlbumSection() {
|
||||||
|
return StreamBuilder(
|
||||||
|
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final specialAlbums = source.sortedAlbums.where((album) {
|
||||||
|
final type = androidFileUtils.getAlbumType(album);
|
||||||
|
return [AlbumType.camera, AlbumType.screenshots].contains(type);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (specialAlbums.isEmpty) return SizedBox.shrink();
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Divider(),
|
||||||
|
...specialAlbums.map(_buildAlbumTile),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// tiles
|
||||||
|
|
||||||
|
Widget get allCollectionTile => CollectionNavTile(
|
||||||
|
source: source,
|
||||||
|
leading: Icon(AIcons.allCollection),
|
||||||
|
title: 'All collection',
|
||||||
|
filter: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get videoTile => CollectionNavTile(
|
||||||
|
source: source,
|
||||||
|
leading: Icon(AIcons.video),
|
||||||
|
title: 'Videos',
|
||||||
|
filter: MimeFilter(MimeTypes.anyVideo),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get favouriteTile => CollectionNavTile(
|
||||||
|
source: source,
|
||||||
|
leading: Icon(AIcons.favourite),
|
||||||
|
title: 'Favourites',
|
||||||
|
filter: FavouriteFilter(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get albumListTile => NavTile(
|
||||||
|
icon: AIcons.album,
|
||||||
|
title: 'Albums',
|
||||||
|
trailing: StreamBuilder(
|
||||||
|
stream: source.eventBus.on<AlbumsChangedEvent>(),
|
||||||
|
builder: (context, _) => Text('${source.sortedAlbums.length}'),
|
||||||
|
),
|
||||||
|
routeName: AlbumListPage.routeName,
|
||||||
|
pageBuilder: (_) => AlbumListPage(source: source),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get countryListTile => NavTile(
|
||||||
|
icon: AIcons.location,
|
||||||
|
title: 'Countries',
|
||||||
|
trailing: StreamBuilder(
|
||||||
|
stream: source.eventBus.on<LocationsChangedEvent>(),
|
||||||
|
builder: (context, _) => Text('${source.sortedCountries.length}'),
|
||||||
|
),
|
||||||
|
routeName: CountryListPage.routeName,
|
||||||
|
pageBuilder: (_) => CountryListPage(source: source),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get tagListTile => NavTile(
|
||||||
|
icon: AIcons.tag,
|
||||||
|
title: 'Tags',
|
||||||
|
trailing: StreamBuilder(
|
||||||
|
stream: source.eventBus.on<TagsChangedEvent>(),
|
||||||
|
builder: (context, _) => Text('${source.sortedTags.length}'),
|
||||||
|
),
|
||||||
|
routeName: TagListPage.routeName,
|
||||||
|
pageBuilder: (_) => TagListPage(source: source),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get settingsTile => NavTile(
|
||||||
|
icon: AIcons.settings,
|
||||||
|
title: 'Settings',
|
||||||
|
routeName: SettingsPage.routeName,
|
||||||
|
pageBuilder: (_) => SettingsPage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get aboutTile => NavTile(
|
||||||
|
icon: AIcons.info,
|
||||||
|
title: 'About',
|
||||||
|
routeName: AboutPage.routeName,
|
||||||
|
pageBuilder: (_) => AboutPage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget get debugTile => NavTile(
|
||||||
|
icon: AIcons.debug,
|
||||||
|
title: 'Debug',
|
||||||
|
routeName: DebugPage.routeName,
|
||||||
|
pageBuilder: (_) => DebugPage(source: source),
|
||||||
|
);
|
||||||
|
}
|
57
lib/widgets/drawer/collection_tile.dart
Normal file
57
lib/widgets/drawer/collection_tile.dart
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
|
import 'package:aves/widgets/album/collection_page.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CollectionNavTile extends StatelessWidget {
|
||||||
|
final CollectionSource source;
|
||||||
|
final Widget leading;
|
||||||
|
final String title;
|
||||||
|
final Widget trailing;
|
||||||
|
final bool dense;
|
||||||
|
final CollectionFilter filter;
|
||||||
|
|
||||||
|
const CollectionNavTile({
|
||||||
|
@required this.source,
|
||||||
|
@required this.leading,
|
||||||
|
@required this.title,
|
||||||
|
this.trailing,
|
||||||
|
bool dense,
|
||||||
|
@required this.filter,
|
||||||
|
}) : dense = dense ?? false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: false,
|
||||||
|
child: ListTile(
|
||||||
|
leading: leading,
|
||||||
|
title: Text(title),
|
||||||
|
trailing: trailing,
|
||||||
|
dense: dense,
|
||||||
|
onTap: () => _goToCollection(context),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _goToCollection(BuildContext context) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: CollectionPage.routeName),
|
||||||
|
builder: (context) => CollectionPage(CollectionLens(
|
||||||
|
source: source,
|
||||||
|
filters: [filter],
|
||||||
|
groupFactor: settings.collectionGroupFactor,
|
||||||
|
sortFactor: settings.collectionSortFactor,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
settings.navRemoveRoutePredicate(CollectionPage.routeName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
59
lib/widgets/drawer/tile.dart
Normal file
59
lib/widgets/drawer/tile.dart
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
import 'package:aves/utils/flutter_utils.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NavTile extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final Widget trailing;
|
||||||
|
final String routeName;
|
||||||
|
final WidgetBuilder pageBuilder;
|
||||||
|
|
||||||
|
const NavTile({
|
||||||
|
@required this.icon,
|
||||||
|
@required this.title,
|
||||||
|
this.trailing,
|
||||||
|
@required this.routeName,
|
||||||
|
@required this.pageBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: false,
|
||||||
|
child: ListTile(
|
||||||
|
key: Key('$title-tile'),
|
||||||
|
leading: Icon(icon),
|
||||||
|
title: Text(title),
|
||||||
|
trailing: trailing != null
|
||||||
|
? Builder(
|
||||||
|
builder: (context) => DefaultTextStyle.merge(
|
||||||
|
style: TextStyle(
|
||||||
|
color: IconTheme.of(context).color.withOpacity(.6),
|
||||||
|
),
|
||||||
|
child: trailing,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
if (routeName != context.currentRouteName) {
|
||||||
|
Navigator.pushAndRemoveUntil(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: routeName),
|
||||||
|
builder: pageBuilder,
|
||||||
|
),
|
||||||
|
settings.navRemoveRoutePredicate(routeName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected: context.currentRouteName == routeName,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
import 'package:aves/model/filters/album.dart';
|
import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/album.dart';
|
import 'package:aves/model/source/album.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/durations.dart';
|
import 'package:aves/utils/durations.dart';
|
||||||
import 'package:aves/widgets/album/empty.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/icons.dart';
|
||||||
import 'package:aves/widgets/common/menu_row.dart';
|
import 'package:aves/widgets/common/menu_row.dart';
|
||||||
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||||
|
@ -17,6 +17,8 @@ import 'package:flutter/scheduler.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class AlbumListPage extends StatelessWidget {
|
class AlbumListPage extends StatelessWidget {
|
||||||
|
static const routeName = '/albums';
|
||||||
|
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
|
|
||||||
const AlbumListPage({@required this.source});
|
const AlbumListPage({@required this.source});
|
||||||
|
@ -35,7 +37,7 @@ class AlbumListPage extends StatelessWidget {
|
||||||
source: source,
|
source: source,
|
||||||
title: 'Albums',
|
title: 'Albums',
|
||||||
actions: _buildActions(),
|
actions: _buildActions(),
|
||||||
filterEntries: _getAlbumEntries(),
|
filterEntries: getAlbumEntries(source),
|
||||||
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)),
|
||||||
emptyBuilder: () => EmptyContent(
|
emptyBuilder: () => EmptyContent(
|
||||||
icon: AIcons.album,
|
icon: AIcons.album,
|
||||||
|
@ -49,7 +51,52 @@ class AlbumListPage extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, ImageEntry> _getAlbumEntries() {
|
List<Widget> _buildActions() {
|
||||||
|
return [
|
||||||
|
Builder(
|
||||||
|
builder: (context) => PopupMenuButton<ChipAction>(
|
||||||
|
key: Key('appbar-menu-button'),
|
||||||
|
itemBuilder: (context) {
|
||||||
|
return [
|
||||||
|
PopupMenuItem(
|
||||||
|
key: Key('menu-sort'),
|
||||||
|
value: ChipAction.sort,
|
||||||
|
child: MenuRow(text: 'Sort...', icon: AIcons.sort),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
onSelected: (action) => _onChipActionSelected(context, action),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChipActionSelected(BuildContext context, ChipAction action) async {
|
||||||
|
// wait for the popup menu to hide before proceeding with the action
|
||||||
|
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
||||||
|
switch (action) {
|
||||||
|
case ChipAction.sort:
|
||||||
|
final factor = await showDialog<ChipSortFactor>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AvesSelectionDialog<ChipSortFactor>(
|
||||||
|
initialValue: settings.albumSortFactor,
|
||||||
|
options: {
|
||||||
|
ChipSortFactor.date: 'By date',
|
||||||
|
ChipSortFactor.name: 'By name',
|
||||||
|
},
|
||||||
|
title: 'Sort',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (factor != null) {
|
||||||
|
settings.albumSortFactor = factor;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// common with album selection page to move/copy entries
|
||||||
|
|
||||||
|
static Map<String, ImageEntry> getAlbumEntries(CollectionSource source) {
|
||||||
final entriesByDate = source.sortedEntriesForFilterList;
|
final entriesByDate = source.sortedEntriesForFilterList;
|
||||||
final albumEntries = source.sortedAlbums.map((album) {
|
final albumEntries = source.sortedAlbums.map((album) {
|
||||||
return MapEntry(
|
return MapEntry(
|
||||||
|
@ -89,40 +136,4 @@ class AlbumListPage extends StatelessWidget {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildActions() {
|
|
||||||
return [
|
|
||||||
Builder(
|
|
||||||
builder: (context) => PopupMenuButton<ChipAction>(
|
|
||||||
key: Key('appbar-menu-button'),
|
|
||||||
itemBuilder: (context) {
|
|
||||||
return [
|
|
||||||
PopupMenuItem(
|
|
||||||
key: Key('menu-sort'),
|
|
||||||
value: ChipAction.sort,
|
|
||||||
child: MenuRow(text: 'Sort...', icon: AIcons.sort),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
onSelected: (action) => _onChipActionSelected(context, action),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onChipActionSelected(BuildContext context, ChipAction action) async {
|
|
||||||
// wait for the popup menu to hide before proceeding with the action
|
|
||||||
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
|
|
||||||
switch (action) {
|
|
||||||
case ChipAction.sort:
|
|
||||||
final factor = await showDialog<ChipSortFactor>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ChipSortDialog(initialValue: settings.albumSortFactor),
|
|
||||||
);
|
|
||||||
if (factor != null) {
|
|
||||||
settings.albumSortFactor = factor;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class CountryListPage extends StatelessWidget {
|
class CountryListPage extends StatelessWidget {
|
||||||
|
static const routeName = '/countries';
|
||||||
|
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
|
|
||||||
const CountryListPage({@required this.source});
|
const CountryListPage({@required this.source});
|
||||||
|
|
|
@ -2,15 +2,16 @@ import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/utils/durations.dart';
|
import 'package:aves/utils/durations.dart';
|
||||||
import 'package:aves/widgets/album/collection_page.dart';
|
import 'package:aves/widgets/album/collection_page.dart';
|
||||||
import 'package:aves/widgets/app_drawer.dart';
|
|
||||||
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
import 'package:aves/widgets/common/app_bar_subtitle.dart';
|
||||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
|
||||||
|
import 'package:aves/widgets/common/double_back_pop.dart';
|
||||||
|
import 'package:aves/widgets/drawer/app_drawer.dart';
|
||||||
import 'package:aves/widgets/filter_grids/decorated_filter_chip.dart';
|
import 'package:aves/widgets/filter_grids/decorated_filter_chip.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
|
@ -56,6 +57,7 @@ class FilterNavigationPage extends StatelessWidget {
|
||||||
onPressed: (filter) => Navigator.pushAndRemoveUntil(
|
onPressed: (filter) => Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: CollectionPage.routeName),
|
||||||
builder: (context) => CollectionPage(CollectionLens(
|
builder: (context) => CollectionPage(CollectionLens(
|
||||||
source: source,
|
source: source,
|
||||||
filters: [filter],
|
filters: [filter],
|
||||||
|
@ -63,7 +65,7 @@ class FilterNavigationPage extends StatelessWidget {
|
||||||
sortFactor: settings.collectionSortFactor,
|
sortFactor: settings.collectionSortFactor,
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
(route) => false,
|
settings.navRemoveRoutePredicate(CollectionPage.routeName),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -95,7 +97,8 @@ class FilterGridPage extends StatelessWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: SafeArea(
|
body: DoubleBackPopScope(
|
||||||
|
child: SafeArea(
|
||||||
child: Selector<MediaQueryData, double>(
|
child: Selector<MediaQueryData, double>(
|
||||||
selector: (c, mq) => mq.size.width,
|
selector: (c, mq) => mq.size.width,
|
||||||
builder: (c, mqWidth, child) {
|
builder: (c, mqWidth, child) {
|
||||||
|
@ -157,6 +160,7 @@ class FilterGridPage extends StatelessWidget {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
drawer: AppDrawer(
|
drawer: AppDrawer(
|
||||||
source: source,
|
source: source,
|
||||||
),
|
),
|
||||||
|
|
|
@ -7,6 +7,8 @@ import 'package:aves/widgets/filter_grids/filter_grid_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class TagListPage extends StatelessWidget {
|
class TagListPage extends StatelessWidget {
|
||||||
|
static const routeName = '/tags';
|
||||||
|
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
|
|
||||||
const TagListPage({@required this.source});
|
const TagListPage({@required this.source});
|
||||||
|
|
|
@ -10,6 +10,8 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class FullscreenDebugPage extends StatefulWidget {
|
class FullscreenDebugPage extends StatefulWidget {
|
||||||
|
static const routeName = '/fullscreen/debug';
|
||||||
|
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
|
||||||
const FullscreenDebugPage({@required this.entry});
|
const FullscreenDebugPage({@required this.entry});
|
||||||
|
|
|
@ -3,6 +3,7 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/utils/change_notifier.dart';
|
import 'package:aves/utils/change_notifier.dart';
|
||||||
import 'package:aves/utils/durations.dart';
|
import 'package:aves/utils/durations.dart';
|
||||||
|
@ -275,9 +276,10 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
Navigator.pushAndRemoveUntil(
|
Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: CollectionPage.routeName),
|
||||||
builder: (context) => CollectionPage(collection.derive(filter)),
|
builder: (context) => CollectionPage(collection.derive(filter)),
|
||||||
),
|
),
|
||||||
(route) => false,
|
settings.navRemoveRoutePredicate(CollectionPage.routeName),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -323,7 +325,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLeave() {
|
void _onLeave() {
|
||||||
if (!ModalRoute.of(context).canPop) {
|
if (!Navigator.canPop(context)) {
|
||||||
// exit app when trying to pop a fullscreen page that is a viewer for a single entry
|
// exit app when trying to pop a fullscreen page that is a viewer for a single entry
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,8 @@ import 'package:aves/widgets/fullscreen/fullscreen_body.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class MultiFullscreenPage extends AnimatedWidget {
|
class MultiFullscreenPage extends AnimatedWidget {
|
||||||
|
static const routeName = '/fullscreen';
|
||||||
|
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final ImageEntry initialEntry;
|
final ImageEntry initialEntry;
|
||||||
|
|
||||||
|
@ -30,6 +32,8 @@ class MultiFullscreenPage extends AnimatedWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SingleFullscreenPage extends StatelessWidget {
|
class SingleFullscreenPage extends StatelessWidget {
|
||||||
|
static const routeName = '/fullscreen';
|
||||||
|
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
|
|
||||||
const SingleFullscreenPage({
|
const SingleFullscreenPage({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/album/empty.dart';
|
import 'package:aves/widgets/album/empty.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/coordinate_format.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/android_app_service.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/borders.dart';
|
||||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
import 'package:aves/widgets/common/fx/blurred.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
|
@ -70,7 +70,11 @@ class MapButtonPanel extends StatelessWidget {
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
final style = await showDialog<EntryMapStyle>(
|
final style = await showDialog<EntryMapStyle>(
|
||||||
context: context,
|
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) {
|
if (style != null) {
|
||||||
settings.infoMapStyle = style;
|
settings.infoMapStyle = style;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/location_section.dart';
|
import 'package:aves/widgets/fullscreen/info/location_section.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/maps/common.dart';
|
import 'package:aves/widgets/fullscreen/info/maps/common.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/maps/common.dart';
|
import 'package:aves/widgets/fullscreen/info/maps/common.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/maps/scale_layer.dart';
|
import 'package:aves/widgets/fullscreen/info/maps/scale_layer.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
|
@ -3,8 +3,7 @@ import 'dart:collection';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/services/metadata_service.dart';
|
import 'package:aves/services/metadata_service.dart';
|
||||||
import 'package:aves/utils/color_utils.dart';
|
import 'package:aves/widgets/common/highlight_title.dart';
|
||||||
import 'package:aves/widgets/common/fx/highlight_decoration.dart';
|
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
||||||
import 'package:expansion_tile_card/expansion_tile_card.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}'),
|
key: Key('tilecard-${dir.name}'),
|
||||||
value: dir.name,
|
value: dir.name,
|
||||||
expandedNotifier: _expandedDirectoryNotifier,
|
expandedNotifier: _expandedDirectoryNotifier,
|
||||||
title: _DirectoryTitle(dir.name),
|
title: HighlightTitle(
|
||||||
|
dir.name,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
children: [
|
children: [
|
||||||
Divider(thickness: 1.0, height: 1.0),
|
Divider(thickness: 1.0, height: 1.0),
|
||||||
Container(
|
Container(
|
||||||
|
@ -138,42 +140,6 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
bool get wantKeepAlive => true;
|
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 {
|
class _MetadataDirectory {
|
||||||
final String name;
|
final String name;
|
||||||
final SplayTreeMap<String, String> tags;
|
final SplayTreeMap<String, String> tags;
|
||||||
|
|
|
@ -3,7 +3,8 @@ import 'dart:ui';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/image_metadata.dart';
|
import 'package:aves/model/image_metadata.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/coordinate_format.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/services/metadata_service.dart';
|
import 'package:aves/services/metadata_service.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/widgets/common/fx/blurred.dart';
|
import 'package:aves/widgets/common/fx/blurred.dart';
|
||||||
|
|
|
@ -126,7 +126,7 @@ class _TopOverlayRow extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
OverlayButton(
|
OverlayButton(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
child: ModalRoute.of(context)?.canPop ?? true ? BackButton() : CloseButton(),
|
child: Navigator.canPop(context) ? BackButton() : CloseButton(),
|
||||||
),
|
),
|
||||||
Spacer(),
|
Spacer(),
|
||||||
...quickActions.map(_buildOverlayButton),
|
...quickActions.map(_buildOverlayButton),
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import 'package:aves/main.dart';
|
import 'package:aves/main.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/home_page.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:aves/services/viewer_service.dart';
|
import 'package:aves/services/viewer_service.dart';
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/widgets/album/collection_page.dart';
|
import 'package:aves/widgets/album/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
|
import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/routes.dart';
|
||||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -17,6 +18,8 @@ import 'package:permission_handler/permission_handler.dart';
|
||||||
import 'package:screen/screen.dart';
|
import 'package:screen/screen.dart';
|
||||||
|
|
||||||
class HomePage extends StatefulWidget {
|
class HomePage extends StatefulWidget {
|
||||||
|
static const routeName = '/';
|
||||||
|
|
||||||
const HomePage();
|
const HomePage();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -26,16 +29,18 @@ class HomePage extends StatefulWidget {
|
||||||
class _HomePageState extends State<HomePage> {
|
class _HomePageState extends State<HomePage> {
|
||||||
MediaStoreSource _mediaStore;
|
MediaStoreSource _mediaStore;
|
||||||
ImageEntry _viewerEntry;
|
ImageEntry _viewerEntry;
|
||||||
Future<void> _appSetup;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_appSetup = _setup();
|
_setup();
|
||||||
imageCache.maximumSizeBytes = 512 * (1 << 20);
|
imageCache.maximumSizeBytes = 512 * (1 << 20);
|
||||||
Screen.keepOn(true);
|
Screen.keepOn(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Scaffold();
|
||||||
|
|
||||||
Future<void> _setup() async {
|
Future<void> _setup() async {
|
||||||
final permissions = await [
|
final permissions = await [
|
||||||
Permission.storage,
|
Permission.storage,
|
||||||
|
@ -80,6 +85,8 @@ class _HomePageState extends State<HomePage> {
|
||||||
await _mediaStore.init();
|
await _mediaStore.init();
|
||||||
unawaited(_mediaStore.refresh());
|
unawaited(_mediaStore.refresh());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unawaited(Navigator.pushReplacement(context, _getRedirectRoute()));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ImageEntry> _initViewerEntry({@required String uri, @required String mimeType}) async {
|
Future<ImageEntry> _initViewerEntry({@required String uri, @required String mimeType}) async {
|
||||||
|
@ -92,30 +99,36 @@ class _HomePageState extends State<HomePage> {
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
Route _getRedirectRoute() {
|
||||||
Widget build(BuildContext context) {
|
switch (AvesApp.mode) {
|
||||||
return FutureBuilder<void>(
|
case AppMode.view:
|
||||||
future: _appSetup,
|
return DirectMaterialPageRoute(
|
||||||
builder: (context, snapshot) {
|
settings: RouteSettings(name: SingleFullscreenPage.routeName),
|
||||||
if (snapshot.hasError) return Icon(AIcons.error);
|
builder: (_) => SingleFullscreenPage(entry: _viewerEntry),
|
||||||
if (snapshot.connectionState != ConnectionState.done) return Scaffold();
|
);
|
||||||
if (AvesApp.mode == AppMode.view) {
|
case AppMode.main:
|
||||||
return SingleFullscreenPage(entry: _viewerEntry);
|
case AppMode.pick:
|
||||||
}
|
|
||||||
if (_mediaStore != null) {
|
if (_mediaStore != null) {
|
||||||
switch (settings.launchPage) {
|
switch (settings.homePage) {
|
||||||
case LaunchPage.albums:
|
case HomePageSetting.albums:
|
||||||
return AlbumListPage(source: _mediaStore);
|
return DirectMaterialPageRoute(
|
||||||
break;
|
settings: RouteSettings(name: AlbumListPage.routeName),
|
||||||
case LaunchPage.collection:
|
builder: (_) => AlbumListPage(source: _mediaStore),
|
||||||
return CollectionPage(CollectionLens(
|
);
|
||||||
|
case HomePageSetting.collection:
|
||||||
|
return DirectMaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: CollectionPage.routeName),
|
||||||
|
builder: (_) => CollectionPage(
|
||||||
|
CollectionLens(
|
||||||
source: _mediaStore,
|
source: _mediaStore,
|
||||||
groupFactor: settings.collectionGroupFactor,
|
groupFactor: settings.collectionGroupFactor,
|
||||||
sortFactor: settings.collectionSortFactor,
|
sortFactor: settings.collectionSortFactor,
|
||||||
));
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return SizedBox.shrink();
|
}
|
||||||
});
|
return 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<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,11 +1,16 @@
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/model/settings/coordinate_format.dart';
|
||||||
|
import 'package:aves/model/settings/home_page.dart';
|
||||||
|
import 'package:aves/model/settings/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/common/data_providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/settings/coordinate_format.dart';
|
import 'package:aves/widgets/common/highlight_title.dart';
|
||||||
import 'package:aves/widgets/settings/launch_page.dart';
|
|
||||||
import 'package:aves/widgets/settings/svg_background.dart';
|
import 'package:aves/widgets/settings/svg_background.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class SettingsPage extends StatelessWidget {
|
class SettingsPage extends StatelessWidget {
|
||||||
|
static const routeName = '/settings';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
|
@ -13,42 +18,78 @@ class SettingsPage extends StatelessWidget {
|
||||||
length: 4,
|
length: 4,
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Preferences'),
|
title: Text('Settings'),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: ListView(
|
child: Consumer<Settings>(
|
||||||
padding: EdgeInsets.all(16),
|
builder: (context, settings, child) => ListView(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
children: [
|
children: [
|
||||||
Text('General', style: Constants.titleTextStyle),
|
SectionTitle('Navigation'),
|
||||||
Row(
|
ListTile(
|
||||||
mainAxisSize: MainAxisSize.min,
|
title: Text('Home'),
|
||||||
children: [
|
subtitle: Text(settings.homePage.name),
|
||||||
Text('Launch page:'),
|
onTap: () async {
|
||||||
SizedBox(width: 8),
|
final value = await showDialog<HomePageSetting>(
|
||||||
Flexible(child: LaunchPageSelector()),
|
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,
|
if (value != null) {
|
||||||
children: [
|
settings.homePage = value;
|
||||||
Text('SVG background:'),
|
}
|
||||||
SizedBox(width: 8),
|
},
|
||||||
Flexible(child: SvgBackgroundSelector()),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
Row(
|
SwitchListTile(
|
||||||
mainAxisSize: MainAxisSize.min,
|
value: settings.mustBackTwiceToExit,
|
||||||
children: [
|
onChanged: (v) => settings.mustBackTwiceToExit = v,
|
||||||
Text('Coordinate format:'),
|
title: Text('Tap “back” twice to exit'),
|
||||||
SizedBox(width: 8),
|
),
|
||||||
Flexible(child: CoordinateFormatSelector()),
|
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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/common/borders.dart';
|
import 'package:aves/widgets/common/borders.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -11,9 +11,10 @@ class _SvgBackgroundSelectorState extends State<SvgBackgroundSelector> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const radius = 24.0;
|
const radius = 24.0;
|
||||||
return DropdownButton<int>(
|
return DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<int>(
|
||||||
items: [0xFFFFFFFF, 0xFF000000, 0x00000000].map((selected) {
|
items: [0xFFFFFFFF, 0xFF000000, 0x00000000].map((selected) {
|
||||||
return DropdownMenuItem(
|
return DropdownMenuItem<int>(
|
||||||
value: selected,
|
value: selected,
|
||||||
child: Container(
|
child: Container(
|
||||||
height: radius,
|
height: radius,
|
||||||
|
@ -38,6 +39,7 @@ class _SvgBackgroundSelectorState extends State<SvgBackgroundSelector> {
|
||||||
settings.svgBackground = selected;
|
settings.svgBackground = selected;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'package:aves/model/filters/filters.dart';
|
import 'package:aves/model/filters/filters.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/utils/color_utils.dart';
|
import 'package:aves/utils/color_utils.dart';
|
||||||
import 'package:aves/widgets/album/collection_page.dart';
|
import 'package:aves/widgets/album/collection_page.dart';
|
||||||
|
@ -90,9 +91,10 @@ class FilterTable extends StatelessWidget {
|
||||||
Navigator.pushAndRemoveUntil(
|
Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: CollectionPage.routeName),
|
||||||
builder: (context) => CollectionPage(collection.derive(filter)),
|
builder: (context) => CollectionPage(collection.derive(filter)),
|
||||||
),
|
),
|
||||||
(route) => false,
|
settings.navRemoveRoutePredicate(CollectionPage.routeName),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/filters/mime.dart';
|
import 'package:aves/model/filters/mime.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/utils/color_utils.dart';
|
import 'package:aves/utils/color_utils.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
|
@ -20,6 +21,8 @@ import 'package:intl/intl.dart';
|
||||||
import 'package:percent_indicator/linear_percent_indicator.dart';
|
import 'package:percent_indicator/linear_percent_indicator.dart';
|
||||||
|
|
||||||
class StatsPage extends StatelessWidget {
|
class StatsPage extends StatelessWidget {
|
||||||
|
static const routeName = '/collection/stats';
|
||||||
|
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final Map<String, int> entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {};
|
final Map<String, int> entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {};
|
||||||
|
|
||||||
|
@ -236,9 +239,10 @@ class StatsPage extends StatelessWidget {
|
||||||
Navigator.pushAndRemoveUntil(
|
Navigator.pushAndRemoveUntil(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: CollectionPage.routeName),
|
||||||
builder: (context) => CollectionPage(collection.derive(filter)),
|
builder: (context) => CollectionPage(collection.derive(filter)),
|
||||||
),
|
),
|
||||||
(route) => false,
|
settings.navRemoveRoutePredicate(CollectionPage.routeName),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/utils/durations.dart';
|
import 'package:aves/utils/durations.dart';
|
||||||
import 'package:aves/widgets/common/aves_logo.dart';
|
import 'package:aves/widgets/common/aves_logo.dart';
|
||||||
import 'package:aves/widgets/common/labeled_checkbox.dart';
|
import 'package:aves/widgets/common/labeled_checkbox.dart';
|
||||||
|
@ -104,12 +104,12 @@ class _WelcomePageState extends State<WelcomePage> {
|
||||||
onPressed: _hasAcceptedTerms
|
onPressed: _hasAcceptedTerms
|
||||||
? () {
|
? () {
|
||||||
settings.hasAcceptedTerms = true;
|
settings.hasAcceptedTerms = true;
|
||||||
Navigator.pushAndRemoveUntil(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: HomePage.routeName),
|
||||||
builder: (context) => HomePage(),
|
builder: (context) => HomePage(),
|
||||||
),
|
),
|
||||||
(route) => false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
|
|
@ -466,6 +466,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.1"
|
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:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
@ -15,7 +15,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.1.8+20
|
version: 1.1.9+21
|
||||||
|
|
||||||
# video_player (as of v0.10.8+2, backed by ExoPlayer):
|
# video_player (as of v0.10.8+2, backed by ExoPlayer):
|
||||||
# - does not support content URIs (by default, but trivial by fork)
|
# - does not support content URIs (by default, but trivial by fork)
|
||||||
|
@ -63,6 +63,7 @@ dependencies:
|
||||||
intl:
|
intl:
|
||||||
latlong: # for flutter_map
|
latlong: # for flutter_map
|
||||||
outline_material_icons:
|
outline_material_icons:
|
||||||
|
overlay_support:
|
||||||
package_info:
|
package_info:
|
||||||
palette_generator:
|
palette_generator:
|
||||||
pdf:
|
pdf:
|
||||||
|
|
|
@ -56,7 +56,7 @@ void agreeToTerms() {
|
||||||
await driver.tap(find.byValueKey('continue-button'));
|
await driver.tap(find.byValueKey('continue-button'));
|
||||||
await driver.waitUntilNoTransientCallbacks();
|
await driver.waitUntilNoTransientCallbacks();
|
||||||
|
|
||||||
expect(await driver.getText(find.byValueKey('appbar-title')), 'Aves');
|
expect(await driver.getText(find.byValueKey('appbar-title')), 'Collection');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ void selectFirstAlbum() {
|
||||||
await driver.tap(find.byValueKey('appbar-leading-button'));
|
await driver.tap(find.byValueKey('appbar-leading-button'));
|
||||||
await driver.waitUntilNoTransientCallbacks();
|
await driver.waitUntilNoTransientCallbacks();
|
||||||
|
|
||||||
await driver.tap(find.byValueKey('albums-tile'));
|
await driver.tap(find.byValueKey('Albums-tile'));
|
||||||
await driver.waitUntilNoTransientCallbacks();
|
await driver.waitUntilNoTransientCallbacks();
|
||||||
|
|
||||||
await driver.tap(find.descendant(
|
await driver.tap(find.descendant(
|
||||||
|
|
Loading…
Reference in a new issue