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_avatar": "Edit avatar",
|
||||||
"edit_date": "Edit date",
|
"edit_date": "Edit date",
|
||||||
"edit_date_and_time": "Edit date and time",
|
"edit_date_and_time": "Edit date and time",
|
||||||
|
"edit_date_time_action_prompt": "{count} date and time edited",
|
||||||
"edit_description": "Edit description",
|
"edit_description": "Edit description",
|
||||||
"edit_description_prompt": "Please select a new description:",
|
"edit_description_prompt": "Please select a new description:",
|
||||||
"edit_exclusion_pattern": "Edit exclusion pattern",
|
"edit_exclusion_pattern": "Edit exclusion pattern",
|
||||||
|
@ -13,6 +13,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
final String? localId;
|
final String? localId;
|
||||||
final String? thumbHash;
|
final String? thumbHash;
|
||||||
final AssetVisibility visibility;
|
final AssetVisibility visibility;
|
||||||
|
final DateTime? localDateTime;
|
||||||
final String ownerId;
|
final String ownerId;
|
||||||
final String? stackId;
|
final String? stackId;
|
||||||
final int stackCount;
|
final int stackCount;
|
||||||
@ -32,6 +33,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
super.isFavorite = false,
|
super.isFavorite = false,
|
||||||
this.thumbHash,
|
this.thumbHash,
|
||||||
this.visibility = AssetVisibility.timeline,
|
this.visibility = AssetVisibility.timeline,
|
||||||
|
this.localDateTime,
|
||||||
super.livePhotoVideoId,
|
super.livePhotoVideoId,
|
||||||
this.stackId,
|
this.stackId,
|
||||||
this.stackCount = 0,
|
this.stackCount = 0,
|
||||||
@ -60,6 +62,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
isFavorite: $isFavorite,
|
isFavorite: $isFavorite,
|
||||||
thumbHash: ${thumbHash ?? "<NA>"},
|
thumbHash: ${thumbHash ?? "<NA>"},
|
||||||
visibility: $visibility,
|
visibility: $visibility,
|
||||||
|
localDateTime: ${localDateTime ?? "<NA>"},
|
||||||
stackId: ${stackId ?? "<NA>"},
|
stackId: ${stackId ?? "<NA>"},
|
||||||
stackCount: $stackCount,
|
stackCount: $stackCount,
|
||||||
checksum: $checksum,
|
checksum: $checksum,
|
||||||
@ -77,6 +80,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
ownerId == other.ownerId &&
|
ownerId == other.ownerId &&
|
||||||
thumbHash == other.thumbHash &&
|
thumbHash == other.thumbHash &&
|
||||||
visibility == other.visibility &&
|
visibility == other.visibility &&
|
||||||
|
localDateTime == other.localDateTime &&
|
||||||
stackId == other.stackId &&
|
stackId == other.stackId &&
|
||||||
stackCount == other.stackCount;
|
stackCount == other.stackCount;
|
||||||
}
|
}
|
||||||
@ -89,6 +93,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
localId.hashCode ^
|
localId.hashCode ^
|
||||||
thumbHash.hashCode ^
|
thumbHash.hashCode ^
|
||||||
visibility.hashCode ^
|
visibility.hashCode ^
|
||||||
|
localDateTime.hashCode ^
|
||||||
stackId.hashCode ^
|
stackId.hashCode ^
|
||||||
stackCount.hashCode;
|
stackCount.hashCode;
|
||||||
|
|
||||||
@ -107,6 +112,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
bool? isFavorite,
|
bool? isFavorite,
|
||||||
String? thumbHash,
|
String? thumbHash,
|
||||||
AssetVisibility? visibility,
|
AssetVisibility? visibility,
|
||||||
|
DateTime? localDateTime,
|
||||||
String? livePhotoVideoId,
|
String? livePhotoVideoId,
|
||||||
String? stackId,
|
String? stackId,
|
||||||
int? stackCount,
|
int? stackCount,
|
||||||
@ -126,6 +132,7 @@ class RemoteAsset extends BaseAsset {
|
|||||||
isFavorite: isFavorite ?? this.isFavorite,
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
thumbHash: thumbHash ?? this.thumbHash,
|
thumbHash: thumbHash ?? this.thumbHash,
|
||||||
visibility: visibility ?? this.visibility,
|
visibility: visibility ?? this.visibility,
|
||||||
|
localDateTime: localDateTime ?? this.localDateTime,
|
||||||
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
|
||||||
stackId: stackId ?? this.stackId,
|
stackId: stackId ?? this.stackId,
|
||||||
stackCount: stackCount ?? this.stackCount,
|
stackCount: stackCount ?? this.stackCount,
|
||||||
|
@ -53,6 +53,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
|||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
|
localDateTime: localDateTime,
|
||||||
thumbHash: thumbHash,
|
thumbHash: thumbHash,
|
||||||
visibility: visibility,
|
visibility: visibility,
|
||||||
livePhotoVideoId: livePhotoVideoId,
|
livePhotoVideoId: livePhotoVideoId,
|
||||||
|
@ -100,6 +100,13 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
.getSingleOrNull();
|
.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() {
|
Future<List<(String, String)>> getPlaces() {
|
||||||
final asset = Subquery(
|
final asset = Subquery(
|
||||||
_db.remoteAssetEntity.select()
|
_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) {
|
Future<void> stack(String userId, StackResponse stack) {
|
||||||
return _db.transaction(() async {
|
return _db.transaction(() async {
|
||||||
final stackIds = await _db.managers.stackEntity
|
final stackIds = await _db.managers.stackEntity
|
||||||
|
@ -1,10 +1,47 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.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 {
|
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
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@ -12,6 +49,7 @@ class EditDateTimeActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 95.0,
|
maxWidth: 95.0,
|
||||||
iconData: Icons.edit_calendar_outlined,
|
iconData: Icons.edit_calendar_outlined,
|
||||||
label: "control_bottom_app_bar_edit_time".t(context: context),
|
label: "control_bottom_app_bar_edit_time".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
|
|||||||
: const DeletePermanentActionButton(
|
: const DeletePermanentActionButton(
|
||||||
source: ActionSource.timeline,
|
source: ActionSource.timeline,
|
||||||
),
|
),
|
||||||
const EditDateTimeActionButton(),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(
|
const MoveToLockFolderActionButton(
|
||||||
source: ActionSource.timeline,
|
source: ActionSource.timeline,
|
||||||
|
@ -44,7 +44,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
|
|||||||
: const DeletePermanentActionButton(
|
: const DeletePermanentActionButton(
|
||||||
source: ActionSource.timeline,
|
source: ActionSource.timeline,
|
||||||
),
|
),
|
||||||
const EditDateTimeActionButton(),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(
|
const MoveToLockFolderActionButton(
|
||||||
source: ActionSource.timeline,
|
source: ActionSource.timeline,
|
||||||
|
@ -47,7 +47,7 @@ class GeneralBottomSheet extends ConsumerWidget {
|
|||||||
if (multiselect.hasLocal || multiselect.hasMerged) ...[
|
if (multiselect.hasLocal || multiselect.hasMerged) ...[
|
||||||
const DeleteLocalActionButton(source: ActionSource.timeline),
|
const DeleteLocalActionButton(source: ActionSource.timeline),
|
||||||
],
|
],
|
||||||
const EditDateTimeActionButton(),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(
|
const MoveToLockFolderActionButton(
|
||||||
source: ActionSource.timeline,
|
source: ActionSource.timeline,
|
||||||
|
@ -47,7 +47,7 @@ class RemoteAlbumBottomSheet extends ConsumerWidget {
|
|||||||
: const DeletePermanentActionButton(
|
: const DeletePermanentActionButton(
|
||||||
source: ActionSource.timeline,
|
source: ActionSource.timeline,
|
||||||
),
|
),
|
||||||
const EditDateTimeActionButton(),
|
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(
|
const MoveToLockFolderActionButton(
|
||||||
source: ActionSource.timeline,
|
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(
|
Future<ActionResult> removeFromAlbum(
|
||||||
ActionSource source,
|
ActionSource source,
|
||||||
String albumId,
|
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 {
|
Future<StackResponse> stack(List<String> ids) async {
|
||||||
final responseDto =
|
final responseDto =
|
||||||
await checkNull(_stacksApi.createStack(StackCreateDto(assetIds: ids)));
|
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/asset_media.repository.dart';
|
||||||
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.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:immich_mobile/widgets/common/location_picker.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
|
import 'package:maplibre_gl/maplibre_gl.dart' as maplibre;
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:timezone/timezone.dart';
|
||||||
|
|
||||||
final actionServiceProvider = Provider<ActionService>(
|
final actionServiceProvider = Provider<ActionService>(
|
||||||
(ref) => ActionService(
|
(ref) => ActionService(
|
||||||
@ -159,7 +161,7 @@ class ActionService {
|
|||||||
) async {
|
) async {
|
||||||
maplibre.LatLng? initialLatLng;
|
maplibre.LatLng? initialLatLng;
|
||||||
if (remoteIds.length == 1) {
|
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) {
|
if (exif?.latitude != null && exif?.longitude != null) {
|
||||||
initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!);
|
initialLatLng = maplibre.LatLng(exif!.latitude!, exif.longitude!);
|
||||||
@ -187,6 +189,64 @@ class ActionService {
|
|||||||
return true;
|
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 {
|
Future<int> removeFromAlbum(List<String> remoteIds, String albumId) async {
|
||||||
int removedCount = 0;
|
int removedCount = 0;
|
||||||
final result = await _albumApiRepository.removeAssets(albumId, remoteIds);
|
final result = await _albumApiRepository.removeAssets(albumId, remoteIds);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user