feat(server): visibility column (#17939)

* 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

View File

@ -3,6 +3,7 @@ import {
AssetMediaStatus, AssetMediaStatus,
AssetResponseDto, AssetResponseDto,
AssetTypeEnum, AssetTypeEnum,
AssetVisibility,
getAssetInfo, getAssetInfo,
getMyUser, getMyUser,
LoginResponseDto, LoginResponseDto,
@ -119,9 +120,9 @@ describe('/asset', () => {
// stats // stats
utils.createAsset(statsUser.accessToken), utils.createAsset(statsUser.accessToken),
utils.createAsset(statsUser.accessToken, { isFavorite: true }), utils.createAsset(statsUser.accessToken, { isFavorite: true }),
utils.createAsset(statsUser.accessToken, { isArchived: true }), utils.createAsset(statsUser.accessToken, { visibility: AssetVisibility.Archive }),
utils.createAsset(statsUser.accessToken, { utils.createAsset(statsUser.accessToken, {
isArchived: true, visibility: AssetVisibility.Archive,
isFavorite: true, isFavorite: true,
assetData: { filename: 'example.mp4' }, assetData: { filename: 'example.mp4' },
}), }),
@ -309,7 +310,7 @@ describe('/asset', () => {
}); });
it('disallows viewing archived assets', async () => { 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) const { status } = await request(app)
.get(`/assets/${asset.id}`) .get(`/assets/${asset.id}`)
@ -353,7 +354,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`) .set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isArchived: true }); .query({ visibility: AssetVisibility.Archive });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ images: 1, videos: 1, total: 2 }); expect(body).toEqual({ images: 1, videos: 1, total: 2 });
@ -363,7 +364,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`) .set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: true, isArchived: true }); .query({ isFavorite: true, visibility: AssetVisibility.Archive });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ images: 0, videos: 1, total: 1 }); expect(body).toEqual({ images: 0, videos: 1, total: 1 });
@ -373,7 +374,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/assets/statistics') .get('/assets/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`) .set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: false, isArchived: false }); .query({ isFavorite: false, visibility: AssetVisibility.Timeline });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ images: 1, videos: 0, total: 1 }); expect(body).toEqual({ images: 1, videos: 0, total: 1 });
@ -459,7 +460,7 @@ describe('/asset', () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`) .put(`/assets/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isArchived: true }); .send({ visibility: AssetVisibility.Archive });
expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true }); expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
expect(status).toEqual(200); 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 { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path'; import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
@ -44,7 +44,7 @@ describe('/map', () => {
it('should get map markers for all non-archived assets', async () => { it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/map/markers') .get('/map/markers')
.query({ isArchived: false }) .query({ visibility: AssetVisibility.Timeline })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); 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 { DateTime } from 'luxon';
import { readFile } from 'node:fs/promises'; import { readFile } from 'node:fs/promises';
import { join } from 'node:path'; 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-6.heic' },
{ filename: '/formats/motionphoto/samsung-one-ui-5.jpg' }, { 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 // used for search suggestions
{ filename: '/formats/png/density_plot.png' }, { filename: '/formats/png/density_plot.png' },
@ -171,12 +178,12 @@ describe('/search', () => {
deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }), deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }),
}, },
{ {
should: 'should search by isArchived (true)', should: 'should search by visibility (AssetVisibility.Archive)',
deferred: () => ({ dto: { isArchived: true }, assets: [assetSprings] }), deferred: () => ({ dto: { visibility: AssetVisibility.Archive }, assets: [assetSprings] }),
}, },
{ {
should: 'should search by isArchived (false)', should: 'should search by visibility (AssetVisibility.Timeline)',
deferred: () => ({ dto: { size: 1, isArchived: false }, assets: [assetLast] }), deferred: () => ({ dto: { size: 1, visibility: AssetVisibility.Timeline }, assets: [assetLast] }),
}, },
{ {
should: 'should search by type (image)', should: 'should search by type (image)',
@ -185,7 +192,7 @@ describe('/search', () => {
{ {
should: 'should search by type (video)', should: 'should search by type (video)',
deferred: () => ({ deferred: () => ({
dto: { type: 'VIDEO' }, dto: { type: 'VIDEO', visibility: AssetVisibility.Hidden },
assets: [ assets: [
// the three live motion photos // the three live motion photos
{ id: expect.any(String) }, { id: expect.any(String) },
@ -229,13 +236,6 @@ describe('/search', () => {
should: 'should search by takenAfter (no results)', should: 'should search by takenAfter (no results)',
deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }), 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', should: 'should search by originalFilename',
deferred: () => ({ deferred: () => ({
@ -265,7 +265,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
city: '', city: '',
isVisible: true, visibility: AssetVisibility.Timeline,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@ -276,7 +276,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
city: null, city: null,
isVisible: true, visibility: AssetVisibility.Timeline,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@ -297,7 +297,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
state: '', state: '',
isVisible: true, visibility: AssetVisibility.Timeline,
withExif: true, withExif: true,
includeNull: true, includeNull: true,
}, },
@ -309,7 +309,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
state: null, state: null,
isVisible: true, visibility: AssetVisibility.Timeline,
includeNull: true, includeNull: true,
}, },
assets: [assetLast, assetNotocactus], assets: [assetLast, assetNotocactus],
@ -330,7 +330,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
country: '', country: '',
isVisible: true, visibility: AssetVisibility.Timeline,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], assets: [assetLast],
@ -341,7 +341,7 @@ describe('/search', () => {
deferred: () => ({ deferred: () => ({
dto: { dto: {
country: null, country: null,
isVisible: true, visibility: AssetVisibility.Timeline,
includeNull: true, includeNull: true,
}, },
assets: [assetLast], 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 { DateTime } from 'luxon';
import { createUserDto } from 'src/fixtures'; import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
@ -104,7 +104,7 @@ describe('/timeline', () => {
const req1 = await request(app) const req1 = await request(app)
.get('/timeline/buckets') .get('/timeline/buckets')
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`) .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.status).toBe(400);
expect(req1.body).toEqual(errorDto.badRequest()); expect(req1.body).toEqual(errorDto.badRequest());
@ -112,7 +112,7 @@ describe('/timeline', () => {
const req2 = await request(app) const req2 = await request(app)
.get('/timeline/buckets') .get('/timeline/buckets')
.set('Authorization', `Bearer ${user.accessToken}`) .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.status).toBe(400);
expect(req2.body).toEqual(errorDto.badRequest()); expect(req2.body).toEqual(errorDto.badRequest());

View File

@ -3,6 +3,7 @@ import {
AssetMediaCreateDto, AssetMediaCreateDto,
AssetMediaResponseDto, AssetMediaResponseDto,
AssetResponseDto, AssetResponseDto,
AssetVisibility,
CheckExistingAssetsDto, CheckExistingAssetsDto,
CreateAlbumDto, CreateAlbumDto,
CreateLibraryDto, CreateLibraryDto,
@ -429,7 +430,10 @@ export const utils = {
}, },
archiveAssets: (accessToken: string, ids: string[]) => 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: (accessToken: string, ids: string[]) =>
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }), deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),

View File

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

View File

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

View File

