feat: commands (#27546)

This commit is contained in:
Jason Rasmussen 2026-04-14 09:34:46 -04:00 committed by GitHub
parent 1ba0989e15
commit 8fb2c7755d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 56 additions and 69 deletions

View file

@ -1392,6 +1392,7 @@
"light_theme": "Switch to light theme", "light_theme": "Switch to light theme",
"like": "Like", "like": "Like",
"like_deleted": "Like deleted", "like_deleted": "Like deleted",
"link": "Link",
"link_motion_video": "Link motion video", "link_motion_video": "Link motion video",
"link_to_docs": "For more information, refer to the <link>documentation</link>.", "link_to_docs": "For more information, refer to the <link>documentation</link>.",
"link_to_oauth": "Link to OAuth", "link_to_oauth": "Link to OAuth",
@ -1562,6 +1563,8 @@
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"mute_memories": "Mute Memories", "mute_memories": "Mute Memories",
"my_albums": "My albums", "my_albums": "My albums",
"my_immich_description": "Copy current page as a My Immich link",
"my_immich_title": "My Immich link",
"name": "Name", "name": "Name",
"name_or_nickname": "Name or nickname", "name_or_nickname": "Name or nickname",
"name_required": "Name is required", "name_required": "Name is required",
@ -1926,6 +1929,8 @@
"scan_settings": "Scan Settings", "scan_settings": "Scan Settings",
"scanning": "Scanning", "scanning": "Scanning",
"scanning_for_album": "Scanning for album...", "scanning_for_album": "Scanning for album...",
"screencast_mode_description": "Show keyboard and mouse event indicators on the screen",
"screencast_mode_title": "Toggle screencast mode",
"search": "Search", "search": "Search",
"search_albums": "Search albums", "search_albums": "Search albums",
"search_by_context": "Search by context", "search_by_context": "Search by context",

18
pnpm-lock.yaml generated
View file

@ -741,8 +741,8 @@ importers:
specifier: workspace:* specifier: workspace:*
version: link:../open-api/typescript-sdk version: link:../open-api/typescript-sdk
'@immich/ui': '@immich/ui':
specifier: ^0.69.0 specifier: ^0.71.0
version: 0.69.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1) version: 0.71.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)
'@mapbox/mapbox-gl-rtl-text': '@mapbox/mapbox-gl-rtl-text':
specifier: 0.3.0 specifier: 0.3.0
version: 0.3.0 version: 0.3.0
@ -3042,13 +3042,13 @@ packages:
resolution: {integrity: sha512-UWhy/+Lf8C1dJip5wPfFytI3Vq/9UyDKQE1ROjXwVhT6E/CPgBkRLwHPetjYGPJ4o1JVVpRLnEEJCXdvzqVpGw==} resolution: {integrity: sha512-UWhy/+Lf8C1dJip5wPfFytI3Vq/9UyDKQE1ROjXwVhT6E/CPgBkRLwHPetjYGPJ4o1JVVpRLnEEJCXdvzqVpGw==}
hasBin: true hasBin: true
'@immich/svelte-markdown-preprocess@0.2.1': '@immich/svelte-markdown-preprocess@0.3.0':
resolution: {integrity: sha512-mbr/g75lO8Zh+ELCuYrZP0XB4gf2UbK8rJcGYMYxFJJzMMunV+sm9FqtV1dbwW2dpXzCZGz1XPCEZ6oo526TbA==} resolution: {integrity: sha512-6xspWnOgaTi+TasteJgI6DjOGjBQQI30mOYiY/FnyEjczNbrV6r5SFWjNbR+JY+Umn7MsPcZf5yzomK+q5AThg==}
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
'@immich/ui@0.69.0': '@immich/ui@0.71.0':
resolution: {integrity: sha512-YQ+27pGQhzdRBOo/7cHcbXnax5BUrrJeYjUc+VdRYp6KMS8SlGWAKQhvZPdcqiPB332fxJMmpHjV+VqXJJjrqg==} resolution: {integrity: sha512-L5of/qSNlliTLAF4aoHYXsshs+JLeuX9+r685RED6LsZIR0mObb33SJcniGlPqbi5oyELI+7Qp/cEoyS7TPqwg==}
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
@ -15225,16 +15225,16 @@ snapshots:
pg-connection-string: 2.12.0 pg-connection-string: 2.12.0
postgres: 3.4.8 postgres: 3.4.8
'@immich/svelte-markdown-preprocess@0.2.1(svelte@5.55.1)': '@immich/svelte-markdown-preprocess@0.3.0(svelte@5.55.1)':
dependencies: dependencies:
front-matter: 4.0.2 front-matter: 4.0.2
marked: 17.0.5 marked: 17.0.5
node-emoji: 2.2.0 node-emoji: 2.2.0
svelte: 5.55.1 svelte: 5.55.1
'@immich/ui@0.69.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)': '@immich/ui@0.71.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)':
dependencies: dependencies:
'@immich/svelte-markdown-preprocess': 0.2.1(svelte@5.55.1) '@immich/svelte-markdown-preprocess': 0.3.0(svelte@5.55.1)
'@internationalized/date': 3.12.0 '@internationalized/date': 3.12.0
'@mdi/js': 7.4.47 '@mdi/js': 7.4.47
bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1) bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)

