mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat(mobile): edit date time action
This commit is contained in:
parent
c7853fbe9d
commit
d0e0c24690
@ -817,6 +817,7 @@
|
||||
"edit_avatar": "Edit avatar",
|
||||
"edit_date": "Edit date",
|
||||
"edit_date_and_time": "Edit date and time",
|
||||
"edit_date_time_action_prompt": "{count} date and time edited",
|
||||
"edit_description": "Edit description",
|
||||
"edit_description_prompt": "Please select a new description:",
|
||||
"edit_exclusion_pattern": "Edit exclusion pattern",
|
||||
|
@ -13,6 +13,7 @@ class RemoteAsset extends BaseAsset {
|
||||
final String? localId;
|
||||
final String? thumbHash;
|
||||
final AssetVisibility visibility;
|
||||
final DateTime? localDateTime;
|
||||
final String ownerId;
|
||||
final String? stackId;
|
||||
final int stackCount;
|
||||
@ -32,6 +33,7 @@ class RemoteAsset extends BaseAsset {
|
||||
super.isFavorite = false,
|
||||
this.thumbHash,
|
||||
this.visibility = AssetVisibility.timeline,
|
||||
this.localDateTime,
|
||||
super.livePhotoVideoId,
|
||||
this.stackId,
|
||||
this.stackCount = 0,
|
||||
@ -60,6 +62,7 @@ class RemoteAsset extends BaseAsset {
|
||||
isFavorite: $isFavorite,
|
||||
thumbHash: ${thumbHash ?? "<NA>"},
|
||||
visibility: $visibility,
|
||||
localDateTime: ${localDateTime ?? "<NA>"},
|
||||
stackId: ${stackId ?? "<NA>"},
|
||||
stackCount: $stackCount,
|
||||
checksum: $checksum,
|
||||
@ -77,6 +80,7 @@ class RemoteAsset extends BaseAsset {
|
||||
ownerId == other.ownerId &&
|
||||
thumbHash == other.thumbHash &&
|
||||
visibility == other.visibility &&
|
||||
localDateTime == other.localDateTime &&
|
||||
stackId == other.stackId &&
|
||||
stackCount == other.stackCount;
|
||||
}
|
||||
@ -89,6 +93,7 @@ class RemoteAsset extends BaseAsset {
|
||||
localId.hashCode ^
|
||||
thumbHash.hashCode ^
|
||||
visibility.hashCode ^
|
||||
localDateTime.hashCode ^
|
||||
stackId.hashCode ^
|
||||
stackCount.hashCode;
|
||||
|
||||
@ -107,6 +112,7 @@ class RemoteAsset extends BaseAsset {
|
||||
bool? isFavorite,
|
||||
String? thumbHash,
|
||||
AssetVisibility? visibility,
|
||||
DateTime? localDateTime,
|
||||
String? livePhotoVideoId,
|
||||
String? stackId,
|
||||
int? stackCount,
|
||||
@ -126,6 +132,7 @@ class RemoteAsset extends BaseAsset {
|
||||
isFavorite: isFavorite ?? this.isFavorite,
|
||||
thumbHash: thumbHash ?? this.thumbHash,
|
||||
visibility: visibility ?? this.visibility,
|
||||
localDateTime: localDateTime ?? this.localDateTime,
|
||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||
stackId: stackId ?? this.stackId,
|
||||
stackCount: stackCount ?? this.stackCount,
|
||||
|
@ -53,6 +53,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
||||
isFavorite: isFavorite,
|
||||
height: height,
|
||||
width: width,
|
||||
localDateTime: localDateTime,
|
||||
thumbHash: thumbHash,
|
||||
visibility: visibility,
|
||||
livePhotoVideoId: livePhotoVideoId,
|
||||
|
@ -100,6 +100,13 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<RemoteAsset?> getAsset(String id) {
|
||||
return _db.managers.remoteAssetEntity
|
||||
.filter((row) => row.id.equals(id))
|
||||
.map((row) => row.toDto())
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<List<(String, String)>> getPlaces() {
|
||||
final asset = Subquery(
|
||||
_db.remoteAssetEntity.select()
|
||||
@ -203,6 +210,29 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateDateTime(List<String> ids, String dateTime) {
|
||||
final localDateTime =
|
||||
dateTime.replaceAll(RegExp(r'[\+|-][0-9]{2}:[0-9]{2}'), '');
|
||||
return _db.batch((batch) async {
|
||||
for (final id in ids) {
|
||||
batch.update(
|
||||
_db.remoteAssetEntity,
|
||||
RemoteAssetEntityCompanion(
|
||||
localDateTime: Value(DateTime.parse(localDateTime).toUtc()),
|
||||
),
|
||||
where: (e) => e.id.equals(id),
|
||||
);
|
||||
batch.update(
|
||||
_db.remoteExifEntity,
|
||||
RemoteExifEntityCompanion(
|
||||
dateTimeOriginal: Value(DateTime.parse(dateTime)),
|
||||
),
|
||||
where: (e) => e.assetId.equals(id),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> stack(String userId, StackResponse stack) {
|
||||
return _db.transaction(() async {
|
||||
final stackIds = await _db.managers.stackEntity
|
||||
|
@ -1,10 +1,47 @@
|
||||
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 EditDateTimeActionButton extends ConsumerWidget {
|
||||
const EditDateTimeActionButton({super.key});
|
||||
final ActionSource source;
|
||||
|
||||
const EditDateTimeActionButton({super.key, required this.source});
|
||||
|
||||
_onTap(BuildContext context, WidgetRef ref) async {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final result =
|
||||
await ref.read(actionProvider.notifier).editDateTime(source, context);
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
final successMessage = 'edit_date_time_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) {
|
||||
@ -12,6 +49,7 @@ class EditDateTimeActionButton extends ConsumerWidget {
|
||||
maxWidth: 95.0,
|
||||
iconData: Icons.edit_calendar_outlined,
|
||||
label: "control_bottom_app_bar_edit_time".t(context: context),
|
||||
onPressed: () => _onTap(context, ref),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
||||
: const DeletePermanentActionButton(
|
||||
source: ActionSource.timeline,
|
||||
),
|
||||
const EditDateTimeActionButton(),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(
|
||||
source: ActionSource.timeline,
|
||||
|
@ -44,7 +44,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
||||
: const DeletePermanentActionButton(
|
||||
source: ActionSource.timeline,
|
||||
),
|
||||
const EditDateTimeActionButton(),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(
|
||||
source: ActionSource.timeline,
|
||||
|
@ -47,7 +47,7 @@ class GeneralBottomSheet extends ConsumerWidget {
|
||||
if (multiselect.hasLocal || multiselect.hasMerged) ...[
|
||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||
],
|
||||
const EditDateTimeActionButton(),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(
|
||||
source: ActionSource.timeline,
|
||||
|
@ -47,7 +47,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
||||
: const DeletePermanentActionButton(
|
||||
source: ActionSource.timeline,
|
||||
),
|
||||
const EditDateTimeActionButton(),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(
|
||||
source: ActionSource.timeline,
|
||||
|
@ -288,6 +288,28 @@ class ActionNotifier extends Notifier<void> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult?> editDateTime(
|
||||
ActionSource source,
|
||||
BuildContext context,
|
||||
) async {
|
||||
final ids = _getOwnedRemoteIdsForSource(source);
|
||||
try {
|
||||
final isEdited = await _service.editDateTime(ids, context);
|
||||
if (!isEdited) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ActionResult(count: ids.length, success: true);
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Failed to edit date and time for assets', error, stack);
|
||||
return ActionResult(
|
||||
count: ids.length,
|
||||
success: false,
|
||||
error: error.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult> removeFromAlbum(
|
||||
ActionSource source,
|
||||
String albumId,
|
||||
|
@ -98,6 +98,18 @@ class AssetApiRepository extends ApiRepository {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateDateTime(
|
||||
List<String> ids,
|
||||
String dateTime,
|
||||
) async {
|
||||
return _api.updateAssets(
|
||||
AssetBulkUpdateDto(
|
||||
ids: ids,
|
||||
dateTimeOriginal: dateTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<StackResponse> stack(List<String> ids) async {
|
||||
final responseDto =
|
||||
await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids)));
|
||||
|
@ -13,9 +13,11 @@ import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset_media.repository.dart';
|
||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/common/date_time_picker.dart';
|
||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:timezone/timezone.dart';
|
||||
|
||||
final actionServiceProvider = Provider<ActionService>(
|
||||
(ref) => ActionService(
|
||||
@ -159,7 +161,7 @@ class ActionService {
|
||||
) async {
|
||||
maplibre.LatLng? initialLatLng;
|
||||
if (remoteIds.length == 1) {
|
||||
final exif = await _remoteAssetRepository.getExif(remoteIds[0]);
|
||||
final exif = await _remoteAssetRepository.getExif(remoteIds.first);
|
||||
|
||||
if (exif?.latitude != null && exif?.longitude != null) {
|
||||
initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!);
|
||||
@ -187,6 +189,64 @@ class ActionService {
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> editDateTime(
|
||||
List<String> remoteIds,
|
||||
BuildContext context,
|
||||
) async {
|
||||
DateTime? initialDateTime;
|
||||
Duration? initialOffset;
|
||||
String? initialTimeZone;
|
||||
if (remoteIds.length == 1) {
|
||||
final asset = await _remoteAssetRepository.getAsset(remoteIds.first);
|
||||
final exif = await _remoteAssetRepository.getExif(remoteIds.first);
|
||||
|
||||
initialDateTime = asset?.localDateTime;
|
||||
initialTimeZone = exif?.timeZone;
|
||||
if (initialDateTime != null && initialTimeZone != null) {
|
||||
try {
|
||||
final location = getLocation(initialTimeZone);
|
||||
initialOffset =
|
||||
TZDateTime.from(initialDateTime, location).timeZoneOffset;
|
||||
} on LocationNotFoundException {
|
||||
RegExp re = RegExp(
|
||||
r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$',
|
||||
caseSensitive: false,
|
||||
);
|
||||
final m = re.firstMatch(initialTimeZone);
|
||||
if (m != null) {
|
||||
final offset = Duration(
|
||||
hours: int.parse(m.group(1) ?? '0'),
|
||||
minutes: int.parse(m.group(2) ?? '0'),
|
||||
);
|
||||
initialOffset = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final dateTime = await showDateTimePicker(
|
||||
context: context,
|
||||
initialDateTime: initialDateTime,
|
||||
initialTZ: initialTimeZone,
|
||||
initialTZOffset: initialOffset,
|
||||
);
|
||||
|
||||
if (dateTime == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await _assetApiRepository.updateDateTime(
|
||||
remoteIds,
|
||||
dateTime,
|
||||
);
|
||||
await _remoteAssetRepository.updateDateTime(
|
||||
remoteIds,
|
||||
dateTime,
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<int> removeFromAlbum(List<String> remoteIds, String albumId) async {
|
||||
int removedCount = 0;
|
||||
final result = await _albumApiRepository.removeAssets(albumId, remoteIds);
|
||||
|
Loading…
x
Reference in New Issue
Block a user