Support adding map to client albums

This commit is contained in:
Ming Ming 2024-10-31 00:30:12 +08:00
parent 12f6f59a1c
commit 4959f06fec
23 changed files with 393 additions and 18 deletions

View file

@ -10,8 +10,10 @@ import 'package:nc_photos/controller/files_controller.dart';
import 'package:nc_photos/controller/server_controller.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/collection.dart';
import 'package:nc_photos/entity/collection/adapter.dart';
import 'package:nc_photos/entity/collection/util.dart';
import 'package:nc_photos/entity/collection_item.dart';
import 'package:nc_photos/entity/collection_item/new_item.dart';
import 'package:nc_photos/entity/collection_item/util.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/sharee.dart';
@ -25,6 +27,7 @@ import 'package:nc_photos/use_case/collection/remove_collections.dart';
import 'package:nc_photos/use_case/collection/share_collection.dart';
import 'package:nc_photos/use_case/collection/unshare_collection.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_collection/np_collection.dart';
import 'package:np_common/or_null.dart';
import 'package:np_common/type.dart';
import 'package:rxdart/rxdart.dart';
@ -208,7 +211,19 @@ class CollectionsController {
knownItems: (item?.items.isEmpty ?? true) ? null : item!.items,
);
});
_updateCollection(c, items);
final newItems = await items?.asyncMap((e) {
if (e is NewCollectionItem) {
try {
return CollectionAdapter.of(_c, account, c).adaptToNewItem(e);
} catch (e, stackTrace) {
_log.severe("[edit] Failed to adapt new item: $e", e, stackTrace);
return Future.value(null);
}
} else {
return Future.value(e);
}
});
_updateCollection(c, newItems?.whereNotNull().toList());
} catch (e, stackTrace) {
_dataStreamController.addError(e, stackTrace);
}

View file

@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/type.dart';
import 'package:np_gps_map/np_gps_map.dart';
import 'package:np_string/np_string.dart';
import 'package:to_string/to_string.dart';
@ -30,6 +31,9 @@ abstract class AlbumItem with EquatableMixin {
case AlbumLabelItem._type:
return AlbumLabelItem.fromJson(
content.cast<String, dynamic>(), addedBy, addedAt);
case AlbumMapItem._type:
return AlbumMapItem.fromJson(
content.cast<String, dynamic>(), addedBy, addedAt);
default:
_log.shout("[fromJson] Unknown type: $type");
throw ArgumentError.value(type, "type");
@ -42,6 +46,8 @@ abstract class AlbumItem with EquatableMixin {
return AlbumFileItem._type;
} else if (this is AlbumLabelItem) {
return AlbumLabelItem._type;
} else if (this is AlbumMapItem) {
return AlbumMapItem._type;
} else {
throw StateError("Unknwon subtype");
}
@ -181,3 +187,60 @@ class AlbumLabelItem extends AlbumItem {
static const _type = "label";
}
@toString
class AlbumMapItem extends AlbumItem {
AlbumMapItem({
required super.addedBy,
required super.addedAt,
required this.location,
});
factory AlbumMapItem.fromJson(
JsonObj json, CiString addedBy, DateTime addedAt) {
return AlbumMapItem(
addedBy: addedBy,
addedAt: addedAt,
location: CameraPosition.fromJson(json["location"]),
);
}
@override
String toString() => _$toString();
@override
JsonObj toContentJson() {
return {
"location": location.toJson(),
};
}
@override
bool compareServerIdentity(AlbumItem other) =>
other is AlbumMapItem &&
location == other.location &&
addedBy == other.addedBy &&
addedAt == other.addedAt;
AlbumMapItem copyWith({
CiString? addedBy,
DateTime? addedAt,
CameraPosition? location,
}) {
return AlbumMapItem(
addedBy: addedBy ?? this.addedBy,
addedAt: addedAt ?? this.addedAt,
location: location ?? this.location,
);
}
@override
List<Object?> get props => [
...super.props,
location,
];
final CameraPosition location;
static const _type = "map";
}

View file

@ -78,3 +78,10 @@ extension _$AlbumLabelItemToString on AlbumLabelItem {
return "AlbumLabelItem {addedBy: $addedBy, addedAt: $addedAt, text: $text}";
}
}
extension _$AlbumMapItemToString on AlbumMapItem {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "AlbumMapItem {addedBy: $addedBy, addedAt: $addedAt, location: $location}";
}
}

