Improve initial location when adding map to album

This commit is contained in:
Ming Ming 2024-11-30 02:19:53 +08:00
parent 281efe0d0d
commit b85afaac07
14 changed files with 311 additions and 40 deletions

View file

@ -22,6 +22,7 @@ import 'package:nc_photos/controller/collection_items_controller.dart';
import 'package:nc_photos/controller/collections_controller.dart';
import 'package:nc_photos/controller/files_controller.dart';
import 'package:nc_photos/controller/pref_controller.dart';
import 'package:nc_photos/db/entity_converter.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/download_handler.dart';
import 'package:nc_photos/entity/collection.dart';
@ -44,6 +45,7 @@ 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_bar_circular_progress_indicator.dart';
import 'package:nc_photos/widget/app_intermediate_circular_progress_indicator.dart';
import 'package:nc_photos/widget/collection_picker.dart';
import 'package:nc_photos/widget/draggable_item_list.dart';
@ -63,8 +65,11 @@ import 'package:nc_photos/widget/simple_input_dialog.dart';
import 'package:nc_photos/widget/sliver_visualized_scale.dart';
import 'package:nc_photos/widget/viewer.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/object_util.dart';
import 'package:np_common/or_null.dart';
import 'package:np_common/unique.dart';
import 'package:np_datetime/np_datetime.dart';
import 'package:np_db/np_db.dart';
import 'package:np_gps_map/np_gps_map.dart';
import 'package:np_ui/np_ui.dart';
import 'package:to_string/to_string.dart';
@ -117,6 +122,7 @@ class CollectionBrowser extends StatelessWidget {
prefController: context.read(),
collectionsController: accountController.collectionsController,
filesController: accountController.filesController,
db: context.read(),
collection: collection,
),
child: const _WrappedCollectionBrowser(),
@ -220,6 +226,29 @@ class _WrappedCollectionBrowserState extends State<_WrappedCollectionBrowser>
}
},
),
_BlocListenerT(
selector: (state) => state.placePickerRequest,
listener: (context, placePickerRequest) async {
if (placePickerRequest.value != null) {
final result =
await Navigator.of(context).pushNamed<CameraPosition>(
PlacePicker.routeName,
arguments: PlacePickerArguments(
initialPosition:
placePickerRequest.value!.initialPosition,
initialZoom:
placePickerRequest.value!.initialPosition == null
? null
: 15.5,
),
);
if (result == null) {
return;
}
context.read<_Bloc>().add(_AddMapToCollection(result));
}
},
),
_BlocListenerT<ExceptionEvent?>(
selector: (state) => state.error,
listener: (context, error) {
@ -418,6 +447,7 @@ 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>();

View file

@ -31,6 +31,8 @@ abstract class $_StateCopyWithWorker {
List<CollectionItem>? editItems,
List<_Item>? editTransformedItems,
CollectionItemSort? editSort,
bool? isAddMapBusy,
Unique<_PlacePickerRequest?>? placePickerRequest,
bool? isDragging,
int? zoom,
double? scale,
@ -61,6 +63,8 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
dynamic editItems = copyWithNull,
dynamic editTransformedItems = copyWithNull,
dynamic editSort = copyWithNull,
dynamic isAddMapBusy,
dynamic placePickerRequest,
dynamic isDragging,
dynamic zoom,
dynamic scale = copyWithNull,
@ -99,6 +103,10 @@ class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
editSort: editSort == copyWithNull
? that.editSort
: editSort as CollectionItemSort?,
isAddMapBusy: isAddMapBusy as bool? ?? that.isAddMapBusy,
placePickerRequest:
placePickerRequest as Unique<_PlacePickerRequest?>? ??
that.placePickerRequest,
isDragging: isDragging as bool? ?? that.isDragging,
zoom: zoom as int? ?? that.zoom,
scale: scale == copyWithNull ? that.scale : scale as double?,
@ -151,7 +159,7 @@ extension _$_BlocNpLog on _Bloc {
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {collection: $collection, coverUrl: $coverUrl, items: [length: ${items.length}], rawItems: [length: ${rawItems.length}], itemsWhitelist: ${itemsWhitelist == null ? null : "{length: ${itemsWhitelist!.length}}"}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, isSelectionRemovable: $isSelectionRemovable, isSelectionManageableFile: $isSelectionManageableFile, isSelectionDeletable: $isSelectionDeletable, isEditMode: $isEditMode, isEditBusy: $isEditBusy, editName: $editName, editItems: ${editItems == null ? null : "[length: ${editItems!.length}]"}, editTransformedItems: ${editTransformedItems == null ? null : "[length: ${editTransformedItems!.length}]"}, editSort: ${editSort == null ? null : "${editSort!.name}"}, isDragging: $isDragging, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, importResult: $importResult, error: $error, message: $message}";
return "_State {collection: $collection, coverUrl: $coverUrl, items: [length: ${items.length}], rawItems: [length: ${rawItems.length}], itemsWhitelist: ${itemsWhitelist == null ? null : "{length: ${itemsWhitelist!.length}}"}, isLoading: $isLoading, transformedItems: [length: ${transformedItems.length}], selectedItems: {length: ${selectedItems.length}}, isSelectionRemovable: $isSelectionRemovable, isSelectionManageableFile: $isSelectionManageableFile, isSelectionDeletable: $isSelectionDeletable, isEditMode: $isEditMode, isEditBusy: $isEditBusy, editName: $editName, editItems: ${editItems == null ? null : "[length: ${editItems!.length}]"}, editTransformedItems: ${editTransformedItems == null ? null : "[length: ${editTransformedItems!.length}]"}, editSort: ${editSort == null ? null : "${editSort!.name}"}, isAddMapBusy: $isAddMapBusy, placePickerRequest: $placePickerRequest, isDragging: $isDragging, zoom: $zoom, scale: ${scale == null ? null : "${scale!.toStringAsFixed(3)}"}, importResult: $importResult, error: $error, message: $message}";
}
}
@ -219,6 +227,13 @@ extension _$_AddLabelToCollectionToString on _AddLabelToCollection {
}
}
extension _$_RequestAddMapToString on _RequestAddMap {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_RequestAddMap {}";
}
}
extension _$_AddMapToCollectionToString on _AddMapToCollection {
String _$toString() {
// ignore: unnecessary_string_interpolations

View file

@ -365,10 +365,20 @@ class _EditAppBar extends StatelessWidget {
onPressed: () => _onAddTextPressed(context),
),
if (capabilitiesAdapter.isPermitted(CollectionCapability.mapItem))
IconButton(
_BlocSelector(
selector: (state) => state.isAddMapBusy,
builder: (context, isAddMapBusy) => isAddMapBusy
? const IconButton(
icon: AppBarProgressIndicator(),
onPressed: null,
)
: IconButton(
icon: const Icon(Icons.map_outlined),
tooltip: L10n.global().albumAddMapTooltip,
onPressed: () => _onAddMapPressed(context),
onPressed: () {
context.addEvent(const _RequestAddMap());
},
),
),
if (capabilitiesAdapter.isPermitted(CollectionCapability.sort))
IconButton(
@ -393,15 +403,6 @@ 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

@ -9,6 +9,7 @@ class _Bloc extends Bloc<_Event, _State>
required this.prefController,
required this.collectionsController,
required this.filesController,
required this.db,
required Collection collection,
}) : _c = container,
_isAdHocCollection = !collectionsController.stream.value.data
@ -30,6 +31,7 @@ class _Bloc extends Bloc<_Event, _State>
on<_BeginEdit>(_onBeginEdit);
on<_EditName>(_onEditName);
on<_AddLabelToCollection>(_onAddLabelToCollection);
on<_RequestAddMap>(_onRequestAddMap);
on<_AddMapToCollection>(_onAddMapToCollection);
on<_EditSort>(_onEditSort);
on<_EditManualSort>(_onEditManualSort);
@ -212,6 +214,34 @@ class _Bloc extends Bloc<_Event, _State>
));
}
Future<void> _onRequestAddMap(_RequestAddMap ev, _Emitter emit) async {
_log.info(ev);
emit(state.copyWith(isAddMapBusy: true));
try {
final location = await db.getFirstLocationOfFileIds(
account: account.toDb(),
fileIds: state.transformedItems
.whereType<_FileItem>()
.map((e) => e.file.fdId)
.toList(),
);
final mapCoord =
location?.let((e) => MapCoord(e.latitude!, e.longitude!));
emit(state.copyWith(
placePickerRequest:
Unique(_PlacePickerRequest(initialPosition: mapCoord)),
));
} catch (e, stackTrace) {
_log.severe("[_onRequestAddMap] Failed while getFirstLocationOfFileIds",
e, stackTrace);
emit(state.copyWith(
placePickerRequest: Unique(const _PlacePickerRequest()),
));
} finally {
emit(state.copyWith(isAddMapBusy: false));
}
}
void _onAddMapToCollection(_AddMapToCollection ev, Emitter<_State> emit) {
_log.info(ev);
assert(isCollectionCapabilityPermitted(CollectionCapability.mapItem));
@ -597,6 +627,7 @@ class _Bloc extends Bloc<_Event, _State>
final PrefController prefController;
final CollectionsController collectionsController;
final FilesController filesController;
final NpDb db;
late final CollectionItemsController itemsController;
/// Specify if the supplied [collection] is an "inline" one, which means it's

View file

@ -21,6 +21,8 @@ class _State {
this.editItems,
this.editTransformedItems,
this.editSort,
required this.isAddMapBusy,
required this.placePickerRequest,
required this.isDragging,
required this.zoom,
this.scale,
@ -47,6 +49,8 @@ class _State {
isSelectionDeletable: true,
isEditMode: false,
isEditBusy: false,
isAddMapBusy: false,
placePickerRequest: Unique(null),
isDragging: false,
zoom: zoom,
);
@ -76,6 +80,8 @@ class _State {
final List<CollectionItem>? editItems;
final List<_Item>? editTransformedItems;
final CollectionItemSort? editSort;
final bool isAddMapBusy;
final Unique<_PlacePickerRequest?> placePickerRequest;
final bool isDragging;
@ -174,6 +180,14 @@ class _AddLabelToCollection implements _Event {
final String label;
}
@toString
class _RequestAddMap implements _Event {
const _RequestAddMap();
@override
String toString() => _$toString();
}
@toString
class _AddMapToCollection implements _Event {
const _AddMapToCollection(this.location);

View file

@ -221,6 +221,14 @@ class _DateItem extends _Item {
final Date date;
}
class _PlacePickerRequest {
const _PlacePickerRequest({
this.initialPosition,
});
final MapCoord? initialPosition;
}
@toString
class _ArchiveFailedError implements Exception {
const _ArchiveFailedError(this.count);

View file

@ -214,7 +214,6 @@ 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) {
@ -242,6 +241,7 @@ class _WrappedAppState extends State<_WrappedApp>
route ??= _handleImageEnhancerRoute(settings);
route ??= _handleCollectionBrowserRoute(settings);
route ??= _handleAccountSettingsRoute(settings);
route ??= _handlePlacePickerRoute(settings);
return route;
}
@ -551,6 +551,18 @@ class _WrappedAppState extends State<_WrappedApp>
return null;
}
Route<dynamic>? _handlePlacePickerRoute(RouteSettings settings) {
try {
if (settings.name == PlacePicker.routeName) {
final args = settings.arguments as PlacePickerArguments?;
return PlacePicker.buildRoute(args, settings);
}
} catch (e) {
_log.severe("[_handlePlacePickerRoute] Failed while handling route", e);
}
return null;
}
late final _bloc = context.read<_Bloc>();
final _scaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
final _navigatorKey = GlobalKey<NavigatorState>();

View file

@ -2,8 +2,13 @@ part of 'place_picker.dart';
@npLog
class _Bloc extends Bloc<_Event, _State> with BlocLogger {
_Bloc() : super(_State.init()) {
_Bloc({
required this.prefController,
required this.initialPosition,
required this.initialZoom,
}) : super(_State.init()) {
on<_SetPosition>(_onSetPosition);
on<_Done>(_onDone);
}
@override
@ -18,4 +23,17 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger {
// _log.info(ev);
emit(state.copyWith(position: ev.value));
}
void _onDone(_Done ev, _Emitter emit) {
_log.info(ev);
if (prefController.mapBrowserPrevPositionValue == null &&
state.position != null) {
prefController.setMapBrowserPrevPosition(state.position!.center);
}
emit(state.copyWith(isDone: true));
}
final PrefController prefController;
final MapCoord? initialPosition;
final double? initialZoom;
}

View file

@ -14,24 +14,54 @@ part 'bloc.dart';
part 'place_picker.g.dart';
part 'state_event.dart';
class PlacePickerArguments {
const PlacePickerArguments({
this.initialPosition,
this.initialZoom,
});
final MapCoord? initialPosition;
final double? initialZoom;
}
class PlacePicker extends StatelessWidget {
static const routeName = "/place-picker";
static Route buildRoute(RouteSettings settings) =>
static Route buildRoute(PlacePickerArguments? args, RouteSettings settings) =>
MaterialPageRoute<CameraPosition>(
builder: (_) => const PlacePicker(),
builder: (_) => PlacePicker.fromArgs(args),
settings: settings,
);
const PlacePicker({super.key});
const PlacePicker({
super.key,
required this.initialPosition,
required this.initialZoom,
});
PlacePicker.fromArgs(
PlacePickerArguments? args, {
Key? key,
}) : this(
key: key,
initialPosition: args?.initialPosition,
initialZoom: args?.initialZoom,
);
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => _Bloc(),
create: (context) => _Bloc(
prefController: context.read(),
initialPosition: initialPosition,
initialZoom: initialZoom,
),
child: const _WrappedPlacePicker(),
);
}
final MapCoord? initialPosition;
final double? initialZoom;
}
@npLog
@ -40,19 +70,31 @@ class _WrappedPlacePicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
return MultiBlocListener(
listeners: [
_BlocListenerT(
selector: (state) => state.isDone,
listener: (context, isDone) {
if (isDone) {
final position = context.state.position;
_log.info("[build] Position picked: $position");
Navigator.of(context).pop(position);
}
},
),
],
child: 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);
context.addEvent(const _Done());
},
icon: const Icon(Icons.check_outlined),
),
),
body: const _BodyView(),
),
);
}
}
@ -62,15 +104,16 @@ class _BodyView extends StatelessWidget {
@override
Widget build(BuildContext context) {
final prevPosition =
context.read<PrefController>().mapBrowserPrevPositionValue;
final position = context.bloc.initialPosition ??
context.bloc.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,
initialPosition: position ?? const MapCoord(0, 0),
initialZoom:
context.bloc.initialZoom ?? (position == null ? 2.5 : 10),
onCameraMove: (position) {
context.addEvent(_SetPosition(position));
},
@ -82,7 +125,7 @@ class _BodyView extends StatelessWidget {
// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>;
// typedef _BlocListener = BlocListener<_Bloc, _State>;
// typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
typedef _BlocListenerT<T> = BlocListenerT<_Bloc, _State, T>;
// typedef _BlocSelector<T> = BlocSelector<_Bloc, _State, T>;
typedef _Emitter = Emitter<_State>;

View file

@ -13,18 +13,19 @@ part of 'place_picker.dart';
// **************************************************************************
abstract class $_StateCopyWithWorker {
_State call({CameraPosition? position});
_State call({CameraPosition? position, bool? isDone});
}
class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker {
_$_StateCopyWithWorkerImpl(this.that);
@override
_State call({dynamic position = copyWithNull}) {
_State call({dynamic position = copyWithNull, dynamic isDone}) {
return _State(
position: position == copyWithNull
? that.position
: position as CameraPosition?);
: position as CameraPosition?,
isDone: isDone as bool? ?? that.isDone);
}
final _State that;
@ -61,7 +62,7 @@ extension _$_BlocNpLog on _Bloc {
extension _$_StateToString on _State {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_State {position: $position}";
return "_State {position: $position, isDone: $isDone}";
}
}
@ -71,3 +72,10 @@ extension _$_SetPositionToString on _SetPosition {
return "_SetPosition {value: $value}";
}
}
extension _$_DoneToString on _Done {
String _$toString() {
// ignore: unnecessary_string_interpolations
return "_Done {}";
}
}

View file

@ -5,14 +5,18 @@ part of 'place_picker.dart';
class _State {
const _State({
this.position,
required this.isDone,
});
factory _State.init() => const _State();
factory _State.init() => const _State(
isDone: false,
);
@override
String toString() => _$toString();
final CameraPosition? position;
final bool isDone;
}
abstract class _Event {}
@ -26,3 +30,11 @@ class _SetPosition implements _Event {
final CameraPosition value;
}
@toString
class _Done implements _Event {
const _Done();
@override
String toString() => _$toString();
}

View file

@ -454,6 +454,13 @@ abstract class NpDb {
List<String>? excludeRelativeRoots,
});
/// Return the location data of the first file (sorted by date time in
/// descending order) in a group of files
Future<DbLocation?> getFirstLocationOfFileIds({
required DbAccount account,
required List<int> fileIds,
});
/// Return the latitude, longitude and the file id of all files
Future<List<DbImageLatLng>> getImageLatLngWithFileIds({
required DbAccount account,

View file

@ -200,6 +200,64 @@ extension SqliteDbImageLocationExtension on SqliteDb {
}).get();
}
Future<ImageLocation?> queryFirstImageLocationByFileIds({
required ByAccount account,
required List<int> fileIds,
}) async {
final candidates = await fileIds.withPartition((sublist) async {
final query = selectOnly(files).join([
innerJoin(accountFiles, accountFiles.file.equalsExp(files.rowId),
useColumns: false),
if (account.dbAccount != null) ...[
innerJoin(accounts, accounts.rowId.equalsExp(accountFiles.account),
useColumns: false),
innerJoin(servers, servers.rowId.equalsExp(accounts.server),
useColumns: false),
],
innerJoin(imageLocations,
imageLocations.accountFile.equalsExp(accountFiles.rowId),
useColumns: false),
]);
query.addColumns([
accountFiles.rowId,
accountFiles.bestDateTime,
]);
if (account.sqlAccount != null) {
query.where(accountFiles.account.equals(account.sqlAccount!.rowId));
} else if (account.dbAccount != null) {
query
..where(servers.address.equals(account.dbAccount!.serverAddress))
..where(accounts.userId
.equals(account.dbAccount!.userId.toCaseInsensitiveString()));
}
query
..where(files.fileId.isIn(sublist))
..where(imageLocations.latitude.isNotNull() &
imageLocations.longitude.isNotNull())
..orderBy([OrderingTerm.desc(accountFiles.bestDateTime)])
..limit(1);
return [
await query
.map((r) => (
rowId: r.read(accountFiles.rowId)!,
dateTime: r.read(accountFiles.bestDateTime)!,
))
.getSingleOrNull()
];
}, _maxByFileIdsSize);
final winner =
candidates.nonNulls.sortedBy((e) => e.dateTime).reversed.firstOrNull;
if (winner == null) {
return null;
}
final reusltQuery = select(imageLocations)
..where((t) => t.accountFile.equals(winner.rowId));
return reusltQuery.getSingleOrNull();
}
Future<List<ImageLocationGroup>> _groupImageLocationsBy({
required ByAccount account,
required GeneratedColumn<String> by,

View file

@ -607,6 +607,20 @@ class NpDbSqlite implements NpDb {
);
}
@override
Future<DbLocation?> getFirstLocationOfFileIds({
required DbAccount account,
required List<int> fileIds,
}) async {
final sqlObj = await _db.use((db) async {
return await db.queryFirstImageLocationByFileIds(
account: ByAccount.db(account),
fileIds: fileIds,
);
});
return sqlObj?.let(ImageLocationConverter.fromSql);
}
@override
Future<List<DbImageLatLng>> getImageLatLngWithFileIds({
required DbAccount account,