#407 quick copy/move
This commit is contained in:
parent
5a6153b970
commit
f57e2306e2
33 changed files with 626 additions and 115 deletions
|
@ -7,7 +7,7 @@ All notable changes to this project will be documented in this file.
|
|||
### Added
|
||||
|
||||
- Viewer: optionally show rating & tags on overlay
|
||||
- Viewer: long press on rating quick action for quicker rating
|
||||
- Viewer: long press on copy/move/rating quick action for quicker action
|
||||
- Search: missing address, portrait, landscape filters
|
||||
- Lithuanian translation (thanks Gediminas Murauskas)
|
||||
|
||||
|
|
|
@ -29,6 +29,8 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
Settings._private();
|
||||
|
||||
static const int moveDestinationAlbumMax = 3;
|
||||
|
||||
static const Set<String> _internalKeys = {
|
||||
hasAcceptedTermsKey,
|
||||
catalogTimeZoneKey,
|
||||
|
@ -37,6 +39,7 @@ class Settings extends ChangeNotifier {
|
|||
platformAccelerometerRotationKey,
|
||||
platformTransitionAnimationScaleKey,
|
||||
topEntryIdsKey,
|
||||
moveDestinationAlbumsKey,
|
||||
};
|
||||
static const _widgetKeyPrefix = 'widget_';
|
||||
|
||||
|
@ -51,6 +54,7 @@ class Settings extends ChangeNotifier {
|
|||
static const tileLayoutPrefixKey = 'tile_layout_';
|
||||
static const entryRenamingPatternKey = 'entry_renaming_pattern';
|
||||
static const topEntryIdsKey = 'top_entry_ids';
|
||||
static const moveDestinationAlbumsKey = 'move_destination_albums';
|
||||
|
||||
// display
|
||||
static const displayRefreshRateModeKey = 'display_refresh_rate_mode';
|
||||
|
@ -314,6 +318,10 @@ class Settings extends ChangeNotifier {
|
|||
|
||||
set topEntryIds(List<int>? newValue) => setAndNotify(topEntryIdsKey, newValue?.map((id) => id.toString()).whereNotNull().toList());
|
||||
|
||||
List<String> get moveDestinationAlbums => getStringList(moveDestinationAlbumsKey) ?? [];
|
||||
|
||||
set moveDestinationAlbums(List<String> newValue) => setAndNotify(moveDestinationAlbumsKey, newValue.take(Settings.moveDestinationAlbumMax).toList());
|
||||
|
||||
// display
|
||||
|
||||
DisplayRefreshRateMode get displayRefreshRateMode => getEnumOrDefault(displayRefreshRateModeKey, SettingsDefaults.displayRefreshRateMode, DisplayRefreshRateMode.values);
|
||||
|
|
|
@ -317,7 +317,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
|
|||
|
||||
Future<void> _move(BuildContext context, {required MoveType moveType}) async {
|
||||
final entries = _getTargetItems(context);
|
||||
await move(context, moveType: moveType, entries: entries);
|
||||
await doMove(context, moveType: moveType, entries: entries);
|
||||
|
||||
_leaveSelectionMode(context);
|
||||
}
|
||||
|
|
|
@ -34,50 +34,20 @@ import 'package:flutter/material.dart';
|
|||
import 'package:provider/provider.dart';
|
||||
|
||||
mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
||||
Future<void> move(
|
||||
Future<void> doQuickMove(
|
||||
BuildContext context, {
|
||||
required MoveType moveType,
|
||||
required Set<AvesEntry> entries,
|
||||
required Map<String, Iterable<AvesEntry>> entriesByDestination,
|
||||
bool hideShowAction = false,
|
||||
VoidCallback? onSuccess,
|
||||
}) async {
|
||||
final entries = entriesByDestination.values.expand((v) => v).toSet();
|
||||
final todoCount = entries.length;
|
||||
assert(todoCount > 0);
|
||||
|
||||
final toBin = moveType == MoveType.toBin;
|
||||
final copy = moveType == MoveType.copy;
|
||||
|
||||
final l10n = context.l10n;
|
||||
if (toBin) {
|
||||
if (!await showConfirmationDialog(
|
||||
context: context,
|
||||
type: ConfirmationDialog.moveToBin,
|
||||
message: l10n.binEntriesConfirmationDialogMessage(todoCount),
|
||||
confirmationButtonLabel: l10n.deleteButtonLabel,
|
||||
)) return;
|
||||
}
|
||||
|
||||
final entriesByDestination = <String, Set<AvesEntry>>{};
|
||||
switch (moveType) {
|
||||
case MoveType.copy:
|
||||
case MoveType.move:
|
||||
case MoveType.export:
|
||||
final destinationAlbum = await pickAlbum(context: context, moveType: moveType);
|
||||
if (destinationAlbum == null) return;
|
||||
entriesByDestination[destinationAlbum] = entries;
|
||||
break;
|
||||
case MoveType.toBin:
|
||||
entriesByDestination[AndroidFileUtils.trashDirPath] = entries;
|
||||
break;
|
||||
case MoveType.fromBin:
|
||||
groupBy<AvesEntry, String?>(entries, (e) => e.directory).forEach((originAlbum, dirEntries) {
|
||||
if (originAlbum != null) {
|
||||
entriesByDestination[originAlbum] = dirEntries.toSet();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// permission for modification at destinations
|
||||
final destinationAlbums = entriesByDestination.keys.toSet();
|
||||
if (!await checkStoragePermissionForAlbums(context, destinationAlbums)) return;
|
||||
|
@ -90,6 +60,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
if (!await checkFreeSpaceForMove(context, entries, destinationAlbum, moveType)) return;
|
||||
});
|
||||
|
||||
final l10n = context.l10n;
|
||||
var nameConflictStrategy = NameConflictStrategy.rename;
|
||||
if (!toBin && destinationAlbums.length == 1) {
|
||||
final destinationDirectory = Directory(destinationAlbums.single);
|
||||
|
@ -174,7 +145,7 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
// local context may be deactivated when action is triggered after navigation
|
||||
final context = AvesApp.navigatorKey.currentContext;
|
||||
if (context != null) {
|
||||
move(
|
||||
doMove(
|
||||
context,
|
||||
moveType: MoveType.fromBin,
|
||||
entries: movedEntries,
|
||||
|
@ -213,6 +184,55 @@ mixin EntryStorageMixin on FeedbackMixin, PermissionAwareMixin, SizeAwareMixin {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> doMove(
|
||||
BuildContext context, {
|
||||
required MoveType moveType,
|
||||
required Set<AvesEntry> entries,
|
||||
bool hideShowAction = false,
|
||||
VoidCallback? onSuccess,
|
||||
}) async {
|
||||
if (moveType == MoveType.toBin) {
|
||||
final l10n = context.l10n;
|
||||
if (!await showConfirmationDialog(
|
||||
context: context,
|
||||
type: ConfirmationDialog.moveToBin,
|
||||
message: l10n.binEntriesConfirmationDialogMessage(entries.length),
|
||||
confirmationButtonLabel: l10n.deleteButtonLabel,
|
||||
)) return;
|
||||
}
|
||||
|
||||
final entriesByDestination = <String, Set<AvesEntry>>{};
|
||||
switch (moveType) {
|
||||
case MoveType.copy:
|
||||
case MoveType.move:
|
||||
case MoveType.export:
|
||||
final destinationAlbum = await pickAlbum(context: context, moveType: moveType);
|
||||
if (destinationAlbum == null) return;
|
||||
|
||||
settings.moveDestinationAlbums = settings.moveDestinationAlbums
|
||||
..remove(destinationAlbum)
|
||||
..insert(0, destinationAlbum);
|
||||
entriesByDestination[destinationAlbum] = entries;
|
||||
break;
|
||||
case MoveType.toBin:
|
||||
entriesByDestination[AndroidFileUtils.trashDirPath] = entries;
|
||||
break;
|
||||
case MoveType.fromBin:
|
||||
groupBy<AvesEntry, String?>(entries, (e) => e.directory).forEach((originAlbum, dirEntries) {
|
||||
if (originAlbum != null) {
|
||||
entriesByDestination[originAlbum] = dirEntries.toSet();
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
await doQuickMove(
|
||||
context,
|
||||
moveType: moveType,
|
||||
entriesByDestination: entriesByDestination,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> rename(
|
||||
BuildContext context, {
|
||||
required Map<AvesEntry, String> entriesToNewName,
|
||||
|
|
67
lib/widgets/common/app_bar/move_button.dart
Normal file
67
lib/widgets/common/app_bar/move_button.dart
Normal file
|
@ -0,0 +1,67 @@
|
|||
import 'package:aves/model/actions/entry_actions.dart';
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/filters/filters.dart';
|
||||
import 'package:aves/model/settings/settings.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/widgets/common/app_bar/quick_choosers/album_chooser.dart';
|
||||
import 'package:aves/widgets/common/app_bar/quick_choosers/chooser_button.dart';
|
||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||
import 'package:aves/widgets/filter_grids/common/filter_nav_page.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class MoveButton extends ChooserQuickButton<String> {
|
||||
final bool copy;
|
||||
|
||||
const MoveButton({
|
||||
super.key,
|
||||
required this.copy,
|
||||
super.chooserPosition,
|
||||
super.onChooserValue,
|
||||
required super.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MoveButton> createState() => _MoveQuickButtonState();
|
||||
}
|
||||
|
||||
class _MoveQuickButtonState extends ChooserQuickButtonState<MoveButton, String> {
|
||||
EntryAction get action => widget.copy ? EntryAction.copy : EntryAction.move;
|
||||
|
||||
@override
|
||||
Widget get icon => action.getIcon();
|
||||
|
||||
@override
|
||||
String get tooltip => action.getText(context);
|
||||
|
||||
@override
|
||||
String? get defaultValue => null;
|
||||
|
||||
@override
|
||||
Widget buildChooser(Animation<double> animation) {
|
||||
final options = settings.moveDestinationAlbums;
|
||||
final takeCount = Settings.moveDestinationAlbumMax - options.length;
|
||||
if (takeCount > 0) {
|
||||
final source = context.read<CollectionSource>();
|
||||
final filters = source.rawAlbums.whereNot(options.contains).map((album) => AlbumFilter(album, null)).toSet();
|
||||
final allMapEntries = filters.map((filter) => FilterGridItem(filter, source.recentEntry(filter))).toList();
|
||||
allMapEntries.sort(FilterNavigationPage.compareFiltersByDate);
|
||||
options.addAll(allMapEntries.take(takeCount).map((v) => v.filter.album));
|
||||
}
|
||||
|
||||
return MediaQueryDataProvider(
|
||||
child: FadeTransition(
|
||||
opacity: animation,
|
||||
child: ScaleTransition(
|
||||
scale: animation,
|
||||
child: AlbumQuickChooser(
|
||||
valueNotifier: chooserValueNotifier,
|
||||
pointerGlobalPosition: pointerGlobalPosition,
|
||||
options: widget.chooserPosition == PopupMenuPosition.over ? options.reversed.toList() : options,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
35
lib/widgets/common/app_bar/quick_choosers/album_chooser.dart
Normal file
35
lib/widgets/common/app_bar/quick_choosers/album_chooser.dart
Normal file
|
@ -0,0 +1,35 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/model/filters/album.dart';
|
||||
import 'package:aves/model/source/collection_source.dart';
|
||||
import 'package:aves/widgets/common/app_bar/quick_choosers/filter_chooser.dart';
|
||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AlbumQuickChooser extends StatelessWidget {
|
||||
final ValueNotifier<String?> valueNotifier;
|
||||
final List<String> options;
|
||||
final Stream<Offset> pointerGlobalPosition;
|
||||
|
||||
const AlbumQuickChooser({
|
||||
super.key,
|
||||
required this.valueNotifier,
|
||||
required this.options,
|
||||
required this.pointerGlobalPosition,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final source = context.read<CollectionSource>();
|
||||
return FilterQuickChooser<String>(
|
||||
valueNotifier: valueNotifier,
|
||||
options: options,
|
||||
pointerGlobalPosition: pointerGlobalPosition,
|
||||
buildFilterChip: (context, album) => AvesFilterChip(
|
||||
filter: AlbumFilter(album, source.getAlbumDisplayName(context, album)),
|
||||
showGenericIcon: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -39,7 +39,7 @@ abstract class ChooserQuickButtonState<T extends ChooserQuickButton<U>, U> exten
|
|||
|
||||
ValueNotifier<U?> get chooserValueNotifier => _chooserValueNotifier;
|
||||
|
||||
Stream<LongPressMoveUpdateDetails> get moveUpdates => _moveUpdateStreamController.stream;
|
||||
Stream<Offset> get pointerGlobalPosition => _moveUpdateStreamController.stream.map((event) => event.globalPosition);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
|
|
146
lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart
Normal file
146
lib/widgets/common/app_bar/quick_choosers/filter_chooser.dart
Normal file
|
@ -0,0 +1,146 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
||||
import 'package:aves_ui/aves_ui.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class FilterQuickChooser<T> extends StatefulWidget {
|
||||
final ValueNotifier<T?> valueNotifier;
|
||||
final List<T> options;
|
||||
final Stream<Offset> pointerGlobalPosition;
|
||||
final Widget Function(BuildContext context, T album) buildFilterChip;
|
||||
|
||||
const FilterQuickChooser({
|
||||
super.key,
|
||||
required this.valueNotifier,
|
||||
required this.options,
|
||||
required this.pointerGlobalPosition,
|
||||
required this.buildFilterChip,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FilterQuickChooser<T>> createState() => _FilterQuickChooserState<T>();
|
||||
}
|
||||
|
||||
class _FilterQuickChooserState<T> extends State<FilterQuickChooser<T>> {
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
final ValueNotifier<Rect> _selectedRowRect = ValueNotifier(Rect.zero);
|
||||
|
||||
ValueNotifier<T?> get valueNotifier => widget.valueNotifier;
|
||||
|
||||
List<T> get options => widget.options;
|
||||
|
||||
static const margin = EdgeInsets.all(8);
|
||||
static const padding = EdgeInsets.all(8);
|
||||
static const double intraPadding = 8;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_registerWidget(widget);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant FilterQuickChooser<T> oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_unregisterWidget(oldWidget);
|
||||
_registerWidget(widget);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_unregisterWidget(widget);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _registerWidget(FilterQuickChooser<T> widget) {
|
||||
_subscriptions.add(widget.pointerGlobalPosition.listen(_onPointerMove));
|
||||
}
|
||||
|
||||
void _unregisterWidget(FilterQuickChooser<T> widget) {
|
||||
_subscriptions
|
||||
..forEach((sub) => sub.cancel())
|
||||
..clear();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: margin,
|
||||
child: Material(
|
||||
shape: AvesDialog.shape(context),
|
||||
child: Padding(
|
||||
padding: padding,
|
||||
child: ValueListenableBuilder<T?>(
|
||||
valueListenable: valueNotifier,
|
||||
builder: (context, selectedValue, child) {
|
||||
return Stack(
|
||||
children: [
|
||||
ValueListenableBuilder<Rect>(
|
||||
valueListenable: _selectedRowRect,
|
||||
builder: (context, selectedRowRect, child) {
|
||||
Widget child = const Center(child: AvesDot());
|
||||
child = AnimatedOpacity(
|
||||
opacity: selectedValue != null ? 1 : 0,
|
||||
curve: Curves.easeInOutCubic,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: child,
|
||||
);
|
||||
child = AnimatedPositioned(
|
||||
top: selectedRowRect.top,
|
||||
height: selectedRowRect.height,
|
||||
curve: Curves.easeInOutCubic,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: child,
|
||||
);
|
||||
return child;
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsetsDirectional.only(start: 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: options.mapIndexed((index, value) {
|
||||
return Padding(
|
||||
padding: index == 0 ? EdgeInsets.zero : const EdgeInsets.only(top: intraPadding),
|
||||
child: widget.buildFilterChip(context, value),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onPointerMove(Offset globalPosition) {
|
||||
final chooserBox = context.findRenderObject() as RenderBox;
|
||||
final chooserSize = chooserBox.size;
|
||||
final contentWidth = chooserSize.width;
|
||||
final contentHeight = chooserSize.height - (margin.vertical + padding.vertical);
|
||||
|
||||
final optionCount = options.length;
|
||||
final itemHeight = (contentHeight - (optionCount - 1) * intraPadding) / optionCount;
|
||||
|
||||
final local = chooserBox.globalToLocal(globalPosition);
|
||||
final dx = local.dx;
|
||||
final dy = local.dy - (margin.vertical + padding.vertical) / 2;
|
||||
|
||||
T? selectedValue;
|
||||
if (0 < dx && dx < contentWidth && 0 < dy && dy < contentHeight) {
|
||||
final index = (options.length * dy / contentHeight).floor();
|
||||
if (0 <= index && index < options.length) {
|
||||
selectedValue = options[index];
|
||||
final top = index * (itemHeight + intraPadding);
|
||||
_selectedRowRect.value = Rect.fromLTWH(0, top, contentWidth, itemHeight);
|
||||
}
|
||||
}
|
||||
valueNotifier.value = selectedValue;
|
||||
}
|
||||
}
|
|
@ -5,13 +5,13 @@ import 'package:aves/widgets/dialogs/aves_dialog.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class RateQuickChooser extends StatefulWidget {
|
||||
final ValueNotifier<int?> ratingNotifier;
|
||||
final Stream<LongPressMoveUpdateDetails> moveUpdates;
|
||||
final ValueNotifier<int?> valueNotifier;
|
||||
final Stream<Offset> pointerGlobalPosition;
|
||||
|
||||
const RateQuickChooser({
|
||||
super.key,
|
||||
required this.ratingNotifier,
|
||||
required this.moveUpdates,
|
||||
required this.valueNotifier,
|
||||
required this.pointerGlobalPosition,
|
||||
});
|
||||
|
||||
@override
|
||||
|
@ -21,6 +21,11 @@ class RateQuickChooser extends StatefulWidget {
|
|||
class _RateQuickChooserState extends State<RateQuickChooser> {
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
|
||||
ValueNotifier<int?> get valueNotifier => widget.valueNotifier;
|
||||
|
||||
static const margin = EdgeInsets.all(8);
|
||||
static const padding = EdgeInsets.all(8);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -41,7 +46,7 @@ class _RateQuickChooserState extends State<RateQuickChooser> {
|
|||
}
|
||||
|
||||
void _registerWidget(RateQuickChooser widget) {
|
||||
_subscriptions.add(widget.moveUpdates.map((event) => event.globalPosition).listen(_onPointerMove));
|
||||
_subscriptions.add(widget.pointerGlobalPosition.listen(_onPointerMove));
|
||||
}
|
||||
|
||||
void _unregisterWidget(RateQuickChooser widget) {
|
||||
|
@ -53,29 +58,27 @@ class _RateQuickChooserState extends State<RateQuickChooser> {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: margin,
|
||||
child: Material(
|
||||
shape: AvesDialog.shape(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: padding,
|
||||
child: ValueListenableBuilder<int?>(
|
||||
valueListenable: widget.ratingNotifier,
|
||||
builder: (context, rating, child) {
|
||||
final _rating = rating ?? 0;
|
||||
valueListenable: valueNotifier,
|
||||
builder: (context, selectedValue, child) {
|
||||
final _rating = selectedValue ?? 0;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
...List.generate(5, (i) {
|
||||
final thisRating = i + 1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
_rating < thisRating ? AIcons.rating : AIcons.ratingFull,
|
||||
color: _rating < thisRating ? Colors.grey : Colors.amber,
|
||||
),
|
||||
);
|
||||
})
|
||||
],
|
||||
children: List.generate(5, (i) {
|
||||
final thisRating = i + 1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Icon(
|
||||
_rating < thisRating ? AIcons.rating : AIcons.ratingFull,
|
||||
color: _rating < thisRating ? Colors.grey : Colors.amber,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -85,9 +88,13 @@ class _RateQuickChooserState extends State<RateQuickChooser> {
|
|||
}
|
||||
|
||||
void _onPointerMove(Offset globalPosition) {
|
||||
final rowBox = context.findRenderObject() as RenderBox;
|
||||
final rowSize = rowBox.size;
|
||||
final local = rowBox.globalToLocal(globalPosition);
|
||||
widget.ratingNotifier.value = (5 * local.dx / rowSize.width).ceil().clamp(0, 5);
|
||||
final chooserBox = context.findRenderObject() as RenderBox;
|
||||
final chooserSize = chooserBox.size;
|
||||
final contentWidth = chooserSize.width - (margin.horizontal + padding.horizontal);
|
||||
|
||||
final local = chooserBox.globalToLocal(globalPosition);
|
||||
final dx = local.dx - (margin.horizontal + padding.horizontal) / 2;
|
||||
|
||||
valueNotifier.value = (5 * dx / contentWidth).ceil().clamp(0, 5);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,8 +34,8 @@ class _RateQuickButtonState extends ChooserQuickButtonState<RateButton, int> {
|
|||
child: ScaleTransition(
|
||||
scale: animation,
|
||||
child: RateQuickChooser(
|
||||
ratingNotifier: chooserValueNotifier,
|
||||
moveUpdates: moveUpdates,
|
||||
valueNotifier: chooserValueNotifier,
|
||||
pointerGlobalPosition: pointerGlobalPosition,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
@ -213,8 +213,8 @@ class _GeoMapState extends State<GeoMap> {
|
|||
MapThemeData.markerImageExtent + MapThemeData.markerOuterBorderWidth * 2 + MapThemeData.markerArrowSize.height,
|
||||
),
|
||||
dotMarkerSize: const Size(
|
||||
DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2,
|
||||
DotMarker.diameter + MapThemeData.markerOuterBorderWidth * 2,
|
||||
MapThemeData.markerDotDiameter + MapThemeData.markerOuterBorderWidth * 2,
|
||||
MapThemeData.markerDotDiameter + MapThemeData.markerOuterBorderWidth * 2,
|
||||
),
|
||||
overlayOpacityNotifier: widget.overlayOpacityNotifier,
|
||||
overlayEntry: widget.overlayEntry,
|
||||
|
|
|
@ -53,7 +53,7 @@ class EntryLeafletMap<T> extends StatefulWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _EntryLeafletMapState<T>();
|
||||
State<EntryLeafletMap<T>> createState() => _EntryLeafletMapState<T>();
|
||||
}
|
||||
|
||||
class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProviderStateMixin {
|
||||
|
|
|
@ -212,7 +212,7 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
|
|||
final emptyAlbums = todoAlbums.whereNot(filledAlbums.contains).toSet();
|
||||
|
||||
if (settings.enableBin && filledAlbums.isNotEmpty) {
|
||||
await move(
|
||||
await doMove(
|
||||
context,
|
||||
moveType: MoveType.toBin,
|
||||
entries: todoEntries,
|
||||
|
|
|
@ -280,6 +280,19 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
}
|
||||
}
|
||||
|
||||
void quickMove(BuildContext context, String? album, {required bool copy}) {
|
||||
final targetEntry = _getTargetEntry(context, EntryAction.editRating);
|
||||
if (album == null || (!copy && targetEntry.directory == album)) return;
|
||||
|
||||
doQuickMove(
|
||||
context,
|
||||
moveType: copy ? MoveType.copy : MoveType.move,
|
||||
entriesByDestination: {
|
||||
album: {targetEntry}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void quickRate(BuildContext context, int? rating) {
|
||||
final targetEntry = _getTargetEntry(context, EntryAction.editRating);
|
||||
_metadataActionDelegate.quickRate(context, targetEntry, rating);
|
||||
|
@ -441,7 +454,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => move(
|
||||
Future<void> _move(BuildContext context, AvesEntry targetEntry, {required MoveType moveType}) => doMove(
|
||||
context,
|
||||
moveType: moveType,
|
||||
entries: {targetEntry},
|
||||
|
|
|
@ -148,7 +148,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
|
|||
}
|
||||
|
||||
Future<void> quickRate(BuildContext context, AvesEntry targetEntry, int? rating) async {
|
||||
if (rating == null) return;
|
||||
if (rating == null || targetEntry.rating == rating) return;
|
||||
|
||||
await edit(context, targetEntry, () => targetEntry.editRating(rating));
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import 'package:aves/model/source/collection_lens.dart';
|
|||
import 'package:aves/theme/durations.dart';
|
||||
import 'package:aves/theme/icons.dart';
|
||||
import 'package:aves/widgets/common/app_bar/favourite_toggler.dart';
|
||||
import 'package:aves/widgets/common/app_bar/move_button.dart';
|
||||
import 'package:aves/widgets/common/app_bar/rate_button.dart';
|
||||
import 'package:aves/widgets/common/basic/menu.dart';
|
||||
import 'package:aves/widgets/common/basic/popup_menu_button.dart';
|
||||
|
@ -204,6 +205,22 @@ class ViewerButtonRowContent extends StatelessWidget {
|
|||
}
|
||||
|
||||
switch (action) {
|
||||
case EntryAction.copy:
|
||||
child = MoveButton(
|
||||
copy: true,
|
||||
chooserPosition: PopupMenuPosition.over,
|
||||
onChooserValue: (album) => _quickMove(context, album, copy: true),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
break;
|
||||
case EntryAction.move:
|
||||
child = MoveButton(
|
||||
copy: false,
|
||||
chooserPosition: PopupMenuPosition.over,
|
||||
onChooserValue: (album) => _quickMove(context, album, copy: false),
|
||||
onPressed: onPressed,
|
||||
);
|
||||
break;
|
||||
case EntryAction.toggleFavourite:
|
||||
child = FavouriteToggler(
|
||||
entries: {favouriteTargetEntry},
|
||||
|
@ -365,5 +382,7 @@ class ViewerButtonRowContent extends StatelessWidget {
|
|||
|
||||
void _onActionSelected(BuildContext context, EntryAction action) => _entryActionDelegate.onActionSelected(context, action);
|
||||
|
||||
void _quickMove(BuildContext context, String? album, {required bool copy}) => _entryActionDelegate.quickMove(context, album, copy: copy);
|
||||
|
||||
void _quickRate(BuildContext context, int? rating) => _entryActionDelegate.quickRate(context, rating);
|
||||
}
|
||||
|
|
|
@ -1,54 +1,18 @@
|
|||
import 'package:aves_map/src/theme.dart';
|
||||
import 'package:aves_ui/aves_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DotMarker extends StatelessWidget {
|
||||
const DotMarker({super.key});
|
||||
|
||||
static const double diameter = 16;
|
||||
static const double outerBorderRadiusDim = diameter;
|
||||
static const double outerBorderWidth = MapThemeData.markerOuterBorderWidth;
|
||||
static const double innerBorderWidth = MapThemeData.markerInnerBorderWidth;
|
||||
static const outerBorderRadius = BorderRadius.all(Radius.circular(outerBorderRadiusDim));
|
||||
static const innerRadius = Radius.circular(outerBorderRadiusDim - outerBorderWidth);
|
||||
static const innerBorderRadius = BorderRadius.all(innerRadius);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final outerBorderColor = MapThemeData.markerThemedOuterBorderColor(isDark);
|
||||
final innerBorderColor = MapThemeData.markerThemedInnerBorderColor(isDark);
|
||||
|
||||
final outerDecoration = BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: outerBorderColor,
|
||||
width: outerBorderWidth,
|
||||
)),
|
||||
borderRadius: outerBorderRadius,
|
||||
);
|
||||
|
||||
final innerDecoration = BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: innerBorderColor,
|
||||
width: innerBorderWidth,
|
||||
)),
|
||||
borderRadius: innerBorderRadius,
|
||||
);
|
||||
|
||||
return Container(
|
||||
decoration: outerDecoration,
|
||||
child: DecoratedBox(
|
||||
decoration: innerDecoration,
|
||||
position: DecorationPosition.foreground,
|
||||
child: ClipRRect(
|
||||
borderRadius: innerBorderRadius,
|
||||
child: Container(
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
return const AvesDot(
|
||||
diameter: MapThemeData.markerDotDiameter,
|
||||
outerBorderWidth: MapThemeData.markerOuterBorderWidth,
|
||||
innerBorderWidth: MapThemeData.markerInnerBorderWidth,
|
||||
getOuterBorderColor: MapThemeData.markerThemedOuterBorderColor,
|
||||
getInnerBorderColor: MapThemeData.markerThemedInnerBorderColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ class MapThemeData {
|
|||
static const double markerInnerBorderWidth = 2;
|
||||
static const double markerImageExtent = 48.0;
|
||||
static const Size markerArrowSize = Size(8, 6);
|
||||
static const double markerDotDiameter = 16;
|
||||
|
||||
static Color markerThemedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26;
|
||||
|
||||
|
|
|
@ -8,6 +8,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.10.0"
|
||||
aves_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "../aves_ui"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -8,6 +8,8 @@ environment:
|
|||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
aves_ui:
|
||||
path: ../aves_ui
|
||||
collection:
|
||||
# TODO TLAD as of 2022/02/22, null safe version is pre-release
|
||||
custom_rounded_rectangle_border: '>=0.2.0-nullsafety.0'
|
||||
|
|
|
@ -15,6 +15,13 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
aves_ui:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "../aves_ui"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -22,6 +22,13 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
aves_ui:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "../aves_ui"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -29,6 +29,13 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
aves_ui:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "../aves_ui"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -22,6 +22,13 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
aves_ui:
|
||||
dependency: transitive
|
||||
description:
|
||||
path: "../aves_ui"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
30
plugins/aves_ui/.gitignore
vendored
Normal file
30
plugins/aves_ui/.gitignore
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
|
||||
#/pubspec.lock
|
||||
**/doc/api/
|
||||
.dart_tool/
|
||||
.packages
|
||||
build/
|
10
plugins/aves_ui/.metadata
Normal file
10
plugins/aves_ui/.metadata
Normal file
|
@ -0,0 +1,10 @@
|
|||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: 5464c5bac742001448fe4fc0597be939379f88ea
|
||||
channel: stable
|
||||
|
||||
project_type: package
|
1
plugins/aves_ui/analysis_options.yaml
Normal file
1
plugins/aves_ui/analysis_options.yaml
Normal file
|
@ -0,0 +1 @@
|
|||
include: ../../analysis_options.yaml
|
3
plugins/aves_ui/lib/aves_ui.dart
Normal file
3
plugins/aves_ui/lib/aves_ui.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
library aves_ui;
|
||||
|
||||
export 'src/dot.dart';
|
62
plugins/aves_ui/lib/src/dot.dart
Normal file
62
plugins/aves_ui/lib/src/dot.dart
Normal file
|
@ -0,0 +1,62 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
class AvesDot extends StatelessWidget {
|
||||
final double diameter, outerBorderWidth, innerBorderWidth;
|
||||
final Color Function(bool isDark) getOuterBorderColor, getInnerBorderColor;
|
||||
|
||||
const AvesDot({
|
||||
super.key,
|
||||
this.diameter = 16,
|
||||
this.outerBorderWidth = 1.5,
|
||||
this.innerBorderWidth = 2,
|
||||
this.getOuterBorderColor = themedOuterBorderColor,
|
||||
this.getInnerBorderColor = themedInnerBorderColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
final outerBorderColor = getOuterBorderColor(isDark);
|
||||
final innerBorderColor = getInnerBorderColor(isDark);
|
||||
final outerBorderRadius = BorderRadius.all(Radius.circular(diameter));
|
||||
final innerRadius = Radius.circular(diameter - outerBorderWidth);
|
||||
final innerBorderRadius = BorderRadius.all(innerRadius);
|
||||
|
||||
final outerDecoration = BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: outerBorderColor,
|
||||
width: outerBorderWidth,
|
||||
)),
|
||||
borderRadius: outerBorderRadius,
|
||||
);
|
||||
|
||||
final innerDecoration = BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(
|
||||
color: innerBorderColor,
|
||||
width: innerBorderWidth,
|
||||
)),
|
||||
borderRadius: innerBorderRadius,
|
||||
);
|
||||
|
||||
return Container(
|
||||
decoration: outerDecoration,
|
||||
child: DecoratedBox(
|
||||
decoration: innerDecoration,
|
||||
position: DecorationPosition.foreground,
|
||||
child: ClipRRect(
|
||||
borderRadius: innerBorderRadius,
|
||||
child: Container(
|
||||
width: diameter,
|
||||
height: diameter,
|
||||
color: theme.colorScheme.secondary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static Color themedOuterBorderColor(bool isDark) => isDark ? Colors.white30 : Colors.black26;
|
||||
|
||||
static Color themedInnerBorderColor(bool isDark) => isDark ? const Color(0xFF212121) : Colors.white;
|
||||
}
|
64
plugins/aves_ui/pubspec.lock
Normal file
64
plugins/aves_ui/pubspec.lock
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: characters
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: collection
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.16.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
lints:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.5"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.8.0"
|
||||
sky_engine:
|
||||
dependency: transitive
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.99"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_math
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
sdks:
|
||||
dart: ">=2.18.0 <3.0.0"
|
15
plugins/aves_ui/pubspec.yaml
Normal file
15
plugins/aves_ui/pubspec.yaml
Normal file
|
@ -0,0 +1,15 @@
|
|||
name: aves_ui
|
||||
version: 0.0.1
|
||||
publish_to: none
|
||||
|
||||
environment:
|
||||
sdk: ">=2.18.0 <3.0.0"
|
||||
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints:
|
||||
|
||||
flutter:
|
|
@ -85,6 +85,13 @@ packages:
|
|||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
aves_ui:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: "plugins/aves_ui"
|
||||
relative: true
|
||||
source: path
|
||||
version: "0.0.1"
|
||||
barcode:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -615,7 +622,7 @@ packages:
|
|||
name: mime
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
version: "1.0.3"
|
||||
motion_sensors:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -37,6 +37,8 @@ dependencies:
|
|||
path: plugins/aves_services
|
||||
aves_services_platform:
|
||||
path: plugins/aves_services_google
|
||||
aves_ui:
|
||||
path: plugins/aves_ui
|
||||
charts_flutter:
|
||||
collection:
|
||||
connectivity_plus:
|
||||
|
|
Loading…
Reference in a new issue