From cf8d182cfe38dd5a14e4d0dac676e2b02a35c50d Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Tue, 16 Mar 2021 10:18:53 +0900 Subject: [PATCH] thumbnail theme provider, app mode provider, thumbnail overlay review --- l10n.yaml | 2 + lib/main.dart | 72 ++++++----- lib/widgets/collection/app_bar.dart | 11 +- lib/widgets/collection/collection_grid.dart | 46 ++++--- lib/widgets/collection/grid/thumbnail.dart | 6 +- .../collection/thumbnail/decorated.dart | 12 +- lib/widgets/collection/thumbnail/overlay.dart | 68 +++++----- lib/widgets/collection/thumbnail/theme.dart | 46 +++++++ .../common/identity/aves_filter_chip.dart | 3 +- lib/widgets/common/identity/aves_icons.dart | 57 ++++----- .../filter_grids/common/filter_nav_page.dart | 4 +- lib/widgets/home_page.dart | 19 +-- lib/widgets/viewer/debug_page.dart | 3 +- lib/widgets/viewer/overlay/multipage.dart | 120 +++++++++--------- pubspec.lock | 8 +- pubspec.yaml | 7 +- 16 files changed, 268 insertions(+), 216 deletions(-) create mode 100644 lib/widgets/collection/thumbnail/theme.dart diff --git a/l10n.yaml b/l10n.yaml index d8250ba3e..3619cab0e 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -6,3 +6,5 @@ preferred-supported-locales: - en + +# untranslated-messages-file: untranslated.json diff --git a/lib/main.dart b/lib/main.dart index 239986259..82f7f7d8b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -43,13 +43,12 @@ void main() { enum AppMode { main, pick, view } class AvesApp extends StatefulWidget { - static AppMode mode; - @override _AvesAppState createState() => _AvesAppState(); } class _AvesAppState extends State { + final ValueNotifier appModeNotifier = ValueNotifier(AppMode.main); Future _appSetup; final _mediaStoreSource = MediaStoreSource(); final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay); @@ -123,38 +122,41 @@ class _AvesAppState extends State { // so it can be used during navigation transitions return ChangeNotifierProvider.value( value: settings, - child: Provider.value( - value: _mediaStoreSource, - child: HighlightInfoProvider( - child: OverlaySupport( - child: FutureBuilder( - future: _appSetup, - builder: (context, snapshot) { - final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done; - final home = initialized - ? getFirstPage() - : Scaffold( - body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox(), - ); - return Selector( - selector: (context, s) => s.locale, - builder: (context, settingsLocale, child) { - return MaterialApp( - navigatorKey: _navigatorKey, - home: home, - navigatorObservers: _navigatorObservers, - onGenerateTitle: (context) => context.l10n.appName, - darkTheme: darkTheme, - themeMode: ThemeMode.dark, - locale: settingsLocale, - localizationsDelegates: [ - ...AppLocalizations.localizationsDelegates, - LocaleNamesLocalizationsDelegate(), - ], - supportedLocales: AppLocalizations.supportedLocales, - ); - }); - }, + child: ListenableProvider>.value( + value: appModeNotifier, + child: Provider.value( + value: _mediaStoreSource, + child: HighlightInfoProvider( + child: OverlaySupport( + child: FutureBuilder( + future: _appSetup, + builder: (context, snapshot) { + final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done; + final home = initialized + ? getFirstPage() + : Scaffold( + body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox(), + ); + return Selector( + selector: (context, s) => s.locale, + builder: (context, settingsLocale, child) { + return MaterialApp( + navigatorKey: _navigatorKey, + home: home, + navigatorObservers: _navigatorObservers, + onGenerateTitle: (context) => context.l10n.appName, + darkTheme: darkTheme, + themeMode: ThemeMode.dark, + locale: settingsLocale, + localizationsDelegates: [ + ...AppLocalizations.localizationsDelegates, + LocaleNamesLocalizationsDelegate(), + ], + supportedLocales: AppLocalizations.supportedLocales, + ); + }); + }, + ), ), ), ), @@ -204,7 +206,7 @@ class _AvesAppState extends State { debugPrint('$runtimeType onNewIntent with intentData=$intentData'); // do not reset when relaunching the app - if (AvesApp.mode == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; + if (appModeNotifier.value == AppMode.main && (intentData == null || intentData.isEmpty == true)) return; FirebaseCrashlytics.instance.log('New intent'); _navigatorKey.currentState.pushReplacement(DirectMaterialPageRoute( diff --git a/lib/widgets/collection/app_bar.dart b/lib/widgets/collection/app_bar.dart index 4d34b4323..932a3a7cd 100644 --- a/lib/widgets/collection/app_bar.dart +++ b/lib/widgets/collection/app_bar.dart @@ -26,6 +26,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:pedantic/pedantic.dart'; +import 'package:provider/provider.dart'; class CollectionAppBar extends StatefulWidget { final ValueNotifier appBarHeightNotifier; @@ -141,8 +142,9 @@ class _CollectionAppBarState extends State with SingleTickerPr Widget _buildAppBarTitle() { if (collection.isBrowsing) { - Widget title = Text(AvesApp.mode == AppMode.pick ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle); - if (AvesApp.mode == AppMode.main) { + final appMode = context.watch>().value; + Widget title = Text(appMode == AppMode.pick ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle); + if (appMode == AppMode.main) { title = SourceStateAwareAppBarTitle( title: title, source: source, @@ -191,6 +193,7 @@ class _CollectionAppBarState extends State with SingleTickerPr itemBuilder: (context) { final isNotEmpty = !collection.isEmpty; final hasSelection = collection.selection.isNotEmpty; + final isMainMode = context.read>().value == AppMode.main; return [ PopupMenuItem( key: Key('menu-sort'), @@ -204,7 +207,7 @@ class _CollectionAppBarState extends State with SingleTickerPr child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group), ), if (collection.isBrowsing) ...[ - if (AvesApp.mode == AppMode.main) + if (isMainMode) PopupMenuItem( value: CollectionAction.select, enabled: isNotEmpty, @@ -215,7 +218,7 @@ class _CollectionAppBarState extends State with SingleTickerPr enabled: isNotEmpty, child: MenuRow(text: context.l10n.menuActionStats, icon: AIcons.stats), ), - if (AvesApp.mode == AppMode.main && canAddShortcuts) + if (isMainMode && canAddShortcuts) PopupMenuItem( value: CollectionAction.addShortcut, child: MenuRow(text: context.l10n.collectionActionAddShortcut, icon: AIcons.addShortcut), diff --git a/lib/widgets/collection/collection_grid.dart b/lib/widgets/collection/collection_grid.dart index cc7771cb1..9002ad256 100644 --- a/lib/widgets/collection/collection_grid.dart +++ b/lib/widgets/collection/collection_grid.dart @@ -15,6 +15,7 @@ import 'package:aves/widgets/collection/grid/section_layout.dart'; import 'package:aves/widgets/collection/grid/selector.dart'; import 'package:aves/widgets/collection/grid/thumbnail.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; +import 'package:aves/widgets/collection/thumbnail/theme.dart'; import 'package:aves/widgets/common/basic/draggable_scrollbar.dart'; import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; @@ -65,22 +66,25 @@ class _CollectionGridContent extends StatelessWidget { final sectionedListLayoutProvider = ValueListenableBuilder( valueListenable: context.select>((controller) => controller.extentNotifier), builder: (context, tileExtent, child) { - return SectionedEntryListLayoutProvider( - collection: collection, - scrollableWidth: context.select((controller) => controller.viewportSize.width), - tileExtent: tileExtent, - columnCount: context.select((controller) => controller.getEffectiveColumnCountForExtent(tileExtent)), - tileBuilder: (entry) => InteractiveThumbnail( - key: ValueKey(entry.contentId), + return ThumbnailTheme( + extent: tileExtent, + child: SectionedEntryListLayoutProvider( collection: collection, - entry: entry, + scrollableWidth: context.select((controller) => controller.viewportSize.width), tileExtent: tileExtent, - isScrollingNotifier: _isScrollingNotifier, - ), - child: _CollectionSectionedContent( - collection: collection, - isScrollingNotifier: _isScrollingNotifier, - scrollController: PrimaryScrollController.of(context), + columnCount: context.select((controller) => controller.getEffectiveColumnCountForExtent(tileExtent)), + tileBuilder: (entry) => InteractiveThumbnail( + key: ValueKey(entry.contentId), + collection: collection, + entry: entry, + tileExtent: tileExtent, + isScrollingNotifier: _isScrollingNotifier, + ), + child: _CollectionSectionedContent( + collection: collection, + isScrollingNotifier: _isScrollingNotifier, + scrollController: PrimaryScrollController.of(context), + ), ), ); }, @@ -136,8 +140,9 @@ class _CollectionSectionedContentState extends State<_CollectionSectionedContent child: scrollView, ); + final isMainMode = context.select, bool>((vn) => vn.value == AppMode.main); final selector = GridSelectionGestureDetector( - selectable: AvesApp.mode == AppMode.main, + selectable: isMainMode, collection: collection, scrollController: scrollController, appBarHeightNotifier: _appBarHeightNotifier, @@ -177,11 +182,14 @@ class _CollectionScaler extends StatelessWidget { ), child: child, ), - scaledBuilder: (entry, extent) => DecoratedThumbnail( - entry: entry, + scaledBuilder: (entry, extent) => ThumbnailTheme( extent: extent, - selectable: false, - highlightable: false, + child: DecoratedThumbnail( + entry: entry, + extent: extent, + selectable: false, + highlightable: false, + ), ), getScaledItemTileRect: (context, entry) { final sectionedListLayout = context.read>(); diff --git a/lib/widgets/collection/grid/thumbnail.dart b/lib/widgets/collection/grid/thumbnail.dart index 09dcd14ca..6b9c5aefe 100644 --- a/lib/widgets/collection/grid/thumbnail.dart +++ b/lib/widgets/collection/grid/thumbnail.dart @@ -7,6 +7,7 @@ import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class InteractiveThumbnail extends StatelessWidget { final CollectionLens collection; @@ -27,13 +28,14 @@ class InteractiveThumbnail extends StatelessWidget { return GestureDetector( key: ValueKey(entry.uri), onTap: () { - if (AvesApp.mode == AppMode.main) { + final appMode = context.read>().value; + if (appMode == AppMode.main) { if (collection.isBrowsing) { _goToViewer(context); } else if (collection.isSelecting) { collection.toggleSelection(entry); } - } else if (AvesApp.mode == AppMode.pick) { + } else if (appMode == AppMode.pick) { ViewerService.pick(entry.uri); } }, diff --git a/lib/widgets/collection/thumbnail/decorated.dart b/lib/widgets/collection/thumbnail/decorated.dart index 2a4fa81b9..5ecc75d95 100644 --- a/lib/widgets/collection/thumbnail/decorated.dart +++ b/lib/widgets/collection/thumbnail/decorated.dart @@ -31,7 +31,8 @@ class DecoratedThumbnail extends StatelessWidget { // between different views of the entry in the same collection (e.g. thumbnails <-> viewer) // but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer) final heroTag = hashValues(collection?.id, entry); - var child = entry.isSvg + final isSvg = entry.isSvg; + var child = isSvg ? VectorImageThumbnail( entry: entry, extent: extent, @@ -45,17 +46,14 @@ class DecoratedThumbnail extends StatelessWidget { ); child = Stack( - alignment: Alignment.center, + alignment: isSvg ? Alignment.center : AlignmentDirectional.bottomStart, children: [ child, - Positioned( - bottom: 0, - left: 0, - child: ThumbnailEntryOverlay( + if (!isSvg) + ThumbnailEntryOverlay( entry: entry, extent: extent, ), - ), if (selectable) ThumbnailSelectionOverlay( entry: entry, diff --git a/lib/widgets/collection/thumbnail/overlay.dart b/lib/widgets/collection/thumbnail/overlay.dart index 5a539790c..26ee3c736 100644 --- a/lib/widgets/collection/thumbnail/overlay.dart +++ b/lib/widgets/collection/thumbnail/overlay.dart @@ -2,16 +2,15 @@ import 'dart:math'; import 'package:aves/model/entry.dart'; import 'package:aves/model/highlight.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/theme/durations.dart'; import 'package:aves/theme/icons.dart'; +import 'package:aves/widgets/collection/thumbnail/theme.dart'; import 'package:aves/widgets/common/fx/sweeper.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'; class ThumbnailEntryOverlay extends StatelessWidget { final AvesEntry entry; @@ -25,38 +24,28 @@ class ThumbnailEntryOverlay extends StatelessWidget { @override Widget build(BuildContext context) { - final fontSize = min(14.0, (extent / 8)).roundToDouble(); - final iconSize = fontSize * 2; - return Selector>( - selector: (context, s) => Tuple3(s.showThumbnailLocation, s.showThumbnailRaw, s.showThumbnailVideoDuration), - builder: (context, s, child) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (entry.hasGps && settings.showThumbnailLocation) GpsIcon(iconSize: iconSize), - if (entry.isRaw && settings.showThumbnailRaw) RawIcon(iconSize: iconSize), - if (entry.isMultipage) MultipageIcon(iconSize: iconSize), - if (entry.isGeotiff) GeotiffIcon(iconSize: iconSize), - if (entry.isAnimated) - AnimatedImageIcon(iconSize: iconSize) - else if (entry.isVideo) - DefaultTextStyle( - style: TextStyle( - color: Colors.grey[200], - fontSize: fontSize, - ), - child: VideoIcon( - entry: entry, - iconSize: iconSize, - showDuration: settings.showThumbnailVideoDuration, - ), - ) - else if (entry.is360) - SphericalImageIcon(iconSize: iconSize), - ], - ); - }); + final children = [ + if (entry.hasGps && context.select((t) => t.showLocation)) GpsIcon(), + if (entry.isVideo) + VideoIcon( + entry: entry, + ) + else if (entry.isAnimated) + AnimatedImageIcon() + else ...[ + if (entry.isRaw && context.select((t) => t.showRaw)) RawIcon(), + if (entry.isMultipage) MultipageIcon(), + if (entry.isGeotiff) GeotiffIcon(), + if (entry.is360) SphericalImageIcon(), + ] + ]; + if (children.isEmpty) return SizedBox.shrink(); + if (children.length == 1) return children.first; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ); } } @@ -64,6 +53,8 @@ class ThumbnailSelectionOverlay extends StatelessWidget { final AvesEntry entry; final double extent; + static const duration = Durations.thumbnailOverlayAnimation; + const ThumbnailSelectionOverlay({ Key key, @required this.entry, @@ -72,9 +63,6 @@ class ThumbnailSelectionOverlay extends StatelessWidget { @override Widget build(BuildContext context) { - const duration = Durations.thumbnailOverlayAnimation; - final fontSize = min(14.0, (extent / 8)).roundToDouble(); - final iconSize = fontSize * 2; final collection = context.watch(); return ValueListenableBuilder( valueListenable: collection.activityNotifier, @@ -88,7 +76,7 @@ class ThumbnailSelectionOverlay extends StatelessWidget { ? OverlayIcon( key: ValueKey(selected), icon: selected ? AIcons.selected : AIcons.unselected, - size: iconSize, + size: context.select((t) => t.iconSize), ) : SizedBox.shrink(); child = AnimatedSwitcher( @@ -139,6 +127,8 @@ class _ThumbnailHighlightOverlayState extends State { AvesEntry get entry => widget.entry; + static const startAngle = pi * -3 / 4; + @override Widget build(BuildContext context) { final highlightInfo = context.watch(); @@ -153,7 +143,7 @@ class _ThumbnailHighlightOverlayState extends State { ), ), toggledNotifier: _highlightedNotifier, - startAngle: pi * -3 / 4, + startAngle: startAngle, centerSweep: false, onSweepEnd: highlightInfo.clear, ); diff --git a/lib/widgets/collection/thumbnail/theme.dart b/lib/widgets/collection/thumbnail/theme.dart new file mode 100644 index 000000000..83bd64c28 --- /dev/null +++ b/lib/widgets/collection/thumbnail/theme.dart @@ -0,0 +1,46 @@ +import 'dart:math'; + +import 'package:aves/model/settings/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class ThumbnailTheme extends StatelessWidget { + final double extent; + final Widget child; + + const ThumbnailTheme({ + @required this.extent, + @required this.child, + }); + + @override + Widget build(BuildContext context) { + return ProxyProvider( + update: (_, settings, __) { + final iconSize = min(28.0, (extent / 4)).roundToDouble(); + final fontSize = (iconSize / 2).floorToDouble(); + return ThumbnailThemeData( + iconSize: iconSize, + fontSize: fontSize, + showLocation: settings.showThumbnailLocation, + showRaw: settings.showThumbnailRaw, + showVideoDuration: settings.showThumbnailVideoDuration, + ); + }, + child: child, + ); + } +} + +class ThumbnailThemeData { + final double iconSize, fontSize; + final bool showLocation, showRaw, showVideoDuration; + + const ThumbnailThemeData({ + @required this.iconSize, + @required this.fontSize, + @required this.showLocation, + @required this.showRaw, + @required this.showVideoDuration, + }); +} diff --git a/lib/widgets/common/identity/aves_filter_chip.dart b/lib/widgets/common/identity/aves_filter_chip.dart index 090b4ca22..c8e72f37e 100644 --- a/lib/widgets/common/identity/aves_filter_chip.dart +++ b/lib/widgets/common/identity/aves_filter_chip.dart @@ -11,6 +11,7 @@ import 'package:aves/widgets/common/basic/menu_row.dart'; import 'package:aves/widgets/filter_grids/common/chip_action_delegate.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:provider/provider.dart'; typedef FilterCallback = void Function(CollectionFilter filter); typedef OffsetFilterCallback = void Function(BuildContext context, CollectionFilter filter, Offset tapPosition); @@ -52,7 +53,7 @@ class AvesFilterChip extends StatefulWidget { super(key: key); static Future showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { - if (AvesApp.mode == AppMode.main) { + if (context.read>().value == AppMode.main) { final actions = [ if (filter is AlbumFilter) ChipAction.goToAlbumPage, if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage, diff --git a/lib/widgets/common/identity/aves_icons.dart b/lib/widgets/common/identity/aves_icons.dart index 953c9ed0c..efba5b527 100644 --- a/lib/widgets/common/identity/aves_icons.dart +++ b/lib/widgets/common/identity/aves_icons.dart @@ -5,114 +5,111 @@ import 'package:aves/model/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/collection/thumbnail/theme.dart'; import 'package:decorated_icon/decorated_icon.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; class VideoIcon extends StatelessWidget { final AvesEntry entry; - final double iconSize; - final bool showDuration; const VideoIcon({ Key key, this.entry, - this.iconSize, - this.showDuration, }) : super(key: key); @override Widget build(BuildContext context) { - return OverlayIcon( + final thumbnailTheme = context.watch(); + final showDuration = thumbnailTheme.showVideoDuration; + Widget child = OverlayIcon( icon: entry.is360 ? AIcons.threesixty : AIcons.play, - size: iconSize, + size: thumbnailTheme.iconSize, text: showDuration ? entry.durationText : null, iconScale: entry.is360 && showDuration ? .9 : 1, ); + if (showDuration) { + child = DefaultTextStyle( + style: TextStyle( + color: Colors.grey[200], + fontSize: thumbnailTheme.fontSize, + ), + child: child, + ); + } + return child; } } class AnimatedImageIcon extends StatelessWidget { - final double iconSize; - - const AnimatedImageIcon({Key key, this.iconSize}) : super(key: key); + const AnimatedImageIcon({Key key}) : super(key: key); @override Widget build(BuildContext context) { return OverlayIcon( icon: AIcons.animated, - size: iconSize, + size: context.select((t) => t.iconSize), iconScale: .8, ); } } class GeotiffIcon extends StatelessWidget { - final double iconSize; - - const GeotiffIcon({Key key, this.iconSize}) : super(key: key); + const GeotiffIcon({Key key}) : super(key: key); @override Widget build(BuildContext context) { return OverlayIcon( icon: AIcons.geo, - size: iconSize, + size: context.select((t) => t.iconSize), ); } } class SphericalImageIcon extends StatelessWidget { - final double iconSize; - - const SphericalImageIcon({Key key, this.iconSize}) : super(key: key); + const SphericalImageIcon({Key key}) : super(key: key); @override Widget build(BuildContext context) { return OverlayIcon( icon: AIcons.threesixty, - size: iconSize, + size: context.select((t) => t.iconSize), ); } } class GpsIcon extends StatelessWidget { - final double iconSize; - - const GpsIcon({Key key, this.iconSize}) : super(key: key); + const GpsIcon({Key key}) : super(key: key); @override Widget build(BuildContext context) { return OverlayIcon( icon: AIcons.location, - size: iconSize, + size: context.select((t) => t.iconSize), ); } } class RawIcon extends StatelessWidget { - final double iconSize; - - const RawIcon({Key key, this.iconSize}) : super(key: key); + const RawIcon({Key key}) : super(key: key); @override Widget build(BuildContext context) { return OverlayIcon( icon: AIcons.raw, - size: iconSize, + size: context.select((t) => t.iconSize), ); } } class MultipageIcon extends StatelessWidget { - final double iconSize; - - const MultipageIcon({Key key, this.iconSize}) : super(key: key); + const MultipageIcon({Key key}) : super(key: key); @override Widget build(BuildContext context) { return OverlayIcon( icon: AIcons.multipage, - size: iconSize, + size: context.select((t) => t.iconSize), iconScale: .8, ); } diff --git a/lib/widgets/filter_grids/common/filter_nav_page.dart b/lib/widgets/filter_grids/common/filter_nav_page.dart index e509f82c1..848283a21 100644 --- a/lib/widgets/filter_grids/common/filter_nav_page.dart +++ b/lib/widgets/filter_grids/common/filter_nav_page.dart @@ -22,6 +22,7 @@ import 'package:aves/widgets/search/search_delegate.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:provider/provider.dart'; class FilterNavigationPage extends StatelessWidget { final CollectionSource source; @@ -47,6 +48,7 @@ class FilterNavigationPage extends StatelessWidget { @override Widget build(BuildContext context) { + final isMainMode = context.select, bool>((vn) => vn.value == AppMode.main); return FilterGridPage( key: ValueKey('filter-grid-page'), appBar: SliverAppBar( @@ -80,7 +82,7 @@ class FilterNavigationPage extends StatelessWidget { )), ), ), - onLongPress: AvesApp.mode == AppMode.main ? _showMenu : null, + onLongPress: isMainMode ? _showMenu : null, ); } diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 76da78c66..e895e8d65 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -66,7 +66,7 @@ class _HomePageState extends State { await androidFileUtils.init(); unawaited(androidFileUtils.initAppNames()); - AvesApp.mode = AppMode.main; + var appMode = AppMode.main; final intentData = widget.intentData ?? await ViewerService.getIntentData(); if (intentData?.isNotEmpty == true) { final action = intentData['action']; @@ -77,11 +77,11 @@ class _HomePageState extends State { mimeType: intentData['mimeType'], ); if (_viewerEntry != null) { - AvesApp.mode = AppMode.view; + appMode = AppMode.view; } break; case 'pick': - AvesApp.mode = AppMode.pick; + appMode = AppMode.pick; // TODO TLAD apply pick mimetype(s) // some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?) String pickMimeTypes = intentData['mimeType']; @@ -97,15 +97,16 @@ class _HomePageState extends State { _shortcutFilters = extraFilters != null ? (extraFilters as List).cast() : null; } } - unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', AvesApp.mode.toString())); + context.read>().value = appMode; + unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', appMode.toString())); - if (AvesApp.mode != AppMode.view) { + if (appMode != AppMode.view) { final source = context.read(); await source.init(); unawaited(source.refresh()); } - unawaited(Navigator.pushReplacement(context, _getRedirectRoute())); + unawaited(Navigator.pushReplacement(context, _getRedirectRoute(appMode))); } Future _initViewerEntry({@required String uri, @required String mimeType}) async { @@ -117,8 +118,8 @@ class _HomePageState extends State { return entry; } - Route _getRedirectRoute() { - if (AvesApp.mode == AppMode.view) { + Route _getRedirectRoute(AppMode appMode) { + if (appMode == AppMode.view) { return DirectMaterialPageRoute( settings: RouteSettings(name: EntryViewerPage.routeName), builder: (_) => EntryViewerPage( @@ -129,7 +130,7 @@ class _HomePageState extends State { String routeName; Iterable filters; - if (AvesApp.mode == AppMode.pick) { + if (appMode == AppMode.pick) { routeName = CollectionPage.routeName; } else { routeName = _shortcutRouteName ?? settings.homePage.routeName; diff --git a/lib/widgets/viewer/debug_page.dart b/lib/widgets/viewer/debug_page.dart index b5854fe88..3489cdbc1 100644 --- a/lib/widgets/viewer/debug_page.dart +++ b/lib/widgets/viewer/debug_page.dart @@ -8,6 +8,7 @@ import 'package:aves/widgets/viewer/debug/metadata.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; +import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; class ViewerDebugPage extends StatelessWidget { @@ -21,7 +22,7 @@ class ViewerDebugPage extends StatelessWidget { Widget build(BuildContext context) { final tabs = >[ Tuple2(Tab(text: 'Entry'), _buildEntryTabView()), - if (AvesApp.mode != AppMode.view) Tuple2(Tab(text: 'DB'), DbTab(entry: entry)), + if (context.select, bool>((vn) => vn.value != AppMode.view)) Tuple2(Tab(text: 'DB'), DbTab(entry: entry)), Tuple2(Tab(icon: Icon(AIcons.android)), MetadataTab(entry: entry)), Tuple2(Tab(icon: Icon(AIcons.image)), _buildThumbnailsTabView()), ]; diff --git a/lib/widgets/viewer/overlay/multipage.dart b/lib/widgets/viewer/overlay/multipage.dart index cc5c5ece5..b40adda34 100644 --- a/lib/widgets/viewer/overlay/multipage.dart +++ b/lib/widgets/viewer/overlay/multipage.dart @@ -4,6 +4,7 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/multipage.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/widgets/collection/thumbnail/decorated.dart'; +import 'package:aves/widgets/collection/thumbnail/theme.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -80,66 +81,69 @@ class _MultiPageOverlayState extends State { final horizontalMargin = SizedBox(width: marginWidth); final separator = SizedBox(width: separatorWidth); - return FutureBuilder( - future: controller.info, - builder: (context, snapshot) { - final multiPageInfo = snapshot.data; - if ((multiPageInfo?.pageCount ?? 0) <= 1) return SizedBox(); - if (multiPageInfo.uri != mainEntry.uri) return SizedBox(); - return SizedBox( - height: extent, - child: ListView.separated( - key: ValueKey(mainEntry), - scrollDirection: Axis.horizontal, - controller: _scrollController, - // default padding in scroll direction matches `MediaQuery.viewPadding`, - // but we already accommodate for it, so make sure horizontal padding is 0 - padding: EdgeInsets.zero, - itemBuilder: (context, index) { - if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin; - final page = index - 1; - final pageEntry = mainEntry.getPageEntry(multiPageInfo.getByIndex(page)); + return ThumbnailTheme( + extent: extent, + child: FutureBuilder( + future: controller.info, + builder: (context, snapshot) { + final multiPageInfo = snapshot.data; + if ((multiPageInfo?.pageCount ?? 0) <= 1) return SizedBox(); + if (multiPageInfo.uri != mainEntry.uri) return SizedBox(); + return SizedBox( + height: extent, + child: ListView.separated( + key: ValueKey(mainEntry), + scrollDirection: Axis.horizontal, + controller: _scrollController, + // default padding in scroll direction matches `MediaQuery.viewPadding`, + // but we already accommodate for it, so make sure horizontal padding is 0 + padding: EdgeInsets.zero, + itemBuilder: (context, index) { + if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin; + final page = index - 1; + final pageEntry = mainEntry.getPageEntry(multiPageInfo.getByIndex(page)); - return Stack( - children: [ - GestureDetector( - onTap: () async { - _syncScroll = false; - controller.page = page; - await _scrollController.animateTo( - pageToScrollOffset(page), - duration: Durations.viewerOverlayPageScrollAnimation, - curve: Curves.easeOutCubic, - ); - _syncScroll = true; - }, - child: DecoratedThumbnail( - entry: pageEntry, - extent: extent, - // the retrieval task queue can pile up for thumbnails of heavy pages - // (e.g. thumbnails of 15MP HEIF images inside 100MB+ HEIC containers) - // so we cancel these requests when possible - cancellableNotifier: _cancellableNotifier, - selectable: false, - highlightable: false, + return Stack( + children: [ + GestureDetector( + onTap: () async { + _syncScroll = false; + controller.page = page; + await _scrollController.animateTo( + pageToScrollOffset(page), + duration: Durations.viewerOverlayPageScrollAnimation, + curve: Curves.easeOutCubic, + ); + _syncScroll = true; + }, + child: DecoratedThumbnail( + entry: pageEntry, + extent: extent, + // the retrieval task queue can pile up for thumbnails of heavy pages + // (e.g. thumbnails of 15MP HEIF images inside 100MB+ HEIC containers) + // so we cancel these requests when possible + cancellableNotifier: _cancellableNotifier, + selectable: false, + highlightable: false, + ), ), - ), - IgnorePointer( - child: AnimatedContainer( - color: controller.page == page ? Colors.transparent : Colors.black45, - width: extent, - height: extent, - duration: Durations.viewerOverlayPageShadeAnimation, - ), - ) - ], - ); - }, - separatorBuilder: (context, index) => separator, - itemCount: multiPageInfo.pageCount + 2, - ), - ); - }, + IgnorePointer( + child: AnimatedContainer( + color: controller.page == page ? Colors.transparent : Colors.black45, + width: extent, + height: extent, + duration: Durations.viewerOverlayPageShadeAnimation, + ), + ) + ], + ); + }, + separatorBuilder: (context, index) => separator, + itemCount: multiPageInfo.pageCount + 2, + ), + ); + }, + ), ); } diff --git a/pubspec.lock b/pubspec.lock index 6d470da10..e3301a163 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -559,7 +559,7 @@ packages: name: overlay_support url: "https://pub.dartlang.org" source: hosted - version: "1.2.0-nullsafety.0" + version: "1.2.0" package_config: dependency: transitive description: @@ -706,7 +706,7 @@ packages: name: printing url: "https://pub.dartlang.org" source: hosted - version: "5.0.2" + version: "5.0.3" process: dependency: transitive description: @@ -1026,7 +1026,7 @@ packages: name: version url: "https://pub.dartlang.org" source: hosted - version: "1.3.1" + version: "2.0.0" vm_service: dependency: transitive description: @@ -1068,7 +1068,7 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.0.4" wkt_parser: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3b9aaec16..46431f7e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,8 +7,6 @@ publish_to: none environment: sdk: '>=2.10.0 <3.0.0' -# TODO TLAD remove explicit `overlay_support` version when 1.2.0 is stable (1.0.5 uses deprecated `ancestorWidgetOfExactType`) - # TODO TLAD switch to Flutter stable when possible, currently on dev/beta because of the following mess: # printing >=5.0.1 depends on pdf ^3.0.1, pdf >=3.0.1 depends on crypto ^3.0.0 and archive ^3.1.0 # but `flutter_driver` (shipped with Flutter) dependencies are too old in stable v2.0.1 @@ -57,7 +55,7 @@ dependencies: intl: latlong: material_design_icons_flutter: - overlay_support: 1.2.0-nullsafety.0 + overlay_support: package_info: palette_generator: panorama: @@ -100,9 +98,6 @@ flutter: # generate `AppLocalizations` # % flutter gen-l10n -# list untranslated messages -# % flutter gen-l10n --untranslated-messages-file untranslated.json - ################################################################################ # Test driver