mirror of
https://github.com/immich-app/immich.git
synced 2024-12-27 10:58:13 +02: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:
parent
9bfaa525db
commit
ebecb60f39
@ -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
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/folders_response.dart
generated
Normal file
BIN
mobile/openapi/lib/model/folders_response.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/folders_update.dart
generated
Normal file
BIN
mobile/openapi/lib/model/folders_update.dart
generated
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/people_response.dart
generated
Normal file
BIN
mobile/openapi/lib/model/people_response.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/people_update.dart
generated
Normal file
BIN
mobile/openapi/lib/model/people_update.dart
generated
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
mobile/openapi/lib/model/tags_response.dart
generated
Normal file
BIN
mobile/openapi/lib/model/tags_response.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/tags_update.dart
generated
Normal file
BIN
mobile/openapi/lib/model/tags_update.dart
generated
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
49
mobile/test/modules/utils/openapi_patching_test.dart
Normal file
49
mobile/test/modules/utils/openapi_patching_test.dart
Normal 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');
|
||||
});
|
||||
});
|
||||
}
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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],
|
||||
},
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
{#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>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if showEditFaces}
|
||||
<PersonSidePanel
|
||||
|
@ -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);
|
||||
|
||||
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}
|
||||
|
@ -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,7 +74,7 @@
|
||||
icon={isPeopleSelected ? mdiAccount : mdiAccountOutline}
|
||||
/>
|
||||
{/if}
|
||||
{#if $sidebarSettings.sharing}
|
||||
|
||||
<SideBarLink
|
||||
title={$t('sharing')}
|
||||
routeId="/(user)/sharing"
|
||||
@ -84,12 +85,12 @@
|
||||
<MoreInformationAlbums albumType="shared" />
|
||||
</svelte:fragment>
|
||||
</SideBarLink>
|
||||
{/if}
|
||||
|
||||
<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>
|
||||
|
||||
{#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
|
||||
<SideBarLink title={$t('tags')} routeId="/(user)/tags" icon={mdiTagMultipleOutline} flippedLogo />
|
||||
{/if}
|
||||
|
||||
{#if $preferences.folders.enabled && $preferences.folders.sidebarWeb}
|
||||
<SideBarLink title={$t('folders')} routeId="/(user)/folders" icon={mdiFolderOutline} flippedLogo />
|
||||
{/if}
|
||||
|
||||
<SideBarLink
|
||||
title={$t('utilities')}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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",
|
||||
|
@ -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',
|
||||
|
@ -81,7 +81,9 @@
|
||||
<ChangeDate menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
<ArchiveAction menuItem onArchive={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||
{#if $preferences.tags.enabled}
|
||||
<TagAction menuItem />
|
||||
{/if}
|
||||
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||
<hr />
|
||||
<AssetJobActions />
|
||||
|
Loading…
Reference in New Issue
Block a user