diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte index 37f875c604..9b0e4b3270 100644 --- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte @@ -71,7 +71,7 @@ <div> <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 mt-4 flex flex-col"> <SettingAccordion key="oauth" title={$t('admin.oauth_settings')} diff --git a/web/src/lib/components/shared-components/settings/setting-accordion.svelte b/web/src/lib/components/shared-components/settings/setting-accordion.svelte index eec4fae9c2..d8b50b2132 100755 --- a/web/src/lib/components/shared-components/settings/setting-accordion.svelte +++ b/web/src/lib/components/shared-components/settings/setting-accordion.svelte @@ -2,6 +2,7 @@ import { slide } from 'svelte/transition'; import { getAccordionState } from './setting-accordion-state.svelte'; import { onDestroy } from 'svelte'; + import Icon from '$lib/components/elements/icon.svelte'; const accordionState = getAccordionState(); @@ -10,6 +11,7 @@ export let key: string; export let isOpen = $accordionState.has(key); export let autoScrollTo = false; + export let icon = ''; let accordionElement: HTMLDivElement; @@ -38,7 +40,12 @@ }); </script> -<div class="border-b-[1px] border-gray-200 py-4 dark:border-gray-700" bind:this={accordionElement}> +<div + class="border rounded-2xl my-4 px-6 py-4 transition-all {isOpen + ? 'border-immich-primary/40 dark:border-immich-dark-primary/50 shadow-md' + : 'dark:border-gray-800'}" + bind:this={accordionElement} +> <button type="button" aria-expanded={isOpen} @@ -46,12 +53,17 @@ class="flex w-full place-items-center justify-between text-left" > <div> - <h2 class="font-medium text-immich-primary dark:text-immich-dark-primary"> - {title} - </h2> + <div class="flex gap-2 place-items-center"> + {#if icon} + <Icon path={icon} class="text-immich-primary dark:text-immich-dark-primary" size="24" ariaHidden /> + {/if} + <h2 class="font-medium text-immich-primary dark:text-immich-dark-primary"> + {title} + </h2> + </div> <slot name="subtitle"> - <p class="text-sm dark:text-immich-dark-fg">{subtitle}</p> + <p class="text-sm dark:text-immich-dark-fg mt-1">{subtitle}</p> </slot> </div> diff --git a/web/src/lib/components/user-settings-page/feature-settings.svelte b/web/src/lib/components/user-settings-page/feature-settings.svelte index dc11dab15e..d04bbc3e7d 100644 --- a/web/src/lib/components/user-settings-page/feature-settings.svelte +++ b/web/src/lib/components/user-settings-page/feature-settings.svelte @@ -55,7 +55,7 @@ <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 mt-4 flex flex-col"> <SettingAccordion key="folders" title={$t('folders')} subtitle={$t('folders_feature_description')}> <div class="ml-4 mt-6"> <SettingSwitch title={$t('enable')} bind:checked={foldersEnabled} /> 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 596efaedef..f355c3105c 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 @@ -19,6 +19,19 @@ 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'; + import { + mdiAccountGroupOutline, + mdiAccountOutline, + mdiApi, + mdiBellOutline, + mdiCogOutline, + mdiDevices, + mdiDownload, + mdiFeatureSearchOutline, + mdiKeyOutline, + mdiOnepassword, + mdiTwoFactorAuthentication, + } from '@mdi/js'; export let keys: ApiKeyResponseDto[] = []; export let sessions: SessionResponseDto[] = []; @@ -29,23 +42,34 @@ </script> <SettingAccordionState queryParam={QueryParameter.IS_OPEN}> - <SettingAccordion key="app-settings" title={$t('app_settings')} subtitle={$t('manage_the_app_settings')}> + <SettingAccordion + icon={mdiCogOutline} + key="app-settings" + title={$t('app_settings')} + subtitle={$t('manage_the_app_settings')} + > <AppSettings /> </SettingAccordion> - <SettingAccordion key="account" title={$t('account')} subtitle={$t('manage_your_account')}> + <SettingAccordion icon={mdiAccountOutline} key="account" title={$t('account')} subtitle={$t('manage_your_account')}> <UserProfileSettings /> </SettingAccordion> - <SettingAccordion key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}> + <SettingAccordion icon={mdiApi} key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}> <UserAPIKeyList bind:keys /> </SettingAccordion> - <SettingAccordion key="authorized-devices" title={$t('authorized_devices')} subtitle={$t('manage_your_devices')}> + <SettingAccordion + icon={mdiDevices} + key="authorized-devices" + title={$t('authorized_devices')} + subtitle={$t('manage_your_devices')} + > <DeviceList bind:devices={sessions} /> </SettingAccordion> <SettingAccordion + icon={mdiDownload} key="download-settings" title={$t('download_settings')} subtitle={$t('download_settings_description')} @@ -53,16 +77,27 @@ <DownloadSettings /> </SettingAccordion> - <SettingAccordion key="feature" title={$t('features')} subtitle={$t('features_setting_description')}> + <SettingAccordion + icon={mdiFeatureSearchOutline} + key="feature" + title={$t('features')} + subtitle={$t('features_setting_description')} + > <FeatureSettings /> </SettingAccordion> - <SettingAccordion key="notifications" title={$t('notifications')} subtitle={$t('notifications_setting_description')}> + <SettingAccordion + icon={mdiBellOutline} + key="notifications" + title={$t('notifications')} + subtitle={$t('notifications_setting_description')} + > <NotificationsSettings /> </SettingAccordion> {#if $featureFlags.loaded && $featureFlags.oauth} <SettingAccordion + icon={mdiTwoFactorAuthentication} key="oauth" title={$t('oauth')} subtitle={$t('manage_your_oauth_connection')} @@ -72,15 +107,21 @@ </SettingAccordion> {/if} - <SettingAccordion key="password" title={$t('password')} subtitle={$t('change_your_password')}> + <SettingAccordion icon={mdiOnepassword} key="password" title={$t('password')} subtitle={$t('change_your_password')}> <ChangePasswordSettings /> </SettingAccordion> - <SettingAccordion key="partner-sharing" title={$t('partner_sharing')} subtitle={$t('manage_sharing_with_partners')}> + <SettingAccordion + icon={mdiAccountGroupOutline} + key="partner-sharing" + title={$t('partner_sharing')} + subtitle={$t('manage_sharing_with_partners')} + > <PartnerSettings user={$user} /> </SettingAccordion> <SettingAccordion + icon={mdiKeyOutline} key="user-purchase-settings" title={$t('user_purchase_settings')} subtitle={$t('user_purchase_settings_description')} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index a788666050..aaa3c77e2b 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -1080,6 +1080,7 @@ "search_options": "Search options", "search_people": "Search people", "search_places": "Search places", + "search_settings": "Search settings", "search_state": "Search state...", "search_tags": "Search tags...", "search_timezone": "Search timezone...", diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index d03865cb39..e443c1b518 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -27,11 +27,33 @@ import { copyToClipboard } from '$lib/utils'; import { downloadBlob } from '$lib/utils/asset-utils'; import type { SystemConfigDto } from '@immich/sdk'; - import { mdiAlert, mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js'; + import { + mdiAccountOutline, + mdiAlert, + mdiBellOutline, + mdiBookshelf, + mdiContentCopy, + mdiDatabaseOutline, + mdiDownload, + mdiFileDocumentOutline, + mdiFolderOutline, + mdiImageOutline, + mdiLockOutline, + mdiMapMarkerOutline, + mdiPaletteOutline, + mdiRobotOutline, + mdiServerOutline, + mdiSync, + mdiTrashCanOutline, + mdiUpdate, + mdiUpload, + mdiVideoOutline, + } from '@mdi/js'; import type { PageData } from './$types'; import { t } from 'svelte-i18n'; import type { ComponentType, SvelteComponent } from 'svelte'; import type { SettingsComponentProps } from '$lib/components/admin-page/settings/admin-settings'; + import SearchBar from '$lib/components/elements/search-bar.svelte'; export let data: PageData; @@ -68,104 +90,128 @@ title: string; subtitle: string; key: string; + icon: string; }> = [ { component: AuthSettings, title: $t('admin.authentication_settings'), subtitle: $t('admin.authentication_settings_description'), - key: 'image', + key: 'authentication', + icon: mdiLockOutline, }, { component: ImageSettings, title: $t('admin.image_settings'), subtitle: $t('admin.image_settings_description'), key: 'image', + icon: mdiImageOutline, }, { component: JobSettings, title: $t('admin.job_settings'), subtitle: $t('admin.job_settings_description'), key: 'job', + icon: mdiSync, }, { component: MetadataSettings, title: $t('admin.metadata_settings'), subtitle: $t('admin.metadata_settings_description'), key: 'metadata', + icon: mdiDatabaseOutline, }, { component: LibrarySettings, title: $t('admin.library_settings'), subtitle: $t('admin.library_settings_description'), key: 'external-library', + icon: mdiBookshelf, }, { component: LoggingSettings, title: $t('admin.logging_settings'), subtitle: $t('admin.manage_log_settings'), key: 'logging', + icon: mdiFileDocumentOutline, }, { component: MachineLearningSettings, title: $t('admin.machine_learning_settings'), subtitle: $t('admin.machine_learning_settings_description'), key: 'machine-learning', + icon: mdiRobotOutline, }, { component: MapSettings, title: $t('admin.map_gps_settings'), subtitle: $t('admin.map_gps_settings_description'), key: 'location', + icon: mdiMapMarkerOutline, }, { component: NotificationSettings, title: $t('admin.notification_settings'), subtitle: $t('admin.notification_settings_description'), key: 'notifications', + icon: mdiBellOutline, }, { component: ServerSettings, title: $t('admin.server_settings'), subtitle: $t('admin.server_settings_description'), key: 'server', + icon: mdiServerOutline, }, { component: StorageTemplateSettings, title: $t('admin.storage_template_settings'), subtitle: $t('admin.storage_template_settings_description'), key: 'storage-template', + icon: mdiFolderOutline, }, { component: ThemeSettings, title: $t('admin.theme_settings'), subtitle: $t('admin.theme_settings_description'), key: 'theme', + icon: mdiPaletteOutline, }, { component: TrashSettings, title: $t('admin.trash_settings'), subtitle: $t('admin.trash_settings_description'), key: 'trash', + icon: mdiTrashCanOutline, }, { component: UserSettings, title: $t('admin.user_settings'), subtitle: $t('admin.user_settings_description'), key: 'user-settings', + icon: mdiAccountOutline, }, { component: NewVersionCheckSettings, title: $t('admin.version_check_settings'), subtitle: $t('admin.version_check_settings_description'), key: 'version-check', + icon: mdiUpdate, }, { component: FFmpegSettings, title: $t('admin.transcoding_settings'), subtitle: $t('admin.transcoding_settings_description'), key: 'video-transcoding', + icon: mdiVideoOutline, }, ]; + + let searchQuery = ''; + + $: filteredSettings = settings.filter(({ title, subtitle }) => { + const query = searchQuery.toLowerCase(); + return title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query); + }); </script> <input bind:this={inputElement} type="file" accept=".json" style="display: none" on:change={uploadConfig} /> @@ -182,6 +228,9 @@ <UserPageLayout title={data.meta.title} admin> <div class="flex justify-end gap-2" slot="buttons"> + <div class="hidden lg:block"> + <SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} /> + </div> <LinkButton on:click={() => copyToClipboard(JSON.stringify(config, null, 2))}> <div class="flex place-items-center gap-2 text-sm"> <Icon path={mdiContentCopy} size="18" /> @@ -206,10 +255,13 @@ <AdminSettings bind:config let:handleReset bind: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]"> + <section class="w-full pb-28 sm:w-5/6 md:w-[896px]"> + <div class="block lg:hidden"> + <SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} /> + </div> <SettingAccordionState queryParam={QueryParameter.IS_OPEN}> - {#each settings as { component: Component, title, subtitle, key }} - <SettingAccordion {title} {subtitle} {key}> + {#each filteredSettings as { component: Component, title, subtitle, key, icon } (key)} + <SettingAccordion {title} {subtitle} {key} {icon}> <Component onSave={(config) => handleSave(config)} onReset={(options) => handleReset(options)}