From c5360e78c53d81f5e0cc6ce016647d804a35047b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 7 Feb 2025 13:05:15 -0500 Subject: [PATCH] feat(web): shared link filters (#15948) --- i18n/en.json | 3 + mobile/openapi/README.md | 2 + mobile/openapi/lib/api.dart | 2 + mobile/openapi/lib/api_client.dart | 4 + .../lib/model/shared_links_response.dart | 107 +++++++++++++++ .../lib/model/shared_links_update.dart | 125 ++++++++++++++++++ .../model/user_preferences_response_dto.dart | 10 +- .../model/user_preferences_update_dto.dart | 19 ++- open-api/immich-openapi-specs.json | 35 +++++ open-api/typescript-sdk/src/fetch-client.ts | 10 ++ server/src/dtos/user-preferences.dto.ts | 19 +++ server/src/entities/user-metadata.entity.ts | 8 ++ .../lib/components/elements/group-tab.svelte | 5 +- .../side-bar/side-bar.svelte | 5 + .../actions/shared-link-edit.svelte | 11 +- .../sharedlinks-page/shared-link-card.svelte | 8 +- .../feature-settings.svelte | 20 +++ web/src/lib/constants.ts | 2 +- .../shared-links/[[id=id]]/+page.svelte | 119 +++++++++++++++++ .../(user)/shared-links/[[id=id]]/+page.ts | 14 ++ .../(user)/sharing/sharedlinks/+page.svelte | 89 ------------- .../(user)/sharing/sharedlinks/+page.ts | 15 +-- 22 files changed, 520 insertions(+), 112 deletions(-) create mode 100644 mobile/openapi/lib/model/shared_links_response.dart create mode 100644 mobile/openapi/lib/model/shared_links_update.dart create mode 100644 web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte create mode 100644 web/src/routes/(user)/shared-links/[[id=id]]/+page.ts delete mode 100644 web/src/routes/(user)/sharing/sharedlinks/+page.svelte diff --git a/i18n/en.json b/i18n/en.json index 55fa27c981b30..72559d4502ca6 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 5e0558db1849b..8a2b6b886a6f8 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -408,6 +408,8 @@ Class | Method | HTTP request | Description - [SharedLinkEditDto](doc//SharedLinkEditDto.md) - [SharedLinkResponseDto](doc//SharedLinkResponseDto.md) - [SharedLinkType](doc//SharedLinkType.md) + - [SharedLinksResponse](doc//SharedLinksResponse.md) + - [SharedLinksUpdate](doc//SharedLinksUpdate.md) - [SignUpDto](doc//SignUpDto.md) - [SmartSearchDto](doc//SmartSearchDto.md) - [SourceType](doc//SourceType.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 73eb02d89ed7a..3455cdb4fd28a 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -221,6 +221,8 @@ part 'model/shared_link_create_dto.dart'; part 'model/shared_link_edit_dto.dart'; part 'model/shared_link_response_dto.dart'; part 'model/shared_link_type.dart'; +part 'model/shared_links_response.dart'; +part 'model/shared_links_update.dart'; part 'model/sign_up_dto.dart'; part 'model/smart_search_dto.dart'; part 'model/source_type.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a6f8d551da81c..3721652b8bdd4 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -496,6 +496,10 @@ class ApiClient { return SharedLinkResponseDto.fromJson(value); case 'SharedLinkType': return SharedLinkTypeTypeTransformer().decode(value); + case 'SharedLinksResponse': + return SharedLinksResponse.fromJson(value); + case 'SharedLinksUpdate': + return SharedLinksUpdate.fromJson(value); case 'SignUpDto': return SignUpDto.fromJson(value); case 'SmartSearchDto': 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 0000000000000..80875e6174da3 --- /dev/null +++ b/mobile/openapi/lib/model/shared_links_response.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SharedLinksResponse { + /// Returns a new [SharedLinksResponse] instance. + SharedLinksResponse({ + this.enabled = true, + this.sidebarWeb = false, + }); + + bool enabled; + + bool sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is SharedLinksResponse && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode) + + (sidebarWeb.hashCode); + + @override + String toString() => 'SharedLinksResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + json[r'sidebarWeb'] = this.sidebarWeb; + return json; + } + + /// Returns a new [SharedLinksResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SharedLinksResponse? fromJson(dynamic value) { + upgradeDto(value, "SharedLinksResponse"); + if (value is Map) { + final json = value.cast(); + + return SharedLinksResponse( + enabled: mapValueOfType(json, r'enabled')!, + sidebarWeb: mapValueOfType(json, r'sidebarWeb')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SharedLinksResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SharedLinksResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SharedLinksResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SharedLinksResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + 'sidebarWeb', + }; +} + 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 0000000000000..5d9eda3001cca --- /dev/null +++ b/mobile/openapi/lib/model/shared_links_update.dart @@ -0,0 +1,125 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SharedLinksUpdate { + /// Returns a new [SharedLinksUpdate] instance. + SharedLinksUpdate({ + this.enabled, + this.sidebarWeb, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? enabled; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is SharedLinksUpdate && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled == null ? 0 : enabled!.hashCode) + + (sidebarWeb == null ? 0 : sidebarWeb!.hashCode); + + @override + String toString() => 'SharedLinksUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + if (this.enabled != null) { + json[r'enabled'] = this.enabled; + } else { + // json[r'enabled'] = null; + } + if (this.sidebarWeb != null) { + json[r'sidebarWeb'] = this.sidebarWeb; + } else { + // json[r'sidebarWeb'] = null; + } + return json; + } + + /// Returns a new [SharedLinksUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SharedLinksUpdate? fromJson(dynamic value) { + upgradeDto(value, "SharedLinksUpdate"); + if (value is Map) { + final json = value.cast(); + + return SharedLinksUpdate( + enabled: mapValueOfType(json, r'enabled'), + sidebarWeb: mapValueOfType(json, r'sidebarWeb'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SharedLinksUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SharedLinksUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SharedLinksUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SharedLinksUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 23d9ea84ecd82..b244284eb0072 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -21,6 +21,7 @@ class UserPreferencesResponseDto { required this.people, required this.purchase, required this.ratings, + required this.sharedLinks, required this.tags, }); @@ -40,6 +41,8 @@ class UserPreferencesResponseDto { RatingsResponse ratings; + SharedLinksResponse sharedLinks; + TagsResponse tags; @override @@ -52,6 +55,7 @@ class UserPreferencesResponseDto { other.people == people && other.purchase == purchase && other.ratings == ratings && + other.sharedLinks == sharedLinks && other.tags == tags; @override @@ -65,10 +69,11 @@ class UserPreferencesResponseDto { (people.hashCode) + (purchase.hashCode) + (ratings.hashCode) + + (sharedLinks.hashCode) + (tags.hashCode); @override - String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, tags=$tags]'; + String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; @@ -80,6 +85,7 @@ class UserPreferencesResponseDto { json[r'people'] = this.people; json[r'purchase'] = this.purchase; json[r'ratings'] = this.ratings; + json[r'sharedLinks'] = this.sharedLinks; json[r'tags'] = this.tags; return json; } @@ -101,6 +107,7 @@ class UserPreferencesResponseDto { people: PeopleResponse.fromJson(json[r'people'])!, purchase: PurchaseResponse.fromJson(json[r'purchase'])!, ratings: RatingsResponse.fromJson(json[r'ratings'])!, + sharedLinks: SharedLinksResponse.fromJson(json[r'sharedLinks'])!, tags: TagsResponse.fromJson(json[r'tags'])!, ); } @@ -157,6 +164,7 @@ class UserPreferencesResponseDto { 'people', 'purchase', 'ratings', + 'sharedLinks', 'tags', }; } diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index 208dbf686078a..3e420df119a3a 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -21,6 +21,7 @@ class UserPreferencesUpdateDto { this.people, this.purchase, this.ratings, + this.sharedLinks, this.tags, }); @@ -88,6 +89,14 @@ class UserPreferencesUpdateDto { /// RatingsUpdate? ratings; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + SharedLinksUpdate? sharedLinks; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -106,6 +115,7 @@ class UserPreferencesUpdateDto { other.people == people && other.purchase == purchase && other.ratings == ratings && + other.sharedLinks == sharedLinks && other.tags == tags; @override @@ -119,10 +129,11 @@ class UserPreferencesUpdateDto { (people == null ? 0 : people!.hashCode) + (purchase == null ? 0 : purchase!.hashCode) + (ratings == null ? 0 : ratings!.hashCode) + + (sharedLinks == null ? 0 : sharedLinks!.hashCode) + (tags == null ? 0 : tags!.hashCode); @override - String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, tags=$tags]'; + String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; @@ -166,6 +177,11 @@ class UserPreferencesUpdateDto { } else { // json[r'ratings'] = null; } + if (this.sharedLinks != null) { + json[r'sharedLinks'] = this.sharedLinks; + } else { + // json[r'sharedLinks'] = null; + } if (this.tags != null) { json[r'tags'] = this.tags; } else { @@ -191,6 +207,7 @@ class UserPreferencesUpdateDto { people: PeopleUpdate.fromJson(json[r'people']), purchase: PurchaseUpdate.fromJson(json[r'purchase']), ratings: RatingsUpdate.fromJson(json[r'ratings']), + sharedLinks: SharedLinksUpdate.fromJson(json[r'sharedLinks']), tags: TagsUpdate.fromJson(json[r'tags']), ); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 94ef49f12e7df..bee8dfcac80a7 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 46ce207883095..d5fea1ed79b06 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 8de7021eaf3c5..5a393a2d712d2 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 2c901426c33db..65c187883a0f0 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 021d5ca96fa80..950c26f3f523f 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 9c49b971bae0e..5493495fd3498 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 70f62475334da..15a4468c0ebd5 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 9a60f83647de5..c3868070c3720 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 b7ea2cfb524ab..db04efa5dbe44 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 0000000000000..436f3b47de534 --- /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 0000000000000..920e5bdba4792 --- /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 b7d4da2941812..0000000000000 --- 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 920e5bdba4792..59530fd83f894 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;