From 237734bb26033ed55ada080ce40ea8de0427746e Mon Sep 17 00:00:00 2001 From: Ben Beckford Date: Wed, 1 Jul 2026 16:12:50 -0700 Subject: [PATCH] feat(web): recently added link in sidebar (#29039) * feat(web): recently added link in sidebar * chore(mobile): update openapi patches --- e2e/src/ui/mock-network/base-network.ts | 3 + e2e/src/ui/specs/asset-viewer/utils.ts | 1 + i18n/en.json | 1 + mobile/lib/utils/openapi_patching.dart | 1 + mobile/openapi/README.md | 2 + mobile/openapi/lib/api.dart | 2 + mobile/openapi/lib/api_client.dart | 4 + .../lib/model/recently_added_response.dart | 100 ++++++++++++++++ .../lib/model/recently_added_update.dart | 108 ++++++++++++++++++ .../model/user_preferences_response_dto.dart | 10 +- .../model/user_preferences_update_dto.dart | 18 ++- open-api/immich-openapi-specs.json | 28 +++++ packages/sdk/src/fetch-client.ts | 10 ++ server/src/dtos/user-preferences.dto.ts | 15 +++ server/src/types.ts | 3 + server/src/utils/preferences.ts | 3 + .../side-bar/UserSidebar.svelte | 9 ++ .../user-settings/FeatureSettings.svelte | 12 ++ .../factories/preferences-factory.ts | 3 + 19 files changed, 331 insertions(+), 2 deletions(-) create mode 100644 mobile/openapi/lib/model/recently_added_response.dart create mode 100644 mobile/openapi/lib/model/recently_added_update.dart diff --git a/e2e/src/ui/mock-network/base-network.ts b/e2e/src/ui/mock-network/base-network.ts index 6680b83dd1..af8d1dbfef 100644 --- a/e2e/src/ui/mock-network/base-network.ts +++ b/e2e/src/ui/mock-network/base-network.ts @@ -82,6 +82,9 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI cast: { gCastEnabled: false, }, + recentlyAdded: { + sidebarWeb: false, + }, }, }); }); diff --git a/e2e/src/ui/specs/asset-viewer/utils.ts b/e2e/src/ui/specs/asset-viewer/utils.ts index adaace8a34..8cc2ad711a 100644 --- a/e2e/src/ui/specs/asset-viewer/utils.ts +++ b/e2e/src/ui/specs/asset-viewer/utils.ts @@ -110,6 +110,7 @@ export async function enableTagsPreference(context: BrowserContext) { download: { archiveSize: 4_294_967_296, includeEmbeddedVideos: false }, purchase: { showSupportBadge: true, hideBuyButtonUntil: '2100-02-12T00:00:00.000Z' }, cast: { gCastEnabled: false }, + recentlyAdded: { sidebarWeb: false }, }, }); }); diff --git a/i18n/en.json b/i18n/en.json index d392c88ef6..45951c6ffe 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1718,6 +1718,7 @@ "recent_searches": "Recent searches", "recently_added": "Recently added", "recently_added_body": "Jump straight to everything you've added lately on a dedicated page.", + "recently_added_description": "Browse your assets sorted by when they were uploaded to Immich", "recently_added_page_title": "Recently Added", "recently_added_title": "Recently added", "recently_taken": "Recently taken", diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index eca190ce8b..711a4a3163 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -26,6 +26,7 @@ final Map> openApiPatches = { 'sharedLinks': SharedLinksResponse(enabled: true, sidebarWeb: false).toJson(), 'cast': CastResponse(gCastEnabled: false).toJson(), 'albums': {'defaultAssetOrder': 'desc'}, + 'recentlyAdded': RecentlyAddedResponse(sidebarWeb: false).toJson(), }, 'ServerConfigDto': { 'mapLightStyleUrl': 'https://tiles.immich.cloud/v1/style/light.json', diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index f220b6e97f..f057b53c5a 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -545,6 +545,8 @@ Class | Method | HTTP request | Description - [RatingsUpdate](doc//RatingsUpdate.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) + - [RecentlyAddedResponse](doc//RecentlyAddedResponse.md) + - [RecentlyAddedUpdate](doc//RecentlyAddedUpdate.md) - [ReleaseChannel](doc//ReleaseChannel.md) - [ReleaseEventV1](doc//ReleaseEventV1.md) - [ReleaseType](doc//ReleaseType.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 413a64040c..4a20fe79a6 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -266,6 +266,8 @@ part 'model/ratings_response.dart'; part 'model/ratings_update.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; +part 'model/recently_added_response.dart'; +part 'model/recently_added_update.dart'; part 'model/release_channel.dart'; part 'model/release_event_v1.dart'; part 'model/release_type.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 90bff45431..137b0a47c5 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -577,6 +577,10 @@ class ApiClient { return ReactionLevelTypeTransformer().decode(value); case 'ReactionType': return ReactionTypeTypeTransformer().decode(value); + case 'RecentlyAddedResponse': + return RecentlyAddedResponse.fromJson(value); + case 'RecentlyAddedUpdate': + return RecentlyAddedUpdate.fromJson(value); case 'ReleaseChannel': return ReleaseChannelTypeTransformer().decode(value); case 'ReleaseEventV1': diff --git a/mobile/openapi/lib/model/recently_added_response.dart b/mobile/openapi/lib/model/recently_added_response.dart new file mode 100644 index 0000000000..11b46f0d0d --- /dev/null +++ b/mobile/openapi/lib/model/recently_added_response.dart @@ -0,0 +1,100 @@ +// +// 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 RecentlyAddedResponse { + /// Returns a new [RecentlyAddedResponse] instance. + RecentlyAddedResponse({ + required this.sidebarWeb, + }); + + /// Whether the recently added page appears in the web sidebar + bool sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is RecentlyAddedResponse && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (sidebarWeb.hashCode); + + @override + String toString() => 'RecentlyAddedResponse[sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + json[r'sidebarWeb'] = this.sidebarWeb; + return json; + } + + /// Returns a new [RecentlyAddedResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static RecentlyAddedResponse? fromJson(dynamic value) { + upgradeDto(value, "RecentlyAddedResponse"); + if (value is Map) { + final json = value.cast(); + + return RecentlyAddedResponse( + 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 = RecentlyAddedResponse.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 = RecentlyAddedResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of RecentlyAddedResponse-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] = RecentlyAddedResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'sidebarWeb', + }; +} + diff --git a/mobile/openapi/lib/model/recently_added_update.dart b/mobile/openapi/lib/model/recently_added_update.dart new file mode 100644 index 0000000000..48714dd19d --- /dev/null +++ b/mobile/openapi/lib/model/recently_added_update.dart @@ -0,0 +1,108 @@ +// +// 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 RecentlyAddedUpdate { + /// Returns a new [RecentlyAddedUpdate] instance. + RecentlyAddedUpdate({ + this.sidebarWeb = const Optional.absent(), + }); + + /// Whether the recently added page appears in the web sidebar + /// + /// 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. + /// + Optional sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is RecentlyAddedUpdate && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (sidebarWeb == null ? 0 : sidebarWeb!.hashCode); + + @override + String toString() => 'RecentlyAddedUpdate[sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + if (this.sidebarWeb.isPresent) { + final value = this.sidebarWeb.value; + json[r'sidebarWeb'] = value; + } + return json; + } + + /// Returns a new [RecentlyAddedUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static RecentlyAddedUpdate? fromJson(dynamic value) { + upgradeDto(value, "RecentlyAddedUpdate"); + if (value is Map) { + final json = value.cast(); + + return RecentlyAddedUpdate( + sidebarWeb: json.containsKey(r'sidebarWeb') ? Optional.present(mapValueOfType(json, r'sidebarWeb')) : const Optional.absent(), + ); + } + 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 = RecentlyAddedUpdate.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 = RecentlyAddedUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of RecentlyAddedUpdate-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] = RecentlyAddedUpdate.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 7a6e0252af..25de4fd985 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -22,6 +22,7 @@ class UserPreferencesResponseDto { required this.people, required this.purchase, required this.ratings, + required this.recentlyAdded, required this.sharedLinks, required this.tags, }); @@ -44,6 +45,8 @@ class UserPreferencesResponseDto { RatingsResponse ratings; + RecentlyAddedResponse recentlyAdded; + SharedLinksResponse sharedLinks; TagsResponse tags; @@ -59,6 +62,7 @@ class UserPreferencesResponseDto { other.people == people && other.purchase == purchase && other.ratings == ratings && + other.recentlyAdded == recentlyAdded && other.sharedLinks == sharedLinks && other.tags == tags; @@ -74,11 +78,12 @@ class UserPreferencesResponseDto { (people.hashCode) + (purchase.hashCode) + (ratings.hashCode) + + (recentlyAdded.hashCode) + (sharedLinks.hashCode) + (tags.hashCode); @override - String toString() => 'UserPreferencesResponseDto[albums=$albums, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesResponseDto[albums=$albums, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, recentlyAdded=$recentlyAdded, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; @@ -91,6 +96,7 @@ class UserPreferencesResponseDto { json[r'people'] = this.people; json[r'purchase'] = this.purchase; json[r'ratings'] = this.ratings; + json[r'recentlyAdded'] = this.recentlyAdded; json[r'sharedLinks'] = this.sharedLinks; json[r'tags'] = this.tags; return json; @@ -114,6 +120,7 @@ class UserPreferencesResponseDto { people: PeopleResponse.fromJson(json[r'people'])!, purchase: PurchaseResponse.fromJson(json[r'purchase'])!, ratings: RatingsResponse.fromJson(json[r'ratings'])!, + recentlyAdded: RecentlyAddedResponse.fromJson(json[r'recentlyAdded'])!, sharedLinks: SharedLinksResponse.fromJson(json[r'sharedLinks'])!, tags: TagsResponse.fromJson(json[r'tags'])!, ); @@ -172,6 +179,7 @@ class UserPreferencesResponseDto { 'people', 'purchase', 'ratings', + 'recentlyAdded', '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 f3f2d85bdf..b1dbd95ae5 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -23,6 +23,7 @@ class UserPreferencesUpdateDto { this.people = const Optional.absent(), this.purchase = const Optional.absent(), this.ratings = const Optional.absent(), + this.recentlyAdded = const Optional.absent(), this.sharedLinks = const Optional.absent(), this.tags = const Optional.absent(), }); @@ -107,6 +108,14 @@ class UserPreferencesUpdateDto { /// Optional 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. + /// + Optional recentlyAdded; + /// /// 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 @@ -135,6 +144,7 @@ class UserPreferencesUpdateDto { other.people == people && other.purchase == purchase && other.ratings == ratings && + other.recentlyAdded == recentlyAdded && other.sharedLinks == sharedLinks && other.tags == tags; @@ -151,11 +161,12 @@ class UserPreferencesUpdateDto { (people == null ? 0 : people!.hashCode) + (purchase == null ? 0 : purchase!.hashCode) + (ratings == null ? 0 : ratings!.hashCode) + + (recentlyAdded == null ? 0 : recentlyAdded!.hashCode) + (sharedLinks == null ? 0 : sharedLinks!.hashCode) + (tags == null ? 0 : tags!.hashCode); @override - String toString() => 'UserPreferencesUpdateDto[albums=$albums, avatar=$avatar, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, sharedLinks=$sharedLinks, tags=$tags]'; + String toString() => 'UserPreferencesUpdateDto[albums=$albums, avatar=$avatar, cast=$cast, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, recentlyAdded=$recentlyAdded, sharedLinks=$sharedLinks, tags=$tags]'; Map toJson() { final json = {}; @@ -199,6 +210,10 @@ class UserPreferencesUpdateDto { final value = this.ratings.value; json[r'ratings'] = value; } + if (this.recentlyAdded.isPresent) { + final value = this.recentlyAdded.value; + json[r'recentlyAdded'] = value; + } if (this.sharedLinks.isPresent) { final value = this.sharedLinks.value; json[r'sharedLinks'] = value; @@ -229,6 +244,7 @@ class UserPreferencesUpdateDto { people: json.containsKey(r'people') ? Optional.present(PeopleUpdate.fromJson(json[r'people'])) : const Optional.absent(), purchase: json.containsKey(r'purchase') ? Optional.present(PurchaseUpdate.fromJson(json[r'purchase'])) : const Optional.absent(), ratings: json.containsKey(r'ratings') ? Optional.present(RatingsUpdate.fromJson(json[r'ratings'])) : const Optional.absent(), + recentlyAdded: json.containsKey(r'recentlyAdded') ? Optional.present(RecentlyAddedUpdate.fromJson(json[r'recentlyAdded'])) : const Optional.absent(), sharedLinks: json.containsKey(r'sharedLinks') ? Optional.present(SharedLinksUpdate.fromJson(json[r'sharedLinks'])) : const Optional.absent(), tags: json.containsKey(r'tags') ? Optional.present(TagsUpdate.fromJson(json[r'tags'])) : const Optional.absent(), ); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 873a7eb161..a138f7db8c 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -21991,6 +21991,27 @@ ], "type": "string" }, + "RecentlyAddedResponse": { + "properties": { + "sidebarWeb": { + "description": "Whether the recently added page appears in the web sidebar", + "type": "boolean" + } + }, + "required": [ + "sidebarWeb" + ], + "type": "object" + }, + "RecentlyAddedUpdate": { + "properties": { + "sidebarWeb": { + "description": "Whether the recently added page appears in the web sidebar", + "type": "boolean" + } + }, + "type": "object" + }, "ReleaseChannel": { "description": "Release channel", "enum": [ @@ -27525,6 +27546,9 @@ "ratings": { "$ref": "#/components/schemas/RatingsResponse" }, + "recentlyAdded": { + "$ref": "#/components/schemas/RecentlyAddedResponse" + }, "sharedLinks": { "$ref": "#/components/schemas/SharedLinksResponse" }, @@ -27542,6 +27566,7 @@ "people", "purchase", "ratings", + "recentlyAdded", "sharedLinks", "tags" ], @@ -27579,6 +27604,9 @@ "ratings": { "$ref": "#/components/schemas/RatingsUpdate" }, + "recentlyAdded": { + "$ref": "#/components/schemas/RecentlyAddedUpdate" + }, "sharedLinks": { "$ref": "#/components/schemas/SharedLinksUpdate" }, diff --git a/packages/sdk/src/fetch-client.ts b/packages/sdk/src/fetch-client.ts index 9483d18869..a168b4fb23 100644 --- a/packages/sdk/src/fetch-client.ts +++ b/packages/sdk/src/fetch-client.ts @@ -342,6 +342,10 @@ export type RatingsResponse = { /** Whether ratings are enabled */ enabled: boolean; }; +export type RecentlyAddedResponse = { + /** Whether the recently added page appears in the web sidebar */ + sidebarWeb: boolean; +}; export type SharedLinksResponse = { /** Whether shared links are enabled */ enabled: boolean; @@ -364,6 +368,7 @@ export type UserPreferencesResponseDto = { people: PeopleResponse; purchase: PurchaseResponse; ratings: RatingsResponse; + recentlyAdded: RecentlyAddedResponse; sharedLinks: SharedLinksResponse; tags: TagsResponse; }; @@ -421,6 +426,10 @@ export type RatingsUpdate = { /** Whether ratings are enabled */ enabled?: boolean; }; +export type RecentlyAddedUpdate = { + /** Whether the recently added page appears in the web sidebar */ + sidebarWeb?: boolean; +}; export type SharedLinksUpdate = { /** Whether shared links are enabled */ enabled?: boolean; @@ -444,6 +453,7 @@ export type UserPreferencesUpdateDto = { people?: PeopleUpdate; purchase?: PurchaseUpdate; ratings?: RatingsUpdate; + recentlyAdded?: RecentlyAddedUpdate; sharedLinks?: SharedLinksUpdate; tags?: TagsUpdate; }; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index b8894e4d51..adaa37d1ac 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -98,6 +98,13 @@ const CastUpdateSchema = z .optional() .meta({ id: 'CastUpdate' }); +const RecentlyAddedUpdateSchema = z + .object({ + sidebarWeb: z.boolean().optional().describe('Whether the recently added page appears in the web sidebar'), + }) + .optional() + .meta({ id: 'RecentlyAddedUpdate' }); + const UserPreferencesUpdateSchema = z .object({ albums: AlbumsUpdateSchema, @@ -112,6 +119,7 @@ const UserPreferencesUpdateSchema = z ratings: RatingsUpdateSchema, sharedLinks: SharedLinksUpdateSchema, tags: TagsUpdateSchema, + recentlyAdded: RecentlyAddedUpdateSchema, }) .meta({ id: 'UserPreferencesUpdateDto' }); @@ -191,6 +199,12 @@ const CastResponseSchema = z }) .meta({ id: 'CastResponse' }); +const RecentlyAddedResponseSchema = z + .object({ + sidebarWeb: z.boolean().describe('Whether the recently added page appears in the web sidebar'), + }) + .meta({ id: 'RecentlyAddedResponse' }); + const UserPreferencesResponseSchema = z .object({ albums: AlbumsResponseSchema, @@ -204,6 +218,7 @@ const UserPreferencesResponseSchema = z download: DownloadResponseSchema, purchase: PurchaseResponseSchema, cast: CastResponseSchema, + recentlyAdded: RecentlyAddedResponseSchema, }) .meta({ id: 'UserPreferencesResponseDto' }); diff --git a/server/src/types.ts b/server/src/types.ts index 598d900e70..55fdf27f7b 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -619,6 +619,9 @@ export type UserPreferences = { cast: { gCastEnabled: boolean; }; + recentlyAdded: { + sidebarWeb: boolean; + }; }; export type UserMetadataItem = { diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index 6b67398d23..bfc17b291c 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -50,6 +50,9 @@ const getDefaultPreferences = (): UserPreferences => { cast: { gCastEnabled: false, }, + recentlyAdded: { + sidebarWeb: false, + }, }; }; diff --git a/web/src/lib/components/shared-components/side-bar/UserSidebar.svelte b/web/src/lib/components/shared-components/side-bar/UserSidebar.svelte index 8c142d68b1..a01b4533fc 100644 --- a/web/src/lib/components/shared-components/side-bar/UserSidebar.svelte +++ b/web/src/lib/components/shared-components/side-bar/UserSidebar.svelte @@ -31,6 +31,7 @@ mdiToolboxOutline, mdiTrashCan, mdiTrashCanOutline, + mdiUploadOutline, } from '@mdi/js'; import { t } from 'svelte-i18n'; import { fly } from 'svelte/transition'; @@ -83,6 +84,14 @@ {/if} + {#if authManager.preferences.recentlyAdded.sidebarWeb} + + {/if} + {#if authManager.preferences.folders.enabled && authManager.preferences.folders.sidebarWeb} {/if} diff --git a/web/src/routes/(user)/user-settings/FeatureSettings.svelte b/web/src/routes/(user)/user-settings/FeatureSettings.svelte index 8077ba190b..5761488fb1 100644 --- a/web/src/routes/(user)/user-settings/FeatureSettings.svelte +++ b/web/src/routes/(user)/user-settings/FeatureSettings.svelte @@ -38,6 +38,9 @@ // Cast let gCastEnabled = $state(authManager.preferences.cast?.gCastEnabled ?? false); + // Recently added + let recentlyAddedSidebar = $state(authManager.preferences.recentlyAdded?.sidebarWeb ?? false); + const handleSave = async () => { try { const response = await updateMyPreferences({ @@ -50,6 +53,7 @@ sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar }, tags: { enabled: tagsEnabled, sidebarWeb: tagsSidebar }, cast: { gCastEnabled }, + recentlyAdded: { sidebarWeb: recentlyAddedSidebar }, }, }); @@ -170,6 +174,14 @@ + +
+ + + +
+
+
diff --git a/web/src/test-data/factories/preferences-factory.ts b/web/src/test-data/factories/preferences-factory.ts index f8ab8615bf..43ce044b12 100644 --- a/web/src/test-data/factories/preferences-factory.ts +++ b/web/src/test-data/factories/preferences-factory.ts @@ -44,4 +44,7 @@ export const preferencesFactory = Sync.makeFactory({ enabled: false, sidebarWeb: false, }, + recentlyAdded: { + sidebarWeb: false, + }, });