1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

feat: user's features preferences (#12099)

* feat: metadata in UserPreference

* feat: web metadata settings

* feat: web metadata settings

* fix: typo

* patch openapi

* fix: missing translation key

* new organization of preference strucutre

* feature settings on web

* localization

* added and used feature settings

* add default value to response dto

* patch openapi

* format en.json file

* implement helper method

* use tags preference logic

* Fix logic bug and add tests

* fix preference can be null in detail panel
This commit is contained in:
Alex 2024-08-29 14:29:04 -05:00 committed by GitHub
parent 9bfaa525db
commit ebecb60f39
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 519 additions and 186 deletions

View file

@ -4,14 +4,30 @@ dynamic upgradeDto(dynamic value, String targetType) {
switch (targetType) {
case 'UserPreferencesResponseDto':
if (value is Map) {
if (value['rating'] == null) {
value['rating'] = RatingResponse().toJson();
}
if (value['download']['includeEmbeddedVideos'] == null) {
value['download']['includeEmbeddedVideos'] = false;
}
addDefault(value, 'download.includeEmbeddedVideos', false);
addDefault(value, 'folders', FoldersResponse().toJson());
addDefault(value, 'memories', MemoriesResponse().toJson());
addDefault(value, 'ratings', RatingsResponse().toJson());
addDefault(value, 'people', PeopleResponse().toJson());
addDefault(value, 'tags', TagsResponse().toJson());
}
break;
}
}
addDefault(dynamic value, String keys, dynamic defaultValue) {
// Loop through the keys and assign the default value if the key is not present
List<String> keyList = keys.split('.');
dynamic current = value;
for (int i = 0; i < keyList.length - 1; i++) {
if (current[keyList[i]] == null) {
current[keyList[i]] = {};
}
current = current[keyList[i]];
}
if (current[keyList.last] == null) {
current[keyList.last] = defaultValue;
}
}

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/lib/model/tags_update.dart generated Normal file

Binary file not shown.

View file

@ -0,0 +1,49 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:openapi/api.dart';
import 'package:immich_mobile/utils/openapi_patching.dart';
void main() {
group('Test OpenApi Patching', () {
test('upgradeDto', () {
dynamic value;
String targetType;
targetType = 'UserPreferencesResponseDto';
value = jsonDecode("""
{
"download": {
"archiveSize": 4294967296,
"includeEmbeddedVideos": false
}
}
""");
upgradeDto(value, targetType);
expect(value['tags'], TagsResponse().toJson());
expect(value['download']['includeEmbeddedVideos'], false);
});
test('addDefault', () {
dynamic value = jsonDecode("""
{
"download": {
"archiveSize": 4294967296,
"includeEmbeddedVideos": false
}
}
""");
String keys = 'download.unknownKey';
dynamic defaultValue = 69420;
addDefault(value, keys, defaultValue);
expect(value['download']['unknownKey'], 69420);
keys = 'alpha.beta';
defaultValue = 'gamma';
addDefault(value, keys, defaultValue);
expect(value['alpha']['beta'], 'gamma');
});
});
}

View file

