thumbnail theme provider, app mode provider, thumbnail overlay review

This commit is contained in:
Thibault Deckers 2021-03-16 10:18:53 +09:00
parent ff6aef1e82
commit cf8d182cfe
16 changed files with 268 additions and 216 deletions

View file

@ -6,3 +6,5 @@
preferred-supported-locales: preferred-supported-locales:
- en - en
# untranslated-messages-file: untranslated.json

View file

@ -43,13 +43,12 @@ void main() {
enum AppMode { main, pick, view } enum AppMode { main, pick, view }
class AvesApp extends StatefulWidget { class AvesApp extends StatefulWidget {
static AppMode mode;
@override @override
_AvesAppState createState() => _AvesAppState(); _AvesAppState createState() => _AvesAppState();
} }
class _AvesAppState extends State<AvesApp> { class _AvesAppState extends State<AvesApp> {
final ValueNotifier<AppMode> appModeNotifier = ValueNotifier(AppMode.main);
Future<void> _appSetup; Future<void> _appSetup;
final _mediaStoreSource = MediaStoreSource(); final _mediaStoreSource = MediaStoreSource();
final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay); final Debouncer _contentChangeDebouncer = Debouncer(delay: Durations.contentChangeDebounceDelay);
@ -123,38 +122,41 @@ class _AvesAppState extends State<AvesApp> {
// so it can be used during navigation transitions // so it can be used during navigation transitions
return ChangeNotifierProvider<Settings>.value( return ChangeNotifierProvider<Settings>.value(
value: settings, value: settings,
child: Provider<CollectionSource>.value( child: ListenableProvider<ValueNotifier<AppMode>>.value(
value: _mediaStoreSource, value: appModeNotifier,
child: HighlightInfoProvider( child: Provider<CollectionSource>.value(
child: OverlaySupport( value: _mediaStoreSource,
child: FutureBuilder<void>( child: HighlightInfoProvider(
future: _appSetup, child: OverlaySupport(
builder: (context, snapshot) { child: FutureBuilder<void>(
final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done; future: _appSetup,
final home = initialized builder: (context, snapshot) {
? getFirstPage() final initialized = !snapshot.hasError && snapshot.connectionState == ConnectionState.done;
: Scaffold( final home = initialized
body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox(), ? getFirstPage()
); : Scaffold(
return Selector<Settings, Locale>( body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox(),
selector: (context, s) => s.locale, );
builder: (context, settingsLocale, child) { return Selector<Settings, Locale>(
return MaterialApp( selector: (context, s) => s.locale,
navigatorKey: _navigatorKey, builder: (context, settingsLocale, child) {
home: home, return MaterialApp(
navigatorObservers: _navigatorObservers, navigatorKey: _navigatorKey,
onGenerateTitle: (context) => context.l10n.appName, home: home,
darkTheme: darkTheme, navigatorObservers: _navigatorObservers,
themeMode: ThemeMode.dark, onGenerateTitle: (context) => context.l10n.appName,
locale: settingsLocale, darkTheme: darkTheme,
localizationsDelegates: [ themeMode: ThemeMode.dark,
...AppLocalizations.localizationsDelegates, locale: settingsLocale,
LocaleNamesLocalizationsDelegate(), localizationsDelegates: [
], ...AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales, LocaleNamesLocalizationsDelegate(),
); ],
}); supportedLocales: AppLocalizations.supportedLocales,
}, );
});
},
),
), ),
), ),
), ),
@ -204,7 +206,7 @@ class _AvesAppState extends State<AvesApp> {
debugPrint('$runtimeType onNewIntent with intentData=$intentData'); debugPrint('$runtimeType onNewIntent with intentData=$intentData');
// do not reset when relaunching the app // 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'); FirebaseCrashlytics.instance.log('New intent');
_navigatorKey.currentState.pushReplacement(DirectMaterialPageRoute( _navigatorKey.currentState.pushReplacement(DirectMaterialPageRoute(

View file

@ -26,6 +26,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:pedantic/pedantic.dart'; import 'package:pedantic/pedantic.dart';
import 'package:provider/provider.dart';
class CollectionAppBar extends StatefulWidget { class CollectionAppBar extends StatefulWidget {
final ValueNotifier<double> appBarHeightNotifier; final ValueNotifier<double> appBarHeightNotifier;
@ -141,8 +142,9 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
Widget _buildAppBarTitle() { Widget _buildAppBarTitle() {
if (collection.isBrowsing) { if (collection.isBrowsing) {
Widget title = Text(AvesApp.mode == AppMode.pick ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle); final appMode = context.watch<ValueNotifier<AppMode>>().value;
if (AvesApp.mode == AppMode.main) { Widget title = Text(appMode == AppMode.pick ? context.l10n.collectionPickPageTitle : context.l10n.collectionPageTitle);
if (appMode == AppMode.main) {
title = SourceStateAwareAppBarTitle( title = SourceStateAwareAppBarTitle(
title: title, title: title,
source: source, source: source,
@ -191,6 +193,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
itemBuilder: (context) { itemBuilder: (context) {
final isNotEmpty = !collection.isEmpty; final isNotEmpty = !collection.isEmpty;
final hasSelection = collection.selection.isNotEmpty; final hasSelection = collection.selection.isNotEmpty;
final isMainMode = context.read<ValueNotifier<AppMode>>().value == AppMode.main;
return [ return [
PopupMenuItem( PopupMenuItem(
key: Key('menu-sort'), key: Key('menu-sort'),
@ -204,7 +207,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group), child: MenuRow(text: context.l10n.menuActionGroup, icon: AIcons.group),
), ),
if (collection.isBrowsing) ...[ if (collection.isBrowsing) ...[
if (AvesApp.mode == AppMode.main) if (isMainMode)
PopupMenuItem( PopupMenuItem(
value: CollectionAction.select, value: CollectionAction.select,
enabled: isNotEmpty, enabled: isNotEmpty,
@ -215,7 +218,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
enabled: isNotEmpty, enabled: isNotEmpty,
child: MenuRow(text: context.l10n.menuActionStats, icon: AIcons.stats), child: MenuRow(text: context.l10n.menuActionStats, icon: AIcons.stats),
), ),
if (AvesApp.mode == AppMode.main && canAddShortcuts) if (isMainMode && canAddShortcuts)
PopupMenuItem( PopupMenuItem(
value: CollectionAction.addShortcut, value: CollectionAction.addShortcut,
child: MenuRow(text: context.l10n.collectionActionAddShortcut, icon: AIcons.addShortcut), child: MenuRow(text: context.l10n.collectionActionAddShortcut, icon: AIcons.addShortcut),

View file

@ -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/selector.dart';
import 'package:aves/widgets/collection/grid/thumbnail.dart'; import 'package:aves/widgets/collection/grid/thumbnail.dart';
import 'package:aves/widgets/collection/thumbnail/decorated.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/draggable_scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart'; import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart'; import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
@ -65,22 +66,25 @@ class _CollectionGridContent extends StatelessWidget {
final sectionedListLayoutProvider = ValueListenableBuilder<double>( final sectionedListLayoutProvider = ValueListenableBuilder<double>(
valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier), valueListenable: context.select<TileExtentController, ValueNotifier<double>>((controller) => controller.extentNotifier),
builder: (context, tileExtent, child) { builder: (context, tileExtent, child) {
return SectionedEntryListLayoutProvider( return ThumbnailTheme(
collection: collection, extent: tileExtent,
scrollableWidth: context.select<TileExtentController, double>((controller) => controller.viewportSize.width), child: SectionedEntryListLayoutProvider(
tileExtent: tileExtent,
columnCount: context.select<TileExtentController, int>((controller) => controller.getEffectiveColumnCountForExtent(tileExtent)),
tileBuilder: (entry) => InteractiveThumbnail(
key: ValueKey(entry.contentId),
collection: collection, collection: collection,
entry: entry, scrollableWidth: context.select<TileExtentController, double>((controller) => controller.viewportSize.width),
tileExtent: tileExtent, tileExtent: tileExtent,
isScrollingNotifier: _isScrollingNotifier, columnCount: context.select<TileExtentController, int>((controller) => controller.getEffectiveColumnCountForExtent(tileExtent)),
), tileBuilder: (entry) => InteractiveThumbnail(
child: _CollectionSectionedContent( key: ValueKey(entry.contentId),
collection: collection, collection: collection,
isScrollingNotifier: _isScrollingNotifier, entry: entry,
scrollController: PrimaryScrollController.of(context), 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, child: scrollView,
); );
final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main);
final selector = GridSelectionGestureDetector( final selector = GridSelectionGestureDetector(
selectable: AvesApp.mode == AppMode.main, selectable: isMainMode,
collection: collection, collection: collection,
scrollController: scrollController, scrollController: scrollController,
appBarHeightNotifier: _appBarHeightNotifier, appBarHeightNotifier: _appBarHeightNotifier,
@ -177,11 +182,14 @@ class _CollectionScaler extends StatelessWidget {
), ),
child: child, child: child,
), ),
scaledBuilder: (entry, extent) => DecoratedThumbnail( scaledBuilder: (entry, extent) => ThumbnailTheme(
entry: entry,
extent: extent, extent: extent,
selectable: false, child: DecoratedThumbnail(
highlightable: false, entry: entry,
extent: extent,
selectable: false,
highlightable: false,
),
), ),
getScaledItemTileRect: (context, entry) { getScaledItemTileRect: (context, entry) {
final sectionedListLayout = context.read<SectionedListLayout<AvesEntry>>(); final sectionedListLayout = context.read<SectionedListLayout<AvesEntry>>();

View file

@ -7,6 +7,7 @@ import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/scaling.dart'; import 'package:aves/widgets/common/scaling.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart'; import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class InteractiveThumbnail extends StatelessWidget { class InteractiveThumbnail extends StatelessWidget {
final CollectionLens collection; final CollectionLens collection;
@ -27,13 +28,14 @@ class InteractiveThumbnail extends StatelessWidget {
return GestureDetector( return GestureDetector(
key: ValueKey(entry.uri), key: ValueKey(entry.uri),
onTap: () { onTap: () {
if (AvesApp.mode == AppMode.main) { final appMode = context.read<ValueNotifier<AppMode>>().value;
if (appMode == AppMode.main) {
if (collection.isBrowsing) { if (collection.isBrowsing) {
_goToViewer(context); _goToViewer(context);
} else if (collection.isSelecting) { } else if (collection.isSelecting) {
collection.toggleSelection(entry); collection.toggleSelection(entry);
} }
} else if (AvesApp.mode == AppMode.pick) { } else if (appMode == AppMode.pick) {
ViewerService.pick(entry.uri); ViewerService.pick(entry.uri);
} }
}, },

View file

@ -31,7 +31,8 @@ class DecoratedThumbnail extends StatelessWidget {
// between different views of the entry in the same collection (e.g. thumbnails <-> viewer) // 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) // but not between different collection instances, even with the same attributes (e.g. reloading collection page via drawer)
final heroTag = hashValues(collection?.id, entry); final heroTag = hashValues(collection?.id, entry);
var child = entry.isSvg final isSvg = entry.isSvg;
var child = isSvg
? VectorImageThumbnail( ? VectorImageThumbnail(
entry: entry, entry: entry,
extent: extent, extent: extent,
@ -45,17 +46,14 @@ class DecoratedThumbnail extends StatelessWidget {
); );
child = Stack( child = Stack(
alignment: Alignment.center, alignment: isSvg ? Alignment.center : AlignmentDirectional.bottomStart,
children: [ children: [
child, child,
Positioned( if (!isSvg)
bottom: 0, ThumbnailEntryOverlay(
left: 0,
child: ThumbnailEntryOverlay(
entry: entry, entry: entry,
extent: extent, extent: extent,
), ),
),
if (selectable) if (selectable)
ThumbnailSelectionOverlay( ThumbnailSelectionOverlay(
entry: entry, entry: entry,

View file

@ -2,16 +2,15 @@ import 'dart:math';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/highlight.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/collection_lens.dart';
import 'package:aves/model/source/enums.dart'; import 'package:aves/model/source/enums.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.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/fx/sweeper.dart';
import 'package:aves/widgets/common/identity/aves_icons.dart'; import 'package:aves/widgets/common/identity/aves_icons.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class ThumbnailEntryOverlay extends StatelessWidget { class ThumbnailEntryOverlay extends StatelessWidget {
final AvesEntry entry; final AvesEntry entry;
@ -25,38 +24,28 @@ class ThumbnailEntryOverlay extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final fontSize = min(14.0, (extent / 8)).roundToDouble(); final children = [
final iconSize = fontSize * 2; if (entry.hasGps && context.select<ThumbnailThemeData, bool>((t) => t.showLocation)) GpsIcon(),
return Selector<Settings, Tuple3<bool, bool, bool>>( if (entry.isVideo)
selector: (context, s) => Tuple3(s.showThumbnailLocation, s.showThumbnailRaw, s.showThumbnailVideoDuration), VideoIcon(
builder: (context, s, child) { entry: entry,
return Column( )
mainAxisSize: MainAxisSize.min, else if (entry.isAnimated)
crossAxisAlignment: CrossAxisAlignment.start, AnimatedImageIcon()
children: [ else ...[
if (entry.hasGps && settings.showThumbnailLocation) GpsIcon(iconSize: iconSize), if (entry.isRaw && context.select<ThumbnailThemeData, bool>((t) => t.showRaw)) RawIcon(),
if (entry.isRaw && settings.showThumbnailRaw) RawIcon(iconSize: iconSize), if (entry.isMultipage) MultipageIcon(),
if (entry.isMultipage) MultipageIcon(iconSize: iconSize), if (entry.isGeotiff) GeotiffIcon(),
if (entry.isGeotiff) GeotiffIcon(iconSize: iconSize), if (entry.is360) SphericalImageIcon(),
if (entry.isAnimated) ]
AnimatedImageIcon(iconSize: iconSize) ];
else if (entry.isVideo) if (children.isEmpty) return SizedBox.shrink();
DefaultTextStyle( if (children.length == 1) return children.first;
style: TextStyle( return Column(
color: Colors.grey[200], mainAxisSize: MainAxisSize.min,
fontSize: fontSize, crossAxisAlignment: CrossAxisAlignment.start,
), children: children,
child: VideoIcon( );
entry: entry,
iconSize: iconSize,
showDuration: settings.showThumbnailVideoDuration,
),
)
else if (entry.is360)
SphericalImageIcon(iconSize: iconSize),
],
);
});
} }
} }
@ -64,6 +53,8 @@ class ThumbnailSelectionOverlay extends StatelessWidget {
final AvesEntry entry; final AvesEntry entry;
final double extent; final double extent;
static const duration = Durations.thumbnailOverlayAnimation;
const ThumbnailSelectionOverlay({ const ThumbnailSelectionOverlay({
Key key, Key key,
@required this.entry, @required this.entry,
@ -72,9 +63,6 @@ class ThumbnailSelectionOverlay extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const duration = Durations.thumbnailOverlayAnimation;
final fontSize = min(14.0, (extent / 8)).roundToDouble();
final iconSize = fontSize * 2;
final collection = context.watch<CollectionLens>(); final collection = context.watch<CollectionLens>();
return ValueListenableBuilder<Activity>( return ValueListenableBuilder<Activity>(
valueListenable: collection.activityNotifier, valueListenable: collection.activityNotifier,
@ -88,7 +76,7 @@ class ThumbnailSelectionOverlay extends StatelessWidget {
? OverlayIcon( ? OverlayIcon(
key: ValueKey(selected), key: ValueKey(selected),
icon: selected ? AIcons.selected : AIcons.unselected, icon: selected ? AIcons.selected : AIcons.unselected,
size: iconSize, size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
) )
: SizedBox.shrink(); : SizedBox.shrink();
child = AnimatedSwitcher( child = AnimatedSwitcher(
@ -139,6 +127,8 @@ class _ThumbnailHighlightOverlayState extends State<ThumbnailHighlightOverlay> {
AvesEntry get entry => widget.entry; AvesEntry get entry => widget.entry;
static const startAngle = pi * -3 / 4;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final highlightInfo = context.watch<HighlightInfo>(); final highlightInfo = context.watch<HighlightInfo>();
@ -153,7 +143,7 @@ class _ThumbnailHighlightOverlayState extends State<ThumbnailHighlightOverlay> {
), ),
), ),
toggledNotifier: _highlightedNotifier, toggledNotifier: _highlightedNotifier,
startAngle: pi * -3 / 4, startAngle: startAngle,
centerSweep: false, centerSweep: false,
onSweepEnd: highlightInfo.clear, onSweepEnd: highlightInfo.clear,
); );

View file

@ -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<Settings, ThumbnailThemeData>(
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,
});
}

View file

@ -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:aves/widgets/filter_grids/common/chip_action_delegate.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
typedef FilterCallback = void Function(CollectionFilter filter); typedef FilterCallback = void Function(CollectionFilter filter);
typedef OffsetFilterCallback = void Function(BuildContext context, CollectionFilter filter, Offset tapPosition); typedef OffsetFilterCallback = void Function(BuildContext context, CollectionFilter filter, Offset tapPosition);
@ -52,7 +53,7 @@ class AvesFilterChip extends StatefulWidget {
super(key: key); super(key: key);
static Future<void> showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async { static Future<void> showDefaultLongPressMenu(BuildContext context, CollectionFilter filter, Offset tapPosition) async {
if (AvesApp.mode == AppMode.main) { if (context.read<ValueNotifier<AppMode>>().value == AppMode.main) {
final actions = [ final actions = [
if (filter is AlbumFilter) ChipAction.goToAlbumPage, if (filter is AlbumFilter) ChipAction.goToAlbumPage,
if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage, if ((filter is LocationFilter && filter.level == LocationLevel.country)) ChipAction.goToCountryPage,

View file

@ -5,114 +5,111 @@ import 'package:aves/model/entry.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/utils/constants.dart'; import 'package:aves/utils/constants.dart';
import 'package:aves/widgets/collection/thumbnail/theme.dart';
import 'package:decorated_icon/decorated_icon.dart'; import 'package:decorated_icon/decorated_icon.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class VideoIcon extends StatelessWidget { class VideoIcon extends StatelessWidget {
final AvesEntry entry; final AvesEntry entry;
final double iconSize;
final bool showDuration;
const VideoIcon({ const VideoIcon({
Key key, Key key,
this.entry, this.entry,
this.iconSize,
this.showDuration,
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OverlayIcon( final thumbnailTheme = context.watch<ThumbnailThemeData>();
final showDuration = thumbnailTheme.showVideoDuration;
Widget child = OverlayIcon(
icon: entry.is360 ? AIcons.threesixty : AIcons.play, icon: entry.is360 ? AIcons.threesixty : AIcons.play,
size: iconSize, size: thumbnailTheme.iconSize,
text: showDuration ? entry.durationText : null, text: showDuration ? entry.durationText : null,
iconScale: entry.is360 && showDuration ? .9 : 1, 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 { class AnimatedImageIcon extends StatelessWidget {
final double iconSize; const AnimatedImageIcon({Key key}) : super(key: key);
const AnimatedImageIcon({Key key, this.iconSize}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OverlayIcon( return OverlayIcon(
icon: AIcons.animated, icon: AIcons.animated,
size: iconSize, size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
iconScale: .8, iconScale: .8,
); );
} }
} }
class GeotiffIcon extends StatelessWidget { class GeotiffIcon extends StatelessWidget {
final double iconSize; const GeotiffIcon({Key key}) : super(key: key);
const GeotiffIcon({Key key, this.iconSize}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OverlayIcon( return OverlayIcon(
icon: AIcons.geo, icon: AIcons.geo,
size: iconSize, size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
); );
} }
} }
class SphericalImageIcon extends StatelessWidget { class SphericalImageIcon extends StatelessWidget {
final double iconSize; const SphericalImageIcon({Key key}) : super(key: key);
const SphericalImageIcon({Key key, this.iconSize}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OverlayIcon( return OverlayIcon(
icon: AIcons.threesixty, icon: AIcons.threesixty,
size: iconSize, size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
); );
} }
} }
class GpsIcon extends StatelessWidget { class GpsIcon extends StatelessWidget {
final double iconSize; const GpsIcon({Key key}) : super(key: key);
const GpsIcon({Key key, this.iconSize}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OverlayIcon( return OverlayIcon(
icon: AIcons.location, icon: AIcons.location,
size: iconSize, size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
); );
} }
} }
class RawIcon extends StatelessWidget { class RawIcon extends StatelessWidget {
final double iconSize; const RawIcon({Key key}) : super(key: key);
const RawIcon({Key key, this.iconSize}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OverlayIcon( return OverlayIcon(
icon: AIcons.raw, icon: AIcons.raw,
size: iconSize, size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
); );
} }
} }
class MultipageIcon extends StatelessWidget { class MultipageIcon extends StatelessWidget {
final double iconSize; const MultipageIcon({Key key}) : super(key: key);
const MultipageIcon({Key key, this.iconSize}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return OverlayIcon( return OverlayIcon(
icon: AIcons.multipage, icon: AIcons.multipage,
size: iconSize, size: context.select<ThumbnailThemeData, double>((t) => t.iconSize),
iconScale: .8, iconScale: .8,
); );
} }

View file

@ -22,6 +22,7 @@ import 'package:aves/widgets/search/search_delegate.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:provider/provider.dart';
class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget { class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
final CollectionSource source; final CollectionSource source;
@ -47,6 +48,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isMainMode = context.select<ValueNotifier<AppMode>, bool>((vn) => vn.value == AppMode.main);
return FilterGridPage<T>( return FilterGridPage<T>(
key: ValueKey('filter-grid-page'), key: ValueKey('filter-grid-page'),
appBar: SliverAppBar( appBar: SliverAppBar(
@ -80,7 +82,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
)), )),
), ),
), ),
onLongPress: AvesApp.mode == AppMode.main ? _showMenu : null, onLongPress: isMainMode ? _showMenu : null,
); );
} }

View file

@ -66,7 +66,7 @@ class _HomePageState extends State<HomePage> {
await androidFileUtils.init(); await androidFileUtils.init();
unawaited(androidFileUtils.initAppNames()); unawaited(androidFileUtils.initAppNames());
AvesApp.mode = AppMode.main; var appMode = AppMode.main;
final intentData = widget.intentData ?? await ViewerService.getIntentData(); final intentData = widget.intentData ?? await ViewerService.getIntentData();
if (intentData?.isNotEmpty == true) { if (intentData?.isNotEmpty == true) {
final action = intentData['action']; final action = intentData['action'];
@ -77,11 +77,11 @@ class _HomePageState extends State<HomePage> {
mimeType: intentData['mimeType'], mimeType: intentData['mimeType'],
); );
if (_viewerEntry != null) { if (_viewerEntry != null) {
AvesApp.mode = AppMode.view; appMode = AppMode.view;
} }
break; break;
case 'pick': case 'pick':
AvesApp.mode = AppMode.pick; appMode = AppMode.pick;
// TODO TLAD apply pick mimetype(s) // TODO TLAD apply pick mimetype(s)
// some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?) // some apps define multiple types, separated by a space (maybe other signs too, like `,` `;`?)
String pickMimeTypes = intentData['mimeType']; String pickMimeTypes = intentData['mimeType'];
@ -97,15 +97,16 @@ class _HomePageState extends State<HomePage> {
_shortcutFilters = extraFilters != null ? (extraFilters as List).cast<String>() : null; _shortcutFilters = extraFilters != null ? (extraFilters as List).cast<String>() : null;
} }
} }
unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', AvesApp.mode.toString())); context.read<ValueNotifier<AppMode>>().value = appMode;
unawaited(FirebaseCrashlytics.instance.setCustomKey('app_mode', appMode.toString()));
if (AvesApp.mode != AppMode.view) { if (appMode != AppMode.view) {
final source = context.read<CollectionSource>(); final source = context.read<CollectionSource>();
await source.init(); await source.init();
unawaited(source.refresh()); unawaited(source.refresh());
} }
unawaited(Navigator.pushReplacement(context, _getRedirectRoute())); unawaited(Navigator.pushReplacement(context, _getRedirectRoute(appMode)));
} }
Future<AvesEntry> _initViewerEntry({@required String uri, @required String mimeType}) async { Future<AvesEntry> _initViewerEntry({@required String uri, @required String mimeType}) async {
@ -117,8 +118,8 @@ class _HomePageState extends State<HomePage> {
return entry; return entry;
} }
Route _getRedirectRoute() { Route _getRedirectRoute(AppMode appMode) {
if (AvesApp.mode == AppMode.view) { if (appMode == AppMode.view) {
return DirectMaterialPageRoute( return DirectMaterialPageRoute(
settings: RouteSettings(name: EntryViewerPage.routeName), settings: RouteSettings(name: EntryViewerPage.routeName),
builder: (_) => EntryViewerPage( builder: (_) => EntryViewerPage(
@ -129,7 +130,7 @@ class _HomePageState extends State<HomePage> {
String routeName; String routeName;
Iterable<CollectionFilter> filters; Iterable<CollectionFilter> filters;
if (AvesApp.mode == AppMode.pick) { if (appMode == AppMode.pick) {
routeName = CollectionPage.routeName; routeName = CollectionPage.routeName;
} else { } else {
routeName = _shortcutRouteName ?? settings.homePage.routeName; routeName = _shortcutRouteName ?? settings.homePage.routeName;

View file

@ -8,6 +8,7 @@ import 'package:aves/widgets/viewer/debug/metadata.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class ViewerDebugPage extends StatelessWidget { class ViewerDebugPage extends StatelessWidget {
@ -21,7 +22,7 @@ class ViewerDebugPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tabs = <Tuple2<Tab, Widget>>[ final tabs = <Tuple2<Tab, Widget>>[
Tuple2(Tab(text: 'Entry'), _buildEntryTabView()), Tuple2(Tab(text: 'Entry'), _buildEntryTabView()),
if (AvesApp.mode != AppMode.view) Tuple2(Tab(text: 'DB'), DbTab(entry: entry)), if (context.select<ValueNotifier<AppMode>, 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.android)), MetadataTab(entry: entry)),
Tuple2(Tab(icon: Icon(AIcons.image)), _buildThumbnailsTabView()), Tuple2(Tab(icon: Icon(AIcons.image)), _buildThumbnailsTabView()),
]; ];

View file

@ -4,6 +4,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/multipage.dart'; import 'package:aves/model/multipage.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/collection/thumbnail/decorated.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:aves/widgets/viewer/multipage.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -80,66 +81,69 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
final horizontalMargin = SizedBox(width: marginWidth); final horizontalMargin = SizedBox(width: marginWidth);
final separator = SizedBox(width: separatorWidth); final separator = SizedBox(width: separatorWidth);
return FutureBuilder<MultiPageInfo>( return ThumbnailTheme(
future: controller.info, extent: extent,
builder: (context, snapshot) { child: FutureBuilder<MultiPageInfo>(
final multiPageInfo = snapshot.data; future: controller.info,
if ((multiPageInfo?.pageCount ?? 0) <= 1) return SizedBox(); builder: (context, snapshot) {
if (multiPageInfo.uri != mainEntry.uri) return SizedBox(); final multiPageInfo = snapshot.data;
return SizedBox( if ((multiPageInfo?.pageCount ?? 0) <= 1) return SizedBox();
height: extent, if (multiPageInfo.uri != mainEntry.uri) return SizedBox();
child: ListView.separated( return SizedBox(
key: ValueKey(mainEntry), height: extent,
scrollDirection: Axis.horizontal, child: ListView.separated(
controller: _scrollController, key: ValueKey(mainEntry),
// default padding in scroll direction matches `MediaQuery.viewPadding`, scrollDirection: Axis.horizontal,
// but we already accommodate for it, so make sure horizontal padding is 0 controller: _scrollController,
padding: EdgeInsets.zero, // default padding in scroll direction matches `MediaQuery.viewPadding`,
itemBuilder: (context, index) { // but we already accommodate for it, so make sure horizontal padding is 0
if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin; padding: EdgeInsets.zero,
final page = index - 1; itemBuilder: (context, index) {
final pageEntry = mainEntry.getPageEntry(multiPageInfo.getByIndex(page)); if (index == 0 || index == multiPageInfo.pageCount + 1) return horizontalMargin;
final page = index - 1;
final pageEntry = mainEntry.getPageEntry(multiPageInfo.getByIndex(page));
return Stack( return Stack(
children: [ children: [
GestureDetector( GestureDetector(
onTap: () async { onTap: () async {
_syncScroll = false; _syncScroll = false;
controller.page = page; controller.page = page;
await _scrollController.animateTo( await _scrollController.animateTo(
pageToScrollOffset(page), pageToScrollOffset(page),
duration: Durations.viewerOverlayPageScrollAnimation, duration: Durations.viewerOverlayPageScrollAnimation,
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
); );
_syncScroll = true; _syncScroll = true;
}, },
child: DecoratedThumbnail( child: DecoratedThumbnail(
entry: pageEntry, entry: pageEntry,
extent: extent, extent: extent,
// the retrieval task queue can pile up for thumbnails of heavy pages // the retrieval task queue can pile up for thumbnails of heavy pages
// (e.g. thumbnails of 15MP HEIF images inside 100MB+ HEIC containers) // (e.g. thumbnails of 15MP HEIF images inside 100MB+ HEIC containers)
// so we cancel these requests when possible // so we cancel these requests when possible
cancellableNotifier: _cancellableNotifier, cancellableNotifier: _cancellableNotifier,
selectable: false, selectable: false,
highlightable: false, highlightable: false,
),
), ),
), IgnorePointer(
IgnorePointer( child: AnimatedContainer(
child: AnimatedContainer( color: controller.page == page ? Colors.transparent : Colors.black45,
color: controller.page == page ? Colors.transparent : Colors.black45, width: extent,
width: extent, height: extent,
height: extent, duration: Durations.viewerOverlayPageShadeAnimation,
duration: Durations.viewerOverlayPageShadeAnimation, ),
), )
) ],
], );
); },
}, separatorBuilder: (context, index) => separator,
separatorBuilder: (context, index) => separator, itemCount: multiPageInfo.pageCount + 2,
itemCount: multiPageInfo.pageCount + 2, ),
), );
); },
}, ),
); );
} }

View file

@ -559,7 +559,7 @@ packages:
name: overlay_support name: overlay_support
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.2.0-nullsafety.0" version: "1.2.0"
package_config: package_config:
dependency: transitive dependency: transitive
description: description:
@ -706,7 +706,7 @@ packages:
name: printing name: printing
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "5.0.2" version: "5.0.3"
process: process:
dependency: transitive dependency: transitive
description: description:
@ -1026,7 +1026,7 @@ packages:
name: version name: version
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.3.1" version: "2.0.0"
vm_service: vm_service:
dependency: transitive dependency: transitive
description: description:
@ -1068,7 +1068,7 @@ packages:
name: win32 name: win32
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.0.3" version: "2.0.4"
wkt_parser: wkt_parser:
dependency: transitive dependency: transitive
description: description:

View file

@ -7,8 +7,6 @@ publish_to: none
environment: environment:
sdk: '>=2.10.0 <3.0.0' 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: # 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 # 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 # but `flutter_driver` (shipped with Flutter) dependencies are too old in stable v2.0.1
@ -57,7 +55,7 @@ dependencies:
intl: intl:
latlong: latlong:
material_design_icons_flutter: material_design_icons_flutter:
overlay_support: 1.2.0-nullsafety.0 overlay_support:
package_info: package_info:
palette_generator: palette_generator:
panorama: panorama:
@ -100,9 +98,6 @@ flutter:
# generate `AppLocalizations` # generate `AppLocalizations`
# % flutter gen-l10n # % flutter gen-l10n
# list untranslated messages
# % flutter gen-l10n --untranslated-messages-file untranslated.json
################################################################################ ################################################################################
# Test driver # Test driver