thumb zoom prep

This commit is contained in:
Thibault Deckers 2023-03-08 14:24:56 +01:00
parent 655d251890
commit 09cf4fef3e
20 changed files with 271 additions and 219 deletions

View file

@ -125,6 +125,7 @@ class AIcons {
static const IconData setCover = MdiIcons.imageEditOutline;
static const IconData share = Icons.share_outlined;
static const IconData show = Icons.visibility_outlined;
static const IconData showFullscreen = MdiIcons.arrowExpand;
static const IconData slideshow = Icons.slideshow_outlined;
static const IconData speed = Icons.speed_outlined;
static const IconData stats = Icons.donut_small_outlined;

View file

@ -5,6 +5,7 @@ import 'package:aves/model/entry.dart';
import 'package:aves/model/favourites.dart';
import 'package:aves/model/filters/favourite.dart';
import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/collection_source.dart';
@ -21,6 +22,7 @@ import 'package:aves/widgets/collection/grid/section_layout.dart';
import 'package:aves/widgets/collection/grid/tile.dart';
import 'package:aves/widgets/common/basic/draggable_scrollbar/scrollbar.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/behaviour/sloppy_scroll_physics.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/extensions/media_query.dart';
@ -39,8 +41,10 @@ import 'package:aves/widgets/common/identity/scroll_thumb.dart';
import 'package:aves/widgets/common/providers/tile_extent_controller_provider.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/common/thumbnail/image.dart';
import 'package:aves/widgets/common/thumbnail/notifications.dart';
import 'package:aves/widgets/common/tile_extent_controller.dart';
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -152,7 +156,12 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
tileAnimationDelay = Duration.zero;
}
return StreamBuilder(
return NotificationListener<OpenViewerNotification>(
onNotification: (notification) {
_goToViewer(collection, notification.entry);
return true;
},
child: StreamBuilder(
stream: source.eventBus.on<AspectRatioChangedEvent>(),
builder: (context, snapshot) => SectionedEntryListLayoutProvider(
collection: collection,
@ -205,6 +214,7 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
tileAnimationDelay: tileAnimationDelay,
child: child!,
),
),
);
},
child: child,
@ -227,6 +237,34 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
},
);
}
void _goToViewer(CollectionLens collection, AvesEntry entry) {
final selection = context.read<Selection<AvesEntry>>();
Navigator.maybeOf(context)?.push(
TransparentMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (context, a, sa) {
final viewerCollection = collection.copyWith(
listenToSource: false,
);
assert(viewerCollection.sortedEntries.map((entry) => entry.id).contains(entry.id));
Widget child = EntryViewerPage(
collection: viewerCollection,
initialEntry: entry,
);
if (selection.isSelecting) {
child = ChangeNotifierProvider<Selection<AvesEntry>>.value(
value: selection,
child: child,
);
}
return child;
},
),
);
}
}
class _CollectionSectionedContent extends StatefulWidget {

View file

@ -6,10 +6,9 @@ import 'package:aves/model/source/enums/enums.dart';
import 'package:aves/services/intent_service.dart';
import 'package:aves/widgets/collection/grid/list_details.dart';
import 'package:aves/widgets/collection/grid/list_details_theme.dart';
import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/grid/scaling.dart';
import 'package:aves/widgets/common/thumbnail/decorated.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/common/thumbnail/notifications.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -40,7 +39,7 @@ class InteractiveTile extends StatelessWidget {
if (selection.isSelecting) {
selection.toggleSelection(entry);
} else {
_goToViewer(context);
OpenViewerNotification(entry).dispatch(context);
}
break;
case AppMode.pickSingleMediaExternal:
@ -79,24 +78,6 @@ class InteractiveTile extends StatelessWidget {
),
);
}
void _goToViewer(BuildContext context) {
Navigator.maybeOf(context)?.push(
TransparentMaterialPageRoute(
settings: const RouteSettings(name: EntryViewerPage.routeName),
pageBuilder: (context, a, sa) {
final viewerCollection = collection.copyWith(
listenToSource: false,
);
assert(viewerCollection.sortedEntries.map((entry) => entry.id).contains(entry.id));
return EntryViewerPage(
collection: viewerCollection,
initialEntry: entry,
);
},
),
);
}
}
class Tile extends StatelessWidget {

View file

@ -22,28 +22,13 @@ class GridItemSelectionOverlay<T> extends StatelessWidget {
@override
Widget build(BuildContext context) {
final isSelecting = context.select<Selection<T>, bool>((selection) => selection.isSelecting);
final child = isSelecting
return AnimatedSwitcher(
duration: duration,
child: isSelecting
? Selector<Selection<T>, bool>(
selector: (context, selection) => selection.isSelected([item]),
builder: (context, isSelected, child) {
var child = isSelecting
? OverlayIcon(
key: ValueKey(isSelected),
icon: isSelected ? AIcons.selected : AIcons.unselected,
margin: EdgeInsets.zero,
)
: const SizedBox();
child = AnimatedSwitcher(
duration: duration,
switchInCurve: Curves.easeOutBack,
switchOutCurve: Curves.easeOutBack,
transitionBuilder: (child, animation) => ScaleTransition(
scale: animation,
child: child,
),
child: child,
);
child = AnimatedContainer(
return AnimatedContainer(
alignment: AlignmentDirectional.topEnd,
padding: padding,
decoration: BoxDecoration(
@ -51,15 +36,24 @@ class GridItemSelectionOverlay<T> extends StatelessWidget {
borderRadius: borderRadius,
),
duration: duration,
child: AnimatedSwitcher(
duration: duration,
switchInCurve: Curves.easeOutBack,
switchOutCurve: Curves.easeOutBack,
transitionBuilder: (child, animation) => ScaleTransition(
scale: animation,
child: child,
),
child: OverlayIcon(
key: ValueKey(isSelected),
icon: isSelected ? AIcons.selected : AIcons.unselected,
margin: EdgeInsets.zero,
),
),
);
return child;
},
)
: const SizedBox();
return AnimatedSwitcher(
duration: duration,
child: child,
: const SizedBox(),
);
}
}

View file

@ -30,10 +30,12 @@ class GridTheme extends StatelessWidget {
final fontSize = (iconSize * .7).floorToDouble();
iconSize *= mq.textScaleFactor;
final highlightBorderWidth = extent * .1;
final interactiveDimension = min(iconSize * 2, kMinInteractiveDimension);
return GridThemeData(
iconSize: iconSize,
fontSize: fontSize,
highlightBorderWidth: highlightBorderWidth,
interactiveDimension: interactiveDimension,
showFavourite: settings.showThumbnailFavourite,
locationIcon: showLocation ? settings.thumbnailLocationIcon : ThumbnailOverlayLocationIcon.none,
tagIcon: settings.thumbnailTagIcon,
@ -52,7 +54,7 @@ class GridTheme extends StatelessWidget {
typedef GridThemeIconBuilder = List<Widget> Function(BuildContext context, AvesEntry entry);
class GridThemeData {
final double iconSize, fontSize, highlightBorderWidth;
final double iconSize, fontSize, highlightBorderWidth, interactiveDimension;
final bool showFavourite, showMotionPhoto, showRating, showRaw, showTrash, showVideoDuration;
final bool showLocated, showUnlocated, showTagged, showUntagged;
late final GridThemeIconBuilder iconBuilder;
@ -61,6 +63,7 @@ class GridThemeData {
required this.iconSize,
required this.fontSize,
required this.highlightBorderWidth,
required this.interactiveDimension,
required this.showFavourite,
required ThumbnailOverlayLocationIcon locationIcon,
required ThumbnailOverlayTagIcon tagIcon,

View file

@ -0,0 +1,9 @@
import 'package:aves/model/entry.dart';
import 'package:flutter/widgets.dart';
@immutable
class OpenViewerNotification extends Notification {
final AvesEntry entry;
const OpenViewerNotification(this.entry);
}

View file

@ -2,6 +2,9 @@ import 'dart:math';
import 'package:aves/model/entry.dart';
import 'package:aves/model/highlight.dart';
import 'package:aves/model/selection.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/grid/theme.dart';
import 'package:flutter/material.dart';
@ -70,3 +73,43 @@ class _ThumbnailHighlightOverlayState extends State<ThumbnailHighlightOverlay> {
);
}
}
class ThumbnailZoomOverlay extends StatelessWidget {
final VoidCallback? onZoom;
const ThumbnailZoomOverlay({
super.key,
this.onZoom,
});
static const alignment = AlignmentDirectional.bottomEnd;
static const duration = Durations.thumbnailOverlayAnimation;
@override
Widget build(BuildContext context) {
final isSelecting = context.select<Selection<AvesEntry>, bool>((selection) => selection.isSelecting);
final interactiveDimension = context.select<GridThemeData, double>((t) => t.interactiveDimension);
return AnimatedSwitcher(
duration: duration,
child: isSelecting
? Align(
alignment: alignment,
child: GestureDetector(
onTap: onZoom,
child: Container(
alignment: alignment,
padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 2),
width: interactiveDimension,
height: interactiveDimension,
child: Icon(
AIcons.showFullscreen,
size: context.select<GridThemeData, double>((t) => t.iconSize),
color: Colors.white70,
),
),
),
)
: const SizedBox(),
);
}
}

View file

@ -1,5 +1,5 @@
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

View file

@ -10,7 +10,7 @@ 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/constants.dart';
import 'package:aves/widgets/common/basic/color_list_tile.dart';
import 'package:aves/widgets/common/basic/list_tiles/color.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/borders.dart';
import 'package:aves/widgets/dialogs/aves_dialog.dart';

View file

@ -2,8 +2,8 @@ import 'package:aves/image_providers/app_icon_image_provider.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart';
import 'package:aves/widgets/common/basic/query_bar.dart';
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:collection/collection.dart';

View file

@ -2,8 +2,8 @@ import 'dart:collection';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/basic/list_tiles/reselectable_radio.dart';
import 'package:aves/widgets/common/basic/query_bar.dart';
import 'package:aves/widgets/common/basic/reselectable_radio_list_tile.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/language/locale_tile.dart';

View file

@ -1,9 +1,9 @@
import 'package:aves/model/settings/enums/enums.dart';
import 'package:aves/model/settings/enums/subtitle_position.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/basic/color_list_tile.dart';
import 'package:aves/widgets/common/basic/list_tiles/color.dart';
import 'package:aves/widgets/common/basic/list_tiles/slider.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/basic/slider_list_tile.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/settings/common/tiles.dart';
import 'package:aves/widgets/settings/video/subtitle_sample.dart';

View file

@ -3,10 +3,8 @@ import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/overlay/bottom.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
import 'package:aves/widgets/viewer/visual/conductor.dart';
import 'package:aves/widgets/viewer/providers.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@ -43,18 +41,18 @@ class _EntryViewerPageState extends State<EntryViewerPage> {
Widget build(BuildContext context) {
final collection = widget.collection;
return AvesScaffold(
body: ViewStateConductorProvider(
child: VideoConductorProvider(
collection: collection,
child: MultiPageConductorProvider(
body: MultiProvider(
providers: [
ViewStateConductorProvider(),
VideoConductorProvider(collection: collection),
MultiPageConductorProvider(),
],
child: EntryViewerStack(
collection: collection,
initialEntry: widget.initialEntry,
viewerController: _viewerController,
),
),
),
),
backgroundColor: Navigator.canPop(context)
? Colors.transparent
: Theme.of(context).brightness == Brightness.dark
@ -64,63 +62,3 @@ class _EntryViewerPageState extends State<EntryViewerPage> {
);
}
}
class ViewStateConductorProvider extends StatelessWidget {
final Widget? child;
const ViewStateConductorProvider({
super.key,
this.child,
});
@override
Widget build(BuildContext context) {
return ProxyProvider<MediaQueryData, ViewStateConductor>(
create: (context) => ViewStateConductor(),
update: (context, mq, value) {
value!.viewportSize = mq.size;
return value;
},
dispose: (context, value) => value.dispose(),
child: child,
);
}
}
class VideoConductorProvider extends StatelessWidget {
final CollectionLens? collection;
final Widget? child;
const VideoConductorProvider({
super.key,
this.collection,
required this.child,
});
@override
Widget build(BuildContext context) {
return Provider<VideoConductor>(
create: (context) => VideoConductor(collection: collection),
dispose: (context, value) => value.dispose(),
child: child,
);
}
}
class MultiPageConductorProvider extends StatelessWidget {
final Widget? child;
const MultiPageConductorProvider({
super.key,
required this.child,
});
@override
Widget build(BuildContext context) {
return Provider<MultiPageConductor>(
create: (context) => MultiPageConductor(),
dispose: (context, value) => value.dispose(),
child: child,
);
}
}

View file

@ -0,0 +1,41 @@
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
import 'package:aves/widgets/viewer/visual/conductor.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class ViewStateConductorProvider extends ProxyProvider<MediaQueryData, ViewStateConductor> {
ViewStateConductorProvider({
super.key,
super.child,
}) : super(
create: (context) => ViewStateConductor(),
update: (context, mq, value) {
value!.viewportSize = mq.size;
return value;
},
dispose: (context, value) => value.dispose(),
);
}
class VideoConductorProvider extends Provider<VideoConductor> {
VideoConductorProvider({
super.key,
CollectionLens? collection,
super.child,
}) : super(
create: (context) => VideoConductor(collection: collection),
dispose: (context, value) => value.dispose(),
);
}
class MultiPageConductorProvider extends Provider<MultiPageConductor> {
MultiPageConductorProvider({
super.key,
super.child,
}) : super(
create: (context) => MultiPageConductor(),
dispose: (context, value) => value.dispose(),
);
}

View file

@ -8,10 +8,11 @@ import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
import 'package:aves/widgets/viewer/providers.dart';
import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ScreenSaverPage extends StatefulWidget {
static const routeName = '/screen_saver';
@ -80,16 +81,17 @@ class _ScreenSaverPageState extends State<ScreenSaverPage> with WidgetsBindingOb
alignment: Alignment.center,
);
} else {
child = ViewStateConductorProvider(
child: VideoConductorProvider(
child: MultiPageConductorProvider(
child = MultiProvider(
providers: [
ViewStateConductorProvider(),
VideoConductorProvider(),
MultiPageConductorProvider(),
],
child: EntryViewerStack(
collection: collection,
initialEntry: entries.first,
viewerController: _viewerController,
),
),
),
);
}
}

View file

@ -14,8 +14,8 @@ import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/settings/viewer/slideshow.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/entry_viewer_stack.dart';
import 'package:aves/widgets/viewer/providers.dart';
import 'package:aves_magnifier/aves_magnifier.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
@ -69,9 +69,12 @@ class _SlideshowPageState extends State<SlideshowPage> {
text: context.l10n.collectionEmptyImages,
alignment: Alignment.center,
)
: ViewStateConductorProvider(
child: VideoConductorProvider(
child: MultiPageConductorProvider(
: MultiProvider(
providers: [
ViewStateConductorProvider(),
VideoConductorProvider(),
MultiPageConductorProvider(),
],
child: NotificationListener<SlideshowActionNotification>(
onNotification: (notification) {
_onActionSelected(notification.action);
@ -86,8 +89,6 @@ class _SlideshowPageState extends State<SlideshowPage> {
),
),
),
),
),
);
}

View file

@ -8,12 +8,12 @@ import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/viewer/controller.dart';
import 'package:aves/widgets/viewer/entry_horizontal_pager.dart';
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/notifications.dart';
import 'package:aves/widgets/viewer/overlay/bottom.dart';
import 'package:aves/widgets/viewer/overlay/video/video.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:aves/widgets/viewer/providers.dart';
import 'package:aves/widgets/viewer/video/conductor.dart';
import 'package:aves/widgets/viewer/video/controller.dart';
import 'package:aves/widgets/viewer/video_action_delegate.dart';
@ -37,14 +37,15 @@ class WallpaperPage extends StatelessWidget {
Widget build(BuildContext context) {
return AvesScaffold(
body: entry != null
? ViewStateConductorProvider(
child: VideoConductorProvider(
child: MultiPageConductorProvider(
? MultiProvider(
providers: [
ViewStateConductorProvider(),
VideoConductorProvider(),
MultiPageConductorProvider(),
],
child: EntryEditor(
entry: entry!,
),
),
),
)
: const SizedBox(),
backgroundColor: Theme.of(context).brightness == Brightness.dark ? Colors.black : Colors.white,