From 639b47a9fc9492397b688a84c3017e658997d63b Mon Sep 17 00:00:00 2001 From: Mees Frensel Date: Wed, 8 Oct 2025 22:37:34 +0200 Subject: [PATCH] feat: make memories slideshow duration configurable --- i18n/en.json | 1 + .../openapi/lib/model/memories_response.dart | 10 +++++++++- mobile/openapi/lib/model/memories_update.dart | 20 ++++++++++++++++++- open-api/immich-openapi-specs.json | 9 +++++++++ open-api/typescript-sdk/src/fetch-client.ts | 2 ++ server/src/dtos/user-preferences.dto.ts | 9 +++++++++ server/src/types.ts | 1 + server/src/utils/preferences.ts | 1 + .../memory-page/memory-viewer.svelte | 2 +- .../feature-settings.svelte | 13 +++++++++++- web/src/lib/stores/slideshow.store.ts | 5 +++-- .../factories/preferences-factory.ts | 1 + 12 files changed, 68 insertions(+), 6 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index bd59c37fac..0b32d80b6d 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1986,6 +1986,7 @@ "they_will_be_merged_together": "They will be merged together", "third_party_resources": "Third-Party Resources", "time_based_memories": "Time-based memories", + "time_based_memories_duration": "Number of seconds to display each image. This setting also affects the default slideshow duration.", "timeline": "Timeline", "timezone": "Timezone", "to_archive": "Archive", diff --git a/mobile/openapi/lib/model/memories_response.dart b/mobile/openapi/lib/model/memories_response.dart index b9f8b5d8b1..cb42f596a6 100644 --- a/mobile/openapi/lib/model/memories_response.dart +++ b/mobile/openapi/lib/model/memories_response.dart @@ -13,25 +13,31 @@ part of openapi.api; class MemoriesResponse { /// Returns a new [MemoriesResponse] instance. MemoriesResponse({ + this.duration = 5, this.enabled = true, }); + int duration; + bool enabled; @override bool operator ==(Object other) => identical(this, other) || other is MemoriesResponse && + other.duration == duration && other.enabled == enabled; @override int get hashCode => // ignore: unnecessary_parenthesis + (duration.hashCode) + (enabled.hashCode); @override - String toString() => 'MemoriesResponse[enabled=$enabled]'; + String toString() => 'MemoriesResponse[duration=$duration, enabled=$enabled]'; Map toJson() { final json = {}; + json[r'duration'] = this.duration; json[r'enabled'] = this.enabled; return json; } @@ -45,6 +51,7 @@ class MemoriesResponse { final json = value.cast(); return MemoriesResponse( + duration: mapValueOfType(json, r'duration')!, enabled: mapValueOfType(json, r'enabled')!, ); } @@ -93,6 +100,7 @@ class MemoriesResponse { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'duration', 'enabled', }; } diff --git a/mobile/openapi/lib/model/memories_update.dart b/mobile/openapi/lib/model/memories_update.dart index 71efd71ae7..39c46ffd2f 100644 --- a/mobile/openapi/lib/model/memories_update.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -13,9 +13,19 @@ part of openapi.api; class MemoriesUpdate { /// Returns a new [MemoriesUpdate] instance. MemoriesUpdate({ + this.duration, this.enabled, }); + /// Minimum value: 1 + /// + /// 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. + /// + int? duration; + /// /// 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 @@ -26,18 +36,25 @@ class MemoriesUpdate { @override bool operator ==(Object other) => identical(this, other) || other is MemoriesUpdate && + other.duration == duration && other.enabled == enabled; @override int get hashCode => // ignore: unnecessary_parenthesis + (duration == null ? 0 : duration!.hashCode) + (enabled == null ? 0 : enabled!.hashCode); @override - String toString() => 'MemoriesUpdate[enabled=$enabled]'; + String toString() => 'MemoriesUpdate[duration=$duration, enabled=$enabled]'; Map toJson() { final json = {}; + if (this.duration != null) { + json[r'duration'] = this.duration; + } else { + // json[r'duration'] = null; + } if (this.enabled != null) { json[r'enabled'] = this.enabled; } else { @@ -55,6 +72,7 @@ class MemoriesUpdate { final json = value.cast(); return MemoriesUpdate( + duration: mapValueOfType(json, r'duration'), enabled: mapValueOfType(json, r'enabled'), ); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b574bc6624..8183139a1f 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -12349,18 +12349,27 @@ }, "MemoriesResponse": { "properties": { + "duration": { + "default": 5, + "type": "integer" + }, "enabled": { "default": true, "type": "boolean" } }, "required": [ + "duration", "enabled" ], "type": "object" }, "MemoriesUpdate": { "properties": { + "duration": { + "minimum": 1, + "type": "integer" + }, "enabled": { "type": "boolean" } diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index c8a69dfe8c..3665bbf6b8 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -151,6 +151,7 @@ export type FoldersResponse = { sidebarWeb: boolean; }; export type MemoriesResponse = { + duration: number; enabled: boolean; }; export type PeopleResponse = { @@ -208,6 +209,7 @@ export type FoldersUpdate = { sidebarWeb?: boolean; }; export type MemoriesUpdate = { + duration?: number; enabled?: boolean; }; export type PeopleUpdate = { diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index b258158ae2..452384b423 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -13,6 +13,12 @@ class AvatarUpdate { class MemoriesUpdate { @ValidateBoolean({ optional: true }) enabled?: boolean; + + @Optional() + @IsInt() + @IsPositive() + @ApiProperty({ type: 'integer' }) + duration?: number; } class RatingsUpdate { @@ -166,6 +172,9 @@ class RatingsResponse { class MemoriesResponse { enabled: boolean = true; + + @ApiProperty({ type: 'integer' }) + duration: number = 5; } class FoldersResponse { diff --git a/server/src/types.ts b/server/src/types.ts index da3889ef7c..d2b4c19ab6 100644 --- a/server/src/types.ts +++ b/server/src/types.ts @@ -493,6 +493,7 @@ export interface UserPreferences { }; memories: { enabled: boolean; + duration: number; }; people: { enabled: boolean; diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index 121bf2826d..b25369670a 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -16,6 +16,7 @@ const getDefaultPreferences = (): UserPreferences => { }, memories: { enabled: true, + duration: 5, }, people: { enabled: true, diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 7f566c41a2..7a315cb9b5 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -106,7 +106,7 @@ }); } else { progressBarController = new Tween(0, { - duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0), + duration: (from: number, to: number) => (to ? $preferences.memories.duration * 1000 * (to - from) : 0), }); } }; 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 18174e2749..46928972fb 100644 --- a/web/src/lib/components/user-settings-page/feature-settings.svelte +++ b/web/src/lib/components/user-settings-page/feature-settings.svelte @@ -4,8 +4,10 @@ NotificationType, } from '$lib/components/shared-components/notification/notification'; import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; + import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte'; import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; + import { SettingInputFieldType } from '$lib/constants'; import { preferences } from '$lib/stores/user.store'; import { AssetOrder, updateMyPreferences } from '@immich/sdk'; import { Button } from '@immich/ui'; @@ -22,6 +24,7 @@ // Memories let memoriesEnabled = $state($preferences?.memories?.enabled ?? true); + let memoriesDuration = $state($preferences?.memories?.duration ?? 5); // People let peopleEnabled = $state($preferences?.people?.enabled ?? false); @@ -47,7 +50,7 @@ userPreferencesUpdateDto: { albums: { defaultAssetOrder }, folders: { enabled: foldersEnabled, sidebarWeb: foldersSidebar }, - memories: { enabled: memoriesEnabled }, + memories: { enabled: memoriesEnabled, duration: memoriesDuration }, people: { enabled: peopleEnabled, sidebarWeb: peopleSidebar }, ratings: { enabled: ratingsEnabled }, sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar }, @@ -107,6 +110,14 @@
+
+ +
diff --git a/web/src/lib/stores/slideshow.store.ts b/web/src/lib/stores/slideshow.store.ts index 48639f4669..ba5feb7ae9 100644 --- a/web/src/lib/stores/slideshow.store.ts +++ b/web/src/lib/stores/slideshow.store.ts @@ -1,5 +1,6 @@ +import { preferences } from '$lib/stores/user.store'; import { persisted } from 'svelte-persisted-store'; -import { writable } from 'svelte/store'; +import { get, writable } from 'svelte/store'; export enum SlideshowState { PlaySlideshow = 'play-slideshow', @@ -37,7 +38,7 @@ function createSlideshowStore() { const slideshowState = writable(SlideshowState.None); const showProgressBar = persisted('slideshow-show-progressbar', true); - const slideshowDelay = persisted('slideshow-delay', 5, {}); + const slideshowDelay = persisted('slideshow-delay', get(preferences)?.memories.duration ?? 5, {}); const slideshowTransition = persisted('slideshow-transition', true); const slideshowAutoplay = persisted('slideshow-autoplay', true, {}); diff --git a/web/src/test-data/factories/preferences-factory.ts b/web/src/test-data/factories/preferences-factory.ts index e7d556b00b..f8ab8615bf 100644 --- a/web/src/test-data/factories/preferences-factory.ts +++ b/web/src/test-data/factories/preferences-factory.ts @@ -23,6 +23,7 @@ export const preferencesFactory = Sync.makeFactory({ }, memories: { enabled: false, + duration: 5, }, people: { enabled: false,