feat: features section (#681)

* feat: new homepage feature section

* update screenshots

* feat: new feature card

* facial recognition screenshot

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen
2026-08-11 16:08:46 -04:00
committed by GitHub
co-authored by Alex
parent 6d757b3dfa
commit e98d680642
24 changed files with 632 additions and 329 deletions
+1 -1
View File
@@ -133,5 +133,5 @@
</Stack>
</Stack>
</Container>
<SiteFooter />
<SiteFooter class="mt-16" />
</AppShell>
Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

@@ -0,0 +1,50 @@
<script lang="ts">
import type { Feature } from '$lib/types';
import { Button, Heading, Icon, Link, Text } from '@immich/ui';
import { tv } from 'tailwind-variants';
type Props = {
feature: Feature;
color?: 'primary' | 'secondary';
};
const { feature, color = 'secondary' }: Props = $props();
const { title, description, icons, links } = $derived(feature);
const styles = tv({
base: 'flex flex-col justify-between rounded-2xl border-2 p-6 transition-all',
variants: {
color: {
primary: 'border border-primary-300 bg-primary-50',
secondary: 'bg-light-50',
},
},
});
</script>
<div class={styles({ color })}>
<div>
<div class="mb-6 flex gap-2">
{#each icons as icon, i (i)}
{#if icon}
{#if typeof icon === 'object' && 'href' in icon && icon.href}
<Link href={icon.href}>
<Icon {icon} size="2em" />
</Link>
{:else}
<Icon {icon} size="2em" />
{/if}
{/if}
{/each}
</div>
<Heading size="medium">{title}</Heading>
<Text color="muted" class="mt-2">{description}</Text>
</div>
{#if links && links.length > 0}
<div class="mt-4">
{#each links as link, i (i)}
<Button size="small" {color} href={link.href}>{link.text}</Button>
{/each}
</div>
{/if}
</div>
@@ -0,0 +1,23 @@
<script lang="ts">
import { cleanClass } from '$common';
import PageContent from '$lib/components/PageContent.svelte';
import { Container, type ContainerSize } from '@immich/ui';
import type { Snippet } from 'svelte';
type Props = {
children?: Snippet;
color?: 'primary' | 'secondary';
class?: string;
size?: ContainerSize;
};
const { size = 'large', color = 'secondary', class: className, children }: Props = $props();
</script>
<PageContent class={cleanClass(color === 'secondary' ? 'bg-light' : 'bg-primary-50')}>
<Container {size} center>
<div class={cleanClass('my-8 lg:h-112 lg:my-0', className)}>
{@render children?.()}
</div>
</Container>
</PageContent>
@@ -0,0 +1,22 @@
<script lang="ts">
import { cleanClass } from '$common';
import FeatureContainer from '$lib/components/FeatureContainer.svelte';
import type { Snippet } from 'svelte';
type Props = {
label: Snippet;
media: Snippet;
reverse?: boolean;
};
const { reverse = false, label, media }: Props = $props();
</script>
<FeatureContainer color={reverse ? 'secondary' : 'primary'} class="flex flex-col gap-12 lg:grid lg:grid-cols-2">
<div class={cleanClass(reverse ? 'lg:order-2' : '', 'col-span-1 my-auto')}>
{@render label?.()}
</div>
<div class="flex items-center justify-around overflow-hidden align-middle">
{@render media?.()}
</div>
</FeatureContainer>
@@ -0,0 +1,17 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import { cleanClass } from '$common';
type Props = {
children?: Snippet;
class?: string;
};
const { children, class: className }: Props = $props();
</script>
<div class={cleanClass('grow', className)}>
<div class="size-full w-full p-4 md:p-6 lg:p-8">
{@render children?.()}
</div>
</div>
@@ -1,5 +1,6 @@
<script lang="ts">
import { siteMetadata } from '$lib';
import PageContent from '$lib/components/PageContent.svelte';
import { Card, CardBody, Container, Heading, SiteMetadata, Text } from '@immich/ui';
import { DateTime } from 'luxon';
import type { Snippet } from 'svelte';
@@ -24,16 +25,18 @@
<SiteMetadata site={siteMetadata} page={pageMetadata} />
<Container size="medium" center>
<section class="flex flex-col gap-2">
<Heading tag="h1" size="title">{pageMetadata.title}</Heading>
<Text color="muted">Last updated: {updatedAt.toLocaleString(DateTime.DATE_FULL)}</Text>
<Text>{pageMetadata.description}</Text>
</section>
<PageContent>
<Container size="medium" center>
<section class="flex flex-col gap-2">
<Heading tag="h1" size="title">{pageMetadata.title}</Heading>
<Text color="muted">Last updated: {updatedAt.toLocaleString(DateTime.DATE_FULL)}</Text>
<Text>{pageMetadata.description}</Text>
</section>
<Card color="secondary" class="mt-8">
<CardBody class="pt-0">
{@render children?.()}
</CardBody>
</Card>
</Container>
<Card color="secondary" class="mt-8">
<CardBody class="pt-0">
{@render children?.()}
</CardBody>
</Card>
</Container>
</PageContent>
@@ -0,0 +1,34 @@
<script lang="ts">
import { Button, Heading, Icon, Text, type IconLike } from '@immich/ui';
import { mdiOpenInNew } from '@mdi/js';
import type { Snippet } from 'svelte';
type Props = {
title: string;
children?: Snippet;
icon: IconLike;
href: string;
text: string;
};
const { title, icon, href, text, children }: Props = $props();
</script>
<div
class="group flex flex-col items-start gap-6 rounded-3xl border border-primary-300 bg-primary-50 p-10 transition-colors hover:border-primary-400 sm:flex-row sm:items-center sm:gap-8"
>
<Icon
{icon}
size="4rem"
class="shrink-0 rotate-[-25deg] text-primary transition-transform duration-200 ease-out group-hover:scale-105 group-hover:rotate-[-10deg]"
/>
<div class="flex flex-1 flex-col gap-1.5 px-2">
<Heading size="medium" tag="h3">{title}</Heading>
<Text color="muted">{@render children?.()}</Text>
</div>
<Button {href} size="large" shape="round" trailingIcon={mdiOpenInNew} class="shrink-0">
{text}
</Button>
</div>
@@ -0,0 +1,57 @@
<script lang="ts">
import { Button, Heading, Icon, type IconLike } from '@immich/ui';
import { mdiOpenInNew } from '@mdi/js';
import { tv } from 'tailwind-variants';
type Props = {
title: string;
icon: IconLike;
buttonIcon?: IconLike;
href: string;
text: string;
color?: 'primary' | 'warning' | 'info';
};
const { title, icon, buttonIcon, href, text, color = 'primary' }: Props = $props();
const iconStyles = tv({
base: 'shrink-0',
variants: {
color: {
primary: 'text-primary',
warning: 'text-warning',
info: 'text-info',
},
},
});
const buttonStyles = tv({
base: 'shrink-0',
variants: {
color: {
primary: 'hover:border-primary hover:text-primary',
warning: 'hover:border-warning hover:text-warning',
info: 'hover:border-info hover:text-info',
},
},
});
</script>
<div class="flex items-center gap-4 rounded-2xl border bg-light-50 p-6">
<Icon {icon} size="1.75rem" class={iconStyles({ color })} />
<Heading size="small" tag="h3" class="flex-1">{title}</Heading>
<Button
{href}
size="small"
shape="round"
variant="outline"
color="secondary"
leadingIcon={buttonIcon}
trailingIcon={mdiOpenInNew}
class={buttonStyles({ color })}
>
{text}
</Button>
</div>
+13
View File
@@ -0,0 +1,13 @@
import type { IconLike } from '@immich/ui';
export type Feature = {
title: string;
description: string;
icons: Array<IconLike & { href?: string }>;
links?: FeatureLink[];
};
export type FeatureLink = {
href: string;
text: string;
};
@@ -1,7 +1,6 @@
<script lang="ts">
import { afterNavigate, beforeNavigate } from '$app/navigation';
import { page } from '$app/state';
import PageContent from '$common/components/PageContent.svelte';
import { posts } from '$lib';
import '$lib/app.css';
import { getSearchProvider } from '$lib/search';
@@ -21,6 +20,7 @@
Logo,
NavbarItem,
ScreencastOverlay,
SiteFooter,
Text,
ThemeSwitcher,
TooltipProvider,
@@ -160,8 +160,9 @@
</div>
</AppShellSidebar>
<PageContent class="mx-auto w-full max-w-(--breakpoint-lg)">
<div class="mx-auto flex h-full flex-col">
{@render children?.()}
</PageContent>
<SiteFooter />
</div>
</AppShell>
</TooltipProvider>
+132 -77
View File
@@ -5,13 +5,21 @@
import screenshotDark from '$common/img/screenshot-dark.webp';
import screenshotLight from '$common/img/screenshot-light.webp';
import { siteMetadata } from '$lib';
import featureFacialRecognition from '$lib/assets/img/feature-facial-recognition.webp';
import featureMobile from '$lib/assets/img/feature-mobile.webp';
import featureSharing from '$lib/assets/img/feature-sharing.webp';
import FeatureContainer from '$lib/components/FeatureContainer.svelte';
import FeatureHighlight from '$lib/components/FeatureHighlight.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import SupportCard from '$lib/components/SupportCard.svelte';
import SupportLinkCard from '$lib/components/SupportLinkCard.svelte';
import {
appStoreBadge,
Button,
Constants,
Container,
Heading,
Icon,
Link,
Logo,
playStoreBadge,
SiteMetadata,
@@ -19,94 +27,141 @@
themeManager,
VStack,
} from '@immich/ui';
import { mdiKeyOutline, mdiOpenInNew, mdiShoppingOutline } from '@mdi/js';
import { siDiscord, siGithub, siWeblate } from 'simple-icons';
const supportItems = [
{ href: Constants.Sites.Buy, text: 'Buy Immich', icon: mdiKeyOutline },
{ href: Constants.Sites.Store, text: 'Buy Merch', icon: mdiShoppingOutline },
{ href: Constants.Socials.Github, text: 'View on GitHub', icon: siGithub.path },
{ href: Constants.Socials.Weblate, text: 'Translate on Weblate', icon: siWeblate.path },
];
import { mdiKeyVariant, mdiOpenInNew, mdiSourcePull, mdiTshirtCrew } from '@mdi/js';
import { siDiscord, siGithub } from 'simple-icons';
</script>
<!-- only SSR SiteMetadata -->
<SiteMetadata site={siteMetadata} />
{#if browser}
<VStack gap={8} class="mt-4 text-center lg:mt-16">
<Logo size="giant" variant="stacked-futo" />
<PageContent>
<Container size="large" center>
<VStack gap={8} class="mt-4 text-center lg:mt-16">
<Logo size="giant" variant="stacked-futo" />
<Heading size="title" tag="h1" fontWeight="extra-bold">
Self-hosted <span class="text-primary">photo and<br class="hidden lg:block" /> video management</span> solution
</Heading>
<Heading size="title" tag="h1" fontWeight="extra-bold">
Self-hosted <span class="text-primary">photo and<br class="hidden lg:block" /> video management</span> solution
</Heading>
<Text size="large">
Easily back up, organize, and manage your photos on your own server. Immich helps you<br
class="hidden lg:block"
<Text size="large">
Easily back up, organize, and manage your photos on your own server. Immich helps you<br
class="hidden lg:block"
/>
browse, search and organize your photos and videos with ease, without sacrificing your privacy.
</Text>
<div class="flex flex-col gap-4">
<div class="flex flex-col justify-center gap-4 sm:flex-row">
<Button size="large" href="/download">Download</Button>
<Button size="large" href={Constants.Sites.Demo} color="secondary">
<span>Open Demo</span>
<Icon icon={mdiOpenInNew} />
</Button>
</div>
<div class="flex justify-center">
<Button href={Constants.Socials.Discord} size="large" variant="ghost" leadingIcon={siDiscord.path}>
Join our Discord
</Button>
</div>
</div>
<div class="relative -z-10">
<img src={themeManager.value === 'dark' ? screenshotDark : screenshotLight} alt="Immich application" />
<div class="absolute top-[-55%] left-0 -z-10 h-[200%] w-full overflow-visible">
<Logo size="giant" class="z-10 mb-2 size-full antialiased opacity-20 blur-3xl" />
<div class="bg-immich-bg/90 absolute top-0 left-0 size-full backdrop-blur-xl dark:bg-transparent"></div>
</div>
</div>
</VStack>
</Container>
</PageContent>
<FeatureHighlight>
{#snippet label()}
<Heading size="large" tag="h3" class="mb-2">Mobile app</Heading>
<Text size="large">
View and manage your photos and videos directly from the mobile app. Immich makes it easy to organize, explore
and share your precious memories on both Apple and Android.
</Text>
<div class="my-8 flex h-full justify-around gap-4 sm:justify-start lg:my-4">
<div class="flex flex-col items-start justify-around gap-4">
<a href={Constants.Get.Android}>
<img src={playStoreBadge} alt="Playstore Badge" class="w-48" />
</a>
<a href={Constants.Get.iOS}>
<img src={appStoreBadge} alt="AppStore Badge" class="w-48" />
</a>
</div>
<img src={themeManager.value === 'dark' ? qrCodeDark : qrCodeLight} alt="QRCode" class="h-36 rounded-xl" />
</div>
{/snippet}
{#snippet media()}
<img src={featureMobile} alt="Mobile app" class="object-scale-down" />
{/snippet}
</FeatureHighlight>
<FeatureHighlight reverse>
{#snippet label()}
<Heading size="large" tag="h3" class="mb-2">Sharing</Heading>
<Text size="large">
Share with friends and family using shared albums, partner sharing, and shared links. Everyone has their own
account and can collaborate together.
</Text>
{/snippet}
{#snippet media()}
<img src={featureSharing} alt="Sharing" class="object-scale-down" />
{/snippet}
</FeatureHighlight>
<FeatureHighlight>
{#snippet label()}
<Heading size="large" tag="h3" class="mb-2">Facial Recognition</Heading>
<Text size="large">
Immich automatically detects and groups similar faces together, providing another way to search, organize, or
browse your photos and videos.
</Text>
{/snippet}
{#snippet media()}
<img
src={featureFacialRecognition}
alt="Face box around a detected person"
class="rounded-xl object-scale-down"
/>
browse, search and organize your photos and videos with ease, without sacrificing your privacy.
</Text>
{/snippet}
</FeatureHighlight>
<div class="flex flex-col gap-4">
<div class="flex flex-col justify-center gap-4 sm:flex-row">
<Button size="large" href="/download">Download</Button>
<Button size="large" href={Constants.Sites.Demo} color="secondary">
<span>Open Demo</span>
<Icon icon={mdiOpenInNew} />
</Button>
</div>
<div class="flex justify-center">
<Button href={Constants.Socials.Discord} size="large" variant="ghost" leadingIcon={siDiscord.path}>
Join our Discord
</Button>
<FeatureContainer size="large" class="flex items-center">
<div>
<Heading size="large" tag="h2" class="mb-8 text-center">Support Immich</Heading>
<div class="flex w-full flex-col gap-5">
<SupportCard title="Product Key" icon={mdiKeyVariant} href={Constants.Sites.Buy} text="Buy a key">
Building Immich takes a lot of time and effort. We have full-time engineers working on it to make it as good
as we possibly can. Consider buying a product key to support the project.
</SupportCard>
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
<SupportLinkCard
title="Merch Store"
icon={mdiTshirtCrew}
color="warning"
href={Constants.Sites.Store}
text="View store"
/>
<SupportLinkCard
title="Contribute"
icon={mdiSourcePull}
color="info"
buttonIcon={siGithub}
href={Constants.Socials.Github}
text="GitHub"
/>
</div>
</div>
</div>
<div class="relative -z-10">
<img src={themeManager.value === 'dark' ? screenshotDark : screenshotLight} alt="Immich application" />
<div class="absolute top-[-55%] left-0 -z-10 h-[200%] w-full overflow-visible">
<Logo size="giant" class="z-10 mb-2 size-full antialiased opacity-20 blur-3xl" />
<div class="bg-immich-bg/90 absolute top-0 left-0 size-full backdrop-blur-xl dark:bg-transparent"></div>
</div>
</div>
<hr class="m-2 w-full max-w-(--breakpoint-sm) border-t" />
<Heading size="title" tag="h2">Download mobile app</Heading>
<Text size="large">Download Immich app and start backing up your photos and videos securely to your own server</Text
>
<div class="flex flex-col gap-4 lg:flex-row">
<Button href={Constants.Get.Android} variant="ghost" class="p-0">
<img src={playStoreBadge} alt="Playstore Badge" class="h-16" />
</Button>
<Button href={Constants.Get.iOS} variant="ghost" class="p-0">
<img src={appStoreBadge} alt="AppStore Badge" class="h-16" />
</Button>
</div>
<img src={themeManager.value === 'dark' ? qrCodeDark : qrCodeLight} alt="QRCode" class="h-36 rounded-xl" />
<hr class="m-8 w-full max-w-(--breakpoint-sm) border-t" />
<Heading size="title" tag="h2">Support the project</Heading>
<Text size="large">
Support Immich by purchasing a <Link href={Constants.Sites.Buy}>product key</Link>,
<Link href={Constants.Sites.Store}>merch</Link>, or contributing on
<Link href={Constants.Socials.Github}>GitHub</Link> or
<Link href={Constants.Socials.Weblate}>Weblate</Link>
</Text>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
{#each supportItems as action, i (i)}
<Button href={action.href} size="large" variant="outline" color="secondary" leadingIcon={action.icon}>
{action.text}
</Button>
{/each}
</div>
</VStack>
</FeatureContainer>
{:else}
<div class="h-screen w-full"></div>
{/if}
@@ -1,5 +1,6 @@
<script lang="ts">
import { page } from '$app/state';
import PageContent from '$lib/components/PageContent.svelte';
import type { Snippet } from 'svelte';
import { tv } from 'tailwind-variants';
@@ -22,6 +23,8 @@
});
</script>
<div class={styles({ releaseNotes: isReleaseNotePage })}>
{@render children?.()}
</div>
<PageContent>
<div class={styles({ releaseNotes: isReleaseNotePage })}>
{@render children?.()}
</div>
</PageContent>
@@ -30,8 +30,10 @@
<SiteMetadata site={blogMetadata} />
<Heading size="title" tag="h1" fontWeight="bold" class="mb-1">Blog</Heading>
<Text color="muted" class="mt-4">{blogMetadata.description}</Text>
<div>
<Heading size="title" tag="h1" fontWeight="bold" class="mb-1">Blog</Heading>
<Text color="muted" class="mt-4">{blogMetadata.description}</Text>
</div>
<div class="flex flex-wrap gap-1">
<a href={Routes.blog()}>
@@ -1,6 +1,7 @@
<script lang="ts">
import type { TimelineItem } from '$lib';
import { posts, siteMetadata } from '$lib';
import PageContent from '$lib/components/PageContent.svelte';
import Timeline from '$lib/components/Timeline.svelte';
import { asGithubLink, Heading, SiteMetadata, Stack, Text } from '@immich/ui';
import {
@@ -236,15 +237,17 @@
<SiteMetadata site={siteMetadata} page={pageMetadata} />
<Stack class="text-center" gap={4}>
<Heading size="title" tag="h1">{pageMetadata.title}</Heading>
<Text>{pageMetadata.description}</Text>
</Stack>
<PageContent>
<Stack class="text-center" gap={4}>
<Heading size="title" tag="h1">{pageMetadata.title}</Heading>
<Text>{pageMetadata.description}</Text>
</Stack>
<div class="mx-auto mt-8 flex w-full max-w-(--breakpoint-md) justify-around">
<Timeline
items={items
.toSorted((a, b) => b.date.getTime() - a.date.getTime())
.map((item) => ({ ...item, getDateLabel: withLanguage(item.date) }))}
/>
</div>
<div class="mx-auto mt-8 flex w-full max-w-(--breakpoint-md) justify-around">
<Timeline
items={items
.toSorted((a, b) => b.date.getTime() - a.date.getTime())
.map((item) => ({ ...item, getDateLabel: withLanguage(item.date) }))}
/>
</div>
</PageContent>
@@ -3,6 +3,7 @@
import truenasIcon from '$common/img/truenas-scale.svg';
import unraidIcon from '$common/img/unraid.svg';
import { siteMetadata } from '$lib';
import PageContent from '$lib/components/PageContent.svelte';
import {
appStoreBadge,
Button,
@@ -13,6 +14,7 @@
CardHeader,
CardTitle,
Constants,
Container,
fdroidBadge,
Heading,
Icon,
@@ -31,109 +33,112 @@
</script>
<SiteMetadata site={siteMetadata} page={pageMetadata} />
<PageContent>
<Container size="large" center>
<Heading size="title" class="mb-4" tag="h1">{pageMetadata.title}</Heading>
<Heading size="title" class="mb-4" tag="h1">{pageMetadata.title}</Heading>
<div class="mb-4">
<Heading size="large" tag="h2">Mobile Apps</Heading>
<Text color="muted">{pageMetadata.description}</Text>
</div>
<div class="mb-4">
<Heading size="large" tag="h2">Mobile Apps</Heading>
<Text color="muted">{pageMetadata.description}</Text>
</div>
<div class="l mb-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2">
<Card color="secondary">
<CardHeader>
<CardTitle>Android</CardTitle>
</CardHeader>
<CardBody>
<Stack gap={4}>
<a href={Constants.Get.Android}>
<img class="mx-auto h-16" alt="Get it on Google Play" src={playStoreBadge} />
</a>
<a href={Constants.Get.FDroid}>
<img class="mx-auto h-16 rounded-xl border border-dark" alt="Get it on F-Droid" src={fdroidBadge} />
</a>
<Button
class="light mx-auto h-16 border"
size="giant"
variant="filled"
href={Constants.Get.GithubRelease}
color="secondary"
>
<Icon icon={mdiAndroid} size="2em" />
<Text>Download APK</Text>
</Button>
</Stack>
</CardBody>
</Card>
<div class="l mb-8 grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-2">
<Card color="secondary">
<CardHeader>
<CardTitle>Android</CardTitle>
</CardHeader>
<CardBody>
<Stack gap={4}>
<a href={Constants.Get.Android}>
<img class="mx-auto h-16" alt="Get it on Google Play" src={playStoreBadge} />
</a>
<a href={Constants.Get.FDroid}>
<img class="mx-auto h-16 rounded-xl border border-dark" alt="Get it on F-Droid" src={fdroidBadge} />
</a>
<Button
class="light mx-auto h-16 border"
size="giant"
variant="filled"
href={Constants.Get.GithubRelease}
color="secondary"
>
<Icon icon={mdiAndroid} size="2em" />
<Text>Download APK</Text>
</Button>
</Stack>
</CardBody>
</Card>
<Card color="secondary">
<CardHeader>
<CardTitle>iOS</CardTitle>
</CardHeader>
<CardBody>
<a href={Constants.Get.iOS}>
<img class="mx-auto mt-4 h-16" alt="Download on the App Store" src={appStoreBadge} />
</a>
</CardBody>
</Card>
</div>
<Card color="secondary">
<CardHeader>
<CardTitle>iOS</CardTitle>
</CardHeader>
<CardBody>
<a href={Constants.Get.iOS}>
<img class="mx-auto mt-4 h-16" alt="Download on the App Store" src={appStoreBadge} />
</a>
</CardBody>
</Card>
</div>
<div class="mb-4">
<Heading size="large" tag="h2">Server</Heading>
<Text color="muted">Download, setup, and run Immich on your own server</Text>
</div>
<div class="mb-4">
<Heading size="large" tag="h2">Server</Heading>
<Text color="muted">Download, setup, and run Immich on your own server</Text>
</div>
<section class="grid auto-rows-fr grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card color="secondary" class="h-full">
<CardHeader>
<CardTitle>Docker Compose</CardTitle>
<CardDescription>Run Immich on <Link href="https://www.docker.com/">Docker</Link></CardDescription>
</CardHeader>
<CardBody class="flex items-center justify-center align-middle">
<Icon icon={mdiDocker} size="6rem" color="#0db7ed" />
</CardBody>
<CardFooter class="flex flex-col gap-2">
<Button fullWidth href="https://immich.app/docs/install/docker-compose">View Instructions</Button>
</CardFooter>
</Card>
<section class="grid auto-rows-fr grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card color="secondary" class="h-full">
<CardHeader>
<CardTitle>Docker Compose</CardTitle>
<CardDescription>Run Immich on <Link href="https://www.docker.com/">Docker</Link></CardDescription>
</CardHeader>
<CardBody class="flex items-center justify-center align-middle">
<Icon icon={mdiDocker} size="6rem" color="#0db7ed" />
</CardBody>
<CardFooter class="flex flex-col gap-2">
<Button fullWidth href="https://immich.app/docs/install/docker-compose">View Instructions</Button>
</CardFooter>
</Card>
<Card color="secondary" class="h-full">
<CardHeader>
<CardTitle>Portainer</CardTitle>
<CardDescription>Run Immich on <Link href="https://www.portainer.io/">Portainer</Link></CardDescription>
</CardHeader>
<CardBody class="flex items-center justify-center align-middle">
<img src={portainerIcon} class="w-20" alt="Portainer logo" />
</CardBody>
<CardFooter>
<Button fullWidth href="https://immich.app/docs/install/portainer">View Instructions</Button>
</CardFooter>
</Card>
<Card color="secondary" class="h-full">
<CardHeader>
<CardTitle>Unraid</CardTitle>
<CardDescription>Run Immich on <Link href="https://unraid.net/">Unraid</Link></CardDescription>
</CardHeader>
<CardBody class="flex items-center justify-center align-middle">
<img src={unraidIcon} class="w-24" alt="UnRaid logo" />
</CardBody>
<CardFooter>
<Button fullWidth href="https://immich.app/docs/install/unraid">View Instructions</Button>
</CardFooter>
</Card>
<Card color="secondary" class="h-full">
<CardHeader>
<CardTitle>Portainer</CardTitle>
<CardDescription>Run Immich on <Link href="https://www.portainer.io/">Portainer</Link></CardDescription>
</CardHeader>
<CardBody class="flex items-center justify-center align-middle">
<img src={portainerIcon} class="w-20" alt="Portainer logo" />
</CardBody>
<CardFooter>
<Button fullWidth href="https://immich.app/docs/install/portainer">View Instructions</Button>
</CardFooter>
</Card>
<Card color="secondary" class="h-full">
<CardHeader>
<CardTitle>Unraid</CardTitle>
<CardDescription>Run Immich on <Link href="https://unraid.net/">Unraid</Link></CardDescription>
</CardHeader>
<CardBody class="flex items-center justify-center align-middle">
<img src={unraidIcon} class="w-24" alt="UnRaid logo" />
</CardBody>
<CardFooter>
<Button fullWidth href="https://immich.app/docs/install/unraid">View Instructions</Button>
</CardFooter>
</Card>
<Card color="secondary" class="h-full">
<CardHeader>
<CardTitle>TrueNAS SCALE</CardTitle>
<CardDescription
>Run Immich on <Link href="https://www.truenas.com/truenas-scale/">TrueNAS SCALE</Link></CardDescription
>
</CardHeader>
<CardBody class="flex items-center justify-center align-middle">
<img src={truenasIcon} class="w-24" alt="TrueNAS SCALE logo" />
</CardBody>
<CardFooter>
<Button fullWidth href="https://immich.app/docs/install/truenas">View Instructions</Button>
</CardFooter>
</Card>
</section>
<Card color="secondary" class="h-full">
<CardHeader>
<CardTitle>TrueNAS SCALE</CardTitle>
<CardDescription
>Run Immich on <Link href="https://www.truenas.com/truenas-scale/">TrueNAS SCALE</Link></CardDescription
>
</CardHeader>
<CardBody class="flex items-center justify-center align-middle">
<img src={truenasIcon} class="w-24" alt="TrueNAS SCALE logo" />
</CardBody>
<CardFooter>
<Button fullWidth href="https://immich.app/docs/install/truenas">View Instructions</Button>
</CardFooter>
</Card>
</section>
</Container>
</PageContent>
@@ -1,30 +1,24 @@
<script lang="ts">
import { siteMetadata } from '$lib';
import {
Card,
CardBody,
CardDescription,
CardHeader,
CardTitle,
Heading,
Icon,
SiteMetadata,
Stack,
Text,
} from '@immich/ui';
import FeatureCard from '$lib/components/FeatureCard.svelte';
import PageContent from '$lib/components/PageContent.svelte';
import type { Feature } from '$lib/types';
import { Container, Heading, SiteMetadata, Text } from '@immich/ui';
import {
mdiAccountCog,
mdiAccountMultiple,
mdiAccountMultipleOutline,
mdiAccountPlusOutline,
mdiAndroid,
mdiApple,
mdiArchiveOutline,
mdiCalendarTodayOutline,
mdiCellphone,
mdiCast,
mdiChartLine,
mdiCloudUpload,
mdiDatabaseOutline,
mdiEmailOutline,
mdiFaceRecognition,
mdiFileMultiple,
mdiFileOutline,
mdiFileUploadOutline,
mdiFolderMultiple,
@@ -34,6 +28,8 @@
mdiImageMultiple,
mdiImageMultipleOutline,
mdiKeyOutline,
mdiLink,
mdiLock,
mdiMagnify,
mdiMapMarkerOutline,
mdiMapOutline,
@@ -43,210 +39,225 @@
mdiRaw,
mdiRotate360,
mdiSecurity,
mdiShareVariant,
mdiShare,
mdiTagMultiple,
mdiThemeLightDark,
mdiTimelineOutline,
mdiTrashCan,
mdiTune,
mdiWeb,
} from '@mdi/js';
type Feature = {
icon: string;
title: string;
description: string;
};
const features: Feature[] = [
{
icons: [mdiApple, mdiAndroid],
title: 'Mobile App',
description: 'View your photos and videos on the go',
icon: mdiCellphone,
description: 'Backup photos and videos from your devices.',
},
{
icons: [mdiShare],
title: 'Sharing',
description: 'Share your library with friends and family.',
},
{
icons: [mdiFaceRecognition],
title: 'Facial Recognition',
description: 'Detect and group faces automatically.',
},
{
icons: [mdiLink],
title: 'Public Links',
description: 'Create secure links for sharing specific collections.',
},
{
icons: [mdiAccountMultiple],
title: 'Multiple Users',
description: 'Each user has their own account and devices.',
},
{
icons: [mdiMotionPlayOutline],
title: 'Motion Photos',
description: 'Play Apple LivePhotos and Android Motion Photos.',
},
{
icons: [mdiCast],
title: 'Cast',
description: 'Enjoy photos and videos on the big screen.',
},
{
icons: [mdiLock],
title: 'Locked Folder',
description: 'Extra protection for sensitive media.',
},
{
icons: [mdiTune],
title: 'Built-in Editor',
description: 'Easily adjust, crop, and rotate photos.',
},
{
icons: [mdiFileMultiple],
title: 'Duplicate Detection',
description: 'Detect and group duplicates automatically.',
},
{
icons: [mdiMagnify],
title: 'Smart Search',
description: 'Search using phrases like "sunset at the beach".',
},
{
icons: [mdiSecurity],
title: 'OAuth Integration',
description: 'Integrate with 3rd party identity providers.',
},
{
title: 'Timeline',
description: 'View your photos and videos in a timeline',
icon: mdiTimelineOutline,
icons: [mdiTimelineOutline],
},
{
title: 'Albums',
description: 'Organize your photos and videos into albums and share them with other users',
icon: mdiImageAlbum,
},
{
title: 'Multi-User',
description: 'Add friends and family to your instance, each with their own private set of photos and videos',
icon: mdiAccountPlusOutline,
},
{
title: 'Shared Links',
description: 'Share your photos and videos with private, secure, and optionally password protected links',
icon: mdiShareVariant,
icons: [mdiImageAlbum],
},
{
title: 'Partner Sharing',
description: 'Share your photos and videos with one or more partners',
icon: mdiAccountMultipleOutline,
icons: [mdiAccountMultipleOutline],
},
{
title: 'Dark Theme',
description: 'View Immich using a dark theme',
icon: mdiThemeLightDark,
},
{
title: 'Facial Recognition',
description: 'Detect faces in your photos and videos',
icon: mdiFaceRecognition,
icons: [mdiThemeLightDark],
},
{
title: 'Map',
description: 'View your photos and videos on a map',
icon: mdiMapOutline,
icons: [mdiMapOutline],
},
{
title: 'Archive',
description: 'Hide non-essential photos and videos from the main timeline',
icon: mdiArchiveOutline,
icons: [mdiArchiveOutline],
},
{
title: 'Memories',
description: 'View photos and videos taken on this day in past years',
icon: mdiCalendarTodayOutline,
icons: [mdiCalendarTodayOutline],
},
{
title: 'Favorites',
description: 'Keep track of your favorite photos and videos',
icon: mdiHeartOutline,
},
{
title: 'Smart Search',
description: 'Search your photos and videos with natural language',
icon: mdiMagnify,
icons: [mdiHeartOutline],
},
{
title: 'Slideshow',
description: 'View your photos and videos as a slideshow',
icon: mdiPlayBoxOutline,
},
{
title: 'Live Photos',
description: 'View Live Photos from Apple devices',
icon: mdiMotionPlayOutline,
},
{
title: 'Motion Photos',
description: 'View Motion PHotos from Android devices',
icon: mdiMotionPlayOutline,
icons: [mdiPlayBoxOutline],
},
{
title: '360° Photos',
description: 'View your 360° photos in Immich',
icon: mdiRotate360,
icons: [mdiRotate360],
},
{
title: 'Panoramas',
description: 'View your panoramas in Immich',
icon: mdiPanoramaOutline,
icons: [mdiPanoramaOutline],
},
{
title: 'RAW',
description: 'View your raw, uncompressed photo and scans in Immich',
icon: mdiRaw,
icons: [mdiRaw],
},
{
title: 'Stacks',
description: 'Combine similar or related photos and videos into a stack',
icon: mdiImageMultipleOutline,
icons: [mdiImageMultipleOutline],
},
{
title: 'Tags',
description: 'Organize your photos and videos with hierarchal tags',
icon: mdiTagMultiple,
icons: [mdiTagMultiple],
},
{
title: 'Drag and Drop',
description: 'Drag and drop photos, videos, and screenshots into Immich',
icon: mdiFileUploadOutline,
icons: [mdiFileUploadOutline],
},
{
title: 'Internationalization',
description: 'View Immich in your preferred language and locale',
icon: mdiWeb,
icons: [mdiWeb],
},
{
title: 'Email Notifications',
description: 'Receive email notifications when new photos or videos are added to an album',
icon: mdiEmailOutline,
icons: [mdiEmailOutline],
},
{
title: 'Trash',
description: 'Automatically delete photos and videos from the trash after a period of time',
icon: mdiTagMultiple,
icons: [mdiTrashCan],
},
{
title: 'Reverse Geocoding',
description: 'View and search your photos and videos by city, state, and country names',
icon: mdiMapMarkerOutline,
icons: [mdiMapMarkerOutline],
},
{
title: 'Activity & Comments',
description: 'View activity and comments on your photos and videos',
icon: mdiForumOutline,
icons: [mdiForumOutline],
},
{
title: 'External Libraries',
description: 'Mount your photos and videos as read-only, external folder',
icon: mdiImageMultiple,
icons: [mdiImageMultiple],
},
{
title: 'Folder View',
description: 'View your photos and videos in a folder structure',
icon: mdiFolderMultiple,
icons: [mdiFolderMultiple],
},
{
title: 'Storage Template',
description: 'Customize how your photos and videos are stored on disk',
icon: mdiTagMultiple,
},
{
title: 'OIDC',
description: 'Login to Immich using OpenID Connect',
icon: mdiSecurity,
icons: [mdiTagMultiple],
},
{
title: 'Hardware Transcoding',
description: 'Use hardware acceleration to transcode your videos',
icon: mdiAccountMultiple,
icons: [mdiAccountMultiple],
},
{
title: 'Database Backups',
description: 'Automatically backup your Immich database',
icon: mdiDatabaseOutline,
icons: [mdiDatabaseOutline],
},
{
title: 'CLI Upload',
description: 'Import photos and videos into Immich using the CLI',
icon: mdiCloudUpload,
icons: [mdiCloudUpload],
},
{
title: 'Api Keys',
title: 'API Keys',
description: 'Programmatically access your data or integrate Immich with third party services',
icon: mdiKeyOutline,
icons: [mdiKeyOutline],
},
{
title: 'Monitoring',
description: 'Monitor the health of your Immich instance via Prometheus',
icon: mdiChartLine,
icons: [mdiChartLine],
},
{
title: 'User Settings',
description: 'Customize your Immich experience',
icon: mdiAccountCog,
icons: [mdiAccountCog],
},
{
title: 'XML Sidecars',
description: 'Sync metadata to and from XMP sidecar files',
icon: mdiFileOutline,
icons: [mdiFileOutline],
},
];
@@ -258,25 +269,17 @@
<SiteMetadata site={siteMetadata} page={pageMetadata} />
<Stack gap={8}>
<Stack>
<Heading size="title" tag="h1">{pageMetadata.title}</Heading>
<Text>{pageMetadata.description}</Text>
</Stack>
<PageContent>
<Container size="giant" center>
<div class="mb-4">
<Heading size="title" tag="h1" fontWeight="bold" class="mb-1">{pageMetadata.title}</Heading>
<Text color="muted">{pageMetadata.description}</Text>
</div>
<Stack gap={2}>
<section class="grid auto-rows-fr grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
<div class="grid w-full grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 lg:gap-6">
{#each features as feature, i (i)}
<Card color="secondary" class="h-full">
<CardHeader>
<CardTitle>{feature.title}</CardTitle>
<CardDescription>{feature.description}</CardDescription>
</CardHeader>
<CardBody class="flex items-center justify-center align-middle text-primary">
<Icon icon={feature.icon} size="3rem" class="m-5" />
</CardBody>
</Card>
<FeatureCard {feature} />
{/each}
</section>
</Stack>
</Stack>
</div>
</Container>
</PageContent>
@@ -1,8 +1,9 @@
<script lang="ts">
import type { TimelineItem } from '$lib';
import { siteMetadata } from '$lib';
import PageContent from '$lib/components/PageContent.svelte';
import Timeline from '$lib/components/Timeline.svelte';
import { Heading, SiteMetadata, Stack, Text } from '@immich/ui';
import { Container, Heading, SiteMetadata, Stack, Text } from '@immich/ui';
import {
mdiAccountGroup,
mdiAccountGroupOutline,
@@ -985,10 +986,14 @@
<SiteMetadata site={siteMetadata} page={pageMetadata} />
<Stack class="text-center" gap={4}>
<Heading size="title" tag="h1">Roadmap</Heading>
<Text>{pageMetadata.description}</Text>
</Stack>
<div class="mx-auto mt-8 flex w-full max-w-(--breakpoint-md) justify-around">
<Timeline items={[...roadmap, ...milestones]} />
</div>
<PageContent>
<Container size="medium" center>
<Stack class="text-center" gap={4}>
<Heading size="title" tag="h1">Roadmap</Heading>
<Text>{pageMetadata.description}</Text>
</Stack>
<div class="mx-auto mt-8 flex w-full max-w-(--breakpoint-md) justify-around">
<Timeline items={[...roadmap, ...milestones]} />
</div>
</Container>
</PageContent>
@@ -105,6 +105,6 @@
<div class="grow">
{@render children?.()}
</div>
<SiteFooter />
<SiteFooter class="mt-16" />
</section>
</AppShell>