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_user_password_reset": "Are you sure you want to reset {user}'s password?",
"confirm_user_pin_code_reset": "Are you sure you want to reset {user}'s PIN code?",
"copy_config_to_clipboard_description": "Copy the current system config as a JSON object to the clipboard",
"create_job": "Create job",
"cron_expression": "Cron expression",
"cron_expression_description": "Set the scanning interval using the cron format. For more information please refer to e.g. <link>Crontab Guru</link>",
@@ -74,6 +75,8 @@
"disable_login": "Disable login",
"duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search",
"exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
"export_config_as_json_description": "Download the current system config as a JSON file",
"external_libraries_page_description": "Admin external library page",
"external_library_management": "External Library Management",
"face_detection": "Face detection",
"face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
@@ -102,6 +105,7 @@
"image_thumbnail_description": "Small thumbnail with stripped metadata, used when viewing groups of photos like the main timeline",
"image_thumbnail_quality_description": "Thumbnail quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness.",
"image_thumbnail_title": "Thumbnail Settings",
"import_config_from_json_description": "Import system config by uploading a JSON config file",
"job_concurrency": "{job} concurrency",
"job_created": "Job created",
"job_not_concurrency_safe": "This job is not concurrency-safe.",
@@ -110,6 +114,7 @@
"job_status": "Job Status",
"jobs_delayed": "{jobCount, plural, other {# delayed}}",
"jobs_failed": "{jobCount, plural, other {# failed}}",
"jobs_page_description": "Admin jobs page",
"library_created": "Created library: {library}",
"library_deleted": "Library deleted",
"library_details": "Library details",
@@ -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",

10
pnpm-lock.yaml generated
View File

@@ -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

View File

@@ -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",

View File

@@ -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),
};

View File

@@ -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 };

View File

@@ -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),
};

View File

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

View File

@@ -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: {

View File

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

View File

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

View File

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

View File

@@ -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, HStack } from '@immich/ui';
import { Alert, CommandPaletteContext, HStack } from '@immich/ui';
import {
mdiAccountOutline,
mdiBackupRestore,
@@ -215,6 +215,8 @@
);
</script>
<CommandPaletteContext commands={[CopyToClipboard, Upload, Download]} />
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
{#snippet buttons()}
<HStack gap={1}>

View File

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

View File

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