put(Isar db) async {
diff --git a/mobile/lib/shared/models/asset.g.dart b/mobile/lib/shared/models/asset.g.dart
index d845b5353a..5912f291b5 100644
--- a/mobile/lib/shared/models/asset.g.dart
+++ b/mobile/lib/shared/models/asset.g.dart
@@ -102,19 +102,24 @@ const AssetSchema = CollectionSchema(
name: r'stackParentId',
type: IsarType.string,
),
- r'type': PropertySchema(
+ r'thumbhash': PropertySchema(
id: 17,
+ name: r'thumbhash',
+ type: IsarType.string,
+ ),
+ r'type': PropertySchema(
+ id: 18,
name: r'type',
type: IsarType.byte,
enumMap: _AssettypeEnumValueMap,
),
r'updatedAt': PropertySchema(
- id: 18,
+ id: 19,
name: r'updatedAt',
type: IsarType.dateTime,
),
r'width': PropertySchema(
- id: 19,
+ id: 20,
name: r'width',
type: IsarType.int,
)
@@ -210,6 +215,12 @@ int _assetEstimateSize(
bytesCount += 3 + value.length * 3;
}
}
+ {
+ final value = object.thumbhash;
+ if (value != null) {
+ bytesCount += 3 + value.length * 3;
+ }
+ }
return bytesCount;
}
@@ -236,9 +247,10 @@ void _assetSerialize(
writer.writeString(offsets[14], object.remoteId);
writer.writeLong(offsets[15], object.stackCount);
writer.writeString(offsets[16], object.stackParentId);
- writer.writeByte(offsets[17], object.type.index);
- writer.writeDateTime(offsets[18], object.updatedAt);
- writer.writeInt(offsets[19], object.width);
+ writer.writeString(offsets[17], object.thumbhash);
+ writer.writeByte(offsets[18], object.type.index);
+ writer.writeDateTime(offsets[19], object.updatedAt);
+ writer.writeInt(offsets[20], object.width);
}
Asset _assetDeserialize(
@@ -266,10 +278,11 @@ Asset _assetDeserialize(
remoteId: reader.readStringOrNull(offsets[14]),
stackCount: reader.readLongOrNull(offsets[15]),
stackParentId: reader.readStringOrNull(offsets[16]),
- type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ??
+ thumbhash: reader.readStringOrNull(offsets[17]),
+ type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ??
AssetType.other,
- updatedAt: reader.readDateTime(offsets[18]),
- width: reader.readIntOrNull(offsets[19]),
+ updatedAt: reader.readDateTime(offsets[19]),
+ width: reader.readIntOrNull(offsets[20]),
);
return object;
}
@@ -316,11 +329,13 @@ P _assetDeserializeProp(
case 16:
return (reader.readStringOrNull(offset)) as P;
case 17:
+ return (reader.readStringOrNull(offset)) as P;
+ case 18:
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
AssetType.other) as P;
- case 18:
- return (reader.readDateTime(offset)) as P;
case 19:
+ return (reader.readDateTime(offset)) as P;
+ case 20:
return (reader.readIntOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -2078,6 +2093,152 @@ extension AssetQueryFilter on QueryBuilder {
});
}
+ QueryBuilder thumbhashIsNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(const FilterCondition.isNull(
+ property: r'thumbhash',
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashIsNotNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(const FilterCondition.isNotNull(
+ property: r'thumbhash',
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.equalTo(
+ property: r'thumbhash',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashGreaterThan(
+ String? value, {
+ bool include = false,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.greaterThan(
+ include: include,
+ property: r'thumbhash',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashLessThan(
+ String? value, {
+ bool include = false,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.lessThan(
+ include: include,
+ property: r'thumbhash',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashBetween(
+ String? lower,
+ String? upper, {
+ bool includeLower = true,
+ bool includeUpper = true,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.between(
+ property: r'thumbhash',
+ lower: lower,
+ includeLower: includeLower,
+ upper: upper,
+ includeUpper: includeUpper,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashStartsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.startsWith(
+ property: r'thumbhash',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashEndsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.endsWith(
+ property: r'thumbhash',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashContains(
+ String value,
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.contains(
+ property: r'thumbhash',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashMatches(
+ String pattern,
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.matches(
+ property: r'thumbhash',
+ wildcard: pattern,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashIsEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.equalTo(
+ property: r'thumbhash',
+ value: '',
+ ));
+ });
+ }
+
+ QueryBuilder thumbhashIsNotEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.greaterThan(
+ property: r'thumbhash',
+ value: '',
+ ));
+ });
+ }
+
QueryBuilder typeEqualTo(
AssetType value) {
return QueryBuilder.apply(this, (query) {
@@ -2462,6 +2623,18 @@ extension AssetQuerySortBy on QueryBuilder {
});
}
+ QueryBuilder sortByThumbhash() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'thumbhash', Sort.asc);
+ });
+ }
+
+ QueryBuilder sortByThumbhashDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'thumbhash', Sort.desc);
+ });
+ }
+
QueryBuilder sortByType() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.asc);
@@ -2716,6 +2889,18 @@ extension AssetQuerySortThenBy on QueryBuilder {
});
}
+ QueryBuilder thenByThumbhash() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'thumbhash', Sort.asc);
+ });
+ }
+
+ QueryBuilder thenByThumbhashDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'thumbhash', Sort.desc);
+ });
+ }
+
QueryBuilder thenByType() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'type', Sort.asc);
@@ -2864,6 +3049,13 @@ extension AssetQueryWhereDistinct on QueryBuilder {
});
}
+ QueryBuilder distinctByThumbhash(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addDistinctBy(r'thumbhash', caseSensitive: caseSensitive);
+ });
+ }
+
QueryBuilder distinctByType() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'type');
@@ -2992,6 +3184,12 @@ extension AssetQueryProperty on QueryBuilder {
});
}
+ QueryBuilder thumbhashProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addPropertyName(r'thumbhash');
+ });
+ }
+
QueryBuilder typeProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'type');
diff --git a/mobile/lib/shared/models/logger_message.model.dart b/mobile/lib/shared/models/logger_message.model.dart
index cb1d45a580..f657257eab 100644
--- a/mobile/lib/shared/models/logger_message.model.dart
+++ b/mobile/lib/shared/models/logger_message.model.dart
@@ -9,6 +9,7 @@ part 'logger_message.model.g.dart';
class LoggerMessage {
Id id = Isar.autoIncrement;
String message;
+ String? details;
@Enumerated(EnumType.ordinal)
LogLevel level = LogLevel.INFO;
DateTime createdAt;
@@ -17,6 +18,7 @@ class LoggerMessage {
LoggerMessage({
required this.message,
+ required this.details,
required this.level,
required this.createdAt,
required this.context1,
diff --git a/mobile/lib/shared/models/logger_message.model.g.dart b/mobile/lib/shared/models/logger_message.model.g.dart
index a6b960eece..76c823704c 100644
--- a/mobile/lib/shared/models/logger_message.model.g.dart
+++ b/mobile/lib/shared/models/logger_message.model.g.dart
@@ -32,14 +32,19 @@ const LoggerMessageSchema = CollectionSchema(
name: r'createdAt',
type: IsarType.dateTime,
),
- r'level': PropertySchema(
+ r'details': PropertySchema(
id: 3,
+ name: r'details',
+ type: IsarType.string,
+ ),
+ r'level': PropertySchema(
+ id: 4,
name: r'level',
type: IsarType.byte,
enumMap: _LoggerMessagelevelEnumValueMap,
),
r'message': PropertySchema(
- id: 4,
+ id: 5,
name: r'message',
type: IsarType.string,
)
@@ -76,6 +81,12 @@ int _loggerMessageEstimateSize(
bytesCount += 3 + value.length * 3;
}
}
+ {
+ final value = object.details;
+ if (value != null) {
+ bytesCount += 3 + value.length * 3;
+ }
+ }
bytesCount += 3 + object.message.length * 3;
return bytesCount;
}
@@ -89,8 +100,9 @@ void _loggerMessageSerialize(
writer.writeString(offsets[0], object.context1);
writer.writeString(offsets[1], object.context2);
writer.writeDateTime(offsets[2], object.createdAt);
- writer.writeByte(offsets[3], object.level.index);
- writer.writeString(offsets[4], object.message);
+ writer.writeString(offsets[3], object.details);
+ writer.writeByte(offsets[4], object.level.index);
+ writer.writeString(offsets[5], object.message);
}
LoggerMessage _loggerMessageDeserialize(
@@ -103,9 +115,10 @@ LoggerMessage _loggerMessageDeserialize(
context1: reader.readStringOrNull(offsets[0]),
context2: reader.readStringOrNull(offsets[1]),
createdAt: reader.readDateTime(offsets[2]),
- level: _LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offsets[3])] ??
+ details: reader.readStringOrNull(offsets[3]),
+ level: _LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offsets[4])] ??
LogLevel.ALL,
- message: reader.readString(offsets[4]),
+ message: reader.readString(offsets[5]),
);
object.id = id;
return object;
@@ -125,9 +138,11 @@ P _loggerMessageDeserializeProp(
case 2:
return (reader.readDateTime(offset)) as P;
case 3:
+ return (reader.readStringOrNull(offset)) as P;
+ case 4:
return (_LoggerMessagelevelValueEnumMap[reader.readByteOrNull(offset)] ??
LogLevel.ALL) as P;
- case 4:
+ case 5:
return (reader.readString(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -619,6 +634,160 @@ extension LoggerMessageQueryFilter
});
}
+ QueryBuilder
+ detailsIsNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(const FilterCondition.isNull(
+ property: r'details',
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsIsNotNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(const FilterCondition.isNotNull(
+ property: r'details',
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.equalTo(
+ property: r'details',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsGreaterThan(
+ String? value, {
+ bool include = false,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.greaterThan(
+ include: include,
+ property: r'details',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsLessThan(
+ String? value, {
+ bool include = false,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.lessThan(
+ include: include,
+ property: r'details',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsBetween(
+ String? lower,
+ String? upper, {
+ bool includeLower = true,
+ bool includeUpper = true,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.between(
+ property: r'details',
+ lower: lower,
+ includeLower: includeLower,
+ upper: upper,
+ includeUpper: includeUpper,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsStartsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.startsWith(
+ property: r'details',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsEndsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.endsWith(
+ property: r'details',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsContains(String value, {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.contains(
+ property: r'details',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsMatches(String pattern, {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.matches(
+ property: r'details',
+ wildcard: pattern,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsIsEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.equalTo(
+ property: r'details',
+ value: '',
+ ));
+ });
+ }
+
+ QueryBuilder
+ detailsIsNotEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.greaterThan(
+ property: r'details',
+ value: '',
+ ));
+ });
+ }
+
QueryBuilder idEqualTo(
Id value) {
return QueryBuilder.apply(this, (query) {
@@ -913,6 +1082,18 @@ extension LoggerMessageQuerySortBy
});
}
+ QueryBuilder sortByDetails() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'details', Sort.asc);
+ });
+ }
+
+ QueryBuilder sortByDetailsDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'details', Sort.desc);
+ });
+ }
+
QueryBuilder sortByLevel() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'level', Sort.asc);
@@ -979,6 +1160,18 @@ extension LoggerMessageQuerySortThenBy
});
}
+ QueryBuilder thenByDetails() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'details', Sort.asc);
+ });
+ }
+
+ QueryBuilder thenByDetailsDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'details', Sort.desc);
+ });
+ }
+
QueryBuilder thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
@@ -1038,6 +1231,13 @@ extension LoggerMessageQueryWhereDistinct
});
}
+ QueryBuilder distinctByDetails(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addDistinctBy(r'details', caseSensitive: caseSensitive);
+ });
+ }
+
QueryBuilder distinctByLevel() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'level');
@@ -1078,6 +1278,12 @@ extension LoggerMessageQueryProperty
});
}
+ QueryBuilder detailsProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addPropertyName(r'details');
+ });
+ }
+
QueryBuilder levelProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'level');
diff --git a/mobile/lib/shared/services/asset.service.dart b/mobile/lib/shared/services/asset.service.dart
index 64a0f28ab7..3086ab9246 100644
--- a/mobile/lib/shared/services/asset.service.dart
+++ b/mobile/lib/shared/services/asset.service.dart
@@ -90,7 +90,7 @@ class AssetService {
return allAssets;
} catch (error, stack) {
log.severe(
- 'Error while getting remote assets: ${error.toString()}',
+ 'Error while getting remote assets',
error,
stack,
);
@@ -117,7 +117,7 @@ class AssetService {
);
return true;
} catch (error, stack) {
- log.severe("Error deleteAssets ${error.toString()}", error, stack);
+ log.severe("Error while deleting assets", error, stack);
}
return false;
}
diff --git a/mobile/lib/shared/services/immich_logger.service.dart b/mobile/lib/shared/services/immich_logger.service.dart
index b66177e570..967ab2d5f2 100644
--- a/mobile/lib/shared/services/immich_logger.service.dart
+++ b/mobile/lib/shared/services/immich_logger.service.dart
@@ -12,7 +12,7 @@ import 'package:share_plus/share_plus.dart';
/// [ImmichLogger] is a custom logger that is built on top of the [logging] package.
/// The logs are written to the database and onto console, using `debugPrint` method.
///
-/// The logs are deleted when exceeding the `maxLogEntries` (default 200) property
+/// The logs are deleted when exceeding the `maxLogEntries` (default 500) property
/// in the class.
///
/// Logs can be shared by calling the `shareLogs` method, which will open a share dialog
@@ -58,6 +58,7 @@ class ImmichLogger {
debugPrint('[${record.level.name}] [${record.time}] ${record.message}');
final lm = LoggerMessage(
message: record.message,
+ details: record.error?.toString(),
level: record.level.toLogLevel(),
createdAt: record.time,
context1: record.loggerName,
diff --git a/mobile/lib/shared/services/share.service.dart b/mobile/lib/shared/services/share.service.dart
index d7daa51b86..be7c0c168d 100644
--- a/mobile/lib/shared/services/share.service.dart
+++ b/mobile/lib/shared/services/share.service.dart
@@ -2,6 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:logging/logging.dart';
@@ -41,7 +42,8 @@ class ShareService {
if (res.statusCode != 200) {
_log.severe(
- "Asset download failed with status - ${res.statusCode} and response - ${res.body}",
+ "Asset download for ${asset.fileName} failed",
+ res.toLoggerString(),
);
continue;
}
@@ -68,7 +70,7 @@ class ShareService {
);
return true;
} catch (error) {
- _log.severe("Share failed with error $error");
+ _log.severe("Share failed", error);
}
return false;
}
diff --git a/mobile/lib/shared/services/sync.service.dart b/mobile/lib/shared/services/sync.service.dart
index d039b34094..a441091d37 100644
--- a/mobile/lib/shared/services/sync.service.dart
+++ b/mobile/lib/shared/services/sync.service.dart
@@ -140,7 +140,7 @@ class SyncService {
try {
await _db.writeTxn(() => a.put(_db));
} on IsarError catch (e) {
- _log.severe("Failed to put new asset into db: $e");
+ _log.severe("Failed to put new asset into db", e);
return false;
}
return true;
@@ -173,7 +173,7 @@ class SyncService {
}
return false;
} on IsarError catch (e) {
- _log.severe("Failed to sync remote assets to db: $e");
+ _log.severe("Failed to sync remote assets to db", e);
}
return null;
}
@@ -232,7 +232,7 @@ class SyncService {
await _db.writeTxn(() => _db.assets.deleteAll(idsToDelete));
await upsertAssetsWithExif(toAdd + toUpdate);
} on IsarError catch (e) {
- _log.severe("Failed to sync remote assets to db: $e");
+ _log.severe("Failed to sync remote assets to db", e);
}
await _updateUserAssetsETag(user, now);
return true;
@@ -364,7 +364,7 @@ class SyncService {
});
_log.info("Synced changes of remote album ${album.name} to DB");
} on IsarError catch (e) {
- _log.severe("Failed to sync remote album to database $e");
+ _log.severe("Failed to sync remote album to database", e);
}
if (album.shared || dto.shared) {
@@ -441,7 +441,7 @@ class SyncService {
assert(ok);
_log.info("Removed local album $album from DB");
} catch (e) {
- _log.severe("Failed to remove local album $album from DB");
+ _log.severe("Failed to remove local album $album from DB", e);
}
}
@@ -577,7 +577,7 @@ class SyncService {
});
_log.info("Synced changes of local album ${ape.name} to DB");
} on IsarError catch (e) {
- _log.severe("Failed to update synced album ${ape.name} in DB: $e");
+ _log.severe("Failed to update synced album ${ape.name} in DB", e);
}
return true;
@@ -623,7 +623,7 @@ class SyncService {
});
_log.info("Fast synced local album ${ape.name} to DB");
} on IsarError catch (e) {
- _log.severe("Failed to fast sync local album ${ape.name} to DB: $e");
+ _log.severe("Failed to fast sync local album ${ape.name} to DB", e);
return false;
}
@@ -656,7 +656,7 @@ class SyncService {
await _db.writeTxn(() => _db.albums.store(a));
_log.info("Added a new local album to DB: ${ape.name}");
} on IsarError catch (e) {
- _log.severe("Failed to add new local album ${ape.name} to DB: $e");
+ _log.severe("Failed to add new local album ${ape.name} to DB", e);
}
}
@@ -706,9 +706,7 @@ class SyncService {
});
_log.info("Upserted ${assets.length} assets into the DB");
} on IsarError catch (e) {
- _log.severe(
- "Failed to upsert ${assets.length} assets into the DB: ${e.toString()}",
- );
+ _log.severe("Failed to upsert ${assets.length} assets into the DB", e);
// give details on the errors
assets.sort(Asset.compareByOwnerChecksum);
final inDb = await _db.assets.getAllByOwnerIdChecksum(
@@ -776,7 +774,7 @@ class SyncService {
});
return true;
} catch (e) {
- _log.severe("Failed to remove all local albums and assets: $e");
+ _log.severe("Failed to remove all local albums and assets", e);
return false;
}
}
diff --git a/mobile/lib/shared/services/user.service.dart b/mobile/lib/shared/services/user.service.dart
index 4d398c3a88..ae65ed31db 100644
--- a/mobile/lib/shared/services/user.service.dart
+++ b/mobile/lib/shared/services/user.service.dart
@@ -42,7 +42,7 @@ class UserService {
final dto = await _apiService.userApi.getAllUsers(isAll);
return dto?.map(User.fromUserDto).toList();
} catch (e) {
- _log.warning("Failed get all users:\n$e");
+ _log.warning("Failed get all users", e);
return null;
}
}
@@ -65,7 +65,7 @@ class UserService {
),
);
} catch (e) {
- _log.warning("Failed to upload profile image:\n$e");
+ _log.warning("Failed to upload profile image", e);
return null;
}
}
diff --git a/mobile/lib/shared/ui/delayed_loading_indicator.dart b/mobile/lib/shared/ui/delayed_loading_indicator.dart
new file mode 100644
index 0000000000..b4d9f4c806
--- /dev/null
+++ b/mobile/lib/shared/ui/delayed_loading_indicator.dart
@@ -0,0 +1,40 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
+
+class DelayedLoadingIndicator extends StatelessWidget {
+ /// The delay to avoid showing the loading indicator
+ final Duration delay;
+
+ /// Defaults to using the [ImmichLoadingIndicator]
+ final Widget? child;
+
+ /// An optional fade in duration to animate the loading
+ final Duration? fadeInDuration;
+
+ const DelayedLoadingIndicator({
+ super.key,
+ this.delay = const Duration(seconds: 3),
+ this.child,
+ this.fadeInDuration,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedSwitcher(
+ duration: fadeInDuration ?? Duration.zero,
+ child: FutureBuilder(
+ future: Future.delayed(delay),
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done) {
+ return child ??
+ const ImmichLoadingIndicator(
+ key: ValueKey('loading'),
+ );
+ }
+
+ return Container(key: const ValueKey('hiding'));
+ },
+ ),
+ );
+ }
+}
diff --git a/mobile/lib/shared/ui/fade_in_placeholder_image.dart b/mobile/lib/shared/ui/fade_in_placeholder_image.dart
new file mode 100644
index 0000000000..e0620ea4f0
--- /dev/null
+++ b/mobile/lib/shared/ui/fade_in_placeholder_image.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/shared/ui/transparent_image.dart';
+
+class FadeInPlaceholderImage extends StatelessWidget {
+ final Widget placeholder;
+ final ImageProvider image;
+ final Duration duration;
+ final BoxFit fit;
+
+ const FadeInPlaceholderImage({
+ super.key,
+ required this.placeholder,
+ required this.image,
+ this.duration = const Duration(milliseconds: 100),
+ this.fit = BoxFit.cover,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox.expand(
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ placeholder,
+ FadeInImage(
+ fadeInDuration: duration,
+ image: image,
+ fit: fit,
+ placeholder: MemoryImage(kTransparentImage),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/mobile/lib/shared/ui/hooks/blurhash_hook.dart b/mobile/lib/shared/ui/hooks/blurhash_hook.dart
new file mode 100644
index 0000000000..24b3c25e13
--- /dev/null
+++ b/mobile/lib/shared/ui/hooks/blurhash_hook.dart
@@ -0,0 +1,17 @@
+import 'dart:convert';
+import 'dart:typed_data';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:thumbhash/thumbhash.dart' as thumbhash;
+
+ObjectRef useBlurHashRef(Asset? asset) {
+ if (asset?.thumbhash == null) {
+ return useRef(null);
+ }
+
+ final rbga = thumbhash.thumbHashToRGBA(
+ base64Decode(asset!.thumbhash!),
+ );
+
+ return useRef(thumbhash.rgbaToBmp(rbga));
+}
diff --git a/mobile/lib/shared/ui/immich_image.dart b/mobile/lib/shared/ui/immich_image.dart
index 280f7de170..3137f63014 100644
--- a/mobile/lib/shared/ui/immich_image.dart
+++ b/mobile/lib/shared/ui/immich_image.dart
@@ -1,5 +1,3 @@
-import 'dart:math';
-
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -9,8 +7,6 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.d
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:octo_image/octo_image.dart';
-import 'package:photo_manager/photo_manager.dart';
-import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
class ImmichImage extends StatelessWidget {
const ImmichImage(
@@ -19,8 +15,6 @@ class ImmichImage extends StatelessWidget {
this.height,
this.fit = BoxFit.cover,
this.placeholder = const ThumbnailPlaceholder(),
- this.isThumbnail = false,
- this.thumbnailSize = 250,
super.key,
});
@@ -29,32 +23,6 @@ class ImmichImage extends StatelessWidget {
final double? width;
final double? height;
final BoxFit fit;
- final bool isThumbnail;
- final int thumbnailSize;
-
- /// Factory constructor to use the thumbnail variant
- factory ImmichImage.thumbnail(
- Asset? asset, {
- BoxFit fit = BoxFit.cover,
- double? width,
- double? height,
- }) {
- // Use the width and height to derive thumbnail size
- final thumbnailSize = max(width ?? 250, height ?? 250).toInt();
-
- return ImmichImage(
- asset,
- isThumbnail: true,
- fit: fit,
- width: width,
- height: height,
- placeholder: ThumbnailPlaceholder(
- height: thumbnailSize.toDouble(),
- width: thumbnailSize.toDouble(),
- ),
- thumbnailSize: thumbnailSize,
- );
- }
// Helper function to return the image provider for the asset
// either by using the asset ID or the asset itself
@@ -66,8 +34,6 @@ class ImmichImage extends StatelessWidget {
static ImageProvider imageProvider({
Asset? asset,
String? assetId,
- bool isThumbnail = false,
- int thumbnailSize = 250,
}) {
if (asset == null && assetId == null) {
throw Exception('Must supply either asset or assetId');
@@ -76,24 +42,18 @@ class ImmichImage extends StatelessWidget {
if (asset == null) {
return ImmichRemoteImageProvider(
assetId: assetId!,
- isThumbnail: isThumbnail,
+ isThumbnail: false,
);
}
- if (useLocal(asset) && isThumbnail) {
- return AssetEntityImageProvider(
- asset.local!,
- isOriginal: false,
- thumbnailSize: ThumbnailSize.square(thumbnailSize),
- );
- } else if (useLocal(asset) && !isThumbnail) {
+ if (useLocal(asset)) {
return ImmichLocalImageProvider(
asset: asset,
);
} else {
return ImmichRemoteImageProvider(
assetId: asset.remoteId!,
- isThumbnail: isThumbnail,
+ isThumbnail: false,
);
}
}
@@ -105,15 +65,11 @@ class ImmichImage extends StatelessWidget {
Widget build(BuildContext context) {
if (asset == null) {
return Container(
- decoration: const BoxDecoration(
- color: Colors.grey,
- ),
- child: SizedBox(
- width: width,
- height: height,
- child: const Center(
- child: Icon(Icons.no_photography),
- ),
+ color: Colors.grey,
+ width: width,
+ height: height,
+ child: const Center(
+ child: Icon(Icons.no_photography),
),
);
}
@@ -131,7 +87,6 @@ class ImmichImage extends StatelessWidget {
},
image: ImmichImage.imageProvider(
asset: asset,
- isThumbnail: isThumbnail,
),
width: width,
height: height,
diff --git a/mobile/lib/shared/ui/immich_thumbnail.dart b/mobile/lib/shared/ui/immich_thumbnail.dart
new file mode 100644
index 0000000000..fe35bdaac2
--- /dev/null
+++ b/mobile/lib/shared/ui/immich_thumbnail.dart
@@ -0,0 +1,89 @@
+import 'dart:typed_data';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_thumbnail_provider.dart';
+import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
+import 'package:immich_mobile/shared/ui/thumbhash_placeholder.dart';
+import 'package:octo_image/octo_image.dart';
+
+class ImmichThumbnail extends HookWidget {
+ const ImmichThumbnail({
+ this.asset,
+ this.width = 250,
+ this.height = 250,
+ this.fit = BoxFit.cover,
+ super.key,
+ });
+
+ final Asset? asset;
+ final double width;
+ final double height;
+ final BoxFit fit;
+
+ /// Helper function to return the image provider for the asset thumbnail
+ /// either by using the asset ID or the asset itself
+ /// [asset] is the Asset to request, or else use [assetId] to get a remote
+ /// image provider
+ static ImageProvider imageProvider({
+ Asset? asset,
+ String? assetId,
+ int thumbnailSize = 256,
+ }) {
+ if (asset == null && assetId == null) {
+ throw Exception('Must supply either asset or assetId');
+ }
+
+ if (asset == null) {
+ return ImmichRemoteImageProvider(
+ assetId: assetId!,
+ isThumbnail: true,
+ );
+ }
+
+ if (useLocal(asset)) {
+ return ImmichLocalThumbnailProvider(
+ asset: asset,
+ height: thumbnailSize,
+ width: thumbnailSize,
+ );
+ } else {
+ return ImmichRemoteImageProvider(
+ assetId: asset.remoteId!,
+ isThumbnail: true,
+ );
+ }
+ }
+
+ static bool useLocal(Asset asset) => !asset.isRemote || asset.isLocal;
+
+ @override
+ Widget build(BuildContext context) {
+ Uint8List? blurhash = useBlurHashRef(asset).value;
+ if (asset == null) {
+ return Container(
+ color: Colors.grey,
+ width: width,
+ height: height,
+ child: const Center(
+ child: Icon(Icons.no_photography),
+ ),
+ );
+ }
+
+ return OctoImage.fromSet(
+ placeholderFadeInDuration: Duration.zero,
+ fadeInDuration: Duration.zero,
+ fadeOutDuration: const Duration(milliseconds: 100),
+ octoSet: blurHashOrPlaceholder(blurhash),
+ image: ImmichThumbnail.imageProvider(
+ asset: asset,
+ ),
+ width: width,
+ height: height,
+ fit: fit,
+ );
+ }
+}
diff --git a/mobile/lib/shared/ui/thumbhash_placeholder.dart b/mobile/lib/shared/ui/thumbhash_placeholder.dart
new file mode 100644
index 0000000000..0ec64d3760
--- /dev/null
+++ b/mobile/lib/shared/ui/thumbhash_placeholder.dart
@@ -0,0 +1,48 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
+import 'package:immich_mobile/shared/ui/fade_in_placeholder_image.dart';
+import 'package:octo_image/octo_image.dart';
+
+/// Simple set to show [OctoPlaceholder.circularProgressIndicator] as
+/// placeholder and [OctoError.icon] as error.
+OctoSet blurHashOrPlaceholder(
+ Uint8List? blurhash, {
+ BoxFit? fit,
+ Text? errorMessage,
+}) {
+ return OctoSet(
+ placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit),
+ errorBuilder: blurHashErrorBuilder(blurhash, fit: fit),
+ );
+}
+
+OctoPlaceholderBuilder blurHashPlaceholderBuilder(
+ Uint8List? blurhash, {
+ BoxFit? fit,
+}) {
+ return (context) => blurhash == null
+ ? const ThumbnailPlaceholder()
+ : FadeInPlaceholderImage(
+ placeholder: const ThumbnailPlaceholder(),
+ image: MemoryImage(blurhash),
+ fit: fit ?? BoxFit.cover,
+ );
+}
+
+OctoErrorBuilder blurHashErrorBuilder(
+ Uint8List? blurhash, {
+ BoxFit? fit,
+ Text? message,
+ IconData? icon,
+ Color? iconColor,
+ double? iconSize,
+}) {
+ return OctoError.placeholderWithErrorIcon(
+ blurHashPlaceholderBuilder(blurhash, fit: fit),
+ message: message,
+ icon: icon,
+ iconColor: iconColor,
+ iconSize: iconSize,
+ );
+}
diff --git a/mobile/lib/shared/views/app_log_detail_page.dart b/mobile/lib/shared/views/app_log_detail_page.dart
index 126f46c8ff..6b99d7f0af 100644
--- a/mobile/lib/shared/views/app_log_detail_page.dart
+++ b/mobile/lib/shared/views/app_log_detail_page.dart
@@ -15,7 +15,7 @@ class AppLogDetailPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
var isDarkTheme = context.isDarkTheme;
- buildStackMessage(String stackTrace) {
+ buildTextWithCopyButton(String header, String text) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
@@ -28,7 +28,7 @@ class AppLogDetailPage extends HookConsumerWidget {
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
- "STACK TRACES",
+ header,
style: TextStyle(
fontSize: 12.0,
color: context.primaryColor,
@@ -38,8 +38,7 @@ class AppLogDetailPage extends HookConsumerWidget {
),
IconButton(
onPressed: () {
- Clipboard.setData(ClipboardData(text: stackTrace))
- .then((_) {
+ Clipboard.setData(ClipboardData(text: text)).then((_) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
@@ -68,73 +67,7 @@ class AppLogDetailPage extends HookConsumerWidget {
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SelectableText(
- stackTrace,
- style: const TextStyle(
- fontSize: 12.0,
- fontWeight: FontWeight.bold,
- fontFamily: "Inconsolata",
- ),
- ),
- ),
- ),
- ],
- ),
- );
- }
-
- buildLogMessage(String message) {
- return Padding(
- padding: const EdgeInsets.all(8.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Padding(
- padding: const EdgeInsets.only(bottom: 8.0),
- child: Text(
- "MESSAGE",
- style: TextStyle(
- fontSize: 12.0,
- color: context.primaryColor,
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- IconButton(
- onPressed: () {
- Clipboard.setData(ClipboardData(text: message)).then((_) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(
- "Copied to clipboard",
- style: context.textTheme.bodyLarge?.copyWith(
- color: context.primaryColor,
- ),
- ),
- ),
- );
- });
- },
- icon: Icon(
- Icons.copy,
- size: 16.0,
- color: context.primaryColor,
- ),
- ),
- ],
- ),
- Container(
- decoration: BoxDecoration(
- color: isDarkTheme ? Colors.grey[900] : Colors.grey[200],
- borderRadius: BorderRadius.circular(15.0),
- ),
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: SelectableText(
- message,
+ text,
style: const TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
@@ -194,11 +127,16 @@ class AppLogDetailPage extends HookConsumerWidget {
body: SafeArea(
child: ListView(
children: [
- buildLogMessage(logMessage.message),
+ buildTextWithCopyButton("MESSAGE", logMessage.message),
+ if (logMessage.details != null)
+ buildTextWithCopyButton("DETAILS", logMessage.details.toString()),
if (logMessage.context1 != null)
buildLogContext1(logMessage.context1.toString()),
if (logMessage.context2 != null)
- buildStackMessage(logMessage.context2.toString()),
+ buildTextWithCopyButton(
+ "STACK TRACE",
+ logMessage.context2.toString(),
+ ),
],
),
),
diff --git a/mobile/lib/shared/views/app_log_page.dart b/mobile/lib/shared/views/app_log_page.dart
index a0c4553f98..993b25c7cf 100644
--- a/mobile/lib/shared/views/app_log_page.dart
+++ b/mobile/lib/shared/views/app_log_page.dart
@@ -69,9 +69,9 @@ class AppLogPage extends HookConsumerWidget {
return Scaffold(
appBar: AppBar(
- title: Text(
- "Logs - ${logMessages.value.length}",
- style: const TextStyle(
+ title: const Text(
+ "Logs",
+ style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
@@ -135,29 +135,15 @@ class AppLogPage extends HookConsumerWidget {
dense: true,
tileColor: getTileColor(logMessage.level),
minLeadingWidth: 10,
- title: Text.rich(
- TextSpan(
- children: [
- TextSpan(
- text: "#$index ",
- style: TextStyle(
- color: isDarkTheme ? Colors.white70 : Colors.grey[600],
- fontSize: 14.0,
- fontWeight: FontWeight.bold,
- ),
- ),
- TextSpan(
- text: truncateLogMessage(logMessage.message, 4),
- style: const TextStyle(
- fontSize: 14.0,
- ),
- ),
- ],
+ title: Text(
+ truncateLogMessage(logMessage.message, 4),
+ style: const TextStyle(
+ fontSize: 14.0,
+ fontFamily: "Inconsolata",
),
- style: const TextStyle(fontSize: 14.0, fontFamily: "Inconsolata"),
),
subtitle: Text(
- "[${logMessage.context1}] Logged on ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)}",
+ "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}",
style: TextStyle(
fontSize: 12.0,
color: Colors.grey[600],
diff --git a/mobile/lib/shared/views/immich_loading_overlay.dart b/mobile/lib/shared/views/immich_loading_overlay.dart
index 85f0123ed9..c600d2a724 100644
--- a/mobile/lib/shared/views/immich_loading_overlay.dart
+++ b/mobile/lib/shared/views/immich_loading_overlay.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
+import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart';
final _loadingEntry = OverlayEntry(
builder: (context) => SizedBox.square(
@@ -9,7 +9,12 @@ final _loadingEntry = OverlayEntry(
child: DecoratedBox(
decoration:
BoxDecoration(color: context.colorScheme.surface.withAlpha(200)),
- child: const Center(child: ImmichLoadingIndicator()),
+ child: const Center(
+ child: DelayedLoadingIndicator(
+ delay: Duration(seconds: 1),
+ fadeInDuration: Duration(milliseconds: 400),
+ ),
+ ),
),
),
);
@@ -27,19 +32,19 @@ class _LoadingOverlay extends Hook> {
class _LoadingOverlayState
extends HookState, _LoadingOverlay> {
- late final _isProcessing = ValueNotifier(false)..addListener(_listener);
- OverlayEntry? overlayEntry;
+ late final _isLoading = ValueNotifier(false)..addListener(_listener);
+ OverlayEntry? _loadingOverlay;
void _listener() {
setState(() {
WidgetsBinding.instance.addPostFrameCallback((_) {
- if (_isProcessing.value) {
- overlayEntry?.remove();
- overlayEntry = _loadingEntry;
+ if (_isLoading.value) {
+ _loadingOverlay?.remove();
+ _loadingOverlay = _loadingEntry;
Overlay.of(context).insert(_loadingEntry);
} else {
- overlayEntry?.remove();
- overlayEntry = null;
+ _loadingOverlay?.remove();
+ _loadingOverlay = null;
}
});
});
@@ -47,17 +52,17 @@ class _LoadingOverlayState
@override
ValueNotifier build(BuildContext context) {
- return _isProcessing;
+ return _isLoading;
}
@override
void dispose() {
- _isProcessing.dispose();
+ _isLoading.dispose();
super.dispose();
}
@override
- Object? get debugValue => _isProcessing.value;
+ Object? get debugValue => _isLoading.value;
@override
String get debugLabel => 'useProcessingOverlay<>';
diff --git a/mobile/lib/shared/views/splash_screen.dart b/mobile/lib/shared/views/splash_screen.dart
index 8dddb60aaa..3c0d65bde9 100644
--- a/mobile/lib/shared/views/splash_screen.dart
+++ b/mobile/lib/shared/views/splash_screen.dart
@@ -35,10 +35,10 @@ class SplashScreenPage extends HookConsumerWidget {
deviceIsOffline = true;
log.fine("Device seems to be offline upon launch");
} else {
- log.severe(e);
+ log.severe("Failed to resolve endpoint", e);
}
} catch (e) {
- log.severe(e);
+ log.severe("Failed to resolve endpoint", e);
}
try {
@@ -53,7 +53,7 @@ class SplashScreenPage extends HookConsumerWidget {
ref.read(authenticationProvider.notifier).logout();
log.severe(
- 'Cannot set success login info: $error',
+ 'Cannot set success login info',
error,
stackTrace,
);
diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES
index 0679a1749d..ea413b4870 100644
--- a/mobile/openapi/.openapi-generator/FILES
+++ b/mobile/openapi/.openapi-generator/FILES
@@ -108,6 +108,7 @@ doc/PersonResponseDto.md
doc/PersonStatisticsResponseDto.md
doc/PersonUpdateDto.md
doc/PersonWithFacesResponseDto.md
+doc/PlacesResponseDto.md
doc/QueueStatusDto.md
doc/ReactionLevel.md
doc/ReactionType.md
@@ -308,6 +309,7 @@ lib/model/person_response_dto.dart
lib/model/person_statistics_response_dto.dart
lib/model/person_update_dto.dart
lib/model/person_with_faces_response_dto.dart
+lib/model/places_response_dto.dart
lib/model/queue_status_dto.dart
lib/model/reaction_level.dart
lib/model/reaction_type.dart
@@ -485,6 +487,7 @@ test/person_response_dto_test.dart
test/person_statistics_response_dto_test.dart
test/person_update_dto_test.dart
test/person_with_faces_response_dto_test.dart
+test/places_response_dto_test.dart
test/queue_status_dto_test.dart
test/reaction_level_test.dart
test/reaction_type_test.dart
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 7bda432fe5..71b2c6056c 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -166,6 +166,7 @@ Class | Method | HTTP request | Description
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
*SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **POST** /search/metadata |
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
+*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |
*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
@@ -306,6 +307,7 @@ Class | Method | HTTP request | Description
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
- [PersonUpdateDto](doc//PersonUpdateDto.md)
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
+ - [PlacesResponseDto](doc//PlacesResponseDto.md)
- [QueueStatusDto](doc//QueueStatusDto.md)
- [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md)
diff --git a/mobile/openapi/doc/PlacesResponseDto.md b/mobile/openapi/doc/PlacesResponseDto.md
new file mode 100644
index 0000000000..a4bf36493c
--- /dev/null
+++ b/mobile/openapi/doc/PlacesResponseDto.md
@@ -0,0 +1,19 @@
+# openapi.model.PlacesResponseDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**admin1name** | **String** | | [optional]
+**admin2name** | **String** | | [optional]
+**latitude** | **num** | |
+**longitude** | **num** | |
+**name** | **String** | |
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+
diff --git a/mobile/openapi/doc/SearchApi.md b/mobile/openapi/doc/SearchApi.md
index f975e94484..f63488222b 100644
--- a/mobile/openapi/doc/SearchApi.md
+++ b/mobile/openapi/doc/SearchApi.md
@@ -14,6 +14,7 @@ Method | HTTP request | Description
[**search**](SearchApi.md#search) | **GET** /search |
[**searchMetadata**](SearchApi.md#searchmetadata) | **POST** /search/metadata |
[**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person |
+[**searchPlaces**](SearchApi.md#searchplaces) | **GET** /search/places |
[**searchSmart**](SearchApi.md#searchsmart) | **POST** /search/smart |
@@ -316,6 +317,61 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+# **searchPlaces**
+> List searchPlaces(name)
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// TODO Configure API key authorization: cookie
+//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer';
+// TODO Configure API key authorization: api_key
+//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY';
+// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
+//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer';
+// TODO Configure HTTP Bearer authorization: bearer
+// Case 1. Use String Token
+//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
+// Case 2. Use Function which generate token.
+// String yourTokenGeneratorFunction() { ... }
+//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction);
+
+final api_instance = SearchApi();
+final name = name_example; // String |
+
+try {
+ final result = api_instance.searchPlaces(name);
+ print(result);
+} catch (e) {
+ print('Exception when calling SearchApi->searchPlaces: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+ **name** | **String**| |
+
+### Return type
+
+[**List**](PlacesResponseDto.md)
+
+### Authorization
+
+[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: Not defined
+ - **Accept**: application/json
+
+[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
+
# **searchSmart**
> SearchResponseDto searchSmart(smartSearchDto)
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 72a6567648..56bd907e0a 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -142,6 +142,7 @@ part 'model/person_response_dto.dart';
part 'model/person_statistics_response_dto.dart';
part 'model/person_update_dto.dart';
part 'model/person_with_faces_response_dto.dart';
+part 'model/places_response_dto.dart';
part 'model/queue_status_dto.dart';
part 'model/reaction_level.dart';
part 'model/reaction_type.dart';
diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart
index 062ca4a50b..3a0bc56bb6 100644
--- a/mobile/openapi/lib/api/search_api.dart
+++ b/mobile/openapi/lib/api/search_api.dart
@@ -360,6 +360,58 @@ class SearchApi {
return null;
}
+ /// Performs an HTTP 'GET /search/places' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [String] name (required):
+ Future searchPlacesWithHttpInfo(String name,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/search/places';
+
+ // ignore: prefer_final_locals
+ Object? postBody;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ queryParams.addAll(_queryParams('', 'name', name));
+
+ const contentTypes = [];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'GET',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [String] name (required):
+ Future?> searchPlaces(String name,) async {
+ final response = await searchPlacesWithHttpInfo(name,);
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ // When a remote server returns no body with a status of 204, we shall not decode it.
+ // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+ // FormatException when trying to decode an empty string.
+ if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+ final responseBody = await _decodeBodyBytes(response);
+ return (await apiClient.deserializeAsync(responseBody, 'List') as List)
+ .cast()
+ .toList(growable: false);
+
+ }
+ return null;
+ }
+
/// Performs an HTTP 'POST /search/smart' operation and returns the [Response].
/// Parameters:
///
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index 2df5e67119..24cffb7cff 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -366,6 +366,8 @@ class ApiClient {
return PersonUpdateDto.fromJson(value);
case 'PersonWithFacesResponseDto':
return PersonWithFacesResponseDto.fromJson(value);
+ case 'PlacesResponseDto':
+ return PlacesResponseDto.fromJson(value);
case 'QueueStatusDto':
return QueueStatusDto.fromJson(value);
case 'ReactionLevel':
diff --git a/mobile/openapi/lib/model/places_response_dto.dart b/mobile/openapi/lib/model/places_response_dto.dart
new file mode 100644
index 0000000000..a2d8378883
--- /dev/null
+++ b/mobile/openapi/lib/model/places_response_dto.dart
@@ -0,0 +1,148 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class PlacesResponseDto {
+ /// Returns a new [PlacesResponseDto] instance.
+ PlacesResponseDto({
+ this.admin1name,
+ this.admin2name,
+ required this.latitude,
+ required this.longitude,
+ required this.name,
+ });
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ String? admin1name;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ String? admin2name;
+
+ num latitude;
+
+ num longitude;
+
+ String name;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is PlacesResponseDto &&
+ other.admin1name == admin1name &&
+ other.admin2name == admin2name &&
+ other.latitude == latitude &&
+ other.longitude == longitude &&
+ other.name == name;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (admin1name == null ? 0 : admin1name!.hashCode) +
+ (admin2name == null ? 0 : admin2name!.hashCode) +
+ (latitude.hashCode) +
+ (longitude.hashCode) +
+ (name.hashCode);
+
+ @override
+ String toString() => 'PlacesResponseDto[admin1name=$admin1name, admin2name=$admin2name, latitude=$latitude, longitude=$longitude, name=$name]';
+
+ Map toJson() {
+ final json = {};
+ if (this.admin1name != null) {
+ json[r'admin1name'] = this.admin1name;
+ } else {
+ // json[r'admin1name'] = null;
+ }
+ if (this.admin2name != null) {
+ json[r'admin2name'] = this.admin2name;
+ } else {
+ // json[r'admin2name'] = null;
+ }
+ json[r'latitude'] = this.latitude;
+ json[r'longitude'] = this.longitude;
+ json[r'name'] = this.name;
+ return json;
+ }
+
+ /// Returns a new [PlacesResponseDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static PlacesResponseDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return PlacesResponseDto(
+ admin1name: mapValueOfType(json, r'admin1name'),
+ admin2name: mapValueOfType(json, r'admin2name'),
+ latitude: num.parse('${json[r'latitude']}'),
+ longitude: num.parse('${json[r'longitude']}'),
+ name: mapValueOfType(json, r'name')!,
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = PlacesResponseDto.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = PlacesResponseDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of PlacesResponseDto-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = PlacesResponseDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'latitude',
+ 'longitude',
+ 'name',
+ };
+}
+
diff --git a/mobile/openapi/test/places_response_dto_test.dart b/mobile/openapi/test/places_response_dto_test.dart
new file mode 100644
index 0000000000..5a320fce64
--- /dev/null
+++ b/mobile/openapi/test/places_response_dto_test.dart
@@ -0,0 +1,47 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.12
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+import 'package:openapi/api.dart';
+import 'package:test/test.dart';
+
+// tests for PlacesResponseDto
+void main() {
+ // final instance = PlacesResponseDto();
+
+ group('test PlacesResponseDto', () {
+ // String admin1name
+ test('to test the property `admin1name`', () async {
+ // TODO
+ });
+
+ // String admin2name
+ test('to test the property `admin2name`', () async {
+ // TODO
+ });
+
+ // num latitude
+ test('to test the property `latitude`', () async {
+ // TODO
+ });
+
+ // num longitude
+ test('to test the property `longitude`', () async {
+ // TODO
+ });
+
+ // String name
+ test('to test the property `name`', () async {
+ // TODO
+ });
+
+
+ });
+
+}
diff --git a/mobile/openapi/test/search_api_test.dart b/mobile/openapi/test/search_api_test.dart
index 14169e461d..aa4a94847b 100644
--- a/mobile/openapi/test/search_api_test.dart
+++ b/mobile/openapi/test/search_api_test.dart
@@ -42,6 +42,11 @@ void main() {
// TODO
});
+ //Future> searchPlaces(String name) async
+ test('test searchPlaces', () async {
+ // TODO
+ });
+
//Future searchSmart(SmartSearchDto smartSearchDto) async
test('test searchSmart', () async {
// TODO
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index ffa57f826b..9e379d4653 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -413,10 +413,10 @@ packages:
dependency: transitive
description:
name: file
- sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
+ sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
- version: "6.1.4"
+ version: "7.0.0"
file_selector_linux:
dependency: transitive
description:
@@ -569,10 +569,10 @@ packages:
dependency: "direct main"
description:
name: flutter_udid
- sha256: "666412097b86d9a6f9803073d0f0ba70de9b198fe6493d89d352a1f8cd6c5c84"
+ sha256: "63384bd96203aaefccfd7137fab642edda18afede12b0e9e1a2c96fe2589fd07"
url: "https://pub.dev"
source: hosted
- version: "2.1.1"
+ version: "3.0.0"
flutter_web_auth:
dependency: "direct main"
description:
@@ -619,10 +619,10 @@ packages:
dependency: "direct main"
description:
name: geolocator
- sha256: e946395fc608842bb2f6c914807e9183f86f3cb787f6b8f832753e5251036f02
+ sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd"
url: "https://pub.dev"
source: hosted
- version: "10.1.0"
+ version: "11.0.0"
geolocator_android:
dependency: transitive
description:
@@ -651,10 +651,10 @@ packages:
dependency: transitive
description:
name: geolocator_web
- sha256: "59083f7e0871b78299918d92bf930a14377f711d2d1156c558cd5ebae6c20d58"
+ sha256: "49d8f846ebeb5e2b6641fe477a7e97e5dd73f03cbfef3fd5c42177b7300fb0ed"
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "3.0.0"
geolocator_windows:
dependency: transitive
description:
@@ -860,6 +860,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.8.1"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.0.0"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.1"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.1"
lints:
dependency: transitive
description:
@@ -907,18 +931,18 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
- version: "0.12.16"
+ version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
+ sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
- version: "0.5.0"
+ version: "0.8.0"
meta:
dependency: "direct overridden"
description:
@@ -1002,10 +1026,10 @@ packages:
dependency: "direct main"
description:
name: path
- sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
- version: "1.8.3"
+ version: "1.9.0"
path_provider:
dependency: "direct main"
description:
@@ -1138,10 +1162,10 @@ packages:
dependency: transitive
description:
name: platform
- sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
+ sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
- version: "3.1.2"
+ version: "3.1.4"
plugin_platform_interface:
dependency: transitive
description:
@@ -1170,10 +1194,10 @@ packages:
dependency: transitive
description:
name: process
- sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
+ sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
url: "https://pub.dev"
source: hosted
- version: "4.2.4"
+ version: "5.0.2"
provider:
dependency: transitive
description:
@@ -1298,10 +1322,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_linux
- sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1"
+ sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.dev"
source: hosted
- version: "2.3.0"
+ version: "2.3.2"
shared_preferences_platform_interface:
dependency: transitive
description:
@@ -1322,10 +1346,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_windows
- sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d
+ sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.dev"
source: hosted
- version: "2.3.0"
+ version: "2.3.2"
shelf:
dependency: transitive
description:
@@ -1467,6 +1491,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.6.1"
+ thumbhash:
+ dependency: "direct main"
+ description:
+ name: thumbhash
+ sha256: "5f6d31c5279ca0b5caa81ec10aae8dcaab098d82cb699ea66ada4ed09c794a37"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.0+1"
time:
dependency: transitive
description:
@@ -1631,10 +1663,10 @@ packages:
dependency: transitive
description:
name: vm_service
- sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
+ sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://pub.dev"
source: hosted
- version: "11.10.0"
+ version: "13.0.0"
wakelock_plus:
dependency: "direct main"
description:
@@ -1679,10 +1711,10 @@ packages:
dependency: transitive
description:
name: webdriver
- sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49"
+ sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
url: "https://pub.dev"
source: hosted
- version: "3.0.2"
+ version: "3.0.3"
win32:
dependency: transitive
description:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 47a4d3805e..50d170904f 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -32,8 +32,8 @@ dependencies:
git:
url: https://github.com/maplibre/flutter-maplibre-gl.git
ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
- geolocator: ^10.1.0 # used to move to current location in map view
- flutter_udid: ^2.1.1
+ geolocator: ^11.0.0 # used to move to current location in map view
+ flutter_udid: ^3.0.0
package_info_plus: ^5.0.1
url_launcher: ^6.2.4
http: 0.13.5
@@ -57,6 +57,7 @@ dependencies:
flutter_local_notifications: ^16.3.2
timezone: ^0.9.2
octo_image: ^2.0.0
+ thumbhash: 0.1.0+1
openapi:
path: openapi
diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json
index 287daa69ad..c08fa73d3c 100644
--- a/open-api/immich-openapi-specs.json
+++ b/open-api/immich-openapi-specs.json
@@ -4700,6 +4700,50 @@
]
}
},
+ "/search/places": {
+ "get": {
+ "operationId": "searchPlaces",
+ "parameters": [
+ {
+ "name": "name",
+ "required": true,
+ "in": "query",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "items": {
+ "$ref": "#/components/schemas/PlacesResponseDto"
+ },
+ "type": "array"
+ }
+ }
+ },
+ "description": ""
+ }
+ },
+ "security": [
+ {
+ "bearer": []
+ },
+ {
+ "cookie": []
+ },
+ {
+ "api_key": []
+ }
+ ],
+ "tags": [
+ "Search"
+ ]
+ }
+ },
"/search/smart": {
"post": {
"operationId": "searchSmart",
@@ -8760,6 +8804,31 @@
],
"type": "object"
},
+ "PlacesResponseDto": {
+ "properties": {
+ "admin1name": {
+ "type": "string"
+ },
+ "admin2name": {
+ "type": "string"
+ },
+ "latitude": {
+ "type": "number"
+ },
+ "longitude": {
+ "type": "number"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "latitude",
+ "longitude",
+ "name"
+ ],
+ "type": "object"
+ },
"QueueStatusDto": {
"properties": {
"isActive": {
diff --git a/open-api/typescript-sdk/axios-client/api.ts b/open-api/typescript-sdk/axios-client/api.ts
index dd65064a1b..73b0523f4b 100644
--- a/open-api/typescript-sdk/axios-client/api.ts
+++ b/open-api/typescript-sdk/axios-client/api.ts
@@ -2988,6 +2988,43 @@ export interface PersonWithFacesResponseDto {
*/
'thumbnailPath': string;
}
+/**
+ *
+ * @export
+ * @interface PlacesResponseDto
+ */
+export interface PlacesResponseDto {
+ /**
+ *
+ * @type {string}
+ * @memberof PlacesResponseDto
+ */
+ 'admin1name'?: string;
+ /**
+ *
+ * @type {string}
+ * @memberof PlacesResponseDto
+ */
+ 'admin2name'?: string;
+ /**
+ *
+ * @type {number}
+ * @memberof PlacesResponseDto
+ */
+ 'latitude': number;
+ /**
+ *
+ * @type {number}
+ * @memberof PlacesResponseDto
+ */
+ 'longitude': number;
+ /**
+ *
+ * @type {string}
+ * @memberof PlacesResponseDto
+ */
+ 'name': string;
+}
/**
*
* @export
@@ -15451,6 +15488,51 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
+ setSearchParams(localVarUrlObj, localVarQueryParameter);
+ let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+ localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+ return {
+ url: toPathString(localVarUrlObj),
+ options: localVarRequestOptions,
+ };
+ },
+ /**
+ *
+ * @param {string} name
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ searchPlaces: async (name: string, options: RawAxiosRequestConfig = {}): Promise => {
+ // verify required parameter 'name' is not null or undefined
+ assertParamExists('searchPlaces', 'name', name)
+ const localVarPath = `/search/places`;
+ // use dummy base URL string because the URL constructor only accepts absolute URLs.
+ const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+ let baseOptions;
+ if (configuration) {
+ baseOptions = configuration.baseOptions;
+ }
+
+ const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
+ const localVarHeaderParameter = {} as any;
+ const localVarQueryParameter = {} as any;
+
+ // authentication cookie required
+
+ // authentication api_key required
+ await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+ // authentication bearer required
+ // http bearer authentication required
+ await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+ if (name !== undefined) {
+ localVarQueryParameter['name'] = name;
+ }
+
+
+
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@@ -15588,6 +15670,18 @@ export const SearchApiFp = function(configuration?: Configuration) {
const operationBasePath = operationServerMap['SearchApi.searchPerson']?.[index]?.url;
return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
},
+ /**
+ *
+ * @param {string} name
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ async searchPlaces(name: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> {
+ const localVarAxiosArgs = await localVarAxiosParamCreator.searchPlaces(name, options);
+ const index = configuration?.serverIndex ?? 0;
+ const operationBasePath = operationServerMap['SearchApi.searchPlaces']?.[index]?.url;
+ return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, operationBasePath || basePath);
+ },
/**
*
* @param {SmartSearchDto} smartSearchDto
@@ -15655,6 +15749,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: RawAxiosRequestConfig): AxiosPromise> {
return localVarFp.searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(axios, basePath));
},
+ /**
+ *
+ * @param {SearchApiSearchPlacesRequest} requestParameters Request parameters.
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ */
+ searchPlaces(requestParameters: SearchApiSearchPlacesRequest, options?: RawAxiosRequestConfig): AxiosPromise> {
+ return localVarFp.searchPlaces(requestParameters.name, options).then((request) => request(axios, basePath));
+ },
/**
*
* @param {SearchApiSearchSmartRequest} requestParameters Request parameters.
@@ -15821,6 +15924,20 @@ export interface SearchApiSearchPersonRequest {
readonly withHidden?: boolean
}
+/**
+ * Request parameters for searchPlaces operation in SearchApi.
+ * @export
+ * @interface SearchApiSearchPlacesRequest
+ */
+export interface SearchApiSearchPlacesRequest {
+ /**
+ *
+ * @type {string}
+ * @memberof SearchApiSearchPlaces
+ */
+ readonly name: string
+}
+
/**
* Request parameters for searchSmart operation in SearchApi.
* @export
@@ -15897,6 +16014,17 @@ export class SearchApi extends BaseAPI {
return SearchApiFp(this.configuration).searchPerson(requestParameters.name, requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath));
}
+ /**
+ *
+ * @param {SearchApiSearchPlacesRequest} requestParameters Request parameters.
+ * @param {*} [options] Override http request option.
+ * @throws {RequiredError}
+ * @memberof SearchApi
+ */
+ public searchPlaces(requestParameters: SearchApiSearchPlacesRequest, options?: RawAxiosRequestConfig) {
+ return SearchApiFp(this.configuration).searchPlaces(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
+ }
+
/**
*
* @param {SearchApiSearchSmartRequest} requestParameters Request parameters.
diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts
index 50dc680e1a..7712e58990 100644
--- a/open-api/typescript-sdk/fetch-client.ts
+++ b/open-api/typescript-sdk/fetch-client.ts
@@ -645,6 +645,13 @@ export type MetadataSearchDto = {
withPeople?: boolean;
withStacked?: boolean;
};
+export type PlacesResponseDto = {
+ admin1name?: string;
+ admin2name?: string;
+ latitude: number;
+ longitude: number;
+ name: string;
+};
export type SmartSearchDto = {
city?: string;
country?: string;
@@ -2199,6 +2206,18 @@ export function searchPerson({ name, withHidden }: {
...opts
}));
}
+export function searchPlaces({ name }: {
+ name: string;
+}, opts?: Oazapfts.RequestOpts) {
+ return oazapfts.ok(oazapfts.fetchJson<{
+ status: 200;
+ data: PlacesResponseDto[];
+ }>(`/search/places${QS.query(QS.explode({
+ name
+ }))}`, {
+ ...opts
+ }));
+}
export function searchSmart({ smartSearchDto }: {
smartSearchDto: SmartSearchDto;
}, opts?: Oazapfts.RequestOpts) {
diff --git a/open-api/typescript-sdk/fetch-errors.ts b/open-api/typescript-sdk/fetch-errors.ts
new file mode 100644
index 0000000000..f21f0ed1c4
--- /dev/null
+++ b/open-api/typescript-sdk/fetch-errors.ts
@@ -0,0 +1,15 @@
+import { HttpError } from '@oazapfts/runtime';
+
+export interface ApiExceptionResponse {
+ message: string;
+ error?: string;
+ statusCode: number;
+}
+
+export interface ApiHttpError extends HttpError {
+ data: ApiExceptionResponse;
+}
+
+export function isHttpError(error: unknown): error is ApiHttpError {
+ return error instanceof HttpError;
+}
diff --git a/open-api/typescript-sdk/fetch.ts b/open-api/typescript-sdk/fetch.ts
index 5441cd8268..5759e66ad9 100644
--- a/open-api/typescript-sdk/fetch.ts
+++ b/open-api/typescript-sdk/fetch.ts
@@ -1 +1,2 @@
export * from './fetch-client';
+export * from './fetch-errors';
diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json
index a918e2d2c9..3359297e26 100644
--- a/open-api/typescript-sdk/package-lock.json
+++ b/open-api/typescript-sdk/package-lock.json
@@ -23,15 +23,15 @@
}
},
"node_modules/@oazapfts/runtime": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.0.tgz",
- "integrity": "sha512-1ovqeaeEvShbYge5/7ctJokpvqB0anBdfDNfU5jWstjV2/Gbe+vvcBM274Z0abM3IM0b9MmSNWYBXnJXYO8KCw==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@oazapfts/runtime/-/runtime-1.0.1.tgz",
+ "integrity": "sha512-CMl7f1gXYpjIyEtDhg4YfXwr2MXfbadbvqwKbMsaHkVtSglmuz5A8jSyefTqaJlmh0MOA2ZNS9jnbfIdtcoDiw==",
"dev": true
},
"node_modules/@types/node": {
- "version": "20.11.19",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
- "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
+ "version": "20.11.20",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
+ "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
diff --git a/server/Dockerfile b/server/Dockerfile
index 7ea2795ea7..0ebd5c44cb 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -1,5 +1,5 @@
# dev build
-FROM ghcr.io/immich-app/base-server-dev:20240222@sha256:2ff467d6ae5c00a2317eb7b13cb40ba5be0fd33c160175dba621b1bf72bc1cd1 as dev
+FROM ghcr.io/immich-app/base-server-dev:20240227@sha256:b1e212c106ce2318a587e0b2ef377215c958e877f61993ed9310534e4589cce4 as dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@@ -40,7 +40,7 @@ RUN npm run build
# prod build
-FROM ghcr.io/immich-app/base-server-prod:20240222@sha256:9ae5eebf95cf7759eec9dcfbd9e48a722701075ac855209f2e0b01c631b76f5c
+FROM ghcr.io/immich-app/base-server-prod:20240227@sha256:d47f5f7f2b6c53957c6353352b2fa24f2845da50e6491a7c74eb779ace10628c
WORKDIR /usr/src/app
ENV NODE_ENV=production \
diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts
index 0e09a68be5..d869775c98 100644
--- a/server/e2e/api/specs/asset.e2e-spec.ts
+++ b/server/e2e/api/specs/asset.e2e-spec.ts
@@ -538,90 +538,6 @@ describe(`${AssetController.name} (e2e)`, () => {
}
});
- describe('GET /asset/:id', () => {
- it('should require authentication', async () => {
- const { status, body } = await request(server).get(`/asset/${uuidStub.notFound}`);
- expect(body).toEqual(errorStub.unauthorized);
- expect(status).toBe(401);
- });
-
- it('should require a valid id', async () => {
- const { status, body } = await request(server)
- .get(`/asset/${uuidStub.invalid}`)
- .set('Authorization', `Bearer ${user1.accessToken}`);
- expect(status).toBe(400);
- expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
- });
-
- it('should require access', async () => {
- const { status, body } = await request(server)
- .get(`/asset/${asset4.id}`)
- .set('Authorization', `Bearer ${user1.accessToken}`);
- expect(status).toBe(400);
- expect(body).toEqual(errorStub.noPermission);
- });
-
- it('should get the asset info', async () => {
- const { status, body } = await request(server)
- .get(`/asset/${asset1.id}`)
- .set('Authorization', `Bearer ${user1.accessToken}`);
- expect(status).toBe(200);
- expect(body).toMatchObject({ id: asset1.id });
- });
-
- it('should work with a shared link', async () => {
- const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
- type: SharedLinkType.INDIVIDUAL,
- assetIds: [asset1.id],
- });
-
- const { status, body } = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`);
- expect(status).toBe(200);
- expect(body).toMatchObject({ id: asset1.id });
- });
-
- it('should not send people data for shared links for un-authenticated users', async () => {
- const personRepository = app.get(IPersonRepository);
- const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
-
- await personRepository.createFaces([
- {
- assetId: asset1.id,
- personId: person.id,
- embedding: Array.from({ length: 512 }, Math.random),
- },
- ]);
-
- const { status, body } = await request(server)
- .put(`/asset/${asset1.id}`)
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .send({ isFavorite: true });
- expect(status).toEqual(200);
- expect(body).toMatchObject({
- id: asset1.id,
- isFavorite: true,
- people: [
- {
- birthDate: null,
- id: expect.any(String),
- isHidden: false,
- name: 'Test Person',
- thumbnailPath: '',
- },
- ],
- });
-
- const sharedLink = await api.sharedLinkApi.create(server, user1.accessToken, {
- type: SharedLinkType.INDIVIDUAL,
- assetIds: [asset1.id],
- });
-
- const data = await request(server).get(`/asset/${asset1.id}?key=${sharedLink.key}`);
- expect(data.status).toBe(200);
- expect(data.body).toMatchObject({ people: [] });
- });
- });
-
describe('POST /asset/upload', () => {
it('should require authentication', async () => {
const { status, body } = await request(server)
@@ -759,286 +675,6 @@ describe(`${AssetController.name} (e2e)`, () => {
});
});
- describe('PUT /asset/:id', () => {
- it('should require authentication', async () => {
- const { status, body } = await request(server).put(`/asset/:${uuidStub.notFound}`);
- expect(status).toBe(401);
- expect(body).toEqual(errorStub.unauthorized);
- });
-
- it('should require a valid id', async () => {
- const { status, body } = await request(server)
- .put(`/asset/${uuidStub.invalid}`)
- .set('Authorization', `Bearer ${user1.accessToken}`);
- expect(status).toBe(400);
- expect(body).toEqual(errorStub.badRequest(['id must be a UUID']));
- });
-
- it('should require access', async () => {
- const { status, body } = await request(server)
- .put(`/asset/${asset4.id}`)
- .set('Authorization', `Bearer ${user1.accessToken}`);
- expect(status).toBe(400);
- expect(body).toEqual(errorStub.noPermission);
- });
-
- it('should favorite an asset', async () => {
- expect(asset1).toMatchObject({ isFavorite: false });
-
- const { status, body } = await request(server)
- .put(`/asset/${asset1.id}`)
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .send({ isFavorite: true });
- expect(body).toMatchObject({ id: asset1.id, isFavorite: true });
- expect(status).toEqual(200);
- });
-
- it('should archive an asset', async () => {
- expect(asset1).toMatchObject({ isArchived: false });
-
- const { status, body } = await request(server)
- .put(`/asset/${asset1.id}`)
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .send({ isArchived: true });
- expect(body).toMatchObject({ id: asset1.id, isArchived: true });
- expect(status).toEqual(200);
- });
-
- it('should update date time original', async () => {
- const { status, body } = await request(server)
- .put(`/asset/${asset1.id}`)
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
-
- expect(body).toMatchObject({
- id: asset1.id,
- exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00.000Z' }),
- });
- expect(status).toEqual(200);
- });
-
- it('should reject invalid gps coordinates', async () => {
- for (const test of [
- { latitude: 12 },
- { longitude: 12 },
- { latitude: 12, longitude: 'abc' },
- { latitude: 'abc', longitude: 12 },
- { latitude: null, longitude: 12 },
- { latitude: 12, longitude: null },
- { latitude: 91, longitude: 12 },
- { latitude: -91, longitude: 12 },
- { latitude: 12, longitude: -181 },
- { latitude: 12, longitude: 181 },
- ]) {
- const { status, body } = await request(server)
- .put(`/asset/${asset1.id}`)
- .send(test)
- .set('Authorization', `Bearer ${user1.accessToken}`);
- expect(status).toBe(400);
- expect(body).toEqual(errorStub.badRequest());
- }
- });
-
- it('should update gps data', async () => {
- const { status, body } = await request(server)
- .put(`/asset/${asset1.id}`)
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .send({ latitude: 12, longitude: 12 });
-
- expect(body).toMatchObject({
- id: asset1.id,
- exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }),
- });
- expect(status).toEqual(200);
- });
-
- it('should set the description', async () => {
- const { status, body } = await request(server)
- .put(`/asset/${asset1.id}`)
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .send({ description: 'Test asset description' });
- expect(body).toMatchObject({
- id: asset1.id,
- exifInfo: expect.objectContaining({ description: 'Test asset description' }),
- });
- expect(status).toEqual(200);
- });
-
- it('should return tagged people', async () => {
- const personRepository = app.get(IPersonRepository);
- const person = await personRepository.create({ ownerId: asset1.ownerId, name: 'Test Person' });
-
- await personRepository.createFaces([
- {
- assetId: asset1.id,
- personId: person.id,
- embedding: Array.from({ length: 512 }, Math.random),
- },
- ]);
-
- const { status, body } = await request(server)
- .put(`/asset/${asset1.id}`)
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .send({ isFavorite: true });
- expect(status).toEqual(200);
- expect(body).toMatchObject({
- id: asset1.id,
- isFavorite: true,
- people: [
- {
- birthDate: null,
- id: expect.any(String),
- isHidden: false,
- name: 'Test Person',
- thumbnailPath: '',
- },
- ],
- });
- });
- });
-
- describe('GET /asset/statistics', () => {
- beforeEach(async () => {
- await api.assetApi.upload(server, user1.accessToken, 'favored_asset', { isFavorite: true });
- await api.assetApi.upload(server, user1.accessToken, 'archived_asset', { isArchived: true });
- await api.assetApi.upload(server, user1.accessToken, 'favored_archived_asset', {
- isFavorite: true,
- isArchived: true,
- });
- });
-
- it('should require authentication', async () => {
- const { status, body } = await request(server).get('/asset/statistics');
-
- expect(status).toBe(401);
- expect(body).toEqual(errorStub.unauthorized);
- });
-
- it('should return stats of all assets', async () => {
- const { status, body } = await request(server)
- .get('/asset/statistics')
- .set('Authorization', `Bearer ${user1.accessToken}`);
-
- expect(body).toEqual({ images: 6, videos: 1, total: 7 });
- expect(status).toBe(200);
- });
-
- it('should return stats of all favored assets', async () => {
- const { status, body } = await request(server)
- .get('/asset/statistics')
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .query({ isFavorite: true });
-
- expect(status).toBe(200);
- expect(body).toEqual({ images: 2, videos: 1, total: 3 });
- });
-
- it('should return stats of all archived assets', async () => {
- const { status, body } = await request(server)
- .get('/asset/statistics')
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .query({ isArchived: true });
-
- expect(status).toBe(200);
- expect(body).toEqual({ images: 3, videos: 0, total: 3 });
- });
-
- it('should return stats of all favored and archived assets', async () => {
- const { status, body } = await request(server)
- .get('/asset/statistics')
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .query({ isFavorite: true, isArchived: true });
-
- expect(status).toBe(200);
- expect(body).toEqual({ images: 1, videos: 0, total: 1 });
- });
-
- it('should return stats of all assets neither favored nor archived', async () => {
- const { status, body } = await request(server)
- .get('/asset/statistics')
- .set('Authorization', `Bearer ${user1.accessToken}`)
- .query({ isFavorite: false, isArchived: false });
-
- expect(status).toBe(200);
- expect(body).toEqual({ images: 2, videos: 0, total: 2 });
- });
- });
-
- describe('GET /asset/random', () => {
- beforeAll(async () => {
- await Promise.all([
- createAsset(user1, new Date('1970-02-01')),
- createAsset(user1, new Date('1970-02-01')),
- createAsset(user1, new Date('1970-02-01')),
- createAsset(user1, new Date('1970-02-01')),
- createAsset(user1, new Date('1970-02-01')),
- createAsset(user1, new Date('1970-02-01')),
- ]);
- });
- it('should require authentication', async () => {
- const { status, body } = await request(server).get('/asset/random');
-
- expect(status).toBe(401);
- expect(body).toEqual(errorStub.unauthorized);
- });
-
- it.each(Array(10))('should return 1 random assets', async () => {
- const { status, body } = await request(server)
- .get('/asset/random')
- .set('Authorization', `Bearer ${user1.accessToken}`);
-
- expect(status).toBe(200);
-
- const assets: AssetResponseDto[] = body;
- expect(assets.length).toBe(1);
- expect(assets[0].ownerId).toBe(user1.userId);
- //
- // assets owned by user2
- expect(assets[0].id).not.toBe(asset4.id);
- // assets owned by user1
- expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id);
- });
-
- it.each(Array(10))('should return 2 random assets', async () => {
- const { status, body } = await request(server)
- .get('/asset/random?count=2')
- .set('Authorization', `Bearer ${user1.accessToken}`);
-
- expect(status).toBe(200);
-
- const assets: AssetResponseDto[] = body;
- expect(assets.length).toBe(2);
-
- for (const asset of assets) {
- expect(asset.ownerId).toBe(user1.userId);
- // assets owned by user1
- expect([asset1.id, asset2.id, asset3.id]).toContain(asset.id);
- // assets owned by user2
- expect(asset.id).not.toBe(asset4.id);
- }
- });
-
- it.each(Array(10))(
- 'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
- async () => {
- const { status, body } = await request(server)
- .get('/[]asset/random')
- .set('Authorization', `Bearer ${user2.accessToken}`);
-
- expect(status).toBe(200);
- expect(body).toEqual([expect.objectContaining({ id: asset4.id })]);
- },
- );
-
- it('should return error', async () => {
- const { status } = await request(server)
- .get('/asset/random?count=ABC')
- .set('Authorization', `Bearer ${user1.accessToken}`);
-
- expect(status).toBe(400);
- });
- });
-
describe('GET /asset/time-buckets', () => {
it('should require authentication', async () => {
const { status, body } = await request(server).get('/asset/time-buckets').query({ size: TimeBucketSize.MONTH });
diff --git a/server/e2e/client/asset-api.ts b/server/e2e/client/asset-api.ts
index 7dd47e06c6..8d2a1b79bc 100644
--- a/server/e2e/client/asset-api.ts
+++ b/server/e2e/client/asset-api.ts
@@ -1,4 +1,4 @@
-import { AssetBulkDeleteDto, AssetResponseDto } from '@app/domain';
+import { AssetResponseDto } from '@app/domain';
import { CreateAssetDto } from '@app/immich/api-v1/asset/dto/create-asset.dto';
import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
import { randomBytes } from 'node:crypto';
@@ -74,8 +74,4 @@ export const assetApi = {
expect(status).toBe(200);
return body;
},
- delete: async (server: any, accessToken: string, dto: AssetBulkDeleteDto) => {
- const { status } = await request(server).delete('/asset').set('Authorization', `Bearer ${accessToken}`).send(dto);
- expect(status).toBe(204);
- },
};
diff --git a/server/e2e/jobs/specs/trash.e2e-spec.ts b/server/e2e/jobs/specs/trash.e2e-spec.ts
deleted file mode 100644
index 5c4b3e9051..0000000000
--- a/server/e2e/jobs/specs/trash.e2e-spec.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import { LoginResponseDto } from '@app/domain';
-import { api } from 'e2e/client';
-import { readFile } from 'node:fs/promises';
-import { basename, join } from 'node:path';
-import type { App } from 'supertest/types';
-import { IMMICH_TEST_ASSET_PATH, testApp } from '../../../src/test-utils/utils';
-
-const assetFilePath = join(IMMICH_TEST_ASSET_PATH, 'formats/png/density_plot.png');
-
-describe(`Trash (e2e)`, () => {
- let server: App;
- let admin: LoginResponseDto;
-
- beforeAll(async () => {
- const app = await testApp.create();
- server = app.getHttpServer();
- });
-
- beforeEach(async () => {
- await testApp.reset();
- await api.authApi.adminSignUp(server);
- admin = await api.authApi.adminLogin(server);
- });
-
- afterAll(async () => {
- await testApp.teardown();
- });
-
- it('should move an asset to trash', async () => {
- const content = await readFile(assetFilePath);
- const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', {
- content,
- filename: basename(assetFilePath),
- });
-
- const uploadedAsset = await api.assetApi.get(server, admin.accessToken, assetId);
- expect(uploadedAsset.isTrashed).toBe(false);
-
- await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] });
-
- const deletedAsset = await api.assetApi.get(server, admin.accessToken, assetId);
- expect(deletedAsset.isTrashed).toBe(true);
- });
-
- it('should delete all trashed assets', async () => {
- const content = await readFile(assetFilePath);
- const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', {
- content,
- filename: basename(assetFilePath),
- });
-
- await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] });
-
- const assetsBeforeEmpty = await api.assetApi.getAllAssets(server, admin.accessToken);
- expect(assetsBeforeEmpty.length).toBe(1);
-
- await api.trashApi.empty(server, admin.accessToken);
-
- const assetsAfterEmpty = await api.assetApi.getAllAssets(server, admin.accessToken);
- expect(assetsAfterEmpty.length).toBe(0);
- });
-
- it('should restore all trashed assets', async () => {
- const content = await readFile(assetFilePath);
- const { id: assetId } = await api.assetApi.upload(server, admin.accessToken, 'test-device-id', {
- content,
- filename: basename(assetFilePath),
- });
-
- await api.assetApi.delete(server, admin.accessToken, { ids: [assetId] });
-
- const deletedAsset = await api.assetApi.get(server, admin.accessToken, assetId);
- expect(deletedAsset.isTrashed).toBe(true);
-
- await api.trashApi.restore(server, admin.accessToken);
-
- const restoredAsset = await api.assetApi.get(server, admin.accessToken, assetId);
- expect(restoredAsset.isTrashed).toBe(false);
- });
-});
diff --git a/server/package-lock.json b/server/package-lock.json
index 97c9dca58e..ceac96222d 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -3179,9 +3179,9 @@
}
},
"node_modules/@types/node": {
- "version": "20.11.19",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
- "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
+ "version": "20.11.20",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
+ "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
"dependencies": {
"undici-types": "~5.26.4"
}
@@ -3281,9 +3281,9 @@
}
},
"node_modules/@types/semver": {
- "version": "7.5.7",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
- "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
+ "version": "7.5.8",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@types/send": {
@@ -3398,16 +3398,16 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
- "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz",
+ "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
- "@typescript-eslint/scope-manager": "7.0.1",
- "@typescript-eslint/type-utils": "7.0.1",
- "@typescript-eslint/utils": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1",
+ "@typescript-eslint/scope-manager": "7.0.2",
+ "@typescript-eslint/type-utils": "7.0.2",
+ "@typescript-eslint/utils": "7.0.2",
+ "@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -3433,15 +3433,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
- "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz",
+ "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "7.0.1",
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/typescript-estree": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1",
+ "@typescript-eslint/scope-manager": "7.0.2",
+ "@typescript-eslint/types": "7.0.2",
+ "@typescript-eslint/typescript-estree": "7.0.2",
+ "@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4"
},
"engines": {
@@ -3461,13 +3461,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
- "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz",
+ "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1"
+ "@typescript-eslint/types": "7.0.2",
+ "@typescript-eslint/visitor-keys": "7.0.2"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -3478,13 +3478,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
- "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz",
+ "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "7.0.1",
- "@typescript-eslint/utils": "7.0.1",
+ "@typescript-eslint/typescript-estree": "7.0.2",
+ "@typescript-eslint/utils": "7.0.2",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -3505,9 +3505,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
- "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz",
+ "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -3518,13 +3518,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
- "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz",
+ "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1",
+ "@typescript-eslint/types": "7.0.2",
+ "@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -3570,17 +3570,17 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
- "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz",
+ "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "7.0.1",
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/typescript-estree": "7.0.1",
+ "@typescript-eslint/scope-manager": "7.0.2",
+ "@typescript-eslint/types": "7.0.2",
+ "@typescript-eslint/typescript-estree": "7.0.2",
"semver": "^7.5.4"
},
"engines": {
@@ -3595,12 +3595,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
- "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz",
+ "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "7.0.1",
+ "@typescript-eslint/types": "7.0.2",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -5504,9 +5504,9 @@
}
},
"node_modules/dotenv": {
- "version": "16.4.4",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz",
- "integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg==",
+ "version": "16.4.5",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"engines": {
"node": ">=12"
},
@@ -8139,9 +8139,9 @@
}
},
"node_modules/joi": {
- "version": "17.12.1",
- "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz",
- "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==",
+ "version": "17.12.2",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz",
+ "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==",
"dependencies": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
@@ -14730,9 +14730,9 @@
}
},
"@types/node": {
- "version": "20.11.19",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
- "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==",
+ "version": "20.11.20",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz",
+ "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==",
"requires": {
"undici-types": "~5.26.4"
}
@@ -14819,9 +14819,9 @@
}
},
"@types/semver": {
- "version": "7.5.7",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
- "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
+ "version": "7.5.8",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"@types/send": {
@@ -14936,16 +14936,16 @@
"dev": true
},
"@typescript-eslint/eslint-plugin": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
- "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz",
+ "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.5.1",
- "@typescript-eslint/scope-manager": "7.0.1",
- "@typescript-eslint/type-utils": "7.0.1",
- "@typescript-eslint/utils": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1",
+ "@typescript-eslint/scope-manager": "7.0.2",
+ "@typescript-eslint/type-utils": "7.0.2",
+ "@typescript-eslint/utils": "7.0.2",
+ "@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -14955,54 +14955,54 @@
}
},
"@typescript-eslint/parser": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
- "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz",
+ "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==",
"dev": true,
"requires": {
- "@typescript-eslint/scope-manager": "7.0.1",
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/typescript-estree": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1",
+ "@typescript-eslint/scope-manager": "7.0.2",
+ "@typescript-eslint/types": "7.0.2",
+ "@typescript-eslint/typescript-estree": "7.0.2",
+ "@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
- "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz",
+ "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1"
+ "@typescript-eslint/types": "7.0.2",
+ "@typescript-eslint/visitor-keys": "7.0.2"
}
},
"@typescript-eslint/type-utils": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
- "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz",
+ "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==",
"dev": true,
"requires": {
- "@typescript-eslint/typescript-estree": "7.0.1",
- "@typescript-eslint/utils": "7.0.1",
+ "@typescript-eslint/typescript-estree": "7.0.2",
+ "@typescript-eslint/utils": "7.0.2",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
}
},
"@typescript-eslint/types": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
- "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz",
+ "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
- "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz",
+ "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1",
+ "@typescript-eslint/types": "7.0.2",
+ "@typescript-eslint/visitor-keys": "7.0.2",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -15032,27 +15032,27 @@
}
},
"@typescript-eslint/utils": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
- "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz",
+ "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "7.0.1",
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/typescript-estree": "7.0.1",
+ "@typescript-eslint/scope-manager": "7.0.2",
+ "@typescript-eslint/types": "7.0.2",
+ "@typescript-eslint/typescript-estree": "7.0.2",
"semver": "^7.5.4"
}
},
"@typescript-eslint/visitor-keys": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
- "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz",
+ "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==",
"dev": true,
"requires": {
- "@typescript-eslint/types": "7.0.1",
+ "@typescript-eslint/types": "7.0.2",
"eslint-visitor-keys": "^3.4.1"
}
},
@@ -16494,9 +16494,9 @@
}
},
"dotenv": {
- "version": "16.4.4",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.4.tgz",
- "integrity": "sha512-XvPXc8XAQThSjAbY6cQ/9PcBXmFoWuw1sQ3b8HqUCR6ziGXjkTi//kB9SWa2UwqlgdAIuRqAa/9hVljzPehbYg=="
+ "version": "16.4.5",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
+ "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg=="
},
"dotenv-expand": {
"version": "10.0.0",
@@ -18453,9 +18453,9 @@
}
},
"joi": {
- "version": "17.12.1",
- "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.1.tgz",
- "integrity": "sha512-vtxmq+Lsc5SlfqotnfVjlViWfOL9nt/avKNbKYizwf6gsCfq9NYY/ceYRMFD8XDdrjJ9abJyScWmhmIiy+XRtQ==",
+ "version": "17.12.2",
+ "resolved": "https://registry.npmjs.org/joi/-/joi-17.12.2.tgz",
+ "integrity": "sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==",
"requires": {
"@hapi/hoek": "^9.3.0",
"@hapi/topo": "^5.1.0",
diff --git a/server/src/domain/domain.constant.ts b/server/src/domain/domain.constant.ts
index 4e7c4d5524..0dc9c54140 100644
--- a/server/src/domain/domain.constant.ts
+++ b/server/src/domain/domain.constant.ts
@@ -91,7 +91,7 @@ export const citiesFile = 'cities500.txt';
export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt');
export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt');
export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt');
-export const geodataCitites500Path = join(GEODATA_ROOT_PATH, citiesFile);
+export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile);
const image: Record = {
'.3fr': ['image/3fr', 'image/x-hasselblad-3fr'],
diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/domain/media/media.service.spec.ts
index f4c9aa53e7..dc5934e7b1 100644
--- a/server/src/domain/media/media.service.spec.ts
+++ b/server/src/domain/media/media.service.spec.ts
@@ -1801,7 +1801,7 @@ describe(MediaService.name, () => {
{
inputOptions: [],
outputOptions: [
- `-c:v hevc_rkmpp_encoder`,
+ `-c:v hevc_rkmpp`,
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
@@ -1810,17 +1810,12 @@ describe(MediaService.name, () => {
'-g 256',
'-tag:v hvc1',
'-v verbose',
+ '-vf scale=-2:720,format=yuv420p',
'-level 153',
'-rc_mode 3',
- '-quality_min 0',
- '-quality_max 100',
'-b:v 10000k',
- '-width 1280',
- '-height 720',
],
twoPass: false,
- ffmpegPath: 'ffmpeg_mpp',
- ldLibraryPath: '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp',
},
);
});
@@ -1841,7 +1836,7 @@ describe(MediaService.name, () => {
{
inputOptions: [],
outputOptions: [
- `-c:v h264_rkmpp_encoder`,
+ `-c:v h264_rkmpp`,
'-c:a copy',
'-movflags faststart',
'-fps_mode passthrough',
@@ -1849,16 +1844,12 @@ describe(MediaService.name, () => {
'-map 0:1',
'-g 256',
'-v verbose',
+ '-vf scale=-2:720,format=yuv420p',
'-level 51',
'-rc_mode 2',
- '-quality_min 51',
- '-quality_max 51',
- '-width 1280',
- '-height 720',
+ '-qp_init 30',
],
twoPass: false,
- ffmpegPath: 'ffmpeg_mpp',
- ldLibraryPath: '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp',
},
);
});
diff --git a/server/src/domain/media/media.util.ts b/server/src/domain/media/media.util.ts
index c9483c3736..e5890bdd03 100644
--- a/server/src/domain/media/media.util.ts
+++ b/server/src/domain/media/media.util.ts
@@ -607,16 +607,6 @@ export class VAAPIConfig extends BaseHWConfig {
}
export class RKMPPConfig extends BaseHWConfig {
- getOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo): TranscodeOptions {
- const options = super.getOptions(target, videoStream, audioStream);
- options.ffmpegPath = 'ffmpeg_mpp';
- options.ldLibraryPath = '/lib/aarch64-linux-gnu:/lib/ffmpeg-mpp';
- if ([TranscodeTarget.ALL, TranscodeTarget.VIDEO].includes(target)) {
- options.outputOptions.push(...this.getSizeOptions(videoStream));
- }
- return options;
- }
-
eligibleForTwoPass(): boolean {
return false;
}
@@ -628,18 +618,6 @@ export class RKMPPConfig extends BaseHWConfig {
return [];
}
- getFilterOptions(videoStream: VideoStreamInfo) {
- return this.shouldToneMap(videoStream) ? this.getToneMapping() : [];
- }
-
- getSizeOptions(videoStream: VideoStreamInfo) {
- if (this.shouldScale(videoStream)) {
- const { width, height } = this.getSize(videoStream);
- return [`-width ${width}`, `-height ${height}`];
- }
- return [];
- }
-
getPresetOptions() {
switch (this.config.targetVideoCodec) {
case VideoCodec.H264: {
@@ -659,12 +637,11 @@ export class RKMPPConfig extends BaseHWConfig {
getBitrateOptions() {
const bitrate = this.getMaxBitrateValue();
if (bitrate > 0) {
- return ['-rc_mode 3', '-quality_min 0', '-quality_max 100', `-b:v ${bitrate}${this.getBitrateUnit()}`];
- } else {
- // convert CQP from 51-10 to 0-100, values below 10 are set to 10
- const quality = Math.floor(125 - Math.max(this.config.crf, 10) * (125 / 51));
- return ['-rc_mode 2', `-quality_min ${quality}`, `-quality_max ${quality}`];
+ // -b:v specifies max bitrate, average bitrate is derived automatically...
+ return ['-rc_mode 3', `-b:v ${bitrate}${this.getBitrateUnit()}`];
}
+ // use CRF value as QP value
+ return ['-rc_mode 2', `-qp_init ${this.config.crf}`];
}
getSupportedCodecs() {
@@ -672,6 +649,6 @@ export class RKMPPConfig extends BaseHWConfig {
}
getVideoCodec(): string {
- return `${this.config.targetVideoCodec}_rkmpp_encoder`;
+ return `${this.config.targetVideoCodec}_rkmpp`;
}
}
diff --git a/server/src/domain/repositories/media.repository.ts b/server/src/domain/repositories/media.repository.ts
index 846b6156d6..ed6f884493 100644
--- a/server/src/domain/repositories/media.repository.ts
+++ b/server/src/domain/repositories/media.repository.ts
@@ -51,8 +51,6 @@ export interface TranscodeOptions {
inputOptions: string[];
outputOptions: string[];
twoPass: boolean;
- ffmpegPath?: string;
- ldLibraryPath?: string;
}
export interface BitrateDistribution {
diff --git a/server/src/domain/repositories/search.repository.ts b/server/src/domain/repositories/search.repository.ts
index 7183e9e3fe..c9fec3cf71 100644
--- a/server/src/domain/repositories/search.repository.ts
+++ b/server/src/domain/repositories/search.repository.ts
@@ -1,4 +1,4 @@
-import { AssetEntity, AssetFaceEntity, AssetType, SmartInfoEntity } from '@app/infra/entities';
+import { AssetEntity, AssetFaceEntity, AssetType, GeodataPlacesEntity, SmartInfoEntity } from '@app/infra/entities';
import { Paginated } from '../domain.util';
export const ISearchRepository = 'ISearchRepository';
@@ -186,4 +186,6 @@ export interface ISearchRepository {
searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated;
searchFaces(search: FaceEmbeddingSearch): Promise;
upsert(smartInfo: Partial, embedding?: Embedding): Promise;
+ searchPlaces(placeName: string): Promise;
+ deleteAllSearchEmbeddings(): Promise;
}
diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts
index 4f2aa18199..877a494e4d 100644
--- a/server/src/domain/search/dto/search.dto.ts
+++ b/server/src/domain/search/dto/search.dto.ts
@@ -1,5 +1,5 @@
import { AssetOrder } from '@app/domain/asset/dto/asset.dto';
-import { AssetType } from '@app/infra/entities';
+import { AssetType, GeodataPlacesEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
@@ -241,6 +241,12 @@ export class SearchDto {
size?: number;
}
+export class SearchPlacesDto {
+ @IsString()
+ @IsNotEmpty()
+ name!: string;
+}
+
export class SearchPeopleDto {
@IsString()
@IsNotEmpty()
@@ -251,3 +257,21 @@ export class SearchPeopleDto {
@Optional()
withHidden?: boolean;
}
+
+export class PlacesResponseDto {
+ name!: string;
+ latitude!: number;
+ longitude!: number;
+ admin1name?: string;
+ admin2name?: string;
+}
+
+export function mapPlaces(place: GeodataPlacesEntity): PlacesResponseDto {
+ return {
+ name: place.name,
+ latitude: place.latitude,
+ longitude: place.longitude,
+ admin1name: place.admin1Name,
+ admin2name: place.admin2Name,
+ };
+}
diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts
index 452c556f41..5b56399981 100644
--- a/server/src/domain/search/search.service.ts
+++ b/server/src/domain/search/search.service.ts
@@ -16,7 +16,15 @@ import {
SearchStrategy,
} from '../repositories';
import { FeatureFlag, SystemConfigCore } from '../system-config';
-import { MetadataSearchDto, SearchDto, SearchPeopleDto, SmartSearchDto } from './dto';
+import {
+ MetadataSearchDto,
+ PlacesResponseDto,
+ SearchDto,
+ SearchPeopleDto,
+ SearchPlacesDto,
+ SmartSearchDto,
+ mapPlaces,
+} from './dto';
import { SearchSuggestionRequestDto, SearchSuggestionType } from './dto/search-suggestion.dto';
import { SearchResponseDto } from './response-dto';
@@ -41,6 +49,11 @@ export class SearchService {
return this.personRepository.getByName(auth.user.id, dto.name, { withHidden: dto.withHidden });
}
+ async searchPlaces(dto: SearchPlacesDto): Promise {
+ const places = await this.searchRepository.searchPlaces(dto.name);
+ return places.map((place) => mapPlaces(place));
+ }
+
async getExploreData(auth: AuthDto): Promise[]> {
await this.configCore.requireFeature(FeatureFlag.SEARCH);
const options = { maxFields: 12, minAssetsPerField: 5 };
@@ -182,26 +195,22 @@ export class SearchService {
}
async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto): Promise {
- if (dto.type === SearchSuggestionType.COUNTRY) {
- return this.metadataRepository.getCountries(auth.user.id);
+ switch (dto.type) {
+ case SearchSuggestionType.COUNTRY: {
+ return this.metadataRepository.getCountries(auth.user.id);
+ }
+ case SearchSuggestionType.STATE: {
+ return this.metadataRepository.getStates(auth.user.id, dto.country);
+ }
+ case SearchSuggestionType.CITY: {
+ return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state);
+ }
+ case SearchSuggestionType.CAMERA_MAKE: {
+ return this.metadataRepository.getCameraMakes(auth.user.id, dto.model);
+ }
+ case SearchSuggestionType.CAMERA_MODEL: {
+ return this.metadataRepository.getCameraModels(auth.user.id, dto.make);
+ }
}
-
- if (dto.type === SearchSuggestionType.STATE) {
- return this.metadataRepository.getStates(auth.user.id, dto.country);
- }
-
- if (dto.type === SearchSuggestionType.CITY) {
- return this.metadataRepository.getCities(auth.user.id, dto.country, dto.state);
- }
-
- if (dto.type === SearchSuggestionType.CAMERA_MAKE) {
- return this.metadataRepository.getCameraMakes(auth.user.id, dto.model);
- }
-
- if (dto.type === SearchSuggestionType.CAMERA_MODEL) {
- return this.metadataRepository.getCameraModels(auth.user.id, dto.make);
- }
-
- return [];
}
}
diff --git a/server/src/domain/smart-info/smart-info.service.spec.ts b/server/src/domain/smart-info/smart-info.service.spec.ts
index 5da7b7824b..9835ea1a53 100644
--- a/server/src/domain/smart-info/smart-info.service.spec.ts
+++ b/server/src/domain/smart-info/smart-info.service.spec.ts
@@ -71,6 +71,7 @@ describe(SmartInfoService.name, () => {
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.SMART_SEARCH);
+ expect(searchMock.deleteAllSearchEmbeddings).not.toHaveBeenCalled();
});
it('should queue all the assets', async () => {
@@ -83,6 +84,7 @@ describe(SmartInfoService.name, () => {
expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.SMART_SEARCH, data: { id: assetStub.image.id } }]);
expect(assetMock.getAll).toHaveBeenCalled();
+ expect(searchMock.deleteAllSearchEmbeddings).toHaveBeenCalled();
});
});
diff --git a/server/src/domain/smart-info/smart-info.service.ts b/server/src/domain/smart-info/smart-info.service.ts
index d193b29b51..19d5668cc5 100644
--- a/server/src/domain/smart-info/smart-info.service.ts
+++ b/server/src/domain/smart-info/smart-info.service.ts
@@ -50,6 +50,10 @@ export class SmartInfoService {
return true;
}
+ if (force) {
+ await this.repository.deleteAllSearchEmbeddings();
+ }
+
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force
? this.assetRepository.getAll(pagination)
diff --git a/server/src/domain/storage-template/storage-template.service.ts b/server/src/domain/storage-template/storage-template.service.ts
index d696982540..857d1df327 100644
--- a/server/src/domain/storage-template/storage-template.service.ts
+++ b/server/src/domain/storage-template/storage-template.service.ts
@@ -117,7 +117,7 @@ export class StorageTemplateService {
return true;
}
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
- this.assetRepository.getAll(pagination),
+ this.assetRepository.getAll(pagination, { withExif: true }),
);
const users = await this.userRepository.getList();
diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts
index 4e57cfaa62..b807da9665 100644
--- a/server/src/immich/controllers/search.controller.ts
+++ b/server/src/immich/controllers/search.controller.ts
@@ -2,9 +2,11 @@ import {
AuthDto,
MetadataSearchDto,
PersonResponseDto,
+ PlacesResponseDto,
SearchDto,
SearchExploreResponseDto,
SearchPeopleDto,
+ SearchPlacesDto,
SearchResponseDto,
SearchService,
SmartSearchDto,
@@ -48,6 +50,11 @@ export class SearchController {
return this.service.searchPerson(auth, dto);
}
+ @Get('places')
+ searchPlaces(@Query() dto: SearchPlacesDto): Promise {
+ return this.service.searchPlaces(dto);
+ }
+
@Get('suggestions')
getSearchSuggestions(@Auth() auth: AuthDto, @Query() dto: SearchSuggestionRequestDto): Promise {
return this.service.getSearchSuggestions(auth, dto);
diff --git a/server/src/infra/entities/geodata-admin1.entity.ts b/server/src/infra/entities/geodata-admin1.entity.ts
deleted file mode 100644
index 36cf0a805e..0000000000
--- a/server/src/infra/entities/geodata-admin1.entity.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Column, Entity, PrimaryColumn } from 'typeorm';
-
-@Entity('geodata_admin1')
-export class GeodataAdmin1Entity {
- @PrimaryColumn({ type: 'varchar' })
- key!: string;
-
- @Column({ type: 'varchar' })
- name!: string;
-}
diff --git a/server/src/infra/entities/geodata-admin2.entity.ts b/server/src/infra/entities/geodata-admin2.entity.ts
deleted file mode 100644
index bd03e83776..0000000000
--- a/server/src/infra/entities/geodata-admin2.entity.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { Column, Entity, PrimaryColumn } from 'typeorm';
-
-@Entity('geodata_admin2')
-export class GeodataAdmin2Entity {
- @PrimaryColumn({ type: 'varchar' })
- key!: string;
-
- @Column({ type: 'varchar' })
- name!: string;
-}
diff --git a/server/src/infra/entities/geodata-places.entity.ts b/server/src/infra/entities/geodata-places.entity.ts
index 244e4261b0..966a50d5c9 100644
--- a/server/src/infra/entities/geodata-places.entity.ts
+++ b/server/src/infra/entities/geodata-places.entity.ts
@@ -1,6 +1,4 @@
-import { GeodataAdmin1Entity } from '@app/infra/entities/geodata-admin1.entity';
-import { GeodataAdmin2Entity } from '@app/infra/entities/geodata-admin2.entity';
-import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
+import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('geodata_places', { synchronize: false })
export class GeodataPlacesEntity {
@@ -21,7 +19,7 @@ export class GeodataPlacesEntity {
// asExpression: 'll_to_earth((latitude)::double precision, (longitude)::double precision)',
// type: 'earth',
// })
- earthCoord!: unknown;
+ // earthCoord!: unknown;
@Column({ type: 'char', length: 2 })
countryCode!: string;
@@ -32,27 +30,14 @@ export class GeodataPlacesEntity {
@Column({ type: 'varchar', length: 80, nullable: true })
admin2Code!: string;
- @Column({
- type: 'varchar',
- generatedType: 'STORED',
- asExpression: `"countryCode" || '.' || "admin1Code"`,
- nullable: true,
- })
- admin1Key!: string;
+ @Column({ type: 'varchar', nullable: true })
+ admin1Name!: string;
- @ManyToOne(() => GeodataAdmin1Entity, { eager: true, nullable: true, createForeignKeyConstraints: false })
- admin1!: GeodataAdmin1Entity;
+ @Column({ type: 'varchar', nullable: true })
+ admin2Name!: string;
- @Column({
- type: 'varchar',
- generatedType: 'STORED',
- asExpression: `"countryCode" || '.' || "admin1Code" || '.' || "admin2Code"`,
- nullable: true,
- })
- admin2Key!: string;
-
- @ManyToOne(() => GeodataAdmin2Entity, { eager: true, nullable: true, createForeignKeyConstraints: false })
- admin2!: GeodataAdmin2Entity;
+ @Column({ type: 'varchar', nullable: true })
+ alternateNames!: string;
@Column({ type: 'date' })
modificationDate!: Date;
diff --git a/server/src/infra/entities/index.ts b/server/src/infra/entities/index.ts
index 957e15a887..af620790ef 100644
--- a/server/src/infra/entities/index.ts
+++ b/server/src/infra/entities/index.ts
@@ -7,8 +7,6 @@ import { AssetStackEntity } from './asset-stack.entity';
import { AssetEntity } from './asset.entity';
import { AuditEntity } from './audit.entity';
import { ExifEntity } from './exif.entity';
-import { GeodataAdmin1Entity } from './geodata-admin1.entity';
-import { GeodataAdmin2Entity } from './geodata-admin2.entity';
import { GeodataPlacesEntity } from './geodata-places.entity';
import { LibraryEntity } from './library.entity';
import { MoveEntity } from './move.entity';
@@ -32,8 +30,6 @@ export * from './asset-stack.entity';
export * from './asset.entity';
export * from './audit.entity';
export * from './exif.entity';
-export * from './geodata-admin1.entity';
-export * from './geodata-admin2.entity';
export * from './geodata-places.entity';
export * from './library.entity';
export * from './move.entity';
@@ -59,8 +55,6 @@ export const databaseEntities = [
AuditEntity,
ExifEntity,
GeodataPlacesEntity,
- GeodataAdmin1Entity,
- GeodataAdmin2Entity,
MoveEntity,
PartnerEntity,
PersonEntity,
diff --git a/server/src/infra/migrations/1708059341865-GeodataLocationSearch.ts b/server/src/infra/migrations/1708059341865-GeodataLocationSearch.ts
new file mode 100644
index 0000000000..136ca2598d
--- /dev/null
+++ b/server/src/infra/migrations/1708059341865-GeodataLocationSearch.ts
@@ -0,0 +1,152 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class GeodataLocationSearch1708059341865 implements MigrationInterface {
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS pg_trgm`);
+ await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS unaccent`);
+
+ // https://stackoverflow.com/a/11007216
+ await queryRunner.query(`
+ CREATE OR REPLACE FUNCTION f_unaccent(text)
+ RETURNS text
+ LANGUAGE sql IMMUTABLE PARALLEL SAFE STRICT
+ RETURN unaccent('unaccent', $1)`);
+
+ await queryRunner.query(`ALTER TABLE geodata_places ADD COLUMN "admin1Name" varchar`);
+ await queryRunner.query(`ALTER TABLE geodata_places ADD COLUMN "admin2Name" varchar`);
+
+ await queryRunner.query(`
+ UPDATE geodata_places
+ SET "admin1Name" = admin1.name
+ FROM geodata_admin1 admin1
+ WHERE admin1.key = "admin1Key"`);
+
+ await queryRunner.query(`
+ UPDATE geodata_places
+ SET "admin2Name" = admin2.name
+ FROM geodata_admin2 admin2
+ WHERE admin2.key = "admin2Key"`);
+
+ await queryRunner.query(`DROP TABLE geodata_admin1 CASCADE`);
+ await queryRunner.query(`DROP TABLE geodata_admin2 CASCADE`);
+
+ await queryRunner.query(`
+ ALTER TABLE geodata_places
+ DROP COLUMN "admin1Key",
+ DROP COLUMN "admin2Key"`);
+
+ await queryRunner.query(`
+ CREATE INDEX idx_geodata_places_name
+ ON geodata_places
+ USING gin (f_unaccent(name) gin_trgm_ops)`);
+
+ await queryRunner.query(`
+ CREATE INDEX idx_geodata_places_admin1_name
+ ON geodata_places
+ USING gin (f_unaccent("admin1Name") gin_trgm_ops)`);
+
+ await queryRunner.query(`
+ CREATE INDEX idx_geodata_places_admin2_name
+ ON geodata_places
+ USING gin (f_unaccent("admin2Name") gin_trgm_ops)`);
+
+ await queryRunner.query(
+ `
+ DELETE FROM "typeorm_metadata"
+ WHERE
+ "type" = $1 AND
+ "name" = $2 AND
+ "database" = $3 AND
+ "schema" = $4 AND
+ "table" = $5`,
+ ['GENERATED_COLUMN', 'admin1Key', 'immich', 'public', 'geodata_places'],
+ );
+
+ await queryRunner.query(
+ `
+ DELETE FROM "typeorm_metadata"
+ WHERE
+ "type" = $1 AND
+ "name" = $2 AND
+ "database" = $3 AND
+ "schema" = $4 AND
+ "table" = $5`,
+ ['GENERATED_COLUMN', 'admin2Key', 'immich', 'public', 'geodata_places'],
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`
+ CREATE TABLE "geodata_admin1" (
+ "key" character varying NOT NULL,
+ "name" character varying NOT NULL,
+ CONSTRAINT "PK_3fe3a89c5aac789d365871cb172" PRIMARY KEY ("key")
+ )`);
+
+ await queryRunner.query(`
+ CREATE TABLE "geodata_admin2" (
+ "key" character varying NOT NULL,
+ "name" character varying NOT NULL,
+ CONSTRAINT "PK_1e3886455dbb684d6f6b4756726" PRIMARY KEY ("key")
+ )`);
+
+ await queryRunner.query(`
+ ALTER TABLE geodata_places
+ ADD COLUMN "admin1Key" character varying
+ GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code") STORED,
+ ADD COLUMN "admin2Key" character varying
+ GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code" || '.' || "admin2Code") STORED`);
+
+ await queryRunner.query(
+ `
+ INSERT INTO "geodata_admin1"
+ SELECT DISTINCT
+ "admin1Key" AS "key",
+ "admin1Name" AS "name"
+ FROM geodata_places
+ WHERE "admin1Name" IS NOT NULL`,
+ );
+
+ await queryRunner.query(
+ `
+ INSERT INTO "geodata_admin2"
+ SELECT DISTINCT
+ "admin2Key" AS "key",
+ "admin2Name" AS "name"
+ FROM geodata_places
+ WHERE "admin2Name" IS NOT NULL`,
+ );
+
+ await queryRunner.query(`
+ UPDATE geodata_places
+ SET "admin1Name" = admin1.name
+ FROM geodata_admin1 admin1
+ WHERE admin1.key = "admin1Key"`);
+
+ await queryRunner.query(`
+ UPDATE geodata_places
+ SET "admin2Name" = admin2.name
+ FROM geodata_admin2 admin2
+ WHERE admin2.key = "admin2Key";`);
+
+ await queryRunner.query(
+ `
+ INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value")
+ VALUES ($1, $2, $3, $4, $5, $6)`,
+ ['immich', 'public', 'geodata_places', 'GENERATED_COLUMN', 'admin1Key', '"countryCode" || \'.\' || "admin1Code"'],
+ );
+
+ await queryRunner.query(
+ `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value")
+ VALUES ($1, $2, $3, $4, $5, $6)`,
+ [
+ 'immich',
+ 'public',
+ 'geodata_places',
+ 'GENERATED_COLUMN',
+ 'admin2Key',
+ '"countryCode" || \'.\' || "admin1Code" || \'.\' || "admin2Code"',
+ ],
+ );
+ }
+}
diff --git a/server/src/infra/migrations/1708116312820-GeonamesEnhancement.ts b/server/src/infra/migrations/1708116312820-GeonamesEnhancement.ts
new file mode 100644
index 0000000000..0cea9a0411
--- /dev/null
+++ b/server/src/infra/migrations/1708116312820-GeonamesEnhancement.ts
@@ -0,0 +1,18 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class GeonamesEnhancement1708116312820 implements MigrationInterface {
+ name = 'GeonamesEnhancement1708116312820'
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE geodata_places ADD COLUMN "alternateNames" varchar`);
+ await queryRunner.query(`
+ CREATE INDEX idx_geodata_places_admin2_alternate_names
+ ON geodata_places
+ USING gin (f_unaccent("alternateNames") gin_trgm_ops)`);
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(`ALTER TABLE geodata_places DROP COLUMN "alternateNames"`);
+ }
+
+}
diff --git a/server/src/infra/repositories/media.repository.ts b/server/src/infra/repositories/media.repository.ts
index a981bbc072..1f9395ff21 100644
--- a/server/src/infra/repositories/media.repository.ts
+++ b/server/src/infra/repositories/media.repository.ts
@@ -76,18 +76,7 @@ export class MediaRepository implements IMediaRepository {
transcode(input: string, output: string | Writable, options: TranscodeOptions): Promise {
if (!options.twoPass) {
return new Promise((resolve, reject) => {
- const oldLdLibraryPath = process.env.LD_LIBRARY_PATH;
- if (options.ldLibraryPath) {
- // fluent ffmpeg does not allow to set environment variables, so we do it manually
- process.env.LD_LIBRARY_PATH = this.chainPath(oldLdLibraryPath || '', options.ldLibraryPath);
- }
- try {
- this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run();
- } finally {
- if (options.ldLibraryPath) {
- process.env.LD_LIBRARY_PATH = oldLdLibraryPath;
- }
- }
+ this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run();
});
}
@@ -121,7 +110,6 @@ export class MediaRepository implements IMediaRepository {
configureFfmpegCall(input: string, output: string | Writable, options: TranscodeOptions) {
return ffmpeg(input, { niceness: 10 })
- .setFfmpegPath(options.ffmpegPath || 'ffmpeg')
.inputOptions(options.inputOptions)
.outputOptions(options.outputOptions)
.output(output)
diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts
index 6a90ad1081..4abfe0eace 100644
--- a/server/src/infra/repositories/metadata.repository.ts
+++ b/server/src/infra/repositories/metadata.repository.ts
@@ -2,7 +2,7 @@ import {
citiesFile,
geodataAdmin1Path,
geodataAdmin2Path,
- geodataCitites500Path,
+ geodataCities500Path,
geodataDatePath,
GeoPoint,
IMetadataRepository,
@@ -10,13 +10,7 @@ import {
ISystemMetadataRepository,
ReverseGeocodeResult,
} from '@app/domain';
-import {
- ExifEntity,
- GeodataAdmin1Entity,
- GeodataAdmin2Entity,
- GeodataPlacesEntity,
- SystemMetadataKey,
-} from '@app/infra/entities';
+import { ExifEntity, GeodataPlacesEntity, SystemMetadataKey } from '@app/infra/entities';
import { ImmichLogger } from '@app/infra/logger';
import { Inject } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
@@ -26,19 +20,16 @@ import { getName } from 'i18n-iso-countries';
import { createReadStream, existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import * as readLine from 'node:readline';
-import { DataSource, DeepPartial, QueryRunner, Repository } from 'typeorm';
+import { DataSource, QueryRunner, Repository } from 'typeorm';
+import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
import { DummyValue, GenerateSql } from '../infra.util';
-type GeoEntity = GeodataPlacesEntity | GeodataAdmin1Entity | GeodataAdmin2Entity;
-type GeoEntityClass = typeof GeodataPlacesEntity | typeof GeodataAdmin1Entity | typeof GeodataAdmin2Entity;
-
export class MetadataRepository implements IMetadataRepository {
constructor(
@InjectRepository(ExifEntity) private exifRepository: Repository,
@InjectRepository(GeodataPlacesEntity) private readonly geodataPlacesRepository: Repository,
- @InjectRepository(GeodataAdmin1Entity) private readonly geodataAdmin1Repository: Repository,
- @InjectRepository(GeodataAdmin2Entity) private readonly geodataAdmin2Repository: Repository,
- @Inject(ISystemMetadataRepository) private readonly systemMetadataRepository: ISystemMetadataRepository,
+ @Inject(ISystemMetadataRepository)
+ private readonly systemMetadataRepository: ISystemMetadataRepository,
@InjectDataSource() private dataSource: DataSource,
) {}
@@ -54,7 +45,6 @@ export class MetadataRepository implements IMetadataRepository {
return;
}
- this.logger.log('Importing geodata to database from file');
await this.importGeodata();
await this.systemMetadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, {
@@ -69,12 +59,14 @@ export class MetadataRepository implements IMetadataRepository {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
+ const admin1 = await this.loadAdmin(geodataAdmin1Path);
+ const admin2 = await this.loadAdmin(geodataAdmin2Path);
+
try {
await queryRunner.startTransaction();
- await this.loadCities500(queryRunner);
- await this.loadAdmin1(queryRunner);
- await this.loadAdmin2(queryRunner);
+ await queryRunner.manager.clear(GeodataPlacesEntity);
+ await this.loadCities500(queryRunner, admin1, admin2);
await queryRunner.commitTransaction();
} catch (error) {
@@ -86,76 +78,73 @@ export class MetadataRepository implements IMetadataRepository {
}
}
- private async loadGeodataToTableFromFile(
+ private async loadGeodataToTableFromFile(
queryRunner: QueryRunner,
- lineToEntityMapper: (lineSplit: string[]) => T,
+ lineToEntityMapper: (lineSplit: string[]) => GeodataPlacesEntity,
filePath: string,
- entity: GeoEntityClass,
) {
if (!existsSync(filePath)) {
this.logger.error(`Geodata file ${filePath} not found`);
throw new Error(`Geodata file ${filePath} not found`);
}
- await queryRunner.manager.clear(entity);
const input = createReadStream(filePath);
- let buffer: DeepPartial[] = [];
- const lineReader = readLine.createInterface({ input: input });
+ let bufferGeodata: QueryDeepPartialEntity[] = [];
+ const lineReader = readLine.createInterface({ input });
for await (const line of lineReader) {
const lineSplit = line.split('\t');
- buffer.push(lineToEntityMapper(lineSplit));
- if (buffer.length > 1000) {
- await queryRunner.manager.save(buffer);
- buffer = [];
+ const geoData = lineToEntityMapper(lineSplit);
+ bufferGeodata.push(geoData);
+ if (bufferGeodata.length > 1000) {
+ await queryRunner.manager.upsert(GeodataPlacesEntity, bufferGeodata, ['id']);
+ bufferGeodata = [];
}
}
- await queryRunner.manager.save(buffer);
+ await queryRunner.manager.upsert(GeodataPlacesEntity, bufferGeodata, ['id']);
}
- private async loadCities500(queryRunner: QueryRunner) {
- await this.loadGeodataToTableFromFile(
+ private async loadCities500(
+ queryRunner: QueryRunner,
+ admin1Map: Map,
+ admin2Map: Map,
+ ) {
+ await this.loadGeodataToTableFromFile(
queryRunner,
(lineSplit: string[]) =>
this.geodataPlacesRepository.create({
id: Number.parseInt(lineSplit[0]),
name: lineSplit[1],
+ alternateNames: lineSplit[3],
latitude: Number.parseFloat(lineSplit[4]),
longitude: Number.parseFloat(lineSplit[5]),
countryCode: lineSplit[8],
admin1Code: lineSplit[10],
admin2Code: lineSplit[11],
modificationDate: lineSplit[18],
+ admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`),
+ admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`),
}),
- geodataCitites500Path,
- GeodataPlacesEntity,
+ geodataCities500Path,
);
}
- private async loadAdmin1(queryRunner: QueryRunner) {
- await this.loadGeodataToTableFromFile(
- queryRunner,
- (lineSplit: string[]) =>
- this.geodataAdmin1Repository.create({
- key: lineSplit[0],
- name: lineSplit[1],
- }),
- geodataAdmin1Path,
- GeodataAdmin1Entity,
- );
- }
+ private async loadAdmin(filePath: string) {
+ if (!existsSync(filePath)) {
+ this.logger.error(`Geodata file ${filePath} not found`);
+ throw new Error(`Geodata file ${filePath} not found`);
+ }
- private async loadAdmin2(queryRunner: QueryRunner) {
- await this.loadGeodataToTableFromFile(
- queryRunner,
- (lineSplit: string[]) =>
- this.geodataAdmin2Repository.create({
- key: lineSplit[0],
- name: lineSplit[1],
- }),
- geodataAdmin2Path,
- GeodataAdmin2Entity,
- );
+ const input = createReadStream(filePath);
+ const lineReader = readLine.createInterface({ input: input });
+
+ const adminMap = new Map();
+ for await (const line of lineReader) {
+ const lineSplit = line.split('\t');
+ adminMap.set(lineSplit[0], lineSplit[1]);
+ }
+
+ return adminMap;
}
async teardown() {
@@ -167,8 +156,6 @@ export class MetadataRepository implements IMetadataRepository {
const response = await this.geodataPlacesRepository
.createQueryBuilder('geoplaces')
- .leftJoinAndSelect('geoplaces.admin1', 'admin1')
- .leftJoinAndSelect('geoplaces.admin2', 'admin2')
.where('earth_box(ll_to_earth(:latitude, :longitude), 25000) @> "earthCoord"', point)
.orderBy('earth_distance(ll_to_earth(:latitude, :longitude), "earthCoord")')
.limit(1)
@@ -183,9 +170,9 @@ export class MetadataRepository implements IMetadataRepository {
this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
- const { countryCode, name: city, admin1, admin2 } = response;
+ const { countryCode, name: city, admin1Name, admin2Name } = response;
const country = getName(countryCode, 'en') ?? null;
- const stateParts = [admin2?.name, admin1?.name].filter((name) => !!name);
+ const stateParts = [admin2Name, admin1Name].filter((name) => !!name);
const state = stateParts.length > 0 ? stateParts.join(', ') : null;
return { country, state, city };
diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts
index 63b3d570ef..3a7ec29466 100644
--- a/server/src/infra/repositories/person.repository.ts
+++ b/server/src/infra/repositories/person.repository.ts
@@ -40,11 +40,11 @@ export class PersonRepository implements IPersonRepository {
}
async deleteAll(): Promise {
- await this.personRepository.delete({});
+ await this.personRepository.clear();
}
async deleteAllFaces(): Promise {
- await this.assetFaceRepository.delete({});
+ await this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE');
}
getAllFaces(
diff --git a/server/src/infra/repositories/search.repository.ts b/server/src/infra/repositories/search.repository.ts
index a30c96b10d..c8dc5070f7 100644
--- a/server/src/infra/repositories/search.repository.ts
+++ b/server/src/infra/repositories/search.repository.ts
@@ -12,7 +12,13 @@ import {
SmartSearchOptions,
} from '@app/domain';
import { getCLIPModelInfo } from '@app/domain/smart-info/smart-info.constant';
-import { AssetEntity, AssetFaceEntity, SmartInfoEntity, SmartSearchEntity } from '@app/infra/entities';
+import {
+ AssetEntity,
+ AssetFaceEntity,
+ GeodataPlacesEntity,
+ SmartInfoEntity,
+ SmartSearchEntity,
+} from '@app/infra/entities';
import { ImmichLogger } from '@app/infra/logger';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
@@ -31,6 +37,7 @@ export class SearchRepository implements ISearchRepository {
@InjectRepository(AssetEntity) private assetRepository: Repository,
@InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository,
@InjectRepository(SmartSearchEntity) private smartSearchRepository: Repository,
+ @InjectRepository(GeodataPlacesEntity) private readonly geodataPlacesRepository: Repository,
) {
this.faceColumns = this.assetFaceRepository.manager.connection
.getMetadata(AssetFaceEntity)
@@ -172,6 +179,27 @@ export class SearchRepository implements ISearchRepository {
}));
}
+ @GenerateSql({ params: [DummyValue.STRING] })
+ async searchPlaces(placeName: string): Promise {
+ return await this.geodataPlacesRepository
+ .createQueryBuilder('geoplaces')
+ .where(`f_unaccent(name) %>> f_unaccent(:placeName)`)
+ .orWhere(`f_unaccent("admin2Name") %>> f_unaccent(:placeName)`)
+ .orWhere(`f_unaccent("admin1Name") %>> f_unaccent(:placeName)`)
+ .orWhere(`f_unaccent("alternateNames") %>> f_unaccent(:placeName)`)
+ .orderBy(
+ `
+ COALESCE(f_unaccent(name) <->>> f_unaccent(:placeName), 0) +
+ COALESCE(f_unaccent("admin2Name") <->>> f_unaccent(:placeName), 0) +
+ COALESCE(f_unaccent("admin1Name") <->>> f_unaccent(:placeName), 0) +
+ COALESCE(f_unaccent("alternateNames") <->>> f_unaccent(:placeName), 0)
+ `,
+ )
+ .setParameters({ placeName })
+ .limit(20)
+ .getMany();
+ }
+
async upsert(smartInfo: Partial, embedding?: Embedding): Promise {
await this.repository.upsert(smartInfo, { conflictPaths: ['assetId'] });
if (!smartInfo.assetId || !embedding) {
@@ -201,25 +229,17 @@ export class SearchRepository implements ISearchRepository {
this.logger.log(`Updating database CLIP dimension size to ${dimSize}.`);
await this.smartSearchRepository.manager.transaction(async (manager) => {
- await manager.query(`DROP TABLE smart_search`);
-
- await manager.query(`
- CREATE TABLE smart_search (
- "assetId" uuid PRIMARY KEY REFERENCES assets(id) ON DELETE CASCADE,
- embedding vector(${dimSize}) NOT NULL )`);
-
- await manager.query(`
- CREATE INDEX clip_index ON smart_search
- USING vectors (embedding vector_cos_ops) WITH (options = $$
- [indexing.hnsw]
- m = 16
- ef_construction = 300
- $$)`);
+ await manager.clear(SmartSearchEntity);
+ await manager.query(`ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`);
});
this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`);
}
+ deleteAllSearchEmbeddings(): Promise {
+ return this.smartSearchRepository.clear();
+ }
+
private async getDimSize(): Promise {
const res = await this.smartSearchRepository.manager.query(`
SELECT atttypmod as dimsize
diff --git a/server/src/infra/sql/search.repository.sql b/server/src/infra/sql/search.repository.sql
index a21697c268..c45d90a7a3 100644
--- a/server/src/infra/sql/search.repository.sql
+++ b/server/src/infra/sql/search.repository.sql
@@ -238,3 +238,37 @@ FROM
WHERE
res.distance <= $3
COMMIT
+
+-- SearchRepository.searchPlaces
+SELECT
+ "geoplaces"."id" AS "geoplaces_id",
+ "geoplaces"."name" AS "geoplaces_name",
+ "geoplaces"."longitude" AS "geoplaces_longitude",
+ "geoplaces"."latitude" AS "geoplaces_latitude",
+ "geoplaces"."countryCode" AS "geoplaces_countryCode",
+ "geoplaces"."admin1Code" AS "geoplaces_admin1Code",
+ "geoplaces"."admin2Code" AS "geoplaces_admin2Code",
+ "geoplaces"."admin1Name" AS "geoplaces_admin1Name",
+ "geoplaces"."admin2Name" AS "geoplaces_admin2Name",
+ "geoplaces"."alternateNames" AS "geoplaces_alternateNames",
+ "geoplaces"."modificationDate" AS "geoplaces_modificationDate"
+FROM
+ "geodata_places" "geoplaces"
+WHERE
+ f_unaccent (name) %>> f_unaccent ($1)
+ OR f_unaccent ("admin2Name") %>> f_unaccent ($1)
+ OR f_unaccent ("admin1Name") %>> f_unaccent ($1)
+ OR f_unaccent ("alternateNames") %>> f_unaccent ($1)
+ORDER BY
+ COALESCE(f_unaccent (name) <->>> f_unaccent ($1), 0) + COALESCE(
+ f_unaccent ("admin2Name") <->>> f_unaccent ($1),
+ 0
+ ) + COALESCE(
+ f_unaccent ("admin1Name") <->>> f_unaccent ($1),
+ 0
+ ) + COALESCE(
+ f_unaccent ("alternateNames") <->>> f_unaccent ($1),
+ 0
+ ) ASC
+LIMIT
+ 20
diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts
index e0bdab269a..5912d77451 100644
--- a/server/test/repositories/search.repository.mock.ts
+++ b/server/test/repositories/search.repository.mock.ts
@@ -7,5 +7,7 @@ export const newSearchRepositoryMock = (): jest.Mocked => {
searchSmart: jest.fn(),
searchFaces: jest.fn(),
upsert: jest.fn(),
+ searchPlaces: jest.fn(),
+ deleteAllSearchEmbeddings: jest.fn(),
};
};
diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs
index ef17242c8e..2b89e5dc7d 100644
--- a/web/.eslintrc.cjs
+++ b/web/.eslintrc.cjs
@@ -13,6 +13,7 @@ module.exports = {
sourceType: 'module',
ecmaVersion: 2022,
extraFileExtensions: ['.svelte'],
+ project: ['./tsconfig.json'],
},
env: {
browser: true,
@@ -32,13 +33,6 @@ module.exports = {
NodeJS: true,
},
rules: {
- 'unicorn/no-useless-undefined': 'off',
- 'unicorn/prefer-spread': 'off',
- 'unicorn/no-null': 'off',
- 'unicorn/prevent-abbreviations': 'off',
- 'unicorn/no-nested-ternary': 'off',
- 'unicorn/consistent-function-scoping': 'off',
- 'unicorn/prefer-top-level-await': 'off',
'@typescript-eslint/no-unused-vars': [
'warn',
{
@@ -48,5 +42,17 @@ module.exports = {
},
],
curly: 2,
+ 'unicorn/no-useless-undefined': 'off',
+ 'unicorn/prefer-spread': 'off',
+ 'unicorn/no-null': 'off',
+ 'unicorn/prevent-abbreviations': 'off',
+ 'unicorn/no-nested-ternary': 'off',
+ 'unicorn/consistent-function-scoping': 'off',
+ 'unicorn/prefer-top-level-await': 'off',
+ // TODO: set recommended-type-checked and remove these rules
+ '@typescript-eslint/await-thenable': 'error',
+ '@typescript-eslint/no-floating-promises': 'error',
+ '@typescript-eslint/no-misused-promises': 'error',
+ '@typescript-eslint/require-await': 'error',
},
};
diff --git a/web/package-lock.json b/web/package-lock.json
index 78e5caf7c5..77a875e517 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -10,58 +10,58 @@
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@immich/sdk": "file:../open-api/typescript-sdk",
- "@mdi/js": "^7.3.67",
- "@photo-sphere-viewer/core": "^5.7.0",
- "@zoom-image/svelte": "^0.2.0",
+ "@mdi/js": "^7.4.47",
+ "@photo-sphere-viewer/core": "^5.7.1",
+ "@zoom-image/svelte": "^0.2.6",
"axios": "^1.6.7",
"buffer": "^6.0.3",
"copy-image-clipboard": "^2.1.2",
"dom-to-image": "^2.6.0",
- "handlebars": "^4.7.7",
+ "handlebars": "^4.7.8",
"justified-layout": "^4.1.0",
"lodash-es": "^4.17.21",
- "luxon": "^3.2.1",
- "socket.io-client": "^4.6.1",
- "svelte-local-storage-store": "^0.6.0",
- "svelte-maplibre": "^0.8.0",
+ "luxon": "^3.4.4",
+ "socket.io-client": "^4.7.4",
+ "svelte-local-storage-store": "^0.6.4",
+ "svelte-maplibre": "^0.8.1",
"thumbhash": "^0.1.1"
},
"devDependencies": {
- "@faker-js/faker": "^8.0.0",
- "@floating-ui/dom": "^1.5.1",
+ "@faker-js/faker": "^8.4.1",
+ "@floating-ui/dom": "^1.6.3",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/enhanced-img": "^0.1.8",
- "@sveltejs/kit": "^2.5.1",
+ "@sveltejs/kit": "^2.5.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
- "@testing-library/jest-dom": "^6.1.5",
- "@testing-library/svelte": "^4.0.3",
- "@types/dom-to-image": "^2.6.4",
- "@types/justified-layout": "^4.1.0",
- "@types/lodash-es": "^4.17.6",
- "@types/luxon": "^3.2.0",
- "@typescript-eslint/eslint-plugin": "^7.0.0",
- "@typescript-eslint/parser": "^7.0.0",
- "@vitest/coverage-v8": "^1.0.4",
- "autoprefixer": "^10.4.13",
- "eslint": "^8.34.0",
+ "@testing-library/jest-dom": "^6.4.2",
+ "@testing-library/svelte": "^4.1.0",
+ "@types/dom-to-image": "^2.6.7",
+ "@types/justified-layout": "^4.1.4",
+ "@types/lodash-es": "^4.17.12",
+ "@types/luxon": "^3.4.2",
+ "@typescript-eslint/eslint-plugin": "^7.1.0",
+ "@typescript-eslint/parser": "^7.1.0",
+ "@vitest/coverage-v8": "^1.3.1",
+ "autoprefixer": "^10.4.17",
+ "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
- "eslint-plugin-svelte": "^2.30.0",
- "eslint-plugin-unicorn": "^51.0.0",
- "factory.ts": "^1.3.0",
+ "eslint-plugin-svelte": "^2.35.1",
+ "eslint-plugin-unicorn": "^51.0.1",
+ "factory.ts": "^1.4.1",
"identity-obj-proxy": "^3.0.0",
- "postcss": "^8.4.21",
- "prettier": "^3.1.0",
+ "postcss": "^8.4.35",
+ "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
- "prettier-plugin-svelte": "^3.1.2",
+ "prettier-plugin-svelte": "^3.2.1",
"rollup-plugin-visualizer": "^5.12.0",
- "svelte": "^4.2.11",
- "svelte-check": "^3.6.4",
- "tailwindcss": "^3.2.7",
- "tslib": "^2.5.0",
+ "svelte": "^4.2.12",
+ "svelte-check": "^3.6.5",
+ "tailwindcss": "^3.4.1",
+ "tslib": "^2.6.2",
"typescript": "^5.3.3",
- "vite": "^5.1.1",
- "vitest": "^1.0.4"
+ "vite": "^5.1.4",
+ "vitest": "^1.3.1"
}
},
"../open-api/typescript-sdk": {
@@ -898,9 +898,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
- "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
+ "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -948,13 +948,13 @@
"dev": true
},
"node_modules/@humanwhocodes/config-array": {
- "version": "0.11.13",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
- "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
+ "version": "0.11.14",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
+ "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"dev": true,
"dependencies": {
- "@humanwhocodes/object-schema": "^2.0.1",
- "debug": "^4.1.1",
+ "@humanwhocodes/object-schema": "^2.0.2",
+ "debug": "^4.3.1",
"minimatch": "^3.0.5"
},
"engines": {
@@ -975,9 +975,9 @@
}
},
"node_modules/@humanwhocodes/object-schema": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
- "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
+ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true
},
"node_modules/@img/sharp-darwin-arm64": {
@@ -1859,9 +1859,9 @@
}
},
"node_modules/@sveltejs/kit": {
- "version": "2.5.1",
- "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.1.tgz",
- "integrity": "sha512-TKj08o3mJCoQNLTdRdGkHPePTCPUGTgkew65RDqjVU3MtPVxljsofXQYfXndHfq0P7KoPRO/0/reF6HesU0Djw==",
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.2.tgz",
+ "integrity": "sha512-1Pm2lsBYURQsjnLyZa+jw75eVD4gYHxGRwPyFe4DAmB3FjTVR8vRNWGeuDLGFcKMh/B1ij6FTUrc9GrerogCng==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
@@ -2284,9 +2284,9 @@
"dev": true
},
"node_modules/@types/semver": {
- "version": "7.5.7",
- "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz",
- "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==",
+ "version": "7.5.8",
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
+ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@types/supercluster": {
@@ -2298,16 +2298,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.1.tgz",
- "integrity": "sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz",
+ "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
- "@typescript-eslint/scope-manager": "7.0.1",
- "@typescript-eslint/type-utils": "7.0.1",
- "@typescript-eslint/utils": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1",
+ "@typescript-eslint/scope-manager": "7.1.0",
+ "@typescript-eslint/type-utils": "7.1.0",
+ "@typescript-eslint/utils": "7.1.0",
+ "@typescript-eslint/visitor-keys": "7.1.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@@ -2366,15 +2366,15 @@
"dev": true
},
"node_modules/@typescript-eslint/parser": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.1.tgz",
- "integrity": "sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz",
+ "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==",
"dev": true,
"dependencies": {
- "@typescript-eslint/scope-manager": "7.0.1",
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/typescript-estree": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1",
+ "@typescript-eslint/scope-manager": "7.1.0",
+ "@typescript-eslint/types": "7.1.0",
+ "@typescript-eslint/typescript-estree": "7.1.0",
+ "@typescript-eslint/visitor-keys": "7.1.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2394,13 +2394,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.1.tgz",
- "integrity": "sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz",
+ "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1"
+ "@typescript-eslint/types": "7.1.0",
+ "@typescript-eslint/visitor-keys": "7.1.0"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -2411,13 +2411,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.1.tgz",
- "integrity": "sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz",
+ "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==",
"dev": true,
"dependencies": {
- "@typescript-eslint/typescript-estree": "7.0.1",
- "@typescript-eslint/utils": "7.0.1",
+ "@typescript-eslint/typescript-estree": "7.1.0",
+ "@typescript-eslint/utils": "7.1.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@@ -2438,9 +2438,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.1.tgz",
- "integrity": "sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz",
+ "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -2451,13 +2451,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.1.tgz",
- "integrity": "sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz",
+ "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/visitor-keys": "7.0.1",
+ "@typescript-eslint/types": "7.1.0",
+ "@typescript-eslint/visitor-keys": "7.1.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -2536,17 +2536,17 @@
"dev": true
},
"node_modules/@typescript-eslint/utils": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.1.tgz",
- "integrity": "sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz",
+ "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
- "@typescript-eslint/scope-manager": "7.0.1",
- "@typescript-eslint/types": "7.0.1",
- "@typescript-eslint/typescript-estree": "7.0.1",
+ "@typescript-eslint/scope-manager": "7.1.0",
+ "@typescript-eslint/types": "7.1.0",
+ "@typescript-eslint/typescript-estree": "7.1.0",
"semver": "^7.5.4"
},
"engines": {
@@ -2594,12 +2594,12 @@
"dev": true
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "7.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.1.tgz",
- "integrity": "sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==",
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz",
+ "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==",
"dev": true,
"dependencies": {
- "@typescript-eslint/types": "7.0.1",
+ "@typescript-eslint/types": "7.1.0",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -2617,24 +2617,23 @@
"dev": true
},
"node_modules/@vitest/browser": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.0.4.tgz",
- "integrity": "sha512-qMT1NhClex73eA2sOwnlwLcSIVCW8B7NFVzIKuXLKxSJD3LsNq8PCKhwOkBxklbSAcZdkOgL/d3/gzQT7k9eng==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-1.3.1.tgz",
+ "integrity": "sha512-pRof8G8nqRWwg3ouyIctyhfIVk5jXgF056uF//sqdi37+pVtDz9kBI/RMu0xlc8tgCyJ2aEMfbgJZPUydlEVaQ==",
"dev": true,
"optional": true,
"peer": true,
"dependencies": {
- "estree-walker": "^3.0.3",
+ "@vitest/utils": "1.3.1",
"magic-string": "^0.30.5",
- "sirv": "^2.0.3"
+ "sirv": "^2.0.4"
},
"funding": {
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"playwright": "*",
- "safaridriver": "*",
- "vitest": "^1.0.0",
+ "vitest": "1.3.1",
"webdriverio": "*"
},
"peerDependenciesMeta": {
@@ -2650,9 +2649,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.2.2.tgz",
- "integrity": "sha512-IHyKnDz18SFclIEEAHb9Y4Uxx0sPKC2VO1kdDCs1BF6Ip4S8rQprs971zIsooLUn7Afs71GRxWMWpkCGZpRMhw==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.3.1.tgz",
+ "integrity": "sha512-UuBnkSJUNE9rdHjDCPyJ4fYuMkoMtnghes1XohYa4At0MS3OQSAo97FrbwSLRshYsXThMZy1+ybD/byK5llyIg==",
"dev": true,
"dependencies": {
"@ampproject/remapping": "^2.2.1",
@@ -2673,17 +2672,17 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "vitest": "^1.0.0"
+ "vitest": "1.3.1"
}
},
"node_modules/@vitest/expect": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.2.2.tgz",
- "integrity": "sha512-3jpcdPAD7LwHUUiT2pZTj2U82I2Tcgg2oVPvKxhn6mDI2On6tfvPQTjAI4628GUGDZrCm4Zna9iQHm5cEexOAg==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.3.1.tgz",
+ "integrity": "sha512-xofQFwIzfdmLLlHa6ag0dPV8YsnKOCP1KdAeVVh34vSjN2dcUiXYCD9htu/9eM7t8Xln4v03U9HLxLpPlsXdZw==",
"dev": true,
"dependencies": {
- "@vitest/spy": "1.2.2",
- "@vitest/utils": "1.2.2",
+ "@vitest/spy": "1.3.1",
+ "@vitest/utils": "1.3.1",
"chai": "^4.3.10"
},
"funding": {
@@ -2691,12 +2690,12 @@
}
},
"node_modules/@vitest/runner": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.2.2.tgz",
- "integrity": "sha512-JctG7QZ4LSDXr5CsUweFgcpEvrcxOV1Gft7uHrvkQ+fsAVylmWQvnaAr/HDp3LAH1fztGMQZugIheTWjaGzYIg==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.3.1.tgz",
+ "integrity": "sha512-5FzF9c3jG/z5bgCnjr8j9LNq/9OxV2uEBAITOXfoe3rdZJTdO7jzThth7FXv/6b+kdY65tpRQB7WaKhNZwX+Kg==",
"dev": true,
"dependencies": {
- "@vitest/utils": "1.2.2",
+ "@vitest/utils": "1.3.1",
"p-limit": "^5.0.0",
"pathe": "^1.1.1"
},
@@ -2732,9 +2731,9 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.2.2.tgz",
- "integrity": "sha512-SmGY4saEw1+bwE1th6S/cZmPxz/Q4JWsl7LvbQIky2tKE35US4gd0Mjzqfr84/4OD0tikGWaWdMja/nWL5NIPA==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.3.1.tgz",
+ "integrity": "sha512-EF++BZbt6RZmOlE3SuTPu/NfwBF6q4ABS37HHXzs2LUVPBLx2QoY/K0fKpRChSo8eLiuxcbCVfqKgx/dplCDuQ==",
"dev": true,
"dependencies": {
"magic-string": "^0.30.5",
@@ -2778,9 +2777,9 @@
"dev": true
},
"node_modules/@vitest/spy": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.2.2.tgz",
- "integrity": "sha512-k9Gcahssw8d7X3pSLq3e3XEu/0L78mUkCjivUqCQeXJm9clfXR/Td8+AP+VC1O6fKPIDLcHDTAmBOINVuv6+7g==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.3.1.tgz",
+ "integrity": "sha512-xAcW+S099ylC9VLU7eZfdT9myV67Nor9w9zhf0mGCYJSO+zM2839tOeROTdikOi/8Qeusffvxb/MyBSOja1Uig==",
"dev": true,
"dependencies": {
"tinyspy": "^2.2.0"
@@ -2790,9 +2789,9 @@
}
},
"node_modules/@vitest/utils": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.2.2.tgz",
- "integrity": "sha512-WKITBHLsBHlpjnDQahr+XK6RE7MiAsgrIkr0pGhQ9ygoxBfUeG0lUG5iLlzqjmKSlBv3+j5EGsriBzh+C3Tq9g==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.3.1.tgz",
+ "integrity": "sha512-d3Waie/299qqRyHTm2DjADeTaNdNSVsnwHPWrs20JMpjh6eiVq7ggggweO8rc4arhf6rRkWuHKwvxGvejUXZZQ==",
"dev": true,
"dependencies": {
"diff-sequences": "^29.6.3",
@@ -2837,9 +2836,9 @@
"dev": true
},
"node_modules/@zoom-image/core": {
- "version": "0.32.1",
- "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.32.1.tgz",
- "integrity": "sha512-R56D749Ck+/1yLWlEJ2FctxjdpTQEje3jPhOAbeEZGzLndIumskO42UqRNixcER6sAzCi01oYopmqnCpDElF0g==",
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.33.0.tgz",
+ "integrity": "sha512-wkMV8+aE7PeknLFhpIb/6vwRl09Z2gWM4UqKdnXO6Mb0pP9BiuDLcLvGGGB4o++uAPINgDwmNn+Loo641XSjDA==",
"dependencies": {
"@namnode/store": "^0.1.0"
},
@@ -2849,11 +2848,11 @@
}
},
"node_modules/@zoom-image/svelte": {
- "version": "0.2.4",
- "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.4.tgz",
- "integrity": "sha512-rgfgn7Q60VrwmE4MPBzDWaFplc+411Lxg1nMdAnq/UTv4HTWSpiwm1IOg8gQZjRp92a8RXcRmUYXU+wFKEMjSg==",
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.6.tgz",
+ "integrity": "sha512-dEpA/egmTjVcptwhtcKHvkhVMTzQCpH17erfcXuJByt+nn5Oo4LnZOxE8gwSVEdPp65Ns6Y/byYD0GSQ/vv+DQ==",
"dependencies": {
- "@zoom-image/core": "0.32.1"
+ "@zoom-image/core": "0.33.0"
},
"funding": {
"type": "github",
@@ -4055,16 +4054,16 @@
}
},
"node_modules/eslint": {
- "version": "8.56.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
- "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
+ "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
- "@eslint/js": "8.56.0",
- "@humanwhocodes/config-array": "^0.11.13",
+ "@eslint/js": "8.57.0",
+ "@humanwhocodes/config-array": "^0.11.14",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
"@ungap/structured-clone": "^1.2.0",
@@ -7991,17 +7990,23 @@
}
},
"node_modules/strip-literal": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-1.3.0.tgz",
- "integrity": "sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==",
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz",
+ "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==",
"dev": true,
"dependencies": {
- "acorn": "^8.10.0"
+ "js-tokens": "^8.0.2"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
+ "node_modules/strip-literal/node_modules/js-tokens": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz",
+ "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==",
+ "dev": true
+ },
"node_modules/sucrase": {
"version": "3.34.0",
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz",
@@ -8077,9 +8082,9 @@
}
},
"node_modules/svelte": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.11.tgz",
- "integrity": "sha512-YIQk3J4X89wOLhjsqIW8tqY3JHPuBdtdOIkASP2PZeAMcSW9RsIjQzMesCrxOF3gdWYC0mKknlKF7OqmLM+Zqg==",
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.12.tgz",
+ "integrity": "sha512-d8+wsh5TfPwqVzbm4/HCXC783/KPHV60NvwitJnyTA5lWn1elhXMNWhXGCJ7PwPa8qFUnyJNIyuIRt2mT0WMug==",
"dependencies": {
"@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
@@ -8101,9 +8106,9 @@
}
},
"node_modules/svelte-check": {
- "version": "3.6.4",
- "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.4.tgz",
- "integrity": "sha512-mY/dqucqm46p72M8yZmn81WPZx9mN6uuw8UVfR3ZKQeLxQg5HDGO3HHm5AZuWZPYNMLJ+TRMn+TeN53HfQ/vsw==",
+ "version": "3.6.5",
+ "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.5.tgz",
+ "integrity": "sha512-5aLgoQEdadvp8ypvKQ2avhnQ+V9YPQQaWrTFlXFw5g/v8xIQBvo+X/WqxTyD+V/ItDqXg3+abUA53rdDHgUjCA==",
"dev": true,
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.17",
@@ -8173,9 +8178,9 @@
}
},
"node_modules/svelte-maplibre": {
- "version": "0.8.0",
- "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.8.0.tgz",
- "integrity": "sha512-sRSj/zQa7LTfHNIzKcYe+sa9qHClt/OAXcdPQ0w3ksLbCMmVHGk4B2yIXHCVk0g4sc18M85N8KGsHVtZoNC+Mw==",
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.8.1.tgz",
+ "integrity": "sha512-CTm/s0+mJzBHSoO5zPKBo3ORmUyiWS3Ex4xvVdNgVg+sDesHasEAJ0N1/NUrd56S33zgRdFZGzRnRguCnKFAzw==",
"dependencies": {
"d3-geo": "^3.1.0",
"just-compare": "^2.3.0",
@@ -8456,9 +8461,9 @@
"integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA=="
},
"node_modules/tinyspy": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.0.tgz",
- "integrity": "sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==",
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz",
+ "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==",
"dev": true,
"engines": {
"node": ">=14.0.0"
@@ -8733,9 +8738,9 @@
}
},
"node_modules/vite": {
- "version": "5.1.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz",
- "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==",
+ "version": "5.1.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.4.tgz",
+ "integrity": "sha512-n+MPqzq+d9nMVTKyewqw6kSt+R3CkvF9QAKY8obiQn8g1fwTscKxyfaYnC632HtBXAQGc1Yjomphwn1dtwGAHg==",
"dev": true,
"dependencies": {
"esbuild": "^0.19.3",
@@ -8801,9 +8806,9 @@
}
},
"node_modules/vite-node": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.2.2.tgz",
- "integrity": "sha512-1as4rDTgVWJO3n1uHmUYqq7nsFgINQ9u+mRcXpjeOMJUmviqNKjcZB7UfRZrlM7MjYXMKpuWp5oGkjaFLnjawg==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.3.1.tgz",
+ "integrity": "sha512-azbRrqRxlWTJEVbzInZCTchx0X69M/XPTCz4H+TLvlTcR/xH/3hkRqhOakT41fMJCMzXTu4UvegkZiEoJAWvng==",
"dev": true,
"dependencies": {
"cac": "^6.7.14",
@@ -8837,18 +8842,17 @@
}
},
"node_modules/vitest": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.2.2.tgz",
- "integrity": "sha512-d5Ouvrnms3GD9USIK36KG8OZ5bEvKEkITFtnGv56HFaSlbItJuYr7hv2Lkn903+AvRAgSixiamozUVfORUekjw==",
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.3.1.tgz",
+ "integrity": "sha512-/1QJqXs8YbCrfv/GPQ05wAZf2eakUPLPa18vkJAKE7RXOKfVHqMZZ1WlTjiwl6Gcn65M5vpNUB6EFLnEdRdEXQ==",
"dev": true,
"dependencies": {
- "@vitest/expect": "1.2.2",
- "@vitest/runner": "1.2.2",
- "@vitest/snapshot": "1.2.2",
- "@vitest/spy": "1.2.2",
- "@vitest/utils": "1.2.2",
+ "@vitest/expect": "1.3.1",
+ "@vitest/runner": "1.3.1",
+ "@vitest/snapshot": "1.3.1",
+ "@vitest/spy": "1.3.1",
+ "@vitest/utils": "1.3.1",
"acorn-walk": "^8.3.2",
- "cac": "^6.7.14",
"chai": "^4.3.10",
"debug": "^4.3.4",
"execa": "^8.0.1",
@@ -8857,11 +8861,11 @@
"pathe": "^1.1.1",
"picocolors": "^1.0.0",
"std-env": "^3.5.0",
- "strip-literal": "^1.3.0",
+ "strip-literal": "^2.0.0",
"tinybench": "^2.5.1",
"tinypool": "^0.8.2",
"vite": "^5.0.0",
- "vite-node": "1.2.2",
+ "vite-node": "1.3.1",
"why-is-node-running": "^2.2.2"
},
"bin": {
@@ -8876,8 +8880,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
- "@vitest/browser": "^1.0.0",
- "@vitest/ui": "^1.0.0",
+ "@vitest/browser": "1.3.1",
+ "@vitest/ui": "1.3.1",
"happy-dom": "*",
"jsdom": "*"
},
diff --git a/web/package.json b/web/package.json
index 2b53d06451..51f07dded0 100644
--- a/web/package.json
+++ b/web/package.json
@@ -22,59 +22,59 @@
"prepare": "svelte-kit sync"
},
"devDependencies": {
- "@faker-js/faker": "^8.0.0",
- "@floating-ui/dom": "^1.5.1",
+ "@faker-js/faker": "^8.4.1",
+ "@floating-ui/dom": "^1.6.3",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/enhanced-img": "^0.1.8",
- "@sveltejs/kit": "^2.5.1",
+ "@sveltejs/kit": "^2.5.2",
"@sveltejs/vite-plugin-svelte": "^3.0.2",
- "@testing-library/jest-dom": "^6.1.5",
- "@testing-library/svelte": "^4.0.3",
- "@types/dom-to-image": "^2.6.4",
- "@types/justified-layout": "^4.1.0",
- "@types/lodash-es": "^4.17.6",
- "@types/luxon": "^3.2.0",
- "@typescript-eslint/eslint-plugin": "^7.0.0",
- "@typescript-eslint/parser": "^7.0.0",
- "@vitest/coverage-v8": "^1.0.4",
- "autoprefixer": "^10.4.13",
- "eslint": "^8.34.0",
+ "@testing-library/jest-dom": "^6.4.2",
+ "@testing-library/svelte": "^4.1.0",
+ "@types/dom-to-image": "^2.6.7",
+ "@types/justified-layout": "^4.1.4",
+ "@types/lodash-es": "^4.17.12",
+ "@types/luxon": "^3.4.2",
+ "@typescript-eslint/eslint-plugin": "^7.1.0",
+ "@typescript-eslint/parser": "^7.1.0",
+ "@vitest/coverage-v8": "^1.3.1",
+ "autoprefixer": "^10.4.17",
+ "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
- "eslint-plugin-svelte": "^2.30.0",
- "eslint-plugin-unicorn": "^51.0.0",
- "factory.ts": "^1.3.0",
+ "eslint-plugin-svelte": "^2.35.1",
+ "eslint-plugin-unicorn": "^51.0.1",
+ "factory.ts": "^1.4.1",
"identity-obj-proxy": "^3.0.0",
- "postcss": "^8.4.21",
- "prettier": "^3.1.0",
+ "postcss": "^8.4.35",
+ "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
- "prettier-plugin-svelte": "^3.1.2",
+ "prettier-plugin-svelte": "^3.2.1",
"rollup-plugin-visualizer": "^5.12.0",
- "svelte": "^4.2.11",
- "svelte-check": "^3.6.4",
- "tailwindcss": "^3.2.7",
- "tslib": "^2.5.0",
+ "svelte": "^4.2.12",
+ "svelte-check": "^3.6.5",
+ "tailwindcss": "^3.4.1",
+ "tslib": "^2.6.2",
"typescript": "^5.3.3",
- "vite": "^5.1.1",
- "vitest": "^1.0.4"
+ "vite": "^5.1.4",
+ "vitest": "^1.3.1"
},
"type": "module",
"dependencies": {
"@immich/sdk": "file:../open-api/typescript-sdk",
- "@mdi/js": "^7.3.67",
- "@photo-sphere-viewer/core": "^5.7.0",
- "@zoom-image/svelte": "^0.2.0",
+ "@mdi/js": "^7.4.47",
+ "@photo-sphere-viewer/core": "^5.7.1",
+ "@zoom-image/svelte": "^0.2.6",
"axios": "^1.6.7",
"buffer": "^6.0.3",
"copy-image-clipboard": "^2.1.2",
"dom-to-image": "^2.6.0",
- "handlebars": "^4.7.7",
+ "handlebars": "^4.7.8",
"justified-layout": "^4.1.0",
"lodash-es": "^4.17.21",
- "luxon": "^3.2.1",
- "socket.io-client": "^4.6.1",
- "svelte-local-storage-store": "^0.6.0",
- "svelte-maplibre": "^0.8.0",
+ "luxon": "^3.4.4",
+ "socket.io-client": "^4.7.4",
+ "svelte-local-storage-store": "^0.6.4",
+ "svelte-maplibre": "^0.8.1",
"thumbhash": "^0.1.1"
}
}
diff --git a/web/src/hooks.client.ts b/web/src/hooks.client.ts
index 1e29371fa9..f30e3ee8ca 100644
--- a/web/src/hooks.client.ts
+++ b/web/src/hooks.client.ts
@@ -1,40 +1,27 @@
+import { isHttpError } from '@immich/sdk';
import type { HandleClientError } from '@sveltejs/kit';
-import type { AxiosError, AxiosResponse } from 'axios';
-const LOG_PREFIX = '[hooks.client.ts]';
const DEFAULT_MESSAGE = 'Hmm, not sure about that. Check the logs or open a ticket?';
const parseError = (error: unknown) => {
- const httpError = error as AxiosError;
- const request = httpError?.request as Request & { path: string };
- const response = httpError?.response as AxiosResponse<{
- message: string;
- statusCode: number;
- error: string;
- }>;
+ const httpError = isHttpError(error) ? error : undefined;
+ const statusCode = httpError?.status || httpError?.data?.statusCode || 500;
+ const message = httpError?.data?.message || (httpError?.data && String(httpError.data)) || httpError?.message;
- let code = response?.data?.statusCode || response?.status || httpError.code || '500';
- if (response) {
- code += ` - ${response.data?.error || response.statusText}`;
- }
-
- if (request && response) {
- console.log({
- status: response.status,
- url: `${request.method} ${request.path}`,
- response: response.data || 'No data',
- });
- }
+ console.log({
+ status: statusCode,
+ response: httpError?.data || 'No data',
+ });
return {
- message: response?.data?.message || httpError?.message || DEFAULT_MESSAGE,
- code,
+ message: message || DEFAULT_MESSAGE,
+ code: statusCode,
stack: httpError?.stack,
};
};
export const handleError: HandleClientError = ({ error }) => {
const result = parseError(error);
- console.error(`${LOG_PREFIX}:handleError ${result.message}`);
+ console.error(`[hooks.client.ts]:handleError ${result.message}`);
return result;
};
diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
index 60756166f2..edf5c48300 100644
--- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
+++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte
@@ -48,11 +48,11 @@
await handleCommand(jobId, dto);
};
- const onConfirm = () => {
+ const onConfirm = async () => {
if (!confirmJob) {
return;
}
- handleCommand(confirmJob, { command: JobCommand.Start, force: true });
+ await handleCommand(confirmJob, { command: JobCommand.Start, force: true });
confirmJob = null;
};
diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-page/settings/admin-settings.svelte
index 1ad962b0de..16b2afc7fe 100644
--- a/web/src/lib/components/admin-page/settings/admin-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/admin-settings.svelte
@@ -54,7 +54,7 @@
});
};
- const resetToDefault = async (configKeys: Array) => {
+ const resetToDefault = (configKeys: Array) => {
for (const key of configKeys) {
config = { ...config, [key]: defaultConfig[key] };
}
diff --git a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
index 6a542d81d4..ba24f3aabd 100644
--- a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte
@@ -112,8 +112,8 @@
desc="Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives."
bind:value={config.machineLearning.facialRecognition.minScore}
step="0.1"
- min="0"
- max="1"
+ min={0}
+ max={1}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
isEdited={config.machineLearning.facialRecognition.minScore !==
savedConfig.machineLearning.facialRecognition.minScore}
@@ -125,8 +125,8 @@
desc="Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible."
bind:value={config.machineLearning.facialRecognition.maxDistance}
step="0.1"
- min="0"
- max="2"
+ min={0}
+ max={2}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
isEdited={config.machineLearning.facialRecognition.maxDistance !==
savedConfig.machineLearning.facialRecognition.maxDistance}
@@ -138,7 +138,7 @@
desc="The minimum number of recognized faces for a person to be created. Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person."
bind:value={config.machineLearning.facialRecognition.minFaces}
step="1"
- min="1"
+ min={1}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
isEdited={config.machineLearning.facialRecognition.minFaces !==
savedConfig.machineLearning.facialRecognition.minFaces}
diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte
index 5fc3b3e222..11e07d0029 100644
--- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte
+++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte
@@ -84,7 +84,26 @@
};
-
+
+
{#await getTemplateOptions() then}
{
if (value.isDragging && value.files.length > 0) {
- fileUploadHandler(value.files, album.id);
+ handlePromiseError(fileUploadHandler(value.files, album.id));
dragAndDropFilesStore.set({ isDragging: false, files: [] });
}
});
@@ -67,7 +68,7 @@
const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
- onMount(async () => {
+ onMount(() => {
document.addEventListener('keydown', onKeyboardPress);
});
diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte
index dff2d7f319..4dd1b75e79 100644
--- a/web/src/lib/components/asset-viewer/activity-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte
@@ -1,7 +1,7 @@
diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte
index 38f65e3df7..7f94857afc 100644
--- a/web/src/lib/components/asset-viewer/detail-panel.svelte
+++ b/web/src/lib/components/asset-viewer/detail-panel.svelte
@@ -7,7 +7,7 @@
import { featureFlags } from '$lib/stores/server-config.store';
import { user } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket';
- import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink } from '$lib/utils';
+ import { getAssetThumbnailUrl, getPeopleThumbnailUrl, isSharedLink, handlePromiseError } from '$lib/utils';
import { delay, getAssetFilename } from '$lib/utils/asset-utils';
import { autoGrowHeight } from '$lib/utils/autogrow';
import { clickOutside } from '$lib/utils/click-outside';
@@ -78,7 +78,7 @@
originalDescription = description;
};
- $: handleNewAsset(asset);
+ $: handlePromiseError(handleNewAsset(asset));
$: latlng = (() => {
const lat = asset.exifInfo?.latitude;
@@ -113,7 +113,7 @@
switch (event.key) {
case 'Enter': {
if (ctrl && event.target === textArea) {
- handleFocusOut();
+ await handleFocusOut();
}
}
}
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte
index e10d5573ca..79e8276153 100644
--- a/web/src/lib/components/asset-viewer/photo-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte
@@ -4,7 +4,7 @@
import { boundingBoxesArray } from '$lib/stores/people.store';
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
import { photoZoomState } from '$lib/stores/zoom-image.store';
- import { getKey } from '$lib/utils';
+ import { getKey, handlePromiseError } from '$lib/utils';
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
import { getBoundingBox } from '$lib/utils/people-utils';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
@@ -102,7 +102,7 @@
}
};
- const doZoomImage = async () => {
+ const doZoomImage = () => {
setZoomImageWheelState({
currentZoom: $zoomImageWheelState.currentZoom === 1 ? 2 : 1,
});
@@ -120,7 +120,7 @@
if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed && !$alwaysLoadOriginalFile) {
hasZoomed = true;
- loadAssetData({ loadOriginal: true });
+ handlePromiseError(loadAssetData({ loadOriginal: true }));
}
});
diff --git a/web/src/lib/components/asset-viewer/slideshow-bar.svelte b/web/src/lib/components/asset-viewer/slideshow-bar.svelte
index c099ae79c4..164b0a7913 100644
--- a/web/src/lib/components/asset-viewer/slideshow-bar.svelte
+++ b/web/src/lib/components/asset-viewer/slideshow-bar.svelte
@@ -1,23 +1,16 @@
- dispatch('close')} title="Exit Slideshow" />
- {#if $slideshowShuffle}
- ($slideshowShuffle = false)} title="Shuffle" />
- {:else}
- ($slideshowShuffle = true)} title="No shuffle" />
- {/if}
+ dispatch('close')} title="Exit Slideshow" />
(progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())}
title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'}
/>
- dispatch('prev')} title="Previous" />
- dispatch('next')} title="Next" />
+ dispatch('prev')} title="Previous" />
+ dispatch('next')} title="Next" />
+ (showSettings = !showSettings)} title="Next" />
+{#if showSettings}
+ (showSettings = false)} />
+{/if}
+
dispatch('next')}
- duration={5000}
/>
diff --git a/web/src/lib/components/asset-viewer/video-viewer.svelte b/web/src/lib/components/asset-viewer/video-viewer.svelte
index 96713d6217..f5ff5f0fc2 100644
--- a/web/src/lib/components/asset-viewer/video-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/video-viewer.svelte
@@ -20,7 +20,7 @@
video.muted = false;
dispatch('onVideoStarted');
} catch (error) {
- await handleError(error, 'Unable to play video');
+ handleError(error, 'Unable to play video');
} finally {
isVideoLoading = false;
}
diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
index ec511d4192..8ee042a1a6 100644
--- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte
+++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte
@@ -3,7 +3,7 @@
import Icon from '$lib/components/elements/icon.svelte';
import { ProjectionType } from '$lib/constants';
import { getAssetFileUrl, getAssetThumbnailUrl, isSharedLink } from '$lib/utils';
- import { timeToSeconds } from '$lib/utils/time-to-seconds';
+ import { timeToSeconds } from '$lib/utils/date-time';
import { AssetTypeEnum, ThumbnailFormat, type AssetResponseDto } from '@immich/sdk';
import {
mdiArchiveArrowDownOutline,
diff --git a/web/src/lib/components/elements/date-input.svelte b/web/src/lib/components/elements/date-input.svelte
new file mode 100644
index 0000000000..e4ec4bcab8
--- /dev/null
+++ b/web/src/lib/components/elements/date-input.svelte
@@ -0,0 +1,24 @@
+
+
+ {
+ updatedValue = e.currentTarget.value;
+
+ // Only update when value is not empty to prevent resetting the input
+ if (updatedValue !== '') {
+ value = updatedValue;
+ }
+ }}
+ on:blur={() => (value = updatedValue)}
+/>
diff --git a/web/src/lib/components/elements/search-bar.svelte b/web/src/lib/components/elements/search-bar.svelte
index 9c6eded224..898601d0ad 100644
--- a/web/src/lib/components/elements/search-bar.svelte
+++ b/web/src/lib/components/elements/search-bar.svelte
@@ -6,6 +6,7 @@
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
export let name: string;
+ export let roundedBottom = true;
export let isSearching: boolean;
export let placeholder: string;
@@ -17,7 +18,11 @@
};
-
+