mirror of
https://github.com/immich-app/immich.git
synced 2026-03-01 22:50:09 -05:00
merge: remote-tracking branch 'origin/main' into feat/database-restores
This commit is contained in:
commit
db7169ea01
2
.github/workflows/close-duplicates.yml
vendored
2
.github/workflows/close-duplicates.yml
vendored
@ -35,7 +35,7 @@ jobs:
|
||||
needs: [get_body, should_run]
|
||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||
container:
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:9c905a4ff69f00c4b2f98b40b6090ab3ab18d1a15ed1379733b8691aa1fcb271
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:73a05fc805dfd3bd29bebc08442aedfec5c419c5ad3421ec73edc5647233891a
|
||||
outputs:
|
||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||
steps:
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@ -70,7 +70,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@ -83,6 +83,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@ -132,7 +132,7 @@ jobs:
|
||||
suffixes: '-rocm'
|
||||
platforms: linux/amd64
|
||||
runner-mapping: '{"linux/amd64": "mich"}'
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@47a2ee86898ccff51592d6572391fb1abcd7f782 # multi-runner-build-workflow-v2.0.1
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@0477486d82313fba68f7c82c034120a4b8981297 # multi-runner-build-workflow-v2.1.0
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
@ -155,7 +155,7 @@ jobs:
|
||||
name: Build and Push Server
|
||||
needs: pre-job
|
||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@47a2ee86898ccff51592d6572391fb1abcd7f782 # multi-runner-build-workflow-v2.0.1
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@0477486d82313fba68f7c82c034120a4b8981297 # multi-runner-build-workflow-v2.1.0
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
2
.github/workflows/fix-format.yml
vendored
2
.github/workflows/fix-format.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
2
.github/workflows/merge-translations.yml
vendored
2
.github/workflows/merge-translations.yml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
- name: Generate a token
|
||||
id: generate_token
|
||||
if: ${{ inputs.skip != true }}
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
6
.github/workflows/prepare-release.yml
vendored
6
.github/workflows/prepare-release.yml
vendored
@ -49,7 +49,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@ -62,7 +62,7 @@ jobs:
|
||||
ref: main
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
@ -126,7 +126,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
6
.github/workflows/release-pr.yml
vendored
6
.github/workflows/release-pr.yml
vendored
@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
@ -30,7 +30,7 @@ jobs:
|
||||
ref: main
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
|
||||
@ -159,7 +159,7 @@ jobs:
|
||||
|
||||
- name: Create PR
|
||||
id: create-pr
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}'
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -52,7 +52,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -571,7 +571,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
|
||||
# with:
|
||||
|
||||
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -52,7 +52,7 @@
|
||||
},
|
||||
"cSpell.words": ["immich"],
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.validate": ["javascript", "svelte"],
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^60.0.0",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
|
||||
@ -299,7 +299,7 @@ describe('crawl', () => {
|
||||
.map(([file]) => file);
|
||||
|
||||
// Compare file's content instead of path since a file can be represent in multiple ways.
|
||||
expect(actual.map((path) => readContent(path)).sort()).toEqual(expected.sort());
|
||||
expect(actual.map((path) => readContent(path)).toSorted()).toEqual(expected.toSorted());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -160,7 +160,7 @@ export const crawl = async (options: CrawlOptions): Promise<string[]> => {
|
||||
ignore: [`**/${exclusionPattern}`],
|
||||
});
|
||||
globbedFiles.push(...crawledFiles);
|
||||
return globbedFiles.sort();
|
||||
return globbedFiles.toSorted();
|
||||
};
|
||||
|
||||
export const sha1 = (filepath: string) => {
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2022",
|
||||
"target": "es2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
|
||||
@ -35,8 +35,8 @@
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^60.0.0",
|
||||
"exiftool-vendored": "^31.1.0",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"exiftool-vendored": "^33.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"jose": "^5.6.3",
|
||||
"luxon": "^3.4.4",
|
||||
@ -45,7 +45,7 @@
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"sharp": "^0.34.4",
|
||||
"sharp": "^0.34.5",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
|
||||
@ -61,7 +61,7 @@ export function selectRandomDays(daysInMonth: number, numDays: number, rng: Seed
|
||||
}
|
||||
}
|
||||
|
||||
return [...selectedDays].sort((a, b) => b - a);
|
||||
return [...selectedDays].toSorted((a, b) => b - a);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2022",
|
||||
"target": "es2023",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
|
||||
13
i18n/en.json
13
i18n/en.json
@ -67,6 +67,7 @@
|
||||
"confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.",
|
||||
"confirm_user_password_reset": "Are you sure you want to reset {user}'s password?",
|
||||
"confirm_user_pin_code_reset": "Are you sure you want to reset {user}'s PIN code?",
|
||||
"copy_config_to_clipboard_description": "Copy the current system config as a JSON object to the clipboard",
|
||||
"create_job": "Create job",
|
||||
"cron_expression": "Cron expression",
|
||||
"cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. <link>Crontab Guru</link>",
|
||||
@ -74,6 +75,8 @@
|
||||
"disable_login": "Disable login",
|
||||
"duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search",
|
||||
"exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
|
||||
"export_config_as_json_description": "Download the current system config as a JSON file",
|
||||
"external_libraries_page_description": "Admin external library page",
|
||||
"external_library_management": "External Library Management",
|
||||
"face_detection": "Face detection",
|
||||
"face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
|
||||
@ -102,6 +105,7 @@
|
||||
"image_thumbnail_description": "Small thumbnail with stripped metadata, used when viewing groups of photos like the main timeline",
|
||||
"image_thumbnail_quality_description": "Thumbnail quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness.",
|
||||
"image_thumbnail_title": "Thumbnail Settings",
|
||||
"import_config_from_json_description": "Import system config by uploading a JSON config file",
|
||||
"job_concurrency": "{job} concurrency",
|
||||
"job_created": "Job created",
|
||||
"job_not_concurrency_safe": "This job is not concurrency-safe.",
|
||||
@ -110,6 +114,7 @@
|
||||
"job_status": "Job Status",
|
||||
"jobs_delayed": "{jobCount, plural, other {# delayed}}",
|
||||
"jobs_failed": "{jobCount, plural, other {# failed}}",
|
||||
"jobs_page_description": "Admin jobs page",
|
||||
"library_created": "Created library: {library}",
|
||||
"library_deleted": "Library deleted",
|
||||
"library_details": "Library details",
|
||||
@ -191,6 +196,7 @@
|
||||
"maintenance_upload_backup": "Upload database backup file",
|
||||
"maintenance_upload_backup_error": "Could not upload backup, is it an .sql/.sql.gz file?",
|
||||
"manage_concurrency": "Manage Concurrency",
|
||||
"manage_concurrency_description": "Navigate to the jobs page to manage job concurrency",
|
||||
"manage_log_settings": "Manage log settings",
|
||||
"map_dark_style": "Dark style",
|
||||
"map_enable_description": "Enable map features",
|
||||
@ -296,8 +302,10 @@
|
||||
"server_public_users_description": "All users (name and email) are listed when adding a user to shared albums. When disabled, the user list will only be available to admin users.",
|
||||
"server_settings": "Server Settings",
|
||||
"server_settings_description": "Manage server settings",
|
||||
"server_stats_page_description": "Admin server statistics page",
|
||||
"server_welcome_message": "Welcome message",
|
||||
"server_welcome_message_description": "A message that is displayed on the login page.",
|
||||
"settings_page_description": "Admin settings page",
|
||||
"sidecar_job": "Sidecar metadata",
|
||||
"sidecar_job_description": "Discover or synchronize sidecar metadata from the filesystem",
|
||||
"slideshow_duration_description": "Number of seconds to display each image",
|
||||
@ -416,6 +424,8 @@
|
||||
"user_restore_scheduled_removal": "Restore user - scheduled removal on {date, date, long}",
|
||||
"user_settings": "User Settings",
|
||||
"user_settings_description": "Manage user settings",
|
||||
"user_successfully_removed": "User {email} has been successfully removed.",
|
||||
"users_page_description": "Admin users page",
|
||||
"version_check_enabled_description": "Enable version check",
|
||||
"version_check_implications": "The version check feature relies on periodic communication with github.com",
|
||||
"version_check_settings": "Version Check",
|
||||
@ -736,6 +746,7 @@
|
||||
"collapse_all": "Collapse all",
|
||||
"color": "Color",
|
||||
"color_theme": "Color theme",
|
||||
"command": "Command",
|
||||
"comment_deleted": "Comment deleted",
|
||||
"comment_options": "Comment options",
|
||||
"comments_and_likes": "Comments & likes",
|
||||
@ -1542,6 +1553,7 @@
|
||||
"other_variables": "Other variables",
|
||||
"owned": "Owned",
|
||||
"owner": "Owner",
|
||||
"page": "Page",
|
||||
"partner": "Partner",
|
||||
"partner_can_access": "{partner} can access",
|
||||
"partner_can_access_assets": "All your photos and videos except those in Archived and Deleted",
|
||||
@ -2102,6 +2114,7 @@
|
||||
"to_select": "to select",
|
||||
"to_trash": "Trash",
|
||||
"toggle_settings": "Toggle settings",
|
||||
"toggle_theme_description": "Toggle theme",
|
||||
"total": "Total",
|
||||
"total_usage": "Total usage",
|
||||
"trash": "Trash",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:fc1f2e357c307c4044133952b203e66a47e7726821a664f603a180a0c5823844 AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:e39286476f84ffedf7c3564b0b74e32c9e1193ec9ca32ee8a11f8c09dbf6aafe AS builder-cpu
|
||||
|
||||
FROM builder-cpu AS builder-openvino
|
||||
|
||||
@ -22,7 +22,7 @@ FROM builder-cpu AS builder-rknn
|
||||
|
||||
# Warning: 25GiB+ disk space required to pull this image
|
||||
# TODO: find a way to reduce the image size
|
||||
FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS builder-rocm
|
||||
FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:6cda50e312f3aac068cea9ec06c560ca1f522ad546bc8b3d2cf06da0fe8e8a76 AS builder-rocm
|
||||
|
||||
# renovate: datasource=github-releases depName=Microsoft/onnxruntime
|
||||
ARG ONNXRUNTIME_VERSION="v1.22.1"
|
||||
@ -68,12 +68,12 @@ RUN if [ "$DEVICE" = "rocm" ]; then \
|
||||
uv pip install /opt/onnxruntime_rocm-*.whl; \
|
||||
fi
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:873f91540d53b36327ed4fb018c9669107a4e2a676719720edb4209c4b15d029 AS prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:2c5bc243b1cc47985ee4fb768bb0bbd4490481c5d0897a62da31b7f30b7304a7 AS prod-cpu
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||
MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:873f91540d53b36327ed4fb018c9669107a4e2a676719720edb4209c4b15d029 AS prod-openvino
|
||||
FROM python:3.11-slim-bookworm@sha256:2c5bc243b1cc47985ee4fb768bb0bbd4490481c5d0897a62da31b7f30b7304a7 AS prod-openvino
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||
@ -102,7 +102,7 @@ COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3
|
||||
COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11
|
||||
COPY --from=builder-cuda /usr/local/lib/libpython3.11.so /usr/local/lib/libpython3.11.so
|
||||
|
||||
FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS prod-rocm
|
||||
FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:6cda50e312f3aac068cea9ec06c560ca1f522ad546bc8b3d2cf06da0fe8e8a76 AS prod-rocm
|
||||
|
||||
FROM prod-cpu AS prod-armnn
|
||||
|
||||
|
||||
3598
machine-learning/uv.lock
generated
3598
machine-learning/uv.lock
generated
File diff suppressed because it is too large
Load Diff
538
pnpm-lock.yaml
generated
538
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -28,7 +28,7 @@ onlyBuiltDependencies:
|
||||
- bcrypt
|
||||
overrides:
|
||||
canvas: 2.11.2
|
||||
sharp: ^0.34.4
|
||||
sharp: ^0.34.5
|
||||
packageExtensions:
|
||||
nestjs-kysely:
|
||||
dependencies:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202511181104@sha256:fd445b91d4db131aae71b143b647d2262818dac80946078ce231c79cb9acecba AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202511261514@sha256:cbcca5851fd11042463f09797e6d6068d94adbb108749e62aa69159df59c0591 AS builder
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp \
|
||||
@ -69,7 +69,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \
|
||||
--mount=type=cache,id=mise-tools,target=/buildcache/mise \
|
||||
cd plugins && mise run build
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202511181104@sha256:1bc2b7cebc4fd3296dc33a5779411e1c7d854ea713066c1e024d54f45f176f89
|
||||
FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202511181104@sha256:fd445b91d4db131aae71b143b647d2262818dac80946078ce231c79cb9acecba AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202511261514@sha256:cbcca5851fd11042463f09797e6d6068d94adbb108749e62aa69159df59c0591 AS dev
|
||||
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
@ -44,18 +44,18 @@ FROM dev-container-server AS dev-container-mobile
|
||||
USER root
|
||||
# Enable multiarch for arm64 if necessary
|
||||
RUN if [ "$(dpkg --print-architecture)" = "arm64" ]; then \
|
||||
dpkg --add-architecture amd64 && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
gnupg \
|
||||
qemu-user-static \
|
||||
libc6:amd64 \
|
||||
libstdc++6:amd64 \
|
||||
libgcc1:amd64; \
|
||||
dpkg --add-architecture amd64 && \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
gnupg \
|
||||
qemu-user-static \
|
||||
libc6:amd64 \
|
||||
libstdc++6:amd64 \
|
||||
libgcc1:amd64; \
|
||||
else \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
gnupg; \
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
gnupg; \
|
||||
fi
|
||||
|
||||
# Flutter SDK
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
"cookie": "^1.0.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cron": "4.3.3",
|
||||
"exiftool-vendored": "^31.1.0",
|
||||
"exiftool-vendored": "^33.0.0",
|
||||
"express": "^5.1.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
@ -105,7 +105,7 @@
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "^2.14.0",
|
||||
"semver": "^7.6.2",
|
||||
"sharp": "^0.34.4",
|
||||
"sharp": "^0.34.5",
|
||||
"sirv": "^3.0.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"tailwindcss-preset-email": "^1.4.0",
|
||||
@ -148,10 +148,10 @@
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^60.0.0",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"node-gyp": "^11.2.0",
|
||||
"node-gyp": "^12.0.0",
|
||||
"pngjs": "^7.0.0",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
@ -169,6 +169,6 @@
|
||||
"node": "24.11.1"
|
||||
},
|
||||
"overrides": {
|
||||
"sharp": "^0.34.4"
|
||||
"sharp": "^0.34.5"
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ export class AssetDeltaSyncResponseDto {
|
||||
export const extraSyncModels: Function[] = [];
|
||||
|
||||
export const ExtraModel = (): ClassDecorator => {
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
return (object: Function) => {
|
||||
extraSyncModels.push(object);
|
||||
};
|
||||
|
||||
@ -257,7 +257,7 @@ describe('getEnv', () => {
|
||||
expect(telemetry).toEqual({
|
||||
apiPort: 8081,
|
||||
microservicesPort: 8082,
|
||||
metrics: new Set([]),
|
||||
metrics: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -669,7 +669,7 @@ describe(AlbumService.name, () => {
|
||||
});
|
||||
|
||||
it('should not allow a shared user with viewer access to add assets', async () => {
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set([]));
|
||||
mocks.access.album.checkSharedAlbumAccess.mockResolvedValue(new Set());
|
||||
mocks.album.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser));
|
||||
|
||||
await expect(
|
||||
|
||||
@ -278,7 +278,7 @@ describe(LibraryService.name, () => {
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
mocks.storage.walk.mockImplementation(async function* generator() {});
|
||||
mocks.asset.getLibraryAssetCount.mockResolvedValue(1);
|
||||
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: BigInt(1) });
|
||||
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: 1n });
|
||||
|
||||
const response = await sut.handleQueueSyncAssets({ id: library.id });
|
||||
|
||||
@ -296,7 +296,7 @@ describe(LibraryService.name, () => {
|
||||
mocks.library.get.mockResolvedValue(library);
|
||||
mocks.storage.walk.mockImplementation(async function* generator() {});
|
||||
mocks.asset.getLibraryAssetCount.mockResolvedValue(0);
|
||||
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: BigInt(1) });
|
||||
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: 1n });
|
||||
|
||||
const response = await sut.handleQueueSyncAssets({ id: library.id });
|
||||
|
||||
@ -311,7 +311,7 @@ describe(LibraryService.name, () => {
|
||||
mocks.storage.walk.mockImplementation(async function* generator() {});
|
||||
mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external]));
|
||||
mocks.asset.getLibraryAssetCount.mockResolvedValue(1);
|
||||
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: BigInt(0) });
|
||||
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: 0n });
|
||||
mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external]));
|
||||
|
||||
const response = await sut.handleQueueSyncAssets({ id: library.id });
|
||||
|
||||
@ -223,7 +223,14 @@ export class LibraryService extends BaseService {
|
||||
ownerId: dto.ownerId,
|
||||
name: dto.name ?? 'New External Library',
|
||||
importPaths: dto.importPaths ?? [],
|
||||
exclusionPatterns: dto.exclusionPatterns ?? ['**/@eaDir/**', '**/._*', '**/#recycle/**', '**/#snapshot/**'],
|
||||
exclusionPatterns: dto.exclusionPatterns ?? [
|
||||
'**/@eaDir/**',
|
||||
'**/._*',
|
||||
'**/#recycle/**',
|
||||
'**/#snapshot/**',
|
||||
'**/.stversions/**',
|
||||
'**/.stfolder/**',
|
||||
],
|
||||
});
|
||||
return mapLibrary(library);
|
||||
}
|
||||
|
||||
@ -561,7 +561,7 @@ export class MediaService extends BaseService {
|
||||
private getMainStream<T extends VideoStreamInfo | AudioStreamInfo>(streams: T[]): T {
|
||||
return streams
|
||||
.filter((stream) => stream.codecName !== 'unknown')
|
||||
.sort((stream1, stream2) => stream2.bitrate - stream1.bitrate)[0];
|
||||
.toSorted((stream1, stream2) => stream2.bitrate - stream1.bitrate)[0];
|
||||
}
|
||||
|
||||
private getTranscodeTarget(
|
||||
|
||||
@ -37,7 +37,6 @@ import { upsertTags } from 'src/utils/tag';
|
||||
const EXIF_DATE_TAGS: Array<keyof ImmichTags> = [
|
||||
'SubSecDateTimeOriginal',
|
||||
'SubSecCreateDate',
|
||||
'SubSecMediaCreateDate',
|
||||
'DateTimeOriginal',
|
||||
'CreationDate',
|
||||
'CreateDate',
|
||||
|
||||
@ -217,7 +217,7 @@ describe(TagService.name, () => {
|
||||
|
||||
describe('addAssets', () => {
|
||||
it('should handle invalid ids', async () => {
|
||||
mocks.tag.getAssetIds.mockResolvedValue(new Set([]));
|
||||
mocks.tag.getAssetIds.mockResolvedValue(new Set());
|
||||
await expect(sut.addAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([
|
||||
{ id: 'asset-1', success: false, error: 'no_permission' },
|
||||
]);
|
||||
|
||||
@ -46,7 +46,7 @@ export const setIsEqual = (source: Set<unknown>, target: Set<unknown>) =>
|
||||
source.size === target.size && [...source].every((x) => target.has(x));
|
||||
|
||||
export const haveEqualColumns = (sourceColumns?: string[], targetColumns?: string[]) => {
|
||||
return setIsEqual(new Set(sourceColumns ?? []), new Set(targetColumns ?? []));
|
||||
return setIsEqual(new Set(sourceColumns), new Set(targetColumns));
|
||||
};
|
||||
|
||||
export const haveEqualOverrides = <T extends { override?: DatabaseOverride }>(source: T, target: T) => {
|
||||
|
||||
@ -704,8 +704,7 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
}
|
||||
|
||||
getBitrateOptions() {
|
||||
const options = [];
|
||||
options.push(`-${this.useCQP() ? 'q:v' : 'global_quality:v'} ${this.config.crf}`);
|
||||
const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'} ${this.config.crf}`];
|
||||
const bitrates = this.getBitrateDistribution();
|
||||
if (bitrates.max > 0) {
|
||||
options.push(`-maxrate ${bitrates.max}${bitrates.unit}`, `-bufsize ${bitrates.max * 2}${bitrates.unit}`);
|
||||
|
||||
@ -139,7 +139,7 @@ function sortKeys<T>(target: T): T {
|
||||
}
|
||||
|
||||
const result: Partial<T> = {};
|
||||
const keys = Object.keys(target).sort() as Array<keyof T>;
|
||||
const keys = Object.keys(target).toSorted() as Array<keyof T>;
|
||||
for (const key of keys) {
|
||||
result[key] = sortKeys(target[key]);
|
||||
}
|
||||
@ -178,10 +178,7 @@ const patchOpenAPI = (document: OpenAPIObject) => {
|
||||
throw new Error(`Invalid number format: ${schemaName}.${key}=float (use double instead). `);
|
||||
}
|
||||
}
|
||||
|
||||
if (schema.required) {
|
||||
schema.required = schema.required.sort();
|
||||
}
|
||||
schema.required?.sort();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,7 +176,7 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
|
||||
ack: expect.stringMatching(new RegExp(String.raw`${SyncEntityType.PartnerAssetExifBackfillV1}\|.+?\|.+`)),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser3.id,
|
||||
}),
|
||||
@ -226,7 +226,7 @@ describe(SyncRequestType.PartnerAssetExifsV1, () => {
|
||||
const newResponse = await ctx.syncStream(auth, [SyncRequestType.PartnerAssetExifsV1]);
|
||||
expect(newResponse).toEqual([
|
||||
{
|
||||
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
|
||||
ack: expect.stringMatching(new RegExp(String.raw`${SyncEntityType.PartnerAssetExifBackfillV1}\|.+?\|.+`)),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser3.id,
|
||||
}),
|
||||
|
||||
@ -38,6 +38,7 @@ export const makeMockWatcher =
|
||||
return () => close();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
return () => Promise.resolve();
|
||||
};
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@immich/ui": "^0.49.1",
|
||||
"@immich/ui": "^0.49.2",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
@ -88,7 +88,7 @@
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-compat": "^6.0.2",
|
||||
"eslint-plugin-svelte": "^3.12.4",
|
||||
"eslint-plugin-unicorn": "^61.0.2",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"factory.ts": "^1.4.1",
|
||||
"globals": "^16.0.0",
|
||||
"happy-dom": "^20.0.0",
|
||||
|
||||
@ -46,8 +46,7 @@ export class AlbumModalRowConverter {
|
||||
): AlbumModalRow[] {
|
||||
// only show recent albums if no search was entered, or we're in the normal albums (non-shared) modal.
|
||||
const recentAlbumsToShow = !this.shared && search.length === 0 ? recentAlbums : [];
|
||||
const rows: AlbumModalRow[] = [];
|
||||
rows.push({ type: AlbumModalRowType.NEW_ALBUM, selected: selectedRowIndex === 0 });
|
||||
const rows: AlbumModalRow[] = [{ type: AlbumModalRowType.NEW_ALBUM, selected: selectedRowIndex === 0 }];
|
||||
|
||||
const filteredAlbums = sortAlbums(
|
||||
search.length > 0 && albums.length > 0
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
checked={selectAllSubItems}
|
||||
onCheckedChange={handleSelectAllSubItems}
|
||||
/>
|
||||
<Label label={title} for={title} class="font-mono text-primary text-lg" />
|
||||
<Label label={title} for="permission-{title}" class="font-mono text-primary text-lg" />
|
||||
</div>
|
||||
<div class="mx-6 mt-3 grid grid-cols-3 gap-2">
|
||||
{#each subItems as item (item)}
|
||||
@ -50,7 +50,7 @@
|
||||
checked={selectedItems.includes(item)}
|
||||
onCheckedChange={() => handleToggleItem(item)}
|
||||
/>
|
||||
<Label label={item} for={item} class="text-sm font-mono" />
|
||||
<Label label={item} for="permission-{item}" class="text-sm font-mono" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@ -2,6 +2,7 @@ import { browser } from '$app/environment';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { PersistedLocalStorage } from '$lib/utils/persisted';
|
||||
import { theme as uiTheme, type Theme as UiTheme } from '@immich/ui';
|
||||
|
||||
export interface ThemeSetting {
|
||||
value: Theme;
|
||||
@ -71,6 +72,8 @@ class ThemeManager {
|
||||
|
||||
this.#theme.current = theme;
|
||||
|
||||
uiTheme.value = theme.value as unknown as UiTheme;
|
||||
|
||||
eventManager.emit('ThemeChange', theme);
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,17 +23,22 @@ import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiPencilOutline, mdiPlusBoxOutline, mdiSync, mdiTrashCanOutline } from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
export const getLibrariesActions = ($t: MessageFormatter) => {
|
||||
export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResponseDto[]) => {
|
||||
const ScanAll: ActionItem = {
|
||||
title: $t('scan_all_libraries'),
|
||||
type: $t('command'),
|
||||
icon: mdiSync,
|
||||
onAction: () => void handleScanAllLibraries(),
|
||||
shortcuts: { shift: true, key: 'r' },
|
||||
$if: () => libraries.length > 0,
|
||||
};
|
||||
|
||||
const Create: ActionItem = {
|
||||
title: $t('create_library'),
|
||||
type: $t('command'),
|
||||
icon: mdiPlusBoxOutline,
|
||||
onAction: () => void handleCreateLibrary(),
|
||||
shortcuts: { shift: true, key: 'n' },
|
||||
};
|
||||
|
||||
return { ScanAll, Create };
|
||||
@ -42,33 +47,41 @@ export const getLibrariesActions = ($t: MessageFormatter) => {
|
||||
export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponseDto) => {
|
||||
const Rename: ActionItem = {
|
||||
icon: mdiPencilOutline,
|
||||
type: $t('command'),
|
||||
title: $t('rename'),
|
||||
onAction: () => void modalManager.show(LibraryRenameModal, { library }),
|
||||
shortcuts: { key: 'r' },
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
icon: mdiTrashCanOutline,
|
||||
type: $t('command'),
|
||||
title: $t('delete'),
|
||||
color: 'danger',
|
||||
onAction: () => void handleDeleteLibrary(library),
|
||||
shortcuts: { key: 'Backspace' },
|
||||
};
|
||||
|
||||
const AddFolder: ActionItem = {
|
||||
icon: mdiPlusBoxOutline,
|
||||
type: $t('command'),
|
||||
title: $t('add'),
|
||||
onAction: () => void modalManager.show(LibraryFolderAddModal, { library }),
|
||||
};
|
||||
|
||||
const AddExclusionPattern: ActionItem = {
|
||||
icon: mdiPlusBoxOutline,
|
||||
type: $t('command'),
|
||||
title: $t('add'),
|
||||
onAction: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }),
|
||||
};
|
||||
|
||||
const Scan: ActionItem = {
|
||||
icon: mdiSync,
|
||||
type: $t('command'),
|
||||
title: $t('scan_library'),
|
||||
onAction: () => void handleScanLibrary(library),
|
||||
shortcuts: { shift: true, key: 'r' },
|
||||
};
|
||||
|
||||
return { Rename, Delete, AddFolder, AddExclusionPattern, Scan };
|
||||
@ -77,12 +90,14 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
||||
export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryResponseDto, folder: string) => {
|
||||
const Edit: ActionItem = {
|
||||
icon: mdiPencilOutline,
|
||||
type: $t('command'),
|
||||
title: $t('edit'),
|
||||
onAction: () => void modalManager.show(LibraryFolderEditModal, { folder, library }),
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
icon: mdiTrashCanOutline,
|
||||
type: $t('command'),
|
||||
title: $t('delete'),
|
||||
onAction: () => void handleDeleteLibraryFolder(library, folder),
|
||||
};
|
||||
@ -97,12 +112,14 @@ export const getLibraryExclusionPatternActions = (
|
||||
) => {
|
||||
const Edit: ActionItem = {
|
||||
icon: mdiPencilOutline,
|
||||
type: $t('command'),
|
||||
title: $t('edit'),
|
||||
onAction: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
icon: mdiTrashCanOutline,
|
||||
type: $t('command'),
|
||||
title: $t('delete'),
|
||||
onAction: () => void handleDeleteExclusionPattern(library, exclusionPattern),
|
||||
};
|
||||
|
||||
@ -17,21 +17,33 @@ export const getSystemConfigActions = (
|
||||
) => {
|
||||
const CopyToClipboard: ActionItem = {
|
||||
title: $t('copy_to_clipboard'),
|
||||
description: $t('admin.copy_config_to_clipboard_description'),
|
||||
type: $t('command'),
|
||||
icon: mdiContentCopy,
|
||||
onAction: () => void handleCopyToClipboard(config),
|
||||
shortcuts: { shift: true, key: 'c' },
|
||||
};
|
||||
|
||||
const Download: ActionItem = {
|
||||
title: $t('export_as_json'),
|
||||
description: $t('admin.export_config_as_json_description'),
|
||||
type: $t('command'),
|
||||
icon: mdiDownload,
|
||||
onAction: () => handleDownloadConfig(config),
|
||||
shortcuts: [
|
||||
{ shift: true, key: 's' },
|
||||
{ shift: true, key: 'd' },
|
||||
],
|
||||
};
|
||||
|
||||
const Upload: ActionItem = {
|
||||
title: $t('import_from_json'),
|
||||
description: $t('admin.import_config_from_json_description'),
|
||||
type: $t('command'),
|
||||
icon: mdiUpload,
|
||||
$if: () => !featureFlags.configFile,
|
||||
onAction: () => handleUploadConfig(),
|
||||
shortcuts: { shift: true, key: 'u' },
|
||||
};
|
||||
|
||||
return { CopyToClipboard, Download, Upload };
|
||||
|
||||
@ -34,8 +34,10 @@ import { get } from 'svelte/store';
|
||||
export const getUserAdminsActions = ($t: MessageFormatter) => {
|
||||
const Create: ActionItem = {
|
||||
title: $t('create_user'),
|
||||
type: $t('command'),
|
||||
icon: mdiPlusBoxOutline,
|
||||
onAction: () => void modalManager.show(UserCreateModal, {}),
|
||||
shortcuts: { shift: true, key: 'n' },
|
||||
};
|
||||
|
||||
return { Create };
|
||||
@ -45,34 +47,39 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
||||
const Update: ActionItem = {
|
||||
icon: mdiPencilOutline,
|
||||
title: $t('edit'),
|
||||
onAction: () => void modalManager.show(UserEditModal, { user }),
|
||||
onAction: () => modalManager.show(UserEditModal, { user }),
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
icon: mdiTrashCanOutline,
|
||||
title: $t('delete'),
|
||||
type: $t('command'),
|
||||
color: 'danger',
|
||||
$if: () => get(authUser).id !== user.id && !user.deletedAt,
|
||||
onAction: () => void modalManager.show(UserDeleteConfirmModal, { user }),
|
||||
onAction: () => modalManager.show(UserDeleteConfirmModal, { user }),
|
||||
shortcuts: { key: 'Backspace' },
|
||||
};
|
||||
|
||||
const Restore: ActionItem = {
|
||||
icon: mdiDeleteRestore,
|
||||
title: $t('restore'),
|
||||
type: $t('command'),
|
||||
color: 'primary',
|
||||
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
|
||||
onAction: () => void modalManager.show(UserRestoreConfirmModal, { user }),
|
||||
onAction: () => modalManager.show(UserRestoreConfirmModal, { user }),
|
||||
};
|
||||
|
||||
const ResetPassword: ActionItem = {
|
||||
icon: mdiLockReset,
|
||||
title: $t('reset_password'),
|
||||
type: $t('command'),
|
||||
$if: () => get(authUser).id !== user.id,
|
||||
onAction: () => void handleResetPasswordUserAdmin(user),
|
||||
};
|
||||
|
||||
const ResetPinCode: ActionItem = {
|
||||
icon: mdiLockSmart,
|
||||
type: $t('command'),
|
||||
title: $t('reset_pin_code'),
|
||||
onAction: () => void handleResetPinCodeUserAdmin(user),
|
||||
};
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||
@ -11,6 +11,7 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
|
||||
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
@ -19,7 +20,8 @@
|
||||
import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils';
|
||||
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
|
||||
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
||||
import { modalManager, setTranslations } from '@immich/ui';
|
||||
import { CommandPaletteContext, modalManager, setTranslations, type ActionItem } from '@immich/ui';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import '../app.css';
|
||||
@ -120,9 +122,57 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const userCommands: ActionItem[] = [
|
||||
{
|
||||
title: $t('theme'),
|
||||
description: $t('toggle_theme_description'),
|
||||
type: $t('command'),
|
||||
icon: mdiThemeLightDark,
|
||||
onAction: () => themeManager.toggleTheme(),
|
||||
shortcuts: { shift: true, key: 't' },
|
||||
isGlobal: true,
|
||||
},
|
||||
];
|
||||
|
||||
const adminCommands: ActionItem[] = [
|
||||
{
|
||||
title: $t('users'),
|
||||
description: $t('admin.users_page_description'),
|
||||
icon: mdiAccountMultipleOutline,
|
||||
onAction: () => goto(AppRoute.ADMIN_USERS),
|
||||
},
|
||||
{
|
||||
title: $t('jobs'),
|
||||
description: $t('admin.jobs_page_description'),
|
||||
icon: mdiSync,
|
||||
onAction: () => goto(AppRoute.ADMIN_JOBS),
|
||||
},
|
||||
{
|
||||
title: $t('settings'),
|
||||
description: $t('admin.jobs_page_description'),
|
||||
icon: mdiCog,
|
||||
onAction: () => goto(AppRoute.ADMIN_SETTINGS),
|
||||
},
|
||||
{
|
||||
title: $t('external_libraries'),
|
||||
description: $t('admin.external_libraries_page_description'),
|
||||
icon: mdiBookshelf,
|
||||
onAction: () => goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT),
|
||||
},
|
||||
{
|
||||
title: $t('server_stats'),
|
||||
description: $t('admin.server_stats_page_description'),
|
||||
icon: mdiServer,
|
||||
onAction: () => goto(AppRoute.ADMIN_STATS),
|
||||
},
|
||||
].map((route) => ({ ...route, type: $t('page'), isGlobal: true, $if: () => $user?.isAdmin }));
|
||||
|
||||
const commands = $derived([...userCommands, ...adminCommands]);
|
||||
</script>
|
||||
|
||||
<OnEvents {onReleaseEvent} />
|
||||
<CommandPaletteContext {commands} />
|
||||
|
||||
<svelte:head>
|
||||
<title>{page.data.meta?.title || 'Web'} - Immich</title>
|
||||
|
||||
@ -2,6 +2,7 @@ import { goto } from '$app/navigation';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import { maintenanceCreateUrl, maintenanceReturnUrl, maintenanceShouldRedirect } from '$lib/utils/maintenance';
|
||||
import { init } from '$lib/utils/server';
|
||||
import { commandPaletteManager } from '@immich/ui';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const ssr = false;
|
||||
@ -21,6 +22,8 @@ export const load = (async ({ fetch, url }) => {
|
||||
error = initError;
|
||||
}
|
||||
|
||||
commandPaletteManager.enable();
|
||||
|
||||
return {
|
||||
error,
|
||||
meta: {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import JobsPanel from '$lib/components/jobs/JobsPanel.svelte';
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
@ -12,7 +13,7 @@
|
||||
runQueueCommandLegacy,
|
||||
type QueuesResponseLegacyDto,
|
||||
} from '@immich/sdk';
|
||||
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
||||
import { Button, CommandPaletteContext, HStack, modalManager, Text, type ActionItem } from '@immich/ui';
|
||||
import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
@ -46,6 +47,27 @@
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateJob = () => modalManager.show(JobCreateModal);
|
||||
|
||||
const jobConcurrencyLink = `${AppRoute.ADMIN_SETTINGS}?isOpen=job`;
|
||||
|
||||
const commands: ActionItem[] = [
|
||||
{
|
||||
title: $t('admin.create_job'),
|
||||
type: $t('command'),
|
||||
icon: mdiPlus,
|
||||
onAction: () => void handleCreateJob(),
|
||||
shortcuts: { shift: true, key: 'n' },
|
||||
},
|
||||
{
|
||||
title: $t('admin.manage_concurrency'),
|
||||
description: $t('admin.manage_concurrency_description'),
|
||||
type: $t('page'),
|
||||
icon: mdiCog,
|
||||
onAction: () => goto(jobConcurrencyLink),
|
||||
},
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
while (running) {
|
||||
jobs = await getQueuesLegacy();
|
||||
@ -58,6 +80,8 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<CommandPaletteContext {commands} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={0}>
|
||||
@ -74,22 +98,10 @@
|
||||
</Text>
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
leadingIcon={mdiPlus}
|
||||
onclick={() => modalManager.show(JobCreateModal, {})}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
>
|
||||
<Button leadingIcon={mdiPlus} onclick={handleCreateJob} size="small" variant="ghost" color="secondary">
|
||||
<Text class="hidden md:block">{$t('admin.create_job')}</Text>
|
||||
</Button>
|
||||
<Button
|
||||
leadingIcon={mdiCog}
|
||||
href="{AppRoute.ADMIN_SETTINGS}?isOpen=job"
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
>
|
||||
<Button leadingIcon={mdiCog} href={jobConcurrencyLink} size="small" variant="ghost" color="secondary">
|
||||
<Text class="hidden md:block">{$t('admin.manage_concurrency')}</Text>
|
||||
</Button>
|
||||
</HStack>
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import { getLibrary, getLibraryStatistics, getUserAdmin, type LibraryResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { Button, CommandPaletteContext } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { PageData } from './$types';
|
||||
@ -49,7 +49,7 @@
|
||||
delete owners[id];
|
||||
};
|
||||
|
||||
const { Create, ScanAll } = $derived(getLibrariesActions($t));
|
||||
const { Create, ScanAll } = $derived(getLibrariesActions($t, libraries));
|
||||
</script>
|
||||
|
||||
<OnEvents
|
||||
@ -58,12 +58,12 @@
|
||||
onLibraryDelete={handleDeleteLibrary}
|
||||
/>
|
||||
|
||||
<CommandPaletteContext commands={[Create, ScanAll]} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
{#snippet buttons()}
|
||||
<div class="flex justify-end gap-2">
|
||||
{#if libraries.length > 0}
|
||||
<HeaderButton action={ScanAll} />
|
||||
{/if}
|
||||
<HeaderButton action={ScanAll} />
|
||||
<HeaderButton action={Create} />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@ -15,7 +15,18 @@
|
||||
getLibraryFolderActions,
|
||||
} from '$lib/services/library.service';
|
||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import { Card, CardBody, CardHeader, CardTitle, Code, Container, Heading, Icon, modalManager } from '@immich/ui';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Code,
|
||||
CommandPaletteContext,
|
||||
Container,
|
||||
Heading,
|
||||
Icon,
|
||||
modalManager,
|
||||
} from '@immich/ui';
|
||||
import { mdiCameraIris, mdiChartPie, mdiFilterMinusOutline, mdiFolderOutline, mdiPlayCircle } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
@ -39,6 +50,8 @@
|
||||
onLibraryDelete={({ id }) => id === library.id && goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT)}
|
||||
/>
|
||||
|
||||
<CommandPaletteContext commands={[Rename, Delete, AddFolder, AddExclusionPattern, Scan]} />
|
||||
|
||||
<AdminPageLayout
|
||||
breadcrumbs={[
|
||||
{ title: $t('admin.external_library_management'), href: AppRoute.ADMIN_LIBRARY_MANAGEMENT },
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { getSystemConfigActions } from '$lib/services/system-config.service';
|
||||
import { Alert, HStack } from '@immich/ui';
|
||||
import { Alert, CommandPaletteContext, HStack } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountOutline,
|
||||
mdiBackupRestore,
|
||||
@ -206,6 +206,8 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<CommandPaletteContext commands={[CopyToClipboard, Upload, Download]} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={1}>
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||
import { Button, HStack, Icon } from '@immich/ui';
|
||||
import { Button, CommandPaletteContext, HStack, Icon } from '@immich/ui';
|
||||
import { mdiInfinity } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
@ -43,6 +43,8 @@
|
||||
{onUserAdminDeleted}
|
||||
/>
|
||||
|
||||
<CommandPaletteContext commands={[Create]} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={1}>
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Code,
|
||||
CommandPaletteContext,
|
||||
Container,
|
||||
getByteUnitString,
|
||||
Heading,
|
||||
@ -105,6 +106,8 @@
|
||||
{onUserAdminDeleted}
|
||||
/>
|
||||
|
||||
<CommandPaletteContext commands={[ResetPassword, ResetPinCode, Update, Delete, Restore]} />
|
||||
|
||||
<AdminPageLayout
|
||||
breadcrumbs={[{ title: $t('admin.user_management'), href: AppRoute.ADMIN_USERS }, { title: user.name }]}
|
||||
>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user