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

@@ -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={ScanAll} />
<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 }]}
>