1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-16 16:56:46 +01:00

fix(web): show trash indicator (#12521)

This commit is contained in:
Jason Rasmussen 2024-09-09 16:03:17 -04:00 committed by GitHub
parent 8c3c3357fe
commit d39917a4db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 59 additions and 23 deletions

View file

@ -7928,6 +7928,9 @@
"id": { "id": {
"type": "string" "type": "string"
}, },
"isTrashed": {
"type": "boolean"
},
"reason": { "reason": {
"enum": [ "enum": [
"duplicate", "duplicate",

View file

@ -395,6 +395,7 @@ export type AssetBulkUploadCheckResult = {
action: Action; action: Action;
assetId?: string; assetId?: string;
id: string; id: string;
isTrashed?: boolean;
reason?: Reason; reason?: Reason;
}; };
export type AssetBulkUploadCheckResponseDto = { export type AssetBulkUploadCheckResponseDto = {

View file

@ -26,6 +26,7 @@ export class AssetBulkUploadCheckResult {
action!: AssetUploadAction; action!: AssetUploadAction;
reason?: AssetRejectReason; reason?: AssetRejectReason;
assetId?: string; assetId?: string;
isTrashed?: boolean;
} }
export class AssetBulkUploadCheckResponseDto { export class AssetBulkUploadCheckResponseDto {

View file

@ -493,6 +493,7 @@ LIMIT
-- AssetRepository.getByChecksums -- AssetRepository.getByChecksums
SELECT SELECT
"AssetEntity"."id" AS "AssetEntity_id", "AssetEntity"."id" AS "AssetEntity_id",
"AssetEntity"."deletedAt" AS "AssetEntity_deletedAt",
"AssetEntity"."checksum" AS "AssetEntity_checksum" "AssetEntity"."checksum" AS "AssetEntity_checksum"
FROM FROM
"assets" "AssetEntity" "assets" "AssetEntity"

View file

@ -8,7 +8,7 @@ FROM
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
AND ("asset"."deletedAt" IS NULL) AND ("asset"."deletedAt" IS NULL)
WHERE WHERE
"asset"."ownerId" = $1 "asset"."ownerId" IN ($1)
-- MetadataRepository.getStates -- MetadataRepository.getStates
SELECT DISTINCT SELECT DISTINCT
@ -18,7 +18,7 @@ FROM
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
AND ("asset"."deletedAt" IS NULL) AND ("asset"."deletedAt" IS NULL)
WHERE WHERE
"asset"."ownerId" = $1 "asset"."ownerId" IN ($1)
AND "exif"."country" = $2 AND "exif"."country" = $2
-- MetadataRepository.getCities -- MetadataRepository.getCities
@ -29,7 +29,7 @@ FROM
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
AND ("asset"."deletedAt" IS NULL) AND ("asset"."deletedAt" IS NULL)
WHERE WHERE
"asset"."ownerId" = $1 "asset"."ownerId" IN ($1)
AND "exif"."country" = $2 AND "exif"."country" = $2
AND "exif"."state" = $3 AND "exif"."state" = $3
@ -41,7 +41,7 @@ FROM
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
AND ("asset"."deletedAt" IS NULL) AND ("asset"."deletedAt" IS NULL)
WHERE WHERE
"asset"."ownerId" = $1 "asset"."ownerId" IN ($1)
AND "exif"."model" = $2 AND "exif"."model" = $2
-- MetadataRepository.getCameraModels -- MetadataRepository.getCameraModels
@ -52,5 +52,5 @@ FROM
LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId"
AND ("asset"."deletedAt" IS NULL) AND ("asset"."deletedAt" IS NULL)
WHERE WHERE
"asset"."ownerId" = $1 "asset"."ownerId" IN ($1)
AND "exif"."make" = $2 AND "exif"."make" = $2

View file

@ -338,6 +338,7 @@ export class AssetRepository implements IAssetRepository {
select: { select: {
id: true, id: true,
checksum: true, checksum: true,
deletedAt: true,
}, },
where: { where: {
ownerId, ownerId,

View file

@ -55,7 +55,7 @@ export class MetadataRepository implements IMetadataRepository {
} }
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [[DummyValue.UUID]] })
async getCountries(userIds: string[]): Promise<string[]> { async getCountries(userIds: string[]): Promise<string[]> {
const results = await this.exifRepository const results = await this.exifRepository
.createQueryBuilder('exif') .createQueryBuilder('exif')
@ -68,7 +68,7 @@ export class MetadataRepository implements IMetadataRepository {
return results.map(({ country }) => country).filter((item) => item !== ''); return results.map(({ country }) => country).filter((item) => item !== '');
} }
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
async getStates(userIds: string[], country: string | undefined): Promise<string[]> { async getStates(userIds: string[], country: string | undefined): Promise<string[]> {
const query = this.exifRepository const query = this.exifRepository
.createQueryBuilder('exif') .createQueryBuilder('exif')
@ -86,7 +86,7 @@ export class MetadataRepository implements IMetadataRepository {
return result.map(({ state }) => state).filter((item) => item !== ''); return result.map(({ state }) => state).filter((item) => item !== '');
} }
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, DummyValue.STRING] }) @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] })
async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise<string[]> { async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise<string[]> {
const query = this.exifRepository const query = this.exifRepository
.createQueryBuilder('exif') .createQueryBuilder('exif')
@ -108,7 +108,7 @@ export class MetadataRepository implements IMetadataRepository {
return results.map(({ city }) => city).filter((item) => item !== ''); return results.map(({ city }) => city).filter((item) => item !== '');
} }
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
async getCameraMakes(userIds: string[], model: string | undefined): Promise<string[]> { async getCameraMakes(userIds: string[], model: string | undefined): Promise<string[]> {
const query = this.exifRepository const query = this.exifRepository
.createQueryBuilder('exif') .createQueryBuilder('exif')
@ -125,7 +125,7 @@ export class MetadataRepository implements IMetadataRepository {
return results.map(({ make }) => make).filter((item) => item !== ''); return results.map(({ make }) => make).filter((item) => item !== '');
} }
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] })
async getCameraModels(userIds: string[], make: string | undefined): Promise<string[]> { async getCameraModels(userIds: string[], make: string | undefined): Promise<string[]> {
const query = this.exifRepository const query = this.exifRepository
.createQueryBuilder('exif') .createQueryBuilder('exif')

View file

@ -589,8 +589,20 @@ describe(AssetMediaService.name, () => {
}), }),
).resolves.toEqual({ ).resolves.toEqual({
results: [ results: [
{ id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE }, {
{ id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE }, id: '1',
assetId: 'asset-1',
action: AssetUploadAction.REJECT,
reason: AssetRejectReason.DUPLICATE,
isTrashed: false,
},
{
id: '2',
assetId: 'asset-2',
action: AssetUploadAction.REJECT,
reason: AssetRejectReason.DUPLICATE,
isTrashed: false,
},
], ],
}); });

