diff --git a/i18n/en.json b/i18n/en.json index 55fa27c98..72559d450 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -436,6 +436,7 @@ "back_close_deselect": "Back, close, or deselect", "backward": "Backward", "birthdate_saved": "Date of birth saved successfully", + "show_shared_links": "Show shared links", "birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.", "blurred_background": "Blurred background", "bugs_and_feature_requests": "Bugs & Feature Requests", @@ -804,6 +805,7 @@ "include_shared_albums": "Include shared albums", "include_shared_partner_assets": "Include shared partner assets", "individual_share": "Individual share", + "individual_shares": "Individual shares", "info": "Info", "interval": { "day_at_onepm": "Every day at 1pm", @@ -1172,6 +1174,7 @@ "shared_from_partner": "Photos from {partner}", "shared_link_options": "Shared link options", "shared_links": "Shared links", + "shared_links_description": "Share photos and videos with a link", "shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}", "shared_with_partner": "Shared with {partner}", "sharing": "Sharing", diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 5e0558db1..8a2b6b886 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 73eb02d89..3455cdb4f 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a6f8d551d..3721652b8 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ diff --git a/mobile/openapi/lib/model/shared_links_response.dart b/mobile/openapi/lib/model/shared_links_response.dart new file mode 100644 index 000000000..80875e617 Binary files /dev/null and b/mobile/openapi/lib/model/shared_links_response.dart differ diff --git a/mobile/openapi/lib/model/shared_links_update.dart b/mobile/openapi/lib/model/shared_links_update.dart new file mode 100644 index 000000000..5d9eda300 Binary files /dev/null and b/mobile/openapi/lib/model/shared_links_update.dart differ diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 23d9ea84e..b244284eb 100644 Binary files a/mobile/openapi/lib/model/user_preferences_response_dto.dart and b/mobile/openapi/lib/model/user_preferences_response_dto.dart differ diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index 208dbf686..3e420df11 100644 Binary files a/mobile/openapi/lib/model/user_preferences_update_dto.dart and b/mobile/openapi/lib/model/user_preferences_update_dto.dart differ diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 94ef49f12..bee8dfcac 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -11514,6 +11514,34 @@ ], "type": "string" }, + "SharedLinksResponse": { + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "sidebarWeb": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "enabled", + "sidebarWeb" + ], + "type": "object" + }, + "SharedLinksUpdate": { + "properties": { + "enabled": { + "type": "boolean" + }, + "sidebarWeb": { + "type": "boolean" + } + }, + "type": "object" + }, "SignUpDto": { "properties": { "email": { @@ -13160,6 +13188,9 @@ "ratings": { "$ref": "#/components/schemas/RatingsResponse" }, + "sharedLinks": { + "$ref": "#/components/schemas/SharedLinksResponse" + }, "tags": { "$ref": "#/components/schemas/TagsResponse" } @@ -13173,6 +13204,7 @@ "people", "purchase", "ratings", + "sharedLinks", "tags" ], "type": "object" @@ -13203,6 +13235,9 @@ "ratings": { "$ref": "#/components/schemas/RatingsUpdate" }, + "sharedLinks": { + "$ref": "#/components/schemas/SharedLinksUpdate" + }, "tags": { "$ref": "#/components/schemas/TagsUpdate" } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 46ce20788..d5fea1ed7 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -113,6 +113,10 @@ export type PurchaseResponse = { export type RatingsResponse = { enabled: boolean; }; +export type SharedLinksResponse = { + enabled: boolean; + sidebarWeb: boolean; +}; export type TagsResponse = { enabled: boolean; sidebarWeb: boolean; @@ -126,6 +130,7 @@ export type UserPreferencesResponseDto = { people: PeopleResponse; purchase: PurchaseResponse; ratings: RatingsResponse; + sharedLinks: SharedLinksResponse; tags: TagsResponse; }; export type AvatarUpdate = { @@ -158,6 +163,10 @@ export type PurchaseUpdate = { export type RatingsUpdate = { enabled?: boolean; }; +export type SharedLinksUpdate = { + enabled?: boolean; + sidebarWeb?: boolean; +}; export type TagsUpdate = { enabled?: boolean; sidebarWeb?: boolean; @@ -171,6 +180,7 @@ export type UserPreferencesUpdateDto = { people?: PeopleUpdate; purchase?: PurchaseUpdate; ratings?: RatingsUpdate; + sharedLinks?: SharedLinksUpdate; tags?: TagsUpdate; }; export type AlbumUserResponseDto = { diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 8de7021ea..5a393a2d7 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -38,6 +38,14 @@ class PeopleUpdate { sidebarWeb?: boolean; } +class SharedLinksUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; + + @ValidateBoolean({ optional: true }) + sidebarWeb?: boolean; +} + class TagsUpdate { @ValidateBoolean({ optional: true }) enabled?: boolean; @@ -98,6 +106,11 @@ export class UserPreferencesUpdateDto { @Type(() => RatingsUpdate) ratings?: RatingsUpdate; + @Optional() + @ValidateNested() + @Type(() => SharedLinksUpdate) + sharedLinks?: SharedLinksUpdate; + @Optional() @ValidateNested() @Type(() => TagsUpdate) @@ -152,6 +165,11 @@ class TagsResponse { sidebarWeb: boolean = true; } +class SharedLinksResponse { + enabled: boolean = true; + sidebarWeb: boolean = false; +} + class EmailNotificationsResponse { enabled!: boolean; albumInvite!: boolean; @@ -175,6 +193,7 @@ export class UserPreferencesResponseDto implements UserPreferences { memories!: MemoriesResponse; people!: PeopleResponse; ratings!: RatingsResponse; + sharedLinks!: SharedLinksResponse; tags!: TagsResponse; avatar!: AvatarResponse; emailNotifications!: EmailNotificationsResponse; diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts index 2c901426c..65c187883 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -34,6 +34,10 @@ export interface UserPreferences { ratings: { enabled: boolean; }; + sharedLinks: { + enabled: boolean; + sidebarWeb: boolean; + }; tags: { enabled: boolean; sidebarWeb: boolean; @@ -74,6 +78,10 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences enabled: true, sidebarWeb: false, }, + sharedLinks: { + enabled: true, + sidebarWeb: false, + }, ratings: { enabled: false, }, diff --git a/web/src/lib/components/elements/group-tab.svelte b/web/src/lib/components/elements/group-tab.svelte index 021d5ca96..950c26f3f 100644 --- a/web/src/lib/components/elements/group-tab.svelte +++ b/web/src/lib/components/elements/group-tab.svelte @@ -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()}`; @@ -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} {/each} diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index 9c49b971b..5493495fd 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -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} + + {/if} + + 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}`); + }; {#if menuItem} diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte index 70f624753..15a4468c0 100644 --- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte +++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte @@ -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 @@ -
@@ -112,7 +110,7 @@ padding="3" hideContent > - + diff --git a/web/src/lib/components/user-settings-page/feature-settings.svelte b/web/src/lib/components/user-settings-page/feature-settings.svelte index 9a60f8364..c3868070c 100644 --- a/web/src/lib/components/user-settings-page/feature-settings.svelte +++ b/web/src/lib/components/user-settings-page/feature-settings.svelte @@ -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 @@
+ +
+ +
+ {#if sharedLinksEnabled} +
+ +
+ {/if} +
+
diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index b7ea2cfb5..db04efa5d 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -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', diff --git a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte b/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte new file mode 100644 index 000000000..436f3b47d --- /dev/null +++ b/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte @@ -0,0 +1,119 @@ + + + + {#snippet buttons()} + + {/snippet} + +
+ {#if sharedLinks.length === 0} +
+

{$t('you_dont_have_any_shared_links')}

+
+ {:else} +
+ {#each filteredSharedLinks as link (link.id)} + handleDeleteLink(link.id)} /> + {/each} +
+ {/if} + + {#if sharedLink} + + {/if} +
+
diff --git a/web/src/routes/(user)/shared-links/[[id=id]]/+page.ts b/web/src/routes/(user)/shared-links/[[id=id]]/+page.ts new file mode 100644 index 000000000..920e5bdba --- /dev/null +++ b/web/src/routes/(user)/shared-links/[[id=id]]/+page.ts @@ -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; diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte deleted file mode 100644 index b7d4da294..000000000 --- a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte +++ /dev/null @@ -1,89 +0,0 @@ - - - goto(backUrl)}> - {#snippet leading()} - {$t('shared_links')} - {/snippet} - - -
-
-

{$t('manage_shared_links')}

-
- {#if sharedLinks.length === 0} -
-

{$t('you_dont_have_any_shared_links')}

-
- {:else} -
- {#each sharedLinks as link (link.id)} - handleDeleteLink(link.id)} onEdit={() => (editSharedLink = link)} /> - {/each} -
- {/if} -
- -{#if editSharedLink} - -{/if} diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.ts b/web/src/routes/(user)/sharing/sharedlinks/+page.ts index 920e5bdba..59530fd83 100644 --- a/web/src/routes/(user)/sharing/sharedlinks/+page.ts +++ b/web/src/routes/(user)/sharing/sharedlinks/+page.ts @@ -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;