feat: command palette (#23693)

This commit is contained in:
Daniel Dietzler
2025-11-26 22:18:50 +01:00
committed by GitHub
parent 64cd4e96e3
commit fffee80e2f
14 changed files with 169 additions and 35 deletions

View File

@@ -67,6 +67,7 @@
"confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.", "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_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?", "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", "create_job": "Create job",
"cron_expression": "Cron expression", "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>", "cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. <link>Crontab Guru</link>",
@@ -74,6 +75,8 @@
"disable_login": "Disable login", "disable_login": "Disable login",
"duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search", "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.", "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", "external_library_management": "External Library Management",
"face_detection": "Face detection", "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.", "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_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_quality_description": "Thumbnail quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness.",
"image_thumbnail_title": "Thumbnail Settings", "image_thumbnail_title": "Thumbnail Settings",
"import_config_from_json_description": "Import system config by uploading a JSON config file",
"job_concurrency": "{job} concurrency", "job_concurrency": "{job} concurrency",
"job_created": "Job created", "job_created": "Job created",
"job_not_concurrency_safe": "This job is not concurrency-safe.", "job_not_concurrency_safe": "This job is not concurrency-safe.",
@@ -110,6 +114,7 @@
"job_status": "Job Status", "job_status": "Job Status",
"jobs_delayed": "{jobCount, plural, other {# delayed}}", "jobs_delayed": "{jobCount, plural, other {# delayed}}",
"jobs_failed": "{jobCount, plural, other {# failed}}", "jobs_failed": "{jobCount, plural, other {# failed}}",
"jobs_page_description": "Admin jobs page",
"library_created": "Created library: {library}", "library_created": "Created library: {library}",
"library_deleted": "Library deleted", "library_deleted": "Library deleted",
"library_details": "Library details", "library_details": "Library details",
@@ -182,6 +187,7 @@
"maintenance_start": "Start maintenance mode", "maintenance_start": "Start maintenance mode",
"maintenance_start_error": "Failed to start maintenance mode.", "maintenance_start_error": "Failed to start maintenance mode.",
"manage_concurrency": "Manage Concurrency", "manage_concurrency": "Manage Concurrency",
"manage_concurrency_description": "Navigate to the jobs page to manage job concurrency",
"manage_log_settings": "Manage log settings", "manage_log_settings": "Manage log settings",
"map_dark_style": "Dark style", "map_dark_style": "Dark style",
"map_enable_description": "Enable map features", "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_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": "Server Settings",
"server_settings_description": "Manage server settings", "server_settings_description": "Manage server settings",
"server_stats_page_description": "Admin server statistics page",
"server_welcome_message": "Welcome message", "server_welcome_message": "Welcome message",
"server_welcome_message_description": "A message that is displayed on the login page.", "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": "Sidecar metadata",
"sidecar_job_description": "Discover or synchronize sidecar metadata from the filesystem", "sidecar_job_description": "Discover or synchronize sidecar metadata from the filesystem",
"slideshow_duration_description": "Number of seconds to display each image", "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_restore_scheduled_removal": "Restore user - scheduled removal on {date, date, long}",
"user_settings": "User Settings", "user_settings": "User Settings",
"user_settings_description": "Manage 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_enabled_description": "Enable version check",
"version_check_implications": "The version check feature relies on periodic communication with github.com", "version_check_implications": "The version check feature relies on periodic communication with github.com",
"version_check_settings": "Version Check", "version_check_settings": "Version Check",
@@ -727,6 +737,7 @@
"collapse_all": "Collapse all", "collapse_all": "Collapse all",
"color": "Color", "color": "Color",
"color_theme": "Color theme", "color_theme": "Color theme",
"command": "Command",
"comment_deleted": "Comment deleted", "comment_deleted": "Comment deleted",
"comment_options": "Comment options", "comment_options": "Comment options",
"comments_and_likes": "Comments & likes", "comments_and_likes": "Comments & likes",
@@ -1511,6 +1522,7 @@
"other_variables": "Other variables", "other_variables": "Other variables",
"owned": "Owned", "owned": "Owned",
"owner": "Owner", "owner": "Owner",
"page": "Page",
"partner": "Partner", "partner": "Partner",
"partner_can_access": "{partner} can access", "partner_can_access": "{partner} can access",
"partner_can_access_assets": "All your photos and videos except those in Archived and Deleted", "partner_can_access_assets": "All your photos and videos except those in Archived and Deleted",
@@ -2071,6 +2083,7 @@
"to_select": "to select", "to_select": "to select",
"to_trash": "Trash", "to_trash": "Trash",
"toggle_settings": "Toggle settings", "toggle_settings": "Toggle settings",
"toggle_theme_description": "Toggle theme",
"total": "Total", "total": "Total",
"total_usage": "Total usage", "total_usage": "Total usage",
"trash": "Trash", "trash": "Trash",

10
pnpm-lock.yaml generated
View File

@@ -717,8 +717,8 @@ importers:
specifier: file:../open-api/typescript-sdk specifier: file:../open-api/typescript-sdk
version: link:../open-api/typescript-sdk version: link:../open-api/typescript-sdk
'@immich/ui': '@immich/ui':
specifier: ^0.49.1 specifier: ^0.49.2
version: 0.49.1(svelte@5.43.12) version: 0.49.2(svelte@5.43.12)
'@mapbox/mapbox-gl-rtl-text': '@mapbox/mapbox-gl-rtl-text':
specifier: 0.2.3 specifier: 0.2.3
version: 0.2.3(mapbox-gl@1.13.3) version: 0.2.3(mapbox-gl@1.13.3)
@@ -2983,8 +2983,8 @@ packages:
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
'@immich/ui@0.49.1': '@immich/ui@0.49.2':
resolution: {integrity: sha512-E8x3iLnGRvkso1XeG3qZGPPjX8l8CoKcrTKxDvn59OjhnK0aZDs1Fv+Nq0lyOhSsH6qyV9vjDbLmhLje6D+thg==} resolution: {integrity: sha512-7Tn/pG5LobXt0FoNICTxQyxjpADRGTy/Yr69Zb/hrAkFxvYUSykK13SPc3rTXiw0rd3ykkNKru8N7kfeCxqHqQ==}
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
@@ -14708,7 +14708,7 @@ snapshots:
dependencies: dependencies:
svelte: 5.43.12 svelte: 5.43.12
'@immich/ui@0.49.1(svelte@5.43.12)': '@immich/ui@0.49.2(svelte@5.43.12)':
dependencies: dependencies:
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.12) '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.43.12)
'@internationalized/date': 3.10.0 '@internationalized/date': 3.10.0

