Skip to content

Commit fffee80

Browse files
feat: command palette (#23693)
1 parent 64cd4e9 commit fffee80

File tree

14 files changed

+169
-35
lines changed

14 files changed

+169
-35
lines changed

i18n/en.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,16 @@
6767
"confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.",
6868
"confirm_user_password_reset": "Are you sure you want to reset {user}'s password?",
6969
"confirm_user_pin_code_reset": "Are you sure you want to reset {user}'s PIN code?",
70+
"copy_config_to_clipboard_description": "Copy the current system config as a JSON object to the clipboard",
7071
"create_job": "Create job",
7172
"cron_expression": "Cron expression",
7273
"cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. <link>Crontab Guru</link>",
7374
"cron_expression_presets": "Cron expression presets",
7475
"disable_login": "Disable login",
7576
"duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search",
7677
"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.",
78+
"export_config_as_json_description": "Download the current system config as a JSON file",
79+
"external_libraries_page_description": "Admin external library page",
7780
"external_library_management": "External Library Management",
7881
"face_detection": "Face detection",
7982
"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 @@
102105
"image_thumbnail_description": "Small thumbnail with stripped metadata, used when viewing groups of photos like the main timeline",
103106
"image_thumbnail_quality_description": "Thumbnail quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness.",
104107
"image_thumbnail_title": "Thumbnail Settings",
108+
"import_config_from_json_description": "Import system config by uploading a JSON config file",
105109
"job_concurrency": "{job} concurrency",
106110
"job_created": "Job created",
107111
"job_not_concurrency_safe": "This job is not concurrency-safe.",
@@ -110,6 +114,7 @@
110114
"job_status": "Job Status",
111115
"jobs_delayed": "{jobCount, plural, other {# delayed}}",
112116
"jobs_failed": "{jobCount, plural, other {# failed}}",
117+
"jobs_page_description": "Admin jobs page",
113118
"library_created": "Created library: {library}",
114119
"library_deleted": "Library deleted",
115120
"library_details": "Library details",
@@ -182,6 +187,7 @@
182187
"maintenance_start": "Start maintenance mode",
183188
"maintenance_start_error": "Failed to start maintenance mode.",
184189
"manage_concurrency": "Manage Concurrency",
190+
"manage_concurrency_description": "Navigate to the jobs page to manage job concurrency",
185191
"manage_log_settings": "Manage log settings",
186192
"map_dark_style": "Dark style",
187193
"map_enable_description": "Enable map features",
@@ -287,8 +293,10 @@
287293
"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.",
288294
"server_settings": "Server Settings",
289295
"server_settings_description": "Manage server settings",
296+
"server_stats_page_description": "Admin server statistics page",
290297
"server_welcome_message": "Welcome message",
291298
"server_welcome_message_description": "A message that is displayed on the login page.",
299+
"settings_page_description": "Admin settings page",
292300
"sidecar_job": "Sidecar metadata",
293301
"sidecar_job_description": "Discover or synchronize sidecar metadata from the filesystem",
294302
"slideshow_duration_description": "Number of seconds to display each image",
@@ -407,6 +415,8 @@
407415
"user_restore_scheduled_removal": "Restore user - scheduled removal on {date, date, long}",
408416
"user_settings": "User Settings",
409417
"user_settings_description": "Manage user settings",
418+
"user_successfully_removed": "User {email} has been successfully removed.",
419+
"users_page_description": "Admin users page",
410420
"version_check_enabled_description": "Enable version check",
411421
"version_check_implications": "The version check feature relies on periodic communication with github.com",
412422
"version_check_settings": "Version Check",
@@ -727,6 +737,7 @@
727737
"collapse_all": "Collapse all",
728738
"color": "Color",
729739
"color_theme": "Color theme",
740+
"command": "Command",
730741
"comment_deleted": "Comment deleted",
731742
"comment_options": "Comment options",
732743
"comments_and_likes": "Comments & likes",
@@ -1511,6 +1522,7 @@
15111522
"other_variables": "Other variables",
15121523
"owned": "Owned",
15131524
"owner": "Owner",
1525+
"page": "Page",
15141526
"partner": "Partner",
15151527
"partner_can_access": "{partner} can access",
15161528
"partner_can_access_assets": "All your photos and videos except those in Archived and Deleted",
@@ -2071,6 +2083,7 @@
20712083
"to_select": "to select",
20722084
"to_trash": "Trash",
20732085
"toggle_settings": "Toggle settings",
2086+
"toggle_theme_description": "Toggle theme",
20742087
"total": "Total",
20752088
"total_usage": "Total usage",
20762089
"trash": "Trash",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"@formatjs/icu-messageformat-parser": "^2.9.8",
2929
"@immich/justified-layout-wasm": "^0.4.3",
3030
"@immich/sdk": "file:../open-api/typescript-sdk",
31-
"@immich/ui": "^0.49.1",
31+
"@immich/ui": "^0.49.2",
3232
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
3333
"@mdi/js": "^7.4.47",
3434
"@photo-sphere-viewer/core": "^5.14.0",

web/src/lib/services/library.service.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,22 @@ import { modalManager, toastManager, type ActionItem } from '@immich/ui';
2323
import { mdiPencilOutline, mdiPlusBoxOutline, mdiSync, mdiTrashCanOutline } from '@mdi/js';
2424
import type { MessageFormatter } from 'svelte-i18n';
2525

26-
export const getLibrariesActions = ($t: MessageFormatter) => {
26+
export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResponseDto[]) => {
2727
const ScanAll: ActionItem = {
2828
title: $t('scan_all_libraries'),
29+
type: $t('command'),
2930
icon: mdiSync,
3031
onAction: () => void handleScanAllLibraries(),
32+
shortcuts: { shift: true, key: 'r' },
33+
$if: () => libraries.length > 0,
3134
};
3235

3336
const Create: ActionItem = {
3437
title: $t('create_library'),
38+
type: $t('command'),
3539
icon: mdiPlusBoxOutline,
3640
onAction: () => void handleCreateLibrary(),
41+
shortcuts: { shift: true, key: 'n' },
3742
};
3843

3944
return { ScanAll, Create };
@@ -42,33 +47,41 @@ export const getLibrariesActions = ($t: MessageFormatter) => {
4247
export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponseDto) => {
4348
const Rename: ActionItem = {
4449
icon: mdiPencilOutline,
50+
type: $t('command'),
4551
title: $t('rename'),
4652
onAction: () => void modalManager.show(LibraryRenameModal, { library }),
53+
shortcuts: { key: 'r' },
4754
};
4855

4956
const Delete: ActionItem = {
5057
icon: mdiTrashCanOutline,
58+
type: $t('command'),
5159
title: $t('delete'),
5260
color: 'danger',
5361
onAction: () => void handleDeleteLibrary(library),
62+
shortcuts: { key: 'Backspace' },
5463
};
5564

5665
const AddFolder: ActionItem = {
5766
icon: mdiPlusBoxOutline,
67+
type: $t('command'),
5868
title: $t('add'),
5969
onAction: () => void modalManager.show(LibraryFolderAddModal, { library }),
6070
};
6171

6272
const AddExclusionPattern: ActionItem = {
6373
icon: mdiPlusBoxOutline,
74+
type: $t('command'),
6475
title: $t('add'),
6576
onAction: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }),
6677
};
6778

6879
const Scan: ActionItem = {
6980
icon: mdiSync,
81+
type: $t('command'),
7082
title: $t('scan_library'),
7183
onAction: () => void handleScanLibrary(library),
84+
shortcuts: { shift: true, key: 'r' },
7285
};
7386

7487
return { Rename, Delete, AddFolder, AddExclusionPattern, Scan };
@@ -77,12 +90,14 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
7790
export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryResponseDto, folder: string) => {
7891
const Edit: ActionItem = {
7992
icon: mdiPencilOutline,
93+
type: $t('command'),
8094
title: $t('edit'),
8195
onAction: () => void modalManager.show(LibraryFolderEditModal, { folder, library }),
8296
};
8397

8498
const Delete: ActionItem = {
8599
icon: mdiTrashCanOutline,
100+
type: $t('command'),
86101
title: $t('delete'),
87102
onAction: () => void handleDeleteLibraryFolder(library, folder),
88103
};
@@ -97,12 +112,14 @@ export const getLibraryExclusionPatternActions = (
97112
) => {
98113
const Edit: ActionItem = {
99114
icon: mdiPencilOutline,
115+
type: $t('command'),
100116
title: $t('edit'),
101117
onAction: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
102118
};
103119

104120
const Delete: ActionItem = {
105121
icon: mdiTrashCanOutline,
122+
type: $t('command'),
106123
title: $t('delete'),
107124
onAction: () => void handleDeleteExclusionPattern(library, exclusionPattern),
108125
};

web/src/lib/services/system-config.service.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,33 @@ export const getSystemConfigActions = (
1717
) => {
1818
const CopyToClipboard: ActionItem = {
1919
title: $t('copy_to_clipboard'),
20+
description: $t('admin.copy_config_to_clipboard_description'),
21+
type: $t('command'),
2022
icon: mdiContentCopy,
2123
onAction: () => void handleCopyToClipboard(config),
24+
shortcuts: { shift: true, key: 'c' },
2225
};
2326

2427
const Download: ActionItem = {
2528
title: $t('export_as_json'),
29+
description: $t('admin.export_config_as_json_description'),
30+
type: $t('command'),
2631
icon: mdiDownload,
2732
onAction: () => handleDownloadConfig(config),
33+
shortcuts: [
34+
{ shift: true, key: 's' },
35+
{ shift: true, key: 'd' },
36+
],
2837
};
2938

3039
const Upload: ActionItem = {
3140
title: $t('import_from_json'),
41+
description: $t('admin.import_config_from_json_description'),
42+
type: $t('command'),
3243
icon: mdiUpload,
3344
$if: () => !featureFlags.configFile,
3445
onAction: () => handleUploadConfig(),
46+
shortcuts: { shift: true, key: 'u' },
3547
};
3648

3749
return { CopyToClipboard, Download, Upload };

web/src/lib/services/user-admin.service.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,10 @@ import { get } from 'svelte/store';
3434
export const getUserAdminsActions = ($t: MessageFormatter) => {
3535
const Create: ActionItem = {
3636
title: $t('create_user'),
37+
type: $t('command'),
3738
icon: mdiPlusBoxOutline,
3839
onAction: () => void modalManager.show(UserCreateModal, {}),
40+
shortcuts: { shift: true, key: 'n' },
3941
};
4042

4143
return { Create };
@@ -45,34 +47,39 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
4547
const Update: ActionItem = {
4648
icon: mdiPencilOutline,
4749
title: $t('edit'),
48-
onAction: () => void modalManager.show(UserEditModal, { user }),
50+
onAction: () => modalManager.show(UserEditModal, { user }),
4951
};
5052

5153
const Delete: ActionItem = {
5254
icon: mdiTrashCanOutline,
5355
title: $t('delete'),
56+
type: $t('command'),
5457
color: 'danger',
5558
$if: () => get(authUser).id !== user.id && !user.deletedAt,
56-
onAction: () => void modalManager.show(UserDeleteConfirmModal, { user }),
59+
onAction: () => modalManager.show(UserDeleteConfirmModal, { user }),
60+
shortcuts: { key: 'Backspace' },
5761
};
5862

5963
const Restore: ActionItem = {
6064
icon: mdiDeleteRestore,
6165
title: $t('restore'),
66+
type: $t('command'),
6267
color: 'primary',
6368
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
64-
onAction: () => void modalManager.show(UserRestoreConfirmModal, { user }),
69+
onAction: () => modalManager.show(UserRestoreConfirmModal, { user }),
6570
};
6671

6772
const ResetPassword: ActionItem = {
6873
icon: mdiLockReset,
6974
title: $t('reset_password'),
75+
type: $t('command'),
7076
$if: () => get(authUser).id !== user.id,
7177
onAction: () => void handleResetPasswordUserAdmin(user),
7278
};
7379

7480
const ResetPinCode: ActionItem = {
7581
icon: mdiLockSmart,
82+
type: $t('command'),
7683
title: $t('reset_pin_code'),
7784
onAction: () => void handleResetPinCodeUserAdmin(user),
7885
};

web/src/routes/+layout.svelte

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { afterNavigate, beforeNavigate } from '$app/navigation';
2+
import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
33
import { page } from '$app/state';
44
import { shortcut } from '$lib/actions/shortcut';
55
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
@@ -11,6 +11,7 @@
1111
import { AppRoute } from '$lib/constants';
1212
import { eventManager } from '$lib/managers/event-manager.svelte';
1313
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
14+
import { themeManager } from '$lib/managers/theme-manager.svelte';
1415
import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
1516
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
1617
import { user } from '$lib/stores/user.store';
@@ -19,7 +20,8 @@
1920
import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils';
2021
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
2122
import { isAssetViewerRoute } from '$lib/utils/navigation';
22-
import { modalManager, setTranslations } from '@immich/ui';
23+
import { CommandPaletteContext, modalManager, setTranslations, type ActionItem } from '@immich/ui';
24+
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js';
2325
import { onMount, type Snippet } from 'svelte';
2426
import { t } from 'svelte-i18n';
2527
import '../app.css';
@@ -120,9 +122,57 @@
120122
});
121123
}
122124
});
125+
126+
const userCommands: ActionItem[] = [
127+
{
128+
title: $t('theme'),
129+
description: $t('toggle_theme_description'),
130+
type: $t('command'),
131+
icon: mdiThemeLightDark,
132+
onAction: () => themeManager.toggleTheme(),
133+
shortcuts: { shift: true, key: 't' },
134+
isGlobal: true,
135+
},
136+
];
137+
138+
const adminCommands: ActionItem[] = [
139+
{
140+
title: $t('users'),
141+
description: $t('admin.users_page_description'),
142+
icon: mdiAccountMultipleOutline,
143+
onAction: () => goto(AppRoute.ADMIN_USERS),
144+
},
145+
{
146+
title: $t('jobs'),
147+
description: $t('admin.jobs_page_description'),
148+
icon: mdiSync,
149+
onAction: () => goto(AppRoute.ADMIN_JOBS),
150+
},
151+
{
152+
title: $t('settings'),
153+
description: $t('admin.jobs_page_description'),
154+
icon: mdiCog,
155+
onAction: () => goto(AppRoute.ADMIN_SETTINGS),
156+
},
157+
{
158+
title: $t('external_libraries'),
159+
description: $t('admin.external_libraries_page_description'),
160+
icon: mdiBookshelf,
161+
onAction: () => goto(AppRoute.ADMIN_LIBRARY_MANAGEMENT),
162+
},
163+
{
164+
title: $t('server_stats'),
165+
description: $t('admin.server_stats_page_description'),
166+
icon: mdiServer,
167+
onAction: () => goto(AppRoute.ADMIN_STATS),
168+
},
169+
].map((route) => ({ ...route, type: $t('page'), isGlobal: true, $if: () => $user?.isAdmin }));
170+
171+
const commands = $derived([...userCommands, ...adminCommands]);
123172
</script>
124173

125174
<OnEvents {onReleaseEvent} />
175+
<CommandPaletteContext {commands} />
126176

127177
<svelte:head>
128178
<title>{page.data.meta?.title || 'Web'} - Immich</title>

0 commit comments

Comments
 (0)