View file

@ -88,6 +88,8 @@ enum CollectionCapability {
rename,
// text labels
labelItem,
// maps
mapItem,
// set the cover image
manualCover,
// share the collection with other user on the same server

View file

@ -54,6 +54,8 @@ class CollectionAlbumAdapter implements CollectionAdapter {
return CollectionFileItemAlbumAdapter(i);
} else if (i is AlbumLabelItem) {
return CollectionLabelItemAlbumAdapter(i);
} else if (i is AlbumMapItem) {
return CollectionMapItemAlbumAdapter(i);
} else {
_log.shout("[listItem] Unknown item type: ${i.runtimeType}");
throw UnimplementedError("Unknown item type: ${i.runtimeType}");
@ -100,6 +102,12 @@ class CollectionAlbumAdapter implements CollectionAdapter {
addedAt: e.createdAt,
text: e.text,
);
} else if (e is NewCollectionMapItem) {
return AlbumMapItem(
addedBy: account.userId,
addedAt: e.createdAt,
location: e.location,
);
} else {
_log.severe("[edit] Unsupported type: ${e.runtimeType}");
return null;
@ -248,6 +256,16 @@ class CollectionAlbumAdapter implements CollectionAdapter {
.reversed
.firstWhere((e) => e.text == original.text);
return CollectionLabelItemAlbumAdapter(item);
} else if (original is NewCollectionMapItem) {
final item = AlbumStaticProvider.of(_provider.album)
.items
.whereType<AlbumMapItem>()
.sorted((a, b) => a.addedAt.compareTo(b.addedAt))
.reversed
.firstWhere((e) =>
e.location == original.location &&
e.addedAt == original.createdAt);
return CollectionMapItemAlbumAdapter(item);
} else {
throw UnsupportedError("Unsupported type: ${original.runtimeType}");
}

View file

@ -56,6 +56,7 @@ class CollectionAlbumProvider
CollectionCapability.manualItem,
CollectionCapability.manualSort,
CollectionCapability.labelItem,
CollectionCapability.mapItem,
CollectionCapability.share,
],
];
@ -66,6 +67,7 @@ class CollectionAlbumProvider
if (album.provider is AlbumStaticProvider) ...[
CollectionCapability.manualItem,
CollectionCapability.labelItem,
CollectionCapability.mapItem,
],
];

View file

@ -59,6 +59,12 @@ class CollectionExporter {
addedAt: clock.now().toUtc(),
text: e.text,
);
} else if (e is CollectionMapItem) {
return AlbumMapItem(
addedBy: account.userId,
addedAt: clock.now().toUtc(),
location: e.location,
);
} else {
return null;
}

View file

@ -1,4 +1,5 @@
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:np_gps_map/np_gps_map.dart';
/// An item in a [Collection]
abstract class CollectionItem {
@ -24,3 +25,13 @@ abstract class CollectionLabelItem implements CollectionItem {
Object get id;
String get text;
}
abstract class CollectionMapItem implements CollectionItem {
const CollectionMapItem();
/// An object used to identify this instance
///
/// [id] should be unique and stable
Object get id;
CameraPosition get location;
}

View file

@ -1,6 +1,7 @@
import 'package:nc_photos/entity/album/item.dart';
import 'package:nc_photos/entity/collection_item.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:np_gps_map/np_gps_map.dart';
import 'package:to_string/to_string.dart';
part 'album_item_adapter.g.dart';
@ -11,6 +12,8 @@ mixin AlbumAdaptedCollectionItem on CollectionItem {
return CollectionFileItemAlbumAdapter(item);
} else if (item is AlbumLabelItem) {
return CollectionLabelItemAlbumAdapter(item);
} else if (item is AlbumMapItem) {
return CollectionMapItemAlbumAdapter(item);
} else {
throw ArgumentError("Unknown type: ${item.runtimeType}");
}
@ -64,3 +67,23 @@ class CollectionLabelItemAlbumAdapter extends CollectionLabelItem
final AlbumLabelItem item;
}
@toString
class CollectionMapItemAlbumAdapter extends CollectionMapItem
with AlbumAdaptedCollectionItem {
const CollectionMapItemAlbumAdapter(this.item);
@override
String toString() => _$toString();
@override
Object get id => item.addedAt;
@override
CameraPosition get location => item.location;
@override
AlbumItem get albumItem => item;
final AlbumMapItem item;
}

