From 4959f06fecd5fa1e68ddcaf001cbdf3a394abd4a Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Thu, 31 Oct 2024 00:30:12 +0800 Subject: [PATCH] Support adding map to client albums --- .../controller/collections_controller.dart | 17 ++++- app/lib/entity/album/item.dart | 63 +++++++++++++++++++ app/lib/entity/album/item.g.dart | 7 +++ app/lib/entity/collection.dart | 2 + app/lib/entity/collection/adapter/album.dart | 18 ++++++ .../collection/content_provider/album.dart | 2 + app/lib/entity/collection/exporter.dart | 6 ++ app/lib/entity/collection_item.dart | 11 ++++ .../collection_item/album_item_adapter.dart | 23 +++++++ .../collection_item/album_item_adapter.g.dart | 8 +++ app/lib/entity/collection_item/new_item.dart | 21 +++++++ .../entity/collection_item/new_item.g.dart | 7 +++ app/lib/l10n/app_en.arb | 4 ++ app/lib/l10n/untranslated-messages.txt | 41 ++++++++---- app/lib/widget/collection_browser.dart | 6 +- app/lib/widget/collection_browser.g.dart | 7 +++ .../widget/collection_browser/app_bar.dart | 15 +++++ app/lib/widget/collection_browser/bloc.dart | 21 +++++++ .../widget/collection_browser/item_view.dart | 55 ++++++++++++++++ .../collection_browser/state_event.dart | 10 +++ app/lib/widget/collection_browser/type.dart | 57 +++++++++++++++++ app/lib/widget/collection_browser/view.dart | 2 + app/lib/widget/draggable_item_list.dart | 8 ++- 23 files changed, 393 insertions(+), 18 deletions(-) diff --git a/app/lib/controller/collections_controller.dart b/app/lib/controller/collections_controller.dart index 5b6b56a2..70b49a92 100644 --- a/app/lib/controller/collections_controller.dart +++ b/app/lib/controller/collections_controller.dart @@ -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); } diff --git a/app/lib/entity/album/item.dart b/app/lib/entity/album/item.dart index cbfa97c5..23d2520a 100644 --- a/app/lib/entity/album/item.dart +++ b/app/lib/entity/album/item.dart @@ -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(), addedBy, addedAt); + case AlbumMapItem._type: + return AlbumMapItem.fromJson( + content.cast(), 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 get props => [ + ...super.props, + location, + ]; + + final CameraPosition location; + + static const _type = "map"; +} diff --git a/app/lib/entity/album/item.g.dart b/app/lib/entity/album/item.g.dart index 12bb28f5..5a0fa1ae 100644 --- a/app/lib/entity/album/item.g.dart +++ b/app/lib/entity/album/item.g.dart @@ -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}"; + } +} diff --git a/app/lib/entity/collection.dart b/app/lib/entity/collection.dart index 2a805a30..181f3a94 100644 --- a/app/lib/entity/collection.dart +++ b/app/lib/entity/collection.dart @@ -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 diff --git a/app/lib/entity/collection/adapter/album.dart b/app/lib/entity/collection/adapter/album.dart index a57608bd..6093a0b9 100644 --- a/app/lib/entity/collection/adapter/album.dart +++ b/app/lib/entity/collection/adapter/album.dart @@ -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() + .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}"); } diff --git a/app/lib/entity/collection/content_provider/album.dart b/app/lib/entity/collection/content_provider/album.dart index 9a077dbb..d45f9e99 100644 --- a/app/lib/entity/collection/content_provider/album.dart +++ b/app/lib/entity/collection/content_provider/album.dart @@ -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, ], ]; diff --git a/app/lib/entity/collection/exporter.dart b/app/lib/entity/collection/exporter.dart index 899669dc..729cab13 100644 --- a/app/lib/entity/collection/exporter.dart +++ b/app/lib/entity/collection/exporter.dart @@ -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; } diff --git a/app/lib/entity/collection_item.dart b/app/lib/entity/collection_item.dart index d8b5e5a2..da54e15e 100644 --- a/app/lib/entity/collection_item.dart +++ b/app/lib/entity/collection_item.dart @@ -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; +} diff --git a/app/lib/entity/collection_item/album_item_adapter.dart b/app/lib/entity/collection_item/album_item_adapter.dart index 59fd77be..6e979759 100644 --- a/app/lib/entity/collection_item/album_item_adapter.dart +++ b/app/lib/entity/collection_item/album_item_adapter.dart @@ -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; +} diff --git a/app/lib/entity/collection_item/album_item_adapter.g.dart b/app/lib/entity/collection_item/album_item_adapter.g.dart index 18769b88..0c7e47cf 100644 --- a/app/lib/entity/collection_item/album_item_adapter.g.dart +++ b/app/lib/entity/collection_item/album_item_adapter.g.dart @@ -21,3 +21,11 @@ extension _$CollectionLabelItemAlbumAdapterToString return "CollectionLabelItemAlbumAdapter {item: $item}"; } } + +extension _$CollectionMapItemAlbumAdapterToString + on CollectionMapItemAlbumAdapter { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "CollectionMapItemAlbumAdapter {item: $item}"; + } +} diff --git a/app/lib/entity/collection_item/new_item.dart b/app/lib/entity/collection_item/new_item.dart index 9d75fd05..5ffbe456 100644 --- a/app/lib/entity/collection_item/new_item.dart +++ b/app/lib/entity/collection_item/new_item.dart @@ -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; +} diff --git a/app/lib/entity/collection_item/new_item.g.dart b/app/lib/entity/collection_item/new_item.g.dart index b22ab8c7..e33518a3 100644 --- a/app/lib/entity/collection_item/new_item.g.dart +++ b/app/lib/entity/collection_item/new_item.g.dart @@ -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}"; + } +} diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index 35bbf9de..0f703d68 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -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": { diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index 72dd89df..262fcee0 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -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" ] } diff --git a/app/lib/widget/collection_browser.dart b/app/lib/widget/collection_browser.dart index 9955d38f..7f5b1073 100644 --- a/app/lib/widget/collection_browser.dart +++ b/app/lib/widget/collection_browser.dart @@ -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 = BlocListenerT<_Bloc, _State, T>; -// typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; +typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; extension on BuildContext { _Bloc get bloc => read<_Bloc>(); diff --git a/app/lib/widget/collection_browser.g.dart b/app/lib/widget/collection_browser.g.dart index eeacd3b0..afe44c02 100644 --- a/app/lib/widget/collection_browser.g.dart +++ b/app/lib/widget/collection_browser.g.dart @@ -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 diff --git a/app/lib/widget/collection_browser/app_bar.dart b/app/lib/widget/collection_browser/app_bar.dart index 9fe15dc7..85802dbd 100644 --- a/app/lib/widget/collection_browser/app_bar.dart +++ b/app/lib/widget/collection_browser/app_bar.dart @@ -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 _onAddMapPressed(BuildContext context) async { + final result = await Navigator.of(context) + .pushNamed(PlacePicker.routeName); + if (result == null) { + return; + } + context.read<_Bloc>().add(_AddMapToCollection(result)); + } + Future _onSortPressed(BuildContext context) async { final current = context .read<_Bloc>() diff --git a/app/lib/widget/collection_browser/bloc.dart b/app/lib/widget/collection_browser/bloc.dart index 88dd09c1..3ed4a0e8 100644 --- a/app/lib/widget/collection_browser/bloc.dart +++ b/app/lib/widget/collection_browser/bloc.dart @@ -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( diff --git a/app/lib/widget/collection_browser/item_view.dart b/app/lib/widget/collection_browser/item_view.dart index 5fe34a8d..95b69c1d 100644 --- a/app/lib/widget/collection_browser/item_view.dart +++ b/app/lib/widget/collection_browser/item_view.dart @@ -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( + stream: context.read().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; +} diff --git a/app/lib/widget/collection_browser/state_event.dart b/app/lib/widget/collection_browser/state_event.dart index 32e97dd1..cca6ee1a 100644 --- a/app/lib/widget/collection_browser/state_event.dart +++ b/app/lib/widget/collection_browser/state_event.dart @@ -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); diff --git a/app/lib/widget/collection_browser/type.dart b/app/lib/widget/collection_browser/type.dart index ba98df09..84427acf 100644 --- a/app/lib/widget/collection_browser/type.dart +++ b/app/lib/widget/collection_browser/type.dart @@ -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, diff --git a/app/lib/widget/collection_browser/view.dart b/app/lib/widget/collection_browser/view.dart index 1aad5f01..f38b2f87 100644 --- a/app/lib/widget/collection_browser/view.dart +++ b/app/lib/widget/collection_browser/view.dart @@ -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)); diff --git a/app/lib/widget/draggable_item_list.dart b/app/lib/widget/draggable_item_list.dart index 2f604ad7..6b6d6676 100644 --- a/app/lib/widget/draggable_item_list.dart +++ b/app/lib/widget/draggable_item_list.dart @@ -24,6 +24,7 @@ class DraggableItemList required this.maxCrossAxisExtent, required this.itemBuilder, required this.itemDragFeedbackBuilder, + this.itemDragFeedbackSize, required this.staggeredTileBuilder, this.onDragResult, this.onDraggingChanged, @@ -38,6 +39,7 @@ class DraggableItemList 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 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),