From a9e6657a37a6aa5c03bc2a7e7c79ccfbffa39f81 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sun, 11 Feb 2024 00:25:02 +0100 Subject: [PATCH] feat(web): store accordion state in search params (#6435) * keep admin settings accordion state in search params * refactor: sync implementation * fix: avoid mutating svelte's internal search params * add query parameter to enum --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> --- .../settings/ffmpeg/ffmpeg-settings.svelte | 7 ++- .../library-settings/library-settings.svelte | 14 ++++- .../machine-learning-settings.svelte | 12 +++- .../settings/map-settings/map-settings.svelte | 4 +- .../settings/setting-accordion.svelte | 18 +++++- .../user-settings-list.svelte | 21 +++---- web/src/lib/constants.ts | 5 +- web/src/lib/utils.ts | 33 +++++++++++ .../routes/admin/system-settings/+page.svelte | 59 ++++++++++++------- 9 files changed, 132 insertions(+), 41 deletions(-) create mode 100644 web/src/lib/utils.ts diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte index f24a9cad65..c4f9f9ea2b 100644 --- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte +++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte @@ -235,6 +235,7 @@ /> <SettingAccordion + key="hardware-acceleration" title="Hardware Acceleration" subtitle="Experimental; much faster, but will have lower quality at the same bitrate" > @@ -296,7 +297,11 @@ </div> </SettingAccordion> - <SettingAccordion title="Advanced" subtitle="Options most users should not need to change"> + <SettingAccordion + key="advanced-options" + title="Advanced" + subtitle="Options most users should not need to change" + > <div class="ml-4 mt-4 flex flex-col gap-4"> <SettingInputField inputType={SettingInputFieldType.NUMBER} diff --git a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte index fbcfe68b4c..27e3c3f7ca 100644 --- a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte +++ b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte @@ -26,7 +26,12 @@ <div> <div in:fade={{ duration: 500 }}> - <SettingAccordion title="Library watching (EXPERIMENTAL)" subtitle="Automatically watch for changed files" isOpen> + <SettingAccordion + key="library-watching" + title="Library watching (EXPERIMENTAL)" + subtitle="Automatically watch for changed files" + isOpen + > <form autocomplete="off" on:submit|preventDefault> <div class="ml-4 mt-4 flex flex-col gap-4"> <SettingSwitch @@ -70,7 +75,12 @@ </form> </SettingAccordion> - <SettingAccordion title="Periodic Scanning" subtitle="Configure periodic library scanning" isOpen> + <SettingAccordion + key="library-scanning" + title="Periodic Scanning" + subtitle="Configure periodic library scanning" + isOpen + > <form autocomplete="off" on:submit|preventDefault> <div class="ml-4 mt-4 flex flex-col gap-4"> <SettingSwitch diff --git a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte index 90f74950db..9362da60cd 100644 --- a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte +++ b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte @@ -42,7 +42,11 @@ /> </div> - <SettingAccordion title="Smart Search" subtitle="Search for images semantically using CLIP embeddings"> + <SettingAccordion + key="smart-search" + title="Smart Search" + subtitle="Search for images semantically using CLIP embeddings" + > <div class="ml-4 mt-4 flex flex-col gap-4"> <SettingSwitch title="ENABLED" @@ -69,7 +73,11 @@ </div> </SettingAccordion> - <SettingAccordion title="Facial Recognition" subtitle="Detect, recognize and group faces in images"> + <SettingAccordion + key="facial-recognition" + title="Facial Recognition" + subtitle="Detect, recognize and group faces in images" + > <div class="ml-4 mt-4 flex flex-col gap-4"> <SettingSwitch title="ENABLED" diff --git a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte index 8cc83f8070..80c59f5778 100644 --- a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte +++ b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte @@ -21,7 +21,7 @@ <div in:fade={{ duration: 500 }}> <form autocomplete="off" on:submit|preventDefault> <div class="flex flex-col gap-4"> - <SettingAccordion title="Map Settings" subtitle="Manage map settings"> + <SettingAccordion key="map" title="Map Settings" subtitle="Manage map settings"> <div class="ml-4 mt-4 flex flex-col gap-4"> <SettingSwitch title="ENABLED" @@ -51,7 +51,7 @@ </div></SettingAccordion > - <SettingAccordion title="Reverse Geocoding Settings"> + <SettingAccordion key="reverse-geocoding" title="Reverse Geocoding Settings"> <svelte:fragment slot="subtitle"> <p class="text-sm dark:text-immich-dark-fg"> Manage <a diff --git a/web/src/lib/components/admin-page/settings/setting-accordion.svelte b/web/src/lib/components/admin-page/settings/setting-accordion.svelte index 1d9290e617..76f29b1f6e 100755 --- a/web/src/lib/components/admin-page/settings/setting-accordion.svelte +++ b/web/src/lib/components/admin-page/settings/setting-accordion.svelte @@ -1,10 +1,24 @@ <script lang="ts"> + import { page } from '$app/stores'; + import { QueryParameter } from '$lib/constants'; + import { hasParamValue, updateParamList } from '$lib/utils'; import { slide } from 'svelte/transition'; + export let title: string; export let subtitle = ''; - + export let key: string; export let isOpen = false; - const toggle = () => (isOpen = !isOpen); + + const syncFromUrl = () => (isOpen = hasParamValue(QueryParameter.IS_OPEN, key)); + const syncToUrl = (isOpen: boolean) => updateParamList({ param: QueryParameter.IS_OPEN, value: key, add: isOpen }); + + isOpen ? syncToUrl(true) : syncFromUrl(); + $: $page.url && syncFromUrl(); + + const toggle = () => { + isOpen = !isOpen; + syncToUrl(isOpen); + }; </script> <div class="border-b-[1px] border-gray-200 py-4 dark:border-gray-700"> diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index e00cac83b5..60a5871775 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -27,32 +27,33 @@ } </script> -<SettingAccordion title="Appearance" subtitle="Manage your Immich appearance"> +<SettingAccordion key="appearance" title="Appearance" subtitle="Manage your Immich appearance"> <AppearanceSettings /> </SettingAccordion> -<SettingAccordion title="Account" subtitle="Manage your account"> +<SettingAccordion key="account" title="Account" subtitle="Manage your account"> <UserProfileSettings user={$user} /> </SettingAccordion> -<SettingAccordion title="API Keys" subtitle="Manage your API keys"> +<SettingAccordion key="api-keys" title="API Keys" subtitle="Manage your API keys"> <UserAPIKeyList bind:keys /> </SettingAccordion> -<SettingAccordion title="Authorized Devices" subtitle="Manage your logged-in devices"> +<SettingAccordion key="authorized-devices" title="Authorized Devices" subtitle="Manage your logged-in devices"> <DeviceList bind:devices /> </SettingAccordion> -<SettingAccordion title="Libraries" subtitle="Manage your asset libraries"> +<SettingAccordion key="libraries" title="Libraries" subtitle="Manage your asset libraries"> <LibraryList /> </SettingAccordion> -<SettingAccordion title="Memories" subtitle="Manage what you see in your memories."> +<SettingAccordion key="memories" title="Memories" subtitle="Manage what you see in your memories."> <MemoriesSettings user={$user} /> </SettingAccordion> {#if $featureFlags.loaded && $featureFlags.oauth} <SettingAccordion + key="oauth" title="OAuth" subtitle="Manage your OAuth connection" isOpen={oauthOpen || @@ -62,18 +63,18 @@ </SettingAccordion> {/if} -<SettingAccordion title="Password" subtitle="Change your password"> +<SettingAccordion key="password" title="Password" subtitle="Change your password"> <ChangePasswordSettings /> </SettingAccordion> -<SettingAccordion title="Sharing" subtitle="Manage sharing with partners"> +<SettingAccordion key="sharing" title="Sharing" subtitle="Manage sharing with partners"> <PartnerSettings user={$user} /> </SettingAccordion> -<SettingAccordion title="Sidebar" subtitle="Manage sidebar settings"> +<SettingAccordion key="sidebar" title="Sidebar" subtitle="Manage sidebar settings"> <SidebarSettings /> </SettingAccordion> -<SettingAccordion title="Trash" subtitle="Manage trash settings"> +<SettingAccordion key="trash" title="Trash" subtitle="Manage trash settings"> <TrashSettings /> </SettingAccordion> diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index a1ac68c216..295bd99433 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -63,14 +63,15 @@ export const dateFormats = { export enum QueryParameter { ACTION = 'action', ASSET_INDEX = 'assetIndex', - SMART_SEARCH = 'smartSearch', + IS_OPEN = 'isOpen', MEMORY_INDEX = 'memoryIndex', ONBOARDING_STEP = 'step', OPEN_SETTING = 'openSetting', - QUERY = 'query', PREVIOUS_ROUTE = 'previousRoute', + QUERY = 'query', SEARCHED_PEOPLE = 'searchedPeople', SEARCH_TERM = 'q', + SMART_SEARCH = 'smartSearch', } export enum OpenSettingQueryParameterValue { diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts new file mode 100644 index 0000000000..059b526508 --- /dev/null +++ b/web/src/lib/utils.ts @@ -0,0 +1,33 @@ +import { goto } from '$app/navigation'; +import { page } from '$app/stores'; +import { get } from 'svelte/store'; + +interface UpdateParamAction { + param: string; + value: string; + add: boolean; +} + +const getParamValues = (param: string) => + new Set((get(page).url.searchParams.get(param) || '').split(' ').filter((x) => x !== '')); + +export const hasParamValue = (param: string, value: string) => getParamValues(param).has(value); + +export const updateParamList = async ({ param, value, add }: UpdateParamAction) => { + const values = getParamValues(param); + + if (add) { + values.add(value); + } else { + values.delete(value); + } + + const searchParams = new URLSearchParams(get(page).url.searchParams); + searchParams.set(param, [...values.values()].join(' ')); + + if (values.size === 0) { + searchParams.delete(param); + } + + await goto(`?${searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true }); +}; diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index a52f797ccb..8bf63d58c6 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -1,5 +1,4 @@ <script lang="ts"> - import { page } from '$app/stores'; import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte'; import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte'; import MachineLearningSettings from '$lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte'; @@ -17,7 +16,7 @@ import { downloadManager } from '$lib/stores/download'; import { featureFlags } from '$lib/stores/server-config.store'; import { downloadBlob } from '$lib/utils/asset-utils'; - import { type SystemConfigDto, copyToClipboard } from '@api'; + import { copyToClipboard } from '@api'; import Icon from '$lib/components/elements/icon.svelte'; import type { PageData } from './$types'; import NewVersionCheckSettings from '$lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte'; @@ -29,7 +28,22 @@ export let data: PageData; let config = data.configs; - let openSettings = ($page.url.searchParams.get('open')?.split(',') || []) as Array<keyof SystemConfigDto>; + + type Settings = + | typeof JobSettings + | typeof LibrarySettings + | typeof LoggingSettings + | typeof MachineLearningSettings + | typeof MapSettings + | typeof OAuthSettings + | typeof PasswordLoginSettings + | typeof ServerSettings + | typeof StorageTemplateSettings + | typeof ThemeSettings + | typeof ThumbnailSettings + | typeof TrashSettings + | typeof NewVersionCheckSettings + | typeof FFmpegSettings; const downloadConfig = () => { const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); @@ -40,90 +54,95 @@ setTimeout(() => downloadManager.clear(downloadKey), 5000); }; - const settings = [ + const settings: Array<{ + item: Settings; + title: string; + subtitle: string; + key: string; + }> = [ { item: JobSettings, title: 'Job Settings', subtitle: 'Manage job concurrency', - isOpen: openSettings.includes('job'), + key: 'job', }, { item: LibrarySettings, title: 'Library', subtitle: 'Manage library settings', - isOpen: openSettings.includes('library'), + key: 'library', }, { item: LoggingSettings, title: 'Logging', subtitle: 'Manage log settings', - isOpen: openSettings.includes('logging'), + key: 'logging', }, { item: MachineLearningSettings, title: 'Machine Learning Settings', subtitle: 'Manage machine learning features and settings', - isOpen: openSettings.includes('machineLearning'), + key: 'machine-learning', }, { item: MapSettings, title: 'Map & GPS Settings', subtitle: 'Manage map related features and setting', - isOpen: openSettings.some((key) => ['map', 'reverseGeocoding'].includes(key)), + key: 'location', }, { item: OAuthSettings, title: 'OAuth Authentication', subtitle: 'Manage the login with OAuth settings', - isOpen: openSettings.includes('oauth'), + key: 'oauth', }, { item: PasswordLoginSettings, title: 'Password Authentication', subtitle: 'Manage the login with password settings', - isOpen: openSettings.includes('passwordLogin'), + key: 'password', }, { item: ServerSettings, title: 'Server Settings', subtitle: 'Manage server settings', - isOpen: openSettings.includes('server'), + key: 'server', }, { item: StorageTemplateSettings, title: 'Storage Template', subtitle: 'Manage the folder structure and file name of the upload asset', - isOpen: openSettings.includes('storageTemplate'), + key: 'storage-template', }, { item: ThemeSettings, title: 'Theme Settings', subtitle: 'Manage customization of the Immich web interface', - isOpen: openSettings.includes('theme'), + key: 'theme', }, { item: ThumbnailSettings, title: 'Thumbnail Settings', subtitle: 'Manage the resolution of thumbnail sizes', - isOpen: openSettings.includes('thumbnail'), + key: 'thumbnail', }, { item: TrashSettings, title: 'Trash Settings', subtitle: 'Manage trash settings', - isOpen: openSettings.includes('trash'), + key: 'trash', }, { item: NewVersionCheckSettings, title: 'Version Check', subtitle: 'Enable/disable the new version notification', - isOpen: openSettings.includes('newVersionCheck'), + key: 'version-check', }, { item: FFmpegSettings, title: 'Video Transcoding Settings', subtitle: 'Manage the resolution and encoding information of the video files', - isOpen: openSettings.includes('ffmpeg'), + key: 'video-transcoding', }, ]; </script> @@ -157,8 +176,8 @@ <AdminSettings bind:config let:handleReset let:handleSave let:savedConfig let:defaultConfig> <section id="setting-content" class="flex place-content-center sm:mx-4"> <section class="w-full pb-28 sm:w-5/6 md:w-[850px]"> - {#each settings as { item, title, subtitle, isOpen }} - <SettingAccordion {title} {subtitle} {isOpen}> + {#each settings as { item, title, subtitle, key }} + <SettingAccordion {title} {subtitle} {key}> <svelte:component this={item} on:save={({ detail }) => handleSave(detail)}