selection: delete
This commit is contained in:
parent
1751b7b3d7
commit
a08c5a3369
8 changed files with 319 additions and 210 deletions
|
@ -31,7 +31,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
groupFactor = groupFactor ?? GroupFactor.month,
|
groupFactor = groupFactor ?? GroupFactor.month,
|
||||||
sortFactor = sortFactor ?? SortFactor.date {
|
sortFactor = sortFactor ?? SortFactor.date {
|
||||||
_subscriptions.add(source.eventBus.on<EntryAddedEvent>().listen((e) => onEntryAdded()));
|
_subscriptions.add(source.eventBus.on<EntryAddedEvent>().listen((e) => onEntryAdded()));
|
||||||
_subscriptions.add(source.eventBus.on<EntryRemovedEvent>().listen((e) => onEntryRemoved(e.entry)));
|
_subscriptions.add(source.eventBus.on<EntryRemovedEvent>().listen((e) => onEntryRemoved(e.entries)));
|
||||||
_subscriptions.add(source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => onMetadataChanged()));
|
_subscriptions.add(source.eventBus.on<CatalogMetadataChangedEvent>().listen((e) => onMetadataChanged()));
|
||||||
onEntryAdded();
|
onEntryAdded();
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
|
|
||||||
int get entryCount => _filteredEntries.length;
|
int get entryCount => _filteredEntries.length;
|
||||||
|
|
||||||
|
// sorted as displayed to the user, i.e. sorted then grouped, not an absolute order on all entries
|
||||||
List<ImageEntry> _sortedEntries;
|
List<ImageEntry> _sortedEntries;
|
||||||
|
|
||||||
List<ImageEntry> get sortedEntries {
|
List<ImageEntry> get sortedEntries {
|
||||||
|
@ -180,11 +181,12 @@ class CollectionLens with ChangeNotifier, CollectionActivityMixin, CollectionSel
|
||||||
_applyGroup();
|
_applyGroup();
|
||||||
}
|
}
|
||||||
|
|
||||||
void onEntryRemoved(ImageEntry entry) {
|
void onEntryRemoved(Iterable<ImageEntry> entries) {
|
||||||
// do not apply sort/group as section order change would surprise the user while browsing
|
// do not apply sort/group as section order change would surprise the user while browsing
|
||||||
_filteredEntries.remove(entry);
|
_filteredEntries.removeWhere(entries.contains);
|
||||||
_sortedEntries?.remove(entry);
|
_sortedEntries?.removeWhere(entries.contains);
|
||||||
sections.forEach((key, entries) => entries.remove(entry));
|
sections.forEach((key, sectionEntries) => sectionEntries.removeWhere(entries.contains));
|
||||||
|
selection.removeAll(entries);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,14 +225,14 @@ mixin CollectionSelectionMixin on CollectionActivityMixin {
|
||||||
|
|
||||||
Set<ImageEntry> get selection => _selection;
|
Set<ImageEntry> get selection => _selection;
|
||||||
|
|
||||||
bool isSelected(List<ImageEntry> entries) => entries.every(selection.contains);
|
bool isSelected(Iterable<ImageEntry> entries) => entries.every(selection.contains);
|
||||||
|
|
||||||
void addToSelection(List<ImageEntry> entries) {
|
void addToSelection(Iterable<ImageEntry> entries) {
|
||||||
_selection.addAll(entries);
|
_selection.addAll(entries);
|
||||||
selectionChangeNotifier.notifyListeners();
|
selectionChangeNotifier.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeFromSelection(List<ImageEntry> entries) {
|
void removeFromSelection(Iterable<ImageEntry> entries) {
|
||||||
_selection.removeAll(entries);
|
_selection.removeAll(entries);
|
||||||
selectionChangeNotifier.notifyListeners();
|
selectionChangeNotifier.notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,13 +138,9 @@ class CollectionSource {
|
||||||
eventBus.fire(const EntryAddedEvent());
|
eventBus.fire(const EntryAddedEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> delete(ImageEntry entry) async {
|
void removeEntries(Iterable<ImageEntry> entries) async {
|
||||||
final success = await entry.delete();
|
_rawEntries.remove(entries);
|
||||||
if (success) {
|
eventBus.fire(EntryRemovedEvent(entries));
|
||||||
_rawEntries.remove(entry);
|
|
||||||
eventBus.fire(EntryRemovedEvent(entry));
|
|
||||||
}
|
|
||||||
return success;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
String getUniqueAlbumName(String album) {
|
String getUniqueAlbumName(String album) {
|
||||||
|
@ -170,7 +166,7 @@ class EntryAddedEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
class EntryRemovedEvent {
|
class EntryRemovedEvent {
|
||||||
final ImageEntry entry;
|
final Iterable<ImageEntry> entries;
|
||||||
|
|
||||||
const EntryRemovedEvent(this.entry);
|
const EntryRemovedEvent(this.entries);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/settings.dart';
|
import 'package:aves/model/settings.dart';
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
|
import 'package:aves/services/image_file_service.dart';
|
||||||
import 'package:aves/utils/constants.dart';
|
import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/widgets/album/filter_bar.dart';
|
import 'package:aves/widgets/album/filter_bar.dart';
|
||||||
import 'package:aves/widgets/album/search/search_delegate.dart';
|
import 'package:aves/widgets/album/search/search_delegate.dart';
|
||||||
|
import 'package:aves/widgets/common/entry_actions.dart';
|
||||||
import 'package:aves/widgets/common/menu_row.dart';
|
import 'package:aves/widgets/common/menu_row.dart';
|
||||||
import 'package:aves/widgets/fullscreen/fullscreen_actions.dart';
|
|
||||||
import 'package:aves/widgets/stats/stats.dart';
|
import 'package:aves/widgets/stats/stats.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flushbar/flushbar.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
|
import 'package:percent_indicator/circular_percent_indicator.dart';
|
||||||
|
|
||||||
class CollectionAppBar extends StatefulWidget {
|
class CollectionAppBar extends StatefulWidget {
|
||||||
final ValueNotifier<double> appBarHeightNotifier;
|
final ValueNotifier<double> appBarHeightNotifier;
|
||||||
|
@ -150,17 +155,16 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
onPressed: _goToSearch,
|
onPressed: _goToSearch,
|
||||||
),
|
),
|
||||||
if (collection.isSelecting)
|
if (collection.isSelecting)
|
||||||
AnimatedBuilder(
|
...EntryActions.selection.map((action) => AnimatedBuilder(
|
||||||
animation: collection.selectionChangeNotifier,
|
animation: collection.selectionChangeNotifier,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
const action = FullscreenAction.share;
|
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: Icon(action.getIcon()),
|
icon: Icon(action.getIcon()),
|
||||||
onPressed: collection.selection.isEmpty ? null : _shareSelection,
|
onPressed: collection.selection.isEmpty ? null : () => _onSelectionActionSelected(action),
|
||||||
tooltip: action.getText(),
|
tooltip: action.getText(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
)),
|
||||||
Builder(
|
Builder(
|
||||||
builder: (context) => PopupMenuButton<CollectionAction>(
|
builder: (context) => PopupMenuButton<CollectionAction>(
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
|
@ -177,7 +181,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
onSelected: _onActionSelected,
|
onSelected: _onCollectionActionSelected,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
@ -221,7 +225,20 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onActionSelected(CollectionAction action) async {
|
void _onActivityChange() {
|
||||||
|
if (collection.isSelecting) {
|
||||||
|
_browseToSelectAnimation.forward();
|
||||||
|
} else {
|
||||||
|
_browseToSelectAnimation.reverse();
|
||||||
|
_searchFieldController.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateHeight() {
|
||||||
|
widget.appBarHeightNotifier.value = kToolbarHeight + (hasFilters ? FilterBar.preferredHeight : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCollectionActionSelected(CollectionAction action) async {
|
||||||
// wait for the popup menu to hide before proceeding with the action
|
// wait for the popup menu to hide before proceeding with the action
|
||||||
await Future.delayed(Constants.popupMenuTransitionDuration);
|
await Future.delayed(Constants.popupMenuTransitionDuration);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
@ -277,22 +294,108 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onSelectionActionSelected(EntryAction action) {
|
||||||
|
switch (action) {
|
||||||
|
case EntryAction.share:
|
||||||
|
_shareSelection();
|
||||||
|
break;
|
||||||
|
case EntryAction.delete:
|
||||||
|
_deleteSelection();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _shareSelection() {
|
void _shareSelection() {
|
||||||
final urisByMimeType = groupBy<ImageEntry, String>(collection.selection, (e) => e.mimeType).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
|
final urisByMimeType = groupBy<ImageEntry, String>(collection.selection, (e) => e.mimeType).map((k, v) => MapEntry(k, v.map((e) => e.uri).toList()));
|
||||||
AndroidAppService.share(urisByMimeType);
|
AndroidAppService.share(urisByMimeType);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onActivityChange() {
|
void _deleteSelection() {
|
||||||
if (collection.isSelecting) {
|
final selection = collection.selection.toList();
|
||||||
_browseToSelectAnimation.forward();
|
_showOpReport(
|
||||||
} else {
|
selection: selection,
|
||||||
_browseToSelectAnimation.reverse();
|
opStream: ImageFileService.delete(selection),
|
||||||
_searchFieldController.clear();
|
onDone: (processed) {
|
||||||
|
final deletedUris = processed.where((e) => e.success).map((e) => e.uri);
|
||||||
|
final deletedCount = deletedUris.length;
|
||||||
|
final selectionCount = selection.length;
|
||||||
|
if (deletedCount < selectionCount) {
|
||||||
|
_showFeedback(context, 'Failed to delete ${selectionCount - deletedCount} items');
|
||||||
}
|
}
|
||||||
|
if (deletedCount > 0) {
|
||||||
|
collection.source.removeEntries(selection.where((e) => deletedUris.contains(e.uri)));
|
||||||
|
}
|
||||||
|
collection.browse();
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateHeight() {
|
// selection action report overlay
|
||||||
widget.appBarHeightNotifier.value = kToolbarHeight + (hasFilters ? FilterBar.preferredHeight : 0);
|
|
||||||
|
OverlayEntry _opReportOverlayEntry;
|
||||||
|
|
||||||
|
static const _overlayAnimationDuration = Duration(milliseconds: 300);
|
||||||
|
|
||||||
|
void _showOpReport({
|
||||||
|
@required List<ImageEntry> selection,
|
||||||
|
@required Stream<ImageOpEvent> opStream,
|
||||||
|
@required void Function(Set<ImageOpEvent> processed) onDone,
|
||||||
|
}) {
|
||||||
|
final processed = <ImageOpEvent>{};
|
||||||
|
_opReportOverlayEntry = OverlayEntry(
|
||||||
|
builder: (context) {
|
||||||
|
return StreamBuilder<ImageOpEvent>(
|
||||||
|
stream: opStream,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
processed.add(snapshot.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget child = const SizedBox.shrink();
|
||||||
|
if (snapshot.hasError || snapshot.connectionState == ConnectionState.done) {
|
||||||
|
_hideOpReportOverlay().then((_) => onDone(processed));
|
||||||
|
} else if (snapshot.connectionState == ConnectionState.active) {
|
||||||
|
final percent = processed.length.toDouble() / selection.length;
|
||||||
|
child = CircularPercentIndicator(
|
||||||
|
percent: percent,
|
||||||
|
lineWidth: 16,
|
||||||
|
radius: 160,
|
||||||
|
backgroundColor: Colors.white24,
|
||||||
|
progressColor: Theme.of(context).accentColor,
|
||||||
|
animation: true,
|
||||||
|
center: Text(NumberFormat.percentPattern().format(percent)),
|
||||||
|
animateFromLastPercent: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AnimatedSwitcher(
|
||||||
|
duration: _overlayAnimationDuration,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Overlay.of(context).insert(_opReportOverlayEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _hideOpReportOverlay() async {
|
||||||
|
await Future.delayed(_overlayAnimationDuration);
|
||||||
|
_opReportOverlayEntry.remove();
|
||||||
|
_opReportOverlayEntry = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showFeedback(BuildContext context, String message) {
|
||||||
|
Flushbar(
|
||||||
|
message: message,
|
||||||
|
margin: const EdgeInsets.all(8),
|
||||||
|
borderRadius: 8,
|
||||||
|
borderColor: Colors.white30,
|
||||||
|
borderWidth: 0.5,
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
flushbarPosition: FlushbarPosition.TOP,
|
||||||
|
animationDuration: const Duration(milliseconds: 600),
|
||||||
|
).show(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,9 +4,9 @@ import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/services/android_app_service.dart';
|
import 'package:aves/services/android_app_service.dart';
|
||||||
import 'package:aves/services/image_file_service.dart';
|
import 'package:aves/services/image_file_service.dart';
|
||||||
|
import 'package:aves/widgets/common/entry_actions.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||||
import 'package:aves/widgets/fullscreen/debug.dart';
|
import 'package:aves/widgets/fullscreen/debug.dart';
|
||||||
import 'package:aves/widgets/fullscreen/fullscreen_actions.dart';
|
|
||||||
import 'package:flushbar/flushbar.dart';
|
import 'package:flushbar/flushbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
@ -15,58 +15,58 @@ import 'package:pdf/widgets.dart' as pdf;
|
||||||
import 'package:pedantic/pedantic.dart';
|
import 'package:pedantic/pedantic.dart';
|
||||||
import 'package:printing/printing.dart';
|
import 'package:printing/printing.dart';
|
||||||
|
|
||||||
class FullscreenActionDelegate {
|
class EntryActionDelegate {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final VoidCallback showInfo;
|
final VoidCallback showInfo;
|
||||||
|
|
||||||
FullscreenActionDelegate({
|
EntryActionDelegate({
|
||||||
@required this.collection,
|
@required this.collection,
|
||||||
@required this.showInfo,
|
@required this.showInfo,
|
||||||
});
|
});
|
||||||
|
|
||||||
bool get hasCollection => collection != null;
|
bool get hasCollection => collection != null;
|
||||||
|
|
||||||
void onActionSelected(BuildContext context, ImageEntry entry, FullscreenAction action) {
|
void onActionSelected(BuildContext context, ImageEntry entry, EntryAction action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case FullscreenAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
entry.toggleFavourite();
|
entry.toggleFavourite();
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.delete:
|
case EntryAction.delete:
|
||||||
_showDeleteDialog(context, entry);
|
_showDeleteDialog(context, entry);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.edit:
|
case EntryAction.edit:
|
||||||
AndroidAppService.edit(entry.uri, entry.mimeType);
|
AndroidAppService.edit(entry.uri, entry.mimeType);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.info:
|
case EntryAction.info:
|
||||||
showInfo();
|
showInfo();
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.rename:
|
case EntryAction.rename:
|
||||||
_showRenameDialog(context, entry);
|
_showRenameDialog(context, entry);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.open:
|
case EntryAction.open:
|
||||||
AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype);
|
AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.openMap:
|
case EntryAction.openMap:
|
||||||
AndroidAppService.openMap(entry.geoUri);
|
AndroidAppService.openMap(entry.geoUri);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.print:
|
case EntryAction.print:
|
||||||
_print(entry);
|
_print(entry);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.rotateCCW:
|
case EntryAction.rotateCCW:
|
||||||
_rotate(context, entry, clockwise: false);
|
_rotate(context, entry, clockwise: false);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.rotateCW:
|
case EntryAction.rotateCW:
|
||||||
_rotate(context, entry, clockwise: true);
|
_rotate(context, entry, clockwise: true);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.setAs:
|
case EntryAction.setAs:
|
||||||
AndroidAppService.setAs(entry.uri, entry.mimeType);
|
AndroidAppService.setAs(entry.uri, entry.mimeType);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.share:
|
case EntryAction.share:
|
||||||
AndroidAppService.share({
|
AndroidAppService.share({
|
||||||
entry.mimeType: [entry.uri]
|
entry.mimeType: [entry.uri]
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.debug:
|
case EntryAction.debug:
|
||||||
_goToDebug(context, entry);
|
_goToDebug(context, entry);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -146,13 +146,16 @@ class FullscreenActionDelegate {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (confirmed == null || !confirmed) return;
|
if (confirmed == null || !confirmed) return;
|
||||||
if (hasCollection) {
|
if (!await entry.delete()) {
|
||||||
if (!await collection.source.delete(entry)) {
|
|
||||||
_showFeedback(context, 'Failed');
|
_showFeedback(context, 'Failed');
|
||||||
} else if (collection.sortedEntries.isEmpty) {
|
} else if (hasCollection) {
|
||||||
|
// update collection
|
||||||
|
collection.source.removeEntries([entry]);
|
||||||
|
if (collection.sortedEntries.isEmpty) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
} else if (await entry.delete()) {
|
} else {
|
||||||
|
// leave viewer
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
99
lib/widgets/common/entry_actions.dart
Normal file
99
lib/widgets/common/entry_actions.dart
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:outline_material_icons/outline_material_icons.dart';
|
||||||
|
|
||||||
|
enum EntryAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share, toggleFavourite, debug }
|
||||||
|
|
||||||
|
class EntryActions {
|
||||||
|
static const selection = [
|
||||||
|
EntryAction.share,
|
||||||
|
EntryAction.delete,
|
||||||
|
];
|
||||||
|
|
||||||
|
static const inApp = [
|
||||||
|
EntryAction.info,
|
||||||
|
EntryAction.toggleFavourite,
|
||||||
|
EntryAction.share,
|
||||||
|
EntryAction.delete,
|
||||||
|
EntryAction.rename,
|
||||||
|
EntryAction.rotateCCW,
|
||||||
|
EntryAction.rotateCW,
|
||||||
|
EntryAction.print,
|
||||||
|
];
|
||||||
|
|
||||||
|
static const externalApp = [
|
||||||
|
EntryAction.edit,
|
||||||
|
EntryAction.open,
|
||||||
|
EntryAction.setAs,
|
||||||
|
EntryAction.openMap,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ExtraEntryAction on EntryAction {
|
||||||
|
String getText() {
|
||||||
|
switch (this) {
|
||||||
|
// in app actions
|
||||||
|
case EntryAction.toggleFavourite:
|
||||||
|
// different data depending on toggle state
|
||||||
|
return null;
|
||||||
|
case EntryAction.delete:
|
||||||
|
return 'Delete';
|
||||||
|
case EntryAction.info:
|
||||||
|
return 'Info';
|
||||||
|
case EntryAction.rename:
|
||||||
|
return 'Rename';
|
||||||
|
case EntryAction.rotateCCW:
|
||||||
|
return 'Rotate left';
|
||||||
|
case EntryAction.rotateCW:
|
||||||
|
return 'Rotate right';
|
||||||
|
case EntryAction.print:
|
||||||
|
return 'Print';
|
||||||
|
case EntryAction.share:
|
||||||
|
return 'Share';
|
||||||
|
// external app actions
|
||||||
|
case EntryAction.edit:
|
||||||
|
return 'Edit with…';
|
||||||
|
case EntryAction.open:
|
||||||
|
return 'Open with…';
|
||||||
|
case EntryAction.setAs:
|
||||||
|
return 'Set as…';
|
||||||
|
case EntryAction.openMap:
|
||||||
|
return 'Show on map…';
|
||||||
|
case EntryAction.debug:
|
||||||
|
return 'Debug';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData getIcon() {
|
||||||
|
switch (this) {
|
||||||
|
// in app actions
|
||||||
|
case EntryAction.toggleFavourite:
|
||||||
|
// different data depending on toggle state
|
||||||
|
return null;
|
||||||
|
case EntryAction.delete:
|
||||||
|
return AIcons.delete;
|
||||||
|
case EntryAction.info:
|
||||||
|
return OMIcons.info;
|
||||||
|
case EntryAction.rename:
|
||||||
|
return OMIcons.title;
|
||||||
|
case EntryAction.rotateCCW:
|
||||||
|
return AIcons.rotateLeft;
|
||||||
|
case EntryAction.rotateCW:
|
||||||
|
return AIcons.rotateRight;
|
||||||
|
case EntryAction.print:
|
||||||
|
return AIcons.print;
|
||||||
|
case EntryAction.share:
|
||||||
|
return AIcons.share;
|
||||||
|
// external app actions
|
||||||
|
case EntryAction.edit:
|
||||||
|
case EntryAction.open:
|
||||||
|
case EntryAction.setAs:
|
||||||
|
case EntryAction.openMap:
|
||||||
|
return null;
|
||||||
|
case EntryAction.debug:
|
||||||
|
return OMIcons.whatshot;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,94 +0,0 @@
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:outline_material_icons/outline_material_icons.dart';
|
|
||||||
|
|
||||||
enum FullscreenAction { delete, edit, info, open, openMap, print, rename, rotateCCW, rotateCW, setAs, share, toggleFavourite, debug }
|
|
||||||
|
|
||||||
class FullscreenActions {
|
|
||||||
static const inApp = [
|
|
||||||
FullscreenAction.info,
|
|
||||||
FullscreenAction.toggleFavourite,
|
|
||||||
FullscreenAction.share,
|
|
||||||
FullscreenAction.delete,
|
|
||||||
FullscreenAction.rename,
|
|
||||||
FullscreenAction.rotateCCW,
|
|
||||||
FullscreenAction.rotateCW,
|
|
||||||
FullscreenAction.print,
|
|
||||||
];
|
|
||||||
|
|
||||||
static const externalApp = [
|
|
||||||
FullscreenAction.edit,
|
|
||||||
FullscreenAction.open,
|
|
||||||
FullscreenAction.setAs,
|
|
||||||
FullscreenAction.openMap,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ExtraFullscreenAction on FullscreenAction {
|
|
||||||
String getText() {
|
|
||||||
switch (this) {
|
|
||||||
// in app actions
|
|
||||||
case FullscreenAction.toggleFavourite:
|
|
||||||
// different data depending on toggle state
|
|
||||||
return null;
|
|
||||||
case FullscreenAction.delete:
|
|
||||||
return 'Delete';
|
|
||||||
case FullscreenAction.info:
|
|
||||||
return 'Info';
|
|
||||||
case FullscreenAction.rename:
|
|
||||||
return 'Rename';
|
|
||||||
case FullscreenAction.rotateCCW:
|
|
||||||
return 'Rotate left';
|
|
||||||
case FullscreenAction.rotateCW:
|
|
||||||
return 'Rotate right';
|
|
||||||
case FullscreenAction.print:
|
|
||||||
return 'Print';
|
|
||||||
case FullscreenAction.share:
|
|
||||||
return 'Share';
|
|
||||||
// external app actions
|
|
||||||
case FullscreenAction.edit:
|
|
||||||
return 'Edit with…';
|
|
||||||
case FullscreenAction.open:
|
|
||||||
return 'Open with…';
|
|
||||||
case FullscreenAction.setAs:
|
|
||||||
return 'Set as…';
|
|
||||||
case FullscreenAction.openMap:
|
|
||||||
return 'Show on map…';
|
|
||||||
case FullscreenAction.debug:
|
|
||||||
return 'Debug';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
IconData getIcon() {
|
|
||||||
switch (this) {
|
|
||||||
// in app actions
|
|
||||||
case FullscreenAction.toggleFavourite:
|
|
||||||
// different data depending on toggle state
|
|
||||||
return null;
|
|
||||||
case FullscreenAction.delete:
|
|
||||||
return AIcons.delete;
|
|
||||||
case FullscreenAction.info:
|
|
||||||
return OMIcons.info;
|
|
||||||
case FullscreenAction.rename:
|
|
||||||
return OMIcons.title;
|
|
||||||
case FullscreenAction.rotateCCW:
|
|
||||||
return AIcons.rotateLeft;
|
|
||||||
case FullscreenAction.rotateCW:
|
|
||||||
return AIcons.rotateRight;
|
|
||||||
case FullscreenAction.print:
|
|
||||||
return AIcons.print;
|
|
||||||
case FullscreenAction.share:
|
|
||||||
return AIcons.share;
|
|
||||||
// external app actions
|
|
||||||
case FullscreenAction.edit:
|
|
||||||
case FullscreenAction.open:
|
|
||||||
case FullscreenAction.setAs:
|
|
||||||
case FullscreenAction.openMap:
|
|
||||||
return null;
|
|
||||||
case FullscreenAction.debug:
|
|
||||||
return OMIcons.whatshot;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -9,7 +9,7 @@ import 'package:aves/utils/constants.dart';
|
||||||
import 'package:aves/widgets/album/collection_page.dart';
|
import 'package:aves/widgets/album/collection_page.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
|
||||||
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
|
||||||
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
|
import 'package:aves/widgets/common/entry_action_delegate.dart';
|
||||||
import 'package:aves/widgets/fullscreen/image_page.dart';
|
import 'package:aves/widgets/fullscreen/image_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/overlay/bottom.dart';
|
import 'package:aves/widgets/fullscreen/overlay/bottom.dart';
|
||||||
|
@ -49,7 +49,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
Animation<double> _topOverlayScale, _bottomOverlayScale;
|
Animation<double> _topOverlayScale, _bottomOverlayScale;
|
||||||
Animation<Offset> _bottomOverlayOffset;
|
Animation<Offset> _bottomOverlayOffset;
|
||||||
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
||||||
FullscreenActionDelegate _actionDelegate;
|
EntryActionDelegate _actionDelegate;
|
||||||
final List<Tuple2<String, IjkMediaController>> _videoControllers = [];
|
final List<Tuple2<String, IjkMediaController>> _videoControllers = [];
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
CollectionLens get collection => widget.collection;
|
||||||
|
@ -93,7 +93,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
curve: Curves.easeOutQuad,
|
curve: Curves.easeOutQuad,
|
||||||
));
|
));
|
||||||
_overlayVisible.addListener(_onOverlayVisibleChange);
|
_overlayVisible.addListener(_onOverlayVisibleChange);
|
||||||
_actionDelegate = FullscreenActionDelegate(
|
_actionDelegate = EntryActionDelegate(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
showInfo: () => _goToVerticalPage(infoPage),
|
showInfo: () => _goToVerticalPage(infoPage),
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/widgets/common/entry_actions.dart';
|
||||||
import 'package:aves/widgets/common/fx/sweeper.dart';
|
import 'package:aves/widgets/common/fx/sweeper.dart';
|
||||||
import 'package:aves/widgets/common/icons.dart';
|
import 'package:aves/widgets/common/icons.dart';
|
||||||
import 'package:aves/widgets/common/menu_row.dart';
|
import 'package:aves/widgets/common/menu_row.dart';
|
||||||
import 'package:aves/widgets/fullscreen/fullscreen_actions.dart';
|
|
||||||
import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -15,7 +15,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
final int index;
|
final int index;
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
final EdgeInsets viewInsets, viewPadding;
|
final EdgeInsets viewInsets, viewPadding;
|
||||||
final Function(FullscreenAction value) onActionSelected;
|
final Function(EntryAction value) onActionSelected;
|
||||||
final bool canToggleFavourite;
|
final bool canToggleFavourite;
|
||||||
|
|
||||||
ImageEntry get entry => entries[index];
|
ImageEntry get entry => entries[index];
|
||||||
|
@ -53,12 +53,12 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
final quickActionCount = min(targetCount, availableCount);
|
final quickActionCount = min(targetCount, availableCount);
|
||||||
|
|
||||||
final quickActions = [
|
final quickActions = [
|
||||||
FullscreenAction.toggleFavourite,
|
EntryAction.toggleFavourite,
|
||||||
FullscreenAction.share,
|
EntryAction.share,
|
||||||
FullscreenAction.delete,
|
EntryAction.delete,
|
||||||
].where(_canDo).take(quickActionCount);
|
].where(_canDo).take(quickActionCount);
|
||||||
final inAppActions = FullscreenActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo);
|
final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo);
|
||||||
final externalAppActions = FullscreenActions.externalApp.where(_canDo);
|
final externalAppActions = EntryActions.externalApp.where(_canDo);
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
@ -70,14 +70,14 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
...quickActions.map(_buildOverlayButton),
|
...quickActions.map(_buildOverlayButton),
|
||||||
OverlayButton(
|
OverlayButton(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
child: PopupMenuButton<FullscreenAction>(
|
child: PopupMenuButton<EntryAction>(
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
...inAppActions.map(_buildPopupMenuItem),
|
...inAppActions.map(_buildPopupMenuItem),
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
...externalAppActions.map(_buildPopupMenuItem),
|
...externalAppActions.map(_buildPopupMenuItem),
|
||||||
if (kDebugMode) ...[
|
if (kDebugMode) ...[
|
||||||
const PopupMenuDivider(),
|
const PopupMenuDivider(),
|
||||||
_buildPopupMenuItem(FullscreenAction.debug),
|
_buildPopupMenuItem(EntryAction.debug),
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
onSelected: onActionSelected,
|
onSelected: onActionSelected,
|
||||||
|
@ -93,37 +93,37 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _canDo(FullscreenAction action) {
|
bool _canDo(EntryAction action) {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case FullscreenAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
return canToggleFavourite;
|
return canToggleFavourite;
|
||||||
case FullscreenAction.delete:
|
case EntryAction.delete:
|
||||||
case FullscreenAction.rename:
|
case EntryAction.rename:
|
||||||
return entry.canEdit;
|
return entry.canEdit;
|
||||||
case FullscreenAction.rotateCCW:
|
case EntryAction.rotateCCW:
|
||||||
case FullscreenAction.rotateCW:
|
case EntryAction.rotateCW:
|
||||||
return entry.canRotate;
|
return entry.canRotate;
|
||||||
case FullscreenAction.print:
|
case EntryAction.print:
|
||||||
return entry.canPrint;
|
return entry.canPrint;
|
||||||
case FullscreenAction.openMap:
|
case EntryAction.openMap:
|
||||||
return entry.hasGps;
|
return entry.hasGps;
|
||||||
case FullscreenAction.share:
|
case EntryAction.share:
|
||||||
case FullscreenAction.info:
|
case EntryAction.info:
|
||||||
case FullscreenAction.open:
|
case EntryAction.open:
|
||||||
case FullscreenAction.edit:
|
case EntryAction.edit:
|
||||||
case FullscreenAction.setAs:
|
case EntryAction.setAs:
|
||||||
return true;
|
return true;
|
||||||
case FullscreenAction.debug:
|
case EntryAction.debug:
|
||||||
return kDebugMode;
|
return kDebugMode;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildOverlayButton(FullscreenAction action) {
|
Widget _buildOverlayButton(EntryAction action) {
|
||||||
Widget child;
|
Widget child;
|
||||||
final onPressed = () => onActionSelected?.call(action);
|
final onPressed = () => onActionSelected?.call(action);
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case FullscreenAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
child = ValueListenableBuilder<bool>(
|
child = ValueListenableBuilder<bool>(
|
||||||
valueListenable: entry.isFavouriteNotifier,
|
valueListenable: entry.isFavouriteNotifier,
|
||||||
builder: (context, isFavourite, child) => Stack(
|
builder: (context, isFavourite, child) => Stack(
|
||||||
|
@ -142,24 +142,24 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.info:
|
case EntryAction.info:
|
||||||
case FullscreenAction.share:
|
case EntryAction.share:
|
||||||
case FullscreenAction.delete:
|
case EntryAction.delete:
|
||||||
case FullscreenAction.rename:
|
case EntryAction.rename:
|
||||||
case FullscreenAction.rotateCCW:
|
case EntryAction.rotateCCW:
|
||||||
case FullscreenAction.rotateCW:
|
case EntryAction.rotateCW:
|
||||||
case FullscreenAction.print:
|
case EntryAction.print:
|
||||||
child = IconButton(
|
child = IconButton(
|
||||||
icon: Icon(action.getIcon()),
|
icon: Icon(action.getIcon()),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
tooltip: action.getText(),
|
tooltip: action.getText(),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.openMap:
|
case EntryAction.openMap:
|
||||||
case FullscreenAction.open:
|
case EntryAction.open:
|
||||||
case FullscreenAction.edit:
|
case EntryAction.edit:
|
||||||
case FullscreenAction.setAs:
|
case EntryAction.setAs:
|
||||||
case FullscreenAction.debug:
|
case EntryAction.debug:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return child != null
|
return child != null
|
||||||
|
@ -173,11 +173,11 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
: const SizedBox.shrink();
|
: const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
PopupMenuEntry<FullscreenAction> _buildPopupMenuItem(FullscreenAction action) {
|
PopupMenuEntry<EntryAction> _buildPopupMenuItem(EntryAction action) {
|
||||||
Widget child;
|
Widget child;
|
||||||
switch (action) {
|
switch (action) {
|
||||||
// in app actions
|
// in app actions
|
||||||
case FullscreenAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
child = entry.isFavouriteNotifier.value
|
child = entry.isFavouriteNotifier.value
|
||||||
? const MenuRow(
|
? const MenuRow(
|
||||||
text: 'Remove from favourites',
|
text: 'Remove from favourites',
|
||||||
|
@ -188,21 +188,21 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
icon: AIcons.favourite,
|
icon: AIcons.favourite,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case FullscreenAction.info:
|
case EntryAction.info:
|
||||||
case FullscreenAction.share:
|
case EntryAction.share:
|
||||||
case FullscreenAction.delete:
|
case EntryAction.delete:
|
||||||
case FullscreenAction.rename:
|
case EntryAction.rename:
|
||||||
case FullscreenAction.rotateCCW:
|
case EntryAction.rotateCCW:
|
||||||
case FullscreenAction.rotateCW:
|
case EntryAction.rotateCW:
|
||||||
case FullscreenAction.print:
|
case EntryAction.print:
|
||||||
case FullscreenAction.debug:
|
case EntryAction.debug:
|
||||||
child = MenuRow(text: action.getText(), icon: action.getIcon());
|
child = MenuRow(text: action.getText(), icon: action.getIcon());
|
||||||
break;
|
break;
|
||||||
// external app actions
|
// external app actions
|
||||||
case FullscreenAction.edit:
|
case EntryAction.edit:
|
||||||
case FullscreenAction.open:
|
case EntryAction.open:
|
||||||
case FullscreenAction.setAs:
|
case EntryAction.setAs:
|
||||||
case FullscreenAction.openMap:
|
case EntryAction.openMap:
|
||||||
child = Text(action.getText());
|
child = Text(action.getText());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue