mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b68c442356 | |||
| 82c6302549 | |||
| aae64b5e2f | |||
| 18bf96b4b2 | |||
| 84f2956941 | |||
| 6044b41648 | |||
| b4e16efdf4 | |||
| 19da655390 | |||
| a1839b3676 | |||
| 7461479f60 | |||
| 01050a3d54 |
@@ -2,6 +2,7 @@
|
|||||||
"name": "Immich - Backend, Frontend and ML",
|
"name": "Immich - Backend, Frontend and ML",
|
||||||
"service": "immich-server",
|
"service": "immich-server",
|
||||||
"runServices": [
|
"runServices": [
|
||||||
|
"immich-init",
|
||||||
"immich-server",
|
"immich-server",
|
||||||
"redis",
|
"redis",
|
||||||
"database",
|
"database",
|
||||||
@@ -31,29 +32,8 @@
|
|||||||
"tasks": {
|
"tasks": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
|
||||||
"label": "Fix Permissions, Install Dependencies",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "[ -f /immich-devcontainer/container-start.sh ] && /immich-devcontainer/container-start.sh || exit 0",
|
|
||||||
"isBackground": true,
|
|
||||||
"presentation": {
|
|
||||||
"echo": true,
|
|
||||||
"reveal": "always",
|
|
||||||
"focus": false,
|
|
||||||
"panel": "dedicated",
|
|
||||||
"showReuseMessage": true,
|
|
||||||
"clear": false,
|
|
||||||
"group": "Devcontainer tasks",
|
|
||||||
"close": true
|
|
||||||
},
|
|
||||||
"runOptions": {
|
|
||||||
"runOn": "default"
|
|
||||||
},
|
|
||||||
"problemMatcher": []
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "Immich API Server (Nest)",
|
"label": "Immich API Server (Nest)",
|
||||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
"command": "[ -f /immich-devcontainer/container-start-backend.sh ] && /immich-devcontainer/container-start-backend.sh || exit 0",
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
@@ -74,7 +54,6 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Immich Web Server (Vite)",
|
"label": "Immich Web Server (Vite)",
|
||||||
"dependsOn": ["Fix Permissions, Install Dependencies"],
|
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
"command": "[ -f /immich-devcontainer/container-start-frontend.sh ] && /immich-devcontainer/container-start-frontend.sh || exit 0",
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
@@ -130,8 +109,8 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"overrideCommand": true,
|
"overrideCommand": true,
|
||||||
"workspaceFolder": "/workspaces/immich",
|
"workspaceFolder": "/usr/src/app",
|
||||||
"remoteUser": "node",
|
"remoteUser": "root",
|
||||||
"userEnvProbe": "loginInteractiveShell",
|
"userEnvProbe": "loginInteractiveShell",
|
||||||
"remoteEnv": {
|
"remoteEnv": {
|
||||||
// The location where your uploaded files are stored
|
// The location where your uploaded files are stored
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
services:
|
services:
|
||||||
|
immich-app-base:
|
||||||
|
image: busybox
|
||||||
immich-server:
|
immich-server:
|
||||||
|
extends:
|
||||||
|
service: immich-app-base
|
||||||
|
profiles: !reset []
|
||||||
|
image: immich-server-dev:latest
|
||||||
build:
|
build:
|
||||||
target: dev-container-mobile
|
target: dev-container-mobile
|
||||||
environment:
|
environment:
|
||||||
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||||
volumes: !override # bind mount host to /workspaces/immich
|
volumes:
|
||||||
- ..:/workspaces/immich
|
|
||||||
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||||
- pnpm-store:/usr/src/app/.pnpm-store
|
|
||||||
- server-node_modules:/usr/src/app/server/node_modules
|
|
||||||
- web-node_modules:/usr/src/app/web/node_modules
|
|
||||||
- github-node_modules:/usr/src/app/.github/node_modules
|
|
||||||
- cli-node_modules:/usr/src/app/cli/node_modules
|
|
||||||
- docs-node_modules:/usr/src/app/docs/node_modules
|
|
||||||
- e2e-node_modules:/usr/src/app/e2e/node_modules
|
|
||||||
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
|
|
||||||
- app-node_modules:/usr/src/app/node_modules
|
|
||||||
- sveltekit:/usr/src/app/web/.svelte-kit
|
|
||||||
- coverage:/usr/src/app/web/coverage
|
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
immich-web:
|
immich-web:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"name": "Immich - Mobile",
|
"name": "Immich - Mobile",
|
||||||
"service": "immich-server",
|
"service": "immich-server",
|
||||||
"runServices": [
|
"runServices": [
|
||||||
|
"immich-init",
|
||||||
"immich-server",
|
"immich-server",
|
||||||
"redis",
|
"redis",
|
||||||
"database",
|
"database",
|
||||||
@@ -35,7 +36,7 @@
|
|||||||
},
|
},
|
||||||
"forwardPorts": [],
|
"forwardPorts": [],
|
||||||
"overrideCommand": true,
|
"overrideCommand": true,
|
||||||
"workspaceFolder": "/workspaces/immich",
|
"workspaceFolder": "/usr/src/app",
|
||||||
"remoteUser": "node",
|
"remoteUser": "node",
|
||||||
"userEnvProbe": "loginInteractiveShell",
|
"userEnvProbe": "loginInteractiveShell",
|
||||||
"remoteEnv": {
|
"remoteEnv": {
|
||||||
|
|||||||
@@ -2,11 +2,6 @@
|
|||||||
export IMMICH_PORT="${DEV_SERVER_PORT:-2283}"
|
export IMMICH_PORT="${DEV_SERVER_PORT:-2283}"
|
||||||
export DEV_PORT="${DEV_PORT:-3000}"
|
export DEV_PORT="${DEV_PORT:-3000}"
|
||||||
|
|
||||||
# search for immich directory inside workspace.
|
|
||||||
# /workspaces/immich is the bind mount, but other directories can be mounted if runing
|
|
||||||
# Devcontainer: Clone [repository|pull request] in container volumne
|
|
||||||
WORKSPACES_DIR="/workspaces"
|
|
||||||
IMMICH_DIR="$WORKSPACES_DIR/immich"
|
|
||||||
IMMICH_DEVCONTAINER_LOG="$HOME/immich-devcontainer.log"
|
IMMICH_DEVCONTAINER_LOG="$HOME/immich-devcontainer.log"
|
||||||
|
|
||||||
log() {
|
log() {
|
||||||
@@ -30,52 +25,8 @@ run_cmd() {
|
|||||||
return "${PIPESTATUS[0]}"
|
return "${PIPESTATUS[0]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find directories excluding /workspaces/immich
|
export IMMICH_WORKSPACE="/usr/src/app"
|
||||||
mapfile -t other_dirs < <(find "$WORKSPACES_DIR" -mindepth 1 -maxdepth 1 -type d ! -path "$IMMICH_DIR" ! -name ".*")
|
|
||||||
|
|
||||||
if [ ${#other_dirs[@]} -gt 1 ]; then
|
|
||||||
log "Error: More than one directory found in $WORKSPACES_DIR other than $IMMICH_DIR."
|
|
||||||
exit 1
|
|
||||||
elif [ ${#other_dirs[@]} -eq 1 ]; then
|
|
||||||
export IMMICH_WORKSPACE="${other_dirs[0]}"
|
|
||||||
else
|
|
||||||
export IMMICH_WORKSPACE="$IMMICH_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Found immich workspace in $IMMICH_WORKSPACE"
|
log "Found immich workspace in $IMMICH_WORKSPACE"
|
||||||
log ""
|
log ""
|
||||||
|
|
||||||
fix_permissions() {
|
|
||||||
|
|
||||||
log "Fixing permissions for ${IMMICH_WORKSPACE}"
|
|
||||||
|
|
||||||
# Change ownership for directories that exist
|
|
||||||
for dir in "${IMMICH_WORKSPACE}/.vscode" \
|
|
||||||
"${IMMICH_WORKSPACE}/server/upload" \
|
|
||||||
"${IMMICH_WORKSPACE}/.pnpm-store" \
|
|
||||||
"${IMMICH_WORKSPACE}/.github/node_modules" \
|
|
||||||
"${IMMICH_WORKSPACE}/cli/node_modules" \
|
|
||||||
"${IMMICH_WORKSPACE}/e2e/node_modules" \
|
|
||||||
"${IMMICH_WORKSPACE}/open-api/typescript-sdk/node_modules" \
|
|
||||||
"${IMMICH_WORKSPACE}/server/node_modules" \
|
|
||||||
"${IMMICH_WORKSPACE}/server/dist" \
|
|
||||||
"${IMMICH_WORKSPACE}/web/node_modules" \
|
|
||||||
"${IMMICH_WORKSPACE}/web/dist"; do
|
|
||||||
if [ -d "$dir" ]; then
|
|
||||||
run_cmd sudo chown node -R "$dir"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
log ""
|
|
||||||
}
|
|
||||||
|
|
||||||
install_dependencies() {
|
|
||||||
|
|
||||||
log "Installing dependencies"
|
|
||||||
(
|
|
||||||
cd "${IMMICH_WORKSPACE}" || exit 1
|
|
||||||
export CI=1 FROZEN=1 OFFLINE=1
|
|
||||||
run_cmd make setup-web-dev setup-server-dev
|
|
||||||
)
|
|
||||||
log ""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
services:
|
services:
|
||||||
|
immich-app-base:
|
||||||
|
image: busybox
|
||||||
immich-server:
|
immich-server:
|
||||||
|
extends:
|
||||||
|
service: immich-app-base
|
||||||
|
profiles: !reset []
|
||||||
|
image: immich-server-dev:latest
|
||||||
build:
|
build:
|
||||||
target: dev-container-server
|
target: dev-container-server
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
hostname: immich-dev
|
hostname: immich-dev
|
||||||
environment:
|
environment:
|
||||||
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
- IMMICH_SERVER_URL=http://127.0.0.1:2283/
|
||||||
volumes: !override
|
volumes:
|
||||||
- ..:/workspaces/immich
|
|
||||||
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
- ${UPLOAD_LOCATION:-upload-devcontainer-volume}${UPLOAD_LOCATION:+/photos}:/data
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
- pnpm-store:/usr/src/app/.pnpm-store
|
- pnpm_store_server:/buildcache/pnpm-store
|
||||||
- server-node_modules:/usr/src/app/server/node_modules
|
|
||||||
- web-node_modules:/usr/src/app/web/node_modules
|
|
||||||
- github-node_modules:/usr/src/app/.github/node_modules
|
|
||||||
- cli-node_modules:/usr/src/app/cli/node_modules
|
|
||||||
- docs-node_modules:/usr/src/app/docs/node_modules
|
|
||||||
- e2e-node_modules:/usr/src/app/e2e/node_modules
|
|
||||||
- sdk-node_modules:/usr/src/app/open-api/typescript-sdk/node_modules
|
|
||||||
- app-node_modules:/usr/src/app/node_modules
|
|
||||||
- sveltekit:/usr/src/app/web/.svelte-kit
|
|
||||||
- coverage:/usr/src/app/web/coverage
|
|
||||||
- ../plugins:/build/corePlugin
|
- ../plugins:/build/corePlugin
|
||||||
immich-web:
|
immich-web:
|
||||||
env_file: !reset []
|
env_file: !reset []
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# shellcheck source=common.sh
|
|
||||||
# shellcheck disable=SC1091
|
|
||||||
source /immich-devcontainer/container-common.sh
|
|
||||||
|
|
||||||
log "Setting up Immich dev container..."
|
|
||||||
fix_permissions
|
|
||||||
|
|
||||||
log "Setup complete, please wait while backend and frontend services automatically start"
|
|
||||||
log
|
|
||||||
log "If necessary, the services may be manually started using"
|
|
||||||
log
|
|
||||||
log "$ /immich-devcontainer/container-start-backend.sh"
|
|
||||||
log "$ /immich-devcontainer/container-start-frontend.sh"
|
|
||||||
log
|
|
||||||
log "From different terminal windows, as these scripts automatically restart the server"
|
|
||||||
log "on error, and will continuously run in a loop"
|
|
||||||
+11
-5
@@ -4,12 +4,18 @@ module.exports = {
|
|||||||
if (!pkg.name) {
|
if (!pkg.name) {
|
||||||
return pkg;
|
return pkg;
|
||||||
}
|
}
|
||||||
|
// make exiftool-vendored.pl a regular dependency since Docker prod
|
||||||
|
// images build with --no-optional to reduce image size
|
||||||
if (pkg.name === "exiftool-vendored") {
|
if (pkg.name === "exiftool-vendored") {
|
||||||
if (pkg.optionalDependencies["exiftool-vendored.pl"]) {
|
const binaryPackage =
|
||||||
// make exiftool-vendored.pl a regular dependency
|
process.platform === "win32"
|
||||||
pkg.dependencies["exiftool-vendored.pl"] =
|
? "exiftool-vendored.exe"
|
||||||
pkg.optionalDependencies["exiftool-vendored.pl"];
|
: "exiftool-vendored.pl";
|
||||||
delete pkg.optionalDependencies["exiftool-vendored.pl"];
|
|
||||||
|
if (pkg.optionalDependencies[binaryPackage]) {
|
||||||
|
pkg.dependencies[binaryPackage] =
|
||||||
|
pkg.optionalDependencies[binaryPackage];
|
||||||
|
delete pkg.optionalDependencies[binaryPackage];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return pkg;
|
return pkg;
|
||||||
|
|||||||
@@ -7,7 +7,15 @@ import { describe, expect, it, MockedFunction, vi } from 'vitest';
|
|||||||
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
|
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
|
||||||
import createFetchMock from 'vitest-fetch-mock';
|
import createFetchMock from 'vitest-fetch-mock';
|
||||||
|
|
||||||
import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset';
|
import {
|
||||||
|
checkForDuplicates,
|
||||||
|
deleteFiles,
|
||||||
|
findSidecar,
|
||||||
|
getAlbumName,
|
||||||
|
startWatch,
|
||||||
|
uploadFiles,
|
||||||
|
UploadOptionsDto,
|
||||||
|
} from 'src/commands/asset';
|
||||||
|
|
||||||
vi.mock('@immich/sdk');
|
vi.mock('@immich/sdk');
|
||||||
|
|
||||||
@@ -309,3 +317,85 @@ describe('startWatch', () => {
|
|||||||
await fs.promises.rm(testFolder, { recursive: true, force: true });
|
await fs.promises.rm(testFolder, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('findSidecar', () => {
|
||||||
|
let testDir: string;
|
||||||
|
let testFilePath: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-sidecar-'));
|
||||||
|
testFilePath = path.join(testDir, 'test.jpg');
|
||||||
|
fs.writeFileSync(testFilePath, 'test');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(testDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find sidecar file with photo.xmp naming convention', () => {
|
||||||
|
const sidecarPath = path.join(testDir, 'test.xmp');
|
||||||
|
fs.writeFileSync(sidecarPath, 'xmp data');
|
||||||
|
|
||||||
|
const result = findSidecar(testFilePath);
|
||||||
|
expect(result).toBe(sidecarPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find sidecar file with photo.ext.xmp naming convention', () => {
|
||||||
|
const sidecarPath = path.join(testDir, 'test.jpg.xmp');
|
||||||
|
fs.writeFileSync(sidecarPath, 'xmp data');
|
||||||
|
|
||||||
|
const result = findSidecar(testFilePath);
|
||||||
|
expect(result).toBe(sidecarPath);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prefer photo.ext.xmp over photo.xmp when both exist', () => {
|
||||||
|
const sidecarPath1 = path.join(testDir, 'test.xmp');
|
||||||
|
const sidecarPath2 = path.join(testDir, 'test.jpg.xmp');
|
||||||
|
fs.writeFileSync(sidecarPath1, 'xmp data 1');
|
||||||
|
fs.writeFileSync(sidecarPath2, 'xmp data 2');
|
||||||
|
|
||||||
|
const result = findSidecar(testFilePath);
|
||||||
|
// Should return the first one found (photo.xmp) based on the order in the code
|
||||||
|
expect(result).toBe(sidecarPath1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined when no sidecar file exists', () => {
|
||||||
|
const result = findSidecar(testFilePath);
|
||||||
|
expect(result).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteFiles', () => {
|
||||||
|
let testDir: string;
|
||||||
|
let testFilePath: string;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-delete-'));
|
||||||
|
testFilePath = path.join(testDir, 'test.jpg');
|
||||||
|
fs.writeFileSync(testFilePath, 'test');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
fs.rmSync(testDir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete asset and sidecar file when main file is deleted', async () => {
|
||||||
|
const sidecarPath = path.join(testDir, 'test.xmp');
|
||||||
|
fs.writeFileSync(sidecarPath, 'xmp data');
|
||||||
|
|
||||||
|
await deleteFiles([{ id: 'test-id', filepath: testFilePath }], [], { delete: true, concurrency: 1 });
|
||||||
|
|
||||||
|
expect(fs.existsSync(testFilePath)).toBe(false);
|
||||||
|
expect(fs.existsSync(sidecarPath)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not delete sidecar file when delete option is false', async () => {
|
||||||
|
const sidecarPath = path.join(testDir, 'test.xmp');
|
||||||
|
fs.writeFileSync(sidecarPath, 'xmp data');
|
||||||
|
|
||||||
|
await deleteFiles([{ id: 'test-id', filepath: testFilePath }], [], { delete: false, concurrency: 1 });
|
||||||
|
|
||||||
|
expect(fs.existsSync(testFilePath)).toBe(true);
|
||||||
|
expect(fs.existsSync(sidecarPath)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+31
-21
@@ -17,7 +17,7 @@ import { Matcher, watch as watchFs } from 'chokidar';
|
|||||||
import { MultiBar, Presets, SingleBar } from 'cli-progress';
|
import { MultiBar, Presets, SingleBar } from 'cli-progress';
|
||||||
import { chunk } from 'lodash-es';
|
import { chunk } from 'lodash-es';
|
||||||
import micromatch from 'micromatch';
|
import micromatch from 'micromatch';
|
||||||
import { Stats, createReadStream } from 'node:fs';
|
import { Stats, createReadStream, existsSync } from 'node:fs';
|
||||||
import { stat, unlink } from 'node:fs/promises';
|
import { stat, unlink } from 'node:fs/promises';
|
||||||
import path, { basename } from 'node:path';
|
import path, { basename } from 'node:path';
|
||||||
import { Queue } from 'src/queue';
|
import { Queue } from 'src/queue';
|
||||||
@@ -403,23 +403,6 @@ export const uploadFiles = async (
|
|||||||
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => {
|
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => {
|
||||||
const { baseUrl, headers } = defaults;
|
const { baseUrl, headers } = defaults;
|
||||||
|
|
||||||
const assetPath = path.parse(input);
|
|
||||||
const noExtension = path.join(assetPath.dir, assetPath.name);
|
|
||||||
|
|
||||||
const sidecarsFiles = await Promise.all(
|
|
||||||
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
|
|
||||||
[`${noExtension}.xmp`, `${input}.xmp`].map(async (sidecarPath) => {
|
|
||||||
try {
|
|
||||||
const stats = await stat(sidecarPath);
|
|
||||||
return new UploadFile(sidecarPath, stats.size);
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const sidecarData = sidecarsFiles.find((file): file is UploadFile => file !== false);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, ''));
|
formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, ''));
|
||||||
formData.append('deviceId', 'CLI');
|
formData.append('deviceId', 'CLI');
|
||||||
@@ -429,8 +412,15 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
|
|||||||
formData.append('isFavorite', 'false');
|
formData.append('isFavorite', 'false');
|
||||||
formData.append('assetData', new UploadFile(input, stats.size));
|
formData.append('assetData', new UploadFile(input, stats.size));
|
||||||
|
|
||||||
if (sidecarData) {
|
const sidecarPath = findSidecar(input);
|
||||||
|
if (sidecarPath) {
|
||||||
|
try {
|
||||||
|
const stats = await stat(sidecarPath);
|
||||||
|
const sidecarData = new UploadFile(sidecarPath, stats.size);
|
||||||
formData.append('sidecarData', sidecarData);
|
formData.append('sidecarData', sidecarData);
|
||||||
|
} catch {
|
||||||
|
// noop
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/assets`, {
|
const response = await fetch(`${baseUrl}/assets`, {
|
||||||
@@ -446,7 +436,19 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
|
|||||||
return response.json();
|
return response.json();
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: UploadOptionsDto): Promise<void> => {
|
export const findSidecar = (filepath: string): string | undefined => {
|
||||||
|
const assetPath = path.parse(filepath);
|
||||||
|
const noExtension = path.join(assetPath.dir, assetPath.name);
|
||||||
|
|
||||||
|
// XMP sidecars can come in two filename formats. For a photo named photo.ext, the filenames are photo.ext.xmp and photo.xmp
|
||||||
|
for (const sidecarPath of [`${noExtension}.xmp`, `${filepath}.xmp`]) {
|
||||||
|
if (existsSync(sidecarPath)) {
|
||||||
|
return sidecarPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: UploadOptionsDto): Promise<void> => {
|
||||||
let fileCount = 0;
|
let fileCount = 0;
|
||||||
if (options.delete) {
|
if (options.delete) {
|
||||||
fileCount += uploaded.length;
|
fileCount += uploaded.length;
|
||||||
@@ -474,7 +476,15 @@ const deleteFiles = async (uploaded: Asset[], duplicates: Asset[], options: Uplo
|
|||||||
|
|
||||||
const chunkDelete = async (files: Asset[]) => {
|
const chunkDelete = async (files: Asset[]) => {
|
||||||
for (const assetBatch of chunk(files, options.concurrency)) {
|
for (const assetBatch of chunk(files, options.concurrency)) {
|
||||||
await Promise.all(assetBatch.map((input: Asset) => unlink(input.filepath)));
|
await Promise.all(
|
||||||
|
assetBatch.map(async (input: Asset) => {
|
||||||
|
await unlink(input.filepath);
|
||||||
|
const sidecarPath = findSidecar(input.filepath);
|
||||||
|
if (sidecarPath) {
|
||||||
|
await unlink(sidecarPath);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
deletionProgress.update(assetBatch.length);
|
deletionProgress.update(assetBatch.length);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ describe('/asset', () => {
|
|||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body.id).toEqual(facesAsset.id);
|
expect(body.id).toEqual(facesAsset.id);
|
||||||
expect(body.people).toMatchObject(expectedFaces);
|
const sortedPeople = body.people.toSorted((a: any, b: any) => a.name.localeCompare(b.name));
|
||||||
|
expect(sortedPeople).toMatchObject(expectedFaces);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const thumbnailUtils = {
|
|||||||
return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`);
|
return page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"] button`);
|
||||||
},
|
},
|
||||||
selectedAsset(page: Page) {
|
selectedAsset(page: Page) {
|
||||||
return page.locator('[data-thumbnail-focus-container]:has(button[aria-checked])');
|
return page.locator('[data-thumbnail-focus-container][data-selected]');
|
||||||
},
|
},
|
||||||
async clickAssetId(page: Page, assetId: string) {
|
async clickAssetId(page: Page, assetId: string) {
|
||||||
await thumbnailUtils.withAssetId(page, assetId).click();
|
await thumbnailUtils.withAssetId(page, assetId).click();
|
||||||
@@ -103,11 +103,8 @@ export const thumbnailUtils = {
|
|||||||
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
|
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
|
||||||
},
|
},
|
||||||
async expectSelectedReadonly(page: Page, assetId: string) {
|
async expectSelectedReadonly(page: Page, assetId: string) {
|
||||||
// todo - need a data attribute for selected
|
|
||||||
await expect(
|
await expect(
|
||||||
page.locator(
|
page.locator(`[data-thumbnail-focus-container][data-asset="${assetId}"][data-selected]`),
|
||||||
`[data-thumbnail-focus-container][data-asset="${assetId}"] > .group.cursor-not-allowed > .rounded-xl`,
|
|
||||||
),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
},
|
},
|
||||||
async expectTimelineHasOnScreenAssets(page: Page) {
|
async expectTimelineHasOnScreenAssets(page: Page) {
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ readme = "README.md"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"aiocache>=0.12.1,<1.0",
|
"aiocache>=0.12.1,<1.0",
|
||||||
"fastapi>=0.95.2,<1.0",
|
"fastapi>=0.95.2,<1.0",
|
||||||
"ftfy>=6.1.1",
|
|
||||||
"gunicorn>=21.1.0",
|
"gunicorn>=21.1.0",
|
||||||
"huggingface-hub>=0.20.1,<1.0",
|
"huggingface-hub>=0.20.1,<1.0",
|
||||||
"insightface>=0.7.3,<1.0",
|
"insightface>=0.7.3,<1.0",
|
||||||
|
|||||||
@@ -150,10 +150,12 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||||||
handleOnSelect(Set<PersonDto> value) {
|
handleOnSelect(Set<PersonDto> value) {
|
||||||
filter.value = filter.value.copyWith(people: value);
|
filter.value = filter.value.copyWith(people: value);
|
||||||
|
|
||||||
peopleCurrentFilterWidget.value = Text(
|
final label = value.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', ');
|
||||||
value.map((e) => e.name != '' ? e.name : 'no_name'.t(context: context)).join(', '),
|
if (label.isNotEmpty) {
|
||||||
style: context.textTheme.labelLarge,
|
peopleCurrentFilterWidget.value = Text(label, style: context.textTheme.labelLarge);
|
||||||
);
|
} else {
|
||||||
|
peopleCurrentFilterWidget.value = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
handleClear() {
|
handleClear() {
|
||||||
@@ -696,7 +698,7 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||||||
label: 'search_filter_location'.t(context: context),
|
label: 'search_filter_location'.t(context: context),
|
||||||
currentFilter: locationCurrentFilterWidget.value,
|
currentFilter: locationCurrentFilterWidget.value,
|
||||||
),
|
),
|
||||||
if (userPreferences.value?.tagsEnabled ?? false)
|
if (userPreferences.valueOrNull?.tagsEnabled ?? false)
|
||||||
SearchFilterChip(
|
SearchFilterChip(
|
||||||
icon: Icons.sell_outlined,
|
icon: Icons.sell_outlined,
|
||||||
onTap: showTagPicker,
|
onTap: showTagPicker,
|
||||||
@@ -722,14 +724,13 @@ class DriftSearchPage extends HookConsumerWidget {
|
|||||||
label: 'search_filter_media_type'.t(context: context),
|
label: 'search_filter_media_type'.t(context: context),
|
||||||
currentFilter: mediaTypeCurrentFilterWidget.value,
|
currentFilter: mediaTypeCurrentFilterWidget.value,
|
||||||
),
|
),
|
||||||
if (userPreferences.value?.ratingsEnabled ?? false) ...[
|
if (userPreferences.valueOrNull?.ratingsEnabled ?? false)
|
||||||
SearchFilterChip(
|
SearchFilterChip(
|
||||||
icon: Icons.star_outline_rounded,
|
icon: Icons.star_outline_rounded,
|
||||||
onTap: showStarRatingPicker,
|
onTap: showStarRatingPicker,
|
||||||
label: 'search_filter_star_rating'.t(context: context),
|
label: 'search_filter_star_rating'.t(context: context),
|
||||||
currentFilter: ratingCurrentFilterWidget.value,
|
currentFilter: ratingCurrentFilterWidget.value,
|
||||||
),
|
),
|
||||||
],
|
|
||||||
SearchFilterChip(
|
SearchFilterChip(
|
||||||
icon: Icons.display_settings_outlined,
|
icon: Icons.display_settings_outlined,
|
||||||
onTap: showDisplayOptionPicker,
|
onTap: showDisplayOptionPicker,
|
||||||
|
|||||||
+1
-2
@@ -60,8 +60,7 @@ class AppearsInDetails extends ConsumerWidget {
|
|||||||
album: album,
|
album: album,
|
||||||
isOwner: isOwner,
|
isOwner: isOwner,
|
||||||
onAlbumSelected: (album) async {
|
onAlbumSelected: (album) async {
|
||||||
ref.invalidate(assetViewerProvider);
|
unawaited(context.router.push(RemoteAlbumRoute(album: album)));
|
||||||
unawaited(context.router.popAndPush(RemoteAlbumRoute(album: album)));
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ class Timeline extends StatefulWidget {
|
|||||||
this.snapToMonth = true,
|
this.snapToMonth = true,
|
||||||
this.initialScrollOffset,
|
this.initialScrollOffset,
|
||||||
this.readOnly = false,
|
this.readOnly = false,
|
||||||
|
this.persistentBottomBar = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
@@ -87,6 +88,7 @@ class Timeline extends StatefulWidget {
|
|||||||
final bool snapToMonth;
|
final bool snapToMonth;
|
||||||
final double? initialScrollOffset;
|
final double? initialScrollOffset;
|
||||||
final bool readOnly;
|
final bool readOnly;
|
||||||
|
final bool persistentBottomBar;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<Timeline> createState() => _TimelineState();
|
State<Timeline> createState() => _TimelineState();
|
||||||
@@ -143,6 +145,7 @@ class _TimelineState extends State<Timeline> {
|
|||||||
appBar: widget.appBar,
|
appBar: widget.appBar,
|
||||||
bottomSheet: widget.bottomSheet,
|
bottomSheet: widget.bottomSheet,
|
||||||
withScrubber: widget.withScrubber,
|
withScrubber: widget.withScrubber,
|
||||||
|
persistentBottomBar: widget.persistentBottomBar,
|
||||||
snapToMonth: widget.snapToMonth,
|
snapToMonth: widget.snapToMonth,
|
||||||
initialScrollOffset: widget.initialScrollOffset,
|
initialScrollOffset: widget.initialScrollOffset,
|
||||||
),
|
),
|
||||||
@@ -173,6 +176,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
this.appBar,
|
this.appBar,
|
||||||
this.bottomSheet,
|
this.bottomSheet,
|
||||||
this.withScrubber = true,
|
this.withScrubber = true,
|
||||||
|
this.persistentBottomBar = false,
|
||||||
this.snapToMonth = true,
|
this.snapToMonth = true,
|
||||||
this.initialScrollOffset,
|
this.initialScrollOffset,
|
||||||
});
|
});
|
||||||
@@ -182,6 +186,7 @@ class _SliverTimeline extends ConsumerStatefulWidget {
|
|||||||
final Widget? appBar;
|
final Widget? appBar;
|
||||||
final Widget? bottomSheet;
|
final Widget? bottomSheet;
|
||||||
final bool withScrubber;
|
final bool withScrubber;
|
||||||
|
final bool persistentBottomBar;
|
||||||
final bool snapToMonth;
|
final bool snapToMonth;
|
||||||
final double? initialScrollOffset;
|
final double? initialScrollOffset;
|
||||||
|
|
||||||
@@ -404,6 +409,9 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
final isSelectionMode = ref.watch(multiSelectProvider.select((s) => s.forceEnable));
|
final isSelectionMode = ref.watch(multiSelectProvider.select((s) => s.forceEnable));
|
||||||
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
final isMultiSelectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
final isMultiSelectStatusVisible = !isSelectionMode && isMultiSelectEnabled;
|
||||||
|
final isBottomWidgetVisible =
|
||||||
|
widget.bottomSheet != null && (isMultiSelectStatusVisible || widget.persistentBottomBar);
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
canPop: !isMultiSelectEnabled,
|
canPop: !isMultiSelectEnabled,
|
||||||
@@ -519,7 +527,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
timeline,
|
timeline,
|
||||||
if (!isSelectionMode && isMultiSelectEnabled) ...[
|
if (isMultiSelectStatusVisible)
|
||||||
Positioned(
|
Positioned(
|
||||||
top: MediaQuery.paddingOf(context).top,
|
top: MediaQuery.paddingOf(context).top,
|
||||||
left: 25,
|
left: 25,
|
||||||
@@ -528,8 +536,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
child: Center(child: _MultiSelectStatusButton()),
|
child: Center(child: _MultiSelectStatusButton()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.bottomSheet != null) widget.bottomSheet!,
|
if (isBottomWidgetVisible) widget.bottomSheet!,
|
||||||
],
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Generated
+1
-1
@@ -11,7 +11,7 @@ overrides:
|
|||||||
|
|
||||||
packageExtensionsChecksum: sha256-3l4AQg4iuprBDup+q+2JaPvbPg/7XodWCE0ZteH+s54=
|
packageExtensionsChecksum: sha256-3l4AQg4iuprBDup+q+2JaPvbPg/7XodWCE0ZteH+s54=
|
||||||
|
|
||||||
pnpmfileChecksum: sha256-AG/qwrPNpmy9q60PZwCpecoYVptglTHgH+N6RKQHOM0=
|
pnpmfileChecksum: sha256-un98do36L0wZyqsjcLozQ3YUadCAn2yz5bXcBbOuyDA=
|
||||||
|
|
||||||
importers:
|
importers:
|
||||||
|
|
||||||
|
|||||||
@@ -27,16 +27,14 @@ ENTRYPOINT ["tini", "--", "/bin/bash", "-c"]
|
|||||||
FROM dev AS dev-container-server
|
FROM dev AS dev-container-server
|
||||||
|
|
||||||
RUN apt-get update --allow-releaseinfo-change && \
|
RUN apt-get update --allow-releaseinfo-change && \
|
||||||
apt-get install sudo inetutils-ping openjdk-21-jre-headless \
|
apt-get install inetutils-ping openjdk-21-jre-headless \
|
||||||
vim nano curl \
|
vim nano curl \
|
||||||
-y --no-install-recommends --fix-missing
|
-y --no-install-recommends --fix-missing
|
||||||
|
|
||||||
RUN usermod -aG sudo node && \
|
RUN mkdir -p /workspaces && \
|
||||||
echo "node ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \
|
ln -s /usr/src/app /workspaces/immich
|
||||||
mkdir -p /workspaces/immich
|
|
||||||
|
|
||||||
RUN chown node:node -R /workspaces
|
COPY --chmod=755 ../.devcontainer/server/*.sh /immich-devcontainer/
|
||||||
COPY --chown=node:node --chmod=755 ../.devcontainer/server/*.sh /immich-devcontainer/
|
|
||||||
|
|
||||||
WORKDIR /workspaces/immich
|
WORKDIR /workspaces/immich
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@
|
|||||||
imageError = imageLoaded = true;
|
imageError = imageLoaded = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
onDestroy(() => imageManager.cancel(asset, targetImageSize));
|
onDestroy(() => imageManager.cancelPreloadUrl(imageLoaderUrl));
|
||||||
|
|
||||||
let imageLoaderUrl = $derived(
|
let imageLoaderUrl = $derived(
|
||||||
getAssetUrl({ asset, sharedLink, forceOriginal: originalImageLoaded || assetViewerManager.zoom > 1 }),
|
getAssetUrl({ asset, sharedLink, forceOriginal: originalImageLoaded || assetViewerManager.zoom > 1 }),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||||
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
import { imageManager } from '$lib/managers/ImageManager.svelte';
|
||||||
import { Icon } from '@immich/ui';
|
import { Icon } from '@immich/ui';
|
||||||
import { mdiEyeOffOutline } from '@mdi/js';
|
import { mdiEyeOffOutline } from '@mdi/js';
|
||||||
import type { ActionReturn } from 'svelte/action';
|
import type { ActionReturn } from 'svelte/action';
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
onComplete?.(false);
|
onComplete?.(false);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
destroy: () => cancelImageUrl(url),
|
destroy: () => imageManager.cancelPreloadUrl(url),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -223,6 +223,7 @@
|
|||||||
bind:this={element}
|
bind:this={element}
|
||||||
data-asset={asset.id}
|
data-asset={asset.id}
|
||||||
data-thumbnail-focus-container
|
data-thumbnail-focus-container
|
||||||
|
data-selected={selected ? true : undefined}
|
||||||
tabindex={0}
|
tabindex={0}
|
||||||
role="link"
|
role="link"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -32,6 +32,12 @@ class ImageManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelPreloadUrl(url: string | undefined) {
|
||||||
|
if (url) {
|
||||||
|
cancelImageUrl(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const imageManager = new ImageManager();
|
export const imageManager = new ImageManager();
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
import { imageManager } from '$lib/managers/ImageManager.svelte';
|
|
||||||
import { getAssetMediaUrl } from '$lib/utils';
|
|
||||||
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
|
||||||
import { AssetMediaSize } from '@immich/sdk';
|
|
||||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
|
||||||
|
|
||||||
vi.mock('$lib/utils/sw-messaging', () => ({
|
|
||||||
cancelImageUrl: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock('$lib/utils', () => ({
|
|
||||||
getAssetMediaUrl: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('ImageManager', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('preload', () => {
|
|
||||||
it('creates an Image with the correct URL', () => {
|
|
||||||
vi.mocked(getAssetMediaUrl).mockReturnValue('/api/assets/123/media');
|
|
||||||
const asset = assetFactory.build();
|
|
||||||
|
|
||||||
imageManager.preload(asset);
|
|
||||||
|
|
||||||
expect(getAssetMediaUrl).toHaveBeenCalledWith({
|
|
||||||
id: asset.id,
|
|
||||||
size: AssetMediaSize.Preview,
|
|
||||||
cacheKey: asset.thumbhash,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does nothing for undefined asset', () => {
|
|
||||||
imageManager.preload(undefined);
|
|
||||||
expect(getAssetMediaUrl).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does nothing when getAssetMediaUrl returns falsy', () => {
|
|
||||||
vi.mocked(getAssetMediaUrl).mockReturnValue('');
|
|
||||||
const asset = assetFactory.build();
|
|
||||||
|
|
||||||
imageManager.preload(asset);
|
|
||||||
|
|
||||||
expect(getAssetMediaUrl).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses the specified size', () => {
|
|
||||||
vi.mocked(getAssetMediaUrl).mockReturnValue('/api/assets/123/media');
|
|
||||||
const asset = assetFactory.build();
|
|
||||||
|
|
||||||
imageManager.preload(asset, AssetMediaSize.Thumbnail);
|
|
||||||
|
|
||||||
expect(getAssetMediaUrl).toHaveBeenCalledWith({
|
|
||||||
id: asset.id,
|
|
||||||
size: AssetMediaSize.Thumbnail,
|
|
||||||
cacheKey: asset.thumbhash,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('cancel', () => {
|
|
||||||
it('calls cancelImageUrl with the correct URL', () => {
|
|
||||||
vi.mocked(getAssetMediaUrl).mockReturnValue('/api/assets/123/media');
|
|
||||||
const asset = assetFactory.build();
|
|
||||||
|
|
||||||
imageManager.cancel(asset, AssetMediaSize.Preview);
|
|
||||||
|
|
||||||
expect(cancelImageUrl).toHaveBeenCalledWith('/api/assets/123/media');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does nothing for undefined asset', () => {
|
|
||||||
imageManager.cancel(undefined);
|
|
||||||
expect(getAssetMediaUrl).not.toHaveBeenCalled();
|
|
||||||
expect(cancelImageUrl).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cancels all sizes when size is "all"', () => {
|
|
||||||
vi.mocked(getAssetMediaUrl).mockImplementation(({ size }) => `/api/assets/123/${size}`);
|
|
||||||
const asset = assetFactory.build();
|
|
||||||
|
|
||||||
imageManager.cancel(asset, 'all');
|
|
||||||
|
|
||||||
expect(getAssetMediaUrl).toHaveBeenCalledTimes(Object.values(AssetMediaSize).length);
|
|
||||||
for (const size of Object.values(AssetMediaSize)) {
|
|
||||||
expect(cancelImageUrl).toHaveBeenCalledWith(`/api/assets/123/${size}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('does not call cancelImageUrl when URL is falsy', () => {
|
|
||||||
vi.mocked(getAssetMediaUrl).mockReturnValue('');
|
|
||||||
const asset = assetFactory.build();
|
|
||||||
|
|
||||||
imageManager.cancel(asset, AssetMediaSize.Preview);
|
|
||||||
|
|
||||||
expect(cancelImageUrl).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
import { handleResetPinCode } from '$lib/services/user.service';
|
import { handleResetPinCode } from '$lib/services/user.service';
|
||||||
import { Field, FormModal, HelperText, Modal, ModalBody, PasswordInput, Stack, type ModalSize } from '@immich/ui';
|
import { BasicModal, Field, FormModal, HelperText, PasswordInput, Stack, type ModalSize } from '@immich/ui';
|
||||||
import { mdiLockReset } from '@mdi/js';
|
import { mdiLockReset } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
const common = $derived({ title: $t('reset'), size: 'small' as ModalSize, icon: mdiLockReset, onClose });
|
const common = $derived({ title: $t('reset'), size: 'small' as ModalSize, icon: mdiLockReset, onClose });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if featureFlagsManager.value.passwordLogin === false}
|
{#if featureFlagsManager.value.passwordLogin}
|
||||||
<FormModal {...common} submitColor="danger" submitText={$t('reset')} disabled={!password} {onSubmit}>
|
<FormModal {...common} submitColor="danger" submitText={$t('reset')} disabled={!password} {onSubmit}>
|
||||||
<Stack gap={4}>
|
<Stack gap={4}>
|
||||||
<div>{$t('reset_pin_code_description')}</div>
|
<div>{$t('reset_pin_code_description')}</div>
|
||||||
@@ -37,9 +37,7 @@
|
|||||||
</Stack>
|
</Stack>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
{:else}
|
{:else}
|
||||||
<Modal {...common} closeOnBackdropClick>
|
<BasicModal {...common}>
|
||||||
<ModalBody>
|
|
||||||
<div>{$t('reset_pin_code_description')}</div>
|
<div>{$t('reset_pin_code_description')}</div>
|
||||||
</ModalBody>
|
</BasicModal>
|
||||||
</Modal>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user