Merge branch 'album-map'

This commit is contained in:
Ming Ming 2024-10-31 01:41:35 +08:00
commit 17916a8434
42 changed files with 872 additions and 129 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

@ -14,7 +14,6 @@ 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/nc_album.dart';
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/use_case/find_file_descriptor.dart';
import 'package:nc_photos/use_case/nc_album/add_file_to_nc_album.dart';
import 'package:nc_photos/use_case/nc_album/edit_nc_album.dart';
@ -98,16 +97,16 @@ class CollectionNcAlbumAdapter
_log.warning(
"[edit] Nextcloud album does not support editing item or sort");
}
final newItems = items?.run((items) => items
.map((e) => e is CollectionFileItem ? e.file : null)
.whereNotNull()
.toList());
// final newItems = items?.run((items) => items
// .map((e) => e is CollectionFileItem ? e.file : null)
// .whereNotNull()
// .toList());
final newAlbum = await EditNcAlbum(_c)(
account,
_provider.album,
name: name,
items: newItems,
itemSort: itemSort,
// items: newItems,
// itemSort: itemSort,
);
return collection.copyWith(
name: name,

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

@ -1,4 +1,6 @@
import 'package:android_intent_plus/android_intent.dart';
import 'package:np_gps_map/np_gps_map.dart';
import 'package:np_platform_util/np_platform_util.dart';
extension GpsMapProviderExtension on GpsMapProvider {
String toUserString() {
@ -10,3 +12,14 @@ extension GpsMapProviderExtension on GpsMapProvider {
}
}
}
void launchExternalMap(CameraPosition location) {
if (getRawPlatform() == NpPlatform.android) {
final intent = AndroidIntent(
action: "action_view",
data: Uri.encodeFull(
"geo:${location.center.latitude},${location.center.longitude}?z=${location.zoom}"),
);
intent.launch();
}
}

View file

@ -1521,6 +1521,11 @@
"@customizeButtonsUnsupportedWarning": {
"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

@ -268,6 +268,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip",
"errorUnauthenticated",
"errorDisconnected",
"errorLocked",
@ -286,7 +288,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"de": [
@ -297,7 +301,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"el": [
@ -453,7 +459,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"es": [
@ -464,7 +472,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"fi": [
@ -511,7 +521,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"fr": [
@ -558,7 +570,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"it": [
@ -610,7 +624,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"nl": [
@ -999,6 +1015,8 @@
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip",
"errorUnauthenticated",
"errorDisconnected",
"errorLocked",
@ -1057,7 +1075,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"pt": [
@ -1124,7 +1144,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"ru": [
@ -1171,7 +1193,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"tr": [
@ -1182,7 +1206,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"zh": [
@ -1260,7 +1286,9 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"placePickerTitle",
"albumAddMapTooltip"
],
"zh_Hant": [
@ -1432,6 +1460,8 @@
"livePhotoTooltip",
"dragAndDropRearrangeButtons",
"customizeCollectionsNavBarDescription",
"customizeButtonsUnsupportedWarning"
"customizeButtonsUnsupportedWarning",
"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,12 +65,14 @@ 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';
part 'collection_browser.g.dart';
part 'collection_browser/app_bar.dart';
part 'collection_browser/bloc.dart';
part 'collection_browser/item_view.dart';
part 'collection_browser/state_event.dart';
part 'collection_browser/type.dart';
part 'collection_browser/view.dart';
@ -412,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);
@ -497,22 +509,23 @@ class _Bloc extends Bloc<_Event, _State>
"[_transformItems] Unsupported file format: ${item.file.fdMime}");
}
} else if (item is CollectionLabelItem) {
if (state.isEditMode) {
transformed.add(_EditLabelListItem(
original: item,
id: item.id,
text: item.text,
onEditPressed: () {
// TODO
},
));
} else {
transformed.add(_LabelItem(
original: item,
id: item.id,
text: item.text,
));
}
transformed.add(_LabelItem(
original: item,
id: item.id,
text: item.text,
onEditPressed: () {
// TODO
},
));
} else if (item is CollectionMapItem) {
transformed.add(_MapItem(
original: item,
id: item.id,
location: item.location,
onEditPressed: () {
// TODO
},
));
}
}
return _TransformResult(

View file

@ -0,0 +1,87 @@
part of '../collection_browser.dart';
class _LabelView extends StatelessWidget {
const _LabelView({
required this.text,
});
@override
Widget build(BuildContext context) {
return PhotoListLabel(text: text);
}
final String text;
}
class _EditLabelView extends StatelessWidget {
const _EditLabelView({
required this.text,
required this.onEditPressed,
});
@override
Widget build(BuildContext context) {
return PhotoListLabelEdit(
text: text,
onEditPressed: onEditPressed,
);
}
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]
@ -101,8 +102,12 @@ class _LabelItem extends _ActualItem {
required super.original,
required this.id,
required this.text,
required this.onEditPressed,
});
@override
bool get isDraggable => true;
@override
bool operator ==(Object other) =>
identical(this, other) || (other is _LabelItem && id == other.id);
@ -115,39 +120,80 @@ class _LabelItem extends _ActualItem {
@override
Widget buildWidget(BuildContext context) {
return PhotoListLabel(
text: text,
return _BlocSelector(
selector: (state) => state.isEditMode,
builder: (context, isEditMode) => isEditMode
? _EditLabelView(
text: text,
onEditPressed: onEditPressed,
)
: _LabelView(text: text),
);
}
@override
Widget? buildDragFeedbackWidget(BuildContext context) {
return _LabelView(text: text);
}
final Object id;
final String text;
final VoidCallback? onEditPressed;
}
class _EditLabelListItem extends _LabelItem {
const _EditLabelListItem({
class _MapItem extends _ActualItem {
const _MapItem({
required super.original,
required super.id,
required super.text,
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 PhotoListLabelEdit(
text: text,
onEditPressed: onEditPressed,
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 super.buildWidget(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;
}

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),

View file

@ -38,6 +38,7 @@ import 'package:nc_photos/widget/image_enhancer.dart';
import 'package:nc_photos/widget/local_file_viewer.dart';
import 'package:nc_photos/widget/map_browser.dart';
import 'package:nc_photos/widget/people_browser.dart';
import 'package:nc_photos/widget/place_picker/place_picker.dart';
import 'package:nc_photos/widget/places_browser.dart';
import 'package:nc_photos/widget/result_viewer.dart';
import 'package:nc_photos/widget/root_picker.dart';
@ -213,6 +214,7 @@ class _WrappedAppState extends State<_WrappedApp>
ArchiveBrowser.routeName: ArchiveBrowser.buildRoute,
TrustedCertManager.routeName: TrustedCertManager.buildRoute,
MapBrowser.routeName: MapBrowser.buildRoute,
PlacePicker.routeName: PlacePicker.buildRoute,
};
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {

View file

@ -0,0 +1,21 @@
part of 'place_picker.dart';
@npLog
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
_Bloc() : super(_State.init()) {
on<_SetPosition>(_onSetPosition);
}
@override
String get tag => _log.fullName;
@override
bool Function(dynamic, dynamic)? get shouldLog => (currentState, nextState) {
return currentState.position == nextState.position;
};
void _onSetPosition(_SetPosition ev, _Emitter emit) {
// _log.info(ev);
emit(state.copyWith(position: ev.value));
}
}

View file

@ -0,0 +1,93 @@
import 'package:copy_with/copy_with.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/bloc_util.dart';
import 'package:nc_photos/controller/pref_controller.dart';
import 'package:nc_photos/stream_util.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_gps_map/np_gps_map.dart';
import 'package:to_string/to_string.dart';
part 'bloc.dart';
part 'place_picker.g.dart';
part 'state_event.dart';
class PlacePicker extends StatelessWidget {
static const routeName = "/place-picker";
static Route buildRoute(RouteSettings settings) =>
MaterialPageRoute<CameraPosition>(
builder: (_) => const PlacePicker(),
settings: settings,
);
const PlacePicker({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _Bloc(),
child: const _WrappedPlacePicker(),
);
}
}
@npLog
class _WrappedPlacePicker extends StatelessWidget {
const _WrappedPlacePicker();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(L10n.global().placePickerTitle),
leading: IconButton(
onPressed: () {
final position = context.state.position;
_log.info("[build] Position picked: $position");
Navigator.of(context).pop(position);
},
icon: const Icon(Icons.check_outlined),
),
),
body: const _BodyView(),
);
}
}
class _BodyView extends StatelessWidget {
const _BodyView();
@override
Widget build(BuildContext context) {
final prevPosition =
context.read<PrefController>().mapBrowserPrevPositionValue;
return ValueStreamBuilderEx<GpsMapProvider>(
stream: context.read<PrefController>().gpsMapProvider,
builder: StreamWidgetBuilder.value(
(context, gpsMapProvider) => PlacePickerView(
providerHint: gpsMapProvider,
initialPosition: prevPosition ?? const MapCoord(0, 0),
initialZoom: prevPosition == null ? 2.5 : 10,
onCameraMove: (position) {
context.addEvent(_SetPosition(position));
},
),
),
);
}
}
// 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 _Emitter = Emitter<_State>;
extension on BuildContext {
_Bloc get bloc => read<_Bloc>();
_State get state => bloc.state;
void addEvent(_Event event) => bloc.add(event);
}

View file

@ -0,0 +1,73 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'place_picker.dart';
// **************************************************************************
// CopyWithLintRuleGenerator
// **************************************************************************
// ignore_for_file: library_private_types_in_public_api, duplicate_ignore
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class $_StateCopyWithWorker {
_State call({CameraPosition? position});
}
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
_$_StateCopyWithWorkerImpl(this.that);
@override
_State call({dynamic position = copyWithNull}) {
return _State(
position: position == copyWithNull
? that.position
: position as CameraPosition?);
}
final _State that;
}
extension $_StateCopyWith on _State {
$_StateCopyWithWorker get copyWith => _$copyWith;
$_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this);
}
// **************************************************************************
// NpLogGenerator
// **************************************************************************
extension _$_WrappedPlacePickerNpLog on _WrappedPlacePicker {
// ignore: unused_element
Logger get _log => log;
static final log =
Logger("widget.place_picker.place_picker._WrappedPlacePicker");
}
extension _$_BlocNpLog on _Bloc {
// ignore: unused_element
Logger get _log => log;
static final log = Logger("widget.place_picker.place_picker._Bloc");
}
// **************************************************************************
// ToStringGenerator
// **************************************************************************
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {position: $position}";
}
}
extension _$_SetPositionToString on _SetPosition {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_SetPosition {value: $value}";
}
}

View file

@ -0,0 +1,28 @@
part of 'place_picker.dart';
@genCopyWith
@toString
class _State {
const _State({
this.position,
});
factory _State.init() => const _State();
@override
String toString() => _$toString();
final CameraPosition? position;
}
abstract class _Event {}
@toString
class _SetPosition implements _Event {
const _SetPosition(this.value);
@override
String toString() => _$toString();
final CameraPosition value;
}

View file

@ -1,6 +1,5 @@
import 'dart:async';
import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
@ -19,6 +18,7 @@ import 'package:nc_photos/entity/exif_extension.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/gps_map_util.dart';
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/platform/features.dart' as features;
@ -355,11 +355,13 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
height: 256,
child: ValueStreamBuilder<GpsMapProvider>(
stream: context.read<PrefController>().gpsMapProvider,
builder: (context, gpsMapProvider) => GpsMap(
builder: (context, gpsMapProvider) => StaticMap(
providerHint: gpsMapProvider.requireData,
center: _gps!,
zoom: 16,
onTap: _onMapTap,
location: CameraPosition(center: _gps!, zoom: 16),
onTap: () => launchExternalMap(CameraPosition(
center: _gps!,
zoom: 16,
)),
),
),
),
@ -493,16 +495,6 @@ class _ViewerDetailPaneState extends State<ViewerDetailPane> {
SetAsHandler(c, context: context).setAsFile(widget.account, _file!);
}
void _onMapTap() {
if (getRawPlatform() == NpPlatform.android) {
final intent = AndroidIntent(
action: "action_view",
data: Uri.encodeFull("geo:${_gps!.latitude},${_gps!.longitude}?z=16"),
);
intent.launch();
}
}
void _onDateTimeTap(BuildContext context) {
assert(_file != null);
showDialog(

View file

@ -1,6 +1,7 @@
library np_gps_map;
export 'src/gps_map.dart';
export 'src/interactive_map.dart';
export 'src/map_coord.dart';
export 'src/place_picker.dart';
export 'src/static_map.dart';
export 'src/type.dart';
export 'src/util.dart' show initGpsMap;

View file

@ -1,8 +1,7 @@
import 'package:flutter/material.dart';
import 'package:np_gps_map/src/gps_map.dart';
import 'package:np_gps_map/src/interactive_map/google.dart';
import 'package:np_gps_map/src/interactive_map/osm.dart';
import 'package:np_gps_map/src/map_coord.dart';
import 'package:np_gps_map/src/type.dart';
import 'package:np_gps_map/src/util.dart';
import 'package:np_platform_util/np_platform_util.dart';
@ -31,6 +30,7 @@ class InteractiveMap extends StatelessWidget {
this.googleClusterBuilder,
this.contentPadding,
this.onMapCreated,
this.onCameraMove,
});
@override
@ -45,6 +45,7 @@ class InteractiveMap extends StatelessWidget {
clusterBuilder: osmClusterBuilder,
contentPadding: contentPadding,
onMapCreated: onMapCreated,
onCameraMove: onCameraMove,
);
} else {
return GoogleInteractiveMap(
@ -55,6 +56,7 @@ class InteractiveMap extends StatelessWidget {
clusterBuilder: googleClusterBuilder,
contentPadding: contentPadding,
onMapCreated: onMapCreated,
onCameraMove: onCameraMove,
);
}
}
@ -70,4 +72,5 @@ class InteractiveMap extends StatelessWidget {
final OsmClusterBuilder? osmClusterBuilder;
final EdgeInsets? contentPadding;
final void Function(InteractiveMapController controller)? onMapCreated;
final void Function(CameraPosition position)? onCameraMove;
}

View file

@ -5,7 +5,7 @@ import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:np_common/object_util.dart';
import 'package:np_gps_map/src/interactive_map.dart';
import 'package:np_gps_map/src/map_coord.dart';
import 'package:np_gps_map/src/type.dart' as type;
typedef GoogleClusterBuilder = FutureOr<BitmapDescriptor> Function(
BuildContext context, List<DataPoint> dataPoints);
@ -20,18 +20,20 @@ class GoogleInteractiveMap extends StatefulWidget {
this.onClusterTap,
this.contentPadding,
this.onMapCreated,
this.onCameraMove,
});
@override
State<StatefulWidget> createState() => _GoogleInteractiveMapState();
final MapCoord? initialPosition;
final type.MapCoord? initialPosition;
final double? initialZoom;
final List<DataPoint>? dataPoints;
final GoogleClusterBuilder? clusterBuilder;
final void Function(List<DataPoint> dataPoints)? onClusterTap;
final EdgeInsets? contentPadding;
final void Function(InteractiveMapController controller)? onMapCreated;
final void Function(type.CameraPosition position)? onCameraMove;
}
class _GoogleInteractiveMapState extends State<GoogleInteractiveMap> {
@ -57,7 +59,14 @@ class _GoogleInteractiveMapState extends State<GoogleInteractiveMap> {
const CameraPosition(target: LatLng(0, 0)),
markers: _markers,
onMapCreated: _onMapCreated,
onCameraMove: _clusterManager.onCameraMove,
onCameraMove: (position) {
_clusterManager.onCameraMove(position);
widget.onCameraMove?.call(type.CameraPosition(
center: position.target.toMapCoord(),
zoom: position.zoom,
rotation: position.bearing,
));
},
onCameraIdle: _clusterManager.updateMap,
padding: widget.contentPadding ?? EdgeInsets.zero,
);
@ -120,7 +129,7 @@ class _ParentController implements InteractiveMapController {
const _ParentController(this.controller);
@override
void setPosition(MapCoord position) {
void setPosition(type.MapCoord position) {
controller
.animateCamera(CameraUpdate.newLatLngZoom(position.toLatLng(), 10));
}
@ -128,7 +137,7 @@ class _ParentController implements InteractiveMapController {
final GoogleMapController controller;
}
extension on MapCoord {
extension on type.MapCoord {
LatLng toLatLng() => LatLng(latitude, longitude);
}

View file

@ -7,7 +7,7 @@ import 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
import 'package:latlong2/latlong.dart';
import 'package:np_common/object_util.dart';
import 'package:np_gps_map/src/interactive_map.dart';
import 'package:np_gps_map/src/map_coord.dart';
import 'package:np_gps_map/src/type.dart';
import 'package:rxdart/rxdart.dart';
typedef OsmClusterBuilder = Widget Function(
@ -23,6 +23,7 @@ class OsmInteractiveMap extends StatefulWidget {
this.onClusterTap,
this.contentPadding,
this.onMapCreated,
this.onCameraMove,
});
@override
@ -35,6 +36,7 @@ class OsmInteractiveMap extends StatefulWidget {
final void Function(List<DataPoint> dataPoints)? onClusterTap;
final EdgeInsets? contentPadding;
final void Function(InteractiveMapController controller)? onMapCreated;
final void Function(CameraPosition position)? onCameraMove;
}
class _OsmInteractiveMapState extends State<OsmInteractiveMap> {
@ -47,6 +49,11 @@ class _OsmInteractiveMapState extends State<OsmInteractiveMap> {
widget.onMapCreated?.call(_parentController!);
_subscriptions.add(_controller.mapEventStream.listen((ev) {
_mapRotationRadSubject.add(ev.camera.rotationRad);
widget.onCameraMove?.call(CameraPosition(
center: ev.camera.center.toMapCoord(),
zoom: ev.camera.zoom,
rotation: (360 - ev.camera.rotation) % 360,
));
}));
}
});

View file

@ -1,9 +0,0 @@
class MapCoord {
const MapCoord(this.latitude, this.longitude);
@override
String toString() => "MapCoord {latitude: $latitude, longitude: $longitude}";
final double latitude;
final double longitude;
}

View file

@ -1,18 +1,17 @@
import 'package:flutter/material.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
import 'package:np_gps_map/src/map_coord.dart';
import 'package:np_gps_map/src/type.dart' as type;
class GoogleGpsMap extends StatelessWidget {
const GoogleGpsMap({
super.key,
required this.center,
required this.zoom,
required this.location,
this.onTap,
});
@override
Widget build(BuildContext context) {
final centerLl = LatLng(center.latitude, center.longitude);
final center = LatLng(location.center.latitude, location.center.longitude);
return GoogleMap(
compassEnabled: false,
mapToolbarEnabled: false,
@ -25,13 +24,14 @@ class GoogleGpsMap extends StatelessWidget {
buildingsEnabled: false,
// liteModeEnabled: true,
initialCameraPosition: CameraPosition(
target: centerLl,
zoom: zoom,
target: center,
zoom: location.zoom,
bearing: location.rotation,
),
markers: {
Marker(
markerId: const MarkerId("at"),
position: centerLl,
position: center,
// for some reason, GoogleMap's onTap is not triggered if
// tapped on top of the marker
onTap: onTap,
@ -46,8 +46,7 @@ class GoogleGpsMap extends StatelessWidget {
);
}
final MapCoord center;
final double zoom;
final type.CameraPosition location;
final VoidCallback? onTap;
}

View file

@ -1,36 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import 'package:np_gps_map/src/map_coord.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:np_gps_map/src/type.dart';
class OsmGpsMap extends StatelessWidget {
const OsmGpsMap({
super.key,
required this.center,
required this.zoom,
required this.location,
this.onTap,
});
@override
Widget build(BuildContext context) {
const double pinSize = 48;
final centerLl = LatLng(center.latitude, center.longitude);
final center = LatLng(location.center.latitude, location.center.longitude);
return GestureDetector(
onTap: () {
launchUrlString(
"https://www.openstreetmap.org/?mlat=${center.latitude}&mlon=${center.longitude}#map=${zoom.toInt()}/${center.latitude}/${center.longitude}",
mode: LaunchMode.externalApplication,
);
},
onTap: onTap,
behavior: HitTestBehavior.opaque,
// IgnorePointer is needed to prevent FlutterMap absorbing all pointer
// events
child: IgnorePointer(
child: FlutterMap(
options: MapOptions(
initialCenter: centerLl,
initialZoom: zoom,
initialCenter: center,
initialZoom: location.zoom,
initialRotation: (360 - location.rotation) % 360,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.none,
),
@ -40,11 +34,12 @@ class OsmGpsMap extends StatelessWidget {
urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png",
),
MarkerLayer(
rotate: true,
markers: [
Marker(
width: pinSize,
height: pinSize,
point: centerLl,
point: center,
alignment: Alignment.topCenter,
child: const Image(
image: AssetImage(
@ -62,7 +57,6 @@ class OsmGpsMap extends StatelessWidget {
);
}
final MapCoord center;
final double zoom;
final CameraPosition location;
final void Function()? onTap;
}

View file

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:np_gps_map/src/interactive_map.dart';
import 'package:np_gps_map/src/type.dart';
class PlacePickerView extends StatelessWidget {
const PlacePickerView({
super.key,
required this.providerHint,
this.initialPosition,
this.initialZoom,
this.contentPadding,
this.onCameraMove,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
InteractiveMap(
providerHint: providerHint,
initialPosition: initialPosition,
initialZoom: initialZoom,
contentPadding: contentPadding,
onCameraMove: onCameraMove,
),
Positioned.fill(
child: Transform.translate(
// 48(height) / 2
offset: const Offset(0, -24),
child: Center(
child: Image.asset("packages/np_gps_map/assets/gps_map_pin.png"),
),
),
),
],
);
}
final GpsMapProvider providerHint;
final MapCoord? initialPosition;
final double? initialZoom;
final EdgeInsets? contentPadding;
final void Function(CameraPosition position)? onCameraMove;
}

View file

@ -1,23 +1,16 @@
import 'package:flutter/material.dart';
import 'package:np_gps_map/src/map_coord.dart';
import 'package:np_gps_map/src/native/google_gps_map.dart'
if (dart.library.html) 'package:np_gps_map/src/web/google_gps_map.dart';
import 'package:np_gps_map/src/osm_gps_map.dart';
import 'package:np_gps_map/src/type.dart';
import 'package:np_gps_map/src/util.dart';
import 'package:np_platform_util/np_platform_util.dart';
enum GpsMapProvider {
google,
osm,
;
}
class GpsMap extends StatelessWidget {
const GpsMap({
class StaticMap extends StatelessWidget {
const StaticMap({
super.key,
required this.providerHint,
required this.center,
required this.zoom,
required this.location,
this.onTap,
});
@ -26,14 +19,12 @@ class GpsMap extends StatelessWidget {
if (providerHint == GpsMapProvider.osm ||
(getRawPlatform() == NpPlatform.android && !isNewGMapsRenderer())) {
return OsmGpsMap(
center: center,
zoom: zoom,
location: location,
onTap: onTap,
);
} else {
return GoogleGpsMap(
center: center,
zoom: zoom,
location: location,
onTap: onTap,
);
}
@ -43,8 +34,6 @@ class GpsMap extends StatelessWidget {
/// actual choice may be different depending on the runtime environment
final GpsMapProvider providerHint;
/// A pair of latitude and longitude coordinates, stored as degrees
final MapCoord center;
final double zoom;
final CameraPosition location;
final void Function()? onTap;
}

View file

@ -0,0 +1,80 @@
import 'package:equatable/equatable.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart' as gmap;
import 'package:latlong2/latlong.dart';
import 'package:np_common/type.dart';
enum GpsMapProvider {
google,
osm,
;
}
/// A pair of latitude and longitude coordinates, stored as degrees
class MapCoord {
const MapCoord(this.latitude, this.longitude);
MapCoord.fromJson(JsonObj json)
: latitude = json["lat"],
longitude = json["lng"];
@override
String toString() => "MapCoord {latitude: $latitude, longitude: $longitude}";
JsonObj toJson() => {
"lat": latitude,
"lng": longitude,
};
final double latitude;
final double longitude;
}
extension GLatLngExtension on gmap.LatLng {
MapCoord toMapCoord() => MapCoord(latitude, longitude);
}
extension LatLngExtension on LatLng {
MapCoord toMapCoord() => MapCoord(latitude, longitude);
}
class CameraPosition with EquatableMixin {
const CameraPosition({
required this.center,
required this.zoom,
this.rotation = 0,
});
factory CameraPosition.fromJson(JsonObj json) {
return CameraPosition(
center: MapCoord.fromJson(json["center"]),
zoom: json["zoom"],
rotation: json["rotation"],
);
}
@override
String toString() => "CameraPosition {"
"center: $center, "
"zoom: $zoom, "
"rotation: $rotation, "
"}";
JsonObj toJson() {
return {
"center": center.toJson(),
"zoom": zoom,
"rotation": rotation,
};
}
@override
List<Object?> get props => [center, zoom, rotation];
final MapCoord center;
final double zoom;
// The camera's bearing in degrees, measured clockwise from north.
//
// A bearing of 0.0, the default, means the camera points north.
// A bearing of 90.0 means the camera points east.
final double rotation;
}

View file

@ -9,6 +9,7 @@ environment:
flutter: ">=3.19.0"
dependencies:
equatable: ^2.0.5
flutter:
sdk: flutter
flutter_map: ^6.1.0
@ -25,7 +26,6 @@ dependencies:
np_platform_util:
path: ../np_platform_util
rxdart: ^0.27.7
url_launcher: ^6.1.11
dependency_overrides:
google_maps_flutter_android: 2.7.0