mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-08 18:28:53 +01:00
Drag photos to rearrange them in album
This commit is contained in:
parent
7644a23f44
commit
8de5597575
5 changed files with 390 additions and 26 deletions
|
@ -547,6 +547,10 @@
|
||||||
"@sortOptionTimeDescendingLabel": {
|
"@sortOptionTimeDescendingLabel": {
|
||||||
"description": "Sort by time, in descending order"
|
"description": "Sort by time, in descending order"
|
||||||
},
|
},
|
||||||
|
"albumEditDragRearrangeNotification": "Long press and drag an item to rearrange it manually",
|
||||||
|
"@albumEditDragRearrangeNotification": {
|
||||||
|
"description": "Instructions on how to rearrange photos"
|
||||||
|
},
|
||||||
|
|
||||||
"changelogTitle": "Changelog",
|
"changelogTitle": "Changelog",
|
||||||
"@changelogTitle": {
|
"@changelogTitle": {
|
||||||
|
|
|
@ -9,5 +9,8 @@ class SessionStorage {
|
||||||
/// Whether the range select notification has been shown to user
|
/// Whether the range select notification has been shown to user
|
||||||
bool hasShowRangeSelectNotification = false;
|
bool hasShowRangeSelectNotification = false;
|
||||||
|
|
||||||
|
/// Whether the drag to rearrange notification has been shown
|
||||||
|
bool hasShowDragRearrangeNotification = false;
|
||||||
|
|
||||||
static SessionStorage _inst = SessionStorage._();
|
static SessionStorage _inst = SessionStorage._();
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,13 @@ import 'package:nc_photos/entity/file_util.dart' as file_util;
|
||||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
import 'package:nc_photos/list_extension.dart';
|
import 'package:nc_photos/list_extension.dart';
|
||||||
|
import 'package:nc_photos/session_storage.dart';
|
||||||
import 'package:nc_photos/snack_bar_manager.dart';
|
import 'package:nc_photos/snack_bar_manager.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/use_case/resync_album.dart';
|
import 'package:nc_photos/use_case/resync_album.dart';
|
||||||
import 'package:nc_photos/use_case/update_album.dart';
|
import 'package:nc_photos/use_case/update_album.dart';
|
||||||
import 'package:nc_photos/widget/album_viewer_mixin.dart';
|
import 'package:nc_photos/widget/album_viewer_mixin.dart';
|
||||||
|
import 'package:nc_photos/widget/draggable_item_list_mixin.dart';
|
||||||
import 'package:nc_photos/widget/photo_list_item.dart';
|
import 'package:nc_photos/widget/photo_list_item.dart';
|
||||||
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
|
import 'package:nc_photos/widget/selectable_item_stream_list_mixin.dart';
|
||||||
import 'package:nc_photos/widget/viewer.dart';
|
import 'package:nc_photos/widget/viewer.dart';
|
||||||
|
@ -59,6 +61,7 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
with
|
with
|
||||||
WidgetsBindingObserver,
|
WidgetsBindingObserver,
|
||||||
SelectableItemStreamListMixin<AlbumViewer>,
|
SelectableItemStreamListMixin<AlbumViewer>,
|
||||||
|
DraggableItemListMixin<AlbumViewer>,
|
||||||
AlbumViewerMixin<AlbumViewer> {
|
AlbumViewerMixin<AlbumViewer> {
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
|
@ -90,6 +93,15 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
enterEditMode() {
|
enterEditMode() {
|
||||||
super.enterEditMode();
|
super.enterEditMode();
|
||||||
_editAlbum = _album.copyWith();
|
_editAlbum = _album.copyWith();
|
||||||
|
|
||||||
|
if (!SessionStorage().hasShowDragRearrangeNotification) {
|
||||||
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
content: Text(
|
||||||
|
AppLocalizations.of(context).albumEditDragRearrangeNotification),
|
||||||
|
duration: k.snackBarDurationNormal,
|
||||||
|
));
|
||||||
|
SessionStorage().hasShowDragRearrangeNotification = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -158,29 +170,39 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget content = CustomScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
slivers: [
|
||||||
|
_buildAppBar(context),
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
sliver: isEditMode
|
||||||
|
? buildDraggableItemList(
|
||||||
|
maxCrossAxisExtent: thumbSize.toDouble(),
|
||||||
|
onMaxExtentChanged: (value) {
|
||||||
|
_itemListMaxExtent = value;
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: buildItemStreamList(
|
||||||
|
maxCrossAxisExtent: thumbSize.toDouble(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
if (isEditMode) {
|
||||||
|
content = Listener(
|
||||||
|
onPointerMove: _onEditModePointerMove,
|
||||||
|
child: content,
|
||||||
|
);
|
||||||
|
}
|
||||||
return buildItemStreamListOuter(
|
return buildItemStreamListOuter(
|
||||||
context,
|
context,
|
||||||
child: Theme(
|
child: Theme(
|
||||||
data: Theme.of(context).copyWith(
|
data: Theme.of(context).copyWith(
|
||||||
accentColor: AppTheme.getOverscrollIndicatorColor(context),
|
accentColor: AppTheme.getOverscrollIndicatorColor(context),
|
||||||
),
|
),
|
||||||
child: CustomScrollView(
|
child: content,
|
||||||
slivers: [
|
|
||||||
_buildAppBar(context),
|
|
||||||
SliverPadding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
sliver: SliverIgnorePointer(
|
|
||||||
ignoring: isEditMode,
|
|
||||||
sliver: SliverOpacity(
|
|
||||||
opacity: isEditMode ? .25 : 1,
|
|
||||||
sliver: buildItemStreamList(
|
|
||||||
maxCrossAxisExtent: thumbSize.toDouble(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -342,21 +364,82 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onEditModePointerMove(PointerMoveEvent event) {
|
||||||
|
assert(isEditMode);
|
||||||
|
if (!_isDragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.position.dy >= MediaQuery.of(context).size.height - 100) {
|
||||||
|
// near bottom of screen
|
||||||
|
if (_isDragScrollingDown == true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final maxExtent =
|
||||||
|
_itemListMaxExtent ?? _scrollController.position.maxScrollExtent;
|
||||||
|
_log.fine("[_onEditModePointerMove] Begin scrolling down");
|
||||||
|
if (_scrollController.offset <
|
||||||
|
_scrollController.position.maxScrollExtent) {
|
||||||
|
_scrollController.animateTo(maxExtent,
|
||||||
|
duration: Duration(
|
||||||
|
milliseconds:
|
||||||
|
((maxExtent - _scrollController.offset) * 1.6).round()),
|
||||||
|
curve: Curves.linear);
|
||||||
|
_isDragScrollingDown = true;
|
||||||
|
}
|
||||||
|
} else if (event.position.dy <= 100) {
|
||||||
|
// near top of screen
|
||||||
|
if (_isDragScrollingDown == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_log.fine("[_onEditModePointerMove] Begin scrolling up");
|
||||||
|
if (_scrollController.offset > 0) {
|
||||||
|
_scrollController.animateTo(0,
|
||||||
|
duration: Duration(
|
||||||
|
milliseconds: (_scrollController.offset * 1.6).round()),
|
||||||
|
curve: Curves.linear);
|
||||||
|
_isDragScrollingDown = false;
|
||||||
|
}
|
||||||
|
} else if (_isDragScrollingDown != null) {
|
||||||
|
_log.fine("[_onEditModePointerMove] Stop scrolling");
|
||||||
|
_scrollController.jumpTo(_scrollController.offset);
|
||||||
|
_isDragScrollingDown = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onItemMoved(int fromIndex, int toIndex, bool isBefore) {
|
||||||
|
if (fromIndex == toIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final item = _sortedItems.removeAt(fromIndex);
|
||||||
|
final newIndex =
|
||||||
|
toIndex + (isBefore ? 0 : 1) + (fromIndex < toIndex ? -1 : 0);
|
||||||
|
_sortedItems.insert(newIndex, item);
|
||||||
|
_editAlbum = _editAlbum.copyWith(
|
||||||
|
sortProvider: AlbumNullSortProvider(),
|
||||||
|
// save the current order
|
||||||
|
provider: AlbumStaticProvider(
|
||||||
|
items: _sortedItems,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setState(() {
|
||||||
|
_transformItems();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _transformItems() {
|
void _transformItems() {
|
||||||
List<AlbumItem> sortedItems;
|
|
||||||
if (_editAlbum != null) {
|
if (_editAlbum != null) {
|
||||||
// edit mode
|
// edit mode
|
||||||
sortedItems = _editAlbum.sortProvider.sort(_getAlbumItemsOf(_editAlbum));
|
_sortedItems = _editAlbum.sortProvider.sort(_getAlbumItemsOf(_editAlbum));
|
||||||
} else {
|
} else {
|
||||||
sortedItems = _album.sortProvider.sort(_getAlbumItemsOf(_album));
|
_sortedItems = _album.sortProvider.sort(_getAlbumItemsOf(_album));
|
||||||
}
|
}
|
||||||
_backingFiles = sortedItems
|
_backingFiles = _sortedItems
|
||||||
.whereType<AlbumFileItem>()
|
.whereType<AlbumFileItem>()
|
||||||
.map((e) => e.file)
|
.map((e) => e.file)
|
||||||
.where((element) => file_util.isSupportedFormat(element))
|
.where((element) => file_util.isSupportedFormat(element))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
itemStreamListItems = () sync* {
|
final items = () sync* {
|
||||||
for (int i = 0; i < _backingFiles.length; ++i) {
|
for (int i = 0; i < _backingFiles.length; ++i) {
|
||||||
final f = _backingFiles[i];
|
final f = _backingFiles[i];
|
||||||
|
|
||||||
|
@ -364,16 +447,38 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
width: thumbSize, height: thumbSize);
|
width: thumbSize, height: thumbSize);
|
||||||
if (file_util.isSupportedImageFormat(f)) {
|
if (file_util.isSupportedImageFormat(f)) {
|
||||||
yield _ImageListItem(
|
yield _ImageListItem(
|
||||||
|
index: i,
|
||||||
file: f,
|
file: f,
|
||||||
account: widget.account,
|
account: widget.account,
|
||||||
previewUrl: previewUrl,
|
previewUrl: previewUrl,
|
||||||
onTap: () => _onItemTap(i),
|
onTap: () => _onItemTap(i),
|
||||||
|
onDropBefore: (dropItem) =>
|
||||||
|
_onItemMoved((dropItem as _ListItem).index, i, true),
|
||||||
|
onDropAfter: (dropItem) =>
|
||||||
|
_onItemMoved((dropItem as _ListItem).index, i, false),
|
||||||
|
onDragStarted: () {
|
||||||
|
_isDragging = true;
|
||||||
|
},
|
||||||
|
onDragEndedAny: () {
|
||||||
|
_isDragging = false;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else if (file_util.isSupportedVideoFormat(f)) {
|
} else if (file_util.isSupportedVideoFormat(f)) {
|
||||||
yield _VideoListItem(
|
yield _VideoListItem(
|
||||||
|
index: i,
|
||||||
account: widget.account,
|
account: widget.account,
|
||||||
previewUrl: previewUrl,
|
previewUrl: previewUrl,
|
||||||
onTap: () => _onItemTap(i),
|
onTap: () => _onItemTap(i),
|
||||||
|
onDropBefore: (dropItem) =>
|
||||||
|
_onItemMoved((dropItem as _ListItem).index, i, true),
|
||||||
|
onDropAfter: (dropItem) =>
|
||||||
|
_onItemMoved((dropItem as _ListItem).index, i, false),
|
||||||
|
onDragStarted: () {
|
||||||
|
_isDragging = true;
|
||||||
|
},
|
||||||
|
onDragEndedAny: () {
|
||||||
|
_isDragging = false;
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_log.shout(
|
_log.shout(
|
||||||
|
@ -382,6 +487,8 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
.toList();
|
.toList();
|
||||||
|
itemStreamListItems = items;
|
||||||
|
draggableItemList = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _shouldPropagateResyncedAlbum(Album album) {
|
bool _shouldPropagateResyncedAlbum(Album album) {
|
||||||
|
@ -415,18 +522,33 @@ class _AlbumViewerState extends State<AlbumViewer>
|
||||||
AlbumStaticProvider.of(a).items;
|
AlbumStaticProvider.of(a).items;
|
||||||
|
|
||||||
Album _album;
|
Album _album;
|
||||||
|
var _sortedItems = <AlbumItem>[];
|
||||||
var _backingFiles = <File>[];
|
var _backingFiles = <File>[];
|
||||||
|
|
||||||
|
final _scrollController = ScrollController();
|
||||||
|
double _itemListMaxExtent;
|
||||||
|
bool _isDragging = false;
|
||||||
|
// == null if not drag scrolling
|
||||||
|
bool _isDragScrollingDown;
|
||||||
final _editFormKey = GlobalKey<FormState>();
|
final _editFormKey = GlobalKey<FormState>();
|
||||||
Album _editAlbum;
|
Album _editAlbum;
|
||||||
|
|
||||||
static final _log = Logger("widget.album_viewer._AlbumViewerState");
|
static final _log = Logger("widget.album_viewer._AlbumViewerState");
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class _ListItem implements SelectableItem {
|
abstract class _ListItem implements SelectableItem, DraggableItem {
|
||||||
_ListItem({
|
_ListItem({
|
||||||
|
@required this.index,
|
||||||
VoidCallback onTap,
|
VoidCallback onTap,
|
||||||
}) : _onTap = onTap;
|
DragTargetAccept<DraggableItem> onDropBefore,
|
||||||
|
DragTargetAccept<DraggableItem> onDropAfter,
|
||||||
|
VoidCallback onDragStarted,
|
||||||
|
VoidCallback onDragEndedAny,
|
||||||
|
}) : _onTap = onTap,
|
||||||
|
_onDropBefore = onDropBefore,
|
||||||
|
_onDropAfter = onDropAfter,
|
||||||
|
_onDragStarted = onDragStarted,
|
||||||
|
_onDragEndedAny = onDragEndedAny;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get onTap => _onTap;
|
get onTap => _onTap;
|
||||||
|
@ -434,19 +556,59 @@ abstract class _ListItem implements SelectableItem {
|
||||||
@override
|
@override
|
||||||
get isSelectable => true;
|
get isSelectable => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
get isDraggable => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onDropBefore => _onDropBefore;
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onDropAfter => _onDropAfter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onDragStarted => _onDragStarted;
|
||||||
|
|
||||||
|
@override
|
||||||
|
get onDragEndedAny => _onDragEndedAny;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
get staggeredTile => const StaggeredTile.count(1, 1);
|
get staggeredTile => const StaggeredTile.count(1, 1);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "$runtimeType {"
|
||||||
|
"index: $index, "
|
||||||
|
"}";
|
||||||
|
}
|
||||||
|
|
||||||
|
final int index;
|
||||||
|
|
||||||
final VoidCallback _onTap;
|
final VoidCallback _onTap;
|
||||||
|
final DragTargetAccept<DraggableItem> _onDropBefore;
|
||||||
|
final DragTargetAccept<DraggableItem> _onDropAfter;
|
||||||
|
final VoidCallback _onDragStarted;
|
||||||
|
final VoidCallback _onDragEndedAny;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ImageListItem extends _ListItem {
|
class _ImageListItem extends _ListItem {
|
||||||
_ImageListItem({
|
_ImageListItem({
|
||||||
|
@required int index,
|
||||||
@required this.file,
|
@required this.file,
|
||||||
@required this.account,
|
@required this.account,
|
||||||
@required this.previewUrl,
|
@required this.previewUrl,
|
||||||
VoidCallback onTap,
|
VoidCallback onTap,
|
||||||
}) : super(onTap: onTap);
|
DragTargetAccept<DraggableItem> onDropBefore,
|
||||||
|
DragTargetAccept<DraggableItem> onDropAfter,
|
||||||
|
VoidCallback onDragStarted,
|
||||||
|
VoidCallback onDragEndedAny,
|
||||||
|
}) : super(
|
||||||
|
index: index,
|
||||||
|
onTap: onTap,
|
||||||
|
onDropBefore: onDropBefore,
|
||||||
|
onDropAfter: onDropAfter,
|
||||||
|
onDragStarted: onDragStarted,
|
||||||
|
onDragEndedAny: onDragEndedAny,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
buildWidget(BuildContext context) {
|
buildWidget(BuildContext context) {
|
||||||
|
@ -464,10 +626,22 @@ class _ImageListItem extends _ListItem {
|
||||||
|
|
||||||
class _VideoListItem extends _ListItem {
|
class _VideoListItem extends _ListItem {
|
||||||
_VideoListItem({
|
_VideoListItem({
|
||||||
|
@required int index,
|
||||||
@required this.account,
|
@required this.account,
|
||||||
@required this.previewUrl,
|
@required this.previewUrl,
|
||||||
VoidCallback onTap,
|
VoidCallback onTap,
|
||||||
}) : super(onTap: onTap);
|
DragTargetAccept<DraggableItem> onDropBefore,
|
||||||
|
DragTargetAccept<DraggableItem> onDropAfter,
|
||||||
|
VoidCallback onDragStarted,
|
||||||
|
VoidCallback onDragEndedAny,
|
||||||
|
}) : super(
|
||||||
|
index: index,
|
||||||
|
onTap: onTap,
|
||||||
|
onDropBefore: onDropBefore,
|
||||||
|
onDropAfter: onDropAfter,
|
||||||
|
onDragStarted: onDragStarted,
|
||||||
|
onDragEndedAny: onDragEndedAny,
|
||||||
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
buildWidget(BuildContext context) {
|
buildWidget(BuildContext context) {
|
||||||
|
|
128
lib/widget/draggable.dart
Normal file
128
lib/widget/draggable.dart
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
class Draggable<T> extends StatelessWidget {
|
||||||
|
Draggable({
|
||||||
|
Key key,
|
||||||
|
@required this.data,
|
||||||
|
@required this.child,
|
||||||
|
this.onDropBefore,
|
||||||
|
this.onDropAfter,
|
||||||
|
this.onDragStarted,
|
||||||
|
this.onDragEndedAny,
|
||||||
|
this.feedbackSize,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
build(BuildContext context) {
|
||||||
|
final buildIndicator = (alignment, isActive) {
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Container(),
|
||||||
|
Visibility(
|
||||||
|
visible: isActive,
|
||||||
|
child: Align(
|
||||||
|
alignment: alignment,
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints.tightFor(width: 2),
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: LongPressDraggable<T>(
|
||||||
|
data: data,
|
||||||
|
dragAnchorStrategy: pointerDragAnchorStrategy,
|
||||||
|
onDragStarted: onDragStarted,
|
||||||
|
onDragEnd: (_) => onDragEndedAny?.call(),
|
||||||
|
onDragCompleted: onDragEndedAny,
|
||||||
|
onDraggableCanceled: (v, o) => onDragEndedAny?.call(),
|
||||||
|
feedback: FractionalTranslation(
|
||||||
|
translation: const Offset(-.5, -.5),
|
||||||
|
child: SizedBox(
|
||||||
|
width: feedbackSize?.width ?? 128,
|
||||||
|
height: feedbackSize?.height ?? 128,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: .5,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
childWhenDragging: Opacity(
|
||||||
|
opacity: .25,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (onDropBefore != null || onDropAfter != null)
|
||||||
|
Positioned.fill(
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
if (onDropBefore != null)
|
||||||
|
Expanded(
|
||||||
|
child: DragTarget<T>(
|
||||||
|
builder: (context, candidateItems, rejectedItems) {
|
||||||
|
return buildIndicator(AlignmentDirectional.centerStart,
|
||||||
|
candidateItems.isNotEmpty);
|
||||||
|
},
|
||||||
|
onAccept: (item) {
|
||||||
|
_log.fine("[build] Dropping $item before $data");
|
||||||
|
onDropBefore(item);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (onDropAfter != null)
|
||||||
|
Expanded(
|
||||||
|
child: DragTarget<T>(
|
||||||
|
builder: (context, candidateItems, rejectedItems) {
|
||||||
|
return buildIndicator(AlignmentDirectional.centerEnd,
|
||||||
|
candidateItems.isNotEmpty);
|
||||||
|
},
|
||||||
|
onAccept: (item) {
|
||||||
|
_log.fine("[build] Dropping $item after $data");
|
||||||
|
onDropAfter(item);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final T data;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Called when some item dropped before this item
|
||||||
|
final DragTargetAccept<T> onDropBefore;
|
||||||
|
|
||||||
|
/// Called when some item dropped after this item
|
||||||
|
final DragTargetAccept<T> onDropAfter;
|
||||||
|
|
||||||
|
final VoidCallback onDragStarted;
|
||||||
|
|
||||||
|
/// Called when either one of onDragEnd, onDragCompleted or
|
||||||
|
/// onDraggableCanceled is called.
|
||||||
|
///
|
||||||
|
/// The callback might be called multiple times per each drag event
|
||||||
|
final VoidCallback onDragEndedAny;
|
||||||
|
|
||||||
|
/// Size of the feedback widget that appears under the pointer.
|
||||||
|
///
|
||||||
|
/// Right now a translucent version of [child] is being shown
|
||||||
|
final Size feedbackSize;
|
||||||
|
|
||||||
|
static final _log = Logger("widget.draggable.Draggable");
|
||||||
|
}
|
55
lib/widget/draggable_item_list_mixin.dart
Normal file
55
lib/widget/draggable_item_list_mixin.dart
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
|
import 'package:nc_photos/widget/draggable.dart' as _;
|
||||||
|
import 'package:nc_photos/widget/measurable_item_list.dart';
|
||||||
|
|
||||||
|
abstract class DraggableItem {
|
||||||
|
Widget buildWidget(BuildContext context);
|
||||||
|
|
||||||
|
bool get isDraggable => false;
|
||||||
|
DragTargetAccept<DraggableItem> get onDropBefore => null;
|
||||||
|
DragTargetAccept<DraggableItem> get onDropAfter => null;
|
||||||
|
VoidCallback get onDragStarted => null;
|
||||||
|
VoidCallback get onDragEndedAny => null;
|
||||||
|
StaggeredTile get staggeredTile => const StaggeredTile.count(1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin DraggableItemListMixin<T extends StatefulWidget> on State<T> {
|
||||||
|
@protected
|
||||||
|
Widget buildDraggableItemList({
|
||||||
|
@required double maxCrossAxisExtent,
|
||||||
|
ValueChanged<double> onMaxExtentChanged,
|
||||||
|
}) {
|
||||||
|
_maxCrossAxisExtent = maxCrossAxisExtent;
|
||||||
|
return MeasurableItemList(
|
||||||
|
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||||
|
itemCount: _items.length,
|
||||||
|
itemBuilder: _buildItem,
|
||||||
|
staggeredTileBuilder: (index) => _items[index].staggeredTile,
|
||||||
|
onMaxExtentChanged: onMaxExtentChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@protected
|
||||||
|
set draggableItemList(List<DraggableItem> newItems) {
|
||||||
|
_items = newItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildItem(BuildContext context, int index) {
|
||||||
|
final item = _items[index];
|
||||||
|
return _.Draggable(
|
||||||
|
data: item,
|
||||||
|
child: item.buildWidget(context),
|
||||||
|
onDropBefore: item.onDropBefore,
|
||||||
|
onDropAfter: item.onDropAfter,
|
||||||
|
onDragStarted: item.onDragStarted,
|
||||||
|
onDragEndedAny: item.onDragEndedAny,
|
||||||
|
feedbackSize: _maxCrossAxisExtent != null
|
||||||
|
? Size(_maxCrossAxisExtent * .65, _maxCrossAxisExtent * .65)
|
||||||
|
: null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
var _items = <DraggableItem>[];
|
||||||
|
double _maxCrossAxisExtent;
|
||||||
|
}
|
Loading…
Reference in a new issue