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:
parent
8c3c3357fe
commit
d39917a4db
14 changed files with 59 additions and 23 deletions
Binary file not shown.
|
@ -7928,6 +7928,9 @@
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"isTrashed": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"reason": {
|
"reason": {
|
||||||
"enum": [
|
"enum": [
|
||||||
"duplicate",
|
"duplicate",
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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=""
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue