feat: make memories slideshow duration configurable

This commit is contained in:
Mees Frensel 2025-10-08 22:37:34 +02:00
parent 53680d9643
commit 639b47a9fc
12 changed files with 68 additions and 6 deletions

View file

@ -1986,6 +1986,7 @@
"they_will_be_merged_together": "They will be merged together", "they_will_be_merged_together": "They will be merged together",
"third_party_resources": "Third-Party Resources", "third_party_resources": "Third-Party Resources",
"time_based_memories": "Time-based memories", "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", "timeline": "Timeline",
"timezone": "Timezone", "timezone": "Timezone",
"to_archive": "Archive", "to_archive": "Archive",

View file

@ -13,25 +13,31 @@ part of openapi.api;
class MemoriesResponse { class MemoriesResponse {
/// Returns a new [MemoriesResponse] instance. /// Returns a new [MemoriesResponse] instance.
MemoriesResponse({ MemoriesResponse({
this.duration = 5,
this.enabled = true, this.enabled = true,
}); });
int duration;
bool enabled; bool enabled;
@override @override
bool operator ==(Object other) => identical(this, other) || other is MemoriesResponse && bool operator ==(Object other) => identical(this, other) || other is MemoriesResponse &&
other.duration == duration &&
other.enabled == enabled; other.enabled == enabled;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(duration.hashCode) +
(enabled.hashCode); (enabled.hashCode);
@override @override
String toString() => 'MemoriesResponse[enabled=$enabled]'; String toString() => 'MemoriesResponse[duration=$duration, enabled=$enabled]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
json[r'duration'] = this.duration;
json[r'enabled'] = this.enabled; json[r'enabled'] = this.enabled;
return json; return json;
} }
@ -45,6 +51,7 @@ class MemoriesResponse {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return MemoriesResponse( return MemoriesResponse(
duration: mapValueOfType<int>(json, r'duration')!,
enabled: mapValueOfType<bool>(json, r'enabled')!, enabled: mapValueOfType<bool>(json, r'enabled')!,
); );
} }
@ -93,6 +100,7 @@ class MemoriesResponse {
/// The list of required keys that must be present in a JSON. /// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{ static const requiredKeys = <String>{
'duration',
'enabled', 'enabled',
}; };
} }

View file

