feat(mobile): edit date time action

This commit is contained in:
wuzihao051119 2025-07-19 18:26:20 +08:00
parent c7853fbe9d
commit d0e0c24690
12 changed files with 177 additions and 6 deletions

View File

@ -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",

View File

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

View File

@ -53,6 +53,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
isFavorite: isFavorite,
height: height,
width: width,
localDateTime: localDateTime,
thumbHash: thumbHash,
visibility: visibility,
livePhotoVideoId: livePhotoVideoId,

View File

@ -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

View File

@ -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),
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)));

View File

@ -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);