mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9a36f0391 | |||
| d10dfc05cc | |||
| 0d5685a3fa | |||
| 4c33fbb5e0 | |||
| b687237d8f | |||
| e75d9f5613 | |||
| f353c99223 | |||
| c0afde91a7 | |||
| ff19cd0107 | |||
| d227077543 | |||
| 6e0005acfd | |||
| 55a196bfa0 | |||
| d0b49846dc | |||
| 8896b2dbf5 | |||
| 251e644b2a | |||
| a02635f9a5 | |||
| 104f3dfcc3 | |||
| b7e3b48a44 |
@@ -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:73a05fc805dfd3bd29bebc08442aedfec5c419c5ad3421ec73edc5647233891a
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:9c905a4ff69f00c4b2f98b40b6090ab3ab18d1a15ed1379733b8691aa1fcb271
|
||||
outputs:
|
||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||
steps:
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
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@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
|
||||
# ℹ️ 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@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
@@ -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@0477486d82313fba68f7c82c034120a4b8981297 # multi-runner-build-workflow-v2.1.0
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@47a2ee86898ccff51592d6572391fb1abcd7f782 # multi-runner-build-workflow-v2.0.1
|
||||
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@0477486d82313fba68f7c82c034120a4b8981297 # multi-runner-build-workflow-v2.1.0
|
||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@47a2ee86898ccff51592d6572391fb1abcd7f782 # multi-runner-build-workflow-v2.0.1
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
- name: Generate a token
|
||||
id: generate_token
|
||||
if: ${{ inputs.skip != true }}
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- 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@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
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@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
|
||||
- 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@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
commit-message: 'chore: release ${{ steps.bump-type.outputs.next }}'
|
||||
|
||||
@@ -52,7 +52,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
@@ -571,7 +571,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244 # v7.1.4
|
||||
uses: astral-sh/setup-uv@5a7eac68fb9809dea845d802897dc5c723910fa3 # v7.1.3
|
||||
- uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
|
||||
# with:
|
||||
|
||||
Vendored
+1
-1
@@ -52,7 +52,7 @@
|
||||
},
|
||||
"cSpell.words": ["immich"],
|
||||
"editor.formatOnSave": true,
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||
"eslint.validate": ["javascript", "svelte"],
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"eslint-plugin-unicorn": "^60.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)).toSorted()).toEqual(expected.toSorted());
|
||||
expect(actual.map((path) => readContent(path)).sort()).toEqual(expected.sort());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
+1
-1
@@ -160,7 +160,7 @@ export const crawl = async (options: CrawlOptions): Promise<string[]> => {
|
||||
ignore: [`**/${exclusionPattern}`],
|
||||
});
|
||||
globbedFiles.push(...crawledFiles);
|
||||
return globbedFiles.toSorted();
|
||||
return globbedFiles.sort();
|
||||
};
|
||||
|
||||
export const sha1 = (filepath: string) => {
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2023",
|
||||
"target": "es2022",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tools]
|
||||
terragrunt = "0.93.10"
|
||||
opentofu = "1.10.7"
|
||||
terragrunt = "0.91.2"
|
||||
opentofu = "1.10.6"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
run = "terragrunt hclfmt"
|
||||
|
||||
@@ -18,7 +18,6 @@ make e2e
|
||||
Before you can run the tests, you need to run the following commands _once_:
|
||||
|
||||
- `pnpm install` (in `e2e/`)
|
||||
- `pnpm run build` (in `cli/`)
|
||||
- `make open-api` (in the project root `/`)
|
||||
|
||||
Once the test environment is running, the e2e tests can be run via:
|
||||
|
||||
@@ -80,7 +80,7 @@ Information on the current workers can be found [here](/administration/jobs-work
|
||||
| `DB_SSL_MODE` | Database SSL mode | | server |
|
||||
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
|
||||
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
||||
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | database |
|
||||
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | server |
|
||||
|
||||
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
|
||||
|
||||
|
||||
+3
-3
@@ -35,8 +35,8 @@
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^62.0.0",
|
||||
"exiftool-vendored": "^33.0.0",
|
||||
"eslint-plugin-unicorn": "^60.0.0",
|
||||
"exiftool-vendored": "^31.1.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.5",
|
||||
"sharp": "^0.34.4",
|
||||
"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].toSorted((a, b) => b - a);
|
||||
return [...selectedDays].sort((a, b) => b - a);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -117,12 +117,9 @@ export const thumbnailUtils = {
|
||||
await expect(thumbnailUtils.withAssetId(page, assetId).locator('[data-icon-archive]')).toHaveCount(0);
|
||||
},
|
||||
async expectSelectedReadonly(page: Page, assetId: string) {
|
||||
// todo - need a data attribute for selected
|
||||
await expect(
|
||||
page.locator(
|
||||
`[data-thumbnail-focus-container][data-asset="${assetId}"] > .group.cursor-not-allowed > .rounded-xl`,
|
||||
),
|
||||
).toBeVisible();
|
||||
const container = thumbnailUtils.withAssetId(page, assetId);
|
||||
await expect(container.locator('[data-selected]')).toBeVisible();
|
||||
await expect(container.locator('[data-disabled]')).toBeVisible();
|
||||
},
|
||||
async expectTimelineHasOnScreenAssets(page: Page) {
|
||||
const first = await thumbnailUtils.getFirstInViewport(page);
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2023",
|
||||
"target": "es2022",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
"metadata": "Metadata",
|
||||
"about": "About",
|
||||
"account": "Account",
|
||||
"account_settings": "Account Settings",
|
||||
@@ -68,7 +67,6 @@
|
||||
"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>",
|
||||
@@ -76,8 +74,6 @@
|
||||
"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.",
|
||||
@@ -106,7 +102,6 @@
|
||||
"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.",
|
||||
@@ -115,7 +110,6 @@
|
||||
"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",
|
||||
@@ -188,7 +182,6 @@
|
||||
"maintenance_start": "Start maintenance mode",
|
||||
"maintenance_start_error": "Failed to start maintenance mode.",
|
||||
"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",
|
||||
@@ -294,10 +287,8 @@
|
||||
"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,8 +407,6 @@
|
||||
"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",
|
||||
@@ -738,7 +727,6 @@
|
||||
"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",
|
||||
@@ -757,7 +745,6 @@
|
||||
"connected_device": "Connected device",
|
||||
"connected_to": "Connected to",
|
||||
"contain": "Contain",
|
||||
"content": "Content",
|
||||
"context": "Context",
|
||||
"continue": "Continue",
|
||||
"control_bottom_app_bar_create_new_album": "Create new album",
|
||||
@@ -1524,7 +1511,6 @@
|
||||
"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",
|
||||
@@ -2085,7 +2071,6 @@
|
||||
"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:e39286476f84ffedf7c3564b0b74e32c9e1193ec9ca32ee8a11f8c09dbf6aafe AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:fc1f2e357c307c4044133952b203e66a47e7726821a664f603a180a0c5823844 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:6cda50e312f3aac068cea9ec06c560ca1f522ad546bc8b3d2cf06da0fe8e8a76 AS builder-rocm
|
||||
FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b 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:2c5bc243b1cc47985ee4fb768bb0bbd4490481c5d0897a62da31b7f30b7304a7 AS prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:873f91540d53b36327ed4fb018c9669107a4e2a676719720edb4209c4b15d029 AS prod-cpu
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||
MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:2c5bc243b1cc47985ee4fb768bb0bbd4490481c5d0897a62da31b7f30b7304a7 AS prod-openvino
|
||||
FROM python:3.11-slim-bookworm@sha256:873f91540d53b36327ed4fb018c9669107a4e2a676719720edb4209c4b15d029 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:6cda50e312f3aac068cea9ec06c560ca1f522ad546bc8b3d2cf06da0fe8e8a76 AS prod-rocm
|
||||
FROM rocm/dev-ubuntu-22.04:6.4.3-complete@sha256:1f7e92ca7e3a3785680473329ed1091fc99db3e90fcb3a1688f2933e870ed76b AS prod-rocm
|
||||
|
||||
FROM prod-cpu AS prod-armnn
|
||||
|
||||
|
||||
Generated
+1747
-1851
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@ experimental_monorepo_root = true
|
||||
node = "24.11.1"
|
||||
flutter = "3.35.7"
|
||||
pnpm = "10.22.0"
|
||||
terragrunt = "0.93.10"
|
||||
opentofu = "1.10.7"
|
||||
terragrunt = "0.91.2"
|
||||
opentofu = "1.10.6"
|
||||
java = "25.0.1"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
|
||||
@@ -143,13 +143,11 @@ class BackupToggleButtonState extends ConsumerState<BackupToggleButton> with Sin
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
"enable_backup".t(context: context),
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
Text(
|
||||
"enable_backup".t(context: context),
|
||||
style: context.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
Generated
+357
-326
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -28,7 +28,7 @@ onlyBuiltDependencies:
|
||||
- bcrypt
|
||||
overrides:
|
||||
canvas: 2.11.2
|
||||
sharp: ^0.34.5
|
||||
sharp: ^0.34.4
|
||||
packageExtensions:
|
||||
nestjs-kysely:
|
||||
dependencies:
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202511261514@sha256:cbcca5851fd11042463f09797e6d6068d94adbb108749e62aa69159df59c0591 AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202511181104@sha256:fd445b91d4db131aae71b143b647d2262818dac80946078ce231c79cb9acecba 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:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29
|
||||
FROM ghcr.io/immich-app/base-server-prod:202511181104@sha256:1bc2b7cebc4fd3296dc33a5779411e1c7d854ea713066c1e024d54f45f176f89
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
+12
-12
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202511261514@sha256:cbcca5851fd11042463f09797e6d6068d94adbb108749e62aa69159df59c0591 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202511181104@sha256:fd445b91d4db131aae71b143b647d2262818dac80946078ce231c79cb9acecba 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
|
||||
|
||||
+5
-5
@@ -70,7 +70,7 @@
|
||||
"cookie": "^1.0.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cron": "4.3.3",
|
||||
"exiftool-vendored": "^33.0.0",
|
||||
"exiftool-vendored": "^31.1.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.5",
|
||||
"sharp": "^0.34.4",
|
||||
"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": "^62.0.0",
|
||||
"eslint-plugin-unicorn": "^60.0.0",
|
||||
"globals": "^16.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"node-gyp": "^12.0.0",
|
||||
"node-gyp": "^11.2.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.5"
|
||||
"sharp": "^0.34.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,6 @@ 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(
|
||||
|
||||
@@ -61,7 +61,7 @@ export class BackupService extends BaseService {
|
||||
const newBackupStyle = file.match(/immich-db-backup-\d{8}T\d{6}-v.*-pg.*\.sql\.gz$/);
|
||||
return oldBackupStyle || newBackupStyle;
|
||||
})
|
||||
.toSorted()
|
||||
.sort()
|
||||
.toReversed();
|
||||
|
||||
const toDelete = backups.slice(config.keepLastAmount);
|
||||
|
||||
@@ -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: 1n });
|
||||
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: BigInt(1) });
|
||||
|
||||
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: 1n });
|
||||
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: BigInt(1) });
|
||||
|
||||
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: 0n });
|
||||
mocks.asset.detectOfflineExternalAssets.mockResolvedValue({ numUpdatedRows: BigInt(0) });
|
||||
mocks.library.streamAssetIds.mockReturnValue(makeStream([assetStub.external]));
|
||||
|
||||
const response = await sut.handleQueueSyncAssets({ id: library.id });
|
||||
|
||||
@@ -223,14 +223,7 @@ export class LibraryService extends BaseService {
|
||||
ownerId: dto.ownerId,
|
||||
name: dto.name ?? 'New External Library',
|
||||
importPaths: dto.importPaths ?? [],
|
||||
exclusionPatterns: dto.exclusionPatterns ?? [
|
||||
'**/@eaDir/**',
|
||||
'**/._*',
|
||||
'**/#recycle/**',
|
||||
'**/#snapshot/**',
|
||||
'**/.stversions/**',
|
||||
'**/.stfolder/**',
|
||||
],
|
||||
exclusionPatterns: dto.exclusionPatterns ?? ['**/@eaDir/**', '**/._*', '**/#recycle/**', '**/#snapshot/**'],
|
||||
});
|
||||
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')
|
||||
.toSorted((stream1, stream2) => stream2.bitrate - stream1.bitrate)[0];
|
||||
.sort((stream1, stream2) => stream2.bitrate - stream1.bitrate)[0];
|
||||
}
|
||||
|
||||
private getTranscodeTarget(
|
||||
|
||||
@@ -37,6 +37,7 @@ 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,7 +704,8 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
|
||||
}
|
||||
|
||||
getBitrateOptions() {
|
||||
const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'} ${this.config.crf}`];
|
||||
const options = [];
|
||||
options.push(`-${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).toSorted() as Array<keyof T>;
|
||||
const keys = Object.keys(target).sort() as Array<keyof T>;
|
||||
for (const key of keys) {
|
||||
result[key] = sortKeys(target[key]);
|
||||
}
|
||||
@@ -178,7 +178,10 @@ const patchOpenAPI = (document: OpenAPIObject) => {
|
||||
throw new Error(`Invalid number format: ${schemaName}.${key}=float (use double instead). `);
|
||||
}
|
||||
}
|
||||
schema.required?.sort();
|
||||
|
||||
if (schema.required) {
|
||||
schema.required = 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(String.raw`${SyncEntityType.PartnerAssetExifBackfillV1}\|.+?\|.+`)),
|
||||
ack: expect.stringMatching(new RegExp(`${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(String.raw`${SyncEntityType.PartnerAssetExifBackfillV1}\|.+?\|.+`)),
|
||||
ack: expect.stringMatching(new RegExp(`${SyncEntityType.PartnerAssetExifBackfillV1}\\|.+?\\|.+`)),
|
||||
data: expect.objectContaining({
|
||||
assetId: assetUser3.id,
|
||||
}),
|
||||
|
||||
@@ -38,7 +38,6 @@ export const makeMockWatcher =
|
||||
return () => close();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
return () => Promise.resolve();
|
||||
};
|
||||
|
||||
|
||||
+3
-3
@@ -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.2",
|
||||
"@immich/ui": "^0.49.1",
|
||||
"@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": "^62.0.0",
|
||||
"eslint-plugin-unicorn": "^61.0.2",
|
||||
"factory.ts": "^1.4.1",
|
||||
"globals": "^16.0.0",
|
||||
"happy-dom": "^20.0.0",
|
||||
@@ -97,7 +97,7 @@
|
||||
"prettier-plugin-sort-json": "^4.1.1",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^6.0.0",
|
||||
"svelte": "5.45.2",
|
||||
"svelte": "5.43.12",
|
||||
"svelte-check": "^4.1.5",
|
||||
"svelte-eslint-parser": "^1.3.3",
|
||||
"tailwindcss": "^4.1.7",
|
||||
|
||||
@@ -512,7 +512,7 @@
|
||||
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
|
||||
.toLowerCase()
|
||||
.endsWith('.insp'))}
|
||||
<ImagePanoramaViewer bind:zoomToggle {asset} />
|
||||
<ImagePanoramaViewer {asset} />
|
||||
{:else if isShowEditor && selectedEditType === 'crop'}
|
||||
<CropArea {asset} />
|
||||
{:else}
|
||||
|
||||
@@ -7,12 +7,11 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
type Props = {
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
zoomToggle?: (() => void) | null;
|
||||
};
|
||||
}
|
||||
|
||||
let { asset, zoomToggle = $bindable() }: Props = $props();
|
||||
const { asset }: Props = $props();
|
||||
|
||||
const loadAssetData = async (id: string) => {
|
||||
const data = await viewAsset({ ...authManager.params, id, size: AssetMediaSize.Preview });
|
||||
@@ -25,7 +24,6 @@
|
||||
<LoadingSpinner />
|
||||
{:then [data, { default: PhotoSphereViewer }]}
|
||||
<PhotoSphereViewer
|
||||
bind:zoomToggle
|
||||
panorama={data}
|
||||
originalPanorama={isWebCompatibleImage(asset)
|
||||
? getAssetOriginalUrl(asset.id)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import { boundingBoxesArray, type Faces } from '$lib/stores/people.store';
|
||||
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import {
|
||||
EquirectangularAdapter,
|
||||
Viewer,
|
||||
@@ -26,23 +24,15 @@
|
||||
strokeLinejoin: 'round',
|
||||
};
|
||||
|
||||
type Props = {
|
||||
interface Props {
|
||||
panorama: string | { source: string };
|
||||
originalPanorama?: string | { source: string };
|
||||
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
||||
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
||||
navbar?: boolean;
|
||||
zoomToggle?: (() => void) | null;
|
||||
};
|
||||
}
|
||||
|
||||
let {
|
||||
panorama,
|
||||
originalPanorama,
|
||||
adapter = EquirectangularAdapter,
|
||||
plugins = [],
|
||||
navbar = false,
|
||||
zoomToggle = $bindable(),
|
||||
}: Props = $props();
|
||||
let { panorama, originalPanorama, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props();
|
||||
|
||||
let container: HTMLDivElement | undefined = $state();
|
||||
let viewer: Viewer;
|
||||
@@ -103,14 +93,6 @@
|
||||
}
|
||||
});
|
||||
|
||||
zoomToggle = () => {
|
||||
if (!viewer) {
|
||||
return;
|
||||
}
|
||||
viewer.animate({ zoom: $photoZoomState.currentZoom > 1 ? 50 : 83.3, speed: 250 });
|
||||
};
|
||||
|
||||
let hasChangedResolution: boolean = false;
|
||||
onMount(() => {
|
||||
if (!container) {
|
||||
return;
|
||||
@@ -157,15 +139,10 @@
|
||||
const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin);
|
||||
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
||||
// zoomLevel range: [0, 100]
|
||||
photoZoomState.set({
|
||||
...$photoZoomState,
|
||||
currentZoom: zoomLevel / 50,
|
||||
});
|
||||
|
||||
if (Math.round(zoomLevel) >= 75 && !hasChangedResolution) {
|
||||
if (Math.round(zoomLevel) >= 75) {
|
||||
// Replace the preview with the original
|
||||
void resolutionPlugin.setResolution('original');
|
||||
hasChangedResolution = true;
|
||||
viewer.removeEventListener(events.ZoomUpdatedEvent.type, zoomHandler);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -181,13 +158,7 @@
|
||||
viewer.destroy();
|
||||
}
|
||||
boundingBoxesUnsubscribe();
|
||||
// zoomHandler is not called on initial load. Viewer initial zoom is 1, but photoZoomState could be != 1.
|
||||
photoZoomState.set({
|
||||
...$photoZoomState,
|
||||
currentZoom: 1,
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:document use:shortcuts={[{ shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: true }]} />
|
||||
<div class="h-full w-full mb-0" bind:this={container}></div>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { getAssetOriginalUrl, getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { timeToSeconds } from '$lib/utils/date-time';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { AssetMediaSize, AssetVisibility } from '@immich/sdk';
|
||||
import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
mdiArchiveArrowDownOutline,
|
||||
mdiCameraBurst,
|
||||
@@ -46,6 +46,7 @@
|
||||
imageClass?: ClassValue;
|
||||
brokenAssetClass?: ClassValue;
|
||||
dimmed?: boolean;
|
||||
albumUsers?: UserResponseDto[];
|
||||
onClick?: (asset: TimelineAsset) => void;
|
||||
onSelect?: (asset: TimelineAsset) => void;
|
||||
onMouseEvent?: (event: { isMouseOver: boolean; selectedGroupIndex: number }) => void;
|
||||
@@ -64,6 +65,7 @@
|
||||
readonly = false,
|
||||
showArchiveIcon = false,
|
||||
showStackedIcon = true,
|
||||
albumUsers = [],
|
||||
onClick = undefined,
|
||||
onSelect = undefined,
|
||||
onMouseEvent = undefined,
|
||||
@@ -85,6 +87,8 @@
|
||||
let width = $derived(thumbnailSize || thumbnailWidth || 235);
|
||||
let height = $derived(thumbnailSize || thumbnailHeight || 235);
|
||||
|
||||
let assetOwner = $derived(albumUsers?.find((user) => user.id === asset.ownerId) ?? null);
|
||||
|
||||
const onIconClickedHandler = (e?: MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
e?.preventDefault();
|
||||
@@ -220,6 +224,8 @@
|
||||
bind:this={element}
|
||||
data-asset={asset.id}
|
||||
data-thumbnail-focus-container
|
||||
data-selected={selected || undefined}
|
||||
data-disabled={disabled || undefined}
|
||||
tabindex={0}
|
||||
role="link"
|
||||
>
|
||||
@@ -268,6 +274,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !!assetOwner}
|
||||
<div class="absolute bottom-1 end-2 max-w-[50%]">
|
||||
<p class="text-xs font-medium text-white drop-shadow-lg max-w-[100%] truncate">
|
||||
{assetOwner.name}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !authManager.isSharedLink && showArchiveIcon && asset.visibility === AssetVisibility.Archive}
|
||||
<div class={['absolute start-2', asset.isFavorite ? 'bottom-10' : 'bottom-2']}>
|
||||
<Icon data-icon-archive icon={mdiArchiveArrowDownOutline} size="24" class="text-white" />
|
||||
|
||||
@@ -46,7 +46,8 @@ 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[] = [{ type: AlbumModalRowType.NEW_ALBUM, selected: selectedRowIndex === 0 }];
|
||||
const rows: AlbumModalRow[] = [];
|
||||
rows.push({ type: AlbumModalRowType.NEW_ALBUM, selected: selectedRowIndex === 0 });
|
||||
|
||||
const filteredAlbums = sortAlbums(
|
||||
search.length > 0 && albums.length > 0
|
||||
|
||||
@@ -81,7 +81,9 @@
|
||||
</script>
|
||||
|
||||
<div id="camera-selection">
|
||||
<div class="grid grid-auto-fit-40 gap-5">
|
||||
<p class="uppercase immich-form-label">{$t('camera')}</p>
|
||||
|
||||
<div class="grid grid-auto-fit-40 gap-5 mt-1">
|
||||
<div class="w-full">
|
||||
<Combobox
|
||||
label={$t('make')}
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
|
||||
<div id="display-options-selection">
|
||||
<fieldset>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-2">
|
||||
<legend class="uppercase immich-form-label">{$t('display_options')}</legend>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox id="not-in-album-checkbox" size="tiny" bind:checked={filters.isNotInAlbum} />
|
||||
<Label label={$t('not_in_any_album')} for="not-in-album-checkbox" />
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
columns?: 1 | 2;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { title, columns = 1, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<fieldset>
|
||||
<legend class="mb-3 text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||
{title}
|
||||
</legend>
|
||||
|
||||
<div
|
||||
class="grid gap-3"
|
||||
class:grid-cols-1={columns === 1}
|
||||
class:md:grid-cols-2={columns === 2}
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -1,23 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Icon } from '@immich/ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
icon: string;
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { title, icon, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<section class="rounded-xl border border-gray-200 bg-white p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<header class="mb-3 flex items-center gap-2">
|
||||
<Icon {icon} size="18" class="text-immich-primary dark:text-immich-dark-primary" />
|
||||
<h3 class="text-sm font-medium text-gray-700 dark:text-gray-200">{title}</h3>
|
||||
</header>
|
||||
|
||||
<div>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</section>
|
||||
@@ -74,7 +74,9 @@
|
||||
</script>
|
||||
|
||||
<div id="location-selection">
|
||||
<div class="grid grid-auto-fit-40 gap-5">
|
||||
<p class="uppercase immich-form-label">{$t('place')}</p>
|
||||
|
||||
<div class="grid grid-auto-fit-40 gap-5 mt-1">
|
||||
<div class="w-full">
|
||||
<Combobox
|
||||
label={$t('country')}
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
<div id="media-type-selection">
|
||||
<fieldset>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-2">
|
||||
<legend class="uppercase immich-form-label">{$t('media_type')}</legend>
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-2 mt-1">
|
||||
<RadioButton name="media-type" id="type-all" bind:group={filteredMedia} label={$t('all')} value={MediaType.All} />
|
||||
<RadioButton
|
||||
name="media-type"
|
||||
|
||||
@@ -62,7 +62,8 @@
|
||||
: filterPeople(people, name).slice(0, numberOfPeople)}
|
||||
|
||||
<div id="people-selection" class="max-h-60 -mb-4 overflow-y-auto immich-scrollbar">
|
||||
<div class="flex items-center w-full justify-end gap-6">
|
||||
<div class="flex items-center w-full justify-between gap-6">
|
||||
<p class="uppercase immich-form-label py-3">{$t('people')}</p>
|
||||
<SearchBar bind:name placeholder={$t('filter_people')} showLoadingSpinner={false} />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -19,13 +19,15 @@
|
||||
</script>
|
||||
|
||||
<div class="grid grid-auto-fit-40 gap-5">
|
||||
<div class="[&_label]:uppercase">
|
||||
<Combobox
|
||||
label=""
|
||||
placeholder={$t('search_rating')}
|
||||
{options}
|
||||
selectedOption={rating === undefined ? undefined : options[rating]}
|
||||
onSelect={(r) => (rating = r === undefined ? undefined : Number.parseInt(r.value))}
|
||||
/>
|
||||
</div>
|
||||
<label class="immich-form-label" for="start-date">
|
||||
<div class="[&_label]:uppercase">
|
||||
<Combobox
|
||||
label={$t('rating')}
|
||||
placeholder={$t('search_rating')}
|
||||
{options}
|
||||
selectedOption={rating === undefined ? undefined : options[rating]}
|
||||
onSelect={(r) => (rating = r === undefined ? undefined : Number.parseInt(r.value))}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -41,14 +41,14 @@
|
||||
</script>
|
||||
|
||||
{#if $preferences?.tags?.enabled}
|
||||
<div id="tags-selection">
|
||||
<div id="location-selection">
|
||||
<form autocomplete="off" id="create-tag-form">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<div class="[&_label]:uppercase">
|
||||
<Combobox
|
||||
label=""
|
||||
disabled={selectedTags === null}
|
||||
onSelect={handleSelect}
|
||||
label={$t('tags')}
|
||||
defaultFirstOption
|
||||
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
|
||||
bind:selectedOption
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { isAssetViewerRoute, navigate } from '$lib/utils/navigation';
|
||||
import { getTimes, type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
import { type AlbumResponseDto, type PersonResponseDto, type UserResponseDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onDestroy, onMount, type Snippet } from 'svelte';
|
||||
import type { UpdatePayload } from 'vite';
|
||||
@@ -49,6 +49,7 @@
|
||||
showArchiveIcon?: boolean;
|
||||
isShared?: boolean;
|
||||
album?: AlbumResponseDto | null;
|
||||
albumUsers?: UserResponseDto[];
|
||||
person?: PersonResponseDto | null;
|
||||
isShowDeleteConfirmation?: boolean;
|
||||
onSelect?: (asset: TimelineAsset) => void;
|
||||
@@ -81,6 +82,7 @@
|
||||
showArchiveIcon = false,
|
||||
isShared = false,
|
||||
album = null,
|
||||
albumUsers = [],
|
||||
person = null,
|
||||
isShowDeleteConfirmation = $bindable(false),
|
||||
onSelect = () => {},
|
||||
@@ -702,6 +704,7 @@
|
||||
showStackedIcon={withStacked}
|
||||
{showArchiveIcon}
|
||||
{asset}
|
||||
{albumUsers}
|
||||
{groupIndex}
|
||||
onClick={(asset) => {
|
||||
if (typeof onThumbnailClick === 'function') {
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
checked={selectAllSubItems}
|
||||
onCheckedChange={handleSelectAllSubItems}
|
||||
/>
|
||||
<Label label={title} for="permission-{title}" class="font-mono text-primary text-lg" />
|
||||
<Label label={title} for={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="permission-{item}" class="text-sm font-mono" />
|
||||
<Label label={item} for={item} class="text-sm font-mono" />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,6 @@ 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;
|
||||
@@ -72,8 +71,6 @@ class ThemeManager {
|
||||
|
||||
this.#theme.current = theme;
|
||||
|
||||
uiTheme.value = theme.value as unknown as UiTheme;
|
||||
|
||||
eventManager.emit('ThemeChange', theme);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,29 +25,18 @@
|
||||
} from '$lib/components/shared-components/search-bar/search-camera-section.svelte';
|
||||
import SearchDateSection from '$lib/components/shared-components/search-bar/search-date-section.svelte';
|
||||
import SearchDisplaySection from '$lib/components/shared-components/search-bar/search-display-section.svelte';
|
||||
import SearchFilterGroup from '$lib/components/shared-components/search-bar/search-filter-group.svelte';
|
||||
import SearchFilterSection from '$lib/components/shared-components/search-bar/search-filter-section.svelte';
|
||||
import SearchLocationSection from '$lib/components/shared-components/search-bar/search-location-section.svelte';
|
||||
import SearchMediaSection from '$lib/components/shared-components/search-bar/search-media-section.svelte';
|
||||
import SearchPeopleSection from '$lib/components/shared-components/search-bar/search-people-section.svelte';
|
||||
import SearchRatingsSection from '$lib/components/shared-components/search-bar/search-ratings-section.svelte';
|
||||
import SearchTagsSection from '$lib/components/shared-components/search-bar/search-tags-section.svelte';
|
||||
import SearchTextSection from '$lib/components/shared-components/search-bar/search-text-section.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { parseUtcDate } from '$lib/utils/date-time';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { AssetTypeEnum, AssetVisibility, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk';
|
||||
import { Button, HStack, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountMultiple,
|
||||
mdiCalendarRange,
|
||||
mdiCamera,
|
||||
mdiCog,
|
||||
mdiImageMultiple,
|
||||
mdiMagnify,
|
||||
mdiMapMarker,
|
||||
mdiStar,
|
||||
mdiTune,
|
||||
} from '@mdi/js';
|
||||
import { mdiTune } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
|
||||
@@ -198,54 +187,47 @@
|
||||
<Modal icon={mdiTune} size="giant" title={$t('search_options')} {onClose}>
|
||||
<ModalBody>
|
||||
<form id={formId} autocomplete="off" {onsubmit} {onreset}>
|
||||
<div class="flex flex-col gap-6" tabindex="-1">
|
||||
<SearchFilterGroup title={$t('content')}>
|
||||
<SearchFilterSection title={$t('search_type')} icon={mdiMagnify}>
|
||||
<SearchTextSection bind:query={filter.query} bind:queryType={filter.queryType} />
|
||||
</SearchFilterSection>
|
||||
<div class="flex flex-col gap-4 pb-10" tabindex="-1">
|
||||
<!-- PEOPLE -->
|
||||
<SearchPeopleSection bind:selectedPeople={filter.personIds} />
|
||||
|
||||
<SearchFilterSection title={$t('people')} icon={mdiAccountMultiple}>
|
||||
<SearchPeopleSection bind:selectedPeople={filter.personIds} />
|
||||
</SearchFilterSection>
|
||||
</SearchFilterGroup>
|
||||
<!-- TEXT -->
|
||||
<SearchTextSection bind:query={filter.query} bind:queryType={filter.queryType} />
|
||||
|
||||
<SearchFilterGroup title={$t('metadata')} columns={2}>
|
||||
<SearchFilterSection title={$t('place')} icon={mdiMapMarker}>
|
||||
<SearchLocationSection bind:filters={filter.location} />
|
||||
</SearchFilterSection>
|
||||
<!-- TAGS -->
|
||||
<SearchTagsSection bind:selectedTags={filter.tagIds} />
|
||||
|
||||
<SearchFilterSection title={$t('camera')} icon={mdiCamera}>
|
||||
<SearchCameraSection bind:filters={filter.camera} />
|
||||
</SearchFilterSection>
|
||||
<!-- LOCATION -->
|
||||
<SearchLocationSection bind:filters={filter.location} />
|
||||
|
||||
<SearchFilterSection title={$t('date_range')} icon={mdiCalendarRange}>
|
||||
<SearchDateSection bind:filters={filter.date} />
|
||||
</SearchFilterSection>
|
||||
<!-- CAMERA MODEL -->
|
||||
<SearchCameraSection bind:filters={filter.camera} />
|
||||
|
||||
{#if $preferences?.ratings.enabled}
|
||||
<SearchFilterSection title={$t('rating')} icon={mdiStar}>
|
||||
<SearchRatingsSection bind:rating={filter.rating} />
|
||||
</SearchFilterSection>
|
||||
{/if}
|
||||
</SearchFilterGroup>
|
||||
<!-- DATE RANGE -->
|
||||
<SearchDateSection bind:filters={filter.date} />
|
||||
|
||||
<SearchFilterGroup title={$t('options')} columns={2}>
|
||||
<SearchFilterSection title={$t('media_type')} icon={mdiImageMultiple}>
|
||||
<SearchMediaSection bind:filteredMedia={filter.mediaType} />
|
||||
</SearchFilterSection>
|
||||
<!-- RATING -->
|
||||
{#if $preferences?.ratings.enabled}
|
||||
<SearchRatingsSection bind:rating={filter.rating} />
|
||||
{/if}
|
||||
|
||||
<SearchFilterSection title={$t('display_options')} icon={mdiCog}>
|
||||
<SearchDisplaySection bind:filters={filter.display} />
|
||||
</SearchFilterSection>
|
||||
</SearchFilterGroup>
|
||||
<div class="grid md:grid-cols-2 gap-x-5 gap-y-10">
|
||||
<!-- MEDIA TYPE -->
|
||||
<SearchMediaSection bind:filteredMedia={filter.mediaType} />
|
||||
|
||||
<!-- DISPLAY OPTIONS -->
|
||||
<SearchDisplaySection bind:filters={filter.display} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<HStack fullWidth>
|
||||
<Button shape="round" type="reset" color="secondary" fullWidth form={formId}>{$t('clear_all')}</Button>
|
||||
<Button shape="round" type="submit" fullWidth form={formId}>{$t('search')}</Button>
|
||||
<Button shape="round" size="large" type="reset" color="secondary" fullWidth form={formId}
|
||||
>{$t('clear_all')}</Button
|
||||
>
|
||||
<Button shape="round" size="large" type="submit" fullWidth form={formId}>{$t('search')}</Button>
|
||||
</HStack>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
const { onClose, baseTag }: Props = $props();
|
||||
|
||||
let tagValue = $state(baseTag?.path ? `${baseTag.path}/` : '');
|
||||
let tagValue = $state(baseTag?.value ? `${baseTag.value}/` : '');
|
||||
|
||||
const createTag = async () => {
|
||||
const [tag] = await upsertTags({ tagUpsertDto: { tags: [tagValue] } });
|
||||
|
||||
@@ -23,22 +23,17 @@ 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, libraries: LibraryResponseDto[]) => {
|
||||
export const getLibrariesActions = ($t: MessageFormatter) => {
|
||||
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 };
|
||||
@@ -47,41 +42,33 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
|
||||
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 };
|
||||
@@ -90,14 +77,12 @@ 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),
|
||||
};
|
||||
@@ -112,14 +97,12 @@ 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,33 +17,21 @@ 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,10 +34,8 @@ 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 };
|
||||
@@ -47,39 +45,34 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
||||
const Update: ActionItem = {
|
||||
icon: mdiPencilOutline,
|
||||
title: $t('edit'),
|
||||
onAction: () => modalManager.show(UserEditModal, { user }),
|
||||
onAction: () => void 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: () => modalManager.show(UserDeleteConfirmModal, { user }),
|
||||
shortcuts: { key: 'Backspace' },
|
||||
onAction: () => void modalManager.show(UserDeleteConfirmModal, { user }),
|
||||
};
|
||||
|
||||
const Restore: ActionItem = {
|
||||
icon: mdiDeleteRestore,
|
||||
title: $t('restore'),
|
||||
type: $t('command'),
|
||||
color: 'primary',
|
||||
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
|
||||
onAction: () => modalManager.show(UserRestoreConfirmModal, { user }),
|
||||
onAction: () => void 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),
|
||||
};
|
||||
|
||||
+20
-1
@@ -66,6 +66,7 @@
|
||||
} from '@immich/sdk';
|
||||
import { Button, Icon, IconButton, modalManager, toastManager } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountEyeOutline,
|
||||
mdiArrowLeft,
|
||||
mdiCogOutline,
|
||||
mdiDeleteOutline,
|
||||
@@ -100,6 +101,7 @@
|
||||
let isCreatingSharedAlbum = $state(false);
|
||||
let isShowActivity = $state(false);
|
||||
let albumOrder: AssetOrder | undefined = $state(data.album.order);
|
||||
let showAlbumUsers = $state(false);
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
const timelineInteraction = new AssetInteraction();
|
||||
@@ -290,6 +292,11 @@
|
||||
let album = $derived(data.album);
|
||||
let albumId = $derived(album.id);
|
||||
|
||||
const containsEditors = $derived(album?.shared && album.albumUsers.some(({ role }) => role === AlbumUserRole.Editor));
|
||||
const albumUsers = $derived(
|
||||
showAlbumUsers && containsEditors ? [album.owner, ...album.albumUsers.map(({ user }) => user)] : [],
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!album.isActivityEnabled && activityManager.commentCount === 0) {
|
||||
isShowActivity = false;
|
||||
@@ -418,7 +425,8 @@
|
||||
<Timeline
|
||||
enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true}
|
||||
{album}
|
||||
bind:timelineManager
|
||||
{albumUsers}
|
||||
{timelineManager}
|
||||
{options}
|
||||
assetInteraction={currentAssetIntersection}
|
||||
{isShared}
|
||||
@@ -597,6 +605,17 @@
|
||||
{#snippet trailing()}
|
||||
<CastButton />
|
||||
|
||||
{#if containsEditors}
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
color="secondary"
|
||||
aria-label="view asset owners"
|
||||
icon={mdiAccountEyeOutline}
|
||||
onclick={() => (showAlbumUsers = !showAlbumUsers)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isEditor}
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
|
||||
import { afterNavigate, beforeNavigate } 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,7 +11,6 @@
|
||||
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';
|
||||
@@ -20,8 +19,7 @@
|
||||
import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils';
|
||||
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
|
||||
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
||||
import { CommandPaletteContext, modalManager, setTranslations, type ActionItem } from '@immich/ui';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js';
|
||||
import { modalManager, setTranslations } from '@immich/ui';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import '../app.css';
|
||||
@@ -122,57 +120,9 @@
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
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,7 +2,6 @@ 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;
|
||||
@@ -22,8 +21,6 @@ export const load = (async ({ fetch, url }) => {
|
||||
error = initError;
|
||||
}
|
||||
|
||||
commandPaletteManager.enable();
|
||||
|
||||
return {
|
||||
error,
|
||||
meta: {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<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';
|
||||
@@ -13,7 +12,7 @@
|
||||
runQueueCommandLegacy,
|
||||
type QueuesResponseLegacyDto,
|
||||
} from '@immich/sdk';
|
||||
import { Button, CommandPaletteContext, HStack, modalManager, Text, type ActionItem } from '@immich/ui';
|
||||
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
||||
import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -47,27 +46,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
@@ -80,8 +58,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<CommandPaletteContext {commands} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={0}>
|
||||
@@ -98,10 +74,22 @@
|
||||
</Text>
|
||||
</Button>
|
||||
{/if}
|
||||
<Button leadingIcon={mdiPlus} onclick={handleCreateJob} size="small" variant="ghost" color="secondary">
|
||||
<Button
|
||||
leadingIcon={mdiPlus}
|
||||
onclick={() => modalManager.show(JobCreateModal, {})}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
>
|
||||
<Text class="hidden md:block">{$t('admin.create_job')}</Text>
|
||||
</Button>
|
||||
<Button leadingIcon={mdiCog} href={jobConcurrencyLink} size="small" variant="ghost" color="secondary">
|
||||
<Button
|
||||
leadingIcon={mdiCog}
|
||||
href="{AppRoute.ADMIN_SETTINGS}?isOpen=job"
|
||||
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, CommandPaletteContext } from '@immich/ui';
|
||||
import { Button } 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, libraries));
|
||||
const { Create, ScanAll } = $derived(getLibrariesActions($t));
|
||||
</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">
|
||||
<HeaderButton action={ScanAll} />
|
||||
{#if libraries.length > 0}
|
||||
<HeaderButton action={ScanAll} />
|
||||
{/if}
|
||||
<HeaderButton action={Create} />
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -15,18 +15,7 @@
|
||||
getLibraryFolderActions,
|
||||
} from '$lib/services/library.service';
|
||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import {
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Code,
|
||||
CommandPaletteContext,
|
||||
Container,
|
||||
Heading,
|
||||
Icon,
|
||||
modalManager,
|
||||
} from '@immich/ui';
|
||||
import { Card, CardBody, CardHeader, CardTitle, Code, 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';
|
||||
@@ -50,8 +39,6 @@
|
||||
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 },
|
||||
|
||||
@@ -27,7 +27,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, CommandPaletteContext, HStack } from '@immich/ui';
|
||||
import { Alert, HStack } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountOutline,
|
||||
mdiBackupRestore,
|
||||
@@ -215,8 +215,6 @@
|
||||
);
|
||||
</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, CommandPaletteContext, HStack, Icon } from '@immich/ui';
|
||||
import { Button, HStack, Icon } from '@immich/ui';
|
||||
import { mdiInfinity } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
@@ -43,8 +43,6 @@
|
||||
{onUserAdminDeleted}
|
||||
/>
|
||||
|
||||
<CommandPaletteContext commands={[Create]} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={1}>
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Code,
|
||||
CommandPaletteContext,
|
||||
Container,
|
||||
getByteUnitString,
|
||||
Heading,
|
||||
@@ -106,8 +105,6 @@
|
||||
{onUserAdminDeleted}
|
||||
/>
|
||||
|
||||
<CommandPaletteContext commands={[ResetPassword, ResetPinCode, Update, Delete, Restore]} />
|
||||
|
||||
<AdminPageLayout
|
||||
breadcrumbs={[{ title: $t('admin.user_management'), href: AppRoute.ADMIN_USERS }, { title: user.name }]}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user