feat(web): shared link filters (#15948)

This commit is contained in:
Jason Rasmussen 2025-02-07 13:05:15 -05:00 committed by GitHub
parent 23014c263b
commit c5360e78c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 520 additions and 112 deletions

View file

@ -3,12 +3,13 @@
interface Props {
filters: string[];
labels?: string[];
selected: string;
label: string;
onSelect: (selected: string) => void;
}
let { filters, selected, label, onSelect }: Props = $props();
let { filters, selected, label, labels, onSelect }: Props = $props();
const id = `group-tab-${generateId()}`;
</script>
@ -32,7 +33,7 @@
for="{id}-{index}"
class="flex h-full cursor-pointer items-center px-4 text-sm hover:bg-gray-300 group-first-of-type:rounded-s-2xl group-last-of-type:rounded-e-2xl peer-checked:bg-gray-300 dark:hover:bg-gray-800 peer-checked:dark:bg-gray-700"
>
{filter}
{labels?.[index] ?? filter}
</label>
</div>
{/each}

View file

@ -21,6 +21,7 @@
mdiToolboxOutline,
mdiFolderOutline,
mdiTagMultipleOutline,
mdiLink,
} from '@mdi/js';
import SideBarSection from './side-bar-section.svelte';
import SideBarLink from './side-bar-link.svelte';
@ -72,6 +73,10 @@
/>
{/if}
{#if $preferences.sharedLinks.enabled && $preferences.sharedLinks.sidebarWeb}
<SideBarLink title={$t('shared_links')} routeId="/(user)/shared-links" icon={mdiLink} />
{/if}
<SideBarLink
title={$t('sharing')}
routeId="/(user)/sharing"

View file

@ -1,15 +1,22 @@
<script lang="ts">
import { goto } from '$app/navigation';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { AppRoute } from '$lib/constants';
import type { SharedLinkResponseDto } from '@immich/sdk';
import { mdiCircleEditOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
menuItem?: boolean;
onEdit: () => void;
sharedLink: SharedLinkResponseDto;
}
let { menuItem = false, onEdit }: Props = $props();
let { sharedLink, menuItem = false }: Props = $props();
const onEdit = async () => {
await goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`);
};
</script>
{#if menuItem}

View file

@ -15,10 +15,9 @@
interface Props {
link: SharedLinkResponseDto;
onDelete: () => void;
onEdit: () => void;
}
let { link, onDelete, onEdit }: Props = $props();
let { link, onDelete }: Props = $props();
let now = DateTime.now();
let expiresAt = $derived(link.expiresAt ? DateTime.fromISO(link.expiresAt) : undefined);
@ -95,10 +94,9 @@
</div>
</div>
</svelte:element>
<div class="flex flex-auto flex-col place-content-center place-items-end text-end ms-4">
<div class="sm:flex hidden">
<SharedLinkEdit {onEdit} />
<SharedLinkEdit sharedLink={link} />
<SharedLinkCopy {link} />
<SharedLinkDelete {onDelete} />
</div>
@ -112,7 +110,7 @@
padding="3"
hideContent
>
<SharedLinkEdit menuItem {onEdit} />
<SharedLinkEdit menuItem sharedLink={link} />
<SharedLinkCopy menuItem {link} />
<SharedLinkDelete menuItem {onDelete} />
</ButtonContextMenu>

View file

@ -27,6 +27,10 @@
// Ratings
let ratingsEnabled = $state($preferences?.ratings?.enabled ?? false);
// Shared links
let sharedLinksEnabled = $state($preferences?.sharedLinks?.enabled ?? true);
let sharedLinkSidebar = $state($preferences?.sharedLinks?.sidebarWeb ?? false);
// Tags
let tagsEnabled = $state($preferences?.tags?.enabled ?? false);
let tagsSidebar = $state($preferences?.tags?.sidebarWeb ?? false);
@ -39,6 +43,7 @@
memories: { enabled: memoriesEnabled },
people: { enabled: peopleEnabled, sidebarWeb: peopleSidebar },
ratings: { enabled: ratingsEnabled },
sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar },
tags: { enabled: tagsEnabled, sidebarWeb: tagsSidebar },
},
});
@ -104,6 +109,21 @@
</div>
</SettingAccordion>
<SettingAccordion key="shared-links" title={$t('shared_links')} subtitle={$t('shared_links_description')}>
<div class="ml-4 mt-6">
<SettingSwitch title={$t('enable')} bind:checked={sharedLinksEnabled} />
</div>
{#if sharedLinksEnabled}
<div class="ml-4 mt-6">
<SettingSwitch
title={$t('sidebar')}
subtitle={$t('sidebar_display_description')}
bind:checked={sharedLinkSidebar}
/>
</div>
{/if}
</SettingAccordion>
<SettingAccordion key="tags" title={$t('tags')} subtitle={$t('tag_feature_description')}>
<div class="ml-4 mt-6">
<SettingSwitch title={$t('enable')} bind:checked={tagsEnabled} />

View file

@ -30,7 +30,7 @@ export enum AppRoute {
EXPLORE = '/explore',
SHARE = '/share',
SHARING = '/sharing',
SHARED_LINKS = '/sharing/sharedlinks',
SHARED_LINKS = '/shared-links',
SEARCH = '/search',
MAP = '/map',
USER_SETTINGS = '/user-settings',

View file

@ -0,0 +1,119 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import GroupTab from '$lib/components/elements/group-tab.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
import { AppRoute } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error';
import { getAllSharedLinks, removeSharedLink, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
type Props = {
data: PageData;
};
const { data }: Props = $props();
let sharedLinks: SharedLinkResponseDto[] = $state([]);
let sharedLink = $derived(sharedLinks.find(({ id }) => id === page.params.id));
const refresh = async () => {
sharedLinks = await getAllSharedLinks();
};
onMount(async () => {
await refresh();
});
const handleDeleteLink = async (id: string) => {
const isConfirmed = await dialogController.show({
title: $t('delete_shared_link'),
prompt: $t('confirm_delete_shared_link'),
confirmText: $t('delete'),
});
if (!isConfirmed) {
return;
}
try {
await removeSharedLink({ id });
notificationController.show({ message: $t('deleted_shared_link'), type: NotificationType.Info });
await refresh();
} catch (error) {
handleError(error, $t('errors.unable_to_delete_shared_link'));
}
};
const handleEditDone = async () => {
await refresh();
await goto(AppRoute.SHARED_LINKS);
};
type Filter = 'all' | 'album' | 'individual';
const filterMap: Record<Filter, string> = {
all: $t('all'),
album: $t('albums'),
individual: $t('individual_shares'),
};
let filters = Object.keys(filterMap);
let labels = Object.values(filterMap);
const getActiveTab = (url: URL) => {
const filter = url.searchParams.get('filter');
return filter && filters.includes(filter) ? filter : 'all';
};
let selectedTab = $derived(getActiveTab(page.url));
const handleSelectTab = async (value: string) => {
await goto(`${AppRoute.SHARED_LINKS}?filter=${value}`);
};
let filteredSharedLinks = $derived(
sharedLinks.filter(
({ type }) =>
selectedTab === 'all' ||
(type === SharedLinkType.Album && selectedTab === 'album') ||
(type === SharedLinkType.Individual && selectedTab === 'individual'),
),
);
</script>
<UserPageLayout title={data.meta.title}>
{#snippet buttons()}
<div class="hidden xl:block h-10">
<GroupTab label={$t('show_shared_links')} {filters} {labels} selected={selectedTab} onSelect={handleSelectTab} />
</div>
{/snippet}
<div>
{#if sharedLinks.length === 0}
<div
class="flex place-content-center place-items-center rounded-lg bg-gray-100 dark:bg-immich-dark-gray dark:text-immich-gray p-12"
>
<p>{$t('you_dont_have_any_shared_links')}</p>
</div>
{:else}
<div class="flex flex-col gap-2">
{#each filteredSharedLinks as link (link.id)}
<SharedLinkCard {link} onDelete={() => handleDeleteLink(link.id)} />
{/each}
</div>
{/if}
{#if sharedLink}
<CreateSharedLinkModal editingLink={sharedLink} onClose={handleEditDone} />
{/if}
</div>
</UserPageLayout>

View file

@ -0,0 +1,14 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
const $t = await getFormatter();
return {
meta: {
title: $t('shared_links'),
},
};
}) satisfies PageLoad;

View file

@ -1,89 +0,0 @@
<script lang="ts">
import { goto, afterNavigate } from '$app/navigation';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
import { AppRoute } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error';
import { getAllSharedLinks, removeSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
import { mdiArrowLeft } from '@mdi/js';
import { onMount } from 'svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n';
let sharedLinks: SharedLinkResponseDto[] = $state([]);
let editSharedLink: SharedLinkResponseDto | null = $state(null);
const refresh = async () => {
sharedLinks = await getAllSharedLinks();
};
onMount(async () => {
await refresh();
});
const handleDeleteLink = async (id: string) => {
const isConfirmed = await dialogController.show({
title: $t('delete_shared_link'),
prompt: $t('confirm_delete_shared_link'),
confirmText: $t('delete'),
});
if (!isConfirmed) {
return;
}
try {
await removeSharedLink({ id });
notificationController.show({ message: $t('deleted_shared_link'), type: NotificationType.Info });
await refresh();
} catch (error) {
handleError(error, $t('errors.unable_to_delete_shared_link'));
}
};
const handleEditDone = async () => {
await refresh();
editSharedLink = null;
};
let backUrl: string = AppRoute.SHARING;
afterNavigate(({ from }) => {
let url: string | undefined = from?.url?.pathname;
backUrl = url || AppRoute.SHARING;
});
</script>
<ControlAppBar backIcon={mdiArrowLeft} onClose={() => goto(backUrl)}>
{#snippet leading()}
{$t('shared_links')}
{/snippet}
</ControlAppBar>
<section class="mt-[120px] flex flex-col pb-[120px] container max-w-screen-lg mx-auto px-3">
<div class="mb-4 dark:text-immich-gray">
<p>{$t('manage_shared_links')}</p>
</div>
{#if sharedLinks.length === 0}
<div
class="flex place-content-center place-items-center rounded-lg bg-gray-100 dark:bg-immich-dark-gray dark:text-immich-gray p-12"
>
<p>{$t('you_dont_have_any_shared_links')}</p>
</div>
{:else}
<div class="flex flex-col">
{#each sharedLinks as link (link.id)}
<SharedLinkCard {link} onDelete={() => handleDeleteLink(link.id)} onEdit={() => (editSharedLink = link)} />
{/each}
</div>
{/if}
</section>
{#if editSharedLink}
<CreateSharedLinkModal editingLink={editSharedLink} onClose={handleEditDone} />
{/if}

View file

@ -1,14 +1,7 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { AppRoute } from '$lib/constants';
import { redirect } from '@sveltejs/kit';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
const $t = await getFormatter();
return {
meta: {
title: $t('shared_links'),
},
};
export const load = (() => {
redirect(307, AppRoute.SHARED_LINKS);
}) satisfies PageLoad;