map browse prep
This commit is contained in:
parent
c0af01578a
commit
fd33904658
19 changed files with 253 additions and 150 deletions
|
@ -43,8 +43,8 @@ class Durations {
|
|||
static const viewerVerticalPageScrollAnimation = Duration(milliseconds: 500);
|
||||
static const viewerOverlayAnimation = Duration(milliseconds: 200);
|
||||
static const viewerOverlayChangeAnimation = Duration(milliseconds: 150);
|
||||
static const viewerOverlayPageScrollAnimation = Duration(milliseconds: 200);
|
||||
static const viewerOverlayPageShadeAnimation = Duration(milliseconds: 150);
|
||||
static const thumbnailScrollerScrollAnimation = Duration(milliseconds: 200);
|
||||
static const thumbnailScrollerShadeAnimation = Duration(milliseconds: 150);
|
||||
static const viewerVideoPlayerTransition = Duration(milliseconds: 500);
|
||||
|
||||
// info animations
|
||||
|
|
|
@ -13,7 +13,6 @@ import 'package:aves/widgets/collection/app_bar.dart';
|
|||
import 'package:aves/widgets/collection/draggable_thumb_label.dart';
|
||||
import 'package:aves/widgets/collection/grid/section_layout.dart';
|
||||
import 'package:aves/widgets/collection/grid/thumbnail.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/decorated.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';
|
||||
|
@ -27,6 +26,7 @@ import 'package:aves/widgets/common/identity/empty.dart';
|
|||
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/scaling.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/common/tile_extent_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||
|
|
|
@ -3,9 +3,9 @@ import 'package:aves/model/entry.dart';
|
|||
import 'package:aves/model/selection.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/services/viewer_service.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/common/behaviour/routes.dart';
|
||||
import 'package:aves/widgets/common/scaling.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/viewer/entry_viewer_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
|
|
@ -117,7 +117,7 @@ class MultiPageIcon extends StatelessWidget {
|
|||
if (entry.isMotionPhoto) {
|
||||
icon = AIcons.motionPhoto;
|
||||
} else {
|
||||
if(entry.isBurst) {
|
||||
if (entry.isBurst) {
|
||||
text = '${entry.burstEntries?.length}';
|
||||
}
|
||||
icon = AIcons.multiPage;
|
||||
|
|
|
@ -27,6 +27,7 @@ class GeoMap extends StatefulWidget {
|
|||
final double? mapHeight;
|
||||
final ValueNotifier<bool> isAnimatingNotifier;
|
||||
final UserZoomChangeCallback? onUserZoomChange;
|
||||
final GeoEntryTapCallback? onEntryTap;
|
||||
|
||||
static const markerImageExtent = 48.0;
|
||||
static const pointerSize = Size(8, 6);
|
||||
|
@ -38,6 +39,7 @@ class GeoMap extends StatefulWidget {
|
|||
this.mapHeight,
|
||||
required this.isAnimatingNotifier,
|
||||
this.onUserZoomChange,
|
||||
this.onEntryTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -130,6 +132,7 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
|||
markerCluster: markerCluster,
|
||||
markerEntries: entries,
|
||||
onUserZoomChange: widget.onUserZoomChange,
|
||||
onEntryTap: widget.onEntryTap,
|
||||
)
|
||||
: EntryLeafletMap(
|
||||
boundsNotifier: boundsNotifier,
|
||||
|
@ -143,6 +146,7 @@ class _GeoMapState extends State<GeoMap> with TickerProviderStateMixin {
|
|||
GeoMap.markerImageExtent + ImageMarker.outerBorderWidth * 2 + GeoMap.pointerSize.height,
|
||||
),
|
||||
onUserZoomChange: widget.onUserZoomChange,
|
||||
onEntryTap: widget.onEntryTap,
|
||||
);
|
||||
|
||||
child = Column(
|
||||
|
@ -214,3 +218,4 @@ class MarkerKey extends LocalKey with EquatableMixin {
|
|||
|
||||
typedef EntryMarkerBuilder = Widget Function(MarkerKey key);
|
||||
typedef UserZoomChangeCallback = void Function(double zoom);
|
||||
typedef GeoEntryTapCallback = void Function(List<GeoEntry> geoEntries);
|
||||
|
|
|
@ -25,6 +25,7 @@ class EntryGoogleMap extends StatefulWidget {
|
|||
final Fluster<GeoEntry> markerCluster;
|
||||
final List<AvesEntry> markerEntries;
|
||||
final UserZoomChangeCallback? onUserZoomChange;
|
||||
final GeoEntryTapCallback? onEntryTap;
|
||||
|
||||
const EntryGoogleMap({
|
||||
Key? key,
|
||||
|
@ -35,6 +36,7 @@ class EntryGoogleMap extends StatefulWidget {
|
|||
required this.markerCluster,
|
||||
required this.markerEntries,
|
||||
this.onUserZoomChange,
|
||||
this.onEntryTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -93,8 +95,8 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
|||
valueListenable: boundsNotifier,
|
||||
builder: (context, visibleRegion, child) {
|
||||
final allEntries = widget.markerEntries;
|
||||
final clusters = visibleRegion != null ? widget.markerCluster.clusters(visibleRegion.boundingBox, visibleRegion.zoom.round()) : <GeoEntry>[];
|
||||
final clusterByMarkerKey = Map.fromEntries(clusters.map((v) {
|
||||
final geoEntries = visibleRegion != null ? widget.markerCluster.clusters(visibleRegion.boundingBox, visibleRegion.zoom.round()) : <GeoEntry>[];
|
||||
final geoEntryByMarkerKey = Map.fromEntries(geoEntries.map((v) {
|
||||
if (v.isCluster!) {
|
||||
final uri = v.childMarkerId;
|
||||
final entry = allEntries.firstWhere((v) => v.uri == uri);
|
||||
|
@ -106,7 +108,7 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
|||
return Stack(
|
||||
children: [
|
||||
MarkerGeneratorWidget<MarkerKey>(
|
||||
markers: clusterByMarkerKey.keys.map(widget.markerBuilder).toList(),
|
||||
markers: geoEntryByMarkerKey.keys.map(widget.markerBuilder).toList(),
|
||||
isReadyToRender: (key) => key.entry.isThumbnailReady(extent: GeoMap.markerImageExtent),
|
||||
onRendered: (key, bitmap) {
|
||||
_markerBitmaps[key] = bitmap;
|
||||
|
@ -115,7 +117,7 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
|||
),
|
||||
MapDecorator(
|
||||
interactive: widget.interactive,
|
||||
child: _buildMap(clusterByMarkerKey),
|
||||
child: _buildMap(geoEntryByMarkerKey),
|
||||
),
|
||||
MapButtonPanel(
|
||||
latLng: bounds.center,
|
||||
|
@ -127,19 +129,26 @@ class _EntryGoogleMapState extends State<EntryGoogleMap> with WidgetsBindingObse
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildMap(Map<MarkerKey, GeoEntry> clusterByMarkerKey) {
|
||||
Widget _buildMap(Map<MarkerKey, GeoEntry> geoEntryByMarkerKey) {
|
||||
return AnimatedBuilder(
|
||||
animation: _markerBitmapChangeNotifier,
|
||||
builder: (context, child) {
|
||||
final markers = <Marker>{};
|
||||
clusterByMarkerKey.forEach((markerKey, cluster) {
|
||||
final onTap = widget.onEntryTap;
|
||||
geoEntryByMarkerKey.forEach((markerKey, geoEntry) {
|
||||
final bytes = _markerBitmaps[markerKey];
|
||||
if (bytes != null) {
|
||||
final latLng = LatLng(cluster.latitude!, cluster.longitude!);
|
||||
final latLng = LatLng(geoEntry.latitude!, geoEntry.longitude!);
|
||||
markers.add(Marker(
|
||||
markerId: MarkerId(cluster.markerId!),
|
||||
markerId: MarkerId(geoEntry.markerId!),
|
||||
icon: BitmapDescriptor.fromBytes(bytes),
|
||||
position: latLng,
|
||||
onTap: onTap != null
|
||||
? () {
|
||||
final clusterId = geoEntry.clusterId;
|
||||
onTap(clusterId != null ? widget.markerCluster.points(clusterId) : [geoEntry]);
|
||||
}
|
||||
: null,
|
||||
));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ class EntryLeafletMap extends StatefulWidget {
|
|||
final List<AvesEntry> markerEntries;
|
||||
final Size markerSize;
|
||||
final UserZoomChangeCallback? onUserZoomChange;
|
||||
final GeoEntryTapCallback? onEntryTap;
|
||||
|
||||
const EntryLeafletMap({
|
||||
Key? key,
|
||||
|
@ -35,6 +36,7 @@ class EntryLeafletMap extends StatefulWidget {
|
|||
required this.markerEntries,
|
||||
required this.markerSize,
|
||||
this.onUserZoomChange,
|
||||
this.onEntryTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -80,8 +82,8 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
|
|||
valueListenable: boundsNotifier,
|
||||
builder: (context, visibleRegion, child) {
|
||||
final allEntries = widget.markerEntries;
|
||||
final clusters = visibleRegion != null ? widget.markerCluster.clusters(visibleRegion.boundingBox, visibleRegion.zoom.round()) : <GeoEntry>[];
|
||||
final clusterByMarkerKey = Map.fromEntries(clusters.map((v) {
|
||||
final geoEntries = visibleRegion != null ? widget.markerCluster.clusters(visibleRegion.boundingBox, visibleRegion.zoom.round()) : <GeoEntry>[];
|
||||
final geoEntryByMarkerKey = Map.fromEntries(geoEntries.map((v) {
|
||||
if (v.isCluster!) {
|
||||
final uri = v.childMarkerId;
|
||||
final entry = allEntries.firstWhere((v) => v.uri == uri);
|
||||
|
@ -94,7 +96,7 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
|
|||
children: [
|
||||
MapDecorator(
|
||||
interactive: widget.interactive,
|
||||
child: _buildMap(clusterByMarkerKey),
|
||||
child: _buildMap(geoEntryByMarkerKey),
|
||||
),
|
||||
MapButtonPanel(
|
||||
latLng: bounds.center,
|
||||
|
@ -106,16 +108,20 @@ class _EntryLeafletMapState extends State<EntryLeafletMap> with TickerProviderSt
|
|||
);
|
||||
}
|
||||
|
||||
Widget _buildMap(Map<MarkerKey, GeoEntry> clusterByMarkerKey) {
|
||||
Widget _buildMap(Map<MarkerKey, GeoEntry> geoEntryByMarkerKey) {
|
||||
final markerSize = widget.markerSize;
|
||||
final markers = clusterByMarkerKey.entries.map((kv) {
|
||||
final markers = geoEntryByMarkerKey.entries.map((kv) {
|
||||
final markerKey = kv.key;
|
||||
final cluster = kv.value;
|
||||
final latLng = LatLng(cluster.latitude!, cluster.longitude!);
|
||||
final geoEntry = kv.value;
|
||||
final latLng = LatLng(geoEntry.latitude!, geoEntry.longitude!);
|
||||
return Marker(
|
||||
point: latLng,
|
||||
builder: (context) => GestureDetector(
|
||||
onTap: () => _moveTo(latLng),
|
||||
onTap: () {
|
||||
final clusterId = geoEntry.clusterId;
|
||||
widget.onEntryTap?.call(clusterId != null ? widget.markerCluster.points(clusterId) : [geoEntry]);
|
||||
_moveTo(latLng);
|
||||
},
|
||||
child: widget.markerBuilder(markerKey),
|
||||
),
|
||||
width: markerSize.width,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||
import 'package:custom_rounded_rectangle_border/custom_rounded_rectangle_border.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/overlay.dart';
|
||||
import 'package:aves/widgets/common/fx/borders.dart';
|
||||
import 'package:aves/widgets/common/grid/overlay.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DecoratedThumbnail extends StatelessWidget {
|
|
@ -8,11 +8,11 @@ import 'package:aves/model/settings/entry_background.dart';
|
|||
import 'package:aves/model/settings/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/error.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/fx/checkered_decoration.dart';
|
||||
import 'package:aves/widgets/common/fx/transition_image.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/error.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
152
lib/widgets/common/thumbnail/scroller.dart
Normal file
152
lib/widgets/common/thumbnail/scroller.dart
Normal file
|
@ -0,0 +1,152 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/decorated.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ThumbnailScroller extends StatefulWidget {
|
||||
final double availableWidth;
|
||||
final int entryCount;
|
||||
final AvesEntry? Function(int index) entryBuilder;
|
||||
final int? initialIndex;
|
||||
final bool Function(int page) isCurrentIndex;
|
||||
final void Function(int index) onIndexChange;
|
||||
|
||||
const ThumbnailScroller({
|
||||
Key? key,
|
||||
required this.availableWidth,
|
||||
required this.entryCount,
|
||||
required this.entryBuilder,
|
||||
required this.initialIndex,
|
||||
required this.isCurrentIndex,
|
||||
required this.onIndexChange,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ThumbnailScrollerState createState() => _ThumbnailScrollerState();
|
||||
}
|
||||
|
||||
class _ThumbnailScrollerState extends State<ThumbnailScroller> {
|
||||
final _cancellableNotifier = ValueNotifier(true);
|
||||
late ScrollController _scrollController;
|
||||
bool _syncScroll = true;
|
||||
|
||||
static const double extent = 48;
|
||||
static const double separatorWidth = 2;
|
||||
|
||||
int get entryCount => widget.entryCount;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerWidget();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ThumbnailScroller oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.initialIndex != widget.initialIndex) {
|
||||
_unregisterWidget();
|
||||
_registerWidget();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unregisterWidget();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget() {
|
||||
final scrollOffset = indexToScrollOffset(widget.initialIndex ?? 0);
|
||||
_scrollController = ScrollController(initialScrollOffset: scrollOffset);
|
||||
_scrollController.addListener(_onScrollChange);
|
||||
}
|
||||
|
||||
void _unregisterWidget() {
|
||||
_scrollController.removeListener(_onScrollChange);
|
||||
_scrollController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final marginWidth = max(0.0, (widget.availableWidth - extent) / 2 - separatorWidth);
|
||||
final horizontalMargin = SizedBox(width: marginWidth);
|
||||
const separator = SizedBox(width: separatorWidth);
|
||||
|
||||
return GridTheme(
|
||||
extent: extent,
|
||||
showLocation: false,
|
||||
child: SizedBox(
|
||||
height: extent,
|
||||
child: ListView.separated(
|
||||
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 == entryCount + 1) return horizontalMargin;
|
||||
final page = index - 1;
|
||||
final pageEntry = widget.entryBuilder(page);
|
||||
if (pageEntry == null) return const SizedBox();
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => _goTo(page),
|
||||
child: DecoratedThumbnail(
|
||||
entry: pageEntry,
|
||||
tileExtent: 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,
|
||||
hero: false,
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
child: AnimatedContainer(
|
||||
color: widget.isCurrentIndex(page) ? Colors.transparent : Colors.black45,
|
||||
width: extent,
|
||||
height: extent,
|
||||
duration: Durations.thumbnailScrollerShadeAnimation,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => separator,
|
||||
itemCount: entryCount + 2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _goTo(int index) async {
|
||||
_syncScroll = false;
|
||||
widget.onIndexChange(index);
|
||||
await _scrollController.animateTo(
|
||||
indexToScrollOffset(index),
|
||||
duration: Durations.thumbnailScrollerScrollAnimation,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
_syncScroll = true;
|
||||
}
|
||||
|
||||
void _onScrollChange() {
|
||||
if (_syncScroll) {
|
||||
widget.onIndexChange(scrollOffsetToIndex(_scrollController.offset));
|
||||
}
|
||||
}
|
||||
|
||||
double indexToScrollOffset(int index) => index * (extent + separatorWidth);
|
||||
|
||||
int scrollOffsetToIndex(double offset) => (offset / (extent + separatorWidth)).round();
|
||||
}
|
|
@ -2,9 +2,9 @@ import 'package:aves/model/covers.dart';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/dialogs/item_pick_dialog.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -13,8 +13,8 @@ import 'package:aves/theme/durations.dart';
|
|||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/utils/android_file_utils.dart';
|
||||
import 'package:aves/utils/constants.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/filter_grid_page.dart';
|
||||
import 'package:decorated_icon/decorated_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
|
@ -5,9 +5,11 @@ import 'package:aves/theme/durations.dart';
|
|||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||
import 'package:aves/widgets/common/map/geo_map.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/scroller.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MapPage extends StatefulWidget {
|
||||
static const routeName = '/collection/map';
|
||||
|
@ -25,6 +27,9 @@ class MapPage extends StatefulWidget {
|
|||
|
||||
class _MapPageState extends State<MapPage> {
|
||||
late final ValueNotifier<bool> _isAnimatingNotifier;
|
||||
int _selectedIndex = 0;
|
||||
|
||||
List<AvesEntry> get entries => widget.entries;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -48,10 +53,34 @@ class _MapPageState extends State<MapPage> {
|
|||
title: Text(context.l10n.mapPageTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
child: GeoMap(
|
||||
entries: widget.entries,
|
||||
interactive: true,
|
||||
isAnimatingNotifier: _isAnimatingNotifier,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: GeoMap(
|
||||
entries: entries,
|
||||
interactive: true,
|
||||
isAnimatingNotifier: _isAnimatingNotifier,
|
||||
onEntryTap: (geoEntries) {
|
||||
debugPrint('TLAD count=${geoEntries.length} entry=${geoEntries.first.entry}');
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
Selector<MediaQueryData, double>(
|
||||
selector: (c, mq) => mq.size.width,
|
||||
builder: (c, mqWidth, child) {
|
||||
return ThumbnailScroller(
|
||||
availableWidth: mqWidth,
|
||||
entryCount: entries.length,
|
||||
entryBuilder: (index) => entries[index],
|
||||
initialIndex: _selectedIndex,
|
||||
isCurrentIndex: (index) => _selectedIndex == index,
|
||||
onIndexChange: (index) => _selectedIndex = index,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -107,7 +107,7 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
|
|||
_lastDetails = snapshot.data;
|
||||
_lastEntry = entry;
|
||||
}
|
||||
if (_lastEntry == null) return const SizedBox.shrink();
|
||||
if (_lastEntry == null) return const SizedBox();
|
||||
final mainEntry = _lastEntry!;
|
||||
|
||||
Widget _buildContent({AvesEntry? pageEntry}) => _BottomOverlayContent(
|
||||
|
@ -261,7 +261,7 @@ class _BottomOverlayContent extends AnimatedWidget {
|
|||
padding: const EdgeInsets.only(top: _interRowPadding),
|
||||
child: _LocationRow(entry: pageEntry),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
: const SizedBox(),
|
||||
);
|
||||
|
||||
Widget _buildSoloShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher(
|
||||
|
@ -275,7 +275,7 @@ class _BottomOverlayContent extends AnimatedWidget {
|
|||
width: subRowWidth,
|
||||
child: _ShootingRow(details!),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
: const SizedBox(),
|
||||
);
|
||||
|
||||
Widget _buildDuoShootingRow(double subRowWidth, bool hasShootingDetails) => AnimatedSwitcher(
|
||||
|
@ -291,7 +291,7 @@ class _BottomOverlayContent extends AnimatedWidget {
|
|||
width: subRowWidth,
|
||||
child: _ShootingRow(details!),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
: const SizedBox(),
|
||||
);
|
||||
|
||||
static Widget _soloTransition(Widget child, Animation<double> animation) => FadeTransition(
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:aves/model/multipage.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/decorated.dart';
|
||||
import 'package:aves/widgets/common/grid/theme.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/scroller.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||
import 'package:aves/widgets/viewer/visual/conductor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
@ -25,18 +20,10 @@ class MultiPageOverlay extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
||||
final _cancellableNotifier = ValueNotifier(true);
|
||||
late ScrollController _scrollController;
|
||||
bool _syncScroll = true;
|
||||
int? _initControllerPage;
|
||||
|
||||
static const double extent = 48;
|
||||
static const double separatorWidth = 2;
|
||||
|
||||
MultiPageController get controller => widget.controller;
|
||||
|
||||
double get availableWidth => widget.availableWidth;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -48,23 +35,12 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
|||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.controller != controller) {
|
||||
_unregisterWidget();
|
||||
_registerWidget();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unregisterWidget();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget() {
|
||||
_initControllerPage = controller.page;
|
||||
final scrollOffset = pageToScrollOffset(_initControllerPage ?? 0);
|
||||
_scrollController = ScrollController(initialScrollOffset: scrollOffset);
|
||||
_scrollController.addListener(_onScrollChange);
|
||||
|
||||
if (_initControllerPage == null) {
|
||||
_correctDefaultPageScroll();
|
||||
}
|
||||
|
@ -75,79 +51,26 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
|||
void _correctDefaultPageScroll() async {
|
||||
await controller.infoStream.first;
|
||||
if (_initControllerPage == null) {
|
||||
_initControllerPage = controller.page;
|
||||
if (_initControllerPage != null && _initControllerPage != 0) {
|
||||
WidgetsBinding.instance!.addPostFrameCallback((_) => _goToPage(_initControllerPage!));
|
||||
}
|
||||
setState(() => _initControllerPage = controller.page);
|
||||
}
|
||||
}
|
||||
|
||||
void _unregisterWidget() {
|
||||
_scrollController.removeListener(_onScrollChange);
|
||||
_scrollController.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final marginWidth = max(0.0, (availableWidth - extent) / 2 - separatorWidth);
|
||||
final horizontalMargin = SizedBox(width: marginWidth);
|
||||
const separator = SizedBox(width: separatorWidth);
|
||||
|
||||
return GridTheme(
|
||||
extent: extent,
|
||||
showLocation: false,
|
||||
child: StreamBuilder<MultiPageInfo?>(
|
||||
stream: controller.infoStream,
|
||||
builder: (context, snapshot) {
|
||||
final multiPageInfo = controller.info;
|
||||
final pageCount = multiPageInfo?.pageCount ?? 0;
|
||||
return SizedBox(
|
||||
height: extent,
|
||||
child: ListView.separated(
|
||||
key: ValueKey(multiPageInfo),
|
||||
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 == pageCount + 1) return horizontalMargin;
|
||||
final page = index - 1;
|
||||
final pageEntry = multiPageInfo!.getPageEntryByIndex(page);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => _goToPage(page),
|
||||
child: DecoratedThumbnail(
|
||||
entry: pageEntry,
|
||||
tileExtent: 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,
|
||||
hero: false,
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
child: AnimatedContainer(
|
||||
color: controller.page == page ? Colors.transparent : Colors.black45,
|
||||
width: extent,
|
||||
height: extent,
|
||||
duration: Durations.viewerOverlayPageShadeAnimation,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => separator,
|
||||
itemCount: pageCount + 2,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
return StreamBuilder<MultiPageInfo?>(
|
||||
stream: controller.infoStream,
|
||||
builder: (context, snapshot) {
|
||||
final multiPageInfo = controller.info;
|
||||
return ThumbnailScroller(
|
||||
key: ValueKey(multiPageInfo),
|
||||
availableWidth: widget.availableWidth,
|
||||
entryCount: multiPageInfo?.pageCount ?? 0,
|
||||
entryBuilder: (page) => multiPageInfo?.getPageEntryByIndex(page),
|
||||
initialIndex: _initControllerPage,
|
||||
isCurrentIndex: (page) => controller.page == page,
|
||||
onIndexChange: _setPage,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -159,25 +82,4 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
|||
controller.page = newPage;
|
||||
context.read<ViewStateConductor>().reset(oldPageEntry);
|
||||
}
|
||||
|
||||
Future<void> _goToPage(int page) async {
|
||||
_syncScroll = false;
|
||||
_setPage(page);
|
||||
await _scrollController.animateTo(
|
||||
pageToScrollOffset(page),
|
||||
duration: Durations.viewerOverlayPageScrollAnimation,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
_syncScroll = true;
|
||||
}
|
||||
|
||||
void _onScrollChange() {
|
||||
if (_syncScroll) {
|
||||
_setPage(scrollOffsetToPage(_scrollController.offset));
|
||||
}
|
||||
}
|
||||
|
||||
double pageToScrollOffset(int page) => page * (extent + separatorWidth);
|
||||
|
||||
int scrollOffsetToPage(double offset) => (offset / (extent + separatorWidth)).round();
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ import 'dart:async';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/widgets/collection/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/common/magnifier/controller/controller.dart';
|
||||
import 'package:aves/widgets/common/magnifier/controller/state.dart';
|
||||
import 'package:aves/widgets/common/magnifier/magnifier.dart';
|
||||
import 'package:aves/widgets/common/magnifier/scale/scale_boundaries.dart';
|
||||
import 'package:aves/widgets/common/magnifier/scale/scale_level.dart';
|
||||
import 'package:aves/widgets/common/magnifier/scale/state.dart';
|
||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||
import 'package:aves/widgets/viewer/hero.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||
|
|
Loading…
Reference in a new issue