diff --git a/i18n/en.json b/i18n/en.json
index 6495e4521515a..7eb9ffbef6767 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -78,7 +78,6 @@
"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.",
"facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 43d2848e1648f..33afa6bc4a83c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -717,8 +717,8 @@ importers:
specifier: file:../open-api/typescript-sdk
version: link:../open-api/typescript-sdk
'@immich/ui':
- specifier: ^0.49.2
- version: 0.49.3(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)
+ specifier: ^0.50.0
+ version: 0.50.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)
'@mapbox/mapbox-gl-rtl-text':
specifier: 0.2.3
version: 0.2.3(mapbox-gl@1.13.3)
@@ -2989,8 +2989,8 @@ packages:
peerDependencies:
svelte: ^5.0.0
- '@immich/ui@0.49.3':
- resolution: {integrity: sha512-joqT72Y6gmGK6z25Suzr2VhYANrLo43g20T4UHmbQenz/z/Ax6sl1Ao9SjIOwEkKMm9N3Txoh7WOOzmHVl04OA==}
+ '@immich/ui@0.50.0':
+ resolution: {integrity: sha512-7AW9SRZTAgal8xlkUAxm7o4+pSG7HcKb+Bh9JpWLaDRRdGyPCZMmsNa9CjZglOQ7wkAD07tQ9u4+zezBLe0dlQ==}
peerDependencies:
svelte: ^5.0.0
@@ -14700,7 +14700,7 @@ snapshots:
dependencies:
svelte: 5.45.2
- '@immich/ui@0.49.3(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)':
+ '@immich/ui@0.50.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)':
dependencies:
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2)
'@internationalized/date': 3.10.0
diff --git a/web/package.json b/web/package.json
index 2e7b74015344f..cfa0f5cc306a6 100644
--- a/web/package.json
+++ b/web/package.json
@@ -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.2",
+ "@immich/ui": "^0.50.0",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.14.0",
diff --git a/web/src/lib/components/HeaderActionButton.svelte b/web/src/lib/components/HeaderActionButton.svelte
new file mode 100644
index 0000000000000..542c22ba43e0a
--- /dev/null
+++ b/web/src/lib/components/HeaderActionButton.svelte
@@ -0,0 +1,24 @@
+
+
+{#if action.$if?.() ?? true}
+
+{/if}
diff --git a/web/src/lib/components/HeaderButton.svelte b/web/src/lib/components/HeaderButton.svelte
deleted file mode 100644
index c4189c06c0851..0000000000000
--- a/web/src/lib/components/HeaderButton.svelte
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-{#if action.$if?.() ?? true}
-
-{/if}
diff --git a/web/src/lib/components/layouts/AdminPageLayout.svelte b/web/src/lib/components/layouts/AdminPageLayout.svelte
index 45d21c9139bc5..d63e306853258 100644
--- a/web/src/lib/components/layouts/AdminPageLayout.svelte
+++ b/web/src/lib/components/layouts/AdminPageLayout.svelte
@@ -1,19 +1,33 @@
@@ -24,11 +38,37 @@
-
+
+
+
+
+ {#if actions.length > 0}
+
+
+ {#each actions as action, i (i)}
+ {#if !isMenuItemType(action) && (action.$if?.() ?? true)}
+
+ {/if}
+ {/each}
+
+
+
+
+ {/if}
+
{@render children?.()}
-
+
diff --git a/web/src/lib/components/layouts/TitleLayout.svelte b/web/src/lib/components/layouts/TitleLayout.svelte
deleted file mode 100644
index 2d867bab2fcc1..0000000000000
--- a/web/src/lib/components/layouts/TitleLayout.svelte
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
- {@render buttons?.()}
-
- {@render children?.()}
-
diff --git a/web/src/lib/services/library.service.ts b/web/src/lib/services/library.service.ts
index 8b4d35a5f67ca..d20eae6af6384 100644
--- a/web/src/lib/services/library.service.ts
+++ b/web/src/lib/services/library.service.ts
@@ -28,7 +28,7 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
title: $t('scan_all_libraries'),
type: $t('command'),
icon: mdiSync,
- onAction: () => void handleScanAllLibraries(),
+ onAction: () => handleScanAllLibraries(),
shortcuts: { shift: true, key: 'r' },
$if: () => libraries.length > 0,
};
@@ -37,7 +37,7 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
title: $t('create_library'),
type: $t('command'),
icon: mdiPlusBoxOutline,
- onAction: () => void handleCreateLibrary(),
+ onAction: () => handleCreateLibrary(),
shortcuts: { shift: true, key: 'n' },
};
@@ -49,7 +49,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
icon: mdiPencilOutline,
type: $t('command'),
title: $t('rename'),
- onAction: () => void modalManager.show(LibraryRenameModal, { library }),
+ onAction: () => modalManager.show(LibraryRenameModal, { library }),
shortcuts: { key: 'r' },
};
@@ -58,7 +58,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
type: $t('command'),
title: $t('delete'),
color: 'danger',
- onAction: () => void handleDeleteLibrary(library),
+ onAction: () => handleDeleteLibrary(library),
shortcuts: { key: 'Backspace' },
};
@@ -66,21 +66,21 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
icon: mdiPlusBoxOutline,
type: $t('command'),
title: $t('add'),
- onAction: () => void modalManager.show(LibraryFolderAddModal, { library }),
+ onAction: () => modalManager.show(LibraryFolderAddModal, { library }),
};
const AddExclusionPattern: ActionItem = {
icon: mdiPlusBoxOutline,
type: $t('command'),
title: $t('add'),
- onAction: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }),
+ onAction: () => modalManager.show(LibraryExclusionPatternAddModal, { library }),
};
const Scan: ActionItem = {
icon: mdiSync,
type: $t('command'),
title: $t('scan_library'),
- onAction: () => void handleScanLibrary(library),
+ onAction: () => handleScanLibrary(library),
shortcuts: { shift: true, key: 'r' },
};
@@ -92,14 +92,14 @@ export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryRe
icon: mdiPencilOutline,
type: $t('command'),
title: $t('edit'),
- onAction: () => void modalManager.show(LibraryFolderEditModal, { folder, library }),
+ onAction: () => modalManager.show(LibraryFolderEditModal, { folder, library }),
};
const Delete: ActionItem = {
icon: mdiTrashCanOutline,
type: $t('command'),
title: $t('delete'),
- onAction: () => void handleDeleteLibraryFolder(library, folder),
+ onAction: () => handleDeleteLibraryFolder(library, folder),
};
return { Edit, Delete };
@@ -114,14 +114,14 @@ export const getLibraryExclusionPatternActions = (
icon: mdiPencilOutline,
type: $t('command'),
title: $t('edit'),
- onAction: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
+ onAction: () => modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
};
const Delete: ActionItem = {
icon: mdiTrashCanOutline,
type: $t('command'),
title: $t('delete'),
- onAction: () => void handleDeleteExclusionPattern(library, exclusionPattern),
+ onAction: () => handleDeleteExclusionPattern(library, exclusionPattern),
};
return { Edit, Delete };
@@ -273,7 +273,7 @@ const handleDeleteLibraryFolder = async (library: LibraryResponseDto, folder: st
});
if (!confirmed) {
- return false;
+ return;
}
try {
@@ -285,10 +285,7 @@ const handleDeleteLibraryFolder = async (library: LibraryResponseDto, folder: st
toastManager.success($t('admin.library_updated'));
} catch (error) {
handleError(error, $t('errors.unable_to_update_library'));
- return false;
}
-
- return true;
};
export const handleAddLibraryExclusionPattern = async (library: LibraryResponseDto, exclusionPattern: string) => {
@@ -345,9 +342,8 @@ const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusi
const $t = await getFormatter();
const confirmed = await modalManager.showDialog({ prompt: $t('admin.library_remove_exclusion_pattern_prompt') });
-
if (!confirmed) {
- return false;
+ return;
}
try {
@@ -361,8 +357,5 @@ const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusi
toastManager.success($t('admin.library_updated'));
} catch (error) {
handleError(error, $t('errors.unable_to_update_library'));
- return false;
}
-
- return true;
};
diff --git a/web/src/lib/services/queue.service.ts b/web/src/lib/services/queue.service.ts
index 2372461d1abb5..46219ef22a461 100644
--- a/web/src/lib/services/queue.service.ts
+++ b/web/src/lib/services/queue.service.ts
@@ -1,11 +1,20 @@
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte';
+import { queueManager } from '$lib/managers/queue-manager.svelte';
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
-import { user } from '$lib/stores/user.store';
+import type { HeaderButtonActionItem } from '$lib/types';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
-import { emptyQueue, getQueue, QueueName, updateQueue, type QueueResponseDto } from '@immich/sdk';
+import {
+ emptyQueue,
+ getQueue,
+ QueueCommand,
+ QueueName,
+ runQueueCommandLegacy,
+ updateQueue,
+ type QueueResponseDto,
+} from '@immich/sdk';
import { modalManager, toastManager, type ActionItem, type IconLike } from '@immich/ui';
import {
mdiClose,
@@ -23,7 +32,6 @@ import {
mdiPlay,
mdiPlus,
mdiStateMachine,
- mdiSync,
mdiTable,
mdiTagFaces,
mdiTrashCanOutline,
@@ -31,7 +39,6 @@ import {
mdiVideo,
} from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
-import { get } from 'svelte/store';
type QueueItem = {
icon: IconLike;
@@ -39,15 +46,17 @@ type QueueItem = {
subtitle?: string;
};
-export const getQueuesActions = ($t: MessageFormatter) => {
- const ViewQueues: ActionItem = {
- title: $t('admin.queues'),
- description: $t('admin.queues_page_description'),
- icon: mdiSync,
- type: $t('page'),
- isGlobal: true,
- $if: () => get(user)?.isAdmin,
- onAction: () => goto(AppRoute.ADMIN_QUEUES),
+export const getQueuesActions = ($t: MessageFormatter, queues: QueueResponseDto[] | undefined) => {
+ const pausedQueues = (queues ?? []).filter(({ isPaused }) => isPaused).map(({ name }) => name);
+
+ const ResumePaused: HeaderButtonActionItem = {
+ title: $t('resume_paused_jobs', { values: { count: pausedQueues.length } }),
+ $if: () => pausedQueues.length > 0,
+ icon: mdiPlay,
+ onAction: () => handleResumePausedJobs(pausedQueues),
+ data: {
+ title: pausedQueues.join(', '),
+ },
};
const CreateJob: ActionItem = {
@@ -68,7 +77,7 @@ export const getQueuesActions = ($t: MessageFormatter) => {
onAction: () => goto(`${AppRoute.ADMIN_SETTINGS}?isOpen=job`),
};
- return { ViewQueues, ManageConcurrency, CreateJob };
+ return { ResumePaused, ManageConcurrency, CreateJob };
};
export const getQueueActions = ($t: MessageFormatter, queue: QueueResponseDto) => {
@@ -126,6 +135,19 @@ export const handleEmptyQueue = async (queue: QueueResponseDto) => {
}
};
+const handleResumePausedJobs = async (queues: QueueName[]) => {
+ const $t = await getFormatter();
+
+ try {
+ for (const name of queues) {
+ await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
+ }
+ await queueManager.refresh();
+ } catch (error) {
+ handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
+ }
+};
+
const handleRemoveFailedJobs = async (queue: QueueResponseDto) => {
const $t = await getFormatter();
diff --git a/web/src/lib/services/shared-link.service.ts b/web/src/lib/services/shared-link.service.ts
index 4e6a942682672..cbea6ddd9db8a 100644
--- a/web/src/lib/services/shared-link.service.ts
+++ b/web/src/lib/services/shared-link.service.ts
@@ -24,26 +24,26 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin
const Edit: ActionItem = {
title: $t('edit_link'),
icon: mdiPencilOutline,
- onAction: () => void goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`),
+ onAction: () => goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`),
};
const Delete: ActionItem = {
title: $t('delete_link'),
icon: mdiTrashCanOutline,
color: 'danger',
- onAction: () => void handleDeleteSharedLink(sharedLink),
+ onAction: () => handleDeleteSharedLink(sharedLink),
};
const Copy: ActionItem = {
title: $t('copy_link'),
icon: mdiContentCopy,
- onAction: () => void copyToClipboard(asUrl(sharedLink)),
+ onAction: () => copyToClipboard(asUrl(sharedLink)),
};
const ViewQrCode: ActionItem = {
title: $t('view_qr_code'),
icon: mdiQrcode,
- onAction: () => void handleShowSharedLinkQrCode(sharedLink),
+ onAction: () => handleShowSharedLinkQrCode(sharedLink),
};
return { Edit, Delete, Copy, ViewQrCode };
@@ -88,7 +88,7 @@ export const handleUpdateSharedLink = async (sharedLink: SharedLinkResponseDto,
}
};
-export const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto): Promise => {
+const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto) => {
const $t = await getFormatter();
const success = await modalManager.showDialog({
title: $t('delete_shared_link'),
@@ -96,17 +96,15 @@ export const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto):
confirmText: $t('delete'),
});
if (!success) {
- return false;
+ return;
}
try {
await removeSharedLink({ id: sharedLink.id });
eventManager.emit('SharedLinkDelete', sharedLink);
toastManager.success($t('deleted_shared_link'));
- return true;
} catch (error) {
handleError(error, $t('errors.unable_to_delete_shared_link'));
- return false;
}
};
diff --git a/web/src/lib/services/system-config.service.ts b/web/src/lib/services/system-config.service.ts
index ffd0094c722bc..b8c7716d477af 100644
--- a/web/src/lib/services/system-config.service.ts
+++ b/web/src/lib/services/system-config.service.ts
@@ -20,7 +20,7 @@ export const getSystemConfigActions = (
description: $t('admin.copy_config_to_clipboard_description'),
type: $t('command'),
icon: mdiContentCopy,
- onAction: () => void handleCopyToClipboard(config),
+ onAction: () => handleCopyToClipboard(config),
shortcuts: { shift: true, key: 'c' },
};
diff --git a/web/src/lib/services/user-admin.service.ts b/web/src/lib/services/user-admin.service.ts
index 7a49f2fbe34ec..997a43fc7ff62 100644
--- a/web/src/lib/services/user-admin.service.ts
+++ b/web/src/lib/services/user-admin.service.ts
@@ -1,11 +1,13 @@
import { goto } from '$app/navigation';
import { eventManager } from '$lib/managers/event-manager.svelte';
+import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
import UserEditModal from '$lib/modals/UserEditModal.svelte';
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
import { user as authUser } from '$lib/stores/user.store';
+import type { HeaderButtonActionItem } from '$lib/types';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import {
@@ -28,6 +30,7 @@ import {
mdiPlusBoxOutline,
mdiTrashCanOutline,
} from '@mdi/js';
+import { DateTime } from 'luxon';
import type { MessageFormatter } from 'svelte-i18n';
import { get } from 'svelte/store';
@@ -36,7 +39,7 @@ export const getUserAdminsActions = ($t: MessageFormatter) => {
title: $t('create_user'),
type: $t('command'),
icon: mdiPlusBoxOutline,
- onAction: () => void modalManager.show(UserCreateModal, {}),
+ onAction: () => modalManager.show(UserCreateModal, {}),
shortcuts: { shift: true, key: 'n' },
};
@@ -60,11 +63,17 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
shortcuts: { key: 'Backspace' },
};
- const Restore: ActionItem = {
+ const getDeleteDate = (deletedAt: string): Date =>
+ DateTime.fromISO(deletedAt).plus({ days: serverConfigManager.value.userDeleteDelay }).toJSDate();
+
+ const Restore: HeaderButtonActionItem = {
icon: mdiDeleteRestore,
title: $t('restore'),
type: $t('command'),
color: 'primary',
+ data: {
+ title: $t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt!) } }),
+ },
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
onAction: () => modalManager.show(UserRestoreConfirmModal, { user }),
};
@@ -74,14 +83,14 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
title: $t('reset_password'),
type: $t('command'),
$if: () => get(authUser).id !== user.id,
- onAction: () => void handleResetPasswordUserAdmin(user),
+ onAction: () => handleResetPasswordUserAdmin(user),
};
const ResetPinCode: ActionItem = {
icon: mdiLockSmart,
type: $t('command'),
title: $t('reset_pin_code'),
- onAction: () => void handleResetPinCodeUserAdmin(user),
+ onAction: () => handleResetPinCodeUserAdmin(user),
};
return { Update, Delete, Restore, ResetPassword, ResetPinCode };
@@ -162,12 +171,12 @@ const generatePassword = (length: number = 16) => {
return generatedPassword;
};
-export const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) => {
+const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) => {
const $t = await getFormatter();
const prompt = $t('admin.confirm_user_password_reset', { values: { user: user.name } });
const success = await modalManager.showDialog({ prompt });
if (!success) {
- return false;
+ return;
}
try {
@@ -176,28 +185,24 @@ export const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) =
eventManager.emit('UserAdminUpdate', response);
toastManager.success();
await modalManager.show(PasswordResetSuccessModal, { newPassword: dto.password });
- return true;
} catch (error) {
handleError(error, $t('errors.unable_to_reset_password'));
- return false;
}
};
-export const handleResetPinCodeUserAdmin = async (user: UserAdminResponseDto) => {
+const handleResetPinCodeUserAdmin = async (user: UserAdminResponseDto) => {
const $t = await getFormatter();
const prompt = $t('admin.confirm_user_pin_code_reset', { values: { user: user.name } });
const success = await modalManager.showDialog({ prompt });
if (!success) {
- return false;
+ return;
}
try {
const response = await updateUserAdmin({ id: user.id, userAdminUpdateDto: { pinCode: null } });
eventManager.emit('UserAdminUpdate', response);
toastManager.success($t('pin_code_reset_successfully'));
- return true;
} catch (error) {
handleError(error, $t('errors.unable_to_reset_pin_code'));
- return false;
}
};
diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts
index e7d38b1a254b5..dbe3c851a04e6 100644
--- a/web/src/lib/types.ts
+++ b/web/src/lib/types.ts
@@ -1,4 +1,5 @@
import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk';
+import type { ActionItem } from '@immich/ui';
export interface ReleaseEvent {
isAvailable: boolean;
@@ -9,3 +10,5 @@ export interface ReleaseEvent {
}
export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] };
+
+export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } };
diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte
index c8f41b6fbc472..77a3d402b24ef 100644
--- a/web/src/routes/+layout.svelte
+++ b/web/src/routes/+layout.svelte
@@ -14,15 +14,15 @@
import { themeManager } from '$lib/managers/theme-manager.svelte';
import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
- import { getQueuesActions } from '$lib/services/queue.service';
+ import { sidebarStore } from '$lib/stores/sidebar.svelte';
import { user } from '$lib/stores/user.store';
import { closeWebsocketConnection, openWebsocketConnection, websocketStore } from '$lib/stores/websocket';
import type { ReleaseEvent } from '$lib/types';
import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils';
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
import { isAssetViewerRoute } from '$lib/utils/navigation';
- import { CommandPaletteContext, modalManager, setTranslations, type ActionItem } from '@immich/ui';
- import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiThemeLightDark } from '@mdi/js';
+ import { CommandPaletteContext, modalManager, setTranslations, toastManager, 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';
@@ -53,6 +53,8 @@
return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
};
+ toastManager.setOptions({ class: 'top-16' });
+
onMount(() => {
const element = document.querySelector('#stencil');
element?.remove();
@@ -62,6 +64,10 @@
eventManager.emit('AppInit');
beforeNavigate(({ from, to }) => {
+ if (sidebarStore.isOpen) {
+ sidebarStore.reset();
+ }
+
if (isAssetViewerRoute(from) && isAssetViewerRoute(to)) {
return;
}
@@ -149,6 +155,13 @@
icon: mdiCog,
onAction: () => goto(AppRoute.ADMIN_SETTINGS),
},
+ {
+ title: $t('admin.queues'),
+ description: $t('admin.queues_page_description'),
+ icon: mdiSync,
+ type: $t('page'),
+ onAction: () => goto(AppRoute.ADMIN_QUEUES),
+ },
{
title: $t('external_libraries'),
description: $t('admin.external_libraries_page_description'),
@@ -163,7 +176,7 @@
},
].map((route) => ({ ...route, type: $t('page'), isGlobal: true, $if: () => $user?.isAdmin }));
- const commands = $derived([...userCommands, ...adminCommands, ...Object.values(getQueuesActions($t))]);
+ const commands = $derived([...userCommands, ...adminCommands]);
diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte
index aef8447d00c36..9aa5af6481895 100644
--- a/web/src/routes/admin/library-management/+page.svelte
+++ b/web/src/routes/admin/library-management/+page.svelte
@@ -1,6 +1,5 @@
- {#snippet buttons()}
-
-
-
-
-
-
-
- {/snippet}
{#if user.deletedAt}