mirror of
https://github.com/immich-app/immich.git
synced 2026-05-30 19:35:19 -04:00
feat: upgrade immich/ui (#27792)
This commit is contained in:
@@ -2219,6 +2219,8 @@
|
||||
"sync_status": "Sync Status",
|
||||
"sync_status_subtitle": "View and manage the sync system",
|
||||
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
||||
"system_theme": "System theme",
|
||||
"system_theme_command_description": "Use the system theme ({value})",
|
||||
"tag": "Tag",
|
||||
"tag_assets": "Tag assets",
|
||||
"tag_created": "Created tag: {tag}",
|
||||
|
||||
Generated
+9
-9
@@ -741,8 +741,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../open-api/typescript-sdk
|
||||
'@immich/ui':
|
||||
specifier: ^0.71.0
|
||||
version: 0.71.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)
|
||||
specifier: ^0.76.0
|
||||
version: 0.76.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)
|
||||
'@mapbox/mapbox-gl-rtl-text':
|
||||
specifier: 0.3.0
|
||||
version: 0.3.0
|
||||
@@ -3042,13 +3042,13 @@ packages:
|
||||
resolution: {integrity: sha512-UWhy/+Lf8C1dJip5wPfFytI3Vq/9UyDKQE1ROjXwVhT6E/CPgBkRLwHPetjYGPJ4o1JVVpRLnEEJCXdvzqVpGw==}
|
||||
hasBin: true
|
||||
|
||||
'@immich/svelte-markdown-preprocess@0.3.0':
|
||||
resolution: {integrity: sha512-6xspWnOgaTi+TasteJgI6DjOGjBQQI30mOYiY/FnyEjczNbrV6r5SFWjNbR+JY+Umn7MsPcZf5yzomK+q5AThg==}
|
||||
'@immich/svelte-markdown-preprocess@0.4.1':
|
||||
resolution: {integrity: sha512-/N5dhu3fnRZUoZ+Z9hrIV61o9wi6Uf70TDxqiinXNYlXfqP81p1o77Z5mhbxtNigTNcp6GwpGeHAXRHQrU9JAQ==}
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
'@immich/ui@0.71.0':
|
||||
resolution: {integrity: sha512-L5of/qSNlliTLAF4aoHYXsshs+JLeuX9+r685RED6LsZIR0mObb33SJcniGlPqbi5oyELI+7Qp/cEoyS7TPqwg==}
|
||||
'@immich/ui@0.76.0':
|
||||
resolution: {integrity: sha512-ghxfbC47UPMwQJ65maOUYdduQ/G/zo87Oc2ZUKe6o8KgoHsWxLVjQUw44T3dZdFOhvyS8SsIlkGLuagVcrM9Bg==}
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
@@ -15225,16 +15225,16 @@ snapshots:
|
||||
pg-connection-string: 2.12.0
|
||||
postgres: 3.4.8
|
||||
|
||||
'@immich/svelte-markdown-preprocess@0.3.0(svelte@5.55.1)':
|
||||
'@immich/svelte-markdown-preprocess@0.4.1(svelte@5.55.1)':
|
||||
dependencies:
|
||||
front-matter: 4.0.2
|
||||
marked: 17.0.5
|
||||
node-emoji: 2.2.0
|
||||
svelte: 5.55.1
|
||||
|
||||
'@immich/ui@0.71.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)':
|
||||
'@immich/ui@0.76.0(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)':
|
||||
dependencies:
|
||||
'@immich/svelte-markdown-preprocess': 0.3.0(svelte@5.55.1)
|
||||
'@immich/svelte-markdown-preprocess': 0.4.1(svelte@5.55.1)
|
||||
'@internationalized/date': 3.12.0
|
||||
'@mdi/js': 7.4.47
|
||||
bits-ui: 2.16.3(@internationalized/date@3.12.0)(@sveltejs/kit@2.57.1(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.1)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@6.0.2)(vite@8.0.5(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.6.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^3.0.0",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@immich/sdk": "workspace:*",
|
||||
"@immich/ui": "^0.71.0",
|
||||
"@immich/ui": "^0.76.0",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.3.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
|
||||
+25
-44
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<html class="dark">
|
||||
<head>
|
||||
<!-- (used for SSR) -->
|
||||
<!-- metadata:tags -->
|
||||
@@ -15,7 +15,22 @@
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180.png" />
|
||||
<link rel="preload" as="font" type="font/ttf" href="%app.font%" crossorigin="anonymous" />
|
||||
<link rel="preload" as="font" type="font/ttf" href="%app.monofont%" crossorigin="anonymous" />
|
||||
|
||||
<script>
|
||||
try {
|
||||
const preference = JSON.parse(localStorage.getItem('immich-ui-theme'));
|
||||
const prefersDark = globalThis.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (preference === 'light' || (preference !== 'dark' && !prefersDark)) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
document.documentElement.classList.add('light');
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
</script>
|
||||
|
||||
%sveltekit.head%
|
||||
|
||||
<style>
|
||||
/* prevent FOUC */
|
||||
html {
|
||||
@@ -23,6 +38,14 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
html.light {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
@@ -57,53 +80,11 @@
|
||||
0s linear 0.3s forwards delayedVisibility,
|
||||
loadspin 8s linear infinite;
|
||||
}
|
||||
|
||||
.bg-immich-bg {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.dark .dark\:bg-immich-dark-bg {
|
||||
background-color: black;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
/**
|
||||
* Prevent FOUC on page load.
|
||||
*/
|
||||
const colorThemeKeyName = 'color-theme';
|
||||
|
||||
let theme = localStorage.getItem(colorThemeKeyName);
|
||||
if (!theme) {
|
||||
theme = { value: 'light', system: true };
|
||||
} else if (theme === 'dark' || theme === 'light') {
|
||||
theme = { value: theme, system: false };
|
||||
localStorage.setItem(colorThemeKeyName, JSON.stringify(theme));
|
||||
} else {
|
||||
theme = JSON.parse(theme);
|
||||
}
|
||||
|
||||
let themeValue = theme.value;
|
||||
if (theme.system) {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
themeValue = 'dark';
|
||||
} else {
|
||||
themeValue = 'light';
|
||||
}
|
||||
}
|
||||
|
||||
if (themeValue === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="/custom.css" />
|
||||
</head>
|
||||
|
||||
<noscript
|
||||
class="absolute z-1000 flex h-screen w-screen place-content-center place-items-center bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg"
|
||||
>
|
||||
<noscript style="position: absolute; top: 0px; z-index: 1000; color: black; background-color: white">
|
||||
To use Immich, you must enable JavaScript or use a JavaScript compatible browser.
|
||||
</noscript>
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { defaultProvider, screencastManager, themeManager, ThemePreference, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountMultipleOutline,
|
||||
mdiBookshelf,
|
||||
mdiCog,
|
||||
mdiKeyboard,
|
||||
mdiServer,
|
||||
mdiSync,
|
||||
mdiThemeLightDark,
|
||||
} from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
export const getPagesProvider = ($t: MessageFormatter) => {
|
||||
const adminPages: ActionItem[] = [
|
||||
{
|
||||
title: $t('admin.user_management'),
|
||||
description: $t('admin.users_page_description'),
|
||||
icon: mdiAccountMultipleOutline,
|
||||
onAction: () => goto(Route.users()),
|
||||
},
|
||||
{
|
||||
title: $t('admin.system_settings'),
|
||||
description: $t('admin.settings_page_description'),
|
||||
icon: mdiCog,
|
||||
onAction: () => goto(Route.systemSettings()),
|
||||
},
|
||||
{
|
||||
title: $t('admin.queues'),
|
||||
description: $t('admin.queues_page_description'),
|
||||
icon: mdiSync,
|
||||
onAction: () => goto(Route.queues()),
|
||||
},
|
||||
{
|
||||
title: $t('external_libraries'),
|
||||
description: $t('admin.external_libraries_page_description'),
|
||||
icon: mdiBookshelf,
|
||||
onAction: () => goto(Route.libraries()),
|
||||
},
|
||||
{
|
||||
title: $t('server_stats'),
|
||||
description: $t('admin.server_stats_page_description'),
|
||||
icon: mdiServer,
|
||||
onAction: () => goto(Route.systemStatistics()),
|
||||
},
|
||||
].map((route) => ({ ...route, $if: () => authManager.authenticated && authManager.user.isAdmin }));
|
||||
|
||||
return defaultProvider({ name: $t('page'), actions: adminPages });
|
||||
};
|
||||
|
||||
const getMyImmichLink = () => {
|
||||
return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
|
||||
};
|
||||
|
||||
export const getSettingsProvider = ($t: MessageFormatter) => {
|
||||
const settings: ActionItem[] = [
|
||||
{
|
||||
title: $t('theme'),
|
||||
description: $t('toggle_theme_description'),
|
||||
icon: mdiThemeLightDark,
|
||||
onAction: () => themeManager.toggle(),
|
||||
shortcuts: { shift: true, key: 't' },
|
||||
},
|
||||
{
|
||||
title: $t('system_theme'),
|
||||
description: $t('system_theme_command_description', {
|
||||
values: { value: themeManager.prefersDark ? $t('dark') : $t('light') },
|
||||
}),
|
||||
icon: mdiThemeLightDark,
|
||||
onAction: () => themeManager.setPreference(ThemePreference.System),
|
||||
},
|
||||
{
|
||||
title: $t('screencast_mode_title'),
|
||||
description: $t('screencast_mode_description'),
|
||||
icon: mdiKeyboard,
|
||||
onAction: () => screencastManager.toggle(),
|
||||
},
|
||||
{
|
||||
title: $t('my_immich_title'),
|
||||
description: $t('my_immich_description'),
|
||||
onAction: () => copyToClipboard(getMyImmichLink().toString()),
|
||||
shortcuts: { ctrl: true, shift: true, key: 'm' },
|
||||
},
|
||||
];
|
||||
|
||||
return defaultProvider({ name: $t('command'), actions: settings });
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||
import type { QueueSnapshot } from '$lib/types';
|
||||
import type { QueueResponseDto } from '@immich/sdk';
|
||||
import { LoadingSpinner, Theme, theme } from '@immich/ui';
|
||||
import { LoadingSpinner, Theme, themeManager } from '@immich/ui';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount } from 'svelte';
|
||||
import uPlot, { type AlignedData, type Axis } from 'uplot';
|
||||
@@ -55,7 +55,7 @@
|
||||
const data = $derived(normalizeData(queueManager.snapshots));
|
||||
|
||||
let chartElement: HTMLDivElement | undefined = $state();
|
||||
let isDark = $derived(theme.value === Theme.Dark);
|
||||
let isDark = $derived(themeManager.value === Theme.Dark);
|
||||
let plot: uPlot;
|
||||
|
||||
const axisOptions: Axis = {
|
||||
@@ -138,7 +138,7 @@
|
||||
|
||||
const onThemeChange = () => plot?.redraw(false);
|
||||
|
||||
$effect(() => theme.value && onThemeChange());
|
||||
$effect(() => themeManager.value && onThemeChange());
|
||||
|
||||
onMount(() => {
|
||||
plot = new uPlot(options, data as AlignedData, chartElement);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { Icon, themeManager, ThemePreference } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
@@ -13,7 +11,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="w-1/2 aspect-square bg-light dark:bg-dark rounded-3xl transition-all shadow-sm hover:shadow-xl border-[3px] border-immich-primary dark:border dark:border-transparent"
|
||||
onclick={() => themeManager.setTheme(Theme.LIGHT)}
|
||||
onclick={() => themeManager.setPreference(ThemePreference.Light)}
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-primary"
|
||||
@@ -25,7 +23,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="w-1/2 aspect-square bg-dark dark:bg-light rounded-3xl transition-all shadow-sm hover:shadow-xl dark:border-[3px] dark:border-immich-dark-primary border border-transparent"
|
||||
onclick={() => themeManager.setTheme(Theme.DARK)}
|
||||
onclick={() => themeManager.setPreference(ThemePreference.Dark)}
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-dark-primary"
|
||||
|
||||
@@ -11,14 +11,12 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import MapSettingsModal from '$lib/modals/MapSettingsModal.svelte';
|
||||
import { mapSettings } from '$lib/stores/preferences.store';
|
||||
import { getAssetMediaUrl, handlePromiseError } from '$lib/utils';
|
||||
import { getMapMarkers, type MapMarkerResponseDto } from '@immich/sdk';
|
||||
import { Icon, modalManager } from '@immich/ui';
|
||||
import { Icon, modalManager, Theme, themeManager } from '@immich/ui';
|
||||
import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
|
||||
import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
|
||||
import { isEqual, omit } from 'lodash-es';
|
||||
@@ -106,9 +104,9 @@
|
||||
let marker: Marker | null = null;
|
||||
let abortController: AbortController;
|
||||
|
||||
const theme = $derived($mapSettings.allowDarkMode ? themeManager.value : Theme.LIGHT);
|
||||
const mapTheme = $derived($mapSettings.allowDarkMode ? themeManager.value : Theme.Light);
|
||||
const styleUrl = $derived(
|
||||
theme === Theme.DARK ? serverConfigManager.value.mapDarkStyleUrl : serverConfigManager.value.mapLightStyleUrl,
|
||||
mapTheme === Theme.Dark ? serverConfigManager.value.mapDarkStyleUrl : serverConfigManager.value.mapLightStyleUrl,
|
||||
);
|
||||
|
||||
export function addClipMapMarker(lng: number, lat: number) {
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import { ThemeSwitcher } from '@immich/ui';
|
||||
|
||||
const handleToggleTheme = () => {
|
||||
if (themeManager.theme.system) {
|
||||
return;
|
||||
}
|
||||
|
||||
themeManager.toggleTheme();
|
||||
};
|
||||
import { themeManager, ThemePreference, ThemeSwitcher } from '@immich/ui';
|
||||
</script>
|
||||
|
||||
<svelte:window use:shortcut={{ shortcut: { key: 't', alt: true }, onShortcut: () => handleToggleTheme() }} />
|
||||
|
||||
{#if !themeManager.theme.system}
|
||||
<ThemeSwitcher
|
||||
size="medium"
|
||||
color="secondary"
|
||||
onChange={(theme) => themeManager.setTheme(theme == 'dark' ? Theme.DARK : Theme.LIGHT)}
|
||||
/>
|
||||
{#if themeManager.preference !== ThemePreference.System}
|
||||
<ThemeSwitcher size="medium" color="secondary" />
|
||||
{/if}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import SettingCombobox from '$lib/components/shared-components/settings/setting-combobox.svelte';
|
||||
import SettingsLanguageSelector from '$lib/components/shared-components/settings/settings-language-selector.svelte';
|
||||
import { fallbackLocale, locales } from '$lib/constants';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import {
|
||||
alwaysLoadOriginalFile,
|
||||
alwaysLoadOriginalVideo,
|
||||
@@ -14,7 +13,7 @@
|
||||
showDeleteModal,
|
||||
} from '$lib/stores/preferences.store';
|
||||
import { createDateFormatter, findLocale } from '$lib/utils';
|
||||
import { Field, Switch, Text } from '@immich/ui';
|
||||
import { Field, Switch, Text, Theme, themeManager, ThemePreference } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -55,13 +54,21 @@
|
||||
value: findLocale(editedLocale).code || fallbackLocale.code,
|
||||
label: findLocale(editedLocale).name || fallbackLocale.name,
|
||||
});
|
||||
|
||||
const handleToggleSystemTheme = (checked: boolean) => {
|
||||
const current = themeManager.value === Theme.Dark ? ThemePreference.Dark : ThemePreference.Light;
|
||||
themeManager.setPreference(checked ? ThemePreference.System : current);
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<div class="sm:ms-8 flex flex-col gap-6">
|
||||
<Field label={$t('theme_selection')} description={$t('theme_selection_description')}>
|
||||
<Switch checked={themeManager.theme.system} onCheckedChange={(checked) => themeManager.setSystem(checked)} />
|
||||
<Switch
|
||||
checked={themeManager.preference === ThemePreference.System}
|
||||
onCheckedChange={handleToggleSystemTheme}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<SettingsLanguageSelector showSettingDescription />
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import type { WorkflowPayload } from '$lib/services/workflow.service';
|
||||
import { Button, Card, CardBody, CardDescription, CardHeader, CardTitle, Icon, VStack } from '@immich/ui';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Icon,
|
||||
Theme,
|
||||
themeManager,
|
||||
VStack,
|
||||
} from '@immich/ui';
|
||||
import { mdiCodeJson } from '@mdi/js';
|
||||
import { JSONEditor, Mode, type Content, type OnChangeStatus } from 'svelte-jsoneditor';
|
||||
|
||||
@@ -15,7 +25,7 @@
|
||||
|
||||
let content: Content = $derived({ json: jsonContent });
|
||||
let canApply = $state(false);
|
||||
let editorClass = $derived(themeManager.isDark ? 'jse-theme-dark' : '');
|
||||
let editorClass = $derived(themeManager.value === Theme.Dark ? 'jse-theme-dark' : '');
|
||||
|
||||
const handleChange = (updated: Content, _: Content, status: OnChangeStatus) => {
|
||||
if (status.contentErrors) {
|
||||
|
||||
@@ -78,12 +78,6 @@ export const timeBeforeShowLoadingSpinner: number = 100;
|
||||
|
||||
export const timeDebounceOnSearch: number = 300;
|
||||
|
||||
// should be the same values as the ones in the app.html
|
||||
export enum Theme {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark',
|
||||
}
|
||||
|
||||
export const fallbackLocale = {
|
||||
code: 'en-US',
|
||||
name: 'English (US)',
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { ThemeSetting } from '$lib/managers/theme-manager.svelte';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
|
||||
import type { TreeNode } from '$lib/utils/tree-utils';
|
||||
@@ -27,7 +26,6 @@ export type Events = {
|
||||
AuthUserLoaded: [UserAdminResponseDto];
|
||||
|
||||
LanguageChange: [{ name: string; code: string; rtl?: boolean }];
|
||||
ThemeChange: [ThemeSetting];
|
||||
|
||||
ApiKeyCreate: [ApiKeyResponseDto];
|
||||
ApiKeyUpdate: [ApiKeyResponseDto];
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { PersistedLocalStorage } from '$lib/utils/persisted';
|
||||
import { onThemeChange as onUiThemeChange, theme as uiTheme, type Theme as UiTheme } from '@immich/ui';
|
||||
|
||||
export interface ThemeSetting {
|
||||
value: Theme;
|
||||
system: boolean;
|
||||
}
|
||||
|
||||
const getDefaultTheme = () => {
|
||||
if (!browser) {
|
||||
return Theme.DARK;
|
||||
}
|
||||
|
||||
return globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT;
|
||||
};
|
||||
|
||||
class ThemeManager {
|
||||
#theme = new PersistedLocalStorage<ThemeSetting>(
|
||||
'color-theme',
|
||||
{ value: getDefaultTheme(), system: false },
|
||||
{
|
||||
valid: (value): value is ThemeSetting => {
|
||||
return Object.values(Theme).includes((value as ThemeSetting)?.value);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
get theme() {
|
||||
return this.#theme.current;
|
||||
}
|
||||
|
||||
value = $derived(this.theme.value);
|
||||
|
||||
isDark = $derived(this.value === Theme.DARK);
|
||||
|
||||
constructor() {
|
||||
eventManager.on({
|
||||
AppInit: () => this.#onAppInit(),
|
||||
});
|
||||
}
|
||||
|
||||
setSystem(system: boolean) {
|
||||
this.#update(system ? 'system' : getDefaultTheme());
|
||||
}
|
||||
|
||||
setTheme(theme: Theme) {
|
||||
this.#update(theme);
|
||||
}
|
||||
|
||||
toggleTheme() {
|
||||
this.#update(this.value === Theme.DARK ? Theme.LIGHT : Theme.DARK);
|
||||
}
|
||||
|
||||
#onAppInit() {
|
||||
const syncSystemTheme = () => {
|
||||
this.#update(this.theme.system ? 'system' : this.theme.value);
|
||||
};
|
||||
|
||||
syncSystemTheme();
|
||||
globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', syncSystemTheme, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
||||
#update(value: Theme | 'system') {
|
||||
const theme: ThemeSetting =
|
||||
value === 'system' ? { system: true, value: getDefaultTheme() } : { system: false, value };
|
||||
|
||||
document.documentElement.classList.toggle('dark', !(theme.value === Theme.LIGHT));
|
||||
|
||||
this.#theme.current = theme;
|
||||
|
||||
uiTheme.value = theme.value as unknown as UiTheme;
|
||||
onUiThemeChange();
|
||||
|
||||
eventManager.emit('ThemeChange', theme);
|
||||
}
|
||||
}
|
||||
|
||||
export const themeManager = new ThemeManager();
|
||||
@@ -99,7 +99,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
const Share: ActionItem = {
|
||||
title: $t('share'),
|
||||
icon: mdiShareVariantOutline,
|
||||
type: $t('assets'),
|
||||
$if: () => !!(authUser && !asset.isTrashed && asset.visibility !== AssetVisibility.Locked),
|
||||
onAction: () => modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] }),
|
||||
};
|
||||
@@ -108,7 +107,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
title: $t('download'),
|
||||
icon: mdiDownload,
|
||||
shortcuts: { key: 'd', shift: true },
|
||||
type: $t('assets'),
|
||||
$if: () => !!authUser,
|
||||
onAction: () => handleDownloadAsset(asset, { edited: true }),
|
||||
};
|
||||
@@ -116,7 +114,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
const DownloadOriginal: ActionItem = {
|
||||
title: $t('download_original'),
|
||||
icon: mdiDownloadBox,
|
||||
type: $t('assets'),
|
||||
$if: () => !!authUser && asset.isEdited,
|
||||
onAction: () => handleDownloadAsset(asset, { edited: false }),
|
||||
};
|
||||
@@ -208,7 +205,6 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
const Tag: ActionItem = {
|
||||
title: $t('add_tag'),
|
||||
icon: mdiTagPlusOutline,
|
||||
type: $t('assets'),
|
||||
$if: () => authManager.authenticated && authManager.preferences.tags.enabled,
|
||||
onAction: () => modalManager.show(AssetTagModal, { assetIds: [asset.id] }),
|
||||
shortcuts: { key: 't' },
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { Theme, defaultLang } from '$lib/constants';
|
||||
import { defaultLang } from '$lib/constants';
|
||||
import { getPreferredLocale } from '$lib/utils/i18n';
|
||||
import { persisted } from 'svelte-persisted-store';
|
||||
|
||||
export interface ThemeSetting {
|
||||
value: Theme;
|
||||
system: boolean;
|
||||
}
|
||||
|
||||
// Locale to use for formatting dates, numbers, etc.
|
||||
export const locale = persisted('locale', 'default', {
|
||||
serializer: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { getPagesProvider, getSettingsProvider } from '$lib/commands';
|
||||
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||
import ErrorLayout from '$lib/components/layouts/ErrorLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
@@ -10,37 +11,30 @@
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||
import { closeWebsocketConnection, openWebsocketConnection, websocketStore } from '$lib/stores/websocket';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
|
||||
import { getServerConfig } from '@immich/sdk';
|
||||
import {
|
||||
CommandPaletteProvider,
|
||||
CORE_PAGE_COMMANDS,
|
||||
defaultProvider,
|
||||
MOBILE_APP_COMMANDS,
|
||||
modalManager,
|
||||
screencastManager,
|
||||
OTHER_SITE_COMMANDS,
|
||||
PROJECT_SUPPORT_COMMANDS,
|
||||
ScreencastOverlay,
|
||||
setLocale,
|
||||
setTranslations,
|
||||
siteCommands,
|
||||
SOCIAL_COMMANDS,
|
||||
Theme,
|
||||
themeManager,
|
||||
toastManager,
|
||||
TooltipProvider,
|
||||
type ActionItem,
|
||||
} from '@immich/ui';
|
||||
import {
|
||||
mdiAccountMultipleOutline,
|
||||
mdiBookshelf,
|
||||
mdiCog,
|
||||
mdiKeyboard,
|
||||
mdiServer,
|
||||
mdiSync,
|
||||
mdiThemeLightDark,
|
||||
} from '@mdi/js';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
@@ -62,7 +56,7 @@
|
||||
prompt_default: $t('are_you_sure_to_do_this'),
|
||||
show_password: $t('show_password'),
|
||||
hide_password: $t('hide_password'),
|
||||
dark_theme: themeManager.isDark ? $t('light_theme') : $t('dark_theme'),
|
||||
dark_theme: themeManager.value === Theme.Dark ? $t('light_theme') : $t('dark_theme'),
|
||||
open_menu: $t('open'),
|
||||
command_palette_prompt_default: $t('command_palette_prompt'),
|
||||
command_palette_to_select: $t('command_palette_to_select'),
|
||||
@@ -87,10 +81,6 @@
|
||||
|
||||
let showNavigationLoadingBar = $state(false);
|
||||
|
||||
const getMyImmichLink = () => {
|
||||
return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
|
||||
};
|
||||
|
||||
toastManager.setOptions({ class: 'top-16 fixed' });
|
||||
|
||||
onMount(() => {
|
||||
@@ -152,61 +142,6 @@
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const commands: ActionItem[] = [
|
||||
{
|
||||
title: $t('theme'),
|
||||
description: $t('toggle_theme_description'),
|
||||
icon: mdiThemeLightDark,
|
||||
onAction: () => themeManager.toggleTheme(),
|
||||
shortcuts: { shift: true, key: 't' },
|
||||
},
|
||||
{
|
||||
title: $t('screencast_mode_title'),
|
||||
description: $t('screencast_mode_description'),
|
||||
icon: mdiKeyboard,
|
||||
onAction: () => screencastManager.toggle(),
|
||||
},
|
||||
{
|
||||
title: $t('my_immich_title'),
|
||||
description: $t('my_immich_description'),
|
||||
onAction: () => copyToClipboard(getMyImmichLink().toString()),
|
||||
shortcuts: { ctrl: true, shift: true, key: 'm' },
|
||||
},
|
||||
];
|
||||
|
||||
const adminPages: ActionItem[] = [
|
||||
{
|
||||
title: $t('admin.user_management'),
|
||||
description: $t('admin.users_page_description'),
|
||||
icon: mdiAccountMultipleOutline,
|
||||
onAction: () => goto(Route.users()),
|
||||
},
|
||||
{
|
||||
title: $t('admin.system_settings'),
|
||||
description: $t('admin.settings_page_description'),
|
||||
icon: mdiCog,
|
||||
onAction: () => goto(Route.systemSettings()),
|
||||
},
|
||||
{
|
||||
title: $t('admin.queues'),
|
||||
description: $t('admin.queues_page_description'),
|
||||
icon: mdiSync,
|
||||
onAction: () => goto(Route.queues()),
|
||||
},
|
||||
{
|
||||
title: $t('external_libraries'),
|
||||
description: $t('admin.external_libraries_page_description'),
|
||||
icon: mdiBookshelf,
|
||||
onAction: () => goto(Route.libraries()),
|
||||
},
|
||||
{
|
||||
title: $t('server_stats'),
|
||||
description: $t('admin.server_stats_page_description'),
|
||||
icon: mdiServer,
|
||||
onAction: () => goto(Route.systemStatistics()),
|
||||
},
|
||||
].map((route) => ({ ...route, $if: () => authManager.authenticated && authManager.user.isAdmin }));
|
||||
</script>
|
||||
|
||||
<OnEvents {onWebsocketConnect} />
|
||||
@@ -269,9 +204,13 @@
|
||||
|
||||
<CommandPaletteProvider
|
||||
providers={[
|
||||
defaultProvider({ name: $t('command'), actions: commands }),
|
||||
defaultProvider({ name: $t('page'), actions: adminPages }),
|
||||
defaultProvider({ name: $t('link'), actions: siteCommands }),
|
||||
getPagesProvider($t),
|
||||
getSettingsProvider($t),
|
||||
defaultProvider({ name: $t('documentation'), types: ['doc', 'documentation'], actions: CORE_PAGE_COMMANDS }),
|
||||
defaultProvider({ name: $t('support'), actions: PROJECT_SUPPORT_COMMANDS }),
|
||||
defaultProvider({ name: 'Socials', types: ['social', 'socials'], actions: SOCIAL_COMMANDS }),
|
||||
defaultProvider({ name: $t('mobile_app'), actions: MOBILE_APP_COMMANDS }),
|
||||
defaultProvider({ name: 'Sites', types: ['site', 'sites'], actions: OTHER_SITE_COMMANDS }),
|
||||
]}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
|
||||
Reference in New Issue
Block a user