View File

@@ -28,7 +28,7 @@
"@formatjs/icu-messageformat-parser": "^2.9.8", "@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/justified-layout-wasm": "^0.4.3", "@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "file:../open-api/typescript-sdk", "@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", "@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.14.0", "@photo-sphere-viewer/core": "^5.14.0",

View File

@@ -23,17 +23,22 @@ import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { mdiPencilOutline, mdiPlusBoxOutline, mdiSync, mdiTrashCanOutline } from '@mdi/js'; import { mdiPencilOutline, mdiPlusBoxOutline, mdiSync, mdiTrashCanOutline } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n'; import type { MessageFormatter } from 'svelte-i18n';
export const getLibrariesActions = ($t: MessageFormatter) => { export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResponseDto[]) => {
const ScanAll: ActionItem = { const ScanAll: ActionItem = {
title: $t('scan_all_libraries'), title: $t('scan_all_libraries'),
type: $t('command'),
icon: mdiSync, icon: mdiSync,
onAction: () => void handleScanAllLibraries(), onAction: () => void handleScanAllLibraries(),
shortcuts: { shift: true, key: 'r' },
$if: () => libraries.length > 0,
}; };
const Create: ActionItem = { const Create: ActionItem = {
title: $t('create_library'), title: $t('create_library'),
type: $t('command'),
icon: mdiPlusBoxOutline, icon: mdiPlusBoxOutline,
onAction: () => void handleCreateLibrary(), onAction: () => void handleCreateLibrary(),
shortcuts: { shift: true, key: 'n' },
}; };
return { ScanAll, Create }; return { ScanAll, Create };
@@ -42,33 +47,41 @@ export const getLibrariesActions = ($t: MessageFormatter) => {
export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponseDto) => { export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponseDto) => {
const Rename: ActionItem = { const Rename: ActionItem = {
icon: mdiPencilOutline, icon: mdiPencilOutline,
type: $t('command'),
title: $t('rename'), title: $t('rename'),
onAction: () => void modalManager.show(LibraryRenameModal, { library }), onAction: () => void modalManager.show(LibraryRenameModal, { library }),
shortcuts: { key: 'r' },
}; };
const Delete: ActionItem = { const Delete: ActionItem = {
icon: mdiTrashCanOutline, icon: mdiTrashCanOutline,
type: $t('command'),
title: $t('delete'), title: $t('delete'),
color: 'danger', color: 'danger',
onAction: () => void handleDeleteLibrary(library), onAction: () => void handleDeleteLibrary(library),
shortcuts: { key: 'Backspace' },
}; };
const AddFolder: ActionItem = { const AddFolder: ActionItem = {
icon: mdiPlusBoxOutline, icon: mdiPlusBoxOutline,
type: $t('command'),
title: $t('add'), title: $t('add'),
onAction: () => void modalManager.show(LibraryFolderAddModal, { library }), onAction: () => void modalManager.show(LibraryFolderAddModal, { library }),
}; };
const AddExclusionPattern: ActionItem = { const AddExclusionPattern: ActionItem = {
icon: mdiPlusBoxOutline, icon: mdiPlusBoxOutline,
type: $t('command'),
title: $t('add'), title: $t('add'),
onAction: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }), onAction: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }),
}; };
const Scan: ActionItem = { const Scan: ActionItem = {
icon: mdiSync, icon: mdiSync,
type: $t('command'),
title: $t('scan_library'), title: $t('scan_library'),
onAction: () => void handleScanLibrary(library), onAction: () => void handleScanLibrary(library),
shortcuts: { shift: true, key: 'r' },
}; };
return { Rename, Delete, AddFolder, AddExclusionPattern, Scan }; 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) => { export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryResponseDto, folder: string) => {
const Edit: ActionItem = { const Edit: ActionItem = {
icon: mdiPencilOutline, icon: mdiPencilOutline,
type: $t('command'),
title: $t('edit'), title: $t('edit'),
onAction: () => void modalManager.show(LibraryFolderEditModal, { folder, library }), onAction: () => void modalManager.show(LibraryFolderEditModal, { folder, library }),
}; };
const Delete: ActionItem = { const Delete: ActionItem = {
icon: mdiTrashCanOutline, icon: mdiTrashCanOutline,
type: $t('command'),
title: $t('delete'), title: $t('delete'),
onAction: () => void handleDeleteLibraryFolder(library, folder), onAction: () => void handleDeleteLibraryFolder(library, folder),
}; };
@@ -97,12 +112,14 @@ export const getLibraryExclusionPatternActions = (
) => { ) => {
const Edit: ActionItem = { const Edit: ActionItem = {
icon: mdiPencilOutline, icon: mdiPencilOutline,
type: $t('command'),
title: $t('edit'), title: $t('edit'),
onAction: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }), onAction: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
}; };
const Delete: ActionItem = { const Delete: ActionItem = {
icon: mdiTrashCanOutline, icon: mdiTrashCanOutline,
type: $t('command'),
title: $t('delete'), title: $t('delete'),
onAction: () => void handleDeleteExclusionPattern(library, exclusionPattern), onAction: () => void handleDeleteExclusionPattern(library, exclusionPattern),
}; };

