mirror of
https://github.com/immich-app/immich.git
synced 2026-06-12 19:11:52 -07:00
fix(mobile): keep timezone when editing asset date time (#28978)
* fix(mobile): keep timezone when editing asset date time * fix(mobile): negative utc offsets with minutes off by an hour
This commit is contained in:
@@ -194,12 +194,15 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateDateTime(List<String> ids, DateTime dateTime) {
|
||||
Future<void> updateDateTime(List<String> ids, DateTime dateTime, {String? timeZone}) {
|
||||
return _db.batch((batch) async {
|
||||
for (final id in ids) {
|
||||
batch.update(
|
||||
_db.remoteExifEntity,
|
||||
RemoteExifEntityCompanion(dateTimeOriginal: Value(dateTime)),
|
||||
RemoteExifEntityCompanion(
|
||||
dateTimeOriginal: Value(dateTime),
|
||||
timeZone: timeZone == null ? const Value.absent() : Value(timeZone),
|
||||
),
|
||||
where: (e) => e.assetId.equals(id),
|
||||
);
|
||||
batch.update(
|
||||
|
||||
@@ -59,10 +59,8 @@ class AssetApiRepository extends ApiRepository {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateDateTime(List<String> ids, DateTime dateTime) async {
|
||||
return _api.updateAssets(
|
||||
AssetBulkUpdateDto(ids: ids, dateTimeOriginal: Optional.present(dateTime.toIso8601String())),
|
||||
);
|
||||
Future<void> updateDateTime(List<String> ids, String dateTime) async {
|
||||
return _api.updateAssets(AssetBulkUpdateDto(ids: ids, dateTimeOriginal: Optional.present(dateTime)));
|
||||
}
|
||||
|
||||
Future<StackResponse> stack(List<String> ids) async {
|
||||
|
||||
@@ -206,15 +206,24 @@ class ActionService {
|
||||
return false;
|
||||
}
|
||||
|
||||
// convert dateTime to DateTime object
|
||||
final parsedDateTime = DateTime.parse(dateTime);
|
||||
|
||||
await _assetApiRepository.updateDateTime(remoteIds, parsedDateTime);
|
||||
await _remoteAssetRepository.updateDateTime(remoteIds, parsedDateTime);
|
||||
await applyDateTime(remoteIds, dateTime);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<void> applyDateTime(List<String> remoteIds, String dateTime) async {
|
||||
final parsedDateTime = DateTime.parse(dateTime);
|
||||
final offset = RegExp(r'[+-]\d{2}:\d{2}$').firstMatch(dateTime)?.group(0);
|
||||
|
||||
await _assetApiRepository.updateDateTime(remoteIds, dateTime);
|
||||
await _remoteAssetRepository.updateDateTime(
|
||||
remoteIds,
|
||||
parsedDateTime,
|
||||
timeZone: offset == null ? null : 'UTC$offset',
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> removeFromAlbum(List<String> remoteIds, String albumId) async {
|
||||
final result = await _albumApiRepository.removeAssets(albumId, remoteIds);
|
||||
if (result.removed.isNotEmpty) {
|
||||
|
||||
@@ -24,7 +24,9 @@ import 'package:timezone/timezone.dart';
|
||||
RegExp re = RegExp(r'^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$', caseSensitive: false);
|
||||
final m = re.firstMatch(timeZone);
|
||||
if (m != null) {
|
||||
final duration = Duration(hours: int.parse(m.group(1) ?? '0'), minutes: int.parse(m.group(2) ?? '0'));
|
||||
final hours = int.parse(m.group(1) ?? '0');
|
||||
final minutes = int.parse(m.group(2) ?? '0');
|
||||
final duration = Duration(hours: hours, minutes: hours.isNegative ? -minutes : minutes);
|
||||
dt = dt.add(duration);
|
||||
return (dt, duration);
|
||||
}
|
||||
|
||||
@@ -99,6 +99,49 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionService.applyDateTime', () {
|
||||
const ids = ['asset_id_1'];
|
||||
|
||||
test('sends the picked value to the api with its offset intact', () async {
|
||||
const picked = '2026-06-10T19:15:00.000+06:00';
|
||||
when(() => assetApiRepository.updateDateTime(ids, picked)).thenAnswer((_) async {});
|
||||
when(
|
||||
() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC+06:00'),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
await sut.applyDateTime(ids, picked);
|
||||
|
||||
verify(() => assetApiRepository.updateDateTime(ids, picked)).called(1);
|
||||
verify(() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC+06:00')).called(1);
|
||||
});
|
||||
|
||||
test('handles negative offsets', () async {
|
||||
const picked = '2026-01-05T08:00:00.000-05:30';
|
||||
when(() => assetApiRepository.updateDateTime(ids, picked)).thenAnswer((_) async {});
|
||||
when(
|
||||
() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC-05:30'),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
await sut.applyDateTime(ids, picked);
|
||||
|
||||
verify(() => assetApiRepository.updateDateTime(ids, picked)).called(1);
|
||||
verify(() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: 'UTC-05:30')).called(1);
|
||||
});
|
||||
|
||||
test('writes no timezone when the value has no offset', () async {
|
||||
const picked = '2026-06-10T13:15:00.000Z';
|
||||
when(() => assetApiRepository.updateDateTime(ids, picked)).thenAnswer((_) async {});
|
||||
when(
|
||||
() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: null),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
await sut.applyDateTime(ids, picked);
|
||||
|
||||
verify(() => assetApiRepository.updateDateTime(ids, picked)).called(1);
|
||||
verify(() => remoteAssetRepository.updateDateTime(ids, DateTime.parse(picked), timeZone: null)).called(1);
|
||||
});
|
||||
});
|
||||
|
||||
group('ActionService.deleteLocal', () {
|
||||
test('routes deleted ids to trashed repository when Android trash handling is enabled', () async {
|
||||
await Store.put(StoreKey.manageLocalMediaAndroid, true);
|
||||
|
||||
@@ -177,6 +177,32 @@ void main() {
|
||||
expect(adjustedTime.minute, 30);
|
||||
expect(offset, const Duration(hours: 5, minutes: 30));
|
||||
});
|
||||
|
||||
test('should handle UTC-05:30 format (negative offset with minutes)', () {
|
||||
final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0);
|
||||
|
||||
final (adjustedTime, offset) = applyTimezoneOffset(
|
||||
dateTime: utcTime,
|
||||
timeZone: 'UTC-05:30',
|
||||
);
|
||||
|
||||
expect(adjustedTime.hour, 6);
|
||||
expect(adjustedTime.minute, 30); // 12:00 UTC - 5:30 = 06:30
|
||||
expect(offset, const Duration(hours: -5, minutes: -30));
|
||||
});
|
||||
|
||||
test('should handle UTC-3:30 format (single digit hour with minutes)', () {
|
||||
final utcTime = DateTime.utc(2024, 6, 15, 12, 0, 0);
|
||||
|
||||
final (adjustedTime, offset) = applyTimezoneOffset(
|
||||
dateTime: utcTime,
|
||||
timeZone: 'UTC-3:30',
|
||||
);
|
||||
|
||||
expect(adjustedTime.hour, 8);
|
||||
expect(adjustedTime.minute, 30); // 12:00 UTC - 3:30 = 08:30
|
||||
expect(offset, const Duration(hours: -3, minutes: -30));
|
||||
});
|
||||
});
|
||||
|
||||
group('with null or invalid timezone', () {
|
||||
|
||||
Reference in New Issue
Block a user