@ -13,9 +13,19 @@ part of openapi.api;
class MemoriesUpdate { class MemoriesUpdate {
/// Returns a new [MemoriesUpdate] instance. /// Returns a new [MemoriesUpdate] instance.
MemoriesUpdate({ MemoriesUpdate({
this.duration,
this.enabled, 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 /// 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 /// does not include a default value (using the "default:" property), however, the generated
@ -26,18 +36,25 @@ class MemoriesUpdate {
@override @override
bool operator ==(Object other) => identical(this, other) || other is MemoriesUpdate && bool operator ==(Object other) => identical(this, other) || other is MemoriesUpdate &&
other.duration == duration &&
other.enabled == enabled; other.enabled == enabled;
@override @override
int get hashCode => int get hashCode =>
// ignore: unnecessary_parenthesis // ignore: unnecessary_parenthesis
(duration == null ? 0 : duration!.hashCode) +
(enabled == null ? 0 : enabled!.hashCode); (enabled == null ? 0 : enabled!.hashCode);
@override @override
String toString() => 'MemoriesUpdate[enabled=$enabled]'; String toString() => 'MemoriesUpdate[duration=$duration, enabled=$enabled]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
if (this.duration != null) {
json[r'duration'] = this.duration;
} else {
// json[r'duration'] = null;
}
if (this.enabled != null) { if (this.enabled != null) {
json[r'enabled'] = this.enabled; json[r'enabled'] = this.enabled;
} else { } else {
@ -55,6 +72,7 @@ class MemoriesUpdate {
final json = value.cast<String, dynamic>(); final json = value.cast<String, dynamic>();
return MemoriesUpdate( return MemoriesUpdate(
duration: mapValueOfType<int>(json, r'duration'),
enabled: mapValueOfType<bool>(json, r'enabled'), enabled: mapValueOfType<bool>(json, r'enabled'),
); );
} }

View file

@ -12349,18 +12349,27 @@
}, },
"MemoriesResponse": { "MemoriesResponse": {
"properties": { "properties": {
"duration": {
"default": 5,
"type": "integer"
},
"enabled": { "enabled": {
"default": true, "default": true,
"type": "boolean" "type": "boolean"
} }
}, },
"required": [ "required": [
"duration",
"enabled" "enabled"
], ],
"type": "object" "type": "object"
}, },
"MemoriesUpdate": { "MemoriesUpdate": {
"properties": { "properties": {
"duration": {
"minimum": 1,
"type": "integer"
},
"enabled": { "enabled": {
"type": "boolean" "type": "boolean"
} }

View file

@ -151,6 +151,7 @@ export type FoldersResponse = {
sidebarWeb: boolean; sidebarWeb: boolean;
}; };
export type MemoriesResponse = { export type MemoriesResponse = {
duration: number;
enabled: boolean; enabled: boolean;
}; };
export type PeopleResponse = { export type PeopleResponse = {
@ -208,6 +209,7 @@ export type FoldersUpdate = {
sidebarWeb?: boolean; sidebarWeb?: boolean;
}; };
export type MemoriesUpdate = { export type MemoriesUpdate = {
duration?: number;
enabled?: boolean; enabled?: boolean;
}; };
export type PeopleUpdate = { export type PeopleUpdate = {

View file

@ -13,6 +13,12 @@ class AvatarUpdate {
class MemoriesUpdate { class MemoriesUpdate {
@ValidateBoolean({ optional: true }) @ValidateBoolean({ optional: true })
enabled?: boolean; enabled?: boolean;
@Optional()
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
duration?: number;
} }
class RatingsUpdate { class RatingsUpdate {
@ -166,6 +172,9 @@ class RatingsResponse {
class MemoriesResponse { class MemoriesResponse {
enabled: boolean = true; enabled: boolean = true;
@ApiProperty({ type: 'integer' })
duration: number = 5;
} }
class FoldersResponse { class FoldersResponse {

View file

@ -493,6 +493,7 @@ export interface UserPreferences {
}; };
memories: { memories: {
enabled: boolean; enabled: boolean;
duration: number;
}; };
people: { people: {
enabled: boolean; enabled: boolean;

View file

@ -16,6 +16,7 @@ const getDefaultPreferences = (): UserPreferences => {
}, },
memories: { memories: {
enabled: true, enabled: true,
duration: 5,
}, },
people: { people: {
enabled: true, enabled: true,

View file

@ -106,7 +106,7 @@
}); });
} else { } else {
progressBarController = new Tween<number>(0, { progressBarController = new Tween<number>(0, {
duration: (from: number, to: number) => (to ? 5000 * (to - from) : 0), duration: (from: number, to: number) => (to ? $preferences.memories.duration * 1000 * (to - from) : 0),
}); });
} }
}; };

View file

@ -4,8 +4,10 @@
NotificationType, NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; 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 SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.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 { preferences } from '$lib/stores/user.store';
import { AssetOrder, updateMyPreferences } from '@immich/sdk'; import { AssetOrder, updateMyPreferences } from '@immich/sdk';
import { Button } from '@immich/ui'; import { Button } from '@immich/ui';
@ -22,6 +24,7 @@
// Memories // Memories
let memoriesEnabled = $state($preferences?.memories?.enabled ?? true); let memoriesEnabled = $state($preferences?.memories?.enabled ?? true);
let memoriesDuration = $state($preferences?.memories?.duration ?? 5);
// People // People
let peopleEnabled = $state($preferences?.people?.enabled ?? false); let peopleEnabled = $state($preferences?.people?.enabled ?? false);
@ -47,7 +50,7 @@
userPreferencesUpdateDto: { userPreferencesUpdateDto: {
albums: { defaultAssetOrder }, albums: { defaultAssetOrder },
folders: { enabled: foldersEnabled, sidebarWeb: foldersSidebar }, folders: { enabled: foldersEnabled, sidebarWeb: foldersSidebar },
memories: { enabled: memoriesEnabled }, memories: { enabled: memoriesEnabled, duration: memoriesDuration },
people: { enabled: peopleEnabled, sidebarWeb: peopleSidebar }, people: { enabled: peopleEnabled, sidebarWeb: peopleSidebar },
ratings: { enabled: ratingsEnabled }, ratings: { enabled: ratingsEnabled },
sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar }, sharedLinks: { enabled: sharedLinksEnabled, sidebarWeb: sharedLinkSidebar },
@ -107,6 +110,14 @@
<div class="ms-4 mt-6"> <div class="ms-4 mt-6">
<SettingSwitch title={$t('enable')} bind:checked={memoriesEnabled} /> <SettingSwitch title={$t('enable')} bind:checked={memoriesEnabled} />
</div> </div>
<div class="ms-4 mt-6">
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('duration')}
description={$t('time_based_memories_duration')}
bind:value={memoriesDuration}
/>
</div>
</SettingAccordion> </SettingAccordion>
<SettingAccordion key="people" title={$t('people')} subtitle={$t('people_feature_description')}> <SettingAccordion key="people" title={$t('people')} subtitle={$t('people_feature_description')}>

View file

@ -1,5 +1,6 @@
import { preferences } from '$lib/stores/user.store';
import { persisted } from 'svelte-persisted-store'; import { persisted } from 'svelte-persisted-store';
import { writable } from 'svelte/store'; import { get, writable } from 'svelte/store';
export enum SlideshowState { export enum SlideshowState {
PlaySlideshow = 'play-slideshow', PlaySlideshow = 'play-slideshow',
@ -37,7 +38,7 @@ function createSlideshowStore() {
const slideshowState = writable<SlideshowState>(SlideshowState.None); const slideshowState = writable<SlideshowState>(SlideshowState.None);
const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true); const showProgressBar = persisted<boolean>('slideshow-show-progressbar', true);
const slideshowDelay = persisted<number>('slideshow-delay', 5, {}); const slideshowDelay = persisted<number>('slideshow-delay', get(preferences)?.memories.duration ?? 5, {});
const slideshowTransition = persisted<boolean>('slideshow-transition', true); const slideshowTransition = persisted<boolean>('slideshow-transition', true);
const slideshowAutoplay = persisted<boolean>('slideshow-autoplay', true, {}); const slideshowAutoplay = persisted<boolean>('slideshow-autoplay', true, {});

View file

@ -23,6 +23,7 @@ export const preferencesFactory = Sync.makeFactory<UserPreferencesResponseDto>({
}, },
memories: { memories: {
enabled: false, enabled: false,
duration: 5,
}, },
people: { people: {
enabled: false, enabled: false,