View file

@ -21,3 +21,11 @@ extension _$CollectionLabelItemAlbumAdapterToString
return "CollectionLabelItemAlbumAdapter {item: $item}";
}
}
extension _$CollectionMapItemAlbumAdapterToString
on CollectionMapItemAlbumAdapter {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "CollectionMapItemAlbumAdapter {item: $item}";
}
}

View file

@ -1,5 +1,6 @@
import 'package:nc_photos/entity/collection_item.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:np_gps_map/np_gps_map.dart';
import 'package:to_string/to_string.dart';
part 'new_item.g.dart';
@ -47,3 +48,23 @@ class NewCollectionLabelItem implements CollectionLabelItem, NewCollectionItem {
final DateTime createdAt;
}
/// A new [CollectionMapItem]
///
/// This class is for marking an intermediate item that has recently been added
/// but not necessarily persisted yet to the provider of this collection
@toString
class NewCollectionMapItem implements CollectionMapItem, NewCollectionItem {
const NewCollectionMapItem(this.location, this.createdAt);
@override
String toString() => _$toString();
@override
Object get id => createdAt;
@override
final CameraPosition location;
final DateTime createdAt;
}

View file

@ -19,3 +19,10 @@ extension _$NewCollectionLabelItemToString on NewCollectionLabelItem {
return "NewCollectionLabelItem {text: $text, createdAt: $createdAt}";
}
}
extension _$NewCollectionMapItemToString on NewCollectionMapItem {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "NewCollectionMapItem {location: $location, createdAt: $createdAt}";
}
}

View file

