mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
feat: download original asset (#25302)
Co-authored-by: bwees <brandonwees@gmail.com>
This commit is contained in:
parent
a2b03f7650
commit
07675a2de4
29 changed files with 336 additions and 9 deletions
|
|
@ -928,6 +928,7 @@
|
||||||
"download_include_embedded_motion_videos": "Embedded videos",
|
"download_include_embedded_motion_videos": "Embedded videos",
|
||||||
"download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file",
|
"download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file",
|
||||||
"download_notfound": "Download not found",
|
"download_notfound": "Download not found",
|
||||||
|
"download_original": "Download original",
|
||||||
"download_paused": "Download paused",
|
"download_paused": "Download paused",
|
||||||
"download_settings": "Download",
|
"download_settings": "Download",
|
||||||
"download_settings_description": "Manage settings related to asset download",
|
"download_settings_description": "Manage settings related to asset download",
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'visibility', 'timeline');
|
addDefault(value, 'visibility', 'timeline');
|
||||||
addDefault(value, 'createdAt', DateTime.now().toIso8601String());
|
addDefault(value, 'createdAt', DateTime.now().toIso8601String());
|
||||||
|
addDefault(value, 'isEdited', false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'UserAdminResponseDto':
|
case 'UserAdminResponseDto':
|
||||||
|
|
@ -46,6 +47,10 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
||||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||||
addDefault(value, 'hasProfileImage', false);
|
addDefault(value, 'hasProfileImage', false);
|
||||||
}
|
}
|
||||||
|
case 'SyncAssetV1':
|
||||||
|
if (value is Map) {
|
||||||
|
addDefault(value, 'editCount', 0);
|
||||||
|
}
|
||||||
case 'ServerFeaturesDto':
|
case 'ServerFeaturesDto':
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'ocr', false);
|
addDefault(value, 'ocr', false);
|
||||||
|
|
|
||||||
BIN
mobile/openapi/lib/model/asset_response_dto.dart
generated
BIN
mobile/openapi/lib/model/asset_response_dto.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/sync_asset_v1.dart
generated
BIN
mobile/openapi/lib/model/sync_asset_v1.dart
generated
Binary file not shown.
|
|
@ -44,6 +44,7 @@ SyncAssetV1 _createAsset({
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
stackId: null,
|
stackId: null,
|
||||||
thumbhash: null,
|
thumbhash: null,
|
||||||
|
editCount: 0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
1
mobile/test/fixtures/sync_stream.stub.dart
vendored
1
mobile/test/fixtures/sync_stream.stub.dart
vendored
|
|
@ -128,6 +128,7 @@ abstract final class SyncStreamStub {
|
||||||
visibility: AssetVisibility.timeline,
|
visibility: AssetVisibility.timeline,
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
|
editCount: 0,
|
||||||
),
|
),
|
||||||
ack: ack,
|
ack: ack,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -16276,6 +16276,20 @@
|
||||||
"isArchived": {
|
"isArchived": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"isEdited": {
|
||||||
|
"type": "boolean",
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v2.5.0",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.5.0",
|
||||||
|
"state": "Beta"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-state": "Beta"
|
||||||
|
},
|
||||||
"isFavorite": {
|
"isFavorite": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
|
@ -16408,6 +16422,7 @@
|
||||||
"height",
|
"height",
|
||||||
"id",
|
"id",
|
||||||
"isArchived",
|
"isArchived",
|
||||||
|
"isEdited",
|
||||||
"isFavorite",
|
"isFavorite",
|
||||||
"isOffline",
|
"isOffline",
|
||||||
"isTrashed",
|
"isTrashed",
|
||||||
|
|
@ -21276,6 +21291,9 @@
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"editCount": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"fileCreatedAt": {
|
"fileCreatedAt": {
|
||||||
"format": "date-time",
|
"format": "date-time",
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
|
|
@ -21346,6 +21364,7 @@
|
||||||
"checksum",
|
"checksum",
|
||||||
"deletedAt",
|
"deletedAt",
|
||||||
"duration",
|
"duration",
|
||||||
|
"editCount",
|
||||||
"fileCreatedAt",
|
"fileCreatedAt",
|
||||||
"fileModifiedAt",
|
"fileModifiedAt",
|
||||||
"height",
|
"height",
|
||||||
|
|
|
||||||
|
|
@ -352,6 +352,7 @@ export type AssetResponseDto = {
|
||||||
height: number | null;
|
height: number | null;
|
||||||
id: string;
|
id: string;
|
||||||
isArchived: boolean;
|
isArchived: boolean;
|
||||||
|
isEdited: boolean;
|
||||||
isFavorite: boolean;
|
isFavorite: boolean;
|
||||||
isOffline: boolean;
|
isOffline: boolean;
|
||||||
isTrashed: boolean;
|
isTrashed: boolean;
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,7 @@ export const columns = {
|
||||||
'asset.libraryId',
|
'asset.libraryId',
|
||||||
'asset.width',
|
'asset.width',
|
||||||
'asset.height',
|
'asset.height',
|
||||||
|
'asset.editCount',
|
||||||
],
|
],
|
||||||
syncAlbumUser: ['album_user.albumId as albumId', 'album_user.userId as userId', 'album_user.role'],
|
syncAlbumUser: ['album_user.albumId as albumId', 'album_user.userId as userId', 'album_user.role'],
|
||||||
syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'],
|
syncStack: ['stack.id', 'stack.createdAt', 'stack.updatedAt', 'stack.primaryAssetId', 'stack.ownerId'],
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,8 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||||
|
|
||||||
@Property({ history: new HistoryBuilder().added('v1').deprecated('v1.113.0') })
|
@Property({ history: new HistoryBuilder().added('v1').deprecated('v1.113.0') })
|
||||||
resized?: boolean;
|
resized?: boolean;
|
||||||
|
@Property({ history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0') })
|
||||||
|
isEdited!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MapAsset = {
|
export type MapAsset = {
|
||||||
|
|
@ -137,6 +139,7 @@ export type MapAsset = {
|
||||||
type: AssetType;
|
type: AssetType;
|
||||||
width: number | null;
|
width: number | null;
|
||||||
height: number | null;
|
height: number | null;
|
||||||
|
editCount: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AssetStackResponseDto {
|
export class AssetStackResponseDto {
|
||||||
|
|
@ -245,5 +248,6 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
|
||||||
resized: true,
|
resized: true,
|
||||||
width: entity.width,
|
width: entity.width,
|
||||||
height: entity.height,
|
height: entity.height,
|
||||||
|
isEdited: entity.editCount > 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -121,6 +121,8 @@ export class SyncAssetV1 {
|
||||||
width!: number | null;
|
width!: number | null;
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
height!: number | null;
|
height!: number | null;
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
editCount!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ExtraModel()
|
@ExtraModel()
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ select
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
"asset"."width",
|
"asset"."width",
|
||||||
"asset"."height",
|
"asset"."height",
|
||||||
|
"asset"."editCount",
|
||||||
"album_asset"."updateId"
|
"album_asset"."updateId"
|
||||||
from
|
from
|
||||||
"album_asset" as "album_asset"
|
"album_asset" as "album_asset"
|
||||||
|
|
@ -103,6 +104,7 @@ select
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
"asset"."width",
|
"asset"."width",
|
||||||
"asset"."height",
|
"asset"."height",
|
||||||
|
"asset"."editCount",
|
||||||
"asset"."updateId"
|
"asset"."updateId"
|
||||||
from
|
from
|
||||||
"asset" as "asset"
|
"asset" as "asset"
|
||||||
|
|
@ -140,7 +142,8 @@ select
|
||||||
"asset"."stackId",
|
"asset"."stackId",
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
"asset"."width",
|
"asset"."width",
|
||||||
"asset"."height"
|
"asset"."height",
|
||||||
|
"asset"."editCount"
|
||||||
from
|
from
|
||||||
"album_asset" as "album_asset"
|
"album_asset" as "album_asset"
|
||||||
inner join "asset" on "asset"."id" = "album_asset"."assetId"
|
inner join "asset" on "asset"."id" = "album_asset"."assetId"
|
||||||
|
|
@ -456,6 +459,7 @@ select
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
"asset"."width",
|
"asset"."width",
|
||||||
"asset"."height",
|
"asset"."height",
|
||||||
|
"asset"."editCount",
|
||||||
"asset"."updateId"
|
"asset"."updateId"
|
||||||
from
|
from
|
||||||
"asset" as "asset"
|
"asset" as "asset"
|
||||||
|
|
@ -751,6 +755,7 @@ select
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
"asset"."width",
|
"asset"."width",
|
||||||
"asset"."height",
|
"asset"."height",
|
||||||
|
"asset"."editCount",
|
||||||
"asset"."updateId"
|
"asset"."updateId"
|
||||||
from
|
from
|
||||||
"asset" as "asset"
|
"asset" as "asset"
|
||||||
|
|
@ -802,6 +807,7 @@ select
|
||||||
"asset"."libraryId",
|
"asset"."libraryId",
|
||||||
"asset"."width",
|
"asset"."width",
|
||||||
"asset"."height",
|
"asset"."height",
|
||||||
|
"asset"."editCount",
|
||||||
"asset"."updateId"
|
"asset"."updateId"
|
||||||
from
|
from
|
||||||
"asset" as "asset"
|
"asset" as "asset"
|
||||||
|
|
|
||||||
|
|
@ -255,3 +255,31 @@ export const asset_face_audit = registerFunction({
|
||||||
RETURN NULL;
|
RETURN NULL;
|
||||||
END`,
|
END`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const asset_edit_insert = registerFunction({
|
||||||
|
name: 'asset_edit_insert',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
BEGIN
|
||||||
|
UPDATE asset
|
||||||
|
SET "editCount" = "editCount" + 1
|
||||||
|
WHERE "id" = NEW."assetId";
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const asset_edit_delete = registerFunction({
|
||||||
|
name: 'asset_edit_delete',
|
||||||
|
returnType: 'TRIGGER',
|
||||||
|
language: 'PLPGSQL',
|
||||||
|
body: `
|
||||||
|
BEGIN
|
||||||
|
UPDATE asset
|
||||||
|
SET "editCount" = "editCount" - 1
|
||||||
|
WHERE "id" = OLD."assetId";
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { Kysely, sql } from 'kysely';
|
||||||
|
|
||||||
|
export async function up(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION asset_edit_insert()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE asset
|
||||||
|
SET "editCount" = "editCount" + 1
|
||||||
|
WHERE "id" = NEW."assetId";
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE FUNCTION asset_edit_delete()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE asset
|
||||||
|
SET "editCount" = "editCount" - 1
|
||||||
|
WHERE "id" = OLD."assetId";
|
||||||
|
RETURN NULL;
|
||||||
|
END
|
||||||
|
$$;`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset" ADD "editCount" integer NOT NULL DEFAULT 0;`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "asset_edit_delete"
|
||||||
|
AFTER DELETE ON "asset_edit"
|
||||||
|
REFERENCING OLD TABLE AS "old"
|
||||||
|
FOR EACH ROW
|
||||||
|
WHEN (pg_trigger_depth() = 0)
|
||||||
|
EXECUTE FUNCTION asset_edit_delete();`.execute(db);
|
||||||
|
await sql`CREATE OR REPLACE TRIGGER "asset_edit_insert"
|
||||||
|
AFTER INSERT ON "asset_edit"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION asset_edit_insert();`.execute(db);
|
||||||
|
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_edit_insert', '{"type":"function","name":"asset_edit_insert","sql":"CREATE OR REPLACE FUNCTION asset_edit_insert()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" + 1\\n WHERE \\"id\\" = NEW.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db);
|
||||||
|
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('function_asset_edit_delete', '{"type":"function","name":"asset_edit_delete","sql":"CREATE OR REPLACE FUNCTION asset_edit_delete()\\n RETURNS TRIGGER\\n LANGUAGE PLPGSQL\\n AS $$\\n BEGIN\\n UPDATE asset\\n SET \\"editCount\\" = \\"editCount\\" - 1\\n WHERE \\"id\\" = OLD.\\"assetId\\";\\n RETURN NULL;\\n END\\n $$;"}'::jsonb);`.execute(db);
|
||||||
|
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_edit_delete', '{"type":"trigger","name":"asset_edit_delete","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_delete\\"\\n AFTER DELETE ON \\"asset_edit\\"\\n REFERENCING OLD TABLE AS \\"old\\"\\n FOR EACH ROW\\n WHEN (pg_trigger_depth() = 0)\\n EXECUTE FUNCTION asset_edit_delete();"}'::jsonb);`.execute(db);
|
||||||
|
await sql`INSERT INTO "migration_overrides" ("name", "value") VALUES ('trigger_asset_edit_insert', '{"type":"trigger","name":"asset_edit_insert","sql":"CREATE OR REPLACE TRIGGER \\"asset_edit_insert\\"\\n AFTER INSERT ON \\"asset_edit\\"\\n FOR EACH ROW\\n EXECUTE FUNCTION asset_edit_insert();"}'::jsonb);`.execute(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function down(db: Kysely<any>): Promise<void> {
|
||||||
|
await sql`DROP TRIGGER "asset_edit_delete" ON "asset_edit";`.execute(db);
|
||||||
|
await sql`DROP TRIGGER "asset_edit_insert" ON "asset_edit";`.execute(db);
|
||||||
|
await sql`ALTER TABLE "asset" DROP COLUMN "editCount";`.execute(db);
|
||||||
|
await sql`DROP FUNCTION asset_edit_insert;`.execute(db);
|
||||||
|
await sql`DROP FUNCTION asset_edit_delete;`.execute(db);
|
||||||
|
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_edit_insert';`.execute(db);
|
||||||
|
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'function_asset_edit_delete';`.execute(db);
|
||||||
|
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_edit_delete';`.execute(db);
|
||||||
|
await sql`DELETE FROM "migration_overrides" WHERE "name" = 'trigger_asset_edit_insert';`.execute(db);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,24 @@
|
||||||
import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto';
|
import { AssetEditAction, AssetEditActionParameter } from 'src/dtos/editing.dto';
|
||||||
|
import { asset_edit_delete, asset_edit_insert } from 'src/schema/functions';
|
||||||
import { AssetTable } from 'src/schema/tables/asset.table';
|
import { AssetTable } from 'src/schema/tables/asset.table';
|
||||||
import { Column, ForeignKeyColumn, Generated, PrimaryGeneratedColumn } from 'src/sql-tools';
|
import {
|
||||||
|
AfterDeleteTrigger,
|
||||||
|
AfterInsertTrigger,
|
||||||
|
Column,
|
||||||
|
ForeignKeyColumn,
|
||||||
|
Generated,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Table,
|
||||||
|
} from 'src/sql-tools';
|
||||||
|
|
||||||
|
@Table('asset_edit')
|
||||||
|
@AfterInsertTrigger({ scope: 'row', function: asset_edit_insert })
|
||||||
|
@AfterDeleteTrigger({
|
||||||
|
scope: 'row',
|
||||||
|
function: asset_edit_delete,
|
||||||
|
referencingOldTableAs: 'old',
|
||||||
|
when: 'pg_trigger_depth() = 0',
|
||||||
|
})
|
||||||
export class AssetEditTable<T extends AssetEditAction = AssetEditAction> {
|
export class AssetEditTable<T extends AssetEditAction = AssetEditAction> {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
id!: Generated<string>;
|
id!: Generated<string>;
|
||||||
|
|
|
||||||
|
|
@ -143,4 +143,7 @@ export class AssetTable {
|
||||||
|
|
||||||
@Column({ type: 'integer', nullable: true })
|
@Column({ type: 'integer', nullable: true })
|
||||||
height!: number | null;
|
height!: number | null;
|
||||||
|
|
||||||
|
@Column({ type: 'integer', default: 0 })
|
||||||
|
editCount!: Generated<number>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -153,6 +153,7 @@ export class JobService extends BaseService {
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
width: asset.width,
|
width: asset.width,
|
||||||
height: asset.height,
|
height: asset.height,
|
||||||
|
editCount: asset.editCount,
|
||||||
},
|
},
|
||||||
exif: {
|
exif: {
|
||||||
assetId: exif.assetId,
|
assetId: exif.assetId,
|
||||||
|
|
|
||||||
28
server/test/fixtures/asset.stub.ts
vendored
28
server/test/fixtures/asset.stub.ts
vendored
|
|
@ -86,6 +86,7 @@ export const assetStub = {
|
||||||
make: 'FUJIFILM',
|
make: 'FUJIFILM',
|
||||||
model: 'X-T50',
|
model: 'X-T50',
|
||||||
lensModel: 'XF27mm F2.8 R WR',
|
lensModel: 'XF27mm F2.8 R WR',
|
||||||
|
editCount: 0,
|
||||||
...asset,
|
...asset,
|
||||||
}),
|
}),
|
||||||
noResizePath: Object.freeze({
|
noResizePath: Object.freeze({
|
||||||
|
|
@ -125,6 +126,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
noWebpPath: Object.freeze({
|
noWebpPath: Object.freeze({
|
||||||
|
|
@ -166,6 +168,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
noThumbhash: Object.freeze({
|
noThumbhash: Object.freeze({
|
||||||
|
|
@ -204,6 +207,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
primaryImage: Object.freeze({
|
primaryImage: Object.freeze({
|
||||||
|
|
@ -252,6 +256,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
image: Object.freeze({
|
image: Object.freeze({
|
||||||
|
|
@ -298,6 +303,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
trashed: Object.freeze({
|
trashed: Object.freeze({
|
||||||
|
|
@ -341,6 +347,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
trashedOffline: Object.freeze({
|
trashedOffline: Object.freeze({
|
||||||
|
|
@ -384,6 +391,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
archived: Object.freeze({
|
archived: Object.freeze({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
|
|
@ -426,6 +434,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
external: Object.freeze({
|
external: Object.freeze({
|
||||||
|
|
@ -468,6 +477,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
image1: Object.freeze({
|
image1: Object.freeze({
|
||||||
|
|
@ -510,6 +520,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
imageFrom2015: Object.freeze({
|
imageFrom2015: Object.freeze({
|
||||||
|
|
@ -551,6 +562,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
video: Object.freeze({
|
video: Object.freeze({
|
||||||
|
|
@ -594,6 +606,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
livePhotoMotionAsset: Object.freeze({
|
livePhotoMotionAsset: Object.freeze({
|
||||||
|
|
@ -614,6 +627,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [] as AssetEditActionItem[],
|
edits: [] as AssetEditActionItem[],
|
||||||
|
editCount: 0,
|
||||||
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif; edits: AssetEditActionItem[] }),
|
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; exifInfo: Exif; edits: AssetEditActionItem[] }),
|
||||||
|
|
||||||
livePhotoStillAsset: Object.freeze({
|
livePhotoStillAsset: Object.freeze({
|
||||||
|
|
@ -635,6 +649,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [] as AssetEditActionItem[],
|
edits: [] as AssetEditActionItem[],
|
||||||
|
editCount: 0,
|
||||||
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
|
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
|
||||||
|
|
||||||
livePhotoWithOriginalFileName: Object.freeze({
|
livePhotoWithOriginalFileName: Object.freeze({
|
||||||
|
|
@ -658,6 +673,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [] as AssetEditActionItem[],
|
edits: [] as AssetEditActionItem[],
|
||||||
|
editCount: 0,
|
||||||
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
|
} as MapAsset & { faces: AssetFace[]; files: AssetFile[]; edits: AssetEditActionItem[] }),
|
||||||
|
|
||||||
withLocation: Object.freeze({
|
withLocation: Object.freeze({
|
||||||
|
|
@ -705,6 +721,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sidecar: Object.freeze({
|
sidecar: Object.freeze({
|
||||||
|
|
@ -743,6 +760,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
sidecarWithoutExt: Object.freeze({
|
sidecarWithoutExt: Object.freeze({
|
||||||
|
|
@ -778,6 +796,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hasEncodedVideo: Object.freeze({
|
hasEncodedVideo: Object.freeze({
|
||||||
|
|
@ -820,6 +839,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
hasFileExtension: Object.freeze({
|
hasFileExtension: Object.freeze({
|
||||||
|
|
@ -859,6 +879,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
imageDng: Object.freeze({
|
imageDng: Object.freeze({
|
||||||
|
|
@ -902,6 +923,7 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
imageHif: Object.freeze({
|
imageHif: Object.freeze({
|
||||||
|
|
@ -945,7 +967,9 @@ export const assetStub = {
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
panoramaTif: Object.freeze({
|
panoramaTif: Object.freeze({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
status: AssetStatus.Active,
|
status: AssetStatus.Active,
|
||||||
|
|
@ -988,6 +1012,7 @@ export const assetStub = {
|
||||||
height: null,
|
height: null,
|
||||||
edits: [],
|
edits: [],
|
||||||
}),
|
}),
|
||||||
|
|
||||||
withCropEdit: Object.freeze({
|
withCropEdit: Object.freeze({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
status: AssetStatus.Active,
|
status: AssetStatus.Active,
|
||||||
|
|
@ -1043,7 +1068,9 @@ export const assetStub = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
] as AssetEditActionItem[],
|
] as AssetEditActionItem[],
|
||||||
|
editCount: 1,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
withoutEdits: Object.freeze({
|
withoutEdits: Object.freeze({
|
||||||
id: 'asset-id',
|
id: 'asset-id',
|
||||||
status: AssetStatus.Active,
|
status: AssetStatus.Active,
|
||||||
|
|
@ -1089,5 +1116,6 @@ export const assetStub = {
|
||||||
width: 2160,
|
width: 2160,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
edits: [],
|
edits: [],
|
||||||
|
editCount: 0,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
1
server/test/fixtures/shared-link.stub.ts
vendored
1
server/test/fixtures/shared-link.stub.ts
vendored
|
|
@ -159,6 +159,7 @@ export const sharedLinkStub = {
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
width: 500,
|
width: 500,
|
||||||
height: 500,
|
height: 500,
|
||||||
|
editCount: 0,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
albumId: null,
|
albumId: null,
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { AccessRepository } from 'src/repositories/access.repository';
|
||||||
import { ActivityRepository } from 'src/repositories/activity.repository';
|
import { ActivityRepository } from 'src/repositories/activity.repository';
|
||||||
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
|
||||||
import { AlbumRepository } from 'src/repositories/album.repository';
|
import { AlbumRepository } from 'src/repositories/album.repository';
|
||||||
|
import { AssetEditRepository } from 'src/repositories/asset-edit.repository';
|
||||||
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
||||||
import { AssetRepository } from 'src/repositories/asset.repository';
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
|
|
@ -384,6 +385,7 @@ const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
|
||||||
case AlbumUserRepository:
|
case AlbumUserRepository:
|
||||||
case ActivityRepository:
|
case ActivityRepository:
|
||||||
case AssetRepository:
|
case AssetRepository:
|
||||||
|
case AssetEditRepository:
|
||||||
case AssetJobRepository:
|
case AssetJobRepository:
|
||||||
case MemoryRepository:
|
case MemoryRepository:
|
||||||
case NotificationRepository:
|
case NotificationRepository:
|
||||||
|
|
@ -535,6 +537,7 @@ const assetInsert = (asset: Partial<Insertable<AssetTable>> = {}) => {
|
||||||
fileModifiedAt: now,
|
fileModifiedAt: now,
|
||||||
localDateTime: now,
|
localDateTime: now,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
|
editCount: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { AssetEditAction, MirrorAxis } from 'src/dtos/editing.dto';
|
||||||
|
import { AssetEditRepository } from 'src/repositories/asset-edit.repository';
|
||||||
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { DB } from 'src/schema';
|
||||||
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
import { newMediumService } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const setup = (db?: Kysely<DB>) => {
|
||||||
|
const { ctx } = newMediumService(BaseService, {
|
||||||
|
database: db || defaultDatabase,
|
||||||
|
real: [],
|
||||||
|
mock: [LoggingRepository],
|
||||||
|
});
|
||||||
|
return { ctx, sut: ctx.get(AssetEditRepository) };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(AssetEditRepository.name, () => {
|
||||||
|
describe('replaceAll', () => {
|
||||||
|
it('should increment editCount on insert', async () => {
|
||||||
|
const { ctx, sut } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
|
||||||
|
).resolves.toEqual({ editCount: 0 });
|
||||||
|
|
||||||
|
await sut.replaceAll(asset.id, [
|
||||||
|
{ action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
|
||||||
|
).resolves.toEqual({ editCount: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should increment editCount when inserting multiple edits', async () => {
|
||||||
|
const { ctx, sut } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
|
||||||
|
).resolves.toEqual({ editCount: 0 });
|
||||||
|
|
||||||
|
await sut.replaceAll(asset.id, [
|
||||||
|
{ action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } },
|
||||||
|
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
|
||||||
|
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
|
||||||
|
).resolves.toEqual({ editCount: 3 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decrement editCount', async () => {
|
||||||
|
const { ctx, sut } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
|
||||||
|
).resolves.toEqual({ editCount: 0 });
|
||||||
|
|
||||||
|
await sut.replaceAll(asset.id, [
|
||||||
|
{ action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } },
|
||||||
|
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
|
||||||
|
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.replaceAll(asset.id, [
|
||||||
|
{ action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
|
||||||
|
).resolves.toEqual({ editCount: 1 });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set editCount to 0 if all edits are deleted', async () => {
|
||||||
|
const { ctx, sut } = setup();
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
const { asset } = await ctx.newAsset({ ownerId: user.id });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
|
||||||
|
).resolves.toEqual({ editCount: 0 });
|
||||||
|
|
||||||
|
await sut.replaceAll(asset.id, [
|
||||||
|
{ action: AssetEditAction.Crop, parameters: { height: 1, width: 1, x: 1, y: 1 } },
|
||||||
|
{ action: AssetEditAction.Mirror, parameters: { axis: MirrorAxis.Horizontal } },
|
||||||
|
{ action: AssetEditAction.Rotate, parameters: { angle: 90 } },
|
||||||
|
]);
|
||||||
|
|
||||||
|
await sut.replaceAll(asset.id, []);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
ctx.database.selectFrom('asset').select('editCount').where('id', '=', asset.id).executeTakeFirstOrThrow(),
|
||||||
|
).resolves.toEqual({ editCount: 0 });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -83,6 +83,7 @@ describe(SyncRequestType.AlbumAssetsV1, () => {
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
width: asset.width,
|
width: asset.width,
|
||||||
height: asset.height,
|
height: asset.height,
|
||||||
|
editCount: asset.editCount,
|
||||||
},
|
},
|
||||||
type: SyncEntityType.AlbumAssetCreateV1,
|
type: SyncEntityType.AlbumAssetCreateV1,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,7 @@ describe(SyncEntityType.AssetV1, () => {
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
width: asset.width,
|
width: asset.width,
|
||||||
height: asset.height,
|
height: asset.height,
|
||||||
|
editCount: asset.editCount,
|
||||||
},
|
},
|
||||||
type: 'AssetV1',
|
type: 'AssetV1',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ describe(SyncRequestType.PartnerAssetsV1, () => {
|
||||||
type: asset.type,
|
type: asset.type,
|
||||||
visibility: asset.visibility,
|
visibility: asset.visibility,
|
||||||
duration: asset.duration,
|
duration: asset.duration,
|
||||||
|
editCount: asset.editCount,
|
||||||
stackId: null,
|
stackId: null,
|
||||||
livePhotoVideoId: null,
|
livePhotoVideoId: null,
|
||||||
libraryId: asset.libraryId,
|
libraryId: asset.libraryId,
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,7 @@ const assetFactory = (asset: Partial<MapAsset> = {}) => ({
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
|
editCount: 0,
|
||||||
...asset,
|
...asset,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -112,8 +112,18 @@
|
||||||
|
|
||||||
const { Cast } = $derived(getGlobalActions($t));
|
const { Cast } = $derived(getGlobalActions($t));
|
||||||
|
|
||||||
const { Share, Download, SharedLinkDownload, Offline, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto, Info } =
|
const {
|
||||||
$derived(getAssetActions($t, asset));
|
Share,
|
||||||
|
Download,
|
||||||
|
DownloadOriginal,
|
||||||
|
SharedLinkDownload,
|
||||||
|
Offline,
|
||||||
|
Favorite,
|
||||||
|
Unfavorite,
|
||||||
|
PlayMotionPhoto,
|
||||||
|
StopMotionPhoto,
|
||||||
|
Info,
|
||||||
|
} = $derived(getAssetActions($t, asset));
|
||||||
const sharedLink = getSharedLink();
|
const sharedLink = getSharedLink();
|
||||||
|
|
||||||
// TODO: Enable when edits are ready for release
|
// TODO: Enable when edits are ready for release
|
||||||
|
|
@ -195,6 +205,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ActionMenuItem action={Download} />
|
<ActionMenuItem action={Download} />
|
||||||
|
<ActionMenuItem action={DownloadOriginal} />
|
||||||
|
|
||||||
{#if !isLocked}
|
{#if !isLocked}
|
||||||
{#if asset.isTrashed}
|
{#if asset.isTrashed}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
if (assets.length === 1) {
|
if (assets.length === 1) {
|
||||||
clearSelect();
|
clearSelect();
|
||||||
let asset = await getAssetInfo({ ...authManager.params, id: assets[0].id });
|
let asset = await getAssetInfo({ ...authManager.params, id: assets[0].id });
|
||||||
await handleDownloadAsset(asset);
|
await handleDownloadAsset(asset, { edited: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiAlertOutline,
|
mdiAlertOutline,
|
||||||
mdiDownload,
|
mdiDownload,
|
||||||
|
mdiDownloadBox,
|
||||||
mdiHeart,
|
mdiHeart,
|
||||||
mdiHeartOutline,
|
mdiHeartOutline,
|
||||||
mdiInformationOutline,
|
mdiInformationOutline,
|
||||||
|
|
@ -51,7 +52,15 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||||
shortcuts: { key: 'd', shift: true },
|
shortcuts: { key: 'd', shift: true },
|
||||||
type: $t('assets'),
|
type: $t('assets'),
|
||||||
$if: () => !!currentAuthUser,
|
$if: () => !!currentAuthUser,
|
||||||
onAction: () => handleDownloadAsset(asset),
|
onAction: () => handleDownloadAsset(asset, { edited: true }),
|
||||||
|
};
|
||||||
|
|
||||||
|
const DownloadOriginal: ActionItem = {
|
||||||
|
title: $t('download_original'),
|
||||||
|
icon: mdiDownloadBox,
|
||||||
|
type: $t('assets'),
|
||||||
|
$if: () => !!currentAuthUser && asset.isEdited,
|
||||||
|
onAction: () => handleDownloadAsset(asset, { edited: false }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const SharedLinkDownload: ActionItem = {
|
const SharedLinkDownload: ActionItem = {
|
||||||
|
|
@ -115,10 +124,21 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||||
shortcuts: [{ key: 'i' }],
|
shortcuts: [{ key: 'i' }],
|
||||||
};
|
};
|
||||||
|
|
||||||
return { Share, Download, SharedLinkDownload, Offline, Info, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto };
|
return {
|
||||||
|
Share,
|
||||||
|
Download,
|
||||||
|
DownloadOriginal,
|
||||||
|
SharedLinkDownload,
|
||||||
|
Offline,
|
||||||
|
Info,
|
||||||
|
Favorite,
|
||||||
|
Unfavorite,
|
||||||
|
PlayMotionPhoto,
|
||||||
|
StopMotionPhoto,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleDownloadAsset = async (asset: AssetResponseDto) => {
|
export const handleDownloadAsset = async (asset: AssetResponseDto, { edited }: { edited: boolean }) => {
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
const assets = [
|
const assets = [
|
||||||
|
|
@ -154,7 +174,12 @@ export const handleDownloadAsset = async (asset: AssetResponseDto) => {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
toastManager.success($t('downloading_asset_filename', { values: { filename: asset.originalFileName } }));
|
toastManager.success($t('downloading_asset_filename', { values: { filename: asset.originalFileName } }));
|
||||||
downloadUrl(getBaseUrl() + `/assets/${id}/original` + (queryParams ? `?${queryParams}` : ''), filename);
|
downloadUrl(
|
||||||
|
getBaseUrl() +
|
||||||
|
`/assets/${id}/original` +
|
||||||
|
(queryParams ? `?${queryParams}&edited=${edited}` : `?edited=${edited}`),
|
||||||
|
filename,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.error_downloading', { values: { filename } }));
|
handleError(error, $t('errors.error_downloading', { values: { filename } }));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export const assetFactory = Sync.makeFactory<AssetResponseDto>({
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
width: faker.number.int({ min: 100, max: 1000 }),
|
width: faker.number.int({ min: 100, max: 1000 }),
|
||||||
height: faker.number.int({ min: 100, max: 1000 }),
|
height: faker.number.int({ min: 100, max: 1000 }),
|
||||||
|
isEdited: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({
|
export const timelineAssetFactory = Sync.makeFactory<TimelineAsset>({
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue