mirror of
https://github.com/immich-app/immich.git
synced 2025-12-06 01:10:00 +03:00
feat: wip new web editor interface
This commit is contained in:
@@ -17,6 +17,14 @@
|
||||
"add_a_title": "Add a title",
|
||||
"add_birthday": "Add a birthday",
|
||||
"add_endpoint": "Add endpoint",
|
||||
"editor_crop_tool_h2_mirror": "Mirror",
|
||||
"mirror_horizontal": "Horizontal",
|
||||
"mirror_vertical": "Vertical",
|
||||
"rotate_ccw": "CCW 90°",
|
||||
"rotate_cw": "CW 90°",
|
||||
"crop_aspect_ratio_free": "Free",
|
||||
"crop_aspect_ratio_fixed": "Fixed",
|
||||
"crop_aspect_ratio_original": "Original",
|
||||
"add_exclusion_pattern": "Add exclusion pattern",
|
||||
"add_location": "Add location",
|
||||
"add_more_users": "Add more users",
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
import ActivityStatus from './activity-status.svelte';
|
||||
import ActivityViewer from './activity-viewer.svelte';
|
||||
import DetailPanel from './detail-panel.svelte';
|
||||
import CropArea from './editor/crop-tool/crop-area.svelte';
|
||||
import CropArea from './editor/transform-tool/crop-area.svelte';
|
||||
import EditorPanel from './editor/editor-panel.svelte';
|
||||
import ImagePanoramaViewer from './image-panorama-viewer.svelte';
|
||||
import OcrButton from './ocr-button.svelte';
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { transformManager, type CropAspectRatio } from '$lib/managers/edit/transform-manager.svelte';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiBackupRestore, mdiCropFree, mdiRotateLeft, mdiRotateRight, mdiSquareOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import CropPreset from './crop-preset.svelte';
|
||||
|
||||
let rotateHorizontal = $derived([90, 270].includes(transformManager.normalizedRotation));
|
||||
const icon_16_9 = `M200-280q-33 0-56.5-23.5T120-360v-240q0-33 23.5-56.5T200-680h560q33 0 56.5 23.5T840-600v240q0 33-23.5 56.5T760-280H200Zm0-80h560v-240H200v240Zm0 0v-240 240Z`;
|
||||
const icon_4_3 = `M19 5H5c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 12H5V7h14v10z`;
|
||||
const icon_3_2 = `M200-240q-33 0-56.5-23.5T120-320v-320q0-33 23.5-56.5T200-720h560q33 0 56.5 23.5T840-640v320q0 33-23.5 56.5T760-240H200Zm0-80h560v-320H200v320Zm0 0v-320 320Z`;
|
||||
const icon_7_5 = `M200-200q-33 0-56.5-23.5T120-280v-400q0-33 23.5-56.5T200-760h560q33 0 56.5 23.5T840-680v400q0 33-23.5 56.5T760-200H200Zm0-80h560v-400H200v400Zm0 0v-400 400Z`;
|
||||
interface Size {
|
||||
icon: string;
|
||||
name: CropAspectRatio;
|
||||
viewBox: string;
|
||||
rotate?: boolean;
|
||||
}
|
||||
let sizes: Size[] = [
|
||||
{
|
||||
icon: mdiCropFree,
|
||||
name: 'free',
|
||||
viewBox: '0 0 24 24',
|
||||
rotate: false,
|
||||
},
|
||||
{
|
||||
name: '1:1',
|
||||
icon: mdiSquareOutline,
|
||||
viewBox: '0 0 24 24',
|
||||
rotate: false,
|
||||
},
|
||||
{
|
||||
name: '16:9',
|
||||
icon: icon_16_9,
|
||||
viewBox: '50 -700 840 400',
|
||||
},
|
||||
{
|
||||
name: '4:3',
|
||||
icon: icon_4_3,
|
||||
viewBox: '0 0 24 24',
|
||||
},
|
||||
{
|
||||
name: '3:2',
|
||||
icon: icon_3_2,
|
||||
viewBox: '50 -720 840 480',
|
||||
},
|
||||
{
|
||||
name: '7:5',
|
||||
icon: icon_7_5,
|
||||
viewBox: '50 -760 840 560',
|
||||
},
|
||||
{
|
||||
name: '9:16',
|
||||
icon: icon_16_9,
|
||||
viewBox: '50 -700 840 400',
|
||||
rotate: true,
|
||||
},
|
||||
{
|
||||
name: '3:4',
|
||||
icon: icon_4_3,
|
||||
viewBox: '0 0 24 24',
|
||||
rotate: true,
|
||||
},
|
||||
{
|
||||
name: '2:3',
|
||||
icon: icon_3_2,
|
||||
viewBox: '50 -720 840 480',
|
||||
rotate: true,
|
||||
},
|
||||
{
|
||||
name: '5:7',
|
||||
icon: icon_7_5,
|
||||
viewBox: '50 -760 840 560',
|
||||
rotate: true,
|
||||
},
|
||||
{
|
||||
name: 'reset',
|
||||
icon: mdiBackupRestore,
|
||||
viewBox: '0 0 24 24',
|
||||
rotate: false,
|
||||
},
|
||||
];
|
||||
|
||||
let sizesRows = $derived([
|
||||
sizes.filter((s) => s.rotate === false),
|
||||
sizes.filter((s) => s.rotate === undefined),
|
||||
sizes.filter((s) => s.rotate === true),
|
||||
]);
|
||||
|
||||
function selectType(size: CropAspectRatio) {
|
||||
if (size === 'reset') {
|
||||
transformManager.cropAspectRatio = 'free';
|
||||
let cropImageSizeM = transformManager.cropImageSize;
|
||||
let cropImageScaleM = transformManager.cropImageScale;
|
||||
transformManager.region = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: cropImageSizeM[0] * cropImageScaleM - 1,
|
||||
height: cropImageSizeM[1] * cropImageScaleM - 1,
|
||||
};
|
||||
size = 'free';
|
||||
}
|
||||
|
||||
transformManager.setAspectRatio(size);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-3 px-4">
|
||||
<div class="flex h-10 w-full items-center justify-between text-sm">
|
||||
<h2 class="uppercase">{$t('crop')}</h2>
|
||||
</div>
|
||||
{#each sizesRows as sizesRow, index (index)}
|
||||
<ul class="flex-wrap flex-row flex gap-x-6 py-2 justify-evenly">
|
||||
{#each sizesRow as size (size.name)}
|
||||
<CropPreset {size} selectedSize={transformManager.cropAspectRatio} {rotateHorizontal} {selectType} />
|
||||
{/each}
|
||||
</ul>
|
||||
{/each}
|
||||
<div class="flex h-10 w-full items-center justify-between text-sm">
|
||||
<h2 class="uppercase">{$t('editor_crop_tool_h2_rotation')}</h2>
|
||||
</div>
|
||||
<ul class="flex-wrap flex-row flex gap-x-6 gap-y-4 justify-center">
|
||||
<li>
|
||||
<IconButton
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
aria-label={$t('anti_clockwise')}
|
||||
onclick={() => transformManager.rotate(-90)}
|
||||
icon={mdiRotateLeft}
|
||||
/>
|
||||
</li>
|
||||
<li>
|
||||
<IconButton
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
aria-label={$t('clockwise')}
|
||||
onclick={() => transformManager.rotate(90)}
|
||||
icon={mdiRotateRight}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
import { transformManager } from '$lib/managers/edit/transform-manager.svelte';
|
||||
import { Button, Field, HStack, IconButton, Select, type SelectItem } from '@immich/ui';
|
||||
import { mdiCursorMove, mdiFlipHorizontal, mdiFlipVertical, mdiLock, mdiRotateLeft, mdiRotateRight } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let cropOrientation = $state<'landscape' | 'portrait'>(
|
||||
transformManager.cropImageSize[0] >= transformManager.cropImageSize[1] ? 'landscape' : 'portrait',
|
||||
);
|
||||
let cropMode = $state<'free' | 'fixed'>('free');
|
||||
let selectedRatio = $state<SelectItem | undefined>();
|
||||
|
||||
const horizontalRatios = ['4:3', '3:2', '7:5', '16:9'];
|
||||
const verticalRatios = ['3:4', '2:3', '5:7', '9:16'];
|
||||
|
||||
let aspectRatios: SelectItem[] = $derived([
|
||||
{ label: $t('crop_aspect_ratio_original'), value: 'original' },
|
||||
{ label: '1:1', value: '1:1' },
|
||||
...(cropOrientation === 'landscape' ? horizontalRatios : verticalRatios).map((ratio) => ({
|
||||
label: ratio,
|
||||
value: ratio,
|
||||
})),
|
||||
]);
|
||||
|
||||
function resetCrop() {
|
||||
transformManager.resetCrop();
|
||||
selectedRatio = undefined;
|
||||
}
|
||||
|
||||
function selectAspectRatio(ratio: 'original' | 'free' | (typeof aspectRatios)[number]['value']) {
|
||||
if (ratio === 'original') {
|
||||
const [width, height] = transformManager.cropImageSize;
|
||||
ratio = `${width}:${height}`;
|
||||
}
|
||||
|
||||
transformManager.setAspectRatio(ratio);
|
||||
}
|
||||
|
||||
function onAspectRatioSelect(ratio: SelectItem) {
|
||||
selectedRatio = ratio;
|
||||
selectAspectRatio(ratio.value);
|
||||
}
|
||||
|
||||
function setFreeCrop() {
|
||||
cropMode = 'free';
|
||||
selectAspectRatio('free');
|
||||
}
|
||||
|
||||
function setFixedCrop() {
|
||||
cropMode = 'fixed';
|
||||
if (!selectedRatio) {
|
||||
selectedRatio = aspectRatios[0];
|
||||
}
|
||||
selectAspectRatio(selectedRatio.value);
|
||||
}
|
||||
|
||||
function rotateCropOrientation() {
|
||||
const newOrientation = cropOrientation === 'landscape' ? 'portrait' : 'landscape';
|
||||
cropOrientation = newOrientation;
|
||||
|
||||
// convert the selected ratio to the new orientation
|
||||
if (selectedRatio && selectedRatio.value !== 'free' && selectedRatio.value !== 'original') {
|
||||
const [width, height] = selectedRatio.value.split(':');
|
||||
const newRatio = `${height}:${width}`;
|
||||
selectedRatio = aspectRatios.find((ratio) => ratio.value === newRatio);
|
||||
selectAspectRatio(newRatio);
|
||||
}
|
||||
}
|
||||
|
||||
async function rotateImage(degrees: number) {
|
||||
await transformManager.rotate(degrees);
|
||||
rotateCropOrientation();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-3 px-4">
|
||||
<div class="flex h-10 w-full items-center justify-between text-sm">
|
||||
<h2 class="uppercase">{$t('crop')}</h2>
|
||||
</div>
|
||||
<HStack gap={0} class="mb-4">
|
||||
<Button
|
||||
leadingIcon={mdiCursorMove}
|
||||
shape="rectangle"
|
||||
class="rounded-l-md"
|
||||
onclick={setFreeCrop}
|
||||
color={cropMode === 'free' ? 'primary' : 'secondary'}
|
||||
fullWidth
|
||||
>
|
||||
{$t('crop_aspect_ratio_free')}
|
||||
</Button>
|
||||
<Button
|
||||
leadingIcon={mdiLock}
|
||||
shape="rectangle"
|
||||
class="rounded-r-md"
|
||||
color={cropMode === 'fixed' ? 'primary' : 'secondary'}
|
||||
onclick={setFixedCrop}
|
||||
fullWidth
|
||||
>
|
||||
{$t('crop_aspect_ratio_fixed')}
|
||||
</Button>
|
||||
</HStack>
|
||||
<HStack>
|
||||
<Field disabled={cropMode === 'free'}>
|
||||
<Select class="w-full" onChange={onAspectRatioSelect} bind:value={selectedRatio} data={aspectRatios} />
|
||||
</Field>
|
||||
<IconButton
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
icon={mdiRotateRight}
|
||||
aria-label={$t('reset')}
|
||||
onclick={rotateCropOrientation}
|
||||
disabled={cropMode === 'free'}
|
||||
/>
|
||||
</HStack>
|
||||
<div class="flex h-10 w-full items-center justify-between text-sm mt-2">
|
||||
<h2 class="uppercase">{$t('editor_crop_tool_h2_rotation')}</h2>
|
||||
</div>
|
||||
<HStack>
|
||||
<Button fullWidth leadingIcon={mdiRotateLeft} onclick={() => rotateImage(-90)}>{$t('rotate_ccw')}</Button>
|
||||
<Button fullWidth trailingIcon={mdiRotateRight} onclick={() => rotateImage(90)}>{$t('rotate_cw')}</Button>
|
||||
</HStack>
|
||||
<div class="flex h-10 w-full items-center justify-between text-sm mt-2">
|
||||
<h2 class="uppercase">{$t('editor_crop_tool_h2_mirror')}</h2>
|
||||
</div>
|
||||
<HStack>
|
||||
<Button fullWidth leadingIcon={mdiFlipHorizontal} onclick={() => rotateImage(-90)}>{$t('mirror_horizontal')}</Button
|
||||
>
|
||||
<Button fullWidth trailingIcon={mdiFlipVertical} onclick={() => rotateImage(90)}>{$t('mirror_vertical')}</Button>
|
||||
</HStack>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
import CropTool from '$lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte';
|
||||
import TransformTool from '$lib/components/asset-viewer/editor/transform-tool/transform-tool.svelte';
|
||||
import { transformManager } from '$lib/managers/edit/transform-manager.svelte';
|
||||
import { waitForWebsocketEvent } from '$lib/stores/websocket';
|
||||
import { editAsset, removeAssetEdits, type AssetEditsDto, type AssetResponseDto } from '@immich/sdk';
|
||||
@@ -33,7 +33,7 @@ export class EditManager {
|
||||
{
|
||||
type: EditToolType.Transform,
|
||||
icon: mdiCropRotate,
|
||||
component: CropTool,
|
||||
component: TransformTool,
|
||||
manager: transformManager,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -40,7 +40,7 @@ class TransformManager implements EditToolManager {
|
||||
cropFrame = $state<HTMLElement | null>(null);
|
||||
cropImageSize = $state([1000, 1000]);
|
||||
cropImageScale = $state(1);
|
||||
cropAspectRatio = $state('free' as CropAspectRatio);
|
||||
cropAspectRatio = $state("free");
|
||||
region = $state({ x: 0, y: 0, width: 100, height: 100 });
|
||||
|
||||
imageRotation = $state(0);
|
||||
@@ -52,7 +52,7 @@ class TransformManager implements EditToolManager {
|
||||
|
||||
edits = $derived.by(() => this.getEdits());
|
||||
|
||||
setAspectRatio(aspectRatio: CropAspectRatio) {
|
||||
setAspectRatio(aspectRatio: string) {
|
||||
this.cropAspectRatio = aspectRatio;
|
||||
|
||||
if (!this.imgElement || !this.cropAreaEl) {
|
||||
@@ -161,7 +161,7 @@ class TransformManager implements EditToolManager {
|
||||
this.onImageLoad();
|
||||
}
|
||||
|
||||
recalculateCrop(aspectRatio: CropAspectRatio = this.cropAspectRatio): CropSettings {
|
||||
recalculateCrop(aspectRatio: string = this.cropAspectRatio): CropSettings {
|
||||
if (!this.cropAreaEl) {
|
||||
return this.region;
|
||||
}
|
||||
@@ -226,7 +226,7 @@ class TransformManager implements EditToolManager {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
keepAspectRatio(newWidth: number, newHeight: number, aspectRatio: CropAspectRatio = this.cropAspectRatio) {
|
||||
keepAspectRatio(newWidth: number, newHeight: number, aspectRatio: string = this.cropAspectRatio) {
|
||||
const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number);
|
||||
|
||||
if (widthRatio && heightRatio) {
|
||||
@@ -240,7 +240,7 @@ class TransformManager implements EditToolManager {
|
||||
adjustDimensions(
|
||||
newWidth: number,
|
||||
newHeight: number,
|
||||
aspectRatio: CropAspectRatio,
|
||||
aspectRatio: string,
|
||||
xLimit: number,
|
||||
yLimit: number,
|
||||
minSize: number,
|
||||
@@ -924,6 +924,16 @@ class TransformManager implements EditToolManager {
|
||||
|
||||
this.isInteracting = !toDark;
|
||||
}
|
||||
|
||||
resetCrop() {
|
||||
this.cropAspectRatio = 'free';
|
||||
this.region = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: this.cropImageSize[0] * this.cropImageScale - 1,
|
||||
height: this.cropImageSize[1] * this.cropImageScale - 1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const transformManager = new TransformManager();
|
||||
|
||||
Reference in New Issue
Block a user