#6 burst support (samsung camera naming pattern)
This commit is contained in:
parent
4d7b9e9065
commit
6c2dc791d5
30 changed files with 504 additions and 310 deletions
|
@ -4,6 +4,7 @@ import 'package:aves/geo/countries.dart';
|
|||
import 'package:aves/model/entry_cache.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/metadata.dart';
|
||||
import 'package:aves/model/multipage.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/video/metadata.dart';
|
||||
import 'package:aves/ref/mime_types.dart';
|
||||
|
@ -39,6 +40,8 @@ class AvesEntry {
|
|||
CatalogMetadata? _catalogMetadata;
|
||||
AddressDetails? _addressDetails;
|
||||
|
||||
List<AvesEntry>? burstEntries;
|
||||
|
||||
final AChangeNotifier imageChangeNotifier = AChangeNotifier(), metadataChangeNotifier = AChangeNotifier(), addressChangeNotifier = AChangeNotifier();
|
||||
|
||||
// TODO TLAD make it dynamic if it depends on OS/lib versions
|
||||
|
@ -64,6 +67,7 @@ class AvesEntry {
|
|||
required int? dateModifiedSecs,
|
||||
required this.sourceDateTakenMillis,
|
||||
required int? durationMillis,
|
||||
this.burstEntries,
|
||||
}) {
|
||||
this.path = path;
|
||||
this.sourceTitle = sourceTitle;
|
||||
|
@ -80,6 +84,7 @@ class AvesEntry {
|
|||
String? path,
|
||||
int? contentId,
|
||||
int? dateModifiedSecs,
|
||||
List<AvesEntry>? burstEntries,
|
||||
}) {
|
||||
final copyContentId = contentId ?? this.contentId;
|
||||
final copied = AvesEntry(
|
||||
|
@ -96,6 +101,7 @@ class AvesEntry {
|
|||
dateModifiedSecs: dateModifiedSecs ?? this.dateModifiedSecs,
|
||||
sourceDateTakenMillis: sourceDateTakenMillis,
|
||||
durationMillis: durationMillis,
|
||||
burstEntries: burstEntries ?? this.burstEntries,
|
||||
)
|
||||
..catalogMetadata = _catalogMetadata?.copyWith(contentId: copyContentId)
|
||||
..addressDetails = _addressDetails?.copyWith(contentId: copyContentId);
|
||||
|
@ -228,10 +234,6 @@ class AvesEntry {
|
|||
|
||||
bool get is360 => _catalogMetadata?.is360 ?? false;
|
||||
|
||||
bool get isMultiPage => _catalogMetadata?.isMultiPage ?? false;
|
||||
|
||||
bool get isMotionPhoto => isMultiPage && mimeType == MimeTypes.jpeg;
|
||||
|
||||
bool get canEdit => path != null;
|
||||
|
||||
bool get canRotateAndFlip => canEdit && canEditExif;
|
||||
|
@ -652,6 +654,51 @@ class AvesEntry {
|
|||
}
|
||||
}
|
||||
|
||||
// multipage
|
||||
|
||||
static final _burstFilenamePattern = RegExp(r'^(\d{8}_\d{6})_(\d+)$');
|
||||
|
||||
bool get isMultiPage => (_catalogMetadata?.isMultiPage ?? false) || isBurst;
|
||||
|
||||
bool get isBurst => burstEntries?.isNotEmpty == true;
|
||||
|
||||
bool get isMotionPhoto => isMultiPage && !isBurst && mimeType == MimeTypes.jpeg;
|
||||
|
||||
String? get burstKey {
|
||||
if (filenameWithoutExtension != null) {
|
||||
final match = _burstFilenamePattern.firstMatch(filenameWithoutExtension!);
|
||||
if (match != null) {
|
||||
return '$directory/${match.group(1)}';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<MultiPageInfo?> getMultiPageInfo() async {
|
||||
if (isBurst) {
|
||||
return MultiPageInfo(
|
||||
mainEntry: this,
|
||||
pages: burstEntries!
|
||||
.mapIndexed((index, entry) => SinglePageInfo(
|
||||
index: index,
|
||||
pageId: entry.contentId!,
|
||||
isDefault: index == 0,
|
||||
uri: entry.uri,
|
||||
mimeType: entry.mimeType,
|
||||
width: entry.width,
|
||||
height: entry.height,
|
||||
rotationDegrees: entry.rotationDegrees,
|
||||
durationMillis: entry.durationMillis,
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
return await metadataService.getMultiPageInfo(this);
|
||||
}
|
||||
}
|
||||
|
||||
// sort
|
||||
|
||||
// compare by:
|
||||
// 1) title ascending
|
||||
// 2) extension ascending
|
||||
|
|
|
@ -22,6 +22,14 @@ class MultiPageInfo {
|
|||
final firstPage = _pages.removeAt(0);
|
||||
_pages.insert(0, firstPage.copyWith(isDefault: true));
|
||||
}
|
||||
|
||||
final burstEntries = mainEntry.burstEntries;
|
||||
if (burstEntries != null) {
|
||||
_pageEntries.addEntries(pages.map((pageInfo) {
|
||||
final pageEntry = burstEntries.firstWhere((entry) => entry.uri == pageInfo.uri);
|
||||
return MapEntry(pageInfo, pageEntry);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -212,9 +212,9 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
// collection
|
||||
|
||||
EntryGroupFactor get collectionGroupFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values);
|
||||
EntryGroupFactor get collectionSectionFactor => getEnumOrDefault(collectionGroupFactorKey, EntryGroupFactor.month, EntryGroupFactor.values);
|
||||
|
||||
set collectionGroupFactor(EntryGroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString());
|
||||
set collectionSectionFactor(EntryGroupFactor newValue) => setAndNotify(collectionGroupFactorKey, newValue.toString());
|
||||
|
||||
EntrySortFactor get collectionSortFactor => getEnumOrDefault(collectionSortFactorKey, EntrySortFactor.date, EntrySortFactor.values);
|
||||
|
||||
|
|
|
@ -13,6 +13,7 @@ import 'package:aves/model/source/location.dart';
|
|||
import 'package:aves/model/source/section_keys.dart';
|
||||
import 'package:aves/model/source/tag.dart';
|
||||
import 'package:aves/utils/change_notifier.dart';
|
||||
import 'package:aves/utils/collection_utils.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
|
@ -21,9 +22,9 @@ import 'enums.dart';
|
|||
class CollectionLens with ChangeNotifier {
|
||||
final CollectionSource source;
|
||||
final Set<CollectionFilter> filters;
|
||||
EntryGroupFactor groupFactor;
|
||||
EntryGroupFactor sectionFactor;
|
||||
EntrySortFactor sortFactor;
|
||||
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortGroupChangeNotifier = AChangeNotifier();
|
||||
final AChangeNotifier filterChangeNotifier = AChangeNotifier(), sortSectionChangeNotifier = AChangeNotifier();
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
int? id;
|
||||
bool listenToSource;
|
||||
|
@ -38,7 +39,7 @@ class CollectionLens with ChangeNotifier {
|
|||
this.id,
|
||||
this.listenToSource = true,
|
||||
}) : filters = (filters ?? {}).whereNotNull().toSet(),
|
||||
groupFactor = settings.collectionGroupFactor,
|
||||
sectionFactor = settings.collectionSectionFactor,
|
||||
sortFactor = settings.collectionSortFactor {
|
||||
id ??= hashCode;
|
||||
if (listenToSource) {
|
||||
|
@ -73,7 +74,7 @@ class CollectionLens with ChangeNotifier {
|
|||
|
||||
int get entryCount => _filteredSortedEntries.length;
|
||||
|
||||
// sorted as displayed to the user, i.e. sorted then grouped, not an absolute order on all entries
|
||||
// sorted as displayed to the user, i.e. sorted then sectioned, not an absolute order on all entries
|
||||
List<AvesEntry>? _sortedEntries;
|
||||
|
||||
List<AvesEntry> get sortedEntries {
|
||||
|
@ -84,9 +85,9 @@ class CollectionLens with ChangeNotifier {
|
|||
bool get showHeaders {
|
||||
if (sortFactor == EntrySortFactor.size) return false;
|
||||
|
||||
if (sortFactor == EntrySortFactor.date && groupFactor == EntryGroupFactor.none) return false;
|
||||
if (sortFactor == EntrySortFactor.date && sectionFactor == EntryGroupFactor.none) return false;
|
||||
|
||||
final albumSections = sortFactor == EntrySortFactor.name || (sortFactor == EntrySortFactor.date && groupFactor == EntryGroupFactor.album);
|
||||
final albumSections = sortFactor == EntrySortFactor.name || (sortFactor == EntrySortFactor.date && sectionFactor == EntryGroupFactor.album);
|
||||
final filterByAlbum = filters.any((f) => f is AlbumFilter);
|
||||
if (albumSections && filterByAlbum) return false;
|
||||
|
||||
|
@ -113,9 +114,33 @@ class CollectionLens with ChangeNotifier {
|
|||
filterChangeNotifier.notifyListeners();
|
||||
}
|
||||
|
||||
final bool groupBursts = true;
|
||||
|
||||
void _applyFilters() {
|
||||
final entries = source.visibleEntries;
|
||||
_filteredSortedEntries = List.of(filters.isEmpty ? entries : entries.where((entry) => filters.every((filter) => filter.test(entry))));
|
||||
|
||||
if (groupBursts) {
|
||||
_groupBursts();
|
||||
}
|
||||
}
|
||||
|
||||
void _groupBursts() {
|
||||
final byBurstKey = groupBy<AvesEntry, String?>(_filteredSortedEntries, (entry) => entry.burstKey).whereNotNullKey();
|
||||
byBurstKey.forEach((burstKey, entries) {
|
||||
if (entries.length > 1) {
|
||||
entries.sort(AvesEntry.compareByName);
|
||||
final mainEntry = entries.first;
|
||||
final burstEntry = mainEntry.copyWith(burstEntries: entries);
|
||||
|
||||
entries.skip(1).toList().forEach((subEntry) {
|
||||
_filteredSortedEntries.remove(subEntry);
|
||||
});
|
||||
final index = _filteredSortedEntries.indexOf(mainEntry);
|
||||
_filteredSortedEntries.removeAt(index);
|
||||
_filteredSortedEntries.insert(index, burstEntry);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _applySort() {
|
||||
|
@ -132,10 +157,10 @@ class CollectionLens with ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
void _applyGroup() {
|
||||
void _applySection() {
|
||||
switch (sortFactor) {
|
||||
case EntrySortFactor.date:
|
||||
switch (groupFactor) {
|
||||
switch (sectionFactor) {
|
||||
case EntryGroupFactor.album:
|
||||
sections = groupBy<AvesEntry, EntryAlbumSectionKey>(_filteredSortedEntries, (entry) => EntryAlbumSectionKey(entry.directory));
|
||||
break;
|
||||
|
@ -168,11 +193,11 @@ class CollectionLens with ChangeNotifier {
|
|||
}
|
||||
|
||||
// metadata change should also trigger a full refresh
|
||||
// as dates impact sorting and grouping
|
||||
// as dates impact sorting and sectioning
|
||||
void _refresh() {
|
||||
_applyFilters();
|
||||
_applySort();
|
||||
_applyGroup();
|
||||
_applySection();
|
||||
}
|
||||
|
||||
void _onFavouritesChanged() {
|
||||
|
@ -183,21 +208,21 @@ class CollectionLens with ChangeNotifier {
|
|||
|
||||
void _onSettingsChanged() {
|
||||
final newSortFactor = settings.collectionSortFactor;
|
||||
final newGroupFactor = settings.collectionGroupFactor;
|
||||
final newSectionFactor = settings.collectionSectionFactor;
|
||||
|
||||
final needSort = sortFactor != newSortFactor;
|
||||
final needGroup = needSort || groupFactor != newGroupFactor;
|
||||
final needSection = needSort || sectionFactor != newSectionFactor;
|
||||
|
||||
if (needSort) {
|
||||
sortFactor = newSortFactor;
|
||||
_applySort();
|
||||
}
|
||||
if (needGroup) {
|
||||
groupFactor = newGroupFactor;
|
||||
_applyGroup();
|
||||
if (needSection) {
|
||||
sectionFactor = newSectionFactor;
|
||||
_applySection();
|
||||
}
|
||||
if (needSort || needGroup) {
|
||||
sortGroupChangeNotifier.notifyListeners();
|
||||
if (needSort || needSection) {
|
||||
sortSectionChangeNotifier.notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,8 +231,27 @@ class CollectionLens with ChangeNotifier {
|
|||
}
|
||||
|
||||
void onEntryRemoved(Set<AvesEntry> entries) {
|
||||
if (groupBursts) {
|
||||
// find impacted burst groups
|
||||
final obsoleteBurstEntries = <AvesEntry>{};
|
||||
final burstKeys = entries.map((entry) => entry.burstKey).whereNotNull().toSet();
|
||||
if (burstKeys.isNotEmpty) {
|
||||
_filteredSortedEntries.where((entry) => entry.isBurst && burstKeys.contains(entry.burstKey)).forEach((mainEntry) {
|
||||
final subEntries = mainEntry.burstEntries!;
|
||||
// remove the deleted sub-entries
|
||||
subEntries.removeWhere(entries.contains);
|
||||
if (subEntries.isEmpty) {
|
||||
// remove the burst entry itself
|
||||
obsoleteBurstEntries.add(mainEntry);
|
||||
}
|
||||
// TODO TLAD [burst] recreate the burst main entry if the first sub-entry got deleted
|
||||
});
|
||||
entries.addAll(obsoleteBurstEntries);
|
||||
}
|
||||
}
|
||||
|
||||
// we should remove obsolete entries and sections
|
||||
// but do not apply sort/group
|
||||
// but do not apply sort/section
|
||||
// as section order change would surprise the user while browsing
|
||||
_filteredSortedEntries.removeWhere(entries.contains);
|
||||
_sortedEntries?.removeWhere(entries.contains);
|
||||
|
|
|
@ -105,7 +105,7 @@ class PlatformMetadataService implements MetadataService {
|
|||
'uri': entry.uri,
|
||||
'sizeBytes': entry.sizeBytes,
|
||||
});
|
||||
final pageMaps = (result as List).cast<Map>();
|
||||
final pageMaps = ((result as List?) ?? []).cast<Map>();
|
||||
if (entry.isMotionPhoto && pageMaps.isNotEmpty) {
|
||||
final imagePage = pageMaps[0];
|
||||
imagePage['width'] = entry.width;
|
||||
|
|
|
@ -302,7 +302,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
final value = await showDialog<EntryGroupFactor>(
|
||||
context: context,
|
||||
builder: (context) => AvesSelectionDialog<EntryGroupFactor>(
|
||||
initialValue: settings.collectionGroupFactor,
|
||||
initialValue: settings.collectionSectionFactor,
|
||||
options: {
|
||||
EntryGroupFactor.album: context.l10n.collectionGroupAlbum,
|
||||
EntryGroupFactor.month: context.l10n.collectionGroupMonth,
|
||||
|
@ -315,7 +315,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
|||
// wait for the dialog to hide as applying the change may block the UI
|
||||
await Future.delayed(Durations.dialogTransitionAnimation * timeDilation);
|
||||
if (value != null) {
|
||||
settings.collectionGroupFactor = value;
|
||||
settings.collectionSectionFactor = value;
|
||||
}
|
||||
break;
|
||||
case CollectionAction.sort:
|
||||
|
|
|
@ -267,13 +267,13 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> {
|
|||
|
||||
void _registerWidget(_CollectionScrollView widget) {
|
||||
widget.collection.filterChangeNotifier.addListener(_scrollToTop);
|
||||
widget.collection.sortGroupChangeNotifier.addListener(_scrollToTop);
|
||||
widget.collection.sortSectionChangeNotifier.addListener(_scrollToTop);
|
||||
widget.scrollController.addListener(_onScrollChange);
|
||||
}
|
||||
|
||||
void _unregisterWidget(_CollectionScrollView widget) {
|
||||
widget.collection.filterChangeNotifier.removeListener(_scrollToTop);
|
||||
widget.collection.sortGroupChangeNotifier.removeListener(_scrollToTop);
|
||||
widget.collection.sortSectionChangeNotifier.removeListener(_scrollToTop);
|
||||
widget.scrollController.removeListener(_onScrollChange);
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ class CollectionDraggableThumbLabel extends StatelessWidget {
|
|||
lineBuilder: (context, entry) {
|
||||
switch (collection.sortFactor) {
|
||||
case EntrySortFactor.date:
|
||||
switch (collection.groupFactor) {
|
||||
switch (collection.sectionFactor) {
|
||||
case EntryGroupFactor.album:
|
||||
return [
|
||||
DraggableThumbLabel.formatMonthThumbLabel(context, entry.bestDate),
|
||||
|
|
|
@ -8,6 +8,7 @@ import 'package:aves/model/filters/album.dart';
|
|||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/selection.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/services/android_app_service.dart';
|
||||
import 'package:aves/services/image_op_events.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
|
@ -33,10 +34,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
_showDeleteDialog(context);
|
||||
break;
|
||||
case EntryAction.share:
|
||||
final selection = context.read<Selection<AvesEntry>>().selection;
|
||||
AndroidAppService.shareEntries(selection).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
_share(context);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
|
@ -59,18 +57,31 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
}
|
||||
}
|
||||
|
||||
void _refreshMetadata(BuildContext context) {
|
||||
final collection = context.read<CollectionLens>();
|
||||
Set<AvesEntry> _getExpandedSelectedItems(Selection<AvesEntry> selection) {
|
||||
return selection.selection.expand((entry) => entry.burstEntries ?? {entry}).toSet();
|
||||
}
|
||||
|
||||
void _share(BuildContext context) {
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
collection.source.refreshMetadata(selection.selection);
|
||||
final selectedItems = _getExpandedSelectedItems(selection);
|
||||
AndroidAppService.shareEntries(selectedItems).then((success) {
|
||||
if (!success) showNoMatchingAppDialog(context);
|
||||
});
|
||||
}
|
||||
|
||||
void _refreshMetadata(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
final selectedItems = _getExpandedSelectedItems(selection);
|
||||
|
||||
source.refreshMetadata(selectedItems);
|
||||
selection.browse();
|
||||
}
|
||||
|
||||
Future<void> _moveSelection(BuildContext context, {required MoveType moveType}) async {
|
||||
final collection = context.read<CollectionLens>();
|
||||
final source = collection.source;
|
||||
final source = context.read<CollectionSource>();
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
final selectedItems = selection.selection;
|
||||
final selectedItems = _getExpandedSelectedItems(selection);
|
||||
|
||||
final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet();
|
||||
if (moveType == MoveType.move) {
|
||||
|
@ -144,6 +155,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
label: context.l10n.showButtonLabel,
|
||||
onPressed: () async {
|
||||
final highlightInfo = context.read<HighlightInfo>();
|
||||
final collection = context.read<CollectionLens>();
|
||||
var targetCollection = collection;
|
||||
if (collection.filters.any((f) => f is AlbumFilter)) {
|
||||
final filter = AlbumFilter(destinationAlbum, source.getAlbumDisplayName(context, destinationAlbum));
|
||||
|
@ -179,10 +191,9 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
}
|
||||
|
||||
Future<void> _showDeleteDialog(BuildContext context) async {
|
||||
final collection = context.read<CollectionLens>();
|
||||
final source = collection.source;
|
||||
final source = context.read<CollectionSource>();
|
||||
final selection = context.read<Selection<AvesEntry>>();
|
||||
final selectedItems = selection.selection;
|
||||
final selectedItems = _getExpandedSelectedItems(selection);
|
||||
final selectionDirs = selectedItems.map((e) => e.directory).whereNotNull().toSet();
|
||||
final todoCount = selectedItems.length;
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ class CollectionSectionHeader extends StatelessWidget {
|
|||
Widget? _buildHeader(BuildContext context) {
|
||||
switch (collection.sortFactor) {
|
||||
case EntrySortFactor.date:
|
||||
switch (collection.groupFactor) {
|
||||
switch (collection.sectionFactor) {
|
||||
case EntryGroupFactor.album:
|
||||
return _buildAlbumHeader(context);
|
||||
case EntryGroupFactor.month:
|
||||
|
|
|
@ -76,7 +76,7 @@ class InteractiveThumbnail extends StatelessWidget {
|
|||
id: collection.id,
|
||||
listenToSource: false,
|
||||
);
|
||||
assert(viewerCollection.sortedEntries.contains(entry));
|
||||
assert(viewerCollection.sortedEntries.map((e) => e.contentId).contains(entry.contentId));
|
||||
return EntryViewerPage(
|
||||
collection: viewerCollection,
|
||||
initialEntry: entry,
|
||||
|
|
|
@ -11,7 +11,7 @@ class DecoratedThumbnail extends StatelessWidget {
|
|||
final double tileExtent;
|
||||
final CollectionLens? collection;
|
||||
final ValueNotifier<bool>? cancellableNotifier;
|
||||
final bool selectable, highlightable;
|
||||
final bool selectable, highlightable, hero;
|
||||
|
||||
static final Color borderColor = Colors.grey.shade700;
|
||||
static final double borderWidth = AvesBorder.borderWidth;
|
||||
|
@ -24,6 +24,7 @@ class DecoratedThumbnail extends StatelessWidget {
|
|||
this.cancellableNotifier,
|
||||
this.selectable = true,
|
||||
this.highlightable = true,
|
||||
this.hero = true,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -33,7 +34,7 @@ class DecoratedThumbnail extends StatelessWidget {
|
|||
// hero tag should include a collection identifier, so that it animates
|
||||
// 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);
|
||||
final heroTag = hero ? hashValues(collection?.id, entry.uri) : null;
|
||||
final isSvg = entry.isSvg;
|
||||
Widget child = ThumbnailImage(
|
||||
entry: entry,
|
||||
|
|
|
@ -28,10 +28,10 @@ class ThumbnailEntryOverlay extends StatelessWidget {
|
|||
const AnimatedImageIcon()
|
||||
else ...[
|
||||
if (entry.isRaw && context.select<GridThemeData, bool>((t) => t.showRaw)) const RawIcon(),
|
||||
if (entry.isMultiPage) MultiPageIcon(entry: entry),
|
||||
if (entry.isGeotiff) const GeotiffIcon(),
|
||||
if (entry.is360) const SphericalImageIcon(),
|
||||
]
|
||||
],
|
||||
if (entry.isMultiPage) MultiPageIcon(entry: entry),
|
||||
];
|
||||
if (children.isEmpty) return const SizedBox.shrink();
|
||||
if (children.length == 1) return children.first;
|
||||
|
|
|
@ -112,10 +112,21 @@ class MultiPageIcon extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
IconData icon;
|
||||
String? text;
|
||||
if (entry.isMotionPhoto) {
|
||||
icon = AIcons.motionPhoto;
|
||||
} else {
|
||||
if(entry.isBurst) {
|
||||
text = '${entry.burstEntries?.length}';
|
||||
}
|
||||
icon = AIcons.multiPage;
|
||||
}
|
||||
return OverlayIcon(
|
||||
icon: entry.isMotionPhoto ? AIcons.motionPhoto : AIcons.multiPage,
|
||||
icon: icon,
|
||||
size: context.select<GridThemeData, double>((t) => t.iconSize),
|
||||
iconScale: .8,
|
||||
text: text,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,10 +28,9 @@ class StatsPage extends StatelessWidget {
|
|||
|
||||
final CollectionSource source;
|
||||
final CollectionLens? parentCollection;
|
||||
late final Set<AvesEntry> entries;
|
||||
final Map<String, int> entryCountPerCountry = {}, entryCountPerPlace = {}, entryCountPerTag = {};
|
||||
|
||||
Set<AvesEntry> get entries => parentCollection?.sortedEntries.toSet() ?? source.visibleEntries;
|
||||
|
||||
static const mimeDonutMinWidth = 124.0;
|
||||
|
||||
StatsPage({
|
||||
|
@ -39,6 +38,7 @@ class StatsPage extends StatelessWidget {
|
|||
required this.source,
|
||||
this.parentCollection,
|
||||
}) : super(key: key) {
|
||||
entries = parentCollection?.sortedEntries.expand((entry) => entry.burstEntries ?? {entry}).toSet() ?? source.visibleEntries;
|
||||
entries.forEach((entry) {
|
||||
if (entry.hasAddress) {
|
||||
final address = entry.addressDetails!;
|
||||
|
|
|
@ -182,7 +182,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
|
||||
final selection = <AvesEntry>{};
|
||||
if (entry.isMultiPage) {
|
||||
final multiPageInfo = await metadataService.getMultiPageInfo(entry);
|
||||
final multiPageInfo = await entry.getMultiPageInfo();
|
||||
if (multiPageInfo != null) {
|
||||
if (entry.isMotionPhoto) {
|
||||
await multiPageInfo.extractMotionPhotoVideo();
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/multipage.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart';
|
||||
import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||
import 'package:aves/widgets/viewer/visual/entry_page_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
@ -12,7 +12,7 @@ class MultiEntryScroller extends StatefulWidget {
|
|||
final CollectionLens collection;
|
||||
final PageController pageController;
|
||||
final ValueChanged<int> onPageChanged;
|
||||
final void Function(String uri) onViewDisposed;
|
||||
final void Function(AvesEntry mainEntry, AvesEntry? pageEntry) onViewDisposed;
|
||||
|
||||
const MultiEntryScroller({
|
||||
Key? key,
|
||||
|
@ -44,27 +44,14 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
|
|||
physics: const MagnifierScrollerPhysics(parent: BouncingScrollPhysics()),
|
||||
onPageChanged: widget.onPageChanged,
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
final mainEntry = entries[index];
|
||||
|
||||
Widget? child;
|
||||
if (entry.isMultiPage) {
|
||||
final multiPageController = context.read<MultiPageConductor>().getController(entry);
|
||||
if (multiPageController != null) {
|
||||
child = StreamBuilder<MultiPageInfo?>(
|
||||
stream: multiPageController.infoStream,
|
||||
builder: (context, snapshot) {
|
||||
final multiPageInfo = multiPageController.info;
|
||||
return ValueListenableBuilder<int?>(
|
||||
valueListenable: multiPageController.pageNotifier,
|
||||
builder: (context, page, child) {
|
||||
return _buildViewer(entry, pageEntry: multiPageInfo?.getPageEntryByIndex(page));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
child ??= _buildViewer(entry);
|
||||
var child = mainEntry.isMultiPage
|
||||
? PageEntryBuilder(
|
||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
||||
builder: (pageEntry) => _buildViewer(mainEntry, pageEntry: pageEntry),
|
||||
)
|
||||
: _buildViewer(mainEntry);
|
||||
|
||||
child = AnimatedBuilder(
|
||||
animation: pageController,
|
||||
|
@ -93,17 +80,11 @@ class _MultiEntryScrollerState extends State<MultiEntryScroller> with AutomaticK
|
|||
}
|
||||
|
||||
Widget _buildViewer(AvesEntry mainEntry, {AvesEntry? pageEntry}) {
|
||||
return Selector<MediaQueryData, Size>(
|
||||
selector: (c, mq) => mq.size,
|
||||
builder: (c, mqSize, child) {
|
||||
return EntryPageView(
|
||||
key: const Key('imageview'),
|
||||
mainEntry: mainEntry,
|
||||
pageEntry: pageEntry ?? mainEntry,
|
||||
viewportSize: mqSize,
|
||||
onDisposed: () => widget.onViewDisposed(mainEntry.uri),
|
||||
);
|
||||
},
|
||||
onDisposed: () => widget.onViewDisposed(mainEntry, pageEntry),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -130,25 +111,12 @@ class _SingleEntryScrollerState extends State<SingleEntryScroller> with Automati
|
|||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
Widget? child;
|
||||
if (mainEntry.isMultiPage) {
|
||||
final multiPageController = context.read<MultiPageConductor>().getController(mainEntry);
|
||||
if (multiPageController != null) {
|
||||
child = StreamBuilder<MultiPageInfo?>(
|
||||
stream: multiPageController.infoStream,
|
||||
builder: (context, snapshot) {
|
||||
final multiPageInfo = multiPageController.info;
|
||||
return ValueListenableBuilder<int?>(
|
||||
valueListenable: multiPageController.pageNotifier,
|
||||
builder: (context, page, child) {
|
||||
return _buildViewer(pageEntry: multiPageInfo?.getPageEntryByIndex(page));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
child ??= _buildViewer();
|
||||
var child = mainEntry.isMultiPage
|
||||
? PageEntryBuilder(
|
||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
||||
builder: (pageEntry) => _buildViewer(pageEntry: pageEntry),
|
||||
)
|
||||
: _buildViewer();
|
||||
|
||||
return MagnifierGestureDetectorScope(
|
||||
axis: const [Axis.vertical],
|
||||
|
@ -157,15 +125,9 @@ class _SingleEntryScrollerState extends State<SingleEntryScroller> with Automati
|
|||
}
|
||||
|
||||
Widget _buildViewer({AvesEntry? pageEntry}) {
|
||||
return Selector<MediaQueryData, Size>(
|
||||
selector: (c, mq) => mq.size,
|
||||
builder: (c, mqSize, child) {
|
||||
return EntryPageView(
|
||||
mainEntry: mainEntry,
|
||||
pageEntry: pageEntry ?? mainEntry,
|
||||
viewportSize: mqSize,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class ViewerVerticalPageView extends StatefulWidget {
|
|||
final PageController horizontalPager, verticalPager;
|
||||
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
|
||||
final VoidCallback onImagePageRequested;
|
||||
final void Function(String uri) onViewDisposed;
|
||||
final void Function(AvesEntry mainEntry, AvesEntry? pageEntry) onViewDisposed;
|
||||
|
||||
const ViewerVerticalPageView({
|
||||
Key? key,
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
|||
import 'package:aves/widgets/viewer/entry_viewer_stack.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/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
@ -23,21 +24,77 @@ class EntryViewerPage extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
return MediaQueryDataProvider(
|
||||
child: Scaffold(
|
||||
body: Provider<VideoConductor>(
|
||||
create: (context) => VideoConductor(),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
child: Provider<MultiPageConductor>(
|
||||
create: (context) => MultiPageConductor(),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
body: ViewStateConductorProvider(
|
||||
child: VideoConductorProvider(
|
||||
child: MultiPageConductorProvider(
|
||||
child: EntryViewerStack(
|
||||
collection: collection,
|
||||
initialEntry: initialEntry,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
backgroundColor: Navigator.canPop(context) ? Colors.transparent : Colors.black,
|
||||
resizeToAvoidBottomInset: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ViewStateConductorProvider extends StatelessWidget {
|
||||
final Widget? child;
|
||||
|
||||
const ViewStateConductorProvider({
|
||||
Key? key,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@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 Widget? child;
|
||||
|
||||
const VideoConductorProvider({
|
||||
Key? key,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Provider<VideoConductor>(
|
||||
create: (context) => VideoConductor(),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MultiPageConductorProvider extends StatelessWidget {
|
||||
final Widget? child;
|
||||
|
||||
const MultiPageConductorProvider({
|
||||
Key? key,
|
||||
this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Provider<MultiPageConductor>(
|
||||
create: (context) => MultiPageConductor(),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import 'dart:math';
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/highlight.dart';
|
||||
import 'package:aves/model/multipage.dart';
|
||||
import 'package:aves/model/settings/enums.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_lens.dart';
|
||||
|
@ -24,17 +23,17 @@ import 'package:aves/widgets/viewer/overlay/bottom/panorama.dart';
|
|||
import 'package:aves/widgets/viewer/overlay/bottom/video.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/top.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.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';
|
||||
import 'package:aves/widgets/viewer/visual/state.dart';
|
||||
import 'package:aves/widgets/viewer/visual/conductor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class EntryViewerStack extends StatefulWidget {
|
||||
final CollectionLens? collection;
|
||||
|
@ -62,7 +61,6 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
late Animation<Offset> _bottomOverlayOffset;
|
||||
EdgeInsets? _frozenViewInsets, _frozenViewPadding;
|
||||
late VideoActionDelegate _videoActionDelegate;
|
||||
final List<Tuple2<String, ValueNotifier<ViewState>>> _viewStateNotifiers = [];
|
||||
final Map<MultiPageController, Future<void> Function()> _multiPageControllerPageListeners = {};
|
||||
final ValueNotifier<HeroInfo?> _heroInfoNotifier = ValueNotifier(null);
|
||||
bool _isEntryTracked = true;
|
||||
|
@ -90,7 +88,11 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
}
|
||||
|
||||
// make sure initial entry is actually among the filtered collection entries
|
||||
final entry = entries.contains(widget.initialEntry) ? widget.initialEntry : entries.firstOrNull;
|
||||
// `initialEntry` may be a dynamic burst entry from another collection lens
|
||||
// so it is, strictly speaking, not contained in the lens used by the viewer,
|
||||
// but it can be found by content ID
|
||||
final initialEntry = widget.initialEntry;
|
||||
final entry = entries.firstWhereOrNull((v) => v.contentId == initialEntry.contentId) ?? entries.firstOrNull;
|
||||
// opening hero, with viewer as target
|
||||
_heroInfoNotifier.value = HeroInfo(collection?.id, entry);
|
||||
_entryNotifier.value = entry;
|
||||
|
@ -169,6 +171,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final viewStateConductor = context.read<ViewStateConductor>();
|
||||
return WillPopScope(
|
||||
onWillPop: () {
|
||||
if (_currentVerticalPage.value == infoPage) {
|
||||
|
@ -186,8 +189,6 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
onNotification: (dynamic notification) {
|
||||
if (notification is FilterSelectedNotification) {
|
||||
_goToCollection(notification.filter);
|
||||
} else if (notification is ViewStateNotification) {
|
||||
_updateViewState(notification.uri, notification.viewState);
|
||||
} else if (notification is EntryDeletedNotification) {
|
||||
_onEntryDeleted(context, notification.entry);
|
||||
}
|
||||
|
@ -208,7 +209,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
onVerticalPageChanged: _onVerticalPageChanged,
|
||||
onHorizontalPageChanged: _onHorizontalPageChanged,
|
||||
onImagePageRequested: () => _goToVerticalPage(imagePage),
|
||||
onViewDisposed: (uri) => _updateViewState(uri, null),
|
||||
onViewDisposed: (mainEntry, pageEntry) => viewStateConductor.reset(pageEntry ?? mainEntry),
|
||||
),
|
||||
_buildTopOverlay(),
|
||||
_buildBottomOverlay(),
|
||||
|
@ -221,23 +222,14 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
);
|
||||
}
|
||||
|
||||
void _updateViewState(String uri, ViewState? viewState) {
|
||||
final viewStateNotifier = _viewStateNotifiers.firstWhereOrNull((kv) => kv.item1 == uri)?.item2;
|
||||
viewStateNotifier?.value = viewState ?? ViewState.zero;
|
||||
}
|
||||
|
||||
Widget _buildTopOverlay() {
|
||||
Widget child = ValueListenableBuilder<AvesEntry?>(
|
||||
valueListenable: _entryNotifier,
|
||||
builder: (context, mainEntry, child) {
|
||||
if (mainEntry == null) return const SizedBox.shrink();
|
||||
|
||||
return NotificationListener<ShowInfoNotification>(
|
||||
onNotification: (notification) {
|
||||
_goToVerticalPage(infoPage);
|
||||
return true;
|
||||
},
|
||||
child: EmbeddedDataOpener(
|
||||
Widget _buildContent({AvesEntry? pageEntry}) {
|
||||
return EmbeddedDataOpener(
|
||||
entry: mainEntry,
|
||||
child: ViewerTopOverlay(
|
||||
mainEntry: mainEntry,
|
||||
|
@ -245,10 +237,22 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
canToggleFavourite: hasCollection,
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
viewStateNotifier: _viewStateNotifiers.firstWhereOrNull((kv) => kv.item1 == mainEntry.uri)?.item2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return NotificationListener<ShowInfoNotification>(
|
||||
onNotification: (notification) {
|
||||
_goToVerticalPage(infoPage);
|
||||
return true;
|
||||
},
|
||||
child: mainEntry.isMultiPage
|
||||
? PageEntryBuilder(
|
||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
||||
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
|
||||
)
|
||||
: _buildContent(),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
|
@ -282,14 +286,17 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
valueListenable: _entryNotifier,
|
||||
builder: (context, mainEntry, child) {
|
||||
if (mainEntry == null) return const SizedBox.shrink();
|
||||
final multiPageController = mainEntry.isMultiPage ? context.read<MultiPageConductor>().getController(mainEntry) : null;
|
||||
|
||||
Widget? _buildExtraBottomOverlay(AvesEntry pageEntry) {
|
||||
Widget? _buildExtraBottomOverlay({AvesEntry? pageEntry}) {
|
||||
final targetEntry = pageEntry ?? mainEntry;
|
||||
Widget? child;
|
||||
// a 360 video is both a video and a panorama but only the video controls are displayed
|
||||
if (pageEntry.isVideo) {
|
||||
return Selector<VideoConductor, AvesVideoController?>(
|
||||
selector: (context, vc) => vc.getController(pageEntry),
|
||||
if (targetEntry.isVideo) {
|
||||
child = Selector<VideoConductor, AvesVideoController?>(
|
||||
selector: (context, vc) => vc.getController(targetEntry),
|
||||
builder: (context, videoController, child) => VideoControlOverlay(
|
||||
entry: pageEntry,
|
||||
entry: targetEntry,
|
||||
controller: videoController,
|
||||
scale: _bottomOverlayScale,
|
||||
onActionSelected: (action) {
|
||||
|
@ -299,40 +306,31 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
},
|
||||
),
|
||||
);
|
||||
} else if (pageEntry.is360) {
|
||||
return PanoramaOverlay(
|
||||
entry: pageEntry,
|
||||
} else if (targetEntry.is360) {
|
||||
child = PanoramaOverlay(
|
||||
entry: targetEntry,
|
||||
scale: _bottomOverlayScale,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
return child != null
|
||||
? ExtraBottomOverlay(
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
child: child,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
final multiPageController = mainEntry.isMultiPage ? context.read<MultiPageConductor>().getController(mainEntry) : null;
|
||||
final extraBottomOverlay = multiPageController != null
|
||||
? StreamBuilder<MultiPageInfo?>(
|
||||
stream: multiPageController.infoStream,
|
||||
builder: (context, snapshot) {
|
||||
final multiPageInfo = multiPageController.info;
|
||||
if (multiPageInfo == null) return const SizedBox.shrink();
|
||||
return ValueListenableBuilder<int?>(
|
||||
valueListenable: multiPageController.pageNotifier,
|
||||
builder: (context, page, child) {
|
||||
final pageEntry = multiPageInfo.getPageEntryByIndex(page);
|
||||
return _buildExtraBottomOverlay(pageEntry) ?? const SizedBox();
|
||||
},
|
||||
);
|
||||
})
|
||||
: _buildExtraBottomOverlay(mainEntry);
|
||||
final extraBottomOverlay = mainEntry.isMultiPage
|
||||
? PageEntryBuilder(
|
||||
multiPageController: multiPageController,
|
||||
builder: (pageEntry) => _buildExtraBottomOverlay(pageEntry: pageEntry) ?? const SizedBox(),
|
||||
)
|
||||
: _buildExtraBottomOverlay();
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
if (extraBottomOverlay != null)
|
||||
ExtraBottomOverlay(
|
||||
viewInsets: _frozenViewInsets,
|
||||
viewPadding: _frozenViewPadding,
|
||||
child: extraBottomOverlay,
|
||||
),
|
||||
if (extraBottomOverlay != null) extraBottomOverlay,
|
||||
SlideTransition(
|
||||
position: _bottomOverlayOffset,
|
||||
child: ViewerBottomOverlay(
|
||||
|
@ -564,7 +562,6 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
Future<void> _initEntryControllers(AvesEntry? entry) async {
|
||||
if (entry == null) return;
|
||||
|
||||
_initViewStateController(entry);
|
||||
if (entry.isVideo) {
|
||||
await _initVideoController(entry);
|
||||
}
|
||||
|
@ -581,20 +578,6 @@ class _EntryViewerStackState extends State<EntryViewerStack> with FeedbackMixin,
|
|||
}
|
||||
}
|
||||
|
||||
void _initViewStateController(AvesEntry entry) {
|
||||
final uri = entry.uri;
|
||||
var controller = _viewStateNotifiers.firstWhereOrNull((kv) => kv.item1 == uri);
|
||||
if (controller != null) {
|
||||
_viewStateNotifiers.remove(controller);
|
||||
} else {
|
||||
controller = Tuple2(uri, ValueNotifier<ViewState>(ViewState.zero));
|
||||
}
|
||||
_viewStateNotifiers.insert(0, controller);
|
||||
while (_viewStateNotifiers.length > 3) {
|
||||
_viewStateNotifiers.removeLast().item2.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initVideoController(AvesEntry entry) async {
|
||||
final controller = context.read<VideoConductor>().getOrCreateController(entry);
|
||||
setState(() {});
|
||||
|
|
|
@ -10,6 +10,8 @@ import 'package:aves/widgets/viewer/info/info_app_bar.dart';
|
|||
import 'package:aves/widgets/viewer/info/location_section.dart';
|
||||
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
|
||||
import 'package:aves/widgets/viewer/info/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
|
@ -33,9 +35,7 @@ class _InfoPageState extends State<InfoPage> {
|
|||
final ScrollController _scrollController = ScrollController();
|
||||
bool _scrollStartFromTop = false;
|
||||
|
||||
CollectionLens? get collection => widget.collection;
|
||||
|
||||
AvesEntry? get entry => widget.entryNotifier.value;
|
||||
static const splitScreenWidthThreshold = 600;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -51,20 +51,31 @@ class _InfoPageState extends State<InfoPage> {
|
|||
builder: (c, mqWidth, child) {
|
||||
return ValueListenableBuilder<AvesEntry?>(
|
||||
valueListenable: widget.entryNotifier,
|
||||
builder: (context, entry, child) {
|
||||
return entry != null
|
||||
? EmbeddedDataOpener(
|
||||
entry: entry,
|
||||
builder: (context, mainEntry, child) {
|
||||
if (mainEntry != null) {
|
||||
Widget _buildContent({AvesEntry? pageEntry}) {
|
||||
final targetEntry = pageEntry ?? mainEntry;
|
||||
return EmbeddedDataOpener(
|
||||
entry: targetEntry,
|
||||
child: _InfoPageContent(
|
||||
collection: collection,
|
||||
entry: entry,
|
||||
collection: widget.collection,
|
||||
entry: targetEntry,
|
||||
isScrollingNotifier: widget.isScrollingNotifier,
|
||||
scrollController: _scrollController,
|
||||
split: mqWidth > 600,
|
||||
split: mqWidth > splitScreenWidthThreshold,
|
||||
goToViewer: _goToViewer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return mainEntry.isBurst
|
||||
? PageEntryBuilder(
|
||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
||||
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
: _buildContent();
|
||||
}
|
||||
return const SizedBox();
|
||||
},
|
||||
);
|
||||
},
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:async';
|
|||
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/multipage.dart';
|
||||
import 'package:aves/services/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
@ -24,9 +23,9 @@ class MultiPageController {
|
|||
set page(int? page) => pageNotifier.value = page;
|
||||
|
||||
MultiPageController(this.entry) {
|
||||
metadataService.getMultiPageInfo(entry).then((value) {
|
||||
entry.getMultiPageInfo().then((value) {
|
||||
if (value == null || _disposed) return;
|
||||
pageNotifier.value = value.defaultPage!.index;
|
||||
pageNotifier.value = value.defaultPage?.index ?? 0;
|
||||
_info = value;
|
||||
_infoStreamController.add(_info);
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import 'package:aves/widgets/common/fx/blurred.dart';
|
|||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/bottom/multipage.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||
import 'package:decorated_icon/decorated_icon.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -107,30 +108,23 @@ class _ViewerBottomOverlayState extends State<ViewerBottomOverlay> {
|
|||
_lastEntry = entry;
|
||||
}
|
||||
if (_lastEntry == null) return const SizedBox.shrink();
|
||||
final mainEntry = _lastEntry!;
|
||||
|
||||
Widget _buildContent({MultiPageInfo? multiPageInfo, int? page}) => _BottomOverlayContent(
|
||||
mainEntry: _lastEntry!,
|
||||
pageEntry: multiPageInfo?.getPageEntryByIndex(page) ?? _lastEntry!,
|
||||
Widget _buildContent({AvesEntry? pageEntry}) => _BottomOverlayContent(
|
||||
mainEntry: mainEntry,
|
||||
pageEntry: pageEntry ?? mainEntry,
|
||||
details: _lastDetails,
|
||||
position: widget.showPosition ? '${widget.index + 1}/${widget.entries.length}' : null,
|
||||
availableWidth: availableWidth,
|
||||
multiPageController: multiPageController,
|
||||
);
|
||||
|
||||
if (multiPageController == null) return _buildContent();
|
||||
|
||||
return StreamBuilder<MultiPageInfo?>(
|
||||
stream: multiPageController!.infoStream,
|
||||
builder: (context, snapshot) {
|
||||
final multiPageInfo = multiPageController!.info;
|
||||
return ValueListenableBuilder<int?>(
|
||||
valueListenable: multiPageController!.pageNotifier,
|
||||
builder: (context, page, child) {
|
||||
return _buildContent(multiPageInfo: multiPageInfo, page: page);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
return multiPageController != null
|
||||
? PageEntryBuilder(
|
||||
multiPageController: multiPageController!,
|
||||
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
|
||||
)
|
||||
: _buildContent();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -370,7 +364,7 @@ class _PositionTitleRow extends StatelessWidget {
|
|||
// but fail to get information about these pages
|
||||
final pageCount = multiPageInfo.pageCount;
|
||||
if (pageCount > 0) {
|
||||
final page = multiPageInfo.getById(entry.pageId) ?? multiPageInfo.defaultPage;
|
||||
final page = multiPageInfo.getById(entry.pageId ?? entry.contentId) ?? multiPageInfo.defaultPage;
|
||||
pagePosition = '${(page?.index ?? 0) + 1}/$pageCount';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,10 @@ 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/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';
|
||||
|
||||
class MultiPageOverlay extends StatefulWidget {
|
||||
final MultiPageController controller;
|
||||
|
@ -126,6 +128,7 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
|||
cancellableNotifier: _cancellableNotifier,
|
||||
selectable: false,
|
||||
highlightable: false,
|
||||
hero: false,
|
||||
),
|
||||
),
|
||||
IgnorePointer(
|
||||
|
@ -148,9 +151,18 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
|||
);
|
||||
}
|
||||
|
||||
void _setPage(int newPage) {
|
||||
final oldPage = controller.page;
|
||||
if (oldPage == newPage) return;
|
||||
|
||||
final oldPageEntry = controller.info!.getPageEntryByIndex(oldPage);
|
||||
controller.page = newPage;
|
||||
context.read<ViewStateConductor>().reset(oldPageEntry);
|
||||
}
|
||||
|
||||
Future<void> _goToPage(int page) async {
|
||||
_syncScroll = false;
|
||||
controller.page = page;
|
||||
_setPage(page);
|
||||
await _scrollController.animateTo(
|
||||
pageToScrollOffset(page),
|
||||
duration: Durations.viewerOverlayPageScrollAnimation,
|
||||
|
@ -161,7 +173,7 @@ class _MultiPageOverlayState extends State<MultiPageOverlay> {
|
|||
|
||||
void _onScrollChange() {
|
||||
if (_syncScroll) {
|
||||
controller.page = scrollOffsetToPage(_scrollController.offset);
|
||||
_setPage(scrollOffsetToPage(_scrollController.offset));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/favourites.dart';
|
||||
import 'package:aves/model/multipage.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
|
@ -12,7 +11,8 @@ import 'package:aves/widgets/viewer/entry_action_delegate.dart';
|
|||
import 'package:aves/widgets/viewer/multipage/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/common.dart';
|
||||
import 'package:aves/widgets/viewer/overlay/minimap.dart';
|
||||
import 'package:aves/widgets/viewer/visual/state.dart';
|
||||
import 'package:aves/widgets/viewer/page_entry_builder.dart';
|
||||
import 'package:aves/widgets/viewer/visual/conductor.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
@ -23,7 +23,6 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
final Animation<double> scale;
|
||||
final EdgeInsets? viewInsets, viewPadding;
|
||||
final bool canToggleFavourite;
|
||||
final ValueNotifier<ViewState>? viewStateNotifier;
|
||||
|
||||
static const double outerPadding = 8;
|
||||
static const double innerPadding = 8;
|
||||
|
@ -35,7 +34,6 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
required this.canToggleFavourite,
|
||||
required this.viewInsets,
|
||||
required this.viewPadding,
|
||||
required this.viewStateNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -50,33 +48,19 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
final buttonWidth = OverlayButton.getSize(context);
|
||||
final availableCount = ((mqWidth - outerPadding * 2 - buttonWidth) / (buttonWidth + innerPadding)).floor();
|
||||
|
||||
Widget? child;
|
||||
if (mainEntry.isMultiPage) {
|
||||
final multiPageController = context.read<MultiPageConductor>().getController(mainEntry);
|
||||
if (multiPageController != null) {
|
||||
child = StreamBuilder<MultiPageInfo?>(
|
||||
stream: multiPageController.infoStream,
|
||||
builder: (context, snapshot) {
|
||||
final multiPageInfo = multiPageController.info;
|
||||
return ValueListenableBuilder<int?>(
|
||||
valueListenable: multiPageController.pageNotifier,
|
||||
builder: (context, page, child) {
|
||||
return _buildOverlay(availableCount, mainEntry, pageEntry: multiPageInfo?.getPageEntryByIndex(page));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return child ??= _buildOverlay(availableCount, mainEntry);
|
||||
return mainEntry.isMultiPage
|
||||
? PageEntryBuilder(
|
||||
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
|
||||
builder: (pageEntry) => _buildOverlay(context, availableCount, mainEntry, pageEntry: pageEntry),
|
||||
)
|
||||
: _buildOverlay(context, availableCount, mainEntry);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOverlay(int availableCount, AvesEntry mainEntry, {AvesEntry? pageEntry}) {
|
||||
Widget _buildOverlay(BuildContext context, int availableCount, AvesEntry mainEntry, {AvesEntry? pageEntry}) {
|
||||
pageEntry ??= mainEntry;
|
||||
|
||||
bool _canDo(EntryAction action) {
|
||||
|
@ -130,8 +114,10 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
},
|
||||
);
|
||||
|
||||
return settings.showOverlayMinimap && viewStateNotifier != null
|
||||
? Column(
|
||||
if (settings.showOverlayMinimap) {
|
||||
final viewStateConductor = context.read<ViewStateConductor>();
|
||||
final viewStateNotifier = viewStateConductor.getOrCreateController(pageEntry);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
buttonRow,
|
||||
|
@ -140,12 +126,13 @@ class ViewerTopOverlay extends StatelessWidget {
|
|||
opacity: scale,
|
||||
child: Minimap(
|
||||
entry: pageEntry,
|
||||
viewStateNotifier: viewStateNotifier!,
|
||||
viewStateNotifier: viewStateNotifier,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
: buttonRow;
|
||||
);
|
||||
}
|
||||
return buttonRow;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,6 +141,8 @@ class _TopOverlayRow extends StatelessWidget {
|
|||
final Animation<double> scale;
|
||||
final AvesEntry mainEntry, pageEntry;
|
||||
|
||||
AvesEntry get favouriteTargetEntry => mainEntry.isBurst ? pageEntry : mainEntry;
|
||||
|
||||
const _TopOverlayRow({
|
||||
Key? key,
|
||||
required this.quickActions,
|
||||
|
@ -204,7 +193,7 @@ class _TopOverlayRow extends StatelessWidget {
|
|||
switch (action) {
|
||||
case EntryAction.toggleFavourite:
|
||||
child = _FavouriteToggler(
|
||||
entry: mainEntry,
|
||||
entry: favouriteTargetEntry,
|
||||
onPressed: onPressed,
|
||||
);
|
||||
break;
|
||||
|
@ -250,7 +239,7 @@ class _TopOverlayRow extends StatelessWidget {
|
|||
// in app actions
|
||||
case EntryAction.toggleFavourite:
|
||||
child = _FavouriteToggler(
|
||||
entry: mainEntry,
|
||||
entry: favouriteTargetEntry,
|
||||
isMenuItem: true,
|
||||
);
|
||||
break;
|
||||
|
@ -300,7 +289,7 @@ class _TopOverlayRow extends StatelessWidget {
|
|||
|
||||
void _onActionSelected(BuildContext context, EntryAction action) {
|
||||
var targetEntry = mainEntry;
|
||||
if (mainEntry.isMultiPage && EntryActions.pageActions.contains(action)) {
|
||||
if (mainEntry.isMultiPage && (mainEntry.isBurst || EntryActions.pageActions.contains(action))) {
|
||||
final multiPageController = context.read<MultiPageConductor>().getController(mainEntry);
|
||||
if (multiPageController != null) {
|
||||
final multiPageInfo = multiPageController.info;
|
||||
|
|
35
lib/widgets/viewer/page_entry_builder.dart
Normal file
35
lib/widgets/viewer/page_entry_builder.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
import 'package:aves/model/entry.dart';
|
||||
import 'package:aves/model/multipage.dart';
|
||||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class PageEntryBuilder extends StatelessWidget {
|
||||
final MultiPageController? multiPageController;
|
||||
final Widget Function(AvesEntry? pageEntry) builder;
|
||||
|
||||
const PageEntryBuilder({
|
||||
Key? key,
|
||||
required this.multiPageController,
|
||||
required this.builder,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = multiPageController;
|
||||
return controller != null
|
||||
? StreamBuilder<MultiPageInfo?>(
|
||||
stream: controller.infoStream,
|
||||
builder: (context, snapshot) {
|
||||
final multiPageInfo = controller.info;
|
||||
return ValueListenableBuilder<int?>(
|
||||
valueListenable: controller.pageNotifier,
|
||||
builder: (context, page, child) {
|
||||
final pageEntry = multiPageInfo?.getPageEntryByIndex(page);
|
||||
return builder(pageEntry);
|
||||
},
|
||||
);
|
||||
},
|
||||
)
|
||||
: builder(null);
|
||||
}
|
||||
}
|
|
@ -48,7 +48,7 @@ class EntryPrinter with FeedbackMixin {
|
|||
}
|
||||
|
||||
if (entry.isMultiPage && !entry.isMotionPhoto) {
|
||||
final multiPageInfo = await metadataService.getMultiPageInfo(entry);
|
||||
final multiPageInfo = await entry.getMultiPageInfo();
|
||||
if (multiPageInfo != null) {
|
||||
final pageCount = multiPageInfo.pageCount;
|
||||
if (pageCount > 1) {
|
||||
|
|
59
lib/widgets/viewer/visual/conductor.dart
Normal file
59
lib/widgets/viewer/visual/conductor.dart
Normal file
|
@ -0,0 +1,59 @@
|
|||
import 'package:aves/model/entry.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/viewer/visual/state.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class ViewStateConductor {
|
||||
final List<Tuple2<String, ValueNotifier<ViewState>>> _controllers = [];
|
||||
Size _viewportSize = Size.zero;
|
||||
|
||||
static const maxControllerCount = 3;
|
||||
|
||||
Future<void> dispose() async {
|
||||
_controllers.clear();
|
||||
}
|
||||
|
||||
set viewportSize(Size size) => _viewportSize = size;
|
||||
|
||||
ValueNotifier<ViewState> getOrCreateController(AvesEntry entry) {
|
||||
var controller = _controllers.firstOrNull;
|
||||
if (controller == null || controller.item1 != entry.uri) {
|
||||
controller = _controllers.firstWhereOrNull((kv) => kv.item1 == entry.uri);
|
||||
if (controller != null) {
|
||||
_controllers.remove(controller);
|
||||
} else {
|
||||
// try to initialize the view state to match magnifier initial state
|
||||
const initialScale = ScaleLevel(ref: ScaleReference.contained);
|
||||
final initialValue = ViewState(
|
||||
Offset.zero,
|
||||
ScaleBoundaries(
|
||||
minScale: initialScale,
|
||||
maxScale: initialScale,
|
||||
initialScale: initialScale,
|
||||
viewportSize: _viewportSize,
|
||||
childSize: entry.displaySize,
|
||||
).initialScale,
|
||||
_viewportSize,
|
||||
);
|
||||
controller = Tuple2(entry.uri, ValueNotifier<ViewState>(initialValue));
|
||||
}
|
||||
_controllers.insert(0, controller);
|
||||
while (_controllers.length > maxControllerCount) {
|
||||
_controllers.removeLast().item2.dispose();
|
||||
}
|
||||
}
|
||||
return controller.item2;
|
||||
}
|
||||
|
||||
void reset(AvesEntry entry) {
|
||||
final uris = <AvesEntry>{
|
||||
entry,
|
||||
...?entry.burstEntries,
|
||||
}.map((v) => v.uri).toSet();
|
||||
_controllers.removeWhere((kv) => uris.contains(kv.item1));
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import 'package:aves/widgets/viewer/hero.dart';
|
|||
import 'package:aves/widgets/viewer/overlay/notifications.dart';
|
||||
import 'package:aves/widgets/viewer/video/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/video/controller.dart';
|
||||
import 'package:aves/widgets/viewer/visual/conductor.dart';
|
||||
import 'package:aves/widgets/viewer/visual/error.dart';
|
||||
import 'package:aves/widgets/viewer/visual/raster.dart';
|
||||
import 'package:aves/widgets/viewer/visual/state.dart';
|
||||
|
@ -26,7 +27,6 @@ import 'package:provider/provider.dart';
|
|||
|
||||
class EntryPageView extends StatefulWidget {
|
||||
final AvesEntry mainEntry, pageEntry;
|
||||
final Size viewportSize;
|
||||
final VoidCallback? onDisposed;
|
||||
|
||||
static const decorationCheckSize = 20.0;
|
||||
|
@ -35,7 +35,6 @@ class EntryPageView extends StatefulWidget {
|
|||
Key? key,
|
||||
required this.mainEntry,
|
||||
required this.pageEntry,
|
||||
required this.viewportSize,
|
||||
this.onDisposed,
|
||||
}) : super(key: key);
|
||||
|
||||
|
@ -44,16 +43,14 @@ class EntryPageView extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _EntryPageViewState extends State<EntryPageView> {
|
||||
late ValueNotifier<ViewState> _viewStateNotifier;
|
||||
late MagnifierController _magnifierController;
|
||||
final ValueNotifier<ViewState> _viewStateNotifier = ValueNotifier(ViewState.zero);
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
AvesEntry get mainEntry => widget.mainEntry;
|
||||
|
||||
AvesEntry get entry => widget.pageEntry;
|
||||
|
||||
Size get viewportSize => widget.viewportSize;
|
||||
|
||||
static const initialScale = ScaleLevel(ref: ScaleReference.contained);
|
||||
static const minScale = ScaleLevel(ref: ScaleReference.contained);
|
||||
static const maxScale = ScaleLevel(factor: 2.0);
|
||||
|
@ -68,9 +65,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
void didUpdateWidget(covariant EntryPageView oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.pageEntry.uri != widget.pageEntry.uri || oldWidget.pageEntry.displaySize != widget.pageEntry.displaySize) {
|
||||
// do not reset the magnifier view state unless main entry or page entry dimensions change,
|
||||
// in effect locking the zoom & position when browsing entry pages of the same size
|
||||
if (oldWidget.pageEntry != widget.pageEntry) {
|
||||
_unregisterWidget();
|
||||
_registerWidget();
|
||||
}
|
||||
|
@ -84,19 +79,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
}
|
||||
|
||||
void _registerWidget() {
|
||||
// try to initialize the view state to match magnifier initial state
|
||||
_viewStateNotifier.value = ViewState(
|
||||
Offset.zero,
|
||||
ScaleBoundaries(
|
||||
minScale: minScale,
|
||||
maxScale: maxScale,
|
||||
initialScale: initialScale,
|
||||
viewportSize: viewportSize,
|
||||
childSize: entry.displaySize,
|
||||
).initialScale,
|
||||
viewportSize,
|
||||
);
|
||||
|
||||
_viewStateNotifier = context.read<ViewStateConductor>().getOrCreateController(entry);
|
||||
_magnifierController = MagnifierController();
|
||||
_subscriptions.add(_magnifierController.stateStream.listen(_onViewStateChanged));
|
||||
_subscriptions.add(_magnifierController.scaleBoundariesStream.listen(_onViewScaleBoundariesChanged));
|
||||
|
@ -134,7 +117,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
|
||||
return Consumer<HeroInfo?>(
|
||||
builder: (context, info, child) => Hero(
|
||||
tag: info != null && info.entry == mainEntry ? hashValues(info.collectionId, mainEntry) : hashCode,
|
||||
tag: info != null && info.entry == mainEntry ? hashValues(info.collectionId, mainEntry.uri) : hashCode,
|
||||
transitionOnUserGestures: true,
|
||||
child: child!,
|
||||
),
|
||||
|
@ -241,7 +224,7 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
}) {
|
||||
return Magnifier(
|
||||
// key includes modified date to refresh when the image is modified by metadata (e.g. rotated)
|
||||
key: ValueKey('${entry.pageId}_${entry.dateModifiedSecs}'),
|
||||
key: ValueKey('${entry.uri}_${entry.pageId}_${entry.dateModifiedSecs}'),
|
||||
controller: _magnifierController,
|
||||
childSize: displaySize ?? entry.displaySize,
|
||||
minScale: minScale,
|
||||
|
@ -260,14 +243,12 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
final current = _viewStateNotifier.value;
|
||||
final viewState = ViewState(v.position, v.scale, current.viewportSize);
|
||||
_viewStateNotifier.value = viewState;
|
||||
ViewStateNotification(entry.uri, viewState).dispatch(context);
|
||||
}
|
||||
|
||||
void _onViewScaleBoundariesChanged(ScaleBoundaries v) {
|
||||
final current = _viewStateNotifier.value;
|
||||
final viewState = ViewState(current.position, current.scale, v.viewportSize);
|
||||
_viewStateNotifier.value = viewState;
|
||||
ViewStateNotification(entry.uri, viewState).dispatch(context);
|
||||
}
|
||||
|
||||
static ScaleState _vectorScaleStateCycle(ScaleState actual) {
|
||||
|
|
|
@ -13,13 +13,3 @@ class ViewState {
|
|||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{position=$position, scale=$scale, viewportSize=$viewportSize}';
|
||||
}
|
||||
|
||||
class ViewStateNotification extends Notification {
|
||||
final String uri;
|
||||
final ViewState viewState;
|
||||
|
||||
const ViewStateNotification(this.uri, this.viewState);
|
||||
|
||||
@override
|
||||
String toString() => '$runtimeType#${shortHash(this)}{uri=$uri, viewState=$viewState}';
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue