mirror of
https://github.com/zebrajr/immich.git
synced 2025-12-06 12:20:54 +01:00
feat(mobile): edit location action (#19645)
* change dto from integer to double * feat(mobile): edit location action * patch openapi * refactor in provider * fix lint * chore: not showing success prompt if dimissed * i18n --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
639ede78c2
commit
83afd49f5c
|
|
@ -799,6 +799,7 @@
|
|||
"edit_key": "Edit key",
|
||||
"edit_link": "Edit link",
|
||||
"edit_location": "Edit location",
|
||||
"edit_location_action_prompt": "{count} location edited",
|
||||
"edit_location_dialog_title": "Location",
|
||||
"edit_name": "Edit name",
|
||||
"edit_people": "Edit people",
|
||||
|
|
|
|||
BIN
mobile/drift_schemas/main/drift_schema_v1.json
generated
BIN
mobile/drift_schemas/main/drift_schema_v1.json
generated
Binary file not shown.
|
|
@ -116,15 +116,15 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin {
|
|||
|
||||
TextColumn get exposureTime => text().nullable()();
|
||||
|
||||
IntColumn get fNumber => integer().nullable()();
|
||||
RealColumn get fNumber => real().nullable()();
|
||||
|
||||
IntColumn get fileSize => integer().nullable()();
|
||||
|
||||
IntColumn get focalLength => integer().nullable()();
|
||||
RealColumn get focalLength => real().nullable()();
|
||||
|
||||
IntColumn get latitude => integer().nullable()();
|
||||
RealColumn get latitude => real().nullable()();
|
||||
|
||||
IntColumn get longitude => integer().nullable()();
|
||||
RealColumn get longitude => real().nullable()();
|
||||
|
||||
IntColumn get iso => integer().nullable()();
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,6 +1,8 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
||||
as entity;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
|
|
@ -41,3 +43,36 @@ class IsarExifRepository extends IsarDatabaseRepository {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
class DriftRemoteExifRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftRemoteExifRepository(this._db) : super(_db);
|
||||
|
||||
Future<ExifInfo?> get(String assetId) {
|
||||
final query = _db.remoteExifEntity.select()
|
||||
..where((exif) => exif.assetId.equals(assetId));
|
||||
|
||||
return query.map((asset) => asset.toDto()).getSingleOrNull();
|
||||
}
|
||||
}
|
||||
|
||||
extension on RemoteExifEntityData {
|
||||
ExifInfo toDto() {
|
||||
return ExifInfo(
|
||||
fileSize: fileSize,
|
||||
description: description,
|
||||
orientation: orientation,
|
||||
timeZone: timeZone,
|
||||
dateTimeOriginal: dateTimeOriginal,
|
||||
latitude: latitude,
|
||||
longitude: longitude,
|
||||
city: city,
|
||||
state: state,
|
||||
country: country,
|
||||
make: make,
|
||||
model: model,
|
||||
f: fNumber,
|
||||
iso: iso,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,13 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
|
||||
final remoteAssetRepositoryProvider = Provider<RemoteAssetRepository>(
|
||||
(ref) => RemoteAssetRepository(ref.watch(driftProvider)),
|
||||
);
|
||||
|
||||
class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
class DriftRemoteAssetRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const RemoteAssetRepository(this._db) : super(_db);
|
||||
const DriftRemoteAssetRepository(this._db) : super(_db);
|
||||
|
||||
Future<void> updateFavorite(List<String> ids, bool isFavorite) {
|
||||
return _db.batch((batch) async {
|
||||
|
|
@ -36,4 +32,19 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateLocation(List<String> ids, LatLng location) {
|
||||
return _db.batch((batch) async {
|
||||
for (final id in ids) {
|
||||
batch.update(
|
||||
_db.remoteExifEntity,
|
||||
RemoteExifEntityCompanion(
|
||||
latitude: Value(location.latitude),
|
||||
longitude: Value(location.longitude),
|
||||
),
|
||||
where: (e) => e.assetId.equals(id),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,16 +1,54 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
class EditLocationActionButton extends ConsumerWidget {
|
||||
const EditLocationActionButton({super.key});
|
||||
final ActionSource source;
|
||||
|
||||
const EditLocationActionButton({super.key, required this.source});
|
||||
|
||||
_onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result =
|
||||
await ref.read(actionProvider.notifier).editLocation(source, context);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
final successMessage = 'edit_location_action_prompt'.t(
|
||||
context: context,
|
||||
args: {'count': result.count.toString()},
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
ImmichToast.show(
|
||||
context: context,
|
||||
msg: result.success
|
||||
? successMessage
|
||||
: 'scaffold_body_error_occurred'.t(context: context),
|
||||
gravity: ToastGravity.BOTTOM,
|
||||
toastType: result.success ? ToastType.success : ToastType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return BaseActionButton(
|
||||
iconData: Icons.edit_location_alt_outlined,
|
||||
label: "control_bottom_app_bar_edit_location".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ class HomeBottomAppBar extends ConsumerWidget {
|
|||
? const TrashActionButton()
|
||||
: const DeletePermanentActionButton(),
|
||||
const EditDateTimeActionButton(),
|
||||
const EditLocationActionButton(),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(
|
||||
source: ActionSource.timeline,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -172,4 +172,26 @@ class ActionNotifier extends Notifier<void> {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult?> editLocation(
|
||||
ActionSource source,
|
||||
BuildContext context,
|
||||
) async {
|
||||
final ids = _getIdsForSource<RemoteAsset>(source);
|
||||
try {
|
||||
final isEdited = await _service.editLocation(ids, context);
|
||||
if (!isEdited) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ActionResult(count: ids.length, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to edit location for assets', error, stack);
|
||||
return ActionResult(
|
||||
count: ids.length,
|
||||
success: false,
|
||||
error: error.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||
|
||||
final localAssetRepository = Provider<DriftLocalAssetRepository>(
|
||||
(ref) => DriftLocalAssetRepository(ref.watch(driftProvider)),
|
||||
);
|
||||
|
||||
final remoteAssetRepository = Provider<DriftRemoteAssetRepository>(
|
||||
(ref) => DriftRemoteAssetRepository(ref.watch(driftProvider)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,3 +8,7 @@ part 'exif.provider.g.dart';
|
|||
@Riverpod(keepAlive: true)
|
||||
IsarExifRepository exifRepository(Ref ref) =>
|
||||
IsarExifRepository(ref.watch(isarProvider));
|
||||
|
||||
final remoteExifRepository = Provider<DriftRemoteExifRepository>(
|
||||
(ref) => DriftRemoteExifRepository(ref.watch(driftProvider)),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/repositories/api.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final assetApiRepositoryProvider = Provider(
|
||||
|
|
@ -65,6 +66,19 @@ class AssetApiRepository extends ApiRepository {
|
|||
);
|
||||
}
|
||||
|
||||
Future<void> updateLocation(
|
||||
List<String> ids,
|
||||
LatLng location,
|
||||
) async {
|
||||
return _api.updateAssets(
|
||||
AssetBulkUpdateDto(
|
||||
ids: ids,
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
|
||||
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
|
||||
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
|
||||
|
|
|
|||
|
|
@ -2,23 +2,34 @@ import 'package:auto_route/auto_route.dart';
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
final actionServiceProvider = Provider<ActionService>(
|
||||
(ref) => ActionService(
|
||||
ref.watch(assetApiRepositoryProvider),
|
||||
ref.watch(remoteAssetRepositoryProvider),
|
||||
ref.watch(remoteAssetRepository),
|
||||
ref.watch(remoteExifRepository),
|
||||
),
|
||||
);
|
||||
|
||||
class ActionService {
|
||||
final AssetApiRepository _assetApiRepository;
|
||||
final RemoteAssetRepository _remoteAssetRepository;
|
||||
final DriftRemoteAssetRepository _remoteAssetRepository;
|
||||
final DriftRemoteExifRepository _remoteExifRepository;
|
||||
|
||||
const ActionService(this._assetApiRepository, this._remoteAssetRepository);
|
||||
const ActionService(
|
||||
this._assetApiRepository,
|
||||
this._remoteAssetRepository,
|
||||
this._remoteExifRepository,
|
||||
);
|
||||
|
||||
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
||||
context.pushRoute(
|
||||
|
|
@ -81,4 +92,38 @@ class ActionService {
|
|||
AssetVisibility.timeline,
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> editLocation(
|
||||
List<String> remoteIds,
|
||||
BuildContext context,
|
||||
) async {
|
||||
LatLng? initialLatLng;
|
||||
if (remoteIds.length == 1) {
|
||||
final exif = await _remoteExifRepository.get(remoteIds[0]);
|
||||
|
||||
if (exif?.latitude != null && exif?.longitude != null) {
|
||||
initialLatLng = LatLng(exif!.latitude!, exif.longitude!);
|
||||
}
|
||||
}
|
||||
|
||||
final location = await showLocationPicker(
|
||||
context: context,
|
||||
initialLatLng: initialLatLng,
|
||||
);
|
||||
|
||||
if (location == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await _assetApiRepository.updateLocation(
|
||||
remoteIds,
|
||||
location,
|
||||
);
|
||||
await _remoteAssetRepository.updateLocation(
|
||||
remoteIds,
|
||||
location,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
mobile/openapi/lib/model/sync_asset_exif_v1.dart
generated
BIN
mobile/openapi/lib/model/sync_asset_exif_v1.dart
generated
Binary file not shown.
|
|
@ -13615,36 +13615,41 @@
|
|||
"type": "string"
|
||||
},
|
||||
"fNumber": {
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"fileSizeInByte": {
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"focalLength": {
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"fps": {
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"iso": {
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"latitude": {
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"lensModel": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"longitude": {
|
||||
"format": "double",
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
"type": "number"
|
||||
},
|
||||
"make": {
|
||||
"nullable": true,
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ class {{{classname}}} {
|
|||
: {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
|
||||
{{/isNumber}}
|
||||
{{#isDouble}}
|
||||
{{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(),
|
||||
{{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}){{#isNullable}}?{{/isNullable}}.toDouble(),
|
||||
{{/isDouble}}
|
||||
{{^isDouble}}
|
||||
{{^isNumber}}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
--- native_class.mustache 2024-09-19 11:41:07.855683995 -0400
|
||||
+++ native_class_temp.mustache 2024-09-19 11:41:57.113249395 -0400
|
||||
--- native_class.mustache 2025-07-01 08:29:23.968133163 +0800
|
||||
+++ native_class_temp.mustache 2025-07-01 08:29:44.225850583 +0800
|
||||
@@ -91,14 +91,14 @@
|
||||
{{/isDateTime}}
|
||||
{{#isNullable}}
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
: {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'),
|
||||
{{/isNumber}}
|
||||
+ {{#isDouble}}
|
||||
+ {{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(),
|
||||
+ {{{name}}}: (mapValueOfType<num>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}){{#isNullable}}?{{/isNullable}}.toDouble(),
|
||||
+ {{/isDouble}}
|
||||
+ {{^isDouble}}
|
||||
{{^isNumber}}
|
||||
|
|
|
|||
|
|
@ -115,9 +115,9 @@ export class SyncAssetExifV1 {
|
|||
dateTimeOriginal!: Date | null;
|
||||
modifyDate!: Date | null;
|
||||
timeZone!: string | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
@ApiProperty({ type: 'number', format: 'double' })
|
||||
latitude!: number | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
@ApiProperty({ type: 'number', format: 'double' })
|
||||
longitude!: number | null;
|
||||
projectionType!: string | null;
|
||||
city!: string | null;
|
||||
|
|
@ -126,9 +126,9 @@ export class SyncAssetExifV1 {
|
|||
make!: string | null;
|
||||
model!: string | null;
|
||||
lensModel!: string | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
@ApiProperty({ type: 'number', format: 'double' })
|
||||
fNumber!: number | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
@ApiProperty({ type: 'number', format: 'double' })
|
||||
focalLength!: number | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
iso!: number | null;
|
||||
|
|
@ -136,7 +136,7 @@ export class SyncAssetExifV1 {
|
|||
profileDescription!: string | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
rating!: number | null;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
@ApiProperty({ type: 'number', format: 'double' })
|
||||
fps!: number | null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user