Compare commits

..

6 Commits

Author SHA1 Message Date
Alex The Bot
31987bc043 Version v1.82.1 2023-10-18 17:14:26 +00:00
Alex
23f0eb6fe8 fix(web): fix websocket mode (#4531) 2023-10-18 12:12:19 -05:00
Jason Rasmussen
0994575bf3 fix(server): album add/remove asset performance (#4516) 2023-10-18 10:56:00 -05:00
Jason Rasmussen
f4a12acd29 fix(web): scrollbar offset (#4518)
* fix(web): scrollbar offset

* fix offset on photo page

* proper fix

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-10-18 10:54:20 -05:00
Jason Rasmussen
335216f6dd feat(server): allow underscores in ML url (#4517) 2023-10-17 21:34:16 +00:00
Efren
5a9acbc05b Fix Issues hyperlink pointing to Releases (#4508) 2023-10-17 07:38:42 +00:00
33 changed files with 177 additions and 132 deletions

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.82.0
* The version of the OpenAPI document: 1.82.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.82.0
* The version of the OpenAPI document: 1.82.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.82.0
* The version of the OpenAPI document: 1.82.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.82.0
* The version of the OpenAPI document: 1.82.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.82.0
* The version of the OpenAPI document: 1.82.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -11,6 +11,6 @@ Running into an issue or have a question? Try the following:
3. Search through existing [GitHub Issues][github-issues].
4. Open a help ticket on [Discord][discord-link].
[github-issues]: https://github.com/immich-app/immich/releases
[github-issues]: https://github.com/immich-app/immich/issues
[github-releases]: https://github.com/immich-app/immich/releases
[discord-link]: https://discord.com/invite/D8JsnBEuKb

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.82.0"
version = "1.82.1"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -36,7 +36,7 @@ platform :android do
build_type: 'Release',
properties: {
"android.injected.version.code" => 106,
"android.injected.version.name" => "1.82.0",
"android.injected.version.name" => "1.82.1",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.82.0"
version_number: "1.82.1"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.82.0
- API version: 1.82.1
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: "none"
version: 1.82.0+106
version: 1.82.1+106
isar_version: &isar_version 3.1.0+1
environment:

View File

@@ -5379,7 +5379,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.82.0",
"version": "1.82.1",
"contact": {}
},
"tags": [],

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.82.0",
"version": "1.82.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.82.0",
"version": "1.82.1",
"license": "UNLICENSED",
"dependencies": {
"@babel/runtime": "^7.22.11",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.82.0",
"version": "1.82.1",
"description": "",
"author": "",
"private": true,

View File

@@ -1,7 +1,6 @@
import { BadRequestException } from '@nestjs/common';
import {
albumStub,
assetStub,
authStub,
IAccessRepositoryMock,
newAccessRepositoryMock,
@@ -225,7 +224,7 @@ describe(AlbumService.name, () => {
}),
).rejects.toBeInstanceOf(BadRequestException);
expect(albumMock.hasAsset).toHaveBeenCalledWith(albumStub.oneAsset.id, 'not-in-album');
expect(albumMock.hasAsset).toHaveBeenCalledWith({ albumId: 'album-4', assetId: 'not-in-album' });
expect(albumMock.update).not.toHaveBeenCalled();
});
@@ -461,6 +460,7 @@ describe(AlbumService.name, () => {
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.hasAsset.mockResolvedValue(false);
await expect(
sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
@@ -473,9 +473,12 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123',
updatedAt: expect.any(Date),
assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
albumThumbnailAssetId: 'asset-1',
});
expect(albumMock.addAssets).toHaveBeenCalledWith({
albumId: 'album-123',
assetIds: ['asset-1', 'asset-2', 'asset-3'],
});
});
it('should not set the thumbnail if the album has one already', async () => {
@@ -490,9 +493,9 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123',
updatedAt: expect.any(Date),
assets: [{ id: 'asset-1' }],
albumThumbnailAssetId: 'asset-id',
});
expect(albumMock.addAssets).toHaveBeenCalled();
});
it('should allow a shared user to add assets', async () => {
@@ -512,9 +515,12 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123',
updatedAt: expect.any(Date),
assets: [{ id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
albumThumbnailAssetId: 'asset-1',
});
expect(albumMock.addAssets).toHaveBeenCalledWith({
albumId: 'album-123',
assetIds: ['asset-1', 'asset-2', 'asset-3'],
});
});
it('should allow a shared link user to add assets', async () => {
@@ -523,6 +529,7 @@ describe(AlbumService.name, () => {
accessMock.album.hasSharedLinkAccess.mockResolvedValue(true);
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.hasAsset.mockResolvedValue(false);
await expect(
sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }),
@@ -535,9 +542,12 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123',
updatedAt: expect.any(Date),
assets: [assetStub.image, { id: 'asset-1' }, { id: 'asset-2' }, { id: 'asset-3' }],
albumThumbnailAssetId: 'asset-1',
});
expect(albumMock.addAssets).toHaveBeenCalledWith({
albumId: 'album-123',
assetIds: ['asset-1', 'asset-2', 'asset-3'],
});
expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith(
authStub.adminSharedLink.sharedLinkId,
@@ -550,6 +560,7 @@ describe(AlbumService.name, () => {
accessMock.asset.hasOwnerAccess.mockResolvedValue(false);
accessMock.asset.hasPartnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.hasAsset.mockResolvedValue(false);
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1'] })).resolves.toEqual([
{ success: true, id: 'asset-1' },
@@ -558,10 +569,8 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123',
updatedAt: expect.any(Date),
assets: [assetStub.image, { id: 'asset-1' }],
albumThumbnailAssetId: 'asset-1',
});
expect(accessMock.asset.hasPartnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'asset-1');
});
@@ -569,6 +578,7 @@ describe(AlbumService.name, () => {
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
accessMock.asset.hasOwnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.hasAsset.mockResolvedValue(true);
await expect(sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
{ success: false, id: 'asset-id', error: BulkIdErrorReason.DUPLICATE },
@@ -620,17 +630,14 @@ describe(AlbumService.name, () => {
it('should allow the owner to remove assets', async () => {
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.hasAsset.mockResolvedValue(true);
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
{ success: true, id: 'asset-id' },
]);
expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123',
updatedAt: expect.any(Date),
assets: [],
albumThumbnailAssetId: null,
});
expect(albumMock.update).toHaveBeenCalledWith({ id: 'album-123', updatedAt: expect.any(Date) });
expect(albumMock.removeAssets).toHaveBeenCalledWith({ assetIds: ['asset-id'], albumId: 'album-123' });
});
it('should skip assets not in the album', async () => {
@@ -647,9 +654,14 @@ describe(AlbumService.name, () => {
it('should skip assets without user permission to remove', async () => {
accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset));
albumMock.hasAsset.mockResolvedValue(true);
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
{ success: false, id: 'asset-id', error: BulkIdErrorReason.NO_PERMISSION },
{
success: false,
id: 'asset-id',
error: BulkIdErrorReason.NO_PERMISSION,
},
]);
expect(albumMock.update).not.toHaveBeenCalled();
@@ -658,6 +670,7 @@ describe(AlbumService.name, () => {
it('should reset the thumbnail if it is removed', async () => {
accessMock.album.hasOwnerAccess.mockResolvedValue(true);
albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets));
albumMock.hasAsset.mockResolvedValue(true);
await expect(sut.removeAssets(authStub.admin, 'album-123', { ids: ['asset-id'] })).resolves.toEqual([
{ success: true, id: 'asset-id' },
@@ -666,9 +679,8 @@ describe(AlbumService.name, () => {
expect(albumMock.update).toHaveBeenCalledWith({
id: 'album-123',
updatedAt: expect.any(Date),
assets: [assetStub.withLocation],
albumThumbnailAssetId: assetStub.withLocation.id,
});
expect(albumMock.updateThumbnails).toHaveBeenCalled();
});
});

View File

@@ -120,7 +120,7 @@ export class AlbumService {
const album = await this.findOrFail(id, { withAssets: true });
if (dto.albumThumbnailAssetId) {
const valid = await this.albumRepository.hasAsset(id, dto.albumThumbnailAssetId);
const valid = await this.albumRepository.hasAsset({ albumId: id, assetId: dto.albumThumbnailAssetId });
if (!valid) {
throw new BadRequestException('Invalid album thumbnail');
}
@@ -148,35 +148,34 @@ export class AlbumService {
}
async addAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: true });
const album = await this.findOrFail(id, { withAssets: false });
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
const results: BulkIdResponseDto[] = [];
for (const id of dto.ids) {
const hasAsset = album.assets.find((asset) => asset.id === id);
for (const assetId of dto.ids) {
const hasAsset = await this.albumRepository.hasAsset({ albumId: id, assetId });
if (hasAsset) {
results.push({ id, success: false, error: BulkIdErrorReason.DUPLICATE });
results.push({ id: assetId, success: false, error: BulkIdErrorReason.DUPLICATE });
continue;
}
const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, id);
const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId);
if (!hasAccess) {
results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION });
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
continue;
}
results.push({ id, success: true });
album.assets.push({ id } as AssetEntity);
results.push({ id: assetId, success: true });
}
const newAsset = results.find(({ success }) => success);
if (newAsset) {
const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id);
if (newAssetIds.length > 0) {
await this.albumRepository.addAssets({ albumId: id, assetIds: newAssetIds });
await this.albumRepository.update({
id,
assets: album.assets,
updatedAt: new Date(),
albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAsset.id,
albumThumbnailAssetId: album.albumThumbnailAssetId ?? newAssetIds[0],
});
}
@@ -184,42 +183,37 @@ export class AlbumService {
}
async removeAssets(authUser: AuthUserDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: true });
const album = await this.findOrFail(id, { withAssets: false });
await this.access.requirePermission(authUser, Permission.ALBUM_READ, id);
const results: BulkIdResponseDto[] = [];
for (const id of dto.ids) {
const hasAsset = album.assets.find((asset) => asset.id === id);
for (const assetId of dto.ids) {
const hasAsset = await this.albumRepository.hasAsset({ albumId: id, assetId });
if (!hasAsset) {
results.push({ id, success: false, error: BulkIdErrorReason.NOT_FOUND });
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NOT_FOUND });
continue;
}
const hasAccess = await this.access.hasAny(authUser, [
{ permission: Permission.ALBUM_REMOVE_ASSET, id },
{ permission: Permission.ASSET_SHARE, id },
{ permission: Permission.ALBUM_REMOVE_ASSET, id: assetId },
{ permission: Permission.ASSET_SHARE, id: assetId },
]);
if (!hasAccess) {
results.push({ id, success: false, error: BulkIdErrorReason.NO_PERMISSION });
results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION });
continue;
}
results.push({ id, success: true });
album.assets = album.assets.filter((asset) => asset.id !== id);
if (album.albumThumbnailAssetId === id) {
album.albumThumbnailAssetId = null;
}
results.push({ id: assetId, success: true });
}
const hasSuccess = results.find(({ success }) => success);
if (hasSuccess) {
await this.albumRepository.update({
id,
assets: album.assets,
updatedAt: new Date(),
albumThumbnailAssetId: album.albumThumbnailAssetId || album.assets[0]?.id || null,
});
const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
if (removedIds.length > 0) {
await this.albumRepository.removeAssets({ albumId: id, assetIds: removedIds });
await this.albumRepository.update({ id, updatedAt: new Date() });
if (album.albumThumbnailAssetId && removedIds.includes(album.albumThumbnailAssetId)) {
await this.albumRepository.updateThumbnails();
}
}
return results;

View File

@@ -11,13 +11,24 @@ export interface AlbumInfoOptions {
withAssets: boolean;
}
export interface AlbumAsset {
albumId: string;
assetId: string;
}
export interface AlbumAssets {
albumId: string;
assetIds: string[];
}
export interface IAlbumRepository {
getById(id: string, options: AlbumInfoOptions): Promise<AlbumEntity | null>;
getByIds(ids: string[]): Promise<AlbumEntity[]>;
getByAssetId(ownerId: string, assetId: string): Promise<AlbumEntity[]>;
hasAsset(id: string, assetId: string): Promise<boolean>;
/** Remove an asset from _all_ albums */
removeAsset(id: string): Promise<void>;
addAssets(assets: AlbumAssets): Promise<void>;
hasAsset(asset: AlbumAsset): Promise<boolean>;
removeAsset(assetId: string): Promise<void>;
removeAssets(assets: AlbumAssets): Promise<void>;
getAssetCountForIds(ids: string[]): Promise<AlbumAssetCount[]>;
getInvalidThumbnail(): Promise<string[]>;
getOwned(ownerId: string): Promise<AlbumEntity[]>;

View File

@@ -6,7 +6,7 @@ export class SystemConfigMachineLearningDto {
@IsBoolean()
enabled!: boolean;
@IsUrl({ require_tld: false })
@IsUrl({ require_tld: false, allow_underscores: true })
@ValidateIf((dto) => dto.enabled)
url!: string;

View File

@@ -189,6 +189,15 @@ describe(SystemConfigService.name, () => {
expect(configMock.readFile).toHaveBeenCalledWith('immich-config.json');
});
it('should allow underscores in the machine learning url', async () => {
process.env.IMMICH_CONFIG_FILE = 'immich-config.json';
const partialConfig = { machineLearning: { url: 'immich_machine_learning' } };
configMock.readFile.mockResolvedValue(Buffer.from(JSON.stringify(partialConfig)));
const config = await sut.getConfig();
expect(config.machineLearning.url).toEqual('immich_machine_learning');
});
const tests = [
{ should: 'validate numbers', config: { ffmpeg: { crf: 'not-a-number' } } },
{ should: 'validate booleans', config: { oauth: { enabled: 'invalid' } } },

View File

@@ -1,4 +1,4 @@
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
import { AlbumAsset, AlbumAssetCount, AlbumAssets, AlbumInfoOptions, IAlbumRepository } from '@app/domain';
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
@@ -168,16 +168,27 @@ export class AlbumRepository implements IAlbumRepository {
.createQueryBuilder()
.delete()
.from('albums_assets_assets')
.where('"albums_assets_assets"."assetsId" = :assetId', { assetId })
.where('"albums_assets_assets"."assetsId" = :assetId', { assetId });
}
async removeAssets(asset: AlbumAssets): Promise<void> {
await this.dataSource
.createQueryBuilder()
.delete()
.from('albums_assets_assets')
.where({
albumsId: asset.albumId,
assetsId: In(asset.assetIds),
})
.execute();
}
hasAsset(id: string, assetId: string): Promise<boolean> {
hasAsset(asset: AlbumAsset): Promise<boolean> {
return this.repository.exist({
where: {
id,
id: asset.albumId,
assets: {
id: assetId,
id: asset.assetId,
},
},
relations: {
@@ -186,6 +197,15 @@ export class AlbumRepository implements IAlbumRepository {
});
}
async addAssets({ albumId, assetIds }: AlbumAssets): Promise<void> {
await this.dataSource
.createQueryBuilder()
.insert()
.into('albums_assets_assets', ['albumsId', 'assetsId'])
.values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId })))
.execute();
}
async create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
return this.save(album);
}

View File

@@ -14,7 +14,9 @@ export const newAlbumRepositoryMock = (): jest.Mocked<IAlbumRepository> => {
softDeleteAll: jest.fn(),
deleteAll: jest.fn(),
getAll: jest.fn(),
addAssets: jest.fn(),
removeAsset: jest.fn(),
removeAssets: jest.fn(),
hasAsset: jest.fn(),
create: jest.fn(),
update: jest.fn(),

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.82.0
* The version of the OpenAPI document: 1.82.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.82.0
* The version of the OpenAPI document: 1.82.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.82.0
* The version of the OpenAPI document: 1.82.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.82.0
* The version of the OpenAPI document: 1.82.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.82.0
* The version of the OpenAPI document: 1.82.1
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@@ -13,6 +13,7 @@
export let admin = false;
$: scrollbarClass = scrollbar ? 'immich-scrollbar p-4 pb-8' : 'scrollbar-hidden pl-4';
$: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full';
</script>
<header>
@@ -32,20 +33,19 @@
<SideBar />
{/if}
</slot>
<slot name="content">
{#if title}
<section class="relative">
<div
class="absolute flex h-16 w-full place-items-center justify-between border-b p-4 dark:border-immich-dark-gray dark:text-immich-dark-fg"
>
<p class="font-medium">{title}</p>
<slot name="buttons" />
</div>
<div class="{scrollbarClass} absolute top-16 h-[calc(100%-theme(spacing.16))] w-full overflow-y-auto">
<slot />
</div>
</section>
<section class="relative">
{#if title}
<div
class="absolute flex h-16 w-full place-items-center justify-between border-b p-4 dark:border-immich-dark-gray dark:text-immich-dark-fg"
>
<p class="font-medium">{title}</p>
<slot name="buttons" />
</div>
{/if}
</slot>
<div class="{scrollbarClass} absolute {hasTitleClass} w-full overflow-y-auto">
<slot />
</div>
</section>
</main>

View File

@@ -25,6 +25,7 @@
export let assetStore: AssetStore;
export let assetInteractionStore: AssetInteractionStore;
export let removeAction: AssetAction | null = null;
$: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash;
export let forceDelete = false;

View File

@@ -16,7 +16,6 @@ export const openWebsocketConnection = () => {
try {
const websocket = io('', {
path: '/api/socket.io',
transports: ['polling'],
reconnection: true,
forceNew: true,
autoConnect: true,

View File

@@ -45,7 +45,7 @@
</AssetSelectControlBar>
{/if}
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title}>
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}>
<EmptyPlaceholder
text="Archive photos and videos to hide them from your Photos view"

View File

@@ -46,7 +46,7 @@
</AssetSelectControlBar>
{/if}
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title}>
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNFAVORITE}>
<EmptyPlaceholder
text="Add favorites to quickly find your best pictures and videos"

View File

@@ -48,39 +48,36 @@
};
</script>
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton>
<svelte:fragment slot="header">
{#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
<CreateSharedLink on:escape={() => (handleEscapeKey = true)} />
<SelectAllAssets {assetStore} {assetInteractionStore} />
<AssetSelectContextMenu icon={Plus} title="Add">
<AddToAlbum />
<AddToAlbum shared />
</AssetSelectContextMenu>
<DeleteAssets
on:escape={() => (handleEscapeKey = true)}
onAssetDelete={(assetId) => assetStore.removeAsset(assetId)}
/>
<AssetSelectContextMenu icon={DotsVertical} title="Menu">
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
<DownloadAction menuItem />
<ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} />
<AssetJobActions />
</AssetSelectContextMenu>
</AssetSelectControlBar>
{#if $isMultiSelectState}
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
<CreateSharedLink on:escape={() => (handleEscapeKey = true)} />
<SelectAllAssets {assetStore} {assetInteractionStore} />
<AssetSelectContextMenu icon={Plus} title="Add">
<AddToAlbum />
<AddToAlbum shared />
</AssetSelectContextMenu>
<DeleteAssets
on:escape={() => (handleEscapeKey = true)}
onAssetDelete={(assetId) => assetStore.removeAsset(assetId)}
/>
<AssetSelectContextMenu icon={DotsVertical} title="Menu">
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
<DownloadAction menuItem />
<ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} />
<AssetJobActions />
</AssetSelectContextMenu>
</AssetSelectControlBar>
{/if}
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton scrollbar={false}>
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE} on:escape={handleEscape}>
{#if data.user.memoriesEnabled}
<MemoryLane />
{/if}
</svelte:fragment>
<svelte:fragment slot="content">
<AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE} on:escape={handleEscape}>
{#if data.user.memoriesEnabled}
<MemoryLane />
{/if}
<EmptyPlaceholder
text="CLICK TO UPLOAD YOUR FIRST PHOTO"
actionHandler={() => openFileUploadDialog()}
slot="empty"
/>
</AssetGrid>
</svelte:fragment>
<EmptyPlaceholder
text="CLICK TO UPLOAD YOUR FIRST PHOTO"
actionHandler={() => openFileUploadDialog()}
slot="empty"
/>
</AssetGrid>
</UserPageLayout>

View File

@@ -70,7 +70,7 @@
{/if}
{#if $featureFlags.loaded && $featureFlags.trash}
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title}>
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={false}>
<div class="flex place-items-center gap-2" slot="buttons">
<LinkButton on:click={handleRestoreTrash}>
<div class="flex place-items-center gap-2 text-sm">
@@ -87,7 +87,7 @@
</div>
<AssetGrid forceDelete {assetStore} {assetInteractionStore}>
<p class="font-medium text-gray-500/60 dark:text-gray-300/60">
<p class="font-medium text-gray-500/60 dark:text-gray-300/60 py-4">
Trashed items will be permanently deleted after {$serverConfig.trashDays} days.
</p>
<EmptyPlaceholder