@ -302,6 +302,7 @@ Class | Method | HTTP request | Description
- [AssetStackResponseDto](doc//AssetStackResponseDto.md) - [AssetStackResponseDto](doc//AssetStackResponseDto.md)
- [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md)
- [AssetTypeEnum](doc//AssetTypeEnum.md) - [AssetTypeEnum](doc//AssetTypeEnum.md)
- [AssetVisibility](doc//AssetVisibility.md)
- [AudioCodec](doc//AudioCodec.md) - [AudioCodec](doc//AudioCodec.md)
- [AvatarUpdate](doc//AvatarUpdate.md) - [AvatarUpdate](doc//AvatarUpdate.md)
- [BulkIdResponseDto](doc//BulkIdResponseDto.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_stack_response_dto.dart';
part 'model/asset_stats_response_dto.dart'; part 'model/asset_stats_response_dto.dart';
part 'model/asset_type_enum.dart'; part 'model/asset_type_enum.dart';
part 'model/asset_visibility.dart';
part 'model/audio_codec.dart'; part 'model/audio_codec.dart';
part 'model/avatar_update.dart'; part 'model/avatar_update.dart';
part 'model/bulk_id_response_dto.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]. /// Performs an HTTP 'GET /assets/statistics' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [bool] isArchived:
///
/// * [bool] isFavorite: /// * [bool] isFavorite:
/// ///
/// * [bool] isTrashed: /// * [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 // ignore: prefer_const_declarations
final apiPath = r'/assets/statistics'; final apiPath = r'/assets/statistics';
@ -358,15 +358,15 @@ class AssetsApi {
final headerParams = <String, String>{}; final headerParams = <String, String>{};
final formParams = <String, String>{}; final formParams = <String, String>{};
if (isArchived != null) {
queryParams.addAll(_queryParams('', 'isArchived', isArchived));
}
if (isFavorite != null) { if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite)); queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
} }
if (isTrashed != null) { if (isTrashed != null) {
queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); queryParams.addAll(_queryParams('', 'isTrashed', isTrashed));
} }
if (visibility != null) {
queryParams.addAll(_queryParams('', 'visibility', visibility));
}
const contentTypes = <String>[]; const contentTypes = <String>[];
@ -384,13 +384,13 @@ class AssetsApi {
/// Parameters: /// Parameters:
/// ///
/// * [bool] isArchived:
///
/// * [bool] isFavorite: /// * [bool] isFavorite:
/// ///
/// * [bool] isTrashed: /// * [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) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }
@ -788,16 +788,14 @@ class AssetsApi {
/// ///
/// * [String] duration: /// * [String] duration:
/// ///
/// * [bool] isArchived:
///
/// * [bool] isFavorite: /// * [bool] isFavorite:
/// ///
/// * [bool] isVisible:
///
/// * [String] livePhotoVideoId: /// * [String] livePhotoVideoId:
/// ///
/// * [MultipartFile] sidecarData: /// * [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 // ignore: prefer_const_declarations
final apiPath = r'/assets'; final apiPath = r'/assets';
@ -845,18 +843,10 @@ class AssetsApi {
hasFields = true; hasFields = true;
mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt); mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt);
} }
if (isArchived != null) {
hasFields = true;
mp.fields[r'isArchived'] = parameterToString(isArchived);
}
if (isFavorite != null) { if (isFavorite != null) {
hasFields = true; hasFields = true;
mp.fields[r'isFavorite'] = parameterToString(isFavorite); mp.fields[r'isFavorite'] = parameterToString(isFavorite);
} }
if (isVisible != null) {
hasFields = true;
mp.fields[r'isVisible'] = parameterToString(isVisible);
}
if (livePhotoVideoId != null) { if (livePhotoVideoId != null) {
hasFields = true; hasFields = true;
mp.fields[r'livePhotoVideoId'] = parameterToString(livePhotoVideoId); mp.fields[r'livePhotoVideoId'] = parameterToString(livePhotoVideoId);
@ -866,6 +856,10 @@ class AssetsApi {
mp.fields[r'sidecarData'] = sidecarData.field; mp.fields[r'sidecarData'] = sidecarData.field;
mp.files.add(sidecarData); mp.files.add(sidecarData);
} }
if (visibility != null) {
hasFields = true;
mp.fields[r'visibility'] = parameterToString(visibility);
}
if (hasFields) { if (hasFields) {
postBody = mp; postBody = mp;
} }
@ -900,17 +894,15 @@ class AssetsApi {
/// ///
/// * [String] duration: /// * [String] duration:
/// ///
/// * [bool] isArchived:
///
/// * [bool] isFavorite: /// * [bool] isFavorite:
/// ///
/// * [bool] isVisible:
///
/// * [String] livePhotoVideoId: /// * [String] livePhotoVideoId:
/// ///
/// * [MultipartFile] sidecarData: /// * [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) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
} }

View File

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

View File

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

View File

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

View File

@ -16,11 +16,11 @@ class AssetBulkUpdateDto {
this.dateTimeOriginal, this.dateTimeOriginal,
this.duplicateId, this.duplicateId,
this.ids = const [], this.ids = const [],
this.isArchived,
this.isFavorite, this.isFavorite,
this.latitude, this.latitude,
this.longitude, this.longitude,
this.rating, this.rating,
this.visibility,
}); });
/// ///
@ -35,14 +35,6 @@ class AssetBulkUpdateDto {
List<String> ids; 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 /// 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 /// does not include a default value (using the "default:" property), however, the generated
@ -77,16 +69,24 @@ class AssetBulkUpdateDto {
/// ///
num? rating; 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 @override
bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto && bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto &&
other.dateTimeOriginal == dateTimeOriginal && other.dateTimeOriginal == dateTimeOriginal &&
other.duplicateId == duplicateId && other.duplicateId == duplicateId &&
_deepEquality.equals(other.ids, ids) && _deepEquality.equals(other.ids, ids) &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.latitude == latitude && other.latitude == latitude &&
other.longitude == longitude && other.longitude == longitude &&
other.rating == rating; other.rating == rating &&
other.visibility == visibility;
@override @override
int get hashCode => int get hashCode =>
@ -94,14 +94,14 @@ class AssetBulkUpdateDto {
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) + (dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
(duplicateId == null ? 0 : duplicateId!.hashCode) + (duplicateId == null ? 0 : duplicateId!.hashCode) +
(ids.hashCode) + (ids.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) + (latitude == null ? 0 : latitude!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode) + (longitude == null ? 0 : longitude!.hashCode) +
(rating == null ? 0 : rating!.hashCode); (rating == null ? 0 : rating!.hashCode) +
(visibility == null ? 0 : visibility!.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -116,11 +116,6 @@ class AssetBulkUpdateDto {
// json[r'duplicateId'] = null; // json[r'duplicateId'] = null;
} }
json[r'ids'] = this.ids; json[r'ids'] = this.ids;
if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived;
} else {
// json[r'isArchived'] = null;
}
if (this.isFavorite != null) { if (this.isFavorite != null) {
json[r'isFavorite'] = this.isFavorite; json[r'isFavorite'] = this.isFavorite;
} else { } else {
@ -141,6 +136,11 @@ class AssetBulkUpdateDto {
} else { } else {
// json[r'rating'] = null; // json[r'rating'] = null;
} }
if (this.visibility != null) {
json[r'visibility'] = this.visibility;
} else {
// json[r'visibility'] = null;
}
return json; return json;
} }
@ -158,11 +158,11 @@ class AssetBulkUpdateDto {
ids: json[r'ids'] is Iterable ids: json[r'ids'] is Iterable
? (json[r'ids'] as Iterable).cast<String>().toList(growable: false) ? (json[r'ids'] as Iterable).cast<String>().toList(growable: false)
: const [], : const [],
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: num.parse('${json[r'latitude']}'), latitude: num.parse('${json[r'latitude']}'),
longitude: num.parse('${json[r'longitude']}'), longitude: num.parse('${json[r'longitude']}'),
rating: num.parse('${json[r'rating']}'), rating: num.parse('${json[r'rating']}'),
visibility: AssetVisibility.fromJson(json[r'visibility']),
); );
} }
return null; 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.deviceId,
this.encodedVideoPath, this.encodedVideoPath,
this.id, this.id,
this.isArchived,
this.isEncoded, this.isEncoded,
this.isFavorite, this.isFavorite,
this.isMotion, this.isMotion,
this.isNotInAlbum, this.isNotInAlbum,
this.isOffline, this.isOffline,
this.isVisible,
this.lensModel, this.lensModel,
this.libraryId, this.libraryId,
this.make, this.make,
@ -52,7 +50,7 @@ class MetadataSearchDto {
this.type, this.type,
this.updatedAfter, this.updatedAfter,
this.updatedBefore, this.updatedBefore,
this.withArchived = false, this.visibility,
this.withDeleted, this.withDeleted,
this.withExif, this.withExif,
this.withPeople, this.withPeople,
@ -127,14 +125,6 @@ class MetadataSearchDto {
/// ///
String? id; 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 /// 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 /// does not include a default value (using the "default:" property), however, the generated
@ -175,14 +165,6 @@ class MetadataSearchDto {
/// ///
bool? isOffline; 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? lensModel;
String? libraryId; String? libraryId;
@ -322,7 +304,13 @@ class MetadataSearchDto {
/// ///
DateTime? updatedBefore; 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 /// Please note: This property should have been non-nullable! Since the specification file
@ -368,13 +356,11 @@ class MetadataSearchDto {
other.deviceId == deviceId && other.deviceId == deviceId &&
other.encodedVideoPath == encodedVideoPath && other.encodedVideoPath == encodedVideoPath &&
other.id == id && other.id == id &&
other.isArchived == isArchived &&
other.isEncoded == isEncoded && other.isEncoded == isEncoded &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.isMotion == isMotion && other.isMotion == isMotion &&
other.isNotInAlbum == isNotInAlbum && other.isNotInAlbum == isNotInAlbum &&
other.isOffline == isOffline && other.isOffline == isOffline &&
other.isVisible == isVisible &&
other.lensModel == lensModel && other.lensModel == lensModel &&
other.libraryId == libraryId && other.libraryId == libraryId &&
other.make == make && other.make == make &&
@ -397,7 +383,7 @@ class MetadataSearchDto {
other.type == type && other.type == type &&
other.updatedAfter == updatedAfter && other.updatedAfter == updatedAfter &&
other.updatedBefore == updatedBefore && other.updatedBefore == updatedBefore &&
other.withArchived == withArchived && other.visibility == visibility &&
other.withDeleted == withDeleted && other.withDeleted == withDeleted &&
other.withExif == withExif && other.withExif == withExif &&
other.withPeople == withPeople && other.withPeople == withPeople &&
@ -416,13 +402,11 @@ class MetadataSearchDto {
(deviceId == null ? 0 : deviceId!.hashCode) + (deviceId == null ? 0 : deviceId!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) + (encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(id == null ? 0 : id!.hashCode) + (id == null ? 0 : id!.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) + (isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) +
(isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) + (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) +
(isOffline == null ? 0 : isOffline!.hashCode) + (isOffline == null ? 0 : isOffline!.hashCode) +
(isVisible == null ? 0 : isVisible!.hashCode) +
(lensModel == null ? 0 : lensModel!.hashCode) + (lensModel == null ? 0 : lensModel!.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) + (libraryId == null ? 0 : libraryId!.hashCode) +
(make == null ? 0 : make!.hashCode) + (make == null ? 0 : make!.hashCode) +
@ -445,14 +429,14 @@ class MetadataSearchDto {
(type == null ? 0 : type!.hashCode) + (type == null ? 0 : type!.hashCode) +
(updatedAfter == null ? 0 : updatedAfter!.hashCode) + (updatedAfter == null ? 0 : updatedAfter!.hashCode) +
(updatedBefore == null ? 0 : updatedBefore!.hashCode) + (updatedBefore == null ? 0 : updatedBefore!.hashCode) +
(withArchived.hashCode) + (visibility == null ? 0 : visibility!.hashCode) +
(withDeleted == null ? 0 : withDeleted!.hashCode) + (withDeleted == null ? 0 : withDeleted!.hashCode) +
(withExif == null ? 0 : withExif!.hashCode) + (withExif == null ? 0 : withExif!.hashCode) +
(withPeople == null ? 0 : withPeople!.hashCode) + (withPeople == null ? 0 : withPeople!.hashCode) +
(withStacked == null ? 0 : withStacked!.hashCode); (withStacked == null ? 0 : withStacked!.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -506,11 +490,6 @@ class MetadataSearchDto {
} else { } else {
// json[r'id'] = null; // json[r'id'] = null;
} }
if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived;
} else {
// json[r'isArchived'] = null;
}
if (this.isEncoded != null) { if (this.isEncoded != null) {
json[r'isEncoded'] = this.isEncoded; json[r'isEncoded'] = this.isEncoded;
} else { } else {
@ -536,11 +515,6 @@ class MetadataSearchDto {
} else { } else {
// json[r'isOffline'] = null; // json[r'isOffline'] = null;
} }
if (this.isVisible != null) {
json[r'isVisible'] = this.isVisible;
} else {
// json[r'isVisible'] = null;
}
if (this.lensModel != null) { if (this.lensModel != null) {
json[r'lensModel'] = this.lensModel; json[r'lensModel'] = this.lensModel;
} else { } else {
@ -639,7 +613,11 @@ class MetadataSearchDto {
} else { } else {
// json[r'updatedBefore'] = null; // 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) { if (this.withDeleted != null) {
json[r'withDeleted'] = this.withDeleted; json[r'withDeleted'] = this.withDeleted;
} else { } else {
@ -682,13 +660,11 @@ class MetadataSearchDto {
deviceId: mapValueOfType<String>(json, r'deviceId'), deviceId: mapValueOfType<String>(json, r'deviceId'),
encodedVideoPath: mapValueOfType<String>(json, r'encodedVideoPath'), encodedVideoPath: mapValueOfType<String>(json, r'encodedVideoPath'),
id: mapValueOfType<String>(json, r'id'), id: mapValueOfType<String>(json, r'id'),
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isEncoded: mapValueOfType<bool>(json, r'isEncoded'), isEncoded: mapValueOfType<bool>(json, r'isEncoded'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isMotion: mapValueOfType<bool>(json, r'isMotion'), isMotion: mapValueOfType<bool>(json, r'isMotion'),
isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'), isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
isOffline: mapValueOfType<bool>(json, r'isOffline'), isOffline: mapValueOfType<bool>(json, r'isOffline'),
isVisible: mapValueOfType<bool>(json, r'isVisible'),
lensModel: mapValueOfType<String>(json, r'lensModel'), lensModel: mapValueOfType<String>(json, r'lensModel'),
libraryId: mapValueOfType<String>(json, r'libraryId'), libraryId: mapValueOfType<String>(json, r'libraryId'),
make: mapValueOfType<String>(json, r'make'), make: mapValueOfType<String>(json, r'make'),
@ -715,7 +691,7 @@ class MetadataSearchDto {
type: AssetTypeEnum.fromJson(json[r'type']), type: AssetTypeEnum.fromJson(json[r'type']),
updatedAfter: mapDateTime(json, r'updatedAfter', r''), updatedAfter: mapDateTime(json, r'updatedAfter', r''),
updatedBefore: mapDateTime(json, r'updatedBefore', 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'), withDeleted: mapValueOfType<bool>(json, r'withDeleted'),
withExif: mapValueOfType<bool>(json, r'withExif'), withExif: mapValueOfType<bool>(json, r'withExif'),
withPeople: mapValueOfType<bool>(json, r'withPeople'), withPeople: mapValueOfType<bool>(json, r'withPeople'),

View File

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

View File

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

View File

@ -19,11 +19,11 @@ class SyncAssetV1 {
required this.fileModifiedAt, required this.fileModifiedAt,
required this.id, required this.id,
required this.isFavorite, required this.isFavorite,
required this.isVisible,
required this.localDateTime, required this.localDateTime,
required this.ownerId, required this.ownerId,
required this.thumbhash, required this.thumbhash,
required this.type, required this.type,
required this.visibility,
}); });
String checksum; String checksum;
@ -38,8 +38,6 @@ class SyncAssetV1 {
bool isFavorite; bool isFavorite;
bool isVisible;
DateTime? localDateTime; DateTime? localDateTime;
String ownerId; String ownerId;
@ -48,6 +46,8 @@ class SyncAssetV1 {
SyncAssetV1TypeEnum type; SyncAssetV1TypeEnum type;
SyncAssetV1VisibilityEnum visibility;
@override @override
bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 && bool operator ==(Object other) => identical(this, other) || other is SyncAssetV1 &&
other.checksum == checksum && other.checksum == checksum &&
@ -56,11 +56,11 @@ class SyncAssetV1 {
other.fileModifiedAt == fileModifiedAt && other.fileModifiedAt == fileModifiedAt &&
other.id == id && other.id == id &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.isVisible == isVisible &&
other.localDateTime == localDateTime && other.localDateTime == localDateTime &&
other.ownerId == ownerId && other.ownerId == ownerId &&
other.thumbhash == thumbhash && other.thumbhash == thumbhash &&
other.type == type; other.type == type &&
other.visibility == visibility;
@override @override
int get hashCode => int get hashCode =>
@ -71,14 +71,14 @@ class SyncAssetV1 {
(fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) + (fileModifiedAt == null ? 0 : fileModifiedAt!.hashCode) +
(id.hashCode) + (id.hashCode) +
(isFavorite.hashCode) + (isFavorite.hashCode) +
(isVisible.hashCode) +
(localDateTime == null ? 0 : localDateTime!.hashCode) + (localDateTime == null ? 0 : localDateTime!.hashCode) +
(ownerId.hashCode) + (ownerId.hashCode) +
(thumbhash == null ? 0 : thumbhash!.hashCode) + (thumbhash == null ? 0 : thumbhash!.hashCode) +
(type.hashCode); (type.hashCode) +
(visibility.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -100,7 +100,6 @@ class SyncAssetV1 {
} }
json[r'id'] = this.id; json[r'id'] = this.id;
json[r'isFavorite'] = this.isFavorite; json[r'isFavorite'] = this.isFavorite;
json[r'isVisible'] = this.isVisible;
if (this.localDateTime != null) { if (this.localDateTime != null) {
json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String(); json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String();
} else { } else {
@ -113,6 +112,7 @@ class SyncAssetV1 {
// json[r'thumbhash'] = null; // json[r'thumbhash'] = null;
} }
json[r'type'] = this.type; json[r'type'] = this.type;
json[r'visibility'] = this.visibility;
return json; return json;
} }
@ -131,11 +131,11 @@ class SyncAssetV1 {
fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''),
id: mapValueOfType<String>(json, r'id')!, id: mapValueOfType<String>(json, r'id')!,
isFavorite: mapValueOfType<bool>(json, r'isFavorite')!, isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
isVisible: mapValueOfType<bool>(json, r'isVisible')!,
localDateTime: mapDateTime(json, r'localDateTime', r''), localDateTime: mapDateTime(json, r'localDateTime', r''),
ownerId: mapValueOfType<String>(json, r'ownerId')!, ownerId: mapValueOfType<String>(json, r'ownerId')!,
thumbhash: mapValueOfType<String>(json, r'thumbhash'), thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: SyncAssetV1TypeEnum.fromJson(json[r'type'])!, type: SyncAssetV1TypeEnum.fromJson(json[r'type'])!,
visibility: SyncAssetV1VisibilityEnum.fromJson(json[r'visibility'])!,
); );
} }
return null; return null;
@ -189,11 +189,11 @@ class SyncAssetV1 {
'fileModifiedAt', 'fileModifiedAt',
'id', 'id',
'isFavorite', 'isFavorite',
'isVisible',
'localDateTime', 'localDateTime',
'ownerId', 'ownerId',
'thumbhash', 'thumbhash',
'type', '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({ UpdateAssetDto({
this.dateTimeOriginal, this.dateTimeOriginal,
this.description, this.description,
this.isArchived,
this.isFavorite, this.isFavorite,
this.latitude, this.latitude,
this.livePhotoVideoId, this.livePhotoVideoId,
this.longitude, this.longitude,
this.rating, this.rating,
this.visibility,
}); });
/// ///
@ -39,14 +39,6 @@ class UpdateAssetDto {
/// ///
String? description; 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 /// 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 /// does not include a default value (using the "default:" property), however, the generated
@ -83,31 +75,39 @@ class UpdateAssetDto {
/// ///
num? rating; 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 @override
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto && bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
other.dateTimeOriginal == dateTimeOriginal && other.dateTimeOriginal == dateTimeOriginal &&
other.description == description && other.description == description &&
other.isArchived == isArchived &&
other.isFavorite == isFavorite && other.isFavorite == isFavorite &&
other.latitude == latitude && other.latitude == latitude &&
other.livePhotoVideoId == livePhotoVideoId && other.livePhotoVideoId == livePhotoVideoId &&
other.longitude == longitude && other.longitude == longitude &&
other.rating == rating; other.rating == rating &&
other.visibility == visibility;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) + (dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) +
(description == null ? 0 : description!.hashCode) + (description == null ? 0 : description!.hashCode) +
(isArchived == null ? 0 : isArchived!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) +
(latitude == null ? 0 : latitude!.hashCode) + (latitude == null ? 0 : latitude!.hashCode) +
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
(longitude == null ? 0 : longitude!.hashCode) + (longitude == null ? 0 : longitude!.hashCode) +
(rating == null ? 0 : rating!.hashCode); (rating == null ? 0 : rating!.hashCode) +
(visibility == null ? 0 : visibility!.hashCode);
@override @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() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@ -121,11 +121,6 @@ class UpdateAssetDto {
} else { } else {
// json[r'description'] = null; // json[r'description'] = null;
} }
if (this.isArchived != null) {
json[r'isArchived'] = this.isArchived;
} else {
// json[r'isArchived'] = null;
}
if (this.isFavorite != null) { if (this.isFavorite != null) {
json[r'isFavorite'] = this.isFavorite; json[r'isFavorite'] = this.isFavorite;
} else { } else {
@ -151,6 +146,11 @@ class UpdateAssetDto {
} else { } else {
// json[r'rating'] = null; // json[r'rating'] = null;
} }
if (this.visibility != null) {
json[r'visibility'] = this.visibility;
} else {
// json[r'visibility'] = null;
}
return json; return json;
} }
@ -165,12 +165,12 @@ class UpdateAssetDto {
return UpdateAssetDto( return UpdateAssetDto(
dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'), dateTimeOriginal: mapValueOfType<String>(json, r'dateTimeOriginal'),
description: mapValueOfType<String>(json, r'description'), description: mapValueOfType<String>(json, r'description'),
isArchived: mapValueOfType<bool>(json, r'isArchived'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'), isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
latitude: num.parse('${json[r'latitude']}'), latitude: num.parse('${json[r'latitude']}'),
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'), livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
longitude: num.parse('${json[r'longitude']}'), longitude: num.parse('${json[r'longitude']}'),
rating: num.parse('${json[r'rating']}'), rating: num.parse('${json[r'rating']}'),
visibility: AssetVisibility.fromJson(json[r'visibility']),
); );
} }
return null; return null;

View File

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

View File

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

View File

@ -100,38 +100,29 @@ describe(AssetMediaController.name, () => {
expect(body).toEqual(factory.responses.badRequest()); 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()) const { status, body } = await request(ctx.getHttpServer())
.post('/assets') .post('/assets')
.attach('assetData', assetData, filename) .attach('assetData', assetData, filename)
.field({ ...makeUploadDto(), isVisible: 'not-a-boolean' }); .field({ ...makeUploadDto(), visibility: 'not-a-boolean' });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest()); expect(body).toEqual(factory.responses.badRequest());
}); });
it('should throw if `isArchived` is not a boolean', async () => { // TODO figure out how to deal with `sendFile`
const { status, body } = await request(ctx.getHttpServer()) describe.skip('GET /assets/:id/original', () => {
.post('/assets') it('should be an authenticated route', async () => {
.attach('assetData', assetData, filename) await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`);
.field({ ...makeUploadDto(), isArchived: 'not-a-boolean' }); expect(ctx.authenticate).toHaveBeenCalled();
expect(status).toBe(400); });
expect(body).toEqual(factory.responses.badRequest());
}); });
});
// TODO figure out how to deal with `sendFile` // TODO figure out how to deal with `sendFile`
describe.skip('GET /assets/:id/original', () => { describe.skip('GET /assets/:id/thumbnail', () => {
it('should be an authenticated route', async () => { it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/original`); await request(ctx.getHttpServer()).get(`/assets/${factory.uuid()}/thumbnail`);
expect(ctx.authenticate).toHaveBeenCalled(); 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'])); 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()) const { status, body } = await request(ctx.getHttpServer())
.post('/search/metadata') .post('/search/metadata')
.send({ isArchived: 'immich' }); .send({ visibility: 'immich' });
expect(status).toBe(400); 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 () => { 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'])); expect(body).toEqual(errorDto.badRequest(['isMotion must be a boolean value']));
}); });
it('should reject an isVisible as not a boolean', async () => { describe('POST /search/random', () => {
const { status, body } = await request(ctx.getHttpServer()) it('should be an authenticated route', async () => {
.post('/search/metadata') await request(ctx.getHttpServer()).post('/search/random');
.send({ isVisible: 'immich' }); expect(ctx.authenticate).toHaveBeenCalled();
expect(status).toBe(400); });
expect(body).toEqual(errorDto.badRequest(['isVisible must be a boolean value']));
});
});
describe('POST /search/random', () => { it('should reject if withStacked is not a boolean', async () => {
it('should be an authenticated route', async () => { const { status, body } = await request(ctx.getHttpServer())
await request(ctx.getHttpServer()).post('/search/random'); .post('/search/random')
expect(ctx.authenticate).toHaveBeenCalled(); .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 () => { describe('POST /search/smart', () => {
const { status, body } = await request(ctx.getHttpServer()) it('should be an authenticated route', async () => {
.post('/search/random') await request(ctx.getHttpServer()).post('/search/smart');
.send({ withStacked: 'immich' }); expect(ctx.authenticate).toHaveBeenCalled();
expect(status).toBe(400); });
expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value']));
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 () => { describe('GET /search/explore', () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/random').send({ withPeople: 'immich' }); it('should be an authenticated route', async () => {
expect(status).toBe(400); await request(ctx.getHttpServer()).get('/search/explore');
expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value'])); expect(ctx.authenticate).toHaveBeenCalled();
}); });
});
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 () => { describe('POST /search/person', () => {
const { status, body } = await request(ctx.getHttpServer()).post('/search/smart').send({}); it('should be an authenticated route', async () => {
expect(status).toBe(400); await request(ctx.getHttpServer()).get('/search/person');
expect(body).toEqual(errorDto.badRequest(['query should not be empty', 'query must be a string'])); expect(ctx.authenticate).toHaveBeenCalled();
}); });
});
describe('GET /search/explore', () => { it('should require a name', async () => {
it('should be an authenticated route', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({});
await request(ctx.getHttpServer()).get('/search/explore'); expect(status).toBe(400);
expect(ctx.authenticate).toHaveBeenCalled(); expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name 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();
}); });
it('should require a name', async () => { describe('GET /search/places', () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({}); it('should be an authenticated route', async () => {
expect(status).toBe(400); await request(ctx.getHttpServer()).get('/search/places');
expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); expect(ctx.authenticate).toHaveBeenCalled();
}); });
});
describe('GET /search/places', () => { it('should require a name', async () => {
it('should be an authenticated route', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({});
await request(ctx.getHttpServer()).get('/search/places'); expect(status).toBe(400);
expect(ctx.authenticate).toHaveBeenCalled(); expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string']));
});
}); });
it('should require a name', async () => { describe('GET /search/cities', () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({}); it('should be an authenticated route', async () => {
expect(status).toBe(400); await request(ctx.getHttpServer()).get('/search/cities');
expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); 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();
});
});
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 () => { describe('GET /search/suggestions', () => {
const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({}); it('should be an authenticated route', async () => {
expect(status).toBe(400); await request(ctx.getHttpServer()).get('/search/suggestions');
expect(body).toEqual( expect(ctx.authenticate).toHaveBeenCalled();
errorDto.badRequest([ });
'type should not be empty',
expect.stringContaining('type must be one of the following values:'), 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, AlbumUserRole,
AssetFileType, AssetFileType,
AssetType, AssetType,
AssetVisibility,
MemoryType, MemoryType,
Permission, Permission,
SharedLinkType, SharedLinkType,
@ -108,7 +109,7 @@ export type Asset = {
fileCreatedAt: Date; fileCreatedAt: Date;
fileModifiedAt: Date; fileModifiedAt: Date;
isExternal: boolean; isExternal: boolean;
isVisible: boolean; visibility: AssetVisibility;
libraryId: string | null; libraryId: string | null;
livePhotoVideoId: string | null; livePhotoVideoId: string | null;
localDateTime: Date; localDateTime: Date;
@ -285,7 +286,7 @@ export const columns = {
'assets.fileCreatedAt', 'assets.fileCreatedAt',
'assets.fileModifiedAt', 'assets.fileModifiedAt',
'assets.isExternal', 'assets.isExternal',
'assets.isVisible', 'assets.visibility',
'assets.libraryId', 'assets.libraryId',
'assets.livePhotoVideoId', 'assets.livePhotoVideoId',
'assets.localDateTime', 'assets.localDateTime',
@ -345,7 +346,7 @@ export const columns = {
'type', 'type',
'deletedAt', 'deletedAt',
'isFavorite', 'isFavorite',
'isVisible', 'visibility',
'updateId', 'updateId',
], ],
stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'], stack: ['stack.id', 'stack.primaryAssetId', 'ownerId'],

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -618,3 +618,13 @@ export enum DatabaseSslMode {
Require = 'require', Require = 'require',
VerifyFull = 'verify-full', 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 and "assets"."deletedAt" is null
where where
"partner"."sharedWithId" = $1 "partner"."sharedWithId" = $1
and "assets"."isArchived" = $2 and (
and "assets"."id" in ($3) "assets"."visibility" = 'timeline'
or "assets"."visibility" = 'hidden'
)
and "assets"."id" in ($2)
-- AccessRepository.asset.checkSharedLinkAccess -- AccessRepository.asset.checkSharedLinkAccess
select select

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,8 +7,7 @@ from
"assets" "assets"
where where
"ownerId" = $2::uuid "ownerId" = $2::uuid
and "isVisible" = $3 and "visibility" = $3
and "isArchived" = $4
and "deletedAt" is null and "deletedAt" is null
and "fileCreatedAt" is not null and "fileCreatedAt" is not null
and "fileModifiedAt" is not null and "fileModifiedAt" is not null
@ -23,13 +22,12 @@ from
left join "exif" on "assets"."id" = "exif"."assetId" left join "exif" on "assets"."id" = "exif"."assetId"
where where
"ownerId" = $1::uuid "ownerId" = $1::uuid
and "isVisible" = $2 and "visibility" = $2
and "isArchived" = $3
and "deletedAt" is null and "deletedAt" is null
and "fileCreatedAt" is not null and "fileCreatedAt" is not null
and "fileModifiedAt" is not null and "fileModifiedAt" is not null
and "localDateTime" is not null and "localDateTime" is not null
and "originalPath" like $4 and "originalPath" like $3
and "originalPath" not like $5 and "originalPath" not like $4
order by 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 { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
import { AlbumUserRole } from 'src/enum'; import { AlbumUserRole, AssetVisibility } from 'src/enum';
import { asUuid } from 'src/utils/database'; import { asUuid } from 'src/utils/database';
class ActivityAccess { class ActivityAccess {
@ -199,7 +199,13 @@ class AssetAccess {
) )
.select('assets.id') .select('assets.id')
.where('partner.sharedWithId', '=', userId) .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]) .where('assets.id', 'in', [...assetIds])
.execute() .execute()
.then((assets) => new Set(assets.map((asset) => asset.id))); .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 { Asset, columns } from 'src/database';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators'; 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 { StorageAsset } from 'src/types';
import { import {
anyUuid, anyUuid,
@ -34,7 +34,7 @@ export class AssetJobRepository {
'ownerId', 'ownerId',
'duplicateId', 'duplicateId',
'stackId', 'stackId',
'isVisible', 'visibility',
'smart_search.embedding', 'smart_search.embedding',
withFiles(eb, AssetFileType.PREVIEW), withFiles(eb, AssetFileType.PREVIEW),
]) ])
@ -70,7 +70,7 @@ export class AssetJobRepository {
.select(['assets.id', 'assets.thumbhash']) .select(['assets.id', 'assets.thumbhash'])
.select(withFiles) .select(withFiles)
.where('assets.deletedAt', 'is', null) .where('assets.deletedAt', 'is', null)
.where('assets.isVisible', '=', true) .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.$if(!force, (qb) => .$if(!force, (qb) =>
qb qb
// If there aren't any entries, metadata extraction hasn't run yet which is required for thumbnails // 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') .selectFrom('assets')
.select([ .select([
'assets.id', 'assets.id',
'assets.isVisible', 'assets.visibility',
'assets.originalFileName', 'assets.originalFileName',
'assets.originalPath', 'assets.originalPath',
'assets.ownerId', 'assets.ownerId',
@ -138,7 +138,7 @@ export class AssetJobRepository {
private assetsWithPreviews() { private assetsWithPreviews() {
return this.db return this.db
.selectFrom('assets') .selectFrom('assets')
.where('assets.isVisible', '=', true) .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.where('assets.deletedAt', 'is', null) .where('assets.deletedAt', 'is', null)
.innerJoin('asset_job_status as job_status', 'assetId', 'assets.id') .innerJoin('asset_job_status as job_status', 'assetId', 'assets.id')
.where('job_status.previewAt', 'is not', null); .where('job_status.previewAt', 'is not', null);
@ -169,7 +169,7 @@ export class AssetJobRepository {
getForClipEncoding(id: string) { getForClipEncoding(id: string) {
return this.db return this.db
.selectFrom('assets') .selectFrom('assets')
.select(['assets.id', 'assets.isVisible']) .select(['assets.id', 'assets.visibility'])
.select((eb) => withFiles(eb, AssetFileType.PREVIEW)) .select((eb) => withFiles(eb, AssetFileType.PREVIEW))
.where('assets.id', '=', id) .where('assets.id', '=', id)
.executeTakeFirst(); .executeTakeFirst();
@ -179,7 +179,7 @@ export class AssetJobRepository {
getForDetectFacesJob(id: string) { getForDetectFacesJob(id: string) {
return this.db return this.db
.selectFrom('assets') .selectFrom('assets')
.select(['assets.id', 'assets.isVisible']) .select(['assets.id', 'assets.visibility'])
.$call(withExifInner) .$call(withExifInner)
.select((eb) => withFaces(eb, true)) .select((eb) => withFaces(eb, true))
.select((eb) => withFiles(eb, AssetFileType.PREVIEW)) .select((eb) => withFiles(eb, AssetFileType.PREVIEW))
@ -209,7 +209,7 @@ export class AssetJobRepository {
.selectFrom('assets') .selectFrom('assets')
.select([ .select([
'assets.id', 'assets.id',
'assets.isVisible', 'assets.visibility',
'assets.libraryId', 'assets.libraryId',
'assets.ownerId', 'assets.ownerId',
'assets.livePhotoVideoId', 'assets.livePhotoVideoId',
@ -228,7 +228,7 @@ export class AssetJobRepository {
.select(['asset_stack.id', 'asset_stack.primaryAssetId']) .select(['asset_stack.id', 'asset_stack.primaryAssetId'])
.select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets')) .select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets'))
.where('stacked.deletedAt', 'is not', null) .where('stacked.deletedAt', 'is not', null)
.where('stacked.isArchived', '=', false) .where('stacked.visibility', '!=', AssetVisibility.ARCHIVE)
.whereRef('stacked.stackId', '=', 'asset_stack.id') .whereRef('stacked.stackId', '=', 'asset_stack.id')
.groupBy('asset_stack.id') .groupBy('asset_stack.id')
.as('stacked_assets'), .as('stacked_assets'),
@ -248,7 +248,7 @@ export class AssetJobRepository {
.$if(!force, (qb) => .$if(!force, (qb) =>
qb qb
.where((eb) => eb.or([eb('assets.encodedVideoPath', 'is', null), eb('assets.encodedVideoPath', '=', '')])) .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) .where('assets.deletedAt', 'is', null)
.stream(); .stream();
@ -275,7 +275,7 @@ export class AssetJobRepository {
.where((eb) => .where((eb) =>
eb.or([eb('asset_job_status.metadataExtractedAt', 'is', null), eb('asset_job_status.assetId', 'is', null)]), 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) .where('assets.deletedAt', 'is', null)
.stream(); .stream();
@ -331,7 +331,7 @@ export class AssetJobRepository {
.$if(!force, (qb) => .$if(!force, (qb) =>
qb.where((eb) => eb.or([eb('assets.sidecarPath', '=', ''), eb('assets.sidecarPath', 'is', null)])), qb.where((eb) => eb.or([eb('assets.sidecarPath', '=', ''), eb('assets.sidecarPath', 'is', null)])),
) )
.where('assets.isVisible', '=', true) .where('assets.visibility', '!=', AssetVisibility.HIDDEN)
.stream(); .stream();
} }

View File

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

View File

@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { Kysely } from 'kysely'; import { Kysely } from 'kysely';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { AssetVisibility } from 'src/enum';
import { anyUuid } from 'src/utils/database'; import { anyUuid } from 'src/utils/database';
const builder = (db: Kysely<DB>) => const builder = (db: Kysely<DB>) =>
@ -31,6 +32,9 @@ export class DownloadRepository {
} }
downloadUserId(userId: string) { 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 { DB, Libraries } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators'; import { DummyValue, GenerateSql } from 'src/decorators';
import { LibraryStatsResponseDto } from 'src/dtos/library.dto'; import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
import { AssetType } from 'src/enum'; import { AssetType, AssetVisibility } from 'src/enum';
export enum AssetSyncResult { export enum AssetSyncResult {
DO_NOTHING, DO_NOTHING,
@ -77,13 +77,17 @@ export class LibraryRepository {
.select((eb) => .select((eb) =>
eb.fn eb.fn
.countAll<number>() .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'), .as('photos'),
) )
.select((eb) => .select((eb) =>
eb.fn eb.fn
.countAll<number>() .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'), .as('videos'),
) )
.select((eb) => eb.fn.coalesce((eb) => eb.fn.sum('exif.fileSizeInByte'), eb.val(0)).as('usage')) .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 { citiesFile } from 'src/constants';
import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db'; import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators'; 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 { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository'; import { LoggingRepository } from 'src/repositories/logging.repository';
import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
@ -75,9 +75,11 @@ export class MapRepository {
} }
@GenerateSql({ params: [[DummyValue.UUID], [DummyValue.UUID]] }) @GenerateSql({ params: [[DummyValue.UUID], [DummyValue.UUID]] })
getMapMarkers(ownerIds: string[], albumIds: string[], options: MapMarkerSearchOptions = {}) { getMapMarkers(
const { isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore } = options; ownerIds: string[],
albumIds: string[],
{ isArchived, isFavorite, fileCreatedAfter, fileCreatedBefore }: MapMarkerSearchOptions = {},
) {
return this.db return this.db
.selectFrom('assets') .selectFrom('assets')
.innerJoin('exif', (builder) => .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']) .select(['id', 'exif.latitude as lat', 'exif.longitude as lon', 'exif.city', 'exif.state', 'exif.country'])
.$narrowType<{ lat: NotNull; lon: NotNull }>() .$narrowType<{ lat: NotNull; lon: NotNull }>()
.where('isVisible', '=', true) .$if(isArchived === true, (qb) =>
.$if(isArchived !== undefined, (q) => q.where('isArchived', '=', isArchived!)) 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(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!))
.$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!)) .$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!))
.$if(fileCreatedBefore !== undefined, (q) => q.where('fileCreatedAt', '<=', fileCreatedBefore!)) .$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 { InjectKysely } from 'nestjs-kysely';
import { AssetFaces, DB, FaceSearch, Person } from 'src/db'; import { AssetFaces, DB, FaceSearch, Person } from 'src/db';
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; 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 { removeUndefinedKeys } from 'src/utils/database';
import { paginationHelper, PaginationOptions } from 'src/utils/pagination'; import { paginationHelper, PaginationOptions } from 'src/utils/pagination';
@ -157,7 +157,7 @@ export class PersonRepository {
.innerJoin('assets', (join) => .innerJoin('assets', (join) =>
join join
.onRef('asset_faces.assetId', '=', 'assets.id') .onRef('asset_faces.assetId', '=', 'assets.id')
.on('assets.isArchived', '=', false) .on('assets.visibility', '!=', AssetVisibility.ARCHIVE)
.on('assets.deletedAt', 'is', null), .on('assets.deletedAt', 'is', null),
) )
.where('person.ownerId', '=', userId) .where('person.ownerId', '=', userId)
@ -248,7 +248,7 @@ export class PersonRepository {
jsonObjectFrom( jsonObjectFrom(
eb eb
.selectFrom('assets') .selectFrom('assets')
.select(['assets.ownerId', 'assets.isArchived', 'assets.fileCreatedAt']) .select(['assets.ownerId', 'assets.visibility', 'assets.fileCreatedAt'])
.whereRef('assets.id', '=', 'asset_faces.assetId'), .whereRef('assets.id', '=', 'asset_faces.assetId'),
).as('asset'), ).as('asset'),
) )
@ -346,7 +346,7 @@ export class PersonRepository {
join join
.onRef('assets.id', '=', 'asset_faces.assetId') .onRef('assets.id', '=', 'asset_faces.assetId')
.on('asset_faces.personId', '=', personId) .on('asset_faces.personId', '=', personId)
.on('assets.isArchived', '=', false) .on('assets.visibility', '!=', AssetVisibility.ARCHIVE)
.on('assets.deletedAt', 'is', null), .on('assets.deletedAt', 'is', null),
) )
.select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count')) .select((eb) => eb.fn.count(eb.fn('distinct', ['assets.id'])).as('count'))
@ -369,7 +369,7 @@ export class PersonRepository {
join join
.onRef('assets.id', '=', 'asset_faces.assetId') .onRef('assets.id', '=', 'asset_faces.assetId')
.on('assets.deletedAt', 'is', null) .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) => eb.fn.count(eb.fn('distinct', ['person.id'])).as('total'))
.select((eb) => .select((eb) =>

View File

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

View File

@ -6,7 +6,7 @@ import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database'; import { columns } from 'src/database';
import { DB, UserMetadata as DbUserMetadata } from 'src/db'; import { DB, UserMetadata as DbUserMetadata } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators'; 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 { UserTable } from 'src/schema/tables/user.table';
import { UserMetadata, UserMetadataItem } from 'src/types'; import { UserMetadata, UserMetadataItem } from 'src/types';
import { asUuid } from 'src/utils/database'; import { asUuid } from 'src/utils/database';
@ -205,13 +205,19 @@ export class UserRepository {
eb.fn eb.fn
.countAll<number>() .countAll<number>()
.filterWhere((eb) => .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'), .as('photos'),
eb.fn eb.fn
.countAll<number>() .countAll<number>()
.filterWhere((eb) => .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'), .as('videos'),
eb.fn eb.fn

View File

@ -2,6 +2,7 @@ import { Kysely } from 'kysely';
import { InjectKysely } from 'nestjs-kysely'; import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db'; import { DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators'; import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetVisibility } from 'src/enum';
import { asUuid, withExif } from 'src/utils/database'; import { asUuid, withExif } from 'src/utils/database';
export class ViewRepository { export class ViewRepository {
@ -14,8 +15,7 @@ export class ViewRepository {
.select((eb) => eb.fn<string>('substring', ['assets.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath')) .select((eb) => eb.fn<string>('substring', ['assets.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath'))
.distinct() .distinct()
.where('ownerId', '=', asUuid(userId)) .where('ownerId', '=', asUuid(userId))
.where('isVisible', '=', true) .where('visibility', '=', AssetVisibility.TIMELINE)
.where('isArchived', '=', false)
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.where('fileCreatedAt', 'is not', null) .where('fileCreatedAt', 'is not', null)
.where('fileModifiedAt', 'is not', null) .where('fileModifiedAt', 'is not', null)
@ -34,8 +34,7 @@ export class ViewRepository {
.selectAll('assets') .selectAll('assets')
.$call(withExif) .$call(withExif)
.where('ownerId', '=', asUuid(userId)) .where('ownerId', '=', asUuid(userId))
.where('isVisible', '=', true) .where('visibility', '=', AssetVisibility.TIMELINE)
.where('isArchived', '=', false)
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.where('fileCreatedAt', 'is not', null) .where('fileCreatedAt', 'is not', null)
.where('fileModifiedAt', '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 { asset_face_source_type, assets_status_enum } from 'src/schema/enums';
import { import {
assets_delete_audit, 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 { UserMetadataTable } from 'src/schema/tables/user-metadata.table';
import { UserTable } from 'src/schema/tables/user.table'; import { UserTable } from 'src/schema/tables/user.table';
import { VersionHistoryTable } from 'src/schema/tables/version-history.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']) @Extensions(['uuid-ossp', 'unaccent', 'cube', 'earthdistance', 'pg_trgm', 'plpgsql'])
@ConfigurationParameter({ name: 'search_path', value: () => '"$user", public, vectors', scope: 'database' }) @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 { 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_status_enum } from 'src/schema/enums';
import { assets_delete_audit } from 'src/schema/functions'; import { assets_delete_audit } from 'src/schema/functions';
import { LibraryTable } from 'src/schema/tables/library.table'; import { LibraryTable } from 'src/schema/tables/library.table';
@ -95,9 +96,6 @@ export class AssetTable {
@Column({ type: 'bytea', index: true }) @Column({ type: 'bytea', index: true })
checksum!: Buffer; // sha1 checksum checksum!: Buffer; // sha1 checksum
@Column({ type: 'boolean', default: true })
isVisible!: boolean;
@ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' }) @ForeignKeyColumn(() => AssetTable, { nullable: true, onUpdate: 'CASCADE', onDelete: 'SET NULL' })
livePhotoVideoId!: string | null; livePhotoVideoId!: string | null;
@ -107,9 +105,6 @@ export class AssetTable {
@CreateDateColumn() @CreateDateColumn()
createdAt!: Date; createdAt!: Date;
@Column({ type: 'boolean', default: false })
isArchived!: boolean;
@Column({ index: true }) @Column({ index: true })
originalFileName!: string; originalFileName!: string;
@ -145,4 +140,7 @@ export class AssetTable {
@UpdateIdColumn({ indexName: 'IDX_assets_update_id' }) @UpdateIdColumn({ indexName: 'IDX_assets_update_id' })
updateId?: string; 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 { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto';
import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto'; import { AssetMediaCreateDto, AssetMediaReplaceDto, AssetMediaSize, UploadFieldName } from 'src/dtos/asset-media.dto';
import { MapAsset } from 'src/dtos/asset-response.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 { AuthRequest } from 'src/middleware/auth.guard';
import { AssetMediaService } from 'src/services/asset-media.service'; import { AssetMediaService } from 'src/services/asset-media.service';
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; 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'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
isFavorite: false, isFavorite: false,
isArchived: false,
duration: '0:00:00.000000', duration: '0:00:00.000000',
}) as AssetMediaCreateDto; }) as AssetMediaCreateDto;
@ -164,7 +163,6 @@ const assetEntity = Object.freeze({
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
updatedAt: new Date('2022-06-19T23:41:36.910Z'), updatedAt: new Date('2022-06-19T23:41:36.910Z'),
isFavorite: false, isFavorite: false,
isArchived: false,
encodedVideoPath: '', encodedVideoPath: '',
duration: '0:00:00.000000', duration: '0:00:00.000000',
files: [] as AssetFile[], files: [] as AssetFile[],
@ -437,7 +435,10 @@ describe(AssetMediaService.name, () => {
}); });
it('should hide the linked motion asset', async () => { 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); mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
await expect( await expect(
@ -452,7 +453,10 @@ describe(AssetMediaService.name, () => {
}); });
expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset'); 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 () => { it('should handle a sidecar file', async () => {

View File

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

View File

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

View File

@ -92,8 +92,12 @@ export class AssetService extends BaseService {
const asset = await this.assetRepository.update({ id, ...rest }); const asset = await this.assetRepository.update({ id, ...rest });
if (previousMotion) { if (previousMotion && asset) {
await onAfterUnlink(repos, { userId: auth.user.id, livePhotoVideoId: previousMotion.id }); await onAfterUnlink(repos, {
userId: auth.user.id,
livePhotoVideoId: previousMotion.id,
visibility: asset.visibility,
});
} }
if (!asset) { if (!asset) {
@ -115,7 +119,7 @@ export class AssetService extends BaseService {
} }
if ( if (
options.isArchived !== undefined || options.visibility !== undefined ||
options.isFavorite !== undefined || options.isFavorite !== undefined ||
options.duplicateId !== undefined || options.duplicateId !== undefined ||
options.rating !== 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 { DuplicateService } from 'src/services/duplicate.service';
import { SearchService } from 'src/services/search.service'; import { SearchService } from 'src/services/search.service';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
@ -22,11 +22,11 @@ const hasEmbedding = {
updateId: 'update-1', updateId: 'update-1',
}, },
], ],
isVisible: true,
stackId: null, stackId: null,
type: AssetType.IMAGE, type: AssetType.IMAGE,
duplicateId: null, duplicateId: null,
embedding: '[1, 2, 3, 4]', embedding: '[1, 2, 3, 4]',
visibility: AssetVisibility.TIMELINE,
}; };
const hasDupe = { const hasDupe = {
@ -207,7 +207,10 @@ describe(SearchService.name, () => {
it('should skip if asset is not visible', async () => { it('should skip if asset is not visible', async () => {
const id = assetStub.livePhotoMotionAsset.id; 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 }); 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 { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto } from 'src/dtos/duplicate.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 { AssetDuplicateResult } from 'src/repositories/search.repository';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { JobItem, JobOf } from 'src/types'; import { JobItem, JobOf } from 'src/types';
@ -65,7 +65,7 @@ export class DuplicateService extends BaseService {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
if (!asset.isVisible) { if (asset.visibility == AssetVisibility.HIDDEN) {
this.logger.debug(`Asset ${id} is not visible, skipping`); this.logger.debug(`Asset ${id} is not visible, skipping`);
return JobStatus.SKIPPED; 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 { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
import { import {
AssetType, AssetType,
AssetVisibility,
BootstrapEventPriority, BootstrapEventPriority,
ImmichWorker, ImmichWorker,
JobCommand, JobCommand,
@ -301,7 +302,7 @@ export class JobService extends BaseService {
} }
await this.jobRepository.queueAll(jobs); 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)); this.eventRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset));
} }

View File

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

View File

@ -4,7 +4,7 @@ import { Stats } from 'node:fs';
import { constants } from 'node:fs/promises'; import { constants } from 'node:fs/promises';
import { defaults } from 'src/config'; import { defaults } from 'src/config';
import { MapAsset } from 'src/dtos/asset-response.dto'; 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 { ImmichTags } from 'src/repositories/metadata.repository';
import { MetadataService } from 'src/services/metadata.service'; import { MetadataService } from 'src/services/metadata.service';
import { assetStub } from 'test/fixtures/asset.stub'; 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 () => { 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); mocks.media.probe.mockResolvedValue(probeStub.matroskaContainer);
await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id });
@ -513,7 +516,7 @@ describe(MetadataService.name, () => {
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled();
expect(mocks.asset.update).not.toHaveBeenCalledWith( 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, fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
id: fileStub.livePhotoMotion.uuid, id: fileStub.livePhotoMotion.uuid,
isVisible: false, visibility: AssetVisibility.HIDDEN,
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
originalFileName: 'asset_1.mp4', originalFileName: 'asset_1.mp4',
@ -638,7 +641,7 @@ describe(MetadataService.name, () => {
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
id: fileStub.livePhotoMotion.uuid, id: fileStub.livePhotoMotion.uuid,
isVisible: false, visibility: AssetVisibility.HIDDEN,
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
originalFileName: 'asset_1.mp4', originalFileName: 'asset_1.mp4',
@ -696,7 +699,7 @@ describe(MetadataService.name, () => {
fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, fileCreatedAt: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt, fileModifiedAt: assetStub.livePhotoWithOriginalFileName.fileModifiedAt,
id: fileStub.livePhotoMotion.uuid, id: fileStub.livePhotoMotion.uuid,
isVisible: false, visibility: AssetVisibility.HIDDEN,
libraryId: assetStub.livePhotoWithOriginalFileName.libraryId, libraryId: assetStub.livePhotoWithOriginalFileName.libraryId,
localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt, localDateTime: assetStub.livePhotoWithOriginalFileName.fileCreatedAt,
originalFileName: 'asset_1.mp4', originalFileName: 'asset_1.mp4',
@ -773,14 +776,17 @@ describe(MetadataService.name, () => {
MicroVideoOffset: 1, MicroVideoOffset: 1,
}); });
mocks.crypto.hashSha1.mockReturnValue(randomBytes(512)); 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); const video = randomBytes(512);
mocks.storage.readFile.mockResolvedValue(video); mocks.storage.readFile.mockResolvedValue(video);
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
expect(mocks.asset.update).toHaveBeenCalledWith({ expect(mocks.asset.update).toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id, id: assetStub.livePhotoMotionAsset.id,
isVisible: false, visibility: AssetVisibility.HIDDEN,
}); });
expect(mocks.asset.update).toHaveBeenCalledWith({ expect(mocks.asset.update).toHaveBeenCalledWith({
id: assetStub.livePhotoStillAsset.id, id: assetStub.livePhotoStillAsset.id,
@ -1301,7 +1307,9 @@ describe(MetadataService.name, () => {
expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id); expect(mocks.assetJob.getForMetadataExtraction).toHaveBeenCalledWith(assetStub.image.id);
expect(mocks.asset.findLivePhotoMatch).not.toHaveBeenCalled(); 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(); expect(mocks.album.removeAsset).not.toHaveBeenCalled();
}); });
@ -1320,7 +1328,9 @@ describe(MetadataService.name, () => {
libraryId: null, libraryId: null,
type: AssetType.IMAGE, 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(); expect(mocks.album.removeAsset).not.toHaveBeenCalled();
}); });
@ -1342,7 +1352,10 @@ describe(MetadataService.name, () => {
id: assetStub.livePhotoStillAsset.id, id: assetStub.livePhotoStillAsset.id,
livePhotoVideoId: assetStub.livePhotoMotionAsset.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); 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 { OnEvent, OnJob } from 'src/decorators';
import { import {
AssetType, AssetType,
AssetVisibility,
DatabaseLock, DatabaseLock,
ExifOrientation, ExifOrientation,
ImmichWorker, ImmichWorker,
@ -156,7 +157,7 @@ export class MetadataService extends BaseService {
const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset]; const [photoAsset, motionAsset] = asset.type === AssetType.IMAGE ? [asset, match] : [match, asset];
await Promise.all([ await Promise.all([
this.assetRepository.update({ id: photoAsset.id, livePhotoVideoId: motionAsset.id }), 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), 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 // Hide the motion photo video asset if it's not already hidden to prepare for linking
if (motionAsset.isVisible) { if (motionAsset.visibility === AssetVisibility.TIMELINE) {
await this.assetRepository.update({ id: motionAsset.id, isVisible: false }); await this.assetRepository.update({
id: motionAsset.id,
visibility: AssetVisibility.HIDDEN,
});
this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`); this.logger.log(`Hid unlinked motion photo video asset (${motionAsset.id})`);
} }
} else { } else {
@ -544,7 +548,7 @@ export class MetadataService extends BaseService {
ownerId: asset.ownerId, ownerId: asset.ownerId,
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId), originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
originalFileName: `${path.parse(asset.originalFileName).name}.mp4`, originalFileName: `${path.parse(asset.originalFileName).name}.mp4`,
isVisible: false, visibility: AssetVisibility.HIDDEN,
deviceAssetId: 'NONE', deviceAssetId: 'NONE',
deviceId: 'NONE', deviceId: 'NONE',
}); });
@ -863,7 +867,7 @@ export class MetadataService extends BaseService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
if (!isSync && (!asset.isVisible || asset.sidecarPath) && !asset.isExternal) { if (!isSync && (asset.visibility === AssetVisibility.HIDDEN || asset.sidecarPath) && !asset.isExternal) {
return JobStatus.FAILED; return JobStatus.FAILED;
} }

View File

@ -26,6 +26,7 @@ import {
} from 'src/dtos/person.dto'; } from 'src/dtos/person.dto';
import { import {
AssetType, AssetType,
AssetVisibility,
CacheControl, CacheControl,
ImageFormat, ImageFormat,
JobName, JobName,
@ -296,7 +297,7 @@ export class PersonService extends BaseService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
if (!asset.isVisible) { if (asset.visibility === AssetVisibility.HIDDEN) {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }
@ -484,7 +485,9 @@ export class PersonService extends BaseService {
this.logger.debug(`Face ${id} has ${matches.length} matches`); 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) { if (!isCore && !deferred) {
this.logger.debug(`Deferring non-core face ${id} for later processing`); this.logger.debug(`Deferring non-core face ${id} for later processing`);
await this.jobRepository.queue({ name: JobName.FACIAL_RECOGNITION, data: { id, deferred: true } }); 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 { SystemConfig } from 'src/config';
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
import { OnEvent, OnJob } from 'src/decorators'; 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 { ArgOf } from 'src/repositories/event.repository';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { JobItem, JobOf } from 'src/types'; import { JobItem, JobOf } from 'src/types';
@ -104,7 +104,7 @@ export class SmartInfoService extends BaseService {
return JobStatus.FAILED; return JobStatus.FAILED;
} }
if (!asset.isVisible) { if (asset.visibility === AssetVisibility.HIDDEN) {
return JobStatus.SKIPPED; return JobStatus.SKIPPED;
} }

View File

@ -14,7 +14,7 @@ import {
SyncAckSetDto, SyncAckSetDto,
SyncStreamDto, SyncStreamDto,
} from 'src/dtos/sync.dto'; } 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 { BaseService } from 'src/services/base.service';
import { SyncAck } from 'src/types'; import { SyncAck } from 'src/types';
import { getMyPartnerIds } from 'src/utils/asset.util'; import { getMyPartnerIds } from 'src/utils/asset.util';
@ -262,7 +262,10 @@ export class SyncService extends BaseService {
needsFullSync: false, needsFullSync: false,
upserted: upserted upserted: upserted
// do not return archived assets for partner users // 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) => .map((a) =>
mapAsset(a, { mapAsset(a, {
auth, auth,

View File

@ -1,4 +1,5 @@
import { BadRequestException } from '@nestjs/common'; import { BadRequestException } from '@nestjs/common';
import { AssetVisibility } from 'src/enum';
import { TimeBucketSize } from 'src/repositories/asset.repository'; import { TimeBucketSize } from 'src/repositories/asset.repository';
import { TimelineService } from 'src/services/timeline.service'; import { TimelineService } from 'src/services/timeline.service';
import { assetStub } from 'test/fixtures/asset.stub'; import { assetStub } from 'test/fixtures/asset.stub';
@ -54,7 +55,7 @@ describe(TimelineService.name, () => {
sut.getTimeBucket(authStub.admin, { sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: true, visibility: AssetVisibility.ARCHIVE,
userId: authStub.admin.user.id, userId: authStub.admin.user.id,
}), }),
).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })]));
@ -63,7 +64,7 @@ describe(TimelineService.name, () => {
expect.objectContaining({ expect.objectContaining({
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: true, visibility: AssetVisibility.ARCHIVE,
userIds: [authStub.admin.user.id], userIds: [authStub.admin.user.id],
}), }),
); );
@ -77,7 +78,7 @@ describe(TimelineService.name, () => {
sut.getTimeBucket(authStub.admin, { sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: false, visibility: AssetVisibility.TIMELINE,
userId: authStub.admin.user.id, userId: authStub.admin.user.id,
withPartners: true, withPartners: true,
}), }),
@ -85,7 +86,7 @@ describe(TimelineService.name, () => {
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', { expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: false, visibility: AssetVisibility.TIMELINE,
withPartners: true, withPartners: true,
userIds: [authStub.admin.user.id], userIds: [authStub.admin.user.id],
}); });
@ -120,7 +121,7 @@ describe(TimelineService.name, () => {
const buckets = await sut.getTimeBucket(auth, { const buckets = await sut.getTimeBucket(auth, {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: true, visibility: AssetVisibility.ARCHIVE,
albumId: 'album-id', albumId: 'album-id',
}); });
@ -129,7 +130,7 @@ describe(TimelineService.name, () => {
expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', { expect(mocks.asset.getTimeBucket).toHaveBeenCalledWith('bucket', {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: true, visibility: AssetVisibility.ARCHIVE,
albumId: 'album-id', 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( await expect(
sut.getTimeBucket(authStub.admin, { sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: true, visibility: AssetVisibility.ARCHIVE,
withPartners: true, withPartners: true,
userId: authStub.admin.user.id, userId: authStub.admin.user.id,
}), }),
@ -169,7 +170,7 @@ describe(TimelineService.name, () => {
sut.getTimeBucket(authStub.admin, { sut.getTimeBucket(authStub.admin, {
size: TimeBucketSize.DAY, size: TimeBucketSize.DAY,
timeBucket: 'bucket', timeBucket: 'bucket',
isArchived: undefined, visibility: undefined,
withPartners: true, withPartners: true,
userId: authStub.admin.user.id, 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 { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto'; import { AuthDto } from 'src/dtos/auth.dto';
import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.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 { TimeBucketOptions } from 'src/repositories/asset.repository';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
import { getMyPartnerIds } from 'src/utils/asset.util'; import { getMyPartnerIds } from 'src/utils/asset.util';
@ -55,7 +55,7 @@ export class TimelineService extends BaseService {
if (dto.userId) { if (dto.userId) {
await this.requireAccess({ auth, permission: Permission.TIMELINE_READ, ids: [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] }); await this.requireAccess({ auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] });
} }
} }
@ -65,7 +65,7 @@ export class TimelineService extends BaseService {
} }
if (dto.withPartners) { 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 requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
const requestedTrash = dto.isTrashed === true; 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 { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { UploadFieldName } from 'src/dtos/asset-media.dto';
import { AuthDto } from 'src/dtos/auth.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 { AuthRequest } from 'src/middleware/auth.guard';
import { AccessRepository } from 'src/repositories/access.repository'; import { AccessRepository } from 'src/repositories/access.repository';
import { AssetRepository } from 'src/repositories/asset.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'); throw new BadRequestException('Live photo video does not belong to the user');
} }
if (motionAsset?.isVisible) { if (motionAsset && motionAsset.visibility === AssetVisibility.TIMELINE) {
await assetRepository.update({ id: livePhotoVideoId, isVisible: false }); await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.HIDDEN });
await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId }); await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId });
} }
}; };
@ -174,9 +174,9 @@ export const onBeforeUnlink = async (
export const onAfterUnlink = async ( export const onAfterUnlink = async (
{ asset: assetRepository, event: eventRepository }: AssetHookRepositories, { 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 }); 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 postgres, { Notice } from 'postgres';
import { columns, Exif, Person } from 'src/database'; import { columns, Exif, Person } from 'src/database';
import { DB } from 'src/db'; 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 { TimeBucketSize } from 'src/repositories/asset.repository';
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository'; import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
import { DatabaseConnectionParams, VectorExtension } from 'src/types'; 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'; export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
// TODO come up with a better query that only selects the fields we need // 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>) { export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
return qb return qb
.leftJoin('exif', 'assets.id', 'exif.assetId') .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 */ /** 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) { export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
options.isArchived ??= options.withArchived ? undefined : false;
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline); options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
const visibility = options.visibility == null ? AssetVisibility.TIMELINE : options.visibility;
return kysely return kysely
.withPlugin(joinDeduplicationPlugin) .withPlugin(joinDeduplicationPlugin)
.selectFrom('assets') .selectFrom('assets')
.selectAll('assets') .selectAll('assets')
.where('assets.visibility', '=', visibility)
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) .$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.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!))
.$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!)) .$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.type, (qb) => qb.where('assets.type', '=', options.type!))
.$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!)) .$if(options.isFavorite !== undefined, (qb) => qb.where('assets.isFavorite', '=', options.isFavorite!))
.$if(options.isOffline !== undefined, (qb) => qb.where('assets.isOffline', '=', options.isOffline!)) .$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) => .$if(options.isEncoded !== undefined, (qb) =>
qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null), qb.where('assets.encodedVideoPath', options.isEncoded ? 'is not' : 'is', null),
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
<script lang="ts" module> <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 { 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 = { export type SearchFilter = {
query: string; query: string;
@ -19,24 +19,24 @@
</script> </script>
<script lang="ts"> <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 { Button } from '@immich/ui';
import { AssetTypeEnum, type SmartSearchDto, type MetadataSearchDto } from '@immich/sdk'; import { mdiTune } from '@mdi/js';
import SearchPeopleSection from './search-people-section.svelte'; import { t } from 'svelte-i18n';
import SearchTagsSection from './search-tags-section.svelte'; import { SvelteSet } from 'svelte/reactivity';
import SearchLocationSection from './search-location-section.svelte';
import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte'; import SearchCameraSection, { type SearchCameraFilter } from './search-camera-section.svelte';
import SearchDateSection from './search-date-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 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 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 { interface Props {
searchQuery: MetadataSearchDto | SmartSearchDto; searchQuery: MetadataSearchDto | SmartSearchDto;
@ -83,7 +83,7 @@
takenBefore: searchQuery.takenBefore ? toStartOfDayDate(searchQuery.takenBefore) : undefined, takenBefore: searchQuery.takenBefore ? toStartOfDayDate(searchQuery.takenBefore) : undefined,
}, },
display: { display: {
isArchive: searchQuery.isArchived, isArchive: searchQuery.visibility === AssetVisibility.Archive,
isFavorite: searchQuery.isFavorite, isFavorite: searchQuery.isFavorite,
isNotInAlbum: 'isNotInAlbum' in searchQuery ? searchQuery.isNotInAlbum : undefined, isNotInAlbum: 'isNotInAlbum' in searchQuery ? searchQuery.isNotInAlbum : undefined,
}, },
@ -132,7 +132,7 @@
model: filter.camera.model, model: filter.camera.model,
takenAfter: parseOptionalDate(filter.date.takenAfter)?.startOf('day').toISO() || undefined, takenAfter: parseOptionalDate(filter.date.takenAfter)?.startOf('day').toISO() || undefined,
takenBefore: parseOptionalDate(filter.date.takenBefore)?.endOf('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, isFavorite: filter.display.isFavorite || undefined,
isNotInAlbum: filter.display.isNotInAlbum || undefined, isNotInAlbum: filter.display.isNotInAlbum || undefined,
personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined, personIds: filter.personIds.size > 0 ? [...filter.personIds] : undefined,

View File

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

View File

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

View File

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

View File

@ -372,7 +372,10 @@
if (viewMode === AlbumPageViewMode.VIEW) { if (viewMode === AlbumPageViewMode.VIEW) {
void assetStore.updateOptions({ albumId, order: albumOrder }); void assetStore.updateOptions({ albumId, order: albumOrder });
} else if (viewMode === AlbumPageViewMode.SELECT_ASSETS) { } 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(); activityManager.reset();
assetStore.destroy(); 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); 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 FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.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 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 EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants'; 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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetStore } from '$lib/stores/assets-store.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 { interface Props {
data: PageData; data: PageData;
@ -26,7 +27,7 @@
let { data }: Props = $props(); let { data }: Props = $props();
const assetStore = new AssetStore(); const assetStore = new AssetStore();
void assetStore.updateOptions({ isArchived: true }); void assetStore.updateOptions({ visibility: AssetVisibility.Archive });
onDestroy(() => assetStore.destroy()); onDestroy(() => assetStore.destroy());
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();

View File

@ -9,19 +9,19 @@
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-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 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 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 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 EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants'; 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 { 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 { interface Props {
data: PageData; data: PageData;

View File

@ -4,16 +4,17 @@
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import AssetGrid from '$lib/components/photos-page/asset-grid.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 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 ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import { AppRoute } from '$lib/constants'; 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 { 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 { interface Props {
data: PageData; data: PageData;
@ -22,7 +23,14 @@
let { data }: Props = $props(); let { data }: Props = $props();
const assetStore = new AssetStore(); 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()); onDestroy(() => assetStore.destroy());
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();

View File

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

View File

@ -32,14 +32,14 @@
type OnUnlink, type OnUnlink,
} from '$lib/utils/actions'; } from '$lib/utils/actions';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; 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 { mdiDotsVertical, mdiPlus } from '@mdi/js';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
let { isViewing: showAssetViewer } = assetViewingStore; let { isViewing: showAssetViewer } = assetViewingStore;
const assetStore = new AssetStore(); 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()); onDestroy(() => assetStore.destroy());
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();

View File

@ -1,25 +1,38 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate, goto } from '$app/navigation'; import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/state'; 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 CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.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 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 ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
import ChangeLocation from '$lib/components/photos-page/actions/change-location-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 CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-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 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 ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.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 SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import { AppRoute, QueryParameter } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; 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 { import {
type AlbumResponseDto, type AlbumResponseDto,
type AssetResponseDto, type AssetResponseDto,
@ -31,21 +44,8 @@
type SmartSearchDto, type SmartSearchDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; 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 { tick } from 'svelte';
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte'; import { t } from 'svelte-i18n';
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';
const MAX_ASSET_COUNT = 5000; const MAX_ASSET_COUNT = 5000;
let { isViewing: showAssetViewer } = assetViewingStore; let { isViewing: showAssetViewer } = assetViewingStore;
@ -186,7 +186,7 @@
const keyMap: Partial<Record<keyof SearchTerms, string>> = { const keyMap: Partial<Record<keyof SearchTerms, string>> = {
takenAfter: $t('start_date'), takenAfter: $t('start_date'),
takenBefore: $t('end_date'), takenBefore: $t('end_date'),
isArchived: $t('in_archive'), visibility: $t('in_archive'),
isFavorite: $t('favorite'), isFavorite: $t('favorite'),
isNotInAlbum: $t('not_in_any_album'), isNotInAlbum: $t('not_in_any_album'),
type: $t('media_type'), type: $t('media_type'),
@ -313,7 +313,7 @@
<div class="flex place-content-center place-items-center text-xs"> <div class="flex place-content-center place-items-center text-xs">
<div <div
class="bg-immich-primary py-2 px-4 text-white dark:text-black dark:bg-immich-dark-primary 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)} {getHumanReadableSearchKey(key as keyof SearchTerms)}
</div> </div>