mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
merge: remote-tracking branch 'origin/main' into feat/integrity-checks-izzy
This commit is contained in:
+2
-2
@@ -27,7 +27,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@immich/ui": "^0.58.4",
|
||||
"@immich/ui": "^0.59.0",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
@@ -71,7 +71,7 @@
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/enhanced-img": "^0.9.0",
|
||||
"@sveltejs/kit": "^2.27.1",
|
||||
"@sveltejs/vite-plugin-svelte": "6.2.3",
|
||||
"@sveltejs/vite-plugin-svelte": "6.2.4",
|
||||
"@tailwindcss/vite": "^4.1.7",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/svelte": "^5.2.8",
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { navigating } from '$app/stores';
|
||||
import { AppRoute, SessionStorageKey } from '$lib/constants';
|
||||
import { SessionStorageKey } from '$lib/constants';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* {@link AppRoute} for subpages that scroll state should be kept while visiting.
|
||||
*
|
||||
* This must be kept the same in all subpages of this route for the scroll memory clearer to work.
|
||||
*/
|
||||
routeStartsWith: AppRoute;
|
||||
routeStartsWith: string;
|
||||
/**
|
||||
* Function to clear additional data/state before scrolling (ex infinite scroll).
|
||||
*/
|
||||
|
||||
@@ -1,48 +1,42 @@
|
||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import { useZoomImageWheel } from '@zoom-image/svelte';
|
||||
import { createZoomImageWheel } from '@zoom-image/core';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolean }) => {
|
||||
const { createZoomImage, zoomImageState, setZoomImageState } = useZoomImageWheel();
|
||||
|
||||
createZoomImage(node, {
|
||||
const state = get(photoZoomState);
|
||||
const zoomInstance = createZoomImageWheel(node, {
|
||||
maxZoom: 10,
|
||||
initialState: state,
|
||||
});
|
||||
|
||||
const state = get(photoZoomState);
|
||||
if (state) {
|
||||
setZoomImageState(state);
|
||||
}
|
||||
const unsubscribes = [
|
||||
photoZoomState.subscribe((state) => zoomInstance.setState(state)),
|
||||
zoomInstance.subscribe(({ state }) => {
|
||||
photoZoomState.set(state);
|
||||
}),
|
||||
];
|
||||
|
||||
// Store original event handlers so we can prevent them when disabled
|
||||
const wheelHandler = (event: WheelEvent) => {
|
||||
const stopIfDisabled = (event: Event) => {
|
||||
if (options?.disabled) {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
const pointerDownHandler = (event: PointerEvent) => {
|
||||
if (options?.disabled) {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
};
|
||||
|
||||
// Add handlers at capture phase with higher priority
|
||||
node.addEventListener('wheel', wheelHandler, { capture: true });
|
||||
node.addEventListener('pointerdown', pointerDownHandler, { capture: true });
|
||||
|
||||
const unsubscribes = [photoZoomState.subscribe(setZoomImageState), zoomImageState.subscribe(photoZoomState.set)];
|
||||
node.addEventListener('wheel', stopIfDisabled, { capture: true });
|
||||
node.addEventListener('pointerdown', stopIfDisabled, { capture: true });
|
||||
|
||||
node.style.overflow = 'visible';
|
||||
return {
|
||||
update(newOptions?: { disabled?: boolean }) {
|
||||
options = newOptions;
|
||||
},
|
||||
destroy() {
|
||||
node.removeEventListener('wheel', wheelHandler, { capture: true });
|
||||
node.removeEventListener('pointerdown', pointerDownHandler, { capture: true });
|
||||
for (const unsubscribe of unsubscribes) {
|
||||
unsubscribe();
|
||||
}
|
||||
node.removeEventListener('wheel', stopIfDisabled, { capture: true });
|
||||
node.removeEventListener('pointerdown', stopIfDisabled, { capture: true });
|
||||
zoomInstance.cleanup();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import QueueCardBadge from '$lib/components/QueueCardBadge.svelte';
|
||||
import QueueCardButton from '$lib/components/QueueCardButton.svelte';
|
||||
import Badge from '$lib/elements/Badge.svelte';
|
||||
import { asQueueItem, getQueueDetailUrl } from '$lib/services/queue.service';
|
||||
import { Route } from '$lib/route';
|
||||
import { asQueueItem } from '$lib/services/queue.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { QueueCommand, type QueueCommandDto, type QueueResponseDto } from '@immich/sdk';
|
||||
import { Icon, IconButton, Link } from '@immich/ui';
|
||||
@@ -50,7 +51,7 @@
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2 p-5 sm:p-7 md:p-9">
|
||||
<div class="flex items-center gap-2 text-xl font-semibold text-primary">
|
||||
<Link class="flex items-center gap-2 hover:underline" href={getQueueDetailUrl(queue)} underline={false}>
|
||||
<Link class="flex items-center gap-2 hover:underline" href={Route.viewQueue(queue)} underline={false}>
|
||||
<Icon {icon} size="1.25em" class="hidden shrink-0 sm:block" />
|
||||
<span class="uppercase">{title}</span>
|
||||
</Link>
|
||||
@@ -60,7 +61,7 @@
|
||||
aria-label={$t('view_details')}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
href={getQueueDetailUrl(queue)}
|
||||
href={Route.viewQueue(queue)}
|
||||
/>
|
||||
<div class="flex gap-2">
|
||||
{#if statistics.failed > 0}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AppRoute, OpenSettingQueryParameterValue, QueryParameter } from '$lib/constants';
|
||||
import { OpenQueryParam } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
@@ -9,10 +10,7 @@
|
||||
values={{ template: $t('admin.storage_template_settings') }}
|
||||
>
|
||||
{#snippet children({ message })}
|
||||
<a
|
||||
href="{AppRoute.ADMIN_SETTINGS}?{QueryParameter.IS_OPEN}={OpenSettingQueryParameterValue.STORAGE_TEMPLATE}"
|
||||
class="text-primary"
|
||||
>
|
||||
<a href={Route.systemSettings({ isOpen: OpenQueryParam.STORAGE_TEMPLATE })} class="text-primary">
|
||||
{message}
|
||||
</a>
|
||||
{/snippet}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { MaintenanceAction, setMaintenanceMode } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let { disabled = false }: Props = $props();
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
await setMaintenanceMode({
|
||||
setMaintenanceModeDto: {
|
||||
action: MaintenanceAction.Start,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.maintenance_start_error'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<div class="ms-4 mt-4 flex items-end gap-4">
|
||||
<Button shape="round" type="submit" {disabled} size="small" onclick={start}
|
||||
>{$t('admin.maintenance_start')}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,14 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import SupportedDatetimePanel from '$lib/components/admin-settings/SupportedDatetimePanel.svelte';
|
||||
import SupportedVariablesPanel from '$lib/components/admin-settings/SupportedVariablesPanel.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { AppRoute, SettingInputFieldType } from '$lib/constants';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleSystemConfigSave } from '$lib/services/system-config.service';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getStorageTemplateOptions, type SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
|
||||
@@ -257,9 +257,7 @@
|
||||
values={{ job: $t('admin.storage_template_migration_job') }}
|
||||
>
|
||||
{#snippet children({ message })}
|
||||
<a href={resolve(AppRoute.ADMIN_QUEUES)} class="text-primary">
|
||||
{message}
|
||||
</a>
|
||||
<a href={Route.queues()} class="text-primary">{message}</a>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import AlbumCard from '$lib/components/album-page/album-card.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { albumViewSettings } from '$lib/stores/preferences.store';
|
||||
import { type AlbumGroup, isAlbumGroupCollapsed, toggleAlbumGroupCollapsing } from '$lib/utils/album-utils';
|
||||
import type { ContextMenuPosition } from '$lib/utils/context-menu';
|
||||
@@ -65,7 +64,7 @@
|
||||
<div class="grid grid-auto-fill-56 gap-y-4" transition:slide={{ duration: 300 }}>
|
||||
{#each albums as album, index (album.id)}
|
||||
<a
|
||||
href={resolve(`${AppRoute.ALBUMS}/${album.id}`)}
|
||||
href={Route.viewAlbum(album)}
|
||||
animate:flip={{ duration: 400 }}
|
||||
oncontextmenu={(event) => oncontextmenu(event, album)}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { AppRoute, dateFormats } from '$lib/constants';
|
||||
import { dateFormats } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import type { ContextMenuPosition } from '$lib/utils/context-menu';
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<tr
|
||||
class="flex w-full place-items-center border-3 border-transparent p-2 text-center even:bg-subtle/20 odd:bg-subtle/80 hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:px-5 md:py-2"
|
||||
onclick={() => goto(resolve(`${AppRoute.ALBUMS}/${album.id}`))}
|
||||
onclick={() => goto(Route.viewAlbum(album))}
|
||||
{oncontextmenu}
|
||||
>
|
||||
<td class="text-md text-ellipsis text-start w-8/12 sm:w-4/12 md:w-4/12 xl:w-[30%] 2xl:w-[40%] items-center">
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { AppRoute, timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||
import { activityManager } from '$lib/managers/activity-manager.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { getAssetType } from '$lib/utils/asset-utils';
|
||||
@@ -139,10 +139,7 @@
|
||||
|
||||
<div class="w-full leading-4 overflow-hidden self-center wrap-break-word text-sm">{reaction.comment}</div>
|
||||
{#if assetId === undefined && reaction.assetId}
|
||||
<a
|
||||
class="aspect-square w-19 h-19"
|
||||
href={resolve(`${AppRoute.ALBUMS}/${albumId}/photos/${reaction.assetId}`)}
|
||||
>
|
||||
<a class="aspect-square w-19 h-19" href={Route.viewAlbumAsset({ albumId, assetId: reaction.assetId })}>
|
||||
<img
|
||||
class="rounded-lg w-19 h-19 object-cover"
|
||||
src={getAssetThumbnailUrl(reaction.assetId)}
|
||||
@@ -194,7 +191,7 @@
|
||||
{#if assetId === undefined && reaction.assetId}
|
||||
<a
|
||||
class="aspect-square w-19 h-19"
|
||||
href={resolve(`${AppRoute.ALBUMS}/${albumId}/photos/${reaction.assetId}`)}
|
||||
href={Route.viewAlbumAsset({ albumId, assetId: reaction.assetId })}
|
||||
>
|
||||
<img
|
||||
class="rounded-lg w-19 h-19 object-cover"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import ActionButton from '$lib/components/ActionButton.svelte';
|
||||
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
|
||||
import type { OnAction, PreAction } from '$lib/components/asset-viewer/actions/action';
|
||||
@@ -8,6 +7,7 @@
|
||||
import AddToStackAction from '$lib/components/asset-viewer/actions/add-to-stack-action.svelte';
|
||||
import ArchiveAction from '$lib/components/asset-viewer/actions/archive-action.svelte';
|
||||
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
|
||||
import EditAction from '$lib/components/asset-viewer/actions/edit-action.svelte';
|
||||
import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/keep-this-delete-others.svelte';
|
||||
import RatingAction from '$lib/components/asset-viewer/actions/rating-action.svelte';
|
||||
import RemoveAssetFromStack from '$lib/components/asset-viewer/actions/remove-asset-from-stack.svelte';
|
||||
@@ -20,8 +20,9 @@
|
||||
import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getGlobalActions } from '$lib/services/app.service';
|
||||
import { getAssetActions, handleReplaceAsset } from '$lib/services/asset.service';
|
||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||
@@ -72,7 +73,7 @@
|
||||
onUndoDelete?: OnUndoDelete;
|
||||
onRunJob: (name: AssetJobName) => void;
|
||||
onPlaySlideshow: () => void;
|
||||
// onEdit: () => void;
|
||||
onEdit: () => void;
|
||||
onClose?: () => void;
|
||||
playOriginalVideo: boolean;
|
||||
setPlayOriginalVideo: (value: boolean) => void;
|
||||
@@ -92,7 +93,7 @@
|
||||
onRunJob,
|
||||
onPlaySlideshow,
|
||||
onClose,
|
||||
// onEdit,
|
||||
onEdit,
|
||||
playOriginalVideo = false,
|
||||
setPlayOriginalVideo,
|
||||
}: Props = $props();
|
||||
@@ -112,22 +113,29 @@
|
||||
|
||||
const { Cast } = $derived(getGlobalActions($t));
|
||||
|
||||
const { Share, Download, SharedLinkDownload, Offline, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto, Info } =
|
||||
$derived(getAssetActions($t, asset));
|
||||
const {
|
||||
Share,
|
||||
Download,
|
||||
DownloadOriginal,
|
||||
SharedLinkDownload,
|
||||
Offline,
|
||||
Favorite,
|
||||
Unfavorite,
|
||||
PlayMotionPhoto,
|
||||
StopMotionPhoto,
|
||||
Info,
|
||||
} = $derived(getAssetActions($t, asset));
|
||||
const sharedLink = getSharedLink();
|
||||
|
||||
// TODO: Enable when edits are ready for release
|
||||
// let showEditorButton = $derived(
|
||||
// isOwner &&
|
||||
// asset.type === AssetTypeEnum.Image &&
|
||||
// !(
|
||||
// asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR ||
|
||||
// (asset.originalPath && asset.originalPath.toLowerCase().endsWith('.insp'))
|
||||
// ) &&
|
||||
// !(asset.originalPath && asset.originalPath.toLowerCase().endsWith('.gif')) &&
|
||||
// !(asset.originalPath && asset.originalPath.toLowerCase().endsWith('.svg')) &&
|
||||
// !asset.livePhotoVideoId,
|
||||
// );
|
||||
const editorDisabled = $derived(
|
||||
!isOwner ||
|
||||
asset.type !== AssetTypeEnum.Image ||
|
||||
asset.livePhotoVideoId ||
|
||||
(asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR &&
|
||||
asset.originalPath.toLowerCase().endsWith('.insp')) ||
|
||||
asset.originalPath.toLowerCase().endsWith('.gif') ||
|
||||
asset.originalPath.toLowerCase().endsWith('.svg'),
|
||||
);
|
||||
</script>
|
||||
|
||||
<CommandPaletteDefaultProvider
|
||||
@@ -180,9 +188,9 @@
|
||||
<RatingAction {asset} {onAction} />
|
||||
{/if}
|
||||
|
||||
<!-- {#if showEditorButton}
|
||||
{#if !editorDisabled}
|
||||
<EditAction onAction={onEdit} />
|
||||
{/if} -->
|
||||
{/if}
|
||||
|
||||
{#if isOwner}
|
||||
<DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />
|
||||
@@ -195,6 +203,7 @@
|
||||
{/if}
|
||||
|
||||
<ActionMenuItem action={Download} />
|
||||
<ActionMenuItem action={DownloadOriginal} />
|
||||
|
||||
{#if !isLocked}
|
||||
{#if asset.isTrashed}
|
||||
@@ -239,7 +248,7 @@
|
||||
{#if !asset.isArchived && !asset.isTrashed}
|
||||
<MenuOption
|
||||
icon={mdiImageSearch}
|
||||
onClick={() => goto(resolve(`${AppRoute.PHOTOS}?at=${stack?.primaryAssetId ?? asset.id}`))}
|
||||
onClick={() => goto(Route.photos({ at: stack?.primaryAssetId ?? asset.id }))}
|
||||
text={$t('view_in_timeline')}
|
||||
/>
|
||||
{/if}
|
||||
@@ -247,8 +256,7 @@
|
||||
{#if !asset.isArchived && !asset.isTrashed && smartSearchEnabled}
|
||||
<MenuOption
|
||||
icon={mdiCompare}
|
||||
onClick={() =>
|
||||
goto(resolve(`${AppRoute.SEARCH}?query={"queryAssetId":"${stack?.primaryAssetId ?? asset.id}"}`))}
|
||||
onClick={() => goto(Route.search({ queryAssetId: stack?.primaryAssetId ?? asset.id }))}
|
||||
text={$t('view_similar_photos')}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -6,12 +6,14 @@
|
||||
import PreviousAssetAction from '$lib/components/asset-viewer/actions/previous-asset-action.svelte';
|
||||
import AssetViewerNavBar from '$lib/components/asset-viewer/asset-viewer-nav-bar.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { AppRoute, AssetAction, ProjectionType } from '$lib/constants';
|
||||
import { AssetAction, ProjectionType } from '$lib/constants';
|
||||
import { activityManager } from '$lib/managers/activity-manager.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { editManager, EditToolType } from '$lib/managers/edit/edit-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { preloadManager } from '$lib/managers/PreloadManager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { ocrManager } from '$lib/stores/ocr.svelte';
|
||||
import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store';
|
||||
@@ -253,12 +255,12 @@
|
||||
});
|
||||
};
|
||||
|
||||
// const showEditor = () => {
|
||||
// if (assetViewerManager.isShowActivityPanel) {
|
||||
// assetViewerManager.isShowActivityPanel = false;
|
||||
// }
|
||||
// isShowEditor = !isShowEditor;
|
||||
// };
|
||||
const showEditor = () => {
|
||||
if (assetViewerManager.isShowActivityPanel) {
|
||||
assetViewerManager.isShowActivityPanel = false;
|
||||
}
|
||||
isShowEditor = !isShowEditor;
|
||||
};
|
||||
|
||||
const handleRunJob = async (name: AssetJobName) => {
|
||||
try {
|
||||
@@ -320,6 +322,11 @@
|
||||
await handleGetAllAlbums();
|
||||
break;
|
||||
}
|
||||
case AssetAction.DELETE:
|
||||
case AssetAction.TRASH: {
|
||||
eventManager.emit('AssetsDelete', [asset.id]);
|
||||
break;
|
||||
}
|
||||
case AssetAction.REMOVE_ASSET_FROM_STACK: {
|
||||
stack = action.stack;
|
||||
if (stack) {
|
||||
@@ -395,7 +402,7 @@
|
||||
}
|
||||
|
||||
await new Promise((promise) => setTimeout(promise, 500));
|
||||
await goto(`${AppRoute.PHOTOS}/${newAssetId}`);
|
||||
await goto(Route.viewAsset({ id: newAssetId }));
|
||||
};
|
||||
|
||||
const onAssetUpdate = (update: AssetResponseDto) => {
|
||||
@@ -465,6 +472,7 @@
|
||||
preAction={handlePreAction}
|
||||
onAction={handleAction}
|
||||
{onUndoDelete}
|
||||
onEdit={showEditor}
|
||||
onRunJob={handleRunJob}
|
||||
onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
|
||||
onClose={onClose ? () => onClose(asset) : undefined}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import AssetTagModal from '$lib/modals/AssetTagModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { removeTag } from '$lib/utils/asset-utils';
|
||||
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
|
||||
import { Icon, modalManager } from '@immich/ui';
|
||||
@@ -46,7 +45,7 @@
|
||||
<div class="flex group transition-all">
|
||||
<a
|
||||
class="inline-block h-min whitespace-nowrap ps-3 pe-1 group-hover:ps-3 py-1 text-center align-baseline leading-none text-gray-100 dark:text-immich-dark-gray bg-primary rounded-s-full hover:bg-immich-primary/80 dark:hover:bg-immich-dark-primary/80 transition-all"
|
||||
href={resolve(`${AppRoute.TAGS}/?path=${encodeURI(tag.value)}`)}
|
||||
href={Route.tags({ path: tag.value })}
|
||||
>
|
||||
<p class="text-sm">
|
||||
{tag.value}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
|
||||
import DetailPanelLocation from '$lib/components/asset-viewer/detail-panel-location.svelte';
|
||||
import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
|
||||
import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte';
|
||||
import { AppRoute, QueryParameter, timeToLoadTheMap } from '$lib/constants';
|
||||
import { timeToLoadTheMap } from '$lib/constants';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import AssetChangeDateModal from '$lib/modals/AssetChangeDateModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
@@ -17,7 +17,6 @@
|
||||
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||
import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { getParentPath } from '$lib/utils/tree-utils';
|
||||
import { AssetMediaSize, getAssetInfo, type AlbumResponseDto, type AssetResponseDto } from '@immich/sdk';
|
||||
@@ -73,6 +72,7 @@
|
||||
})(),
|
||||
);
|
||||
let previousId: string | undefined = $state();
|
||||
let previousRoute = $derived(currentAlbum?.id ? Route.viewAlbum(currentAlbum) : Route.photos());
|
||||
|
||||
$effect(() => {
|
||||
if (!previousId) {
|
||||
@@ -100,11 +100,8 @@
|
||||
};
|
||||
|
||||
const getAssetFolderHref = (asset: AssetResponseDto) => {
|
||||
const folderUrl = new URL(AppRoute.FOLDERS, globalThis.location.href);
|
||||
// Remove the last part of the path to get the parent path
|
||||
const assetParentPath = getParentPath(asset.originalPath);
|
||||
folderUrl.searchParams.set(QueryParameter.PATH, assetParentPath);
|
||||
return folderUrl.href;
|
||||
return Route.folders({ path: getParentPath(asset.originalPath) });
|
||||
};
|
||||
|
||||
const toggleAssetPath = () => (showAssetPath = !showAssetPath);
|
||||
@@ -205,11 +202,7 @@
|
||||
{#if showingHiddenPeople || !person.isHidden}
|
||||
<a
|
||||
class="w-22"
|
||||
href={resolve(
|
||||
`${AppRoute.PEOPLE}/${person.id}?${QueryParameter.PREVIOUS_ROUTE}=${
|
||||
currentAlbum?.id ? `${AppRoute.ALBUMS}/${currentAlbum?.id}` : AppRoute.PHOTOS
|
||||
}`,
|
||||
)}
|
||||
href={Route.viewPerson(person, { previousRoute })}
|
||||
onfocus={() => ($boundingBoxesArray = people[index].faces)}
|
||||
onblur={() => ($boundingBoxesArray = [])}
|
||||
onmouseover={() => ($boundingBoxesArray = people[index].faces)}
|
||||
@@ -385,12 +378,10 @@
|
||||
{#if asset.exifInfo?.make || asset.exifInfo?.model}
|
||||
<p>
|
||||
<a
|
||||
href={resolve(
|
||||
`${AppRoute.SEARCH}?${getMetadataSearchQuery({
|
||||
...(asset.exifInfo?.make ? { make: asset.exifInfo.make } : {}),
|
||||
...(asset.exifInfo?.model ? { model: asset.exifInfo.model } : {}),
|
||||
})}`,
|
||||
)}
|
||||
href={Route.search({
|
||||
make: asset.exifInfo?.make ?? undefined,
|
||||
model: asset.exifInfo?.model ?? undefined,
|
||||
})}
|
||||
title="{$t('search_for')} {asset.exifInfo.make || ''} {asset.exifInfo.model || ''}"
|
||||
class="hover:text-primary"
|
||||
>
|
||||
@@ -421,7 +412,7 @@
|
||||
{#if asset.exifInfo?.lensModel}
|
||||
<p>
|
||||
<a
|
||||
href={resolve(`${AppRoute.SEARCH}?${getMetadataSearchQuery({ lensModel: asset.exifInfo.lensModel })}`)}
|
||||
href={Route.search({ lensModel: asset.exifInfo.lensModel })}
|
||||
title="{$t('search_for')} {asset.exifInfo.lensModel}"
|
||||
class="hover:text-primary line-clamp-1"
|
||||
>
|
||||
@@ -474,7 +465,7 @@
|
||||
simplified
|
||||
useLocationPin
|
||||
showSimpleControls={!showEditFaces}
|
||||
onOpenInMapView={() => goto(resolve(`${AppRoute.MAP}#12.5/${latlng.lat}/${latlng.lng}`))}
|
||||
onOpenInMapView={() => goto(Route.map({ ...latlng, zoom: 12.5 }))}
|
||||
>
|
||||
{#snippet popup({ marker })}
|
||||
{@const { lat, lon } = marker}
|
||||
@@ -515,7 +506,7 @@
|
||||
<section class="px-6 py-6 dark:text-immich-dark-fg">
|
||||
<p class="uppercase pb-4 text-sm">{$t('appears_in')}</p>
|
||||
{#each albums as album (album.id)}
|
||||
<a href={resolve(`${AppRoute.ALBUMS}/${album.id}`)}>
|
||||
<a href={Route.viewAlbum(album)}>
|
||||
<div class="flex gap-4 pt-2 hover:cursor-pointer items-center">
|
||||
<div>
|
||||
<img
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
/>
|
||||
<p class="text-lg text-immich-fg dark:text-immich-dark-fg capitalize">{$t('editor')}</p>
|
||||
</HStack>
|
||||
<Button shape="round" size="small" onclick={applyEdits}>{$t('save')}</Button>
|
||||
<Button shape="round" size="small" onclick={applyEdits} loading={editManager.isApplyingEdits}>{$t('save')}</Button>
|
||||
</HStack>
|
||||
|
||||
<section>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk';
|
||||
import { Button, Icon, IconButton, modalManager, toastManager } from '@immich/ui';
|
||||
@@ -39,8 +38,7 @@
|
||||
|
||||
const handleSwapPeople = async () => {
|
||||
[person, selectedPeople[0]] = [selectedPeople[0], person];
|
||||
page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
|
||||
await goto(`${AppRoute.PEOPLE}/${person.id}?${page.url.searchParams.toString()}`);
|
||||
await goto(Route.viewPerson(person, { previousRoute: Route.people(), action: 'merge' }));
|
||||
};
|
||||
|
||||
const onSelect = async (selected: PersonResponseDto) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { focusOutside } from '$lib/actions/focus-outside';
|
||||
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { getPersonActions } from '$lib/services/person.service';
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { type PersonResponseDto } from '@immich/sdk';
|
||||
@@ -42,7 +42,7 @@
|
||||
use:focusOutside={{ onFocusOut: () => (showVerticalDots = false) }}
|
||||
>
|
||||
<a
|
||||
href="{AppRoute.PEOPLE}/{person.id}?{QueryParameter.PREVIOUS_ROUTE}={AppRoute.PEOPLE}"
|
||||
href={Route.viewPerson(person, { previousRoute: Route.people() })}
|
||||
draggable="false"
|
||||
onfocus={() => (showVerticalDots = true)}
|
||||
>
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import BreadcrumbActionPage from '$lib/components/BreadcrumbActionPage.svelte';
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||
import type { HeaderButtonActionItem } from '$lib/types';
|
||||
import { AppShell, AppShellHeader, AppShellSidebar, MenuItemType, NavbarItem, type BreadcrumbItem } from '@immich/ui';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiTrayFull } from '@mdi/js';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiTrayFull, mdiWrench } from '@mdi/js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -28,12 +28,12 @@
|
||||
class="border-none shadow-none h-full flex flex-col justify-between gap-2"
|
||||
>
|
||||
<div class="flex flex-col pt-8 pe-4 gap-1">
|
||||
<NavbarItem title={$t('users')} href={AppRoute.ADMIN_USERS} icon={mdiAccountMultipleOutline} />
|
||||
<NavbarItem title={$t('external_libraries')} href={AppRoute.ADMIN_LIBRARIES} icon={mdiBookshelf} />
|
||||
<NavbarItem title={$t('admin.queues')} href={AppRoute.ADMIN_QUEUES} icon={mdiTrayFull} />
|
||||
<NavbarItem title={$t('settings')} href={AppRoute.ADMIN_SETTINGS} icon={mdiCog} />
|
||||
<NavbarItem title={$t('admin.maintenance_settings')} href={AppRoute.ADMIN_MAINTENANCE_SETTINGS} />
|
||||
<NavbarItem title={$t('server_stats')} href={AppRoute.ADMIN_STATS} icon={mdiServer} />
|
||||
<NavbarItem title={$t('users')} href={Route.users()} icon={mdiAccountMultipleOutline} />
|
||||
<NavbarItem title={$t('external_libraries')} href={Route.libraries()} icon={mdiBookshelf} />
|
||||
<NavbarItem title={$t('admin.queues')} href={Route.queues()} icon={mdiTrayFull} />
|
||||
<NavbarItem title={$t('settings')} href={Route.systemSettings()} icon={mdiCog} />
|
||||
<NavbarItem title={$t('admin.maintenance_settings')} href={Route.systemMaintenance()} icon={mdiWrench} />
|
||||
<NavbarItem title={$t('server_stats')} href={Route.systemStatistics()} icon={mdiServer} />
|
||||
</div>
|
||||
|
||||
<div class="mb-2 me-4">
|
||||
|
||||
@@ -5,24 +5,27 @@
|
||||
title?: string;
|
||||
children?: Snippet;
|
||||
withHeader?: boolean;
|
||||
withBackdrop?: boolean;
|
||||
}
|
||||
|
||||
let { title, children, withHeader = true }: Props = $props();
|
||||
let { title, children, withHeader = true, withBackdrop = true }: Props = $props();
|
||||
</script>
|
||||
|
||||
<section class="min-w-dvw flex min-h-dvh items-center justify-center relative">
|
||||
<div class="absolute -z-10 w-full h-full flex place-items-center place-content-center">
|
||||
<img
|
||||
src={immichLogo}
|
||||
class="max-w-(--breakpoint-md) mx-auto h-full mb-2 antialiased overflow-hidden"
|
||||
alt="Immich logo"
|
||||
/>
|
||||
<div
|
||||
class="w-full h-[99%] absolute start-0 top-0 backdrop-blur-[200px] bg-transparent dark:bg-immich-dark-bg/20"
|
||||
></div>
|
||||
</div>
|
||||
{#if withBackdrop}
|
||||
<div class="absolute -z-10 w-full h-full flex place-items-center place-content-center">
|
||||
<img
|
||||
src={immichLogo}
|
||||
class="max-w-(--breakpoint-md) mx-auto h-full mb-2 antialiased overflow-hidden"
|
||||
alt="Immich logo"
|
||||
/>
|
||||
<div
|
||||
class="w-full h-[99%] absolute start-0 top-0 backdrop-blur-[200px] bg-transparent dark:bg-immich-dark-bg/20"
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<Card color="secondary" class="w-full max-w-lg border m-2">
|
||||
<Card color="secondary" class="w-full max-w-xl border m-2">
|
||||
{#if withHeader}
|
||||
<CardHeader class="mt-6">
|
||||
<VStack>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<script lang="ts">
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { BackupFileStatus } from '$lib/constants';
|
||||
import { getDatabaseBackupActions, handleRestoreDatabaseBackup } from '$lib/services/database-backups.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import { Button, Card, CardBody, ContextMenuButton, HStack, Icon, Stack, Text } from '@immich/ui';
|
||||
import { mdiAlertCircle, mdiCheckCircle, mdiDatabaseRefreshOutline } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
filename: string;
|
||||
filesize: number;
|
||||
expectedVersion: string;
|
||||
};
|
||||
|
||||
const { filename, filesize, expectedVersion }: Props = $props();
|
||||
|
||||
const filesizeText = $derived(getBytesWithUnit(filesize, 1));
|
||||
|
||||
const backupDateTime = $derived.by(() => {
|
||||
const dateMatch = filename.match(/\d+T\d+/);
|
||||
if (dateMatch) {
|
||||
return DateTime.fromFormat(dateMatch[0], "yyyyMMdd'T'HHmmss", { zone: 'utc' }).toLocal();
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const timeDisplay = $derived(backupDateTime?.toLocaleString(DateTime.TIME_SIMPLE));
|
||||
const relativeTime = $derived(backupDateTime?.toRelative({ locale: $locale }));
|
||||
|
||||
const version = $derived(filename.match(/-v(.*)-/)?.[1]);
|
||||
|
||||
const status = $derived.by(() => {
|
||||
if (!version) {
|
||||
return BackupFileStatus.UnknownVersion;
|
||||
}
|
||||
if (version !== expectedVersion) {
|
||||
return BackupFileStatus.DifferentVersion;
|
||||
}
|
||||
return BackupFileStatus.OK;
|
||||
});
|
||||
|
||||
const { Download, Delete } = $derived(getDatabaseBackupActions($t, filename));
|
||||
|
||||
let isDeleting = $state(false);
|
||||
|
||||
function onBackupDeleteStatus(event: { filename: string; isDeleting: boolean }) {
|
||||
if (event.filename === filename) {
|
||||
isDeleting = event.isDeleting;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<OnEvents {onBackupDeleteStatus} />
|
||||
|
||||
<Card class="dark:bg-dark-900">
|
||||
<CardBody class="pt-3 pb-4 px-6">
|
||||
<Stack gap={3} class="grow min-w-0">
|
||||
<div class="flex justify-between items-center gap-3">
|
||||
<HStack gap={2} class="min-w-0">
|
||||
{#if status === BackupFileStatus.OK}
|
||||
<Icon icon={mdiCheckCircle} size="18" class="text-success" />
|
||||
{:else if status === BackupFileStatus.DifferentVersion}
|
||||
<Icon icon={mdiAlertCircle} size="18" class="text-warning" />
|
||||
{:else}
|
||||
<Icon icon={mdiAlertCircle} size="18" class="text-danger" />
|
||||
{/if}
|
||||
|
||||
{#if timeDisplay}
|
||||
<Text class="font-medium" size="small">{timeDisplay}</Text>
|
||||
{:else}
|
||||
<Text class="font-medium" size="small">{$t('unknown_date')}</Text>
|
||||
{/if}
|
||||
{#if relativeTime}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-1 h-1 bg-light-500"></div>
|
||||
<Text size="tiny" color="muted">{relativeTime}</Text>
|
||||
</div>
|
||||
{/if}
|
||||
</HStack>
|
||||
|
||||
<HStack gap={1}>
|
||||
<Button size="small" onclick={() => handleRestoreDatabaseBackup(filename)} disabled={isDeleting}
|
||||
>{$t('restore')}</Button
|
||||
>
|
||||
<ContextMenuButton
|
||||
disabled={isDeleting}
|
||||
position="top-right"
|
||||
aria-label={$t('open')}
|
||||
items={[Download, Delete]}
|
||||
/>
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
<HStack>
|
||||
<Icon icon={mdiDatabaseRefreshOutline} size="16" color="gray" />
|
||||
<Text size="small" class="break-all font-mono">{filename}</Text>
|
||||
</HStack>
|
||||
|
||||
{#if status === BackupFileStatus.UnknownVersion}
|
||||
<Text size="small" color="danger">
|
||||
{$t('admin.maintenance_restore_backup_unknown_version')}
|
||||
</Text>
|
||||
{:else if status === BackupFileStatus.DifferentVersion}
|
||||
<Text size="small" color="warning">
|
||||
{$t('admin.maintenance_restore_backup_different_version')}
|
||||
</Text>
|
||||
{/if}
|
||||
|
||||
<HStack gap={8}>
|
||||
<div class="flex gap-1">
|
||||
<Text size="tiny" color="muted">{$t('version')}:</Text>
|
||||
<Text size="tiny" fontWeight="medium">{version ? `v${version}` : $t('unknown')}</Text>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<Text size="tiny" color="muted">{$t('size')}:</Text>
|
||||
<Text size="tiny" fontWeight="medium">{filesizeText[0]} {filesizeText[1]}</Text>
|
||||
</div>
|
||||
</HStack>
|
||||
</Stack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script lang="ts">
|
||||
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
|
||||
import MaintenanceBackupEntry from '$lib/components/maintenance/MaintenanceBackupEntry.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { handleUploadDatabaseBackup } from '$lib/services/database-backups.service';
|
||||
import type { DatabaseBackupDto } from '@immich/sdk';
|
||||
import { listDatabaseBackups } from '@immich/sdk';
|
||||
import { Card, CardBody, HStack, Icon, ProgressBar, Stack, Text } from '@immich/ui';
|
||||
import { mdiCalendar, mdiTrayArrowUp } from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
backups?: DatabaseBackupDto[];
|
||||
expectedVersion: string;
|
||||
};
|
||||
|
||||
let props: Props = $props();
|
||||
|
||||
let backups = $state(props.backups ?? []);
|
||||
|
||||
async function reloadBackups() {
|
||||
const result = await listDatabaseBackups();
|
||||
backups = result.backups;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!props.backups) {
|
||||
void reloadBackups();
|
||||
}
|
||||
});
|
||||
|
||||
let uploadProgress = $state(-1);
|
||||
|
||||
function onBackupDeleted(event: { filename: string }) {
|
||||
backups = backups.filter((backup) => backup.filename !== event.filename);
|
||||
}
|
||||
|
||||
function onBackupUpload(event: { progress: number; isComplete: boolean }) {
|
||||
uploadProgress = event.progress;
|
||||
|
||||
if (event.isComplete) {
|
||||
void reloadBackups();
|
||||
}
|
||||
}
|
||||
|
||||
const groupedBackups = $derived.by(() => {
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const groups = new Map<string, { date: DateTime; backups: DatabaseBackupDto[] }>();
|
||||
const unknownDateKey = $t('unknown_date');
|
||||
|
||||
for (const backup of backups) {
|
||||
const dateMatch = backup.filename.match(/\d+T\d+/);
|
||||
let dateKey: string;
|
||||
let dt: DateTime;
|
||||
|
||||
if (dateMatch) {
|
||||
dt = DateTime.fromFormat(dateMatch[0], "yyyyMMdd'T'HHmmss", { zone: 'utc' });
|
||||
dateKey = dt.toFormat('LLLL d, yyyy');
|
||||
} else {
|
||||
dt = DateTime.fromMillis(0);
|
||||
dateKey = unknownDateKey;
|
||||
}
|
||||
|
||||
if (!groups.has(dateKey)) {
|
||||
groups.set(dateKey, { date: dt.startOf('day'), backups: [] });
|
||||
}
|
||||
groups.get(dateKey)!.backups.push(backup);
|
||||
}
|
||||
|
||||
// Sort by date descending (newest first), but put unknown date at the top
|
||||
const sortedEntries = [...groups.entries()].sort((a, b) => {
|
||||
if (a[0] === unknownDateKey) {
|
||||
return -1;
|
||||
}
|
||||
if (b[0] === unknownDateKey) {
|
||||
return 1;
|
||||
}
|
||||
return b[1].date.toMillis() - a[1].date.toMillis();
|
||||
});
|
||||
|
||||
return new Map(sortedEntries.map(([key, value]) => [key, value.backups]));
|
||||
});
|
||||
</script>
|
||||
|
||||
<OnEvents {onBackupDeleted} {onBackupUpload} />
|
||||
|
||||
<Stack gap={4} class="mt-4 text-left">
|
||||
<Card color="info">
|
||||
<CardBody>
|
||||
{#if uploadProgress === -1}
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex gap-2 items-end w-max">
|
||||
<Icon icon={mdiTrayArrowUp} size="20" class="text-muted"></Icon>
|
||||
<Text class="grow">{$t('admin.maintenance_upload_backup')}</Text>
|
||||
</div>
|
||||
<HeaderActionButton
|
||||
action={{
|
||||
color: 'primary',
|
||||
title: $t('select_from_computer'),
|
||||
onAction: handleUploadDatabaseBackup,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<HStack gap={8}>
|
||||
<Text class="grow">{$t('asset_uploading')}</Text>
|
||||
<ProgressBar progress={uploadProgress} size="tiny" />
|
||||
</HStack>
|
||||
{/if}
|
||||
</CardBody>
|
||||
</Card>
|
||||
|
||||
<hr />
|
||||
|
||||
{#each [...groupedBackups.entries()] as [dateGroup, groupBackups] (dateGroup)}
|
||||
<Stack gap={2}>
|
||||
<div class="mt-5 mb-1">
|
||||
<div class="bg-primary-50 flex gap-2 px-4 py-2 rounded-xl w-max place-items-center">
|
||||
<Icon icon={mdiCalendar} size="18" />
|
||||
<Text size="small" fontWeight="medium" color="muted">{dateGroup}</Text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#each groupBackups as backup (backup.filename)}
|
||||
<MaintenanceBackupEntry
|
||||
filename={backup.filename}
|
||||
filesize={backup.filesize}
|
||||
expectedVersion={props.expectedVersion}
|
||||
/>
|
||||
{/each}
|
||||
</Stack>
|
||||
{/each}
|
||||
</Stack>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import RestoreFlowDetectInstall from '$lib/components/maintenance/restore-flow/RestoreFlowDetectInstall.svelte';
|
||||
import RestoreFlowSelectBackup from '$lib/components/maintenance/restore-flow/RestoreFlowSelectBackup.svelte';
|
||||
|
||||
type Props = {
|
||||
end: () => void;
|
||||
expectedVersion: string;
|
||||
};
|
||||
|
||||
const { end, expectedVersion }: Props = $props();
|
||||
|
||||
let stage = $state(0);
|
||||
</script>
|
||||
|
||||
{#if stage === 0}
|
||||
<RestoreFlowDetectInstall next={() => stage++} {end} />
|
||||
{:else}
|
||||
<RestoreFlowSelectBackup previous={() => stage--} {end} {expectedVersion} />
|
||||
{/if}
|
||||
@@ -0,0 +1,98 @@
|
||||
<script lang="ts">
|
||||
import { detectPriorInstall, type MaintenanceDetectInstallResponseDto } from '@immich/sdk';
|
||||
import { Button, Heading, HStack, Icon, Stack, Text } from '@immich/ui';
|
||||
import { mdiAlert, mdiArrowRight, mdiCheck, mdiClose, mdiRefresh } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
next: () => void;
|
||||
end: () => void;
|
||||
};
|
||||
|
||||
const { next, end }: Props = $props();
|
||||
|
||||
let detectedInstall: MaintenanceDetectInstallResponseDto | undefined = $state();
|
||||
|
||||
const reload = async () => {
|
||||
detectedInstall = await detectPriorInstall();
|
||||
};
|
||||
|
||||
const getLibraryFolderCheckStatus = (writable: boolean, readable: boolean) => {
|
||||
if (writable) {
|
||||
return $t('maintenance_restore_library_folder_pass');
|
||||
} else if (readable) {
|
||||
return $t('maintenance_restore_library_folder_write_fail');
|
||||
} else {
|
||||
return $t('maintenance_restore_library_folder_read_fail');
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => reload());
|
||||
</script>
|
||||
|
||||
<Heading size="large" color="primary" tag="h1">{$t('maintenance_restore_library')}</Heading>
|
||||
<Text>{$t('maintenance_restore_library_description')}</Text>
|
||||
<div class="bg-white dark:bg-black w-full m-4 p-6 rounded-xl border border-light-300">
|
||||
<Stack>
|
||||
{#if detectedInstall}
|
||||
{#each detectedInstall.storage as { folder, readable, writable } (folder)}
|
||||
<HStack>
|
||||
<Icon icon={writable ? mdiCheck : mdiClose} class={writable ? 'text-success' : 'text-danger'} />
|
||||
<Text>{folder} ({getLibraryFolderCheckStatus(writable, readable)})</Text>
|
||||
</HStack>
|
||||
{/each}
|
||||
{#each detectedInstall.storage as { folder, files } (folder)}
|
||||
{#if folder !== 'backups'}
|
||||
<HStack class="items-start">
|
||||
<Icon
|
||||
class={`mt-1 ${files ? 'text-success' : folder === 'profile' || folder === 'upload' ? 'text-danger' : 'text-warning'}`}
|
||||
icon={files ? mdiCheck : folder === 'profile' || folder === 'upload' ? mdiClose : mdiAlert}
|
||||
/>
|
||||
<Stack gap={0} class="items-start">
|
||||
<Text>
|
||||
{#if files}
|
||||
{$t('maintenance_restore_library_folder_has_files', {
|
||||
values: {
|
||||
folder,
|
||||
count: files,
|
||||
},
|
||||
})}
|
||||
{:else}
|
||||
{$t('maintenance_restore_library_folder_no_files', {
|
||||
values: {
|
||||
folder,
|
||||
},
|
||||
})}
|
||||
{/if}
|
||||
</Text>
|
||||
{#if !files}
|
||||
{#if folder === 'profile' || folder === 'upload'}
|
||||
<Text variant="italic">{$t('maintenance_restore_library_hint_missing_files')}</Text>
|
||||
{/if}
|
||||
{#if folder === 'encoded-video' || folder === 'thumbs'}
|
||||
<Text variant="italic">{$t('maintenance_restore_library_hint_regenerate_later')}</Text>
|
||||
{/if}
|
||||
{#if folder === 'library'}
|
||||
<Text variant="italic">{$t('maintenance_restore_library_hint_storage_template_missing_files')}</Text>
|
||||
{/if}
|
||||
{/if}
|
||||
</Stack>
|
||||
</HStack>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<Button leadingIcon={mdiRefresh} variant="ghost" onclick={reload}>{$t('refresh')}</Button>
|
||||
{:else}
|
||||
<HStack>
|
||||
<Icon icon={mdiRefresh} color="rgb(var(--immich-ui-primary))" />
|
||||
<Text>{$t('maintenance_restore_library_loading')}</Text>
|
||||
</HStack>
|
||||
{/if}
|
||||
</Stack>
|
||||
</div>
|
||||
<Text>{$t('maintenance_restore_library_confirm')}</Text>
|
||||
<HStack>
|
||||
<Button onclick={end} variant="ghost">{$t('cancel')}</Button>
|
||||
<Button onclick={next} trailingIcon={mdiArrowRight}>{$t('next')}</Button>
|
||||
</HStack>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { Button, Heading, HStack, Scrollable } from '@immich/ui';
|
||||
import { mdiArrowLeft } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import MaintenanceBackupsList from '../MaintenanceBackupsList.svelte';
|
||||
|
||||
type Props = {
|
||||
previous: () => void;
|
||||
end: () => void;
|
||||
expectedVersion: string;
|
||||
};
|
||||
|
||||
const { previous, end, expectedVersion }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Heading size="large" color="primary" tag="h1">{$t('maintenance_restore_from_backup')}</Heading>
|
||||
<Scrollable class="max-h-120 bg-white dark:bg-black p-4 rounded-2xl border border-light-300 w-full">
|
||||
<MaintenanceBackupsList {expectedVersion} />
|
||||
</Scrollable>
|
||||
<HStack>
|
||||
<Button onclick={end} variant="ghost">{$t('cancel')}</Button>
|
||||
<Button onclick={previous} variant="ghost" leadingIcon={mdiArrowLeft}>{$t('back')}</Button>
|
||||
</HStack>
|
||||
@@ -21,9 +21,10 @@
|
||||
import FavoriteAction from '$lib/components/timeline/actions/FavoriteAction.svelte';
|
||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import { Route } from '$lib/route';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { memoryStore, type MemoryAsset } from '$lib/stores/memory.store.svelte';
|
||||
@@ -111,7 +112,7 @@
|
||||
const handlePreviousAsset = () => handleNavigate(current?.previous?.asset);
|
||||
const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
|
||||
const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]);
|
||||
const handleEscape = async () => goto(AppRoute.PHOTOS);
|
||||
const handleEscape = async () => goto(Route.photos());
|
||||
const handleSelectAll = () =>
|
||||
assetInteraction.selectAssets(current?.memory.assets.map((a) => toTimelineAsset(a)) || []);
|
||||
|
||||
@@ -238,7 +239,7 @@
|
||||
|
||||
const init = (target: Page | NavigationTarget | null) => {
|
||||
if (memoryStore.memories.length === 0) {
|
||||
return handlePromiseError(goto(AppRoute.PHOTOS));
|
||||
return handlePromiseError(goto(Route.photos()));
|
||||
}
|
||||
|
||||
current = loadFromParams(target);
|
||||
@@ -362,7 +363,7 @@
|
||||
use:resizeObserver={({ height, width }) => ((viewport.height = height), (viewport.width = width))}
|
||||
>
|
||||
{#if current}
|
||||
<ControlAppBar onClose={() => goto(AppRoute.PHOTOS)} forceDark multiRow>
|
||||
<ControlAppBar onClose={() => goto(Route.photos())} forceDark multiRow>
|
||||
{#snippet leading()}
|
||||
{#if current}
|
||||
<p class="text-lg">
|
||||
@@ -532,7 +533,7 @@
|
||||
|
||||
<div>
|
||||
<IconButton
|
||||
href="{AppRoute.PHOTOS}?at={current.asset.id}"
|
||||
href={Route.photos({ at: current.asset.id })}
|
||||
icon={mdiImageSearch}
|
||||
aria-label={$t('view_in_timeline')}
|
||||
color="secondary"
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { placesViewSettings } from '$lib/stores/preferences.store';
|
||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||
import { type PlacesGroup, isPlacesGroupCollapsed, togglePlacesGroupCollapsing } from '$lib/utils/places-utils';
|
||||
import { AssetMediaSize, type AssetResponseDto } from '@immich/sdk';
|
||||
import { Icon } from '@immich/ui';
|
||||
@@ -41,7 +40,7 @@
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each places as item (item.id)}
|
||||
{@const city = item.exifInfo?.city}
|
||||
<a class="relative" href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city })}" draggable="false">
|
||||
<a class="relative" href={Route.search({ city })} draggable="false">
|
||||
<div
|
||||
class="flex w-[calc((100vw-(72px+5rem))/2)] max-w-39 justify-center overflow-hidden rounded-xl brightness-75 filter"
|
||||
>
|
||||
|
||||
@@ -4,9 +4,10 @@
|
||||
import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte';
|
||||
import RemoveFromSharedLink from '$lib/components/timeline/actions/RemoveFromSharedLinkAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { AppRoute, AssetAction } from '$lib/constants';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import { Route } from '$lib/route';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
@@ -76,7 +77,7 @@
|
||||
case AssetAction.ARCHIVE:
|
||||
case AssetAction.DELETE:
|
||||
case AssetAction.TRASH: {
|
||||
await goto(AppRoute.PHOTOS);
|
||||
await goto(Route.photos());
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -106,7 +107,7 @@
|
||||
{/if}
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
<ControlAppBar onClose={() => goto(AppRoute.PHOTOS)} backIcon={mdiArrowLeft} showBackButton={false}>
|
||||
<ControlAppBar onClose={() => goto(Route.photos())} backIcon={mdiArrowLeft} showBackButton={false}>
|
||||
{#snippet leading()}
|
||||
<a data-sveltekit-preload-data="hover" class="ms-4" href="/">
|
||||
<Logo variant={mobileDevice.maxMd ? 'icon' : 'inline'} class="min-w-10" />
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
import { AppRoute, AssetAction } from '$lib/constants';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import Portal from '$lib/elements/Portal.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import AssetDeleteConfirmModal from '$lib/modals/AssetDeleteConfirmModal.svelte';
|
||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||
@@ -256,7 +257,7 @@
|
||||
|
||||
const shortcuts: ShortcutOptions[] = [
|
||||
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
|
||||
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
|
||||
{ shortcut: { key: '/' }, onShortcut: () => goto(Route.explore()) },
|
||||
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets() },
|
||||
...(arrowNavigation
|
||||
? [
|
||||
@@ -306,7 +307,7 @@
|
||||
1,
|
||||
);
|
||||
if (assets.length === 0) {
|
||||
return await goto(AppRoute.PHOTOS);
|
||||
return await goto(Route.photos());
|
||||
}
|
||||
if (assetCursor.nextAsset) {
|
||||
await navigateToAsset(assetCursor.nextAsset);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
@@ -292,8 +293,14 @@
|
||||
|
||||
untrack(() => map?.jumpTo({ center, zoom }));
|
||||
});
|
||||
|
||||
const onAssetsDelete = async () => {
|
||||
mapMarkers = await loadMapMarkers();
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents {onAssetsDelete} />
|
||||
|
||||
<!-- We handle style loading ourselves so we set style blank here -->
|
||||
<MapLibre
|
||||
{hash}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { focusTrap } from '$lib/actions/focus-trap';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import AvatarEditModal from '$lib/modals/AvatarEditModal.svelte';
|
||||
import HelpAndFeedbackModal from '$lib/modals/HelpAndFeedbackModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<Button
|
||||
href={AppRoute.USER_SETTINGS}
|
||||
href={Route.userSettings()}
|
||||
onclick={onClose}
|
||||
size="small"
|
||||
color="secondary"
|
||||
@@ -78,7 +78,7 @@
|
||||
</Button>
|
||||
{#if $user.isAdmin}
|
||||
<Button
|
||||
href={AppRoute.ADMIN_SETTINGS}
|
||||
href={Route.systemSettings()}
|
||||
onclick={onClose}
|
||||
shape="round"
|
||||
variant="ghost"
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
import ActionButton from '$lib/components/ActionButton.svelte';
|
||||
import NotificationPanel from '$lib/components/shared-components/navigation-bar/notification-panel.svelte';
|
||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import SkipLink from '$lib/elements/SkipLink.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getGlobalActions } from '$lib/services/app.service';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||
@@ -78,7 +78,7 @@
|
||||
}}
|
||||
class="sidebar:hidden"
|
||||
/>
|
||||
<a data-sveltekit-preload-data="hover" href={AppRoute.PHOTOS}>
|
||||
<a data-sveltekit-preload-data="hover" href={Route.photos()}>
|
||||
<Logo variant={mobileDevice.isFullSidebar ? 'inline' : 'icon'} class="max-md:h-12" />
|
||||
</a>
|
||||
</div>
|
||||
@@ -97,7 +97,7 @@
|
||||
variant="ghost"
|
||||
size="medium"
|
||||
icon={mdiMagnify}
|
||||
href={AppRoute.SEARCH}
|
||||
href={Route.search()}
|
||||
id="search-button"
|
||||
class="sm:hidden"
|
||||
aria-label={$t('go_to_search')}
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { focusOutside } from '$lib/actions/focus-outside';
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import SearchFilterModal from '$lib/modals/SearchFilterModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { searchStore } from '$lib/stores/search.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||
import { Button, IconButton, modalManager } from '@immich/ui';
|
||||
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
|
||||
@@ -42,11 +41,9 @@
|
||||
});
|
||||
|
||||
const handleSearch = async (payload: SmartSearchDto | MetadataSearchDto) => {
|
||||
const params = getMetadataSearchQuery(payload);
|
||||
|
||||
closeDropdown();
|
||||
searchStore.isSearchEnabled = false;
|
||||
await goto(`${AppRoute.SEARCH}?${params}`);
|
||||
await goto(Route.search(payload));
|
||||
};
|
||||
|
||||
const clearSearchTerm = (searchTerm: string) => {
|
||||
@@ -256,7 +253,7 @@
|
||||
draggable="false"
|
||||
autocomplete="off"
|
||||
class="select-text text-sm"
|
||||
action={AppRoute.SEARCH}
|
||||
action={Route.search()}
|
||||
onreset={() => (value = '')}
|
||||
{onsubmit}
|
||||
onfocusin={onFocusIn}
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
|
||||
<input
|
||||
bind:this={input}
|
||||
class="immich-form-input w-full pb-2"
|
||||
class="immich-form-input w-full pb-2 min-w-[50px]"
|
||||
class:color-picker={inputType === SettingInputFieldType.COLOR}
|
||||
aria-describedby={description ? `${label}-desc` : undefined}
|
||||
aria-labelledby="{label}-label"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { OpenQueryParam } from '$lib/constants';
|
||||
import Portal from '$lib/elements/Portal.svelte';
|
||||
import PurchaseModal from '$lib/modals/PurchaseModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { getAccountAge } from '$lib/utils/auth';
|
||||
@@ -73,7 +74,7 @@
|
||||
<div class="license-status ps-4 text-sm">
|
||||
{#if $isPurchased && $preferences.purchase.showSupportBadge}
|
||||
<button
|
||||
onclick={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-purchase-settings`)}
|
||||
onclick={() => goto(Route.userSettings({ isOpen: OpenQueryParam.PURCHASE_SETTINGS }))}
|
||||
class="w-full mt-2"
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Route } from '$lib/route';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -25,7 +26,7 @@
|
||||
|
||||
{#each albums as album (album.id)}
|
||||
<a
|
||||
href={'/albums/' + album.id}
|
||||
href={Route.viewAlbum(album)}
|
||||
title={album.albumName}
|
||||
class="flex w-full place-items-center justify-between gap-4 rounded-e-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-subtle hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary ps-10 group-hover:sm:px-10 md:px-10"
|
||||
>
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiChevronDown, mdiChevronLeft } from '@mdi/js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
href: string;
|
||||
icon: string;
|
||||
flippedLogo?: boolean;
|
||||
isSelected?: boolean;
|
||||
preloadData?: boolean;
|
||||
dropDownContent?: Snippet;
|
||||
dropdownOpen?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
title,
|
||||
href,
|
||||
icon,
|
||||
flippedLogo = false,
|
||||
isSelected = $bindable(false),
|
||||
preloadData = true,
|
||||
dropDownContent: hasDropdown,
|
||||
dropdownOpen = $bindable(false),
|
||||
}: Props = $props();
|
||||
|
||||
$effect(() => {
|
||||
isSelected = page.url.pathname.startsWith(href);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
{#if hasDropdown}
|
||||
<span class="hidden md:block absolute start-1 h-full">
|
||||
<button
|
||||
type="button"
|
||||
aria-label={$t('recent-albums')}
|
||||
class="relative flex cursor-default pt-4 pb-4 select-none justify-center hover:cursor-pointer hover:bg-subtle hover:fill-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary rounded h-fill"
|
||||
onclick={() => (dropdownOpen = !dropdownOpen)}
|
||||
>
|
||||
<Icon
|
||||
icon={dropdownOpen ? mdiChevronDown : mdiChevronLeft}
|
||||
size="1em"
|
||||
class="shrink-0 delay-100 duration-100 "
|
||||
flipped={flippedLogo}
|
||||
aria-hidden
|
||||
/>
|
||||
</button>
|
||||
</span>
|
||||
{/if}
|
||||
<!-- safari still needs a tabIndex=0 -->
|
||||
<a
|
||||
tabindex="0"
|
||||
{href}
|
||||
data-sveltekit-preload-data={preloadData ? 'hover' : 'off'}
|
||||
draggable="false"
|
||||
aria-current={isSelected ? 'page' : undefined}
|
||||
class="flex w-full place-items-center gap-4 rounded-e-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-subtle hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary
|
||||
{isSelected
|
||||
? 'bg-immich-primary/10 dark:text-primary text-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10'
|
||||
: ''}"
|
||||
>
|
||||
<div class="flex w-full place-items-center gap-4 ps-5 overflow-hidden truncate">
|
||||
<Icon {icon} size="1.5em" class="shrink-0" flipped={flippedLogo} aria-hidden />
|
||||
<span class="text-sm font-medium">{title}</span>
|
||||
</div>
|
||||
<div></div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if hasDropdown && dropdownOpen}
|
||||
{@render hasDropdown?.()}
|
||||
{/if}
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
|
||||
import RecentAlbums from '$lib/components/shared-components/side-bar/recent-albums.svelte';
|
||||
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { recentAlbumsDropdown } from '$lib/stores/preferences.store';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { NavbarGroup, NavbarItem } from '@immich/ui';
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiAccountMultiple,
|
||||
@@ -33,119 +34,72 @@
|
||||
} from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fly } from 'svelte/transition';
|
||||
import SideBarLink from './side-bar-link.svelte';
|
||||
|
||||
let isArchiveSelected: boolean = $state(false);
|
||||
let isFavoritesSelected: boolean = $state(false);
|
||||
let isMapSelected: boolean = $state(false);
|
||||
let isPeopleSelected: boolean = $state(false);
|
||||
let isPhotosSelected: boolean = $state(false);
|
||||
let isSharingSelected: boolean = $state(false);
|
||||
let isTrashSelected: boolean = $state(false);
|
||||
let isUtilitiesSelected: boolean = $state(false);
|
||||
let isLockedFolderSelected: boolean = $state(false);
|
||||
</script>
|
||||
|
||||
<Sidebar ariaLabel={$t('primary')}>
|
||||
<SideBarLink
|
||||
title={$t('photos')}
|
||||
href={resolve('/(user)/photos')}
|
||||
bind:isSelected={isPhotosSelected}
|
||||
icon={isPhotosSelected ? mdiImageMultiple : mdiImageMultipleOutline}
|
||||
></SideBarLink>
|
||||
<NavbarItem title={$t('photos')} href={Route.photos()} icon={mdiImageMultipleOutline} activeIcon={mdiImageMultiple} />
|
||||
|
||||
{#if featureFlagsManager.value.search}
|
||||
<SideBarLink title={$t('explore')} href={resolve('/(user)/explore')} icon={mdiMagnify} />
|
||||
<NavbarItem title={$t('explore')} href={Route.explore()} icon={mdiMagnify} />
|
||||
{/if}
|
||||
|
||||
{#if featureFlagsManager.value.map}
|
||||
<SideBarLink
|
||||
title={$t('map')}
|
||||
href={resolve('/(user)/map')}
|
||||
bind:isSelected={isMapSelected}
|
||||
icon={isMapSelected ? mdiMap : mdiMapOutline}
|
||||
/>
|
||||
<NavbarItem title={$t('map')} href={Route.map()} icon={mdiMapOutline} activeIcon={mdiMap} />
|
||||
{/if}
|
||||
|
||||
{#if $preferences.people.enabled && $preferences.people.sidebarWeb}
|
||||
<SideBarLink
|
||||
title={$t('people')}
|
||||
href={resolve('/(user)/people')}
|
||||
bind:isSelected={isPeopleSelected}
|
||||
icon={isPeopleSelected ? mdiAccount : mdiAccountOutline}
|
||||
/>
|
||||
<NavbarItem title={$t('people')} href={Route.people()} icon={mdiAccountOutline} activeIcon={mdiAccount} />
|
||||
{/if}
|
||||
|
||||
{#if $preferences.sharedLinks.enabled && $preferences.sharedLinks.sidebarWeb}
|
||||
<SideBarLink title={$t('shared_links')} href={resolve('/(user)/shared-links')} icon={mdiLink} />
|
||||
<NavbarItem title={$t('shared_links')} href={Route.sharedLinks()} icon={mdiLink} />
|
||||
{/if}
|
||||
|
||||
<SideBarLink
|
||||
<NavbarItem
|
||||
title={$t('sharing')}
|
||||
href={resolve('/(user)/sharing')}
|
||||
icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline}
|
||||
bind:isSelected={isSharingSelected}
|
||||
></SideBarLink>
|
||||
href={Route.sharing()}
|
||||
icon={mdiAccountMultipleOutline}
|
||||
activeIcon={mdiAccountMultiple}
|
||||
/>
|
||||
|
||||
<p class="text-xs py-5 ps-6 dark:text-immich-dark-fg uppercase">{$t('library')}</p>
|
||||
<NavbarGroup title={$t('library')} size="tiny" />
|
||||
|
||||
<SideBarLink
|
||||
title={$t('favorites')}
|
||||
href={resolve('/(user)/favorites')}
|
||||
icon={isFavoritesSelected ? mdiHeart : mdiHeartOutline}
|
||||
bind:isSelected={isFavoritesSelected}
|
||||
></SideBarLink>
|
||||
<NavbarItem title={$t('favorites')} href={Route.favorites()} icon={mdiHeartOutline} activeIcon={mdiHeart} />
|
||||
|
||||
<SideBarLink
|
||||
<NavbarItem
|
||||
title={$t('albums')}
|
||||
href={resolve('/(user)/albums')}
|
||||
icon={mdiImageAlbum}
|
||||
flippedLogo
|
||||
bind:dropdownOpen={$recentAlbumsDropdown}
|
||||
href={Route.albums()}
|
||||
icon={{ icon: mdiImageAlbum, flipped: true }}
|
||||
bind:expanded={$recentAlbumsDropdown}
|
||||
>
|
||||
{#snippet dropDownContent()}
|
||||
{#snippet items()}
|
||||
<span in:fly={{ y: -20 }} class="hidden md:block">
|
||||
<RecentAlbums />
|
||||
</span>
|
||||
{/snippet}
|
||||
</SideBarLink>
|
||||
</NavbarItem>
|
||||
|
||||
{#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
|
||||
<SideBarLink title={$t('tags')} href={resolve('/(user)/tags')} icon={mdiTagMultipleOutline} flippedLogo />
|
||||
<NavbarItem title={$t('tags')} href={Route.tags()} icon={{ icon: mdiTagMultipleOutline, flipped: true }} />
|
||||
{/if}
|
||||
|
||||
{#if $preferences.folders.enabled && $preferences.folders.sidebarWeb}
|
||||
<SideBarLink title={$t('folders')} href={resolve('/(user)/folders')} icon={mdiFolderOutline} flippedLogo />
|
||||
<NavbarItem title={$t('folders')} href={Route.folders()} icon={{ icon: mdiFolderOutline, flipped: true }} />
|
||||
{/if}
|
||||
|
||||
<SideBarLink
|
||||
title={$t('utilities')}
|
||||
href={resolve('/(user)/utilities')}
|
||||
bind:isSelected={isUtilitiesSelected}
|
||||
icon={isUtilitiesSelected ? mdiToolbox : mdiToolboxOutline}
|
||||
></SideBarLink>
|
||||
<NavbarItem title={$t('utilities')} href={Route.utilities()} icon={mdiToolboxOutline} activeIcon={mdiToolbox} />
|
||||
|
||||
<SideBarLink
|
||||
<NavbarItem
|
||||
title={$t('archive')}
|
||||
href={resolve('/(user)/archive')}
|
||||
bind:isSelected={isArchiveSelected}
|
||||
icon={isArchiveSelected ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline}
|
||||
></SideBarLink>
|
||||
href={Route.archive()}
|
||||
icon={mdiArchiveArrowDownOutline}
|
||||
activeIcon={mdiArchiveArrowDown}
|
||||
/>
|
||||
|
||||
<SideBarLink
|
||||
title={$t('locked_folder')}
|
||||
href={resolve('/(user)/locked')}
|
||||
bind:isSelected={isLockedFolderSelected}
|
||||
icon={isLockedFolderSelected ? mdiLock : mdiLockOutline}
|
||||
></SideBarLink>
|
||||
<NavbarItem title={$t('locked_folder')} href={Route.locked()} icon={mdiLockOutline} activeIcon={mdiLock} />
|
||||
|
||||
{#if featureFlagsManager.value.trash}
|
||||
<SideBarLink
|
||||
title={$t('trash')}
|
||||
href={resolve('/(user)/trash')}
|
||||
bind:isSelected={isTrashSelected}
|
||||
icon={isTrashSelected ? mdiTrashCan : mdiTrashCanOutline}
|
||||
></SideBarLink>
|
||||
<NavbarItem title={$t('trash')} href={Route.trash()} icon={mdiTrashCanOutline} activeIcon={mdiTrashCan} />
|
||||
{/if}
|
||||
|
||||
<BottomInfo />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
import type { UploadAsset } from '$lib/types';
|
||||
@@ -34,10 +34,6 @@
|
||||
uploadAssetsStore.removeItem(uploadAsset.id);
|
||||
await fileUploadHandler({ files: [uploadAsset.file], albumId: uploadAsset.albumId });
|
||||
};
|
||||
|
||||
const asLink = (asset: UploadAsset) => {
|
||||
return asset.isTrashed ? `${AppRoute.TRASH}/${asset.assetId}` : `${AppRoute.PHOTOS}/${uploadAsset.assetId}`;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -69,7 +65,9 @@
|
||||
{#if uploadAsset.state === UploadState.DUPLICATED && uploadAsset.assetId}
|
||||
<div class="flex items-center justify-between gap-1">
|
||||
<a
|
||||
href={asLink(uploadAsset)}
|
||||
href={uploadAsset.isTrashed
|
||||
? Route.viewTrashedAsset({ id: uploadAsset.assetId })
|
||||
: Route.viewAsset({ id: uploadAsset.assetId })}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class=""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ActionButton from '$lib/components/ActionButton.svelte';
|
||||
import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { getSharedLinkActions } from '$lib/services/shared-link.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
@@ -61,7 +61,7 @@
|
||||
>
|
||||
<svelte:element
|
||||
this={isExpired ? 'div' : 'a'}
|
||||
href={isExpired ? undefined : `${AppRoute.SHARE}/${sharedLink.key}`}
|
||||
href={isExpired ? undefined : Route.viewSharedLink(sharedLink)}
|
||||
class="flex gap-4 w-full py-4"
|
||||
>
|
||||
<ShareCover class="transition-all duration-300 hover:shadow-lg" {sharedLink} />
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
if (assets.length === 1) {
|
||||
clearSelect();
|
||||
let asset = await getAssetInfo({ ...authManager.params, id: assets[0].id });
|
||||
await handleDownloadAsset(asset);
|
||||
await handleDownloadAsset(asset, { edited: true });
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
setFocusToAsset as setFocusAssetInit,
|
||||
setFocusTo as setFocusToInit,
|
||||
} from '$lib/components/timeline/actions/focus-actions';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
@@ -13,6 +12,7 @@
|
||||
import AssetDeleteConfirmModal from '$lib/modals/AssetDeleteConfirmModal.svelte';
|
||||
import NavigateToDateModal from '$lib/modals/NavigateToDateModal.svelte';
|
||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||
@@ -149,7 +149,7 @@
|
||||
|
||||
const shortcuts: ShortcutOptions[] = [
|
||||
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
|
||||
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
|
||||
{ shortcut: { key: '/' }, onShortcut: () => goto(Route.explore()) },
|
||||
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(timelineManager, assetInteraction) },
|
||||
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') },
|
||||
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') },
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte';
|
||||
import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte';
|
||||
import UserUsageStatistic from '$lib/components/user-settings-page/user-usage-statistic.svelte';
|
||||
import { OpenSettingQueryParameterValue, QueryParameter } from '$lib/constants';
|
||||
import { OpenQueryParam, QueryParameter } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { oauth } from '$lib/utils';
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
let oauthOpen =
|
||||
oauth.isCallback(globalThis.location) ||
|
||||
$page.url.searchParams.get(QueryParameter.OPEN_SETTING) === OpenSettingQueryParameterValue.OAUTH;
|
||||
$page.url.searchParams.get(QueryParameter.OPEN_SETTING) === OpenQueryParam.OAUTH;
|
||||
</script>
|
||||
|
||||
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
|
||||
@@ -105,7 +105,7 @@
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiBellOutline}
|
||||
key="notifications"
|
||||
key={OpenQueryParam.NOTIFICATIONS}
|
||||
title={$t('notifications')}
|
||||
subtitle={$t('notifications_setting_description')}
|
||||
>
|
||||
@@ -115,7 +115,7 @@
|
||||
{#if featureFlagsManager.value.oauth}
|
||||
<SettingAccordion
|
||||
icon={mdiTwoFactorAuthentication}
|
||||
key="oauth"
|
||||
key={OpenQueryParam.OAUTH}
|
||||
title={$t('oauth')}
|
||||
subtitle={$t('manage_your_oauth_connection')}
|
||||
isOpen={oauthOpen || undefined}
|
||||
@@ -154,7 +154,7 @@
|
||||
|
||||
<SettingAccordion
|
||||
icon={mdiKeyOutline}
|
||||
key="user-purchase-settings"
|
||||
key={OpenQueryParam.PURCHASE_SETTINGS}
|
||||
title={$t('user_purchase_settings')}
|
||||
subtitle={$t('user_purchase_settings_description')}
|
||||
autoScrollTo={true}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import AppDownloadModal from '$lib/modals/AppDownloadModal.svelte';
|
||||
import ObtainiumConfigModal from '$lib/modals/ObtainiumConfigModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { Icon, modalManager } from '@immich/ui';
|
||||
import {
|
||||
mdiCellphoneArrowDownVariant,
|
||||
@@ -14,10 +14,10 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const links = [
|
||||
{ href: AppRoute.DUPLICATES, icon: mdiContentDuplicate, label: $t('review_duplicates') },
|
||||
{ href: AppRoute.LARGE_FILES, icon: mdiImageSizeSelectLarge, label: $t('review_large_files') },
|
||||
{ href: AppRoute.GEOLOCATION, icon: mdiCrosshairsGps, label: $t('manage_geolocation') },
|
||||
{ href: AppRoute.WORKFLOWS, icon: mdiStateMachine, label: $t('workflows') },
|
||||
{ href: Route.duplicatesUtility(), icon: mdiContentDuplicate, label: $t('review_duplicates') },
|
||||
{ href: Route.largeFileUtility(), icon: mdiImageSizeSelectLarge, label: $t('review_large_files') },
|
||||
{ href: Route.geolocationUtility(), icon: mdiCrosshairsGps, label: $t('manage_geolocation') },
|
||||
{ href: Route.workflows(), icon: mdiStateMachine, label: $t('workflows') },
|
||||
];
|
||||
</script>
|
||||
|
||||
|
||||
+11
-55
@@ -19,55 +19,7 @@ export enum AssetAction {
|
||||
RATING = 'rating',
|
||||
}
|
||||
|
||||
export enum AppRoute {
|
||||
ADMIN_USERS = '/admin/users',
|
||||
ADMIN_USERS_NEW = '/admin/users/new',
|
||||
ADMIN_LIBRARIES = '/admin/library-management',
|
||||
ADMIN_LIBRARIES_NEW = '/admin/library-management/new',
|
||||
ADMIN_SETTINGS = '/admin/system-settings',
|
||||
ADMIN_MAINTENANCE_SETTINGS = '/admin/maintenance',
|
||||
ADMIN_MAINTENANCE_INTEGRITY_REPORT = '/admin/maintenance/integrity-report/',
|
||||
ADMIN_STATS = '/admin/server-status',
|
||||
ADMIN_QUEUES = '/admin/queues',
|
||||
ADMIN_REPAIR = '/admin/repair',
|
||||
|
||||
ALBUMS = '/albums',
|
||||
LIBRARIES = '/libraries',
|
||||
ARCHIVE = '/archive',
|
||||
FAVORITES = '/favorites',
|
||||
PEOPLE = '/people',
|
||||
PLACES = '/places',
|
||||
PHOTOS = '/photos',
|
||||
EXPLORE = '/explore',
|
||||
SHARE = '/share',
|
||||
SHARING = '/sharing',
|
||||
SHARED_LINKS = '/shared-links',
|
||||
SEARCH = '/search',
|
||||
MAP = '/map',
|
||||
USER_SETTINGS = '/user-settings',
|
||||
MEMORY = '/memory',
|
||||
TRASH = '/trash',
|
||||
PARTNERS = '/partners',
|
||||
BUY = '/buy',
|
||||
|
||||
AUTH_LOGIN = '/auth/login',
|
||||
AUTH_REGISTER = '/auth/register',
|
||||
AUTH_CHANGE_PASSWORD = '/auth/change-password',
|
||||
AUTH_ONBOARDING = '/auth/onboarding',
|
||||
AUTH_PIN_PROMPT = '/auth/pin-prompt',
|
||||
|
||||
UTILITIES = '/utilities',
|
||||
DUPLICATES = '/utilities/duplicates',
|
||||
LARGE_FILES = '/utilities/large-files',
|
||||
GEOLOCATION = '/utilities/geolocation',
|
||||
WORKFLOWS = '/utilities/workflows',
|
||||
|
||||
FOLDERS = '/folders',
|
||||
TAGS = '/tags',
|
||||
LOCKED = '/locked',
|
||||
|
||||
MAINTENANCE = '/maintenance',
|
||||
}
|
||||
export type SharedLinkTab = 'all' | 'album' | 'individual';
|
||||
|
||||
export enum ProjectionType {
|
||||
EQUIRECTANGULAR = 'EQUIRECTANGULAR',
|
||||
@@ -97,7 +49,6 @@ export enum QueryParameter {
|
||||
ACTION = 'action',
|
||||
ID = 'id',
|
||||
IS_OPEN = 'isOpen',
|
||||
ONBOARDING_STEP = 'step',
|
||||
OPEN_SETTING = 'openSetting',
|
||||
PREVIOUS_ROUTE = 'previousRoute',
|
||||
QUERY = 'query',
|
||||
@@ -112,14 +63,13 @@ export enum SessionStorageKey {
|
||||
SCROLL_POSITION = 'scrollPosition',
|
||||
}
|
||||
|
||||
export enum OpenSettingQueryParameterValue {
|
||||
// TODO split into user settings vs system settings
|
||||
export enum OpenQueryParam {
|
||||
OAUTH = 'oauth',
|
||||
JOB = 'job',
|
||||
STORAGE_TEMPLATE = 'storage-template',
|
||||
}
|
||||
|
||||
export enum ActionQueryParameterValue {
|
||||
MERGE = 'merge',
|
||||
NOTIFICATIONS = 'notifications',
|
||||
PURCHASE_SETTINGS = 'user-purchase-settings',
|
||||
}
|
||||
|
||||
export const maximumLengthSearchPeople = 1000;
|
||||
@@ -446,4 +396,10 @@ export enum ToggleVisibility {
|
||||
SHOW_ALL = 'show-all',
|
||||
}
|
||||
|
||||
export enum BackupFileStatus {
|
||||
OK,
|
||||
DifferentVersion,
|
||||
UnknownVersion,
|
||||
}
|
||||
|
||||
export const assetViewerFadeDuration: number = 150;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { isSharedLinkRoute } from '$lib/utils/navigation';
|
||||
import { logout } from '@immich/sdk';
|
||||
|
||||
@@ -21,7 +21,7 @@ class AuthManager {
|
||||
console.log('Error logging out:', error);
|
||||
}
|
||||
|
||||
redirectUri = redirectUri ?? AppRoute.AUTH_LOGIN;
|
||||
redirectUri = redirectUri ?? Route.login();
|
||||
|
||||
try {
|
||||
if (redirectUri.startsWith('/')) {
|
||||
|
||||
@@ -115,7 +115,7 @@ export class EditManager {
|
||||
// Setup the websocket listener before sending the edit request
|
||||
const editCompleted = waitForWebsocketEvent(
|
||||
'AssetEditReadyV1',
|
||||
(event) => event.assetId === this.currentAsset!.id,
|
||||
(event) => event.asset.id === this.currentAsset!.id,
|
||||
10_000,
|
||||
);
|
||||
|
||||
|
||||
@@ -47,6 +47,10 @@ export type Events = {
|
||||
|
||||
PersonUpdate: [PersonResponseDto];
|
||||
|
||||
BackupDeleteStatus: [{ filename: string; isDeleting: boolean }];
|
||||
BackupDeleted: [{ filename: string }];
|
||||
BackupUpload: [{ progress: number; isComplete: boolean }];
|
||||
|
||||
QueueUpdate: [QueueResponseDto];
|
||||
|
||||
SharedLinkCreate: [SharedLinkResponseDto];
|
||||
@@ -67,6 +71,8 @@ export type Events = {
|
||||
// confirmed permanently deleted from server
|
||||
UserAdminDeleted: [{ id: string }];
|
||||
|
||||
SessionLocked: [];
|
||||
|
||||
SystemConfigUpdate: [SystemConfigDto];
|
||||
|
||||
IntegrityReportDeleteStatus: [{ type?: IntegrityReportType; id?: string; isDeleting: boolean }];
|
||||
@@ -78,6 +84,7 @@ export type Events = {
|
||||
LibraryUpdate: [LibraryResponseDto];
|
||||
LibraryDelete: [{ id: string }];
|
||||
|
||||
WorkflowCreate: [WorkflowResponseDto];
|
||||
WorkflowUpdate: [WorkflowResponseDto];
|
||||
WorkflowDelete: [WorkflowResponseDto];
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { OpenQueryParam } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
|
||||
describe('Route', () => {
|
||||
describe(Route.login.name, () => {
|
||||
it('should encode continue', () => {
|
||||
expect(Route.login({ continue: '/some/path?with=query', autoLaunch: 1 })).toBe(
|
||||
'/auth/login?continue=%2Fsome%2Fpath%3Fwith%3Dquery&autoLaunch=1',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(Route.search.name, () => {
|
||||
it('should work', () => {
|
||||
expect(Route.search({})).toBe('/search');
|
||||
});
|
||||
|
||||
it('should work', () => {
|
||||
expect(Route.search({ make: undefined, model: 'Immich' })).toBe('/search?query=%7B%22model%22%3A%22Immich%22%7D');
|
||||
});
|
||||
|
||||
it('should support query parameters', () => {
|
||||
expect(Route.systemSettings({ isOpen: OpenQueryParam.OAUTH })).toBe('/admin/system-settings?isOpen=oauth');
|
||||
});
|
||||
});
|
||||
|
||||
describe(Route.tags.name, () => {
|
||||
it('should work', () => {
|
||||
expect(Route.tags()).toBe('/tags');
|
||||
});
|
||||
|
||||
it('should support query parameters', () => {
|
||||
expect(Route.tags({ path: '/some/path' })).toBe('/tags?path=%2Fsome%2Fpath');
|
||||
});
|
||||
|
||||
it('should ignore an empty path', () => {
|
||||
expect(Route.tags({ path: '' })).toBe('/tags');
|
||||
});
|
||||
});
|
||||
|
||||
describe(Route.systemSettings.name, () => {
|
||||
it('should work', () => {
|
||||
expect(Route.systemSettings()).toBe('/admin/system-settings');
|
||||
});
|
||||
|
||||
it('should support query parameters', () => {
|
||||
expect(Route.systemSettings({ isOpen: OpenQueryParam.OAUTH })).toBe('/admin/system-settings?isOpen=oauth');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import { OpenQueryParam, type SharedLinkTab } from '$lib/constants';
|
||||
import { QueueName, type MetadataSearchDto, type SmartSearchDto } from '@immich/sdk';
|
||||
import { omitBy } from 'lodash-es';
|
||||
|
||||
const asQueueSlug = (name: QueueName) => {
|
||||
return name.replaceAll(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
||||
};
|
||||
|
||||
export const fromQueueSlug = (slug: string): QueueName | undefined => {
|
||||
const name = slug.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||
if (Object.values(QueueName).includes(name as QueueName)) {
|
||||
return name as QueueName;
|
||||
}
|
||||
};
|
||||
|
||||
type QueryValue = number | string;
|
||||
const asQueryString = (
|
||||
params?: Record<string, QueryValue | undefined>,
|
||||
options?: { skipEmptyStrings?: boolean; skipNullValues?: boolean },
|
||||
) => {
|
||||
const { skipEmptyStrings = true, skipNullValues = true } = options ?? {};
|
||||
const items = Object.entries(params ?? {})
|
||||
.filter((item): item is [string, QueryValue] => {
|
||||
const value = item[1];
|
||||
|
||||
if (value === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skipNullValues && value === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skipEmptyStrings && value === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
||||
|
||||
return items.length === 0 ? '' : `?${items.join('&')}`;
|
||||
};
|
||||
|
||||
export const Route = {
|
||||
// auth
|
||||
login: (params?: { continue?: string; autoLaunch?: 0 | 1 }) => '/auth/login' + asQueryString(params),
|
||||
register: () => '/auth/register',
|
||||
changePassword: () => '/auth/change-password',
|
||||
onboarding: (params?: { step?: string }) => '/auth/onboarding' + asQueryString(params),
|
||||
pinPrompt: (params?: { continue?: string }) => '/auth/pin-prompt' + asQueryString({ continue: params?.continue }),
|
||||
|
||||
// albums
|
||||
albums: () => '/albums',
|
||||
viewAlbum: ({ id }: { id: string }) => `/albums/${id}`,
|
||||
viewAlbumAsset: ({ albumId, assetId }: { albumId: string; assetId: string }) =>
|
||||
`/albums/${albumId}/photos/${assetId}`,
|
||||
|
||||
// buy
|
||||
buy: () => '/buy',
|
||||
|
||||
// explore
|
||||
explore: () => '/explore',
|
||||
places: () => '/places',
|
||||
|
||||
// folders
|
||||
folders: (params?: { path?: string }) => '/folders' + asQueryString(params),
|
||||
|
||||
// libraries
|
||||
libraries: () => '/admin/library-management',
|
||||
newLibrary: () => '/admin/library-management/new',
|
||||
viewLibrary: ({ id }: { id: string }) => `/admin/library-management/${id}`,
|
||||
editLibrary: ({ id }: { id: string }) => `/admin/library-management/${id}/edit`,
|
||||
|
||||
// maintenance
|
||||
maintenanceMode: (params?: { continue?: string }) => '/maintenance' + asQueryString(params),
|
||||
|
||||
// map
|
||||
map: (point?: { zoom: number; lat: number; lng: number }) =>
|
||||
'/map' + (point ? `#${point.zoom}/${point.lat}/${point.lng}` : ''),
|
||||
|
||||
// memories
|
||||
memories: (params?: { id?: string }) => '/memory' + asQueryString(params),
|
||||
|
||||
// partners
|
||||
viewPartner: ({ id }: { id: string }) => `/partners/${id}`,
|
||||
|
||||
// people
|
||||
people: () => '/people',
|
||||
viewPerson: ({ id }: { id: string }, params?: { previousRoute?: string; action?: 'merge' }) =>
|
||||
`/people/${id}` + asQueryString(params),
|
||||
|
||||
// photos
|
||||
photos: (params?: { at?: string }) => '/photos' + asQueryString(params),
|
||||
viewAsset: ({ id }: { id: string }) => `/photos/${id}`,
|
||||
archive: () => '/archive',
|
||||
favorites: () => '/favorites',
|
||||
locked: () => '/locked',
|
||||
trash: () => '/trash',
|
||||
viewTrashedAsset: ({ id }: { id: string }) => `/trash/photos/${id}`,
|
||||
|
||||
// search
|
||||
search: (dto?: MetadataSearchDto | SmartSearchDto) => {
|
||||
const metadata = omitBy(dto ?? {}, (value) => value === undefined);
|
||||
const query = Object.keys(metadata).length === 0 ? undefined : JSON.stringify(metadata);
|
||||
return `/search` + asQueryString({ query });
|
||||
},
|
||||
|
||||
// sharing
|
||||
sharing: () => '/sharing',
|
||||
|
||||
// shared links
|
||||
sharedLinks: (params?: { filter?: SharedLinkTab }) => '/shared-links' + asQueryString(params),
|
||||
editSharedLink: ({ id }: { id: string }) => `/shared-links/${id}/edit`,
|
||||
viewSharedLink: ({ slug, key }: { slug?: string | null; key: string }) => (slug ? `/s/${slug}` : `/share/${key}`),
|
||||
|
||||
// settings
|
||||
userSettings: (params?: { isOpen?: OpenQueryParam }) => '/user-settings' + asQueryString(params),
|
||||
|
||||
// system
|
||||
systemSettings: (params?: { isOpen?: OpenQueryParam }) => '/admin/system-settings' + asQueryString(params),
|
||||
systemStatistics: () => '/admin/server-status',
|
||||
systemMaintenance: (params?: { continue?: string }) => '/admin/maintenance' + asQueryString(params),
|
||||
|
||||
// tags
|
||||
tags: (params?: { path?: string }) => '/tags' + asQueryString(params),
|
||||
|
||||
// users
|
||||
users: () => '/admin/users',
|
||||
newUser: () => `/admin/users/new`,
|
||||
viewUser: ({ id }: { id: string }) => `/admin/users/${id}`,
|
||||
editUser: ({ id }: { id: string }) => `/admin/users/${id}/edit`,
|
||||
|
||||
// utilities
|
||||
utilities: () => '/utilities',
|
||||
duplicatesUtility: (params?: { index?: number }) => '/utilities/duplicates' + asQueryString(params),
|
||||
largeFileUtility: () => '/utilities/large-files',
|
||||
geolocationUtility: () => '/utilities/geolocation',
|
||||
|
||||
// workflows
|
||||
workflows: () => '/utilities/workflows',
|
||||
viewWorkflow: ({ id }: { id: string }) => `/utilities/workflows/${id}`,
|
||||
|
||||
// queues
|
||||
queues: () => '/admin/queues',
|
||||
viewQueue: ({ name }: { name: QueueName }) => `/admin/queues/${asQueueSlug(name)}`,
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import ToastAction from '$lib/components/ToastAction.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import AlbumAddUsersModal from '$lib/modals/AlbumAddUsersModal.svelte';
|
||||
import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { createAlbumAndRedirect } from '$lib/utils/album-utils';
|
||||
import { downloadArchive } from '$lib/utils/asset-utils';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -28,6 +29,16 @@ import { mdiLink, mdiPlus, mdiPlusBoxOutline, mdiShareVariantOutline, mdiUpload
|
||||
import { type MessageFormatter } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export const getAlbumsActions = ($t: MessageFormatter) => {
|
||||
const Create: ActionItem = {
|
||||
title: $t('create_album'),
|
||||
icon: mdiPlusBoxOutline,
|
||||
onAction: () => createAlbumAndRedirect(),
|
||||
};
|
||||
|
||||
return { Create };
|
||||
};
|
||||
|
||||
export const getAlbumActions = ($t: MessageFormatter, album: AlbumResponseDto) => {
|
||||
const isOwned = get(user).id === album.ownerId;
|
||||
|
||||
@@ -161,9 +172,7 @@ export const handleUpdateAlbum = async ({ id }: { id: string }, dto: UpdateAlbum
|
||||
button: {
|
||||
text: $t('view_album'),
|
||||
color: 'primary',
|
||||
onClick() {
|
||||
return goto(`${AppRoute.ALBUMS}/${id}`);
|
||||
},
|
||||
onClick: () => goto(Route.viewAlbum({ id })),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiAlertOutline,
|
||||
mdiDownload,
|
||||
mdiDownloadBox,
|
||||
mdiHeart,
|
||||
mdiHeartOutline,
|
||||
mdiInformationOutline,
|
||||
@@ -51,7 +52,15 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
shortcuts: { key: 'd', shift: true },
|
||||
type: $t('assets'),
|
||||
$if: () => !!currentAuthUser,
|
||||
onAction: () => handleDownloadAsset(asset),
|
||||
onAction: () => handleDownloadAsset(asset, { edited: true }),
|
||||
};
|
||||
|
||||
const DownloadOriginal: ActionItem = {
|
||||
title: $t('download_original'),
|
||||
icon: mdiDownloadBox,
|
||||
type: $t('assets'),
|
||||
$if: () => !!currentAuthUser && asset.isEdited,
|
||||
onAction: () => handleDownloadAsset(asset, { edited: false }),
|
||||
};
|
||||
|
||||
const SharedLinkDownload: ActionItem = {
|
||||
@@ -115,10 +124,21 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
shortcuts: [{ key: 'i' }],
|
||||
};
|
||||
|
||||
return { Share, Download, SharedLinkDownload, Offline, Info, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto };
|
||||
return {
|
||||
Share,
|
||||
Download,
|
||||
DownloadOriginal,
|
||||
SharedLinkDownload,
|
||||
Offline,
|
||||
Info,
|
||||
Favorite,
|
||||
Unfavorite,
|
||||
PlayMotionPhoto,
|
||||
StopMotionPhoto,
|
||||
};
|
||||
};
|
||||
|
||||
export const handleDownloadAsset = async (asset: AssetResponseDto) => {
|
||||
export const handleDownloadAsset = async (asset: AssetResponseDto, { edited }: { edited: boolean }) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
const assets = [
|
||||
@@ -154,7 +174,12 @@ export const handleDownloadAsset = async (asset: AssetResponseDto) => {
|
||||
|
||||
try {
|
||||
toastManager.success($t('downloading_asset_filename', { values: { filename: asset.originalFileName } }));
|
||||
downloadUrl(getBaseUrl() + `/assets/${id}/original` + (queryParams ? `?${queryParams}` : ''), filename);
|
||||
downloadUrl(
|
||||
getBaseUrl() +
|
||||
`/assets/${id}/original` +
|
||||
(queryParams ? `?${queryParams}&edited=${edited}` : `?edited=${edited}`),
|
||||
filename,
|
||||
);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.error_downloading', { values: { filename } }));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { uploadRequest } from '$lib/utils';
|
||||
import { openFilePicker } from '$lib/utils/file-uploader';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import {
|
||||
deleteDatabaseBackup,
|
||||
getBaseUrl,
|
||||
MaintenanceAction,
|
||||
setMaintenanceMode,
|
||||
type DatabaseBackupUploadDto,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiDownload, mdiTrashCanOutline } from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
export const getDatabaseBackupActions = ($t: MessageFormatter, filename: string) => {
|
||||
const Download: ActionItem = {
|
||||
type: $t('command'),
|
||||
title: $t('download'),
|
||||
icon: mdiDownload,
|
||||
onAction: () => handleDownloadDatabaseBackup(filename),
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
type: $t('command'),
|
||||
title: $t('delete'),
|
||||
icon: mdiTrashCanOutline,
|
||||
color: 'danger',
|
||||
onAction: () => handleDeleteDatabaseBackup(filename),
|
||||
};
|
||||
|
||||
return { Download, Delete };
|
||||
};
|
||||
|
||||
export const handleRestoreDatabaseBackup = async (filename: string) => {
|
||||
const $t = await getFormatter();
|
||||
const confirm = await modalManager.showDialog({
|
||||
confirmText: $t('restore'),
|
||||
title: $t('admin.maintenance_restore_backup'),
|
||||
prompt: $t('admin.maintenance_restore_backup_description'),
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await setMaintenanceMode({
|
||||
setMaintenanceModeDto: {
|
||||
action: MaintenanceAction.RestoreDatabase,
|
||||
restoreBackupFilename: filename,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.maintenance_start_error'));
|
||||
}
|
||||
};
|
||||
|
||||
export const handleDeleteDatabaseBackup = async (...filenames: string[]) => {
|
||||
const $t = await getFormatter();
|
||||
const confirm = await modalManager.showDialog({
|
||||
confirmText: $t('delete'),
|
||||
title: $t('admin.maintenance_delete_backup'),
|
||||
prompt: $t('admin.maintenance_delete_backup_description'),
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const filename of filenames) {
|
||||
eventManager.emit('BackupDeleteStatus', { filename, isDeleting: true });
|
||||
}
|
||||
|
||||
await deleteDatabaseBackup({
|
||||
databaseBackupDeleteDto: {
|
||||
backups: filenames,
|
||||
},
|
||||
});
|
||||
|
||||
for (const filename of filenames) {
|
||||
eventManager.emit('BackupDeleted', { filename });
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.maintenance_delete_error'));
|
||||
|
||||
for (const filename of filenames) {
|
||||
eventManager.emit('BackupDeleteStatus', { filename, isDeleting: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const handleDownloadDatabaseBackup = (filename: string) => {
|
||||
location.href = getBaseUrl() + '/admin/database-backups/' + filename;
|
||||
};
|
||||
|
||||
export const handleUploadDatabaseBackup = async () => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
const [file] = await openFilePicker({ multiple: false });
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
await uploadRequest<DatabaseBackupUploadDto>({
|
||||
url: getBaseUrl() + '/admin/database-backups/upload',
|
||||
data: formData,
|
||||
onUploadProgress(event) {
|
||||
eventManager.emit('BackupUpload', { progress: event.loaded / event.total, isComplete: false });
|
||||
},
|
||||
});
|
||||
|
||||
eventManager.emit('BackupUpload', { progress: 1, isComplete: true });
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.maintenance_upload_backup_error'));
|
||||
} finally {
|
||||
eventManager.emit('BackupUpload', { progress: -1, isComplete: false });
|
||||
}
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import LibraryExclusionPatternAddModal from '$lib/modals/LibraryExclusionPatternAddModal.svelte';
|
||||
import LibraryExclusionPatternEditModal from '$lib/modals/LibraryExclusionPatternEditModal.svelte';
|
||||
import LibraryFolderAddModal from '$lib/modals/LibraryFolderAddModal.svelte';
|
||||
import LibraryFolderEditModal from '$lib/modals/LibraryFolderEditModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import {
|
||||
@@ -37,7 +37,7 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
|
||||
title: $t('create_library'),
|
||||
type: $t('command'),
|
||||
icon: mdiPlusBoxOutline,
|
||||
onAction: () => goto(AppRoute.ADMIN_LIBRARIES_NEW),
|
||||
onAction: () => goto(Route.newLibrary()),
|
||||
shortcuts: { shift: true, key: 'n' },
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
||||
icon: mdiPencilOutline,
|
||||
type: $t('command'),
|
||||
title: $t('edit'),
|
||||
onAction: () => goto(`${AppRoute.ADMIN_LIBRARIES}/${library.id}/edit`),
|
||||
onAction: () => goto(Route.editLibrary(library)),
|
||||
shortcuts: { key: 'r' },
|
||||
};
|
||||
|
||||
@@ -148,10 +148,6 @@ const handleScanLibrary = async (library: LibraryResponseDto) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const handleViewLibrary = async (library: LibraryResponseDto) => {
|
||||
await goto(`${AppRoute.ADMIN_LIBRARIES}/${library.id}`);
|
||||
};
|
||||
|
||||
export const handleCreateLibrary = async (dto: CreateLibraryDto) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
|
||||
@@ -4,7 +4,13 @@ import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { updatePerson, type PersonResponseDto } from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiCalendarEditOutline } from '@mdi/js';
|
||||
import {
|
||||
mdiCalendarEditOutline,
|
||||
mdiEyeOffOutline,
|
||||
mdiEyeOutline,
|
||||
mdiHeartMinusOutline,
|
||||
mdiHeartOutline,
|
||||
} from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
export const getPersonActions = ($t: MessageFormatter, person: PersonResponseDto) => {
|
||||
@@ -14,7 +20,83 @@ export const getPersonActions = ($t: MessageFormatter, person: PersonResponseDto
|
||||
onAction: () => modalManager.show(PersonEditBirthDateModal, { person }),
|
||||
};
|
||||
|
||||
return { SetDateOfBirth };
|
||||
const Favorite: ActionItem = {
|
||||
title: $t('to_favorite'),
|
||||
icon: mdiHeartOutline,
|
||||
$if: () => !person.isFavorite,
|
||||
onAction: () => handleFavoritePerson(person),
|
||||
};
|
||||
|
||||
const Unfavorite: ActionItem = {
|
||||
title: $t('unfavorite'),
|
||||
icon: mdiHeartMinusOutline,
|
||||
$if: () => !!person.isFavorite,
|
||||
onAction: () => handleUnfavoritePerson(person),
|
||||
};
|
||||
|
||||
const HidePerson: ActionItem = {
|
||||
title: $t('hide_person'),
|
||||
icon: mdiEyeOffOutline,
|
||||
$if: () => !person.isHidden,
|
||||
onAction: () => handleHidePerson(person),
|
||||
};
|
||||
|
||||
const ShowPerson: ActionItem = {
|
||||
title: $t('unhide_person'),
|
||||
icon: mdiEyeOutline,
|
||||
$if: () => !!person.isHidden,
|
||||
onAction: () => handleShowPerson(person),
|
||||
};
|
||||
|
||||
return { SetDateOfBirth, Favorite, Unfavorite, HidePerson, ShowPerson };
|
||||
};
|
||||
|
||||
const handleFavoritePerson = async (person: { id: string }) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
const response = await updatePerson({ id: person.id, personUpdateDto: { isFavorite: true } });
|
||||
eventManager.emit('PersonUpdate', response);
|
||||
toastManager.success($t('added_to_favorites'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: false } }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleUnfavoritePerson = async (person: { id: string }) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
const response = await updatePerson({ id: person.id, personUpdateDto: { isFavorite: false } });
|
||||
eventManager.emit('PersonUpdate', response);
|
||||
toastManager.success($t('removed_from_favorites'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: false } }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleHidePerson = async (person: { id: string }) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
const response = await updatePerson({ id: person.id, personUpdateDto: { isHidden: true } });
|
||||
toastManager.success($t('changed_visibility_successfully'));
|
||||
eventManager.emit('PersonUpdate', response);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_hide_person'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleShowPerson = async (person: { id: string }) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
const response = await updatePerson({ id: person.id, personUpdateDto: { isHidden: false } });
|
||||
toastManager.success($t('changed_visibility_successfully'));
|
||||
eventManager.emit('PersonUpdate', response);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.something_went_wrong'));
|
||||
}
|
||||
};
|
||||
|
||||
export const handleUpdatePersonBirthDate = async (person: PersonResponseDto, birthDate: string) => {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { OpenQueryParam } 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 { Route } from '$lib/route';
|
||||
import type { HeaderButtonActionItem } from '$lib/types';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
@@ -73,7 +74,7 @@ export const getQueuesActions = ($t: MessageFormatter, queues: QueueResponseDto[
|
||||
title: $t('admin.manage_concurrency'),
|
||||
description: $t('admin.manage_concurrency_description'),
|
||||
type: $t('page'),
|
||||
onAction: () => goto(`${AppRoute.ADMIN_SETTINGS}?isOpen=job`),
|
||||
onAction: () => goto(Route.systemSettings({ isOpen: OpenQueryParam.JOB })),
|
||||
};
|
||||
|
||||
return { ResumePaused, ManageConcurrency, CreateJob };
|
||||
@@ -254,22 +255,3 @@ export const asQueueItem = ($t: MessageFormatter, queue: { name: QueueName }): Q
|
||||
|
||||
return items[queue.name];
|
||||
};
|
||||
|
||||
export const asQueueSlug = (name: QueueName) => {
|
||||
return name.replaceAll(/[A-Z]/g, (m) => '-' + m.toLowerCase());
|
||||
};
|
||||
|
||||
export const fromQueueSlug = (slug: string): QueueName | undefined => {
|
||||
const name = slug.replaceAll(/-([a-z])/g, (_, c) => c.toUpperCase());
|
||||
if (Object.values(QueueName).includes(name as QueueName)) {
|
||||
return name as QueueName;
|
||||
}
|
||||
};
|
||||
|
||||
export const getQueueDetailUrl = (queue: QueueResponseDto) => {
|
||||
return `${AppRoute.ADMIN_QUEUES}/${asQueueSlug(queue.name)}`;
|
||||
};
|
||||
|
||||
export const handleViewQueue = (queue: QueueResponseDto) => {
|
||||
return goto(getQueueDetailUrl(queue));
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
@@ -18,14 +18,24 @@ import {
|
||||
type SharedLinkResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiContentCopy, mdiPencilOutline, mdiQrcode, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { mdiContentCopy, mdiLink, mdiPencilOutline, mdiQrcode, mdiTrashCanOutline } from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
export const getSharedLinksActions = ($t: MessageFormatter) => {
|
||||
const ViewAll: ActionItem = {
|
||||
title: $t('shared_links'),
|
||||
icon: mdiLink,
|
||||
onAction: () => goto(Route.sharedLinks()),
|
||||
};
|
||||
|
||||
return { ViewAll };
|
||||
};
|
||||
|
||||
export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLinkResponseDto) => {
|
||||
const Edit: ActionItem = {
|
||||
title: $t('edit_link'),
|
||||
icon: mdiPencilOutline,
|
||||
onAction: () => goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}/edit`),
|
||||
onAction: () => goto(Route.editSharedLink(sharedLink)),
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
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 UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
|
||||
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { user as authUser } from '$lib/stores/user.store';
|
||||
import type { HeaderButtonActionItem } from '$lib/types';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
@@ -38,7 +38,7 @@ export const getUserAdminsActions = ($t: MessageFormatter) => {
|
||||
title: $t('create_user'),
|
||||
type: $t('command'),
|
||||
icon: mdiPlusBoxOutline,
|
||||
onAction: () => goto(AppRoute.ADMIN_USERS_NEW),
|
||||
onAction: () => goto(Route.newUser()),
|
||||
shortcuts: { shift: true, key: 'n' },
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
||||
const Update: ActionItem = {
|
||||
icon: mdiPencilOutline,
|
||||
title: $t('edit'),
|
||||
onAction: () => goto(`${AppRoute.ADMIN_USERS}/${user.id}/edit`),
|
||||
onAction: () => goto(Route.editUser(user)),
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
|
||||
@@ -1,8 +1,38 @@
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { changePassword, resetPinCode, type ChangePasswordDto, type PinCodeResetDto } from '@immich/sdk';
|
||||
import { toastManager } from '@immich/ui';
|
||||
import {
|
||||
changePassword,
|
||||
lockAuthSession,
|
||||
resetPinCode,
|
||||
type ChangePasswordDto,
|
||||
type PinCodeResetDto,
|
||||
} from '@immich/sdk';
|
||||
import { toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiLockOutline } from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
export const getUserActions = ($t: MessageFormatter) => {
|
||||
const LockSession: ActionItem = {
|
||||
title: $t('lock'),
|
||||
color: 'primary',
|
||||
icon: mdiLockOutline,
|
||||
onAction: () => handleLockSession(),
|
||||
};
|
||||
|
||||
return { LockSession };
|
||||
};
|
||||
|
||||
const handleLockSession = async () => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
await lockAuthSession();
|
||||
eventManager.emit('SessionLocked');
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.something_went_wrong'));
|
||||
}
|
||||
};
|
||||
|
||||
export const handleResetPinCode = async (dto: PinCodeResetDto) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import {
|
||||
@@ -17,12 +17,13 @@ import {
|
||||
type PluginFilterResponseDto,
|
||||
type PluginTriggerResponseDto,
|
||||
type WorkflowActionItemDto,
|
||||
type WorkflowCreateDto,
|
||||
type WorkflowFilterItemDto,
|
||||
type WorkflowResponseDto,
|
||||
type WorkflowUpdateDto,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiCodeJson, mdiDelete, mdiPause, mdiPencil, mdiPlay } from '@mdi/js';
|
||||
import { mdiCodeJson, mdiDelete, mdiPause, mdiPencil, mdiPlay, mdiPlus } from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
export type PickerSubType = 'album-picker' | 'people-picker';
|
||||
@@ -318,6 +319,23 @@ export const handleUpdateWorkflow = async (
|
||||
return updateWorkflow({ id: workflowId, workflowUpdateDto: updateDto });
|
||||
};
|
||||
|
||||
export const getWorkflowsActions = ($t: MessageFormatter) => {
|
||||
const Create: ActionItem = {
|
||||
title: $t('create_workflow'),
|
||||
icon: mdiPlus,
|
||||
onAction: () =>
|
||||
handleCreateWorkflow({
|
||||
name: $t('untitled_workflow'),
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
filters: [],
|
||||
actions: [],
|
||||
enabled: false,
|
||||
}),
|
||||
};
|
||||
|
||||
return { Create };
|
||||
};
|
||||
|
||||
export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowResponseDto) => {
|
||||
const ToggleEnabled: ActionItem = {
|
||||
title: workflow.enabled ? $t('disable') : $t('enable'),
|
||||
@@ -331,7 +349,7 @@ export const getWorkflowActions = ($t: MessageFormatter, workflow: WorkflowRespo
|
||||
const Edit: ActionItem = {
|
||||
title: $t('edit'),
|
||||
icon: mdiPencil,
|
||||
onAction: () => handleNavigateToWorkflow(workflow),
|
||||
onAction: () => goto(Route.viewWorkflow(workflow)),
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
@@ -356,22 +374,12 @@ export const getWorkflowShowSchemaAction = (
|
||||
onAction: onToggle,
|
||||
});
|
||||
|
||||
export const handleCreateWorkflow = async (): Promise<WorkflowResponseDto | undefined> => {
|
||||
const handleCreateWorkflow = async (dto: WorkflowCreateDto) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
const workflow = await createWorkflow({
|
||||
workflowCreateDto: {
|
||||
name: $t('untitled_workflow'),
|
||||
triggerType: PluginTriggerType.AssetCreate,
|
||||
filters: [],
|
||||
actions: [],
|
||||
enabled: false,
|
||||
},
|
||||
});
|
||||
|
||||
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
|
||||
return workflow;
|
||||
const response = await createWorkflow({ workflowCreateDto: dto });
|
||||
eventManager.emit('WorkflowCreate', response);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_create'));
|
||||
}
|
||||
@@ -419,10 +427,6 @@ export const handleDeleteWorkflow = async (workflow: WorkflowResponseDto): Promi
|
||||
}
|
||||
};
|
||||
|
||||
export const handleNavigateToWorkflow = async (workflow: WorkflowResponseDto): Promise<void> => {
|
||||
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
|
||||
};
|
||||
|
||||
export const fetchPickerMetadata = async (
|
||||
value: string | string[] | undefined,
|
||||
subType: PickerSubType,
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { type MaintenanceAuthDto } from '@immich/sdk';
|
||||
import { type MaintenanceAuthDto, type MaintenanceStatusResponseDto } from '@immich/sdk';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const maintenanceAuth = writable<MaintenanceAuthDto>();
|
||||
export const maintenanceStore = {
|
||||
auth: writable<MaintenanceAuthDto>(),
|
||||
status: writable<MaintenanceStatusResponseDto | undefined>(),
|
||||
};
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { page } from '$app/state';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { maintenanceStore } from '$lib/stores/maintenance.store';
|
||||
import { notificationManager } from '$lib/stores/notification-manager.svelte';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { createEventEmitter } from '$lib/utils/eventemitter';
|
||||
import { type AssetResponseDto, type NotificationDto, type ServerVersionResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
MaintenanceAction,
|
||||
type AssetResponseDto,
|
||||
type MaintenanceStatusResponseDto,
|
||||
type NotificationDto,
|
||||
type ServerVersionResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { user } from './user.store';
|
||||
@@ -31,7 +38,9 @@ export interface Events {
|
||||
on_notification: (notification: NotificationDto) => void;
|
||||
|
||||
AppRestartV1: (event: AppRestartEvent) => void;
|
||||
AssetEditReadyV1: (data: { assetId: string }) => void;
|
||||
|
||||
MaintenanceStatusV1: (event: MaintenanceStatusResponseDto) => void;
|
||||
AssetEditReadyV1: (data: { asset: { id: string } }) => void;
|
||||
}
|
||||
|
||||
const websocket: Socket<Events> = io({
|
||||
@@ -55,6 +64,15 @@ websocket
|
||||
.on('disconnect', () => websocketStore.connected.set(false))
|
||||
.on('on_server_version', (serverVersion) => websocketStore.serverVersion.set(serverVersion))
|
||||
.on('AppRestartV1', (mode) => websocketStore.serverRestarting.set(mode))
|
||||
.on('MaintenanceStatusV1', (status) => {
|
||||
maintenanceStore.status.set(status);
|
||||
|
||||
if (status.action === MaintenanceAction.End) {
|
||||
websocketStore.serverRestarting.set({
|
||||
isMaintenanceMode: false,
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('on_new_release', (event) => eventManager.emit('ReleaseEvent', event))
|
||||
.on('on_session_delete', () => authManager.logout())
|
||||
.on('on_user_delete', (id) => eventManager.emit('UserAdminDeleted', { id }))
|
||||
@@ -63,7 +81,7 @@ websocket
|
||||
|
||||
export const openWebsocketConnection = () => {
|
||||
try {
|
||||
if (get(user) || page.url.pathname.startsWith(AppRoute.MAINTENANCE)) {
|
||||
if (get(user) || get(websocketStore.serverRestarting) || page.url.pathname.startsWith(Route.maintenanceMode())) {
|
||||
websocket.connect();
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
import type { ZoomImageWheelState } from '@zoom-image/core';
|
||||
import { writable } from 'svelte/store';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
|
||||
export const photoZoomState = writable<ZoomImageWheelState>();
|
||||
export const photoZoomState = writable<ZoomImageWheelState>({
|
||||
currentRotation: 0,
|
||||
currentZoom: 1,
|
||||
enable: true,
|
||||
currentPositionX: 0,
|
||||
currentPositionY: 0,
|
||||
});
|
||||
|
||||
export const photoZoomTransform = derived(
|
||||
photoZoomState,
|
||||
($state) => `translate(${$state.currentPositionX}px,${$state.currentPositionY}px) scale(${$state.currentZoom})`,
|
||||
);
|
||||
|
||||
export const resetZoomState = () => {
|
||||
photoZoomState.set({
|
||||
currentRotation: 0,
|
||||
currentZoom: 1,
|
||||
enable: true,
|
||||
currentPositionX: 0,
|
||||
currentPositionY: 0,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import {
|
||||
AlbumFilter,
|
||||
AlbumGroupBy,
|
||||
@@ -39,7 +39,7 @@ export const createAlbum = async (name?: string, assetIds?: string[]) => {
|
||||
export const createAlbumAndRedirect = async (name?: string, assetIds?: string[]) => {
|
||||
const newAlbum = await createAlbum(name, assetIds);
|
||||
if (newAlbum) {
|
||||
await goto(`${AppRoute.ALBUMS}/${newAlbum.id}`);
|
||||
await goto(Route.viewAlbum(newAlbum));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import ToastAction from '$lib/components/ToastAction.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
@@ -73,7 +73,7 @@ export const addAssetsToAlbum = async (albumId: string, assetIds: string[], show
|
||||
text: $t('view_album'),
|
||||
color: 'primary',
|
||||
onClick() {
|
||||
return goto(`${AppRoute.ALBUMS}/${albumId}`);
|
||||
return goto(Route.viewAlbum({ id: albumId }));
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
import { preferences as preferences$, user as user$ } from '$lib/stores/user.store';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
@@ -7,7 +8,6 @@ import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/s
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { DateTime } from 'luxon';
|
||||
import { get } from 'svelte/store';
|
||||
import { AppRoute } from '../constants';
|
||||
|
||||
export interface AuthOptions {
|
||||
admin?: true;
|
||||
@@ -62,11 +62,11 @@ export const authenticate = async (url: URL, options?: AuthOptions) => {
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
redirect(307, `${AppRoute.AUTH_LOGIN}?continue=${encodeURIComponent(url.pathname + url.search)}`);
|
||||
redirect(307, Route.login({ continue: url.pathname + url.search }));
|
||||
}
|
||||
|
||||
if (adminRoute && !user.isAdmin) {
|
||||
redirect(307, AppRoute.PHOTOS);
|
||||
redirect(307, Route.photos());
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -43,19 +43,23 @@ export const addDummyItems = () => {
|
||||
|
||||
export const uploadExecutionQueue = new ExecutorQueue({ concurrency: 2 });
|
||||
|
||||
type FilePickerParam = { multiple?: boolean; extensions?: string[] };
|
||||
type FileUploadParam = { multiple?: boolean; albumId?: string };
|
||||
|
||||
export const openFileUploadDialog = async (options: FileUploadParam = {}) => {
|
||||
const { albumId, multiple = true } = options;
|
||||
const extensions = uploadManager.getExtensions();
|
||||
export const openFilePicker = async (options: FilePickerParam = {}) => {
|
||||
const { multiple = true, extensions } = options;
|
||||
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
return new Promise<File[]>((resolve, reject) => {
|
||||
try {
|
||||
const fileSelector = document.createElement('input');
|
||||
|
||||
fileSelector.type = 'file';
|
||||
fileSelector.multiple = multiple;
|
||||
fileSelector.accept = extensions.join(',');
|
||||
|
||||
if (extensions) {
|
||||
fileSelector.accept = extensions.join(',');
|
||||
}
|
||||
|
||||
fileSelector.addEventListener(
|
||||
'change',
|
||||
(e: Event) => {
|
||||
@@ -63,9 +67,9 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => {
|
||||
if (!target.files) {
|
||||
return;
|
||||
}
|
||||
const files = Array.from(target.files);
|
||||
|
||||
resolve(fileUploadHandler({ files, albumId }));
|
||||
const files = Array.from(target.files);
|
||||
resolve(files);
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
@@ -78,6 +82,17 @@ export const openFileUploadDialog = async (options: FileUploadParam = {}) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const openFileUploadDialog = async (options: FileUploadParam = {}) => {
|
||||
const { albumId, multiple = true } = options;
|
||||
const extensions = uploadManager.getExtensions();
|
||||
const files = await openFilePicker({
|
||||
multiple,
|
||||
extensions,
|
||||
});
|
||||
|
||||
return fileUploadHandler({ files, albumId });
|
||||
};
|
||||
|
||||
type FileUploadHandlerParams = Omit<FileUploaderParams, 'deviceAssetId' | 'assetFile'> & {
|
||||
files: File[];
|
||||
};
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { maintenanceAuth as maintenanceAuth$ } from '$lib/stores/maintenance.store';
|
||||
import { maintenanceLogin } from '@immich/sdk';
|
||||
import { Route } from '$lib/route';
|
||||
import { maintenanceStore } from '$lib/stores/maintenance.store';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import { getMaintenanceStatus, MaintenanceAction, maintenanceLogin } from '@immich/sdk';
|
||||
|
||||
export function maintenanceCreateUrl(url: URL) {
|
||||
const target = new URL(AppRoute.MAINTENANCE, url.origin);
|
||||
target.searchParams.set('continue', url.pathname + url.search);
|
||||
return target.href;
|
||||
return new URL(Route.maintenanceMode({ continue: url.pathname + url.search }), url.origin).href;
|
||||
}
|
||||
|
||||
export function maintenanceReturnUrl(searchParams: URLSearchParams) {
|
||||
@@ -13,7 +12,7 @@ export function maintenanceReturnUrl(searchParams: URLSearchParams) {
|
||||
}
|
||||
|
||||
export function maintenanceShouldRedirect(maintenanceMode: boolean, currentUrl: URL | Location) {
|
||||
return maintenanceMode !== currentUrl.pathname.startsWith(AppRoute.MAINTENANCE);
|
||||
return maintenanceMode !== currentUrl.pathname.startsWith(Route.maintenanceMode());
|
||||
}
|
||||
|
||||
export const loadMaintenanceAuth = async () => {
|
||||
@@ -26,8 +25,33 @@ export const loadMaintenanceAuth = async () => {
|
||||
},
|
||||
});
|
||||
|
||||
maintenanceAuth$.set(auth);
|
||||
maintenanceStore.auth.set(auth);
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
};
|
||||
|
||||
export const loadMaintenanceStatus = async () => {
|
||||
while (true) {
|
||||
try {
|
||||
const status = await getMaintenanceStatus();
|
||||
maintenanceStore.status.set(status);
|
||||
|
||||
if (status.action === MaintenanceAction.End) {
|
||||
websocketStore.serverRestarting.set({
|
||||
isMaintenanceMode: false,
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
} catch (error) {
|
||||
const status = (error as { status: number })?.status;
|
||||
if (status && status >= 500 && status < 600) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import type { MetadataSearchDto } from '@immich/sdk';
|
||||
|
||||
export function getMetadataSearchQuery(metadata: MetadataSearchDto) {
|
||||
const searchParams = new URLSearchParams({
|
||||
[QueryParameter.QUERY]: JSON.stringify(metadata),
|
||||
});
|
||||
return searchParams.toString();
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import type { RouteId } from '$app/types';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { assetCacheManager } from '$lib/managers/AssetCacheManager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export type AssetGridRouteSearchParams = {
|
||||
@@ -33,7 +33,7 @@ function currentUrlWithoutAsset() {
|
||||
// This contains special casing for the /photos/:assetId route, which hangs directly
|
||||
// off / instead of a subpath, unlike every other asset-containing route.
|
||||
return isPhotosRoute($page.route.id)
|
||||
? AppRoute.PHOTOS + $page.url.search
|
||||
? Route.photos() + $page.url.search
|
||||
: $page.url.pathname.replace(/(\/photos.*)$/, '') + $page.url.search;
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export function currentUrlReplaceAssetId(assetId: string) {
|
||||
// this contains special casing for the /photos/:assetId photos route, which hangs directly
|
||||
// off / instead of a subpath, unlike every other asset-containing route.
|
||||
return isPhotosRoute($page.route.id)
|
||||
? `${AppRoute.PHOTOS}/${assetId}${searchparams}`
|
||||
? `${Route.viewAsset({ id: assetId })}${searchparams}`
|
||||
: `${$page.url.pathname.replace(/\/photos\/[^/]+$/, '')}/photos/${assetId}${searchparams}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import Albums from '$lib/components/album-page/albums-list.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import GroupTab from '$lib/elements/GroupTab.svelte';
|
||||
import SearchBar from '$lib/elements/SearchBar.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { AlbumFilter, albumViewSettings } from '$lib/stores/preferences.store';
|
||||
import { createAlbumAndRedirect } from '$lib/utils/album-utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -22,7 +22,7 @@
|
||||
let albumGroups: string[] = $state([]);
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title} use={[[scrollMemory, { routeStartsWith: AppRoute.ALBUMS }]]}>
|
||||
<UserPageLayout title={data.meta.title} use={[[scrollMemory, { routeStartsWith: Route.albums() }]]}>
|
||||
{#snippet buttons()}
|
||||
<div class="flex place-items-center gap-2">
|
||||
<AlbumsControls {albumGroups} bind:searchQuery />
|
||||
|
||||
+8
-7
@@ -29,7 +29,7 @@
|
||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import { AlbumPageViewMode, AppRoute } from '$lib/constants';
|
||||
import { AlbumPageViewMode } from '$lib/constants';
|
||||
import { activityManager } from '$lib/managers/activity-manager.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
@@ -37,6 +37,7 @@
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import {
|
||||
getAlbumActions,
|
||||
getAlbumAssetsActions,
|
||||
@@ -90,7 +91,7 @@
|
||||
|
||||
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
|
||||
|
||||
let backUrl: string = $state(AppRoute.ALBUMS);
|
||||
let backUrl: string = $state(Route.albums());
|
||||
let viewMode: AlbumPageViewMode = $state(AlbumPageViewMode.VIEW);
|
||||
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
@@ -108,13 +109,13 @@
|
||||
}
|
||||
|
||||
if (isAlbumsRoute(route) || isPeopleRoute(route)) {
|
||||
url = AppRoute.ALBUMS;
|
||||
url = Route.albums();
|
||||
}
|
||||
|
||||
backUrl = url || AppRoute.ALBUMS;
|
||||
backUrl = url || Route.albums();
|
||||
|
||||
if (backUrl === AppRoute.SHARED_LINKS) {
|
||||
backUrl = history.state?.backUrl || AppRoute.ALBUMS;
|
||||
if (backUrl === Route.sharedLinks()) {
|
||||
backUrl = history.state?.backUrl || Route.albums();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -347,7 +348,7 @@
|
||||
/>
|
||||
<CommandPaletteDefaultProvider name={$t('album')} actions={[AddAssets, Upload]} />
|
||||
|
||||
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
|
||||
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: Route.albums() }}>
|
||||
<div class="relative w-full shrink">
|
||||
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
|
||||
<Timeline
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte';
|
||||
import LicenseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
||||
import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||
import { Alert, Container, Stack } from '@immich/ui';
|
||||
import { mdiAlertCircleOutline } from '@mdi/js';
|
||||
@@ -32,7 +32,7 @@
|
||||
{/if}
|
||||
|
||||
{#if showLicenseActivated || data.isActivated === true}
|
||||
<LicenseActivationSuccess onDone={() => goto(AppRoute.PHOTOS, { replaceState: false })} />
|
||||
<LicenseActivationSuccess onDone={() => goto(Route.photos(), { replaceState: false })} />
|
||||
{:else}
|
||||
<LicenseContent
|
||||
onActivate={() => {
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||
import { AssetMediaSize, type SearchExploreResponseDto } from '@immich/sdk';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiHeart } from '@mdi/js';
|
||||
@@ -47,7 +46,7 @@
|
||||
<div class="flex justify-between">
|
||||
<p class="mb-4 font-medium dark:text-immich-dark-fg">{$t('people')}</p>
|
||||
<a
|
||||
href={AppRoute.PEOPLE}
|
||||
href={Route.people()}
|
||||
class="pe-4 text-sm font-medium hover:text-immich-primary dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
|
||||
draggable="false">{$t('view_all')}</a
|
||||
>
|
||||
@@ -55,7 +54,7 @@
|
||||
<SingleGridRow class="grid grid-flow-col md:grid-auto-fill-28 grid-auto-fill-20 gap-x-4">
|
||||
{#snippet children({ itemCount })}
|
||||
{#each people.slice(0, itemCount) as person (person.id)}
|
||||
<a href="{AppRoute.PEOPLE}/{person.id}" class="text-center relative">
|
||||
<a href={Route.viewPerson(person)} class="text-center relative">
|
||||
<ImageThumbnail
|
||||
circle
|
||||
shadow
|
||||
@@ -81,7 +80,7 @@
|
||||
<div class="flex justify-between">
|
||||
<p class="mb-4 font-medium dark:text-immich-dark-fg">{$t('places')}</p>
|
||||
<a
|
||||
href={AppRoute.PLACES}
|
||||
href={Route.places()}
|
||||
class="pe-4 text-sm font-medium hover:text-immich-primary dark:text-immich-dark-fg dark:hover:text-immich-dark-primary"
|
||||
draggable="false">{$t('view_all')}</a
|
||||
>
|
||||
@@ -89,11 +88,7 @@
|
||||
<SingleGridRow class="grid grid-flow-col md:grid-auto-fill-36 grid-auto-fill-28 gap-x-4">
|
||||
{#snippet children({ itemCount })}
|
||||
{#each places.slice(0, itemCount) as item (item.data.id)}
|
||||
<a
|
||||
class="relative"
|
||||
href="{AppRoute.SEARCH}?{getMetadataSearchQuery({ city: item.value })}"
|
||||
draggable="false"
|
||||
>
|
||||
<a class="relative" href={Route.search({ city: item.value })} draggable="false">
|
||||
<div class="flex justify-center overflow-hidden rounded-xl brightness-75 filter">
|
||||
<img
|
||||
src={getAssetThumbnailUrl({ id: item.data.id, size: AssetMediaSize.Thumbnail })}
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
import FavoriteAction from '$lib/components/timeline/actions/FavoriteAction.svelte';
|
||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import SkipLink from '$lib/elements/SkipLink.svelte';
|
||||
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import { Route } from '$lib/route';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { foldersStore } from '$lib/stores/folders.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
@@ -44,11 +44,7 @@
|
||||
|
||||
const handleNavigateToFolder = (folderName: string) => navigateToView(joinPaths(data.tree.path, folderName));
|
||||
|
||||
function getLinkForPath(path: string) {
|
||||
const url = new URL(AppRoute.FOLDERS, globalThis.location.href);
|
||||
url.searchParams.set(QueryParameter.PATH, path);
|
||||
return url.href;
|
||||
}
|
||||
const getLinkForPath = (path: string) => Route.folders({ path });
|
||||
|
||||
afterNavigate(function clearAssetSelection() {
|
||||
// Clear the asset selection when we navigate (like going to another folder)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import ChangeDate from '$lib/components/timeline/actions/ChangeDateAction.svelte';
|
||||
@@ -11,12 +12,13 @@
|
||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import { AppRoute, AssetAction } from '$lib/constants';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getUserActions } from '$lib/services/user.service';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetVisibility, lockAuthSession } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { mdiDotsVertical, mdiLockOutline } from '@mdi/js';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { mdiDotsVertical } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
@@ -43,19 +45,21 @@
|
||||
timelineManager.removeAssets(assetIds);
|
||||
};
|
||||
|
||||
const handleLock = async () => {
|
||||
await lockAuthSession();
|
||||
await goto(AppRoute.PHOTOS);
|
||||
const { LockSession } = $derived(getUserActions($t));
|
||||
|
||||
const onSessionLocked = async () => {
|
||||
await goto(Route.photos());
|
||||
};
|
||||
</script>
|
||||
|
||||
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
|
||||
{#snippet buttons()}
|
||||
<Button size="small" variant="ghost" color="primary" leadingIcon={mdiLockOutline} onclick={handleLock}>
|
||||
{$t('lock')}
|
||||
</Button>
|
||||
{/snippet}
|
||||
<OnEvents {onSessionLocked} />
|
||||
|
||||
<UserPageLayout
|
||||
title={data.meta.title}
|
||||
actions={[LockSession]}
|
||||
hideNavbar={assetInteraction.selectionActive}
|
||||
scrollbar={false}
|
||||
>
|
||||
<Timeline
|
||||
enableRouting={true}
|
||||
bind:timelineManager
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getAuthStatus } from '@immich/sdk';
|
||||
@@ -10,7 +10,7 @@ export const load = (async ({ url }) => {
|
||||
|
||||
const { isElevated, pinCode } = await getAuthStatus();
|
||||
if (!isElevated || !pinCode) {
|
||||
redirect(307, `${AppRoute.AUTH_PIN_PROMPT}?continue=${encodeURIComponent(url.pathname + url.search)}`);
|
||||
redirect(307, Route.pinPrompt({ continue: url.pathname + url.search }));
|
||||
}
|
||||
|
||||
const $t = await getFormatter();
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import type { AssetCursor } from '$lib/components/asset-viewer/asset-viewer.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import { AppRoute, timeToLoadTheMap } from '$lib/constants';
|
||||
import { timeToLoadTheMap } from '$lib/constants';
|
||||
import Portal from '$lib/elements/Portal.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { delay } from '$lib/utils/asset-utils';
|
||||
@@ -30,7 +31,7 @@
|
||||
});
|
||||
|
||||
if (!featureFlagsManager.value.map) {
|
||||
handlePromiseError(goto(AppRoute.PHOTOS));
|
||||
handlePromiseError(goto(Route.photos()));
|
||||
}
|
||||
|
||||
async function onViewAssets(assetIds: string[]) {
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@
|
||||
import DownloadAction from '$lib/components/timeline/actions/DownloadAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { mdiArrowLeft, mdiPlus } from '@mdi/js';
|
||||
@@ -53,7 +53,7 @@
|
||||
<DownloadAction />
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(AppRoute.SHARING)}>
|
||||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(Route.sharing())}>
|
||||
{#snippet leading()}
|
||||
<p class="whitespace-nowrap text-immich-fg dark:text-immich-dark-fg">
|
||||
{data.partner.name}'s photos
|
||||
|
||||
@@ -10,8 +10,9 @@
|
||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { ActionQueryParameterValue, AppRoute, QueryParameter, SessionStorageKey } from '$lib/constants';
|
||||
import { QueryParameter, SessionStorageKey } from '$lib/constants';
|
||||
import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
@@ -205,9 +206,7 @@
|
||||
};
|
||||
|
||||
const handleMergePeople = async (detail: PersonResponseDto) => {
|
||||
await goto(
|
||||
`${AppRoute.PEOPLE}/${detail.id}?${QueryParameter.ACTION}=${ActionQueryParameterValue.MERGE}&${QueryParameter.PREVIOUS_ROUTE}=${AppRoute.PEOPLE}`,
|
||||
);
|
||||
await goto(Route.viewPerson(detail, { previousRoute: Route.people(), action: 'merge' }));
|
||||
};
|
||||
|
||||
const onResetSearchBar = async () => {
|
||||
@@ -300,7 +299,7 @@
|
||||
[
|
||||
scrollMemory,
|
||||
{
|
||||
routeStartsWith: AppRoute.PEOPLE,
|
||||
routeStartsWith: Route.people(),
|
||||
beforeSave: () => {
|
||||
if (currentPage) {
|
||||
sessionStorage.setItem(SessionStorageKey.INFINITE_SCROLL_PAGE, currentPage.toString());
|
||||
|
||||
+36
-68
@@ -4,7 +4,6 @@
|
||||
import { clickOutside } from '$lib/actions/click-outside';
|
||||
import { listNavigation } from '$lib/actions/list-navigation';
|
||||
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
|
||||
import ActionMenuItem from '$lib/components/ActionMenuItem.svelte';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
|
||||
import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte';
|
||||
@@ -27,10 +26,11 @@
|
||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
|
||||
import { PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getPersonActions } from '$lib/services/person.service';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
@@ -41,16 +41,12 @@
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { isExternalUrl } from '$lib/utils/navigation';
|
||||
import { AssetVisibility, searchPerson, updatePerson, type PersonResponseDto } from '@immich/sdk';
|
||||
import { LoadingSpinner, modalManager, toastManager } from '@immich/ui';
|
||||
import { ContextMenuButton, LoadingSpinner, modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountBoxOutline,
|
||||
mdiAccountMultipleCheckOutline,
|
||||
mdiArrowLeft,
|
||||
mdiDotsVertical,
|
||||
mdiEyeOffOutline,
|
||||
mdiEyeOutline,
|
||||
mdiHeartMinusOutline,
|
||||
mdiHeartOutline,
|
||||
mdiPlus,
|
||||
} from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
@@ -73,7 +69,7 @@
|
||||
|
||||
let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
|
||||
let isEditingName = $state(false);
|
||||
let previousRoute: string = $state(AppRoute.EXPLORE);
|
||||
let previousRoute = $state<string>(Route.explore());
|
||||
let personMerge1: PersonResponseDto | undefined = $state();
|
||||
let personMerge2: PersonResponseDto | undefined = $state();
|
||||
let potentialMergePeople: PersonResponseDto[] = $state([]);
|
||||
@@ -143,37 +139,6 @@
|
||||
viewMode = PersonPageViewMode.UNASSIGN_ASSETS;
|
||||
};
|
||||
|
||||
const toggleHidePerson = async () => {
|
||||
try {
|
||||
await updatePerson({
|
||||
id: person.id,
|
||||
personUpdateDto: { isHidden: !person.isHidden },
|
||||
});
|
||||
|
||||
toastManager.success($t('changed_visibility_successfully'));
|
||||
|
||||
await goto(previousRoute);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_hide_person'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleFavorite = async () => {
|
||||
try {
|
||||
const updatedPerson = await updatePerson({
|
||||
id: person.id,
|
||||
personUpdateDto: { isFavorite: !person.isFavorite },
|
||||
});
|
||||
|
||||
// Invalidate to reload the page data and have the favorite status updated
|
||||
await invalidateAll();
|
||||
|
||||
toastManager.success(updatedPerson.isFavorite ? $t('added_to_favorites') : $t('removed_from_favorites'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_add_remove_favorites', { values: { favorite: person.isFavorite } }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleMerge = async (person: PersonResponseDto) => {
|
||||
await updateAssetCount();
|
||||
await handleGoBack();
|
||||
@@ -218,7 +183,7 @@
|
||||
await updateAssetCount();
|
||||
return { merged: true };
|
||||
}
|
||||
await goto(`${AppRoute.PEOPLE}/${personToBeMergedInto.id}`, { replaceState: true });
|
||||
await goto(Route.viewPerson(personToBeMergedInto), { replaceState: true });
|
||||
return { merged: true };
|
||||
};
|
||||
|
||||
@@ -324,13 +289,35 @@
|
||||
assetInteraction.clearMultiselect();
|
||||
};
|
||||
|
||||
const onPersonUpdate = (response: PersonResponseDto) => {
|
||||
if (person.id === response.id) {
|
||||
return (person = response);
|
||||
const onPersonUpdate = async (response: PersonResponseDto) => {
|
||||
if (response.id !== person.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.isHidden) {
|
||||
await goto(previousRoute);
|
||||
return;
|
||||
}
|
||||
|
||||
person = response;
|
||||
};
|
||||
|
||||
const { SetDateOfBirth } = $derived(getPersonActions($t, person));
|
||||
const { SetDateOfBirth, Favorite, Unfavorite, HidePerson, ShowPerson } = $derived(getPersonActions($t, person));
|
||||
const SelectFeaturePhoto: ActionItem = {
|
||||
title: $t('select_featured_photo'),
|
||||
icon: mdiAccountBoxOutline,
|
||||
onAction: () => {
|
||||
viewMode = PersonPageViewMode.SELECT_PERSON;
|
||||
},
|
||||
};
|
||||
|
||||
const Merge: ActionItem = {
|
||||
title: $t('merge_people'),
|
||||
icon: mdiAccountMultipleCheckOutline,
|
||||
onAction: () => {
|
||||
viewMode = PersonPageViewMode.MERGE_PEOPLE;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents {onPersonUpdate} onAssetsDelete={updateAssetCount} onAssetsArchive={updateAssetCount} />
|
||||
@@ -338,7 +325,7 @@
|
||||
<main
|
||||
class="relative z-0 h-dvh overflow-hidden px-2 md:px-6 md:pt-(--navbar-height-md) pt-(--navbar-height)"
|
||||
use:scrollMemoryClearer={{
|
||||
routeStartsWith: AppRoute.PEOPLE,
|
||||
routeStartsWith: Route.people(),
|
||||
beforeClear: () => {
|
||||
sessionStorage.removeItem(SessionStorageKey.INFINITE_SCROLL_PAGE);
|
||||
},
|
||||
@@ -506,29 +493,10 @@
|
||||
{#if viewMode === PersonPageViewMode.VIEW_ASSETS}
|
||||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} onClose={() => goto(previousRoute)}>
|
||||
{#snippet trailing()}
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<MenuOption
|
||||
text={$t('select_featured_photo')}
|
||||
icon={mdiAccountBoxOutline}
|
||||
onClick={() => (viewMode = PersonPageViewMode.SELECT_PERSON)}
|
||||
/>
|
||||
<MenuOption
|
||||
text={person.isHidden ? $t('unhide_person') : $t('hide_person')}
|
||||
icon={person.isHidden ? mdiEyeOutline : mdiEyeOffOutline}
|
||||
onClick={() => toggleHidePerson()}
|
||||
/>
|
||||
<ActionMenuItem action={SetDateOfBirth} />
|
||||
<MenuOption
|
||||
text={$t('merge_people')}
|
||||
icon={mdiAccountMultipleCheckOutline}
|
||||
onClick={() => (viewMode = PersonPageViewMode.MERGE_PEOPLE)}
|
||||
/>
|
||||
<MenuOption
|
||||
icon={person.isFavorite ? mdiHeartMinusOutline : mdiHeartOutline}
|
||||
text={person.isFavorite ? $t('unfavorite') : $t('to_favorite')}
|
||||
onClick={handleToggleFavorite}
|
||||
/>
|
||||
</ButtonContextMenu>
|
||||
<ContextMenuButton
|
||||
items={[SelectFeaturePhoto, HidePerson, ShowPerson, SetDateOfBirth, Merge, Favorite, Unfavorite]}
|
||||
aria-label={$t('open')}
|
||||
/>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
|
||||
@@ -20,8 +20,9 @@
|
||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import { AppRoute, AssetAction, QueryParameter } from '$lib/constants';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
@@ -95,7 +96,7 @@
|
||||
memoryStore.memories.map((memory) => ({
|
||||
id: memory.id,
|
||||
title: $memoryLaneTitle(memory),
|
||||
href: `${AppRoute.MEMORY}?${QueryParameter.ID}=${memory.assets[0].id}`,
|
||||
href: Route.memories({ id: memory.assets[0].id }),
|
||||
alt: $t('memory_lane_title', { values: { title: $getAltText(toTimelineAsset(memory.assets[0])) } }),
|
||||
src: getAssetThumbnailUrl(memory.assets[0].id),
|
||||
})),
|
||||
|
||||
@@ -20,9 +20,10 @@
|
||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import { QueryParameter } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import { Route } from '$lib/route';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { lang, locale } from '$lib/stores/preferences.store';
|
||||
@@ -55,7 +56,7 @@
|
||||
// The GalleryViewer pushes it's own history state, which causes weird
|
||||
// behavior for history.back(). To prevent that we store the previous page
|
||||
// manually and navigate back to that.
|
||||
let previousRoute = $state(AppRoute.EXPLORE as string);
|
||||
let previousRoute = $state<string>(Route.explore());
|
||||
|
||||
let nextPage = $state(1);
|
||||
let searchResultAlbums: AlbumResponseDto[] = $state([]);
|
||||
@@ -108,11 +109,11 @@
|
||||
const route = from?.route?.id;
|
||||
|
||||
if (isPeopleRoute(route)) {
|
||||
previousRoute = AppRoute.PHOTOS;
|
||||
previousRoute = Route.photos();
|
||||
}
|
||||
|
||||
if (isAlbumsRoute(route)) {
|
||||
previousRoute = AppRoute.EXPLORE;
|
||||
previousRoute = Route.explore();
|
||||
}
|
||||
|
||||
tick()
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/SharedLinkCard.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { type SharedLinkTab } from '$lib/constants';
|
||||
import GroupTab from '$lib/elements/GroupTab.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAllSharedLinks, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { Container } from '@immich/ui';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
@@ -29,9 +30,7 @@
|
||||
await refresh();
|
||||
});
|
||||
|
||||
type Filter = 'all' | 'album' | 'individual';
|
||||
|
||||
const filterMap: Record<Filter, string> = {
|
||||
const filterMap: Record<SharedLinkTab, string> = {
|
||||
all: $t('all'),
|
||||
album: $t('albums'),
|
||||
individual: $t('individual_shares'),
|
||||
@@ -46,9 +45,6 @@
|
||||
};
|
||||
|
||||
let selectedTab = $derived(getActiveTab(page.url));
|
||||
const handleSelectTab = async (value: string) => {
|
||||
await goto(`${AppRoute.SHARED_LINKS}?filter=${value}`);
|
||||
};
|
||||
|
||||
let filteredSharedLinks = $derived(
|
||||
sharedLinks.filter(
|
||||
@@ -76,7 +72,13 @@
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
{#snippet buttons()}
|
||||
<div class="hidden xl:block h-10">
|
||||
<GroupTab label={$t('show_shared_links')} {filters} {labels} selected={selectedTab} onSelect={handleSelectTab} />
|
||||
<GroupTab
|
||||
label={$t('show_shared_links')}
|
||||
{filters}
|
||||
{labels}
|
||||
selected={selectedTab}
|
||||
onSelect={(value) => goto(Route.sharedLinks({ filter: value as SharedLinkTab }))}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AppRoute, UUID_REGEX } from '$lib/constants';
|
||||
import { UUID_REGEX } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getAllSharedLinks } from '@immich/sdk';
|
||||
@@ -9,12 +10,12 @@ export const load = (async ({ params, url }) => {
|
||||
await authenticate(url);
|
||||
|
||||
if (!UUID_REGEX.test(params.id)) {
|
||||
redirect(307, AppRoute.SHARED_LINKS);
|
||||
redirect(307, Route.sharedLinks());
|
||||
}
|
||||
|
||||
const [sharedLink] = await getAllSharedLinks({ id: params.id });
|
||||
if (!sharedLink) {
|
||||
redirect(307, AppRoute.SHARED_LINKS);
|
||||
redirect(307, Route.sharedLinks());
|
||||
}
|
||||
|
||||
const $t = await getFormatter();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import SharedLinkExpiration from '$lib/components/SharedLinkExpiration.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleUpdateSharedLink } from '$lib/services/shared-link.service';
|
||||
import { SharedLinkType } from '@immich/sdk';
|
||||
import { Field, FormModal, Input, PasswordInput, Switch, Text } from '@immich/ui';
|
||||
@@ -27,7 +27,7 @@
|
||||
let expiresAt = $state(sharedLink.expiresAt);
|
||||
|
||||
const onClose = async () => {
|
||||
await goto(`${AppRoute.SHARED_LINKS}`);
|
||||
await goto(Route.sharedLinks());
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { getAlbumsActions } from '$lib/services/album.service';
|
||||
import { getSharedLinksActions } from '$lib/services/shared-link.service';
|
||||
import {
|
||||
AlbumFilter,
|
||||
AlbumGroupBy,
|
||||
@@ -13,15 +15,12 @@
|
||||
SortOrder,
|
||||
type AlbumViewSettings,
|
||||
} from '$lib/stores/preferences.store';
|
||||
import { createAlbumAndRedirect } from '$lib/utils/album-utils';
|
||||
import { Button, HStack, Text } from '@immich/ui';
|
||||
import { mdiLink, mdiPlusBoxOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
data: PageData;
|
||||
}
|
||||
};
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
@@ -34,26 +33,12 @@
|
||||
sortOrder: SortOrder.Desc,
|
||||
collapsedGroups: {},
|
||||
};
|
||||
|
||||
const { Create: CreateAlbum } = $derived(getAlbumsActions($t));
|
||||
const { ViewAll: ViewSharedLinks } = $derived(getSharedLinksActions($t));
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={0}>
|
||||
<Button
|
||||
leadingIcon={mdiPlusBoxOutline}
|
||||
onclick={() => createAlbumAndRedirect()}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
>
|
||||
<Text class="hidden md:block">{$t('create_album')}</Text>
|
||||
</Button>
|
||||
<Button leadingIcon={mdiLink} href={AppRoute.SHARED_LINKS} size="small" variant="ghost" color="secondary">
|
||||
<Text class="hidden md:block">{$t('shared_links')}</Text>
|
||||
</Button>
|
||||
</HStack>
|
||||
{/snippet}
|
||||
|
||||
<UserPageLayout title={data.meta.title} actions={[CreateAlbum, ViewSharedLinks]}>
|
||||
<div class="flex flex-col">
|
||||
{#if data.partners.length > 0}
|
||||
<div class="mb-6 mt-2">
|
||||
@@ -64,7 +49,7 @@
|
||||
<div class="flex flex-row flex-wrap gap-4">
|
||||
{#each data.partners as partner (partner.id)}
|
||||
<a
|
||||
href="{AppRoute.PARTNERS}/{partner.id}"
|
||||
href={Route.viewPartner(partner)}
|
||||
class="flex gap-4 rounded-lg px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
|
||||
>
|
||||
<UserAvatar user={partner} size="lg" />
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (() => {
|
||||
redirect(307, AppRoute.SHARED_LINKS);
|
||||
}) satisfies PageLoad;
|
||||
export const load = (() => redirect(307, Route.sharedLinks())) satisfies PageLoad;
|
||||
|
||||
@@ -21,9 +21,10 @@
|
||||
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
|
||||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||
import { AppRoute, AssetAction, QueryParameter } from '$lib/constants';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import SkipLink from '$lib/elements/SkipLink.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getTagActions } from '$lib/services/tag.service';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
@@ -50,11 +51,7 @@
|
||||
|
||||
const handleNavigation = (tag: string) => navigateToView(joinPaths(data.path, tag));
|
||||
|
||||
const getLink = (path: string) => {
|
||||
const url = new URL(AppRoute.TAGS, globalThis.location.href);
|
||||
url.searchParams.set(QueryParameter.PATH, path);
|
||||
return url.href;
|
||||
};
|
||||
const getLink = (path: string) => Route.tags({ path });
|
||||
|
||||
const navigateToView = (path: string) => goto(getLink(path));
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getTrashActions } from '$lib/services/trash.service';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
@@ -30,7 +30,7 @@
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
if (!featureFlagsManager.value.trash) {
|
||||
handlePromiseError(goto(AppRoute.PHOTOS));
|
||||
handlePromiseError(goto(Route.photos()));
|
||||
}
|
||||
|
||||
const handleEscape = () => {
|
||||
|
||||
+12
-21
@@ -4,10 +4,10 @@
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import DuplicatesCompareControl from '$lib/components/utilities-page/duplicates/duplicates-compare-control.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import DuplicatesInformationModal from '$lib/modals/DuplicatesInformationModal.svelte';
|
||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { stackAssets } from '$lib/utils/asset-utils';
|
||||
@@ -107,7 +107,7 @@
|
||||
duplicates = duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
|
||||
deletedNotification(trashIds.length);
|
||||
await correctDuplicatesIndexAndGo(duplicatesIndex);
|
||||
await navigateToIndex(duplicatesIndex);
|
||||
},
|
||||
trashIds.length > 0 && !featureFlagsManager.value.trash ? $t('delete_duplicates_confirmation') : undefined,
|
||||
trashIds.length > 0 && !featureFlagsManager.value.trash ? $t('permanently_delete') : undefined,
|
||||
@@ -119,7 +119,7 @@
|
||||
const duplicateAssetIds = assets.map((asset) => asset.id);
|
||||
await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } });
|
||||
duplicates = duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId);
|
||||
await correctDuplicatesIndexAndGo(duplicatesIndex);
|
||||
await navigateToIndex(duplicatesIndex);
|
||||
};
|
||||
|
||||
const handleDeduplicateAll = async () => {
|
||||
@@ -152,7 +152,7 @@
|
||||
deletedNotification(idsToDelete.length);
|
||||
|
||||
page.url.searchParams.delete('index');
|
||||
await goto(`${AppRoute.DUPLICATES}`);
|
||||
await goto(Route.duplicatesUtility());
|
||||
},
|
||||
prompt,
|
||||
confirmText,
|
||||
@@ -169,41 +169,32 @@
|
||||
|
||||
toastManager.success($t('resolved_all_duplicates'));
|
||||
page.url.searchParams.delete('index');
|
||||
await goto(`${AppRoute.DUPLICATES}`);
|
||||
await goto(Route.duplicatesUtility());
|
||||
},
|
||||
$t('bulk_keep_duplicates_confirmation', { values: { count: ids.length } }),
|
||||
$t('confirm'),
|
||||
);
|
||||
};
|
||||
|
||||
const handleFirst = async () => {
|
||||
await correctDuplicatesIndexAndGo(0);
|
||||
};
|
||||
const handlePrevious = async () => {
|
||||
await correctDuplicatesIndexAndGo(Math.max(duplicatesIndex - 1, 0));
|
||||
};
|
||||
const handleFirst = () => navigateToIndex(0);
|
||||
const handlePrevious = () => navigateToIndex(Math.max(duplicatesIndex - 1, 0));
|
||||
const handlePreviousShortcut = async () => {
|
||||
if ($showAssetViewer) {
|
||||
return;
|
||||
}
|
||||
await handlePrevious();
|
||||
};
|
||||
const handleNext = async () => {
|
||||
await correctDuplicatesIndexAndGo(Math.min(duplicatesIndex + 1, duplicates.length - 1));
|
||||
};
|
||||
const handleNext = async () => navigateToIndex(Math.min(duplicatesIndex + 1, duplicates.length - 1));
|
||||
const handleNextShortcut = async () => {
|
||||
if ($showAssetViewer) {
|
||||
return;
|
||||
}
|
||||
await handleNext();
|
||||
};
|
||||
const handleLast = async () => {
|
||||
await correctDuplicatesIndexAndGo(duplicates.length - 1);
|
||||
};
|
||||
const correctDuplicatesIndexAndGo = async (index: number) => {
|
||||
page.url.searchParams.set('index', correctDuplicatesIndex(index).toString());
|
||||
await goto(`${AppRoute.DUPLICATES}?${page.url.searchParams.toString()}`);
|
||||
};
|
||||
const handleLast = () => navigateToIndex(duplicates.length - 1);
|
||||
|
||||
const navigateToIndex = async (index: number) =>
|
||||
goto(Route.duplicatesUtility({ index: correctDuplicatesIndex(index) }));
|
||||
</script>
|
||||
|
||||
<svelte:document
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (({ params }) => {
|
||||
const photoId = params.photoId;
|
||||
return redirect(307, `${AppRoute.PHOTOS}/${photoId}`);
|
||||
}) satisfies PageLoad;
|
||||
export const load = (({ params }) => redirect(307, Route.viewAsset({ id: params.photoId }))) satisfies PageLoad;
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import emptyWorkflows from '$lib/assets/empty-workflows.svg';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import {
|
||||
getWorkflowActions,
|
||||
getWorkflowsActions,
|
||||
getWorkflowShowSchemaAction,
|
||||
handleCreateWorkflow,
|
||||
type WorkflowPayload,
|
||||
} from '$lib/services/workflow.service';
|
||||
import type { PluginFilterResponseDto, WorkflowResponseDto } from '@immich/sdk';
|
||||
import { type PluginFilterResponseDto, type WorkflowResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
@@ -18,15 +20,13 @@
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CodeBlock,
|
||||
HStack,
|
||||
Icon,
|
||||
IconButton,
|
||||
MenuItemType,
|
||||
menuManager,
|
||||
Text,
|
||||
VStack,
|
||||
} from '@immich/ui';
|
||||
import { mdiClose, mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||
import { mdiClose, mdiDotsVertical } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||
import type { PageData } from './$types';
|
||||
@@ -40,7 +40,6 @@
|
||||
let workflows = $state<WorkflowResponseDto[]>(data.workflows);
|
||||
|
||||
const expandedWorkflows = new SvelteSet<string>();
|
||||
|
||||
const pluginFilterLookup = new SvelteMap<string, PluginFilterResponseDto>();
|
||||
const pluginActionLookup = new SvelteMap<string, PluginFilterResponseDto>();
|
||||
|
||||
@@ -90,16 +89,6 @@
|
||||
|
||||
const getJson = (workflow: WorkflowResponseDto) => JSON.stringify(constructPayload(workflow), null, 2);
|
||||
|
||||
const onWorkflowUpdate = (updatedWorkflow: WorkflowResponseDto) => {
|
||||
workflows = workflows.map((currentWorkflow) =>
|
||||
currentWorkflow.id === updatedWorkflow.id ? updatedWorkflow : currentWorkflow,
|
||||
);
|
||||
};
|
||||
|
||||
const onWorkflowDelete = (deletedWorkflow: WorkflowResponseDto) => {
|
||||
workflows = workflows.filter((currentWorkflow) => currentWorkflow.id !== deletedWorkflow.id);
|
||||
};
|
||||
|
||||
const getFilterLabel = (filterId: string) => {
|
||||
const meta = pluginFilterLookup.get(filterId);
|
||||
return meta?.title ?? $t('filter');
|
||||
@@ -138,9 +127,23 @@
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
const { Create } = $derived(getWorkflowsActions($t));
|
||||
|
||||
const onWorkflowCreate = async (response: WorkflowResponseDto) => {
|
||||
await goto(Route.viewWorkflow(response));
|
||||
};
|
||||
|
||||
const onWorkflowUpdate = (response: WorkflowResponseDto) => {
|
||||
workflows = workflows.map((workflow) => (workflow.id === response.id ? response : workflow));
|
||||
};
|
||||
|
||||
const onWorkflowDelete = (response: WorkflowResponseDto) => {
|
||||
workflows = workflows.filter(({ id }) => id !== response.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents {onWorkflowUpdate} {onWorkflowDelete} />
|
||||
<OnEvents {onWorkflowCreate} {onWorkflowUpdate} {onWorkflowDelete} />
|
||||
|
||||
{#snippet chipItem(title: string)}
|
||||
<span class="rounded-xl border border-gray-200/80 px-3 py-1.5 text-sm dark:border-gray-600 bg-light">
|
||||
@@ -148,23 +151,14 @@
|
||||
</span>
|
||||
{/snippet}
|
||||
|
||||
<UserPageLayout title={data.meta.title} scrollbar={false}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={1}>
|
||||
<Button size="small" variant="ghost" color="secondary" onclick={handleCreateWorkflow}>
|
||||
<Icon icon={mdiPlus} size="18" />
|
||||
{$t('create_workflow')}
|
||||
</Button>
|
||||
</HStack>
|
||||
{/snippet}
|
||||
|
||||
<UserPageLayout title={data.meta.title} actions={[Create]} scrollbar={false}>
|
||||
<section class="flex place-content-center sm:mx-4">
|
||||
<section class="w-full pb-28 sm:w-5/6 md:w-4xl">
|
||||
{#if workflows.length === 0}
|
||||
<EmptyPlaceholder
|
||||
title={$t('create_first_workflow')}
|
||||
text={$t('workflows_help_text')}
|
||||
onClick={handleCreateWorkflow}
|
||||
onClick={() => Create.onAction(Create)}
|
||||
src={emptyWorkflows}
|
||||
class="mt-10 mx-auto"
|
||||
/>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
import WorkflowJsonEditor from '$lib/components/workflows/WorkflowJsonEditor.svelte';
|
||||
import WorkflowSummarySidebar from '$lib/components/workflows/WorkflowSummary.svelte';
|
||||
import WorkflowTriggerCard from '$lib/components/workflows/WorkflowTriggerCard.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import AddWorkflowStepModal from '$lib/modals/AddWorkflowStepModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import {
|
||||
buildWorkflowPayload,
|
||||
getActionsByContext,
|
||||
@@ -580,7 +580,7 @@
|
||||
<WorkflowSummarySidebar trigger={selectedTrigger} filters={selectedFilters} actions={selectedActions} />
|
||||
</main>
|
||||
|
||||
<ControlAppBar onClose={() => goto(AppRoute.WORKFLOWS)} backIcon={mdiArrowLeft} tailwindClasses="fixed! top-0! w-full">
|
||||
<ControlAppBar onClose={() => goto(Route.workflows())} backIcon={mdiArrowLeft} tailwindClasses="fixed! top-0! w-full">
|
||||
{#snippet leading()}
|
||||
<Text>{data.meta.title}</Text>
|
||||
{/snippet}
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
|
||||
import NavigationLoadingBar from '$lib/components/shared-components/navigation-loading-bar.svelte';
|
||||
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
|
||||
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 { Route } from '$lib/route';
|
||||
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { closeWebsocketConnection, openWebsocketConnection, websocketStore } from '$lib/stores/websocket';
|
||||
@@ -87,16 +87,16 @@
|
||||
showNavigationLoadingBar = false;
|
||||
});
|
||||
|
||||
const { serverRestarting } = websocketStore;
|
||||
|
||||
$effect.pre(() => {
|
||||
if ($user || page.url.pathname.startsWith(AppRoute.MAINTENANCE)) {
|
||||
if ($user || $serverRestarting || page.url.pathname.startsWith(Route.maintenanceMode())) {
|
||||
openWebsocketConnection();
|
||||
} else {
|
||||
closeWebsocketConnection();
|
||||
}
|
||||
});
|
||||
|
||||
const { serverRestarting } = websocketStore;
|
||||
|
||||
const onReleaseEvent = async (release: ReleaseEvent) => {
|
||||
if (!release.isAvailable || !$user.isAdmin) {
|
||||
return;
|
||||
@@ -155,32 +155,32 @@
|
||||
title: $t('users'),
|
||||
description: $t('admin.users_page_description'),
|
||||
icon: mdiAccountMultipleOutline,
|
||||
onAction: () => goto(AppRoute.ADMIN_USERS),
|
||||
onAction: () => goto(Route.users()),
|
||||
},
|
||||
{
|
||||
title: $t('settings'),
|
||||
description: $t('admin.settings_page_description'),
|
||||
icon: mdiCog,
|
||||
onAction: () => goto(AppRoute.ADMIN_SETTINGS),
|
||||
onAction: () => goto(Route.systemSettings()),
|
||||
},
|
||||
{
|
||||
title: $t('admin.queues'),
|
||||
description: $t('admin.queues_page_description'),
|
||||
icon: mdiSync,
|
||||
type: $t('page'),
|
||||
onAction: () => goto(AppRoute.ADMIN_QUEUES),
|
||||
onAction: () => goto(Route.queues()),
|
||||
},
|
||||
{
|
||||
title: $t('external_libraries'),
|
||||
description: $t('admin.external_libraries_page_description'),
|
||||
icon: mdiBookshelf,
|
||||
onAction: () => goto(AppRoute.ADMIN_LIBRARIES),
|
||||
onAction: () => goto(Route.libraries()),
|
||||
},
|
||||
{
|
||||
title: $t('server_stats'),
|
||||
description: $t('admin.server_stats_page_description'),
|
||||
icon: mdiServer,
|
||||
onAction: () => goto(AppRoute.ADMIN_STATS),
|
||||
onAction: () => goto(Route.systemStatistics()),
|
||||
},
|
||||
].map((route) => ({ ...route, type: $t('page'), $if: () => $user?.isAdmin }));
|
||||
|
||||
|
||||
@@ -1,17 +1,35 @@
|
||||
<script lang="ts">
|
||||
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Button, Heading } from '@immich/ui';
|
||||
import { Route } from '$lib/route';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { startDatabaseRestoreFlow } from '@immich/sdk';
|
||||
import { Button, Heading, Stack } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
async function switchToMaintenance() {
|
||||
try {
|
||||
websocketStore.serverRestarting.set({
|
||||
isMaintenanceMode: true,
|
||||
});
|
||||
|
||||
await startDatabaseRestoreFlow();
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.maintenance_start_error'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<AuthPageLayout>
|
||||
<div class="flex flex-col place-items-center text-center gap-12">
|
||||
<Heading size="large" color="primary" tag="h1">{$t('welcome_to_immich')}</Heading>
|
||||
<div>
|
||||
<Button href={AppRoute.AUTH_REGISTER} size="medium" shape="round">
|
||||
<Stack>
|
||||
<Button href={Route.register()} size="large" shape="round">
|
||||
<span class="px-2 font-semibold">{$t('getting_started')}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<Button size="small" shape="round" variant="ghost" onclick={switchToMaintenance}>
|
||||
<span class="px-2 font-semibold">{$t('maintenance_restore_from_backup')}</span>
|
||||
</Button>
|
||||
</Stack>
|
||||
</div>
|
||||
</AuthPageLayout>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { init } from '$lib/utils/server';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
@@ -14,17 +14,17 @@ export const load = (async ({ fetch }) => {
|
||||
await init(fetch);
|
||||
|
||||
if (serverConfigManager.value.maintenanceMode) {
|
||||
redirect(307, AppRoute.MAINTENANCE);
|
||||
redirect(307, Route.maintenanceMode());
|
||||
}
|
||||
|
||||
const authenticated = await loadUser();
|
||||
if (authenticated) {
|
||||
redirect(307, AppRoute.PHOTOS);
|
||||
redirect(307, Route.photos());
|
||||
}
|
||||
|
||||
if (serverConfigManager.value.isInitialized) {
|
||||
// Redirect to login page if there exists an admin account (i.e. server is initialized)
|
||||
redirect(307, AppRoute.AUTH_LOGIN);
|
||||
redirect(307, Route.login());
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (() => {
|
||||
redirect(307, AppRoute.ADMIN_SETTINGS);
|
||||
}) satisfies PageLoad;
|
||||
export const load = (() => redirect(307, Route.systemSettings())) satisfies PageLoad;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (() => redirect(307, AppRoute.ADMIN_QUEUES)) satisfies PageLoad;
|
||||
export const load = (() => redirect(307, Route.queues())) satisfies PageLoad;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { getLibrariesActions, handleViewLibrary } from '$lib/services/library.service';
|
||||
import { Route } from '$lib/route';
|
||||
import { getLibrariesActions } from '$lib/services/library.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import { getLibrary, getLibraryStatistics, type LibraryResponseDto } from '@immich/sdk';
|
||||
@@ -36,7 +36,7 @@
|
||||
let owners = $state(data.owners);
|
||||
|
||||
const onLibraryCreate = async (library: LibraryResponseDto) => {
|
||||
await goto(`${AppRoute.ADMIN_LIBRARIES}/${library.id}`);
|
||||
await goto(Route.viewLibrary(library));
|
||||
};
|
||||
|
||||
const onLibraryUpdate = async (library: LibraryResponseDto) => {
|
||||
@@ -96,7 +96,7 @@
|
||||
<TableCell class={classes.column4}>{videos.toLocaleString($locale)}</TableCell>
|
||||
<TableCell class={classes.column5}>{diskUsage} {diskUsageUnit}</TableCell>
|
||||
<TableCell class={classes.column6}>
|
||||
<Button size="small" onclick={() => handleViewLibrary(library)}>{$t('view')}</Button>
|
||||
<Button size="small" href={Route.viewLibrary(library)}>{$t('view')}</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
@@ -106,7 +106,7 @@
|
||||
<EmptyPlaceholder
|
||||
fullWidth
|
||||
text={$t('no_libraries_message')}
|
||||
onClick={() => goto(AppRoute.ADMIN_LIBRARIES_NEW)}
|
||||
onClick={() => goto(Route.newLibrary())}
|
||||
class="mt-10 mx-auto"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { Route } from '$lib/route';
|
||||
import { handleCreateLibrary } from '$lib/services/library.service';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { Field, FormModal, HelperText, Select } from '@immich/ui';
|
||||
@@ -18,13 +18,13 @@
|
||||
const users = $state(data.allUsers);
|
||||
|
||||
const onClose = async () => {
|
||||
await goto(AppRoute.ADMIN_LIBRARIES);
|
||||
await goto(Route.libraries());
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const library = await handleCreateLibrary({ ownerId });
|
||||
if (library) {
|
||||
await goto(`${AppRoute.ADMIN_LIBRARIES}/${library.id}`, { replaceState: true });
|
||||
await goto(Route.viewLibrary(library), { replaceState: true });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user