mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
feat(mobile): edit date time action
This commit is contained in:
parent
f2f3db3a79
commit
12764666e9
@ -790,6 +790,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",
|
||||
|
@ -12,6 +12,7 @@ class RemoteAsset extends BaseAsset {
|
||||
final String id;
|
||||
final String? localId;
|
||||
final String? thumbHash;
|
||||
final DateTime? localDateTime;
|
||||
final AssetVisibility visibility;
|
||||
final String ownerId;
|
||||
|
||||
@ -29,6 +30,7 @@ class RemoteAsset extends BaseAsset {
|
||||
super.durationInSeconds,
|
||||
super.isFavorite = false,
|
||||
this.thumbHash,
|
||||
this.localDateTime,
|
||||
this.visibility = AssetVisibility.timeline,
|
||||
});
|
||||
|
||||
@ -51,6 +53,7 @@ class RemoteAsset extends BaseAsset {
|
||||
localId: ${localId ?? "<NA>"},
|
||||
isFavorite: $isFavorite,
|
||||
thumbHash: ${thumbHash ?? "<NA>"},
|
||||
localDateTime: ${localDateTime ?? "<NA>"},
|
||||
visibility: $visibility,
|
||||
}''';
|
||||
}
|
||||
@ -64,6 +67,7 @@ class RemoteAsset extends BaseAsset {
|
||||
ownerId == other.ownerId &&
|
||||
localId == other.localId &&
|
||||
thumbHash == other.thumbHash &&
|
||||
localDateTime == other.localDateTime &&
|
||||
visibility == other.visibility;
|
||||
}
|
||||
|
||||
@ -74,5 +78,6 @@ class RemoteAsset extends BaseAsset {
|
||||
ownerId.hashCode ^
|
||||
localId.hashCode ^
|
||||
thumbHash.hashCode ^
|
||||
localDateTime.hashCode ^
|
||||
visibility.hashCode;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:drift/drift.dart' hide Query;
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart' as domain;
|
||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||
@ -143,3 +144,22 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin {
|
||||
@override
|
||||
Set<Column> get primaryKey => {assetId};
|
||||
}
|
||||
|
||||
extension RemoteExifEntityDataDomainEx on RemoteExifEntityData {
|
||||
domain.ExifInfo toDto() => domain.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,
|
||||
);
|
||||
}
|
||||
|
@ -49,6 +49,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
|
||||
isFavorite: isFavorite,
|
||||
height: height,
|
||||
width: width,
|
||||
localDateTime: localDateTime,
|
||||
thumbHash: thumbHash,
|
||||
visibility: visibility,
|
||||
localId: null,
|
||||
|
@ -2,7 +2,6 @@ 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';
|
||||
|
||||
@ -52,27 +51,6 @@ class DriftRemoteExifRepository extends DriftDatabaseRepository {
|
||||
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,
|
||||
);
|
||||
return query.map((exif) => exif.toDto()).getSingleOrNull();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import 'package:drift/drift.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.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
@ -9,6 +10,13 @@ class DriftRemoteAssetRepository extends DriftDatabaseRepository {
|
||||
final Drift _db;
|
||||
const DriftRemoteAssetRepository(this._db) : super(_db);
|
||||
|
||||
Future<RemoteAsset?> get(String assetId) {
|
||||
final query = _db.remoteAssetEntity.select()
|
||||
..where((asset) => asset.id.equals(assetId));
|
||||
|
||||
return query.map((asset) => asset.toDto()).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<void> updateFavorite(List<String> ids, bool isFavorite) {
|
||||
return _db.batch((batch) async {
|
||||
for (final id in ids) {
|
||||
@ -47,4 +55,26 @@ class DriftRemoteAssetRepository 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),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,49 @@
|
||||
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/infrastructure/timeline.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;
|
||||
}
|
||||
|
||||
await ref.read(timelineServiceProvider).reloadBucket();
|
||||
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 +51,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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ 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/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
|
||||
@ -24,6 +25,7 @@ class EditLocationActionButton extends ConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
await ref.read(timelineServiceProvider).reloadBucket();
|
||||
ref.read(multiSelectProvider.notifier).reset();
|
||||
|
||||
final successMessage = 'edit_location_action_prompt'.t(
|
||||
|
@ -41,7 +41,7 @@ class HomeBottomAppBar extends ConsumerWidget {
|
||||
isTrashEnable
|
||||
? const TrashActionButton()
|
||||
: const DeletePermanentActionButton(),
|
||||
const EditDateTimeActionButton(),
|
||||
const EditDateTimeActionButton(source: ActionSource.timeline),
|
||||
const EditLocationActionButton(source: ActionSource.timeline),
|
||||
const MoveToLockFolderActionButton(
|
||||
source: ActionSource.timeline,
|
||||
|
@ -194,6 +194,28 @@ class ActionNotifier extends Notifier<void> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ActionResult?> editDateTime(
|
||||
ActionSource source,
|
||||
BuildContext context,
|
||||
) async {
|
||||
final ids = _getOwnedRemoteForSource(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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension on Iterable<RemoteAsset> {
|
||||
|
@ -79,6 +79,18 @@ class AssetApiRepository extends ApiRepository {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateDateTime(
|
||||
List<String> ids,
|
||||
String dateTime,
|
||||
) async {
|
||||
return _api.updateAssets(
|
||||
AssetBulkUpdateDto(
|
||||
ids: ids,
|
||||
dateTimeOriginal: dateTime,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_mapVisibility(AssetVisibilityEnum visibility) => switch (visibility) {
|
||||
AssetVisibilityEnum.timeline => AssetVisibility.timeline,
|
||||
AssetVisibilityEnum.hidden => AssetVisibility.hidden,
|
||||
|
@ -8,9 +8,11 @@ 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/date_time_picker.dart';
|
||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:timezone/timezone.dart';
|
||||
|
||||
final actionServiceProvider = Provider<ActionService>(
|
||||
(ref) => ActionService(
|
||||
@ -99,7 +101,7 @@ class ActionService {
|
||||
) async {
|
||||
LatLng? initialLatLng;
|
||||
if (remoteIds.length == 1) {
|
||||
final exif = await _remoteExifRepository.get(remoteIds[0]);
|
||||
final exif = await _remoteExifRepository.get(remoteIds.first);
|
||||
|
||||
if (exif?.latitude != null && exif?.longitude != null) {
|
||||
initialLatLng = LatLng(exif!.latitude!, exif.longitude!);
|
||||
@ -126,4 +128,61 @@ 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.get(remoteIds.first);
|
||||
final exif = await _remoteExifRepository.get(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;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user