View file

@ -289,10 +289,10 @@ export class AssetMediaService {
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> { async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum)); const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum));
const results = await this.assetRepository.getByChecksums(auth.user.id, checksums); const results = await this.assetRepository.getByChecksums(auth.user.id, checksums);
const checksumMap: Record<string, string> = {}; const checksumMap: Record<string, { id: string; isTrashed: boolean }> = {};
for (const { id, checksum } of results) { for (const { id, deletedAt, checksum } of results) {
checksumMap[checksum.toString('hex')] = id; checksumMap[checksum.toString('hex')] = { id, isTrashed: !!deletedAt };
} }
return { return {
@ -301,14 +301,13 @@ export class AssetMediaService {
if (duplicate) { if (duplicate) {
return { return {
id, id,
assetId: duplicate,
action: AssetUploadAction.REJECT, action: AssetUploadAction.REJECT,
reason: AssetRejectReason.DUPLICATE, reason: AssetRejectReason.DUPLICATE,
assetId: duplicate.id,
isTrashed: duplicate.isTrashed,
}; };
} }
// TODO mime-check
return { return {
id, id,
action: AssetUploadAction.ACCEPT, action: AssetUploadAction.ACCEPT,

View file

@ -15,6 +15,7 @@
mdiLoading, mdiLoading,
mdiOpenInNew, mdiOpenInNew,
mdiRestart, mdiRestart,
mdiTrashCan,
} from '@mdi/js'; } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@ -29,6 +30,10 @@
uploadAssetsStore.removeItem(uploadAsset.id); uploadAssetsStore.removeItem(uploadAsset.id);
await fileUploadHandler([uploadAsset.file], uploadAsset.albumId); await fileUploadHandler([uploadAsset.file], uploadAsset.albumId);
}; };
const asLink = (asset: UploadAsset) => {
return asset.isTrashed ? `${AppRoute.TRASH}/${asset.assetId}` : `${AppRoute.PHOTOS}/${uploadAsset.assetId}`;
};
</script> </script>
<div <div
@ -45,7 +50,11 @@
{:else if uploadAsset.state === UploadState.ERROR} {:else if uploadAsset.state === UploadState.ERROR}
<Icon path={mdiAlertCircle} size="24" class="text-immich-error" title={$t('error')} /> <Icon path={mdiAlertCircle} size="24" class="text-immich-error" title={$t('error')} />
{:else if uploadAsset.state === UploadState.DUPLICATED} {:else if uploadAsset.state === UploadState.DUPLICATED}
<Icon path={mdiAlertCircle} size="24" class="text-immich-warning" title={$t('asset_skipped')} /> {#if uploadAsset.isTrashed}
<Icon path={mdiTrashCan} size="24" class="text-gray-500" title={$t('asset_skipped_in_trash')} />
{:else}
<Icon path={mdiAlertCircle} size="24" class="text-immich-warning" title={$t('asset_skipped')} />
{/if}
{:else if uploadAsset.state === UploadState.DONE} {:else if uploadAsset.state === UploadState.DONE}
<Icon path={mdiCheckCircle} size="24" class="text-immich-success" title={$t('asset_uploaded')} /> <Icon path={mdiCheckCircle} size="24" class="text-immich-success" title={$t('asset_uploaded')} />
{/if} {/if}
@ -56,7 +65,7 @@
{#if uploadAsset.state === UploadState.DUPLICATED && uploadAsset.assetId} {#if uploadAsset.state === UploadState.DUPLICATED && uploadAsset.assetId}
<div class="flex items-center justify-between gap-1"> <div class="flex items-center justify-between gap-1">
<a <a
href="{AppRoute.PHOTOS}/{uploadAsset.assetId}" href={asLink(uploadAsset)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="" class=""

View file

@ -387,6 +387,7 @@
"asset_offline": "Asset offline", "asset_offline": "Asset offline",
"asset_offline_description": "This asset is offline. Immich can not access its file location. Please ensure the asset is available and then rescan the library.", "asset_offline_description": "This asset is offline. Immich can not access its file location. Please ensure the asset is available and then rescan the library.",
"asset_skipped": "Skipped", "asset_skipped": "Skipped",
"asset_skipped_in_trash": "In trash",
"asset_uploaded": "Uploaded", "asset_uploaded": "Uploaded",
"asset_uploading": "Uploading...", "asset_uploading": "Uploading...",
"assets": "Assets", "assets": "Assets",

View file

@ -10,6 +10,7 @@ export type UploadAsset = {
id: string; id: string;
file: File; file: File;
assetId?: string; assetId?: string;
isTrashed?: boolean;
albumId?: string; albumId?: string;
progress?: number; progress?: number;
state?: UploadState; state?: UploadState;

View file

@ -28,7 +28,9 @@ export const addDummyItems = () => {
uploadAssetsStore.addItem({ id: 'asset-3', file: { name: 'asset3.jpg', size: 123_456 } as File }); uploadAssetsStore.addItem({ id: 'asset-3', file: { name: 'asset3.jpg', size: 123_456 } as File });
uploadAssetsStore.updateItem('asset-3', { state: UploadState.DUPLICATED, assetId: 'asset-2' }); uploadAssetsStore.updateItem('asset-3', { state: UploadState.DUPLICATED, assetId: 'asset-2' });
uploadAssetsStore.addItem({ id: 'asset-4', file: { name: 'asset3.jpg', size: 123_456 } as File }); uploadAssetsStore.addItem({ id: 'asset-4', file: { name: 'asset3.jpg', size: 123_456 } as File });
uploadAssetsStore.updateItem('asset-4', { state: UploadState.DONE }); uploadAssetsStore.updateItem('asset-4', { state: UploadState.DUPLICATED, assetId: 'asset-2', isTrashed: true });
uploadAssetsStore.addItem({ id: 'asset-10', file: { name: 'asset3.jpg', size: 123_456 } as File });
uploadAssetsStore.updateItem('asset-10', { state: UploadState.DONE });
uploadAssetsStore.track('error'); uploadAssetsStore.track('error');
uploadAssetsStore.track('success'); uploadAssetsStore.track('success');
uploadAssetsStore.track('duplicate'); uploadAssetsStore.track('duplicate');
@ -122,7 +124,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
formData.append(key, value); formData.append(key, value);
} }
let responseData: AssetMediaResponseDto | undefined; let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined;
const key = getKey(); const key = getKey();
if (crypto?.subtle?.digest && !key) { if (crypto?.subtle?.digest && !key) {
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') }); uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
@ -138,7 +140,11 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
results: [checkUploadResult], results: [checkUploadResult],
} = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: [{ id: assetFile.name, checksum }] } }); } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: [{ id: assetFile.name, checksum }] } });
if (checkUploadResult.action === Action.Reject && checkUploadResult.assetId) { if (checkUploadResult.action === Action.Reject && checkUploadResult.assetId) {
responseData = { status: AssetMediaStatus.Duplicate, id: checkUploadResult.assetId }; responseData = {
status: AssetMediaStatus.Duplicate,
id: checkUploadResult.assetId,
isTrashed: checkUploadResult.isTrashed,
};
} }
} catch (error) { } catch (error) {
console.error(`Error calculating sha1 file=${assetFile.name})`, error); console.error(`Error calculating sha1 file=${assetFile.name})`, error);
@ -185,6 +191,7 @@ async function fileUploader(assetFile: File, albumId?: string, replaceAssetId?:
uploadAssetsStore.updateItem(deviceAssetId, { uploadAssetsStore.updateItem(deviceAssetId, {
state: responseData.status === AssetMediaStatus.Duplicate ? UploadState.DUPLICATED : UploadState.DONE, state: responseData.status === AssetMediaStatus.Duplicate ? UploadState.DUPLICATED : UploadState.DONE,
assetId: responseData.id, assetId: responseData.id,
isTrashed: responseData.isTrashed,
}); });
if (responseData.status !== AssetMediaStatus.Duplicate) { if (responseData.status !== AssetMediaStatus.Duplicate) {