Compare commits

..

1 Commits

Author SHA1 Message Date
bwees
049aeabcc1 fix: disable editing
oops
2026-01-19 12:27:31 -06:00
22 changed files with 412 additions and 472 deletions

View File

@@ -3,9 +3,11 @@
import {
Breadcrumbs,
Button,
Container,
ContextMenuButton,
HStack,
MenuItemType,
Scrollable,
isMenuItemType,
type BreadcrumbItem,
} from '@immich/ui';
@@ -53,5 +55,7 @@
<ContextMenuButton aria-label={$t('open')} items={actions} class="md:hidden" />
{/if}
</div>
{@render children?.()}
<Scrollable class="grow">
<Container class="p-2 pb-16" {children} />
</Scrollable>
</div>

View File

@@ -1,17 +0,0 @@
<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>

View File

@@ -7,7 +7,6 @@
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,7 +19,6 @@
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 { 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';
@@ -73,7 +71,7 @@
onUndoDelete?: OnUndoDelete;
onRunJob: (name: AssetJobName) => void;
onPlaySlideshow: () => void;
onEdit: () => void;
// onEdit: () => void;
onClose?: () => void;
playOriginalVideo: boolean;
setPlayOriginalVideo: (value: boolean) => void;
@@ -93,7 +91,7 @@
onRunJob,
onPlaySlideshow,
onClose,
onEdit,
// onEdit,
playOriginalVideo = false,
setPlayOriginalVideo,
}: Props = $props();
@@ -127,15 +125,18 @@
} = $derived(getAssetActions($t, asset));
const sharedLink = getSharedLink();
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'),
);
// 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,
// );
</script>
<CommandPaletteDefaultProvider
@@ -188,9 +189,9 @@
<RatingAction {asset} {onAction} />
{/if}
{#if !editorDisabled}
<!-- {#if showEditorButton}
<EditAction onAction={onEdit} />
{/if}
{/if} -->
{#if isOwner}
<DeleteAction {asset} {onAction} {preAction} {onUndoDelete} />

View File

@@ -254,12 +254,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 {
@@ -466,7 +466,6 @@
preAction={handlePreAction}
onAction={handleAction}
{onUndoDelete}
onEdit={showEditor}
onRunJob={handleRunJob}
onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
onClose={onClose ? () => onClose(asset) : undefined}

View File

@@ -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} loading={editManager.isApplyingEdits}>{$t('save')}</Button>
<Button shape="round" size="small" onclick={applyEdits}>{$t('save')}</Button>
</HStack>
<section>

View File

@@ -1,6 +1,5 @@
<script lang="ts">
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 BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
import { Route } from '$lib/route';
@@ -36,12 +35,12 @@
<NavbarItem title={$t('server_stats')} href={Route.systemStatistics()} icon={mdiServer} />
</div>
<div class="pe-6">
<div class="mb-2 me-4">
<BottomInfo />
</div>
</AppShellSidebar>
<BreadcrumbActionPage {breadcrumbs} {actions}>
<PageContent {children} />
{@render children?.()}
</BreadcrumbActionPage>
</AppShell>

View File

@@ -1,42 +0,0 @@
<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>

View File

@@ -1,10 +1,11 @@
<script lang="ts" module>
export const headerId = 'user-page-header';
</script>
<script lang="ts">
import { useActions, type ActionArray } from '$lib/actions/use-actions';
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 Sidebar from '$lib/components/sidebar/sidebar.svelte';
import { headerId } from '$lib/constants';
import type { HeaderButtonActionItem } from '$lib/types';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { Button, ContextMenuButton, HStack, isMenuItemType, type MenuItemType } from '@immich/ui';
@@ -60,10 +61,7 @@
{#if sidebar}
{@render sidebar()}
{:else}
<Sidebar ariaLabel={$t('primary')}>
<UserSidebar />
<BottomInfo />
</Sidebar>
<UserSidebar />
{/if}
<main class="relative">

View File

@@ -4,8 +4,12 @@
import StorageSpace from './storage-space.svelte';
</script>
<div class="mt-auto flex flex-col gap-2 mb-4">
<div class="mt-auto">
<StorageSpace />
<PurchaseInfo />
</div>
<PurchaseInfo />
<div class="mb-6 mt-2">
<ServerStatus />
</div>

View File

@@ -1,5 +1,7 @@
<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 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';
@@ -34,67 +36,71 @@
import { fly } from 'svelte/transition';
</script>
<NavbarItem title={$t('photos')} href={Route.photos()} icon={mdiImageMultipleOutline} activeIcon={mdiImageMultiple} />
<Sidebar ariaLabel={$t('primary')}>
<NavbarItem title={$t('photos')} href={Route.photos()} icon={mdiImageMultipleOutline} activeIcon={mdiImageMultiple} />
{#if featureFlagsManager.value.search}
<NavbarItem title={$t('explore')} href={Route.explore()} icon={mdiMagnify} />
{/if}
{#if featureFlagsManager.value.search}
<NavbarItem title={$t('explore')} href={Route.explore()} icon={mdiMagnify} />
{/if}
{#if featureFlagsManager.value.map}
<NavbarItem title={$t('map')} href={Route.map()} icon={mdiMapOutline} activeIcon={mdiMap} />
{/if}
{#if featureFlagsManager.value.map}
<NavbarItem title={$t('map')} href={Route.map()} icon={mdiMapOutline} activeIcon={mdiMap} />
{/if}
{#if $preferences.people.enabled && $preferences.people.sidebarWeb}
<NavbarItem title={$t('people')} href={Route.people()} icon={mdiAccountOutline} activeIcon={mdiAccount} />
{/if}
{#if $preferences.people.enabled && $preferences.people.sidebarWeb}
<NavbarItem title={$t('people')} href={Route.people()} icon={mdiAccountOutline} activeIcon={mdiAccount} />
{/if}
{#if $preferences.sharedLinks.enabled && $preferences.sharedLinks.sidebarWeb}
<NavbarItem title={$t('shared_links')} href={Route.sharedLinks()} icon={mdiLink} />
{/if}
{#if $preferences.sharedLinks.enabled && $preferences.sharedLinks.sidebarWeb}
<NavbarItem title={$t('shared_links')} href={Route.sharedLinks()} icon={mdiLink} />
{/if}
<NavbarItem
title={$t('sharing')}
href={Route.sharing()}
icon={mdiAccountMultipleOutline}
activeIcon={mdiAccountMultiple}
/>
<NavbarItem
title={$t('sharing')}
href={Route.sharing()}
icon={mdiAccountMultipleOutline}
activeIcon={mdiAccountMultiple}
/>
<NavbarGroup title={$t('library')} size="tiny" />
<NavbarGroup title={$t('library')} size="tiny" />
<NavbarItem title={$t('favorites')} href={Route.favorites()} icon={mdiHeartOutline} activeIcon={mdiHeart} />
<NavbarItem title={$t('favorites')} href={Route.favorites()} icon={mdiHeartOutline} activeIcon={mdiHeart} />
<NavbarItem
title={$t('albums')}
href={Route.albums()}
icon={{ icon: mdiImageAlbum, flipped: true }}
bind:expanded={$recentAlbumsDropdown}
>
{#snippet items()}
<span in:fly={{ y: -20 }} class="hidden md:block">
<RecentAlbums />
</span>
{/snippet}
</NavbarItem>
<NavbarItem
title={$t('albums')}
href={Route.albums()}
icon={{ icon: mdiImageAlbum, flipped: true }}
bind:expanded={$recentAlbumsDropdown}
>
{#snippet items()}
<span in:fly={{ y: -20 }} class="hidden md:block">
<RecentAlbums />
</span>
{/snippet}
</NavbarItem>
{#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
<NavbarItem title={$t('tags')} href={Route.tags()} icon={{ icon: mdiTagMultipleOutline, flipped: true }} />
{/if}
{#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
<NavbarItem title={$t('tags')} href={Route.tags()} icon={{ icon: mdiTagMultipleOutline, flipped: true }} />
{/if}
{#if $preferences.folders.enabled && $preferences.folders.sidebarWeb}
<NavbarItem title={$t('folders')} href={Route.folders()} icon={{ icon: mdiFolderOutline, flipped: true }} />
{/if}
{#if $preferences.folders.enabled && $preferences.folders.sidebarWeb}
<NavbarItem title={$t('folders')} href={Route.folders()} icon={{ icon: mdiFolderOutline, flipped: true }} />
{/if}
<NavbarItem title={$t('utilities')} href={Route.utilities()} icon={mdiToolboxOutline} activeIcon={mdiToolbox} />
<NavbarItem title={$t('utilities')} href={Route.utilities()} icon={mdiToolboxOutline} activeIcon={mdiToolbox} />
<NavbarItem
title={$t('archive')}
href={Route.archive()}
icon={mdiArchiveArrowDownOutline}
activeIcon={mdiArchiveArrowDown}
/>
<NavbarItem
title={$t('archive')}
href={Route.archive()}
icon={mdiArchiveArrowDownOutline}
activeIcon={mdiArchiveArrowDown}
/>
<NavbarItem title={$t('locked_folder')} href={Route.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={Route.trash()} icon={mdiTrashCanOutline} activeIcon={mdiTrashCan} />
{/if}
{#if featureFlagsManager.value.trash}
<NavbarItem title={$t('trash')} href={Route.trash()} icon={mdiTrashCanOutline} activeIcon={mdiTrashCan} />
{/if}
<BottomInfo />
</Sidebar>

View File

@@ -0,0 +1,57 @@
<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>

View File

@@ -397,5 +397,3 @@ export enum ToggleVisibility {
}
export const assetViewerFadeDuration: number = 150;
export const headerId = 'user-page-header';

View File

@@ -1,29 +1,28 @@
<script lang="ts">
import { goto } from '$app/navigation';
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.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 SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte';
import { Route } from '$lib/route';
import { purchaseStore } from '$lib/stores/purchase.store';
import { Alert, Stack } from '@immich/ui';
import { Alert, Container, Stack } from '@immich/ui';
import { mdiAlertCircleOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
type Props = {
interface Props {
data: PageData;
};
}
let { data }: Props = $props();
let showLicenseActivated = $state(false);
const { isPurchased } = purchaseStore;
</script>
<UserPageLayout title={data.meta.title}>
<PageContent size="medium" center class="pt-10">
<Stack gap={4}>
<UserPageLayout title={$t('buy')}>
<Container size="medium" center>
<Stack gap={4} class="mt-4">
{#if data.isActivated === false}
<Alert icon={mdiAlertCircleOutline} color="danger" title={$t('purchase_failed_activation')} />
{/if}
@@ -42,5 +41,5 @@
/>
{/if}
</Stack>
</PageContent>
</Container>
</UserPageLayout>

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import PageContent from '$lib/components/PageContent.svelte';
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 { Route } from '$lib/route';
@@ -14,9 +13,9 @@
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
type Props = {
interface Props {
data: PageData;
};
}
let { data }: Props = $props();
@@ -42,76 +41,74 @@
</script>
<UserPageLayout title={data.meta.title}>
<PageContent>
{#if hasPeople}
<div class="mb-6 mt-2">
<div class="flex justify-between">
<p class="mb-4 font-medium dark:text-immich-dark-fg">{$t('people')}</p>
<a
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
>
</div>
<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={Route.viewPerson(person)} class="text-center relative">
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(person)}
altText={person.name}
widthStyle="100%"
/>
{#if person.isFavorite}
<div class="absolute top-2 start-2">
<Icon icon={mdiHeart} size="24" class="text-white" />
</div>
{/if}
<p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
</a>
{/each}
{/snippet}
</SingleGridRow>
{#if hasPeople}
<div class="mb-6 mt-2">
<div class="flex justify-between">
<p class="mb-4 font-medium dark:text-immich-dark-fg">{$t('people')}</p>
<a
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
>
</div>
{/if}
{#if places.length > 0}
<div class="mb-6 mt-2">
<div class="flex justify-between">
<p class="mb-4 font-medium dark:text-immich-dark-fg">{$t('places')}</p>
<a
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
>
</div>
<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={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 })}
alt={item.value}
class="object-cover aspect-square w-full"
/>
<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={Route.viewPerson(person)} class="text-center relative">
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(person)}
altText={person.name}
widthStyle="100%"
/>
{#if person.isFavorite}
<div class="absolute top-2 start-2">
<Icon icon={mdiHeart} size="24" class="text-white" />
</div>
<span
class="absolute bottom-2 w-full text-ellipsis px-1 text-center text-sm font-medium capitalize text-white backdrop-blur-[1px] hover:cursor-pointer"
>
{item.value}
</span>
</a>
{/each}
{/snippet}
</SingleGridRow>
</div>
{/if}
{/if}
<p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
</a>
{/each}
{/snippet}
</SingleGridRow>
</div>
{/if}
{#if !hasPeople && places.length === 0}
<EmptyPlaceholder text={$t('no_explore_results_message')} class="mt-10 mx-auto" />
{/if}
</PageContent>
{#if places.length > 0}
<div class="mb-6 mt-2">
<div class="flex justify-between">
<p class="mb-4 font-medium dark:text-immich-dark-fg">{$t('places')}</p>
<a
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
>
</div>
<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={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 })}
alt={item.value}
class="object-cover aspect-square w-full"
/>
</div>
<span
class="absolute bottom-2 w-full text-ellipsis px-1 text-center text-sm font-medium capitalize text-white backdrop-blur-[1px] hover:cursor-pointer"
>
{item.value}
</span>
</a>
{/each}
{/snippet}
</SingleGridRow>
</div>
{/if}
{#if !hasPeople && places.length === 0}
<EmptyPlaceholder text={$t('no_explore_results_message')} class="mt-10 mx-auto" />
{/if}
</UserPageLayout>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { afterNavigate, goto, invalidateAll } from '$app/navigation';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.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 Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte';
@@ -19,7 +19,6 @@
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 { headerId } from '$lib/constants';
import SkipLink from '$lib/elements/SkipLink.svelte';
import type { Viewport } from '$lib/managers/timeline-manager/types';
import { Route } from '$lib/route';

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { goto } from '$app/navigation';
import type { AssetCursor } from '$lib/components/asset-viewer/asset-viewer.svelte';
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import { timeToLoadTheMap } from '$lib/constants';
import Portal from '$lib/elements/Portal.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import empty2Url from '$lib/assets/empty-2.svg';
import Albums from '$lib/components/album-page/albums-list.svelte';
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import PageContent from '$lib/components/PageContent.svelte';
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 { Route } from '$lib/route';
@@ -39,8 +38,8 @@
const { ViewAll: ViewSharedLinks } = $derived(getSharedLinksActions($t));
</script>
<UserPageLayout title={data.meta.title} actions={[CreateAlbum, ViewSharedLinks]}>
<PageContent>
<UserPageLayout title={data.meta.title} actions={[ViewSharedLinks, CreateAlbum]}>
<div class="flex flex-col">
{#if data.partners.length > 0}
<div class="mb-6 mt-2">
<div>
@@ -85,5 +84,5 @@
</Albums>
</div>
</div>
</PageContent>
</div>
</UserPageLayout>

View File

@@ -1,11 +1,12 @@
<script lang="ts">
import { goto } from '$app/navigation';
import OnEvents from '$lib/components/OnEvents.svelte';
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.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 TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.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 Timeline from '$lib/components/timeline/Timeline.svelte';
import AddToAlbum from '$lib/components/timeline/actions/AddToAlbumAction.svelte';
@@ -20,7 +21,7 @@
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 { AssetAction, headerId } 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';
@@ -29,14 +30,13 @@
import { preferences, user } from '$lib/stores/user.store';
import { joinPaths, TreeNode } from '$lib/utils/tree-utils';
import { getAllTags, type TagResponseDto } from '@immich/sdk';
import { NavbarGroup } from '@immich/ui';
import { mdiDotsVertical, mdiPlus, mdiTag, mdiTagMultiple } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
type Props = {
interface Props {
data: PageData;
};
}
let { data }: Props = $props();
@@ -79,17 +79,20 @@
<UserPageLayout title={data.meta.title} actions={[Create, Update, Delete]}>
{#snippet sidebar()}
<SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} breakpoint="md" />
<section class="me-6">
<NavbarGroup title={$t('explorer')} />
<div class="h-full">
<TreeItems icons={{ default: mdiTag, active: mdiTag }} {tree} active={tag.path} {getLink} />
</div>
</section>
<Sidebar>
<SkipLink target={`#${headerId}`} text={$t('skip_to_tags')} breakpoint="md" />
<section>
<div class="uppercase text-xs ps-4 mb-2 dark:text-white">{$t('explorer')}</div>
<div class="h-full">
<TreeItems icons={{ default: mdiTag, active: mdiTag }} {tree} active={tag.path} {getLink} />
</div>
</section>
</Sidebar>
{/snippet}
<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}
<Timeline
enableRouting={true}
@@ -105,7 +108,7 @@
{:else}
<TreeItemThumbnails items={tag.children} icon={mdiTag} onClick={handleNavigation} />
{/if}
</div>
</section>
</UserPageLayout>
<section>

View File

@@ -1,39 +1,31 @@
<script lang="ts">
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 ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
import { CommandPaletteDefaultProvider, modalManager, type ActionItem } from '@immich/ui';
import { Container, IconButton, modalManager } from '@immich/ui';
import { mdiKeyboard } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
type Props = {
interface Props {
data: PageData;
};
}
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>
<CommandPaletteDefaultProvider name={data.meta.title} actions={[Shortcuts]} />
<UserPageLayout title={data.meta.title} actions={[Shortcuts]}>
<PageContent size="medium" center>
<UserPageLayout title={data.meta.title}>
{#snippet buttons()}
<IconButton
shape="round"
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} />
</PageContent>
</Container>
</UserPageLayout>

View File

@@ -1,27 +1,7 @@
<script lang="ts">
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 UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import type { PageData } from './$types';
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') },
];
import UtilitiesMenu from '$lib/components/utilities-page/utilities-menu.svelte';
interface Props {
data: PageData;
@@ -31,44 +11,9 @@
</script>
<UserPageLayout title={data.meta.title}>
<PageContent center size="small" class="pt-10">
<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 class="w-full max-w-xl m-auto">
<div class="mt-5">
<UtilitiesMenu />
</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>
</PageContent>
</div>
</UserPageLayout>

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import type { Action } from '$lib/components/asset-viewer/actions/action';
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import LargeAssetData from '$lib/components/utilities-page/large-assets/large-asset-data.svelte';
import Portal from '$lib/elements/Portal.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
@@ -55,20 +54,18 @@
});
</script>
<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">
{#if assets && data.assets.length > 0}
{#each assets as asset (asset.id)}
<LargeAssetData {asset} {onViewAsset} />
{/each}
{:else}
<p class="text-center text-lg dark:text-white flex place-items-center place-content-center">
{$t('no_assets_to_show')}
</p>
{/if}
</div>
</PageContent>
<UserPageLayout title={data.meta.title} scrollbar={true}>
<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}
{#each assets as asset (asset.id)}
<LargeAssetData {asset} {onViewAsset} />
{/each}
{:else}
<p class="text-center text-lg dark:text-white flex place-items-center place-content-center">
{$t('no_assets_to_show')}
</p>
{/if}
</div>
</UserPageLayout>
{#if $showAssetViewer}

View File

@@ -1,9 +1,8 @@
<script lang="ts">
import { goto } from '$app/navigation';
import emptyWorkflows from '$lib/assets/empty-workflows.svg';
import UserPageLayout from '$lib/components/layouts/UserPageLayout.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.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 { Route } from '$lib/route';
import {
@@ -152,128 +151,131 @@
</span>
{/snippet}
<UserPageLayout title={data.meta.title} actions={[Create]}>
<PageContent center size="large" class="pt-10">
{#if workflows.length === 0}
<EmptyPlaceholder
title={$t('create_first_workflow')}
text={$t('workflows_help_text')}
onClick={() => Create.onAction(Create)}
src={emptyWorkflows}
class="mt-10 mx-auto"
/>
{:else}
<div class="grid gap-6">
{#each workflows as workflow (workflow.id)}
<Card class="border border-light-200">
<CardHeader
class={`flex flex-row px-8 py-6 gap-4 sm:items-center sm:gap-6 ${
workflow.enabled
? 'bg-linear-to-r from-green-50 to-white dark:from-green-800/50 dark:to-green-950/45'
: 'bg-neutral-50 dark:bg-neutral-900'
}`}
>
<div class="flex-1">
<div class="flex items-center gap-3">
<span class="rounded-full {workflow.enabled ? 'h-3 w-3 bg-success' : 'h-3 w-3 rounded-full bg-muted'}"
></span>
<CardTitle>{workflow.name}</CardTitle>
</div>
<CardDescription class="mt-1 text-sm">
{workflow.description || $t('workflows_help_text')}
</CardDescription>
</div>
<div class="flex items-center gap-4">
<div class="text-right hidden sm:block">
<Text size="tiny">{$t('created_at')}</Text>
<Text size="small" fontWeight="medium">
{formatTimestamp(workflow.createdAt)}
</Text>
</div>
<IconButton
shape="round"
variant="ghost"
color="secondary"
icon={mdiDotsVertical}
aria-label={$t('menu')}
onclick={(event: MouseEvent) => showWorkflowMenu(event, workflow)}
/>
</div>
</CardHeader>
<CardBody class="space-y-6">
<div class="grid gap-4 md:grid-cols-3">
<!-- Trigger Section -->
<div class="rounded-2xl border p-4 bg-light-50 border-light-200">
<div class="mb-3">
<Text class="text-xs uppercase tracking-widest" color="muted" fontWeight="semi-bold"
>{$t('trigger')}</Text
>
<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={() => Create.onAction(Create)}
src={emptyWorkflows}
class="mt-10 mx-auto"
/>
{:else}
<div class="my-6 grid gap-6">
{#each workflows as workflow (workflow.id)}
<Card class="border border-light-200">
<CardHeader
class={`flex flex-row px-8 py-6 gap-4 sm:items-center sm:gap-6 ${
workflow.enabled
? 'bg-linear-to-r from-green-50 to-white dark:from-green-800/50 dark:to-green-950/45'
: 'bg-neutral-50 dark:bg-neutral-900'
}`}
>
<div class="flex-1">
<div class="flex items-center gap-3">
<span
class="rounded-full {workflow.enabled ? 'h-3 w-3 bg-success' : 'h-3 w-3 rounded-full bg-muted'}"
></span>
<CardTitle>{workflow.name}</CardTitle>
</div>
{@render chipItem(getTriggerLabel(workflow.triggerType))}
<CardDescription class="mt-1 text-sm">
{workflow.description || $t('workflows_help_text')}
</CardDescription>
</div>
<!-- Filters Section -->
<div class="rounded-2xl border p-4 bg-light-50 border-light-200">
<div class="mb-3">
<Text class="text-xs uppercase tracking-widest" color="muted" fontWeight="semi-bold"
>{$t('filters')}</Text
>
<div class="flex items-center gap-4">
<div class="text-right hidden sm:block">
<Text size="tiny">{$t('created_at')}</Text>
<Text size="small" fontWeight="medium">
{formatTimestamp(workflow.createdAt)}
</Text>
</div>
<div class="flex flex-wrap gap-2">
{#if workflow.filters.length === 0}
<span class="text-sm text-light-600">
{$t('no_filters_added')}
</span>
{:else}
{#each workflow.filters as workflowFilter (workflowFilter.id)}
{@render chipItem(getFilterLabel(workflowFilter.pluginFilterId))}
{/each}
{/if}
</div>
</div>
<!-- Actions Section -->
<div class="rounded-2xl border p-4 bg-light-50 border-light-200">
<div class="mb-3">
<Text class="text-xs uppercase tracking-widest" color="muted" fontWeight="semi-bold"
>{$t('actions')}</Text
>
</div>
<div>
{#if workflow.actions.length === 0}
<span class="text-sm text-light-600">
{$t('no_actions_added')}
</span>
{:else}
<div class="flex flex-wrap gap-2">
{#each workflow.actions as workflowAction (workflowAction.id)}
{@render chipItem(getActionLabel(workflowAction.pluginActionId))}
{/each}
</div>
{/if}
</div>
</div>
</div>
{#if expandedWorkflows.has(workflow.id)}
<VStack gap={2} class="w-full rounded-2xl border bg-light-50 p-4 border-light-200 ">
<CodeBlock code={getJson(workflow)} lineNumbers />
<Button
leadingIcon={mdiClose}
fullWidth
<IconButton
shape="round"
variant="ghost"
color="secondary"
onclick={() => toggleShowingSchema(workflow.id)}>{$t('close')}</Button
>
</VStack>
{/if}
</CardBody>
</Card>
{/each}
</div>
{/if}
</PageContent>
icon={mdiDotsVertical}
aria-label={$t('menu')}
onclick={(event: MouseEvent) => showWorkflowMenu(event, workflow)}
/>
</div>
</CardHeader>
<CardBody class="space-y-6">
<div class="grid gap-4 md:grid-cols-3">
<!-- Trigger Section -->
<div class="rounded-2xl border p-4 bg-light-50 border-light-200">
<div class="mb-3">
<Text class="text-xs uppercase tracking-widest" color="muted" fontWeight="semi-bold"
>{$t('trigger')}</Text
>
</div>
{@render chipItem(getTriggerLabel(workflow.triggerType))}
</div>
<!-- Filters Section -->
<div class="rounded-2xl border p-4 bg-light-50 border-light-200">
<div class="mb-3">
<Text class="text-xs uppercase tracking-widest" color="muted" fontWeight="semi-bold"
>{$t('filters')}</Text
>
</div>
<div class="flex flex-wrap gap-2">
{#if workflow.filters.length === 0}
<span class="text-sm text-light-600">
{$t('no_filters_added')}
</span>
{:else}
{#each workflow.filters as workflowFilter (workflowFilter.id)}
{@render chipItem(getFilterLabel(workflowFilter.pluginFilterId))}
{/each}
{/if}
</div>
</div>
<!-- Actions Section -->
<div class="rounded-2xl border p-4 bg-light-50 border-light-200">
<div class="mb-3">
<Text class="text-xs uppercase tracking-widest" color="muted" fontWeight="semi-bold"
>{$t('actions')}</Text
>
</div>
<div>
{#if workflow.actions.length === 0}
<span class="text-sm text-light-600">
{$t('no_actions_added')}
</span>
{:else}
<div class="flex flex-wrap gap-2">
{#each workflow.actions as workflowAction (workflowAction.id)}
{@render chipItem(getActionLabel(workflowAction.pluginActionId))}
{/each}
</div>
{/if}
</div>
</div>
</div>
{#if expandedWorkflows.has(workflow.id)}
<VStack gap={2} class="w-full rounded-2xl border bg-light-50 p-4 border-light-200 ">
<CodeBlock code={getJson(workflow)} lineNumbers />
<Button
leadingIcon={mdiClose}
fullWidth
variant="ghost"
color="secondary"
onclick={() => toggleShowingSchema(workflow.id)}>{$t('close')}</Button
>
</VStack>
{/if}
</CardBody>
</Card>
{/each}
</div>
{/if}
</section>
</section>
</UserPageLayout>