mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
feat(web): manual stacking asset (#4650)
Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
parent
72dcde9e0f
commit
8b5b6d0821
46
cli/src/api/open-api/api.ts
generated
46
cli/src/api/open-api/api.ts
generated
@ -6323,11 +6323,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isTrashed]
|
* @param {boolean} [isTrashed]
|
||||||
|
* @param {boolean} [withStacked]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'size' is not null or undefined
|
// verify required parameter 'size' is not null or undefined
|
||||||
assertParamExists('getByTimeBucket', 'size', size)
|
assertParamExists('getByTimeBucket', 'size', size)
|
||||||
// verify required parameter 'timeBucket' is not null or undefined
|
// verify required parameter 'timeBucket' is not null or undefined
|
||||||
@ -6381,6 +6382,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
localVarQueryParameter['isTrashed'] = isTrashed;
|
localVarQueryParameter['isTrashed'] = isTrashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (withStacked !== undefined) {
|
||||||
|
localVarQueryParameter['withStacked'] = withStacked;
|
||||||
|
}
|
||||||
|
|
||||||
if (timeBucket !== undefined) {
|
if (timeBucket !== undefined) {
|
||||||
localVarQueryParameter['timeBucket'] = timeBucket;
|
localVarQueryParameter['timeBucket'] = timeBucket;
|
||||||
}
|
}
|
||||||
@ -6691,11 +6696,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isTrashed]
|
* @param {boolean} [isTrashed]
|
||||||
|
* @param {boolean} [withStacked]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'size' is not null or undefined
|
// verify required parameter 'size' is not null or undefined
|
||||||
assertParamExists('getTimeBuckets', 'size', size)
|
assertParamExists('getTimeBuckets', 'size', size)
|
||||||
const localVarPath = `/asset/time-buckets`;
|
const localVarPath = `/asset/time-buckets`;
|
||||||
@ -6747,6 +6753,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
localVarQueryParameter['isTrashed'] = isTrashed;
|
localVarQueryParameter['isTrashed'] = isTrashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (withStacked !== undefined) {
|
||||||
|
localVarQueryParameter['withStacked'] = withStacked;
|
||||||
|
}
|
||||||
|
|
||||||
if (key !== undefined) {
|
if (key !== undefined) {
|
||||||
localVarQueryParameter['key'] = key;
|
localVarQueryParameter['key'] = key;
|
||||||
}
|
}
|
||||||
@ -7485,12 +7495,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isTrashed]
|
* @param {boolean} [isTrashed]
|
||||||
|
* @param {boolean} [withStacked]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, key, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -7565,12 +7576,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isTrashed]
|
* @param {boolean} [isTrashed]
|
||||||
|
* @param {boolean} [withStacked]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
|
async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -7815,7 +7827,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
|
getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
|
||||||
return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(axios, basePath));
|
return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -7876,7 +7888,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> {
|
getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> {
|
||||||
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(axios, basePath));
|
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all asset of a device that are in the database, ID only.
|
* Get all asset of a device that are in the database, ID only.
|
||||||
@ -8251,6 +8263,13 @@ export interface AssetApiGetByTimeBucketRequest {
|
|||||||
*/
|
*/
|
||||||
readonly isTrashed?: boolean
|
readonly isTrashed?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AssetApiGetByTimeBucket
|
||||||
|
*/
|
||||||
|
readonly withStacked?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -8405,6 +8424,13 @@ export interface AssetApiGetTimeBucketsRequest {
|
|||||||
*/
|
*/
|
||||||
readonly isTrashed?: boolean
|
readonly isTrashed?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AssetApiGetTimeBuckets
|
||||||
|
*/
|
||||||
|
readonly withStacked?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -8820,7 +8846,7 @@ export class AssetApi extends BaseAPI {
|
|||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) {
|
public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -8895,7 +8921,7 @@ export class AssetApi extends BaseAPI {
|
|||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) {
|
public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
BIN
mobile/openapi/doc/AssetApi.md
generated
BIN
mobile/openapi/doc/AssetApi.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/asset_api.dart
generated
BIN
mobile/openapi/lib/api/asset_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/asset_api_test.dart
generated
BIN
mobile/openapi/test/asset_api_test.dart
generated
Binary file not shown.
@ -1841,6 +1841,14 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "withStacked",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "timeBucket",
|
"name": "timeBucket",
|
||||||
"required": true,
|
"required": true,
|
||||||
@ -1961,6 +1969,14 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "withStacked",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "key",
|
"name": "key",
|
||||||
"required": false,
|
"required": false,
|
||||||
|
@ -201,7 +201,7 @@ export class AssetService {
|
|||||||
await this.timeBucketChecks(authUser, dto);
|
await this.timeBucketChecks(authUser, dto);
|
||||||
const assets = await this.assetRepository.getByTimeBucket(dto.timeBucket, dto);
|
const assets = await this.assetRepository.getByTimeBucket(dto.timeBucket, dto);
|
||||||
if (authUser.isShowMetadata) {
|
if (authUser.isShowMetadata) {
|
||||||
return assets.map((asset) => mapAsset(asset));
|
return assets.map((asset) => mapAsset(asset, { withStack: true }));
|
||||||
} else {
|
} else {
|
||||||
return assets.map((asset) => mapAsset(asset, { stripMetadata: true }));
|
return assets.map((asset) => mapAsset(asset, { stripMetadata: true }));
|
||||||
}
|
}
|
||||||
|
@ -33,6 +33,11 @@ export class TimeBucketDto {
|
|||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@Transform(toBoolean)
|
@Transform(toBoolean)
|
||||||
isTrashed?: boolean;
|
isTrashed?: boolean;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsBoolean()
|
||||||
|
@Transform(toBoolean)
|
||||||
|
withStacked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TimeBucketAssetDto extends TimeBucketDto {
|
export class TimeBucketAssetDto extends TimeBucketDto {
|
||||||
|
@ -65,6 +65,7 @@ export interface TimeBucketOptions {
|
|||||||
albumId?: string;
|
albumId?: string;
|
||||||
personId?: string;
|
personId?: string;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
|
withStacked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimeBucketItem {
|
export interface TimeBucketItem {
|
||||||
|
@ -30,7 +30,9 @@ const truncateMap: Record<TimeBucketSize, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const dateTrunc = (options: TimeBucketOptions) =>
|
const dateTrunc = (options: TimeBucketOptions) =>
|
||||||
`(date_trunc('${truncateMap[options.size]}', ("localDateTime" at time zone 'UTC')) at time zone 'UTC')::timestamptz`;
|
`(date_trunc('${
|
||||||
|
truncateMap[options.size]
|
||||||
|
}', (asset."localDateTime" at time zone 'UTC')) at time zone 'UTC')::timestamptz`;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AssetRepository implements IAssetRepository {
|
export class AssetRepository implements IAssetRepository {
|
||||||
@ -505,13 +507,14 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getBuilder(options: TimeBucketOptions) {
|
private getBuilder(options: TimeBucketOptions) {
|
||||||
const { isArchived, isFavorite, isTrashed, albumId, personId, userId } = options;
|
const { isArchived, isFavorite, isTrashed, albumId, personId, userId, withStacked } = options;
|
||||||
|
|
||||||
let builder = this.repository
|
let builder = this.repository
|
||||||
.createQueryBuilder('asset')
|
.createQueryBuilder('asset')
|
||||||
.where('asset.isVisible = true')
|
.where('asset.isVisible = true')
|
||||||
.andWhere('asset.fileCreatedAt < NOW()')
|
.andWhere('asset.fileCreatedAt < NOW()')
|
||||||
.leftJoinAndSelect('asset.exifInfo', 'exifInfo');
|
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
|
||||||
|
.leftJoinAndSelect('asset.stack', 'stack');
|
||||||
|
|
||||||
if (albumId) {
|
if (albumId) {
|
||||||
builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId });
|
builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId });
|
||||||
@ -540,11 +543,9 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
.andWhere('person.id = :personId', { personId });
|
.andWhere('person.id = :personId', { personId });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide stack children only in main timeline
|
if (withStacked) {
|
||||||
// Uncomment after adding support for stacked assets in web client
|
builder = builder.andWhere('asset.stackParentId IS NULL');
|
||||||
// if (!isArchived && !isFavorite && !personId && !albumId && !isTrashed) {
|
}
|
||||||
// builder = builder.andWhere('asset.stackParent IS NULL');
|
|
||||||
// }
|
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
46
web/src/api/open-api/api.ts
generated
46
web/src/api/open-api/api.ts
generated
@ -6323,11 +6323,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isTrashed]
|
* @param {boolean} [isTrashed]
|
||||||
|
* @param {boolean} [withStacked]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'size' is not null or undefined
|
// verify required parameter 'size' is not null or undefined
|
||||||
assertParamExists('getByTimeBucket', 'size', size)
|
assertParamExists('getByTimeBucket', 'size', size)
|
||||||
// verify required parameter 'timeBucket' is not null or undefined
|
// verify required parameter 'timeBucket' is not null or undefined
|
||||||
@ -6381,6 +6382,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
localVarQueryParameter['isTrashed'] = isTrashed;
|
localVarQueryParameter['isTrashed'] = isTrashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (withStacked !== undefined) {
|
||||||
|
localVarQueryParameter['withStacked'] = withStacked;
|
||||||
|
}
|
||||||
|
|
||||||
if (timeBucket !== undefined) {
|
if (timeBucket !== undefined) {
|
||||||
localVarQueryParameter['timeBucket'] = timeBucket;
|
localVarQueryParameter['timeBucket'] = timeBucket;
|
||||||
}
|
}
|
||||||
@ -6691,11 +6696,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isTrashed]
|
* @param {boolean} [isTrashed]
|
||||||
|
* @param {boolean} [withStacked]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
// verify required parameter 'size' is not null or undefined
|
// verify required parameter 'size' is not null or undefined
|
||||||
assertParamExists('getTimeBuckets', 'size', size)
|
assertParamExists('getTimeBuckets', 'size', size)
|
||||||
const localVarPath = `/asset/time-buckets`;
|
const localVarPath = `/asset/time-buckets`;
|
||||||
@ -6747,6 +6753,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||||||
localVarQueryParameter['isTrashed'] = isTrashed;
|
localVarQueryParameter['isTrashed'] = isTrashed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (withStacked !== undefined) {
|
||||||
|
localVarQueryParameter['withStacked'] = withStacked;
|
||||||
|
}
|
||||||
|
|
||||||
if (key !== undefined) {
|
if (key !== undefined) {
|
||||||
localVarQueryParameter['key'] = key;
|
localVarQueryParameter['key'] = key;
|
||||||
}
|
}
|
||||||
@ -7485,12 +7495,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isTrashed]
|
* @param {boolean} [isTrashed]
|
||||||
|
* @param {boolean} [withStacked]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, key, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -7565,12 +7576,13 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||||||
* @param {boolean} [isArchived]
|
* @param {boolean} [isArchived]
|
||||||
* @param {boolean} [isFavorite]
|
* @param {boolean} [isFavorite]
|
||||||
* @param {boolean} [isTrashed]
|
* @param {boolean} [isTrashed]
|
||||||
|
* @param {boolean} [withStacked]
|
||||||
* @param {string} [key]
|
* @param {string} [key]
|
||||||
* @param {*} [options] Override http request option.
|
* @param {*} [options] Override http request option.
|
||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
|
async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@ -7815,7 +7827,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
|
getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> {
|
||||||
return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(axios, basePath));
|
return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
@ -7876,7 +7888,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||||||
* @throws {RequiredError}
|
* @throws {RequiredError}
|
||||||
*/
|
*/
|
||||||
getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> {
|
getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> {
|
||||||
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(axios, basePath));
|
return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Get all asset of a device that are in the database, ID only.
|
* Get all asset of a device that are in the database, ID only.
|
||||||
@ -8251,6 +8263,13 @@ export interface AssetApiGetByTimeBucketRequest {
|
|||||||
*/
|
*/
|
||||||
readonly isTrashed?: boolean
|
readonly isTrashed?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AssetApiGetByTimeBucket
|
||||||
|
*/
|
||||||
|
readonly withStacked?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -8405,6 +8424,13 @@ export interface AssetApiGetTimeBucketsRequest {
|
|||||||
*/
|
*/
|
||||||
readonly isTrashed?: boolean
|
readonly isTrashed?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AssetApiGetTimeBuckets
|
||||||
|
*/
|
||||||
|
readonly withStacked?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@ -8820,7 +8846,7 @@ export class AssetApi extends BaseAPI {
|
|||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) {
|
public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -8895,7 +8921,7 @@ export class AssetApi extends BaseAPI {
|
|||||||
* @memberof AssetApi
|
* @memberof AssetApi
|
||||||
*/
|
*/
|
||||||
public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) {
|
public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) {
|
||||||
return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,10 +32,11 @@
|
|||||||
export let showDownloadButton: boolean;
|
export let showDownloadButton: boolean;
|
||||||
export let showDetailButton: boolean;
|
export let showDetailButton: boolean;
|
||||||
export let showSlideshow = false;
|
export let showSlideshow = false;
|
||||||
|
export let hasStackChildern = false;
|
||||||
|
|
||||||
$: isOwner = asset.ownerId === $page.data.user?.id;
|
$: isOwner = asset.ownerId === $page.data.user?.id;
|
||||||
|
|
||||||
type MenuItemEvent = 'addToAlbum' | 'addToSharedAlbum' | 'asProfileImage' | 'runJob' | 'playSlideShow';
|
type MenuItemEvent = 'addToAlbum' | 'addToSharedAlbum' | 'asProfileImage' | 'runJob' | 'playSlideShow' | 'unstack';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
goBack: void;
|
goBack: void;
|
||||||
@ -51,6 +52,7 @@
|
|||||||
asProfileImage: void;
|
asProfileImage: void;
|
||||||
runJob: AssetJobName;
|
runJob: AssetJobName;
|
||||||
playSlideShow: void;
|
playSlideShow: void;
|
||||||
|
unstack: void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let contextMenuPosition = { x: 0, y: 0 };
|
let contextMenuPosition = { x: 0, y: 0 };
|
||||||
@ -173,6 +175,11 @@
|
|||||||
text={asset.isArchived ? 'Unarchive' : 'Archive'}
|
text={asset.isArchived ? 'Unarchive' : 'Archive'}
|
||||||
/>
|
/>
|
||||||
<MenuOption on:click={() => onMenuClick('asProfileImage')} text="As profile picture" />
|
<MenuOption on:click={() => onMenuClick('asProfileImage')} text="As profile picture" />
|
||||||
|
|
||||||
|
{#if hasStackChildern}
|
||||||
|
<MenuOption on:click={() => onMenuClick('unstack')} text="Un-Stack" />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<MenuOption
|
<MenuOption
|
||||||
on:click={() => onJobClick(AssetJobName.RefreshMetadata)}
|
on:click={() => onJobClick(AssetJobName.RefreshMetadata)}
|
||||||
text={api.getAssetJobName(AssetJobName.RefreshMetadata)}
|
text={api.getAssetJobName(AssetJobName.RefreshMetadata)}
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiImageBrokenVariant, mdiPause, mdiPlay } from '@mdi/js';
|
import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiImageBrokenVariant, mdiPause, mdiPlay } from '@mdi/js';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
|
||||||
|
import { stackAssetsStore } from '$lib/stores/stacked-asset.store';
|
||||||
|
|
||||||
export let assetStore: AssetStore | null = null;
|
export let assetStore: AssetStore | null = null;
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
@ -32,6 +34,7 @@
|
|||||||
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
|
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
|
||||||
$: isTrashEnabled = $featureFlags.trash;
|
$: isTrashEnabled = $featureFlags.trash;
|
||||||
export let force = false;
|
export let force = false;
|
||||||
|
export let withStacked = false;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
archived: AssetResponseDto;
|
archived: AssetResponseDto;
|
||||||
@ -41,6 +44,7 @@
|
|||||||
close: void;
|
close: void;
|
||||||
next: void;
|
next: void;
|
||||||
previous: void;
|
previous: void;
|
||||||
|
unstack: void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let appearsInAlbums: AlbumResponseDto[] = [];
|
let appearsInAlbums: AlbumResponseDto[] = [];
|
||||||
@ -52,6 +56,21 @@
|
|||||||
let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline;
|
let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline;
|
||||||
let shouldShowDetailButton = asset.hasMetadata;
|
let shouldShowDetailButton = asset.hasMetadata;
|
||||||
let canCopyImagesToClipboard: boolean;
|
let canCopyImagesToClipboard: boolean;
|
||||||
|
let previewStackedAsset: AssetResponseDto | undefined;
|
||||||
|
$: displayedAsset = previewStackedAsset || asset;
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (asset.stackCount && asset.stack) {
|
||||||
|
$stackAssetsStore = asset.stack;
|
||||||
|
$stackAssetsStore = [...$stackAssetsStore, asset].sort(
|
||||||
|
(a, b) => new Date(b.fileCreatedAt).getTime() - new Date(a.fileCreatedAt).getTime(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$stackAssetsStore.map((a) => a.id).includes(asset.id)) {
|
||||||
|
$stackAssetsStore = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo);
|
const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo);
|
||||||
|
|
||||||
@ -66,6 +85,15 @@
|
|||||||
// TODO: Move to regular import once the package correctly supports ESM.
|
// TODO: Move to regular import once the package correctly supports ESM.
|
||||||
const module = await import('copy-image-clipboard');
|
const module = await import('copy-image-clipboard');
|
||||||
canCopyImagesToClipboard = module.canCopyImagesToClipboard();
|
canCopyImagesToClipboard = module.canCopyImagesToClipboard();
|
||||||
|
|
||||||
|
if (asset.stackCount && asset.stack) {
|
||||||
|
$stackAssetsStore = asset.stack;
|
||||||
|
$stackAssetsStore = [...$stackAssetsStore, asset].sort(
|
||||||
|
(a, b) => new Date(a.fileCreatedAt).getTime() - new Date(b.fileCreatedAt).getTime(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$stackAssetsStore = [];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@ -351,6 +379,35 @@
|
|||||||
progressBar.restart(false);
|
progressBar.restart(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStackedAssetMouseEvent = (e: CustomEvent<{ isMouseOver: boolean }>, asset: AssetResponseDto) => {
|
||||||
|
const { isMouseOver } = e.detail;
|
||||||
|
|
||||||
|
if (isMouseOver) {
|
||||||
|
previewStackedAsset = asset;
|
||||||
|
} else {
|
||||||
|
previewStackedAsset = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnstack = async () => {
|
||||||
|
try {
|
||||||
|
const ids = $stackAssetsStore.map(({ id }) => id);
|
||||||
|
await api.assetApi.updateAssets({ assetBulkUpdateDto: { ids, removeParent: true } });
|
||||||
|
for (const child of $stackAssetsStore) {
|
||||||
|
child.stackParentId = null;
|
||||||
|
assetStore?.addAsset(child);
|
||||||
|
}
|
||||||
|
asset.stackCount = 0;
|
||||||
|
asset.stack = [];
|
||||||
|
assetStore?.updateAsset(asset);
|
||||||
|
|
||||||
|
dispatch('unstack');
|
||||||
|
notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 });
|
||||||
|
} catch (error) {
|
||||||
|
await handleError(error, `Unable to unstack`);
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@ -390,6 +447,7 @@
|
|||||||
showDownloadButton={shouldShowDownloadButton}
|
showDownloadButton={shouldShowDownloadButton}
|
||||||
showDetailButton={shouldShowDetailButton}
|
showDetailButton={shouldShowDetailButton}
|
||||||
showSlideshow={!!assetStore}
|
showSlideshow={!!assetStore}
|
||||||
|
hasStackChildern={$stackAssetsStore.length > 0}
|
||||||
on:goBack={closeViewer}
|
on:goBack={closeViewer}
|
||||||
on:showDetail={showDetailInfoHandler}
|
on:showDetail={showDetailInfoHandler}
|
||||||
on:download={() => downloadFile(asset)}
|
on:download={() => downloadFile(asset)}
|
||||||
@ -403,6 +461,7 @@
|
|||||||
on:asProfileImage={() => (isShowProfileImageCrop = true)}
|
on:asProfileImage={() => (isShowProfileImageCrop = true)}
|
||||||
on:runJob={({ detail: job }) => handleRunJob(job)}
|
on:runJob={({ detail: job }) => handleRunJob(job)}
|
||||||
on:playSlideShow={handlePlaySlideshow}
|
on:playSlideShow={handlePlaySlideshow}
|
||||||
|
on:unstack={handleUnstack}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -413,7 +472,23 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Asset Viewer -->
|
||||||
<div class="col-span-4 col-start-1 row-span-full row-start-1">
|
<div class="col-span-4 col-start-1 row-span-full row-start-1">
|
||||||
|
<!-- Condition to show preview of stacked asset on hovered -->
|
||||||
|
{#if displayedAsset}
|
||||||
|
{#key displayedAsset.id}
|
||||||
|
{#if displayedAsset.type === AssetTypeEnum.Image}
|
||||||
|
<PhotoViewer asset={displayedAsset} on:close={closeViewer} haveFadeTransition={false} />
|
||||||
|
{:else}
|
||||||
|
<VideoViewer
|
||||||
|
assetId={displayedAsset.id}
|
||||||
|
on:close={closeViewer}
|
||||||
|
on:onVideoEnded={handleVideoEnded}
|
||||||
|
on:onVideoStarted={handleVideoStarted}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/key}
|
||||||
|
{:else}
|
||||||
{#key asset.id}
|
{#key asset.id}
|
||||||
{#if !asset.resized}
|
{#if !asset.resized}
|
||||||
<div class="flex h-full w-full justify-center">
|
<div class="flex h-full w-full justify-center">
|
||||||
@ -446,7 +521,45 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/key}
|
{/key}
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if $stackAssetsStore.length > 0 && withStacked}
|
||||||
|
<div
|
||||||
|
id="stack-slideshow"
|
||||||
|
class="z-[1005] flex place-item-center place-content-center absolute bottom-0 w-full col-span-4 col-start-1 mb-1 overflow-x-auto horizontal-scrollbar"
|
||||||
|
>
|
||||||
|
<div class="relative whitespace-nowrap transition-all">
|
||||||
|
{#each $stackAssetsStore as stackedAsset (stackedAsset.id)}
|
||||||
|
<div
|
||||||
|
class="{stackedAsset.id == asset.id
|
||||||
|
? '-translate-y-[1px]'
|
||||||
|
: '-translate-y-0'} inline-block px-1 transition-transform"
|
||||||
|
>
|
||||||
|
<Thumbnail
|
||||||
|
class="{stackedAsset.id == asset.id
|
||||||
|
? 'bg-transparent border-2 border-white'
|
||||||
|
: 'bg-gray-700/40'} inline-block hover:bg-transparent"
|
||||||
|
asset={stackedAsset}
|
||||||
|
on:click={() => (asset = stackedAsset)}
|
||||||
|
on:mouse-event={(e) => handleStackedAssetMouseEvent(e, stackedAsset)}
|
||||||
|
readonly
|
||||||
|
thumbnailSize={stackedAsset.id == asset.id ? 65 : 60}
|
||||||
|
showStackedIcon={false}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if stackedAsset.id == asset.id}
|
||||||
|
<div class="w-full flex place-items-center place-content-center">
|
||||||
|
<div class="w-2 h-2 bg-white rounded-full flex mt-[2px]" />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stack & Stack Controller -->
|
||||||
|
|
||||||
{#if !isSlideshowMode && showNavigation}
|
{#if !isSlideshowMode && showNavigation}
|
||||||
<div class="z-[999] col-span-1 col-start-4 row-span-1 row-start-2 mb-[60px] justify-self-end">
|
<div class="z-[999] col-span-1 col-start-4 row-span-1 row-start-2 mb-[60px] justify-self-end">
|
||||||
@ -458,7 +571,7 @@
|
|||||||
<div
|
<div
|
||||||
transition:fly={{ duration: 150 }}
|
transition:fly={{ duration: 150 }}
|
||||||
id="detail-panel"
|
id="detail-panel"
|
||||||
class="z-[1002] row-span-full w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg"
|
class="z-[1002] row-start-1 row-span-5 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg"
|
||||||
translate="yes"
|
translate="yes"
|
||||||
>
|
>
|
||||||
<DetailPanel
|
<DetailPanel
|
||||||
@ -512,4 +625,27 @@
|
|||||||
#immich-asset-viewer {
|
#immich-asset-viewer {
|
||||||
contain: layout;
|
contain: layout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.horizontal-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Track */
|
||||||
|
.horizontal-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: #000000;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle */
|
||||||
|
.horizontal-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(159, 159, 159, 0.408);
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Handle on hover */
|
||||||
|
.horizontal-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #adcbfa;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -40,6 +40,13 @@
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (!asset.exifInfo) {
|
||||||
|
api.assetApi.getAssetById({ id: asset.id }).then((res) => {
|
||||||
|
asset.exifInfo = res.data?.exifInfo;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
$: lat = latlng ? latlng[0] : undefined;
|
$: lat = latlng ? latlng[0] : undefined;
|
||||||
$: lng = latlng ? latlng[1] : undefined;
|
$: lng = latlng ? latlng[1] : undefined;
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let element: HTMLDivElement | undefined = undefined;
|
export let element: HTMLDivElement | undefined = undefined;
|
||||||
|
export let haveFadeTransition = true;
|
||||||
|
|
||||||
let imgElement: HTMLDivElement;
|
let imgElement: HTMLDivElement;
|
||||||
let assetData: string;
|
let assetData: string;
|
||||||
@ -116,7 +117,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
transition:fade={{ duration: 150 }}
|
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
||||||
class="flex h-full select-none place-content-center place-items-center"
|
class="flex h-full select-none place-content-center place-items-center"
|
||||||
>
|
>
|
||||||
{#await loadAssetData({ loadOriginal: false })}
|
{#await loadAssetData({ loadOriginal: false })}
|
||||||
@ -124,7 +125,7 @@
|
|||||||
{:then}
|
{:then}
|
||||||
<div bind:this={imgElement} class="h-full w-full">
|
<div bind:this={imgElement} class="h-full w-full">
|
||||||
<img
|
<img
|
||||||
transition:fade={{ duration: 150 }}
|
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
||||||
src={assetData}
|
src={assetData}
|
||||||
alt={asset.id}
|
alt={asset.id}
|
||||||
class="h-full w-full object-contain"
|
class="h-full w-full object-contain"
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
import VideoThumbnail from './video-thumbnail.svelte';
|
import VideoThumbnail from './video-thumbnail.svelte';
|
||||||
import {
|
import {
|
||||||
mdiArchiveArrowDownOutline,
|
mdiArchiveArrowDownOutline,
|
||||||
|
mdiCameraBurst,
|
||||||
mdiCheckCircle,
|
mdiCheckCircle,
|
||||||
mdiHeart,
|
mdiHeart,
|
||||||
mdiImageBrokenVariant,
|
mdiImageBrokenVariant,
|
||||||
@ -18,7 +19,11 @@
|
|||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher<{
|
||||||
|
click: { asset: AssetResponseDto };
|
||||||
|
select: { asset: AssetResponseDto };
|
||||||
|
'mouse-event': { isMouseOver: boolean; selectedGroupIndex: number };
|
||||||
|
}>();
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let groupIndex = 0;
|
export let groupIndex = 0;
|
||||||
@ -31,6 +36,10 @@
|
|||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
export let showArchiveIcon = false;
|
export let showArchiveIcon = false;
|
||||||
|
export let showStackedIcon = true;
|
||||||
|
|
||||||
|
let className = '';
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
let mouseOver = false;
|
let mouseOver = false;
|
||||||
|
|
||||||
@ -66,6 +75,14 @@
|
|||||||
dispatch('select', { asset });
|
dispatch('select', { asset });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onMouseEnter = () => {
|
||||||
|
mouseOver = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseLeave = () => {
|
||||||
|
mouseOver = false;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IntersectionObserver once={false} let:intersecting>
|
<IntersectionObserver once={false} let:intersecting>
|
||||||
@ -78,13 +95,13 @@
|
|||||||
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
||||||
class:cursor-not-allowed={disabled}
|
class:cursor-not-allowed={disabled}
|
||||||
class:hover:cursor-pointer={!disabled}
|
class:hover:cursor-pointer={!disabled}
|
||||||
on:mouseenter={() => (mouseOver = true)}
|
on:mouseenter={() => onMouseEnter()}
|
||||||
on:mouseleave={() => (mouseOver = false)}
|
on:mouseleave={() => onMouseLeave()}
|
||||||
on:click={thumbnailClickedHandler}
|
on:click={thumbnailClickedHandler}
|
||||||
on:keydown={thumbnailKeyDownHandler}
|
on:keydown={thumbnailKeyDownHandler}
|
||||||
>
|
>
|
||||||
{#if intersecting}
|
{#if intersecting}
|
||||||
<div class="absolute z-20 h-full w-full">
|
<div class="absolute z-20 h-full w-full {className}">
|
||||||
<!-- Select asset button -->
|
<!-- Select asset button -->
|
||||||
{#if !readonly && (mouseOver || selected || selectionCandidate)}
|
{#if !readonly && (mouseOver || selected || selectionCandidate)}
|
||||||
<button
|
<button
|
||||||
@ -140,6 +157,21 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Stacked asset -->
|
||||||
|
|
||||||
|
{#if asset.stackCount && showStackedIcon}
|
||||||
|
<div
|
||||||
|
class="absolute {asset.type == AssetTypeEnum.Image && asset.livePhotoVideoId == null
|
||||||
|
? 'top-0 right-0'
|
||||||
|
: 'top-7 right-1'} z-20 flex place-items-center gap-1 text-xs font-medium text-white"
|
||||||
|
>
|
||||||
|
<span class="pr-2 pt-2 flex place-items-center gap-1">
|
||||||
|
<p>{asset.stackCount}</p>
|
||||||
|
<Icon path={mdiCameraBurst} size="24" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if asset.resized}
|
{#if asset.resized}
|
||||||
<ImageThumbnail
|
<ImageThumbnail
|
||||||
url={api.getAssetThumbnailUrl(asset.id, format)}
|
url={api.getAssetThumbnailUrl(asset.id, format)}
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
|
import { api } from '@api';
|
||||||
|
import { OnStack, getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||||
|
import {
|
||||||
|
NotificationType,
|
||||||
|
notificationController,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
|
||||||
|
export let onStack: OnStack | undefined = undefined;
|
||||||
|
|
||||||
|
const { getAssets, clearSelect } = getAssetControlContext();
|
||||||
|
|
||||||
|
const handleStack = async () => {
|
||||||
|
try {
|
||||||
|
const assets = Array.from(getAssets());
|
||||||
|
const parent = assets.at(0);
|
||||||
|
|
||||||
|
if (parent == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = assets.slice(1);
|
||||||
|
const ids = children.map(({ id }) => id);
|
||||||
|
|
||||||
|
if (children.length > 0) {
|
||||||
|
await api.assetApi.updateAssets({ assetBulkUpdateDto: { ids, stackParentId: parent.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
let childrenCount = parent.stackCount ?? 0;
|
||||||
|
for (const asset of children) {
|
||||||
|
asset.stackParentId = parent?.id;
|
||||||
|
// Add grand-children's count to new parent
|
||||||
|
childrenCount += asset.stackCount == null ? 1 : asset.stackCount + 1;
|
||||||
|
// Reset children stack info
|
||||||
|
asset.stackCount = null;
|
||||||
|
asset.stack = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
parent.stackCount = childrenCount;
|
||||||
|
onStack?.(ids);
|
||||||
|
|
||||||
|
notificationController.show({
|
||||||
|
message: `Stacked ${ids.length + 1} assets`,
|
||||||
|
type: NotificationType.Info,
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
|
||||||
|
clearSelect();
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, `Unable to stack`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<MenuOption text="Stack" on:click={handleStack} />
|
@ -20,6 +20,7 @@
|
|||||||
export let isSelectionMode = false;
|
export let isSelectionMode = false;
|
||||||
export let viewport: Viewport;
|
export let viewport: Viewport;
|
||||||
export let singleSelect = false;
|
export let singleSelect = false;
|
||||||
|
export let withStacked = false;
|
||||||
|
|
||||||
export let assetStore: AssetStore;
|
export let assetStore: AssetStore;
|
||||||
export let assetInteractionStore: AssetInteractionStore;
|
export let assetInteractionStore: AssetInteractionStore;
|
||||||
@ -178,6 +179,7 @@
|
|||||||
style="width: {box.width}px; height: {box.height}px; top: {box.top}px; left: {box.left}px"
|
style="width: {box.width}px; height: {box.height}px; top: {box.top}px; left: {box.left}px"
|
||||||
>
|
>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
|
showStackedIcon={withStacked}
|
||||||
{asset}
|
{asset}
|
||||||
{groupIndex}
|
{groupIndex}
|
||||||
on:click={() => assetClickHandler(asset, groupAssets, groupTitle)}
|
on:click={() => assetClickHandler(asset, groupAssets, groupTitle)}
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
export let assetStore: AssetStore;
|
export let assetStore: AssetStore;
|
||||||
export let assetInteractionStore: AssetInteractionStore;
|
export let assetInteractionStore: AssetInteractionStore;
|
||||||
export let removeAction: AssetAction | null = null;
|
export let removeAction: AssetAction | null = null;
|
||||||
|
export let withStacked = false;
|
||||||
|
|
||||||
$: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
|
$: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
|
||||||
export let forceDelete = false;
|
export let forceDelete = false;
|
||||||
@ -365,6 +366,7 @@
|
|||||||
<div id={'bucket_' + bucket.bucketDate} style:height={bucket.bucketHeight + 'px'}>
|
<div id={'bucket_' + bucket.bucketDate} style:height={bucket.bucketHeight + 'px'}>
|
||||||
{#if intersecting}
|
{#if intersecting}
|
||||||
<AssetDateGroup
|
<AssetDateGroup
|
||||||
|
{withStacked}
|
||||||
{assetStore}
|
{assetStore}
|
||||||
{assetInteractionStore}
|
{assetInteractionStore}
|
||||||
{isSelectionMode}
|
{isSelectionMode}
|
||||||
@ -389,6 +391,7 @@
|
|||||||
<Portal target="body">
|
<Portal target="body">
|
||||||
{#if $showAssetViewer}
|
{#if $showAssetViewer}
|
||||||
<AssetViewer
|
<AssetViewer
|
||||||
|
{withStacked}
|
||||||
{assetStore}
|
{assetStore}
|
||||||
asset={$viewingAsset}
|
asset={$viewingAsset}
|
||||||
force={forceDelete || !isTrashEnabled}
|
force={forceDelete || !isTrashEnabled}
|
||||||
@ -399,6 +402,7 @@
|
|||||||
on:unarchived={({ detail: asset }) => handleAction(asset, AssetAction.UNARCHIVE)}
|
on:unarchived={({ detail: asset }) => handleAction(asset, AssetAction.UNARCHIVE)}
|
||||||
on:favorite={({ detail: asset }) => handleAction(asset, AssetAction.FAVORITE)}
|
on:favorite={({ detail: asset }) => handleAction(asset, AssetAction.FAVORITE)}
|
||||||
on:unfavorite={({ detail: asset }) => handleAction(asset, AssetAction.UNFAVORITE)}
|
on:unfavorite={({ detail: asset }) => handleAction(asset, AssetAction.UNFAVORITE)}
|
||||||
|
on:unstack={() => handleClose()}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Portal>
|
</Portal>
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
export type OnRestore = (ids: string[]) => void;
|
export type OnRestore = (ids: string[]) => void;
|
||||||
export type OnArchive = (ids: string[], isArchived: boolean) => void;
|
export type OnArchive = (ids: string[], isArchived: boolean) => void;
|
||||||
export type OnFavorite = (ids: string[], favorite: boolean) => void;
|
export type OnFavorite = (ids: string[], favorite: boolean) => void;
|
||||||
|
export type OnStack = (ids: string[]) => void;
|
||||||
|
|
||||||
export interface AssetControlContext {
|
export interface AssetControlContext {
|
||||||
// Wrap assets in a function, because context isn't reactive.
|
// Wrap assets in a function, because context isn't reactive.
|
||||||
|
@ -222,6 +222,7 @@ export class AssetStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bucket.assets = assets;
|
bucket.assets = assets;
|
||||||
|
|
||||||
this.emit(true);
|
this.emit(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Failed to load assets');
|
handleError(error, 'Failed to load assets');
|
||||||
@ -251,7 +252,7 @@ export class AssetStore {
|
|||||||
return scrollTimeline ? delta : 0;
|
return scrollTimeline ? delta : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private addAsset(asset: AssetResponseDto): void {
|
addAsset(asset: AssetResponseDto): void {
|
||||||
if (
|
if (
|
||||||
this.assetToBucket[asset.id] ||
|
this.assetToBucket[asset.id] ||
|
||||||
this.options.userId ||
|
this.options.userId ||
|
||||||
|
4
web/src/lib/stores/stacked-asset.store.ts
Normal file
4
web/src/lib/stores/stacked-asset.store.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import type { AssetResponseDto } from '../../api/open-api';
|
||||||
|
|
||||||
|
export const stackAssetsStore = writable<AssetResponseDto[]>([]);
|
@ -7,6 +7,7 @@
|
|||||||
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 StackAction from '$lib/components/photos-page/actions/stack-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 AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
|
||||||
@ -25,7 +26,7 @@
|
|||||||
|
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
let handleEscapeKey = false;
|
let handleEscapeKey = false;
|
||||||
const assetStore = new AssetStore({ isArchived: false });
|
const assetStore = new AssetStore({ isArchived: false, withStacked: true });
|
||||||
const assetInteractionStore = createAssetInteractionStore();
|
const assetInteractionStore = createAssetInteractionStore();
|
||||||
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
const { isMultiSelectState, selectedAssets } = assetInteractionStore;
|
||||||
|
|
||||||
@ -62,13 +63,22 @@
|
|||||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||||
<DownloadAction menuItem />
|
<DownloadAction menuItem />
|
||||||
<ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} />
|
<ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} />
|
||||||
|
{#if $selectedAssets.size > 1}
|
||||||
|
<StackAction onStack={(ids) => assetStore.removeAssets(ids)} />
|
||||||
|
{/if}
|
||||||
<AssetJobActions />
|
<AssetJobActions />
|
||||||
</AssetSelectContextMenu>
|
</AssetSelectContextMenu>
|
||||||
</AssetSelectControlBar>
|
</AssetSelectControlBar>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton scrollbar={false}>
|
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton scrollbar={false}>
|
||||||
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE} on:escape={handleEscape}>
|
<AssetGrid
|
||||||
|
{assetStore}
|
||||||
|
{assetInteractionStore}
|
||||||
|
removeAction={AssetAction.ARCHIVE}
|
||||||
|
on:escape={handleEscape}
|
||||||
|
withStacked
|
||||||
|
>
|
||||||
{#if data.user.memoriesEnabled}
|
{#if data.user.memoriesEnabled}
|
||||||
<MemoryLane />
|
<MemoryLane />
|
||||||
{/if}
|
{/if}
|
||||||
|
Loading…
Reference in New Issue
Block a user