@ -1522,6 +1522,10 @@
"description": "Some button can't be removed. This message will be shown instead when user try to do so"
},
"placePickerTitle": "Pick a place",
"albumAddMapTooltip": "Add map",
"@albumAddMapTooltip": {
"description": "Add a map that display between photos to an album"
},
"errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues",
"@errorUnauthenticated": {

View file

@ -269,6 +269,7 @@
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip",
"errorUnauthenticated",
"errorDisconnected",
"errorLocked",
@ -288,7 +289,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"de": [
@ -300,7 +302,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"el": [
@ -457,7 +460,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"es": [
@ -469,7 +473,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"fi": [
@ -517,7 +522,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"fr": [
@ -565,7 +571,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"it": [
@ -618,7 +625,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"nl": [
@ -1008,6 +1016,7 @@
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip",
"errorUnauthenticated",
"errorDisconnected",
"errorLocked",
@ -1067,7 +1076,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"pt": [
@ -1135,7 +1145,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"ru": [
@ -1183,7 +1194,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"tr": [
@ -1195,7 +1207,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"zh": [
@ -1274,7 +1287,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
],
"zh_Hant": [
@ -1447,6 +1461,7 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle"
"placePickerTitle",
"albumAddMapTooltip"
]
}

View file

@ -36,11 +36,13 @@ import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/exception_event.dart';
import 'package:nc_photos/flutter_util.dart' as flutter_util;
import 'package:nc_photos/gps_map_util.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/np_api_util.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/session_storage.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/stream_util.dart';
import 'package:nc_photos/widget/album_share_outlier_browser.dart';
import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart';
import 'package:nc_photos/widget/collection_picker.dart';
@ -52,6 +54,7 @@ import 'package:nc_photos/widget/network_thumbnail.dart';
import 'package:nc_photos/widget/page_visibility_mixin.dart';
import 'package:nc_photos/widget/photo_list_item.dart';
import 'package:nc_photos/widget/photo_list_util.dart' as photo_list_util;
import 'package:nc_photos/widget/place_picker/place_picker.dart';
import 'package:nc_photos/widget/selectable_item_list.dart';
import 'package:nc_photos/widget/selection_app_bar.dart';
import 'package:nc_photos/widget/share_collection_dialog.dart';
@ -62,6 +65,7 @@ import 'package:nc_photos/widget/viewer.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/or_null.dart';
import 'package:np_datetime/np_datetime.dart';
import 'package:np_gps_map/np_gps_map.dart';
import 'package:np_ui/np_ui.dart';
import 'package:to_string/to_string.dart';
@ -413,7 +417,7 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
typedef _BlocListener = BlocListener<_Bloc, _State>;
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
// typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
extension on BuildContext {
_Bloc get bloc => read<_Bloc>();

View file

@ -219,6 +219,13 @@ extension _$_AddLabelToCollectionToString on _AddLabelToCollection {
}
}
extension _$_AddMapToCollectionToString on _AddMapToCollection {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_AddMapToCollection {location: $location}";
}
}
extension _$_EditSortToString on _EditSort {
String _$toString() {
// ignore: unnecessary_string_interpolations

View file

@ -364,6 +364,12 @@ class _EditAppBar extends StatelessWidget {
tooltip: L10n.global().albumAddTextTooltip,
onPressed: () => _onAddTextPressed(context),
),
if (capabilitiesAdapter.isPermitted(CollectionCapability.mapItem))
IconButton(
icon: const Icon(Icons.map_outlined),
tooltip: L10n.global().albumAddMapTooltip,
onPressed: () => _onAddMapPressed(context),
),
if (capabilitiesAdapter.isPermitted(CollectionCapability.sort))
IconButton(
icon: const Icon(Icons.sort_by_alpha_outlined),
@ -387,6 +393,15 @@ class _EditAppBar extends StatelessWidget {
context.read<_Bloc>().add(_AddLabelToCollection(result));
}
Future<void> _onAddMapPressed(BuildContext context) async {
final result = await Navigator.of(context)
.pushNamed<CameraPosition>(PlacePicker.routeName);
if (result == null) {
return;
}
context.read<_Bloc>().add(_AddMapToCollection(result));
}
Future<void> _onSortPressed(BuildContext context) async {
final current = context
.read<_Bloc>()

View file

@ -30,6 +30,7 @@ class _Bloc extends Bloc<_Event, _State>
on<_BeginEdit>(_onBeginEdit);
on<_EditName>(_onEditName);
on<_AddLabelToCollection>(_onAddLabelToCollection);
on<_AddMapToCollection>(_onAddMapToCollection);
on<_EditSort>(_onEditSort);
on<_EditManualSort>(_onEditManualSort);
on<_TransformEditItems>(_onTransformEditItems);
@ -210,6 +211,17 @@ class _Bloc extends Bloc<_Event, _State>
));
}
void _onAddMapToCollection(_AddMapToCollection ev, Emitter<_State> emit) {
_log.info(ev);
assert(isCollectionCapabilityPermitted(CollectionCapability.mapItem));
emit(state.copyWith(
editItems: [
NewCollectionMapItem(ev.location, clock.now().toUtc()),
...state.editItems ?? state.items,
],
));
}
void _onEditSort(_EditSort ev, Emitter<_State> emit) {
_log.info(ev);
final result = _transformItems(state.editItems ?? state.items, ev.sort);
@ -505,6 +517,15 @@ class _Bloc extends Bloc<_Event, _State>
// TODO
},
));
} else if (item is CollectionMapItem) {
transformed.add(_MapItem(
original: item,
id: item.id,
location: item.location,
onEditPressed: () {
// TODO
},
));
}
}
return _TransformResult(

View file

@ -30,3 +30,58 @@ class _EditLabelView extends StatelessWidget {
final String text;
final VoidCallback? onEditPressed;
}
class _MapView extends StatelessWidget {
const _MapView({
required this.location,
this.onTap,
});
@override
Widget build(BuildContext context) {
return ValueStreamBuilderEx<GpsMapProvider>(
stream: context.read<PrefController>().gpsMapProvider,
builder: StreamWidgetBuilder.value(
(context, gpsMapProvider) => StaticMap(
key: Key(location.toString()),
providerHint: gpsMapProvider,
location: location,
onTap: onTap,
),
),
);
}
final CameraPosition location;
final VoidCallback? onTap;
}
class _EditMapView extends StatelessWidget {
const _EditMapView({
required this.location,
required this.onEditPressed,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
AbsorbPointer(
absorbing: true,
child: _MapView(location: location),
),
Positioned(
top: 8,
right: 8,
child: FloatingActionButton.small(
onPressed: onEditPressed,
child: const Icon(Icons.edit_outlined),
),
),
],
);
}
final CameraPosition location;
final VoidCallback? onEditPressed;
}

View file

@ -174,6 +174,16 @@ class _AddLabelToCollection implements _Event {
final String label;
}
@toString
class _AddMapToCollection implements _Event {
const _AddMapToCollection(this.location);
@override
String toString() => _$toString();
final CameraPosition location;
}
@toString
class _EditSort implements _Event {
const _EditSort(this.sort);

View file

@ -8,6 +8,7 @@ abstract class _Item implements SelectableItemMetadata, DraggableItemMetadata {
Widget buildWidget(BuildContext context);
Widget? buildDragFeedbackWidget(BuildContext context) => null;
Size? dragFeedbackWidgetSize() => null;
}
/// Items backed by an actual [CollectionItem]
@ -140,6 +141,62 @@ class _LabelItem extends _ActualItem {
final VoidCallback? onEditPressed;
}
class _MapItem extends _ActualItem {
const _MapItem({
required super.original,
required this.id,
required this.location,
required this.onEditPressed,
});
@override
bool get isDraggable => true;
@override
bool operator ==(Object other) =>
identical(this, other) || (other is _MapItem && id == other.id);
@override
int get hashCode => id.hashCode;
@override
StaggeredTile get staggeredTile => const StaggeredTile.extent(99, 256);
@override
Widget buildWidget(BuildContext context) {
return _BlocSelector(
selector: (state) => state.isEditMode,
builder: (context, isEditMode) => isEditMode
? _EditMapView(
location: location,
onEditPressed: onEditPressed,
)
: _MapView(
location: location,
onTap: () {
launchExternalMap(location);
},
),
);
}
@override
Widget? buildDragFeedbackWidget(BuildContext context) {
return Icon(
Icons.place,
color: Theme.of(context).colorScheme.primary,
size: 48,
);
}
@override
Size? dragFeedbackWidgetSize() => const Size.square(48);
final Object id;
final CameraPosition location;
final VoidCallback? onEditPressed;
}
class _DateItem extends _Item {
const _DateItem({
required this.date,

View file

@ -120,6 +120,8 @@ class _EditContentList extends StatelessWidget {
itemDragFeedbackBuilder: (context, _, item) =>
item.buildDragFeedbackWidget(context) ??
item.buildWidget(context),
itemDragFeedbackSize: (_, item) =>
item.dragFeedbackWidgetSize(),
staggeredTileBuilder: (_, item) => item.staggeredTile,
onDragResult: (results) {
context.addEvent(_EditManualSort(results));

View file

@ -24,6 +24,7 @@ class DraggableItemList<T extends DraggableItemMetadata>
required this.maxCrossAxisExtent,
required this.itemBuilder,
required this.itemDragFeedbackBuilder,
this.itemDragFeedbackSize,
required this.staggeredTileBuilder,
this.onDragResult,
this.onDraggingChanged,
@ -38,6 +39,7 @@ class DraggableItemList<T extends DraggableItemMetadata>
itemBuilder;
final Widget? Function(BuildContext context, int index, T metadata)?
itemDragFeedbackBuilder;
final Size? Function(int index, T metadata)? itemDragFeedbackSize;
final StaggeredTile? Function(int index, T metadata) staggeredTileBuilder;
/// Called when an item is dropped to a new place
@ -63,9 +65,9 @@ class _DraggableItemListState<T extends DraggableItemMetadata>
if (meta.isDraggable) {
return my.Draggable<_DraggableData>(
data: _DraggableData(i, meta),
feedback: SizedBox(
width: widget.maxCrossAxisExtent * .65,
height: widget.maxCrossAxisExtent * .65,
feedback: SizedBox.fromSize(
size: widget.itemDragFeedbackSize?.call(i, meta) ??
Size.square(widget.maxCrossAxisExtent * .65),
child: widget.itemDragFeedbackBuilder?.call(context, i, meta),
),
onDropBefore: (data) => _onMoved(data.index, i, true),