feat(server): visibility column ()

* feat: private view

* pr feedback

* sql generation

* feat: visibility column

* fix: set visibility value as the same as the still part after unlinked live photos

* fix: test

* pr feedback
This commit is contained in:
Alex 2025-05-06 12:12:48 -05:00 committed by GitHub
parent 016d7a6ceb
commit d33ce13561
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
90 changed files with 1137 additions and 867 deletions
e2e/src
mobile
open-api
server
web/src
lib
components
shared-components/search-bar
user-settings-page
stores
utils
routes/(user)
albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]
archive/[[photos=photos]]/[[assetId=id]]
favorites/[[photos=photos]]/[[assetId=id]]
partners/[userId]/[[photos=photos]]/[[assetId=id]]
people/[personId]/[[photos=photos]]/[[assetId=id]]
photos/[[assetId=id]]
search/[[photos=photos]]/[[assetId=id]]

View file

@ -3,6 +3,7 @@ import {
AssetMediaStatus,
AssetResponseDto,
AssetTypeEnum,
AssetVisibility,
getAssetInfo,
getMyUser,
LoginResponseDto,
@ -119,9 +120,9 @@ describe('/asset', () => {
// stats
utils.createAsset(statsUser.accessToken),
utils.createAsset(statsUser.accessToken, { isFavorite: true }),
utils.createAsset(statsUser.accessToken, { isArchived: true }),
utils.createAsset(statsUser.accessToken, { visibility: AssetVisibility.Archive }),
utils.createAsset(statsUser.accessToken, {
isArchived: true,
visibility: AssetVisibility.Archive,
isFavorite: true,
assetData: { filename: 'example.mp4' },
}),
@ -309,7 +310,7 @@ describe('/asset', () => {
});
it('disallows viewing archived assets', async () => {
const asset = await utils.createAsset(user1.accessToken, { isArchived: true });
const asset = await utils.createAsset(user1.accessToken, { visibility: AssetVisibility.Archive });
const { status } = await request(app)
.get(`/assets/${asset.id}`)
@ -353,7 +354,7 @@ describe('/asset', () => {
const { status, body } = await request(app)
.get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isArchived: true });
.query({ visibility: AssetVisibility.Archive });
expect(status).toBe(200);
expect(body).toEqual({ images: 1, videos: 1, total: 2 });
@ -363,7 +364,7 @@ describe('/asset', () => {
const { status, body } = await request(app)
.get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: true, isArchived: true });
.query({ isFavorite: true, visibility: AssetVisibility.Archive });
expect(status).toBe(200);
expect(body).toEqual({ images: 0, videos: 1, total: 1 });
@ -373,7 +374,7 @@ describe('/asset', () => {
const { status, body } = await request(app)
.get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: false, isArchived: false });
.query({ isFavorite: false, visibility: AssetVisibility.Timeline });
expect(status).toBe(200);
expect(body).toEqual({ images: 1, videos: 0, total: 1 });
@ -459,7 +460,7 @@ describe('/asset', () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isArchived: true });
.send({ visibility: AssetVisibility.Archive });
expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
expect(status).toEqual(200);
});

View file

@ -1,4 +1,4 @@
import { LoginResponseDto } from '@immich/sdk';
import { AssetVisibility, LoginResponseDto } from '@immich/sdk';
import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client';
@ -44,7 +44,7 @@ describe('/map', () => {
it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app)
.get('/map/markers')
.query({ isArchived: false })
.query({ visibility: AssetVisibility.Timeline })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);

View file

@ -1,4 +1,11 @@
import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk';
import {
AssetMediaResponseDto,
AssetResponseDto,
AssetVisibility,
deleteAssets,
LoginResponseDto,
updateAsset,
} from '@immich/sdk';
import { DateTime } from 'luxon';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
@ -49,7 +56,7 @@ describe('/search', () => {
{ filename: '/formats/motionphoto/samsung-one-ui-6.heic' },
{ filename: '/formats/motionphoto/samsung-one-ui-5.jpg' },
{ filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } },
{ filename: '/metadata/gps-position/thompson-springs.jpg', dto: { visibility: AssetVisibility.Archive } },
// used for search suggestions
{ filename: '/formats/png/density_plot.png' },
@ -171,12 +178,12 @@ describe('/search', () => {
deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }),
},
{
should: 'should search by isArchived (true)',
deferred: () => ({ dto: { isArchived: true }, assets: [assetSprings] }),
should: 'should search by visibility (AssetVisibility.Archive)',
deferred: () => ({ dto: { visibility: AssetVisibility.Archive }, assets: [assetSprings] }),
},
{
should: 'should search by isArchived (false)',
deferred: () => ({ dto: { size: 1, isArchived: false }, assets: [assetLast] }),
should: 'should search by visibility (AssetVisibility.Timeline)',
deferred: () => ({ dto: { size: 1, visibility: AssetVisibility.Timeline }, assets: [assetLast] }),
},
{
should: 'should search by type (image)',
@ -185,7 +192,7 @@ describe('/search', () => {
{
should: 'should search by type (video)',
deferred: () => ({
dto: { type: 'VIDEO' },
dto: { type: 'VIDEO', visibility: AssetVisibility.Hidden },
assets: [
// the three live motion photos
{ id: expect.any(String) },
@ -229,13 +236,6 @@ describe('/search', () => {
should: 'should search by takenAfter (no results)',
deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }),
},
// {
// should: 'should search by originalPath',
// deferred: () => ({
// dto: { originalPath: asset1.originalPath },
// assets: [asset1],
// }),
// },
{
should: 'should search by originalFilename',
deferred: () => ({
@ -265,7 +265,7 @@ describe('/search', () => {
deferred: () => ({
dto: {
city: '',
isVisible: true,
visibility: AssetVisibility.Timeline,
includeNull: true,
},
assets: [assetLast],
@ -276,7 +276,7 @@ describe('/search', () => {
deferred: () => ({
dto: {
city: null,
isVisible: true,
visibility: AssetVisibility.Timeline,
includeNull: true,
},
assets: [assetLast],
@ -297,7 +297,7 @@ describe('/search', () => {
deferred: () => ({
dto: {
state: '',
isVisible: true,
visibility: AssetVisibility.Timeline,
withExif: true,
includeNull: true,
},
@ -309,7 +309,7 @@ describe('/search', () => {
deferred: () => ({
dto: {
state: null,
isVisible: true,
visibility: AssetVisibility.Timeline,
includeNull: true,
},
assets: [assetLast, assetNotocactus],
@ -330,7 +330,7 @@ describe('/search', () => {
deferred: () => ({
dto: {
country: '',
isVisible: true,
visibility: AssetVisibility.Timeline,
includeNull: true,
},
assets: [assetLast],
@ -341,7 +341,7 @@ describe('/search', () => {
deferred: () => ({
dto: {
country: null,
isVisible: true,
visibility: AssetVisibility.Timeline,
includeNull: true,
},
assets: [assetLast],

View file

@ -1,4 +1,4 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
import { AssetMediaResponseDto, AssetVisibility, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
import { DateTime } from 'luxon';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
@ -104,7 +104,7 @@ describe('/timeline', () => {
const req1 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
.query({ size: TimeBucketSize.Month, withPartners: true, visibility: AssetVisibility.Archive });
expect(req1.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest());
@ -112,7 +112,7 @@ describe('/timeline', () => {
const req2 = await request(app)
.get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`)
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
.query({ size: TimeBucketSize.Month, withPartners: true, visibility: undefined });
expect(req2.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest());

View file

@ -3,6 +3,7 @@ import {
AssetMediaCreateDto,
AssetMediaResponseDto,
AssetResponseDto,
AssetVisibility,
CheckExistingAssetsDto,
CreateAlbumDto,
CreateLibraryDto,
@ -429,7 +430,10 @@ export const utils = {
},
archiveAssets: (accessToken: string, ids: string[]) =>
updateAssets({ assetBulkUpdateDto: { ids, isArchived: true } }, { headers: asBearerAuth(accessToken) }),
updateAssets(
{ assetBulkUpdateDto: { ids, visibility: AssetVisibility.Archive } },
{ headers: asBearerAuth(accessToken) },
),
deleteAssets: (accessToken: string, ids: string[]) =>
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),

View file

@ -197,7 +197,7 @@ class AssetService {
ids: assets.map((e) => e.remoteId!).toList(),
dateTimeOriginal: updateAssetDto.dateTimeOriginal,
isFavorite: updateAssetDto.isFavorite,
isArchived: updateAssetDto.isArchived,
visibility: updateAssetDto.visibility,
latitude: updateAssetDto.latitude,
longitude: updateAssetDto.longitude,
),
@ -229,7 +229,13 @@ class AssetService {
bool isArchived,
) async {
try {
await updateAssets(assets, UpdateAssetDto(isArchived: isArchived));
await updateAssets(
assets,
UpdateAssetDto(
visibility:
isArchived ? AssetVisibility.archive : AssetVisibility.timeline,
),
);
for (var element in assets) {
element.isArchived = isArchived;

View file

@ -68,7 +68,9 @@ class SearchService {
model: filter.camera.model,
takenAfter: filter.date.takenAfter,
takenBefore: filter.date.takenBefore,
isArchived: filter.display.isArchive ? true : null,
visibility: filter.display.isArchive
? AssetVisibility.archive
: AssetVisibility.timeline,
isFavorite: filter.display.isFavorite ? true : null,
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
personIds: filter.people.map((e) => e.id).toList(),
@ -95,7 +97,9 @@ class SearchService {
model: filter.camera.model,
takenAfter: filter.date.takenAfter,
takenBefore: filter.date.takenBefore,
isArchived: filter.display.isArchive ? true : null,
visibility: filter.display.isArchive
? AssetVisibility.archive
: AssetVisibility.timeline,
isFavorite: filter.display.isFavorite ? true : null,
isNotInAlbum: filter.display.isNotInAlbum ? true : null,
personIds: filter.people.map((e) => e.id).toList(),

View file

@ -302,6 +302,7 @@ Class | Method | HTTP request | Description
- [AssetStackResponseDto](doc//AssetStackResponseDto.md)
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
- [AssetTypeEnum](doc//AssetTypeEnum.md)
- [AssetVisibility](doc//AssetVisibility.md)
- [AudioCodec](doc//AudioCodec.md)
- [AvatarUpdate](doc//AvatarUpdate.md)
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)

View file

@ -106,6 +106,7 @@ part 'model/asset_response_dto.dart';
part 'model/asset_stack_response_dto.dart';
part 'model/asset_stats_response_dto.dart';
part 'model/asset_type_enum.dart';
part 'model/asset_visibility.dart';
part 'model/audio_codec.dart';
part 'model/avatar_update.dart';
part 'model/bulk_id_response_dto.dart';

View file

@ -342,12 +342,12 @@ class AssetsApi {
/// Performs an HTTP 'GET /assets/statistics' operation and returns the [Response].
/// Parameters:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
Future<Response> getAssetStatisticsWithHttpInfo({ bool? isArchived, bool? isFavorite, bool? isTrashed, }) async {
///
/// * [AssetVisibility] visibility:
Future<Response> getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets/statistics';
@ -358,15 +358,15 @@ class AssetsApi {
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (isArchived != null) {
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (isTrashed != null) {
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
}
if (visibility != null) {
queryParams.addAll(_queryParams('', 'visibility', visibility));
}
const contentTypes = <String>[];
@ -384,13 +384,13 @@ class AssetsApi {
/// Parameters:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
Future<AssetStatsResponseDto?> getAssetStatistics({ bool? isArchived, bool? isFavorite, bool? isTrashed, }) async {
final response = await getAssetStatisticsWithHttpInfo( isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, );
///
/// * [AssetVisibility] visibility:
Future<AssetStatsResponseDto?> getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async {
final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@ -788,16 +788,14 @@ class AssetsApi {
///
/// * [String] duration:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [bool] isVisible:
///
/// * [String] livePhotoVideoId:
///
/// * [MultipartFile] sidecarData:
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async {
///
/// * [AssetVisibility] visibility:
Future<Response> uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/assets';
@ -845,18 +843,10 @@ class AssetsApi {
hasFields = true;
mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt);
}
if (isArchived != null) {
hasFields = true;
mp.fields[r'isArchived'] = parameterToString(isArchived);
}
if (isFavorite != null) {
hasFields = true;
mp.fields[r'isFavorite'] = parameterToString(isFavorite);
}
if (isVisible != null) {
hasFields = true;
mp.fields[r'isVisible'] = parameterToString(isVisible);
}
if (livePhotoVideoId != null) {
hasFields = true;
mp.fields[r'livePhotoVideoId'] = parameterToString(livePhotoVideoId);
@ -866,6 +856,10 @@ class AssetsApi {
mp.fields[r'sidecarData'] = sidecarData.field;
mp.files.add(sidecarData);
}
if (visibility != null) {
hasFields = true;
mp.fields[r'visibility'] = parameterToString(visibility);
}
if (hasFields) {
postBody = mp;
}
@ -900,17 +894,15 @@ class AssetsApi {
///
/// * [String] duration:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [bool] isVisible:
///
/// * [String] livePhotoVideoId:
///
/// * [MultipartFile] sidecarData:
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async {
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, xImmichChecksum: xImmichChecksum, duration: duration, isArchived: isArchived, isFavorite: isFavorite, isVisible: isVisible, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, );
///
/// * [AssetVisibility] visibility:
Future<AssetMediaResponseDto?> uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isFavorite, String? livePhotoVideoId, MultipartFile? sidecarData, AssetVisibility? visibility, }) async {
final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, xImmichChecksum: xImmichChecksum, duration: duration, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, visibility: visibility, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View file

@ -25,8 +25,6 @@ class TimelineApi {
///
/// * [String] albumId:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
@ -41,10 +39,12 @@ class TimelineApi {
///
/// * [String] userId:
///
/// * [AssetVisibility] visibility:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/timeline/bucket';
@ -58,9 +58,6 @@ class TimelineApi {
if (albumId != null) {
queryParams.addAll(_queryParams('', 'albumId', albumId));
}
if (isArchived != null) {
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
@ -84,6 +81,9 @@ class TimelineApi {
if (userId != null) {
queryParams.addAll(_queryParams('', 'userId', userId));
}
if (visibility != null) {
queryParams.addAll(_queryParams('', 'visibility', visibility));
}
if (withPartners != null) {
queryParams.addAll(_queryParams('', 'withPartners', withPartners));
}
@ -113,8 +113,6 @@ class TimelineApi {
///
/// * [String] albumId:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
@ -129,11 +127,13 @@ class TimelineApi {
///
/// * [String] userId:
///
/// * [AssetVisibility] visibility:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
Future<List<AssetResponseDto>?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@ -157,8 +157,6 @@ class TimelineApi {
///
/// * [String] albumId:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
@ -173,10 +171,12 @@ class TimelineApi {
///
/// * [String] userId:
///
/// * [AssetVisibility] visibility:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/timeline/buckets';
@ -190,9 +190,6 @@ class TimelineApi {
if (albumId != null) {
queryParams.addAll(_queryParams('', 'albumId', albumId));
}
if (isArchived != null) {
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
@ -215,6 +212,9 @@ class TimelineApi {
if (userId != null) {
queryParams.addAll(_queryParams('', 'userId', userId));
}
if (visibility != null) {
queryParams.addAll(_queryParams('', 'visibility', visibility));
}
if (withPartners != null) {
queryParams.addAll(_queryParams('', 'withPartners', withPartners));
}
@ -242,8 +242,6 @@ class TimelineApi {
///
/// * [String] albumId:
///
/// * [bool] isArchived:
///
/// * [bool] isFavorite:
///
/// * [bool] isTrashed:
@ -258,11 +256,13 @@ class TimelineApi {
///
/// * [String] userId:
///
/// * [AssetVisibility] visibility:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, AssetVisibility? visibility, bool? withPartners, bool? withStacked, }) async {
final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, visibility: visibility, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}

View file

@ -268,6 +268,8 @@ class ApiClient {
return AssetStatsResponseDto.fromJson(value);
case 'AssetTypeEnum':
return AssetTypeEnumTypeTransformer().decode(value);
case 'AssetVisibility':
return AssetVisibilityTypeTransformer().decode(value);
case 'AudioCodec':
return AudioCodecTypeTransformer().decode(value);
case 'AvatarUpdate':

View file

@ -73,6 +73,9 @@ String parameterToString(dynamic value) {
if (value is AssetTypeEnum) {
return AssetTypeEnumTypeTransformer().encode(value).toString();
}
if (value is AssetVisibility) {
return AssetVisibilityTypeTransformer().encode(value).toString();
}
if (value is AudioCodec) {
return AudioCodecTypeTransformer().encode(value).toString();
}

View file

@ -16,11 +16,11 @@ class AssetBulkUpdateDto {
this.dateTimeOriginal,
this.duplicateId,
this.ids = const [],
this.isArchived,
this.isFavorite,
this.latitude,
this.longitude,
this.rating,
this.visibility,
});
///
@ -35,14 +35,6 @@ class AssetBulkUpdateDto {
List<String> ids;
///
/// 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.
///
bool? isArchived;
///
/// 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
@ -77,16 +69,24 @@ class AssetBulkUpdateDto {
///
num? rating;
///
/// 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.
///
AssetVisibility? visibility;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
other.dateTimeOriginal == dateTimeOriginal &&
other.duplicateId == duplicateId &&
_deepEquality.equals(other.ids, ids) &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite &&
other.latitude == latitude &&
other.longitude == longitude &&
other.rating == rating;
other.rating == rating &&
other.visibility == visibility;
@override
int get hashCode =>
@ -94,14 +94,14 @@ class AssetBulkUpdateDto {
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
(duplicateId == null ? 0 : duplicateId!.hashCode) +
(ids.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode) +
(rating == null ? 0 : rating!.hashCode);
(rating == null ? 0 : rating!.hashCode) +
(visibility == null ? 0 : visibility!.hashCode);
@override
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]';
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, visibility=$visibility]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -116,11 +116,6 @@ class AssetBulkUpdateDto {
// json[r'duplicateId'] = null;
}
json[r'ids'] = this.ids;
if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived;
} else {
// json[r'isArchived'] = null;
}
if (this.isFavorite != null) {
json[r'isFavorite'] = this.isFavorite;
} else {
@ -141,6 +136,11 @@ class AssetBulkUpdateDto {
} else {
// json[r'rating'] = null;
}
if (this.visibility != null) {
json[r'visibility'] = this.visibility;
} else {
// json[r'visibility'] = null;
}
return json;
}
@ -158,11 +158,11 @@ class AssetBulkUpdateDto {
ids: json[r'ids'] is Iterable
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
: const [],
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: num.parse('${json[r'latitude']}'),
longitude: num.parse('${json[r'longitude']}'),
rating: num.parse('${json[r'rating']}'),
visibility: AssetVisibility.fromJson(json[r'visibility']),
);
}
return null;

View file

@ -0,0 +1,88 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 AssetVisibility {
/// Instantiate a new enum with the provided [value].
const AssetVisibility._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const archive = AssetVisibility._(r'archive');
static const timeline = AssetVisibility._(r'timeline');
static const hidden = AssetVisibility._(r'hidden');
/// List of all possible values in this [enum][AssetVisibility].
static const values = <AssetVisibility>[
archive,
timeline,
hidden,
];
static AssetVisibility? fromJson(dynamic value) => AssetVisibilityTypeTransformer().decode(value);
static List<AssetVisibility> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetVisibility>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetVisibility.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [AssetVisibility] to String,
/// and [decode] dynamic data back to [AssetVisibility].
class AssetVisibilityTypeTransformer {
factory AssetVisibilityTypeTransformer() => _instance ??= const AssetVisibilityTypeTransformer._();
const AssetVisibilityTypeTransformer._();
String encode(AssetVisibility data) => data.value;
/// Decodes a [dynamic value][data] to a AssetVisibility.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
AssetVisibility? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'archive': return AssetVisibility.archive;
case r'timeline': return AssetVisibility.timeline;
case r'hidden': return AssetVisibility.hidden;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [AssetVisibilityTypeTransformer] instance.
static AssetVisibilityTypeTransformer? _instance;
}

View file

@ -23,13 +23,11 @@ class MetadataSearchDto {
this.deviceId,
this.encodedVideoPath,
this.id,
this.isArchived,
this.isEncoded,
this.isFavorite,
this.isMotion,
this.isNotInAlbum,
this.isOffline,
this.isVisible,
this.lensModel,
this.libraryId,
this.make,
@ -52,7 +50,7 @@ class MetadataSearchDto {
this.type,
this.updatedAfter,
this.updatedBefore,
this.withArchived = false,
this.visibility,
this.withDeleted,
this.withExif,
this.withPeople,
@ -127,14 +125,6 @@ class MetadataSearchDto {
///
String? id;
///
/// 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.
///
bool? isArchived;
///
/// 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
@ -175,14 +165,6 @@ class MetadataSearchDto {
///
bool? isOffline;
///
/// 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.
///
bool? isVisible;
String? lensModel;
String? libraryId;
@ -322,7 +304,13 @@ class MetadataSearchDto {
///
DateTime? updatedBefore;
bool withArchived;
///
/// 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.
///
AssetVisibility? visibility;
///
/// Please note: This property should have been non-nullable! Since the specification file
@ -368,13 +356,11 @@ class MetadataSearchDto {
other.deviceId == deviceId &&
other.encodedVideoPath == encodedVideoPath &&
other.id == id &&
other.isArchived == isArchived &&
other.isEncoded == isEncoded &&
other.isFavorite == isFavorite &&
other.isMotion == isMotion &&
other.isNotInAlbum == isNotInAlbum &&
other.isOffline == isOffline &&
other.isVisible == isVisible &&
other.lensModel == lensModel &&
other.libraryId == libraryId &&
other.make == make &&
@ -397,7 +383,7 @@ class MetadataSearchDto {
other.type == type &&
other.updatedAfter == updatedAfter &&
other.updatedBefore == updatedBefore &&
other.withArchived == withArchived &&
other.visibility == visibility &&
other.withDeleted == withDeleted &&
other.withExif == withExif &&
other.withPeople == withPeople &&
@ -416,13 +402,11 @@ class MetadataSearchDto {
(deviceId == null ? 0 : deviceId!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(id == null ? 0 : id!.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) +
(isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
(isOffline == null ? 0 : isOffline!.hashCode) +
(isVisible == null ? 0 : isVisible!.hashCode) +
(lensModel == null ? 0 : lensModel!.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) +
(make == null ? 0 : make!.hashCode) +
@ -445,14 +429,14 @@ class MetadataSearchDto {
(type == null ? 0 : type!.hashCode) +
(updatedAfter == null ? 0 : updatedAfter!.hashCode) +
(updatedBefore == null ? 0 : updatedBefore!.hashCode) +
(withArchived.hashCode) +
(visibility == null ? 0 : visibility!.hashCode) +
(withDeleted == null ? 0 : withDeleted!.hashCode) +
(withExif == null ? 0 : withExif!.hashCode) +
(withPeople == null ? 0 : withPeople!.hashCode) +
(withStacked == null ? 0 : withStacked!.hashCode);
@override
String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -506,11 +490,6 @@ class MetadataSearchDto {
} else {
// json[r'id'] = null;
}
if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived;
} else {
// json[r'isArchived'] = null;
}
if (this.isEncoded != null) {
json[r'isEncoded'] = this.isEncoded;
} else {
@ -536,11 +515,6 @@ class MetadataSearchDto {
} else {
// json[r'isOffline'] = null;
}
if (this.isVisible != null) {
json[r'isVisible'] = this.isVisible;
} else {
// json[r'isVisible'] = null;
}
if (this.lensModel != null) {
json[r'lensModel'] = this.lensModel;
} else {
@ -639,7 +613,11 @@ class MetadataSearchDto {
} else {
// json[r'updatedBefore'] = null;
}
json[r'withArchived'] = this.withArchived;
if (this.visibility != null) {
json[r'visibility'] = this.visibility;
} else {
// json[r'visibility'] = null;
}
if (this.withDeleted != null) {
json[r'withDeleted'] = this.withDeleted;
} else {
@ -682,13 +660,11 @@ class MetadataSearchDto {
deviceId: mapValueOfType<String>(json, r'deviceId'),
encodedVideoPath: mapValueOfType<String>(json, r'encodedVideoPath'),
id: mapValueOfType<String>(json, r'id'),
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isEncoded: mapValueOfType<bool>(json, r'isEncoded'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isMotion: mapValueOfType<bool>(json, r'isMotion'),
isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
isOffline: mapValueOfType<bool>(json, r'isOffline'),
isVisible: mapValueOfType<bool>(json, r'isVisible'),
lensModel: mapValueOfType<String>(json, r'lensModel'),
libraryId: mapValueOfType<String>(json, r'libraryId'),
make: mapValueOfType<String>(json, r'make'),
@ -715,7 +691,7 @@ class MetadataSearchDto {
type: AssetTypeEnum.fromJson(json[r'type']),
updatedAfter: mapDateTime(json, r'updatedAfter', r''),
updatedBefore: mapDateTime(json, r'updatedBefore', r''),
withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false,
visibility: AssetVisibility.fromJson(json[r'visibility']),
withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
withExif: mapValueOfType<bool>(json, r'withExif'),
withPeople: mapValueOfType<bool>(json, r'withPeople'),

View file

@ -18,13 +18,11 @@ class RandomSearchDto {
this.createdAfter,
this.createdBefore,
this.deviceId,
this.isArchived,
this.isEncoded,
this.isFavorite,
this.isMotion,
this.isNotInAlbum,
this.isOffline,
this.isVisible,
this.lensModel,
this.libraryId,
this.make,
@ -41,7 +39,7 @@ class RandomSearchDto {
this.type,
this.updatedAfter,
this.updatedBefore,
this.withArchived = false,
this.visibility,
this.withDeleted,
this.withExif,
this.withPeople,
@ -76,14 +74,6 @@ class RandomSearchDto {
///
String? deviceId;
///
/// 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.
///
bool? isArchived;
///
/// 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
@ -124,14 +114,6 @@ class RandomSearchDto {
///
bool? isOffline;
///
/// 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.
///
bool? isVisible;
String? lensModel;
String? libraryId;
@ -228,7 +210,13 @@ class RandomSearchDto {
///
DateTime? updatedBefore;
bool withArchived;
///
/// 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.
///
AssetVisibility? visibility;
///
/// Please note: This property should have been non-nullable! Since the specification file
@ -269,13 +257,11 @@ class RandomSearchDto {
other.createdAfter == createdAfter &&
other.createdBefore == createdBefore &&
other.deviceId == deviceId &&
other.isArchived == isArchived &&
other.isEncoded == isEncoded &&
other.isFavorite == isFavorite &&
other.isMotion == isMotion &&
other.isNotInAlbum == isNotInAlbum &&
other.isOffline == isOffline &&
other.isVisible == isVisible &&
other.lensModel == lensModel &&
other.libraryId == libraryId &&
other.make == make &&
@ -292,7 +278,7 @@ class RandomSearchDto {
other.type == type &&
other.updatedAfter == updatedAfter &&
other.updatedBefore == updatedBefore &&
other.withArchived == withArchived &&
other.visibility == visibility &&
other.withDeleted == withDeleted &&
other.withExif == withExif &&
other.withPeople == withPeople &&
@ -306,13 +292,11 @@ class RandomSearchDto {
(createdAfter == null ? 0 : createdAfter!.hashCode) +
(createdBefore == null ? 0 : createdBefore!.hashCode) +
(deviceId == null ? 0 : deviceId!.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) +
(isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
(isOffline == null ? 0 : isOffline!.hashCode) +
(isVisible == null ? 0 : isVisible!.hashCode) +
(lensModel == null ? 0 : lensModel!.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) +
(make == null ? 0 : make!.hashCode) +
@ -329,14 +313,14 @@ class RandomSearchDto {
(type == null ? 0 : type!.hashCode) +
(updatedAfter == null ? 0 : updatedAfter!.hashCode) +
(updatedBefore == null ? 0 : updatedBefore!.hashCode) +
(withArchived.hashCode) +
(visibility == null ? 0 : visibility!.hashCode) +
(withDeleted == null ? 0 : withDeleted!.hashCode) +
(withExif == null ? 0 : withExif!.hashCode) +
(withPeople == null ? 0 : withPeople!.hashCode) +
(withStacked == null ? 0 : withStacked!.hashCode);
@override
String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, 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, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
String toString() => 'RandomSearchDto[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, 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, withPeople=$withPeople, withStacked=$withStacked]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -365,11 +349,6 @@ class RandomSearchDto {
} else {
// json[r'deviceId'] = null;
}
if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived;
} else {
// json[r'isArchived'] = null;
}
if (this.isEncoded != null) {
json[r'isEncoded'] = this.isEncoded;
} else {
@ -395,11 +374,6 @@ class RandomSearchDto {
} else {
// json[r'isOffline'] = null;
}
if (this.isVisible != null) {
json[r'isVisible'] = this.isVisible;
} else {
// json[r'isVisible'] = null;
}
if (this.lensModel != null) {
json[r'lensModel'] = this.lensModel;
} else {
@ -472,7 +446,11 @@ class RandomSearchDto {
} else {
// json[r'updatedBefore'] = null;
}
json[r'withArchived'] = this.withArchived;
if (this.visibility != null) {
json[r'visibility'] = this.visibility;
} else {
// json[r'visibility'] = null;
}
if (this.withDeleted != null) {
json[r'withDeleted'] = this.withDeleted;
} else {
@ -510,13 +488,11 @@ class RandomSearchDto {
createdAfter: mapDateTime(json, r'createdAfter', r''),
createdBefore: mapDateTime(json, r'createdBefore', r''),
deviceId: mapValueOfType<String>(json, r'deviceId'),
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isEncoded: mapValueOfType<bool>(json, r'isEncoded'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isMotion: mapValueOfType<bool>(json, r'isMotion'),
isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
isOffline: mapValueOfType<bool>(json, r'isOffline'),
isVisible: mapValueOfType<bool>(json, r'isVisible'),
lensModel: mapValueOfType<String>(json, r'lensModel'),
libraryId: mapValueOfType<String>(json, r'libraryId'),
make: mapValueOfType<String>(json, r'make'),
@ -537,7 +513,7 @@ class RandomSearchDto {
type: AssetTypeEnum.fromJson(json[r'type']),
updatedAfter: mapDateTime(json, r'updatedAfter', r''),
updatedBefore: mapDateTime(json, r'updatedBefore', r''),
withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false,
visibility: AssetVisibility.fromJson(json[r'visibility']),
withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
withExif: mapValueOfType<bool>(json, r'withExif'),
withPeople: mapValueOfType<bool>(json, r'withPeople'),

View file

@ -18,13 +18,11 @@ class SmartSearchDto {
this.createdAfter,
this.createdBefore,
this.deviceId,
this.isArchived,
this.isEncoded,
this.isFavorite,
this.isMotion,
this.isNotInAlbum,
this.isOffline,
this.isVisible,
this.language,
this.lensModel,
this.libraryId,
@ -44,7 +42,7 @@ class SmartSearchDto {
this.type,
this.updatedAfter,
this.updatedBefore,
this.withArchived = false,
this.visibility,
this.withDeleted,
this.withExif,
});
@ -77,14 +75,6 @@ class SmartSearchDto {
///
String? deviceId;
///
/// 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.
///
bool? isArchived;
///
/// 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
@ -125,14 +115,6 @@ class SmartSearchDto {
///
bool? isOffline;
///
/// 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.
///
bool? isVisible;
///
/// 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
@ -248,7 +230,13 @@ class SmartSearchDto {
///
DateTime? updatedBefore;
bool withArchived;
///
/// 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.
///
AssetVisibility? visibility;
///
/// Please note: This property should have been non-nullable! Since the specification file
@ -273,13 +261,11 @@ class SmartSearchDto {
other.createdAfter == createdAfter &&
other.createdBefore == createdBefore &&
other.deviceId == deviceId &&
other.isArchived == isArchived &&
other.isEncoded == isEncoded &&
other.isFavorite == isFavorite &&
other.isMotion == isMotion &&
other.isNotInAlbum == isNotInAlbum &&
other.isOffline == isOffline &&
other.isVisible == isVisible &&
other.language == language &&
other.lensModel == lensModel &&
other.libraryId == libraryId &&
@ -299,7 +285,7 @@ class SmartSearchDto {
other.type == type &&
other.updatedAfter == updatedAfter &&
other.updatedBefore == updatedBefore &&
other.withArchived == withArchived &&
other.visibility == visibility &&
other.withDeleted == withDeleted &&
other.withExif == withExif;
@ -311,13 +297,11 @@ class SmartSearchDto {
(createdAfter == null ? 0 : createdAfter!.hashCode) +
(createdBefore == null ? 0 : createdBefore!.hashCode) +
(deviceId == null ? 0 : deviceId!.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) +
(isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
(isOffline == null ? 0 : isOffline!.hashCode) +
(isVisible == null ? 0 : isVisible!.hashCode) +
(language == null ? 0 : language!.hashCode) +
(lensModel == null ? 0 : lensModel!.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) +
@ -337,12 +321,12 @@ class SmartSearchDto {
(type == null ? 0 : type!.hashCode) +
(updatedAfter == null ? 0 : updatedAfter!.hashCode) +
(updatedBefore == null ? 0 : updatedBefore!.hashCode) +
(withArchived.hashCode) +
(visibility == null ? 0 : visibility!.hashCode) +
(withDeleted == null ? 0 : withDeleted!.hashCode) +
(withExif == null ? 0 : withExif!.hashCode);
@override
String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]';
String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, 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]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -371,11 +355,6 @@ class SmartSearchDto {
} else {
// json[r'deviceId'] = null;
}
if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived;
} else {
// json[r'isArchived'] = null;
}
if (this.isEncoded != null) {
json[r'isEncoded'] = this.isEncoded;
} else {
@ -401,11 +380,6 @@ class SmartSearchDto {
} else {
// json[r'isOffline'] = null;
}
if (this.isVisible != null) {
json[r'isVisible'] = this.isVisible;
} else {
// json[r'isVisible'] = null;
}
if (this.language != null) {
json[r'language'] = this.language;
} else {
@ -489,7 +463,11 @@ class SmartSearchDto {
} else {
// json[r'updatedBefore'] = null;
}
json[r'withArchived'] = this.withArchived;
if (this.visibility != null) {
json[r'visibility'] = this.visibility;
} else {
// json[r'visibility'] = null;
}
if (this.withDeleted != null) {
json[r'withDeleted'] = this.withDeleted;
} else {
@ -517,13 +495,11 @@ class SmartSearchDto {
createdAfter: mapDateTime(json, r'createdAfter', r''),
createdBefore: mapDateTime(json, r'createdBefore', r''),
deviceId: mapValueOfType<String>(json, r'deviceId'),
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isEncoded: mapValueOfType<bool>(json, r'isEncoded'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isMotion: mapValueOfType<bool>(json, r'isMotion'),
isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
isOffline: mapValueOfType<bool>(json, r'isOffline'),
isVisible: mapValueOfType<bool>(json, r'isVisible'),
language: mapValueOfType<String>(json, r'language'),
lensModel: mapValueOfType<String>(json, r'lensModel'),
libraryId: mapValueOfType<String>(json, r'libraryId'),
@ -547,7 +523,7 @@ class SmartSearchDto {
type: AssetTypeEnum.fromJson(json[r'type']),
updatedAfter: mapDateTime(json, r'updatedAfter', r''),
updatedBefore: mapDateTime(json, r'updatedBefore', r''),
withArchived: mapValueOfType<bool>(json, r'withArchived') ?? false,
visibility: AssetVisibility.fromJson(json[r'visibility']),
withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
withExif: mapValueOfType<bool>(json, r'withExif'),
);

View file

@ -19,11 +19,11 @@ class SyncAssetV1 {
required this.fileModifiedAt,
required this.id,
required this.isFavorite,
required this.isVisible,
required this.localDateTime,
required this.ownerId,
required this.thumbhash,
required this.type,
required this.visibility,
});
String checksum;
@ -38,8 +38,6 @@ class SyncAssetV1 {
bool isFavorite;
bool isVisible;
DateTime? localDateTime;
String ownerId;
@ -48,6 +46,8 @@ class SyncAssetV1 {
SyncAssetV1TypeEnum type;
SyncAssetV1VisibilityEnum visibility;
@override
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 &&
other.checksum == checksum &&
@ -56,11 +56,11 @@ class SyncAssetV1 {
other.fileModifiedAt == fileModifiedAt &&
other.id == id &&
other.isFavorite == isFavorite &&
other.isVisible == isVisible &&
other.localDateTime == localDateTime &&
other.ownerId == ownerId &&
other.thumbhash == thumbhash &&
other.type == type;
other.type == type &&
other.visibility == visibility;
@override
int get hashCode =>
@ -71,14 +71,14 @@ class SyncAssetV1 {
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
(id.hashCode) +
(isFavorite.hashCode) +
(isVisible.hashCode) +
(localDateTime == null ? 0 : localDateTime!.hashCode) +
(ownerId.hashCode) +
(thumbhash == null ? 0 : thumbhash!.hashCode) +
(type.hashCode);
(type.hashCode) +
(visibility.hashCode);
@override
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, isVisible=$isVisible, localDateTime=$localDateTime, ownerId=$ownerId, thumbhash=$thumbhash, type=$type]';
String toString() => 'SyncAssetV1[checksum=$checksum, deletedAt=$deletedAt, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, id=$id, isFavorite=$isFavorite, localDateTime=$localDateTime, ownerId=$ownerId, thumbhash=$thumbhash, type=$type, visibility=$visibility]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -100,7 +100,6 @@ class SyncAssetV1 {
}
json[r'id'] = this.id;
json[r'isFavorite'] = this.isFavorite;
json[r'isVisible'] = this.isVisible;
if (this.localDateTime != null) {
json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String();
} else {
@ -113,6 +112,7 @@ class SyncAssetV1 {
// json[r'thumbhash'] = null;
}
json[r'type'] = this.type;
json[r'visibility'] = this.visibility;
return json;
}
@ -131,11 +131,11 @@ class SyncAssetV1 {
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
isVisible: mapValueOfType<bool>(json, r'isVisible')!,
localDateTime: mapDateTime(json, r'localDateTime', r''),
ownerId: mapValueOfType<String>(json, r'ownerId')!,
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: SyncAssetV1TypeEnum.fromJson(json[r'type'])!,
visibility: SyncAssetV1VisibilityEnum.fromJson(json[r'visibility'])!,
);
}
return null;
@ -189,11 +189,11 @@ class SyncAssetV1 {
'fileModifiedAt',
'id',
'isFavorite',
'isVisible',
'localDateTime',
'ownerId',
'thumbhash',
'type',
'visibility',
};
}
@ -277,3 +277,80 @@ class SyncAssetV1TypeEnumTypeTransformer {
}
class SyncAssetV1VisibilityEnum {
/// Instantiate a new enum with the provided [value].
const SyncAssetV1VisibilityEnum._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const archive = SyncAssetV1VisibilityEnum._(r'archive');
static const timeline = SyncAssetV1VisibilityEnum._(r'timeline');
static const hidden = SyncAssetV1VisibilityEnum._(r'hidden');
/// List of all possible values in this [enum][SyncAssetV1VisibilityEnum].
static const values = <SyncAssetV1VisibilityEnum>[
archive,
timeline,
hidden,
];
static SyncAssetV1VisibilityEnum? fromJson(dynamic value) => SyncAssetV1VisibilityEnumTypeTransformer().decode(value);
static List<SyncAssetV1VisibilityEnum> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SyncAssetV1VisibilityEnum>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SyncAssetV1VisibilityEnum.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SyncAssetV1VisibilityEnum] to String,
/// and [decode] dynamic data back to [SyncAssetV1VisibilityEnum].
class SyncAssetV1VisibilityEnumTypeTransformer {
factory SyncAssetV1VisibilityEnumTypeTransformer() => _instance ??= const SyncAssetV1VisibilityEnumTypeTransformer._();
const SyncAssetV1VisibilityEnumTypeTransformer._();
String encode(SyncAssetV1VisibilityEnum data) => data.value;
/// Decodes a [dynamic value][data] to a SyncAssetV1VisibilityEnum.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
SyncAssetV1VisibilityEnum? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'archive': return SyncAssetV1VisibilityEnum.archive;
case r'timeline': return SyncAssetV1VisibilityEnum.timeline;
case r'hidden': return SyncAssetV1VisibilityEnum.hidden;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [SyncAssetV1VisibilityEnumTypeTransformer] instance.
static SyncAssetV1VisibilityEnumTypeTransformer? _instance;
}

View file

@ -15,12 +15,12 @@ class UpdateAssetDto {
UpdateAssetDto({
this.dateTimeOriginal,
this.description,
this.isArchived,
this.isFavorite,
this.latitude,
this.livePhotoVideoId,
this.longitude,
this.rating,
this.visibility,
});
///
@ -39,14 +39,6 @@ class UpdateAssetDto {
///
String? description;
///
/// 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.
///
bool? isArchived;
///
/// 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
@ -83,31 +75,39 @@ class UpdateAssetDto {
///
num? rating;
///
/// 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.
///
AssetVisibility? visibility;
@override
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
other.dateTimeOriginal == dateTimeOriginal &&
other.description == description &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite &&
other.latitude == latitude &&
other.livePhotoVideoId == livePhotoVideoId &&
other.longitude == longitude &&
other.rating == rating;
other.rating == rating &&
other.visibility == visibility;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode) +
(rating == null ? 0 : rating!.hashCode);
(rating == null ? 0 : rating!.hashCode) +
(visibility == null ? 0 : visibility!.hashCode);
@override
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, rating=$rating]';
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, rating=$rating, visibility=$visibility]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -121,11 +121,6 @@ class UpdateAssetDto {
} else {
// json[r'description'] = null;
}
if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived;
} else {
// json[r'isArchived'] = null;
}
if (this.isFavorite != null) {
json[r'isFavorite'] = this.isFavorite;
} else {
@ -151,6 +146,11 @@ class UpdateAssetDto {
} else {
// json[r'rating'] = null;
}
if (this.visibility != null) {
json[r'visibility'] = this.visibility;
} else {
// json[r'visibility'] = null;
}
return json;
}
@ -165,12 +165,12 @@ class UpdateAssetDto {
return UpdateAssetDto(
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
description: mapValueOfType<String>(json, r'description'),
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: num.parse('${json[r'latitude']}'),
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
longitude: num.parse('${json[r'longitude']}'),
rating: num.parse('${json[r'rating']}'),
visibility: AssetVisibility.fromJson(json[r'visibility']),
);
}
return null;

View file

@ -1781,14 +1781,6 @@
"get": {
"operationId": "getAssetStatistics",
"parameters": [
{
"name": "isArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isFavorite",
"required": false,
@ -1804,6 +1796,14 @@
"schema": {
"type": "boolean"
}
},
{
"name": "visibility",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/AssetVisibility"
}
}
],
"responses": {
@ -6909,14 +6909,6 @@
"type": "string"
}
},
{
"name": "isArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isFavorite",
"required": false,
@ -6992,6 +6984,14 @@
"type": "string"
}
},
{
"name": "visibility",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/AssetVisibility"
}
},
{
"name": "withPartners",
"required": false,
@ -7053,14 +7053,6 @@
"type": "string"
}
},
{
"name": "isArchived",
"required": false,
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isFavorite",
"required": false,
@ -7128,6 +7120,14 @@
"type": "string"
}
},
{
"name": "visibility",
"required": false,
"in": "query",
"schema": {
"$ref": "#/components/schemas/AssetVisibility"
}
},
{
"name": "withPartners",
"required": false,
@ -8273,9 +8273,6 @@
},
"type": "array"
},
"isArchived": {
"type": "boolean"
},
"isFavorite": {
"type": "boolean"
},
@ -8289,6 +8286,13 @@
"maximum": 5,
"minimum": -1,
"type": "number"
},
"visibility": {
"allOf": [
{
"$ref": "#/components/schemas/AssetVisibility"
}
]
}
},
"required": [
@ -8713,15 +8717,9 @@
"format": "date-time",
"type": "string"
},
"isArchived": {
"type": "boolean"
},
"isFavorite": {
"type": "boolean"
},
"isVisible": {
"type": "boolean"
},
"livePhotoVideoId": {
"format": "uuid",
"type": "string"
@ -8729,6 +8727,13 @@
"sidecarData": {
"format": "binary",
"type": "string"
},
"visibility": {
"allOf": [
{
"$ref": "#/components/schemas/AssetVisibility"
}
]
}
},
"required": [
@ -9009,6 +9014,14 @@
],
"type": "string"
},
"AssetVisibility": {
"enum": [
"archive",
"timeline",
"hidden"
],
"type": "string"
},
"AudioCodec": {
"enum": [
"mp3",
@ -10204,9 +10217,6 @@
"format": "uuid",
"type": "string"
},
"isArchived": {
"type": "boolean"
},
"isEncoded": {
"type": "boolean"
},
@ -10222,9 +10232,6 @@
"isOffline": {
"type": "boolean"
},
"isVisible": {
"type": "boolean"
},
"lensModel": {
"nullable": true,
"type": "string"
@ -10324,9 +10331,12 @@
"format": "date-time",
"type": "string"
},
"withArchived": {
"default": false,
"type": "boolean"
"visibility": {
"allOf": [
{
"$ref": "#/components/schemas/AssetVisibility"
}
]
},
"withDeleted": {
"type": "boolean"
@ -11041,9 +11051,6 @@
"deviceId": {
"type": "string"
},
"isArchived": {
"type": "boolean"
},
"isEncoded": {
"type": "boolean"
},
@ -11059,9 +11066,6 @@
"isOffline": {
"type": "boolean"
},
"isVisible": {
"type": "boolean"
},
"lensModel": {
"nullable": true,
"type": "string"
@ -11137,9 +11141,12 @@
"format": "date-time",
"type": "string"
},
"withArchived": {
"default": false,
"type": "boolean"
"visibility": {
"allOf": [
{
"$ref": "#/components/schemas/AssetVisibility"
}
]
},
"withDeleted": {
"type": "boolean"
@ -11989,9 +11996,6 @@
"deviceId": {
"type": "string"
},
"isArchived": {
"type": "boolean"
},
"isEncoded": {
"type": "boolean"
},
@ -12007,9 +12011,6 @@
"isOffline": {
"type": "boolean"
},
"isVisible": {
"type": "boolean"
},
"language": {
"type": "string"
},
@ -12095,9 +12096,12 @@
"format": "date-time",
"type": "string"
},
"withArchived": {
"default": false,
"type": "boolean"
"visibility": {
"allOf": [
{
"$ref": "#/components/schemas/AssetVisibility"
}
]
},
"withDeleted": {
"type": "boolean"
@ -12381,9 +12385,6 @@
"isFavorite": {
"type": "boolean"
},
"isVisible": {
"type": "boolean"
},
"localDateTime": {
"format": "date-time",
"nullable": true,
@ -12404,6 +12405,14 @@
"OTHER"
],
"type": "string"
},
"visibility": {
"enum": [
"archive",
"timeline",
"hidden"
],
"type": "string"
}
},
"required": [
@ -12413,11 +12422,11 @@
"fileModifiedAt",
"id",
"isFavorite",
"isVisible",
"localDateTime",
"ownerId",
"thumbhash",
"type"
"type",
"visibility"
],
"type": "object"
},
@ -13671,9 +13680,6 @@
"description": {
"type": "string"
},
"isArchived": {
"type": "boolean"
},
"isFavorite": {
"type": "boolean"
},
@ -13692,6 +13698,13 @@
"maximum": 5,
"minimum": -1,
"type": "number"
},
"visibility": {
"allOf": [
{
"$ref": "#/components/schemas/AssetVisibility"
}
]
}
},
"type": "object"

View file

@ -413,11 +413,10 @@ export type AssetMediaCreateDto = {
duration?: string;
fileCreatedAt: string;
fileModifiedAt: string;
isArchived?: boolean;
isFavorite?: boolean;
isVisible?: boolean;
livePhotoVideoId?: string;
sidecarData?: Blob;
visibility?: AssetVisibility;
};
export type AssetMediaResponseDto = {
id: string;
@ -427,11 +426,11 @@ export type AssetBulkUpdateDto = {
dateTimeOriginal?: string;
duplicateId?: string | null;
ids: string[];
isArchived?: boolean;
isFavorite?: boolean;
latitude?: number;
longitude?: number;
rating?: number;
visibility?: AssetVisibility;
};
export type AssetBulkUploadCheckItem = {
/** base64 or hex encoded sha1 hash */
@ -470,12 +469,12 @@ export type AssetStatsResponseDto = {
export type UpdateAssetDto = {
dateTimeOriginal?: string;
description?: string;
isArchived?: boolean;
isFavorite?: boolean;
latitude?: number;
livePhotoVideoId?: string | null;
longitude?: number;
rating?: number;
visibility?: AssetVisibility;
};
export type AssetMediaReplaceDto = {
assetData: Blob;
@ -815,13 +814,11 @@ export type MetadataSearchDto = {
deviceId?: string;
encodedVideoPath?: string;
id?: string;
isArchived?: boolean;
isEncoded?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isNotInAlbum?: boolean;
isOffline?: boolean;
isVisible?: boolean;
lensModel?: string | null;
libraryId?: string | null;
make?: string;
@ -844,7 +841,7 @@ export type MetadataSearchDto = {
"type"?: AssetTypeEnum;
updatedAfter?: string;
updatedBefore?: string;
withArchived?: boolean;
visibility?: AssetVisibility;
withDeleted?: boolean;
withExif?: boolean;
withPeople?: boolean;
@ -888,13 +885,11 @@ export type RandomSearchDto = {
createdAfter?: string;
createdBefore?: string;
deviceId?: string;
isArchived?: boolean;
isEncoded?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isNotInAlbum?: boolean;
isOffline?: boolean;
isVisible?: boolean;
lensModel?: string | null;
libraryId?: string | null;
make?: string;
@ -911,7 +906,7 @@ export type RandomSearchDto = {
"type"?: AssetTypeEnum;
updatedAfter?: string;
updatedBefore?: string;
withArchived?: boolean;
visibility?: AssetVisibility;
withDeleted?: boolean;
withExif?: boolean;
withPeople?: boolean;
@ -923,13 +918,11 @@ export type SmartSearchDto = {
createdAfter?: string;
createdBefore?: string;
deviceId?: string;
isArchived?: boolean;
isEncoded?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isNotInAlbum?: boolean;
isOffline?: boolean;
isVisible?: boolean;
language?: string;
lensModel?: string | null;
libraryId?: string | null;
@ -949,7 +942,7 @@ export type SmartSearchDto = {
"type"?: AssetTypeEnum;
updatedAfter?: string;
updatedBefore?: string;
withArchived?: boolean;
visibility?: AssetVisibility;
withDeleted?: boolean;
withExif?: boolean;
};
@ -1877,18 +1870,18 @@ export function getRandom({ count }: {
...opts
}));
}
export function getAssetStatistics({ isArchived, isFavorite, isTrashed }: {
isArchived?: boolean;
export function getAssetStatistics({ isFavorite, isTrashed, visibility }: {
isFavorite?: boolean;
isTrashed?: boolean;
visibility?: AssetVisibility;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: AssetStatsResponseDto;
}>(`/assets/statistics${QS.query(QS.explode({
isArchived,
isFavorite,
isTrashed
isTrashed,
visibility
}))}`, {
...opts
}));
@ -3242,9 +3235,8 @@ export function tagAssets({ id, bulkIdsDto }: {
body: bulkIdsDto
})));
}
export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, withPartners, withStacked }: {
export function getTimeBucket({ albumId, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, visibility, withPartners, withStacked }: {
albumId?: string;
isArchived?: boolean;
isFavorite?: boolean;
isTrashed?: boolean;
key?: string;
@ -3254,6 +3246,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
tagId?: string;
timeBucket: string;
userId?: string;
visibility?: AssetVisibility;
withPartners?: boolean;
withStacked?: boolean;
}, opts?: Oazapfts.RequestOpts) {
@ -3262,7 +3255,6 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
data: AssetResponseDto[];
}>(`/timeline/bucket${QS.query(QS.explode({
albumId,
isArchived,
isFavorite,
isTrashed,
key,
@ -3272,15 +3264,15 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key,
tagId,
timeBucket,
userId,
visibility,
withPartners,
withStacked
}))}`, {
...opts
}));
}
export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, userId, withPartners, withStacked }: {
export function getTimeBuckets({ albumId, isFavorite, isTrashed, key, order, personId, size, tagId, userId, visibility, withPartners, withStacked }: {
albumId?: string;
isArchived?: boolean;
isFavorite?: boolean;
isTrashed?: boolean;
key?: string;
@ -3289,6 +3281,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
size: TimeBucketSize;
tagId?: string;
userId?: string;
visibility?: AssetVisibility;
withPartners?: boolean;
withStacked?: boolean;
}, opts?: Oazapfts.RequestOpts) {
@ -3297,7 +3290,6 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
data: TimeBucketResponseDto[];
}>(`/timeline/buckets${QS.query(QS.explode({
albumId,
isArchived,
isFavorite,
isTrashed,
key,
@ -3306,6 +3298,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key
size,
tagId,
userId,
visibility,
withPartners,
withStacked
}))}`, {
@ -3620,6 +3613,11 @@ export enum Permission {
AdminUserUpdate = "admin.user.update",
AdminUserDelete = "admin.user.delete"
}
export enum AssetVisibility {
Archive = "archive",
Timeline = "timeline",
Hidden = "hidden"
}
export enum AssetMediaStatus {
Created = "created",
Replaced = "replaced",

View file

@ -100,38 +100,29 @@ describe(AssetMediaController.name, () => {
expect(body).toEqual(factory.responses.badRequest());
});
it('should throw if `isVisible` is not a boolean', async () => {
it('should throw if `visibility` is not an enum', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto(), isVisible: 'not-a-boolean' });
.field({ ...makeUploadDto(), visibility: 'not-a-boolean' });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
});
it('should throw if `isArchived` is not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/assets')
.attach('assetData', assetData, filename)
.field({ ...makeUploadDto(), isArchived: 'not-a-boolean' });
expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest());
// TODO figure out how to deal with `sendFile`
describe.skip('GET /assets/:id/original', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
});
// TODO figure out how to deal with `sendFile`
describe.skip('GET /assets/:id/original', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
// TODO figure out how to deal with `sendFile`
describe.skip('GET /assets/:id/thumbnail', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`);
expect(ctx.authenticate).toHaveBeenCalled();
// TODO figure out how to deal with `sendFile`
describe.skip('GET /assets/:id/thumbnail', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
});
});

View file

@ -60,12 +60,14 @@ describe(SearchController.name, () => {
expect(body).toEqual(errorDto.badRequest(['size must not be less than 1', 'size must be an integer number']));
});
it('should reject an isArchived as not a boolean', async () => {
it('should reject an visibility as not an enum', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/metadata')
.send({ isArchived: 'immich' });
.send({ visibility: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['isArchived must be a boolean value']));
expect(body).toEqual(
errorDto.badRequest(['visibility must be one of the following values: archive, timeline, hidden']),
);
});
it('should reject an isFavorite as not a boolean', async () => {
@ -98,104 +100,98 @@ describe(SearchController.name, () => {
expect(body).toEqual(errorDto.badRequest(['isMotion must be a boolean value']));
});
it('should reject an isVisible as not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/metadata')
.send({ isVisible: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['isVisible must be a boolean value']));
});
});
describe('POST /search/random', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/search/random');
expect(ctx.authenticate).toHaveBeenCalled();
});
describe('POST /search/random', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/search/random');
expect(ctx.authenticate).toHaveBeenCalled();
it('should reject if withStacked is not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/random')
.send({ withStacked: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value']));
});
it('should reject if withPeople is not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/random')
.send({ withPeople: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value']));
});
});
it('should reject if withStacked is not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/random')
.send({ withStacked: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value']));
describe('POST /search/smart', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/search/smart');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a query', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string']));
});
});
it('should reject if withPeople is not a boolean', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/random').send({ withPeople: 'immich' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value']));
});
});
describe('POST /search/smart', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/search/smart');
expect(ctx.authenticate).toHaveBeenCalled();
describe('GET /search/explore', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/explore');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
it('should require a query', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string']));
});
});
describe('POST /search/person', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/person');
expect(ctx.authenticate).toHaveBeenCalled();
});
describe('GET /search/explore', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/explore');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('POST /search/person', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/person');
expect(ctx.authenticate).toHaveBeenCalled();
it('should require a name', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
});
});
it('should require a name', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
});
});
describe('GET /search/places', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/places');
expect(ctx.authenticate).toHaveBeenCalled();
});
describe('GET /search/places', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/places');
expect(ctx.authenticate).toHaveBeenCalled();
it('should require a name', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
});
});
it('should require a name', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
});
});
describe('GET /search/cities', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/cities');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
describe('GET /search/suggestions', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/suggestions');
expect(ctx.authenticate).toHaveBeenCalled();
describe('GET /search/cities', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/cities');
expect(ctx.authenticate).toHaveBeenCalled();
});
});
it('should require a type', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({});
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest([
'type should not be empty',
expect.stringContaining('type must be one of the following values:'),
]),
);
describe('GET /search/suggestions', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get('/search/suggestions');
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a type', async () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({});
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest([
'type should not be empty',
expect.stringContaining('type must be one of the following values:'),
]),
);
});
});
});
});

View file

@ -5,6 +5,7 @@ import {
AlbumUserRole,
AssetFileType,
AssetType,
AssetVisibility,
MemoryType,
Permission,
SharedLinkType,
@ -108,7 +109,7 @@ export type Asset = {
fileCreatedAt: Date;
fileModifiedAt: Date;
isExternal: boolean;
isVisible: boolean;
visibility: AssetVisibility;
libraryId: string | null;
livePhotoVideoId: string | null;
localDateTime: Date;
@ -285,7 +286,7 @@ export const columns = {
'assets.fileCreatedAt',
'assets.fileModifiedAt',
'assets.isExternal',
'assets.isVisible',
'assets.visibility',
'assets.libraryId',
'assets.livePhotoVideoId',
'assets.localDateTime',
@ -345,7 +346,7 @@ export const columns = {
'type',
'deletedAt',
'isFavorite',
'isVisible',
'visibility',
'updateId',
],
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],

4
server/src/db.d.ts vendored
View file

@ -10,6 +10,7 @@ import {
AssetOrder,
AssetStatus,
AssetType,
AssetVisibility,
MemoryType,
NotificationLevel,
NotificationType,
@ -148,11 +149,10 @@ export interface Assets {
fileCreatedAt: Timestamp;
fileModifiedAt: Timestamp;
id: Generated<string>;
isArchived: Generated<boolean>;
isExternal: Generated<boolean>;
isFavorite: Generated<boolean>;
isOffline: Generated<boolean>;
isVisible: Generated<boolean>;
visibility: Generated<AssetVisibility>;
libraryId: string | null;
livePhotoVideoId: string | null;
localDateTime: Timestamp;

View file

@ -1,7 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
import { AssetVisibility } from 'src/enum';
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
export enum AssetMediaSize {
/**
@ -55,11 +56,8 @@ export class AssetMediaCreateDto extends AssetMediaBase {
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ValidateBoolean({ optional: true })
isArchived?: boolean;
@ValidateBoolean({ optional: true })
isVisible?: boolean;
@ValidateAssetVisibility({ optional: true })
visibility?: AssetVisibility;
@ValidateUUID({ optional: true })
livePhotoVideoId?: string;

View file

@ -12,7 +12,7 @@ import {
} from 'src/dtos/person.dto';
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetStatus, AssetType } from 'src/enum';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { mimeTypes } from 'src/utils/mime-types';
export class SanitizedAssetResponseDto {
@ -74,11 +74,10 @@ export type MapAsset = {
fileCreatedAt: Date;
fileModifiedAt: Date;
files?: AssetFile[];
isArchived: boolean;
isExternal: boolean;
isFavorite: boolean;
isOffline: boolean;
isVisible: boolean;
visibility: AssetVisibility;
libraryId: string | null;
livePhotoVideoId: string | null;
localDateTime: Date;
@ -183,7 +182,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
localDateTime: entity.localDateTime,
updatedAt: entity.updatedAt,
isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false,
isArchived: entity.isArchived,
isArchived: entity.visibility === AssetVisibility.ARCHIVE,
isTrashed: !!entity.deletedAt,
duration: entity.duration ?? '0:00:00.00000',
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,

View file

@ -14,9 +14,9 @@ import {
ValidateIf,
} from 'class-validator';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AssetType } from 'src/enum';
import { AssetType, AssetVisibility } from 'src/enum';
import { AssetStats } from 'src/repositories/asset.repository';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation';
export class DeviceIdDto {
@IsNotEmpty()
@ -32,8 +32,8 @@ export class UpdateAssetBase {
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ValidateBoolean({ optional: true })
isArchived?: boolean;
@ValidateAssetVisibility({ optional: true })
visibility?: AssetVisibility;
@Optional()
@IsDateString()
@ -105,8 +105,8 @@ export class AssetJobsDto extends AssetIdsDto {
}
export class AssetStatsDto {
@ValidateBoolean({ optional: true })
isArchived?: boolean;
@ValidateAssetVisibility({ optional: true })
visibility?: AssetVisibility;
@ValidateBoolean({ optional: true })
isFavorite?: boolean;

View file

@ -5,8 +5,8 @@ import { Place } from 'src/database';
import { PropertyLifecycle } from 'src/decorators';
import { AlbumResponseDto } from 'src/dtos/album.dto';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AssetOrder, AssetType } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
import { AssetOrder, AssetType, AssetVisibility } from 'src/enum';
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
class BaseSearchDto {
@ValidateUUID({ optional: true, nullable: true })
@ -22,13 +22,6 @@ class BaseSearchDto {
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type?: AssetType;
@ValidateBoolean({ optional: true })
isArchived?: boolean;
@ValidateBoolean({ optional: true })
@ApiProperty({ default: false })
withArchived?: boolean;
@ValidateBoolean({ optional: true })
isEncoded?: boolean;
@ -41,8 +34,8 @@ class BaseSearchDto {
@ValidateBoolean({ optional: true })
isOffline?: boolean;
@ValidateBoolean({ optional: true })
isVisible?: boolean;
@ValidateAssetVisibility({ optional: true })
visibility?: AssetVisibility;
@ValidateBoolean({ optional: true })
withDeleted?: boolean;

View file

@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsInt, IsPositive, IsString } from 'class-validator';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AssetType, SyncEntityType, SyncRequestType } from 'src/enum';
import { AssetType, AssetVisibility, SyncEntityType, SyncRequestType } from 'src/enum';
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
export class AssetFullSyncDto {
@ -67,7 +67,7 @@ export class SyncAssetV1 {
type!: AssetType;
deletedAt!: Date | null;
isFavorite!: boolean;
isVisible!: boolean;
visibility!: AssetVisibility;
}
export class SyncAssetDeleteV1 {

View file

@ -1,8 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { AssetOrder } from 'src/enum';
import { AssetOrder, AssetVisibility } from 'src/enum';
import { TimeBucketSize } from 'src/repositories/asset.repository';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation';
export class TimeBucketDto {
@IsNotEmpty()
@ -22,9 +22,6 @@ export class TimeBucketDto {
@ValidateUUID({ optional: true })
tagId?: string;
@ValidateBoolean({ optional: true })
isArchived?: boolean;
@ValidateBoolean({ optional: true })
isFavorite?: boolean;
@ -41,6 +38,9 @@ export class TimeBucketDto {
@Optional()
@ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
order?: AssetOrder;
@ValidateAssetVisibility({ optional: true })
visibility?: AssetVisibility;
}
export class TimeBucketAssetDto extends TimeBucketDto {

View file

@ -618,3 +618,13 @@ export enum DatabaseSslMode {
Require = 'require',
VerifyFull = 'verify-full',
}
export enum AssetVisibility {
ARCHIVE = 'archive',
TIMELINE = 'timeline',
/**
* Video part of the LivePhotos and MotionPhotos
*/
HIDDEN = 'hidden',
}

View file

@ -110,8 +110,11 @@ from
and "assets"."deletedAt" is null
where
"partner"."sharedWithId" = $1
and "assets"."isArchived" = $2
and "assets"."id" in ($3)
and (
"assets"."visibility" = 'timeline'
or "assets"."visibility" = 'hidden'
)
and "assets"."id" in ($2)
-- AccessRepository.asset.checkSharedLinkAccess
select

View file

@ -7,7 +7,7 @@ select
"ownerId",
"duplicateId",
"stackId",
"isVisible",
"visibility",
"smart_search"."embedding",
(
select
@ -83,7 +83,7 @@ from
inner join "asset_job_status" on "asset_job_status"."assetId" = "assets"."id"
where
"assets"."deletedAt" is null
and "assets"."isVisible" = $1
and "assets"."visibility" != $1
and (
"asset_job_status"."previewAt" is null
or "asset_job_status"."thumbnailAt" is null
@ -118,7 +118,7 @@ where
-- AssetJobRepository.getForGenerateThumbnailJob
select
"assets"."id",
"assets"."isVisible",
"assets"."visibility",
"assets"."originalFileName",
"assets"."originalPath",
"assets"."ownerId",
@ -155,7 +155,7 @@ select
"assets"."fileCreatedAt",
"assets"."fileModifiedAt",
"assets"."isExternal",
"assets"."isVisible",
"assets"."visibility",
"assets"."libraryId",
"assets"."livePhotoVideoId",
"assets"."localDateTime",
@ -201,7 +201,7 @@ from
"assets"
inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id"
where
"assets"."isVisible" = $1
"assets"."visibility" != $1
and "assets"."deletedAt" is null
and "job_status"."previewAt" is not null
and not exists (
@ -220,7 +220,7 @@ from
"assets"
inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id"
where
"assets"."isVisible" = $1
"assets"."visibility" != $1
and "assets"."deletedAt" is null
and "job_status"."previewAt" is not null
and not exists (
@ -234,7 +234,7 @@ where
-- AssetJobRepository.getForClipEncoding
select
"assets"."id",
"assets"."isVisible",
"assets"."visibility",
(
select
coalesce(json_agg(agg), '[]')
@ -259,7 +259,7 @@ where
-- AssetJobRepository.getForDetectFacesJob
select
"assets"."id",
"assets"."isVisible",
"assets"."visibility",
to_json("exif") as "exifInfo",
(
select
@ -312,7 +312,7 @@ where
-- AssetJobRepository.getForAssetDeletion
select
"assets"."id",
"assets"."isVisible",
"assets"."visibility",
"assets"."libraryId",
"assets"."ownerId",
"assets"."livePhotoVideoId",
@ -372,7 +372,7 @@ from
"assets" as "stacked"
where
"stacked"."deletedAt" is not null
and "stacked"."isArchived" = $1
and "stacked"."visibility" != $1
and "stacked"."stackId" = "asset_stack"."id"
group by
"asset_stack"."id"
@ -391,7 +391,7 @@ where
"assets"."encodedVideoPath" is null
or "assets"."encodedVideoPath" = $2
)
and "assets"."isVisible" = $3
and "assets"."visibility" != $3
and "assets"."deletedAt" is null
-- AssetJobRepository.getForVideoConversion
@ -417,7 +417,7 @@ where
"asset_job_status"."metadataExtractedAt" is null
or "asset_job_status"."assetId" is null
)
and "assets"."isVisible" = $1
and "assets"."visibility" != $1
and "assets"."deletedAt" is null
-- AssetJobRepository.getForStorageTemplateJob
@ -480,7 +480,7 @@ where
"assets"."sidecarPath" = $1
or "assets"."sidecarPath" is null
)
and "assets"."isVisible" = $2
and "assets"."visibility" != $2
-- AssetJobRepository.streamForDetectFacesJob
select
@ -489,7 +489,7 @@ from
"assets"
inner join "asset_job_status" as "job_status" on "assetId" = "assets"."id"
where
"assets"."isVisible" = $1
"assets"."visibility" != $1
and "assets"."deletedAt" is null
and "job_status"."previewAt" is not null
and "job_status"."facesRecognizedAt" is null

View file

@ -43,21 +43,20 @@ with
"asset_job_status"."previewAt" is not null
and (assets."localDateTime" at time zone 'UTC')::date = today.date
and "assets"."ownerId" = any ($3::uuid[])
and "assets"."isVisible" = $4
and "assets"."isArchived" = $5
and "assets"."visibility" = $4
and exists (
select
from
"asset_files"
where
"assetId" = "assets"."id"
and "asset_files"."type" = $6
and "asset_files"."type" = $5
)
and "assets"."deletedAt" is null
order by
(assets."localDateTime" at time zone 'UTC')::date desc
limit
$7
$6
) as "a" on true
inner join "exif" on "a"."id" = "exif"."assetId"
)
@ -159,7 +158,7 @@ from
where
"ownerId" = $1::uuid
and "deviceId" = $2
and "isVisible" = $3
and "visibility" != $3
and "deletedAt" is null
-- AssetRepository.getLivePhotoCount
@ -241,7 +240,10 @@ with
"assets"
where
"assets"."deletedAt" is null
and "assets"."isVisible" = $2
and (
"assets"."visibility" = $2
or "assets"."visibility" = $3
)
)
select
"timeBucket",
@ -271,7 +273,7 @@ from
where
"stacked"."stackId" = "asset_stack"."id"
and "stacked"."deletedAt" is null
and "stacked"."isArchived" = $1
and "stacked"."visibility" != $1
group by
"asset_stack"."id"
) as "stacked_assets" on "asset_stack"."id" is not null
@ -281,8 +283,11 @@ where
or "assets"."stackId" is null
)
and "assets"."deletedAt" is null
and "assets"."isVisible" = $2
and date_trunc($3, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $4
and (
"assets"."visibility" = $2
or "assets"."visibility" = $3
)
and date_trunc($4, "localDateTime" at time zone 'UTC') at time zone 'UTC' = $5
order by
"assets"."localDateTime" desc
@ -307,7 +312,7 @@ with
"assets"."ownerId" = $1::uuid
and "assets"."duplicateId" is not null
and "assets"."deletedAt" is null
and "assets"."isVisible" = $2
and "assets"."visibility" != $2
and "assets"."stackId" is null
group by
"assets"."duplicateId"
@ -365,12 +370,11 @@ from
inner join "cities" on "exif"."city" = "cities"."city"
where
"ownerId" = $2::uuid
and "isVisible" = $3
and "isArchived" = $4
and "type" = $5
and "visibility" = $3
and "type" = $4
and "deletedAt" is null
limit
$6
$5
-- AssetRepository.getAllForUserFullSync
select
@ -394,7 +398,7 @@ from
) as "stacked_assets" on "asset_stack"."id" is not null
where
"assets"."ownerId" = $1::uuid
and "assets"."isVisible" = $2
and "assets"."visibility" != $2
and "assets"."updatedAt" <= $3
and "assets"."id" > $4
order by
@ -424,7 +428,7 @@ from
) as "stacked_assets" on "asset_stack"."id" is not null
where
"assets"."ownerId" = any ($1::uuid[])
and "assets"."isVisible" = $2
and "assets"."visibility" != $2
and "assets"."updatedAt" > $3
limit
$4

View file

@ -35,14 +35,14 @@ select
where
(
"assets"."type" = $1
and "assets"."isVisible" = $2
and "assets"."visibility" != $2
)
) as "photos",
count(*) filter (
where
(
"assets"."type" = $3
and "assets"."isVisible" = $4
and "assets"."visibility" != $4
)
) as "videos",
coalesce(sum("exif"."fileSizeInByte"), $5) as "usage"

View file

@ -14,7 +14,7 @@ from
and "exif"."latitude" is not null
and "exif"."longitude" is not null
where
"isVisible" = $1
"assets"."visibility" = $1
and "deletedAt" is null
and (
"ownerId" in ($2)

View file

@ -107,7 +107,7 @@ select
(
select
"assets"."ownerId",
"assets"."isArchived",
"assets"."visibility",
"assets"."fileCreatedAt"
from
"assets"
@ -203,7 +203,7 @@ from
"asset_faces"
left join "assets" on "assets"."id" = "asset_faces"."assetId"
and "asset_faces"."personId" = $1
and "assets"."isArchived" = $2
and "assets"."visibility" != $2
and "assets"."deletedAt" is null
where
"asset_faces"."deletedAt" is null
@ -220,7 +220,7 @@ from
inner join "asset_faces" on "asset_faces"."personId" = "person"."id"
inner join "assets" on "assets"."id" = "asset_faces"."assetId"
and "assets"."deletedAt" is null
and "assets"."isArchived" = $2
and "assets"."visibility" != $2
where
"person"."ownerId" = $3
and "asset_faces"."deletedAt" is null

View file

@ -7,11 +7,11 @@ from
"assets"
inner join "exif" on "assets"."id" = "exif"."assetId"
where
"assets"."fileCreatedAt" >= $1
and "exif"."lensModel" = $2
and "assets"."ownerId" = any ($3::uuid[])
and "assets"."isFavorite" = $4
and "assets"."isArchived" = $5
"assets"."visibility" = $1
and "assets"."fileCreatedAt" >= $2
and "exif"."lensModel" = $3
and "assets"."ownerId" = any ($4::uuid[])
and "assets"."isFavorite" = $5
and "assets"."deletedAt" is null
order by
"assets"."fileCreatedAt" desc
@ -28,11 +28,11 @@ offset
"assets"
inner join "exif" on "assets"."id" = "exif"."assetId"
where
"assets"."fileCreatedAt" >= $1
and "exif"."lensModel" = $2
and "assets"."ownerId" = any ($3::uuid[])
and "assets"."isFavorite" = $4
and "assets"."isArchived" = $5
"assets"."visibility" = $1
and "assets"."fileCreatedAt" >= $2
and "exif"."lensModel" = $3
and "assets"."ownerId" = any ($4::uuid[])
and "assets"."isFavorite" = $5
and "assets"."deletedAt" is null
and "assets"."id" < $6
order by
@ -48,11 +48,11 @@ union all
"assets"
inner join "exif" on "assets"."id" = "exif"."assetId"
where
"assets"."fileCreatedAt" >= $8
and "exif"."lensModel" = $9
and "assets"."ownerId" = any ($10::uuid[])
and "assets"."isFavorite" = $11
and "assets"."isArchived" = $12
"assets"."visibility" = $8
and "assets"."fileCreatedAt" >= $9
and "exif"."lensModel" = $10
and "assets"."ownerId" = any ($11::uuid[])
and "assets"."isFavorite" = $12
and "assets"."deletedAt" is null
and "assets"."id" > $13
order by
@ -71,11 +71,11 @@ from
inner join "exif" on "assets"."id" = "exif"."assetId"
inner join "smart_search" on "assets"."id" = "smart_search"."assetId"
where
"assets"."fileCreatedAt" >= $1
and "exif"."lensModel" = $2
and "assets"."ownerId" = any ($3::uuid[])
and "assets"."isFavorite" = $4
and "assets"."isArchived" = $5
"assets"."visibility" = $1
and "assets"."fileCreatedAt" >= $2
and "exif"."lensModel" = $3
and "assets"."ownerId" = any ($4::uuid[])
and "assets"."isFavorite" = $5
and "assets"."deletedAt" is null
order by
smart_search.embedding <=> $6
@ -97,7 +97,7 @@ with
where
"assets"."ownerId" = any ($2::uuid[])
and "assets"."deletedAt" is null
and "assets"."isVisible" = $3
and "assets"."visibility" != $3
and "assets"."type" = $4
and "assets"."id" != $5::uuid
and "assets"."stackId" is null
@ -176,14 +176,13 @@ with recursive
inner join "assets" on "assets"."id" = "exif"."assetId"
where
"assets"."ownerId" = any ($1::uuid[])
and "assets"."isVisible" = $2
and "assets"."isArchived" = $3
and "assets"."type" = $4
and "assets"."visibility" = $2
and "assets"."type" = $3
and "assets"."deletedAt" is null
order by
"city"
limit
$5
$4
)
union all
(
@ -200,16 +199,15 @@ with recursive
"exif"
inner join "assets" on "assets"."id" = "exif"."assetId"
where
"assets"."ownerId" = any ($6::uuid[])
and "assets"."isVisible" = $7
and "assets"."isArchived" = $8
and "assets"."type" = $9
"assets"."ownerId" = any ($5::uuid[])
and "assets"."visibility" = $6
and "assets"."type" = $7
and "assets"."deletedAt" is null
and "exif"."city" > "cte"."city"
order by
"city"
limit
$10
$8
) as "l" on true
)
)
@ -231,7 +229,7 @@ from
inner join "assets" on "assets"."id" = "exif"."assetId"
where
"ownerId" = any ($1::uuid[])
and "isVisible" = $2
and "visibility" != $2
and "deletedAt" is null
and "state" is not null
@ -243,7 +241,7 @@ from
inner join "assets" on "assets"."id" = "exif"."assetId"
where
"ownerId" = any ($1::uuid[])
and "isVisible" = $2
and "visibility" != $2
and "deletedAt" is null
and "city" is not null
@ -255,7 +253,7 @@ from
inner join "assets" on "assets"."id" = "exif"."assetId"
where
"ownerId" = any ($1::uuid[])
and "isVisible" = $2
and "visibility" != $2
and "deletedAt" is null
and "make" is not null
@ -267,6 +265,6 @@ from
inner join "assets" on "assets"."id" = "exif"."assetId"
where
"ownerId" = any ($1::uuid[])
and "isVisible" = $2
and "visibility" != $2
and "deletedAt" is null
and "model" is not null

View file

@ -84,7 +84,7 @@ select
"type",
"deletedAt",
"isFavorite",
"isVisible",
"visibility",
"updateId"
from
"assets"
@ -106,7 +106,7 @@ select
"type",
"deletedAt",
"isFavorite",
"isVisible",
"visibility",
"updateId"
from
"assets"

View file

@ -285,14 +285,14 @@ select
where
(
"assets"."type" = 'IMAGE'
and "assets"."isVisible" = true
and "assets"."visibility" != 'hidden'
)
) as "photos",
count(*) filter (
where
(
"assets"."type" = 'VIDEO'
and "assets"."isVisible" = true
and "assets"."visibility" != 'hidden'
)
) as "videos",
coalesce(

View file

@ -7,8 +7,7 @@ from
"assets"
where
"ownerId" = $2::uuid
and "isVisible" = $3
and "isArchived" = $4
and "visibility" = $3
and "deletedAt" is null
and "fileCreatedAt" is not null
and "fileModifiedAt" is not null
@ -23,13 +22,12 @@ from
left join "exif" on "assets"."id" = "exif"."assetId"
where
"ownerId" = $1::uuid
and "isVisible" = $2
and "isArchived" = $3
and "visibility" = $2
and "deletedAt" is null
and "fileCreatedAt" is not null
and "fileModifiedAt" is not null
and "localDateTime" is not null
and "originalPath" like $4
and "originalPath" not like $5
and "originalPath" like $3
and "originalPath" not like $4
order by
regexp_replace("assets"."originalPath", $6, $7) asc
regexp_replace("assets"."originalPath", $5, $6) asc

View file

@ -3,7 +3,7 @@ import { Kysely, sql } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db';
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
import { AlbumUserRole } from 'src/enum';
import { AlbumUserRole, AssetVisibility } from 'src/enum';
import { asUuid } from 'src/utils/database';
class ActivityAccess {
@ -199,7 +199,13 @@ class AssetAccess {
)
.select('assets.id')
.where('partner.sharedWithId', '=', userId)
.where('assets.isArchived', '=', false)
.where((eb) =>
eb.or([
eb('assets.visibility', '=', sql.lit(AssetVisibility.TIMELINE)),
eb('assets.visibility', '=', sql.lit(AssetVisibility.HIDDEN)),
]),
)
.where('assets.id', 'in', [...assetIds])
.execute()
.then((assets) => new Set(assets.map((asset) => asset.id)));

View file

@ -5,7 +5,7 @@ import { InjectKysely } from 'nestjs-kysely';
import { Asset, columns } from 'src/database';
import { DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetFileType, AssetType } from 'src/enum';
import { AssetFileType, AssetType, AssetVisibility } from 'src/enum';
import { StorageAsset } from 'src/types';
import {
anyUuid,
@ -34,7 +34,7 @@ export class AssetJobRepository {
'ownerId',
'duplicateId',
'stackId',
'isVisible',
'visibility',
'smart_search.embedding',
withFiles(eb, AssetFileType.PREVIEW),
])
@ -70,7 +70,7 @@ export class AssetJobRepository {
.select(['assets.id', 'assets.thumbhash'])
.select(withFiles)
.where('assets.deletedAt', 'is', null)
.where('assets.isVisible', '=', true)
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.$if(!force, (qb) =>
qb
// If there aren't any entries, metadata extraction hasn't run yet which is required for thumbnails
@ -102,7 +102,7 @@ export class AssetJobRepository {
.selectFrom('assets')
.select([
'assets.id',
'assets.isVisible',
'assets.visibility',
'assets.originalFileName',
'assets.originalPath',
'assets.ownerId',
@ -138,7 +138,7 @@ export class AssetJobRepository {
private assetsWithPreviews() {
return this.db
.selectFrom('assets')
.where('assets.isVisible', '=', true)
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.where('assets.deletedAt', 'is', null)
.innerJoin('asset_job_status as job_status', 'assetId', 'assets.id')
.where('job_status.previewAt', 'is not', null);
@ -169,7 +169,7 @@ export class AssetJobRepository {
getForClipEncoding(id: string) {
return this.db
.selectFrom('assets')
.select(['assets.id', 'assets.isVisible'])
.select(['assets.id', 'assets.visibility'])
.select((eb) => withFiles(eb, AssetFileType.PREVIEW))
.where('assets.id', '=', id)
.executeTakeFirst();
@ -179,7 +179,7 @@ export class AssetJobRepository {
getForDetectFacesJob(id: string) {
return this.db
.selectFrom('assets')
.select(['assets.id', 'assets.isVisible'])
.select(['assets.id', 'assets.visibility'])
.$call(withExifInner)
.select((eb) => withFaces(eb, true))
.select((eb) => withFiles(eb, AssetFileType.PREVIEW))
@ -209,7 +209,7 @@ export class AssetJobRepository {
.selectFrom('assets')
.select([
'assets.id',
'assets.isVisible',
'assets.visibility',
'assets.libraryId',
'assets.ownerId',
'assets.livePhotoVideoId',
@ -228,7 +228,7 @@ export class AssetJobRepository {
.select(['asset_stack.id', 'asset_stack.primaryAssetId'])
.select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets'))
.where('stacked.deletedAt', 'is not', null)
.where('stacked.isArchived', '=', false)
.where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
.whereRef('stacked.stackId', '=', 'asset_stack.id')
.groupBy('asset_stack.id')
.as('stacked_assets'),
@ -248,7 +248,7 @@ export class AssetJobRepository {
.$if(!force, (qb) =>
qb
.where((eb) => eb.or([eb('assets.encodedVideoPath', 'is', null), eb('assets.encodedVideoPath', '=', '')]))
.where('assets.isVisible', '=', true),
.where('assets.visibility', '!=', AssetVisibility.HIDDEN),
)
.where('assets.deletedAt', 'is', null)
.stream();
@ -275,7 +275,7 @@ export class AssetJobRepository {
.where((eb) =>
eb.or([eb('asset_job_status.metadataExtractedAt', 'is', null), eb('asset_job_status.assetId', 'is', null)]),
)
.where('assets.isVisible', '=', true),
.where('assets.visibility', '!=', AssetVisibility.HIDDEN),
)
.where('assets.deletedAt', 'is', null)
.stream();
@ -331,7 +331,7 @@ export class AssetJobRepository {
.$if(!force, (qb) =>
qb.where((eb) => eb.or([eb('assets.sidecarPath', '=', ''), eb('assets.sidecarPath', 'is', null)])),
)
.where('assets.isVisible', '=', true)
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.stream();
}

View file

@ -6,7 +6,7 @@ import { Stack } from 'src/database';
import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum';
import { AssetFileType, AssetOrder, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import {
anyUuid,
asUuid,
@ -14,6 +14,7 @@ import {
removeUndefinedKeys,
truncatedDate,
unnest,
withDefaultVisibility,
withExif,
withFaces,
withFacesAndPeople,
@ -30,8 +31,8 @@ export type AssetStats = Record<AssetType, number>;
export interface AssetStatsOptions {
isFavorite?: boolean;
isArchived?: boolean;
isTrashed?: boolean;
visibility?: AssetVisibility;
}
export interface LivePhotoSearchOptions {
@ -52,7 +53,6 @@ export enum TimeBucketSize {
}
export interface AssetBuilderOptions {
isArchived?: boolean;
isFavorite?: boolean;
isTrashed?: boolean;
isDuplicate?: boolean;
@ -64,6 +64,7 @@ export interface AssetBuilderOptions {
exifInfo?: boolean;
status?: AssetStatus;
assetType?: AssetType;
visibility?: AssetVisibility;
}
export interface TimeBucketOptions extends AssetBuilderOptions {
@ -258,8 +259,7 @@ export class AssetRepository {
.where('asset_job_status.previewAt', 'is not', null)
.where(sql`(assets."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`)
.where('assets.ownerId', '=', anyUuid(ownerIds))
.where('assets.isVisible', '=', true)
.where('assets.isArchived', '=', false)
.where('assets.visibility', '=', AssetVisibility.TIMELINE)
.where((eb) =>
eb.exists((qb) =>
qb
@ -348,7 +348,7 @@ export class AssetRepository {
.select(['deviceAssetId'])
.where('ownerId', '=', asUuid(ownerId))
.where('deviceId', '=', deviceId)
.where('isVisible', '=', true)
.where('visibility', '!=', AssetVisibility.HIDDEN)
.where('deletedAt', 'is', null)
.execute();
@ -393,7 +393,7 @@ export class AssetRepository {
.whereRef('stacked.stackId', '=', 'asset_stack.id')
.whereRef('stacked.id', '!=', 'asset_stack.primaryAssetId')
.where('stacked.deletedAt', 'is', null)
.where('stacked.isArchived', '=', false)
.where('stacked.visibility', '=', AssetVisibility.TIMELINE)
.groupBy('asset_stack.id')
.as('stacked_assets'),
(join) => join.on('asset_stack.id', 'is not', null),
@ -503,7 +503,7 @@ export class AssetRepository {
.executeTakeFirst();
}
getStatistics(ownerId: string, { isArchived, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
getStatistics(ownerId: string, { visibility, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
return this.db
.selectFrom('assets')
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.AUDIO).as(AssetType.AUDIO))
@ -511,8 +511,8 @@ export class AssetRepository {
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO))
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER))
.where('ownerId', '=', asUuid(ownerId))
.where('isVisible', '=', true)
.$if(isArchived !== undefined, (qb) => qb.where('isArchived', '=', isArchived!))
.$if(visibility === undefined, withDefaultVisibility)
.$if(!!visibility, (qb) => qb.where('assets.visibility', '=', visibility!))
.$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!))
.$if(!!isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
.where('deletedAt', isTrashed ? 'is not' : 'is', null)
@ -525,7 +525,7 @@ export class AssetRepository {
.selectAll('assets')
.$call(withExif)
.where('ownerId', '=', anyUuid(userIds))
.where('isVisible', '=', true)
.where('visibility', '!=', AssetVisibility.HIDDEN)
.where('deletedAt', 'is', null)
.orderBy((eb) => eb.fn('random'))
.limit(take)
@ -542,7 +542,8 @@ export class AssetRepository {
.select(truncatedDate<Date>(options.size).as('timeBucket'))
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.where('assets.isVisible', '=', true)
.$if(options.visibility === undefined, withDefaultVisibility)
.$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!))
.$if(!!options.albumId, (qb) =>
qb
.innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId')
@ -559,7 +560,6 @@ export class AssetRepository {
.where((eb) => eb.or([eb('assets.stackId', 'is', null), eb(eb.table('asset_stack'), 'is not', null)])),
)
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
.$if(!!options.assetType, (qb) => qb.where('assets.type', '=', options.assetType!))
.$if(options.isDuplicate !== undefined, (qb) =>
@ -594,7 +594,6 @@ export class AssetRepository {
)
.$if(!!options.personId, (qb) => hasPeople(qb, [options.personId!]))
.$if(!!options.userIds, (qb) => qb.where('assets.ownerId', '=', anyUuid(options.userIds!)))
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
.$if(!!options.withStacked, (qb) =>
qb
@ -610,7 +609,7 @@ export class AssetRepository {
.select((eb) => eb.fn.count(eb.table('stacked')).as('assetCount'))
.whereRef('stacked.stackId', '=', 'asset_stack.id')
.where('stacked.deletedAt', 'is', null)
.where('stacked.isArchived', '=', false)
.where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
.groupBy('asset_stack.id')
.as('stacked_assets'),
(join) => join.on('asset_stack.id', 'is not', null),
@ -624,7 +623,8 @@ export class AssetRepository {
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.where('assets.isVisible', '=', true)
.$if(options.visibility == undefined, withDefaultVisibility)
.$if(!!options.visibility, (qb) => qb.where('assets.visibility', '=', options.visibility!))
.where(truncatedDate(options.size), '=', timeBucket.replace(/^[+-]/, ''))
.orderBy('assets.localDateTime', options.order ?? 'desc')
.execute();
@ -658,7 +658,7 @@ export class AssetRepository {
.where('assets.duplicateId', 'is not', null)
.$narrowType<{ duplicateId: NotNull }>()
.where('assets.deletedAt', 'is', null)
.where('assets.isVisible', '=', true)
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.where('assets.stackId', 'is', null)
.groupBy('assets.duplicateId'),
)
@ -703,8 +703,7 @@ export class AssetRepository {
.select(['assetId as data', 'exif.city as value'])
.$narrowType<{ value: NotNull }>()
.where('ownerId', '=', asUuid(ownerId))
.where('isVisible', '=', true)
.where('isArchived', '=', false)
.where('visibility', '=', AssetVisibility.TIMELINE)
.where('type', '=', AssetType.IMAGE)
.where('deletedAt', 'is', null)
.limit(maxFields)
@ -743,7 +742,7 @@ export class AssetRepository {
)
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack'))
.where('assets.ownerId', '=', asUuid(ownerId))
.where('assets.isVisible', '=', true)
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.where('assets.updatedAt', '<=', updatedUntil)
.$if(!!lastId, (qb) => qb.where('assets.id', '>', lastId!))
.orderBy('assets.id')
@ -771,7 +770,7 @@ export class AssetRepository {
)
.select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo<Stack | null>()).as('stack'))
.where('assets.ownerId', '=', anyUuid(options.userIds))
.where('assets.isVisible', '=', true)
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.where('assets.updatedAt', '>', options.updatedAfter)
.limit(options.limit)
.execute();

View file

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { Kysely } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db';
import { AssetVisibility } from 'src/enum';
import { anyUuid } from 'src/utils/database';
const builder = (db: Kysely<DB>) =>
@ -31,6 +32,9 @@ export class DownloadRepository {
}
downloadUserId(userId: string) {
return builder(this.db).where('assets.ownerId', '=', userId).where('assets.isVisible', '=', true).stream();
return builder(this.db)
.where('assets.ownerId', '=', userId)
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.stream();
}
}

View file

@ -4,7 +4,7 @@ import { InjectKysely } from 'nestjs-kysely';
import { DB, Libraries } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
import { AssetType } from 'src/enum';
import { AssetType, AssetVisibility } from 'src/enum';
export enum AssetSyncResult {
DO_NOTHING,
@ -77,13 +77,17 @@ export class LibraryRepository {
.select((eb) =>
eb.fn
.countAll<number>()
.filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.isVisible', '=', true)]))
.filterWhere((eb) =>
eb.and([eb('assets.type', '=', AssetType.IMAGE), eb('assets.visibility', '!=', AssetVisibility.HIDDEN)]),
)
.as('photos'),
)
.select((eb) =>
eb.fn
.countAll<number>()
.filterWhere((eb) => eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.isVisible', '=', true)]))
.filterWhere((eb) =>
eb.and([eb('assets.type', '=', AssetType.VIDEO), eb('assets.visibility', '!=', AssetVisibility.HIDDEN)]),
)
.as('videos'),
)
.select((eb) => eb.fn.coalesce((eb) => eb.fn.sum('exif.fileSizeInByte'), eb.val(0)).as('usage'))

View file

@ -8,7 +8,7 @@ import readLine from 'node:readline';
import { citiesFile } from 'src/constants';
import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { SystemMetadataKey } from 'src/enum';
import { AssetVisibility, SystemMetadataKey } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
@ -75,9 +75,11 @@ export class MapRepository {
}
@GenerateSql({ params: [[DummyValue.UUID], [DummyValue.UUID]] })
getMapMarkers(ownerIds: string[], albumIds: string[], options: MapMarkerSearchOptions = {}) {
const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options;
getMapMarkers(
ownerIds: string[],
albumIds: string[],
{ isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore }: MapMarkerSearchOptions = {},
) {
return this.db
.selectFrom('assets')
.innerJoin('exif', (builder) =>
@ -88,8 +90,17 @@ export class MapRepository {
)
.select(['id', 'exif.latitude as lat', 'exif.longitude as lon', 'exif.city', 'exif.state', 'exif.country'])
.$narrowType<{ lat: NotNull; lon: NotNull }>()
.where('isVisible', '=', true)
.$if(isArchived !== undefined, (q) => q.where('isArchived', '=', isArchived!))
.$if(isArchived === true, (qb) =>
qb.where((eb) =>
eb.or([
eb('assets.visibility', '=', AssetVisibility.TIMELINE),
eb('assets.visibility', '=', AssetVisibility.ARCHIVE),
]),
),
)
.$if(isArchived === false || isArchived === undefined, (qb) =>
qb.where('assets.visibility', '=', AssetVisibility.TIMELINE),
)
.$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!))
.$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!))
.$if(fileCreatedBefore !== undefined, (q) => q.where('fileCreatedAt', '<=', fileCreatedBefore!))

View file

@ -4,7 +4,7 @@ import { jsonObjectFrom } from 'kysely/helpers/postgres';
import { InjectKysely } from 'nestjs-kysely';
import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { AssetFileType, SourceType } from 'src/enum';
import { AssetFileType, AssetVisibility, SourceType } from 'src/enum';
import { removeUndefinedKeys } from 'src/utils/database';
import { paginationHelper, PaginationOptions } from 'src/utils/pagination';
@ -157,7 +157,7 @@ export class PersonRepository {
.innerJoin('assets', (join) =>
join
.onRef('asset_faces.assetId', '=', 'assets.id')
.on('assets.isArchived', '=', false)
.on('assets.visibility', '!=', AssetVisibility.ARCHIVE)
.on('assets.deletedAt', 'is', null),
)
.where('person.ownerId', '=', userId)
@ -248,7 +248,7 @@ export class PersonRepository {
jsonObjectFrom(
eb
.selectFrom('assets')
.select(['assets.ownerId', 'assets.isArchived', 'assets.fileCreatedAt'])
.select(['assets.ownerId', 'assets.visibility', 'assets.fileCreatedAt'])
.whereRef('assets.id', '=', 'asset_faces.assetId'),
).as('asset'),
)
@ -346,7 +346,7 @@ export class PersonRepository {
join
.onRef('assets.id', '=', 'asset_faces.assetId')
.on('asset_faces.personId', '=', personId)
.on('assets.isArchived', '=', false)
.on('assets.visibility', '!=', AssetVisibility.ARCHIVE)
.on('assets.deletedAt', 'is', null),
)
.select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
@ -369,7 +369,7 @@ export class PersonRepository {
join
.onRef('assets.id', '=', 'asset_faces.assetId')
.on('assets.deletedAt', 'is', null)
.on('assets.isArchived', '=', false),
.on('assets.visibility', '!=', AssetVisibility.ARCHIVE),
)
.select((eb) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total'))
.select((eb) =>

View file

@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto';
import { DB, Exif } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetStatus, AssetType } from 'src/enum';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository';
import { anyUuid, asUuid, searchAssetBuilder, vectorIndexQuery } from 'src/utils/database';
import { paginationHelper } from 'src/utils/pagination';
@ -26,17 +26,16 @@ export interface SearchUserIdOptions {
export type SearchIdOptions = SearchAssetIdOptions & SearchUserIdOptions;
export interface SearchStatusOptions {
isArchived?: boolean;
isEncoded?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isOffline?: boolean;
isVisible?: boolean;
isNotInAlbum?: boolean;
type?: AssetType;
status?: AssetStatus;
withArchived?: boolean;
withDeleted?: boolean;
visibility?: AssetVisibility;
}
export interface SearchOneToOneRelationOptions {
@ -276,7 +275,7 @@ export class SearchRepository {
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
.where('assets.ownerId', '=', anyUuid(userIds))
.where('assets.deletedAt', 'is', null)
.where('assets.isVisible', '=', true)
.where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.where('assets.type', '=', type)
.where('assets.id', '!=', asUuid(assetId))
.where('assets.stackId', 'is', null)
@ -367,8 +366,7 @@ export class SearchRepository {
.select(['city', 'assetId'])
.innerJoin('assets', 'assets.id', 'exif.assetId')
.where('assets.ownerId', '=', anyUuid(userIds))
.where('assets.isVisible', '=', true)
.where('assets.isArchived', '=', false)
.where('assets.visibility', '=', AssetVisibility.TIMELINE)
.where('assets.type', '=', AssetType.IMAGE)
.where('assets.deletedAt', 'is', null)
.orderBy('city')
@ -384,8 +382,7 @@ export class SearchRepository {
.select(['city', 'assetId'])
.innerJoin('assets', 'assets.id', 'exif.assetId')
.where('assets.ownerId', '=', anyUuid(userIds))
.where('assets.isVisible', '=', true)
.where('assets.isArchived', '=', false)
.where('assets.visibility', '=', AssetVisibility.TIMELINE)
.where('assets.type', '=', AssetType.IMAGE)
.where('assets.deletedAt', 'is', null)
.whereRef('exif.city', '>', 'cte.city')
@ -518,7 +515,7 @@ export class SearchRepository {
.distinctOn(field)
.innerJoin('assets', 'assets.id', 'exif.assetId')
.where('ownerId', '=', anyUuid(userIds))
.where('isVisible', '=', true)
.where('visibility', '!=', AssetVisibility.HIDDEN)
.where('deletedAt', 'is', null)
.where(field, 'is not', null);
}

View file

@ -6,7 +6,7 @@ import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database';
import { DB, UserMetadata as DbUserMetadata } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetType, UserStatus } from 'src/enum';
import { AssetType, AssetVisibility, UserStatus } from 'src/enum';
import { UserTable } from 'src/schema/tables/user.table';
import { UserMetadata, UserMetadataItem } from 'src/types';
import { asUuid } from 'src/utils/database';
@ -205,13 +205,19 @@ export class UserRepository {
eb.fn
.countAll<number>()
.filterWhere((eb) =>
eb.and([eb('assets.type', '=', sql.lit(AssetType.IMAGE)), eb('assets.isVisible', '=', sql.lit(true))]),
eb.and([
eb('assets.type', '=', sql.lit(AssetType.IMAGE)),
eb('assets.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)),
]),
)
.as('photos'),
eb.fn
.countAll<number>()
.filterWhere((eb) =>
eb.and([eb('assets.type', '=', sql.lit(AssetType.VIDEO)), eb('assets.isVisible', '=', sql.lit(true))]),
eb.and([
eb('assets.type', '=', sql.lit(AssetType.VIDEO)),
eb('assets.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)),
]),
)
.as('videos'),
eb.fn

View file

@ -2,6 +2,7 @@ import { Kysely } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetVisibility } from 'src/enum';
import { asUuid, withExif } from 'src/utils/database';
export class ViewRepository {
@ -14,8 +15,7 @@ export class ViewRepository {
.select((eb) => eb.fn<string>('substring', ['assets.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath'))
.distinct()
.where('ownerId', '=', asUuid(userId))
.where('isVisible', '=', true)
.where('isArchived', '=', false)
.where('visibility', '=', AssetVisibility.TIMELINE)
.where('deletedAt', 'is', null)
.where('fileCreatedAt', 'is not', null)
.where('fileModifiedAt', 'is not', null)
@ -34,8 +34,7 @@ export class ViewRepository {
.selectAll('assets')
.$call(withExif)
.where('ownerId', '=', asUuid(userId))
.where('isVisible', '=', true)
.where('isArchived', '=', false)
.where('visibility', '=', AssetVisibility.TIMELINE)
.where('deletedAt', 'is', null)
.where('fileCreatedAt', 'is not', null)
.where('fileModifiedAt', 'is not', null)

View file

@ -1,3 +1,4 @@
import { AssetVisibility } from 'src/enum';
import { asset_face_source_type, assets_status_enum } from 'src/schema/enums';
import {
assets_delete_audit,
@ -45,7 +46,12 @@ import { UserAuditTable } from 'src/schema/tables/user-audit.table';
import { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
import { UserTable } from 'src/schema/tables/user.table';
import { VersionHistoryTable } from 'src/schema/tables/version-history.table';
import { ConfigurationParameter, Database, Extensions } from 'src/sql-tools';
import { ConfigurationParameter, Database, Extensions, registerEnum } from 'src/sql-tools';
export const asset_visibility_enum = registerEnum({
name: 'asset_visibility_enum',
values: Object.values(AssetVisibility),
});
@Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
@ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' })

View file

@ -0,0 +1,37 @@
import { Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE TYPE "asset_visibility_enum" AS ENUM ('archive','timeline','hidden');`.execute(db);
await sql`ALTER TABLE "assets"
ADD "visibility" asset_visibility_enum NOT NULL DEFAULT 'timeline';`.execute(db);
await sql`
UPDATE "assets"
SET "visibility" = CASE
WHEN "isArchived" THEN 'archive'::asset_visibility_enum
WHEN "isVisible" THEN 'timeline'::asset_visibility_enum
ELSE 'hidden'::asset_visibility_enum
END;
`.execute(db);
await sql`ALTER TABLE "assets" DROP COLUMN "isVisible";`.execute(db);
await sql`ALTER TABLE "assets" DROP COLUMN "isArchived";`.execute(db);
}
export async function down(db: Kysely<any>): Promise<void> {
await sql`ALTER TABLE "assets" ADD COLUMN "isArchived" BOOLEAN NOT NULL DEFAULT FALSE;`.execute(db);
await sql`ALTER TABLE "assets" ADD COLUMN "isVisible" BOOLEAN NOT NULL DEFAULT TRUE;`.execute(db);
await sql`
UPDATE "assets"
SET
"isArchived" = ("visibility" = 'archive'::asset_visibility_enum),
"isVisible" = CASE
WHEN "visibility" = 'timeline'::asset_visibility_enum THEN TRUE
WHEN "visibility" = 'archive'::asset_visibility_enum THEN TRUE
ELSE FALSE
END;
`.execute(db);
await sql`ALTER TABLE "assets" DROP COLUMN "visibility";`.execute(db);
await sql`DROP TYPE "asset_visibility_enum";`.execute(db);
}

View file

@ -1,5 +1,6 @@
import { UpdatedAtTrigger, UpdateIdColumn } from 'src/decorators';
import { AssetStatus, AssetType } from 'src/enum';
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { asset_visibility_enum } from 'src/schema';
import { assets_status_enum } from 'src/schema/enums';
import { assets_delete_audit } from 'src/schema/functions';
import { LibraryTable } from 'src/schema/tables/library.table';
@ -95,9 +96,6 @@ export class AssetTable {
@Column({ type: 'bytea', index: true })
checksum!: Buffer; // sha1 checksum
@Column({ type: 'boolean', default: true })
isVisible!: boolean;
@ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
livePhotoVideoId!: string | null;
@ -107,9 +105,6 @@ export class AssetTable {
@CreateDateColumn()
createdAt!: Date;
@Column({ type: 'boolean', default: false })
isArchived!: boolean;
@Column({ index: true })
originalFileName!: string;
@ -145,4 +140,7 @@ export class AssetTable {
@UpdateIdColumn({ indexName: 'IDX_assets_update_id' })
updateId?: string;
@Column({ enum: asset_visibility_enum, default: AssetVisibility.TIMELINE })
visibility!: AssetVisibility;
}

View file

@ -9,7 +9,7 @@ import { AssetFile } from 'src/database';
import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetFileType, AssetStatus, AssetType, CacheControl, JobName } from 'src/enum';
import { AssetFileType, AssetStatus, AssetType, AssetVisibility, CacheControl, JobName } from 'src/enum';
import { AuthRequest } from 'src/middleware/auth.guard';
import { AssetMediaService } from 'src/services/asset-media.service';
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';
@ -142,7 +142,6 @@ const createDto = Object.freeze({
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
isFavorite: false,
isArchived: false,
duration: '0:00:00.000000',
}) as AssetMediaCreateDto;
@ -164,7 +163,6 @@ const assetEntity = Object.freeze({
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
updatedAt: new Date('2022-06-19T23:41:36.910Z'),
isFavorite: false,
isArchived: false,
encodedVideoPath: '',
duration: '0:00:00.000000',
files: [] as AssetFile[],
@ -437,7 +435,10 @@ describe(AssetMediaService.name, () => {
});
it('should hide the linked motion asset', async () => {
mocks.asset.getById.mockResolvedValueOnce({ ...assetStub.livePhotoMotionAsset, isVisible: true });
mocks.asset.getById.mockResolvedValueOnce({
...assetStub.livePhotoMotionAsset,
visibility: AssetVisibility.TIMELINE,
});
mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
await expect(
@ -452,7 +453,10 @@ describe(AssetMediaService.name, () => {
});
expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset');
expect(mocks.asset.update).toHaveBeenCalledWith({ id: 'live-photo-motion-asset', isVisible: false });
expect(mocks.asset.update).toHaveBeenCalledWith({
id: 'live-photo-motion-asset',
visibility: AssetVisibility.HIDDEN,
});
});
it('should handle a sidecar file', async () => {

View file

@ -21,7 +21,7 @@ import {
UploadFieldName,
} from 'src/dtos/asset-media.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetStatus, AssetType, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
import { AssetStatus, AssetType, AssetVisibility, CacheControl, JobName, Permission, StorageFolder } from 'src/enum';
import { AuthRequest } from 'src/middleware/auth.guard';
import { BaseService } from 'src/services/base.service';
import { UploadFile } from 'src/types';
@ -146,7 +146,6 @@ export class AssetMediaService extends BaseService {
{ userId: auth.user.id, livePhotoVideoId: dto.livePhotoVideoId },
);
}
const asset = await this.create(auth.user.id, dto, file, sidecarFile);
await this.userRepository.updateUsage(auth.user.id, file.size);
@ -416,9 +415,8 @@ export class AssetMediaService extends BaseService {
type: mimeTypes.assetType(file.originalPath),
isFavorite: dto.isFavorite,
isArchived: dto.isArchived ?? false,
duration: dto.duration || null,
isVisible: dto.isVisible ?? true,
visibility: dto.visibility ?? AssetVisibility.TIMELINE,
livePhotoVideoId: dto.livePhotoVideoId,
originalFileName: file.originalName,
sidecarPath: sidecarFile?.originalPath,

View file

@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common';
import { DateTime } from 'luxon';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto';
import { AssetStatus, AssetType, JobName, JobStatus } from 'src/enum';
import { AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
import { AssetStats } from 'src/repositories/asset.repository';
import { AssetService } from 'src/services/asset.service';
import { assetStub } from 'test/fixtures/asset.stub';
@ -46,14 +46,22 @@ describe(AssetService.name, () => {
describe('getStatistics', () => {
it('should get the statistics for a user, excluding archived assets', async () => {
mocks.asset.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: false })).resolves.toEqual(statResponse);
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: false });
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.TIMELINE })).resolves.toEqual(
statResponse,
);
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
visibility: AssetVisibility.TIMELINE,
});
});
it('should get the statistics for a user for archived assets', async () => {
mocks.asset.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { isArchived: true })).resolves.toEqual(statResponse);
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { isArchived: true });
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.ARCHIVE })).resolves.toEqual(
statResponse,
);
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
visibility: AssetVisibility.ARCHIVE,
});
});
it('should get the statistics for a user for favorite assets', async () => {
@ -192,9 +200,9 @@ describe(AssetService.name, () => {
describe('update', () => {
it('should require asset write access for the id', async () => {
await expect(sut.update(authStub.admin, 'asset-1', { isArchived: false })).rejects.toBeInstanceOf(
BadRequestException,
);
await expect(
sut.update(authStub.admin, 'asset-1', { visibility: AssetVisibility.TIMELINE }),
).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.asset.update).not.toHaveBeenCalled();
});
@ -242,7 +250,10 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(mocks.asset.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE,
});
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
@ -263,7 +274,10 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(mocks.asset.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE,
});
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
@ -284,7 +298,10 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(mocks.asset.update).not.toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(mocks.asset.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE,
});
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
@ -296,7 +313,7 @@ describe(AssetService.name, () => {
mocks.asset.getById.mockResolvedValueOnce({
...assetStub.livePhotoMotionAsset,
ownerId: authStub.admin.user.id,
isVisible: true,
visibility: AssetVisibility.TIMELINE,
});
mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
mocks.asset.update.mockResolvedValue(assetStub.image);
@ -305,7 +322,10 @@ describe(AssetService.name, () => {
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
expect(mocks.asset.update).toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.HIDDEN,
});
expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
@ -335,7 +355,10 @@ describe(AssetService.name, () => {
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: null,
});
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: true });
expect(mocks.asset.update).toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: assetStub.livePhotoStillAsset.visibility,
});
expect(mocks.event.emit).toHaveBeenCalledWith('asset.show', {
assetId: assetStub.livePhotoMotionAsset.id,
userId: userStub.admin.id,
@ -361,7 +384,6 @@ describe(AssetService.name, () => {
await expect(
sut.updateAll(authStub.admin, {
ids: ['asset-1'],
isArchived: false,
}),
).rejects.toBeInstanceOf(BadRequestException);
});
@ -369,9 +391,11 @@ describe(AssetService.name, () => {
it('should update all assets', async () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true });
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.ARCHIVE });
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true });
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], {
visibility: AssetVisibility.ARCHIVE,
});
});
it('should not update Assets table if no relevant fields are provided', async () => {
@ -381,7 +405,6 @@ describe(AssetService.name, () => {
ids: ['asset-1'],
latitude: 0,
longitude: 0,
isArchived: undefined,
isFavorite: undefined,
duplicateId: undefined,
rating: undefined,
@ -389,14 +412,14 @@ describe(AssetService.name, () => {
expect(mocks.asset.updateAll).not.toHaveBeenCalled();
});
it('should update Assets table if isArchived field is provided', async () => {
it('should update Assets table if visibility field is provided', async () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1']));
await sut.updateAll(authStub.admin, {
ids: ['asset-1'],
latitude: 0,
longitude: 0,
isArchived: undefined,
visibility: undefined,
isFavorite: false,
duplicateId: undefined,
rating: undefined,
@ -416,7 +439,6 @@ describe(AssetService.name, () => {
latitude: 30,
longitude: 50,
dateTimeOriginal,
isArchived: undefined,
isFavorite: false,
duplicateId: undefined,
rating: undefined,
@ -439,7 +461,6 @@ describe(AssetService.name, () => {
ids: ['asset-1'],
latitude: 0,
longitude: 0,
isArchived: undefined,
isFavorite: undefined,
duplicateId: null,
rating: undefined,

View file

@ -92,8 +92,12 @@ export class AssetService extends BaseService {
const asset = await this.assetRepository.update({ id, ...rest });
if (previousMotion) {
await onAfterUnlink(repos, { userId: auth.user.id, livePhotoVideoId: previousMotion.id });
if (previousMotion && asset) {
await onAfterUnlink(repos, {
userId: auth.user.id,
livePhotoVideoId: previousMotion.id,
visibility: asset.visibility,
});
}
if (!asset) {
@ -115,7 +119,7 @@ export class AssetService extends BaseService {
}
if (
options.isArchived !== undefined ||
options.visibility !== undefined ||
options.isFavorite !== undefined ||
options.duplicateId !== undefined ||
options.rating !== undefined

View file

@ -1,4 +1,4 @@
import { AssetFileType, AssetType, JobName, JobStatus } from 'src/enum';
import { AssetFileType, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
import { DuplicateService } from 'src/services/duplicate.service';
import { SearchService } from 'src/services/search.service';
import { assetStub } from 'test/fixtures/asset.stub';
@ -22,11 +22,11 @@ const hasEmbedding = {
updateId: 'update-1',
},
],
isVisible: true,
stackId: null,
type: AssetType.IMAGE,
duplicateId: null,
embedding: '[1, 2, 3, 4]',
visibility: AssetVisibility.TIMELINE,
};
const hasDupe = {
@ -207,7 +207,10 @@ describe(SearchService.name, () => {
it('should skip if asset is not visible', async () => {
const id = assetStub.livePhotoMotionAsset.id;
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ ...hasEmbedding, isVisible: false });
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({
...hasEmbedding,
visibility: AssetVisibility.HIDDEN,
});
const result = await sut.handleSearchDuplicates({ id });

View file

@ -4,7 +4,7 @@ import { OnJob } from 'src/decorators';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
import { AssetFileType, JobName, JobStatus, QueueName } from 'src/enum';
import { AssetFileType, AssetVisibility, JobName, JobStatus, QueueName } from 'src/enum';
import { AssetDuplicateResult } from 'src/repositories/search.repository';
import { BaseService } from 'src/services/base.service';
import { JobItem, JobOf } from 'src/types';
@ -65,7 +65,7 @@ export class DuplicateService extends BaseService {
return JobStatus.SKIPPED;
}
if (!asset.isVisible) {
if (asset.visibility == AssetVisibility.HIDDEN) {
this.logger.debug(`Asset ${id} is not visible, skipping`);
return JobStatus.SKIPPED;
}

View file

@ -6,6 +6,7 @@ import { mapAsset } from 'src/dtos/asset-response.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
import {
AssetType,
AssetVisibility,
BootstrapEventPriority,
ImmichWorker,
JobCommand,
@ -301,7 +302,7 @@ export class JobService extends BaseService {
}
await this.jobRepository.queueAll(jobs);
if (asset.isVisible) {
if (asset.visibility === AssetVisibility.TIMELINE || asset.visibility === AssetVisibility.ARCHIVE) {
this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset));
}

View file

@ -8,6 +8,7 @@ import {
AssetFileType,
AssetPathType,
AssetType,
AssetVisibility,
AudioCodec,
Colorspace,
JobName,
@ -152,7 +153,7 @@ export class MediaService extends BaseService {
return JobStatus.FAILED;
}
if (!asset.isVisible) {
if (asset.visibility === AssetVisibility.HIDDEN) {
this.logger.verbose(`Thumbnail generation skipped for asset ${id}: not visible`);
return JobStatus.SKIPPED;
}

View file

@ -4,7 +4,7 @@ import { Stats } from 'node:fs';
import { constants } from 'node:fs/promises';
import { defaults } from 'src/config';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetType, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum';
import { AssetType, AssetVisibility, ExifOrientation, ImmichWorker, JobName, JobStatus, SourceType } from 'src/enum';
import { ImmichTags } from 'src/repositories/metadata.repository';
import { MetadataService } from 'src/services/metadata.service';
import { assetStub } from 'test/fixtures/asset.stub';
@ -504,7 +504,10 @@ describe(MetadataService.name, () => {
});
it('should not apply motion photos if asset is video', async () => {
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true });
mocks.assetJob.getForMetadataExtraction.mockResolvedValue({
...assetStub.livePhotoMotionAsset,
visibility: AssetVisibility.TIMELINE,
});
mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
@ -513,7 +516,7 @@ describe(MetadataService.name, () => {
expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled();
expect(mocks.asset.update).not.toHaveBeenCalledWith(
expect.objectContaining({ assetType: AssetType.VIDEO, isVisible: false }),
expect.objectContaining({ assetType: AssetType.VIDEO, visibility: AssetVisibility.HIDDEN }),
);
});
@ -580,7 +583,7 @@ describe(MetadataService.name, () => {
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
id: fileStub.livePhotoMotion.uuid,
isVisible: false,
visibility: AssetVisibility.HIDDEN,
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
originalFileName: 'asset_1.mp4',
@ -638,7 +641,7 @@ describe(MetadataService.name, () => {
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
id: fileStub.livePhotoMotion.uuid,
isVisible: false,
visibility: AssetVisibility.HIDDEN,
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
originalFileName: 'asset_1.mp4',
@ -696,7 +699,7 @@ describe(MetadataService.name, () => {
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
id: fileStub.livePhotoMotion.uuid,
isVisible: false,
visibility: AssetVisibility.HIDDEN,
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
originalFileName: 'asset_1.mp4',
@ -773,14 +776,17 @@ describe(MetadataService.name, () => {
MicroVideoOffset: 1,
});
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512));
mocks.asset.getByChecksum.mockResolvedValue({ ...assetStub.livePhotoMotionAsset, isVisible: true });
mocks.asset.getByChecksum.mockResolvedValue({
...assetStub.livePhotoMotionAsset,
visibility: AssetVisibility.TIMELINE,
});
const video = randomBytes(512);
mocks.storage.readFile.mockResolvedValue(video);
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
expect(mocks.asset.update).toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
isVisible: false,
visibility: AssetVisibility.HIDDEN,
});
expect(mocks.asset.update).toHaveBeenCalledWith({
id: assetStub.livePhotoStillAsset.id,
@ -1301,7 +1307,9 @@ describe(MetadataService.name, () => {
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled();
expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
expect(mocks.asset.update).not.toHaveBeenCalledWith(
expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
);
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
});
@ -1320,7 +1328,9 @@ describe(MetadataService.name, () => {
libraryId: null,
type: AssetType.IMAGE,
});
expect(mocks.asset.update).not.toHaveBeenCalledWith(expect.objectContaining({ isVisible: false }));
expect(mocks.asset.update).not.toHaveBeenCalledWith(
expect.objectContaining({ visibility: AssetVisibility.HIDDEN }),
);
expect(mocks.album.removeAsset).not.toHaveBeenCalled();
});
@ -1342,7 +1352,10 @@ describe(MetadataService.name, () => {
id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.id,
});
expect(mocks.asset.update).toHaveBeenCalledWith({ id: assetStub.livePhotoMotionAsset.id, isVisible: false });
expect(mocks.asset.update).toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.HIDDEN,
});
expect(mocks.album.removeAsset).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.id);
});

View file

@ -14,6 +14,7 @@ import { AssetFaces, Exif, Person } from 'src/db';
import { OnEvent, OnJob } from 'src/decorators';
import {
AssetType,
AssetVisibility,
DatabaseLock,
ExifOrientation,
ImmichWorker,
@ -156,7 +157,7 @@ export class MetadataService extends BaseService {
const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset];
await Promise.all([
this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }),
this.assetRepository.update({ id: motionAsset.id, isVisible: false }),
this.assetRepository.update({ id: motionAsset.id, visibility: AssetVisibility.HIDDEN }),
this.albumRepository.removeAsset(motionAsset.id),
]);
@ -527,8 +528,11 @@ export class MetadataService extends BaseService {
});
// Hide the motion photo video asset if it's not already hidden to prepare for linking
if (motionAsset.isVisible) {
await this.assetRepository.update({ id: motionAsset.id, isVisible: false });
if (motionAsset.visibility === AssetVisibility.TIMELINE) {
await this.assetRepository.update({
id: motionAsset.id,
visibility: AssetVisibility.HIDDEN,
});
this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`);
}
} else {
@ -544,7 +548,7 @@ export class MetadataService extends BaseService {
ownerId: asset.ownerId,
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
originalFileName: `${path.parse(asset.originalFileName).name}.mp4`,
isVisible: false,
visibility: AssetVisibility.HIDDEN,
deviceAssetId: 'NONE',
deviceId: 'NONE',
});
@ -863,7 +867,7 @@ export class MetadataService extends BaseService {
return JobStatus.FAILED;
}
if (!isSync && (!asset.isVisible || asset.sidecarPath) && !asset.isExternal) {
if (!isSync && (asset.visibility === AssetVisibility.HIDDEN || asset.sidecarPath) && !asset.isExternal) {
return JobStatus.FAILED;
}

View file

@ -26,6 +26,7 @@ import {
} from 'src/dtos/person.dto';
import {
AssetType,
AssetVisibility,
CacheControl,
ImageFormat,
JobName,
@ -296,7 +297,7 @@ export class PersonService extends BaseService {
return JobStatus.FAILED;
}
if (!asset.isVisible) {
if (asset.visibility === AssetVisibility.HIDDEN) {
return JobStatus.SKIPPED;
}
@ -484,7 +485,9 @@ export class PersonService extends BaseService {
this.logger.debug(`Face ${id} has ${matches.length} matches`);
const isCore = matches.length >= machineLearning.facialRecognition.minFaces && !face.asset.isArchived;
const isCore =
matches.length >= machineLearning.facialRecognition.minFaces &&
face.asset.visibility === AssetVisibility.TIMELINE;
if (!isCore && !deferred) {
this.logger.debug(`Deferring non-core face ${id} for later processing`);
await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } });

View file

@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
import { OnEvent, OnJob } from 'src/decorators';
import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
import { AssetVisibility, DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName } from 'src/enum';
import { ArgOf } from 'src/repositories/event.repository';
import { BaseService } from 'src/services/base.service';
import { JobItem, JobOf } from 'src/types';
@ -104,7 +104,7 @@ export class SmartInfoService extends BaseService {
return JobStatus.FAILED;
}
if (!asset.isVisible) {
if (asset.visibility === AssetVisibility.HIDDEN) {
return JobStatus.SKIPPED;
}

View file

@ -14,7 +14,7 @@ import {
SyncAckSetDto,
SyncStreamDto,
} from 'src/dtos/sync.dto';
import { DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum';
import { AssetVisibility, DatabaseAction, EntityType, Permission, SyncEntityType, SyncRequestType } from 'src/enum';
import { BaseService } from 'src/services/base.service';
import { SyncAck } from 'src/types';
import { getMyPartnerIds } from 'src/utils/asset.util';
@ -262,7 +262,10 @@ export class SyncService extends BaseService {
needsFullSync: false,
upserted: upserted
// do not return archived assets for partner users
.filter((a) => a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && !a.isArchived))
.filter(
(a) =>
a.ownerId === auth.user.id || (a.ownerId !== auth.user.id && a.visibility === AssetVisibility.TIMELINE),
)
.map((a) =>
mapAsset(a, {
auth,

View file

@ -1,4 +1,5 @@
import { BadRequestException } from '@nestjs/common';
import { AssetVisibility } from 'src/enum';
import { TimeBucketSize } from 'src/repositories/asset.repository';
import { TimelineService } from 'src/services/timeline.service';
import { assetStub } from 'test/fixtures/asset.stub';
@ -54,7 +55,7 @@ describe(TimelineService.name, () => {
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
visibility: AssetVisibility.ARCHIVE,
userId: authStub.admin.user.id,
}),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
@ -63,7 +64,7 @@ describe(TimelineService.name, () => {
expect.objectContaining({
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
visibility: AssetVisibility.ARCHIVE,
userIds: [authStub.admin.user.id],
}),
);
@ -77,7 +78,7 @@ describe(TimelineService.name, () => {
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: false,
visibility: AssetVisibility.TIMELINE,
userId: authStub.admin.user.id,
withPartners: true,
}),
@ -85,7 +86,7 @@ describe(TimelineService.name, () => {
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: false,
visibility: AssetVisibility.TIMELINE,
withPartners: true,
userIds: [authStub.admin.user.id],
});
@ -120,7 +121,7 @@ describe(TimelineService.name, () => {
const buckets = await sut.getTimeBucket(auth, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
visibility: AssetVisibility.ARCHIVE,
albumId: 'album-id',
});
@ -129,7 +130,7 @@ describe(TimelineService.name, () => {
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
visibility: AssetVisibility.ARCHIVE,
albumId: 'album-id',
});
});
@ -154,12 +155,12 @@ describe(TimelineService.name, () => {
);
});
it('should throw an error if withParners is true and isArchived true or undefined', async () => {
it('should throw an error if withParners is true and visibility true or undefined', async () => {
await expect(
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: true,
visibility: AssetVisibility.ARCHIVE,
withPartners: true,
userId: authStub.admin.user.id,
}),
@ -169,7 +170,7 @@ describe(TimelineService.name, () => {
sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY,
timeBucket: 'bucket',
isArchived: undefined,
visibility: undefined,
withPartners: true,
userId: authStub.admin.user.id,
}),

View file

@ -2,7 +2,7 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
import { Permission } from 'src/enum';
import { AssetVisibility, Permission } from 'src/enum';
import { TimeBucketOptions } from 'src/repositories/asset.repository';
import { BaseService } from 'src/services/base.service';
import { getMyPartnerIds } from 'src/utils/asset.util';
@ -55,7 +55,7 @@ export class TimelineService extends BaseService {
if (dto.userId) {
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] });
if (dto.isArchived !== false) {
if (dto.visibility === AssetVisibility.ARCHIVE) {
await this.requireAccess({ auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
}
}
@ -65,7 +65,7 @@ export class TimelineService extends BaseService {
}
if (dto.withPartners) {
const requestedArchived = dto.isArchived === true || dto.isArchived === undefined;
const requestedArchived = dto.visibility === AssetVisibility.ARCHIVE || dto.visibility === undefined;
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
const requestedTrash = dto.isTrashed === true;

View file

@ -4,7 +4,7 @@ import { AssetFile } from 'src/database';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { UploadFieldName } from 'src/dtos/asset-media.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFileType, AssetType, Permission } from 'src/enum';
import { AssetFileType, AssetType, AssetVisibility, Permission } from 'src/enum';
import { AuthRequest } from 'src/middleware/auth.guard';
import { AccessRepository } from 'src/repositories/access.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
@ -150,8 +150,8 @@ export const onBeforeLink = async (
throw new BadRequestException('Live photo video does not belong to the user');
}
if (motionAsset?.isVisible) {
await assetRepository.update({ id: livePhotoVideoId, isVisible: false });
if (motionAsset && motionAsset.visibility === AssetVisibility.TIMELINE) {
await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.HIDDEN });
await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId });
}
};
@ -174,9 +174,9 @@ export const onBeforeUnlink = async (
export const onAfterUnlink = async (
{ asset: assetRepository, event: eventRepository }: AssetHookRepositories,
{ userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string },
{ userId, livePhotoVideoId, visibility }: { userId: string; livePhotoVideoId: string; visibility: AssetVisibility },
) => {
await assetRepository.update({ id: livePhotoVideoId, isVisible: true });
await assetRepository.update({ id: livePhotoVideoId, visibility });
await eventRepository.emit('asset.show', { assetId: livePhotoVideoId, userId });
};

View file

@ -17,7 +17,7 @@ import { parse } from 'pg-connection-string';
import postgres, { Notice } from 'postgres';
import { columns, Exif, Person } from 'src/database';
import { DB } from 'src/db';
import { AssetFileType, DatabaseExtension, DatabaseSslMode } from 'src/enum';
import { AssetFileType, AssetVisibility, DatabaseExtension, DatabaseSslMode } from 'src/enum';
import { TimeBucketSize } from 'src/repositories/asset.repository';
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
import { DatabaseConnectionParams, VectorExtension } from 'src/types';
@ -155,6 +155,15 @@ export function toJson<DB, TB extends keyof DB & string, T extends TB | Expressi
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
// TODO come up with a better query that only selects the fields we need
export function withDefaultVisibility<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
return qb.where((qb) =>
qb.or([
qb('assets.visibility', '=', AssetVisibility.TIMELINE),
qb('assets.visibility', '=', AssetVisibility.ARCHIVE),
]),
);
}
export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
return qb
.leftJoin('exif', 'assets.id', 'exif.assetId')
@ -280,12 +289,14 @@ const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
options.isArchived ??= options.withArchived ? undefined : false;
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
const visibility = options.visibility == null ? AssetVisibility.TIMELINE : options.visibility;
return kysely
.withPlugin(joinDeduplicationPlugin)
.selectFrom('assets')
.selectAll('assets')
.where('assets.visibility', '=', visibility)
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
.$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!))
.$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!))
@ -356,8 +367,6 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
.$if(!!options.type, (qb) => qb.where('assets.type', '=', options.type!))
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
.$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!))
.$if(options.isVisible !== undefined, (qb) => qb.where('assets.isVisible', '=', options.isVisible!))
.$if(options.isArchived !== undefined, (qb) => qb.where('assets.isArchived', '=', options.isArchived!))
.$if(options.isEncoded !== undefined, (qb) =>
qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null),
)

View file

@ -12,6 +12,7 @@ import {
IsArray,
IsBoolean,
IsDate,
IsEnum,
IsHexColor,
IsNotEmpty,
IsOptional,
@ -29,6 +30,7 @@ import {
import { CronJob } from 'cron';
import { DateTime } from 'luxon';
import sanitize from 'sanitize-filename';
import { AssetVisibility } from 'src/enum';
import { isIP, isIPRange } from 'validator';
@Injectable()
@ -146,6 +148,17 @@ export const ValidateDate = (options?: DateOptions) => {
return applyDecorators(...decorators);
};
type AssetVisibilityOptions = { optional?: boolean };
export const ValidateAssetVisibility = (options?: AssetVisibilityOptions) => {
const { optional } = { optional: false, ...options };
const decorators = [IsEnum(AssetVisibility), ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility })];
if (optional) {
decorators.push(Optional());
}
return applyDecorators(...decorators);
};
type BooleanOptions = { optional?: boolean };
export const ValidateBoolean = (options?: BooleanOptions) => {
const { optional } = { optional: false, ...options };

View file

@ -1,6 +1,6 @@
import { AssetFace, AssetFile, Exif } from 'src/database';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AssetFileType, AssetStatus, AssetType } from 'src/enum';
import { AssetFileType, AssetStatus, AssetType, AssetVisibility } from 'src/enum';
import { StorageAsset } from 'src/types';
import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub';
@ -74,9 +74,7 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
@ -90,6 +88,7 @@ export const assetStub = {
libraryId: null,
stackId: null,
updateId: '42',
visibility: AssetVisibility.TIMELINE,
}),
noWebpPath: Object.freeze({
@ -111,9 +110,7 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
@ -130,6 +127,7 @@ export const assetStub = {
libraryId: null,
stackId: null,
updateId: '42',
visibility: AssetVisibility.TIMELINE,
}),
noThumbhash: Object.freeze({
@ -151,9 +149,7 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
duration: null,
isVisible: true,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
@ -167,6 +163,7 @@ export const assetStub = {
libraryId: null,
stackId: null,
updateId: '42',
visibility: AssetVisibility.TIMELINE,
}),
primaryImage: Object.freeze({
@ -188,9 +185,7 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
duration: null,
isVisible: true,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
@ -214,6 +209,7 @@ export const assetStub = {
isOffline: false,
updateId: '42',
libraryId: null,
visibility: AssetVisibility.TIMELINE,
}),
image: Object.freeze({
@ -235,9 +231,7 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2025-01-01T01:02:03.456Z'),
isFavorite: true,
isArchived: false,
duration: null,
isVisible: true,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
@ -257,6 +251,7 @@ export const assetStub = {
duplicateId: null,
isOffline: false,
stack: null,
visibility: AssetVisibility.TIMELINE,
}),
trashed: Object.freeze({
@ -278,9 +273,7 @@ export const assetStub = {
deletedAt: new Date('2023-02-24T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: false,
isArchived: false,
duration: null,
isVisible: true,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
@ -299,6 +292,7 @@ export const assetStub = {
libraryId: null,
stackId: null,
updateId: '42',
visibility: AssetVisibility.TIMELINE,
}),
trashedOffline: Object.freeze({
@ -321,10 +315,8 @@ export const assetStub = {
deletedAt: new Date('2023-02-24T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: false,
isArchived: false,
duration: null,
libraryId: 'library-id',
isVisible: true,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
@ -341,6 +333,7 @@ export const assetStub = {
isOffline: true,
stackId: null,
updateId: '42',
visibility: AssetVisibility.TIMELINE,
}),
archived: Object.freeze({
id: 'asset-id',
@ -361,9 +354,7 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: true,
duration: null,
isVisible: true,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
@ -382,6 +373,7 @@ export const assetStub = {
libraryId: null,
stackId: null,
updateId: '42',
visibility: AssetVisibility.TIMELINE,
}),
external: Object.freeze({
@ -403,10 +395,8 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isExternal: true,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
libraryId: 'library-id',
@ -423,6 +413,7 @@ export const assetStub = {
updateId: '42',
stackId: null,
stack: null,
visibility: AssetVisibility.TIMELINE,
}),
image1: Object.freeze({
@ -445,9 +436,7 @@ export const assetStub = {
deletedAt: null,
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
isExternal: false,
@ -464,6 +453,7 @@ export const assetStub = {
stackId: null,
libraryId: null,
stack: null,
visibility: AssetVisibility.TIMELINE,
}),
imageFrom2015: Object.freeze({
@ -485,10 +475,8 @@ export const assetStub = {
updatedAt: new Date('2015-02-23T05:06:29.716Z'),
localDateTime: new Date('2015-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isExternal: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
@ -501,6 +489,7 @@ export const assetStub = {
deletedAt: null,
duplicateId: null,
isOffline: false,
visibility: AssetVisibility.TIMELINE,
}),
video: Object.freeze({
@ -523,10 +512,8 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isExternal: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
@ -543,6 +530,7 @@ export const assetStub = {
updateId: '42',
libraryId: null,
stackId: null,
visibility: AssetVisibility.TIMELINE,
}),
livePhotoMotionAsset: Object.freeze({
@ -551,7 +539,6 @@ export const assetStub = {
originalPath: fileStub.livePhotoMotion.originalPath,
ownerId: authStub.user1.user.id,
type: AssetType.VIDEO,
isVisible: false,
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
exifInfo: {
@ -559,6 +546,7 @@ export const assetStub = {
timeZone: `America/New_York`,
},
libraryId: null,
visibility: AssetVisibility.HIDDEN,
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif }),
livePhotoStillAsset: Object.freeze({
@ -568,7 +556,6 @@ export const assetStub = {
ownerId: authStub.user1.user.id,
type: AssetType.IMAGE,
livePhotoVideoId: 'live-photo-motion-asset',
isVisible: true,
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
exifInfo: {
@ -577,6 +564,7 @@ export const assetStub = {
},
files,
faces: [] as AssetFace[],
visibility: AssetVisibility.TIMELINE,
} as MapAsset & { faces: AssetFace[] }),
livePhotoWithOriginalFileName: Object.freeze({
@ -587,7 +575,6 @@ export const assetStub = {
ownerId: authStub.user1.user.id,
type: AssetType.IMAGE,
livePhotoVideoId: 'live-photo-motion-asset',
isVisible: true,
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
exifInfo: {
@ -596,6 +583,7 @@ export const assetStub = {
},
libraryId: null,
faces: [] as AssetFace[],
visibility: AssetVisibility.TIMELINE,
} as MapAsset & { faces: AssetFace[] }),
withLocation: Object.freeze({
@ -618,10 +606,8 @@ export const assetStub = {
updatedAt: new Date('2023-02-22T05:06:29.716Z'),
localDateTime: new Date('2020-12-31T23:59:00.000Z'),
isFavorite: false,
isArchived: false,
isExternal: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
updateId: 'foo',
@ -642,6 +628,7 @@ export const assetStub = {
duplicateId: null,
isOffline: false,
tags: [],
visibility: AssetVisibility.TIMELINE,
}),
sidecar: Object.freeze({
@ -663,10 +650,8 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isExternal: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
@ -679,6 +664,7 @@ export const assetStub = {
updateId: 'foo',
libraryId: null,
stackId: null,
visibility: AssetVisibility.TIMELINE,
}),
sidecarWithoutExt: Object.freeze({
@ -700,10 +686,8 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isExternal: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
@ -713,6 +697,7 @@ export const assetStub = {
deletedAt: null,
duplicateId: null,
isOffline: false,
visibility: AssetVisibility.TIMELINE,
}),
hasEncodedVideo: Object.freeze({
@ -735,10 +720,8 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isExternal: false,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
sharedLinks: [],
@ -754,6 +737,7 @@ export const assetStub = {
libraryId: null,
stackId: null,
stack: null,
visibility: AssetVisibility.TIMELINE,
}),
hasFileExtension: Object.freeze({
@ -775,10 +759,8 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isExternal: true,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
libraryId: 'library-id',
@ -792,6 +774,7 @@ export const assetStub = {
} as Exif,
duplicateId: null,
isOffline: false,
visibility: AssetVisibility.TIMELINE,
}),
imageDng: Object.freeze({
@ -813,9 +796,7 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
duration: null,
isVisible: true,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
@ -834,6 +815,7 @@ export const assetStub = {
updateId: '42',
libraryId: null,
stackId: null,
visibility: AssetVisibility.TIMELINE,
}),
imageHif: Object.freeze({
@ -855,9 +837,7 @@ export const assetStub = {
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
duration: null,
isVisible: true,
isExternal: false,
livePhotoVideo: null,
livePhotoVideoId: null,
@ -876,5 +856,6 @@ export const assetStub = {
updateId: '42',
libraryId: null,
stackId: null,
visibility: AssetVisibility.TIMELINE,
}),
};

View file

@ -4,7 +4,7 @@ import { AssetResponseDto, MapAsset } from 'src/dtos/asset-response.dto';
import { ExifResponseDto } from 'src/dtos/exif.dto';
import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
import { mapUser } from 'src/dtos/user.dto';
import { AssetOrder, AssetStatus, AssetType, SharedLinkType } from 'src/enum';
import { AssetOrder, AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub';
@ -206,7 +206,6 @@ export const sharedLinkStub = {
thumbhash: null,
encodedVideoPath: '',
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
originalFileName: 'asset_1.jpeg',
@ -251,6 +250,7 @@ export const sharedLinkStub = {
updateId: '42',
libraryId: null,
stackId: null,
visibility: AssetVisibility.TIMELINE,
},
],
},

View file

@ -5,7 +5,7 @@ import { createHash, randomBytes } from 'node:crypto';
import { Writable } from 'node:stream';
import { AssetFace } from 'src/database';
import { AssetJobStatus, Assets, DB, FaceSearch, Person, Sessions } from 'src/db';
import { AssetType, SourceType } from 'src/enum';
import { AssetType, AssetVisibility, SourceType } from 'src/enum';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
@ -227,16 +227,37 @@ const getRepositoryMock = <K extends keyof RepositoryMocks>(key: K) => {
case 'database': {
return automock(DatabaseRepository, {
args: [undefined, { setContext: () => {} }, { getEnv: () => ({ database: { vectorExtension: '' } }) }],
args: [
undefined,
{
setContext: () => {},
},
{ getEnv: () => ({ database: { vectorExtension: '' } }) },
],
});
}
case 'email': {
return automock(EmailRepository, { args: [{ setContext: () => {} }] });
return automock(EmailRepository, {
args: [
{
setContext: () => {},
},
],
});
}
case 'job': {
return automock(JobRepository, { args: [undefined, undefined, undefined, { setContext: () => {} }] });
return automock(JobRepository, {
args: [
undefined,
undefined,
undefined,
{
setContext: () => {},
},
],
});
}
case 'logger': {
@ -345,11 +366,11 @@ const assetInsert = (asset: Partial<Insertable<Assets>> = {}) => {
type: AssetType.IMAGE,
originalPath: '/path/to/something.jpg',
ownerId: '@immich.cloud',
isVisible: true,
isFavorite: false,
fileCreatedAt: now,
fileModifiedAt: now,
localDateTime: now,
visibility: AssetVisibility.TIMELINE,
};
return {

View file

@ -456,9 +456,9 @@ describe(SyncService.name, () => {
fileCreatedAt: asset.fileCreatedAt,
fileModifiedAt: asset.fileModifiedAt,
isFavorite: asset.isFavorite,
isVisible: asset.isVisible,
localDateTime: asset.localDateTime,
type: asset.type,
visibility: asset.visibility,
},
type: 'AssetV1',
},
@ -573,9 +573,9 @@ describe(SyncService.name, () => {
fileCreatedAt: date,
fileModifiedAt: date,
isFavorite: false,
isVisible: true,
localDateTime: date,
type: asset.type,
visibility: asset.visibility,
},
type: SyncEntityType.PartnerAssetV1,
},

View file

@ -15,7 +15,7 @@ import {
} from 'src/database';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { AssetStatus, AssetType, MemoryType, Permission, UserStatus } from 'src/enum';
import { AssetStatus, AssetType, AssetVisibility, MemoryType, Permission, UserStatus } from 'src/enum';
import { OnThisDayData } from 'src/types';
export const newUuid = () => randomUUID() as string;
@ -202,11 +202,9 @@ const assetFactory = (asset: Partial<MapAsset> = {}) => ({
encodedVideoPath: null,
fileCreatedAt: newDate(),
fileModifiedAt: newDate(),
isArchived: false,
isExternal: false,
isFavorite: false,
isOffline: false,
isVisible: true,
libraryId: null,
livePhotoVideoId: null,
localDateTime: newDate(),
@ -217,6 +215,7 @@ const assetFactory = (asset: Partial<MapAsset> = {}) => ({
stackId: null,
thumbhash: null,
type: AssetType.IMAGE,
visibility: AssetVisibility.TIMELINE,
...asset,
});

View file

@ -1,8 +1,8 @@
<script lang="ts" module>
import type { SearchLocationFilter } from './search-location-section.svelte';
import type { SearchDisplayFilters } from './search-display-section.svelte';
import type { SearchDateFilter } from './search-date-section.svelte';
import { MediaType, QueryType, validQueryTypes } from '$lib/constants';
import type { SearchDateFilter } from './search-date-section.svelte';
import type { SearchDisplayFilters } from './search-display-section.svelte';
import type { SearchLocationFilter } from './search-location-section.svelte';
export type SearchFilter = {
query: string;
@ -19,24 +19,24 @@
</script>
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { preferences } from '$lib/stores/user.store';
import { parseUtcDate } from '$lib/utils/date-time';
import { generateId } from '$lib/utils/generate-id';
import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk';
import { Button } from '@immich/ui';
import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk';
import SearchPeopleSection from './search-people-section.svelte';
import SearchTagsSection from './search-tags-section.svelte';
import SearchLocationSection from './search-location-section.svelte';
import { mdiTune } from '@mdi/js';
import { t } from 'svelte-i18n';
import { SvelteSet } from 'svelte/reactivity';
import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte';
import SearchDateSection from './search-date-section.svelte';
import SearchMediaSection from './search-media-section.svelte';
import SearchRatingsSection from './search-ratings-section.svelte';
import { parseUtcDate } from '$lib/utils/date-time';
import SearchDisplaySection from './search-display-section.svelte';
import SearchLocationSection from './search-location-section.svelte';
import SearchMediaSection from './search-media-section.svelte';
import SearchPeopleSection from './search-people-section.svelte';
import SearchRatingsSection from './search-ratings-section.svelte';
import SearchTagsSection from './search-tags-section.svelte';
import SearchTextSection from './search-text-section.svelte';
import { t } from 'svelte-i18n';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { mdiTune } from '@mdi/js';
import { generateId } from '$lib/utils/generate-id';
import { SvelteSet } from 'svelte/reactivity';
import { preferences } from '$lib/stores/user.store';
interface Props {
searchQuery: MetadataSearchDto | SmartSearchDto;
@ -83,7 +83,7 @@
takenBefore: searchQuery.takenBefore ? toStartOfDayDate(searchQuery.takenBefore) : undefined,
},
display: {
isArchive: searchQuery.isArchived,
isArchive: searchQuery.visibility === AssetVisibility.Archive,
isFavorite: searchQuery.isFavorite,
isNotInAlbum: 'isNotInAlbum' in searchQuery ? searchQuery.isNotInAlbum : undefined,
},
@ -132,7 +132,7 @@
model: filter.camera.model,
takenAfter: parseOptionalDate(filter.date.takenAfter)?.startOf('day').toISO() || undefined,
takenBefore: parseOptionalDate(filter.date.takenBefore)?.endOf('day').toISO() || undefined,
isArchived: filter.display.isArchive || undefined,
visibility: filter.display.isArchive ? AssetVisibility.Archive : undefined,
isFavorite: filter.display.isFavorite || undefined,
isNotInAlbum: filter.display.isNotInAlbum || undefined,
personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined,

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { locale } from '$lib/stores/preferences.store';
import {
AssetVisibility,
getAlbumStatistics,
getAssetStatistics,
type AlbumStatisticsResponseDto,
@ -41,9 +42,9 @@
const getUsage = async () => {
[timelineStats, favoriteStats, archiveStats, trashStats, albumStats] = await Promise.all([
getAssetStatistics({ isArchived: false }),
getAssetStatistics({ visibility: AssetVisibility.Timeline }),
getAssetStatistics({ isFavorite: true }),
getAssetStatistics({ isArchived: true }),
getAssetStatistics({ visibility: AssetVisibility.Archive }),
getAssetStatistics({ isTrashed: true }),
getAlbumStatistics(),
]);

View file

@ -10,6 +10,7 @@ import { formatDateGroupTitle, fromLocalDateTime } from '$lib/utils/timeline-uti
import { TUNABLES } from '$lib/utils/tunables';
import {
AssetOrder,
AssetVisibility,
getAssetInfo,
getTimeBucket,
getTimeBuckets,
@ -1375,7 +1376,7 @@ export class AssetStore {
isExcluded(asset: AssetResponseDto) {
return (
isMismatched(this.#options.isArchived, asset.isArchived) ||
isMismatched(this.#options.visibility === AssetVisibility.Archive, asset.isArchived) ||
isMismatched(this.#options.isFavorite, asset.isFavorite) ||
isMismatched(this.#options.isTrashed, asset.isTrashed)
);

View file

@ -1,7 +1,7 @@
import { goto } from '$app/navigation';
import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte';
import type { InterpolationValues } from '$lib/components/i18n/format-message';
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { downloadManager } from '$lib/managers/download-manager.svelte';
@ -15,6 +15,7 @@ import { getFormatter } from '$lib/utils/i18n';
import { navigate } from '$lib/utils/navigation';
import {
addAssetsToAlbum as addAssets,
AssetVisibility,
createStack,
deleteAssets,
deleteStacks,
@ -507,7 +508,7 @@ export const toggleArchive = async (asset: AssetResponseDto) => {
const data = await updateAsset({
id: asset.id,
updateAssetDto: {
isArchived: !asset.isArchived,
visibility: asset.isArchived ? AssetVisibility.Timeline : AssetVisibility.Archive,
},
});
@ -531,7 +532,9 @@ export const archiveAssets = async (assets: AssetResponseDto[], archive: boolean
try {
if (ids.length > 0) {
await updateAssets({ assetBulkUpdateDto: { ids, isArchived } });
await updateAssets({
assetBulkUpdateDto: { ids, visibility: isArchived ? AssetVisibility.Archive : AssetVisibility.Timeline },
});
}
for (const asset of assets) {

View file

@ -372,7 +372,10 @@
if (viewMode === AlbumPageViewMode.VIEW) {
void assetStore.updateOptions({ albumId, order: albumOrder });
} else if (viewMode === AlbumPageViewMode.SELECT_ASSETS) {
void assetStore.updateOptions({ isArchived: false, withPartners: true, timelineAlbumId: albumId });
void assetStore.updateOptions({
withPartners: true,
timelineAlbumId: albumId,
});
}
});
@ -385,9 +388,6 @@
activityManager.reset();
assetStore.destroy();
});
// let timelineStore = new AssetStore();
// $effect(() => void timelineStore.updateOptions({ isArchived: false, withPartners: true, timelineAlbumId: albumId }));
// onDestroy(() => timelineStore.destroy());
let isOwned = $derived($user.id == album.ownerId);

View file

@ -8,17 +8,18 @@
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants';
import type { PageData } from './$types';
import { mdiPlus, mdiDotsVertical } from '@mdi/js';
import { t } from 'svelte-i18n';
import { onDestroy } from 'svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { AssetVisibility } from '@immich/sdk';
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
data: PageData;
@ -26,7 +27,7 @@
let { data }: Props = $props();
const assetStore = new AssetStore();
void assetStore.updateOptions({ isArchived: true });
void assetStore.updateOptions({ visibility: AssetVisibility.Archive });
onDestroy(() => assetStore.destroy());
const assetInteraction = new AssetInteraction();

View file

@ -9,19 +9,19 @@
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import type { PageData } from './$types';
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
import { t } from 'svelte-i18n';
import { onDestroy } from 'svelte';
import { preferences } from '$lib/stores/user.store';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { preferences } from '$lib/stores/user.store';
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
data: PageData;

View file

@ -4,16 +4,17 @@
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import { AppRoute } from '$lib/constants';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { onDestroy } from 'svelte';
import type { PageData } from './$types';
import { mdiPlus, mdiArrowLeft } from '@mdi/js';
import { t } from 'svelte-i18n';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { AssetVisibility } from '@immich/sdk';
import { mdiArrowLeft, mdiPlus } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
data: PageData;
@ -22,7 +23,14 @@
let { data }: Props = $props();
const assetStore = new AssetStore();
$effect(() => void assetStore.updateOptions({ userId: data.partner.id, isArchived: false, withStacked: true }));
$effect(
() =>
void assetStore.updateOptions({
userId: data.partner.id,
visibility: AssetVisibility.Timeline,
withStacked: true,
}),
);
onDestroy(() => assetStore.destroy());
const assetInteraction = new AssetInteraction();

View file

@ -34,12 +34,14 @@
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { locale } from '$lib/stores/preferences.store';
import { preferences } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket';
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { isExternalUrl } from '$lib/utils/navigation';
import {
AssetVisibility,
getPersonStatistics,
mergePerson,
searchPerson,
@ -59,11 +61,10 @@
mdiHeartOutline,
mdiPlus,
} from '@mdi/js';
import { DateTime } from 'luxon';
import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import { locale } from '$lib/stores/preferences.store';
import { DateTime } from 'luxon';
interface Props {
data: PageData;
@ -75,7 +76,7 @@
let { isViewing: showAssetViewer } = assetViewingStore;
const assetStore = new AssetStore();
$effect(() => void assetStore.updateOptions({ isArchived: false, personId: data.person.id }));
$effect(() => void assetStore.updateOptions({ visibility: AssetVisibility.Timeline, personId: data.person.id }));
onDestroy(() => assetStore.destroy());
const assetInteraction = new AssetInteraction();

View file

@ -32,14 +32,14 @@
type OnUnlink,
} from '$lib/utils/actions';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { AssetTypeEnum } from '@immich/sdk';
import { AssetTypeEnum, AssetVisibility } from '@immich/sdk';
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n';
let { isViewing: showAssetViewer } = assetViewingStore;
const assetStore = new AssetStore();
void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true });
void assetStore.updateOptions({ visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true });
onDestroy(() => assetStore.destroy());
const assetInteraction = new AssetInteraction();

View file

@ -1,25 +1,38 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/state';
import { shortcut } from '$lib/actions/shortcut';
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
import { cancelMultiselect } from '$lib/utils/asset-utils';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import { AppRoute, QueryParameter } from '$lib/constants';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { shortcut } from '$lib/actions/shortcut';
import type { Viewport } from '$lib/stores/assets-store.svelte';
import { lang, locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store';
import { preferences } from '$lib/stores/user.store';
import { handlePromiseError } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils';
import { parseUtcDate } from '$lib/utils/date-time';
import { handleError } from '$lib/utils/handle-error';
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
import {
type AlbumResponseDto,
type AssetResponseDto,
@ -31,21 +44,8 @@
type SmartSearchDto,
} from '@immich/sdk';
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
import type { Viewport } from '$lib/stores/assets-store.svelte';
import { lang, locale } from '$lib/stores/preferences.store';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { handlePromiseError } from '$lib/utils';
import { parseUtcDate } from '$lib/utils/date-time';
import { featureFlags } from '$lib/stores/server-config.store';
import { handleError } from '$lib/utils/handle-error';
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
import { t } from 'svelte-i18n';
import { tick } from 'svelte';
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
import { preferences } from '$lib/stores/user.store';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { t } from 'svelte-i18n';
const MAX_ASSET_COUNT = 5000;
let { isViewing: showAssetViewer } = assetViewingStore;
@ -186,7 +186,7 @@
const keyMap: Partial<Record<keyof SearchTerms, string>> = {
takenAfter: $t('start_date'),
takenBefore: $t('end_date'),
isArchived: $t('in_archive'),
visibility: $t('in_archive'),
isFavorite: $t('favorite'),
isNotInAlbum: $t('not_in_any_album'),
type: $t('media_type'),
@ -313,7 +313,7 @@
<div class="flex place-content-center place-items-center text-xs">
<div
class="bg-immich-primary py-2 px-4 text-white dark:text-black dark:bg-immich-dark-primary
{value === true ? 'rounded-full' : 'roudned-s-full'}"
{value === true ? 'rounded-full' : 'rounded-s-full'}"
>
{getHumanReadableSearchKey(key as keyof SearchTerms)}
</div>