@ -9164,6 +9164,34 @@
],
"type": "object"
},
"FoldersResponse": {
"properties": {
"enabled": {
"default": false,
"type": "boolean"
},
"sidebarWeb": {
"default": false,
"type": "boolean"
}
},
"required": [
"enabled",
"sidebarWeb"
],
"type": "object"
},
"FoldersUpdate": {
"properties": {
"enabled": {
"type": "boolean"
},
"sidebarWeb": {
"type": "boolean"
}
},
"type": "object"
},
"ImageFormat": {
"enum": [
"jpeg",
@ -9534,6 +9562,26 @@
],
"type": "string"
},
"MemoriesResponse": {
"properties": {
"enabled": {
"default": true,
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"MemoriesUpdate": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
},
"MemoryCreateDto": {
"properties": {
"assetIds": {
@ -9586,17 +9634,6 @@
],
"type": "object"
},
"MemoryResponse": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"MemoryResponseDto": {
"properties": {
"assets": {
@ -9660,14 +9697,6 @@
],
"type": "string"
},
"MemoryUpdate": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"type": "object"
},
"MemoryUpdateDto": {
"properties": {
"isSaved": {
@ -9953,6 +9982,23 @@
],
"type": "string"
},
"PeopleResponse": {
"properties": {
"enabled": {
"default": true,
"type": "boolean"
},
"sidebarWeb": {
"default": false,
"type": "boolean"
}
},
"required": [
"enabled",
"sidebarWeb"
],
"type": "object"
},
"PeopleResponseDto": {
"properties": {
"hasNextPage": {
@ -9979,6 +10025,17 @@
],
"type": "object"
},
"PeopleUpdate": {
"properties": {
"enabled": {
"type": "boolean"
},
"sidebarWeb": {
"type": "boolean"
}
},
"type": "object"
},
"PeopleUpdateDto": {
"properties": {
"people": {
@ -10300,7 +10357,7 @@
],
"type": "object"
},
"RatingResponse": {
"RatingsResponse": {
"properties": {
"enabled": {
"default": false,
@ -10312,7 +10369,7 @@
],
"type": "object"
},
"RatingUpdate": {
"RatingsUpdate": {
"properties": {
"enabled": {
"type": "boolean"
@ -12002,6 +12059,34 @@
],
"type": "object"
},
"TagsResponse": {
"properties": {
"enabled": {
"default": true,
"type": "boolean"
},
"sidebarWeb": {
"default": true,
"type": "boolean"
}
},
"required": [
"enabled",
"sidebarWeb"
],
"type": "object"
},
"TagsUpdate": {
"properties": {
"enabled": {
"type": "boolean"
},
"sidebarWeb": {
"type": "boolean"
}
},
"type": "object"
},
"TimeBucketResponseDto": {
"properties": {
"count": {
@ -12379,23 +12464,35 @@
"emailNotifications": {
"$ref": "#/components/schemas/EmailNotificationsResponse"
},
"folders": {
"$ref": "#/components/schemas/FoldersResponse"
},
"memories": {
"$ref": "#/components/schemas/MemoryResponse"
"$ref": "#/components/schemas/MemoriesResponse"
},
"people": {
"$ref": "#/components/schemas/PeopleResponse"
},
"purchase": {
"$ref": "#/components/schemas/PurchaseResponse"
},
"rating": {
"$ref": "#/components/schemas/RatingResponse"
"ratings": {
"$ref": "#/components/schemas/RatingsResponse"
},
"tags": {
"$ref": "#/components/schemas/TagsResponse"
}
},
"required": [
"avatar",
"download",
"emailNotifications",
"folders",
"memories",
"people",
"purchase",
"rating"
"ratings",
"tags"
],
"type": "object"
},
@ -12410,14 +12507,23 @@
"emailNotifications": {
"$ref": "#/components/schemas/EmailNotificationsUpdate"
},
"folders": {
"$ref": "#/components/schemas/FoldersUpdate"
},
"memories": {
"$ref": "#/components/schemas/MemoryUpdate"
"$ref": "#/components/schemas/MemoriesUpdate"
},
"people": {
"$ref": "#/components/schemas/PeopleUpdate"
},
"purchase": {
"$ref": "#/components/schemas/PurchaseUpdate"
},
"rating": {
"$ref": "#/components/schemas/RatingUpdate"
"ratings": {
"$ref": "#/components/schemas/RatingsUpdate"
},
"tags": {
"$ref": "#/components/schemas/TagsUpdate"
}
},
"type": "object"

View file

@ -93,23 +93,38 @@ export type EmailNotificationsResponse = {
albumUpdate: boolean;
enabled: boolean;
};
export type MemoryResponse = {
export type FoldersResponse = {
enabled: boolean;
sidebarWeb: boolean;
};
export type MemoriesResponse = {
enabled: boolean;
};
export type PeopleResponse = {
enabled: boolean;
sidebarWeb: boolean;
};
export type PurchaseResponse = {
hideBuyButtonUntil: string;
showSupportBadge: boolean;
};
export type RatingResponse = {
export type RatingsResponse = {
enabled: boolean;
};
export type TagsResponse = {
enabled: boolean;
sidebarWeb: boolean;
};
export type UserPreferencesResponseDto = {
avatar: AvatarResponse;
download: DownloadResponse;
emailNotifications: EmailNotificationsResponse;
memories: MemoryResponse;
folders: FoldersResponse;
memories: MemoriesResponse;
people: PeopleResponse;
purchase: PurchaseResponse;
rating: RatingResponse;
ratings: RatingsResponse;
tags: TagsResponse;
};
export type AvatarUpdate = {
color?: UserAvatarColor;
@ -123,23 +138,38 @@ export type EmailNotificationsUpdate = {
albumUpdate?: boolean;
enabled?: boolean;
};
export type MemoryUpdate = {
export type FoldersUpdate = {
enabled?: boolean;
sidebarWeb?: boolean;
};
export type MemoriesUpdate = {
enabled?: boolean;
};
export type PeopleUpdate = {
enabled?: boolean;
sidebarWeb?: boolean;
};
export type PurchaseUpdate = {
hideBuyButtonUntil?: string;
showSupportBadge?: boolean;
};
export type RatingUpdate = {
export type RatingsUpdate = {
enabled?: boolean;
};
export type TagsUpdate = {
enabled?: boolean;
sidebarWeb?: boolean;
};
export type UserPreferencesUpdateDto = {
avatar?: AvatarUpdate;
download?: DownloadUpdate;
emailNotifications?: EmailNotificationsUpdate;
memories?: MemoryUpdate;
folders?: FoldersUpdate;
memories?: MemoriesUpdate;
people?: PeopleUpdate;
purchase?: PurchaseUpdate;
rating?: RatingUpdate;
ratings?: RatingsUpdate;
tags?: TagsUpdate;
};
export type AlbumUserResponseDto = {
role: AlbumUserRole;

View file

@ -12,16 +12,40 @@ class AvatarUpdate {
color?: UserAvatarColor;
}
class MemoryUpdate {
class MemoriesUpdate {
@ValidateBoolean({ optional: true })
enabled?: boolean;
}
class RatingUpdate {
class RatingsUpdate {
@ValidateBoolean({ optional: true })
enabled?: boolean;
}
class FoldersUpdate {
@ValidateBoolean({ optional: true })
enabled?: boolean;
@ValidateBoolean({ optional: true })
sidebarWeb?: boolean;
}
class PeopleUpdate {
@ValidateBoolean({ optional: true })
enabled?: boolean;
@ValidateBoolean({ optional: true })
sidebarWeb?: boolean;
}
class TagsUpdate {
@ValidateBoolean({ optional: true })
enabled?: boolean;
@ValidateBoolean({ optional: true })
sidebarWeb?: boolean;
}
class EmailNotificationsUpdate {
@ValidateBoolean({ optional: true })
enabled?: boolean;
@ -56,19 +80,34 @@ class PurchaseUpdate {
export class UserPreferencesUpdateDto {
@Optional()
@ValidateNested()
@Type(() => RatingUpdate)
rating?: RatingUpdate;
@Type(() => FoldersUpdate)
folders?: FoldersUpdate;
@Optional()
@ValidateNested()
@Type(() => MemoriesUpdate)
memories?: MemoriesUpdate;
@Optional()
@ValidateNested()
@Type(() => PeopleUpdate)
people?: PeopleUpdate;
@Optional()
@ValidateNested()
@Type(() => RatingsUpdate)
ratings?: RatingsUpdate;
@Optional()
@ValidateNested()
@Type(() => TagsUpdate)
tags?: TagsUpdate;
@Optional()
@ValidateNested()
@Type(() => AvatarUpdate)
avatar?: AvatarUpdate;
@Optional()
@ValidateNested()
@Type(() => MemoryUpdate)
memories?: MemoryUpdate;
@Optional()
@ValidateNested()
@Type(() => EmailNotificationsUpdate)
@ -90,12 +129,27 @@ class AvatarResponse {
color!: UserAvatarColor;
}
class RatingResponse {
class RatingsResponse {
enabled: boolean = false;
}
class MemoryResponse {
enabled!: boolean;
class MemoriesResponse {
enabled: boolean = true;
}
class FoldersResponse {
enabled: boolean = false;
sidebarWeb: boolean = false;
}
class PeopleResponse {
enabled: boolean = true;
sidebarWeb: boolean = false;
}
class TagsResponse {
enabled: boolean = true;
sidebarWeb: boolean = true;
}
class EmailNotificationsResponse {
@ -117,8 +171,11 @@ class PurchaseResponse {
}
export class UserPreferencesResponseDto implements UserPreferences {
rating!: RatingResponse;
memories!: MemoryResponse;
folders!: FoldersResponse;
memories!: MemoriesResponse;
people!: PeopleResponse;
ratings!: RatingsResponse;
tags!: TagsResponse;
avatar!: AvatarResponse;
emailNotifications!: EmailNotificationsResponse;
download!: DownloadResponse;

View file

@ -19,12 +19,24 @@ export class UserMetadataEntity<T extends keyof UserMetadata = UserMetadataKey>
}
export interface UserPreferences {
rating: {
folders: {
enabled: boolean;
sidebarWeb: boolean;
};
memories: {
enabled: boolean;
};
people: {
enabled: boolean;
sidebarWeb: boolean;
};
ratings: {
enabled: boolean;
};
tags: {
enabled: boolean;
sidebarWeb: boolean;
};
avatar: {
color: UserAvatarColor;
};
@ -50,12 +62,24 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences
);
return {
rating: {
folders: {
enabled: false,
sidebarWeb: false,
},
memories: {
enabled: true,
},
people: {
enabled: true,
sidebarWeb: false,
},
ratings: {
enabled: false,
},
tags: {
enabled: false,
sidebarWeb: false,
},
avatar: {
color: values[randomIndex],
},

View file

@ -20,7 +20,7 @@
};
</script>
{#if !isSharedLink() && $preferences?.rating?.enabled}
{#if !isSharedLink() && $preferences?.ratings.enabled}
<section class="px-4 pt-2">
<StarRating {rating} readOnly={!isOwner} onRating={(rating) => handlePromiseError(handleChangeRating(rating))} />
</section>

View file

@ -6,7 +6,7 @@
import { boundingBoxesArray } from '$lib/stores/people.store';
import { locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store';
import { user } from '$lib/stores/user.store';
import { preferences, user } from '$lib/stores/user.store';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError, isSharedLink } from '$lib/utils';
import { delay, isFlipped } from '$lib/utils/asset-utils';
import {
@ -502,9 +502,11 @@
</section>
{/if}
<section class="relative px-2 pb-12 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
<DetailPanelTags {asset} {isOwner} />
</section>
{#if $preferences?.tags?.enabled}
<section class="relative px-2 pb-12 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
<DetailPanelTags {asset} {isOwner} />
</section>
{/if}
{#if showEditFaces}
<PersonSidePanel

View file

@ -9,6 +9,7 @@
export let subtitle = '';
export let key: string;
export let isOpen = $accordionState.has(key);
export let autoScrollTo = false;
let accordionElement: HTMLDivElement;
@ -18,12 +19,14 @@
if (isOpen) {
$accordionState = $accordionState.add(key);
setTimeout(() => {
accordionElement.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}, 200);
if (autoScrollTo) {
setTimeout(() => {
accordionElement.scrollIntoView({
behavior: 'smooth',
block: 'start',
});
}, 200);
}
} else {
$accordionState.delete(key);
$accordionState = $accordionState;
@ -72,7 +75,7 @@
</button>
{#if isOpen}
<ul transition:slide={{ duration: 250 }} class="mb-2 ml-4">
<ul transition:slide={{ duration: 150 }} class="mb-2 ml-4">
<slot />
</ul>
{/if}

View file

@ -1,5 +1,4 @@
<script lang="ts">
import { sidebarSettings } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store';
import {
mdiAccount,
@ -29,6 +28,7 @@
import MoreInformationAlbums from '$lib/components/shared-components/side-bar/more-information-albums.svelte';
import { t } from 'svelte-i18n';
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
import { preferences } from '$lib/stores/user.store';
let isArchiveSelected: boolean;
let isFavoritesSelected: boolean;
@ -52,6 +52,7 @@
<MoreInformationAssets assetStats={{ isArchived: false }} />
</svelte:fragment>
</SideBarLink>
{#if $featureFlags.search}
<SideBarLink title={$t('explore')} routeId="/(user)/explore" icon={mdiMagnify} />
{/if}
@ -65,7 +66,7 @@
/>
{/if}
{#if $sidebarSettings.people}
{#if $preferences.people.enabled && $preferences.people.sidebarWeb}
<SideBarLink
title={$t('people')}
routeId="/(user)/people"
@ -73,23 +74,23 @@
icon={isPeopleSelected ? mdiAccount : mdiAccountOutline}
/>
{/if}
{#if $sidebarSettings.sharing}
<SideBarLink
title={$t('sharing')}
routeId="/(user)/sharing"
icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline}
bind:isSelected={isSharingSelected}
>
<svelte:fragment slot="moreInformation">
<MoreInformationAlbums albumType="shared" />
</svelte:fragment>
</SideBarLink>
{/if}
<SideBarLink
title={$t('sharing')}
routeId="/(user)/sharing"
icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline}
bind:isSelected={isSharingSelected}
>
<svelte:fragment slot="moreInformation">
<MoreInformationAlbums albumType="shared" />
</svelte:fragment>
</SideBarLink>
<div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
<p class="hidden p-6 group-hover:sm:block md:block">{$t('library').toUpperCase()}</p>
<hr class="mx-4 mb-[31px] mt-8 block group-hover:sm:hidden md:hidden" />
</div>
<SideBarLink
title={$t('favorites')}
routeId="/(user)/favorites"
@ -100,15 +101,20 @@
<MoreInformationAssets assetStats={{ isFavorite: true }} />
</svelte:fragment>
</SideBarLink>
<SideBarLink title={$t('albums')} routeId="/(user)/albums" icon={mdiImageAlbum} flippedLogo>
<svelte:fragment slot="moreInformation">
<MoreInformationAlbums albumType="owned" />
</svelte:fragment>
</SideBarLink>
<SideBarLink title={$t('tags')} routeId="/(user)/tags" icon={mdiTagMultipleOutline} flippedLogo />
{#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
<SideBarLink title={$t('tags')} routeId="/(user)/tags" icon={mdiTagMultipleOutline} flippedLogo />
{/if}
<SideBarLink title={$t('folders')} routeId="/(user)/folders" icon={mdiFolderOutline} flippedLogo />
{#if $preferences.folders.enabled && $preferences.folders.sidebarWeb}
<SideBarLink title={$t('folders')} routeId="/(user)/folders" icon={mdiFolderOutline} flippedLogo />
{/if}
<SideBarLink
title={$t('utilities')}

View file

@ -11,7 +11,6 @@
loopVideo,
playVideoThumbnailOnHover,
showDeleteModal,
sidebarSettings,
} from '$lib/stores/preferences.store';
import { findLocale } from '$lib/utils';
import { getClosestAvailableLocale, langCodes } from '$lib/utils/i18n';
@ -19,13 +18,6 @@
import { locale as i18nLocale, t } from 'svelte-i18n';
import { fade } from 'svelte/transition';
import { invalidateAll } from '$app/navigation';
import { preferences } from '$lib/stores/user.store';
import { updateMyPreferences } from '@immich/sdk';
import { handleError } from '../../utils/handle-error';
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
let time = new Date();
@ -46,7 +38,6 @@
label: findLocale(editedLocale).name || fallbackLocale.name,
};
$: closestLanguage = getClosestAvailableLocale([$lang], langCodes);
$: ratingEnabled = $preferences?.rating?.enabled;
onMount(() => {
const interval = setInterval(() => {
@ -98,17 +89,6 @@
$locale = newLocale;
}
};
const handleRatingChange = async (enabled: boolean) => {
try {
const data = await updateMyPreferences({ userPreferencesUpdateDto: { rating: { enabled } } });
$preferences.rating.enabled = data.rating.enabled;
notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info });
} catch (error) {
handleError(error, $t('errors.unable_to_update_settings'));
}
};
</script>
<section class="my-4">
@ -189,29 +169,6 @@
bind:checked={$showDeleteModal}
/>
</div>
<div class="ml-4">
<SettingSwitch
title={$t('people')}
subtitle={$t('people_sidebar_description')}
bind:checked={$sidebarSettings.people}
/>
</div>
<div class="ml-4">
<SettingSwitch
title={$t('sharing')}
subtitle={$t('sharing_sidebar_description')}
bind:checked={$sidebarSettings.sharing}
/>
</div>
<div class="ml-4">
<SettingSwitch
title={$t('rating')}
subtitle={$t('rating_description')}
bind:checked={ratingEnabled}
on:toggle={({ detail: enabled }) => handleRatingChange(enabled)}
/>
</div>
</div>
</div>
</section>

View file

@ -0,0 +1,124 @@
<script lang="ts">
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { updateMyPreferences } from '@immich/sdk';
import { fade } from 'svelte/transition';
import { handleError } from '../../utils/handle-error';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { preferences } from '$lib/stores/user.store';
import Button from '../elements/buttons/button.svelte';
import { t } from 'svelte-i18n';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
// Folders
let foldersEnabled = $preferences?.folders?.enabled ?? false;
let foldersSidebar = $preferences?.folders?.sidebarWeb ?? false;
// Memories
let memoriesEnabled = $preferences?.memories?.enabled ?? true;
// People
let peopleEnabled = $preferences?.people?.enabled ?? false;
let peopleSidebar = $preferences?.people?.sidebarWeb ?? false;
// Ratings
let ratingsEnabled = $preferences?.ratings?.enabled ?? false;
// Tags
let tagsEnabled = $preferences?.tags?.enabled ?? false;
let tagsSidebar = $preferences?.tags?.sidebarWeb ?? false;
const handleSave = async () => {
try {
const data = await updateMyPreferences({
userPreferencesUpdateDto: {
folders: { enabled: foldersEnabled, sidebarWeb: foldersSidebar },
memories: { enabled: memoriesEnabled },
people: { enabled: peopleEnabled, sidebarWeb: peopleSidebar },
ratings: { enabled: ratingsEnabled },
tags: { enabled: tagsEnabled, sidebarWeb: tagsSidebar },
},
});
$preferences = { ...data };
notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info });
} catch (error) {
handleError(error, $t('errors.unable_to_update_settings'));
}
};
</script>
<section class="my-4">
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingAccordion key="folders" title={$t('folders')} subtitle={$t('folders_feature_description')}>
<div class="ml-4 mt-6">
<SettingSwitch title={$t('enable')} bind:checked={foldersEnabled} />
</div>
{#if foldersEnabled}
<div class="ml-4 mt-6">
<SettingSwitch
title={$t('sidebar')}
subtitle={$t('sidebar_display_description')}
bind:checked={foldersSidebar}
/>
</div>
{/if}
</SettingAccordion>
<SettingAccordion key="memories" title={$t('time_based_memories')} subtitle={$t('photos_from_previous_years')}>
<div class="ml-4 mt-6">
<SettingSwitch title={$t('enable')} bind:checked={memoriesEnabled} />
</div>
</SettingAccordion>
<SettingAccordion key="people" title={$t('people')} subtitle={$t('people_feature_description')}>
<div class="ml-4 mt-6">
<SettingSwitch title={$t('enable')} bind:checked={peopleEnabled} />
</div>
{#if peopleEnabled}
<div class="ml-4 mt-6">
<SettingSwitch
title={$t('sidebar')}
subtitle={$t('sidebar_display_description')}
bind:checked={peopleSidebar}
/>
</div>
{/if}
</SettingAccordion>
<SettingAccordion key="rating" title={$t('rating')} subtitle={$t('rating_description')}>
<div class="ml-4 mt-6">
<SettingSwitch title={$t('enable')} bind:checked={ratingsEnabled} />
</div>
</SettingAccordion>
<SettingAccordion key="tags" title={$t('tags')} subtitle={$t('tag_feature_description')}>
<div class="ml-4 mt-6">
<SettingSwitch title={$t('enable')} bind:checked={tagsEnabled} />
</div>
{#if tagsEnabled}
<div class="ml-4 mt-6">
<SettingSwitch
title={$t('sidebar')}
subtitle={$t('sidebar_display_description')}
bind:checked={tagsSidebar}
/>
</div>
{/if}
</SettingAccordion>
<div class="flex justify-end">
<Button type="submit" size="sm" on:click={() => handleSave()}>{$t('save')}</Button>
</div>
</div>
</form>
</div>
</section>

View file

@ -1,46 +0,0 @@
<script lang="ts">
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { updateMyPreferences } from '@immich/sdk';
import { fade } from 'svelte/transition';
import { handleError } from '../../utils/handle-error';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { preferences } from '$lib/stores/user.store';
import Button from '../elements/buttons/button.svelte';
import { t } from 'svelte-i18n';
let memoriesEnabled = $preferences?.memories?.enabled ?? false;
const handleSave = async () => {
try {
const data = await updateMyPreferences({ userPreferencesUpdateDto: { memories: { enabled: memoriesEnabled } } });
$preferences.memories.enabled = data.memories.enabled;
notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info });
} catch (error) {
handleError(error, $t('errors.unable_to_update_settings'));
}
};
</script>
<section class="my-4">
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<div class="ml-4 mt-4 flex flex-col gap-4">
<div class="ml-4">
<SettingSwitch
title={$t('time_based_memories')}
subtitle={$t('photos_from_previous_years')}
bind:checked={memoriesEnabled}
/>
</div>
<div class="flex justify-end">
<Button type="submit" size="sm" on:click={() => handleSave()}>{$t('save')}</Button>
</div>
</div>
</form>
</div>
</section>

View file

@ -10,7 +10,6 @@
import AppSettings from './app-settings.svelte';
import ChangePasswordSettings from './change-password-settings.svelte';
import DeviceList from './device-list.svelte';
import MemoriesSettings from './memories-settings.svelte';
import OAuthSettings from './oauth-settings.svelte';
import PartnerSettings from './partner-settings.svelte';
import UserAPIKeyList from './user-api-key-list.svelte';
@ -19,6 +18,7 @@
import { t } from 'svelte-i18n';
import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte';
import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte';
import FeatureSettings from '$lib/components/user-settings-page/feature-settings.svelte';
export let keys: ApiKeyResponseDto[] = [];
export let sessions: SessionResponseDto[] = [];
@ -53,8 +53,8 @@
<DownloadSettings />
</SettingAccordion>
<SettingAccordion key="memories" title={$t('memories')} subtitle={$t('memories_setting_description')}>
<MemoriesSettings />
<SettingAccordion key="feature" title={$t('features')} subtitle={$t('features_setting_description')}>
<FeatureSettings />
</SettingAccordion>
<SettingAccordion key="notifications" title={$t('notifications')} subtitle={$t('notifications_setting_description')}>
@ -84,6 +84,7 @@
key="user-purchase-settings"
title={$t('user_purchase_settings')}
subtitle={$t('user_purchase_settings_description')}
autoScrollTo={true}
>
<UserPurchaseSettings />
</SettingAccordion>

View file

@ -701,6 +701,8 @@
"favorite_or_unfavorite_photo": "Favorite or unfavorite photo",
"favorites": "Favorites",
"feature_photo_updated": "Feature photo updated",
"features": "Features",
"features_setting_description": "Manage the app features",
"file_name": "File name",
"file_name_or_extension": "File name or extension",
"filename": "Filename",
@ -709,6 +711,7 @@
"find_them_fast": "Find them fast by name with search",
"fix_incorrect_match": "Fix incorrect match",
"folders": "Folders",
"folders_feature_description": "Browsing the folder view for the photos and videos on the file system",
"force_re-scan_library_files": "Force Re-scan All Library Files",
"forward": "Forward",
"general": "General",
@ -912,6 +915,7 @@
"pending": "Pending",
"people": "People",
"people_edits_count": "Edited {count, plural, one {# person} other {# people}}",
"people_feature_description": "Browsing photos and videos grouped by people",
"people_sidebar_description": "Display a link to People in the sidebar",
"permanent_deletion_warning": "Permanent deletion warning",
"permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets",
@ -981,7 +985,7 @@
"rating": "Star rating",
"rating_clear": "Clear rating",
"rating_count": "{count, plural, one {# star} other {# stars}}",
"rating_description": "Display the exif rating in the info panel",
"rating_description": "Display the EXIF rating in the info panel",
"reaction_options": "Reaction options",
"read_changelog": "Read Changelog",
"reassign": "Reassign",
@ -1130,6 +1134,8 @@
"show_supporter_badge": "Supporter badge",
"show_supporter_badge_description": "Show a supporter badge",
"shuffle": "Shuffle",
"sidebar": "Sidebar",
"sidebar_display_description": "Display a link to the view in the sidebar",
"sign_out": "Sign Out",
"sign_up": "Sign up",
"size": "Size",
@ -1169,6 +1175,7 @@
"tag": "Tag",
"tag_assets": "Tag assets",
"tag_created": "Created tag: {tag}",
"tag_feature_description": "Browsing photos and videos grouped by logical tag topics",
"tag_updated": "Updated tag: {tag}",
"tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}",
"tags": "Tags",

View file

@ -96,11 +96,6 @@ export interface SidebarSettings {
sharing: boolean;
}
export const sidebarSettings = persisted<SidebarSettings>('sidebar-settings-1', {
people: false,
sharing: true,
});
export enum SortOrder {
Asc = 'asc',
Desc = 'desc',

View file

@ -81,7 +81,9 @@
<ChangeDate menuItem />
<ChangeLocation menuItem />
<ArchiveAction menuItem onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
<TagAction menuItem />
{#if $preferences.tags.enabled}
<TagAction menuItem />
{/if}
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
<hr />
<AssetJobActions />