View file

@ -27,7 +27,7 @@
"@formatjs/icu-messageformat-parser": "^3.0.0", "@formatjs/icu-messageformat-parser": "^3.0.0",
"@immich/justified-layout-wasm": "^0.4.3", "@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "workspace:*", "@immich/sdk": "workspace:*",
"@immich/ui": "^0.69.0", "@immich/ui": "^0.71.0",
"@mapbox/mapbox-gl-rtl-text": "0.3.0", "@mapbox/mapbox-gl-rtl-text": "0.3.0",
"@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

@ -90,7 +90,6 @@
const Close: ActionItem = $derived({ const Close: ActionItem = $derived({
title: $t('go_back'), title: $t('go_back'),
type: $t('assets'),
icon: languageManager.rtl ? mdiArrowRight : mdiArrowLeft, icon: languageManager.rtl ? mdiArrowRight : mdiArrowLeft,
$if: () => !!onClose && !assetViewerManager.isFaceEditMode, $if: () => !!onClose && !assetViewerManager.isFaceEditMode,
onAction: () => onClose?.(), onAction: () => onClose?.(),

View file

@ -46,7 +46,6 @@ export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) =
const Share: ActionItem = { const Share: ActionItem = {
title: $t('share'), title: $t('share'),
type: $t('command'),
icon: mdiShareVariantOutline, icon: mdiShareVariantOutline,
$if: () => isOwned, $if: () => isOwned,
onAction: () => modalManager.show(AlbumOptionsModal, { album }), onAction: () => modalManager.show(AlbumOptionsModal, { album }),
@ -54,7 +53,6 @@ export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) =
const AddUsers: ActionItem = { const AddUsers: ActionItem = {
title: $t('invite_people'), title: $t('invite_people'),
type: $t('command'),
icon: mdiPlus, icon: mdiPlus,
color: 'primary', color: 'primary',
onAction: () => modalManager.show(AlbumAddUsersModal, { album }), onAction: () => modalManager.show(AlbumAddUsersModal, { album }),
@ -62,7 +60,6 @@ export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) =
const CreateSharedLink: ActionItem = { const CreateSharedLink: ActionItem = {
title: $t('create_link'), title: $t('create_link'),
type: $t('command'),
icon: mdiLink, icon: mdiLink,
color: 'primary', color: 'primary',
onAction: () => modalManager.show(SharedLinkCreateModal, { albumId: album.id }), onAction: () => modalManager.show(SharedLinkCreateModal, { albumId: album.id }),
@ -74,7 +71,6 @@ export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) =
export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponseDto, assets: TimelineAsset[]) => { export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponseDto, assets: TimelineAsset[]) => {
const AddAssets: ActionItem = { const AddAssets: ActionItem = {
title: $t('add_assets'), title: $t('add_assets'),
type: $t('command'),
color: 'primary', color: 'primary',
icon: mdiPlusBoxOutline, icon: mdiPlusBoxOutline,
$if: () => assets.length > 0, $if: () => assets.length > 0,
@ -89,7 +85,6 @@ export const getAlbumAssetsActions = ($t: MessageFormatter, album: AlbumResponse
const Upload: ActionItem = { const Upload: ActionItem = {
title: $t('select_from_computer'), title: $t('select_from_computer'),
description: $t('album_upload_assets'), description: $t('album_upload_assets'),
type: $t('command'),
icon: mdiUpload, icon: mdiUpload,
onAction: () => void openFileUploadDialog({ albumId: album.id }), onAction: () => void openFileUploadDialog({ albumId: album.id }),
}; };

View file

@ -129,7 +129,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
const PlayMotionPhoto: ActionItem = { const PlayMotionPhoto: ActionItem = {
title: $t('play_motion_photo'), title: $t('play_motion_photo'),
icon: mdiMotionPlayOutline, icon: mdiMotionPlayOutline,
type: $t('assets'),
$if: () => !!asset.livePhotoVideoId && !assetViewerManager.isPlayingMotionPhoto, $if: () => !!asset.livePhotoVideoId && !assetViewerManager.isPlayingMotionPhoto,
onAction: () => { onAction: () => {
assetViewerManager.isPlayingMotionPhoto = true; assetViewerManager.isPlayingMotionPhoto = true;
@ -139,7 +138,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
const StopMotionPhoto: ActionItem = { const StopMotionPhoto: ActionItem = {
title: $t('stop_motion_photo'), title: $t('stop_motion_photo'),
icon: mdiMotionPauseOutline, icon: mdiMotionPauseOutline,
type: $t('assets'),
$if: () => !!asset.livePhotoVideoId && assetViewerManager.isPlayingMotionPhoto, $if: () => !!asset.livePhotoVideoId && assetViewerManager.isPlayingMotionPhoto,
onAction: () => { onAction: () => {
assetViewerManager.isPlayingMotionPhoto = false; assetViewerManager.isPlayingMotionPhoto = false;
@ -149,7 +147,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
const Favorite: ActionItem = { const Favorite: ActionItem = {
title: $t('to_favorite'), title: $t('to_favorite'),
icon: mdiHeartOutline, icon: mdiHeartOutline,
type: $t('assets'),
$if: () => isOwner && !asset.isFavorite, $if: () => isOwner && !asset.isFavorite,
onAction: () => handleFavorite(asset), onAction: () => handleFavorite(asset),
shortcuts: [{ key: 'f' }], shortcuts: [{ key: 'f' }],
@ -158,7 +155,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
const Unfavorite: ActionItem = { const Unfavorite: ActionItem = {
title: $t('unfavorite'), title: $t('unfavorite'),
icon: mdiHeart, icon: mdiHeart,
type: $t('assets'),
$if: () => isOwner && asset.isFavorite, $if: () => isOwner && asset.isFavorite,
onAction: () => handleUnfavorite(asset), onAction: () => handleUnfavorite(asset),
shortcuts: [{ key: 'f' }], shortcuts: [{ key: 'f' }],
@ -175,7 +171,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
const Offline: ActionItem = { const Offline: ActionItem = {
title: $t('asset_offline'), title: $t('asset_offline'),
icon: mdiAlertOutline, icon: mdiAlertOutline,
type: $t('assets'),
color: 'danger', color: 'danger',
$if: () => !!asset.isOffline, $if: () => !!asset.isOffline,
onAction: () => assetViewerManager.toggleDetailPanel(), onAction: () => assetViewerManager.toggleDetailPanel(),
@ -205,7 +200,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
const Info: ActionItem = { const Info: ActionItem = {
title: $t('info'), title: $t('info'),
icon: mdiInformationOutline, icon: mdiInformationOutline,
type: $t('assets'),
$if: () => asset.hasMetadata, $if: () => asset.hasMetadata,
onAction: () => assetViewerManager.toggleDetailPanel(), onAction: () => assetViewerManager.toggleDetailPanel(),
shortcuts: { key: 'i' }, shortcuts: { key: 'i' },
@ -223,7 +217,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
const TagPeople: ActionItem = { const TagPeople: ActionItem = {
title: $t('tag_people'), title: $t('tag_people'),
icon: mdiFaceRecognition, icon: mdiFaceRecognition,
type: $t('assets'),
$if: () => isOwner && asset.type === AssetTypeEnum.Image && !asset.isTrashed, $if: () => isOwner && asset.type === AssetTypeEnum.Image && !asset.isTrashed,
onAction: () => assetViewerManager.toggleFaceEditMode(), onAction: () => assetViewerManager.toggleFaceEditMode(),
shortcuts: { key: 'p' }, shortcuts: { key: 'p' },

View file

@ -16,14 +16,12 @@ import type { MessageFormatter } from 'svelte-i18n';
export const getDatabaseBackupActions = ($t: MessageFormatter, filename: string) => { export const getDatabaseBackupActions = ($t: MessageFormatter, filename: string) => {
const Download: ActionItem = { const Download: ActionItem = {
type: $t('command'),
title: $t('download'), title: $t('download'),
icon: mdiDownload, icon: mdiDownload,
onAction: () => handleDownloadDatabaseBackup(filename), onAction: () => handleDownloadDatabaseBackup(filename),
}; };
const Delete: ActionItem = { const Delete: ActionItem = {
type: $t('command'),
title: $t('delete'), title: $t('delete'),
icon: mdiTrashCanOutline, icon: mdiTrashCanOutline,
color: 'danger', color: 'danger',

View file

@ -26,7 +26,6 @@ import type { MessageFormatter } from 'svelte-i18n';
export const getLibrariesActions = ($t: MessageFormatter) => { export const getLibrariesActions = ($t: MessageFormatter) => {
const ScanAll: ActionItem = { const ScanAll: ActionItem = {
title: $t('scan_all_libraries'), title: $t('scan_all_libraries'),
type: $t('command'),
icon: mdiSync, icon: mdiSync,
onAction: () => handleScanAllLibraries(), onAction: () => handleScanAllLibraries(),
shortcuts: { shift: true, key: 'r' }, shortcuts: { shift: true, key: 'r' },
@ -34,7 +33,6 @@ export const getLibrariesActions = ($t: MessageFormatter) => {
const Create: ActionItem = { const Create: ActionItem = {
title: $t('create_library'), title: $t('create_library'),
type: $t('command'),
icon: mdiPlusBoxOutline, icon: mdiPlusBoxOutline,
onAction: () => goto(Route.newLibrary()), onAction: () => goto(Route.newLibrary()),
shortcuts: { shift: true, key: 'n' }, shortcuts: { shift: true, key: 'n' },
@ -46,14 +44,12 @@ export const getLibrariesActions = ($t: MessageFormatter) => {
export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponseDto) => { export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponseDto) => {
const Detail: ActionItem = { const Detail: ActionItem = {
icon: mdiInformationOutline, icon: mdiInformationOutline,
type: $t('command'),
title: $t('details'), title: $t('details'),
onAction: () => goto(Route.viewLibrary(library)), onAction: () => goto(Route.viewLibrary(library)),
}; };
const Edit: ActionItem = { const Edit: ActionItem = {
icon: mdiPencilOutline, icon: mdiPencilOutline,
type: $t('command'),
title: $t('edit'), title: $t('edit'),
onAction: () => goto(Route.editLibrary(library)), onAction: () => goto(Route.editLibrary(library)),
shortcuts: { key: 'r' }, shortcuts: { key: 'r' },
@ -61,7 +57,6 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
const Delete: ActionItem = { const Delete: ActionItem = {
icon: mdiTrashCanOutline, icon: mdiTrashCanOutline,
type: $t('command'),
title: $t('delete'), title: $t('delete'),
color: 'danger', color: 'danger',
onAction: () => handleDeleteLibrary(library), onAction: () => handleDeleteLibrary(library),
@ -71,21 +66,18 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
const AddFolder: ActionItem = { const AddFolder: ActionItem = {
icon: mdiPlusBoxOutline, icon: mdiPlusBoxOutline,
type: $t('command'),
title: $t('add'), title: $t('add'),
onAction: () => modalManager.show(LibraryFolderAddModal, { library }), onAction: () => modalManager.show(LibraryFolderAddModal, { library }),
}; };
const AddExclusionPattern: ActionItem = { const AddExclusionPattern: ActionItem = {
icon: mdiPlusBoxOutline, icon: mdiPlusBoxOutline,
type: $t('command'),
title: $t('add'), title: $t('add'),
onAction: () => modalManager.show(LibraryExclusionPatternAddModal, { library }), onAction: () => 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: () => handleScanLibrary(library), onAction: () => handleScanLibrary(library),
shortcuts: { shift: true, key: 'r' }, shortcuts: { shift: true, key: 'r' },
@ -97,14 +89,12 @@ 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: () => modalManager.show(LibraryFolderEditModal, { folder, library }), onAction: () => 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: () => handleDeleteLibraryFolder(library, folder), onAction: () => handleDeleteLibraryFolder(library, folder),
}; };
@ -119,14 +109,12 @@ export const getLibraryExclusionPatternActions = (
) => { ) => {
const Edit: ActionItem = { const Edit: ActionItem = {
icon: mdiPencilOutline, icon: mdiPencilOutline,
type: $t('command'),
title: $t('edit'), title: $t('edit'),
onAction: () => modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }), onAction: () => 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: () => handleDeleteExclusionPattern(library, exclusionPattern), onAction: () => handleDeleteExclusionPattern(library, exclusionPattern),
}; };

View file

@ -64,7 +64,6 @@ export const getQueuesActions = ($t: MessageFormatter, queues: QueueResponseDto[
const CreateJob: ActionItem = { const CreateJob: ActionItem = {
icon: mdiPlus, icon: mdiPlus,
title: $t('admin.create_job'), title: $t('admin.create_job'),
type: $t('command'),
shortcuts: { shift: true, key: 'n' }, shortcuts: { shift: true, key: 'n' },
onAction: () => modalManager.show(JobCreateModal, {}), onAction: () => modalManager.show(JobCreateModal, {}),
}; };
@ -73,7 +72,6 @@ export const getQueuesActions = ($t: MessageFormatter, queues: QueueResponseDto[
icon: mdiCog, icon: mdiCog,
title: $t('admin.manage_concurrency'), title: $t('admin.manage_concurrency'),
description: $t('admin.manage_concurrency_description'), description: $t('admin.manage_concurrency_description'),
type: $t('page'),
onAction: () => goto(Route.systemSettings({ isOpen: OpenQueryParam.JOB })), onAction: () => goto(Route.systemSettings({ isOpen: OpenQueryParam.JOB })),
}; };

View file

@ -18,7 +18,6 @@ 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'), description: $t('admin.copy_config_to_clipboard_description'),
type: $t('command'),
icon: mdiContentCopy, icon: mdiContentCopy,
onAction: () => handleCopyToClipboard(config), onAction: () => handleCopyToClipboard(config),
shortcuts: { shift: true, key: 'c' }, shortcuts: { shift: true, key: 'c' },
@ -27,7 +26,6 @@ export const getSystemConfigActions = (
const Download: ActionItem = { const Download: ActionItem = {
title: $t('export_as_json'), title: $t('export_as_json'),
description: $t('admin.export_config_as_json_description'), description: $t('admin.export_config_as_json_description'),
type: $t('command'),
icon: mdiDownload, icon: mdiDownload,
onAction: () => handleDownloadConfig(config), onAction: () => handleDownloadConfig(config),
shortcuts: [ shortcuts: [
@ -39,7 +37,6 @@ export const getSystemConfigActions = (
const Upload: ActionItem = { const Upload: ActionItem = {
title: $t('import_from_json'), title: $t('import_from_json'),
description: $t('admin.import_config_from_json_description'), 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(),

View file

@ -36,7 +36,6 @@ import type { MessageFormatter } from 'svelte-i18n';
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: () => goto(Route.newUser()), onAction: () => goto(Route.newUser()),
shortcuts: { shift: true, key: 'n' }, shortcuts: { shift: true, key: 'n' },
@ -61,7 +60,6 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
const Delete: ActionItem = { const Delete: ActionItem = {
icon: mdiTrashCanOutline, icon: mdiTrashCanOutline,
title: $t('delete'), title: $t('delete'),
type: $t('command'),
color: 'danger', color: 'danger',
$if: () => authManager.user.id !== user.id && !user.deletedAt, $if: () => authManager.user.id !== user.id && !user.deletedAt,
onAction: () => modalManager.show(UserDeleteConfirmModal, { user }), onAction: () => modalManager.show(UserDeleteConfirmModal, { user }),
@ -75,7 +73,6 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
const Restore: HeaderButtonActionItem = { const Restore: HeaderButtonActionItem = {
icon: mdiDeleteRestore, icon: mdiDeleteRestore,
title: $t('restore'), title: $t('restore'),
type: $t('command'),
color: 'primary', color: 'primary',
data: { data: {
title: $t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt!) } }), title: $t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt!) } }),
@ -87,14 +84,12 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
const ResetPassword: ActionItem = { const ResetPassword: ActionItem = {
icon: mdiLockReset, icon: mdiLockReset,
title: $t('reset_password'), title: $t('reset_password'),
type: $t('command'),
$if: () => authManager.user.id !== user.id, $if: () => authManager.user.id !== user.id,
onAction: () => handleResetPasswordUserAdmin(user), onAction: () => 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: () => handleResetPinCodeUserAdmin(user), onAction: () => handleResetPinCodeUserAdmin(user),
}; };

View file

@ -315,7 +315,6 @@
const Close = $derived({ const Close = $derived({
title: $t('go_back'), title: $t('go_back'),
type: $t('command'),
icon: mdiArrowLeft, icon: mdiArrowLeft,
onAction: handleEscape, onAction: handleEscape,
$if: () => !assetViewerManager.isViewing, $if: () => !assetViewerManager.isViewing,

View file

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate, beforeNavigate, goto } 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 DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte'; import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
import ErrorLayout from '$lib/components/layouts/ErrorLayout.svelte'; import ErrorLayout from '$lib/components/layouts/ErrorLayout.svelte';
import OnEvents from '$lib/components/OnEvents.svelte'; import OnEvents from '$lib/components/OnEvents.svelte';
@ -22,15 +21,27 @@
import { isAssetViewerRoute } from '$lib/utils/navigation'; import { isAssetViewerRoute } from '$lib/utils/navigation';
import { getServerConfig } from '@immich/sdk'; import { getServerConfig } from '@immich/sdk';
import { import {
CommandPaletteDefaultProvider, CommandPaletteProvider,
TooltipProvider, defaultProvider,
modalManager, modalManager,
screencastManager,
ScreencastOverlay,
setLocale, setLocale,
setTranslations, setTranslations,
siteCommands,
toastManager, toastManager,
TooltipProvider,
type ActionItem, type ActionItem,
} from '@immich/ui'; } from '@immich/ui';
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js'; import {
mdiAccountMultipleOutline,
mdiBookshelf,
mdiCog,
mdiKeyboard,
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 { get } from 'svelte/store'; import { get } from 'svelte/store';
@ -139,26 +150,37 @@
} }
}; };
const userCommands: ActionItem[] = [ const commands: ActionItem[] = [
{ {
title: $t('theme'), title: $t('theme'),
description: $t('toggle_theme_description'), description: $t('toggle_theme_description'),
type: $t('command'),
icon: mdiThemeLightDark, icon: mdiThemeLightDark,
onAction: () => themeManager.toggleTheme(), onAction: () => themeManager.toggleTheme(),
shortcuts: { shift: true, key: 't' }, shortcuts: { shift: true, key: 't' },
}, },
{
title: $t('screencast_mode_title'),
description: $t('screencast_mode_description'),
icon: mdiKeyboard,
onAction: () => screencastManager.toggle(),
},
{
title: $t('my_immich_title'),
description: $t('my_immich_description'),
onAction: () => copyToClipboard(getMyImmichLink().toString()),
shortcuts: { ctrl: true, shift: true, key: 'm' },
},
]; ];
const adminCommands: ActionItem[] = [ const adminPages: ActionItem[] = [
{ {
title: $t('users'), title: $t('admin.user_management'),
description: $t('admin.users_page_description'), description: $t('admin.users_page_description'),
icon: mdiAccountMultipleOutline, icon: mdiAccountMultipleOutline,
onAction: () => goto(Route.users()), onAction: () => goto(Route.users()),
}, },
{ {
title: $t('settings'), title: $t('admin.system_settings'),
description: $t('admin.settings_page_description'), description: $t('admin.settings_page_description'),
icon: mdiCog, icon: mdiCog,
onAction: () => goto(Route.systemSettings()), onAction: () => goto(Route.systemSettings()),
@ -167,7 +189,6 @@
title: $t('admin.queues'), title: $t('admin.queues'),
description: $t('admin.queues_page_description'), description: $t('admin.queues_page_description'),
icon: mdiSync, icon: mdiSync,
type: $t('page'),
onAction: () => goto(Route.queues()), onAction: () => goto(Route.queues()),
}, },
{ {
@ -182,14 +203,11 @@
icon: mdiServer, icon: mdiServer,
onAction: () => goto(Route.systemStatistics()), onAction: () => goto(Route.systemStatistics()),
}, },
].map((route) => ({ ...route, type: $t('page'), $if: () => authManager.user.isAdmin })); ].map((route) => ({ ...route, $if: () => authManager.authenticated && authManager.user.isAdmin }));
const commands = $derived([...userCommands, ...adminCommands]);
</script> </script>
<OnEvents {onWebsocketConnect} /> <OnEvents {onWebsocketConnect} />
<CommandPaletteDefaultProvider name="Global" actions={commands} />
<VersionAnnouncement /> <VersionAnnouncement />
<svelte:head> <svelte:head>
@ -231,13 +249,6 @@
{/if} {/if}
</svelte:head> </svelte:head>
<svelte:document
use:shortcut={{
shortcut: { ctrl: true, shift: true, key: 'm' },
onShortcut: () => copyToClipboard(getMyImmichLink().toString()),
}}
/>
<TooltipProvider> <TooltipProvider>
{#if page.data.error} {#if page.data.error}
<ErrorLayout error={page.data.error}></ErrorLayout> <ErrorLayout error={page.data.error}></ErrorLayout>
@ -251,4 +262,13 @@
<DownloadPanel /> <DownloadPanel />
<UploadPanel /> <UploadPanel />
<ScreencastOverlay />
<CommandPaletteProvider
providers={[
defaultProvider({ name: $t('command'), actions: commands }),
defaultProvider({ name: $t('page'), actions: adminPages }),
defaultProvider({ name: $t('link'), actions: siteCommands }),
]}
/>
</TooltipProvider> </TooltipProvider>