View File

@@ -17,21 +17,33 @@ export const getSystemConfigActions = (
) => { ) => {
const CopyToClipboard: ActionItem = { const CopyToClipboard: ActionItem = {
title: $t('copy_to_clipboard'), title: $t('copy_to_clipboard'),
description: $t('admin.copy_config_to_clipboard_description'),
type: $t('command'),
icon: mdiContentCopy, icon: mdiContentCopy,
onAction: () => void handleCopyToClipboard(config), onAction: () => void handleCopyToClipboard(config),
shortcuts: { shift: true, key: 'c' },
}; };
const Download: ActionItem = { const Download: ActionItem = {
title: $t('export_as_json'), title: $t('export_as_json'),
description: $t('admin.export_config_as_json_description'),
type: $t('command'),
icon: mdiDownload, icon: mdiDownload,
onAction: () => handleDownloadConfig(config), onAction: () => handleDownloadConfig(config),
shortcuts: [
{ shift: true, key: 's' },
{ shift: true, key: 'd' },
],
}; };
const Upload: ActionItem = { const Upload: ActionItem = {
title: $t('import_from_json'), title: $t('import_from_json'),
description: $t('admin.import_config_from_json_description'),
type: $t('command'),
icon: mdiUpload, icon: mdiUpload,
$if: () => !featureFlags.configFile, $if: () => !featureFlags.configFile,
onAction: () => handleUploadConfig(), onAction: () => handleUploadConfig(),
shortcuts: { shift: true, key: 'u' },
}; };
return { CopyToClipboard, Download, Upload }; return { CopyToClipboard, Download, Upload };

View File

@@ -34,8 +34,10 @@ import { get } from 'svelte/store';
export const getUserAdminsActions = ($t: MessageFormatter) => { export const getUserAdminsActions = ($t: MessageFormatter) => {
const Create: ActionItem = { const Create: ActionItem = {
title: $t('create_user'), title: $t('create_user'),
type: $t('command'),
icon: mdiPlusBoxOutline, icon: mdiPlusBoxOutline,
onAction: () => void modalManager.show(UserCreateModal, {}), onAction: () => void modalManager.show(UserCreateModal, {}),
shortcuts: { shift: true, key: 'n' },
}; };
return { Create }; return { Create };
@@ -45,34 +47,39 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
const Update: ActionItem = { const Update: ActionItem = {
icon: mdiPencilOutline, icon: mdiPencilOutline,
title: $t('edit'), title: $t('edit'),
onAction: () => void modalManager.show(UserEditModal, { user }), onAction: () => modalManager.show(UserEditModal, { user }),
}; };
const Delete: ActionItem = { const Delete: ActionItem = {
icon: mdiTrashCanOutline, icon: mdiTrashCanOutline,
title: $t('delete'), title: $t('delete'),
type: $t('command'),
color: 'danger', color: 'danger',
$if: () => get(authUser).id !== user.id && !user.deletedAt, $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 = { const Restore: ActionItem = {
icon: mdiDeleteRestore, icon: mdiDeleteRestore,
title: $t('restore'), title: $t('restore'),
type: $t('command'),
color: 'primary', color: 'primary',
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted, $if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
onAction: () => void modalManager.show(UserRestoreConfirmModal, { user }), onAction: () => modalManager.show(UserRestoreConfirmModal, { user }),
}; };
const ResetPassword: ActionItem = { const ResetPassword: ActionItem = {
icon: mdiLockReset, icon: mdiLockReset,
title: $t('reset_password'), title: $t('reset_password'),
type: $t('command'),
$if: () => get(authUser).id !== user.id, $if: () => get(authUser).id !== user.id,
onAction: () => void handleResetPasswordUserAdmin(user), onAction: () => void handleResetPasswordUserAdmin(user),
}; };
const ResetPinCode: ActionItem = { const ResetPinCode: ActionItem = {
icon: mdiLockSmart, icon: mdiLockSmart,
type: $t('command'),
title: $t('reset_pin_code'), title: $t('reset_pin_code'),
onAction: () => void handleResetPinCodeUserAdmin(user), onAction: () => void handleResetPinCodeUserAdmin(user),
}; };

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate, beforeNavigate } from '$app/navigation'; import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
import { page } from '$app/state'; import { page } from '$app/state';
import { shortcut } from '$lib/actions/shortcut'; import { shortcut } from '$lib/actions/shortcut';
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte'; import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
@@ -11,6 +11,7 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import { serverConfigManager } from '$lib/managers/server-config-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 ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte'; import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
@@ -19,7 +20,8 @@
import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils'; import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils';
import { maintenanceShouldRedirect } from '$lib/utils/maintenance'; import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
import { isAssetViewerRoute } from '$lib/utils/navigation'; import { isAssetViewerRoute } from '$lib/utils/navigation';
import { modalManager, setTranslations } from '@immich/ui'; import { CommandPaletteContext, modalManager, setTranslations, type ActionItem } from '@immich/ui';
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js';
import { onMount, type Snippet } from 'svelte'; import { onMount, type Snippet } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import '../app.css'; import '../app.css';
@@ -120,9 +122,57 @@
}); });
} }
}); });
const userCommands: ActionItem[] = [
{
title: $t('theme'),
description: $t('toggle_theme_description'),
type: $t('command'),
icon: mdiThemeLightDark,
onAction: () => themeManager.toggleTheme(),
shortcuts: { shift: true, key: 't' },
isGlobal: true,
},
];
const adminCommands: ActionItem[] = [
{
title: $t('users'),
description: $t('admin.users_page_description'),
icon: mdiAccountMultipleOutline,
onAction: () => goto(AppRoute.ADMIN_USERS),
},
{
title: $t('jobs'),
description: $t('admin.jobs_page_description'),
icon: mdiSync,
onAction: () => goto(AppRoute.ADMIN_JOBS),
},
{
title: $t('settings'),
description: $t('admin.jobs_page_description'),
icon: mdiCog,
onAction: () => goto(AppRoute.ADMIN_SETTINGS),
},
{
title: $t('external_libraries'),
description: $t('admin.external_libraries_page_description'),
icon: mdiBookshelf,
onAction: () => goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT),
},
{
title: $t('server_stats'),
description: $t('admin.server_stats_page_description'),
icon: mdiServer,
onAction: () => goto(AppRoute.ADMIN_STATS),
},
].map((route) => ({ ...route, type: $t('page'), isGlobal: true, $if: () => $user?.isAdmin }));
const commands = $derived([...userCommands, ...adminCommands]);
</script> </script>
<OnEvents {onReleaseEvent} /> <OnEvents {onReleaseEvent} />
<CommandPaletteContext {commands} />
<svelte:head> <svelte:head>
<title>{page.data.meta?.title || 'Web'} - Immich</title> <title>{page.data.meta?.title || 'Web'} - Immich</title>

