#197 preview button when selecting items
This commit is contained in:
parent
09cf4fef3e
commit
433ac537dd
17 changed files with 225 additions and 165 deletions
|
@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Collection: preview button when selecting items
|
||||||
- Vaults: custom pattern lock
|
- Vaults: custom pattern lock
|
||||||
- Video: picture-in-picture
|
- Video: picture-in-picture
|
||||||
- Video: handle skip next/previous media buttons
|
- Video: handle skip next/previous media buttons
|
||||||
|
|
|
@ -19,6 +19,11 @@ extension ExtraAppMode on AppMode {
|
||||||
AppMode.pickMultipleMediaExternal,
|
AppMode.pickMultipleMediaExternal,
|
||||||
}.contains(this);
|
}.contains(this);
|
||||||
|
|
||||||
|
bool get canEditEntry => {
|
||||||
|
AppMode.main,
|
||||||
|
AppMode.view,
|
||||||
|
}.contains(this);
|
||||||
|
|
||||||
bool get canSelectMedia => {
|
bool get canSelectMedia => {
|
||||||
AppMode.main,
|
AppMode.main,
|
||||||
AppMode.pickMultipleMediaExternal,
|
AppMode.pickMultipleMediaExternal,
|
||||||
|
|
|
@ -254,8 +254,11 @@ class _CollectionGridContentState extends State<_CollectionGridContent> {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (selection.isSelecting) {
|
if (selection.isSelecting) {
|
||||||
child = ChangeNotifierProvider<Selection<AvesEntry>>.value(
|
child = MultiProvider(
|
||||||
value: selection,
|
providers: [
|
||||||
|
ListenableProvider<ValueNotifier<AppMode>>.value(value: ValueNotifier(AppMode.pickMediaInternal)),
|
||||||
|
ChangeNotifierProvider<Selection<AvesEntry>>.value(value: selection),
|
||||||
|
],
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,8 +73,8 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
|
||||||
children: _diffs.map((diff) {
|
children: _diffs.map((diff) {
|
||||||
final oldText = diff.item1;
|
final oldText = diff.item1;
|
||||||
final newText = diff.item2;
|
final newText = diff.item2;
|
||||||
final oldWidth = diff.item3;
|
final oldSize = diff.item3;
|
||||||
final newWidth = diff.item4;
|
final newSize = diff.item4;
|
||||||
final text = (_animation.value == 0 ? oldText : newText) ?? '';
|
final text = (_animation.value == 0 ? oldText : newText) ?? '';
|
||||||
return WidgetSpan(
|
return WidgetSpan(
|
||||||
child: AnimatedSize(
|
child: AnimatedSize(
|
||||||
|
@ -91,9 +91,10 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
|
||||||
children: [
|
children: [
|
||||||
...previousChildren.map(
|
...previousChildren.map(
|
||||||
(child) => ConstrainedBox(
|
(child) => ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints.tight(Size(
|
||||||
maxWidth: min(oldWidth, newWidth),
|
min(oldSize.width, newSize.width),
|
||||||
),
|
min(oldSize.height, newSize.height),
|
||||||
|
)),
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -116,14 +117,16 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double textWidth(String text) {
|
Size textSize(String text) {
|
||||||
final para = RenderParagraph(
|
final para = RenderParagraph(
|
||||||
TextSpan(text: text, style: widget.textStyle),
|
TextSpan(text: text, style: widget.textStyle),
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
textScaleFactor: MediaQuery.textScaleFactorOf(context),
|
textScaleFactor: MediaQuery.textScaleFactorOf(context),
|
||||||
strutStyle: widget.strutStyle,
|
strutStyle: widget.strutStyle,
|
||||||
)..layout(const BoxConstraints(), parentUsesSize: true);
|
)..layout(const BoxConstraints(), parentUsesSize: true);
|
||||||
return para.getMaxIntrinsicWidth(double.infinity);
|
final width = para.getMaxIntrinsicWidth(double.infinity);
|
||||||
|
final height = para.getMaxIntrinsicHeight(double.infinity);
|
||||||
|
return Size(width, height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// use an adaptation of Google's `Diff Match and Patch`
|
// use an adaptation of Google's `Diff Match and Patch`
|
||||||
|
@ -140,15 +143,15 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(d.map((diff) {
|
..addAll(d.map((diff) {
|
||||||
final text = diff.text;
|
final text = diff.text;
|
||||||
|
final size = textSize(text);
|
||||||
switch (diff.operation) {
|
switch (diff.operation) {
|
||||||
case Operation.delete:
|
case Operation.delete:
|
||||||
return Tuple4(text, null, textWidth(text), .0);
|
return Tuple4(text, null, size, Size.zero);
|
||||||
case Operation.insert:
|
case Operation.insert:
|
||||||
return Tuple4(null, text, .0, textWidth(text));
|
return Tuple4(null, text, Size.zero, size);
|
||||||
case Operation.equal:
|
case Operation.equal:
|
||||||
default:
|
default:
|
||||||
final width = textWidth(text);
|
return Tuple4(text, text, size, size);
|
||||||
return Tuple4(text, text, width, width);
|
|
||||||
}
|
}
|
||||||
}).fold<List<_TextDiff>>([], (prev, v) {
|
}).fold<List<_TextDiff>>([], (prev, v) {
|
||||||
if (prev.isNotEmpty) {
|
if (prev.isNotEmpty) {
|
||||||
|
@ -168,109 +171,6 @@ class _AnimatedDiffTextState extends State<AnimatedDiffText> with SingleTickerPr
|
||||||
return [...prev, v];
|
return [...prev, v];
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// void _computeDiff(String oldText, String newText) {
|
|
||||||
// final oldCharacters = oldText.characters.toList();
|
|
||||||
// final newCharacters = newText.characters.toList();
|
|
||||||
// final diffResult = calculateListDiff<String>(oldCharacters, newCharacters, detectMoves: false);
|
|
||||||
// final updates = diffResult.getUpdatesWithData().toList();
|
|
||||||
// List<TextDiff> diffs = [];
|
|
||||||
// DataDiffUpdate<String>? pendingUpdate;
|
|
||||||
// int lastPos = oldCharacters.length;
|
|
||||||
// void addKeep(int pos) {
|
|
||||||
// if (pos < lastPos) {
|
|
||||||
// final text = oldCharacters.sublist(pos, lastPos).join();
|
|
||||||
// final width = textWidth(text);
|
|
||||||
// diffs.insert(0, Tuple4(text, text, width, width));
|
|
||||||
// lastPos = pos;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// void commit(DataDiffUpdate<String>? update) {
|
|
||||||
// update?.when(
|
|
||||||
// insert: (pos, data) {
|
|
||||||
// addKeep(pos);
|
|
||||||
// diffs.insert(0, Tuple4(null, data, 0, textWidth(data)));
|
|
||||||
// lastPos = pos;
|
|
||||||
// },
|
|
||||||
// remove: (pos, data) {
|
|
||||||
// addKeep(pos + data.length);
|
|
||||||
// diffs.insert(0, Tuple4(data, null, textWidth(data), 0));
|
|
||||||
// lastPos = pos;
|
|
||||||
// },
|
|
||||||
// change: (pos, oldData, newData) {
|
|
||||||
// addKeep(pos + oldData.length);
|
|
||||||
// diffs.insert(0, Tuple4(oldData, newData, textWidth(oldData), textWidth(newData)));
|
|
||||||
// lastPos = pos;
|
|
||||||
// },
|
|
||||||
// move: (from, to, data) {
|
|
||||||
// assert(false, '`move` update: from=$from, to=$from, data=$data');
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// for (var update in updates) {
|
|
||||||
// update.when(
|
|
||||||
// insert: (pos, data) {
|
|
||||||
// if (pendingUpdate == null) {
|
|
||||||
// pendingUpdate = update;
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (pendingUpdate is DataInsert) {
|
|
||||||
// final pendingInsert = pendingUpdate as DataInsert;
|
|
||||||
// if (pendingInsert.position == pos) {
|
|
||||||
// // merge insertions
|
|
||||||
// pendingUpdate = DataInsert(position: pos, data: data + pendingInsert.data);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// } else if (pendingUpdate is DataRemove) {
|
|
||||||
// final pendingRemove = pendingUpdate as DataRemove;
|
|
||||||
// if (pendingRemove.position == pos) {
|
|
||||||
// // convert to change
|
|
||||||
// pendingUpdate = DataChange(position: pos, oldData: pendingRemove.data, newData: data);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// } else if (pendingUpdate is DataChange) {
|
|
||||||
// final pendingChange = pendingUpdate as DataChange;
|
|
||||||
// if (pendingChange.position == pos) {
|
|
||||||
// // merge changes
|
|
||||||
// pendingUpdate = DataChange(position: pos, oldData: pendingChange.oldData, newData: data + pendingChange.newData);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// commit(pendingUpdate);
|
|
||||||
// pendingUpdate = update;
|
|
||||||
// },
|
|
||||||
// remove: (pos, data) {
|
|
||||||
// if (pendingUpdate == null) {
|
|
||||||
// pendingUpdate = update;
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// if (pendingUpdate is DataRemove) {
|
|
||||||
// final pendingRemove = pendingUpdate as DataRemove;
|
|
||||||
// if (pendingRemove.position == pos + data.length) {
|
|
||||||
// // merge removals
|
|
||||||
// pendingUpdate = DataRemove(position: pos, data: data + pendingRemove.data);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// commit(pendingUpdate);
|
|
||||||
// pendingUpdate = update;
|
|
||||||
// },
|
|
||||||
// change: (pos, oldData, newData) {
|
|
||||||
// assert(false, '`change` update: from=$pos, oldData=$oldData, newData=$newData');
|
|
||||||
// },
|
|
||||||
// move: (from, to, data) {
|
|
||||||
// assert(false, '`move` update: from=$from, to=$from, data=$data');
|
|
||||||
// },
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// commit(pendingUpdate);
|
|
||||||
// addKeep(0);
|
|
||||||
// _diffs
|
|
||||||
// ..clear()
|
|
||||||
// ..addAll(diffs);
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef _TextDiff = Tuple4<String?, String?, double, double>;
|
typedef _TextDiff = Tuple4<String?, String?, Size, Size>;
|
||||||
|
|
|
@ -26,7 +26,7 @@ class GridItemSelectionOverlay<T> extends StatelessWidget {
|
||||||
duration: duration,
|
duration: duration,
|
||||||
child: isSelecting
|
child: isSelecting
|
||||||
? Selector<Selection<T>, bool>(
|
? Selector<Selection<T>, bool>(
|
||||||
selector: (context, selection) => selection.isSelected([item]),
|
selector: (context, selection) => selection.isSelected({item}),
|
||||||
builder: (context, isSelected, child) {
|
builder: (context, isSelected, child) {
|
||||||
return AnimatedContainer(
|
return AnimatedContainer(
|
||||||
alignment: AlignmentDirectional.topEnd,
|
alignment: AlignmentDirectional.topEnd,
|
||||||
|
|
|
@ -51,6 +51,8 @@ class MapButtonPanel extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case MapNavigationButton.none:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter);
|
final showCoordinateFilter = context.select<MapThemeData, bool>((v) => v.showCoordinateFilter);
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/widgets/common/fx/borders.dart';
|
import 'package:aves/widgets/common/fx/borders.dart';
|
||||||
import 'package:aves/widgets/common/grid/overlay.dart';
|
import 'package:aves/widgets/common/grid/overlay.dart';
|
||||||
import 'package:aves/widgets/common/thumbnail/image.dart';
|
import 'package:aves/widgets/common/thumbnail/image.dart';
|
||||||
|
import 'package:aves/widgets/common/thumbnail/notifications.dart';
|
||||||
import 'package:aves/widgets/common/thumbnail/overlay.dart';
|
import 'package:aves/widgets/common/thumbnail/overlay.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
@ -44,11 +45,15 @@ class DecoratedThumbnail extends StatelessWidget {
|
||||||
children: [
|
children: [
|
||||||
child,
|
child,
|
||||||
ThumbnailEntryOverlay(entry: entry),
|
ThumbnailEntryOverlay(entry: entry),
|
||||||
if (selectable)
|
if (selectable) ...[
|
||||||
GridItemSelectionOverlay<AvesEntry>(
|
GridItemSelectionOverlay<AvesEntry>(
|
||||||
item: entry,
|
item: entry,
|
||||||
padding: const EdgeInsets.all(2),
|
padding: const EdgeInsets.all(2),
|
||||||
),
|
),
|
||||||
|
ThumbnailZoomOverlay(
|
||||||
|
onZoom: () => OpenViewerNotification(entry).dispatch(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
if (highlightable) ThumbnailHighlightOverlay(entry: entry),
|
if (highlightable) ThumbnailHighlightOverlay(entry: entry),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -47,7 +47,10 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
|
|
||||||
EntryActionDelegate(this.mainEntry, this.pageEntry, this.collection);
|
EntryActionDelegate(this.mainEntry, this.pageEntry, this.collection);
|
||||||
|
|
||||||
bool isVisible(EntryAction action) {
|
bool isVisible({
|
||||||
|
required AppMode appMode,
|
||||||
|
required EntryAction action,
|
||||||
|
}) {
|
||||||
if (mainEntry.trashed) {
|
if (mainEntry.trashed) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntryAction.delete:
|
case EntryAction.delete:
|
||||||
|
@ -60,7 +63,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry;
|
final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry;
|
||||||
final canWrite = !settings.isReadOnly;
|
final canWrite = appMode.canEditEntry && !settings.isReadOnly;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntryAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
return collection != null;
|
return collection != null;
|
||||||
|
@ -120,7 +123,11 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
||||||
case EntryAction.showGeoTiffOnMap:
|
case EntryAction.showGeoTiffOnMap:
|
||||||
case EntryAction.convertMotionPhotoToStillImage:
|
case EntryAction.convertMotionPhotoToStillImage:
|
||||||
case EntryAction.viewMotionPhotoVideo:
|
case EntryAction.viewMotionPhotoVideo:
|
||||||
return _metadataActionDelegate.isVisible(targetEntry, action);
|
return _metadataActionDelegate.isVisible(
|
||||||
|
appMode: appMode,
|
||||||
|
targetEntry: targetEntry,
|
||||||
|
action: action,
|
||||||
|
);
|
||||||
case EntryAction.debug:
|
case EntryAction.debug:
|
||||||
return kDebugMode;
|
return kDebugMode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_actions.dart';
|
||||||
import 'package:aves/model/actions/events.dart';
|
import 'package:aves/model/actions/events.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
|
@ -29,8 +30,12 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
||||||
|
|
||||||
Stream<ActionEvent<EntryAction>> get eventStream => _eventStreamController.stream;
|
Stream<ActionEvent<EntryAction>> get eventStream => _eventStreamController.stream;
|
||||||
|
|
||||||
bool isVisible(AvesEntry targetEntry, EntryAction action) {
|
bool isVisible({
|
||||||
final canWrite = !settings.isReadOnly;
|
required AppMode appMode,
|
||||||
|
required AvesEntry targetEntry,
|
||||||
|
required EntryAction action,
|
||||||
|
}) {
|
||||||
|
final canWrite = appMode.canEditEntry && !settings.isReadOnly;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
// general
|
// general
|
||||||
case EntryAction.editDate:
|
case EntryAction.editDate:
|
||||||
|
@ -39,16 +44,17 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
||||||
case EntryAction.editRating:
|
case EntryAction.editRating:
|
||||||
case EntryAction.editTags:
|
case EntryAction.editTags:
|
||||||
case EntryAction.removeMetadata:
|
case EntryAction.removeMetadata:
|
||||||
case EntryAction.exportMetadata:
|
|
||||||
return canWrite;
|
return canWrite;
|
||||||
|
case EntryAction.exportMetadata:
|
||||||
|
return true;
|
||||||
// GeoTIFF
|
// GeoTIFF
|
||||||
case EntryAction.showGeoTiffOnMap:
|
case EntryAction.showGeoTiffOnMap:
|
||||||
return targetEntry.isGeotiff;
|
return appMode.canNavigate && targetEntry.isGeotiff;
|
||||||
// motion photo
|
// motion photo
|
||||||
case EntryAction.convertMotionPhotoToStillImage:
|
case EntryAction.convertMotionPhotoToStillImage:
|
||||||
return canWrite && targetEntry.isMotionPhoto;
|
return canWrite && targetEntry.isMotionPhoto;
|
||||||
case EntryAction.viewMotionPhotoVideo:
|
case EntryAction.viewMotionPhotoVideo:
|
||||||
return targetEntry.isMotionPhoto;
|
return appMode.canNavigate && targetEntry.isMotionPhoto;
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -562,12 +562,19 @@ class _EntryViewerStackState extends State<EntryViewerStack> with EntryViewContr
|
||||||
|
|
||||||
void _onVerticalPageChanged(int page) {
|
void _onVerticalPageChanged(int page) {
|
||||||
_currentVerticalPage.value = page;
|
_currentVerticalPage.value = page;
|
||||||
if (page == transitionPage) {
|
switch (page) {
|
||||||
dismissFeedback(context);
|
case transitionPage:
|
||||||
_popVisual();
|
dismissFeedback(context);
|
||||||
} else if (page == infoPage) {
|
_popVisual();
|
||||||
// prevent hero when viewer is offscreen
|
break;
|
||||||
_heroInfoNotifier.value = null;
|
case imagePage:
|
||||||
|
reportService.log('Nav move to Image page');
|
||||||
|
break;
|
||||||
|
case infoPage:
|
||||||
|
reportService.log('Nav move to Info page');
|
||||||
|
// prevent hero when viewer is offscreen
|
||||||
|
_heroInfoNotifier.value = null;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -150,11 +150,20 @@ class _BasicSectionState extends State<BasicSection> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEditButtons(BuildContext context) {
|
Widget _buildEditButtons(BuildContext context) {
|
||||||
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
final entry = widget.entry;
|
final entry = widget.entry;
|
||||||
final children = [
|
final children = [
|
||||||
EntryAction.editRating,
|
EntryAction.editRating,
|
||||||
EntryAction.editTags,
|
EntryAction.editTags,
|
||||||
].where((v) => actionDelegate.canApply(entry, v)).map((v) => _buildEditMetadataButton(context, v)).toList();
|
]
|
||||||
|
.where((v) => actionDelegate.isVisible(
|
||||||
|
appMode: appMode,
|
||||||
|
targetEntry: entry,
|
||||||
|
action: v,
|
||||||
|
))
|
||||||
|
.where((v) => actionDelegate.canApply(entry, v))
|
||||||
|
.map((v) => _buildEditMetadataButton(context, v))
|
||||||
|
.toList();
|
||||||
|
|
||||||
return children.isEmpty
|
return children.isEmpty
|
||||||
? const SizedBox()
|
? const SizedBox()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_actions.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
@ -14,6 +15,7 @@ import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class InfoAppBar extends StatelessWidget {
|
class InfoAppBar extends StatelessWidget {
|
||||||
final AvesEntry entry;
|
final AvesEntry entry;
|
||||||
|
@ -33,8 +35,14 @@ class InfoAppBar extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final commonActions = EntryActions.commonMetadataActions.where((v) => actionDelegate.isVisible(entry, v));
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where((v) => actionDelegate.isVisible(entry, v));
|
bool isVisible(EntryAction action) => actionDelegate.isVisible(
|
||||||
|
appMode: appMode,
|
||||||
|
targetEntry: entry,
|
||||||
|
action: action,
|
||||||
|
);
|
||||||
|
final commonActions = EntryActions.commonMetadataActions.where(isVisible);
|
||||||
|
final formatSpecificActions = EntryActions.formatSpecificMetadataActions.where(isVisible);
|
||||||
final useTvLayout = settings.useTvLayout;
|
final useTvLayout = settings.useTvLayout;
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
leading: useTvLayout
|
leading: useTvLayout
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/filters/location.dart';
|
import 'package:aves/model/filters/location.dart';
|
||||||
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
import 'package:aves/model/settings/enums/coordinate_format.dart';
|
||||||
|
@ -13,6 +14,7 @@ import 'package:aves/widgets/map/map_page.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
import 'package:aves_map/aves_map.dart';
|
import 'package:aves_map/aves_map.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class LocationSection extends StatefulWidget {
|
class LocationSection extends StatefulWidget {
|
||||||
final CollectionLens? collection;
|
final CollectionLens? collection;
|
||||||
|
@ -72,6 +74,7 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (!entry.hasGps) return const SizedBox();
|
if (!entry.hasGps) return const SizedBox();
|
||||||
|
|
||||||
|
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -79,7 +82,7 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
MapTheme(
|
MapTheme(
|
||||||
interactive: false,
|
interactive: false,
|
||||||
showCoordinateFilter: false,
|
showCoordinateFilter: false,
|
||||||
navigationButton: MapNavigationButton.map,
|
navigationButton: canNavigate ? MapNavigationButton.map : MapNavigationButton.none,
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
mapHeight: 200,
|
mapHeight: 200,
|
||||||
child: GeoMap(
|
child: GeoMap(
|
||||||
|
@ -87,7 +90,7 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
entries: [entry],
|
entries: [entry],
|
||||||
isAnimatingNotifier: widget.isScrollingNotifier,
|
isAnimatingNotifier: widget.isScrollingNotifier,
|
||||||
onUserZoomChange: (zoom) => settings.infoMapZoom = zoom.roundToDouble(),
|
onUserZoomChange: (zoom) => settings.infoMapZoom = zoom.roundToDouble(),
|
||||||
onMarkerTap: collection != null ? (location, entry) => _openMapPage(context) : null,
|
onMarkerTap: collection != null && canNavigate ? (location, entry) => _openMapPage(context) : null,
|
||||||
openMapPage: collection != null ? _openMapPage : null,
|
openMapPage: collection != null ? _openMapPage : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/selection.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/widgets/common/extensions/media_query.dart';
|
import 'package:aves/widgets/common/extensions/media_query.dart';
|
||||||
|
@ -9,6 +10,7 @@ import 'package:aves/widgets/viewer/entry_vertical_pager.dart';
|
||||||
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
import 'package:aves/widgets/viewer/multipage/controller.dart';
|
||||||
import 'package:aves/widgets/viewer/notifications.dart';
|
import 'package:aves/widgets/viewer/notifications.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/multipage.dart';
|
import 'package:aves/widgets/viewer/overlay/multipage.dart';
|
||||||
|
import 'package:aves/widgets/viewer/overlay/selection_button.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/thumbnail_preview.dart';
|
import 'package:aves/widgets/viewer/overlay/thumbnail_preview.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
|
import 'package:aves/widgets/viewer/overlay/viewer_buttons.dart';
|
||||||
import 'package:aves/widgets/viewer/overlay/wallpaper_buttons.dart';
|
import 'package:aves/widgets/viewer/overlay/wallpaper_buttons.dart';
|
||||||
|
@ -183,30 +185,36 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
|
||||||
]),
|
]),
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
|
final viewInsetsPadding = (widget.viewInsets ?? EdgeInsets.zero) + (widget.viewPadding ?? EdgeInsets.zero);
|
||||||
final viewerButtonRow = FocusableActionDetector(
|
final selection = context.read<Selection<AvesEntry>?>();
|
||||||
focusNode: _buttonRowFocusScopeNode,
|
final viewerButtonRow = (selection?.isSelecting ?? false)
|
||||||
shortcuts: settings.useTvLayout ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null,
|
? SelectionButton(
|
||||||
actions: {TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))},
|
mainEntry: mainEntry,
|
||||||
child: SafeArea(
|
scale: _buttonScale,
|
||||||
top: false,
|
)
|
||||||
bottom: false,
|
: FocusableActionDetector(
|
||||||
minimum: EdgeInsets.only(
|
focusNode: _buttonRowFocusScopeNode,
|
||||||
left: viewInsetsPadding.left,
|
shortcuts: settings.useTvLayout ? const {SingleActivator(LogicalKeyboardKey.arrowUp): TvShowLessInfoIntent()} : null,
|
||||||
right: viewInsetsPadding.right,
|
actions: {TvShowLessInfoIntent: CallbackAction<Intent>(onInvoke: (intent) => TvShowLessInfoNotification().dispatch(context))},
|
||||||
),
|
child: SafeArea(
|
||||||
child: isWallpaperMode
|
top: false,
|
||||||
? WallpaperButtons(
|
bottom: false,
|
||||||
entry: pageEntry,
|
minimum: EdgeInsets.only(
|
||||||
scale: _buttonScale,
|
left: viewInsetsPadding.left,
|
||||||
)
|
right: viewInsetsPadding.right,
|
||||||
: ViewerButtons(
|
|
||||||
mainEntry: mainEntry,
|
|
||||||
pageEntry: pageEntry,
|
|
||||||
collection: widget.collection,
|
|
||||||
scale: _buttonScale,
|
|
||||||
),
|
),
|
||||||
),
|
child: isWallpaperMode
|
||||||
);
|
? WallpaperButtons(
|
||||||
|
entry: pageEntry,
|
||||||
|
scale: _buttonScale,
|
||||||
|
)
|
||||||
|
: ViewerButtons(
|
||||||
|
mainEntry: mainEntry,
|
||||||
|
pageEntry: pageEntry,
|
||||||
|
collection: widget.collection,
|
||||||
|
scale: _buttonScale,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null;
|
final showMultiPageOverlay = mainEntry.isMultiPage && multiPageController != null;
|
||||||
final collapsedPageScroller = mainEntry.isMotionPhoto;
|
final collapsedPageScroller = mainEntry.isMotionPhoto;
|
||||||
|
|
83
lib/widgets/viewer/overlay/selection_button.dart
Normal file
83
lib/widgets/viewer/overlay/selection_button.dart
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
import 'package:aves/model/entry.dart';
|
||||||
|
import 'package:aves/model/selection.dart';
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/utils/constants.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/text/animated_diff.dart';
|
||||||
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
|
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class SelectionButton extends StatelessWidget {
|
||||||
|
final AvesEntry mainEntry;
|
||||||
|
final Animation<double> scale;
|
||||||
|
|
||||||
|
static const double padding = 8;
|
||||||
|
static const duration = Durations.thumbnailOverlayAnimation;
|
||||||
|
|
||||||
|
const SelectionButton({
|
||||||
|
super.key,
|
||||||
|
required this.mainEntry,
|
||||||
|
required this.scale,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final selection = context.read<Selection<AvesEntry>>();
|
||||||
|
return SafeArea(
|
||||||
|
top: false,
|
||||||
|
bottom: false,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
ScalingOverlayTextButton(
|
||||||
|
scale: scale,
|
||||||
|
onPressed: () => selection.toggleSelection(mainEntry),
|
||||||
|
child: Selector<Selection<AvesEntry>?, int>(
|
||||||
|
selector: (context, selection) => selection?.selectedItems.length ?? 0,
|
||||||
|
builder: (context, count, child) {
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
AnimatedDiffText(
|
||||||
|
count == 0 ? l10n.collectionSelectPageTitle : l10n.itemCount(count),
|
||||||
|
duration: duration,
|
||||||
|
),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 8),
|
||||||
|
child: Text(Constants.separator),
|
||||||
|
),
|
||||||
|
Selector<Selection<AvesEntry>, bool>(
|
||||||
|
selector: (context, selection) => selection.isSelected({mainEntry}),
|
||||||
|
builder: (context, isSelected, child) {
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: duration,
|
||||||
|
switchInCurve: Curves.easeOutBack,
|
||||||
|
switchOutCurve: Curves.easeOutBack,
|
||||||
|
transitionBuilder: (child, animation) => ScaleTransition(
|
||||||
|
scale: animation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
isSelected ? AIcons.selected : AIcons.unselected,
|
||||||
|
key: ValueKey(isSelected),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:aves/app_mode.dart';
|
||||||
import 'package:aves/model/actions/entry_actions.dart';
|
import 'package:aves/model/actions/entry_actions.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
|
@ -61,6 +62,12 @@ class ViewerButtons extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
|
bool isVisible(EntryAction action) => actionDelegate.isVisible(
|
||||||
|
appMode: appMode,
|
||||||
|
action: action,
|
||||||
|
);
|
||||||
|
|
||||||
final trashed = mainEntry.trashed;
|
final trashed = mainEntry.trashed;
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
top: false,
|
top: false,
|
||||||
|
@ -72,9 +79,9 @@ class ViewerButtons extends StatelessWidget {
|
||||||
return Selector<Settings, bool>(
|
return Selector<Settings, bool>(
|
||||||
selector: (context, s) => s.isRotationLocked,
|
selector: (context, s) => s.isRotationLocked,
|
||||||
builder: (context, s, child) {
|
builder: (context, s, child) {
|
||||||
final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(actionDelegate.isVisible).where(actionDelegate.canApply).take(availableCount - 1).toList();
|
final quickActions = (trashed ? EntryActions.trashed : settings.viewerQuickActions).where(isVisible).where(actionDelegate.canApply).take(availableCount - 1).toList();
|
||||||
List<EntryAction> getMenuActions(List<EntryAction> categoryActions) {
|
List<EntryAction> getMenuActions(List<EntryAction> categoryActions) {
|
||||||
return categoryActions.where((action) => !quickActions.contains(action)).where(actionDelegate.isVisible).toList();
|
return categoryActions.where((action) => !quickActions.contains(action)).where(isVisible).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ViewerButtonRowContent(
|
return ViewerButtonRowContent(
|
||||||
|
@ -109,6 +116,7 @@ class _TvButtonRowContent extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final appMode = context.watch<ValueNotifier<AppMode>>().value;
|
||||||
return Selector<VideoConductor, AvesVideoController?>(
|
return Selector<VideoConductor, AvesVideoController?>(
|
||||||
selector: (context, vc) => vc.getController(pageEntry),
|
selector: (context, vc) => vc.getController(pageEntry),
|
||||||
builder: (context, videoController, child) {
|
builder: (context, videoController, child) {
|
||||||
|
@ -120,7 +128,12 @@ class _TvButtonRowContent extends StatelessWidget {
|
||||||
...EntryActions.export,
|
...EntryActions.export,
|
||||||
...EntryActions.videoPlayback,
|
...EntryActions.videoPlayback,
|
||||||
...EntryActions.video,
|
...EntryActions.video,
|
||||||
].where(actionDelegate.isVisible).map((action) {
|
]
|
||||||
|
.where((action) => actionDelegate.isVisible(
|
||||||
|
appMode: appMode,
|
||||||
|
action: action,
|
||||||
|
))
|
||||||
|
.map((action) {
|
||||||
final enabled = actionDelegate.canApply(action);
|
final enabled = actionDelegate.canApply(action);
|
||||||
return CaptionedButton(
|
return CaptionedButton(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
enum MapNavigationButton { back, map }
|
enum MapNavigationButton { back, map, none }
|
||||||
|
|
||||||
class MapThemeData {
|
class MapThemeData {
|
||||||
final bool interactive, showCoordinateFilter;
|
final bool interactive, showCoordinateFilter;
|
||||||
|
|
Loading…
Reference in a new issue