From edb8796ba2f42204f8b6ec5a9bd1e64948f7db97 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 16 Nov 2020 11:35:34 +0900 Subject: [PATCH 01/33] fixed aspect ratio for badly registered entries --- lib/model/image_entry.dart | 7 ++++- lib/utils/math_utils.dart | 11 +++++-- .../media_store_collection_provider.dart | 7 +---- test/utils/math_utils_test.dart | 31 +++++++++++++++++++ 4 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 test/utils/math_utils_test.dart diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 1c6e1d02c..a4b55a66d 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -217,7 +217,12 @@ class ImageEntry { } } - bool get isPortrait => rotationDegrees % 180 == 90; + // The additional comparison of width to height is a workaround for badly registered entries. + // e.g. a portrait FHD video should be registered as width=1920, height=1080, orientation=90, + // but is incorrectly registered in the Media Store as width=1080, height=1920, orientation=0 + // Double-checking the width/height during loading or cataloguing is the proper solution, + // but it would take space and time, so a basic workaround will do. + bool get isPortrait => rotationDegrees % 180 == 90 && (catalogMetadata?.rotationDegrees == null || width > height); String get resolutionText { final w = width ?? '?'; diff --git a/lib/utils/math_utils.dart b/lib/utils/math_utils.dart index c83bde20b..299404753 100644 --- a/lib/utils/math_utils.dart +++ b/lib/utils/math_utils.dart @@ -1,11 +1,16 @@ import 'dart:math'; +final double _log2 = log(2); const double _piOver180 = pi / 180.0; -final double log2 = log(2); - double toDegrees(num radians) => radians / _piOver180; double toRadians(num degrees) => degrees * _piOver180; -int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / log2).floor()); +int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / _log2).floor()); + +// e.g. x=12345, precision=3 should return 13000 +int ceilBy(num x, int precision) { + final factor = pow(10, precision); + return (x / factor).ceil() * factor; +} diff --git a/lib/widgets/common/data_providers/media_store_collection_provider.dart b/lib/widgets/common/data_providers/media_store_collection_provider.dart index f10dd760b..ec6ab0e35 100644 --- a/lib/widgets/common/data_providers/media_store_collection_provider.dart +++ b/lib/widgets/common/data_providers/media_store_collection_provider.dart @@ -6,6 +6,7 @@ import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; +import 'package:aves/utils/math_utils.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; @@ -91,10 +92,4 @@ class MediaStoreSource extends CollectionSource { onError: (error) => debugPrint('$runtimeType stream error=$error'), ); } - - // e.g. x=12345, precision=3 should return 13000 - int ceilBy(num x, int precision) { - final factor = pow(10, precision); - return (x / factor).ceil() * factor; - } } diff --git a/test/utils/math_utils_test.dart b/test/utils/math_utils_test.dart new file mode 100644 index 000000000..13eed0641 --- /dev/null +++ b/test/utils/math_utils_test.dart @@ -0,0 +1,31 @@ +import 'dart:math'; + +import 'package:aves/utils/math_utils.dart'; +import 'package:test/test.dart'; + +void main() { + test('convert angles in radians to degrees', () { + expect(toDegrees(pi), 180); + expect(toDegrees(-pi / 2), -90); + }); + + test('convert angles in degrees to radians', () { + expect(toRadians(180), pi); + expect(toRadians(-270), pi * -3 / 2); + }); + + test('highest power of 2 that is smaller than or equal to the number', () { + expect(highestPowerOf2(1024), 1024); + expect(highestPowerOf2(42), 32); + expect(highestPowerOf2(0), 0); + expect(highestPowerOf2(-42), 0); + }); + + test('rounding up to a given precision before the decimal', () { + expect(ceilBy(12345.678, 3), 13000); + expect(ceilBy(42, 3), 1000); + expect(ceilBy(0, 3), 0); + expect(ceilBy(-42, 3), 0); + expect(ceilBy(-12345.678, 3), -12000); + }); +} From 93e385d7c3da7e87a4a597f86c1ca1b88a8ec3a5 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 16 Nov 2020 12:30:48 +0900 Subject: [PATCH 02/33] fixed opening file media URI with no mime type in Media Store --- .../model/provider/MediaStoreImageProvider.kt | 75 ++++++++++--------- .../deckers/thibault/aves/utils/MimeTypes.kt | 2 +- 2 files changed, 42 insertions(+), 35 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index ce3b59aca..30dae1737 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -47,10 +47,10 @@ class MediaStoreImageProvider : ImageProvider() { val contentUri = ContentUris.withAppendedId(VIDEO_CONTENT_URI, id) if (fetchFrom(context, alwaysValid, onSuccess, contentUri, VIDEO_PROJECTION) > 0) return } - // the uri can be a file media uri (e.g. "content://0@media/external/file/30050") + // the uri can be a file media URI (e.g. "content://0@media/external/file/30050") // without an equivalent image/video if it is shared from a file browser // but the file is not publicly visible - if (fetchFrom(context, alwaysValid, onSuccess, uri, BASE_PROJECTION) > 0) return + if (fetchFrom(context, alwaysValid, onSuccess, uri, BASE_PROJECTION, fileMimeType = mimeType) > 0) return callback.onFailure(Exception("failed to fetch entry at uri=$uri")) } @@ -87,6 +87,7 @@ class MediaStoreImageProvider : ImageProvider() { handleNewEntry: NewEntryHandler, contentUri: Uri, projection: Array, + fileMimeType: String? = null, ): Int { var newEntryCount = 0 val orderBy = "${MediaStore.MediaColumns.DATE_MODIFIED} DESC" @@ -123,45 +124,51 @@ class MediaStoreImageProvider : ImageProvider() { // for multiple items, `contentUri` is the root without ID, // but for single items, `contentUri` already contains the ID val itemUri = if (contentUriContainsId) contentUri else ContentUris.withAppendedId(contentUri, contentId.toLong()) - val mimeType = cursor.getString(mimeTypeColumn) + // `mimeType` can be registered as null for file media URIs with unsupported media types (e.g. TIFF on old devices) + // in that case we try to use the mime type provided along the URI + val mimeType: String? = cursor.getString(mimeTypeColumn) ?: fileMimeType val width = cursor.getInt(widthColumn) val height = cursor.getInt(heightColumn) val durationMillis = if (durationColumn != -1) cursor.getLong(durationColumn) else 0L - var entryMap: FieldMap = hashMapOf( - "uri" to itemUri.toString(), - "path" to cursor.getString(pathColumn), - "sourceMimeType" to mimeType, - "width" to width, - "height" to height, - "sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0, - "sizeBytes" to cursor.getLong(sizeColumn), - "title" to cursor.getString(titleColumn), - "dateModifiedSecs" to dateModifiedSecs, - "sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null, - "durationMillis" to durationMillis, - // only for map export - "contentId" to contentId, - ) + if (mimeType == null) { + Log.w(LOG_TAG, "failed to make entry from uri=$itemUri because of null MIME type") + } else { + var entryMap: FieldMap = hashMapOf( + "uri" to itemUri.toString(), + "path" to cursor.getString(pathColumn), + "sourceMimeType" to mimeType, + "width" to width, + "height" to height, + "sourceRotationDegrees" to if (orientationColumn != -1) cursor.getInt(orientationColumn) else 0, + "sizeBytes" to cursor.getLong(sizeColumn), + "title" to cursor.getString(titleColumn), + "dateModifiedSecs" to dateModifiedSecs, + "sourceDateTakenMillis" to if (dateTakenColumn != -1) cursor.getLong(dateTakenColumn) else null, + "durationMillis" to durationMillis, + // only for map export + "contentId" to contentId, + ) - if (MimeTypes.isRaw(mimeType) - || (width <= 0 || height <= 0) && needSize(mimeType) - || durationMillis == 0L && needDuration - ) { - // Some images are incorrectly registered in the Media Store, - // missing some attributes such as width, height, orientation. - // Also, the reported size of raw images is inconsistent across devices - // and Android versions (sometimes the raw size, sometimes the decoded size). - val entry = SourceImageEntry(entryMap).fillPreCatalogMetadata(context) - entryMap = entry.toMap() - } + if (MimeTypes.isRaw(mimeType) + || (width <= 0 || height <= 0) && needSize(mimeType) + || durationMillis == 0L && needDuration + ) { + // Some images are incorrectly registered in the Media Store, + // missing some attributes such as width, height, orientation. + // Also, the reported size of raw images is inconsistent across devices + // and Android versions (sometimes the raw size, sometimes the decoded size). + val entry = SourceImageEntry(entryMap).fillPreCatalogMetadata(context) + entryMap = entry.toMap() + } - handleNewEntry(entryMap) - // TODO TLAD is this necessary? - if (newEntryCount % 30 == 0) { - delay(10) + handleNewEntry(entryMap) + // TODO TLAD is this necessary? + if (newEntryCount % 30 == 0) { + delay(10) + } + newEntryCount++ } - newEntryCount++ } } cursor.close() diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt index 924d50b65..be9117260 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/utils/MimeTypes.kt @@ -44,7 +44,7 @@ object MimeTypes { else -> isVideo(mimeType) } - fun isRaw(mimeType: String?): Boolean { + fun isRaw(mimeType: String): Boolean { return when (mimeType) { ARW, CR2, DNG, NEF, NRW, ORF, PEF, RAF, RW2, SRW -> true else -> false From 0f773563f4180e93642f6873f5f3df73019ff516 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 16 Nov 2020 19:03:10 +0900 Subject: [PATCH 03/33] SVG: view source XML --- .../aves/model/provider/ImageProvider.kt | 2 +- .../model/provider/MediaStoreImageProvider.kt | 3 +- lib/model/image_entry.dart | 1 + lib/utils/constants.dart | 6 ++ lib/widgets/collection/app_bar.dart | 1 + .../entry_action_delegate.dart | 43 +++++--- lib/widgets/common/aves_highlight.dart | 101 ++++++++++++++++++ lib/widgets/common/entry_actions.dart | 6 ++ lib/widgets/fullscreen/overlay/top.dart | 4 + .../fullscreen/source_viewer_page.dart | 77 +++++++++++++ pubspec.lock | 14 +++ pubspec.yaml | 1 + 12 files changed, 245 insertions(+), 14 deletions(-) create mode 100644 lib/widgets/common/aves_highlight.dart create mode 100644 lib/widgets/fullscreen/source_viewer_page.dart diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt index cbf6d5e2b..c1eeca279 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/ImageProvider.kt @@ -182,7 +182,7 @@ abstract class ImageProvider { } if (newFields.isEmpty()) { - cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri")) + cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)")) } else { cont.resume(newFields) } diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt index 30dae1737..5c285de53 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/provider/MediaStoreImageProvider.kt @@ -321,7 +321,8 @@ class MediaStoreImageProvider : ImageProvider() { MediaStore.MediaColumns._ID, MediaColumns.PATH, MediaStore.MediaColumns.MIME_TYPE, - MediaStore.MediaColumns.SIZE, // TODO TLAD use `DISPLAY_NAME` instead/along `TITLE`? + MediaStore.MediaColumns.SIZE, + // TODO TLAD use `DISPLAY_NAME` instead/along `TITLE`? MediaStore.MediaColumns.TITLE, MediaStore.MediaColumns.WIDTH, MediaStore.MediaColumns.HEIGHT, diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index a4b55a66d..07b64f7e7 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -23,6 +23,7 @@ class ImageEntry { String _path, _directory, _filename, _extension; int contentId; final String sourceMimeType; + // TODO TLAD use SVG viewport as width/height int width; int height; int sourceRotationDegrees; diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index e7e88d044..8cf139144 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -107,6 +107,12 @@ class Constants { licenseUrl: 'https://github.com/AndreHaueisen/flushbar/blob/master/LICENSE', sourceUrl: 'https://github.com/AndreHaueisen/flushbar', ), + Dependency( + name: 'Flutter Highlight', + license: 'MIT', + licenseUrl: 'https://github.com/git-touch/highlight/blob/master/LICENSE', + sourceUrl: 'https://github.com/git-touch/highlight', + ), Dependency( name: 'Flutter ijkplayer', license: 'MIT', diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 5e67c0bf3..6329c68f4 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -282,6 +282,7 @@ class _CollectionAppBarState extends State with SingleTickerPr void _onCollectionActionSelected(CollectionAction action) async { // wait for the popup menu to hide before proceeding with the action await Future.delayed(Durations.popupMenuAnimation * timeDilation); + switch (action) { case CollectionAction.copy: case CollectionAction.move: diff --git a/lib/widgets/common/action_delegates/entry_action_delegate.dart b/lib/widgets/common/action_delegates/entry_action_delegate.dart index e62b4631e..de7a06885 100644 --- a/lib/widgets/common/action_delegates/entry_action_delegate.dart +++ b/lib/widgets/common/action_delegates/entry_action_delegate.dart @@ -2,6 +2,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; +import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/action_delegates/feedback.dart'; import 'package:aves/widgets/common/action_delegates/permission_aware.dart'; import 'package:aves/widgets/common/action_delegates/rename_entry_dialog.dart'; @@ -9,7 +10,9 @@ import 'package:aves/widgets/common/aves_dialog.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/image_providers/uri_image_provider.dart'; import 'package:aves/widgets/fullscreen/fullscreen_debug_page.dart'; +import 'package:aves/widgets/fullscreen/source_viewer_page.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:pdf/pdf.dart'; @@ -28,46 +31,52 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { bool get hasCollection => collection != null; - void onActionSelected(BuildContext context, ImageEntry entry, EntryAction action) { + void onActionSelected(BuildContext context, ImageEntry entry, EntryAction action) async { + // wait for the popup menu to hide before proceeding with the action + await Future.delayed(Durations.popupMenuAnimation * timeDilation); + switch (action) { case EntryAction.toggleFavourite: entry.toggleFavourite(); break; case EntryAction.delete: - _showDeleteDialog(context, entry); + unawaited(_showDeleteDialog(context, entry)); break; case EntryAction.edit: - AndroidAppService.edit(entry.uri, entry.mimeType); + unawaited(AndroidAppService.edit(entry.uri, entry.mimeType)); break; case EntryAction.info: showInfo(); break; case EntryAction.rename: - _showRenameDialog(context, entry); + unawaited(_showRenameDialog(context, entry)); break; case EntryAction.open: - AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype); + unawaited(AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype)); break; case EntryAction.openMap: - AndroidAppService.openMap(entry.geoUri); + unawaited(AndroidAppService.openMap(entry.geoUri)); break; case EntryAction.print: - _print(entry); + unawaited(_print(entry)); break; case EntryAction.rotateCCW: - _rotate(context, entry, clockwise: false); + unawaited(_rotate(context, entry, clockwise: false)); break; case EntryAction.rotateCW: - _rotate(context, entry, clockwise: true); + unawaited(_rotate(context, entry, clockwise: true)); break; case EntryAction.flip: - _flip(context, entry); + unawaited(_flip(context, entry)); break; case EntryAction.setAs: - AndroidAppService.setAs(entry.uri, entry.mimeType); + unawaited(AndroidAppService.setAs(entry.uri, entry.mimeType)); break; case EntryAction.share: - AndroidAppService.share({entry}); + unawaited(AndroidAppService.share({entry})); + break; + case EntryAction.viewSource: + _goToSourceViewer(context, entry); break; case EntryAction.debug: _goToDebug(context, entry); @@ -181,6 +190,16 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { showFeedback(context, await entry.rename(newName) ? 'Done!' : 'Failed'); } + void _goToSourceViewer(BuildContext context, ImageEntry entry) { + Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: SourceViewerPage.routeName), + builder: (context) => SourceViewerPage(entry: entry), + ), + ); + } + void _goToDebug(BuildContext context, ImageEntry entry) { Navigator.push( context, diff --git a/lib/widgets/common/aves_highlight.dart b/lib/widgets/common/aves_highlight.dart new file mode 100644 index 000000000..13df2ac11 --- /dev/null +++ b/lib/widgets/common/aves_highlight.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:highlight/highlight.dart' show highlight, Node; + +// TODO TLAD use the TextSpan getter instead of this modified `HighlightView` when this is fixed: https://github.com/git-touch/highlight/issues/6 + +/// Highlight Flutter Widget +class AvesHighlightView extends StatelessWidget { + /// The original code to be highlighted + final String source; + + /// Highlight language + /// + /// It is recommended to give it a value for performance + /// + /// [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages) + final String language; + + /// Highlight theme + /// + /// [All available themes](https://github.com/pd4d10/highlight/blob/master/flutter_highlight/lib/themes) + final Map theme; + + /// Padding + final EdgeInsetsGeometry padding; + + /// Text styles + /// + /// Specify text styles such as font family and font size + final TextStyle textStyle; + + AvesHighlightView( + String input, { + this.language, + this.theme = const {}, + this.padding, + this.textStyle, + int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087 + }) : source = input.replaceAll('\t', ' ' * tabSize); + + List _convert(List nodes) { + final spans = []; + var currentSpans = spans; + final stack = >[]; + + void _traverse(Node node) { + if (node.value != null) { + currentSpans.add(node.className == null ? TextSpan(text: node.value) : TextSpan(text: node.value, style: theme[node.className])); + } else if (node.children != null) { + final tmp = []; + currentSpans.add(TextSpan(children: tmp, style: theme[node.className])); + stack.add(currentSpans); + currentSpans = tmp; + + node.children.forEach((n) { + _traverse(n); + if (n == node.children.last) { + currentSpans = stack.isEmpty ? spans : stack.removeLast(); + } + }); + } + } + + for (var node in nodes) { + _traverse(node); + } + + return spans; + } + + static const _rootKey = 'root'; + static const _defaultFontColor = Color(0xff000000); + static const _defaultBackgroundColor = Color(0xffffffff); + + // TODO: dart:io is not available at web platform currently + // See: https://github.com/flutter/flutter/issues/39998 + // So we just use monospace here for now + static const _defaultFontFamily = 'monospace'; + + @override + Widget build(BuildContext context) { + var _textStyle = TextStyle( + fontFamily: _defaultFontFamily, + color: theme[_rootKey]?.color ?? _defaultFontColor, + ); + if (textStyle != null) { + _textStyle = _textStyle.merge(textStyle); + } + + return Container( + color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor, + padding: padding, + child: SelectableText.rich( + TextSpan( + style: _textStyle, + children: _convert(highlight.parse(source, language: language).nodes), + ), + ), + ); + } +} diff --git a/lib/widgets/common/entry_actions.dart b/lib/widgets/common/entry_actions.dart index 2fadd1889..0b9e70b2f 100644 --- a/lib/widgets/common/entry_actions.dart +++ b/lib/widgets/common/entry_actions.dart @@ -15,6 +15,7 @@ enum EntryAction { setAs, share, toggleFavourite, + viewSource, debug, } @@ -31,6 +32,7 @@ class EntryActions { EntryAction.delete, EntryAction.rename, EntryAction.print, + EntryAction.viewSource, ]; static const externalApp = [ @@ -64,6 +66,8 @@ extension ExtraEntryAction on EntryAction { return 'Print'; case EntryAction.share: return 'Share'; + case EntryAction.viewSource: + return 'View source'; // external app actions case EntryAction.edit: return 'Edit with…'; @@ -101,6 +105,8 @@ extension ExtraEntryAction on EntryAction { return AIcons.print; case EntryAction.share: return AIcons.share; + case EntryAction.viewSource: + return AIcons.vector; // external app actions case EntryAction.edit: case EntryAction.open: diff --git a/lib/widgets/fullscreen/overlay/top.dart b/lib/widgets/fullscreen/overlay/top.dart index bfd209e36..db02ebf4b 100644 --- a/lib/widgets/fullscreen/overlay/top.dart +++ b/lib/widgets/fullscreen/overlay/top.dart @@ -109,6 +109,8 @@ class FullscreenTopOverlay extends StatelessWidget { return entry.canPrint; case EntryAction.openMap: return entry.hasGps; + case EntryAction.viewSource: + return entry.isSvg; case EntryAction.share: case EntryAction.info: case EntryAction.open: @@ -191,6 +193,7 @@ class _TopOverlayRow extends StatelessWidget { case EntryAction.rotateCW: case EntryAction.flip: case EntryAction.print: + case EntryAction.viewSource: child = IconButton( icon: Icon(action.getIcon()), onPressed: onPressed, @@ -233,6 +236,7 @@ class _TopOverlayRow extends StatelessWidget { case EntryAction.rotateCW: case EntryAction.flip: case EntryAction.print: + case EntryAction.viewSource: case EntryAction.debug: child = MenuRow(text: action.getText(), icon: action.getIcon()); break; diff --git a/lib/widgets/fullscreen/source_viewer_page.dart b/lib/widgets/fullscreen/source_viewer_page.dart new file mode 100644 index 000000000..d6a5a0557 --- /dev/null +++ b/lib/widgets/fullscreen/source_viewer_page.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; + +import 'package:aves/model/image_entry.dart'; +import 'package:aves/services/image_file_service.dart'; +import 'package:aves/widgets/common/aves_highlight.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_highlight/themes/darcula.dart'; + +class SourceViewerPage extends StatefulWidget { + static const routeName = '/fullscreen/source'; + + final ImageEntry entry; + + const SourceViewerPage({ + @required this.entry, + }); + + @override + _SourceViewerPageState createState() => _SourceViewerPageState(); +} + +class _SourceViewerPageState extends State { + Future _loader; + + ImageEntry get entry => widget.entry; + + @override + void initState() { + super.initState(); + _loader = ImageFileService.getImage(entry.uri, entry.mimeType, 0, false).then(utf8.decode); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('Source'), + ), + body: SafeArea( + child: FutureBuilder( + future: _loader, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Text(snapshot.error.toString()); + } + if (snapshot.connectionState != ConnectionState.done) { + return SizedBox.shrink(); + } + + final source = snapshot.data; + final highlightView = AvesHighlightView( + source, + language: 'xml', + theme: darculaTheme, + padding: EdgeInsets.all(8), + textStyle: TextStyle( + fontSize: 12, + ), + tabSize: 4, + ); + return Container( + constraints: BoxConstraints.expand(), + child: Scrollbar( + child: SingleChildScrollView( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: highlightView, + ), + ), + ), + ); + }, + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index cc04bfa10..d6168ff0a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -288,6 +288,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_highlight: + dependency: "direct main" + description: + name: flutter_highlight + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" flutter_ijkplayer: dependency: "direct main" description: @@ -389,6 +396,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + highlight: + dependency: transitive + description: + name: highlight + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.0" http: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 1eef71a3a..a6b8ddc1f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,7 @@ dependencies: firebase_analytics: firebase_crashlytics: flushbar: + flutter_highlight: flutter_ijkplayer: # path: ../flutter_ijkplayer git: From 613fe45fc2a57e536c376bcc72390dc718322147 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 17 Nov 2020 15:47:57 +0900 Subject: [PATCH 04/33] fixed tiled view contained scale state not kept on orientation change --- lib/widgets/fullscreen/image_view.dart | 11 +++++++++++ lib/widgets/fullscreen/overlay/minimap.dart | 5 ++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index f79584088..327cc27c2 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -39,6 +39,7 @@ class ImageView extends StatefulWidget { class _ImageViewState extends State { final PhotoViewController _photoViewController = PhotoViewController(); + final PhotoViewScaleStateController _photoViewScaleStateController = PhotoViewScaleStateController(); final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); StreamSubscription _subscription; Size _photoViewChildSize; @@ -164,6 +165,15 @@ class _ImageViewState extends State { child: Selector( selector: (context, mq) => mq.size, builder: (context, mqSize, child) { + // When the scale state is cycled to be in its `initial` state (i.e. `contained`), and the device is rotated, + // `PhotoView` keeps the scale state as `contained`, but the controller does not update or notify the new scale value. + // We cannot use `scaleStateChangedCallback` as a workaround, because the scale state is updated before animating the scale change, + // so we keep receiving scale updates after the scale state update. + // Instead we check the scale state here when the constraints change, so we can reset the obsolete scale value. + if (_photoViewScaleStateController.scaleState == PhotoViewScaleState.initial) { + final value = PhotoViewControllerValue(position: Offset.zero, scale: 0, rotation: 0, rotationFocusPoint: null); + WidgetsBinding.instance.addPostFrameCallback((_) => _onViewChanged(value)); + } return TiledImageView( entry: entry, viewportSize: mqSize, @@ -176,6 +186,7 @@ class _ImageViewState extends State { childSize: entry.displaySize, backgroundDecoration: backgroundDecoration, controller: _photoViewController, + scaleStateController: _photoViewScaleStateController, maxScale: maxScale, minScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained, diff --git a/lib/widgets/fullscreen/overlay/minimap.dart b/lib/widgets/fullscreen/overlay/minimap.dart index edceade7c..d8a33446f 100644 --- a/lib/widgets/fullscreen/overlay/minimap.dart +++ b/lib/widgets/fullscreen/overlay/minimap.dart @@ -56,7 +56,10 @@ class MinimapPainter extends CustomPainter { @required this.viewScale, this.minimapBorderColor = Colors.white, this.viewportBorderColor = Colors.white, - }); + }) : assert(viewportSize != null), + assert(entrySize != null), + assert(viewCenterOffset != null), + assert(viewScale != null); @override void paint(Canvas canvas, Size size) { From 8c607640dc3b27a9b95157662186f563ccd17186 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 17 Nov 2020 16:27:23 +0900 Subject: [PATCH 05/33] fixed propagating reading rights to open/set-as for non-media store entries opened by ACTION_VIEW --- .../aves/channel/calls/AppAdapterHandler.kt | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt index 181d0955d..7f942d055 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/AppAdapterHandler.kt @@ -162,6 +162,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { uri ?: return false val intent = Intent(Intent.ACTION_VIEW) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setDataAndType(uri, mimeType) return safeStartActivityChooser(title, intent) } @@ -177,12 +178,14 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { uri ?: return false val intent = Intent(Intent.ACTION_ATTACH_DATA) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setDataAndType(uri, mimeType) return safeStartActivityChooser(title, intent) } private fun shareSingle(title: String?, uri: Uri, mimeType: String): Boolean { val intent = Intent(Intent.ACTION_SEND) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .setType(mimeType) when (uri.scheme?.toLowerCase(Locale.ROOT)) { ContentResolver.SCHEME_FILE -> { @@ -190,7 +193,6 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { val applicationId = context.applicationContext.packageName val apkUri = FileProvider.getUriForFile(context, "$applicationId.fileprovider", File(path)) intent.putExtra(Intent.EXTRA_STREAM, apkUri) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } else -> intent.putExtra(Intent.EXTRA_STREAM, uri) } @@ -222,25 +224,32 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler { } val intent = Intent(Intent.ACTION_SEND_MULTIPLE) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) .putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList) .setType(mimeType) return safeStartActivityChooser(title, intent) } private fun safeStartActivity(intent: Intent): Boolean { - val canResolve = intent.resolveActivity(context.packageManager) != null - if (canResolve) { + if (intent.resolveActivity(context.packageManager) == null) return false + try { context.startActivity(intent) + return true + } catch (e: SecurityException) { + Log.w(LOG_TAG, "failed to start activity for intent=$intent", e) } - return canResolve + return false } private fun safeStartActivityChooser(title: String?, intent: Intent): Boolean { - val canResolve = intent.resolveActivity(context.packageManager) != null - if (canResolve) { + if (intent.resolveActivity(context.packageManager) == null) return false + try { context.startActivity(Intent.createChooser(intent, title)) + return true + } catch (e: SecurityException) { + Log.w(LOG_TAG, "failed to start activity chooser for intent=$intent", e) } - return canResolve + return false } companion object { From 408744d28604c4953cbc8e20e2285fcae392bd80 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 18 Nov 2020 10:04:19 +0900 Subject: [PATCH 06/33] google-services update, packages & android dependencies upgrade --- android/app/build.gradle | 2 +- android/app/google-services.json | 24 ++++++++++++++++++++++++ android/build.gradle | 2 +- pubspec.lock | 4 ++-- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 6e9bfd207..775910c2c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,7 +98,7 @@ repositories { dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' - implementation 'androidx.core:core-ktx:1.5.0-alpha04' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts + implementation 'androidx.core:core-ktx:1.5.0-alpha05' // v1.5.0-alpha02+ for ShortcutManagerCompat.setDynamicShortcuts implementation 'androidx.exifinterface:exifinterface:1.3.1' implementation 'com.commonsware.cwac:document:0.4.1' implementation 'com.drewnoakes:metadata-extractor:2.15.0' diff --git a/android/app/google-services.json b/android/app/google-services.json index 3e529cffb..21cde835b 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -14,6 +14,14 @@ } }, "oauth_client": [ + { + "client_id": "100907092477-1mredcehjo66opfirr6k3kokjqmc99ee.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "deckers.thibault.aves", + "certificate_hash": "59a50013fa7a2f97911b52d681cafaebf83505e8" + } + }, { "client_id": "100907092477-ml1c4hr4l24ekg7l7nqid06n03kek6c8.apps.googleusercontent.com", "client_type": 1, @@ -51,6 +59,14 @@ } }, "oauth_client": [ + { + "client_id": "100907092477-8vgakbtass73c6dad5mqflq2dd4h4904.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "deckers.thibault.aves.debug", + "certificate_hash": "744592fedb021fd82372966a6cb30569579fa9cc" + } + }, { "client_id": "100907092477-u7sm8gp5t2sotn42oq20ufhtn3craodu.apps.googleusercontent.com", "client_type": 3 @@ -80,6 +96,14 @@ } }, "oauth_client": [ + { + "client_id": "100907092477-4a6968gloaaq70uti1offkk7raduond6.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "deckers.thibault.aves.profile", + "certificate_hash": "744592fedb021fd82372966a6cb30569579fa9cc" + } + }, { "client_id": "100907092477-u7sm8gp5t2sotn42oq20ufhtn3craodu.apps.googleusercontent.com", "client_type": 3 diff --git a/android/build.gradle b/android/build.gradle index 4f36b3be7..83edf9619 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -10,7 +10,7 @@ buildscript { classpath 'com.android.tools.build:gradle:3.6.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.4' - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.3.0' + classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1' } } diff --git a/pubspec.lock b/pubspec.lock index d6168ff0a..8da6ecfb9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -395,7 +395,7 @@ packages: name: google_maps_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.4" + version: "1.0.5" highlight: dependency: transitive description: @@ -1116,7 +1116,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "1.7.3" + version: "1.7.4" wkt_parser: dependency: transitive description: From dea00555e9adbac92c2fb0abe07d41740d002273 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 18 Nov 2020 12:59:32 +0900 Subject: [PATCH 07/33] check free space before move, copy & album renaming --- .../aves/channel/calls/StorageHandler.kt | 30 +++++++++ lib/services/android_file_service.dart | 13 ++++ .../selection_action_delegate.dart | 5 +- .../common/action_delegates/size_aware.dart | 51 +++++++++++++++ lib/widgets/debug/storage.dart | 63 +++++++++++++------ .../common/chip_action_delegate.dart | 5 +- 6 files changed, 147 insertions(+), 20 deletions(-) create mode 100644 lib/widgets/common/action_delegates/size_aware.dart diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt index 5115f7c70..ab2147817 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/StorageHandler.kt @@ -28,6 +28,7 @@ class StorageHandler(private val context: Context) : MethodCallHandler { } result.success(volumes) } + "getFreeSpace" -> getFreeSpace(call, result) "getGrantedDirectories" -> result.success(ArrayList(PermissionManager.getGrantedDirs(context))) "getInaccessibleDirectories" -> getInaccessibleDirectories(call, result) "revokeDirectoryAccess" -> revokeDirectoryAccess(call, result) @@ -62,6 +63,35 @@ class StorageHandler(private val context: Context) : MethodCallHandler { return volumes } + private fun getFreeSpace(call: MethodCall, result: MethodChannel.Result) { + val path = call.argument("path") + if (path == null) { + result.error("getFreeSpace-args", "failed because of missing arguments", null) + return + } + + val sm = context.getSystemService(StorageManager::class.java) + if (sm == null) { + result.error("getFreeSpace-sm", "failed because of missing Storage Manager", null) + return + } + + val file = File(path) + val volume = sm.getStorageVolume(file) + if (volume == null) { + result.error("getFreeSpace-volume", "failed because of missing volume for path=$path", null) + return + } + + // `StorageStatsManager` `getFreeBytes()` is only available from API 26, + // and non-primary volume UUIDs cannot be used with it + try { + result.success(file.freeSpace) + } catch (e: SecurityException) { + result.error("getFreeSpace-security", "failed because of missing access", e.message) + } + } + private fun getInaccessibleDirectories(call: MethodCall, result: MethodChannel.Result) { val dirPaths = call.argument>("dirPaths") if (dirPaths == null) { diff --git a/lib/services/android_file_service.dart b/lib/services/android_file_service.dart index 979bb1331..0a0bb2f12 100644 --- a/lib/services/android_file_service.dart +++ b/lib/services/android_file_service.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:streams_channel/streams_channel.dart'; @@ -18,6 +19,18 @@ class AndroidFileService { return []; } + static Future getFreeSpace(StorageVolume volume) async { + try { + final result = await platform.invokeMethod('getFreeSpace', { + 'path': volume.path, + }); + return result as int; + } on PlatformException catch (e) { + debugPrint('getFreeSpace failed with code=${e.code}, exception=${e.message}, details=${e.details}}'); + } + return 0; + } + static Future> getGrantedDirectories() async { try { final result = await platform.invokeMethod('getGrantedDirectories'); diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 837776778..966ed4272 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -12,6 +12,7 @@ import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart'; import 'package:aves/widgets/common/action_delegates/feedback.dart'; import 'package:aves/widgets/common/action_delegates/permission_aware.dart'; +import 'package:aves/widgets/common/action_delegates/size_aware.dart'; import 'package:aves/widgets/common/aves_dialog.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/icons.dart'; @@ -25,7 +26,7 @@ import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; -class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin { +class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionLens collection; SelectionActionDelegate({ @@ -116,6 +117,8 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin { final selection = collection.selection.toList(); if (!await checkStoragePermission(context, selection)) return; + if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, copy)) return; + showOpReport( context: context, selection: selection, diff --git a/lib/widgets/common/action_delegates/size_aware.dart b/lib/widgets/common/action_delegates/size_aware.dart new file mode 100644 index 000000000..4198846bb --- /dev/null +++ b/lib/widgets/common/action_delegates/size_aware.dart @@ -0,0 +1,51 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:aves/model/image_entry.dart'; +import 'package:aves/services/android_file_service.dart'; +import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/utils/file_utils.dart'; +import 'package:aves/widgets/common/aves_dialog.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +mixin SizeAwareMixin { + Future checkFreeSpaceForMove(BuildContext context, List selection, String destinationAlbum, bool copy) async { + final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum); + final free = await AndroidFileService.getFreeSpace(destinationVolume); + int needed; + int sumSize(sum, entry) => sum + entry.sizeBytes; + if (copy) { + needed = selection.fold(0, sumSize); + } else { + // when moving, we only need space for the entries that are not already on the destination volume + final byVolume = groupBy(selection, (entry) => androidFileUtils.getStorageVolume(entry.path)); + final otherVolumes = byVolume.keys.where((volume) => volume != destinationVolume); + final fromOtherVolumes = otherVolumes.fold(0, (sum, volume) => sum + byVolume[volume].fold(0, sumSize)); + // and we need at least as much space as the largest entry because individual entries are copied then deleted + final largestSingle = selection.fold(0, (largest, entry) => max(largest, entry.sizeBytes)); + needed = max(fromOtherVolumes, largestSingle); + } + + final hasEnoughSpace = needed < free; + if (!hasEnoughSpace) { + await showDialog( + context: context, + builder: (context) { + return AvesDialog( + title: 'Not Enough Space', + content: Text('This operation needs ${formatFilesize(needed)} of free space on “${destinationVolume.description}” to complete, but there is only ${formatFilesize(free)} left.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('OK'.toUpperCase()), + ), + ], + ); + }, + ); + } + return hasEnoughSpace; + } +} diff --git a/lib/widgets/debug/storage.dart b/lib/widgets/debug/storage.dart index 804f2f1d1..1e62b90bb 100644 --- a/lib/widgets/debug/storage.dart +++ b/lib/widgets/debug/storage.dart @@ -1,32 +1,59 @@ +import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/android_file_utils.dart'; +import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/aves_expansion_tile.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:flutter/material.dart'; -class DebugStorageSection extends StatelessWidget { +class DebugStorageSection extends StatefulWidget { + @override + _DebugStorageSectionState createState() => _DebugStorageSectionState(); +} + +class _DebugStorageSectionState extends State with AutomaticKeepAliveClientMixin { + final Map _freeSpaceByVolume = {}; + + @override + void initState() { + super.initState(); + androidFileUtils.storageVolumes.forEach((volume) async { + final byteCount = await AndroidFileService.getFreeSpace(volume); + setState(() => _freeSpaceByVolume[volume.path] = byteCount); + }); + } + @override Widget build(BuildContext context) { + super.build(context); + return AvesExpansionTile( title: 'Storage Volumes', children: [ - ...androidFileUtils.storageVolumes.expand((v) => [ - Padding( - padding: EdgeInsets.all(8), - child: Text(v.path), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 8), - child: InfoRowGroup({ - 'description': '${v.description}', - 'isEmulated': '${v.isEmulated}', - 'isPrimary': '${v.isPrimary}', - 'isRemovable': '${v.isRemovable}', - 'state': '${v.state}', - }), - ), - Divider(), - ]) + ...androidFileUtils.storageVolumes.expand((v) { + final freeSpace = _freeSpaceByVolume[v.path]; + return [ + Padding( + padding: EdgeInsets.all(8), + child: Text(v.path), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 8), + child: InfoRowGroup({ + 'description': '${v.description}', + 'isEmulated': '${v.isEmulated}', + 'isPrimary': '${v.isPrimary}', + 'isRemovable': '${v.isRemovable}', + 'state': '${v.state}', + if (freeSpace != null) 'freeSpace': formatFilesize(freeSpace), + }), + ), + Divider(), + ]; + }) ], ); } + + @override + bool get wantKeepAlive => true; } diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 135443064..248e966da 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -7,6 +7,7 @@ import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/action_delegates/feedback.dart'; import 'package:aves/widgets/common/action_delegates/permission_aware.dart'; import 'package:aves/widgets/common/action_delegates/rename_album_dialog.dart'; +import 'package:aves/widgets/common/action_delegates/size_aware.dart'; import 'package:aves/widgets/common/aves_dialog.dart'; import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; import 'package:flutter/material.dart'; @@ -33,7 +34,7 @@ class ChipActionDelegate { } } -class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin { +class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionSource source; AlbumChipActionDelegate({ @@ -113,6 +114,8 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per final selection = source.rawEntries.where(filter.filter).toList(); final destinationAlbum = path.join(path.dirname(album), newName); + if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, false)) return; + showOpReport( context: context, selection: selection, From d28ea44ff22970e8acffaea69b166826c8b9da55 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 18 Nov 2020 14:53:48 +0900 Subject: [PATCH 08/33] improved metadata refreshing to include initial store data --- lib/model/image_entry.dart | 1 + lib/model/metadata_db.dart | 6 +++-- lib/model/source/collection_lens.dart | 9 ------- lib/model/source/collection_source.dart | 8 ++++-- lib/widgets/collection/app_bar.dart | 6 +---- .../entry_action_delegate.dart | 8 +++--- .../common/action_delegates/feedback.dart | 2 +- .../action_delegates/permission_aware.dart | 2 +- .../selection_action_delegate.dart | 26 +++++++------------ .../common/action_delegates/size_aware.dart | 2 +- .../media_store_collection_provider.dart | 13 ++++++++-- .../common/chip_action_delegate.dart | 4 +-- .../common/chip_set_action_delegate.dart | 6 +---- lib/widgets/fullscreen/debug/db.dart | 15 ----------- lib/widgets/fullscreen/image_view.dart | 7 ++--- 15 files changed, 47 insertions(+), 68 deletions(-) diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 07b64f7e7..ba7d520cc 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -23,6 +23,7 @@ class ImageEntry { String _path, _directory, _filename, _extension; int contentId; final String sourceMimeType; + // TODO TLAD use SVG viewport as width/height int width; int height; diff --git a/lib/model/metadata_db.dart b/lib/model/metadata_db.dart index 721659cd1..681a38605 100644 --- a/lib/model/metadata_db.dart +++ b/lib/model/metadata_db.dart @@ -143,7 +143,7 @@ class MetadataDb { await init(); } - void removeIds(List contentIds) async { + void removeIds(Set contentIds, {@required bool updateFavourites}) async { if (contentIds == null || contentIds.isEmpty) return; final stopwatch = Stopwatch()..start(); @@ -157,7 +157,9 @@ class MetadataDb { batch.delete(dateTakenTable, where: where, whereArgs: whereArgs); batch.delete(metadataTable, where: where, whereArgs: whereArgs); batch.delete(addressTable, where: where, whereArgs: whereArgs); - batch.delete(favouriteTable, where: where, whereArgs: whereArgs); + if (updateFavourites) { + batch.delete(favouriteTable, where: where, whereArgs: whereArgs); + } }); await batch.commit(noResult: true); debugPrint('$runtimeType removeIds complete in ${stopwatch.elapsed.inMilliseconds}ms for ${contentIds.length} entries'); diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index 21b169c91..f70a7dd4b 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -4,7 +4,6 @@ import 'dart:collection'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/utils/change_notifier.dart'; @@ -50,14 +49,6 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel super.dispose(); } - factory CollectionLens.empty() { - return CollectionLens( - source: CollectionSource(), - groupFactor: settings.collectionGroupFactor, - sortFactor: settings.collectionSortFactor, - ); - } - CollectionLens derive(CollectionFilter filter) { return CollectionLens( source: source, diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 7c99427bd..3c179ab25 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -37,7 +37,7 @@ mixin SourceBase { void setProgress({@required int done, @required int total}) => _progressStreamController.add(ProgressEvent(done: done, total: total)); } -class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { +abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { @override List get sortedEntriesForFilterList => CollectionLens( source: this, @@ -109,7 +109,7 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { } void updateAfterMove({ - @required List selection, + @required Set selection, @required bool copy, @required String destinationAlbum, @required Iterable movedOps, @@ -163,6 +163,10 @@ class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagMixin { int count(CollectionFilter filter) { return _filterEntryCountMap.putIfAbsent(filter, () => _rawEntries.where((entry) => filter.filter(entry)).length); } + + Future refresh(); + + Future refreshMetadata(Set entries); } enum SourceState { loading, cataloguing, locating, ready } diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 6329c68f4..7642e7cf3 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -14,7 +14,6 @@ import 'package:aves/widgets/common/action_delegates/selection_action_delegate.d import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; import 'package:aves/widgets/common/aves_selection_dialog.dart'; -import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/menu_row.dart'; @@ -290,10 +289,7 @@ class _CollectionAppBarState extends State with SingleTickerPr _actionDelegate.onCollectionActionSelected(context, action); break; case CollectionAction.refresh: - if (source is MediaStoreSource) { - source.clearEntries(); - unawaited((source as MediaStoreSource).refresh()); - } + unawaited(source.refresh()); break; case CollectionAction.select: collection.select(); diff --git a/lib/widgets/common/action_delegates/entry_action_delegate.dart b/lib/widgets/common/action_delegates/entry_action_delegate.dart index de7a06885..ec4ccd357 100644 --- a/lib/widgets/common/action_delegates/entry_action_delegate.dart +++ b/lib/widgets/common/action_delegates/entry_action_delegate.dart @@ -128,14 +128,14 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { } Future _flip(BuildContext context, ImageEntry entry) async { - if (!await checkStoragePermission(context, [entry])) return; + if (!await checkStoragePermission(context, {entry})) return; final success = await entry.flip(); if (!success) showFeedback(context, 'Failed'); } Future _rotate(BuildContext context, ImageEntry entry, {@required bool clockwise}) async { - if (!await checkStoragePermission(context, [entry])) return; + if (!await checkStoragePermission(context, {entry})) return; final success = await entry.rotate(clockwise: clockwise); if (!success) showFeedback(context, 'Failed'); @@ -162,7 +162,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { ); if (confirmed == null || !confirmed) return; - if (!await checkStoragePermission(context, [entry])) return; + if (!await checkStoragePermission(context, {entry})) return; if (!await entry.delete()) { showFeedback(context, 'Failed'); @@ -185,7 +185,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { ); if (newName == null || newName.isEmpty) return; - if (!await checkStoragePermission(context, [entry])) return; + if (!await checkStoragePermission(context, {entry})) return; showFeedback(context, await entry.rename(newName) ? 'Done!' : 'Failed'); } diff --git a/lib/widgets/common/action_delegates/feedback.dart b/lib/widgets/common/action_delegates/feedback.dart index f103df3cd..4f87464b0 100644 --- a/lib/widgets/common/action_delegates/feedback.dart +++ b/lib/widgets/common/action_delegates/feedback.dart @@ -31,7 +31,7 @@ mixin FeedbackMixin { void showOpReport({ @required BuildContext context, - @required List selection, + @required Set selection, @required Stream opStream, @required void Function(Set processed) onDone, }) { diff --git a/lib/widgets/common/action_delegates/permission_aware.dart b/lib/widgets/common/action_delegates/permission_aware.dart index 18ba08630..13098a4d4 100644 --- a/lib/widgets/common/action_delegates/permission_aware.dart +++ b/lib/widgets/common/action_delegates/permission_aware.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import '../aves_dialog.dart'; mixin PermissionAwareMixin { - Future checkStoragePermission(BuildContext context, Iterable entries) { + Future checkStoragePermission(BuildContext context, Set entries) { return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).toSet()); } diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 966ed4272..b089d1f89 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:aves/model/filters/album.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_source.dart'; @@ -29,6 +30,10 @@ import 'package:provider/provider.dart'; class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionLens collection; + CollectionSource get source => collection.source; + + Set get selection => collection.selection; + SelectionActionDelegate({ @required this.collection, }); @@ -39,7 +44,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar _showDeleteDialog(context); break; case EntryAction.share: - AndroidAppService.share(collection.selection); + AndroidAppService.share(selection); break; default: break; @@ -55,7 +60,9 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar _moveSelection(context, copy: false); break; case CollectionAction.refreshMetadata: - _refreshSelectionMetadata(); + source.refreshMetadata(selection); + collection.clearSelection(); + collection.browse(); break; default: break; @@ -63,7 +70,6 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar } Future _moveSelection(BuildContext context, {@required bool copy}) async { - final source = collection.source; final chipSetActionDelegate = AlbumChipSetActionDelegate(source: source); final destinationAlbum = await Navigator.push( context, @@ -114,7 +120,6 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar if (destinationAlbum == null || destinationAlbum.isEmpty) return; if (!await checkStoragePermissionForAlbums(context, {destinationAlbum})) return; - final selection = collection.selection.toList(); if (!await checkStoragePermission(context, selection)) return; if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, copy)) return; @@ -146,18 +151,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar ); } - Future _refreshSelectionMetadata() async { - collection.selection.forEach((entry) => entry.clearMetadata()); - final source = collection.source; - source.stateNotifier.value = SourceState.cataloguing; - await source.catalogEntries(); - source.stateNotifier.value = SourceState.locating; - await source.locateEntries(); - source.stateNotifier.value = SourceState.ready; - } - Future _showDeleteDialog(BuildContext context) async { - final selection = collection.selection.toList(); final count = selection.length; final confirmed = await showDialog( @@ -195,7 +189,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar showFeedback(context, 'Failed to delete ${Intl.plural(count, one: '$count item', other: '$count items')}'); } if (deletedCount > 0) { - collection.source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)).toList()); + source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)).toList()); } collection.clearSelection(); collection.browse(); diff --git a/lib/widgets/common/action_delegates/size_aware.dart b/lib/widgets/common/action_delegates/size_aware.dart index 4198846bb..f9af342c7 100644 --- a/lib/widgets/common/action_delegates/size_aware.dart +++ b/lib/widgets/common/action_delegates/size_aware.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; mixin SizeAwareMixin { - Future checkFreeSpaceForMove(BuildContext context, List selection, String destinationAlbum, bool copy) async { + Future checkFreeSpaceForMove(BuildContext context, Set selection, String destinationAlbum, bool copy) async { final destinationVolume = androidFileUtils.getStorageVolume(destinationAlbum); final free = await AndroidFileService.getFreeSpace(destinationVolume); int needed; diff --git a/lib/widgets/common/data_providers/media_store_collection_provider.dart b/lib/widgets/common/data_providers/media_store_collection_provider.dart index ec6ab0e35..c0afae1aa 100644 --- a/lib/widgets/common/data_providers/media_store_collection_provider.dart +++ b/lib/widgets/common/data_providers/media_store_collection_provider.dart @@ -31,14 +31,16 @@ class MediaStoreSource extends CollectionSource { debugPrint('$runtimeType init done, elapsed=${stopwatch.elapsed}'); } + @override Future refresh() async { debugPrint('$runtimeType refresh start'); final stopwatch = Stopwatch()..start(); stateNotifier.value = SourceState.loading; + clearEntries(); final oldEntries = await metadataDb.loadEntries(); // 400ms for 5500 entries final knownEntryMap = Map.fromEntries(oldEntries.map((entry) => MapEntry(entry.contentId, entry.dateModifiedSecs))); - final obsoleteEntries = await ImageFileService.getObsoleteEntries(knownEntryMap.keys.toList()); + final obsoleteEntries = (await ImageFileService.getObsoleteEntries(knownEntryMap.keys.toList())).toSet(); oldEntries.removeWhere((entry) => obsoleteEntries.contains(entry.contentId)); // show known entries @@ -48,7 +50,7 @@ class MediaStoreSource extends CollectionSource { debugPrint('$runtimeType refresh loaded ${oldEntries.length} known entries, elapsed=${stopwatch.elapsed}'); // clean up obsolete entries - metadataDb.removeIds(obsoleteEntries); + metadataDb.removeIds(obsoleteEntries, updateFavourites: true); // fetch new entries var refreshCount = 10; @@ -92,4 +94,11 @@ class MediaStoreSource extends CollectionSource { onError: (error) => debugPrint('$runtimeType stream error=$error'), ); } + + @override + Future refreshMetadata(Set entries) { + final contentIds = entries.map((entry) => entry.contentId).toSet(); + metadataDb.removeIds(contentIds, updateFavourites: false); + return refresh(); + } } diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 248e966da..5b842db7b 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -57,7 +57,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per } Future _showDeleteDialog(BuildContext context, AlbumFilter filter) async { - final selection = source.rawEntries.where(filter.filter).toList(); + final selection = source.rawEntries.where(filter.filter).toSet(); final count = selection.length; final confirmed = await showDialog( @@ -111,7 +111,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per if (!await checkStoragePermissionForAlbums(context, {album})) return; - final selection = source.rawEntries.where(filter.filter).toList(); + final selection = source.rawEntries.where(filter.filter).toSet(); final destinationAlbum = path.join(path.dirname(album), newName); if (!await checkFreeSpaceForMove(context, selection, destinationAlbum, false)) return; diff --git a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart index 29911ebd0..39efb54a6 100644 --- a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart @@ -4,7 +4,6 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/durations.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/filter_grids/common/chip_actions.dart'; import 'package:aves/widgets/stats/stats.dart'; import 'package:flutter/material.dart'; @@ -27,10 +26,7 @@ abstract class ChipSetActionDelegate { await _showSortDialog(context); break; case ChipSetAction.refresh: - if (source is MediaStoreSource) { - source.clearEntries(); - unawaited((source as MediaStoreSource).refresh()); - } + unawaited(source.refresh()); break; case ChipSetAction.stats: _goToStats(context); diff --git a/lib/widgets/fullscreen/debug/db.dart b/lib/widgets/fullscreen/debug/db.dart index ce2b89699..4abe39a68 100644 --- a/lib/widgets/fullscreen/debug/db.dart +++ b/lib/widgets/fullscreen/debug/db.dart @@ -41,21 +41,6 @@ class _DbTabState extends State { return ListView( padding: EdgeInsets.all(16), children: [ - Row( - children: [ - Expanded( - child: Text('DB'), - ), - SizedBox(width: 8), - ElevatedButton( - onPressed: () async { - await metadataDb.removeIds([entry.contentId]); - _loadDatabase(); - }, - child: Text('Remove from DB'), - ), - ], - ), FutureBuilder( future: _dbDateLoader, builder: (context, snapshot) { diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index 327cc27c2..f1e57d2ca 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -72,7 +72,9 @@ class _ImageViewState extends State { Widget build(BuildContext context) { Widget child; if (entry.isVideo) { - child = _buildVideoView(); + if (entry.width > 0 && entry.height > 0) { + child = _buildVideoView(); + } } else if (entry.isSvg) { child = _buildSvgView(); } else if (entry.canDecode) { @@ -81,9 +83,8 @@ class _ImageViewState extends State { } else { child = _buildImageView(); } - } else { - child = _buildError(); } + child ??= _buildError(); // if the hero tag is defined in the `loadingBuilder` and also set by the `heroAttributes`, // the route transition becomes visible if the final image is loaded before the hero animation is done. From be2c9ed9143922edffdb093a7bfd5cfa0d255131 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 18 Nov 2020 15:51:53 +0900 Subject: [PATCH 09/33] fixed size for videos unsupported by MediaMetadataRetriever but supported by metadata-extractor --- .../thibault/aves/model/SourceImageEntry.kt | 17 ++++++++++------- lib/widgets/collection/thumbnail/raster.dart | 19 +++++++++---------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt index 44b153617..aaf961622 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/model/SourceImageEntry.kt @@ -122,17 +122,15 @@ class SourceImageEntry { if (isVideo) { fillVideoByMediaMetadataRetriever(context) if (isSized && hasDuration) return this - } - // skip metadata-extractor for raw images because it reports the decoded dimensions instead of the raw dimensions - if (!MimeTypes.isRaw(sourceMimeType) && MimeTypes.isSupportedByMetadataExtractor(sourceMimeType)) { + fillByMetadataExtractor(context) + } else { fillByMetadataExtractor(context) if (isSized && foundExif) return this - } - if (ExifInterface.isSupportedMimeType(sourceMimeType)) { fillByExifInterface(context) - if (isSized) return this } - fillByBitmapDecode(context) + if (!isSized) { + fillByBitmapDecode(context) + } return this } @@ -156,6 +154,9 @@ class SourceImageEntry { // finds: width, height, orientation, date, duration private fun fillByMetadataExtractor(context: Context) { + // skip raw images because `metadata-extractor` reports the decoded dimensions instead of the raw dimensions + if (!MimeTypes.isSupportedByMetadataExtractor(sourceMimeType) || MimeTypes.isRaw(sourceMimeType)) return + try { StorageUtils.openInputStream(context, uri)?.use { input -> val metadata = ImageMetadataReader.readMetadata(input) @@ -206,6 +207,8 @@ class SourceImageEntry { // finds: width, height, orientation, date private fun fillByExifInterface(context: Context) { + if (!ExifInterface.isSupportedMimeType(sourceMimeType)) return; + try { StorageUtils.openInputStream(context, uri)?.use { input -> val exif = ExifInterface(input) diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index 05d7ce36f..0b70212dd 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -98,16 +98,13 @@ class _ThumbnailRasterImageState extends State { @override Widget build(BuildContext context) { if (!entry.canDecode) { - return ErrorThumbnail( - entry: entry, - extent: extent, - tooltip: '${entry.mimeType} not supported', - ); + return _buildError(context, '${entry.mimeType} not supported', null); } final fastImage = Image( key: ValueKey('LQ'), image: _fastThumbnailProvider, + errorBuilder: _buildError, width: extent, height: extent, fit: BoxFit.cover, @@ -137,11 +134,7 @@ class _ThumbnailRasterImageState extends State { child: frame == null ? fastImage : child, ); }, - errorBuilder: (context, error, stackTrace) => ErrorThumbnail( - entry: entry, - extent: extent, - tooltip: error.toString(), - ), + errorBuilder: _buildError, width: extent, height: extent, fit: BoxFit.cover, @@ -173,6 +166,12 @@ class _ThumbnailRasterImageState extends State { ); } + Widget _buildError(BuildContext context, Object error, StackTrace stackTrace) => ErrorThumbnail( + entry: entry, + extent: extent, + tooltip: error.toString(), + ); + // when the entry image itself changed (e.g. after rotation) void _onImageChanged() async { // rebuild to refresh the thumbnails From ba031a0144e5cb039f3e2f4a8e2d205adbf7d05e Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 18 Nov 2020 15:53:24 +0900 Subject: [PATCH 10/33] workaround for Firebase "Service not registered" issue --- android/app/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android/app/build.gradle b/android/app/build.gradle index 775910c2c..8880a3e39 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -109,6 +109,9 @@ dependencies { implementation 'com.github.deckerst:Android-TiffBitmapFactory:7efb450636' implementation 'com.github.bumptech.glide:glide:4.11.0' + // TODO TLAD remove when this is fixed: https://github.com/firebase/firebase-android-sdk/issues/1662 https://github.com/FirebaseExtended/flutterfire/issues/3990 + implementation 'com.google.firebase:firebase-analytics:18.0.0' + kapt 'androidx.annotation:annotation:1.1.0' kapt 'com.github.bumptech.glide:compiler:4.11.0' From ced286186094a041c158bfc6376ae29814070052 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 19 Nov 2020 10:54:41 +0900 Subject: [PATCH 11/33] info: improved layout --- lib/widgets/fullscreen/info/common.dart | 84 ++++++++++++++++++------- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/lib/widgets/fullscreen/info/common.dart b/lib/widgets/fullscreen/info/common.dart index a91078d9d..5df0ffc7c 100644 --- a/lib/widgets/fullscreen/info/common.dart +++ b/lib/widgets/fullscreen/info/common.dart @@ -1,6 +1,9 @@ +import 'dart:math'; + import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; class SectionRow extends StatelessWidget { final IconData icon; @@ -54,36 +57,71 @@ class _InfoRowGroupState extends State { int get maxValueLength => widget.maxValueLength; + static const keyValuePadding = 16; + static final baseStyle = TextStyle(fontFamily: 'Concourse'); + static final keyStyle = baseStyle.copyWith(color: Colors.white70, height: 1.7); + @override Widget build(BuildContext context) { if (keyValues.isEmpty) return SizedBox.shrink(); + + // compute the size of keys and space in order to align values + final textScaleFactor = MediaQuery.textScaleFactorOf(context); + final keySizes = Map.fromEntries(keyValues.keys.map((key) => MapEntry(key, _getSpanWidth(TextSpan(text: '$key', style: keyStyle), textScaleFactor)))); + final baseSpaceWidth = _getSpanWidth(TextSpan(text: '\u200A' * 100, style: baseStyle), textScaleFactor); + final lastKey = keyValues.keys.last; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText.rich( - TextSpan( - children: keyValues.entries.expand( - (kv) { - final key = kv.key; - var value = kv.value; - final showPreviewOnly = maxValueLength > 0 && value.length > maxValueLength && !_expandedKeys.contains(key); - if (showPreviewOnly) { - value = '${value.substring(0, maxValueLength)}…'; - } - return [ - TextSpan(text: '$key ', style: TextStyle(color: Colors.white70, height: 1.7)), - TextSpan(text: '$value${key == lastKey ? '' : '\n'}', recognizer: showPreviewOnly ? _buildTapRecognizer(key) : null), - ]; - }, - ).toList(), - ), - style: TextStyle(fontFamily: 'Concourse'), - ), - ], + return LayoutBuilder( + builder: (context, constraints) { + // find longest key below threshold + final maxBaseValueX = constraints.maxWidth / 3; + final baseValueX = keySizes.values.where((size) => size < maxBaseValueX).fold(0.0, max); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText.rich( + TextSpan( + children: keyValues.entries.expand( + (kv) { + final key = kv.key; + var value = kv.value; + // long values are clipped, and made expandable by tapping them + final showPreviewOnly = maxValueLength > 0 && value.length > maxValueLength && !_expandedKeys.contains(key); + if (showPreviewOnly) { + value = '${value.substring(0, maxValueLength)}…'; + } + + // as of Flutter v1.22.4, `SelectableText` cannot contain `WidgetSpan` + // so we add padding using multiple hair spaces instead + final thisSpaceSize = max(0.0, (baseValueX - keySizes[key])) + keyValuePadding; + final spaceCount = (100 * thisSpaceSize / baseSpaceWidth).round(); + + return [ + TextSpan(text: '$key', style: keyStyle), + TextSpan(text: '\u200A' * spaceCount), + TextSpan(text: '$value${key == lastKey ? '' : '\n'}', recognizer: showPreviewOnly ? _buildTapRecognizer(key) : null), + ]; + }, + ).toList(), + ), + style: baseStyle, + ), + ], + ); + }, ); } + double _getSpanWidth(TextSpan span, double textScaleFactor) { + final para = RenderParagraph( + span, + textDirection: TextDirection.ltr, + textScaleFactor: textScaleFactor, + )..layout(BoxConstraints(), parentUsesSize: true); + return para.getMaxIntrinsicWidth(double.infinity); + } + GestureRecognizer _buildTapRecognizer(String key) { return TapGestureRecognizer()..onTap = () => setState(() => _expandedKeys.add(key)); } From 258d06198d472ed5c0bc5201f10e6964a837fb83 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 19 Nov 2020 15:37:52 +0900 Subject: [PATCH 12/33] about: changed layout, added credits, added generated license list --- lib/utils/constants.dart | 25 ++++--- lib/widgets/about/about_page.dart | 67 ++++++++++++++----- lib/widgets/about/licenses.dart | 58 +++++++++------- lib/widgets/common/aves_expansion_tile.dart | 3 + lib/widgets/common/highlight_title.dart | 4 +- .../fullscreen/info/location_section.dart | 2 +- pubspec.lock | 2 +- pubspec.yaml | 1 - 8 files changed, 108 insertions(+), 54 deletions(-) diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 8cf139144..1d20ed1e5 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -3,6 +3,9 @@ import 'package:flutter/painting.dart'; import 'package:tuple/tuple.dart'; class Constants { + static const Color androidGreen = Color(0xFF3DDC84); + static const Color flutterBlue = Color(0xFF47D1FD); + // as of Flutter v1.22.3, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped // so we give it a `strutStyle` with a slightly larger height static const overflowStrutStyle = StrutStyle(height: 1.3); @@ -26,6 +29,18 @@ class Constants { static const int infoGroupMaxValueLength = 140; static const List androidDependencies = [ + Dependency( + name: 'AndroidX Core-KTX', + license: 'Apache 2.0', + licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/LICENSE.txt', + sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/core/core-ktx', + ), + Dependency( + name: 'AndroidX Exifinterface', + license: 'Apache 2.0', + licenseUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/LICENSE.txt', + sourceUrl: 'https://android.googlesource.com/platform/frameworks/support/+/androidx-master-dev/exifinterface/exifinterface', + ), Dependency( name: 'Android-TiffBitmapFactory', license: 'MIT', @@ -96,7 +111,7 @@ class Constants { sourceUrl: 'https://github.com/Skylled/expansion_tile_card', ), Dependency( - name: 'FlutterFire', + name: 'FlutterFire (Core, Analytics, Crashlytics)', license: 'BSD 3-Clause', licenseUrl: 'https://github.com/FirebaseExtended/flutterfire/blob/master/LICENSE', sourceUrl: 'https://github.com/FirebaseExtended/flutterfire', @@ -186,7 +201,7 @@ class Constants { sourceUrl: 'https://github.com/boyan01/overlay_support', ), Dependency( - name: 'Package info', + name: 'Package Info', license: 'BSD 3-Clause', licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/package_info/LICENSE', sourceUrl: 'https://github.com/flutter/plugins/tree/master/packages/package_info', @@ -275,12 +290,6 @@ class Constants { licenseUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/LICENSE', sourceUrl: 'https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher', ), - Dependency( - name: 'UUID', - license: 'MIT', - licenseUrl: 'https://github.com/Daegalus/dart-uuid/blob/master/LICENSE', - sourceUrl: 'https://github.com/Daegalus/dart-uuid', - ), ]; } diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index 6e35112f6..a2bba1eee 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -3,7 +3,6 @@ import 'package:aves/widgets/about/licenses.dart'; import 'package:aves/widgets/common/aves_logo.dart'; import 'package:aves/widgets/common/link_chip.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:package_info/package_info.dart'; class AboutPage extends StatelessWidget { @@ -16,24 +15,58 @@ class AboutPage extends StatelessWidget { title: Text('About'), ), body: SafeArea( - child: AnimationLimiter( - child: CustomScrollView( - slivers: [ - SliverPadding( - padding: EdgeInsets.only(top: 16), - sliver: SliverList( - delegate: SliverChildListDelegate( - [ - AppReference(), - SizedBox(height: 16), - Divider(), - ], - ), + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.only(top: 16), + sliver: SliverList( + delegate: SliverChildListDelegate( + [ + AppReference(), + SizedBox(height: 16), + Divider(), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ConstrainedBox( + constraints: BoxConstraints(minHeight: 48), + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text( + 'Credits', + style: Theme.of(context).textTheme.headline6.copyWith(fontFamily: 'Concourse Caps'), + ), + ), + ), + RichText( + text: TextSpan( + children: [ + TextSpan(text: 'This app uses the font '), + WidgetSpan( + child: LinkChip( + text: 'Concourse', + url: 'https://mbtype.com/fonts/concourse/', + textStyle: TextStyle(fontWeight: FontWeight.bold), + ), + alignment: PlaceholderAlignment.middle, + ), + TextSpan(text: ' for titles and the media information page.'), + ], + ), + ), + SizedBox(height: 16), + ], + ), + ), + Divider(), + ], ), ), - Licenses(), - ], - ), + ), + Licenses(), + ], ), ), ); diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index e8241cc70..f56456504 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -1,11 +1,10 @@ import 'package:aves/utils/constants.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/widgets/common/aves_expansion_tile.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/link_chip.dart'; import 'package:aves/widgets/common/menu_row.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; class Licenses extends StatefulWidget { @override @@ -13,18 +12,20 @@ class Licenses extends StatefulWidget { } class _LicensesState extends State { + final ValueNotifier _expandedNotifier = ValueNotifier(null); LicenseSort _sort = LicenseSort.name; - List _packages; + List _platform, _flutter; @override void initState() { super.initState(); - _packages = [...Constants.androidDependencies, ...Constants.flutterPackages]; + _platform = List.from(Constants.androidDependencies); + _flutter = List.from(Constants.flutterPackages); _sortPackages(); } void _sortPackages() { - _packages.sort((a, b) { + int compare(Dependency a, Dependency b) { switch (_sort) { case LicenseSort.license: final c = compareAsciiUpperCase(a.license, b.license); @@ -33,7 +34,10 @@ class _LicensesState extends State { default: return compareAsciiUpperCase(a.name, b.name); } - }); + } + + _platform.sort(compare); + _flutter.sort(compare); } @override @@ -41,25 +45,29 @@ class _LicensesState extends State { return SliverPadding( padding: EdgeInsets.symmetric(horizontal: 8), sliver: SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index-- == 0) { - return _buildHeader(); - } - final child = LicenseRow(_packages[index]); - return AnimationConfiguration.staggeredList( - position: index, - duration: Durations.staggeredAnimation, - delay: Durations.staggeredAnimationDelay, - child: SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: child, - ), + delegate: SliverChildListDelegate( + [ + _buildHeader(), + SizedBox(height: 16), + AvesExpansionTile( + title: 'Android Libraries', + color: Constants.androidGreen, + expandedNotifier: _expandedNotifier, + children: _platform.map((package) => LicenseRow(package)).toList(), + ), + AvesExpansionTile( + title: 'Flutter Packages', + color: Constants.flutterBlue, + expandedNotifier: _expandedNotifier, + children: _flutter.map((package) => LicenseRow(package)).toList(), + ), + Center( + child: TextButton( + onPressed: () => showLicensePage(context: context), + child: Text('All Licenses'.toUpperCase()), ), - ); - }, - childCount: _packages.length + 1, + ), + ], ), ), ); @@ -122,7 +130,7 @@ class LicenseRow extends StatelessWidget { final subColor = bodyTextStyle.color.withOpacity(.6); return Padding( - padding: EdgeInsets.only(top: 16), + padding: EdgeInsets.symmetric(vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/lib/widgets/common/aves_expansion_tile.dart b/lib/widgets/common/aves_expansion_tile.dart index 9155483ed..e1a60a9fd 100644 --- a/lib/widgets/common/aves_expansion_tile.dart +++ b/lib/widgets/common/aves_expansion_tile.dart @@ -4,11 +4,13 @@ import 'package:flutter/material.dart'; class AvesExpansionTile extends StatelessWidget { final String title; + final Color color; final List children; final ValueNotifier expandedNotifier; const AvesExpansionTile({ @required this.title, + this.color, this.expandedNotifier, @required this.children, }); @@ -27,6 +29,7 @@ class AvesExpansionTile extends StatelessWidget { expandedNotifier: expandedNotifier, title: HighlightTitle( title, + color: color, fontSize: 18, enabled: enabled, ), diff --git a/lib/widgets/common/highlight_title.dart b/lib/widgets/common/highlight_title.dart index 11c8ba448..f34e69880 100644 --- a/lib/widgets/common/highlight_title.dart +++ b/lib/widgets/common/highlight_title.dart @@ -4,11 +4,13 @@ import 'package:flutter/material.dart'; class HighlightTitle extends StatelessWidget { final String name; + final Color color; final double fontSize; final bool enabled; const HighlightTitle( this.name, { + this.color, this.fontSize = 20, this.enabled = true, }) : assert(name != null); @@ -21,7 +23,7 @@ class HighlightTitle extends StatelessWidget { alignment: AlignmentDirectional.centerStart, child: Container( decoration: HighlightDecoration( - color: enabled ? stringToColor(name) : disabledColor, + color: enabled ? color ?? stringToColor(name) : disabledColor, ), margin: EdgeInsets.symmetric(vertical: 4.0), child: Text( diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index c1c98445a..c6a1e3db2 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -173,7 +173,7 @@ extension ExtraEntryMapStyle on EntryMapStyle { case EntryMapStyle.googleTerrain: return 'Google Maps (Terrain)'; case EntryMapStyle.osmHot: - return 'Humanitarian OpenStreetMap'; + return 'Humanitarian OSM'; case EntryMapStyle.stamenToner: return 'Stamen Toner'; case EntryMapStyle.stamenWatercolor: diff --git a/pubspec.lock b/pubspec.lock index 8da6ecfb9..b505bdcad 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1048,7 +1048,7 @@ packages: source: hosted version: "0.9.0+5" uuid: - dependency: "direct main" + dependency: transitive description: name: uuid url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index a6b8ddc1f..696fa5589 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -93,7 +93,6 @@ dependencies: streams_channel: tuple: url_launcher: - uuid: dev_dependencies: flutter_test: From 37d575a1b33628a236c9145dace5de08480adedb Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 19 Nov 2020 19:29:40 +0900 Subject: [PATCH 13/33] info: split directories by parent if necessary, hide useless Exif dir, fixed XMP tag order --- .../aves/channel/calls/MetadataHandler.kt | 5 +- .../aves/metadata/ExifInterfaceHelper.kt | 5 +- .../fullscreen/info/metadata_section.dart | 60 ++++++++++++------- 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt index 446649bdb..b5f9197cd 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/channel/calls/MetadataHandler.kt @@ -103,7 +103,10 @@ class MetadataHandler(private val context: Context) : MethodCallHandler { for (dir in metadata.directories.filter { it.tagCount > 0 && it !is FileTypeDirectory }) { // directory name - val dirName = dir.name ?: "" + var dirName = dir.name + // optional parent to distinguish child directories of the same type + dir.parent?.name?.let { dirName = "$it/$dirName" } + val dirMap = metadataMap.getOrDefault(dirName, HashMap()) metadataMap[dirName] = dirMap diff --git a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt index e7815d942..9eb1de5c6 100644 --- a/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt +++ b/android/app/src/main/kotlin/deckers/thibault/aves/metadata/ExifInterfaceHelper.kt @@ -217,8 +217,11 @@ object ExifInterfaceHelper { // so that we can rely on metadata-extractor descriptions val dirs = DirType.values().map { Pair(it, it.createDirectory()) }.toMap() + // exclude Exif directory when it only includes image size + val isUselessExif: (Map) -> Boolean = { it.size == 2 && it.containsKey("Image Height") && it.containsKey("Image Width") } + return HashMap>().apply { - put("Exif", describeDir(exif, dirs, baseTags)) + put("Exif", describeDir(exif, dirs, baseTags).takeUnless(isUselessExif) ?: hashMapOf()) put("Exif Thumbnail", describeDir(exif, dirs, thumbnailTags)) put(Metadata.DIR_GPS, describeDir(exif, dirs, gpsTags)) put(Metadata.DIR_XMP, describeDir(exif, dirs, xmpTags)) diff --git a/lib/widgets/fullscreen/info/metadata_section.dart b/lib/widgets/fullscreen/info/metadata_section.dart index fccd18ed4..1ef58eeaa 100644 --- a/lib/widgets/fullscreen/info/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata_section.dart @@ -2,6 +2,7 @@ import 'dart:collection'; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/metadata_service.dart'; +import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/aves_expansion_tile.dart'; @@ -28,7 +29,7 @@ class MetadataSectionSliver extends StatefulWidget { } class _MetadataSectionSliverState extends State with AutomaticKeepAliveClientMixin { - List<_MetadataDirectory> _metadata = []; + Map _metadata = {}; final ValueNotifier _loadedMetadataUri = ValueNotifier(null); final ValueNotifier _expandedDirectoryNotifier = ValueNotifier(null); @@ -41,6 +42,10 @@ class _MetadataSectionSliverState extends State with Auto static const xmpDirectory = 'XMP'; // from metadata-extractor static const mediaDirectory = 'Media'; // additional media (video/audio/images) directory + // directory names may contain the name of their parent directory + // if so, they are separated by this character + static const parentChildSeparator = '/'; + @override void initState() { super.initState(); @@ -87,8 +92,6 @@ class _MetadataSectionSliverState extends State with Auto if (_metadata.isEmpty) { content = SizedBox.shrink(); } else { - final directoriesWithoutTitle = _metadata.where((dir) => dir.name.isEmpty).toList(); - final directoriesWithTitle = _metadata.where((dir) => dir.name.isNotEmpty).toList(); content = Column( children: AnimationConfiguration.toStaggeredList( duration: Durations.staggeredAnimation, @@ -101,8 +104,7 @@ class _MetadataSectionSliverState extends State with Auto ), children: [ SectionRow(AIcons.info), - ...directoriesWithoutTitle.map(_buildDirTileWithoutTitle), - ...directoriesWithTitle.map(_buildDirTileWithTitle), + ..._metadata.entries.map((kv) => _buildDirTile(kv.key, kv.value)), ], ), ); @@ -118,11 +120,7 @@ class _MetadataSectionSliverState extends State with Auto ); } - Widget _buildDirTileWithoutTitle(_MetadataDirectory dir) { - return InfoRowGroup(dir.tags, maxValueLength: Constants.infoGroupMaxValueLength); - } - - Widget _buildDirTileWithTitle(_MetadataDirectory dir) { + Widget _buildDirTile(String title, _MetadataDirectory dir) { if (dir.name == xmpDirectory) { return _buildXmpDirTile(dir); } @@ -151,7 +149,8 @@ class _MetadataSectionSliverState extends State with Auto } return AvesExpansionTile( - title: dir.name, + title: title, + color: stringToColor(dir.name), expandedNotifier: _expandedDirectoryNotifier, children: [ if (prefixChildren.isNotEmpty) Wrap(children: prefixChildren), @@ -173,7 +172,7 @@ class _MetadataSectionSliverState extends State with Auto if (i == -1) return ''; return fullKey.substring(0, i); }), - compareAsciiLowerCase, + compareAsciiUpperCase, ); return AvesExpansionTile( title: dir.name, @@ -188,10 +187,11 @@ class _MetadataSectionSliverState extends State with Auto final ns = kv.key; final hasNamespace = ns.isNotEmpty; final i = hasNamespace ? ns.length + 1 : 0; - final tags = Map.fromEntries(kv.value.map((kv) => MapEntry(kv.key.substring(i), kv.value))); + final entries = kv.value.map((kv) => MapEntry(kv.key.substring(i), kv.value)).toList(); + entries.sort((a, b) => compareAsciiUpperCaseNatural(a.key, b.key)); return [ if (hasNamespace) HighlightTitle(ns), - InfoRowGroup(tags, maxValueLength: Constants.infoGroupMaxValueLength), + InfoRowGroup(Map.fromEntries(entries), maxValueLength: Constants.infoGroupMaxValueLength), ]; }).toList(), ), @@ -202,7 +202,7 @@ class _MetadataSectionSliverState extends State with Auto void _onMetadataChanged() { _loadedMetadataUri.value = null; - _metadata = []; + _metadata = {}; _getMetadata(); } @@ -211,8 +211,16 @@ class _MetadataSectionSliverState extends State with Auto if (_loadedMetadataUri.value == entry.uri) return; if (isVisible) { final rawMetadata = await MetadataService.getAllMetadata(entry) ?? {}; - _metadata = rawMetadata.entries.map((dirKV) { - final directoryName = dirKV.key as String ?? ''; + final directories = rawMetadata.entries.map((dirKV) { + var directoryName = dirKV.key as String ?? ''; + + String parent; + final parts = directoryName.split(parentChildSeparator); + if (parts.length > 1) { + parent = parts[0]; + directoryName = parts[1]; + } + final rawTags = dirKV.value as Map ?? {}; final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) { final value = tagKV.value as String ?? ''; @@ -220,12 +228,21 @@ class _MetadataSectionSliverState extends State with Auto final tagName = tagKV.key as String ?? ''; return MapEntry(tagName, value); }).where((kv) => kv != null))); - return _MetadataDirectory(directoryName, tags); + return _MetadataDirectory(directoryName, parent, tags); + }).toList(); + + final titledDirectories = directories.map((dir) { + var title = dir.name; + if (directories.where((dir) => dir.name == title).length > 1 && dir.parent?.isNotEmpty == true) { + title = '${dir.parent}/$title'; + } + return MapEntry(title, dir); }).toList() - ..sort((a, b) => compareAsciiUpperCase(a.name, b.name)); + ..sort((a, b) => compareAsciiUpperCase(a.key, b.key)); + _metadata = Map.fromEntries(titledDirectories); _loadedMetadataUri.value = entry.uri; } else { - _metadata = []; + _metadata = {}; _loadedMetadataUri.value = null; } _expandedDirectoryNotifier.value = null; @@ -237,7 +254,8 @@ class _MetadataSectionSliverState extends State with Auto class _MetadataDirectory { final String name; + final String parent; final SplayTreeMap tags; - const _MetadataDirectory(this.name, this.tags); + const _MetadataDirectory(this.name, this.parent, this.tags); } From edc90f085cf80d17542e9657e1dba66ac0e16093 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 20 Nov 2020 12:24:23 +0900 Subject: [PATCH 14/33] info: improved XMP titles & keys --- lib/utils/brand_colors.dart | 21 +++++ lib/utils/constants.dart | 3 - lib/utils/xmp.dart | 27 +++++++ lib/widgets/about/licenses.dart | 5 +- lib/widgets/fullscreen/info/info_page.dart | 2 +- .../info/{ => metadata}/metadata_section.dart | 57 ++++---------- .../{ => metadata}/metadata_thumbnail.dart | 0 .../fullscreen/info/metadata/xmp_tile.dart | 78 +++++++++++++++++++ 8 files changed, 143 insertions(+), 50 deletions(-) create mode 100644 lib/utils/brand_colors.dart create mode 100644 lib/utils/xmp.dart rename lib/widgets/fullscreen/info/{ => metadata}/metadata_section.dart (81%) rename lib/widgets/fullscreen/info/{ => metadata}/metadata_thumbnail.dart (100%) create mode 100644 lib/widgets/fullscreen/info/metadata/xmp_tile.dart diff --git a/lib/utils/brand_colors.dart b/lib/utils/brand_colors.dart new file mode 100644 index 000000000..1e5c99a91 --- /dev/null +++ b/lib/utils/brand_colors.dart @@ -0,0 +1,21 @@ +import 'package:flutter/painting.dart'; + +class BrandColors { + static const Color adobeIllustrator = Color(0xFFFF9B00); + static const Color adobePhotoshop = Color(0xFF2DAAFF); + static const Color android = Color(0xFF3DDC84); + static const Color flutter = Color(0xFF47D1FD); + + static Color get(String text) { + if (text != null) { + switch (text.toLowerCase()) { + case 'illustrator': + return adobeIllustrator; + case 'photoshop': + case 'lightroom': + return adobePhotoshop; + } + } + return null; + } +} diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 1d20ed1e5..9c8682314 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -3,9 +3,6 @@ import 'package:flutter/painting.dart'; import 'package:tuple/tuple.dart'; class Constants { - static const Color androidGreen = Color(0xFF3DDC84); - static const Color flutterBlue = Color(0xFF47D1FD); - // as of Flutter v1.22.3, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped // so we give it a `strutStyle` with a slightly larger height static const overflowStrutStyle = StrutStyle(height: 1.3); diff --git a/lib/utils/xmp.dart b/lib/utils/xmp.dart new file mode 100644 index 000000000..53e7c962a --- /dev/null +++ b/lib/utils/xmp.dart @@ -0,0 +1,27 @@ +class XMP { + static const namespaceSeparator = ':'; + static const structFieldSeparator = '/'; + + static const Map namespaces = { + 'Camera': 'Camera', + 'crs': 'Camera Raw Settings', + 'dc': 'Dublin Core', + 'exif': 'Exif', + 'GIMP': 'GIMP', + 'illustrator': 'Illustrator', + 'Iptc4xmpCore': 'IPTC Core', + 'lr': 'Lightroom', + 'MicrosoftPhoto': 'Microsoft Photo', + 'pdf': 'PDF', + 'pdfx': 'PDF/X', + 'photomechanic': 'Photo Mechanic', + 'photoshop': 'Photoshop', + 'tiff': 'TIFF', + 'xmp': 'Basic', + 'xmpBJ': 'Basic Job Ticket', + 'xmpDM': 'Dynamic Media', + 'xmpMM': 'Media Management', + 'xmpRights': 'Rights Management', + 'xmpTPg': 'Paged-Text', + }; +} diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index f56456504..c47615cd1 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -1,3 +1,4 @@ +import 'package:aves/utils/brand_colors.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/aves_expansion_tile.dart'; import 'package:aves/widgets/common/icons.dart'; @@ -51,13 +52,13 @@ class _LicensesState extends State { SizedBox(height: 16), AvesExpansionTile( title: 'Android Libraries', - color: Constants.androidGreen, + color: BrandColors.android, expandedNotifier: _expandedNotifier, children: _platform.map((package) => LicenseRow(package)).toList(), ), AvesExpansionTile( title: 'Flutter Packages', - color: Constants.flutterBlue, + color: BrandColors.flutter, expandedNotifier: _expandedNotifier, children: _flutter.map((package) => LicenseRow(package)).toList(), ), diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index 9e9e4a6a1..42858849b 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -6,7 +6,7 @@ import 'package:aves/widgets/common/data_providers/media_query_data_provider.dar import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/basic_section.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart'; -import 'package:aves/widgets/fullscreen/info/metadata_section.dart'; +import 'package:aves/widgets/fullscreen/info/metadata/metadata_section.dart'; import 'package:aves/widgets/fullscreen/info/notifications.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/fullscreen/info/metadata_section.dart b/lib/widgets/fullscreen/info/metadata/metadata_section.dart similarity index 81% rename from lib/widgets/fullscreen/info/metadata_section.dart rename to lib/widgets/fullscreen/info/metadata/metadata_section.dart index 1ef58eeaa..b3f558e12 100644 --- a/lib/widgets/fullscreen/info/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata/metadata_section.dart @@ -2,14 +2,15 @@ import 'dart:collection'; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/metadata_service.dart'; +import 'package:aves/utils/brand_colors.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/aves_expansion_tile.dart'; -import 'package:aves/widgets/common/highlight_title.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; -import 'package:aves/widgets/fullscreen/info/metadata_thumbnail.dart'; +import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart'; +import 'package:aves/widgets/fullscreen/info/metadata/xmp_tile.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -121,12 +122,17 @@ class _MetadataSectionSliverState extends State with Auto } Widget _buildDirTile(String title, _MetadataDirectory dir) { - if (dir.name == xmpDirectory) { - return _buildXmpDirTile(dir); + final dirName = dir.name; + if (dirName == xmpDirectory) { + return XmpDirTile( + entry: entry, + tags: dir.tags, + expandedNotifier: _expandedDirectoryNotifier, + ); } Widget thumbnail; final prefixChildren = []; - switch (dir.name) { + switch (dirName) { case exifThumbnailDirectory: thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry); break; @@ -150,7 +156,7 @@ class _MetadataSectionSliverState extends State with Auto return AvesExpansionTile( title: title, - color: stringToColor(dir.name), + color: BrandColors.get(dirName) ?? stringToColor(dirName), expandedNotifier: _expandedDirectoryNotifier, children: [ if (prefixChildren.isNotEmpty) Wrap(children: prefixChildren), @@ -163,43 +169,6 @@ class _MetadataSectionSliverState extends State with Auto ); } - Widget _buildXmpDirTile(_MetadataDirectory dir) { - final thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry); - final byNamespace = SplayTreeMap.of( - groupBy, String>(dir.tags.entries, (kv) { - final fullKey = kv.key; - final i = fullKey.indexOf(':'); - if (i == -1) return ''; - return fullKey.substring(0, i); - }), - compareAsciiUpperCase, - ); - return AvesExpansionTile( - title: dir.name, - expandedNotifier: _expandedDirectoryNotifier, - children: [ - if (thumbnail != null) thumbnail, - Padding( - padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: byNamespace.entries.expand((kv) { - final ns = kv.key; - final hasNamespace = ns.isNotEmpty; - final i = hasNamespace ? ns.length + 1 : 0; - final entries = kv.value.map((kv) => MapEntry(kv.key.substring(i), kv.value)).toList(); - entries.sort((a, b) => compareAsciiUpperCaseNatural(a.key, b.key)); - return [ - if (hasNamespace) HighlightTitle(ns), - InfoRowGroup(Map.fromEntries(entries), maxValueLength: Constants.infoGroupMaxValueLength), - ]; - }).toList(), - ), - ), - ], - ); - } - void _onMetadataChanged() { _loadedMetadataUri.value = null; _metadata = {}; @@ -223,7 +192,7 @@ class _MetadataSectionSliverState extends State with Auto final rawTags = dirKV.value as Map ?? {}; final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) { - final value = tagKV.value as String ?? ''; + final value = (tagKV.value as String ?? '').trim(); if (value.isEmpty) return null; final tagName = tagKV.key as String ?? ''; return MapEntry(tagName, value); diff --git a/lib/widgets/fullscreen/info/metadata_thumbnail.dart b/lib/widgets/fullscreen/info/metadata/metadata_thumbnail.dart similarity index 100% rename from lib/widgets/fullscreen/info/metadata_thumbnail.dart rename to lib/widgets/fullscreen/info/metadata/metadata_thumbnail.dart diff --git a/lib/widgets/fullscreen/info/metadata/xmp_tile.dart b/lib/widgets/fullscreen/info/metadata/xmp_tile.dart new file mode 100644 index 000000000..f7a7175ee --- /dev/null +++ b/lib/widgets/fullscreen/info/metadata/xmp_tile.dart @@ -0,0 +1,78 @@ +import 'dart:collection'; + +import 'package:aves/model/image_entry.dart'; +import 'package:aves/utils/brand_colors.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/utils/xmp.dart'; +import 'package:aves/widgets/common/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/highlight_title.dart'; +import 'package:aves/widgets/fullscreen/info/common.dart'; +import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +class XmpDirTile extends StatelessWidget { + final ImageEntry entry; + final SplayTreeMap tags; + final ValueNotifier expandedNotifier; + + const XmpDirTile({ + @required this.entry, + @required this.tags, + @required this.expandedNotifier, + }); + + @override + Widget build(BuildContext context) { + final thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.xmp, entry: entry); + final sections = SplayTreeMap.of( + groupBy, String>(tags.entries, (kv) { + final fullKey = kv.key; + final i = fullKey.indexOf(XMP.namespaceSeparator); + if (i == -1) return ''; + final namespace = fullKey.substring(0, i); + return XMP.namespaces[namespace] ?? namespace; + }), + compareAsciiUpperCase, + ); + return AvesExpansionTile( + title: 'XMP', + expandedNotifier: expandedNotifier, + children: [ + if (thumbnail != null) thumbnail, + Padding( + padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: sections.entries.expand((sectionEntry) { + final title = sectionEntry.key; + + final entries = sectionEntry.value.map((kv) { + final key = kv.key.splitMapJoin(XMP.structFieldSeparator, onNonMatch: (s) { + // strip namespace + final key = s.split(XMP.namespaceSeparator).last; + // uppercase first letter + return key.replaceFirstMapped(RegExp('.'), (m) => m.group(0).toUpperCase()); + }); + return MapEntry(key, kv.value); + }).toList() + ..sort((a, b) => compareAsciiUpperCaseNatural(a.key, b.key)); + return [ + if (title.isNotEmpty) + Padding( + padding: EdgeInsets.only(top: 8), + child: HighlightTitle( + title, + color: BrandColors.get(title), + ), + ), + InfoRowGroup(Map.fromEntries(entries), maxValueLength: Constants.infoGroupMaxValueLength), + ]; + }).toList(), + ), + ), + ], + ); + } +} From 3fb3cf1f88f7b27db8daab1ecd8f9662e4cbc2ce Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 20 Nov 2020 15:29:29 +0900 Subject: [PATCH 15/33] minor fixes --- lib/model/mime_types.dart | 25 +++++++++++++-------- lib/utils/xmp.dart | 3 +++ lib/widgets/collection/thumbnail/error.dart | 4 +++- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/lib/model/mime_types.dart b/lib/model/mime_types.dart index 0af76645f..02f954ae4 100644 --- a/lib/model/mime_types.dart +++ b/lib/model/mime_types.dart @@ -44,14 +44,21 @@ class MimeTypes { static const List undecodable = [crw, psd]; // TODO TLAD make it dynamic if it depends on OS/lib versions static String displayType(String mime) { - final patterns = [ - RegExp('.*/'), // remove type, keep subtype - RegExp('(X-|VND.(WAP.)?)'), // noisy prefixes - '+XML', // noisy suffix - RegExp('ADOBE\\\.'), // for PSD - ]; - mime = mime.toUpperCase(); - patterns.forEach((pattern) => mime = mime.replaceFirst(pattern, '')); - return mime; + switch (mime) { + case 'image/x-icon': + return 'ICO'; + case 'image/vnd.adobe.photoshop': + case 'image/x-photoshop': + return 'PSD'; + default: + final patterns = [ + RegExp('.*/'), // remove type, keep subtype + RegExp('(X-|VND.(WAP.)?)'), // noisy prefixes + '+XML', // noisy suffix + ]; + mime = mime.toUpperCase(); + patterns.forEach((pattern) => mime = mime.replaceFirst(pattern, '')); + return mime; + } } } diff --git a/lib/utils/xmp.dart b/lib/utils/xmp.dart index 53e7c962a..55ddc7eda 100644 --- a/lib/utils/xmp.dart +++ b/lib/utils/xmp.dart @@ -2,7 +2,9 @@ class XMP { static const namespaceSeparator = ':'; static const structFieldSeparator = '/'; + // cf https://exiftool.org/TagNames/XMP.html static const Map namespaces = { + 'aux': 'Auxiliary Exif', 'Camera': 'Camera', 'crs': 'Camera Raw Settings', 'dc': 'Dublin Core', @@ -12,6 +14,7 @@ class XMP { 'Iptc4xmpCore': 'IPTC Core', 'lr': 'Lightroom', 'MicrosoftPhoto': 'Microsoft Photo', + 'panorama': 'Panorama', 'pdf': 'PDF', 'pdfx': 'PDF/X', 'photomechanic': 'Photo Mechanic', diff --git a/lib/widgets/collection/thumbnail/error.dart b/lib/widgets/collection/thumbnail/error.dart index 3bfeb9b1c..3054616cf 100644 --- a/lib/widgets/collection/thumbnail/error.dart +++ b/lib/widgets/collection/thumbnail/error.dart @@ -15,7 +15,9 @@ class ErrorThumbnail extends StatelessWidget { @override Widget build(BuildContext context) { - return Center( + return Container( + alignment: Alignment.center, + color: Colors.black, child: Tooltip( message: tooltip, preferBelow: false, From 318010b66c7ff2e11e10ce2d194e203dc8c590a6 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sat, 21 Nov 2020 12:06:35 +0900 Subject: [PATCH 16/33] album picker: added filter field --- android/build.gradle | 2 +- lib/model/source/collection_source.dart | 2 +- lib/utils/debouncer.dart | 16 +++ lib/utils/durations.dart | 5 +- .../selection_action_delegate.dart | 54 +++----- lib/widgets/filter_grids/album_pick.dart | 122 ++++++++++++++++++ lib/widgets/filter_grids/albums_page.dart | 6 +- lib/widgets/fullscreen/image_view.dart | 2 +- lib/widgets/search/search_delegate.dart | 2 +- lib/widgets/search/search_page.dart | 25 ++-- 10 files changed, 185 insertions(+), 51 deletions(-) create mode 100644 lib/utils/debouncer.dart create mode 100644 lib/widgets/filter_grids/album_pick.dart diff --git a/android/build.gradle b/android/build.gradle index 83edf9619..82eba64c0 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -6,7 +6,7 @@ buildscript { jcenter() } dependencies { - // TODO TLAD upgrade AGP to 4+ when this is fixed: https://github.com/flutter/flutter/issues/58247 + // TODO TLAD upgrade AGP to 4+ when this lands on stable: https://github.com/flutter/flutter/pull/70808 classpath 'com.android.tools.build:gradle:3.6.4' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.4' diff --git a/lib/model/source/collection_source.dart b/lib/model/source/collection_source.dart index 3c179ab25..d3e669e18 100644 --- a/lib/model/source/collection_source.dart +++ b/lib/model/source/collection_source.dart @@ -45,7 +45,7 @@ abstract class CollectionSource with SourceBase, AlbumMixin, LocationMixin, TagM sortFactor: EntrySortFactor.date, ).sortedEntries; - ValueNotifier stateNotifier = ValueNotifier(SourceState.ready); + ValueNotifier stateNotifier = ValueNotifier(SourceState.ready); List _savedDates; diff --git a/lib/utils/debouncer.dart b/lib/utils/debouncer.dart new file mode 100644 index 000000000..b41cd90bc --- /dev/null +++ b/lib/utils/debouncer.dart @@ -0,0 +1,16 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +class Debouncer { + final Duration delay; + + Timer _timer; + + Debouncer({@required this.delay}); + + void call(Function action) { + _timer?.cancel(); + _timer = Timer(delay, action); + } +} diff --git a/lib/utils/durations.dart b/lib/utils/durations.dart index 68394e51b..af41013c9 100644 --- a/lib/utils/durations.dart +++ b/lib/utils/durations.dart @@ -12,8 +12,10 @@ class Durations { static const staggeredAnimation = Duration(milliseconds: 375); static const dialogFieldReachAnimation = Duration(milliseconds: 300); - // collection animations static const appBarTitleAnimation = Duration(milliseconds: 300); + static const appBarActionChangeAnimation = Duration(milliseconds: 200); + + // collection animations static const filterBarRemovalAnimation = Duration(milliseconds: 400); static const collectionOpOverlayAnimation = Duration(milliseconds: 300); static const collectionScalingBackgroundAnimation = Duration(milliseconds: 200); @@ -40,4 +42,5 @@ class Durations { static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation; static const doubleBackTimerDelay = Duration(milliseconds: 1000); static const softKeyboardDisplayDelay = Duration(milliseconds: 300); + static const searchDebounceDelay = Duration(milliseconds: 200); } diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index b089d1f89..8823c3efa 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -10,15 +10,14 @@ import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/widgets/collection/collection_actions.dart'; import 'package:aves/widgets/collection/empty.dart'; -import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart'; import 'package:aves/widgets/common/action_delegates/feedback.dart'; import 'package:aves/widgets/common/action_delegates/permission_aware.dart'; import 'package:aves/widgets/common/action_delegates/size_aware.dart'; import 'package:aves/widgets/common/aves_dialog.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/filter_grids/album_pick.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; -import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:flutter/foundation.dart'; @@ -70,47 +69,34 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar } Future _moveSelection(BuildContext context, {@required bool copy}) async { + final filterNotifier = ValueNotifier(''); final chipSetActionDelegate = AlbumChipSetActionDelegate(source: source); final destinationAlbum = await Navigator.push( context, MaterialPageRoute( builder: (context) { + Widget appBar = AlbumPickAppBar( + copy: copy, + actionDelegate: chipSetActionDelegate, + onFilterChanged: (filter) => filterNotifier.value = filter, + ); + return Selector( selector: (context, s) => s.albumSortFactor, builder: (context, sortFactor, child) { - return FilterGridPage( - source: source, - appBar: SliverAppBar( - leading: BackButton(), - title: Text(copy ? 'Copy to Album' : 'Move to Album'), - actions: [ - IconButton( - icon: Icon(AIcons.createAlbum), - onPressed: () async { - final newAlbum = await showDialog( - context: context, - builder: (context) => CreateAlbumDialog(), - ); - if (newAlbum != null && newAlbum.isNotEmpty) { - Navigator.pop(context, newAlbum); - } - }, - tooltip: 'Create album', - ), - IconButton( - icon: Icon(AIcons.sort), - onPressed: () => chipSetActionDelegate.onActionSelected(context, ChipSetAction.sort), - ), - ], - floating: true, + return ValueListenableBuilder( + valueListenable: filterNotifier, + builder: (context, filter, child) => FilterGridPage( + source: source, + appBar: appBar, + filterEntries: AlbumListPage.getAlbumEntries(source, filter: filter), + filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), + emptyBuilder: () => EmptyContent( + icon: AIcons.album, + text: 'No albums', + ), + onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), ), - filterEntries: AlbumListPage.getAlbumEntries(source), - filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), - emptyBuilder: () => EmptyContent( - icon: AIcons.album, - text: 'No albums', - ), - onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), ); }, ); diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart new file mode 100644 index 000000000..09c3accea --- /dev/null +++ b/lib/widgets/filter_grids/album_pick.dart @@ -0,0 +1,122 @@ +import 'package:aves/utils/debouncer.dart'; +import 'package:aves/utils/durations.dart'; +import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart'; +import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; +import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +class AlbumPickAppBar extends StatelessWidget { + final bool copy; + final AlbumChipSetActionDelegate actionDelegate; + final ValueChanged onFilterChanged; + + const AlbumPickAppBar({ + @required this.copy, + @required this.actionDelegate, + @required this.onFilterChanged, + }); + + @override + Widget build(BuildContext context) { + return SliverAppBar( + leading: BackButton(), + title: Text(copy ? 'Copy to Album' : 'Move to Album'), + bottom: AlbumFilterBar( + onChanged: onFilterChanged, + ), + actions: [ + IconButton( + icon: Icon(AIcons.createAlbum), + onPressed: () async { + final newAlbum = await showDialog( + context: context, + builder: (context) => CreateAlbumDialog(), + ); + if (newAlbum != null && newAlbum.isNotEmpty) { + Navigator.pop(context, newAlbum); + } + }, + tooltip: 'Create album', + ), + IconButton( + icon: Icon(AIcons.sort), + onPressed: () => actionDelegate.onActionSelected(context, ChipSetAction.sort), + ), + ], + floating: true, + ); + } +} + +class AlbumFilterBar extends StatefulWidget implements PreferredSizeWidget { + final ValueChanged onChanged; + + const AlbumFilterBar({@required this.onChanged}); + + @override + Size get preferredSize => Size.fromHeight(kToolbarHeight); + + @override + _AlbumFilterBarState createState() => _AlbumFilterBarState(); +} + +class _AlbumFilterBarState extends State { + final TextEditingController _controller = TextEditingController(text: ''); + final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay); + + @override + Widget build(BuildContext context) { + final clearButton = IconButton( + icon: Icon(AIcons.clear), + onPressed: () { + _controller.clear(); + widget.onChanged(''); + }, + tooltip: 'Clear', + ); + return Container( + height: kToolbarHeight, + alignment: Alignment.topCenter, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Icon(AIcons.search), + Expanded( + child: TextField( + controller: _controller, + decoration: InputDecoration( + icon: Padding( + padding: EdgeInsetsDirectional.only(start: 16), + child: Icon(AIcons.search), + ), + // border: OutlineInputBorder(), + hintText: MaterialLocalizations.of(context).searchFieldLabel, + hintStyle: Theme.of(context).inputDecorationTheme.hintStyle, + ), + textInputAction: TextInputAction.search, + onChanged: (s) => _debouncer(() => widget.onChanged(s)), + ), + ), + AnimatedBuilder( + animation: _controller, + builder: (context, child) => AnimatedSwitcher( + duration: Durations.appBarActionChangeAnimation, + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SizeTransition( + axis: Axis.horizontal, + sizeFactor: animation, + child: child, + ), + ), + child: _controller.text.isNotEmpty ? clearButton : SizedBox(width: 16), + ), + ) + ], + ), + ); + } +} diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index b10093d0e..a83481524 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -58,12 +58,16 @@ class AlbumListPage extends StatelessWidget { // common with album selection page to move/copy entries - static Map getAlbumEntries(CollectionSource source) { + static Map getAlbumEntries(CollectionSource source, {String filter}) { final pinned = settings.pinnedFilters.whereType().map((f) => f.album); final entriesByDate = source.sortedEntriesForFilterList; // albums are initially sorted by name at the source level var sortedAlbums = source.sortedAlbums; + if (filter != null && filter.isNotEmpty) { + filter = filter.toUpperCase(); + sortedAlbums = sortedAlbums.where((album) => source.getUniqueAlbumName(album).toUpperCase().contains(filter)).toList(); + } if (settings.albumSortFactor == ChipSortFactor.name) { final pinnedAlbums = [], regularAlbums = [], appAlbums = [], specialAlbums = []; diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index f1e57d2ca..1ac44a6b7 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -40,7 +40,7 @@ class ImageView extends StatefulWidget { class _ImageViewState extends State { final PhotoViewController _photoViewController = PhotoViewController(); final PhotoViewScaleStateController _photoViewScaleStateController = PhotoViewScaleStateController(); - final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); + final ValueNotifier _viewStateNotifier = ValueNotifier(ViewState.zero); StreamSubscription _subscription; Size _photoViewChildSize; diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 1ba7d3503..b7b343bd1 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -259,7 +259,7 @@ class ImageSearchDelegate { queryTextController.text = value; } - final ValueNotifier currentBodyNotifier = ValueNotifier(null); + final ValueNotifier currentBodyNotifier = ValueNotifier(null); SearchBody get currentBody => currentBodyNotifier.value; diff --git a/lib/widgets/search/search_page.dart b/lib/widgets/search/search_page.dart index 5919ffc7f..b1a7698a0 100644 --- a/lib/widgets/search/search_page.dart +++ b/lib/widgets/search/search_page.dart @@ -1,3 +1,5 @@ +import 'package:aves/utils/debouncer.dart'; +import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -18,7 +20,8 @@ class SearchPage extends StatefulWidget { } class _SearchPageState extends State { - FocusNode focusNode = FocusNode(); + final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay); + final FocusNode _focusNode = FocusNode(); @override void initState() { @@ -26,8 +29,8 @@ class _SearchPageState extends State { widget.delegate.queryTextController.addListener(_onQueryChanged); widget.animation.addStatusListener(_onAnimationStatusChanged); widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged); - focusNode.addListener(_onFocusChanged); - widget.delegate.focusNode = focusNode; + _focusNode.addListener(_onFocusChanged); + widget.delegate.focusNode = _focusNode; } @override @@ -37,7 +40,7 @@ class _SearchPageState extends State { widget.animation.removeStatusListener(_onAnimationStatusChanged); widget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged); widget.delegate.focusNode = null; - focusNode.dispose(); + _focusNode.dispose(); } void _onAnimationStatusChanged(AnimationStatus status) { @@ -45,7 +48,7 @@ class _SearchPageState extends State { return; } widget.animation.removeStatusListener(_onAnimationStatusChanged); - focusNode.requestFocus(); + _focusNode.requestFocus(); } @override @@ -57,20 +60,20 @@ class _SearchPageState extends State { oldWidget.delegate.currentBodyNotifier.removeListener(_onSearchBodyChanged); widget.delegate.currentBodyNotifier.addListener(_onSearchBodyChanged); oldWidget.delegate.focusNode = null; - widget.delegate.focusNode = focusNode; + widget.delegate.focusNode = _focusNode; } } void _onFocusChanged() { - if (focusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) { + if (_focusNode.hasFocus && widget.delegate.currentBody != SearchBody.suggestions) { widget.delegate.showSuggestions(context); } } void _onQueryChanged() { - setState(() { - // rebuild ourselves because query changed. - }); + _debouncer(() => setState(() { + // rebuild ourselves because query changed. + })); } void _onSearchBodyChanged() { @@ -106,7 +109,7 @@ class _SearchPageState extends State { leading: widget.delegate.buildLeading(context), title: TextField( controller: widget.delegate.queryTextController, - focusNode: focusNode, + focusNode: _focusNode, style: theme.textTheme.headline6, textInputAction: TextInputAction.search, onSubmitted: (_) => widget.delegate.showResults(context), From ed249f77939e8dbdbf1c93b414ee002b4e01f38b Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 22 Nov 2020 18:37:42 +0900 Subject: [PATCH 17/33] added missing tooltips --- lib/widgets/filter_grids/album_pick.dart | 1 + lib/widgets/fullscreen/info/info_page.dart | 2 +- lib/widgets/search/expandable_filter_row.dart | 1 + lib/widgets/search/search_button.dart | 1 + 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 09c3accea..81b9581fe 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -44,6 +44,7 @@ class AlbumPickAppBar extends StatelessWidget { IconButton( icon: Icon(AIcons.sort), onPressed: () => actionDelegate.onActionSelected(context, ChipSetAction.sort), + tooltip: 'Sort…', ), ], floating: true, diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index 42858849b..aa64afb0b 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -43,7 +43,7 @@ class InfoPageState extends State { key: Key('back-button'), icon: Icon(AIcons.goUp), onPressed: _goToImage, - tooltip: 'Back to image', + tooltip: 'Back to media', ), title: Text('Info'), floating: true, diff --git a/lib/widgets/search/expandable_filter_row.dart b/lib/widgets/search/expandable_filter_row.dart index decf75f9f..d7f366083 100644 --- a/lib/widgets/search/expandable_filter_row.dart +++ b/lib/widgets/search/expandable_filter_row.dart @@ -45,6 +45,7 @@ class ExpandableFilterRow extends StatelessWidget { IconButton( icon: Icon(isExpanded ? AIcons.collapse : AIcons.expand), onPressed: () => expandedNotifier.value = isExpanded ? null : title, + tooltip: isExpanded ? 'Collapse' : 'Expand', ), ], ), diff --git a/lib/widgets/search/search_button.dart b/lib/widgets/search/search_button.dart index 5e6a4b3cf..4fac9bb1d 100644 --- a/lib/widgets/search/search_button.dart +++ b/lib/widgets/search/search_button.dart @@ -16,6 +16,7 @@ class SearchButton extends StatelessWidget { key: Key('search-button'), icon: Icon(AIcons.search), onPressed: () => _goToSearch(context), + tooltip: 'Search', ); } From 0916ed1f6b6f53c897c702be73d62defb54f1039 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 22 Nov 2020 19:04:08 +0900 Subject: [PATCH 18/33] fixed delay for actions triggered by popup menu --- lib/widgets/collection/app_bar.dart | 10 +++---- .../entry_action_delegate.dart | 28 ++++++++----------- .../common/chip_action_delegate.dart | 16 ++++------- .../common/chip_set_action_delegate.dart | 12 ++------ .../filter_grids/common/filter_grid_page.dart | 10 +++++-- lib/widgets/fullscreen/overlay/top.dart | 9 ++++-- 6 files changed, 39 insertions(+), 46 deletions(-) diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 7642e7cf3..937e2b771 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -258,7 +258,10 @@ class _CollectionAppBarState extends State with SingleTickerPr ] ]; }, - onSelected: _onCollectionActionSelected, + onSelected: (action) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => _onCollectionActionSelected(action)); + }, ); }, ), @@ -278,10 +281,7 @@ class _CollectionAppBarState extends State with SingleTickerPr widget.appBarHeightNotifier.value = kToolbarHeight + (hasFilters ? FilterBar.preferredHeight : 0); } - void _onCollectionActionSelected(CollectionAction action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - + Future _onCollectionActionSelected(CollectionAction action) async { switch (action) { case CollectionAction.copy: case CollectionAction.move: diff --git a/lib/widgets/common/action_delegates/entry_action_delegate.dart b/lib/widgets/common/action_delegates/entry_action_delegate.dart index ec4ccd357..72f24b886 100644 --- a/lib/widgets/common/action_delegates/entry_action_delegate.dart +++ b/lib/widgets/common/action_delegates/entry_action_delegate.dart @@ -2,7 +2,6 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; -import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/action_delegates/feedback.dart'; import 'package:aves/widgets/common/action_delegates/permission_aware.dart'; import 'package:aves/widgets/common/action_delegates/rename_entry_dialog.dart'; @@ -31,49 +30,46 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { bool get hasCollection => collection != null; - void onActionSelected(BuildContext context, ImageEntry entry, EntryAction action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - + void onActionSelected(BuildContext context, ImageEntry entry, EntryAction action) { switch (action) { case EntryAction.toggleFavourite: entry.toggleFavourite(); break; case EntryAction.delete: - unawaited(_showDeleteDialog(context, entry)); + _showDeleteDialog(context, entry); break; case EntryAction.edit: - unawaited(AndroidAppService.edit(entry.uri, entry.mimeType)); + AndroidAppService.edit(entry.uri, entry.mimeType); break; case EntryAction.info: showInfo(); break; case EntryAction.rename: - unawaited(_showRenameDialog(context, entry)); + _showRenameDialog(context, entry); break; case EntryAction.open: - unawaited(AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype)); + AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype); break; case EntryAction.openMap: - unawaited(AndroidAppService.openMap(entry.geoUri)); + AndroidAppService.openMap(entry.geoUri); break; case EntryAction.print: - unawaited(_print(entry)); + _print(entry); break; case EntryAction.rotateCCW: - unawaited(_rotate(context, entry, clockwise: false)); + _rotate(context, entry, clockwise: false); break; case EntryAction.rotateCW: - unawaited(_rotate(context, entry, clockwise: true)); + _rotate(context, entry, clockwise: true); break; case EntryAction.flip: - unawaited(_flip(context, entry)); + _flip(context, entry); break; case EntryAction.setAs: - unawaited(AndroidAppService.setAs(entry.uri, entry.mimeType)); + AndroidAppService.setAs(entry.uri, entry.mimeType); break; case EntryAction.share: - unawaited(AndroidAppService.share({entry})); + AndroidAppService.share({entry}); break; case EntryAction.viewSource: _goToSourceViewer(context, entry); diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 5b842db7b..8b8ab2b47 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -3,7 +3,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; -import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/action_delegates/feedback.dart'; import 'package:aves/widgets/common/action_delegates/permission_aware.dart'; import 'package:aves/widgets/common/action_delegates/rename_album_dialog.dart'; @@ -11,16 +10,11 @@ import 'package:aves/widgets/common/action_delegates/size_aware.dart'; import 'package:aves/widgets/common/aves_dialog.dart'; import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:intl/intl.dart'; import 'package:path/path.dart' as path; -import 'package:pedantic/pedantic.dart'; class ChipActionDelegate { - Future onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - + void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) { switch (action) { case ChipAction.pin: settings.pinnedFilters = settings.pinnedFilters..add(filter); @@ -42,14 +36,14 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per }); @override - Future onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) async { - await super.onActionSelected(context, filter, action); + void onActionSelected(BuildContext context, CollectionFilter filter, ChipAction action) { + super.onActionSelected(context, filter, action); switch (action) { case ChipAction.delete: - unawaited(_showDeleteDialog(context, filter as AlbumFilter)); + _showDeleteDialog(context, filter as AlbumFilter); break; case ChipAction.rename: - unawaited(_showRenameDialog(context, filter as AlbumFilter)); + _showRenameDialog(context, filter as AlbumFilter); break; default: break; diff --git a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart index 39efb54a6..6f507e17d 100644 --- a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart @@ -2,13 +2,10 @@ 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/model/source/enums.dart'; -import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/aves_selection_dialog.dart'; import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; import 'package:aves/widgets/stats/stats.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:pedantic/pedantic.dart'; abstract class ChipSetActionDelegate { CollectionSource get source; @@ -17,16 +14,13 @@ abstract class ChipSetActionDelegate { set sortFactor(ChipSortFactor factor); - Future onActionSelected(BuildContext context, ChipSetAction action) async { - // wait for the popup menu to hide before proceeding with the action - await Future.delayed(Durations.popupMenuAnimation * timeDilation); - + void onActionSelected(BuildContext context, ChipSetAction action) { switch (action) { case ChipSetAction.sort: - await _showSortDialog(context); + _showSortDialog(context); break; case ChipSetAction.refresh: - unawaited(source.refresh()); + source.refresh(); break; case ChipSetAction.stats: _goToStats(context); diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 3ecf62bf7..04a3fd5c0 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -27,8 +27,8 @@ import 'package:collection/collection.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; -import 'package:pedantic/pedantic.dart'; import 'package:provider/provider.dart'; class FilterNavigationPage extends StatelessWidget { @@ -107,7 +107,8 @@ class FilterNavigationPage extends StatelessWidget { .toList(), ); if (selectedAction != null) { - unawaited(chipActionDelegate.onActionSelected(context, filter, selectedAction)); + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipActionDelegate.onActionSelected(context, filter, selectedAction)); } } @@ -134,7 +135,10 @@ class FilterNavigationPage extends StatelessWidget { ), ]; }, - onSelected: (action) => chipSetActionDelegate.onActionSelected(context, action), + onSelected: (action) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipSetActionDelegate.onActionSelected(context, action)); + }, ), ]; } diff --git a/lib/widgets/fullscreen/overlay/top.dart b/lib/widgets/fullscreen/overlay/top.dart index db02ebf4b..0d4c03aa5 100644 --- a/lib/widgets/fullscreen/overlay/top.dart +++ b/lib/widgets/fullscreen/overlay/top.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/fx/sweeper.dart'; import 'package:aves/widgets/common/icons.dart'; @@ -12,6 +13,7 @@ import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:aves/widgets/fullscreen/overlay/minimap.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -168,7 +170,10 @@ class _TopOverlayRow extends StatelessWidget { _buildPopupMenuItem(EntryAction.debug), ] ], - onSelected: onActionSelected, + onSelected: (action) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => onActionSelected(action)); + }, ), ), ], @@ -177,7 +182,7 @@ class _TopOverlayRow extends StatelessWidget { Widget _buildOverlayButton(EntryAction action) { Widget child; - void onPressed() => onActionSelected?.call(action); + void onPressed() => onActionSelected(action); switch (action) { case EntryAction.toggleFavourite: child = _FavouriteToggler( From d989b6010fd697ee472768a59266b146d128256c Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 22 Nov 2020 19:49:25 +0900 Subject: [PATCH 19/33] show feedback when there is no app to handle a share/open/edit action --- .../entry_action_delegate.dart | 32 ++++++++++++------- .../selection_action_delegate.dart | 4 ++- lib/widgets/common/aves_dialog.dart | 18 +++++++++++ lib/widgets/fullscreen/info/maps/common.dart | 5 ++- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/lib/widgets/common/action_delegates/entry_action_delegate.dart b/lib/widgets/common/action_delegates/entry_action_delegate.dart index 72f24b886..19c9a03ec 100644 --- a/lib/widgets/common/action_delegates/entry_action_delegate.dart +++ b/lib/widgets/common/action_delegates/entry_action_delegate.dart @@ -38,21 +38,12 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { case EntryAction.delete: _showDeleteDialog(context, entry); break; - case EntryAction.edit: - AndroidAppService.edit(entry.uri, entry.mimeType); - break; case EntryAction.info: showInfo(); break; case EntryAction.rename: _showRenameDialog(context, entry); break; - case EntryAction.open: - AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype); - break; - case EntryAction.openMap: - AndroidAppService.openMap(entry.geoUri); - break; case EntryAction.print: _print(entry); break; @@ -65,11 +56,30 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { case EntryAction.flip: _flip(context, entry); break; + case EntryAction.edit: + AndroidAppService.edit(entry.uri, entry.mimeType).then((success) { + if (!success) showNoMatchingAppDialog(context); + }); + break; + case EntryAction.open: + AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype).then((success) { + if (!success) showNoMatchingAppDialog(context); + }); + break; + case EntryAction.openMap: + AndroidAppService.openMap(entry.geoUri).then((success) { + if (!success) showNoMatchingAppDialog(context); + }); + break; case EntryAction.setAs: - AndroidAppService.setAs(entry.uri, entry.mimeType); + AndroidAppService.setAs(entry.uri, entry.mimeType).then((success) { + if (!success) showNoMatchingAppDialog(context); + }); break; case EntryAction.share: - AndroidAppService.share({entry}); + AndroidAppService.share({entry}).then((success) { + if (!success) showNoMatchingAppDialog(context); + }); break; case EntryAction.viewSource: _goToSourceViewer(context, entry); diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 8823c3efa..b110940a4 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -43,7 +43,9 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar _showDeleteDialog(context); break; case EntryAction.share: - AndroidAppService.share(selection); + AndroidAppService.share(selection).then((success) { + if (!success) showNoMatchingAppDialog(context); + }); break; default: break; diff --git a/lib/widgets/common/aves_dialog.dart b/lib/widgets/common/aves_dialog.dart index aa3829d67..6863f6273 100644 --- a/lib/widgets/common/aves_dialog.dart +++ b/lib/widgets/common/aves_dialog.dart @@ -74,3 +74,21 @@ class DialogTitle extends StatelessWidget { ); } } + +void showNoMatchingAppDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) { + return AvesDialog( + title: 'No Matching App', + content: Text('There are no apps that can handle this.'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text('OK'.toUpperCase()), + ), + ], + ); + }, + ); +} diff --git a/lib/widgets/fullscreen/info/maps/common.dart b/lib/widgets/fullscreen/info/maps/common.dart index de76c8c64..37a311cc3 100644 --- a/lib/widgets/fullscreen/info/maps/common.dart +++ b/lib/widgets/fullscreen/info/maps/common.dart @@ -1,6 +1,7 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/utils/durations.dart'; +import 'package:aves/widgets/common/aves_dialog.dart'; import 'package:aves/widgets/common/aves_selection_dialog.dart'; import 'package:aves/widgets/common/borders.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; @@ -63,7 +64,9 @@ class MapButtonPanel extends StatelessWidget { children: [ MapOverlayButton( icon: AIcons.openInNew, - onPressed: () => AndroidAppService.openMap(geoUri), + onPressed: () => AndroidAppService.openMap(geoUri).then((success) { + if (!success) showNoMatchingAppDialog(context); + }), tooltip: 'Show on map…', ), SizedBox(height: padding), From 6fed7b0939788e4e5e71c1f7dff8a203815abb04 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 22 Nov 2020 21:39:09 +0900 Subject: [PATCH 20/33] minor changes to theme & dialogs --- lib/main.dart | 1 + lib/widgets/about/licenses.dart | 15 +++++- .../action_delegates/add_shortcut_dialog.dart | 1 + .../action_delegates/create_album_dialog.dart | 1 + .../entry_action_delegate.dart | 1 + .../action_delegates/permission_aware.dart | 1 + .../action_delegates/rename_album_dialog.dart | 1 + .../action_delegates/rename_entry_dialog.dart | 1 + .../selection_action_delegate.dart | 1 + .../common/action_delegates/size_aware.dart | 1 + lib/widgets/common/aves_dialog.dart | 44 +++++++++------- lib/widgets/common/aves_selection_dialog.dart | 1 + lib/widgets/drawer/app_drawer.dart | 51 ++++++++----------- .../common/chip_action_delegate.dart | 1 + lib/widgets/fullscreen/info/info_page.dart | 2 +- 15 files changed, 73 insertions(+), 50 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index e5ee4e35a..350685a69 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -57,6 +57,7 @@ class _AvesAppState extends State { accentColor: accentColor, scaffoldBackgroundColor: Colors.grey[900], buttonColor: accentColor, + dialogBackgroundColor: Colors.grey[850], toggleableActiveColor: accentColor, tooltipTheme: TooltipThemeData( verticalOffset: 32, diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index c47615cd1..1944dcbbe 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -64,8 +64,19 @@ class _LicensesState extends State { ), Center( child: TextButton( - onPressed: () => showLicensePage(context: context), - child: Text('All Licenses'.toUpperCase()), + onPressed: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Theme( + data: Theme.of(context).copyWith( + // as of Flutter v1.22.4, `cardColor` is used as a background color by `LicensePage` + cardColor: Theme.of(context).scaffoldBackgroundColor, + ), + child: LicensePage(), + ), + ), + ), + child: Text('Show All Licenses'.toUpperCase()), ), ), ], diff --git a/lib/widgets/common/action_delegates/add_shortcut_dialog.dart b/lib/widgets/common/action_delegates/add_shortcut_dialog.dart index f17316720..93e2522a2 100644 --- a/lib/widgets/common/action_delegates/add_shortcut_dialog.dart +++ b/lib/widgets/common/action_delegates/add_shortcut_dialog.dart @@ -37,6 +37,7 @@ class _AddShortcutDialogState extends State { @override Widget build(BuildContext context) { return AvesDialog( + context: context, content: TextField( controller: _nameController, decoration: InputDecoration( diff --git a/lib/widgets/common/action_delegates/create_album_dialog.dart b/lib/widgets/common/action_delegates/create_album_dialog.dart index 498bb4edf..02d41d050 100644 --- a/lib/widgets/common/action_delegates/create_album_dialog.dart +++ b/lib/widgets/common/action_delegates/create_album_dialog.dart @@ -41,6 +41,7 @@ class _CreateAlbumDialogState extends State { @override Widget build(BuildContext context) { return AvesDialog( + context: context, title: 'New Album', scrollController: _scrollController, scrollableContent: [ diff --git a/lib/widgets/common/action_delegates/entry_action_delegate.dart b/lib/widgets/common/action_delegates/entry_action_delegate.dart index 19c9a03ec..3b0e4c63e 100644 --- a/lib/widgets/common/action_delegates/entry_action_delegate.dart +++ b/lib/widgets/common/action_delegates/entry_action_delegate.dart @@ -152,6 +152,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin { context: context, builder: (context) { return AvesDialog( + context: context, content: Text('Are you sure?'), actions: [ TextButton( diff --git a/lib/widgets/common/action_delegates/permission_aware.dart b/lib/widgets/common/action_delegates/permission_aware.dart index 13098a4d4..69816d96f 100644 --- a/lib/widgets/common/action_delegates/permission_aware.dart +++ b/lib/widgets/common/action_delegates/permission_aware.dart @@ -25,6 +25,7 @@ mixin PermissionAwareMixin { context: context, builder: (context) { return AvesDialog( + context: context, title: 'Storage Volume Access', content: Text('Please select the $dirDisplayName directory of “$volumeDescription” in the next screen, so that this app can access it and complete your request.'), actions: [ diff --git a/lib/widgets/common/action_delegates/rename_album_dialog.dart b/lib/widgets/common/action_delegates/rename_album_dialog.dart index 7679fb620..76e90580a 100644 --- a/lib/widgets/common/action_delegates/rename_album_dialog.dart +++ b/lib/widgets/common/action_delegates/rename_album_dialog.dart @@ -39,6 +39,7 @@ class _RenameAlbumDialogState extends State { @override Widget build(BuildContext context) { return AvesDialog( + context: context, content: ValueListenableBuilder( valueListenable: _existsNotifier, builder: (context, exists, child) { diff --git a/lib/widgets/common/action_delegates/rename_entry_dialog.dart b/lib/widgets/common/action_delegates/rename_entry_dialog.dart index 87d258fc5..7bd875f14 100644 --- a/lib/widgets/common/action_delegates/rename_entry_dialog.dart +++ b/lib/widgets/common/action_delegates/rename_entry_dialog.dart @@ -37,6 +37,7 @@ class _RenameEntryDialogState extends State { @override Widget build(BuildContext context) { return AvesDialog( + context: context, content: TextField( controller: _nameController, decoration: InputDecoration( diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index b110940a4..698cd5178 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -146,6 +146,7 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar context: context, builder: (context) { return AvesDialog( + context: context, content: Text('Are you sure you want to delete ${Intl.plural(count, one: 'this item', other: 'these $count items')}?'), actions: [ TextButton( diff --git a/lib/widgets/common/action_delegates/size_aware.dart b/lib/widgets/common/action_delegates/size_aware.dart index f9af342c7..1b49dedda 100644 --- a/lib/widgets/common/action_delegates/size_aware.dart +++ b/lib/widgets/common/action_delegates/size_aware.dart @@ -34,6 +34,7 @@ mixin SizeAwareMixin { context: context, builder: (context) { return AvesDialog( + context: context, title: 'Not Enough Space', content: Text('This operation needs ${formatFilesize(needed)} of free space on “${destinationVolume.description}” to complete, but there is only ${formatFilesize(free)} left.'), actions: [ diff --git a/lib/widgets/common/aves_dialog.dart b/lib/widgets/common/aves_dialog.dart index 6863f6273..2753de93d 100644 --- a/lib/widgets/common/aves_dialog.dart +++ b/lib/widgets/common/aves_dialog.dart @@ -3,8 +3,10 @@ import 'package:flutter/widgets.dart'; class AvesDialog extends AlertDialog { static const contentHorizontalPadding = EdgeInsets.symmetric(horizontal: 24); + static const borderWidth = 1.0; AvesDialog({ + @required BuildContext context, String title, ScrollController scrollController, List scrollableContent, @@ -12,31 +14,35 @@ class AvesDialog extends AlertDialog { @required List actions, }) : assert((scrollableContent != null) ^ (content != null)), super( - title: title != null ? DialogTitle(title: title) : null, + title: title != null ? Padding( + // padding to avoid transparent border overlapping + padding: EdgeInsets.symmetric(horizontal: borderWidth), + child: DialogTitle(title: title), + ) : null, titlePadding: EdgeInsets.zero, // the `scrollable` flag of `AlertDialog` makes it // scroll both the title and the content together, // and overflow feedback ignores the dialog shape, // so we restrict scrolling to the content instead content: scrollableContent != null - ? Builder( - builder: (context) => Container( - // workaround because the dialog tries - // to size itself to the content intrinsic size, - // but the `ListView` viewport does not have one - width: 1, - child: DecoratedBox( - decoration: BoxDecoration( - border: Border( - bottom: Divider.createBorderSide(context, width: 1), - ), - ), - child: ListView( - controller: scrollController ?? ScrollController(), - shrinkWrap: true, - children: scrollableContent, + ? Container( + // padding to avoid transparent border overlapping + padding: EdgeInsets.symmetric(horizontal: borderWidth), + // workaround because the dialog tries + // to size itself to the content intrinsic size, + // but the `ListView` viewport does not have one + width: 1, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + bottom: Divider.createBorderSide(context, width: borderWidth), ), ), + child: ListView( + controller: scrollController ?? ScrollController(), + shrinkWrap: true, + children: scrollableContent, + ), ), ) : content, @@ -44,6 +50,7 @@ class AvesDialog extends AlertDialog { actions: actions, actionsPadding: EdgeInsets.symmetric(horizontal: 8), shape: RoundedRectangleBorder( + side: Divider.createBorderSide(context, width: borderWidth), borderRadius: BorderRadius.circular(24), ), ); @@ -61,7 +68,7 @@ class DialogTitle extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 20), decoration: BoxDecoration( border: Border( - bottom: Divider.createBorderSide(context, width: 1), + bottom: Divider.createBorderSide(context, width: AvesDialog.borderWidth), ), ), child: Text( @@ -80,6 +87,7 @@ void showNoMatchingAppDialog(BuildContext context) { context: context, builder: (context) { return AvesDialog( + context: context, title: 'No Matching App', content: Text('There are no apps that can handle this.'), actions: [ diff --git a/lib/widgets/common/aves_selection_dialog.dart b/lib/widgets/common/aves_selection_dialog.dart index 5c32ca2e6..920626ffe 100644 --- a/lib/widgets/common/aves_selection_dialog.dart +++ b/lib/widgets/common/aves_selection_dialog.dart @@ -35,6 +35,7 @@ class _AvesSelectionDialogState extends State { @override Widget build(BuildContext context) { return AvesDialog( + context: context, title: widget.title, scrollableContent: widget.options.entries.map((kv) => _buildRadioListTile(kv.key, kv.value)).toList(), actions: [ diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index c09791276..cbb1d70ad 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -38,37 +38,30 @@ class _AppDrawerState extends State { @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', - ), + 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', ), - ], - ), + ), + ], ), - ], - ), + ), + ], ), ), ); diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 8b8ab2b47..40e708dc7 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -58,6 +58,7 @@ class AlbumChipActionDelegate extends ChipActionDelegate with FeedbackMixin, Per context: context, builder: (context) { return AvesDialog( + context: context, content: Text('Are you sure you want to delete this album and its ${Intl.plural(count, one: 'item', other: '$count items')}?'), actions: [ TextButton( diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index aa64afb0b..edaedd16d 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -43,7 +43,7 @@ class InfoPageState extends State { key: Key('back-button'), icon: Icon(AIcons.goUp), onPressed: _goToImage, - tooltip: 'Back to media', + tooltip: 'Back to viewer', ), title: Text('Info'), floating: true, From 272916eaa6961cce88ac64ee20a64c82989b67be Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 22 Nov 2020 22:57:11 +0900 Subject: [PATCH 21/33] album pick page layout fixes --- lib/utils/durations.dart | 2 +- .../selection_action_delegate.dart | 40 +----- lib/widgets/filter_grids/album_pick.dart | 116 ++++++++++++++---- .../filter_grids/common/filter_grid_page.dart | 14 ++- 4 files changed, 110 insertions(+), 62 deletions(-) diff --git a/lib/utils/durations.dart b/lib/utils/durations.dart index af41013c9..4c794da88 100644 --- a/lib/utils/durations.dart +++ b/lib/utils/durations.dart @@ -42,5 +42,5 @@ class Durations { static Duration staggeredAnimationDelay = Durations.staggeredAnimation ~/ 6 * timeDilation; static const doubleBackTimerDelay = Duration(milliseconds: 1000); static const softKeyboardDisplayDelay = Duration(milliseconds: 300); - static const searchDebounceDelay = Duration(milliseconds: 200); + static const searchDebounceDelay = Duration(milliseconds: 250); } diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/common/action_delegates/selection_action_delegate.dart index 698cd5178..4c15a613b 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/common/action_delegates/selection_action_delegate.dart @@ -1,30 +1,21 @@ import 'dart:async'; -import 'package:aves/model/filters/album.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_source.dart'; -import 'package:aves/model/source/enums.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/widgets/collection/collection_actions.dart'; -import 'package:aves/widgets/collection/empty.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/size_aware.dart'; import 'package:aves/widgets/common/aves_dialog.dart'; import 'package:aves/widgets/common/entry_actions.dart'; -import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; -import 'package:aves/widgets/filter_grids/albums_page.dart'; -import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; -import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -import 'package:provider/provider.dart'; class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionLens collection; @@ -71,38 +62,11 @@ class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwar } Future _moveSelection(BuildContext context, {@required bool copy}) async { - final filterNotifier = ValueNotifier(''); - final chipSetActionDelegate = AlbumChipSetActionDelegate(source: source); final destinationAlbum = await Navigator.push( context, MaterialPageRoute( - builder: (context) { - Widget appBar = AlbumPickAppBar( - copy: copy, - actionDelegate: chipSetActionDelegate, - onFilterChanged: (filter) => filterNotifier.value = filter, - ); - - return Selector( - selector: (context, s) => s.albumSortFactor, - builder: (context, sortFactor, child) { - return ValueListenableBuilder( - valueListenable: filterNotifier, - builder: (context, filter, child) => FilterGridPage( - source: source, - appBar: appBar, - filterEntries: AlbumListPage.getAlbumEntries(source, filter: filter), - filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), - emptyBuilder: () => EmptyContent( - icon: AIcons.album, - text: 'No albums', - ), - onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), - ), - ); - }, - ); - }, + settings: RouteSettings(name: AlbumPickPage.routeName), + builder: (context) => AlbumPickPage(source: source, copy: copy), ), ); if (destinationAlbum == null || destinationAlbum.isEmpty) return; diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 81b9581fe..09c653e15 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -1,22 +1,83 @@ +import 'package:aves/model/filters/album.dart'; +import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/utils/durations.dart'; +import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart'; import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; +import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class AlbumPickPage extends StatefulWidget { + static const routeName = '/album_pick'; + + final CollectionSource source; + final bool copy; + + const AlbumPickPage({ + @required this.source, + @required this.copy, + }); + + @override + _AlbumPickPageState createState() => _AlbumPickPageState(); +} + +class _AlbumPickPageState extends State { + final _filterNotifier = ValueNotifier(''); + + CollectionSource get source => widget.source; + + @override + Widget build(BuildContext context) { + Widget appBar = AlbumPickAppBar( + copy: widget.copy, + actionDelegate: AlbumChipSetActionDelegate(source: source), + filterNotifier: _filterNotifier, + ); + + return Selector( + selector: (context, s) => s.albumSortFactor, + builder: (context, sortFactor, child) { + return ValueListenableBuilder( + valueListenable: _filterNotifier, + builder: (context, filter, child) => FilterGridPage( + source: source, + appBar: appBar, + filterEntries: AlbumListPage.getAlbumEntries(source, filter: filter), + filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), + emptyBuilder: () => EmptyContent( + icon: AIcons.album, + text: 'No albums', + ), + appBarHeight: AlbumPickAppBar.preferredHeight, + onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), + ), + ); + }, + ); + } +} class AlbumPickAppBar extends StatelessWidget { final bool copy; final AlbumChipSetActionDelegate actionDelegate; - final ValueChanged onFilterChanged; + final ValueNotifier filterNotifier; + + static const preferredHeight = kToolbarHeight + AlbumFilterBar.preferredHeight; const AlbumPickAppBar({ @required this.copy, @required this.actionDelegate, - @required this.onFilterChanged, + @required this.filterNotifier, }); @override @@ -25,7 +86,7 @@ class AlbumPickAppBar extends StatelessWidget { leading: BackButton(), title: Text(copy ? 'Copy to Album' : 'Move to Album'), bottom: AlbumFilterBar( - onChanged: onFilterChanged, + filterNotifier: filterNotifier, ), actions: [ IconButton( @@ -53,20 +114,30 @@ class AlbumPickAppBar extends StatelessWidget { } class AlbumFilterBar extends StatefulWidget implements PreferredSizeWidget { - final ValueChanged onChanged; + final ValueNotifier filterNotifier; - const AlbumFilterBar({@required this.onChanged}); + static const preferredHeight = kToolbarHeight; + + const AlbumFilterBar({@required this.filterNotifier}); @override - Size get preferredSize => Size.fromHeight(kToolbarHeight); + Size get preferredSize => Size.fromHeight(preferredHeight); @override _AlbumFilterBarState createState() => _AlbumFilterBarState(); } class _AlbumFilterBarState extends State { - final TextEditingController _controller = TextEditingController(text: ''); final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay); + TextEditingController _controller; + + ValueNotifier get filterNotifier => widget.filterNotifier; + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: filterNotifier.value); + } @override Widget build(BuildContext context) { @@ -74,12 +145,12 @@ class _AlbumFilterBarState extends State { icon: Icon(AIcons.clear), onPressed: () { _controller.clear(); - widget.onChanged(''); + filterNotifier.value = ''; }, tooltip: 'Clear', ); return Container( - height: kToolbarHeight, + height: AlbumFilterBar.preferredHeight, alignment: Alignment.topCenter, child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -98,22 +169,25 @@ class _AlbumFilterBarState extends State { hintStyle: Theme.of(context).inputDecorationTheme.hintStyle, ), textInputAction: TextInputAction.search, - onChanged: (s) => _debouncer(() => widget.onChanged(s)), + onChanged: (s) => _debouncer(() => filterNotifier.value = s), ), ), - AnimatedBuilder( - animation: _controller, - builder: (context, child) => AnimatedSwitcher( - duration: Durations.appBarActionChangeAnimation, - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: SizeTransition( - axis: Axis.horizontal, - sizeFactor: animation, - child: child, + ConstrainedBox( + constraints: BoxConstraints(minWidth: 16), + child: AnimatedBuilder( + animation: _controller, + builder: (context, child) => AnimatedSwitcher( + duration: Durations.appBarActionChangeAnimation, + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SizeTransition( + axis: Axis.horizontal, + sizeFactor: animation, + child: child, + ), ), + child: _controller.text.isNotEmpty ? clearButton : SizedBox.shrink(), ), - child: _controller.text.isNotEmpty ? clearButton : SizedBox(width: 16), ), ) ], diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 04a3fd5c0..012e8ad40 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -170,6 +170,7 @@ class FilterGridPage extends StatelessWidget { final Map filterEntries; final CollectionFilter Function(String key) filterBuilder; final Widget Function() emptyBuilder; + final double appBarHeight; final FilterCallback onTap; final OffsetFilterCallback onLongPress; @@ -179,6 +180,7 @@ class FilterGridPage extends StatelessWidget { @required this.filterEntries, @required this.filterBuilder, @required this.emptyBuilder, + this.appBarHeight = kToolbarHeight, @required this.onTap, this.onLongPress, }); @@ -227,7 +229,7 @@ class FilterGridPage extends StatelessWidget { controller: PrimaryScrollController.of(context), padding: EdgeInsets.only( // padding to keep scroll thumb between app bar above and nav bar below - top: kToolbarHeight, + top: appBarHeight, bottom: mqViewInsetsBottom, ), child: scrollView, @@ -243,7 +245,15 @@ class FilterGridPage extends StatelessWidget { appBar, filterKeys.isEmpty ? SliverFillRemaining( - child: emptyBuilder(), + child: Selector( + selector: (context, mq) => mq.viewInsets.bottom, + builder: (context, mqViewInsetsBottom, child) { + return Padding( + padding: EdgeInsets.only(bottom: mqViewInsetsBottom), + child: emptyBuilder(), + ); + }, + ), hasScrollBody: false, ) : SliverPadding( From ad2d9b3552304d3311fb5327b4a851b4fe265054 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 23 Nov 2020 11:48:30 +0900 Subject: [PATCH 22/33] filter chip decoration pin animation --- lib/utils/durations.dart | 3 ++ lib/widgets/common/aves_filter_chip.dart | 2 +- .../common/decorated_filter_chip.dart | 32 ++++++++++++++----- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/lib/utils/durations.dart b/lib/utils/durations.dart index 4c794da88..0481bf563 100644 --- a/lib/utils/durations.dart +++ b/lib/utils/durations.dart @@ -15,6 +15,9 @@ class Durations { static const appBarTitleAnimation = Duration(milliseconds: 300); static const appBarActionChangeAnimation = Duration(milliseconds: 200); + // filter grids animations + static const chipDecorationAnimation = Duration(milliseconds: 200); + // collection animations static const filterBarRemovalAnimation = Duration(milliseconds: 400); static const collectionOpOverlayAnimation = Duration(milliseconds: 300); diff --git a/lib/widgets/common/aves_filter_chip.dart b/lib/widgets/common/aves_filter_chip.dart index 314a2a38b..89b40b0d6 100644 --- a/lib/widgets/common/aves_filter_chip.dart +++ b/lib/widgets/common/aves_filter_chip.dart @@ -111,7 +111,7 @@ class _AvesFilterChipState extends State { mainAxisSize: MainAxisSize.min, children: [ content, - Flexible(child: widget.details), + widget.details, ], ); } diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index 9bfb5b714..541949993 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -6,6 +6,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/collection/thumbnail/raster.dart'; import 'package:aves/widgets/collection/thumbnail/vector.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; @@ -63,16 +64,31 @@ class DecoratedFilterChip extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - if (pinned) - Padding( - padding: EdgeInsets.only(right: 8), - child: DecoratedIcon( - AIcons.pin, - color: FilterGridPage.detailColor, - shadows: [Constants.embossShadow], - size: 16, + AnimatedSwitcher( + duration: Durations.chipDecorationAnimation, + switchInCurve: Curves.easeInOutCubic, + switchOutCurve: Curves.easeInOutCubic, + transitionBuilder: (child, animation) => FadeTransition( + opacity: animation, + child: SizeTransition( + axis: Axis.horizontal, + sizeFactor: animation, + axisAlignment: 1.0, + child: child, ), ), + child: pinned + ? Padding( + padding: EdgeInsets.only(right: 8), + child: DecoratedIcon( + AIcons.pin, + color: FilterGridPage.detailColor, + shadows: [Constants.embossShadow], + size: 16, + ), + ) + : SizedBox.shrink(), + ), if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)) Padding( padding: EdgeInsets.only(right: 8), From 5898c9052ab72d1f5f7d4b6ca6a288e51c1f5938 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 23 Nov 2020 19:17:23 +0900 Subject: [PATCH 23/33] changed navigation history handling --- lib/model/settings/settings.dart | 8 --- lib/widgets/collection/app_bar.dart | 3 +- lib/widgets/common/aves_filter_chip.dart | 2 +- lib/widgets/drawer/collection_tile.dart | 2 +- lib/widgets/drawer/tile.dart | 3 +- .../common/chip_set_action_delegate.dart | 7 +-- .../common/decorated_filter_chip.dart | 35 +++++------- .../filter_grids/common/filter_grid_page.dart | 3 +- lib/widgets/fullscreen/fullscreen_body.dart | 2 +- lib/widgets/search/search_delegate.dart | 12 ++-- lib/widgets/stats/filter_table.dart | 26 ++------- lib/widgets/stats/stats.dart | 57 ++++++++++++++----- 12 files changed, 75 insertions(+), 85 deletions(-) diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 160d42ae2..1e44c52e5 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -198,14 +198,6 @@ class Settings extends ChangeNotifier { set searchHistory(List newValue) => setAndNotify(searchHistoryKey, newValue.map((filter) => filter.toJson()).toList()); - // utils - - // `RoutePredicate` - RoutePredicate navRemoveRoutePredicate(String pushedRouteName) { - final home = homePage.routeName; - return (route) => pushedRouteName != home && route.settings?.name == home; - } - // convenience methods // ignore: avoid_positional_boolean_parameters diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 937e2b771..c0145fc58 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -374,7 +374,8 @@ class _CollectionAppBarState extends State with SingleTickerPr MaterialPageRoute( settings: RouteSettings(name: StatsPage.routeName), builder: (context) => StatsPage( - collection: collection, + source: source, + parentCollection: collection, ), ), ); diff --git a/lib/widgets/common/aves_filter_chip.dart b/lib/widgets/common/aves_filter_chip.dart index 89b40b0d6..314a2a38b 100644 --- a/lib/widgets/common/aves_filter_chip.dart +++ b/lib/widgets/common/aves_filter_chip.dart @@ -111,7 +111,7 @@ class _AvesFilterChipState extends State { mainAxisSize: MainAxisSize.min, children: [ content, - widget.details, + Flexible(child: widget.details), ], ); } diff --git a/lib/widgets/drawer/collection_tile.dart b/lib/widgets/drawer/collection_tile.dart index 9cff6347f..e255a8059 100644 --- a/lib/widgets/drawer/collection_tile.dart +++ b/lib/widgets/drawer/collection_tile.dart @@ -51,7 +51,7 @@ class CollectionNavTile extends StatelessWidget { sortFactor: settings.collectionSortFactor, )), ), - settings.navRemoveRoutePredicate(CollectionPage.routeName), + (route) => false, ); } } diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart index 6b044cd98..3fbbfbecb 100644 --- a/lib/widgets/drawer/tile.dart +++ b/lib/widgets/drawer/tile.dart @@ -1,6 +1,5 @@ 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'; @@ -52,7 +51,7 @@ class NavTile extends StatelessWidget { Navigator.pushAndRemoveUntil( context, route, - settings.navRemoveRoutePredicate(routeName), + (route) => false, ); } else { Navigator.push(context, route); diff --git a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart index 6f507e17d..b71436362 100644 --- a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart @@ -1,5 +1,4 @@ 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/model/source/enums.dart'; import 'package:aves/widgets/common/aves_selection_dialog.dart'; @@ -52,11 +51,7 @@ abstract class ChipSetActionDelegate { MaterialPageRoute( settings: RouteSettings(name: StatsPage.routeName), builder: (context) => StatsPage( - collection: CollectionLens( - source: source, - groupFactor: settings.collectionGroupFactor, - sortFactor: settings.collectionSortFactor, - ), + source: source, ), ), ); diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index 541949993..19a2d4908 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -64,30 +64,21 @@ class DecoratedFilterChip extends StatelessWidget { return Row( mainAxisSize: MainAxisSize.min, children: [ - AnimatedSwitcher( - duration: Durations.chipDecorationAnimation, - switchInCurve: Curves.easeInOutCubic, - switchOutCurve: Curves.easeInOutCubic, - transitionBuilder: (child, animation) => FadeTransition( - opacity: animation, - child: SizeTransition( - axis: Axis.horizontal, - sizeFactor: animation, - axisAlignment: 1.0, - child: child, + AnimatedCrossFade( + firstChild: Padding( + padding: EdgeInsets.only(right: 8), + child: DecoratedIcon( + AIcons.pin, + color: FilterGridPage.detailColor, + shadows: [Constants.embossShadow], + size: 16, ), ), - child: pinned - ? Padding( - padding: EdgeInsets.only(right: 8), - child: DecoratedIcon( - AIcons.pin, - color: FilterGridPage.detailColor, - shadows: [Constants.embossShadow], - size: 16, - ), - ) - : SizedBox.shrink(), + secondChild: SizedBox.shrink(), + sizeCurve: Curves.easeInOutCubic, + alignment: AlignmentDirectional.centerEnd, + crossFadeState: pinned ? CrossFadeState.showFirst : CrossFadeState.showSecond, + duration: Durations.chipDecorationAnimation, ), if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)) Padding( diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 012e8ad40..33288111d 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -76,7 +76,7 @@ class FilterNavigationPage extends StatelessWidget { return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink(); }, ), - onTap: (filter) => Navigator.pushAndRemoveUntil( + onTap: (filter) => Navigator.push( context, MaterialPageRoute( settings: RouteSettings(name: CollectionPage.routeName), @@ -87,7 +87,6 @@ class FilterNavigationPage extends StatelessWidget { sortFactor: settings.collectionSortFactor, )), ), - settings.navRemoveRoutePredicate(CollectionPage.routeName), ), onLongPress: AvesApp.mode == AppMode.main ? (filter, tapPosition) => _showMenu(context, filter, tapPosition) : null, ); diff --git a/lib/widgets/fullscreen/fullscreen_body.dart b/lib/widgets/fullscreen/fullscreen_body.dart index 11b25828f..bbb8b3011 100644 --- a/lib/widgets/fullscreen/fullscreen_body.dart +++ b/lib/widgets/fullscreen/fullscreen_body.dart @@ -296,7 +296,7 @@ class FullscreenBodyState extends State with SingleTickerProvide settings: RouteSettings(name: CollectionPage.routeName), builder: (context) => CollectionPage(collection.derive(filter)), ), - settings.navRemoveRoutePredicate(CollectionPage.routeName), + (route) => false, ); } diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index b7b343bd1..4e64b4e29 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -22,8 +22,8 @@ import 'package:flutter/services.dart'; class ImageSearchDelegate { final CollectionSource source; - final ValueNotifier expandedSectionNotifier = ValueNotifier(null); final CollectionLens parentCollection; + final ValueNotifier expandedSectionNotifier = ValueNotifier(null); static const searchHistoryCount = 10; @@ -188,14 +188,12 @@ class ImageSearchDelegate { if (parentCollection != null) { _applyToParentCollectionPage(context, filter); } else { - _goToCollectionPage(context, filter); + _jumpToCollectionPage(context, filter); } } void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) { - if (filter != null) { - parentCollection.addFilter(filter); - } + parentCollection.addFilter(filter); // we post closing the search page after applying the filter selection // so that hero animation target is ready in the `FilterBar`, // even when the target is a child of an `AnimatedList` @@ -209,7 +207,7 @@ class ImageSearchDelegate { Navigator.pop(context); } - void _goToCollectionPage(BuildContext context, CollectionFilter filter) { + void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) { _clean(); Navigator.pushAndRemoveUntil( context, @@ -222,7 +220,7 @@ class ImageSearchDelegate { sortFactor: settings.collectionSortFactor, )), ), - settings.navRemoveRoutePredicate(CollectionPage.routeName), + (route) => false, ); } diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index 6c64666d5..ed12727e5 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -1,9 +1,6 @@ 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/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -11,14 +8,16 @@ import 'package:intl/intl.dart'; import 'package:percent_indicator/linear_percent_indicator.dart'; class FilterTable extends StatelessWidget { - final CollectionLens collection; + final int totalEntryCount; final Map entryCountMap; final CollectionFilter Function(String key) filterBuilder; + final FilterCallback onFilterSelection; const FilterTable({ - @required this.collection, + @required this.totalEntryCount, @required this.entryCountMap, @required this.filterBuilder, + @required this.onFilterSelection, }); static const chipWidth = AvesFilterChip.maxChipWidth; @@ -27,7 +26,6 @@ class FilterTable extends StatelessWidget { @override Widget build(BuildContext context) { - final maxCount = collection.entryCount; final sortedEntries = entryCountMap.entries.toList() ..sort((kv1, kv2) { final c = kv2.value.compareTo(kv1.value); @@ -47,7 +45,7 @@ class FilterTable extends StatelessWidget { final filter = filterBuilder(kv.key); final label = filter.label; final count = kv.value; - final percent = count / maxCount; + final percent = count / totalEntryCount; return TableRow( children: [ Container( @@ -58,7 +56,7 @@ class FilterTable extends StatelessWidget { alignment: AlignmentDirectional.centerStart, child: AvesFilterChip( filter: filter, - onTap: (filter) => _goToCollection(context, filter), + onTap: onFilterSelection, ), ), if (showPercentIndicator) @@ -92,16 +90,4 @@ class FilterTable extends StatelessWidget { ), ); } - - void _goToCollection(BuildContext context, CollectionFilter filter) { - if (collection == null) return; - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - settings: RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage(collection.derive(filter)), - ), - settings.navRemoveRoutePredicate(CollectionPage.routeName), - ); - } } diff --git a/lib/widgets/stats/stats.dart b/lib/widgets/stats/stats.dart index 93183f133..797c2449d 100644 --- a/lib/widgets/stats/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -8,6 +8,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/mime_types.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/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/collection/collection_page.dart'; @@ -25,14 +26,18 @@ import 'package:percent_indicator/linear_percent_indicator.dart'; class StatsPage extends StatelessWidget { static const routeName = '/collection/stats'; - final CollectionLens collection; + final CollectionSource source; + final CollectionLens parentCollection; final Map entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {}; - List get entries => collection.sortedEntries; + List get entries => parentCollection?.sortedEntries ?? source.rawEntries; static const mimeDonutMinWidth = 124.0; - StatsPage({this.collection}) { + StatsPage({ + @required this.source, + this.parentCollection, + }) : assert(source != null) { entries.forEach((entry) { if (entry.isLocated) { final address = entry.addressDetails; @@ -55,7 +60,7 @@ class StatsPage extends StatelessWidget { @override Widget build(BuildContext context) { Widget child; - if (collection.isEmpty) { + if (entries.isEmpty) { child = EmptyContent( icon: AIcons.image, text: 'No images', @@ -75,7 +80,7 @@ class StatsPage extends StatelessWidget { final catalogued = entries.where((entry) => entry.isCatalogued); final withGps = catalogued.where((entry) => entry.hasGps); - final withGpsPercent = withGps.length / collection.entryCount; + final withGpsPercent = withGps.length / entries.length; final textScaleFactor = MediaQuery.textScaleFactorOf(context); final lineHeight = 16 * textScaleFactor; final locationIndicator = Padding( @@ -105,9 +110,9 @@ class StatsPage extends StatelessWidget { children: [ mimeDonuts, locationIndicator, - ..._buildTopFilters('Top Countries', entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)), - ..._buildTopFilters('Top Places', entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)), - ..._buildTopFilters('Top Tags', entryCountPerTag, (s) => TagFilter(s)), + ..._buildTopFilters(context, 'Top Countries', entryCountPerCountry, (s) => LocationFilter(LocationLevel.country, s)), + ..._buildTopFilters(context, 'Top Places', entryCountPerPlace, (s) => LocationFilter(LocationLevel.place, s)), + ..._buildTopFilters(context, 'Top Tags', entryCountPerTag, (s) => TagFilter(s)), ], ); } @@ -178,7 +183,7 @@ class StatsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: seriesData .map((d) => GestureDetector( - onTap: () => _goToCollection(context, MimeFilter(d.mimeType)), + onTap: () => _onFilterSelection(context, MimeFilter(d.mimeType)), child: Text.rich( TextSpan( children: [ @@ -218,6 +223,7 @@ class StatsPage extends StatelessWidget { } List _buildTopFilters( + BuildContext context, String title, Map entryCountMap, CollectionFilter Function(String key) filterBuilder, @@ -233,22 +239,45 @@ class StatsPage extends StatelessWidget { ), ), FilterTable( - collection: collection, + totalEntryCount: entries.length, entryCountMap: entryCountMap, filterBuilder: filterBuilder, + onFilterSelection: (filter) => _onFilterSelection(context, filter), ), ]; } - void _goToCollection(BuildContext context, CollectionFilter filter) { - if (collection == null) return; + void _onFilterSelection(BuildContext context, CollectionFilter filter) { + if (parentCollection != null) { + _applyToParentCollectionPage(context, filter); + } else { + _jumpToCollectionPage(context, filter); + } + } + + void _applyToParentCollectionPage(BuildContext context, CollectionFilter filter) { + parentCollection.addFilter(filter); + // we post closing the search page after applying the filter selection + // so that hero animation target is ready in the `FilterBar`, + // even when the target is a child of an `AnimatedList` + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.pop(context); + }); + } + + void _jumpToCollectionPage(BuildContext context, CollectionFilter filter) { Navigator.pushAndRemoveUntil( context, MaterialPageRoute( settings: RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage(collection.derive(filter)), + builder: (context) => CollectionPage(CollectionLens( + source: source, + filters: [filter], + groupFactor: settings.collectionGroupFactor, + sortFactor: settings.collectionSortFactor, + )), ), - settings.navRemoveRoutePredicate(CollectionPage.routeName), + (route) => false, ); } } From 78f5abc39c51062b3257d2434c5eabfff8956cb0 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 24 Nov 2020 15:32:06 +0900 Subject: [PATCH 24/33] location: use LatLng instead of Tuple for coordinates, approximation when calling geocoder, locate without storing address line --- lib/model/image_entry.dart | 48 ++++++++++++++----- lib/model/image_metadata.dart | 8 +--- lib/model/settings/coordinate_format.dart | 5 +- lib/model/source/location.dart | 37 +++++++++----- lib/utils/constants.dart | 4 +- lib/utils/geo_utils.dart | 17 +++---- lib/utils/math_utils.dart | 4 ++ lib/widgets/fullscreen/debug/db.dart | 1 - .../fullscreen/info/location_section.dart | 47 ++++++++++++++---- .../fullscreen/info/maps/leaflet_map.dart | 8 ++-- test/utils/geo_utils_test.dart | 11 +++-- test/utils/math_utils_test.dart | 5 ++ 12 files changed, 132 insertions(+), 63 deletions(-) diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index ba7d520cc..340a69dca 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -8,13 +8,14 @@ import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/services/service_policy.dart'; import 'package:aves/utils/change_notifier.dart'; +import 'package:aves/utils/math_utils.dart'; import 'package:aves/utils/time_utils.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:geocoder/geocoder.dart'; +import 'package:latlong/latlong.dart'; import 'package:path/path.dart' as ppath; -import 'package:tuple/tuple.dart'; import 'mime_types.dart'; @@ -295,9 +296,14 @@ class ImageEntry { bool get isLocated => _addressDetails != null; - Tuple2 get latLng => isCatalogued ? Tuple2(_catalogMetadata.latitude, _catalogMetadata.longitude) : null; + LatLng get latLng => isCatalogued ? LatLng(_catalogMetadata.latitude, _catalogMetadata.longitude) : null; - String get geoUri => hasGps ? 'geo:${_catalogMetadata.latitude},${_catalogMetadata.longitude}?q=${_catalogMetadata.latitude},${_catalogMetadata.longitude}' : null; + String get geoUri { + if (!hasGps) return null; + final latitude = roundToPrecision(_catalogMetadata.latitude, decimals: 6); + final longitude = roundToPrecision(_catalogMetadata.longitude, decimals: 6); + return 'geo:$latitude,$longitude?q=$latitude,$longitude'; + } List get xmpSubjects => _catalogMetadata?.xmpSubjects?.split(';')?.where((tag) => tag.isNotEmpty)?.toList() ?? []; @@ -366,7 +372,6 @@ class ImageEntry { final address = addresses.first; addressDetails = AddressDetails( contentId: contentId, - addressLine: address.addressLine, countryCode: address.countryCode, countryName: address.countryName, adminArea: address.adminArea, @@ -378,11 +383,29 @@ class ImageEntry { } } + Future findAddressLine() async { + final latitude = _catalogMetadata?.latitude; + final longitude = _catalogMetadata?.longitude; + if (latitude == null || longitude == null || (latitude == 0 && longitude == 0)) return null; + + final coordinates = Coordinates(latitude, longitude); + try { + final addresses = await Geocoder.local.findAddressesFromCoordinates(coordinates); + if (addresses != null && addresses.isNotEmpty) { + final address = addresses.first; + return address.addressLine; + } + } catch (error, stackTrace) { + debugPrint('$runtimeType findAddressLine failed with path=$path coordinates=$coordinates error=$error\n$stackTrace'); + } + return null; + } + String get shortAddress { if (!isLocated) return ''; - // admin area examples: Seoul, Geneva, null - // locality examples: Mapo-gu, Geneva, Annecy + // `admin area` examples: Seoul, Geneva, null + // `locality` examples: Mapo-gu, Geneva, Annecy return { _addressDetails.countryName, _addressDetails.adminArea, @@ -390,12 +413,13 @@ class ImageEntry { }.where((part) => part != null && part.isNotEmpty).join(', '); } - bool search(String query) { - if (bestTitle?.toUpperCase()?.contains(query) ?? false) return true; - if (_catalogMetadata?.xmpSubjects?.toUpperCase()?.contains(query) ?? false) return true; - if (_addressDetails?.addressLine?.toUpperCase()?.contains(query) ?? false) return true; - return false; - } + bool search(String query) => { + bestTitle, + _catalogMetadata?.xmpSubjects, + _addressDetails?.countryName, + _addressDetails?.adminArea, + _addressDetails?.locality, + }.any((s) => s != null && s.toUpperCase().contains(query)); Future _applyNewFields(Map newFields) async { final uri = newFields['uri']; diff --git a/lib/model/image_metadata.dart b/lib/model/image_metadata.dart index 42ec4f5fd..57f98a0eb 100644 --- a/lib/model/image_metadata.dart +++ b/lib/model/image_metadata.dart @@ -142,13 +142,12 @@ class OverlayMetadata { class AddressDetails { final int contentId; - final String addressLine, countryCode, countryName, adminArea, locality; + final String countryCode, countryName, adminArea, locality; String get place => locality != null && locality.isNotEmpty ? locality : adminArea; AddressDetails({ this.contentId, - this.addressLine, this.countryCode, this.countryName, this.adminArea, @@ -160,7 +159,6 @@ class AddressDetails { }) { return AddressDetails( contentId: contentId ?? this.contentId, - addressLine: addressLine, countryCode: countryCode, countryName: countryName, adminArea: adminArea, @@ -171,7 +169,6 @@ class AddressDetails { factory AddressDetails.fromMap(Map map) { return AddressDetails( contentId: map['contentId'], - addressLine: map['addressLine'] ?? '', countryCode: map['countryCode'] ?? '', countryName: map['countryName'] ?? '', adminArea: map['adminArea'] ?? '', @@ -181,7 +178,6 @@ class AddressDetails { Map toMap() => { 'contentId': contentId, - 'addressLine': addressLine, 'countryCode': countryCode, 'countryName': countryName, 'adminArea': adminArea, @@ -190,7 +186,7 @@ class AddressDetails { @override String toString() { - return 'AddressDetails{contentId=$contentId, addressLine=$addressLine, countryCode=$countryCode, countryName=$countryName, adminArea=$adminArea, locality=$locality}'; + return 'AddressDetails{contentId=$contentId, countryCode=$countryCode, countryName=$countryName, adminArea=$adminArea, locality=$locality}'; } } diff --git a/lib/model/settings/coordinate_format.dart b/lib/model/settings/coordinate_format.dart index be88e0ca1..7b968ba88 100644 --- a/lib/model/settings/coordinate_format.dart +++ b/lib/model/settings/coordinate_format.dart @@ -1,4 +1,5 @@ import 'package:aves/utils/geo_utils.dart'; +import 'package:latlong/latlong.dart'; import 'package:tuple/tuple.dart'; enum CoordinateFormat { dms, decimal } @@ -15,12 +16,12 @@ extension ExtraCoordinateFormat on CoordinateFormat { } } - String format(Tuple2 latLng) { + String format(LatLng latLng) { switch (this) { case CoordinateFormat.dms: return toDMS(latLng).join(', '); case CoordinateFormat.decimal: - return [latLng.item1, latLng.item2].map((n) => n.toStringAsFixed(6)).join(', '); + return [latLng.latitude, latLng.longitude].map((n) => n.toStringAsFixed(6)).join(', '); default: return toString(); } diff --git a/lib/model/source/location.dart b/lib/model/source/location.dart index 789a1e1eb..6312bffeb 100644 --- a/lib/model/source/location.dart +++ b/lib/model/source/location.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:aves/model/filters/location.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_metadata.dart'; @@ -30,15 +32,25 @@ mixin LocationMixin on SourceBase { final todo = byLocated[false] ?? []; if (todo.isEmpty) return; - // cache known locations to avoid querying the geocoder unless necessary - // measuring the time it takes to process ~3000 coordinates (with ~10% of duplicates) - // does not clearly show whether it is an actual optimization, - // as results vary wildly (durations in "min:sec"): - // - with no cache: 06:17, 08:36, 08:34 - // - with cache: 08:28, 05:42, 08:03, 05:58 - // anyway, in theory it should help! - final knownLocations = , AddressDetails>{}; - byLocated[true]?.forEach((entry) => knownLocations.putIfAbsent(entry.latLng, () => entry.addressDetails)); + // geocoder calls take between 150ms and 250ms + // approximation and caching can reduce geocoder usage + // for example, for a set of 2932 entries: + // - 2476 calls (84%) when approximating to 6 decimal places (~10cm - individual humans) + // - 2433 calls (83%) when approximating to 5 decimal places (~1m - individual trees, houses) + // - 2277 calls (78%) when approximating to 4 decimal places (~10m - individual street, large buildings) + // - 1521 calls (52%) when approximating to 3 decimal places (~100m - neighborhood, street) + // - 652 calls (22%) when approximating to 2 decimal places (~1km - town or village) + // cf https://en.wikipedia.org/wiki/Decimal_degrees#Precision + final latLngFactor = pow(10, 2); + Tuple2 approximateLatLng(ImageEntry entry) { + final lat = entry.catalogMetadata?.latitude; + final lng = entry.catalogMetadata?.longitude; + if (lat == null || lng == null) return null; + return Tuple2((lat * latLngFactor).round(), (lng * latLngFactor).round()); + } + + final knownLocations = {}; + byLocated[true]?.forEach((entry) => knownLocations.putIfAbsent(approximateLatLng(entry), () => entry.addressDetails)); var progressDone = 0; final progressTotal = todo.length; @@ -46,13 +58,14 @@ mixin LocationMixin on SourceBase { final newAddresses = []; await Future.forEach(todo, (entry) async { - if (knownLocations.containsKey(entry.latLng)) { - entry.addressDetails = knownLocations[entry.latLng]?.copyWith(contentId: entry.contentId); + final latLng = approximateLatLng(entry); + if (knownLocations.containsKey(latLng)) { + entry.addressDetails = knownLocations[latLng]?.copyWith(contentId: entry.contentId); } else { await entry.locate(background: true); // it is intended to insert `null` if the geocoder failed, // so that we skip geocoding of following entries with the same coordinates - knownLocations[entry.latLng] = entry.addressDetails; + knownLocations[latLng] = entry.addressDetails; } if (entry.isLocated) { newAddresses.add(entry.addressDetails); diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 9c8682314..59d567b65 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/painting.dart'; -import 'package:tuple/tuple.dart'; +import 'package:latlong/latlong.dart'; class Constants { // as of Flutter v1.22.3, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped @@ -21,7 +21,7 @@ class Constants { static const String overlayUnknown = '—'; // em dash static const String infoUnknown = 'unknown'; - static const pointNemo = Tuple2(-48.876667, -123.393333); + static final pointNemo = LatLng(-48.876667, -123.393333); static const int infoGroupMaxValueLength = 140; diff --git a/lib/utils/geo_utils.dart b/lib/utils/geo_utils.dart index 226eaa95b..62a942c2a 100644 --- a/lib/utils/geo_utils.dart +++ b/lib/utils/geo_utils.dart @@ -1,15 +1,12 @@ -import 'dart:math'; - +import 'package:aves/utils/math_utils.dart'; import 'package:intl/intl.dart'; -import 'package:tuple/tuple.dart'; +import 'package:latlong/latlong.dart'; String _decimal2sexagesimal(final double degDecimal) { - double _round(final double value, {final int decimals = 6}) => (value * pow(10, decimals)).round() / pow(10, decimals); - List _split(final double value) { // NumberFormat is necessary to create digit after comma if the value // has no decimal point (only necessary for browser) - final tmp = NumberFormat('0.0#####').format(_round(value, decimals: 10)).split('.'); + final tmp = NumberFormat('0.0#####').format(roundToPrecision(value, decimals: 10)).split('.'); return [ int.parse(tmp[0]).abs(), int.parse(tmp[1]), @@ -21,14 +18,14 @@ String _decimal2sexagesimal(final double degDecimal) { final min = _split(minDecimal)[0]; final sec = (minDecimal - min) * 60; - return '$deg° $min′ ${_round(sec, decimals: 2).toStringAsFixed(2)}″'; + return '$deg° $min′ ${roundToPrecision(sec, decimals: 2).toStringAsFixed(2)}″'; } // return coordinates formatted as DMS, e.g. ['41° 24′ 12.2″ N', '2° 10′ 26.5″ E'] -List toDMS(Tuple2 latLng) { +List toDMS(LatLng latLng) { if (latLng == null) return []; - final lat = latLng.item1; - final lng = latLng.item2; + final lat = latLng.latitude; + final lng = latLng.longitude; return [ '${_decimal2sexagesimal(lat)} ${lat < 0 ? 'S' : 'N'}', '${_decimal2sexagesimal(lng)} ${lng < 0 ? 'W' : 'E'}', diff --git a/lib/utils/math_utils.dart b/lib/utils/math_utils.dart index 299404753..541ebb5c5 100644 --- a/lib/utils/math_utils.dart +++ b/lib/utils/math_utils.dart @@ -1,5 +1,7 @@ import 'dart:math'; +import 'package:flutter/foundation.dart'; + final double _log2 = log(2); const double _piOver180 = pi / 180.0; @@ -9,6 +11,8 @@ double toRadians(num degrees) => degrees * _piOver180; int highestPowerOf2(num x) => x < 1 ? 0 : pow(2, (log(x) / _log2).floor()); +double roundToPrecision(final double value, {@required final int decimals}) => (value * pow(10, decimals)).round() / pow(10, decimals); + // e.g. x=12345, precision=3 should return 13000 int ceilBy(num x, int precision) { final factor = pow(10, precision); diff --git a/lib/widgets/fullscreen/debug/db.dart b/lib/widgets/fullscreen/debug/db.dart index 4abe39a68..745368d1d 100644 --- a/lib/widgets/fullscreen/debug/db.dart +++ b/lib/widgets/fullscreen/debug/db.dart @@ -128,7 +128,6 @@ class _DbTabState extends State { Text('DB address:${data == null ? ' no row' : ''}'), if (data != null) InfoRowGroup({ - 'addressLine': '${data.addressLine}', 'countryCode': '${data.countryCode}', 'countryName': '${data.countryName}', 'adminArea': '${data.adminArea}', diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index c6a1e3db2..d80b1ab80 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -12,6 +12,7 @@ import 'package:aves/widgets/fullscreen/info/maps/google_map.dart'; import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart'; import 'package:aves/widgets/fullscreen/info/maps/marker.dart'; import 'package:flutter/material.dart'; +import 'package:tuple/tuple.dart'; class LocationSection extends StatefulWidget { final CollectionLens collection; @@ -79,11 +80,9 @@ class _LocationSectionState extends State with TickerProviderSt final showMap = (_loadedUri == entry.uri) || (entry.hasGps && widget.visibleNotifier.value); if (showMap) { _loadedUri = entry.uri; - var location = ''; final filters = []; if (entry.isLocated) { final address = entry.addressDetails; - location = address.addressLine; final country = address.countryName; if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}')); final place = address.place; @@ -114,7 +113,8 @@ class _LocationSectionState extends State with TickerProviderSt vsync: this, child: settings.infoMapStyle.isGoogleMaps ? EntryGoogleMap( - latLng: entry.latLng, + // `LatLng` used by `google_maps_flutter` is not the one from `latlong` package + latLng: Tuple2(entry.latLng.latitude, entry.latLng.longitude), geoUri: entry.geoUri, initialZoom: settings.infoMapZoom, markerId: entry.uri ?? entry.path, @@ -130,11 +130,7 @@ class _LocationSectionState extends State with TickerProviderSt ), ), ), - if (entry.hasGps) - InfoRowGroup(Map.fromEntries([ - MapEntry('Coordinates', settings.coordinateFormat.format(entry.latLng)), - if (location.isNotEmpty) MapEntry('Address', location), - ])), + if (entry.hasGps) _AddressInfoGroup(entry: entry), if (filters.isNotEmpty) Padding( padding: EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + EdgeInsets.only(top: 8), @@ -160,6 +156,41 @@ class _LocationSectionState extends State with TickerProviderSt void _handleChange() => setState(() {}); } +class _AddressInfoGroup extends StatefulWidget { + final ImageEntry entry; + + const _AddressInfoGroup({@required this.entry}); + + @override + _AddressInfoGroupState createState() => _AddressInfoGroupState(); +} + +class _AddressInfoGroupState extends State<_AddressInfoGroup> { + Future _addressLineLoader; + + ImageEntry get entry => widget.entry; + + @override + void initState() { + super.initState(); + _addressLineLoader = entry.findAddressLine(); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _addressLineLoader, + builder: (context, snapshot) { + final address = !snapshot.hasError && snapshot.connectionState == ConnectionState.done ? snapshot.data : ''; + return InfoRowGroup({ + 'Coordinates': settings.coordinateFormat.format(entry.latLng), + if (address.isNotEmpty) 'Address': address, + }); + }, + ); + } +} + // browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } diff --git a/lib/widgets/fullscreen/info/maps/leaflet_map.dart b/lib/widgets/fullscreen/info/maps/leaflet_map.dart index cc6fd5fcd..84c8bc203 100644 --- a/lib/widgets/fullscreen/info/maps/leaflet_map.dart +++ b/lib/widgets/fullscreen/info/maps/leaflet_map.dart @@ -5,7 +5,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:latlong/latlong.dart'; -import 'package:tuple/tuple.dart'; import 'package:url_launcher/url_launcher.dart'; import '../location_section.dart'; @@ -18,16 +17,15 @@ class EntryLeafletMap extends StatefulWidget { final Size markerSize; final WidgetBuilder markerBuilder; - EntryLeafletMap({ + const EntryLeafletMap({ Key key, - Tuple2 latLng, + this.latLng, this.geoUri, this.initialZoom, this.style, this.markerBuilder, this.markerSize, - }) : latLng = LatLng(latLng.item1, latLng.item2), - super(key: key); + }) : super(key: key); @override State createState() => EntryLeafletMapState(); diff --git a/test/utils/geo_utils_test.dart b/test/utils/geo_utils_test.dart index 0057dfb10..c728d71c2 100644 --- a/test/utils/geo_utils_test.dart +++ b/test/utils/geo_utils_test.dart @@ -1,12 +1,13 @@ import 'package:aves/utils/geo_utils.dart'; +import 'package:latlong/latlong.dart'; import 'package:test/test.dart'; -import 'package:tuple/tuple.dart'; void main() { test('Decimal degrees to DMS (sexagesimal)', () { - expect(toDMS(Tuple2(37.496667, 127.0275)), ['37° 29′ 48.00″ N', '127° 1′ 39.00″ E']); // Gangnam - expect(toDMS(Tuple2(78.9243503, 11.9230465)), ['78° 55′ 27.66″ N', '11° 55′ 22.97″ E']); // Ny-Ålesund - expect(toDMS(Tuple2(-38.6965891, 175.9830047)), ['38° 41′ 47.72″ S', '175° 58′ 58.82″ E']); // Taupo - expect(toDMS(Tuple2(-64.249391, -56.6556145)), ['64° 14′ 57.81″ S', '56° 39′ 20.21″ W']); // Marambio + expect(toDMS(LatLng(37.496667, 127.0275)), ['37° 29′ 48.00″ N', '127° 1′ 39.00″ E']); // Gangnam + expect(toDMS(LatLng(78.9243503, 11.9230465)), ['78° 55′ 27.66″ N', '11° 55′ 22.97″ E']); // Ny-Ålesund + expect(toDMS(LatLng(-38.6965891, 175.9830047)), ['38° 41′ 47.72″ S', '175° 58′ 58.82″ E']); // Taupo + expect(toDMS(LatLng(-64.249391, -56.6556145)), ['64° 14′ 57.81″ S', '56° 39′ 20.21″ W']); // Marambio + expect(toDMS(LatLng(0, 0)), ['0° 0′ 0.00″ N', '0° 0′ 0.00″ E']); }); } diff --git a/test/utils/math_utils_test.dart b/test/utils/math_utils_test.dart index 13eed0641..a0440626f 100644 --- a/test/utils/math_utils_test.dart +++ b/test/utils/math_utils_test.dart @@ -21,6 +21,11 @@ void main() { expect(highestPowerOf2(-42), 0); }); + test('rounding to a given precision after the decimal', () { + expect(roundToPrecision(1.2345678, decimals: 3), 1.235); + expect(roundToPrecision(0, decimals: 3), 0); + }); + test('rounding up to a given precision before the decimal', () { expect(ceilBy(12345.678, 3), 13000); expect(ceilBy(42, 3), 1000); From 893e3b5fa0e0a9339420d233fed61bf00c067d25 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 24 Nov 2020 16:41:27 +0900 Subject: [PATCH 25/33] packages upgrade, reverted svg colorFilter move (undeprecated in flutter_svg 0.19.2) --- lib/model/settings/coordinate_format.dart | 1 - lib/widgets/collection/thumbnail/vector.dart | 2 +- .../image_providers/uri_picture_provider.dart | 13 ++++++++----- lib/widgets/fullscreen/image_view.dart | 2 +- pubspec.lock | 8 ++++---- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/model/settings/coordinate_format.dart b/lib/model/settings/coordinate_format.dart index 7b968ba88..6bc89007b 100644 --- a/lib/model/settings/coordinate_format.dart +++ b/lib/model/settings/coordinate_format.dart @@ -1,6 +1,5 @@ import 'package:aves/utils/geo_utils.dart'; import 'package:latlong/latlong.dart'; -import 'package:tuple/tuple.dart'; enum CoordinateFormat { dms, decimal } diff --git a/lib/widgets/collection/thumbnail/vector.dart b/lib/widgets/collection/thumbnail/vector.dart index e93ddc8bb..58f71609d 100644 --- a/lib/widgets/collection/thumbnail/vector.dart +++ b/lib/widgets/collection/thumbnail/vector.dart @@ -32,10 +32,10 @@ class ThumbnailVectorImage extends StatelessWidget { UriPicture( uri: entry.uri, mimeType: entry.mimeType, + colorFilter: colorFilter, ), width: extent, height: extent, - colorFilter: colorFilter, ); }, ), diff --git a/lib/widgets/common/image_providers/uri_picture_provider.dart b/lib/widgets/common/image_providers/uri_picture_provider.dart index f2e86ddaa..9165fc3f1 100644 --- a/lib/widgets/common/image_providers/uri_picture_provider.dart +++ b/lib/widgets/common/image_providers/uri_picture_provider.dart @@ -8,10 +8,13 @@ class UriPicture extends PictureProvider { const UriPicture({ @required this.uri, @required this.mimeType, + this.colorFilter, }) : assert(uri != null); final String uri, mimeType; + final ColorFilter colorFilter; + @override Future obtainKey(PictureConfiguration configuration) { return SynchronousFuture(this); @@ -34,22 +37,22 @@ class UriPicture extends PictureProvider { final decoder = SvgPicture.svgByteDecoder; if (onError != null) { - final future = decoder(data, null, key.toString()); + final future = decoder(data, colorFilter, key.toString()); unawaited(future.catchError(onError)); return future; } - return decoder(data, null, key.toString()); + return decoder(data, colorFilter, key.toString()); } @override bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; - return other is UriPicture && other.uri == uri; + return other is UriPicture && other.uri == uri && other.colorFilter == colorFilter; } @override - int get hashCode => uri.hashCode; + int get hashCode => hashValues(uri, colorFilter); @override - String toString() => '${objectRuntimeType(this, 'UriPicture')}(uri=$uri, mimeType=$mimeType)'; + String toString() => '${objectRuntimeType(this, 'UriPicture')}(uri=$uri, mimeType=$mimeType, colorFilter=$colorFilter)'; } diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index 1ac44a6b7..b40fa028a 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -203,9 +203,9 @@ class _ImageViewState extends State { UriPicture( uri: entry.uri, mimeType: entry.mimeType, + colorFilter: colorFilter, ), placeholderBuilder: (context) => _loadingBuilder(context, fastThumbnailProvider), - colorFilter: colorFilter, ), backgroundDecoration: backgroundDecoration, controller: _photoViewController, diff --git a/pubspec.lock b/pubspec.lock index b505bdcad..b83655a87 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -21,7 +21,7 @@ packages: name: ansicolor url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.1" archive: dependency: transitive description: @@ -324,7 +324,7 @@ packages: name: flutter_markdown url: "https://pub.dartlang.org" source: hosted - version: "0.5.0" + version: "0.5.1" flutter_native_timezone: dependency: "direct main" description: @@ -535,7 +535,7 @@ packages: name: node_interop url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.2.1" node_io: dependency: transitive description: @@ -647,7 +647,7 @@ packages: name: pdf url: "https://pub.dartlang.org" source: hosted - version: "1.11.2" + version: "1.12.0" pedantic: dependency: "direct main" description: From 7ba78e0f9aaadbd006feee7221ba8ac74261e92f Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 24 Nov 2020 20:53:48 +0900 Subject: [PATCH 26/33] info: bugfix to handle null address line --- lib/widgets/fullscreen/info/location_section.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index d80b1ab80..acd8e5e59 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -181,10 +181,10 @@ class _AddressInfoGroupState extends State<_AddressInfoGroup> { return FutureBuilder( future: _addressLineLoader, builder: (context, snapshot) { - final address = !snapshot.hasError && snapshot.connectionState == ConnectionState.done ? snapshot.data : ''; + final address = !snapshot.hasError && snapshot.connectionState == ConnectionState.done ? snapshot.data : null; return InfoRowGroup({ 'Coordinates': settings.coordinateFormat.format(entry.latLng), - if (address.isNotEmpty) 'Address': address, + if (address?.isNotEmpty == true) 'Address': address, }); }, ); From 1b6b8b92b6f0a7a15f64ee761f1182939f6a8940 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 25 Nov 2020 10:27:23 +0900 Subject: [PATCH 27/33] fixed thumbnail extent update modifying user preference --- lib/widgets/collection/grid/scaling.dart | 13 +++--- .../collection/grid/tile_extent_manager.dart | 45 ++++++++++--------- .../collection/thumbnail_collection.dart | 5 +-- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/lib/widgets/collection/grid/scaling.dart b/lib/widgets/collection/grid/scaling.dart index 6eaf09931..c64c31308 100644 --- a/lib/widgets/collection/grid/scaling.dart +++ b/lib/widgets/collection/grid/scaling.dart @@ -16,8 +16,7 @@ class GridScaleGestureDetector extends StatefulWidget { final GlobalKey scrollableKey; final ValueNotifier appBarHeightNotifier; final ValueNotifier extentNotifier; - final Size mqSize; - final double mqHorizontalPadding; + final Size viewportSize; final void Function(ImageEntry entry) onScaled; final Widget child; @@ -25,8 +24,7 @@ class GridScaleGestureDetector extends StatefulWidget { this.scrollableKey, @required this.appBarHeightNotifier, @required this.extentNotifier, - @required this.mqSize, - @required this.mqHorizontalPadding, + @required this.viewportSize, this.onScaled, @required this.child, }); @@ -73,7 +71,7 @@ class _GridScaleGestureDetectorState extends State { // not the same as `MediaQuery.size.width`, because of screen insets/padding final gridWidth = scrollableBox.size.width; _extentMin = gridWidth / (gridWidth / TileExtentManager.tileExtentMin).round(); - _extentMax = gridWidth / (gridWidth / TileExtentManager.extentMaxForSize(widget.mqSize)).round(); + _extentMax = gridWidth / (gridWidth / TileExtentManager.extentMaxForSize(widget.viewportSize)).round(); final halfExtent = _startExtent / 2; final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent)); _overlayEntry = OverlayEntry( @@ -104,10 +102,9 @@ class _GridScaleGestureDetectorState extends State { final oldExtent = tileExtentNotifier.value; // sanitize and update grid layout if necessary final newExtent = TileExtentManager.applyTileExtent( - widget.mqSize, - widget.mqHorizontalPadding, + widget.viewportSize, tileExtentNotifier, - newExtent: _scaledExtentNotifier.value, + userPreferredExtent: _scaledExtentNotifier.value, ); _scaledExtentNotifier = null; if (newExtent == oldExtent) { diff --git a/lib/widgets/collection/grid/tile_extent_manager.dart b/lib/widgets/collection/grid/tile_extent_manager.dart index 5b920bde9..1ddf1f922 100644 --- a/lib/widgets/collection/grid/tile_extent_manager.dart +++ b/lib/widgets/collection/grid/tile_extent_manager.dart @@ -7,35 +7,40 @@ class TileExtentManager { static const int columnCountMin = 2; static const int columnCountDefault = 4; static const double tileExtentMin = 46.0; - static const screenDimensionMin = tileExtentMin * columnCountMin; + static const viewportSizeMin = Size.square(tileExtentMin * columnCountMin); - static double applyTileExtent(Size mqSize, double mqHorizontalPadding, ValueNotifier extentNotifier, {double newExtent}) { + static double applyTileExtent( + Size viewportSize, + ValueNotifier extentNotifier, { + double userPreferredExtent = 0, + }) { // sanitize screen size (useful when reloading while screen is off, reporting a 0,0 size) - mqSize = Size(max(mqSize.width, screenDimensionMin), max(mqSize.height, screenDimensionMin)); + viewportSize = Size(max(viewportSize.width, viewportSizeMin.width), max(viewportSize.height, viewportSizeMin.height)); - final availableWidth = mqSize.width - mqHorizontalPadding; - var numColumns; - if ((newExtent ?? 0) == 0) { - newExtent = extentNotifier.value; - } - if ((newExtent ?? 0) == 0) { - newExtent = settings.collectionTileExtent; - } - if ((newExtent ?? 0) == 0) { - numColumns = columnCountDefault; + final oldUserPreferredExtent = settings.collectionTileExtent; + final currentExtent = extentNotifier.value; + final targetExtent = userPreferredExtent > 0 + ? userPreferredExtent + : oldUserPreferredExtent > 0 + ? oldUserPreferredExtent + : currentExtent; + + int columnCount; + if (targetExtent > 0) { + columnCount = max(columnCountMin, (viewportSize.width / targetExtent.clamp(tileExtentMin, extentMaxForSize(viewportSize))).round()); } else { - newExtent = newExtent.clamp(tileExtentMin, extentMaxForSize(mqSize)); - numColumns = max(columnCountMin, (availableWidth / newExtent).round()); + columnCount = columnCountDefault; } - newExtent = availableWidth / numColumns; - if (extentNotifier.value != newExtent) { + final newExtent = viewportSize.width / columnCount; + + if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) { settings.collectionTileExtent = newExtent; + } + if (extentNotifier.value != newExtent) { extentNotifier.value = newExtent; } return newExtent; } - static double extentMaxForSize(Size mqSize) { - return mqSize.shortestSide / columnCountMin; - } + static double extentMaxForSize(Size viewportSize) => viewportSize.shortestSide / columnCountMin; } diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index d880f4835..3e1d1f781 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -38,7 +38,7 @@ class ThumbnailCollection extends StatelessWidget { if (mqSize.isEmpty) return SizedBox.shrink(); - TileExtentManager.applyTileExtent(mqSize, mqHorizontalPadding, _tileExtentNotifier); + TileExtentManager.applyTileExtent(Size(mqSize.width - mqHorizontalPadding, mqSize.height), _tileExtentNotifier); final cacheExtent = TileExtentManager.extentMaxForSize(mqSize) * 2; // do not replace by Provider.of @@ -62,8 +62,7 @@ class ThumbnailCollection extends StatelessWidget { scrollableKey: _scrollableKey, appBarHeightNotifier: _appBarHeightNotifier, extentNotifier: _tileExtentNotifier, - mqSize: mqSize, - mqHorizontalPadding: mqHorizontalPadding, + viewportSize: Size(mqSize.width - mqHorizontalPadding, mqSize.height), onScaled: collection.highlight, child: scrollView, ); From a4fab7339d2ff68c450ccabcfe67afe5c0e9e490 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 25 Nov 2020 16:46:34 +0900 Subject: [PATCH 28/33] project reorganization, filter scaling prep --- .../app_icon_image_provider.dart | 0 .../image_providers/region_provider.dart | 0 .../image_providers/thumbnail_provider.dart | 0 .../image_providers/uri_image_provider.dart | 0 .../image_providers/uri_picture_provider.dart | 0 lib/main.dart | 8 +-- .../actions}/chip_actions.dart | 2 +- .../actions}/collection_actions.dart | 9 +-- .../actions}/entry_actions.dart | 4 +- lib/model/entry_cache.dart | 4 +- lib/model/filters/album.dart | 5 +- lib/model/filters/favourite.dart | 2 +- lib/model/filters/location.dart | 2 +- lib/model/filters/mime.dart | 6 +- lib/model/filters/query.dart | 2 +- lib/model/filters/tag.dart | 2 +- lib/model/image_entry.dart | 7 +- .../source/media_store_source.dart} | 0 lib/{utils => ref}/brand_colors.dart | 0 lib/{model => ref}/mime_types.dart | 20 ------ lib/{utils => ref}/xmp.dart | 0 lib/services/image_file_service.dart | 2 +- lib/{utils => theme}/durations.dart | 0 lib/theme/icons.dart | 67 ++++++++++++++++++ lib/utils/flutter_utils.dart | 5 -- lib/utils/mime_utils.dart | 20 ++++++ lib/widgets/about/about_page.dart | 4 +- lib/widgets/about/licenses.dart | 10 +-- lib/widgets/collection/app_bar.dart | 20 +++--- lib/widgets/collection/collection_page.dart | 4 +- .../entry_set_action_delegate.dart} | 16 ++--- lib/widgets/collection/filter_bar.dart | 4 +- lib/widgets/collection/grid/header_album.dart | 3 +- .../collection/grid/header_generic.dart | 4 +- lib/widgets/collection/grid/list_sliver.dart | 12 +--- lib/widgets/collection/grid/scaling.dart | 66 +++++++++--------- lib/widgets/collection/thumbnail/error.dart | 4 +- lib/widgets/collection/thumbnail/overlay.dart | 5 +- lib/widgets/collection/thumbnail/raster.dart | 8 +-- lib/widgets/collection/thumbnail/vector.dart | 2 +- .../collection/thumbnail_collection.dart | 44 +++++++----- .../feedback.dart | 2 +- .../permission_aware.dart | 3 +- .../size_aware.dart | 2 +- lib/widgets/common/app_bar_subtitle.dart | 2 +- .../common/{ => basic}/labeled_checkbox.dart | 0 lib/widgets/common/{ => basic}/link_chip.dart | 2 +- lib/widgets/common/{ => basic}/menu_row.dart | 2 +- .../common/{fx => basic}/outlined_text.dart | 0 .../reselectable_radio_list_tile.dart} | 4 +- .../{ => behaviour}/double_back_pop.dart | 4 +- .../common/behaviour}/route_tracker.dart | 0 .../common/{ => behaviour}/routes.dart | 4 ++ .../sloppy_scroll_physics.dart | 0 lib/widgets/common/{ => fx}/borders.dart | 0 lib/widgets/common/fx/sweeper.dart | 2 +- .../common/{ => fx}/transition_image.dart | 0 .../{ => identity}/aves_expansion_tile.dart | 2 +- .../{ => identity}/aves_filter_chip.dart | 7 +- .../{icons.dart => identity/aves_icons.dart} | 69 +------------------ .../common/{ => identity}/aves_logo.dart | 0 .../{ => identity}/highlight_title.dart | 0 .../common/{ => identity}/scroll_thumb.dart | 0 .../media_query_data_provider.dart | 0 .../settings_provider.dart | 0 lib/widgets/debug/android_env.dart | 2 +- lib/widgets/debug/app_debug_page.dart | 4 +- lib/widgets/debug/cache.dart | 2 +- lib/widgets/debug/database.dart | 2 +- lib/widgets/debug/firebase.dart | 2 +- lib/widgets/debug/settings.dart | 2 +- lib/widgets/debug/storage.dart | 2 +- .../add_shortcut_dialog.dart | 2 +- .../{common => dialogs}/aves_dialog.dart | 12 ++-- .../aves_selection_dialog.dart | 4 +- .../create_album_dialog.dart | 4 +- .../rename_album_dialog.dart | 2 +- .../rename_entry_dialog.dart | 2 +- lib/widgets/drawer/app_drawer.dart | 7 +- lib/widgets/drawer/tile.dart | 2 +- lib/widgets/filter_grids/album_pick.dart | 8 +-- lib/widgets/filter_grids/albums_page.dart | 4 +- .../common/chip_action_delegate.dart | 12 ++-- .../common/chip_set_action_delegate.dart | 4 +- .../common/decorated_filter_chip.dart | 6 +- .../filter_grids/common/filter_grid_page.dart | 16 ++--- lib/widgets/filter_grids/countries_page.dart | 4 +- lib/widgets/filter_grids/tags_page.dart | 4 +- lib/widgets/fullscreen/debug/metadata.dart | 2 +- .../entry_action_delegate.dart | 12 ++-- lib/widgets/fullscreen/fullscreen_body.dart | 4 +- .../fullscreen/fullscreen_debug_page.dart | 6 +- lib/widgets/fullscreen/fullscreen_page.dart | 2 +- lib/widgets/fullscreen/image_view.dart | 8 +-- .../fullscreen/info/basic_section.dart | 4 +- lib/widgets/fullscreen/info/common.dart | 2 +- lib/widgets/fullscreen/info/info_page.dart | 6 +- .../fullscreen/info/location_section.dart | 6 +- lib/widgets/fullscreen/info/maps/common.dart | 10 +-- .../fullscreen/info/maps/scale_layer.dart | 2 +- .../info/metadata/metadata_section.dart | 8 +-- .../fullscreen/info/metadata/xmp_tile.dart | 8 +-- lib/widgets/fullscreen/overlay/bottom.dart | 4 +- lib/widgets/fullscreen/overlay/common.dart | 2 +- lib/widgets/fullscreen/overlay/top.dart | 8 +-- lib/widgets/fullscreen/overlay/video.dart | 6 +- lib/widgets/fullscreen/tiled_view.dart | 2 +- lib/widgets/fullscreen/video_view.dart | 2 +- lib/widgets/home_page.dart | 4 +- lib/widgets/search/expandable_filter_row.dart | 6 +- lib/widgets/search/search_button.dart | 2 +- lib/widgets/search/search_delegate.dart | 6 +- lib/widgets/search/search_page.dart | 2 +- lib/widgets/settings/settings_page.dart | 10 +-- lib/widgets/settings/svg_background.dart | 2 +- lib/widgets/stats/filter_table.dart | 2 +- lib/widgets/stats/stats.dart | 8 +-- lib/widgets/welcome_page.dart | 6 +- test/model/filters_test.dart | 2 +- 119 files changed, 377 insertions(+), 364 deletions(-) rename lib/{widgets/common => }/image_providers/app_icon_image_provider.dart (100%) rename lib/{widgets/common => }/image_providers/region_provider.dart (100%) rename lib/{widgets/common => }/image_providers/thumbnail_provider.dart (100%) rename lib/{widgets/common => }/image_providers/uri_image_provider.dart (100%) rename lib/{widgets/common => }/image_providers/uri_picture_provider.dart (100%) rename lib/{widgets/filter_grids/common => model/actions}/chip_actions.dart (93%) rename lib/{widgets/collection => model/actions}/collection_actions.dart (86%) rename lib/{widgets/common => model/actions}/entry_actions.dart (96%) rename lib/{widgets/common/data_providers/media_store_collection_provider.dart => model/source/media_store_source.dart} (100%) rename lib/{utils => ref}/brand_colors.dart (100%) rename lib/{model => ref}/mime_types.dart (72%) rename lib/{utils => ref}/xmp.dart (100%) rename lib/{utils => theme}/durations.dart (100%) create mode 100644 lib/theme/icons.dart delete mode 100644 lib/utils/flutter_utils.dart create mode 100644 lib/utils/mime_utils.dart rename lib/widgets/{common/action_delegates/selection_action_delegate.dart => collection/entry_set_action_delegate.dart} (90%) rename lib/widgets/common/{action_delegates => action_mixins}/feedback.dart (98%) rename lib/widgets/common/{action_delegates => action_mixins}/permission_aware.dart (97%) rename lib/widgets/common/{action_delegates => action_mixins}/size_aware.dart (97%) rename lib/widgets/common/{ => basic}/labeled_checkbox.dart (100%) rename lib/widgets/common/{ => basic}/link_chip.dart (96%) rename lib/widgets/common/{ => basic}/menu_row.dart (93%) rename lib/widgets/common/{fx => basic}/outlined_text.dart (100%) rename lib/widgets/common/{aves_radio_list_tile.dart => basic/reselectable_radio_list_tile.dart} (96%) rename lib/widgets/common/{ => behaviour}/double_back_pop.dart (92%) rename lib/{utils => widgets/common/behaviour}/route_tracker.dart (100%) rename lib/widgets/common/{ => behaviour}/routes.dart (90%) rename lib/widgets/common/{ => behaviour}/sloppy_scroll_physics.dart (100%) rename lib/widgets/common/{ => fx}/borders.dart (100%) rename lib/widgets/common/{ => fx}/transition_image.dart (100%) rename lib/widgets/common/{ => identity}/aves_expansion_tile.dart (95%) rename lib/widgets/common/{ => identity}/aves_filter_chip.dart (98%) rename lib/widgets/common/{icons.dart => identity/aves_icons.dart} (52%) rename lib/widgets/common/{ => identity}/aves_logo.dart (100%) rename lib/widgets/common/{ => identity}/highlight_title.dart (100%) rename lib/widgets/common/{ => identity}/scroll_thumb.dart (100%) rename lib/widgets/common/{data_providers => providers}/media_query_data_provider.dart (100%) rename lib/widgets/common/{data_providers => providers}/settings_provider.dart (100%) rename lib/widgets/{common/action_delegates => dialogs}/add_shortcut_dialog.dart (98%) rename lib/widgets/{common => dialogs}/aves_dialog.dart (91%) rename lib/widgets/{common => dialogs}/aves_selection_dialog.dart (94%) rename lib/widgets/{common/action_delegates => dialogs}/create_album_dialog.dart (98%) rename lib/widgets/{common/action_delegates => dialogs}/rename_album_dialog.dart (98%) rename lib/widgets/{common/action_delegates => dialogs}/rename_entry_dialog.dart (98%) rename lib/widgets/{common/action_delegates => fullscreen}/entry_action_delegate.dart (94%) diff --git a/lib/widgets/common/image_providers/app_icon_image_provider.dart b/lib/image_providers/app_icon_image_provider.dart similarity index 100% rename from lib/widgets/common/image_providers/app_icon_image_provider.dart rename to lib/image_providers/app_icon_image_provider.dart diff --git a/lib/widgets/common/image_providers/region_provider.dart b/lib/image_providers/region_provider.dart similarity index 100% rename from lib/widgets/common/image_providers/region_provider.dart rename to lib/image_providers/region_provider.dart diff --git a/lib/widgets/common/image_providers/thumbnail_provider.dart b/lib/image_providers/thumbnail_provider.dart similarity index 100% rename from lib/widgets/common/image_providers/thumbnail_provider.dart rename to lib/image_providers/thumbnail_provider.dart diff --git a/lib/widgets/common/image_providers/uri_image_provider.dart b/lib/image_providers/uri_image_provider.dart similarity index 100% rename from lib/widgets/common/image_providers/uri_image_provider.dart rename to lib/image_providers/uri_image_provider.dart diff --git a/lib/widgets/common/image_providers/uri_picture_provider.dart b/lib/image_providers/uri_picture_provider.dart similarity index 100% rename from lib/widgets/common/image_providers/uri_picture_provider.dart rename to lib/image_providers/uri_picture_provider.dart diff --git a/lib/main.dart b/lib/main.dart index 350685a69..92cd32fbc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,10 +2,10 @@ import 'dart:isolate'; import 'dart:ui'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/utils/route_tracker.dart'; -import 'package:aves/widgets/common/data_providers/settings_provider.dart'; -import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/common/routes.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/behaviour/route_tracker.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/providers/settings_provider.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/welcome_page.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; diff --git a/lib/widgets/filter_grids/common/chip_actions.dart b/lib/model/actions/chip_actions.dart similarity index 93% rename from lib/widgets/filter_grids/common/chip_actions.dart rename to lib/model/actions/chip_actions.dart index a63edfd8f..f5cfd1a6e 100644 --- a/lib/widgets/filter_grids/common/chip_actions.dart +++ b/lib/model/actions/chip_actions.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:flutter/widgets.dart'; enum ChipSetAction { diff --git a/lib/widgets/collection/collection_actions.dart b/lib/model/actions/collection_actions.dart similarity index 86% rename from lib/widgets/collection/collection_actions.dart rename to lib/model/actions/collection_actions.dart index c71ba093c..531c8e5f9 100644 --- a/lib/widgets/collection/collection_actions.dart +++ b/lib/model/actions/collection_actions.dart @@ -1,13 +1,14 @@ enum CollectionAction { addShortcut, - copy, + sort, group, - move, refresh, - refreshMetadata, select, selectAll, selectNone, - sort, stats, + // apply to entry set + copy, + move, + refreshMetadata, } diff --git a/lib/widgets/common/entry_actions.dart b/lib/model/actions/entry_actions.dart similarity index 96% rename from lib/widgets/common/entry_actions.dart rename to lib/model/actions/entry_actions.dart index 0b9e70b2f..805df42f0 100644 --- a/lib/widgets/common/entry_actions.dart +++ b/lib/model/actions/entry_actions.dart @@ -1,5 +1,5 @@ -import 'package:aves/widgets/common/icons.dart'; -import 'package:flutter/material.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:flutter/widgets.dart'; enum EntryAction { delete, diff --git a/lib/model/entry_cache.dart b/lib/model/entry_cache.dart index a00303656..b0ef230cc 100644 --- a/lib/model/entry_cache.dart +++ b/lib/model/entry_cache.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:math'; -import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart'; -import 'package:aves/widgets/common/image_providers/uri_image_provider.dart'; +import 'package:aves/image_providers/thumbnail_provider.dart'; +import 'package:aves/image_providers/uri_image_provider.dart'; class EntryCache { static Future evict( diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index aec589bb7..68e0b8e3d 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -1,8 +1,9 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart'; +import 'package:aves/widgets/common/identity/aves_icons.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:palette_generator/palette_generator.dart'; diff --git a/lib/model/filters/favourite.dart b/lib/model/filters/favourite.dart index cee2aff50..d4e9716c1 100644 --- a/lib/model/filters/favourite.dart +++ b/lib/model/filters/favourite.dart @@ -1,6 +1,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/model/filters/location.dart b/lib/model/filters/location.dart index a8bafbe0e..2c4a4a7cc 100644 --- a/lib/model/filters/location.dart +++ b/lib/model/filters/location.dart @@ -1,6 +1,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/model/filters/mime.dart b/lib/model/filters/mime.dart index 891bd48d6..02ac50252 100644 --- a/lib/model/filters/mime.dart +++ b/lib/model/filters/mime.dart @@ -1,7 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/mime_types.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/mime_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -35,7 +35,7 @@ class MimeFilter extends CollectionFilter { _label ??= lowMime.split('/')[0].toUpperCase(); } else { _filter = (entry) => entry.mimeType == lowMime; - _label = MimeTypes.displayType(lowMime); + _label = MimeUtils.displayType(lowMime); } _icon ??= AIcons.vector; } diff --git a/lib/model/filters/query.dart b/lib/model/filters/query.dart index 4a07c2671..b23b27f38 100644 --- a/lib/model/filters/query.dart +++ b/lib/model/filters/query.dart @@ -1,6 +1,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/model/filters/tag.dart b/lib/model/filters/tag.dart index 30198b8f9..3188d583c 100644 --- a/lib/model/filters/tag.dart +++ b/lib/model/filters/tag.dart @@ -1,6 +1,6 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index 340a69dca..63b78dde1 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -17,7 +17,7 @@ import 'package:geocoder/geocoder.dart'; import 'package:latlong/latlong.dart'; import 'package:path/path.dart' as ppath; -import 'mime_types.dart'; +import '../ref/mime_types.dart'; class ImageEntry { String uri; @@ -40,6 +40,9 @@ class ImageEntry { final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier(); + // TODO TLAD make it dynamic if it depends on OS/lib versions + static const List undecodable = [MimeTypes.crw, MimeTypes.psd]; + ImageEntry({ this.uri, String path, @@ -59,7 +62,7 @@ class ImageEntry { this.dateModifiedSecs = dateModifiedSecs; } - bool get canDecode => !MimeTypes.undecodable.contains(mimeType); + bool get canDecode => !undecodable.contains(mimeType); ImageEntry copyWith({ @required String uri, diff --git a/lib/widgets/common/data_providers/media_store_collection_provider.dart b/lib/model/source/media_store_source.dart similarity index 100% rename from lib/widgets/common/data_providers/media_store_collection_provider.dart rename to lib/model/source/media_store_source.dart diff --git a/lib/utils/brand_colors.dart b/lib/ref/brand_colors.dart similarity index 100% rename from lib/utils/brand_colors.dart rename to lib/ref/brand_colors.dart diff --git a/lib/model/mime_types.dart b/lib/ref/mime_types.dart similarity index 72% rename from lib/model/mime_types.dart rename to lib/ref/mime_types.dart index 02f954ae4..537d81121 100644 --- a/lib/model/mime_types.dart +++ b/lib/ref/mime_types.dart @@ -41,24 +41,4 @@ class MimeTypes { // groups static const List rawImages = [arw, cr2, crw, dcr, dng, erf, k25, kdc, mrw, nef, nrw, orf, pef, raf, raw, rw2, sr2, srf, srw, x3f]; - static const List undecodable = [crw, psd]; // TODO TLAD make it dynamic if it depends on OS/lib versions - - static String displayType(String mime) { - switch (mime) { - case 'image/x-icon': - return 'ICO'; - case 'image/vnd.adobe.photoshop': - case 'image/x-photoshop': - return 'PSD'; - default: - final patterns = [ - RegExp('.*/'), // remove type, keep subtype - RegExp('(X-|VND.(WAP.)?)'), // noisy prefixes - '+XML', // noisy suffix - ]; - mime = mime.toUpperCase(); - patterns.forEach((pattern) => mime = mime.replaceFirst(pattern, '')); - return mime; - } - } } diff --git a/lib/utils/xmp.dart b/lib/ref/xmp.dart similarity index 100% rename from lib/utils/xmp.dart rename to lib/ref/xmp.dart diff --git a/lib/services/image_file_service.dart b/lib/services/image_file_service.dart index 0eec5e1ff..c27d72d30 100644 --- a/lib/services/image_file_service.dart +++ b/lib/services/image_file_service.dart @@ -4,7 +4,7 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/mime_types.dart'; +import 'package:aves/ref/mime_types.dart'; import 'package:aves/services/service_policy.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/utils/durations.dart b/lib/theme/durations.dart similarity index 100% rename from lib/utils/durations.dart rename to lib/theme/durations.dart diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart new file mode 100644 index 000000000..6d33fce2b --- /dev/null +++ b/lib/theme/icons.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class AIcons { + static const IconData allCollection = Icons.collections_outlined; + static const IconData image = Icons.photo_outlined; + static const IconData video = Icons.movie_outlined; + static const IconData audio = Icons.audiotrack_outlined; + static const IconData vector = Icons.code_outlined; + + static const IconData android = Icons.android; + static const IconData checked = Icons.done_outlined; + static const IconData date = Icons.calendar_today_outlined; + static const IconData disc = Icons.fiber_manual_record; + static const IconData error = Icons.error_outline; + static const IconData location = Icons.place_outlined; + static const IconData locationOff = Icons.location_off_outlined; + static const IconData raw = Icons.camera_outlined; + static const IconData shooting = Icons.camera_outlined; + static const IconData removableStorage = Icons.sd_storage_outlined; + static const IconData settings = Icons.settings_outlined; + static const IconData text = Icons.format_quote_outlined; + static const IconData tag = Icons.local_offer_outlined; + static const IconData tagOff = MdiIcons.tagOffOutline; + + // actions + static const IconData addShortcut = Icons.add_to_home_screen_outlined; + static const IconData clear = Icons.clear_outlined; + static const IconData collapse = Icons.expand_less_outlined; + static const IconData createAlbum = Icons.add_circle_outline; + static const IconData debug = Icons.whatshot_outlined; + static const IconData delete = Icons.delete_outlined; + static const IconData expand = Icons.expand_more_outlined; + static const IconData flip = Icons.flip_outlined; + static const IconData favourite = Icons.favorite_border; + static const IconData favouriteActive = Icons.favorite; + static const IconData goUp = Icons.arrow_upward_outlined; + static const IconData group = Icons.group_work_outlined; + static const IconData info = Icons.info_outlined; + static const IconData layers = Icons.layers_outlined; + static const IconData openInNew = Icons.open_in_new_outlined; + static const IconData pin = Icons.push_pin_outlined; + static const IconData print = Icons.print_outlined; + static const IconData refresh = Icons.refresh_outlined; + static const IconData rename = Icons.title_outlined; + static const IconData rotateLeft = Icons.rotate_left_outlined; + static const IconData rotateRight = Icons.rotate_right_outlined; + static const IconData search = Icons.search_outlined; + static const IconData select = Icons.select_all_outlined; + static const IconData share = Icons.share_outlined; + static const IconData sort = Icons.sort_outlined; + static const IconData stats = Icons.pie_chart_outlined; + static const IconData zoomIn = Icons.add_outlined; + static const IconData zoomOut = Icons.remove_outlined; + + // albums + static const IconData album = Icons.photo_album_outlined; + static const IconData cameraAlbum = Icons.photo_camera_outlined; + static const IconData downloadAlbum = Icons.file_download; + static const IconData screenshotAlbum = Icons.smartphone_outlined; + + // thumbnail overlay + static const IconData animated = Icons.slideshow; + static const IconData play = Icons.play_circle_outline; + static const IconData selected = Icons.check_circle_outline; + static const IconData unselected = Icons.radio_button_unchecked; +} diff --git a/lib/utils/flutter_utils.dart b/lib/utils/flutter_utils.dart deleted file mode 100644 index 3f06767b8..000000000 --- a/lib/utils/flutter_utils.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:flutter/widgets.dart'; - -extension ExtraContext on BuildContext { - String get currentRouteName => ModalRoute.of(this)?.settings?.name; -} diff --git a/lib/utils/mime_utils.dart b/lib/utils/mime_utils.dart new file mode 100644 index 000000000..52c223f29 --- /dev/null +++ b/lib/utils/mime_utils.dart @@ -0,0 +1,20 @@ +class MimeUtils { + static String displayType(String mime) { + switch (mime) { + case 'image/x-icon': + return 'ICO'; + case 'image/vnd.adobe.photoshop': + case 'image/x-photoshop': + return 'PSD'; + default: + final patterns = [ + RegExp('.*/'), // remove type, keep subtype + RegExp('(X-|VND.(WAP.)?)'), // noisy prefixes + '+XML', // noisy suffix + ]; + mime = mime.toUpperCase(); + patterns.forEach((pattern) => mime = mime.replaceFirst(pattern, '')); + return mime; + } + } +} diff --git a/lib/widgets/about/about_page.dart b/lib/widgets/about/about_page.dart index a2bba1eee..8abc4a8db 100644 --- a/lib/widgets/about/about_page.dart +++ b/lib/widgets/about/about_page.dart @@ -1,7 +1,7 @@ import 'package:aves/flutter_version.dart'; import 'package:aves/widgets/about/licenses.dart'; -import 'package:aves/widgets/common/aves_logo.dart'; -import 'package:aves/widgets/common/link_chip.dart'; +import 'package:aves/widgets/common/basic/link_chip.dart'; +import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:flutter/material.dart'; import 'package:package_info/package_info.dart'; diff --git a/lib/widgets/about/licenses.dart b/lib/widgets/about/licenses.dart index 1944dcbbe..3138ee81c 100644 --- a/lib/widgets/about/licenses.dart +++ b/lib/widgets/about/licenses.dart @@ -1,9 +1,9 @@ -import 'package:aves/utils/brand_colors.dart'; +import 'package:aves/ref/brand_colors.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.dart'; -import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/common/link_chip.dart'; -import 'package:aves/widgets/common/menu_row.dart'; +import 'package:aves/widgets/common/basic/link_chip.dart'; +import 'package:aves/widgets/common/basic/menu_row.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index c0145fc58..2aaef5061 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -6,17 +6,17 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/services/app_shortcut_service.dart'; -import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/collection/collection_actions.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/model/actions/collection_actions.dart'; import 'package:aves/widgets/collection/filter_bar.dart'; -import 'package:aves/widgets/common/action_delegates/add_shortcut_dialog.dart'; -import 'package:aves/widgets/common/action_delegates/selection_action_delegate.dart'; +import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; +import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; -import 'package:aves/widgets/common/aves_selection_dialog.dart'; -import 'package:aves/widgets/common/entry_actions.dart'; -import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/common/menu_row.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/model/actions/entry_actions.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/search/search_button.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/stats/stats.dart'; @@ -42,7 +42,7 @@ class CollectionAppBar extends StatefulWidget { class _CollectionAppBarState extends State with SingleTickerProviderStateMixin { final TextEditingController _searchFieldController = TextEditingController(); - SelectionActionDelegate _actionDelegate; + EntrySetActionDelegate _actionDelegate; AnimationController _browseToSelectAnimation; Future _canAddShortcutsLoader; @@ -55,7 +55,7 @@ class _CollectionAppBarState extends State with SingleTickerPr @override void initState() { super.initState(); - _actionDelegate = SelectionActionDelegate( + _actionDelegate = EntrySetActionDelegate( collection: collection, ); _browseToSelectAnimation = AnimationController( diff --git a/lib/widgets/collection/collection_page.dart b/lib/widgets/collection/collection_page.dart index 35fdb5981..7a52c08f2 100644 --- a/lib/widgets/collection/collection_page.dart +++ b/lib/widgets/collection/collection_page.dart @@ -1,7 +1,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/thumbnail_collection.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/common/behaviour/double_back_pop.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/action_delegates/selection_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart similarity index 90% rename from lib/widgets/common/action_delegates/selection_action_delegate.dart rename to lib/widgets/collection/entry_set_action_delegate.dart index 4c15a613b..33fc671a5 100644 --- a/lib/widgets/common/action_delegates/selection_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -5,26 +5,26 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; -import 'package:aves/widgets/collection/collection_actions.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/size_aware.dart'; -import 'package:aves/widgets/common/aves_dialog.dart'; -import 'package:aves/widgets/common/entry_actions.dart'; +import 'package:aves/model/actions/collection_actions.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; +import 'package:aves/widgets/common/action_mixins/size_aware.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:intl/intl.dart'; -class SelectionActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { +class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMixin { final CollectionLens collection; CollectionSource get source => collection.source; Set get selection => collection.selection; - SelectionActionDelegate({ + EntrySetActionDelegate({ @required this.collection, }); diff --git a/lib/widgets/collection/filter_bar.dart b/lib/widgets/collection/filter_bar.dart index e447f934a..712f21fa3 100644 --- a/lib/widgets/collection/filter_bar.dart +++ b/lib/widgets/collection/filter_bar.dart @@ -1,6 +1,6 @@ import 'package:aves/model/filters/filters.dart'; -import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/common/aves_filter_chip.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/material.dart'; class FilterBar extends StatefulWidget implements PreferredSizeWidget { diff --git a/lib/widgets/collection/grid/header_album.dart b/lib/widgets/collection/grid/header_album.dart index 5afd82fa3..d5ef85b6a 100644 --- a/lib/widgets/collection/grid/header_album.dart +++ b/lib/widgets/collection/grid/header_album.dart @@ -1,6 +1,7 @@ +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/grid/header_generic.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:flutter/material.dart'; class AlbumSectionHeader extends StatelessWidget { diff --git a/lib/widgets/collection/grid/header_generic.dart b/lib/widgets/collection/grid/header_generic.dart index eabeb55d0..6588e746a 100644 --- a/lib/widgets/collection/grid/header_generic.dart +++ b/lib/widgets/collection/grid/header_generic.dart @@ -5,10 +5,10 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/grid/header_album.dart'; import 'package:aves/widgets/collection/grid/header_date.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/collection/grid/list_sliver.dart b/lib/widgets/collection/grid/list_sliver.dart index 3ed09aea1..e275fc223 100644 --- a/lib/widgets/collection/grid/list_sliver.dart +++ b/lib/widgets/collection/grid/list_sliver.dart @@ -4,8 +4,9 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/viewer_service.dart'; import 'package:aves/widgets/collection/grid/list_known_extent.dart'; import 'package:aves/widgets/collection/grid/list_section_layout.dart'; +import 'package:aves/widgets/collection/grid/scaling.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; -import 'package:aves/widgets/common/routes.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -70,7 +71,7 @@ class GridThumbnail extends StatelessWidget { } }, child: MetaData( - metaData: ThumbnailMetadata(entry), + metaData: ScalerMetadata(entry), child: DecoratedThumbnail( entry: entry, extent: tileExtent, @@ -94,10 +95,3 @@ class GridThumbnail extends StatelessWidget { ); } } - -// metadata to identify entry from RenderObject hit test during collection scaling -class ThumbnailMetadata { - final ImageEntry entry; - - const ThumbnailMetadata(this.entry); -} diff --git a/lib/widgets/collection/grid/scaling.dart b/lib/widgets/collection/grid/scaling.dart index c64c31308..fab83f78f 100644 --- a/lib/widgets/collection/grid/scaling.dart +++ b/lib/widgets/collection/grid/scaling.dart @@ -1,23 +1,28 @@ import 'dart:math'; import 'dart:ui' as ui; -import 'package:aves/model/image_entry.dart'; -import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/collection/grid/list_section_layout.dart'; -import 'package:aves/widgets/collection/grid/list_sliver.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/grid/tile_extent_manager.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; -import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:provider/provider.dart'; -class GridScaleGestureDetector extends StatefulWidget { +// metadata to identify entry from RenderObject hit test during collection scaling +class ScalerMetadata { + final T item; + + const ScalerMetadata(this.item); +} + +class GridScaleGestureDetector extends StatefulWidget { final GlobalKey scrollableKey; final ValueNotifier appBarHeightNotifier; final ValueNotifier extentNotifier; final Size viewportSize; - final void Function(ImageEntry entry) onScaled; + final Widget Function(T item, double extent) scaledBuilder; + final Rect Function(T item) getScaledItemTileRect; + final void Function(T item) onScaled; final Widget child; const GridScaleGestureDetector({ @@ -25,20 +30,22 @@ class GridScaleGestureDetector extends StatefulWidget { @required this.appBarHeightNotifier, @required this.extentNotifier, @required this.viewportSize, - this.onScaled, + @required this.scaledBuilder, + @required this.getScaledItemTileRect, + @required this.onScaled, @required this.child, }); @override - _GridScaleGestureDetectorState createState() => _GridScaleGestureDetectorState(); + _GridScaleGestureDetectorState createState() => _GridScaleGestureDetectorState(); } -class _GridScaleGestureDetectorState extends State { +class _GridScaleGestureDetectorState extends State { double _startExtent, _extentMin, _extentMax; bool _applyingScale = false; ValueNotifier _scaledExtentNotifier; OverlayEntry _overlayEntry; - ThumbnailMetadata _metadata; + ScalerMetadata _metadata; ValueNotifier get tileExtentNotifier => widget.extentNotifier; @@ -60,7 +67,7 @@ class _GridScaleGestureDetectorState extends State { scrollableBox.hitTest(result, position: details.localFocalPoint); // find `RenderObject`s at the gesture focal point - T firstOf(BoxHitTestResult result) => result.path.firstWhere((el) => el.target is T, orElse: () => null)?.target as T; + U firstOf(BoxHitTestResult result) => result.path.firstWhere((el) => el.target is U, orElse: () => null)?.target as U; final renderMetaData = firstOf(result); // abort if we cannot find an image to show on overlay if (renderMetaData == null) return; @@ -75,14 +82,12 @@ class _GridScaleGestureDetectorState extends State { final halfExtent = _startExtent / 2; final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent)); _overlayEntry = OverlayEntry( - builder: (context) { - return ScaleOverlay( - imageEntry: _metadata.entry, - center: thumbnailCenter, - gridWidth: gridWidth, - scaledExtentNotifier: _scaledExtentNotifier, - ); - }, + builder: (context) => ScaleOverlay( + builder: (extent) => widget.scaledBuilder(_metadata.item, extent), + center: thumbnailCenter, + gridWidth: gridWidth, + scaledExtentNotifier: _scaledExtentNotifier, + ), ); Overlay.of(scrollableContext).insert(_overlayEntry); }, @@ -112,8 +117,8 @@ class _GridScaleGestureDetectorState extends State { } else { // scroll to show the focal point thumbnail at its new position WidgetsBinding.instance.addPostFrameCallback((_) { - final entry = _metadata.entry; - _scrollToEntry(entry); + final entry = _metadata.item; + _scrollToItem(entry); // warning: posting `onScaled` in the next frame with `addPostFrameCallback` // would trigger only when the scrollable offset actually changes Future.delayed(Durations.collectionScalingCompleteNotificationDelay).then((_) => widget.onScaled?.call(entry)); @@ -129,11 +134,10 @@ class _GridScaleGestureDetectorState extends State { // `Scrollable.ensureVisible` only works on already rendered objects // `RenderViewport.showOnScreen` can find any `RenderSliver`, but not always a `RenderMetadata` // `RenderViewport.scrollOffsetOf` is a good alternative - void _scrollToEntry(ImageEntry entry) { + void _scrollToItem(T item) { final scrollableContext = widget.scrollableKey.currentContext; final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height; - final sectionedListLayout = Provider.of(context, listen: false); - final tileRect = sectionedListLayout.getTileRect(entry) ?? Rect.zero; + final tileRect = widget.getScaledItemTileRect(item); // most of the time the app bar will be scrolled away after scaling, // so we compensate for it to center the focal point thumbnail final appBarHeight = widget.appBarHeightNotifier.value; @@ -144,13 +148,13 @@ class _GridScaleGestureDetectorState extends State { } class ScaleOverlay extends StatefulWidget { - final ImageEntry imageEntry; + final Widget Function(double extent) builder; final Offset center; final double gridWidth; final ValueNotifier scaledExtentNotifier; const ScaleOverlay({ - @required this.imageEntry, + @required this.builder, @required this.center, @required this.gridWidth, @required this.scaledExtentNotifier, @@ -225,11 +229,7 @@ class _ScaleOverlayState extends State { top: clampedCenter.dy - extent / 2, child: DefaultTextStyle( style: TextStyle(), - child: DecoratedThumbnail( - entry: widget.imageEntry, - extent: extent, - showOverlay: false, - ), + child: widget.builder(extent), ), ), ], diff --git a/lib/widgets/collection/thumbnail/error.dart b/lib/widgets/collection/thumbnail/error.dart index 3054616cf..f66274fde 100644 --- a/lib/widgets/collection/thumbnail/error.dart +++ b/lib/widgets/collection/thumbnail/error.dart @@ -1,5 +1,5 @@ import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/mime_types.dart'; +import 'package:aves/utils/mime_utils.dart'; import 'package:flutter/material.dart'; class ErrorThumbnail extends StatelessWidget { @@ -22,7 +22,7 @@ class ErrorThumbnail extends StatelessWidget { message: tooltip, preferBelow: false, child: Text( - MimeTypes.displayType(entry.mimeType), + MimeUtils.displayType(entry.mimeType), style: TextStyle( color: Colors.blueGrey, fontSize: extent / 5, diff --git a/lib/widgets/collection/thumbnail/overlay.dart b/lib/widgets/collection/thumbnail/overlay.dart index bde0dd789..8dba26cbf 100644 --- a/lib/widgets/collection/thumbnail/overlay.dart +++ b/lib/widgets/collection/thumbnail/overlay.dart @@ -4,9 +4,10 @@ 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/enums.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/fx/sweeper.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; diff --git a/lib/widgets/collection/thumbnail/raster.dart b/lib/widgets/collection/thumbnail/raster.dart index 0b70212dd..573caa1dd 100644 --- a/lib/widgets/collection/thumbnail/raster.dart +++ b/lib/widgets/collection/thumbnail/raster.dart @@ -1,11 +1,11 @@ import 'dart:math'; +import 'package:aves/image_providers/thumbnail_provider.dart'; +import 'package:aves/image_providers/uri_image_provider.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/error.dart'; -import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart'; -import 'package:aves/widgets/common/image_providers/uri_image_provider.dart'; -import 'package:aves/widgets/common/transition_image.dart'; +import 'package:aves/widgets/common/fx/transition_image.dart'; import 'package:flutter/material.dart'; class ThumbnailRasterImage extends StatefulWidget { diff --git a/lib/widgets/collection/thumbnail/vector.dart b/lib/widgets/collection/thumbnail/vector.dart index 58f71609d..ed238e02e 100644 --- a/lib/widgets/collection/thumbnail/vector.dart +++ b/lib/widgets/collection/thumbnail/vector.dart @@ -1,6 +1,6 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart'; +import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index 3e1d1f781..e7788cf94 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -2,24 +2,25 @@ import 'dart:async'; 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/image_entry.dart'; +import 'package:aves/ref/mime_types.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/app_bar.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/collection/grid/list_section_layout.dart'; import 'package:aves/widgets/collection/grid/list_sliver.dart'; import 'package:aves/widgets/collection/grid/scaling.dart'; import 'package:aves/widgets/collection/grid/tile_extent_manager.dart'; -import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/common/scroll_thumb.dart'; -import 'package:aves/widgets/common/sloppy_scroll_physics.dart'; +import 'package:aves/widgets/collection/thumbnail/decorated.dart'; +import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; -import 'package:tuple/tuple.dart'; class ThumbnailCollection extends StatelessWidget { final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); @@ -30,16 +31,14 @@ class ThumbnailCollection extends StatelessWidget { @override Widget build(BuildContext context) { return SafeArea( - child: Selector>( - selector: (context, mq) => Tuple2(mq.size, mq.padding.horizontal), - builder: (context, mq, child) { - final mqSize = mq.item1; - final mqHorizontalPadding = mq.item2; + child: LayoutBuilder( + builder: (context, constraints) { + final viewportSize = constraints.biggest; + assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); + if (viewportSize.isEmpty) return SizedBox.shrink(); - if (mqSize.isEmpty) return SizedBox.shrink(); - - TileExtentManager.applyTileExtent(Size(mqSize.width - mqHorizontalPadding, mqSize.height), _tileExtentNotifier); - final cacheExtent = TileExtentManager.extentMaxForSize(mqSize) * 2; + TileExtentManager.applyTileExtent(viewportSize, _tileExtentNotifier); + final cacheExtent = TileExtentManager.extentMaxForSize(viewportSize) * 2; // do not replace by Provider.of // so that view updates on collection filter changes @@ -58,11 +57,20 @@ class ThumbnailCollection extends StatelessWidget { cacheExtent: cacheExtent, ); - final scaler = GridScaleGestureDetector( + final scaler = GridScaleGestureDetector( scrollableKey: _scrollableKey, appBarHeightNotifier: _appBarHeightNotifier, extentNotifier: _tileExtentNotifier, - viewportSize: Size(mqSize.width - mqHorizontalPadding, mqSize.height), + viewportSize: viewportSize, + scaledBuilder: (entry, extent) => DecoratedThumbnail( + entry: entry, + extent: extent, + showOverlay: false, + ), + getScaledItemTileRect: (entry) { + final sectionedListLayout = Provider.of(context, listen: false); + return sectionedListLayout.getTileRect(entry) ?? Rect.zero; + }, onScaled: collection.highlight, child: scrollView, ); @@ -71,7 +79,7 @@ class ThumbnailCollection extends StatelessWidget { valueListenable: _tileExtentNotifier, builder: (context, tileExtent, child) => SectionedListLayoutProvider( collection: collection, - scrollableWidth: mqSize.width - mqHorizontalPadding, + scrollableWidth: viewportSize.width, tileExtent: tileExtent, thumbnailBuilder: (entry) => GridThumbnail( key: ValueKey(entry.contentId), diff --git a/lib/widgets/common/action_delegates/feedback.dart b/lib/widgets/common/action_mixins/feedback.dart similarity index 98% rename from lib/widgets/common/action_delegates/feedback.dart rename to lib/widgets/common/action_mixins/feedback.dart index 4f87464b0..eca65b6c9 100644 --- a/lib/widgets/common/action_delegates/feedback.dart +++ b/lib/widgets/common/action_mixins/feedback.dart @@ -1,6 +1,6 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/services/image_file_service.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:flushbar/flushbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; diff --git a/lib/widgets/common/action_delegates/permission_aware.dart b/lib/widgets/common/action_mixins/permission_aware.dart similarity index 97% rename from lib/widgets/common/action_delegates/permission_aware.dart rename to lib/widgets/common/action_mixins/permission_aware.dart index 69816d96f..58b896830 100644 --- a/lib/widgets/common/action_delegates/permission_aware.dart +++ b/lib/widgets/common/action_mixins/permission_aware.dart @@ -1,9 +1,8 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/services/android_file_service.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/material.dart'; -import '../aves_dialog.dart'; - mixin PermissionAwareMixin { Future checkStoragePermission(BuildContext context, Set entries) { return checkStoragePermissionForAlbums(context, entries.where((e) => e.path != null).map((e) => e.directory).toSet()); diff --git a/lib/widgets/common/action_delegates/size_aware.dart b/lib/widgets/common/action_mixins/size_aware.dart similarity index 97% rename from lib/widgets/common/action_delegates/size_aware.dart rename to lib/widgets/common/action_mixins/size_aware.dart index 1b49dedda..d3d7bd18c 100644 --- a/lib/widgets/common/action_delegates/size_aware.dart +++ b/lib/widgets/common/action_mixins/size_aware.dart @@ -5,7 +5,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; -import 'package:aves/widgets/common/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; diff --git a/lib/widgets/common/app_bar_subtitle.dart b/lib/widgets/common/app_bar_subtitle.dart index 497bf3d69..8b44a7520 100644 --- a/lib/widgets/common/app_bar_subtitle.dart +++ b/lib/widgets/common/app_bar_subtitle.dart @@ -1,5 +1,5 @@ import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:flutter/material.dart'; class SourceStateAwareAppBarTitle extends StatelessWidget { diff --git a/lib/widgets/common/labeled_checkbox.dart b/lib/widgets/common/basic/labeled_checkbox.dart similarity index 100% rename from lib/widgets/common/labeled_checkbox.dart rename to lib/widgets/common/basic/labeled_checkbox.dart diff --git a/lib/widgets/common/link_chip.dart b/lib/widgets/common/basic/link_chip.dart similarity index 96% rename from lib/widgets/common/link_chip.dart rename to lib/widgets/common/basic/link_chip.dart index 8b7e801d8..c0f54a506 100644 --- a/lib/widgets/common/link_chip.dart +++ b/lib/widgets/common/basic/link_chip.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/lib/widgets/common/menu_row.dart b/lib/widgets/common/basic/menu_row.dart similarity index 93% rename from lib/widgets/common/menu_row.dart rename to lib/widgets/common/basic/menu_row.dart index 29cf302bb..c14161c84 100644 --- a/lib/widgets/common/menu_row.dart +++ b/lib/widgets/common/basic/menu_row.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:flutter/material.dart'; class MenuRow extends StatelessWidget { diff --git a/lib/widgets/common/fx/outlined_text.dart b/lib/widgets/common/basic/outlined_text.dart similarity index 100% rename from lib/widgets/common/fx/outlined_text.dart rename to lib/widgets/common/basic/outlined_text.dart diff --git a/lib/widgets/common/aves_radio_list_tile.dart b/lib/widgets/common/basic/reselectable_radio_list_tile.dart similarity index 96% rename from lib/widgets/common/aves_radio_list_tile.dart rename to lib/widgets/common/basic/reselectable_radio_list_tile.dart index 424a26b10..f90a4a159 100644 --- a/lib/widgets/common/aves_radio_list_tile.dart +++ b/lib/widgets/common/basic/reselectable_radio_list_tile.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; // `RadioListTile` that can trigger `onChanged` on tap when already selected, if `reselectable` is true -class AvesRadioListTile extends StatelessWidget { +class ReselectableRadioListTile extends StatelessWidget { final T value; final T groupValue; final ValueChanged onChanged; @@ -19,7 +19,7 @@ class AvesRadioListTile extends StatelessWidget { bool get checked => value == groupValue; - const AvesRadioListTile({ + const ReselectableRadioListTile({ Key key, @required this.value, @required this.groupValue, diff --git a/lib/widgets/common/double_back_pop.dart b/lib/widgets/common/behaviour/double_back_pop.dart similarity index 92% rename from lib/widgets/common/double_back_pop.dart rename to lib/widgets/common/behaviour/double_back_pop.dart index dda008461..4a844fdda 100644 --- a/lib/widgets/common/double_back_pop.dart +++ b/lib/widgets/common/behaviour/double_back_pop.dart @@ -1,8 +1,8 @@ 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:aves/theme/durations.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:overlay_support/overlay_support.dart'; diff --git a/lib/utils/route_tracker.dart b/lib/widgets/common/behaviour/route_tracker.dart similarity index 100% rename from lib/utils/route_tracker.dart rename to lib/widgets/common/behaviour/route_tracker.dart diff --git a/lib/widgets/common/routes.dart b/lib/widgets/common/behaviour/routes.dart similarity index 90% rename from lib/widgets/common/routes.dart rename to lib/widgets/common/behaviour/routes.dart index a5ac9d079..03e460480 100644 --- a/lib/widgets/common/routes.dart +++ b/lib/widgets/common/behaviour/routes.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; +extension ExtraContext on BuildContext { + String get currentRouteName => ModalRoute.of(this)?.settings?.name; +} + class DirectMaterialPageRoute extends PageRouteBuilder { DirectMaterialPageRoute({ RouteSettings settings, diff --git a/lib/widgets/common/sloppy_scroll_physics.dart b/lib/widgets/common/behaviour/sloppy_scroll_physics.dart similarity index 100% rename from lib/widgets/common/sloppy_scroll_physics.dart rename to lib/widgets/common/behaviour/sloppy_scroll_physics.dart diff --git a/lib/widgets/common/borders.dart b/lib/widgets/common/fx/borders.dart similarity index 100% rename from lib/widgets/common/borders.dart rename to lib/widgets/common/fx/borders.dart diff --git a/lib/widgets/common/fx/sweeper.dart b/lib/widgets/common/fx/sweeper.dart index 92bf7bd42..9a44bc053 100644 --- a/lib/widgets/common/fx/sweeper.dart +++ b/lib/widgets/common/fx/sweeper.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; diff --git a/lib/widgets/common/transition_image.dart b/lib/widgets/common/fx/transition_image.dart similarity index 100% rename from lib/widgets/common/transition_image.dart rename to lib/widgets/common/fx/transition_image.dart diff --git a/lib/widgets/common/aves_expansion_tile.dart b/lib/widgets/common/identity/aves_expansion_tile.dart similarity index 95% rename from lib/widgets/common/aves_expansion_tile.dart rename to lib/widgets/common/identity/aves_expansion_tile.dart index e1a60a9fd..7ae1393c6 100644 --- a/lib/widgets/common/aves_expansion_tile.dart +++ b/lib/widgets/common/identity/aves_expansion_tile.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/common/highlight_title.dart'; +import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:expansion_tile_card/expansion_tile_card.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart similarity index 98% rename from lib/widgets/common/aves_filter_chip.dart rename to lib/widgets/common/identity/aves_filter_chip.dart index 314a2a38b..732d27485 100644 --- a/lib/widgets/common/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -1,6 +1,6 @@ import 'package:aves/model/filters/filters.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/icons.dart'; import 'package:flutter/material.dart'; typedef FilterCallback = void Function(CollectionFilter filter); @@ -28,7 +28,7 @@ class AvesFilterChip extends StatefulWidget { const AvesFilterChip({ Key key, - this.filter, + @required this.filter, this.removable = false, this.showGenericIcon = true, this.background, @@ -36,7 +36,8 @@ class AvesFilterChip extends StatefulWidget { this.heroType = HeroType.onTap, @required this.onTap, this.onLongPress, - }) : super(key: key); + }) : assert(filter != null), + super(key: key); @override _AvesFilterChipState createState() => _AvesFilterChipState(); diff --git a/lib/widgets/common/icons.dart b/lib/widgets/common/identity/aves_icons.dart similarity index 52% rename from lib/widgets/common/icons.dart rename to lib/widgets/common/identity/aves_icons.dart index 13fd1fa36..c6ed8fb77 100644 --- a/lib/widgets/common/icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -1,77 +1,12 @@ import 'dart:ui'; +import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/image_providers/app_icon_image_provider.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; -import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; - -class AIcons { - static const IconData allCollection = Icons.collections_outlined; - static const IconData image = Icons.photo_outlined; - static const IconData video = Icons.movie_outlined; - static const IconData audio = Icons.audiotrack_outlined; - static const IconData vector = Icons.code_outlined; - - static const IconData android = Icons.android; - static const IconData checked = Icons.done_outlined; - static const IconData date = Icons.calendar_today_outlined; - static const IconData disc = Icons.fiber_manual_record; - static const IconData error = Icons.error_outline; - static const IconData location = Icons.place_outlined; - static const IconData locationOff = Icons.location_off_outlined; - static const IconData raw = Icons.camera_outlined; - static const IconData shooting = Icons.camera_outlined; - static const IconData removableStorage = Icons.sd_storage_outlined; - static const IconData settings = Icons.settings_outlined; - static const IconData text = Icons.format_quote_outlined; - static const IconData tag = Icons.local_offer_outlined; - static const IconData tagOff = MdiIcons.tagOffOutline; - - // actions - static const IconData addShortcut = Icons.add_to_home_screen_outlined; - static const IconData clear = Icons.clear_outlined; - static const IconData collapse = Icons.expand_less_outlined; - static const IconData createAlbum = Icons.add_circle_outline; - static const IconData debug = Icons.whatshot_outlined; - static const IconData delete = Icons.delete_outlined; - static const IconData expand = Icons.expand_more_outlined; - static const IconData flip = Icons.flip_outlined; - static const IconData favourite = Icons.favorite_border; - static const IconData favouriteActive = Icons.favorite; - static const IconData goUp = Icons.arrow_upward_outlined; - static const IconData group = Icons.group_work_outlined; - static const IconData info = Icons.info_outlined; - static const IconData layers = Icons.layers_outlined; - static const IconData openInNew = Icons.open_in_new_outlined; - static const IconData pin = Icons.push_pin_outlined; - static const IconData print = Icons.print_outlined; - static const IconData refresh = Icons.refresh_outlined; - static const IconData rename = Icons.title_outlined; - static const IconData rotateLeft = Icons.rotate_left_outlined; - static const IconData rotateRight = Icons.rotate_right_outlined; - static const IconData search = Icons.search_outlined; - static const IconData select = Icons.select_all_outlined; - static const IconData share = Icons.share_outlined; - static const IconData sort = Icons.sort_outlined; - static const IconData stats = Icons.pie_chart_outlined; - static const IconData zoomIn = Icons.add_outlined; - static const IconData zoomOut = Icons.remove_outlined; - - // albums - static const IconData album = Icons.photo_album_outlined; - static const IconData cameraAlbum = Icons.photo_camera_outlined; - static const IconData downloadAlbum = Icons.file_download; - static const IconData screenshotAlbum = Icons.smartphone_outlined; - - // thumbnail overlay - static const IconData animated = Icons.slideshow; - static const IconData play = Icons.play_circle_outline; - static const IconData selected = Icons.check_circle_outline; - static const IconData unselected = Icons.radio_button_unchecked; -} class VideoIcon extends StatelessWidget { final ImageEntry entry; diff --git a/lib/widgets/common/aves_logo.dart b/lib/widgets/common/identity/aves_logo.dart similarity index 100% rename from lib/widgets/common/aves_logo.dart rename to lib/widgets/common/identity/aves_logo.dart diff --git a/lib/widgets/common/highlight_title.dart b/lib/widgets/common/identity/highlight_title.dart similarity index 100% rename from lib/widgets/common/highlight_title.dart rename to lib/widgets/common/identity/highlight_title.dart diff --git a/lib/widgets/common/scroll_thumb.dart b/lib/widgets/common/identity/scroll_thumb.dart similarity index 100% rename from lib/widgets/common/scroll_thumb.dart rename to lib/widgets/common/identity/scroll_thumb.dart diff --git a/lib/widgets/common/data_providers/media_query_data_provider.dart b/lib/widgets/common/providers/media_query_data_provider.dart similarity index 100% rename from lib/widgets/common/data_providers/media_query_data_provider.dart rename to lib/widgets/common/providers/media_query_data_provider.dart diff --git a/lib/widgets/common/data_providers/settings_provider.dart b/lib/widgets/common/providers/settings_provider.dart similarity index 100% rename from lib/widgets/common/data_providers/settings_provider.dart rename to lib/widgets/common/providers/settings_provider.dart diff --git a/lib/widgets/debug/android_env.dart b/lib/widgets/debug/android_env.dart index cb0fa1aa5..e590c9cfe 100644 --- a/lib/widgets/debug/android_env.dart +++ b/lib/widgets/debug/android_env.dart @@ -1,7 +1,7 @@ import 'dart:collection'; import 'package:aves/services/android_app_service.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/debug/app_debug_page.dart b/lib/widgets/debug/app_debug_page.dart index 2ae163bf1..a3c609ab7 100644 --- a/lib/widgets/debug/app_debug_page.dart +++ b/lib/widgets/debug/app_debug_page.dart @@ -1,7 +1,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.dart'; -import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/debug/android_env.dart'; import 'package:aves/widgets/debug/cache.dart'; import 'package:aves/widgets/debug/database.dart'; diff --git a/lib/widgets/debug/cache.dart b/lib/widgets/debug/cache.dart index eeb85e308..09b8e9aa3 100644 --- a/lib/widgets/debug/cache.dart +++ b/lib/widgets/debug/cache.dart @@ -1,6 +1,6 @@ import 'package:aves/services/image_file_service.dart'; import 'package:aves/utils/file_utils.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; diff --git a/lib/widgets/debug/database.dart b/lib/widgets/debug/database.dart index 5c8f9ff19..bcf80693b 100644 --- a/lib/widgets/debug/database.dart +++ b/lib/widgets/debug/database.dart @@ -3,7 +3,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/utils/file_utils.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:flutter/material.dart'; class DebugAppDatabaseSection extends StatefulWidget { diff --git a/lib/widgets/debug/firebase.dart b/lib/widgets/debug/firebase.dart index f98d14a91..9551cbfe1 100644 --- a/lib/widgets/debug/firebase.dart +++ b/lib/widgets/debug/firebase.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/common/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index 36e68c18f..05e58c137 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -1,5 +1,5 @@ import 'package:aves/model/settings/settings.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/debug/storage.dart b/lib/widgets/debug/storage.dart index 1e62b90bb..2c22b29f9 100644 --- a/lib/widgets/debug/storage.dart +++ b/lib/widgets/debug/storage.dart @@ -1,7 +1,7 @@ import 'package:aves/services/android_file_service.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/action_delegates/add_shortcut_dialog.dart b/lib/widgets/dialogs/add_shortcut_dialog.dart similarity index 98% rename from lib/widgets/common/action_delegates/add_shortcut_dialog.dart rename to lib/widgets/dialogs/add_shortcut_dialog.dart index 93e2522a2..b321ec0c1 100644 --- a/lib/widgets/common/action_delegates/add_shortcut_dialog.dart +++ b/lib/widgets/dialogs/add_shortcut_dialog.dart @@ -1,7 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:flutter/material.dart'; -import '../aves_dialog.dart'; +import 'aves_dialog.dart'; class AddShortcutDialog extends StatefulWidget { final Set filters; diff --git a/lib/widgets/common/aves_dialog.dart b/lib/widgets/dialogs/aves_dialog.dart similarity index 91% rename from lib/widgets/common/aves_dialog.dart rename to lib/widgets/dialogs/aves_dialog.dart index 2753de93d..adf8898f8 100644 --- a/lib/widgets/common/aves_dialog.dart +++ b/lib/widgets/dialogs/aves_dialog.dart @@ -14,11 +14,13 @@ class AvesDialog extends AlertDialog { @required List actions, }) : assert((scrollableContent != null) ^ (content != null)), super( - title: title != null ? Padding( - // padding to avoid transparent border overlapping - padding: EdgeInsets.symmetric(horizontal: borderWidth), - child: DialogTitle(title: title), - ) : null, + title: title != null + ? Padding( + // padding to avoid transparent border overlapping + padding: EdgeInsets.symmetric(horizontal: borderWidth), + child: DialogTitle(title: title), + ) + : null, titlePadding: EdgeInsets.zero, // the `scrollable` flag of `AlertDialog` makes it // scroll both the title and the content together, diff --git a/lib/widgets/common/aves_selection_dialog.dart b/lib/widgets/dialogs/aves_selection_dialog.dart similarity index 94% rename from lib/widgets/common/aves_selection_dialog.dart rename to lib/widgets/dialogs/aves_selection_dialog.dart index 920626ffe..c4e5c7e4b 100644 --- a/lib/widgets/common/aves_selection_dialog.dart +++ b/lib/widgets/dialogs/aves_selection_dialog.dart @@ -1,4 +1,4 @@ -import 'package:aves/widgets/common/aves_radio_list_tile.dart'; +import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -48,7 +48,7 @@ class _AvesSelectionDialogState extends State { } Widget _buildRadioListTile(T value, String title) { - return AvesRadioListTile( + return ReselectableRadioListTile( key: Key(value.toString()), value: value, groupValue: _selectedValue, diff --git a/lib/widgets/common/action_delegates/create_album_dialog.dart b/lib/widgets/dialogs/create_album_dialog.dart similarity index 98% rename from lib/widgets/common/action_delegates/create_album_dialog.dart rename to lib/widgets/dialogs/create_album_dialog.dart index 02d41d050..101d35d4e 100644 --- a/lib/widgets/common/action_delegates/create_album_dialog.dart +++ b/lib/widgets/dialogs/create_album_dialog.dart @@ -1,12 +1,12 @@ import 'dart:io'; import 'package:aves/utils/android_file_utils.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:path/path.dart'; -import '../aves_dialog.dart'; +import 'aves_dialog.dart'; class CreateAlbumDialog extends StatefulWidget { @override diff --git a/lib/widgets/common/action_delegates/rename_album_dialog.dart b/lib/widgets/dialogs/rename_album_dialog.dart similarity index 98% rename from lib/widgets/common/action_delegates/rename_album_dialog.dart rename to lib/widgets/dialogs/rename_album_dialog.dart index 76e90580a..f9e3f10f4 100644 --- a/lib/widgets/common/action_delegates/rename_album_dialog.dart +++ b/lib/widgets/dialogs/rename_album_dialog.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; -import '../aves_dialog.dart'; +import '../dialogs/aves_dialog.dart'; class RenameAlbumDialog extends StatefulWidget { final String album; diff --git a/lib/widgets/common/action_delegates/rename_entry_dialog.dart b/lib/widgets/dialogs/rename_entry_dialog.dart similarity index 98% rename from lib/widgets/common/action_delegates/rename_entry_dialog.dart rename to lib/widgets/dialogs/rename_entry_dialog.dart index 7bd875f14..b28e25730 100644 --- a/lib/widgets/common/action_delegates/rename_entry_dialog.dart +++ b/lib/widgets/dialogs/rename_entry_dialog.dart @@ -4,7 +4,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' as path; -import '../aves_dialog.dart'; +import 'aves_dialog.dart'; class RenameEntryDialog extends StatefulWidget { final ImageEntry entry; diff --git a/lib/widgets/drawer/app_drawer.dart b/lib/widgets/drawer/app_drawer.dart index cbb1d70ad..1daea1536 100644 --- a/lib/widgets/drawer/app_drawer.dart +++ b/lib/widgets/drawer/app_drawer.dart @@ -3,15 +3,16 @@ 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/ref/mime_types.dart'; +import 'package:aves/theme/icons.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/common/identity/aves_icons.dart'; +import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/debug/app_debug_page.dart'; import 'package:aves/widgets/drawer/collection_tile.dart'; import 'package:aves/widgets/drawer/tile.dart'; diff --git a/lib/widgets/drawer/tile.dart b/lib/widgets/drawer/tile.dart index 3fbbfbecb..b2bbb7614 100644 --- a/lib/widgets/drawer/tile.dart +++ b/lib/widgets/drawer/tile.dart @@ -1,6 +1,6 @@ import 'dart:ui'; -import 'package:aves/utils/flutter_utils.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 09c653e15..0ac6b4413 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -1,14 +1,14 @@ +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/debouncer.dart'; -import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/collection/empty.dart'; -import 'package:aves/widgets/common/action_delegates/create_album_dialog.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/dialogs/create_album_dialog.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; -import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index a83481524..a7604e3ef 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -7,9 +7,9 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/empty.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; -import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:collection/collection.dart'; diff --git a/lib/widgets/filter_grids/common/chip_action_delegate.dart b/lib/widgets/filter_grids/common/chip_action_delegate.dart index 40e708dc7..15fec7a8c 100644 --- a/lib/widgets/filter_grids/common/chip_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_action_delegate.dart @@ -1,14 +1,14 @@ +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/image_file_service.dart'; -import 'package:aves/widgets/common/action_delegates/feedback.dart'; -import 'package:aves/widgets/common/action_delegates/permission_aware.dart'; -import 'package:aves/widgets/common/action_delegates/rename_album_dialog.dart'; -import 'package:aves/widgets/common/action_delegates/size_aware.dart'; -import 'package:aves/widgets/common/aves_dialog.dart'; -import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; +import 'package:aves/widgets/common/action_mixins/size_aware.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/rename_album_dialog.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import 'package:path/path.dart' as path; diff --git a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart index b71436362..4e2db7de1 100644 --- a/lib/widgets/filter_grids/common/chip_set_action_delegate.dart +++ b/lib/widgets/filter_grids/common/chip_set_action_delegate.dart @@ -1,8 +1,8 @@ +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; -import 'package:aves/widgets/common/aves_selection_dialog.dart'; -import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/stats/stats.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index 19a2d4908..245b8cd12 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -6,11 +6,11 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/raster.dart'; import 'package:aves/widgets/collection/thumbnail/vector.dart'; -import 'package:aves/widgets/common/aves_filter_chip.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 33288111d..1a14338fb 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -6,19 +6,19 @@ 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_source.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.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/double_back_pop.dart'; -import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/common/menu_row.dart'; -import 'package:aves/widgets/common/scroll_thumb.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; +import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/menu_row.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:aves/widgets/drawer/app_drawer.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; -import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart'; import 'package:aves/widgets/search/search_button.dart'; diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index 170c837f0..0e5bec313 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/image_entry.dart'; @@ -5,10 +6,9 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/location.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; -import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; -import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:collection/collection.dart'; diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index 97ebc9468..312f4d926 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -6,9 +6,9 @@ import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/tag.dart'; import 'package:aves/widgets/collection/empty.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; -import 'package:aves/widgets/filter_grids/common/chip_actions.dart'; +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:collection/collection.dart'; diff --git a/lib/widgets/fullscreen/debug/metadata.dart b/lib/widgets/fullscreen/debug/metadata.dart index d4b03340f..dca1ec6ef 100644 --- a/lib/widgets/fullscreen/debug/metadata.dart +++ b/lib/widgets/fullscreen/debug/metadata.dart @@ -4,7 +4,7 @@ import 'dart:typed_data'; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/metadata_service.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/common/action_delegates/entry_action_delegate.dart b/lib/widgets/fullscreen/entry_action_delegate.dart similarity index 94% rename from lib/widgets/common/action_delegates/entry_action_delegate.dart rename to lib/widgets/fullscreen/entry_action_delegate.dart index 3b0e4c63e..e77491913 100644 --- a/lib/widgets/common/action_delegates/entry_action_delegate.dart +++ b/lib/widgets/fullscreen/entry_action_delegate.dart @@ -1,13 +1,13 @@ +import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; -import 'package:aves/widgets/common/action_delegates/feedback.dart'; -import 'package:aves/widgets/common/action_delegates/permission_aware.dart'; -import 'package:aves/widgets/common/action_delegates/rename_entry_dialog.dart'; -import 'package:aves/widgets/common/aves_dialog.dart'; -import 'package:aves/widgets/common/entry_actions.dart'; -import 'package:aves/widgets/common/image_providers/uri_image_provider.dart'; +import 'package:aves/widgets/common/action_mixins/feedback.dart'; +import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/rename_entry_dialog.dart'; import 'package:aves/widgets/fullscreen/fullscreen_debug_page.dart'; import 'package:aves/widgets/fullscreen/source_viewer_page.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/fullscreen/fullscreen_body.dart b/lib/widgets/fullscreen/fullscreen_body.dart index bbb8b3011..3fce014bf 100644 --- a/lib/widgets/fullscreen/fullscreen_body.dart +++ b/lib/widgets/fullscreen/fullscreen_body.dart @@ -6,9 +6,9 @@ import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/utils/change_notifier.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/collection_page.dart'; -import 'package:aves/widgets/common/action_delegates/entry_action_delegate.dart'; +import 'package:aves/widgets/fullscreen/entry_action_delegate.dart'; import 'package:aves/widgets/fullscreen/image_page.dart'; import 'package:aves/widgets/fullscreen/image_view.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; diff --git a/lib/widgets/fullscreen/fullscreen_debug_page.dart b/lib/widgets/fullscreen/fullscreen_debug_page.dart index 2a79c4481..50e55e24e 100644 --- a/lib/widgets/fullscreen/fullscreen_debug_page.dart +++ b/lib/widgets/fullscreen/fullscreen_debug_page.dart @@ -1,8 +1,8 @@ import 'package:aves/main.dart'; import 'package:aves/model/image_entry.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/uri_picture_provider.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/image_providers/thumbnail_provider.dart'; +import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/widgets/fullscreen/debug/db.dart'; import 'package:aves/widgets/fullscreen/debug/metadata.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; diff --git a/lib/widgets/fullscreen/fullscreen_page.dart b/lib/widgets/fullscreen/fullscreen_page.dart index 142bfd344..8691f2f3e 100644 --- a/lib/widgets/fullscreen/fullscreen_page.dart +++ b/lib/widgets/fullscreen/fullscreen_page.dart @@ -1,6 +1,6 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/fullscreen/fullscreen_body.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/fullscreen/image_view.dart b/lib/widgets/fullscreen/image_view.dart index b40fa028a..c5ee6c31d 100644 --- a/lib/widgets/fullscreen/image_view.dart +++ b/lib/widgets/fullscreen/image_view.dart @@ -1,12 +1,12 @@ import 'dart:async'; +import 'package:aves/image_providers/thumbnail_provider.dart'; +import 'package:aves/image_providers/uri_image_provider.dart'; +import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.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/uri_image_provider.dart'; -import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart'; import 'package:aves/widgets/fullscreen/tiled_view.dart'; import 'package:aves/widgets/fullscreen/video_view.dart'; import 'package:flutter/foundation.dart'; diff --git a/lib/widgets/fullscreen/info/basic_section.dart b/lib/widgets/fullscreen/info/basic_section.dart index 6fa89b94c..7b07a39a9 100644 --- a/lib/widgets/fullscreen/info/basic_section.dart +++ b/lib/widgets/fullscreen/info/basic_section.dart @@ -4,11 +4,11 @@ import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/mime_types.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/ref/mime_types.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/file_utils.dart'; -import 'package:aves/widgets/common/aves_filter_chip.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/fullscreen/info/common.dart b/lib/widgets/fullscreen/info/common.dart index 5df0ffc7c..d6d7acd4c 100644 --- a/lib/widgets/fullscreen/info/common.dart +++ b/lib/widgets/fullscreen/info/common.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/widgets/common/aves_filter_chip.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index edaedd16d..bf9f10780 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -1,9 +1,9 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/fullscreen/info/basic_section.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:aves/widgets/fullscreen/info/metadata/metadata_section.dart'; diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index acd8e5e59..e6d1475d7 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -3,9 +3,9 @@ import 'package:aves/model/image_entry.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/utils/durations.dart'; -import 'package:aves/widgets/common/aves_filter_chip.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:aves/widgets/fullscreen/info/maps/common.dart'; import 'package:aves/widgets/fullscreen/info/maps/google_map.dart'; diff --git a/lib/widgets/fullscreen/info/maps/common.dart b/lib/widgets/fullscreen/info/maps/common.dart index 37a311cc3..97e057672 100644 --- a/lib/widgets/fullscreen/info/maps/common.dart +++ b/lib/widgets/fullscreen/info/maps/common.dart @@ -1,11 +1,11 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/android_app_service.dart'; -import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/common/aves_dialog.dart'; -import 'package:aves/widgets/common/aves_selection_dialog.dart'; -import 'package:aves/widgets/common/borders.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/fullscreen/info/maps/scale_layer.dart b/lib/widgets/fullscreen/info/maps/scale_layer.dart index 7d821ee83..0c9aa1360 100644 --- a/lib/widgets/fullscreen/info/maps/scale_layer.dart +++ b/lib/widgets/fullscreen/info/maps/scale_layer.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'package:aves/widgets/common/fx/outlined_text.dart'; +import 'package:aves/widgets/common/basic/outlined_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/plugin_api.dart'; diff --git a/lib/widgets/fullscreen/info/metadata/metadata_section.dart b/lib/widgets/fullscreen/info/metadata/metadata_section.dart index b3f558e12..5084c5088 100644 --- a/lib/widgets/fullscreen/info/metadata/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata/metadata_section.dart @@ -2,12 +2,12 @@ import 'dart:collection'; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/metadata_service.dart'; -import 'package:aves/utils/brand_colors.dart'; +import 'package:aves/ref/brand_colors.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart'; import 'package:aves/widgets/fullscreen/info/metadata/xmp_tile.dart'; diff --git a/lib/widgets/fullscreen/info/metadata/xmp_tile.dart b/lib/widgets/fullscreen/info/metadata/xmp_tile.dart index f7a7175ee..49d469aff 100644 --- a/lib/widgets/fullscreen/info/metadata/xmp_tile.dart +++ b/lib/widgets/fullscreen/info/metadata/xmp_tile.dart @@ -1,11 +1,11 @@ import 'dart:collection'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/utils/brand_colors.dart'; +import 'package:aves/ref/brand_colors.dart'; +import 'package:aves/ref/xmp.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/utils/xmp.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.dart'; -import 'package:aves/widgets/common/highlight_title.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/identity/highlight_title.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart'; import 'package:collection/collection.dart'; diff --git a/lib/widgets/fullscreen/overlay/bottom.dart b/lib/widgets/fullscreen/overlay/bottom.dart index af5d068c2..1144ff0b7 100644 --- a/lib/widgets/fullscreen/overlay/bottom.dart +++ b/lib/widgets/fullscreen/overlay/bottom.dart @@ -6,9 +6,9 @@ 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/utils/constants.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/fullscreen/overlay/common.dart b/lib/widgets/fullscreen/overlay/common.dart index c5830d2d5..9c7183bee 100644 --- a/lib/widgets/fullscreen/overlay/common.dart +++ b/lib/widgets/fullscreen/overlay/common.dart @@ -1,5 +1,5 @@ -import 'package:aves/widgets/common/borders.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; import 'package:flutter/material.dart'; const kOverlayBackgroundColor = Colors.black26; diff --git a/lib/widgets/fullscreen/overlay/top.dart b/lib/widgets/fullscreen/overlay/top.dart index 0d4c03aa5..5626c06f5 100644 --- a/lib/widgets/fullscreen/overlay/top.dart +++ b/lib/widgets/fullscreen/overlay/top.dart @@ -1,13 +1,13 @@ import 'dart:math'; +import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/common/entry_actions.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/common/fx/sweeper.dart'; -import 'package:aves/widgets/common/icons.dart'; -import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/fullscreen/image_view.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:aves/widgets/fullscreen/overlay/minimap.dart'; diff --git a/lib/widgets/fullscreen/overlay/video.dart b/lib/widgets/fullscreen/overlay/video.dart index df66e30d9..47e7a69b8 100644 --- a/lib/widgets/fullscreen/overlay/video.dart +++ b/lib/widgets/fullscreen/overlay/video.dart @@ -2,11 +2,11 @@ import 'dart:async'; import 'package:aves/model/image_entry.dart'; import 'package:aves/services/android_app_service.dart'; -import 'package:aves/utils/durations.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/time_utils.dart'; -import 'package:aves/widgets/common/borders.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; diff --git a/lib/widgets/fullscreen/tiled_view.dart b/lib/widgets/fullscreen/tiled_view.dart index 2afd375c3..ad123a960 100644 --- a/lib/widgets/fullscreen/tiled_view.dart +++ b/lib/widgets/fullscreen/tiled_view.dart @@ -2,7 +2,7 @@ import 'dart:math'; import 'package:aves/model/image_entry.dart'; import 'package:aves/utils/math_utils.dart'; -import 'package:aves/widgets/common/image_providers/region_provider.dart'; +import 'package:aves/image_providers/region_provider.dart'; import 'package:aves/widgets/fullscreen/image_view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/fullscreen/video_view.dart b/lib/widgets/fullscreen/video_view.dart index d98aa2073..7007ebb22 100644 --- a/lib/widgets/fullscreen/video_view.dart +++ b/lib/widgets/fullscreen/video_view.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:ui'; +import 'package:aves/image_providers/uri_image_provider.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/widgets/common/image_providers/uri_image_provider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index a75c777d4..e848d0a0b 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -5,12 +5,12 @@ import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/model/source/media_store_source.dart'; import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/viewer_service.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; -import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; -import 'package:aves/widgets/common/routes.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/filter_grids/albums_page.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; import 'package:aves/widgets/search/search_delegate.dart'; diff --git a/lib/widgets/search/expandable_filter_row.dart b/lib/widgets/search/expandable_filter_row.dart index d7f366083..db02cadeb 100644 --- a/lib/widgets/search/expandable_filter_row.dart +++ b/lib/widgets/search/expandable_filter_row.dart @@ -1,8 +1,8 @@ import 'package:aves/model/filters/filters.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/common/aves_filter_chip.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:flutter/material.dart'; class ExpandableFilterRow extends StatelessWidget { diff --git a/lib/widgets/search/search_button.dart b/lib/widgets/search/search_button.dart index 4fac9bb1d..70fbefb06 100644 --- a/lib/widgets/search/search_button.dart +++ b/lib/widgets/search/search_button.dart @@ -1,6 +1,6 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/search/search_delegate.dart b/lib/widgets/search/search_delegate.dart index 4e64b4e29..754150d56 100644 --- a/lib/widgets/search/search_delegate.dart +++ b/lib/widgets/search/search_delegate.dart @@ -5,16 +5,16 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/mime_types.dart'; import 'package:aves/model/settings/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/ref/mime_types.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/collection_page.dart'; -import 'package:aves/widgets/common/aves_filter_chip.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/search/expandable_filter_row.dart'; import 'package:aves/widgets/search/search_page.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/search/search_page.dart b/lib/widgets/search/search_page.dart index b1a7698a0..171f0a8b6 100644 --- a/lib/widgets/search/search_page.dart +++ b/lib/widgets/search/search_page.dart @@ -1,5 +1,5 @@ +import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; -import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index 2000c2405..8661c13d5 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -2,12 +2,12 @@ import 'package:aves/model/settings/coordinate_format.dart'; import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/theme/durations.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/common/aves_expansion_tile.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/highlight_title.dart'; +import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; +import 'package:aves/widgets/common/identity/highlight_title.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/settings/access_grants.dart'; import 'package:aves/widgets/settings/svg_background.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/settings/svg_background.dart b/lib/widgets/settings/svg_background.dart index b022a2671..929b5a8ad 100644 --- a/lib/widgets/settings/svg_background.dart +++ b/lib/widgets/settings/svg_background.dart @@ -1,5 +1,5 @@ import 'package:aves/model/settings/settings.dart'; -import 'package:aves/widgets/common/borders.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; import 'package:flutter/material.dart'; class SvgBackgroundSelector extends StatefulWidget { diff --git a/lib/widgets/stats/filter_table.dart b/lib/widgets/stats/filter_table.dart index ed12727e5..97964940e 100644 --- a/lib/widgets/stats/filter_table.dart +++ b/lib/widgets/stats/filter_table.dart @@ -1,7 +1,7 @@ import 'package:aves/model/filters/filters.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/widgets/common/aves_filter_chip.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; diff --git a/lib/widgets/stats/stats.dart b/lib/widgets/stats/stats.dart index 797c2449d..4b343fbca 100644 --- a/lib/widgets/stats/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -5,16 +5,16 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/model/mime_types.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/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; +import 'package:aves/utils/mime_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/empty.dart'; -import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/stats/filter_table.dart'; import 'package:charts_flutter/flutter.dart' as charts; import 'package:collection/collection.dart'; @@ -290,7 +290,7 @@ class EntryByMimeDatum { EntryByMimeDatum({ @required this.mimeType, @required this.entryCount, - }) : displayText = MimeTypes.displayType(mimeType); + }) : displayText = MimeUtils.displayType(mimeType); Color get color => stringToColor(displayText); diff --git a/lib/widgets/welcome_page.dart b/lib/widgets/welcome_page.dart index fb8b1f64b..e56ea9eaf 100644 --- a/lib/widgets/welcome_page.dart +++ b/lib/widgets/welcome_page.dart @@ -1,7 +1,7 @@ import 'package:aves/model/settings/settings.dart'; -import 'package:aves/utils/durations.dart'; -import 'package:aves/widgets/common/aves_logo.dart'; -import 'package:aves/widgets/common/labeled_checkbox.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/widgets/common/basic/labeled_checkbox.dart'; +import 'package:aves/widgets/common/identity/aves_logo.dart'; import 'package:aves/widgets/home_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/test/model/filters_test.dart b/test/model/filters_test.dart index 28bf84b9f..35d4f5076 100644 --- a/test/model/filters_test.dart +++ b/test/model/filters_test.dart @@ -5,7 +5,7 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/tag.dart'; -import 'package:aves/model/mime_types.dart'; +import 'package:aves/ref/mime_types.dart'; import 'package:test/test.dart'; void main() { From e218afc6b6ceeb5d16b593453559bd673a1f2dc0 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 26 Nov 2020 14:44:22 +0900 Subject: [PATCH 29/33] filter grid scaling (WIP) --- lib/model/settings/settings.dart | 14 +- .../collection/grid/list_section_layout.dart | 4 +- lib/widgets/collection/grid/list_sliver.dart | 2 +- .../collection/grid/tile_extent_manager.dart | 46 --- .../collection/thumbnail_collection.dart | 29 +- .../common/identity/aves_filter_chip.dart | 19 +- .../{collection/grid => common}/scaling.dart | 75 +++-- lib/widgets/common/tile_extent_manager.dart | 70 ++++ lib/widgets/debug/settings.dart | 9 +- lib/widgets/filter_grids/albums_page.dart | 6 +- .../common/decorated_filter_chip.dart | 52 +-- .../filter_grids/common/filter_grid_page.dart | 302 +++++++----------- .../filter_grids/common/filter_nav_page.dart | 156 +++++++++ lib/widgets/filter_grids/countries_page.dart | 2 +- lib/widgets/filter_grids/tags_page.dart | 6 +- 15 files changed, 469 insertions(+), 323 deletions(-) delete mode 100644 lib/widgets/collection/grid/tile_extent_manager.dart rename lib/widgets/{collection/grid => common}/scaling.dart (83%) create mode 100644 lib/widgets/common/tile_extent_manager.dart create mode 100644 lib/widgets/filter_grids/common/filter_nav_page.dart diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 1e44c52e5..87f6e0380 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -29,11 +29,11 @@ class Settings extends ChangeNotifier { static const keepScreenOnKey = 'keep_screen_on'; static const homePageKey = 'home_page'; static const catalogTimeZoneKey = 'catalog_time_zone'; + static const tileExtentPrefixKey = 'tile_extent_'; // collection static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionSortFactorKey = 'collection_sort_factor'; - static const collectionTileExtentKey = 'collection_tile_extent'; static const showThumbnailLocationKey = 'show_thumbnail_location'; static const showThumbnailRawKey = 'show_thumbnail_raw'; static const showThumbnailVideoDurationKey = 'show_thumbnail_video_duration'; @@ -112,6 +112,12 @@ class Settings extends ChangeNotifier { set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); + double getTileExtent(String routeName) => _prefs.getDouble(tileExtentPrefixKey + routeName) ?? 0; + + // do not notify, as tile extents are only used internally by `TileExtentManager` + // and should not trigger rebuilding by change notification + void setTileExtent(String routeName, double newValue) => setAndNotify(tileExtentPrefixKey + routeName, newValue, notify: false); + // collection EntryGroupFactor get collectionGroupFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values); @@ -122,12 +128,6 @@ class Settings extends ChangeNotifier { set collectionSortFactor(EntrySortFactor newValue) => setAndNotify(collectionSortFactorKey, newValue.toString()); - double get collectionTileExtent => _prefs.getDouble(collectionTileExtentKey) ?? 0; - - // do not notify, as `collectionTileExtent` is only used internally by `TileExtentManager` - // and should not trigger rebuilding by change notification - set collectionTileExtent(double newValue) => setAndNotify(collectionTileExtentKey, newValue, notify: false); - bool get showThumbnailLocation => getBoolOrDefault(showThumbnailLocationKey, true); set showThumbnailLocation(bool newValue) => setAndNotify(showThumbnailLocationKey, newValue); diff --git a/lib/widgets/collection/grid/list_section_layout.dart b/lib/widgets/collection/grid/list_section_layout.dart index fe88bfd51..540beee50 100644 --- a/lib/widgets/collection/grid/list_section_layout.dart +++ b/lib/widgets/collection/grid/list_section_layout.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/collection/grid/header_generic.dart'; -import 'package:aves/widgets/collection/grid/tile_extent_manager.dart'; +import 'package:aves/widgets/collection/thumbnail_collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -22,7 +22,7 @@ class SectionedListLayoutProvider extends StatelessWidget { @required this.thumbnailBuilder, @required this.child, }) : assert(scrollableWidth != 0), - columnCount = max((scrollableWidth / tileExtent).round(), TileExtentManager.columnCountMin); + columnCount = max((scrollableWidth / tileExtent).round(), ThumbnailCollection.columnCountMin); @override Widget build(BuildContext context) { diff --git a/lib/widgets/collection/grid/list_sliver.dart b/lib/widgets/collection/grid/list_sliver.dart index e275fc223..91089845c 100644 --- a/lib/widgets/collection/grid/list_sliver.dart +++ b/lib/widgets/collection/grid/list_sliver.dart @@ -4,7 +4,7 @@ import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/viewer_service.dart'; import 'package:aves/widgets/collection/grid/list_known_extent.dart'; import 'package:aves/widgets/collection/grid/list_section_layout.dart'; -import 'package:aves/widgets/collection/grid/scaling.dart'; +import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; diff --git a/lib/widgets/collection/grid/tile_extent_manager.dart b/lib/widgets/collection/grid/tile_extent_manager.dart deleted file mode 100644 index 1ddf1f922..000000000 --- a/lib/widgets/collection/grid/tile_extent_manager.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'dart:math'; - -import 'package:aves/model/settings/settings.dart'; -import 'package:flutter/widgets.dart'; - -class TileExtentManager { - static const int columnCountMin = 2; - static const int columnCountDefault = 4; - static const double tileExtentMin = 46.0; - static const viewportSizeMin = Size.square(tileExtentMin * columnCountMin); - - static double applyTileExtent( - Size viewportSize, - ValueNotifier extentNotifier, { - double userPreferredExtent = 0, - }) { - // sanitize screen size (useful when reloading while screen is off, reporting a 0,0 size) - viewportSize = Size(max(viewportSize.width, viewportSizeMin.width), max(viewportSize.height, viewportSizeMin.height)); - - final oldUserPreferredExtent = settings.collectionTileExtent; - final currentExtent = extentNotifier.value; - final targetExtent = userPreferredExtent > 0 - ? userPreferredExtent - : oldUserPreferredExtent > 0 - ? oldUserPreferredExtent - : currentExtent; - - int columnCount; - if (targetExtent > 0) { - columnCount = max(columnCountMin, (viewportSize.width / targetExtent.clamp(tileExtentMin, extentMaxForSize(viewportSize))).round()); - } else { - columnCount = columnCountDefault; - } - final newExtent = viewportSize.width / columnCount; - - if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) { - settings.collectionTileExtent = newExtent; - } - if (extentNotifier.value != newExtent) { - extentNotifier.value = newExtent; - } - return newExtent; - } - - static double extentMaxForSize(Size viewportSize) => viewportSize.shortestSide / columnCountMin; -} diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index e7788cf94..e0c230aff 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -3,19 +3,20 @@ import 'dart:async'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/ref/mime_types.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/ref/mime_types.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/app_bar.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/collection/grid/list_section_layout.dart'; import 'package:aves/widgets/collection/grid/list_sliver.dart'; -import 'package:aves/widgets/collection/grid/scaling.dart'; -import 'package:aves/widgets/collection/grid/tile_extent_manager.dart'; +import 'package:aves/widgets/common/scaling.dart'; +import 'package:aves/widgets/common/tile_extent_manager.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; @@ -28,6 +29,10 @@ class ThumbnailCollection extends StatelessWidget { final ValueNotifier _isScrollingNotifier = ValueNotifier(false); final GlobalKey _scrollableKey = GlobalKey(); + static const columnCountMin = 2; + static const columnCountDefault = 4; + static const extentMin = 46.0; + @override Widget build(BuildContext context) { return SafeArea( @@ -37,8 +42,15 @@ class ThumbnailCollection extends StatelessWidget { assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); if (viewportSize.isEmpty) return SizedBox.shrink(); - TileExtentManager.applyTileExtent(viewportSize, _tileExtentNotifier); - final cacheExtent = TileExtentManager.extentMaxForSize(viewportSize) * 2; + final tileExtentManager = TileExtentManager( + routeName: context.currentRouteName, + columnCountMin: columnCountMin, + columnCountDefault: columnCountDefault, + extentMin: extentMin, + extentNotifier: _tileExtentNotifier, + spacing: 0, + )..applyTileExtent(viewportSize: viewportSize); + final cacheExtent = tileExtentManager.getEffectiveExtentMax(viewportSize) * 2; // do not replace by Provider.of // so that view updates on collection filter changes @@ -58,16 +70,17 @@ class ThumbnailCollection extends StatelessWidget { ); final scaler = GridScaleGestureDetector( + tileExtentManager: tileExtentManager, scrollableKey: _scrollableKey, appBarHeightNotifier: _appBarHeightNotifier, - extentNotifier: _tileExtentNotifier, viewportSize: viewportSize, + showScaledGrid: true, scaledBuilder: (entry, extent) => DecoratedThumbnail( entry: entry, extent: extent, showOverlay: false, ), - getScaledItemTileRect: (entry) { + getScaledItemTileRect: (context, entry) { final sectionedListLayout = Provider.of(context, listen: false); return sectionedListLayout.getTileRect(entry) ?? Rect.zero; }, diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 732d27485..bbcfb83c4 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -14,6 +14,7 @@ class AvesFilterChip extends StatefulWidget { final bool showGenericIcon; final Widget background; final Widget details; + final double padding; final HeroType heroType; final FilterCallback onTap; final OffsetFilterCallback onLongPress; @@ -24,7 +25,6 @@ class AvesFilterChip extends StatefulWidget { static const double minChipWidth = 80; static const double maxChipWidth = 160; static const double iconSize = 20; - static const double padding = 6; const AvesFilterChip({ Key key, @@ -33,8 +33,9 @@ class AvesFilterChip extends StatefulWidget { this.showGenericIcon = true, this.background, this.details, + this.padding = 6.0, this.heroType = HeroType.onTap, - @required this.onTap, + this.onTap, this.onLongPress, }) : assert(filter != null), super(key: key); @@ -51,6 +52,8 @@ class _AvesFilterChipState extends State { CollectionFilter get filter => widget.filter; + double get padding => widget.padding; + @override void initState() { super.initState(); @@ -80,9 +83,11 @@ class _AvesFilterChipState extends State { @override Widget build(BuildContext context) { + const iconSize = AvesFilterChip.iconSize; + final hasBackground = widget.background != null; - final leading = filter.iconBuilder(context, AvesFilterChip.iconSize, showGenericIcon: widget.showGenericIcon, embossed: hasBackground); - final trailing = widget.removable ? Icon(AIcons.clear, size: AvesFilterChip.iconSize) : null; + final leading = filter.iconBuilder(context, iconSize, showGenericIcon: widget.showGenericIcon, embossed: hasBackground); + final trailing = widget.removable ? Icon(AIcons.clear, size: iconSize) : null; Widget content = Row( mainAxisSize: hasBackground ? MainAxisSize.max : MainAxisSize.min, @@ -90,7 +95,7 @@ class _AvesFilterChipState extends State { children: [ if (leading != null) ...[ leading, - SizedBox(width: AvesFilterChip.padding), + SizedBox(width: padding), ], Flexible( child: Text( @@ -101,7 +106,7 @@ class _AvesFilterChipState extends State { ), ), if (trailing != null) ...[ - SizedBox(width: AvesFilterChip.padding), + SizedBox(width: padding), trailing, ], ], @@ -118,7 +123,7 @@ class _AvesFilterChipState extends State { } content = Padding( - padding: EdgeInsets.symmetric(horizontal: AvesFilterChip.padding * 2, vertical: 2), + padding: EdgeInsets.symmetric(horizontal: padding * 2, vertical: 2), child: content, ); diff --git a/lib/widgets/collection/grid/scaling.dart b/lib/widgets/common/scaling.dart similarity index 83% rename from lib/widgets/collection/grid/scaling.dart rename to lib/widgets/common/scaling.dart index fab83f78f..c5bb1603c 100644 --- a/lib/widgets/collection/grid/scaling.dart +++ b/lib/widgets/common/scaling.dart @@ -2,9 +2,9 @@ import 'dart:math'; import 'dart:ui' as ui; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/collection/grid/tile_extent_manager.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/tile_extent_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -16,20 +16,22 @@ class ScalerMetadata { } class GridScaleGestureDetector extends StatefulWidget { + final TileExtentManager tileExtentManager; final GlobalKey scrollableKey; final ValueNotifier appBarHeightNotifier; - final ValueNotifier extentNotifier; final Size viewportSize; + final bool showScaledGrid; final Widget Function(T item, double extent) scaledBuilder; - final Rect Function(T item) getScaledItemTileRect; + final Rect Function(BuildContext context, T item) getScaledItemTileRect; final void Function(T item) onScaled; final Widget child; const GridScaleGestureDetector({ - this.scrollableKey, + @required this.tileExtentManager, + @required this.scrollableKey, @required this.appBarHeightNotifier, - @required this.extentNotifier, @required this.viewportSize, + @required this.showScaledGrid, @required this.scaledBuilder, @required this.getScaledItemTileRect, @required this.onScaled, @@ -40,14 +42,16 @@ class GridScaleGestureDetector extends StatefulWidget { _GridScaleGestureDetectorState createState() => _GridScaleGestureDetectorState(); } -class _GridScaleGestureDetectorState extends State { +class _GridScaleGestureDetectorState extends State> { double _startExtent, _extentMin, _extentMax; bool _applyingScale = false; ValueNotifier _scaledExtentNotifier; OverlayEntry _overlayEntry; ScalerMetadata _metadata; - ValueNotifier get tileExtentNotifier => widget.extentNotifier; + TileExtentManager get tileExtentManager => widget.tileExtentManager; + + Size get viewportSize => widget.viewportSize; @override Widget build(BuildContext context) { @@ -72,13 +76,15 @@ class _GridScaleGestureDetectorState extends State // abort if we cannot find an image to show on overlay if (renderMetaData == null) return; _metadata = renderMetaData.metaData; - _startExtent = tileExtentNotifier.value; + _startExtent = renderMetaData.size.width; _scaledExtentNotifier = ValueNotifier(_startExtent); // not the same as `MediaQuery.size.width`, because of screen insets/padding final gridWidth = scrollableBox.size.width; - _extentMin = gridWidth / (gridWidth / TileExtentManager.tileExtentMin).round(); - _extentMax = gridWidth / (gridWidth / TileExtentManager.extentMaxForSize(widget.viewportSize)).round(); + + _extentMin = tileExtentManager.getEffectiveExtentMin(viewportSize); + _extentMax = tileExtentManager.getEffectiveExtentMax(viewportSize); + final halfExtent = _startExtent / 2; final thumbnailCenter = renderMetaData.localToGlobal(Offset(halfExtent, halfExtent)); _overlayEntry = OverlayEntry( @@ -87,6 +93,7 @@ class _GridScaleGestureDetectorState extends State center: thumbnailCenter, gridWidth: gridWidth, scaledExtentNotifier: _scaledExtentNotifier, + showScaledGrid: widget.showScaledGrid, ), ); Overlay.of(scrollableContext).insert(_overlayEntry); @@ -104,11 +111,10 @@ class _GridScaleGestureDetectorState extends State } _applyingScale = true; - final oldExtent = tileExtentNotifier.value; + final oldExtent = tileExtentManager.extentNotifier.value; // sanitize and update grid layout if necessary - final newExtent = TileExtentManager.applyTileExtent( - widget.viewportSize, - tileExtentNotifier, + final newExtent = tileExtentManager.applyTileExtent( + viewportSize: widget.viewportSize, userPreferredExtent: _scaledExtentNotifier.value, ); _scaledExtentNotifier = null; @@ -137,7 +143,7 @@ class _GridScaleGestureDetectorState extends State void _scrollToItem(T item) { final scrollableContext = widget.scrollableKey.currentContext; final scrollableHeight = (scrollableContext.findRenderObject() as RenderBox).size.height; - final tileRect = widget.getScaledItemTileRect(item); + final tileRect = widget.getScaledItemTileRect(context, item); // most of the time the app bar will be scrolled away after scaling, // so we compensate for it to center the focal point thumbnail final appBarHeight = widget.appBarHeightNotifier.value; @@ -152,12 +158,14 @@ class ScaleOverlay extends StatefulWidget { final Offset center; final double gridWidth; final ValueNotifier scaledExtentNotifier; + final bool showScaledGrid; const ScaleOverlay({ @required this.builder, @required this.center, @required this.gridWidth, @required this.scaledExtentNotifier, + @required this.showScaledGrid, }); @override @@ -217,24 +225,29 @@ class _ScaleOverlayState extends State { } final clampedCenter = center.translate(dx, 0); - return CustomPaint( - painter: GridPainter( - center: clampedCenter, - extent: extent, - ), - child: Stack( - children: [ - Positioned( - left: clampedCenter.dx - extent / 2, - top: clampedCenter.dy - extent / 2, - child: DefaultTextStyle( - style: TextStyle(), - child: widget.builder(extent), - ), + var child = widget.builder(extent); + child = Stack( + children: [ + Positioned( + left: clampedCenter.dx - extent / 2, + top: clampedCenter.dy - extent / 2, + child: DefaultTextStyle( + style: TextStyle(), + child: child, ), - ], - ), + ), + ], ); + if (widget.showScaledGrid) { + child = CustomPaint( + painter: GridPainter( + center: clampedCenter, + extent: extent, + ), + child: child, + ); + } + return child; }, ), ), diff --git a/lib/widgets/common/tile_extent_manager.dart b/lib/widgets/common/tile_extent_manager.dart new file mode 100644 index 000000000..7e54ebab3 --- /dev/null +++ b/lib/widgets/common/tile_extent_manager.dart @@ -0,0 +1,70 @@ +import 'dart:math'; + +import 'package:aves/model/settings/settings.dart'; +import 'package:flutter/widgets.dart'; + +class TileExtentManager { + final String routeName; + final int columnCountMin, columnCountDefault; + final double spacing, extentMin; + final ValueNotifier extentNotifier; + + const TileExtentManager({ + @required this.routeName, + @required this.columnCountMin, + @required this.columnCountDefault, + @required this.extentMin, + @required this.extentNotifier, + @required this.spacing, + }); + + double applyTileExtent({ + @required Size viewportSize, + double userPreferredExtent = 0, + }) { + // sanitize screen size (useful when reloading while screen is off, reporting a 0,0 size) + final viewportSizeMin = Size.square(extentMin * columnCountMin); + viewportSize = Size(max(viewportSize.width, viewportSizeMin.width), max(viewportSize.height, viewportSizeMin.height)); + + final oldUserPreferredExtent = settings.getTileExtent(routeName); + final currentExtent = extentNotifier.value; + final targetExtent = userPreferredExtent > 0 + ? userPreferredExtent + : oldUserPreferredExtent > 0 + ? oldUserPreferredExtent + : currentExtent; + + final columnCount = getEffectiveColumnCountForExtent(viewportSize, targetExtent); + final newExtent = _extentForColumnCount(viewportSize, columnCount); + + if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) { + settings.setTileExtent(routeName, newExtent); + } + if (extentNotifier.value != newExtent) { + extentNotifier.value = newExtent; + } + return newExtent; + } + + double _extentMax(Size viewportSize) => (viewportSize.shortestSide - spacing * (columnCountMin - 1)) / columnCountMin; + + double _columnCountForExtent(Size viewportSize, double extent) => (viewportSize.width + spacing) / (extent + spacing); + + double _extentForColumnCount(Size viewportSize, int columnCount) => (viewportSize.width - spacing * (columnCount - 1)) / columnCount; + + int _effectiveColumnCountMin(Size viewportSize) => _columnCountForExtent(viewportSize, _extentMax(viewportSize)).ceil(); + + int _effectiveColumnCountMax(Size viewportSize) => _columnCountForExtent(viewportSize, extentMin).floor(); + + double getEffectiveExtentMin(Size viewportSize) => _extentForColumnCount(viewportSize, _effectiveColumnCountMax(viewportSize)); + + double getEffectiveExtentMax(Size viewportSize) => _extentForColumnCount(viewportSize, _effectiveColumnCountMin(viewportSize)); + + int getEffectiveColumnCountForExtent(Size viewportSize, double extent) { + if (extent > 0) { + final columnCount = _columnCountForExtent(viewportSize, extent); + return columnCount.clamp(_effectiveColumnCountMin(viewportSize), _effectiveColumnCountMax(viewportSize)).round(); + } + return columnCountDefault; + } +} diff --git a/lib/widgets/debug/settings.dart b/lib/widgets/debug/settings.dart index 05e58c137..d3a1a7ce4 100644 --- a/lib/widgets/debug/settings.dart +++ b/lib/widgets/debug/settings.dart @@ -1,5 +1,9 @@ import 'package:aves/model/settings/settings.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/identity/aves_expansion_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/fullscreen/info/common.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -27,7 +31,10 @@ class DebugSettingsSection extends StatelessWidget { Padding( padding: EdgeInsets.only(left: 8, right: 8, bottom: 8), child: InfoRowGroup({ - 'collectionTileExtent': '${settings.collectionTileExtent}', + 'tileExtent - Collection': '${settings.getTileExtent(CollectionPage.routeName)}', + 'tileExtent - Albums': '${settings.getTileExtent(AlbumListPage.routeName)}', + 'tileExtent - Countries': '${settings.getTileExtent(CountryListPage.routeName)}', + 'tileExtent - Tags': '${settings.getTileExtent(TagListPage.routeName)}', 'infoMapZoom': '${settings.infoMapZoom}', 'pinnedFilters': toMultiline(settings.pinnedFilters), 'searchHistory': toMultiline(settings.searchHistory), diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index a7604e3ef..642957d60 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; @@ -5,13 +6,12 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/collection/empty.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; -import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; -import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; +import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index 245b8cd12..f7690b276 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -1,16 +1,17 @@ +import 'dart:math'; import 'dart:ui'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/raster.dart'; import 'package:aves/widgets/collection/thumbnail/vector.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; @@ -19,6 +20,7 @@ class DecoratedFilterChip extends StatelessWidget { final CollectionSource source; final CollectionFilter filter; final ImageEntry entry; + final double extent; final bool pinned; final FilterCallback onTap; final OffsetFilterCallback onLongPress; @@ -28,8 +30,9 @@ class DecoratedFilterChip extends StatelessWidget { @required this.source, @required this.filter, @required this.entry, + @required this.extent, this.pinned = false, - @required this.onTap, + this.onTap, this.onLongPress, }) : super(key: key); @@ -40,57 +43,60 @@ class DecoratedFilterChip extends StatelessWidget { : entry.isSvg ? ThumbnailVectorImage( entry: entry, - extent: FilterGridPage.maxCrossAxisExtent, + extent: extent, ) : ThumbnailRasterImage( entry: entry, - extent: FilterGridPage.maxCrossAxisExtent, + extent: extent, ); + final titlePadding = min(6.0, extent / 16); return AvesFilterChip( filter: filter, showGenericIcon: false, background: backgroundImage, details: _buildDetails(filter), + padding: titlePadding, onTap: onTap, onLongPress: onLongPress, ); } Widget _buildDetails(CollectionFilter filter) { - final count = Text( - '${source.count(filter)}', - style: TextStyle(color: FilterGridPage.detailColor), - ); + final padding = min(8.0, extent / 16); + final iconSize = min(14.0, extent / 8); + final fontSize = min(14.0, (extent / 6).roundToDouble()); return Row( mainAxisSize: MainAxisSize.min, children: [ - AnimatedCrossFade( - firstChild: Padding( - padding: EdgeInsets.only(right: 8), + if (pinned) + AnimatedPadding( + padding: EdgeInsets.only(right: padding), child: DecoratedIcon( AIcons.pin, color: FilterGridPage.detailColor, shadows: [Constants.embossShadow], - size: 16, + size: iconSize, ), + duration: Durations.chipDecorationAnimation, ), - secondChild: SizedBox.shrink(), - sizeCurve: Curves.easeInOutCubic, - alignment: AlignmentDirectional.centerEnd, - crossFadeState: pinned ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: Durations.chipDecorationAnimation, - ), if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)) - Padding( - padding: EdgeInsets.only(right: 8), + AnimatedPadding( + padding: EdgeInsets.only(right: padding), child: DecoratedIcon( AIcons.removableStorage, color: FilterGridPage.detailColor, shadows: [Constants.embossShadow], - size: 16, + size: iconSize, ), + duration: Durations.chipDecorationAnimation, ), - count, + Text( + '${source.count(filter)}', + style: TextStyle( + color: FilterGridPage.detailColor, + fontSize: fontSize, + ), + ), ], ); } diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 1a14338fb..ee75af44a 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -1,193 +1,59 @@ import 'dart:ui'; -import 'package:aves/main.dart'; import 'package:aves/model/filters/filters.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_source.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/collection/collection_page.dart'; -import 'package:aves/widgets/common/app_bar_subtitle.dart'; -import 'package:aves/widgets/common/app_bar_title.dart'; -import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/widgets/common/basic/menu_row.dart'; -import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/behaviour/routes.dart'; +import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; +import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/scaling.dart'; +import 'package:aves/widgets/common/tile_extent_manager.dart'; import 'package:aves/widgets/drawer/app_drawer.dart'; -import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; -import 'package:aves/model/actions/chip_actions.dart'; -import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/decorated_filter_chip.dart'; -import 'package:aves/widgets/search/search_button.dart'; -import 'package:aves/widgets/search/search_delegate.dart'; -import 'package:collection/collection.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; -class FilterNavigationPage extends StatelessWidget { - final CollectionSource source; - final String title; - final ChipSetActionDelegate chipSetActionDelegate; - final ChipActionDelegate chipActionDelegate; - final Map filterEntries; - final CollectionFilter Function(String key) filterBuilder; - final Widget Function() emptyBuilder; - final List Function(CollectionFilter filter) chipActionsBuilder; - - const FilterNavigationPage({ - @required this.source, - @required this.title, - @required this.chipSetActionDelegate, - @required this.chipActionDelegate, - @required this.chipActionsBuilder, - @required this.filterEntries, - @required this.filterBuilder, - @required this.emptyBuilder, - }); - - @override - Widget build(BuildContext context) { - return FilterGridPage( - source: source, - appBar: SliverAppBar( - title: TappableAppBarTitle( - onTap: () => _goToSearch(context), - child: SourceStateAwareAppBarTitle( - title: Text(title), - source: source, - ), - ), - actions: _buildActions(context), - titleSpacing: 0, - floating: true, - ), - filterEntries: filterEntries, - filterBuilder: filterBuilder, - emptyBuilder: () => ValueListenableBuilder( - valueListenable: source.stateNotifier, - builder: (context, sourceState, child) { - return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink(); - }, - ), - onTap: (filter) => Navigator.push( - context, - MaterialPageRoute( - settings: RouteSettings(name: CollectionPage.routeName), - builder: (context) => CollectionPage(CollectionLens( - source: source, - filters: [filter], - groupFactor: settings.collectionGroupFactor, - sortFactor: settings.collectionSortFactor, - )), - ), - ), - onLongPress: AvesApp.mode == AppMode.main ? (filter, tapPosition) => _showMenu(context, filter, tapPosition) : null, - ); - } - - Future _showMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { - final RenderBox overlay = Overlay.of(context).context.findRenderObject(); - final touchArea = Size(40, 40); - final selectedAction = await showMenu( - context: context, - position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), - items: chipActionsBuilder(filter) - .map((action) => PopupMenuItem( - value: action, - child: MenuRow(text: action.getText(), icon: action.getIcon()), - )) - .toList(), - ); - if (selectedAction != null) { - // wait for the popup menu to hide before proceeding with the action - Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipActionDelegate.onActionSelected(context, filter, selectedAction)); - } - } - - List _buildActions(BuildContext context) { - return [ - SearchButton(source), - PopupMenuButton( - key: Key('appbar-menu-button'), - itemBuilder: (context) { - return [ - PopupMenuItem( - key: Key('menu-sort'), - value: ChipSetAction.sort, - child: MenuRow(text: 'Sort…', icon: AIcons.sort), - ), - if (kDebugMode) - PopupMenuItem( - value: ChipSetAction.refresh, - child: MenuRow(text: 'Refresh', icon: AIcons.refresh), - ), - PopupMenuItem( - value: ChipSetAction.stats, - child: MenuRow(text: 'Stats', icon: AIcons.stats), - ), - ]; - }, - onSelected: (action) { - // wait for the popup menu to hide before proceeding with the action - Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipSetActionDelegate.onActionSelected(context, action)); - }, - ), - ]; - } - - void _goToSearch(BuildContext context) { - Navigator.push( - context, - SearchPageRoute( - delegate: ImageSearchDelegate( - source: source, - ), - )); - } - - static int compareChipsByDate(MapEntry a, MapEntry b) { - final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1; - return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); - } - - static int compareChipsByEntryCount(MapEntry a, MapEntry b) { - final c = b.value.compareTo(a.value) ?? -1; - return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); - } -} - class FilterGridPage extends StatelessWidget { final CollectionSource source; final Widget appBar; final Map filterEntries; final CollectionFilter Function(String key) filterBuilder; final Widget Function() emptyBuilder; - final double appBarHeight; final FilterCallback onTap; final OffsetFilterCallback onLongPress; - const FilterGridPage({ + final ValueNotifier _appBarHeightNotifier = ValueNotifier(0); + final ValueNotifier _tileExtentNotifier = ValueNotifier(0); + final GlobalKey _scrollableKey = GlobalKey(); + + static const spacing = 8.0; + + FilterGridPage({ @required this.source, @required this.appBar, @required this.filterEntries, @required this.filterBuilder, @required this.emptyBuilder, - this.appBarHeight = kToolbarHeight, + double appBarHeight = kToolbarHeight, @required this.onTap, this.onLongPress, - }); + }) { + _appBarHeightNotifier.value = appBarHeight; + } List get filterKeys => filterEntries.keys.toList(); static const Color detailColor = Color(0xFFE0E0E0); - static const double maxCrossAxisExtent = 180; + + // TODO TLAD enforce max extent? + // static const double maxCrossAxisExtent = 180; @override Widget build(BuildContext context) { @@ -195,13 +61,59 @@ class FilterGridPage extends StatelessWidget { child: Scaffold( body: DoubleBackPopScope( child: SafeArea( - child: Selector( - selector: (c, mq) => mq.size.width, - builder: (c, mqWidth, child) { - final columnCount = (mqWidth / maxCrossAxisExtent).ceil(); - final scrollView = _buildScrollView(context, columnCount); - return AnimationLimiter( - child: _buildDraggableScrollView(scrollView), + child: LayoutBuilder( + builder: (context, constraints) { + final viewportSize = constraints.biggest; + assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); + if (viewportSize.isEmpty) return SizedBox.shrink(); + + final tileExtentManager = TileExtentManager( + routeName: context.currentRouteName, + columnCountMin: 2, + columnCountDefault: 2, + extentMin: 60, + extentNotifier: _tileExtentNotifier, + spacing: spacing, + )..applyTileExtent(viewportSize: viewportSize); + + return ValueListenableBuilder( + valueListenable: _tileExtentNotifier, + builder: (context, tileExtent, child) { + final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent); + final scrollView = AnimationLimiter( + child: _buildDraggableScrollView(_buildScrollView(context, columnCount)), + ); + + return GridScaleGestureDetector( + tileExtentManager: tileExtentManager, + scrollableKey: _scrollableKey, + appBarHeightNotifier: _appBarHeightNotifier, + viewportSize: viewportSize, + showScaledGrid: false, + scaledBuilder: (item, extent) { + final filter = item.filter; + return SizedBox( + width: extent, + height: extent, + child: DecoratedFilterChip( + source: source, + filter: filter, + entry: item.entry, + extent: extent, + pinned: settings.pinnedFilters.contains(filter), + ), + ); + }, + getScaledItemTileRect: (context, item) { + // TODO TLAD + return Rect.zero; + }, + onScaled: (item) { + // TODO TLAD + }, + child: scrollView, + ); + }, ); }, ), @@ -228,7 +140,7 @@ class FilterGridPage extends StatelessWidget { controller: PrimaryScrollController.of(context), padding: EdgeInsets.only( // padding to keep scroll thumb between app bar above and nav bar below - top: appBarHeight, + top: _appBarHeightNotifier.value, bottom: mqViewInsetsBottom, ), child: scrollView, @@ -239,6 +151,7 @@ class FilterGridPage extends StatelessWidget { ScrollView _buildScrollView(BuildContext context, int columnCount) { final pinnedFilters = settings.pinnedFilters; return CustomScrollView( + key: _scrollableKey, controller: PrimaryScrollController.of(context), slivers: [ appBar, @@ -255,42 +168,44 @@ class FilterGridPage extends StatelessWidget { ), hasScrollBody: false, ) - : SliverPadding( - padding: EdgeInsets.all(AvesFilterChip.outlineWidth), - sliver: SliverGrid( - delegate: SliverChildBuilderDelegate( - (context, i) { - final key = filterKeys[i]; - final filter = filterBuilder(key); - final child = DecoratedFilterChip( + : SliverGrid( + delegate: SliverChildBuilderDelegate( + (context, i) { + final key = filterKeys[i]; + final filter = filterBuilder(key); + final entry = filterEntries[key]; + final child = MetaData( + metaData: ScalerMetadata(FilterGridItem(filter, entry)), + child: DecoratedFilterChip( key: Key(key), source: source, filter: filter, - entry: filterEntries[key], + entry: entry, + extent: _tileExtentNotifier.value, pinned: pinnedFilters.contains(filter), onTap: onTap, onLongPress: onLongPress, - ); - return AnimationConfiguration.staggeredGrid( - position: i, - columnCount: columnCount, - duration: Durations.staggeredAnimation, - delay: Durations.staggeredAnimationDelay, - child: SlideAnimation( - verticalOffset: 50.0, - child: FadeInAnimation( - child: child, - ), + ), + ); + return AnimationConfiguration.staggeredGrid( + position: i, + columnCount: columnCount, + duration: Durations.staggeredAnimation, + delay: Durations.staggeredAnimationDelay, + child: SlideAnimation( + verticalOffset: 50.0, + child: FadeInAnimation( + child: child, ), - ); - }, - childCount: filterKeys.length, - ), - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: maxCrossAxisExtent, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - ), + ), + ); + }, + childCount: filterKeys.length, + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columnCount, + mainAxisSpacing: spacing, + crossAxisSpacing: spacing, ), ), SliverToBoxAdapter( @@ -305,3 +220,10 @@ class FilterGridPage extends StatelessWidget { ); } } + +class FilterGridItem { + final CollectionFilter filter; + final ImageEntry entry; + + const FilterGridItem(this.filter, this.entry); +} diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart new file mode 100644 index 000000000..36d65821b --- /dev/null +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -0,0 +1,156 @@ +import 'dart:ui'; + +import 'package:aves/main.dart'; +import 'package:aves/model/actions/chip_actions.dart'; +import 'package:aves/model/filters/filters.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_source.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/app_bar_subtitle.dart'; +import 'package:aves/widgets/common/app_bar_title.dart'; +import 'package:aves/widgets/common/basic/menu_row.dart'; +import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; +import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; +import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; +import 'package:aves/widgets/search/search_button.dart'; +import 'package:aves/widgets/search/search_delegate.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +class FilterNavigationPage extends StatelessWidget { + final CollectionSource source; + final String title; + final ChipSetActionDelegate chipSetActionDelegate; + final ChipActionDelegate chipActionDelegate; + final Map filterEntries; + final CollectionFilter Function(String key) filterBuilder; + final Widget Function() emptyBuilder; + final List Function(CollectionFilter filter) chipActionsBuilder; + + const FilterNavigationPage({ + @required this.source, + @required this.title, + @required this.chipSetActionDelegate, + @required this.chipActionDelegate, + @required this.chipActionsBuilder, + @required this.filterEntries, + @required this.filterBuilder, + @required this.emptyBuilder, + }); + + @override + Widget build(BuildContext context) { + return FilterGridPage( + source: source, + appBar: SliverAppBar( + title: TappableAppBarTitle( + onTap: () => _goToSearch(context), + child: SourceStateAwareAppBarTitle( + title: Text(title), + source: source, + ), + ), + actions: _buildActions(context), + titleSpacing: 0, + floating: true, + ), + filterEntries: filterEntries, + filterBuilder: filterBuilder, + emptyBuilder: () => ValueListenableBuilder( + valueListenable: source.stateNotifier, + builder: (context, sourceState, child) { + return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink(); + }, + ), + onTap: (filter) => Navigator.push( + context, + MaterialPageRoute( + settings: RouteSettings(name: CollectionPage.routeName), + builder: (context) => CollectionPage(CollectionLens( + source: source, + filters: [filter], + groupFactor: settings.collectionGroupFactor, + sortFactor: settings.collectionSortFactor, + )), + ), + ), + onLongPress: AvesApp.mode == AppMode.main ? (filter, tapPosition) => _showMenu(context, filter, tapPosition) : null, + ); + } + + Future _showMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { + final RenderBox overlay = Overlay.of(context).context.findRenderObject(); + final touchArea = Size(40, 40); + final selectedAction = await showMenu( + context: context, + position: RelativeRect.fromRect(tapPosition & touchArea, Offset.zero & overlay.size), + items: chipActionsBuilder(filter) + .map((action) => PopupMenuItem( + value: action, + child: MenuRow(text: action.getText(), icon: action.getIcon()), + )) + .toList(), + ); + if (selectedAction != null) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipActionDelegate.onActionSelected(context, filter, selectedAction)); + } + } + + List _buildActions(BuildContext context) { + return [ + SearchButton(source), + PopupMenuButton( + key: Key('appbar-menu-button'), + itemBuilder: (context) { + return [ + PopupMenuItem( + key: Key('menu-sort'), + value: ChipSetAction.sort, + child: MenuRow(text: 'Sort…', icon: AIcons.sort), + ), + if (kDebugMode) + PopupMenuItem( + value: ChipSetAction.refresh, + child: MenuRow(text: 'Refresh', icon: AIcons.refresh), + ), + PopupMenuItem( + value: ChipSetAction.stats, + child: MenuRow(text: 'Stats', icon: AIcons.stats), + ), + ]; + }, + onSelected: (action) { + // wait for the popup menu to hide before proceeding with the action + Future.delayed(Durations.popupMenuAnimation * timeDilation, () => chipSetActionDelegate.onActionSelected(context, action)); + }, + ), + ]; + } + + void _goToSearch(BuildContext context) { + Navigator.push( + context, + SearchPageRoute( + delegate: ImageSearchDelegate( + source: source, + ), + )); + } + + static int compareChipsByDate(MapEntry a, MapEntry b) { + final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1; + return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); + } + + static int compareChipsByEntryCount(MapEntry a, MapEntry b) { + final c = b.value.compareTo(a.value) ?? -1; + return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); + } +} diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index 0e5bec313..1c8587477 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -10,7 +10,7 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; -import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; +import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index 312f4d926..1de87a33d 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -1,3 +1,4 @@ +import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.dart'; @@ -5,12 +6,11 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/tag.dart'; -import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; -import 'package:aves/model/actions/chip_actions.dart'; import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; -import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; +import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; From f86eb078a4bfe4510a473a73bffb0e0ff17e6ace Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 27 Nov 2020 10:40:36 +0900 Subject: [PATCH 30/33] filter grid scaling: border radius by extent, shared extent for album list & pick, fixed rebuild on query change, fixed pinned item sort, scroll to scaled item --- .../collection/thumbnail_collection.dart | 2 +- .../common/identity/aves_filter_chip.dart | 8 +- lib/widgets/common/tile_extent_manager.dart | 8 +- lib/widgets/filter_grids/album_pick.dart | 39 ++++--- lib/widgets/filter_grids/albums_page.dart | 50 ++++---- .../common/decorated_filter_chip.dart | 2 + .../filter_grids/common/filter_grid_page.dart | 107 ++++++++++-------- .../filter_grids/common/filter_nav_page.dart | 23 ++-- lib/widgets/filter_grids/countries_page.dart | 21 ++-- lib/widgets/filter_grids/tags_page.dart | 21 ++-- 10 files changed, 143 insertions(+), 138 deletions(-) diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index e0c230aff..be2943041 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -43,7 +43,7 @@ class ThumbnailCollection extends StatelessWidget { if (viewportSize.isEmpty) return SizedBox.shrink(); final tileExtentManager = TileExtentManager( - routeName: context.currentRouteName, + settingsRouteKey: context.currentRouteName, columnCountMin: columnCountMin, columnCountDefault: columnCountDefault, extentMin: extentMin, diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index bbcfb83c4..50fd94e47 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -18,8 +18,9 @@ class AvesFilterChip extends StatefulWidget { final HeroType heroType; final FilterCallback onTap; final OffsetFilterCallback onLongPress; + final BorderRadius borderRadius; - static final BorderRadius borderRadius = BorderRadius.circular(32); + static const double defaultRadius = 32; static const double outlineWidth = 2; static const double minChipHeight = kMinInteractiveDimension; static const double minChipWidth = 80; @@ -33,6 +34,7 @@ class AvesFilterChip extends StatefulWidget { this.showGenericIcon = true, this.background, this.details, + this.borderRadius = const BorderRadius.all(Radius.circular(defaultRadius)), this.padding = 6.0, this.heroType = HeroType.onTap, this.onTap, @@ -52,6 +54,8 @@ class _AvesFilterChipState extends State { CollectionFilter get filter => widget.filter; + BorderRadius get borderRadius => widget.borderRadius; + double get padding => widget.padding; @override @@ -141,8 +145,6 @@ class _AvesFilterChipState extends State { ); } - final borderRadius = AvesFilterChip.borderRadius; - Widget chip = Container( constraints: BoxConstraints( minWidth: AvesFilterChip.minChipWidth, diff --git a/lib/widgets/common/tile_extent_manager.dart b/lib/widgets/common/tile_extent_manager.dart index 7e54ebab3..5001f84a2 100644 --- a/lib/widgets/common/tile_extent_manager.dart +++ b/lib/widgets/common/tile_extent_manager.dart @@ -4,13 +4,13 @@ import 'package:aves/model/settings/settings.dart'; import 'package:flutter/widgets.dart'; class TileExtentManager { - final String routeName; + final String settingsRouteKey; final int columnCountMin, columnCountDefault; final double spacing, extentMin; final ValueNotifier extentNotifier; const TileExtentManager({ - @required this.routeName, + @required this.settingsRouteKey, @required this.columnCountMin, @required this.columnCountDefault, @required this.extentMin, @@ -26,7 +26,7 @@ class TileExtentManager { final viewportSizeMin = Size.square(extentMin * columnCountMin); viewportSize = Size(max(viewportSize.width, viewportSizeMin.width), max(viewportSize.height, viewportSizeMin.height)); - final oldUserPreferredExtent = settings.getTileExtent(routeName); + final oldUserPreferredExtent = settings.getTileExtent(settingsRouteKey); final currentExtent = extentNotifier.value; final targetExtent = userPreferredExtent > 0 ? userPreferredExtent @@ -38,7 +38,7 @@ class TileExtentManager { final newExtent = _extentForColumnCount(viewportSize, columnCount); if (userPreferredExtent > 0 || oldUserPreferredExtent == 0) { - settings.setTileExtent(routeName, newExtent); + settings.setTileExtent(settingsRouteKey, newExtent); } if (extentNotifier.value != newExtent) { extentNotifier.value = newExtent; diff --git a/lib/widgets/filter_grids/album_pick.dart b/lib/widgets/filter_grids/album_pick.dart index 0ac6b4413..2828a8266 100644 --- a/lib/widgets/filter_grids/album_pick.dart +++ b/lib/widgets/filter_grids/album_pick.dart @@ -32,7 +32,7 @@ class AlbumPickPage extends StatefulWidget { } class _AlbumPickPageState extends State { - final _filterNotifier = ValueNotifier(''); + final _queryNotifier = ValueNotifier(''); CollectionSource get source => widget.source; @@ -41,26 +41,29 @@ class _AlbumPickPageState extends State { Widget appBar = AlbumPickAppBar( copy: widget.copy, actionDelegate: AlbumChipSetActionDelegate(source: source), - filterNotifier: _filterNotifier, + queryNotifier: _queryNotifier, ); return Selector( selector: (context, s) => s.albumSortFactor, builder: (context, sortFactor, child) { - return ValueListenableBuilder( - valueListenable: _filterNotifier, - builder: (context, filter, child) => FilterGridPage( - source: source, - appBar: appBar, - filterEntries: AlbumListPage.getAlbumEntries(source, filter: filter), - filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), - emptyBuilder: () => EmptyContent( - icon: AIcons.album, - text: 'No albums', - ), - appBarHeight: AlbumPickAppBar.preferredHeight, - onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), + return FilterGridPage( + source: source, + appBar: appBar, + filterEntries: AlbumListPage.getAlbumEntries(source), + applyQuery: (filters, query) { + if (query == null || query.isEmpty) return filters; + query = query.toUpperCase(); + return filters.where((filter) => filter.uniqueName.toUpperCase().contains(query)).toList(); + }, + queryNotifier: _queryNotifier, + emptyBuilder: () => EmptyContent( + icon: AIcons.album, + text: 'No albums', ), + settingsRouteKey: AlbumListPage.routeName, + appBarHeight: AlbumPickAppBar.preferredHeight, + onTap: (filter) => Navigator.pop(context, (filter as AlbumFilter)?.album), ); }, ); @@ -70,14 +73,14 @@ class _AlbumPickPageState extends State { class AlbumPickAppBar extends StatelessWidget { final bool copy; final AlbumChipSetActionDelegate actionDelegate; - final ValueNotifier filterNotifier; + final ValueNotifier queryNotifier; static const preferredHeight = kToolbarHeight + AlbumFilterBar.preferredHeight; const AlbumPickAppBar({ @required this.copy, @required this.actionDelegate, - @required this.filterNotifier, + @required this.queryNotifier, }); @override @@ -86,7 +89,7 @@ class AlbumPickAppBar extends StatelessWidget { leading: BackButton(), title: Text(copy ? 'Copy to Album' : 'Move to Album'), bottom: AlbumFilterBar( - filterNotifier: filterNotifier, + filterNotifier: queryNotifier, ), actions: [ IconButton( diff --git a/lib/widgets/filter_grids/albums_page.dart b/lib/widgets/filter_grids/albums_page.dart index 642957d60..4c78eac83 100644 --- a/lib/widgets/filter_grids/albums_page.dart +++ b/lib/widgets/filter_grids/albums_page.dart @@ -33,7 +33,7 @@ class AlbumListPage extends StatelessWidget { animation: androidFileUtils.appNameChangeNotifier, builder: (context, child) => StreamBuilder( stream: source.eventBus.on(), - builder: (context, snapshot) => FilterNavigationPage( + builder: (context, snapshot) => FilterNavigationPage( source: source, title: 'Albums', chipSetActionDelegate: AlbumChipSetActionDelegate(source: source), @@ -44,7 +44,6 @@ class AlbumListPage extends StatelessWidget { ChipAction.delete, ], filterEntries: getAlbumEntries(source), - filterBuilder: (album) => AlbumFilter(album, source.getUniqueAlbumName(album)), emptyBuilder: () => EmptyContent( icon: AIcons.album, text: 'No albums', @@ -58,56 +57,53 @@ class AlbumListPage extends StatelessWidget { // common with album selection page to move/copy entries - static Map getAlbumEntries(CollectionSource source, {String filter}) { - final pinned = settings.pinnedFilters.whereType().map((f) => f.album); + static Map getAlbumEntries(CollectionSource source) { + final pinned = settings.pinnedFilters.whereType(); final entriesByDate = source.sortedEntriesForFilterList; + AlbumFilter _buildFilter(String album) => AlbumFilter(album, source.getUniqueAlbumName(album)); + // albums are initially sorted by name at the source level - var sortedAlbums = source.sortedAlbums; - if (filter != null && filter.isNotEmpty) { - filter = filter.toUpperCase(); - sortedAlbums = sortedAlbums.where((album) => source.getUniqueAlbumName(album).toUpperCase().contains(filter)).toList(); - } + var sortedFilters = source.sortedAlbums.map(_buildFilter); if (settings.albumSortFactor == ChipSortFactor.name) { - final pinnedAlbums = [], regularAlbums = [], appAlbums = [], specialAlbums = []; - for (var album in sortedAlbums) { - if (pinned.contains(album)) { - pinnedAlbums.add(album); + final pinnedAlbums = [], regularAlbums = [], appAlbums = [], specialAlbums = []; + for (var filter in sortedFilters) { + if (pinned.contains(filter)) { + pinnedAlbums.add(filter); } else { - switch (androidFileUtils.getAlbumType(album)) { + switch (androidFileUtils.getAlbumType(filter.album)) { case AlbumType.regular: - regularAlbums.add(album); + regularAlbums.add(filter); break; case AlbumType.app: - appAlbums.add(album); + appAlbums.add(filter); break; default: - specialAlbums.add(album); + specialAlbums.add(filter); break; } } } - return Map.fromEntries([...pinnedAlbums, ...specialAlbums, ...appAlbums, ...regularAlbums].map((album) { + return Map.fromEntries([...pinnedAlbums, ...specialAlbums, ...appAlbums, ...regularAlbums].map((filter) { return MapEntry( - album, - entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null), + filter, + entriesByDate.firstWhere((entry) => entry.directory == filter.album, orElse: () => null), ); })); } if (settings.albumSortFactor == ChipSortFactor.count) { - CollectionFilter _buildFilter(String album) => AlbumFilter(album, source.getUniqueAlbumName(album)); - var filtersWithCount = List.of(sortedAlbums.map((s) => MapEntry(s, source.count(_buildFilter(s))))); + final filtersWithCount = List.of(sortedFilters.map((filter) => MapEntry(filter, source.count(filter)))); filtersWithCount.sort(FilterNavigationPage.compareChipsByEntryCount); - sortedAlbums = filtersWithCount.map((kv) => kv.key).toList(); + sortedFilters = filtersWithCount.map((kv) => kv.key).toList(); } - final allMapEntries = sortedAlbums.map((album) => MapEntry( - album, - entriesByDate.firstWhere((entry) => entry.directory == album, orElse: () => null), + final allMapEntries = sortedFilters.map((filter) => MapEntry( + filter, + entriesByDate.firstWhere((entry) => entry.directory == filter.album, orElse: () => null), )); - final byPin = groupBy, bool>(allMapEntries, (e) => pinned.contains(e.key)); + final byPin = groupBy, bool>(allMapEntries, (e) => pinned.contains(e.key)); final pinnedMapEntries = (byPin[true] ?? []); final unpinnedMapEntries = (byPin[false] ?? []); diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index f7690b276..c2aa2d52f 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -49,12 +49,14 @@ class DecoratedFilterChip extends StatelessWidget { entry: entry, extent: extent, ); + final borderRadius = min(AvesFilterChip.defaultRadius, extent / 4); final titlePadding = min(6.0, extent / 16); return AvesFilterChip( filter: filter, showGenericIcon: false, background: backgroundImage, details: _buildDetails(filter), + borderRadius: BorderRadius.all(Radius.circular(borderRadius)), padding: titlePadding, onTap: onTap, onLongPress: onLongPress, diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index ee75af44a..6240c3a39 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -20,12 +20,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; -class FilterGridPage extends StatelessWidget { +class FilterGridPage extends StatelessWidget { final CollectionSource source; final Widget appBar; - final Map filterEntries; - final CollectionFilter Function(String key) filterBuilder; + final Map filterEntries; + final ValueNotifier queryNotifier; final Widget Function() emptyBuilder; + final String settingsRouteKey; + final Iterable Function(Iterable filters, String query) applyQuery; final FilterCallback onTap; final OffsetFilterCallback onLongPress; @@ -39,8 +41,10 @@ class FilterGridPage extends StatelessWidget { @required this.source, @required this.appBar, @required this.filterEntries, - @required this.filterBuilder, + @required this.queryNotifier, + this.applyQuery, @required this.emptyBuilder, + this.settingsRouteKey, double appBarHeight = kToolbarHeight, @required this.onTap, this.onLongPress, @@ -48,13 +52,8 @@ class FilterGridPage extends StatelessWidget { _appBarHeightNotifier.value = appBarHeight; } - List get filterKeys => filterEntries.keys.toList(); - static const Color detailColor = Color(0xFFE0E0E0); - // TODO TLAD enforce max extent? - // static const double maxCrossAxisExtent = 180; - @override Widget build(BuildContext context) { return MediaQueryDataProvider( @@ -68,7 +67,7 @@ class FilterGridPage extends StatelessWidget { if (viewportSize.isEmpty) return SizedBox.shrink(); final tileExtentManager = TileExtentManager( - routeName: context.currentRouteName, + settingsRouteKey: settingsRouteKey ?? context.currentRouteName, columnCountMin: 2, columnCountDefault: 2, extentMin: 60, @@ -80,38 +79,51 @@ class FilterGridPage extends StatelessWidget { valueListenable: _tileExtentNotifier, builder: (context, tileExtent, child) { final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent); - final scrollView = AnimationLimiter( - child: _buildDraggableScrollView(_buildScrollView(context, columnCount)), - ); - return GridScaleGestureDetector( - tileExtentManager: tileExtentManager, - scrollableKey: _scrollableKey, - appBarHeightNotifier: _appBarHeightNotifier, - viewportSize: viewportSize, - showScaledGrid: false, - scaledBuilder: (item, extent) { - final filter = item.filter; - return SizedBox( - width: extent, - height: extent, - child: DecoratedFilterChip( - source: source, - filter: filter, - entry: item.entry, - extent: extent, - pinned: settings.pinnedFilters.contains(filter), - ), + return ValueListenableBuilder( + valueListenable: queryNotifier, + builder: (context, query, child) { + final allFilters = filterEntries.keys; + final visibleFilters = (applyQuery != null ? applyQuery(allFilters, query) : allFilters).toList(); + + final scrollView = AnimationLimiter( + child: _buildDraggableScrollView(_buildScrollView(context, columnCount, visibleFilters)), + ); + + return GridScaleGestureDetector( + tileExtentManager: tileExtentManager, + scrollableKey: _scrollableKey, + appBarHeightNotifier: _appBarHeightNotifier, + viewportSize: viewportSize, + showScaledGrid: false, + scaledBuilder: (item, extent) { + final filter = item.filter; + return SizedBox( + width: extent, + height: extent, + child: DecoratedFilterChip( + source: source, + filter: filter, + entry: item.entry, + extent: extent, + pinned: settings.pinnedFilters.contains(filter), + ), + ); + }, + getScaledItemTileRect: (context, item) { + final index = visibleFilters.indexOf(item.filter); + final column = index % columnCount; + final row = (index / columnCount).floor(); + final left = tileExtent * column + spacing * (column - 1); + final top = tileExtent * row + spacing * (row - 1); + return Rect.fromLTWH(left, top, tileExtent, tileExtent); + }, + onScaled: (item) { + // TODO TLAD highlight scaled item + }, + child: scrollView, ); }, - getScaledItemTileRect: (context, item) { - // TODO TLAD - return Rect.zero; - }, - onScaled: (item) { - // TODO TLAD - }, - child: scrollView, ); }, ); @@ -148,14 +160,14 @@ class FilterGridPage extends StatelessWidget { ); } - ScrollView _buildScrollView(BuildContext context, int columnCount) { + ScrollView _buildScrollView(BuildContext context, int columnCount, List visibleFilters) { final pinnedFilters = settings.pinnedFilters; return CustomScrollView( key: _scrollableKey, controller: PrimaryScrollController.of(context), slivers: [ appBar, - filterKeys.isEmpty + visibleFilters.isEmpty ? SliverFillRemaining( child: Selector( selector: (context, mq) => mq.viewInsets.bottom, @@ -171,13 +183,12 @@ class FilterGridPage extends StatelessWidget { : SliverGrid( delegate: SliverChildBuilderDelegate( (context, i) { - final key = filterKeys[i]; - final filter = filterBuilder(key); - final entry = filterEntries[key]; + final filter = visibleFilters[i]; + final entry = filterEntries[filter]; final child = MetaData( metaData: ScalerMetadata(FilterGridItem(filter, entry)), child: DecoratedFilterChip( - key: Key(key), + key: Key(filter.key), source: source, filter: filter, entry: entry, @@ -200,7 +211,7 @@ class FilterGridPage extends StatelessWidget { ), ); }, - childCount: filterKeys.length, + childCount: visibleFilters.length, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: columnCount, @@ -221,8 +232,8 @@ class FilterGridPage extends StatelessWidget { } } -class FilterGridItem { - final CollectionFilter filter; +class FilterGridItem { + final T filter; final ImageEntry entry; const FilterGridItem(this.filter, this.entry); diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index 36d65821b..d2a66d8bf 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -18,20 +18,18 @@ import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; import 'package:aves/widgets/search/search_button.dart'; import 'package:aves/widgets/search/search_delegate.dart'; -import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -class FilterNavigationPage extends StatelessWidget { +class FilterNavigationPage extends StatelessWidget { final CollectionSource source; final String title; final ChipSetActionDelegate chipSetActionDelegate; final ChipActionDelegate chipActionDelegate; - final Map filterEntries; - final CollectionFilter Function(String key) filterBuilder; + final Map filterEntries; final Widget Function() emptyBuilder; - final List Function(CollectionFilter filter) chipActionsBuilder; + final List Function(T filter) chipActionsBuilder; const FilterNavigationPage({ @required this.source, @@ -40,13 +38,12 @@ class FilterNavigationPage extends StatelessWidget { @required this.chipActionDelegate, @required this.chipActionsBuilder, @required this.filterEntries, - @required this.filterBuilder, @required this.emptyBuilder, }); @override Widget build(BuildContext context) { - return FilterGridPage( + return FilterGridPage( source: source, appBar: SliverAppBar( title: TappableAppBarTitle( @@ -61,7 +58,7 @@ class FilterNavigationPage extends StatelessWidget { floating: true, ), filterEntries: filterEntries, - filterBuilder: filterBuilder, + queryNotifier: ValueNotifier(''), emptyBuilder: () => ValueListenableBuilder( valueListenable: source.stateNotifier, builder: (context, sourceState, child) { @@ -84,7 +81,7 @@ class FilterNavigationPage extends StatelessWidget { ); } - Future _showMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { + Future _showMenu(BuildContext context, T filter, Offset tapPosition) async { final RenderBox overlay = Overlay.of(context).context.findRenderObject(); final touchArea = Size(40, 40); final selectedAction = await showMenu( @@ -144,13 +141,13 @@ class FilterNavigationPage extends StatelessWidget { )); } - static int compareChipsByDate(MapEntry a, MapEntry b) { + static int compareChipsByDate(MapEntry a, MapEntry b) { final c = b.value.bestDate?.compareTo(a.value.bestDate) ?? -1; - return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); + return c != 0 ? c : a.key.compareTo(b.key); } - static int compareChipsByEntryCount(MapEntry a, MapEntry b) { + static int compareChipsByEntryCount(MapEntry a, MapEntry b) { final c = b.value.compareTo(a.value) ?? -1; - return c != 0 ? c : compareAsciiUpperCase(a.key, b.key); + return c != 0 ? c : a.key.compareTo(b.key); } } diff --git a/lib/widgets/filter_grids/countries_page.dart b/lib/widgets/filter_grids/countries_page.dart index 1c8587477..5f3dd6ce4 100644 --- a/lib/widgets/filter_grids/countries_page.dart +++ b/lib/widgets/filter_grids/countries_page.dart @@ -39,7 +39,6 @@ class CountryListPage extends StatelessWidget { settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin, ], filterEntries: _getCountryEntries(), - filterBuilder: _buildFilter, emptyBuilder: () => EmptyContent( icon: AIcons.location, text: 'No countries', @@ -50,31 +49,29 @@ class CountryListPage extends StatelessWidget { ); } - CollectionFilter _buildFilter(String location) => LocationFilter(LocationLevel.country, location); - - Map _getCountryEntries() { - final pinned = settings.pinnedFilters.whereType().map((f) => f.countryNameAndCode); + Map _getCountryEntries() { + final pinned = settings.pinnedFilters.whereType(); final entriesByDate = source.sortedEntriesForFilterList; // countries are initially sorted by name at the source level - var sortedCountries = source.sortedCountries; + var sortedFilters = source.sortedCountries.map((location) => LocationFilter(LocationLevel.country, location)); if (settings.countrySortFactor == ChipSortFactor.count) { - var filtersWithCount = List.of(sortedCountries.map((s) => MapEntry(s, source.count(_buildFilter(s))))); + final filtersWithCount = List.of(sortedFilters.map((filter) => MapEntry(filter, source.count(filter)))); filtersWithCount.sort(FilterNavigationPage.compareChipsByEntryCount); - sortedCountries = filtersWithCount.map((kv) => kv.key).toList(); + sortedFilters = filtersWithCount.map((kv) => kv.key).toList(); } final locatedEntries = entriesByDate.where((entry) => entry.isLocated); - final allMapEntries = sortedCountries.map((countryNameAndCode) { - final split = countryNameAndCode.split(LocationFilter.locationSeparator); + final allMapEntries = sortedFilters.map((filter) { + final split = filter.countryNameAndCode.split(LocationFilter.locationSeparator); ImageEntry entry; if (split.length > 1) { final countryCode = split[1]; entry = locatedEntries.firstWhere((entry) => entry.addressDetails.countryCode == countryCode, orElse: () => null); } - return MapEntry(countryNameAndCode, entry); + return MapEntry(filter, entry); }); - final byPin = groupBy, bool>(allMapEntries, (e) => pinned.contains(e.key)); + final byPin = groupBy, bool>(allMapEntries, (e) => pinned.contains(e.key)); final pinnedMapEntries = (byPin[true] ?? []); final unpinnedMapEntries = (byPin[false] ?? []); diff --git a/lib/widgets/filter_grids/tags_page.dart b/lib/widgets/filter_grids/tags_page.dart index 1de87a33d..7d29f6f7c 100644 --- a/lib/widgets/filter_grids/tags_page.dart +++ b/lib/widgets/filter_grids/tags_page.dart @@ -39,7 +39,6 @@ class TagListPage extends StatelessWidget { settings.pinnedFilters.contains(filter) ? ChipAction.unpin : ChipAction.pin, ], filterEntries: _getTagEntries(), - filterBuilder: _buildFilter, emptyBuilder: () => EmptyContent( icon: AIcons.tag, text: 'No tags', @@ -50,25 +49,23 @@ class TagListPage extends StatelessWidget { ); } - CollectionFilter _buildFilter(String tag) => TagFilter(tag); - - Map _getTagEntries() { - final pinned = settings.pinnedFilters.whereType().map((f) => f.tag); + Map _getTagEntries() { + final pinned = settings.pinnedFilters.whereType(); final entriesByDate = source.sortedEntriesForFilterList; // tags are initially sorted by name at the source level - var sortedTags = source.sortedTags; + var sortedFilters = source.sortedTags.map((tag) => TagFilter(tag)); if (settings.tagSortFactor == ChipSortFactor.count) { - var filtersWithCount = List.of(sortedTags.map((s) => MapEntry(s, source.count(_buildFilter(s))))); + final filtersWithCount = List.of(sortedFilters.map((filter) => MapEntry(filter, source.count(filter)))); filtersWithCount.sort(FilterNavigationPage.compareChipsByEntryCount); - sortedTags = filtersWithCount.map((kv) => kv.key).toList(); + sortedFilters = filtersWithCount.map((kv) => kv.key).toList(); } - final allMapEntries = sortedTags.map((tag) => MapEntry( - tag, - entriesByDate.firstWhere((entry) => entry.xmpSubjects.contains(tag), orElse: () => null), + final allMapEntries = sortedFilters.map((filter) => MapEntry( + filter, + entriesByDate.firstWhere((entry) => entry.xmpSubjects.contains(filter.tag), orElse: () => null), )); - final byPin = groupBy, bool>(allMapEntries, (e) => pinned.contains(e.key)); + final byPin = groupBy, bool>(allMapEntries, (e) => pinned.contains(e.key)); final pinnedMapEntries = (byPin[true] ?? []); final unpinnedMapEntries = (byPin[false] ?? []); From 548d723223e919462e21693d95b51a5415adc06c Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 27 Nov 2020 13:37:38 +0900 Subject: [PATCH 31/33] filter grid scaling: grid with spacing --- lib/model/image_metadata.dart | 2 +- lib/widgets/common/scaling.dart | 23 +++++++++++++++---- .../common/decorated_filter_chip.dart | 4 ++-- .../filter_grids/common/filter_grid_page.dart | 2 +- 4 files changed, 22 insertions(+), 9 deletions(-) diff --git a/lib/model/image_metadata.dart b/lib/model/image_metadata.dart index 57f98a0eb..9471d0ec0 100644 --- a/lib/model/image_metadata.dart +++ b/lib/model/image_metadata.dart @@ -48,7 +48,7 @@ class CatalogMetadata { this.xmpTitleDescription, double latitude, double longitude, - }) + }) // Geocoder throws an IllegalArgumentException when a coordinate has a funky values like 1.7056881853375E7 : latitude = latitude == null || latitude < -90.0 || latitude > 90.0 ? null : latitude, longitude = longitude == null || longitude < -180.0 || longitude > 180.0 ? null : longitude; diff --git a/lib/widgets/common/scaling.dart b/lib/widgets/common/scaling.dart index c5bb1603c..9f1cc5cb1 100644 --- a/lib/widgets/common/scaling.dart +++ b/lib/widgets/common/scaling.dart @@ -92,6 +92,7 @@ class _GridScaleGestureDetectorState extends State widget.scaledBuilder(_metadata.item, extent), center: thumbnailCenter, gridWidth: gridWidth, + spacing: tileExtentManager.spacing, scaledExtentNotifier: _scaledExtentNotifier, showScaledGrid: widget.showScaledGrid, ), @@ -157,6 +158,7 @@ class ScaleOverlay extends StatefulWidget { final Widget Function(double extent) builder; final Offset center; final double gridWidth; + final double spacing; final ValueNotifier scaledExtentNotifier; final bool showScaledGrid; @@ -164,6 +166,7 @@ class ScaleOverlay extends StatefulWidget { @required this.builder, @required this.center, @required this.gridWidth, + @required this.spacing, @required this.scaledExtentNotifier, @required this.showScaledGrid, }); @@ -243,6 +246,7 @@ class _ScaleOverlayState extends State { painter: GridPainter( center: clampedCenter, extent: extent, + spacing: widget.spacing, ), child: child, ); @@ -258,11 +262,12 @@ class _ScaleOverlayState extends State { class GridPainter extends CustomPainter { final Offset center; - final double extent; + final double extent, spacing; const GridPainter({ @required this.center, @required this.extent, + @required this.spacing, }); @override @@ -271,7 +276,7 @@ class GridPainter extends CustomPainter { ..strokeWidth = DecoratedThumbnail.borderWidth ..shader = ui.Gradient.radial( center, - size.width / 2, + size.width * .7, [ DecoratedThumbnail.borderColor, Colors.transparent, @@ -281,10 +286,18 @@ class GridPainter extends CustomPainter { 1, ], ); + void draw(Offset topLeft) { + for (var i = -2; i <= 3; i++) { + final ref = (extent + spacing) * i; + canvas.drawLine(Offset(0, topLeft.dy + ref), Offset(size.width, topLeft.dy + ref), paint); + canvas.drawLine(Offset(topLeft.dx + ref, 0), Offset(topLeft.dx + ref, size.height), paint); + } + } + final topLeft = center.translate(-extent / 2, -extent / 2); - for (var i = -1; i <= 2; i++) { - canvas.drawLine(Offset(0, topLeft.dy + extent * i), Offset(size.width, topLeft.dy + extent * i), paint); - canvas.drawLine(Offset(topLeft.dx + extent * i, 0), Offset(topLeft.dx + extent * i, size.height), paint); + draw(topLeft); + if (spacing > 0) { + draw(topLeft.translate(-spacing, -spacing)); } } diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index c2aa2d52f..f971fb46b 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -66,7 +66,7 @@ class DecoratedFilterChip extends StatelessWidget { Widget _buildDetails(CollectionFilter filter) { final padding = min(8.0, extent / 16); final iconSize = min(14.0, extent / 8); - final fontSize = min(14.0, (extent / 6).roundToDouble()); + final fontSize = min(14.0, extent / 6); return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -84,13 +84,13 @@ class DecoratedFilterChip extends StatelessWidget { if (filter is AlbumFilter && androidFileUtils.isOnRemovableStorage(filter.album)) AnimatedPadding( padding: EdgeInsets.only(right: padding), + duration: Durations.chipDecorationAnimation, child: DecoratedIcon( AIcons.removableStorage, color: FilterGridPage.detailColor, shadows: [Constants.embossShadow], size: iconSize, ), - duration: Durations.chipDecorationAnimation, ), Text( '${source.count(filter)}', diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index 6240c3a39..b9536b4db 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -95,7 +95,7 @@ class FilterGridPage extends StatelessWidget { scrollableKey: _scrollableKey, appBarHeightNotifier: _appBarHeightNotifier, viewportSize: viewportSize, - showScaledGrid: false, + showScaledGrid: true, scaledBuilder: (item, extent) { final filter = item.filter; return SizedBox( From d21cd23ac81af51de731795e936ef2577b26dcb6 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 27 Nov 2020 15:36:15 +0900 Subject: [PATCH 32/33] filter grid scaling: highlight --- lib/model/filters/album.dart | 4 +- lib/model/highlight.dart | 24 +++ lib/model/source/collection_lens.dart | 5 - lib/widgets/collection/app_bar.dart | 12 +- .../collection/entry_set_action_delegate.dart | 4 +- .../collection/grid/header_generic.dart | 4 +- .../collection/thumbnail/decorated.dart | 38 +++-- lib/widgets/collection/thumbnail/overlay.dart | 39 +++-- .../collection/thumbnail_collection.dart | 145 +++++++++--------- .../providers/highlight_info_provider.dart | 17 ++ lib/widgets/dialogs/create_album_dialog.dart | 2 +- .../common/decorated_filter_chip.dart | 26 +++- .../filter_grids/common/filter_grid_page.dart | 131 ++++++++-------- lib/widgets/filter_grids/common/overlay.dart | 50 ++++++ .../fullscreen/fullscreen_debug_page.dart | 4 +- lib/widgets/fullscreen/info/maps/common.dart | 6 +- .../info/metadata/metadata_section.dart | 6 +- lib/widgets/fullscreen/overlay/bottom.dart | 4 +- lib/widgets/stats/stats.dart | 2 +- 19 files changed, 319 insertions(+), 204 deletions(-) create mode 100644 lib/model/highlight.dart create mode 100644 lib/widgets/common/providers/highlight_info_provider.dart create mode 100644 lib/widgets/filter_grids/common/overlay.dart diff --git a/lib/model/filters/album.dart b/lib/model/filters/album.dart index 68e0b8e3d..b5062e80e 100644 --- a/lib/model/filters/album.dart +++ b/lib/model/filters/album.dart @@ -1,9 +1,9 @@ +import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/image_entry.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart'; -import 'package:aves/theme/icons.dart'; -import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:palette_generator/palette_generator.dart'; diff --git a/lib/model/highlight.dart b/lib/model/highlight.dart new file mode 100644 index 000000000..ca620a153 --- /dev/null +++ b/lib/model/highlight.dart @@ -0,0 +1,24 @@ +import 'dart:collection'; + +import 'package:flutter/foundation.dart'; + +class HighlightInfo extends ChangeNotifier { + final Queue _items = Queue(); + + void add(Object item) { + if (_items.contains(item)) return; + + _items.addFirst(item); + while (_items.length > 5) { + _items.removeLast(); + } + notifyListeners(); + } + + void remove(Object item) { + _items.removeWhere((element) => element == item); + notifyListeners(); + } + + bool contains(Object item) => _items.contains(item); +} diff --git a/lib/model/source/collection_lens.dart b/lib/model/source/collection_lens.dart index f70a7dd4b..9b54aa18f 100644 --- a/lib/model/source/collection_lens.dart +++ b/lib/model/source/collection_lens.dart @@ -18,7 +18,6 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel EntryGroupFactor groupFactor; EntrySortFactor sortFactor; final AChangeNotifier filterChangeNotifier = AChangeNotifier(); - final StreamController _highlightController = StreamController.broadcast(); List _filteredEntries; List _subscriptions = []; @@ -70,10 +69,6 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel return _sortedEntries; } - Stream get highlightStream => _highlightController.stream; - - void highlight(ImageEntry entry) => _highlightController.add(entry); - bool get showHeaders { if (sortFactor == EntrySortFactor.size) return false; diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 2aaef5061..ff4601a4c 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -1,22 +1,22 @@ import 'dart:async'; import 'package:aves/main.dart'; +import 'package:aves/model/actions/collection_actions.dart'; +import 'package:aves/model/actions/entry_actions.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/model/source/enums.dart'; import 'package:aves/services/app_shortcut_service.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/model/actions/collection_actions.dart'; -import 'package:aves/widgets/collection/filter_bar.dart'; -import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; +import 'package:aves/widgets/collection/filter_bar.dart'; import 'package:aves/widgets/common/app_bar_subtitle.dart'; import 'package:aves/widgets/common/app_bar_title.dart'; -import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/model/actions/entry_actions.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/basic/menu_row.dart'; +import 'package:aves/widgets/dialogs/add_shortcut_dialog.dart'; +import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/search/search_button.dart'; import 'package:aves/widgets/search/search_delegate.dart'; import 'package:aves/widgets/stats/stats.dart'; diff --git a/lib/widgets/collection/entry_set_action_delegate.dart b/lib/widgets/collection/entry_set_action_delegate.dart index 33fc671a5..9ecf90e61 100644 --- a/lib/widgets/collection/entry_set_action_delegate.dart +++ b/lib/widgets/collection/entry_set_action_delegate.dart @@ -1,16 +1,16 @@ import 'dart:async'; +import 'package:aves/model/actions/collection_actions.dart'; +import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/services/image_file_service.dart'; -import 'package:aves/model/actions/collection_actions.dart'; import 'package:aves/widgets/common/action_mixins/feedback.dart'; import 'package:aves/widgets/common/action_mixins/permission_aware.dart'; import 'package:aves/widgets/common/action_mixins/size_aware.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; -import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/widgets/filter_grids/album_pick.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/collection/grid/header_generic.dart b/lib/widgets/collection/grid/header_generic.dart index 6588e746a..2396cf2c3 100644 --- a/lib/widgets/collection/grid/header_generic.dart +++ b/lib/widgets/collection/grid/header_generic.dart @@ -3,12 +3,12 @@ import 'dart:math'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/enums.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/grid/header_album.dart'; import 'package:aves/widgets/collection/grid/header_date.dart'; -import 'package:aves/theme/icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; diff --git a/lib/widgets/collection/thumbnail/decorated.dart b/lib/widgets/collection/thumbnail/decorated.dart index 2f2ac1676..71dd0377c 100644 --- a/lib/widgets/collection/thumbnail/decorated.dart +++ b/lib/widgets/collection/thumbnail/decorated.dart @@ -10,7 +10,7 @@ class DecoratedThumbnail extends StatelessWidget { final double extent; final CollectionLens collection; final ValueNotifier isScrollingNotifier; - final bool showOverlay; + final bool selectable, highlightable; final Object heroTag; static final Color borderColor = Colors.grey.shade700; @@ -22,7 +22,8 @@ class DecoratedThumbnail extends StatelessWidget { @required this.extent, this.collection, this.isScrollingNotifier, - this.showOverlay = true, + this.selectable = true, + this.highlightable = true, }) : heroTag = collection?.heroTag(entry), super(key: key); @@ -40,29 +41,32 @@ class DecoratedThumbnail extends StatelessWidget { isScrollingNotifier: isScrollingNotifier, heroTag: heroTag, ); - if (showOverlay) { - child = Stack( - children: [ - child, - Positioned( - bottom: 0, - left: 0, - child: ThumbnailEntryOverlay( - entry: entry, - extent: extent, - ), + + child = Stack( + fit: StackFit.passthrough, + children: [ + child, + Positioned( + bottom: 0, + left: 0, + child: ThumbnailEntryOverlay( + entry: entry, + extent: extent, ), + ), + if (selectable) ThumbnailSelectionOverlay( entry: entry, extent: extent, ), + if (highlightable) ThumbnailHighlightOverlay( - highlightedStream: collection.highlightStream.map((highlighted) => highlighted == entry), + entry: entry, extent: extent, ), - ], - ); - } + ], + ); + return Container( foregroundDecoration: BoxDecoration( border: Border.all( diff --git a/lib/widgets/collection/thumbnail/overlay.dart b/lib/widgets/collection/thumbnail/overlay.dart index 8dba26cbf..6e9179bd3 100644 --- a/lib/widgets/collection/thumbnail/overlay.dart +++ b/lib/widgets/collection/thumbnail/overlay.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:aves/model/highlight.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; @@ -116,13 +117,13 @@ class ThumbnailSelectionOverlay extends StatelessWidget { } class ThumbnailHighlightOverlay extends StatefulWidget { + final ImageEntry entry; final double extent; - final Stream highlightedStream; const ThumbnailHighlightOverlay({ Key key, + @required this.entry, @required this.extent, - @required this.highlightedStream, }) : super(key: key); @override @@ -132,27 +133,25 @@ class ThumbnailHighlightOverlay extends StatefulWidget { class _ThumbnailHighlightOverlayState extends State { final ValueNotifier _highlightedNotifier = ValueNotifier(false); + ImageEntry get entry => widget.entry; + @override Widget build(BuildContext context) { - return StreamBuilder( - stream: widget.highlightedStream, - builder: (context, snapshot) { - _highlightedNotifier.value = snapshot.hasData && snapshot.data; - return Sweeper( - builder: (context) => Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).accentColor, - width: widget.extent * .1, - ), - ), + final highlightInfo = context.watch(); + _highlightedNotifier.value = highlightInfo.contains(entry); + return Sweeper( + builder: (context) => Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).accentColor, + width: widget.extent * .1, ), - toggledNotifier: _highlightedNotifier, - startAngle: pi * -3 / 4, - centerSweep: false, - onSweepEnd: () => _highlightedNotifier.value = false, - ); - }, + ), + ), + toggledNotifier: _highlightedNotifier, + startAngle: pi * -3 / 4, + centerSweep: false, + onSweepEnd: () => highlightInfo.remove(entry), ); } } diff --git a/lib/widgets/collection/thumbnail_collection.dart b/lib/widgets/collection/thumbnail_collection.dart index be2943041..d1c1fa12d 100644 --- a/lib/widgets/collection/thumbnail_collection.dart +++ b/lib/widgets/collection/thumbnail_collection.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; +import 'package:aves/model/highlight.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -12,12 +13,13 @@ import 'package:aves/widgets/collection/app_bar.dart'; import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/collection/grid/list_section_layout.dart'; import 'package:aves/widgets/collection/grid/list_sliver.dart'; -import 'package:aves/widgets/common/scaling.dart'; -import 'package:aves/widgets/common/tile_extent_manager.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; +import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; +import 'package:aves/widgets/common/scaling.dart'; +import 'package:aves/widgets/common/tile_extent_manager.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -35,79 +37,82 @@ class ThumbnailCollection extends StatelessWidget { @override Widget build(BuildContext context) { - return SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - final viewportSize = constraints.biggest; - assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); - if (viewportSize.isEmpty) return SizedBox.shrink(); + return HighlightInfoProvider( + child: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final viewportSize = constraints.biggest; + assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); + if (viewportSize.isEmpty) return SizedBox.shrink(); - final tileExtentManager = TileExtentManager( - settingsRouteKey: context.currentRouteName, - columnCountMin: columnCountMin, - columnCountDefault: columnCountDefault, - extentMin: extentMin, - extentNotifier: _tileExtentNotifier, - spacing: 0, - )..applyTileExtent(viewportSize: viewportSize); - final cacheExtent = tileExtentManager.getEffectiveExtentMax(viewportSize) * 2; + final tileExtentManager = TileExtentManager( + settingsRouteKey: context.currentRouteName, + columnCountMin: columnCountMin, + columnCountDefault: columnCountDefault, + extentMin: extentMin, + extentNotifier: _tileExtentNotifier, + spacing: 0, + )..applyTileExtent(viewportSize: viewportSize); + final cacheExtent = tileExtentManager.getEffectiveExtentMax(viewportSize) * 2; - // do not replace by Provider.of - // so that view updates on collection filter changes - return Consumer( - builder: (context, collection, child) { - final scrollView = CollectionScrollView( - scrollableKey: _scrollableKey, - collection: collection, - appBar: CollectionAppBar( - appBarHeightNotifier: _appBarHeightNotifier, + // do not replace by Provider.of + // so that view updates on collection filter changes + return Consumer( + builder: (context, collection, child) { + final scrollView = CollectionScrollView( + scrollableKey: _scrollableKey, collection: collection, - ), - appBarHeightNotifier: _appBarHeightNotifier, - isScrollingNotifier: _isScrollingNotifier, - scrollController: PrimaryScrollController.of(context), - cacheExtent: cacheExtent, - ); - - final scaler = GridScaleGestureDetector( - tileExtentManager: tileExtentManager, - scrollableKey: _scrollableKey, - appBarHeightNotifier: _appBarHeightNotifier, - viewportSize: viewportSize, - showScaledGrid: true, - scaledBuilder: (entry, extent) => DecoratedThumbnail( - entry: entry, - extent: extent, - showOverlay: false, - ), - getScaledItemTileRect: (context, entry) { - final sectionedListLayout = Provider.of(context, listen: false); - return sectionedListLayout.getTileRect(entry) ?? Rect.zero; - }, - onScaled: collection.highlight, - child: scrollView, - ); - - final sectionedListLayoutProvider = ValueListenableBuilder( - valueListenable: _tileExtentNotifier, - builder: (context, tileExtent, child) => SectionedListLayoutProvider( - collection: collection, - scrollableWidth: viewportSize.width, - tileExtent: tileExtent, - thumbnailBuilder: (entry) => GridThumbnail( - key: ValueKey(entry.contentId), + appBar: CollectionAppBar( + appBarHeightNotifier: _appBarHeightNotifier, collection: collection, - entry: entry, - tileExtent: tileExtent, - isScrollingNotifier: _isScrollingNotifier, ), - child: scaler, - ), - ); - return sectionedListLayoutProvider; - }, - ); - }, + appBarHeightNotifier: _appBarHeightNotifier, + isScrollingNotifier: _isScrollingNotifier, + scrollController: PrimaryScrollController.of(context), + cacheExtent: cacheExtent, + ); + + final scaler = GridScaleGestureDetector( + tileExtentManager: tileExtentManager, + scrollableKey: _scrollableKey, + appBarHeightNotifier: _appBarHeightNotifier, + viewportSize: viewportSize, + showScaledGrid: true, + scaledBuilder: (entry, extent) => DecoratedThumbnail( + entry: entry, + extent: extent, + selectable: false, + highlightable: false, + ), + getScaledItemTileRect: (context, entry) { + final sectionedListLayout = Provider.of(context, listen: false); + return sectionedListLayout.getTileRect(entry) ?? Rect.zero; + }, + onScaled: (entry) => Provider.of(context, listen: false).add(entry), + child: scrollView, + ); + + final sectionedListLayoutProvider = ValueListenableBuilder( + valueListenable: _tileExtentNotifier, + builder: (context, tileExtent, child) => SectionedListLayoutProvider( + collection: collection, + scrollableWidth: viewportSize.width, + tileExtent: tileExtent, + thumbnailBuilder: (entry) => GridThumbnail( + key: ValueKey(entry.contentId), + collection: collection, + entry: entry, + tileExtent: tileExtent, + isScrollingNotifier: _isScrollingNotifier, + ), + child: scaler, + ), + ); + return sectionedListLayoutProvider; + }, + ); + }, + ), ), ); } diff --git a/lib/widgets/common/providers/highlight_info_provider.dart b/lib/widgets/common/providers/highlight_info_provider.dart new file mode 100644 index 000000000..8b09c1695 --- /dev/null +++ b/lib/widgets/common/providers/highlight_info_provider.dart @@ -0,0 +1,17 @@ +import 'package:aves/model/highlight.dart'; +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +class HighlightInfoProvider extends StatelessWidget { + final Widget child; + + const HighlightInfoProvider({@required this.child}); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => HighlightInfo(), + child: child, + ); + } +} diff --git a/lib/widgets/dialogs/create_album_dialog.dart b/lib/widgets/dialogs/create_album_dialog.dart index 101d35d4e..0b746a5c3 100644 --- a/lib/widgets/dialogs/create_album_dialog.dart +++ b/lib/widgets/dialogs/create_album_dialog.dart @@ -1,7 +1,7 @@ import 'dart:io'; -import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/utils/android_file_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:path/path.dart'; diff --git a/lib/widgets/filter_grids/common/decorated_filter_chip.dart b/lib/widgets/filter_grids/common/decorated_filter_chip.dart index f971fb46b..385b4a37b 100644 --- a/lib/widgets/filter_grids/common/decorated_filter_chip.dart +++ b/lib/widgets/filter_grids/common/decorated_filter_chip.dart @@ -13,6 +13,7 @@ import 'package:aves/widgets/collection/thumbnail/raster.dart'; import 'package:aves/widgets/collection/thumbnail/vector.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart'; +import 'package:aves/widgets/filter_grids/common/overlay.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; @@ -21,7 +22,7 @@ class DecoratedFilterChip extends StatelessWidget { final CollectionFilter filter; final ImageEntry entry; final double extent; - final bool pinned; + final bool pinned, highlightable; final FilterCallback onTap; final OffsetFilterCallback onLongPress; @@ -32,6 +33,7 @@ class DecoratedFilterChip extends StatelessWidget { @required this.entry, @required this.extent, this.pinned = false, + this.highlightable = true, this.onTap, this.onLongPress, }) : super(key: key); @@ -49,18 +51,34 @@ class DecoratedFilterChip extends StatelessWidget { entry: entry, extent: extent, ); - final borderRadius = min(AvesFilterChip.defaultRadius, extent / 4); + final radius = min(AvesFilterChip.defaultRadius, extent / 4); final titlePadding = min(6.0, extent / 16); - return AvesFilterChip( + final borderRadius = BorderRadius.all(Radius.circular(radius)); + Widget child = AvesFilterChip( filter: filter, showGenericIcon: false, background: backgroundImage, details: _buildDetails(filter), - borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + borderRadius: borderRadius, padding: titlePadding, onTap: onTap, onLongPress: onLongPress, ); + + child = Stack( + fit: StackFit.passthrough, + children: [ + child, + if (highlightable) + ChipHighlightOverlay( + filter: filter, + extent: extent, + borderRadius: borderRadius, + ), + ], + ); + + return child; } Widget _buildDetails(CollectionFilter filter) { diff --git a/lib/widgets/filter_grids/common/filter_grid_page.dart b/lib/widgets/filter_grids/common/filter_grid_page.dart index b9536b4db..c6a3a7862 100644 --- a/lib/widgets/filter_grids/common/filter_grid_page.dart +++ b/lib/widgets/filter_grids/common/filter_grid_page.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/highlight.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_source.dart'; @@ -9,6 +10,7 @@ import 'package:aves/widgets/common/behaviour/double_back_pop.dart'; import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/identity/scroll_thumb.dart'; +import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/common/tile_extent_manager.dart'; @@ -59,75 +61,76 @@ class FilterGridPage extends StatelessWidget { return MediaQueryDataProvider( child: Scaffold( body: DoubleBackPopScope( - child: SafeArea( - child: LayoutBuilder( - builder: (context, constraints) { - final viewportSize = constraints.biggest; - assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); - if (viewportSize.isEmpty) return SizedBox.shrink(); + child: HighlightInfoProvider( + child: SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + final viewportSize = constraints.biggest; + assert(viewportSize.isFinite, 'Cannot layout collection with unbounded constraints.'); + if (viewportSize.isEmpty) return SizedBox.shrink(); - final tileExtentManager = TileExtentManager( - settingsRouteKey: settingsRouteKey ?? context.currentRouteName, - columnCountMin: 2, - columnCountDefault: 2, - extentMin: 60, - extentNotifier: _tileExtentNotifier, - spacing: spacing, - )..applyTileExtent(viewportSize: viewportSize); + final tileExtentManager = TileExtentManager( + settingsRouteKey: settingsRouteKey ?? context.currentRouteName, + columnCountMin: 2, + columnCountDefault: 2, + extentMin: 60, + extentNotifier: _tileExtentNotifier, + spacing: spacing, + )..applyTileExtent(viewportSize: viewportSize); - return ValueListenableBuilder( - valueListenable: _tileExtentNotifier, - builder: (context, tileExtent, child) { - final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent); + return ValueListenableBuilder( + valueListenable: _tileExtentNotifier, + builder: (context, tileExtent, child) { + final columnCount = tileExtentManager.getEffectiveColumnCountForExtent(viewportSize, tileExtent); - return ValueListenableBuilder( - valueListenable: queryNotifier, - builder: (context, query, child) { - final allFilters = filterEntries.keys; - final visibleFilters = (applyQuery != null ? applyQuery(allFilters, query) : allFilters).toList(); + return ValueListenableBuilder( + valueListenable: queryNotifier, + builder: (context, query, child) { + final allFilters = filterEntries.keys; + final visibleFilters = (applyQuery != null ? applyQuery(allFilters, query) : allFilters).toList(); - final scrollView = AnimationLimiter( - child: _buildDraggableScrollView(_buildScrollView(context, columnCount, visibleFilters)), - ); + final scrollView = AnimationLimiter( + child: _buildDraggableScrollView(_buildScrollView(context, columnCount, visibleFilters)), + ); - return GridScaleGestureDetector( - tileExtentManager: tileExtentManager, - scrollableKey: _scrollableKey, - appBarHeightNotifier: _appBarHeightNotifier, - viewportSize: viewportSize, - showScaledGrid: true, - scaledBuilder: (item, extent) { - final filter = item.filter; - return SizedBox( - width: extent, - height: extent, - child: DecoratedFilterChip( - source: source, - filter: filter, - entry: item.entry, - extent: extent, - pinned: settings.pinnedFilters.contains(filter), - ), - ); - }, - getScaledItemTileRect: (context, item) { - final index = visibleFilters.indexOf(item.filter); - final column = index % columnCount; - final row = (index / columnCount).floor(); - final left = tileExtent * column + spacing * (column - 1); - final top = tileExtent * row + spacing * (row - 1); - return Rect.fromLTWH(left, top, tileExtent, tileExtent); - }, - onScaled: (item) { - // TODO TLAD highlight scaled item - }, - child: scrollView, - ); - }, - ); - }, - ); - }, + return GridScaleGestureDetector( + tileExtentManager: tileExtentManager, + scrollableKey: _scrollableKey, + appBarHeightNotifier: _appBarHeightNotifier, + viewportSize: viewportSize, + showScaledGrid: true, + scaledBuilder: (item, extent) { + final filter = item.filter; + return SizedBox( + width: extent, + height: extent, + child: DecoratedFilterChip( + source: source, + filter: filter, + entry: item.entry, + extent: extent, + pinned: settings.pinnedFilters.contains(filter), + highlightable: false, + ), + ); + }, + getScaledItemTileRect: (context, item) { + final index = visibleFilters.indexOf(item.filter); + final column = index % columnCount; + final row = (index / columnCount).floor(); + final left = tileExtent * column + spacing * (column - 1); + final top = tileExtent * row + spacing * (row - 1); + return Rect.fromLTWH(left, top, tileExtent, tileExtent); + }, + onScaled: (item) => Provider.of(context, listen: false).add(item.filter), + child: scrollView, + ); + }, + ); + }, + ); + }, + ), ), ), ), diff --git a/lib/widgets/filter_grids/common/overlay.dart b/lib/widgets/filter_grids/common/overlay.dart new file mode 100644 index 000000000..18148fac1 --- /dev/null +++ b/lib/widgets/filter_grids/common/overlay.dart @@ -0,0 +1,50 @@ +import 'dart:math'; + +import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/highlight.dart'; +import 'package:aves/widgets/common/fx/sweeper.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ChipHighlightOverlay extends StatefulWidget { + final CollectionFilter filter; + final double extent; + final BorderRadius borderRadius; + + const ChipHighlightOverlay({ + Key key, + @required this.filter, + @required this.extent, + @required this.borderRadius, + }) : super(key: key); + + @override + _ChipHighlightOverlayState createState() => _ChipHighlightOverlayState(); +} + +class _ChipHighlightOverlayState extends State { + final ValueNotifier _highlightedNotifier = ValueNotifier(false); + + CollectionFilter get filter => widget.filter; + + @override + Widget build(BuildContext context) { + final highlightInfo = context.watch(); + _highlightedNotifier.value = highlightInfo.contains(filter); + return Sweeper( + builder: (context) => Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).accentColor, + width: widget.extent * .1, + ), + borderRadius: widget.borderRadius, + ), + ), + toggledNotifier: _highlightedNotifier, + startAngle: pi * -3 / 4, + centerSweep: false, + onSweepEnd: () => highlightInfo.remove(filter), + ); + } +} diff --git a/lib/widgets/fullscreen/fullscreen_debug_page.dart b/lib/widgets/fullscreen/fullscreen_debug_page.dart index 50e55e24e..3c796a253 100644 --- a/lib/widgets/fullscreen/fullscreen_debug_page.dart +++ b/lib/widgets/fullscreen/fullscreen_debug_page.dart @@ -1,8 +1,8 @@ +import 'package:aves/image_providers/thumbnail_provider.dart'; +import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/main.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/theme/icons.dart'; -import 'package:aves/image_providers/thumbnail_provider.dart'; -import 'package:aves/image_providers/uri_picture_provider.dart'; import 'package:aves/widgets/fullscreen/debug/db.dart'; import 'package:aves/widgets/fullscreen/debug/metadata.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; diff --git a/lib/widgets/fullscreen/info/maps/common.dart b/lib/widgets/fullscreen/info/maps/common.dart index 97e057672..452d6ef00 100644 --- a/lib/widgets/fullscreen/info/maps/common.dart +++ b/lib/widgets/fullscreen/info/maps/common.dart @@ -1,11 +1,11 @@ import 'package:aves/model/settings/settings.dart'; import 'package:aves/services/android_app_service.dart'; import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; -import 'package:aves/widgets/common/fx/borders.dart'; -import 'package:aves/widgets/common/fx/blurred.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/fullscreen/info/metadata/metadata_section.dart b/lib/widgets/fullscreen/info/metadata/metadata_section.dart index 5084c5088..438fb3807 100644 --- a/lib/widgets/fullscreen/info/metadata/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata/metadata_section.dart @@ -1,13 +1,13 @@ import 'dart:collection'; import 'package:aves/model/image_entry.dart'; -import 'package:aves/services/metadata_service.dart'; import 'package:aves/ref/brand_colors.dart'; +import 'package:aves/services/metadata_service.dart'; +import 'package:aves/theme/durations.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; -import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/common/identity/aves_expansion_tile.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/fullscreen/info/common.dart'; import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart'; import 'package:aves/widgets/fullscreen/info/metadata/xmp_tile.dart'; diff --git a/lib/widgets/fullscreen/overlay/bottom.dart b/lib/widgets/fullscreen/overlay/bottom.dart index 1144ff0b7..b2d08980f 100644 --- a/lib/widgets/fullscreen/overlay/bottom.dart +++ b/lib/widgets/fullscreen/overlay/bottom.dart @@ -5,10 +5,10 @@ import 'package:aves/model/image_metadata.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/utils/constants.dart'; import 'package:aves/theme/durations.dart'; -import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/utils/constants.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/stats/stats.dart b/lib/widgets/stats/stats.dart index 4b343fbca..96abd7807 100644 --- a/lib/widgets/stats/stats.dart +++ b/lib/widgets/stats/stats.dart @@ -8,12 +8,12 @@ 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_source.dart'; +import 'package:aves/theme/icons.dart'; import 'package:aves/utils/color_utils.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/utils/mime_utils.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/empty.dart'; -import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/stats/filter_table.dart'; import 'package:charts_flutter/flutter.dart' as charts; From 154ceecae01aed5ca71f815b17e2c8019145becd Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 27 Nov 2020 17:24:13 +0900 Subject: [PATCH 33/33] version bump --- CHANGELOG.md | 13 +++++++++++++ pubspec.yaml | 2 +- whatsnew/whatsnew-en-US | 8 ++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a805011e6..01178df31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,19 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +## [v1.2.8] - 2020-11-27 +### Added +- Albums / Countries / Tags: pinch to change tile size +- Album picker: added a field to filter by name +- check free space before moving entries +- SVG source viewer + +### Changed +- Navigation: changed page history handling +- Info: improved layout, especially for XMP +- About: improved layout +- faster locating of new entries + ## [v1.2.7] - 2020-11-15 ### Added - Support for TIFF images (single page) diff --git a/pubspec.yaml b/pubspec.yaml index 696fa5589..4dffbafbc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.2.7+33 +version: 1.2.8+34 # brendan-duncan/image (as of v2.1.19): # - does not support TIFF with JPEG compression (issue #184) diff --git a/whatsnew/whatsnew-en-US b/whatsnew/whatsnew-en-US index a4f88aadb..d4ed49813 100644 --- a/whatsnew/whatsnew-en-US +++ b/whatsnew/whatsnew-en-US @@ -1,6 +1,6 @@ Thanks for using Aves! -v1.2.7: -- subsampling and tiling of large images -- support for TIFF images (single page only) -- optional minimap in viewer overlay +v1.2.8: +- pinch to scale albums, countries & tags +- SVG source viewer +- improved detailed metadata layout Full changelog available on Github \ No newline at end of file