View File

@@ -2,6 +2,7 @@ import { goto } from '$app/navigation';
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte'; import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import { maintenanceCreateUrl, maintenanceReturnUrl, maintenanceShouldRedirect } from '$lib/utils/maintenance'; import { maintenanceCreateUrl, maintenanceReturnUrl, maintenanceShouldRedirect } from '$lib/utils/maintenance';
import { init } from '$lib/utils/server'; import { init } from '$lib/utils/server';
import { commandPaletteManager } from '@immich/ui';
import type { LayoutLoad } from './$types'; import type { LayoutLoad } from './$types';
export const ssr = false; export const ssr = false;
@@ -21,6 +22,8 @@ export const load = (async ({ fetch, url }) => {
error = initError; error = initError;
} }
commandPaletteManager.enable();
return { return {
error, error,
meta: { meta: {

View File

@@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation';
import JobsPanel from '$lib/components/jobs/JobsPanel.svelte'; import JobsPanel from '$lib/components/jobs/JobsPanel.svelte';
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte'; import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
@@ -12,7 +13,7 @@
runQueueCommandLegacy, runQueueCommandLegacy,
type QueuesResponseLegacyDto, type QueuesResponseLegacyDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { Button, HStack, modalManager, Text } from '@immich/ui'; import { Button, CommandPaletteContext, HStack, modalManager, Text, type ActionItem } from '@immich/ui';
import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js'; import { mdiCog, mdiPlay, mdiPlus } from '@mdi/js';
import { onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@@ -46,6 +47,27 @@
} }
}; };
const handleCreateJob = () => modalManager.show(JobCreateModal);
const jobConcurrencyLink = `${AppRoute.ADMIN_SETTINGS}?isOpen=job`;
const commands: ActionItem[] = [
{
title: $t('admin.create_job'),
type: $t('command'),
icon: mdiPlus,
onAction: () => void handleCreateJob(),
shortcuts: { shift: true, key: 'n' },
},
{
title: $t('admin.manage_concurrency'),
description: $t('admin.manage_concurrency_description'),
type: $t('page'),
icon: mdiCog,
onAction: () => goto(jobConcurrencyLink),
},
];
onMount(async () => { onMount(async () => {
while (running) { while (running) {
jobs = await getQueuesLegacy(); jobs = await getQueuesLegacy();
@@ -58,6 +80,8 @@
}); });
</script> </script>
<CommandPaletteContext {commands} />
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}> <AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
{#snippet buttons()} {#snippet buttons()}
<HStack gap={0}> <HStack gap={0}>
@@ -74,22 +98,10 @@
</Text> </Text>
</Button> </Button>
{/if} {/if}
<Button <Button leadingIcon={mdiPlus} onclick={handleCreateJob} size="small" variant="ghost" color="secondary">
leadingIcon={mdiPlus}
onclick={() => modalManager.show(JobCreateModal, {})}
size="small"
variant="ghost"
color="secondary"
>
<Text class="hidden md:block">{$t('admin.create_job')}</Text> <Text class="hidden md:block">{$t('admin.create_job')}</Text>
</Button> </Button>
<Button <Button leadingIcon={mdiCog} href={jobConcurrencyLink} size="small" variant="ghost" color="secondary">
leadingIcon={mdiCog}
href="{AppRoute.ADMIN_SETTINGS}?isOpen=job"
size="small"
variant="ghost"
color="secondary"
>
<Text class="hidden md:block">{$t('admin.manage_concurrency')}</Text> <Text class="hidden md:block">{$t('admin.manage_concurrency')}</Text>
</Button> </Button>
</HStack> </HStack>

View File

@@ -9,7 +9,7 @@
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { getBytesWithUnit } from '$lib/utils/byte-units'; import { getBytesWithUnit } from '$lib/utils/byte-units';
import { getLibrary, getLibraryStatistics, getUserAdmin, type LibraryResponseDto } from '@immich/sdk'; 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 { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -49,7 +49,7 @@
delete owners[id]; delete owners[id];
}; };
const { Create, ScanAll } = $derived(getLibrariesActions($t)); const { Create, ScanAll } = $derived(getLibrariesActions($t, libraries));
</script> </script>
<OnEvents <OnEvents
@@ -58,12 +58,12 @@
onLibraryDelete={handleDeleteLibrary} onLibraryDelete={handleDeleteLibrary}
/> />
<CommandPaletteContext commands={[Create, ScanAll]} />
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}> <AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
{#snippet buttons()} {#snippet buttons()}
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
{#if libraries.length > 0} <HeaderButton action={ScanAll} />
<HeaderButton action={ScanAll} />
{/if}
<HeaderButton action={Create} /> <HeaderButton action={Create} />
</div> </div>
{/snippet} {/snippet}

View File

@@ -15,7 +15,18 @@
getLibraryFolderActions, getLibraryFolderActions,
} from '$lib/services/library.service'; } from '$lib/services/library.service';
import { getBytesWithUnit } from '$lib/utils/byte-units'; 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 { mdiCameraIris, mdiChartPie, mdiFilterMinusOutline, mdiFolderOutline, mdiPlayCircle } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -39,6 +50,8 @@
onLibraryDelete={({ id }) => id === library.id && goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT)} onLibraryDelete={({ id }) => id === library.id && goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT)}
/> />
<CommandPaletteContext commands={[Rename, Delete, AddFolder, AddExclusionPattern, Scan]} />
<AdminPageLayout <AdminPageLayout
breadcrumbs={[ breadcrumbs={[
{ title: $t('admin.external_library_management'), href: AppRoute.ADMIN_LIBRARY_MANAGEMENT }, { title: $t('admin.external_library_management'), href: AppRoute.ADMIN_LIBRARY_MANAGEMENT },

View File

@@ -27,7 +27,7 @@
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte'; import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
import { getSystemConfigActions } from '$lib/services/system-config.service'; import { getSystemConfigActions } from '$lib/services/system-config.service';
import { Alert, HStack } from '@immich/ui'; import { Alert, CommandPaletteContext, HStack } from '@immich/ui';
import { import {
mdiAccountOutline, mdiAccountOutline,
mdiBackupRestore, mdiBackupRestore,
@@ -215,6 +215,8 @@
); );
</script> </script>
<CommandPaletteContext commands={[CopyToClipboard, Upload, Download]} />
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}> <AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
{#snippet buttons()} {#snippet buttons()}
<HStack gap={1}> <HStack gap={1}>

View File

@@ -6,7 +6,7 @@
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { getByteUnitString } from '$lib/utils/byte-units'; import { getByteUnitString } from '$lib/utils/byte-units';
import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk'; 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 { mdiInfinity } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -43,6 +43,8 @@
{onUserAdminDeleted} {onUserAdminDeleted}
/> />
<CommandPaletteContext commands={[Create]} />
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}> <AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
{#snippet buttons()} {#snippet buttons()}
<HStack gap={1}> <HStack gap={1}>

View File

@@ -22,6 +22,7 @@
CardHeader, CardHeader,
CardTitle, CardTitle,
Code, Code,
CommandPaletteContext,
Container, Container,
getByteUnitString, getByteUnitString,
Heading, Heading,
@@ -105,6 +106,8 @@
{onUserAdminDeleted} {onUserAdminDeleted}
/> />
<CommandPaletteContext commands={[ResetPassword, ResetPinCode, Update, Delete, Restore]} />
<AdminPageLayout <AdminPageLayout
breadcrumbs={[{ title: $t('admin.user_management'), href: AppRoute.ADMIN_USERS }, { title: user.name }]} breadcrumbs={[{ title: $t('admin.user_management'), href: AppRoute.ADMIN_USERS }, { title: user.name }]}
> >