Compare commits

...

1 Commits

Author SHA1 Message Date
Jason Rasmussen 8ba1fae29d refactor(web): sidebar 2026-01-19 14:38:32 -05:00
19 changed files with 450 additions and 390 deletions
@@ -3,11 +3,9 @@
import { import {
Breadcrumbs, Breadcrumbs,
Button, Button,
Container,
ContextMenuButton, ContextMenuButton,
HStack, HStack,
MenuItemType, MenuItemType,
Scrollable,
isMenuItemType, isMenuItemType,
type BreadcrumbItem, type BreadcrumbItem,
} from '@immich/ui'; } from '@immich/ui';
@@ -55,7 +53,5 @@
<ContextMenuButton aria-label={$t('open')} items={actions} class="md:hidden" /> <ContextMenuButton aria-label={$t('open')} items={actions} class="md:hidden" />
{/if} {/if}
</div> </div>
<Scrollable class="grow"> {@render children?.()}
<Container class="p-2 pb-16" {children} />
</Scrollable>
</div> </div>
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import { Container, Scrollable, type Size } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
size?: Size;
center?: boolean;
children?: Snippet;
class?: string;
};
const { size, center, class: className, children }: Props = $props();
</script>
<Scrollable class="grow">
<Container {size} {center} {children} class="p-2 pb-16 {className ?? ''}" />
</Scrollable>
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import BreadcrumbActionPage from '$lib/components/BreadcrumbActionPage.svelte'; import BreadcrumbActionPage from '$lib/components/BreadcrumbActionPage.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.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 BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
@@ -35,12 +36,12 @@
<NavbarItem title={$t('server_stats')} href={Route.systemStatistics()} icon={mdiServer} /> <NavbarItem title={$t('server_stats')} href={Route.systemStatistics()} icon={mdiServer} />
</div> </div>
<div class="mb-2 me-4"> <div class="pe-6">
<BottomInfo /> <BottomInfo />
</div> </div>
</AppShellSidebar> </AppShellSidebar>
<BreadcrumbActionPage {breadcrumbs} {actions}> <BreadcrumbActionPage {breadcrumbs} {actions}>
{@render children?.()} <PageContent {children} />
</BreadcrumbActionPage> </BreadcrumbActionPage>
</AppShell> </AppShell>
@@ -0,0 +1,42 @@
<script lang="ts">
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 UserSidebar from '$lib/components/shared-components/side-bar/user-sidebar.svelte';
import { sidebarStore } from '$lib/stores/sidebar.svelte';
import type { HeaderButtonActionItem } from '$lib/types';
import { AppShell, AppShellHeader, AppShellSidebar, MenuItemType, type BreadcrumbItem } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
title: string;
breadcrumbs?: BreadcrumbItem[];
actions?: Array<HeaderButtonActionItem | MenuItemType>;
sidebar?: Snippet;
children?: Snippet;
};
let { title, breadcrumbs = [], actions, sidebar, children }: Props = $props();
</script>
<AppShell>
<AppShellHeader>
<NavigationBar noBorder />
</AppShellHeader>
<AppShellSidebar bind:open={sidebarStore.isOpen} border={false} class="h-full flex flex-col justify-between gap-2">
{#if sidebar}
{@render sidebar()}
{:else}
<div class="flex flex-col pt-8 pe-6 gap-1">
<UserSidebar />
</div>
<div class="pe-6">
<BottomInfo />
</div>
{/if}
</AppShellSidebar>
<BreadcrumbActionPage breadcrumbs={[{ title }, ...breadcrumbs]} {actions}>
{@render children?.()}
</BreadcrumbActionPage>
</AppShell>
@@ -1,11 +1,10 @@
<script lang="ts" module>
export const headerId = 'user-page-header';
</script>
<script lang="ts"> <script lang="ts">
import { useActions, type ActionArray } from '$lib/actions/use-actions'; import { useActions, type ActionArray } from '$lib/actions/use-actions';
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.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 UserSidebar from '$lib/components/shared-components/side-bar/user-sidebar.svelte'; import UserSidebar from '$lib/components/shared-components/side-bar/user-sidebar.svelte';
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
import { headerId } from '$lib/constants';
import type { HeaderButtonActionItem } from '$lib/types'; import type { HeaderButtonActionItem } from '$lib/types';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { Button, ContextMenuButton, HStack, isMenuItemType, type MenuItemType } from '@immich/ui'; import { Button, ContextMenuButton, HStack, isMenuItemType, type MenuItemType } from '@immich/ui';
@@ -61,7 +60,10 @@
{#if sidebar} {#if sidebar}
{@render sidebar()} {@render sidebar()}
{:else} {:else}
<Sidebar ariaLabel={$t('primary')}>
<UserSidebar /> <UserSidebar />
<BottomInfo />
</Sidebar>
{/if} {/if}
<main class="relative"> <main class="relative">
@@ -4,12 +4,8 @@
import StorageSpace from './storage-space.svelte'; import StorageSpace from './storage-space.svelte';
</script> </script>
<div class="mt-auto"> <div class="mt-auto flex flex-col gap-2 mb-4">
<StorageSpace /> <StorageSpace />
</div>
<PurchaseInfo /> <PurchaseInfo />
<div class="mb-6 mt-2">
<ServerStatus /> <ServerStatus />
</div> </div>
@@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
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 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 { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { recentAlbumsDropdown } from '$lib/stores/preferences.store'; import { recentAlbumsDropdown } from '$lib/stores/preferences.store';
@@ -36,7 +34,6 @@
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
</script> </script>
<Sidebar ariaLabel={$t('primary')}>
<NavbarItem title={$t('photos')} href={Route.photos()} icon={mdiImageMultipleOutline} activeIcon={mdiImageMultiple} /> <NavbarItem title={$t('photos')} href={Route.photos()} icon={mdiImageMultipleOutline} activeIcon={mdiImageMultiple} />
{#if featureFlagsManager.value.search} {#if featureFlagsManager.value.search}
@@ -101,6 +98,3 @@
{#if featureFlagsManager.value.trash} {#if featureFlagsManager.value.trash}
<NavbarItem title={$t('trash')} href={Route.trash()} icon={mdiTrashCanOutline} activeIcon={mdiTrashCan} /> <NavbarItem title={$t('trash')} href={Route.trash()} icon={mdiTrashCanOutline} activeIcon={mdiTrashCan} />
{/if} {/if}
<BottomInfo />
</Sidebar>
@@ -1,57 +0,0 @@
<script lang="ts">
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,
mdiContentDuplicate,
mdiCrosshairsGps,
mdiImageSizeSelectLarge,
mdiLinkEdit,
mdiStateMachine,
} from '@mdi/js';
import { t } from 'svelte-i18n';
const links = [
{ 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>
<div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white">
<p class="uppercase text-xs font-medium p-4">{$t('organize_your_library')}</p>
{#each links as link (link.href)}
<a href={link.href} class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4">
<span><Icon icon={link.icon} class="text-primary" size="24" /> </span>
{link.label}
</a>
{/each}
</div>
<br />
<div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white">
<p class="uppercase text-xs font-medium p-4">{$t('download')}</p>
<button
type="button"
onclick={() => modalManager.show(ObtainiumConfigModal, {})}
class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4"
>
<span>
<Icon icon={mdiLinkEdit} class="text-immich-primary dark:text-immich-dark-primary" size="24" />
</span>
{$t('obtainium_configurator')}
</button>
<button
type="button"
onclick={() => modalManager.show(AppDownloadModal, {})}
class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4"
>
<span>
<Icon icon={mdiCellphoneArrowDownVariant} class="text-immich-primary dark:text-immich-dark-primary" size="24" />
</span>
{$t('app_download_links')}
</button>
</div>
+2
View File
@@ -397,3 +397,5 @@ export enum ToggleVisibility {
} }
export const assetViewerFadeDuration: number = 150; export const assetViewerFadeDuration: number = 150;
export const headerId = 'user-page-header';
+9 -8
View File
@@ -1,28 +1,29 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte'; import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte';
import LicenseContent from '$lib/components/shared-components/purchasing/purchase-content.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 SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { purchaseStore } from '$lib/stores/purchase.store'; import { purchaseStore } from '$lib/stores/purchase.store';
import { Alert, Container, Stack } from '@immich/ui'; import { Alert, Stack } from '@immich/ui';
import { mdiAlertCircleOutline } from '@mdi/js'; import { mdiAlertCircleOutline } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
interface Props { type Props = {
data: PageData; data: PageData;
} };
let { data }: Props = $props(); let { data }: Props = $props();
let showLicenseActivated = $state(false); let showLicenseActivated = $state(false);
const { isPurchased } = purchaseStore; const { isPurchased } = purchaseStore;
</script> </script>
<UserPageLayout title={$t('buy')}> <UserPageLayout title={data.meta.title}>
<Container size="medium" center> <PageContent size="medium" center class="pt-10">
<Stack gap={4} class="mt-4"> <Stack gap={4}>
{#if data.isActivated === false} {#if data.isActivated === false}
<Alert icon={mdiAlertCircleOutline} color="danger" title={$t('purchase_failed_activation')} /> <Alert icon={mdiAlertCircleOutline} color="danger" title={$t('purchase_failed_activation')} />
{/if} {/if}
@@ -41,5 +42,5 @@
/> />
{/if} {/if}
</Stack> </Stack>
</Container> </PageContent>
</UserPageLayout> </UserPageLayout>
+6 -3
View File
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte'; import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
@@ -13,9 +14,9 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
interface Props { type Props = {
data: PageData; data: PageData;
} };
let { data }: Props = $props(); let { data }: Props = $props();
@@ -41,6 +42,7 @@
</script> </script>
<UserPageLayout title={data.meta.title}> <UserPageLayout title={data.meta.title}>
<PageContent>
{#if hasPeople} {#if hasPeople}
<div class="mb-6 mt-2"> <div class="mb-6 mt-2">
<div class="flex justify-between"> <div class="flex justify-between">
@@ -111,4 +113,5 @@
{#if !hasPeople && places.length === 0} {#if !hasPeople && places.length === 0}
<EmptyPlaceholder text={$t('no_explore_results_message')} class="mt-10 mx-auto" /> <EmptyPlaceholder text={$t('no_explore_results_message')} class="mt-10 mx-auto" />
{/if} {/if}
</PageContent>
</UserPageLayout> </UserPageLayout>
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate, goto, invalidateAll } from '$app/navigation'; import { afterNavigate, goto, invalidateAll } from '$app/navigation';
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
@@ -19,6 +19,7 @@
import FavoriteAction from '$lib/components/timeline/actions/FavoriteAction.svelte'; import FavoriteAction from '$lib/components/timeline/actions/FavoriteAction.svelte';
import TagAction from '$lib/components/timeline/actions/TagAction.svelte'; import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
import { headerId } from '$lib/constants';
import SkipLink from '$lib/elements/SkipLink.svelte'; import SkipLink from '$lib/elements/SkipLink.svelte';
import type { Viewport } from '$lib/managers/timeline-manager/types'; import type { Viewport } from '$lib/managers/timeline-manager/types';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { AssetCursor } from '$lib/components/asset-viewer/asset-viewer.svelte'; import type { AssetCursor } from '$lib/components/asset-viewer/asset-viewer.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import { timeToLoadTheMap } from '$lib/constants'; import { timeToLoadTheMap } from '$lib/constants';
import Portal from '$lib/elements/Portal.svelte'; import Portal from '$lib/elements/Portal.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
+5 -4
View File
@@ -1,7 +1,8 @@
<script lang="ts"> <script lang="ts">
import empty2Url from '$lib/assets/empty-2.svg'; import empty2Url from '$lib/assets/empty-2.svg';
import Albums from '$lib/components/album-page/albums-list.svelte'; import Albums from '$lib/components/album-page/albums-list.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
@@ -38,8 +39,8 @@
const { ViewAll: ViewSharedLinks } = $derived(getSharedLinksActions($t)); const { ViewAll: ViewSharedLinks } = $derived(getSharedLinksActions($t));
</script> </script>
<UserPageLayout title={data.meta.title} actions={[ViewSharedLinks, CreateAlbum]}> <UserPageLayout title={data.meta.title} actions={[CreateAlbum, ViewSharedLinks]}>
<div class="flex flex-col"> <PageContent>
{#if data.partners.length > 0} {#if data.partners.length > 0}
<div class="mb-6 mt-2"> <div class="mb-6 mt-2">
<div> <div>
@@ -84,5 +85,5 @@
</Albums> </Albums>
</div> </div>
</div> </div>
</div> </PageContent>
</UserPageLayout> </UserPageLayout>
@@ -1,12 +1,11 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import OnEvents from '$lib/components/OnEvents.svelte'; import OnEvents from '$lib/components/OnEvents.svelte';
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte'; import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
import Timeline from '$lib/components/timeline/Timeline.svelte'; import Timeline from '$lib/components/timeline/Timeline.svelte';
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte'; import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
@@ -21,7 +20,7 @@
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte'; import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte'; import SetVisibilityAction from '$lib/components/timeline/actions/SetVisibilityAction.svelte';
import TagAction from '$lib/components/timeline/actions/TagAction.svelte'; import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
import { AssetAction } from '$lib/constants'; import { AssetAction, headerId } from '$lib/constants';
import SkipLink from '$lib/elements/SkipLink.svelte'; import SkipLink from '$lib/elements/SkipLink.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
@@ -30,13 +29,14 @@
import { preferences, user } from '$lib/stores/user.store'; import { preferences, user } from '$lib/stores/user.store';
import { joinPaths, TreeNode } from '$lib/utils/tree-utils'; import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
import { getAllTags, type TagResponseDto } from '@immich/sdk'; import { getAllTags, type TagResponseDto } from '@immich/sdk';
import { NavbarGroup } from '@immich/ui';
import { mdiDotsVertical, mdiPlus, mdiTag, mdiTagMultiple } from '@mdi/js'; import { mdiDotsVertical, mdiPlus, mdiTag, mdiTagMultiple } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
interface Props { type Props = {
data: PageData; data: PageData;
} };
let { data }: Props = $props(); let { data }: Props = $props();
@@ -79,20 +79,17 @@
<UserPageLayout title={data.meta.title} actions={[Create, Update, Delete]}> <UserPageLayout title={data.meta.title} actions={[Create, Update, Delete]}>
{#snippet sidebar()} {#snippet sidebar()}
<Sidebar>
<SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} breakpoint="md" /> <SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} breakpoint="md" />
<section> <section class="me-6">
<div class="uppercase text-xs ps-4 mb-2 dark:text-white">{$t('explorer')}</div> <NavbarGroup title={$t('explorer')} />
<div class="h-full"> <div class="h-full">
<TreeItems icons={{ default: mdiTag, active: mdiTag }} {tree} active={tag.path} {getLink} /> <TreeItems icons={{ default: mdiTag, active: mdiTag }} {tree} active={tag.path} {getLink} />
</div> </div>
</section> </section>
</Sidebar>
{/snippet} {/snippet}
<Breadcrumbs node={tag} icon={mdiTagMultiple} title={$t('tags')} {getLink} /> <Breadcrumbs node={tag} icon={mdiTagMultiple} title={$t('tags')} {getLink} />
<div class="p-2 h-full w-full">
<section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar">
{#if tag.hasAssets} {#if tag.hasAssets}
<Timeline <Timeline
enableRouting={true} enableRouting={true}
@@ -108,7 +105,7 @@
{:else} {:else}
<TreeItemThumbnails items={tag.children} icon={mdiTag} onClick={handleNavigation} /> <TreeItemThumbnails items={tag.children} icon={mdiTag} onClick={handleNavigation} />
{/if} {/if}
</section> </div>
</UserPageLayout> </UserPageLayout>
<section> <section>
@@ -1,31 +1,39 @@
<script lang="ts"> <script lang="ts">
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte'; import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte';
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
import { Container, IconButton, modalManager } from '@immich/ui'; import { CommandPaletteDefaultProvider, modalManager, type ActionItem } from '@immich/ui';
import { mdiKeyboard } from '@mdi/js'; import { mdiKeyboard } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
interface Props { type Props = {
data: PageData; data: PageData;
} };
let { data }: Props = $props(); let { data }: Props = $props();
let open = $state(false);
const Shortcuts = $derived<ActionItem>({
title: $t('show_keyboard_shortcuts'),
icon: mdiKeyboard,
onAction: async () => {
if (!open) {
open = true;
await modalManager.show(ShortcutsModal, {});
open = false;
}
},
shortcuts: [{ key: '?', shift: true }],
});
</script> </script>
<UserPageLayout title={data.meta.title}> <CommandPaletteDefaultProvider name={data.meta.title} actions={[Shortcuts]} />
{#snippet buttons()}
<IconButton <UserPageLayout title={data.meta.title} actions={[Shortcuts]}>
shape="round" <PageContent size="medium" center>
color="secondary"
variant="ghost"
icon={mdiKeyboard}
aria-label={$t('show_keyboard_shortcuts')}
onclick={() => modalManager.show(ShortcutsModal, {})}
/>
{/snippet}
<Container size="medium" center>
<UserSettingsList keys={data.keys} sessions={data.sessions} /> <UserSettingsList keys={data.keys} sessions={data.sessions} />
</Container> </PageContent>
</UserPageLayout> </UserPageLayout>
+60 -5
View File
@@ -1,7 +1,27 @@
<script lang="ts"> <script lang="ts">
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import PageContent from '$lib/components/PageContent.svelte';
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,
mdiContentDuplicate,
mdiCrosshairsGps,
mdiImageSizeSelectLarge,
mdiLinkEdit,
mdiStateMachine,
} from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
import UtilitiesMenu from '$lib/components/utilities-page/utilities-menu.svelte';
const links = [
{ 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') },
];
interface Props { interface Props {
data: PageData; data: PageData;
@@ -11,9 +31,44 @@
</script> </script>
<UserPageLayout title={data.meta.title}> <UserPageLayout title={data.meta.title}>
<div class="w-full max-w-xl m-auto"> <PageContent center size="small" class="pt-10">
<div class="mt-5"> <div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white">
<UtilitiesMenu /> <p class="uppercase text-xs font-medium p-4">{$t('organize_your_library')}</p>
{#each links as link (link.href)}
<a href={link.href} class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4">
<span><Icon icon={link.icon} class="text-primary" size="24" /> </span>
{link.label}
</a>
{/each}
</div> </div>
<br />
<div class="border border-gray-300 dark:border-immich-dark-gray rounded-3xl pt-1 pb-6 dark:text-white">
<p class="uppercase text-xs font-medium p-4">{$t('download')}</p>
<button
type="button"
onclick={() => modalManager.show(ObtainiumConfigModal, {})}
class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4"
>
<span>
<Icon icon={mdiLinkEdit} class="text-immich-primary dark:text-immich-dark-primary" size="24" />
</span>
{$t('obtainium_configurator')}
</button>
<button
type="button"
onclick={() => modalManager.show(AppDownloadModal, {})}
class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4"
>
<span>
<Icon
icon={mdiCellphoneArrowDownVariant}
class="text-immich-primary dark:text-immich-dark-primary"
size="24"
/>
</span>
{$t('app_download_links')}
</button>
</div> </div>
</PageContent>
</UserPageLayout> </UserPageLayout>
@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import type { Action } from '$lib/components/asset-viewer/actions/action'; import type { Action } from '$lib/components/asset-viewer/actions/action';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import LargeAssetData from '$lib/components/utilities-page/large-assets/large-asset-data.svelte'; import LargeAssetData from '$lib/components/utilities-page/large-assets/large-asset-data.svelte';
import Portal from '$lib/elements/Portal.svelte'; import Portal from '$lib/elements/Portal.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
@@ -54,7 +55,8 @@
}); });
</script> </script>
<UserPageLayout title={data.meta.title} scrollbar={true}> <UserPageLayout title={data.meta.title}>
<PageContent>
<div class="grid gap-2 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6"> <div class="grid gap-2 grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6">
{#if assets && data.assets.length > 0} {#if assets && data.assets.length > 0}
{#each assets as asset (asset.id)} {#each assets as asset (asset.id)}
@@ -66,6 +68,7 @@
</p> </p>
{/if} {/if}
</div> </div>
</PageContent>
</UserPageLayout> </UserPageLayout>
{#if $showAssetViewer} {#if $showAssetViewer}
@@ -1,8 +1,9 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import emptyWorkflows from '$lib/assets/empty-workflows.svg'; import emptyWorkflows from '$lib/assets/empty-workflows.svg';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import OnEvents from '$lib/components/OnEvents.svelte'; import OnEvents from '$lib/components/OnEvents.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { Route } from '$lib/route'; import { Route } from '$lib/route';
import { import {
@@ -151,9 +152,8 @@
</span> </span>
{/snippet} {/snippet}
<UserPageLayout title={data.meta.title} actions={[Create]} scrollbar={false}> <UserPageLayout title={data.meta.title} actions={[Create]}>
<section class="flex place-content-center sm:mx-4"> <PageContent center size="large" class="pt-10">
<section class="w-full pb-28 sm:w-5/6 md:w-4xl">
{#if workflows.length === 0} {#if workflows.length === 0}
<EmptyPlaceholder <EmptyPlaceholder
title={$t('create_first_workflow')} title={$t('create_first_workflow')}
@@ -163,7 +163,7 @@
class="mt-10 mx-auto" class="mt-10 mx-auto"
/> />
{:else} {:else}
<div class="my-6 grid gap-6"> <div class="grid gap-6">
{#each workflows as workflow (workflow.id)} {#each workflows as workflow (workflow.id)}
<Card class="border border-light-200"> <Card class="border border-light-200">
<CardHeader <CardHeader
@@ -175,8 +175,7 @@
> >
<div class="flex-1"> <div class="flex-1">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<span <span class="rounded-full {workflow.enabled ? 'h-3 w-3 bg-success' : 'h-3 w-3 rounded-full bg-muted'}"
class="rounded-full {workflow.enabled ? 'h-3 w-3 bg-success' : 'h-3 w-3 rounded-full bg-muted'}"
></span> ></span>
<CardTitle>{workflow.name}</CardTitle> <CardTitle>{workflow.name}</CardTitle>
</div> </div>
@@ -276,6 +275,5 @@
{/each} {/each}
</div> </div>
{/if} {/if}
</section> </PageContent>
</section>
</UserPageLayout> </UserPageLayout>