refactor(web): routes (#25313)

This commit is contained in:
Jason Rasmussen
2026-01-16 16:11:09 -05:00
committed by GitHub
parent 07675a2de4
commit 8196bd9bbd
87 changed files with 425 additions and 383 deletions
+2 -4
View File
@@ -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).
*/
+4 -3
View File
@@ -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,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';
@@ -20,8 +19,8 @@
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 { 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';
@@ -250,7 +249,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}
@@ -258,8 +257,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,13 @@
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 { 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';
@@ -395,7 +396,7 @@
}
await new Promise((promise) => setTimeout(promise, 500));
await goto(`${AppRoute.PHOTOS}/${newAssetId}`);
await goto(Route.viewAsset({ id: newAssetId }));
};
const onAssetUpdate = (update: AssetResponseDto) => {
@@ -10,6 +10,7 @@
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 +18,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';
@@ -207,7 +207,7 @@
class="w-22"
href={resolve(
`${AppRoute.PEOPLE}/${person.id}?${QueryParameter.PREVIOUS_ROUTE}=${
currentAlbum?.id ? `${AppRoute.ALBUMS}/${currentAlbum?.id}` : AppRoute.PHOTOS
currentAlbum?.id ? Route.viewAlbum(currentAlbum) : Route.photos()
}`,
)}
onfocus={() => ($boundingBoxesArray = people[index].faces)}
@@ -385,12 +385,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 +419,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"
>
@@ -515,7 +513,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
@@ -2,7 +2,7 @@
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';
@@ -28,11 +28,11 @@
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('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('server_stats')} href={Route.systemStatistics()} icon={mdiServer} />
</div>
<div class="mb-2 me-4">
@@ -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);
@@ -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}
@@ -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"
>
@@ -4,6 +4,7 @@
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
import { AppRoute } from '$lib/constants';
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';
@@ -37,15 +38,10 @@
</script>
<Sidebar ariaLabel={$t('primary')}>
<NavbarItem
title={$t('photos')}
href={AppRoute.PHOTOS}
icon={mdiImageMultipleOutline}
activeIcon={mdiImageMultiple}
/>
<NavbarItem title={$t('photos')} href={Route.photos()} icon={mdiImageMultipleOutline} activeIcon={mdiImageMultiple} />
{#if featureFlagsManager.value.search}
<NavbarItem title={$t('explore')} href={AppRoute.EXPLORE} icon={mdiMagnify} />
<NavbarItem title={$t('explore')} href={Route.explore()} icon={mdiMagnify} />
{/if}
{#if featureFlagsManager.value.map}
@@ -57,23 +53,23 @@
{/if}
{#if $preferences.sharedLinks.enabled && $preferences.sharedLinks.sidebarWeb}
<NavbarItem title={$t('shared_links')} href={AppRoute.SHARED_LINKS} icon={mdiLink} />
<NavbarItem title={$t('shared_links')} href={Route.sharedLinks()} icon={mdiLink} />
{/if}
<NavbarItem
title={$t('sharing')}
href={AppRoute.SHARING}
href={Route.sharing()}
icon={mdiAccountMultipleOutline}
activeIcon={mdiAccountMultiple}
/>
<NavbarGroup title={$t('library')} size="tiny" />
<NavbarItem title={$t('favorites')} href={AppRoute.FAVORITES} icon={mdiHeartOutline} activeIcon={mdiHeart} />
<NavbarItem title={$t('favorites')} href={Route.favorites()} icon={mdiHeartOutline} activeIcon={mdiHeart} />
<NavbarItem
title={$t('albums')}
href={AppRoute.ALBUMS}
href={Route.albums()}
icon={{ icon: mdiImageAlbum, flipped: true }}
bind:expanded={$recentAlbumsDropdown}
>
@@ -92,19 +88,19 @@
<NavbarItem title={$t('folders')} href={AppRoute.FOLDERS} icon={{ icon: mdiFolderOutline, flipped: true }} />
{/if}
<NavbarItem title={$t('utilities')} href={AppRoute.UTILITIES} icon={mdiToolboxOutline} activeIcon={mdiToolbox} />
<NavbarItem title={$t('utilities')} href={Route.utilities()} icon={mdiToolboxOutline} activeIcon={mdiToolbox} />
<NavbarItem
title={$t('archive')}
href={AppRoute.ARCHIVE}
href={Route.archive()}
icon={mdiArchiveArrowDownOutline}
activeIcon={mdiArchiveArrowDown}
/>
<NavbarItem title={$t('locked_folder')} href={AppRoute.LOCKED} icon={mdiLockOutline} activeIcon={mdiLock} />
<NavbarItem title={$t('locked_folder')} href={Route.locked()} icon={mdiLockOutline} activeIcon={mdiLock} />
{#if featureFlagsManager.value.trash}
<NavbarItem title={$t('trash')} href={AppRoute.TRASH} icon={mdiTrashCanOutline} activeIcon={mdiTrashCan} />
<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} />
@@ -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>
+6 -39
View File
@@ -20,52 +20,17 @@ export enum AssetAction {
}
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_STATS = '/admin/server-status',
ADMIN_QUEUES = '/admin/queues',
ADMIN_REPAIR = '/admin/repair',
ALBUMS = '/albums',
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',
CUBEMAP = 'CUBEMAP',
@@ -94,7 +59,6 @@ export enum QueryParameter {
ACTION = 'action',
ID = 'id',
IS_OPEN = 'isOpen',
ONBOARDING_STEP = 'step',
OPEN_SETTING = 'openSetting',
PREVIOUS_ROUTE = 'previousRoute',
QUERY = 'query',
@@ -109,10 +73,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',
NOTIFICATIONS = 'notifications',
PURCHASE_SETTINGS = 'user-purchase-settings',
}
export enum ActionQueryParameterValue {
+2 -2
View File
@@ -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('/')) {
+36
View File
@@ -0,0 +1,36 @@
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.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');
});
});
});
+105
View File
@@ -0,0 +1,105 @@
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>) => {
const items = Object.entries(params ?? {})
.filter((item): item is [string, QueryValue] => item[1] !== undefined)
.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}`,
// explore
explore: () => '/explore',
places: () => '/places',
// 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`,
// memories
memories: (params?: { id?: string }) => '/memory' + asQueryString(params),
// partners
viewPartner: ({ id }: { id: string }) => `/partners/${id}`,
// 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',
// 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)}`,
};
+2 -4
View File
@@ -1,11 +1,11 @@
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 { downloadArchive } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
@@ -161,9 +161,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 })),
},
},
});
+3 -7
View File
@@ -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();
+3 -21
View File
@@ -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 };
@@ -250,22 +251,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));
};
+2 -2
View File
@@ -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';
@@ -25,7 +25,7 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin
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 = {
+3 -3
View File
@@ -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 = {
+3 -7
View File
@@ -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 {
@@ -331,7 +331,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 = {
@@ -370,7 +370,7 @@ export const handleCreateWorkflow = async (): Promise<WorkflowResponseDto | unde
},
});
await goto(`${AppRoute.WORKFLOWS}/${workflow.id}`);
await goto(Route.viewWorkflow(workflow));
return workflow;
} catch (error) {
handleError(error, $t('errors.unable_to_create'));
@@ -419,10 +419,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,
+2 -2
View File
@@ -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));
}
};
+2 -2
View File
@@ -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 }));
},
},
},
+3 -3
View File
@@ -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());
}
};
-9
View File
@@ -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();
}
+3 -3
View File
@@ -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}`;
}