From fffee80e2fa1df96fd64d80abc37e689bd8c5244 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:18:50 +0100 Subject: [PATCH] feat: command palette (#23693) --- i18n/en.json | 13 +++++ pnpm-lock.yaml | 10 ++-- web/package.json | 2 +- web/src/lib/services/library.service.ts | 19 ++++++- web/src/lib/services/system-config.service.ts | 12 +++++ web/src/lib/services/user-admin.service.ts | 13 +++-- web/src/routes/+layout.svelte | 54 ++++++++++++++++++- web/src/routes/+layout.ts | 3 ++ web/src/routes/admin/jobs-status/+page.svelte | 42 +++++++++------ .../admin/library-management/+page.svelte | 10 ++-- .../library-management/[id]/+page.svelte | 15 +++++- .../routes/admin/system-settings/+page.svelte | 4 +- web/src/routes/admin/users/+page.svelte | 4 +- web/src/routes/admin/users/[id]/+page.svelte | 3 ++ 14 files changed, 169 insertions(+), 35 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 210e05459d..42965e06a8 100644 --- a/i18n/en.json +++ b/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. Crontab Guru", @@ -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", @@ -182,6 +187,7 @@ "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", @@ -287,8 +293,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", @@ -407,6 +415,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", @@ -727,6 +737,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", @@ -1511,6 +1522,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", @@ -2071,6 +2083,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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ff91f7b316..3cc8543108 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -717,8 +717,8 @@ importers: specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.49.1 - version: 0.49.1(svelte@5.43.12) + specifier: ^0.49.2 + version: 0.49.2(svelte@5.43.12) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -2983,8 +2983,8 @@ packages: peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.49.1': - resolution: {integrity: sha512-E8x3iLnGRvkso1XeG3qZGPPjX8l8CoKcrTKxDvn59OjhnK0aZDs1Fv+Nq0lyOhSsH6qyV9vjDbLmhLje6D+thg==} + '@immich/ui@0.49.2': + resolution: {integrity: sha512-7Tn/pG5LobXt0FoNICTxQyxjpADRGTy/Yr69Zb/hrAkFxvYUSykK13SPc3rTXiw0rd3ykkNKru8N7kfeCxqHqQ==} peerDependencies: svelte: ^5.0.0 @@ -14708,7 +14708,7 @@ snapshots: dependencies: svelte: 5.43.12 - '@immich/ui@0.49.1(svelte@5.43.12)': + '@immich/ui@0.49.2(svelte@5.43.12)': dependencies: '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.12) '@internationalized/date': 3.10.0 diff --git a/web/package.json b/web/package.json index 9cf1b6b981..b7db849166 100644 --- a/web/package.json +++ b/web/package.json @@ -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", diff --git a/web/src/lib/services/library.service.ts b/web/src/lib/services/library.service.ts index 93cf836c82..8b4d35a5f6 100644 --- a/web/src/lib/services/library.service.ts +++ b/web/src/lib/services/library.service.ts @@ -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), }; diff --git a/web/src/lib/services/system-config.service.ts b/web/src/lib/services/system-config.service.ts index 62034886b9..ffd0094c72 100644 --- a/web/src/lib/services/system-config.service.ts +++ b/web/src/lib/services/system-config.service.ts @@ -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 }; diff --git a/web/src/lib/services/user-admin.service.ts b/web/src/lib/services/user-admin.service.ts index b8a4c648c1..7a49f2fbe3 100644 --- a/web/src/lib/services/user-admin.service.ts +++ b/web/src/lib/services/user-admin.service.ts @@ -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), }; diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 379b6b00d6..3d065ab2f1 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -1,5 +1,5 @@ + {page.data.meta?.title || 'Web'} - Immich diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts index 2d3bd92d48..ab4b91f9cc 100644 --- a/web/src/routes/+layout.ts +++ b/web/src/routes/+layout.ts @@ -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: { diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index 6a3195f447..1a61ea6b23 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -1,4 +1,5 @@ + + {#snippet buttons()} @@ -74,22 +98,10 @@ {/if} - - diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 6aa2b3007a..aef8447d00 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -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)); + + {#snippet buttons()}
- {#if libraries.length > 0} - - {/if} +
{/snippet} diff --git a/web/src/routes/admin/library-management/[id]/+page.svelte b/web/src/routes/admin/library-management/[id]/+page.svelte index 32367e78a8..db73952b3c 100644 --- a/web/src/routes/admin/library-management/[id]/+page.svelte +++ b/web/src/routes/admin/library-management/[id]/+page.svelte @@ -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)} /> + + + + {#snippet buttons()} diff --git a/web/src/routes/admin/users/+page.svelte b/web/src/routes/admin/users/+page.svelte index ef20a94b86..de2c1ef85a 100644 --- a/web/src/routes/admin/users/+page.svelte +++ b/web/src/routes/admin/users/+page.svelte @@ -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} /> + + {#snippet buttons()} diff --git a/web/src/routes/admin/users/[id]/+page.svelte b/web/src/routes/admin/users/[id]/+page.svelte index 5fa7030173..a9721c02f2 100644 --- a/web/src/routes/admin/users/[id]/+page.svelte +++ b/web/src/routes/admin/users/[id]/+page.svelte @@ -22,6 +22,7 @@ CardHeader, CardTitle, Code, + CommandPaletteContext, Container, getByteUnitString, Heading, @@ -105,6 +106,8 @@ {onUserAdminDeleted} /> + +