mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: find large files utility (#18040)
feat: large asset utility Co-authored-by: Jason Rasmussen <jason@rasm.me>
This commit is contained in:
parent
7d759edfcc
commit
ae1d60e259
@ -1154,6 +1154,7 @@
|
|||||||
"language_no_results_title": "No languages found",
|
"language_no_results_title": "No languages found",
|
||||||
"language_search_hint": "Search languages...",
|
"language_search_hint": "Search languages...",
|
||||||
"language_setting_description": "Select your preferred language",
|
"language_setting_description": "Select your preferred language",
|
||||||
|
"large_files": "Large Files",
|
||||||
"last_seen": "Last seen",
|
"last_seen": "Last seen",
|
||||||
"latest_version": "Latest Version",
|
"latest_version": "Latest Version",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
@ -1588,6 +1589,7 @@
|
|||||||
"resume": "Resume",
|
"resume": "Resume",
|
||||||
"retry_upload": "Retry upload",
|
"retry_upload": "Retry upload",
|
||||||
"review_duplicates": "Review duplicates",
|
"review_duplicates": "Review duplicates",
|
||||||
|
"review_large_files": "Review large files",
|
||||||
"role": "Role",
|
"role": "Role",
|
||||||
"role_editor": "Editor",
|
"role_editor": "Editor",
|
||||||
"role_viewer": "Viewer",
|
"role_viewer": "Viewer",
|
||||||
|
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
@ -184,6 +184,7 @@ Class | Method | HTTP request | Description
|
|||||||
*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
|
*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
|
||||||
*SearchApi* | [**searchAssetStatistics**](doc//SearchApi.md#searchassetstatistics) | **POST** /search/statistics |
|
*SearchApi* | [**searchAssetStatistics**](doc//SearchApi.md#searchassetstatistics) | **POST** /search/statistics |
|
||||||
*SearchApi* | [**searchAssets**](doc//SearchApi.md#searchassets) | **POST** /search/metadata |
|
*SearchApi* | [**searchAssets**](doc//SearchApi.md#searchassets) | **POST** /search/metadata |
|
||||||
|
*SearchApi* | [**searchLargeAssets**](doc//SearchApi.md#searchlargeassets) | **POST** /search/large-assets |
|
||||||
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
|
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
|
||||||
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
|
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
|
||||||
*SearchApi* | [**searchRandom**](doc//SearchApi.md#searchrandom) | **POST** /search/random |
|
*SearchApi* | [**searchRandom**](doc//SearchApi.md#searchrandom) | **POST** /search/random |
|
||||||
|
264
mobile/openapi/lib/api/search_api.dart
generated
264
mobile/openapi/lib/api/search_api.dart
generated
@ -287,6 +287,270 @@ class SearchApi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs an HTTP 'POST /search/large-assets' operation and returns the [Response].
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [List<String>] albumIds:
|
||||||
|
///
|
||||||
|
/// * [String] city:
|
||||||
|
///
|
||||||
|
/// * [String] country:
|
||||||
|
///
|
||||||
|
/// * [DateTime] createdAfter:
|
||||||
|
///
|
||||||
|
/// * [DateTime] createdBefore:
|
||||||
|
///
|
||||||
|
/// * [String] deviceId:
|
||||||
|
///
|
||||||
|
/// * [bool] isEncoded:
|
||||||
|
///
|
||||||
|
/// * [bool] isFavorite:
|
||||||
|
///
|
||||||
|
/// * [bool] isMotion:
|
||||||
|
///
|
||||||
|
/// * [bool] isNotInAlbum:
|
||||||
|
///
|
||||||
|
/// * [bool] isOffline:
|
||||||
|
///
|
||||||
|
/// * [String] lensModel:
|
||||||
|
///
|
||||||
|
/// * [String] libraryId:
|
||||||
|
///
|
||||||
|
/// * [String] make:
|
||||||
|
///
|
||||||
|
/// * [int] minFileSize:
|
||||||
|
///
|
||||||
|
/// * [String] model:
|
||||||
|
///
|
||||||
|
/// * [List<String>] personIds:
|
||||||
|
///
|
||||||
|
/// * [num] rating:
|
||||||
|
///
|
||||||
|
/// * [num] size:
|
||||||
|
///
|
||||||
|
/// * [String] state:
|
||||||
|
///
|
||||||
|
/// * [List<String>] tagIds:
|
||||||
|
///
|
||||||
|
/// * [DateTime] takenAfter:
|
||||||
|
///
|
||||||
|
/// * [DateTime] takenBefore:
|
||||||
|
///
|
||||||
|
/// * [DateTime] trashedAfter:
|
||||||
|
///
|
||||||
|
/// * [DateTime] trashedBefore:
|
||||||
|
///
|
||||||
|
/// * [AssetTypeEnum] type:
|
||||||
|
///
|
||||||
|
/// * [DateTime] updatedAfter:
|
||||||
|
///
|
||||||
|
/// * [DateTime] updatedBefore:
|
||||||
|
///
|
||||||
|
/// * [AssetVisibility] visibility:
|
||||||
|
///
|
||||||
|
/// * [bool] withDeleted:
|
||||||
|
///
|
||||||
|
/// * [bool] withExif:
|
||||||
|
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/search/large-assets';
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (albumIds != null) {
|
||||||
|
queryParams.addAll(_queryParams('multi', 'albumIds', albumIds));
|
||||||
|
}
|
||||||
|
if (city != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'city', city));
|
||||||
|
}
|
||||||
|
if (country != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'country', country));
|
||||||
|
}
|
||||||
|
if (createdAfter != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'createdAfter', createdAfter));
|
||||||
|
}
|
||||||
|
if (createdBefore != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'createdBefore', createdBefore));
|
||||||
|
}
|
||||||
|
if (deviceId != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'deviceId', deviceId));
|
||||||
|
}
|
||||||
|
if (isEncoded != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isEncoded', isEncoded));
|
||||||
|
}
|
||||||
|
if (isFavorite != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
|
||||||
|
}
|
||||||
|
if (isMotion != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isMotion', isMotion));
|
||||||
|
}
|
||||||
|
if (isNotInAlbum != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isNotInAlbum', isNotInAlbum));
|
||||||
|
}
|
||||||
|
if (isOffline != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'isOffline', isOffline));
|
||||||
|
}
|
||||||
|
if (lensModel != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'lensModel', lensModel));
|
||||||
|
}
|
||||||
|
if (libraryId != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'libraryId', libraryId));
|
||||||
|
}
|
||||||
|
if (make != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'make', make));
|
||||||
|
}
|
||||||
|
if (minFileSize != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'minFileSize', minFileSize));
|
||||||
|
}
|
||||||
|
if (model != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'model', model));
|
||||||
|
}
|
||||||
|
if (personIds != null) {
|
||||||
|
queryParams.addAll(_queryParams('multi', 'personIds', personIds));
|
||||||
|
}
|
||||||
|
if (rating != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'rating', rating));
|
||||||
|
}
|
||||||
|
if (size != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'size', size));
|
||||||
|
}
|
||||||
|
if (state != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'state', state));
|
||||||
|
}
|
||||||
|
if (tagIds != null) {
|
||||||
|
queryParams.addAll(_queryParams('multi', 'tagIds', tagIds));
|
||||||
|
}
|
||||||
|
if (takenAfter != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'takenAfter', takenAfter));
|
||||||
|
}
|
||||||
|
if (takenBefore != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'takenBefore', takenBefore));
|
||||||
|
}
|
||||||
|
if (trashedAfter != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter));
|
||||||
|
}
|
||||||
|
if (trashedBefore != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'trashedBefore', trashedBefore));
|
||||||
|
}
|
||||||
|
if (type != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'type', type));
|
||||||
|
}
|
||||||
|
if (updatedAfter != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter));
|
||||||
|
}
|
||||||
|
if (updatedBefore != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'updatedBefore', updatedBefore));
|
||||||
|
}
|
||||||
|
if (visibility != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'visibility', visibility));
|
||||||
|
}
|
||||||
|
if (withDeleted != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'withDeleted', withDeleted));
|
||||||
|
}
|
||||||
|
if (withExif != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'withExif', withExif));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'POST',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [List<String>] albumIds:
|
||||||
|
///
|
||||||
|
/// * [String] city:
|
||||||
|
///
|
||||||
|
/// * [String] country:
|
||||||
|
///
|
||||||
|
/// * [DateTime] createdAfter:
|
||||||
|
///
|
||||||
|
/// * [DateTime] createdBefore:
|
||||||
|
///
|
||||||
|
/// * [String] deviceId:
|
||||||
|
///
|
||||||
|
/// * [bool] isEncoded:
|
||||||
|
///
|
||||||
|
/// * [bool] isFavorite:
|
||||||
|
///
|
||||||
|
/// * [bool] isMotion:
|
||||||
|
///
|
||||||
|
/// * [bool] isNotInAlbum:
|
||||||
|
///
|
||||||
|
/// * [bool] isOffline:
|
||||||
|
///
|
||||||
|
/// * [String] lensModel:
|
||||||
|
///
|
||||||
|
/// * [String] libraryId:
|
||||||
|
///
|
||||||
|
/// * [String] make:
|
||||||
|
///
|
||||||
|
/// * [int] minFileSize:
|
||||||
|
///
|
||||||
|
/// * [String] model:
|
||||||
|
///
|
||||||
|
/// * [List<String>] personIds:
|
||||||
|
///
|
||||||
|
/// * [num] rating:
|
||||||
|
///
|
||||||
|
/// * [num] size:
|
||||||
|
///
|
||||||
|
/// * [String] state:
|
||||||
|
///
|
||||||
|
/// * [List<String>] tagIds:
|
||||||
|
///
|
||||||
|
/// * [DateTime] takenAfter:
|
||||||
|
///
|
||||||
|
/// * [DateTime] takenBefore:
|
||||||
|
///
|
||||||
|
/// * [DateTime] trashedAfter:
|
||||||
|
///
|
||||||
|
/// * [DateTime] trashedBefore:
|
||||||
|
///
|
||||||
|
/// * [AssetTypeEnum] type:
|
||||||
|
///
|
||||||
|
/// * [DateTime] updatedAfter:
|
||||||
|
///
|
||||||
|
/// * [DateTime] updatedBefore:
|
||||||
|
///
|
||||||
|
/// * [AssetVisibility] visibility:
|
||||||
|
///
|
||||||
|
/// * [bool] withDeleted:
|
||||||
|
///
|
||||||
|
/// * [bool] withExif:
|
||||||
|
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, String? deviceId, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, List<String>? personIds, num? rating, num? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, }) async {
|
||||||
|
final response = await searchLargeAssetsWithHttpInfo( albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, deviceId: deviceId, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, );
|
||||||
|
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<AssetResponseDto>') as List)
|
||||||
|
.cast<AssetResponseDto>()
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an HTTP 'GET /search/person' operation and returns the [Response].
|
/// Performs an HTTP 'GET /search/person' operation and returns the [Response].
|
||||||
/// Parameters:
|
/// Parameters:
|
||||||
///
|
///
|
||||||
|
@ -5307,6 +5307,323 @@
|
|||||||
"x-immich-permission": "asset.read"
|
"x-immich-permission": "asset.read"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/search/large-assets": {
|
||||||
|
"post": {
|
||||||
|
"operationId": "searchLargeAssets",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "albumIds",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "city",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "country",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "createdAfter",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "createdBefore",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "deviceId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isEncoded",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isFavorite",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isMotion",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isNotInAlbum",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isOffline",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "lensModel",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "libraryId",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "make",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "minFileSize",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "model",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "personIds",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "rating",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"minimum": -1,
|
||||||
|
"maximum": 5,
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "size",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 1000,
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "state",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "tagIds",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"nullable": true,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "takenAfter",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "takenBefore",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trashedAfter",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "trashedBefore",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AssetTypeEnum"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updatedAfter",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "updatedBefore",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "visibility",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/AssetVisibility"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withDeleted",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "withExif",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/AssetResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Search"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/search/metadata": {
|
"/search/metadata": {
|
||||||
"post": {
|
"post": {
|
||||||
"operationId": "searchAssets",
|
"operationId": "searchAssets",
|
||||||
|
@ -2954,6 +2954,79 @@ export function getExploreData(opts?: Oazapfts.RequestOpts) {
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
export function searchLargeAssets({ albumIds, city, country, createdAfter, createdBefore, deviceId, isEncoded, isFavorite, isMotion, isNotInAlbum, isOffline, lensModel, libraryId, make, minFileSize, model, personIds, rating, size, state, tagIds, takenAfter, takenBefore, trashedAfter, trashedBefore, $type, updatedAfter, updatedBefore, visibility, withDeleted, withExif }: {
|
||||||
|
albumIds?: string[];
|
||||||
|
city?: string | null;
|
||||||
|
country?: string | null;
|
||||||
|
createdAfter?: string;
|
||||||
|
createdBefore?: string;
|
||||||
|
deviceId?: string;
|
||||||
|
isEncoded?: boolean;
|
||||||
|
isFavorite?: boolean;
|
||||||
|
isMotion?: boolean;
|
||||||
|
isNotInAlbum?: boolean;
|
||||||
|
isOffline?: boolean;
|
||||||
|
lensModel?: string | null;
|
||||||
|
libraryId?: string | null;
|
||||||
|
make?: string;
|
||||||
|
minFileSize?: number;
|
||||||
|
model?: string | null;
|
||||||
|
personIds?: string[];
|
||||||
|
rating?: number;
|
||||||
|
size?: number;
|
||||||
|
state?: string | null;
|
||||||
|
tagIds?: string[] | null;
|
||||||
|
takenAfter?: string;
|
||||||
|
takenBefore?: string;
|
||||||
|
trashedAfter?: string;
|
||||||
|
trashedBefore?: string;
|
||||||
|
$type?: AssetTypeEnum;
|
||||||
|
updatedAfter?: string;
|
||||||
|
updatedBefore?: string;
|
||||||
|
visibility?: AssetVisibility;
|
||||||
|
withDeleted?: boolean;
|
||||||
|
withExif?: boolean;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchJson<{
|
||||||
|
status: 200;
|
||||||
|
data: AssetResponseDto[];
|
||||||
|
}>(`/search/large-assets${QS.query(QS.explode({
|
||||||
|
albumIds,
|
||||||
|
city,
|
||||||
|
country,
|
||||||
|
createdAfter,
|
||||||
|
createdBefore,
|
||||||
|
deviceId,
|
||||||
|
isEncoded,
|
||||||
|
isFavorite,
|
||||||
|
isMotion,
|
||||||
|
isNotInAlbum,
|
||||||
|
isOffline,
|
||||||
|
lensModel,
|
||||||
|
libraryId,
|
||||||
|
make,
|
||||||
|
minFileSize,
|
||||||
|
model,
|
||||||
|
personIds,
|
||||||
|
rating,
|
||||||
|
size,
|
||||||
|
state,
|
||||||
|
tagIds,
|
||||||
|
takenAfter,
|
||||||
|
takenBefore,
|
||||||
|
trashedAfter,
|
||||||
|
trashedBefore,
|
||||||
|
"type": $type,
|
||||||
|
updatedAfter,
|
||||||
|
updatedBefore,
|
||||||
|
visibility,
|
||||||
|
withDeleted,
|
||||||
|
withExif
|
||||||
|
}))}`, {
|
||||||
|
...opts,
|
||||||
|
method: "POST"
|
||||||
|
}));
|
||||||
|
}
|
||||||
export function searchAssets({ metadataSearchDto }: {
|
export function searchAssets({ metadataSearchDto }: {
|
||||||
metadataSearchDto: MetadataSearchDto;
|
metadataSearchDto: MetadataSearchDto;
|
||||||
}, opts?: Oazapfts.RequestOpts) {
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
@ -4,6 +4,7 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { PersonResponseDto } from 'src/dtos/person.dto';
|
import { PersonResponseDto } from 'src/dtos/person.dto';
|
||||||
import {
|
import {
|
||||||
|
LargeAssetSearchDto,
|
||||||
MetadataSearchDto,
|
MetadataSearchDto,
|
||||||
PlacesResponseDto,
|
PlacesResponseDto,
|
||||||
RandomSearchDto,
|
RandomSearchDto,
|
||||||
@ -46,6 +47,13 @@ export class SearchController {
|
|||||||
return this.service.searchRandom(auth, dto);
|
return this.service.searchRandom(auth, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('large-assets')
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Authenticated({ permission: Permission.AssetRead })
|
||||||
|
searchLargeAssets(@Auth() auth: AuthDto, @Query() dto: LargeAssetSearchDto): Promise<AssetResponseDto[]> {
|
||||||
|
return this.service.searchLargeAssets(auth, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Post('smart')
|
@Post('smart')
|
||||||
@HttpCode(HttpStatus.OK)
|
@HttpCode(HttpStatus.OK)
|
||||||
@Authenticated({ permission: Permission.AssetRead })
|
@Authenticated({ permission: Permission.AssetRead })
|
||||||
|
@ -126,6 +126,15 @@ export class RandomSearchDto extends BaseSearchWithResultsDto {
|
|||||||
withPeople?: boolean;
|
withPeople?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class LargeAssetSearchDto extends BaseSearchWithResultsDto {
|
||||||
|
@Optional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Type(() => Number)
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
minFileSize?: number;
|
||||||
|
}
|
||||||
|
|
||||||
export class MetadataSearchDto extends RandomSearchDto {
|
export class MetadataSearchDto extends RandomSearchDto {
|
||||||
@ValidateUUID({ optional: true })
|
@ValidateUUID({ optional: true })
|
||||||
id?: string;
|
id?: string;
|
||||||
|
@ -77,6 +77,27 @@ union all
|
|||||||
limit
|
limit
|
||||||
$15
|
$15
|
||||||
|
|
||||||
|
-- SearchRepository.searchLargeAssets
|
||||||
|
select
|
||||||
|
"asset".*,
|
||||||
|
to_json("asset_exif") as "exifInfo"
|
||||||
|
from
|
||||||
|
"asset"
|
||||||
|
inner join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||||
|
left join "asset_exif" on "asset"."id" = "asset_exif"."assetId"
|
||||||
|
where
|
||||||
|
"asset"."visibility" = $1
|
||||||
|
and "asset"."fileCreatedAt" >= $2
|
||||||
|
and "asset_exif"."lensModel" = $3
|
||||||
|
and "asset"."ownerId" = any ($4::uuid[])
|
||||||
|
and "asset"."isFavorite" = $5
|
||||||
|
and "asset"."deletedAt" is null
|
||||||
|
and "asset_exif"."fileSizeInByte" > $6
|
||||||
|
order by
|
||||||
|
"asset_exif"."fileSizeInByte" desc
|
||||||
|
limit
|
||||||
|
$7
|
||||||
|
|
||||||
-- SearchRepository.searchSmart
|
-- SearchRepository.searchSmart
|
||||||
begin
|
begin
|
||||||
set
|
set
|
||||||
|
@ -8,7 +8,7 @@ import { AssetStatus, AssetType, AssetVisibility, VectorIndex } from 'src/enum';
|
|||||||
import { probes } from 'src/repositories/database.repository';
|
import { probes } from 'src/repositories/database.repository';
|
||||||
import { DB } from 'src/schema';
|
import { DB } from 'src/schema';
|
||||||
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
|
||||||
import { anyUuid, searchAssetBuilder } from 'src/utils/database';
|
import { anyUuid, searchAssetBuilder, withExif } from 'src/utils/database';
|
||||||
import { paginationHelper } from 'src/utils/pagination';
|
import { paginationHelper } from 'src/utils/pagination';
|
||||||
import { isValidInteger } from 'src/validation';
|
import { isValidInteger } from 'src/validation';
|
||||||
|
|
||||||
@ -129,6 +129,8 @@ export type SmartSearchOptions = SearchDateOptions &
|
|||||||
SearchPeopleOptions &
|
SearchPeopleOptions &
|
||||||
SearchTagOptions;
|
SearchTagOptions;
|
||||||
|
|
||||||
|
export type LargeAssetSearchOptions = AssetSearchOptions & { minFileSize?: number };
|
||||||
|
|
||||||
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
||||||
hasPerson?: boolean;
|
hasPerson?: boolean;
|
||||||
numResults: number;
|
numResults: number;
|
||||||
@ -237,6 +239,29 @@ export class SearchRepository {
|
|||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({
|
||||||
|
params: [
|
||||||
|
100,
|
||||||
|
{
|
||||||
|
takenAfter: DummyValue.DATE,
|
||||||
|
lensModel: DummyValue.STRING,
|
||||||
|
withStacked: true,
|
||||||
|
isFavorite: true,
|
||||||
|
userIds: [DummyValue.UUID],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
searchLargeAssets(size: number, options: LargeAssetSearchOptions) {
|
||||||
|
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection;
|
||||||
|
return searchAssetBuilder(this.db, options)
|
||||||
|
.selectAll('asset')
|
||||||
|
.$call(withExif)
|
||||||
|
.where('asset_exif.fileSizeInByte', '>', options.minFileSize || 0)
|
||||||
|
.orderBy('asset_exif.fileSizeInByte', orderDirection)
|
||||||
|
.limit(size)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({
|
@GenerateSql({
|
||||||
params: [
|
params: [
|
||||||
{ page: 1, size: 200 },
|
{ page: 1, size: 200 },
|
||||||
|
@ -4,6 +4,7 @@ import { AssetMapOptions, AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { mapPerson, PersonResponseDto } from 'src/dtos/person.dto';
|
import { mapPerson, PersonResponseDto } from 'src/dtos/person.dto';
|
||||||
import {
|
import {
|
||||||
|
LargeAssetSearchDto,
|
||||||
mapPlaces,
|
mapPlaces,
|
||||||
MetadataSearchDto,
|
MetadataSearchDto,
|
||||||
PlacesResponseDto,
|
PlacesResponseDto,
|
||||||
@ -91,6 +92,16 @@ export class SearchService extends BaseService {
|
|||||||
return items.map((item) => mapAsset(item, { auth }));
|
return items.map((item) => mapAsset(item, { auth }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async searchLargeAssets(auth: AuthDto, dto: LargeAssetSearchDto): Promise<AssetResponseDto[]> {
|
||||||
|
if (dto.visibility === AssetVisibility.Locked) {
|
||||||
|
requireElevatedPermission(auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
|
const items = await this.searchRepository.searchLargeAssets(dto.size || 250, { ...dto, userIds });
|
||||||
|
return items.map((item) => mapAsset(item, { auth }));
|
||||||
|
}
|
||||||
|
|
||||||
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||||
if (dto.visibility === AssetVisibility.Locked) {
|
if (dto.visibility === AssetVisibility.Locked) {
|
||||||
requireElevatedPermission(auth);
|
requireElevatedPermission(auth);
|
||||||
|
55
server/test/medium/specs/services/search.service.spec.ts
Normal file
55
server/test/medium/specs/services/search.service.spec.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
|
import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||||
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { PartnerRepository } from 'src/repositories/partner.repository';
|
||||||
|
import { PersonRepository } from 'src/repositories/person.repository';
|
||||||
|
import { SearchRepository } from 'src/repositories/search.repository';
|
||||||
|
import { DB } from 'src/schema';
|
||||||
|
import { SearchService } from 'src/services/search.service';
|
||||||
|
import { newMediumService } from 'test/medium.factory';
|
||||||
|
import { factory } from 'test/small.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = (db?: Kysely<DB>) => {
|
||||||
|
return newMediumService(SearchService, {
|
||||||
|
database: db || defaultDatabase,
|
||||||
|
real: [AccessRepository, DatabaseRepository, SearchRepository, PartnerRepository, PersonRepository],
|
||||||
|
mock: [LoggingRepository],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(SearchService.name, () => {
|
||||||
|
it('should work', () => {
|
||||||
|
const { sut } = setup();
|
||||||
|
expect(sut).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return assets', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
|
||||||
|
const assets = [];
|
||||||
|
const sizes = [12_334, 599, 123_456];
|
||||||
|
|
||||||
|
for (let i = 0; i < sizes.length; i++) {
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
await ctx.newExif({ assetId: asset.id, fileSizeInByte: sizes[i] });
|
||||||
|
assets.push(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
const auth = factory.auth({ user: { id: user.id } });
|
||||||
|
|
||||||
|
await expect(sut.searchLargeAssets(auth, {})).resolves.toEqual([
|
||||||
|
expect.objectContaining({ id: assets[2].id }),
|
||||||
|
expect.objectContaining({ id: assets[0].id }),
|
||||||
|
expect.objectContaining({ id: assets[1].id }),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,58 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||||
|
import { getAssetResolution, getFileSize } from '$lib/utils/asset-utils';
|
||||||
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
|
import { type AssetResponseDto } from '@immich/sdk';
|
||||||
|
import { mdiHeart } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
asset: AssetResponseDto;
|
||||||
|
onViewAsset: (asset: AssetResponseDto) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { asset, onViewAsset }: Props = $props();
|
||||||
|
|
||||||
|
let assetData = $derived(JSON.stringify(asset, null, 2));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="max-w-60 rounded-xl border-4 transition-colors font-semibold text-xs bg-gray-200 dark:bg-gray-800 border-gray-200 dark:border-gray-800"
|
||||||
|
>
|
||||||
|
<div class="relative w-full">
|
||||||
|
<button type="button" onclick={() => onViewAsset(asset)} class="block relative w-full" aria-label={$t('keep')}>
|
||||||
|
<!-- THUMBNAIL-->
|
||||||
|
<img
|
||||||
|
src={getAssetThumbnailUrl(asset.id)}
|
||||||
|
alt={$getAltText(toTimelineAsset(asset))}
|
||||||
|
title={assetData}
|
||||||
|
class="h-60 object-cover rounded-t-xl w-full"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- OVERLAY CHIP -->
|
||||||
|
{#if !!asset.libraryId}
|
||||||
|
<div class="absolute bottom-1 end-3 px-4 py-1 rounded-xl text-xs transition-colors bg-red-300/90">External</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- FAVORITE ICON -->
|
||||||
|
{#if asset.isFavorite}
|
||||||
|
<div class="absolute bottom-2 start-2">
|
||||||
|
<Icon path={mdiHeart} size="24" class="text-white" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-between items-center pl-2 pr-4 gap-2">
|
||||||
|
<div class="grid gap-y-2 py-2 text-xs transition-colors dark:text-white">
|
||||||
|
<div class="text-left text-ellipsis truncate">{asset.originalFileName}</div>
|
||||||
|
<span>{getAssetResolution(asset)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="dark:text-white text-lg font-bold whitespace-nowrap w-max">
|
||||||
|
{getFileSize(asset, 1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { mdiContentDuplicate } from '@mdi/js';
|
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { mdiContentDuplicate, mdiImageSizeSelectLarge } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -17,4 +17,13 @@
|
|||||||
</span>
|
</span>
|
||||||
{$t('review_duplicates')}
|
{$t('review_duplicates')}
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href={AppRoute.LARGE_FILES}
|
||||||
|
class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><Icon path={mdiImageSizeSelectLarge} class="text-immich-primary dark:text-immich-dark-primary" size="24" />
|
||||||
|
</span>
|
||||||
|
{$t('review_large_files')}
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -51,6 +51,7 @@ export enum AppRoute {
|
|||||||
|
|
||||||
UTILITIES = '/utilities',
|
UTILITIES = '/utilities',
|
||||||
DUPLICATES = '/utilities/duplicates',
|
DUPLICATES = '/utilities/duplicates',
|
||||||
|
LARGE_FILES = '/utilities/large-files',
|
||||||
|
|
||||||
FOLDERS = '/folders',
|
FOLDERS = '/folders',
|
||||||
TAGS = '/tags',
|
TAGS = '/tags',
|
||||||
|
@ -275,9 +275,9 @@ export function isFlipped(orientation?: string | null) {
|
|||||||
return value && (isRotated270CW(value) || isRotated90CW(value));
|
return value && (isRotated270CW(value) || isRotated90CW(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFileSize(asset: AssetResponseDto): string {
|
export function getFileSize(asset: AssetResponseDto, maxPrecision = 4): string {
|
||||||
const size = asset.exifInfo?.fileSizeInByte || 0;
|
const size = asset.exifInfo?.fileSizeInByte || 0;
|
||||||
return size > 0 ? getByteUnitString(size, undefined, 4) : 'Invalid Data';
|
return size > 0 ? getByteUnitString(size, undefined, maxPrecision) : 'Invalid Data';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAssetResolution(asset: AssetResponseDto): string {
|
export function getAssetResolution(asset: AssetResponseDto): string {
|
||||||
|
@ -0,0 +1,89 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||||
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
|
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||||
|
import LargeAssetData from '$lib/components/utilities-page/large-assets/large-asset-data.svelte';
|
||||||
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
|
import { handlePromiseError } from '$lib/utils';
|
||||||
|
import { navigate } from '$lib/utils/navigation';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: PageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data }: Props = $props();
|
||||||
|
|
||||||
|
let assets = $derived(data.assets);
|
||||||
|
const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
|
||||||
|
const getAssetIndex = (id: string) => assets.findIndex((asset) => asset.id === id);
|
||||||
|
|
||||||
|
const onNext = () => {
|
||||||
|
const index = getAssetIndex($viewingAsset.id) + 1;
|
||||||
|
if (index >= assets.length) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
setAsset(assets[index]);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPrevious = () => {
|
||||||
|
const index = getAssetIndex($viewingAsset.id) - 1;
|
||||||
|
if (index < 0) {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
setAsset(assets[index]);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRandom = () => {
|
||||||
|
if (assets.length <= 0) {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
const index = Math.floor(Math.random() * assets.length);
|
||||||
|
const asset = assets[index];
|
||||||
|
setAsset(asset);
|
||||||
|
return Promise.resolve(asset);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAction = (payload: Action) => {
|
||||||
|
if (payload.type == 'trash') {
|
||||||
|
assets = assets.filter((a) => a.id != payload.asset.id);
|
||||||
|
$showAssetViewer = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<UserPageLayout title={data.meta.title} scrollbar={true}>
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
{#if assets && data.assets.length > 0}
|
||||||
|
{#each assets as asset (asset.id)}
|
||||||
|
<LargeAssetData {asset} onViewAsset={(asset) => setAsset(asset)} />
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<p class="text-center text-lg dark:text-white flex place-items-center place-content-center">
|
||||||
|
{$t('no_assets_to_show')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</UserPageLayout>
|
||||||
|
|
||||||
|
{#if $showAssetViewer}
|
||||||
|
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||||
|
<Portal target="body">
|
||||||
|
<AssetViewer
|
||||||
|
asset={$viewingAsset}
|
||||||
|
showNavigation={assets.length > 1}
|
||||||
|
{onNext}
|
||||||
|
{onPrevious}
|
||||||
|
{onRandom}
|
||||||
|
{onAction}
|
||||||
|
onClose={() => {
|
||||||
|
assetViewingStore.showAssetViewer(false);
|
||||||
|
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
{/await}
|
||||||
|
{/if}
|
@ -0,0 +1,17 @@
|
|||||||
|
import { authenticate } from '$lib/utils/auth';
|
||||||
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
|
import { searchLargeAssets } from '@immich/sdk';
|
||||||
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
|
export const load = (async ({ url }) => {
|
||||||
|
await authenticate(url);
|
||||||
|
const assets = await searchLargeAssets({ minFileSize: 0 });
|
||||||
|
const $t = await getFormatter();
|
||||||
|
|
||||||
|
return {
|
||||||
|
assets,
|
||||||
|
meta: {
|
||||||
|
title: $t('large_files'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}) satisfies PageLoad;
|
Loading…
x
Reference in New Issue
Block a user