mirror of
https://github.com/immich-app/immich.git
synced 2025-05-30 19:54:52 -04:00
refactor: theme manager (#17976)
This commit is contained in:
parent
2c2dd01bf0
commit
038a82c4f1
@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { mdiArrowRight, mdiThemeLightDark } from '@mdi/js';
|
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import OnboardingCard from './onboarding-card.svelte';
|
|
||||||
import { colorTheme } from '$lib/stores/preferences.store';
|
|
||||||
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
|
||||||
import { Theme } from '$lib/constants';
|
import { Theme } from '$lib/constants';
|
||||||
|
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||||
|
import { mdiArrowRight, mdiThemeLightDark } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import OnboardingCard from './onboarding-card.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
@ -24,7 +24,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-1/2 aspect-square bg-immich-bg rounded-3xl transition-all shadow-sm hover:shadow-xl border-[3px] border-immich-dark-primary/80 border-immich-primary dark:border dark:border-transparent"
|
class="w-1/2 aspect-square bg-immich-bg rounded-3xl transition-all shadow-sm hover:shadow-xl border-[3px] border-immich-dark-primary/80 border-immich-primary dark:border dark:border-transparent"
|
||||||
onclick={() => ($colorTheme.value = Theme.LIGHT)}
|
onclick={() => themeManager.setTheme(Theme.LIGHT)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-primary"
|
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-primary"
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="w-1/2 aspect-square bg-immich-dark-bg rounded-3xl dark:border-[3px] dark:border-immich-dark-primary/80 dark:border-immich-dark-primary border border-transparent"
|
class="w-1/2 aspect-square bg-immich-dark-bg rounded-3xl dark:border-[3px] dark:border-immich-dark-primary/80 dark:border-immich-dark-primary border border-transparent"
|
||||||
onclick={() => ($colorTheme.value = Theme.DARK)}
|
onclick={() => themeManager.setTheme(Theme.DARK)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-dark-primary"
|
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-dark-primary"
|
||||||
|
@ -9,15 +9,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { Theme } from '$lib/constants';
|
import { Theme } from '$lib/constants';
|
||||||
import { colorTheme, mapSettings } from '$lib/stores/preferences.store';
|
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||||
|
import { mapSettings } from '$lib/stores/preferences.store';
|
||||||
import { serverConfig } from '$lib/stores/server-config.store';
|
import { serverConfig } from '$lib/stores/server-config.store';
|
||||||
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||||
import { type MapMarkerResponseDto } from '@immich/sdk';
|
import { type MapMarkerResponseDto } from '@immich/sdk';
|
||||||
import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js?url';
|
import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js?url';
|
||||||
import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
|
import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js';
|
||||||
import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
|
import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
|
||||||
import { type GeoJSONSource, GlobeControl, type LngLatLike } from 'maplibre-gl';
|
import maplibregl, { GlobeControl, type GeoJSONSource, type LngLatLike } from 'maplibre-gl';
|
||||||
import maplibregl from 'maplibre-gl';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import {
|
import {
|
||||||
AttributionControl,
|
AttributionControl,
|
||||||
@ -68,7 +68,7 @@
|
|||||||
let map: maplibregl.Map | undefined = $state();
|
let map: maplibregl.Map | undefined = $state();
|
||||||
let marker: maplibregl.Marker | null = null;
|
let marker: maplibregl.Marker | null = null;
|
||||||
|
|
||||||
const theme = $derived($mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT);
|
const theme = $derived($mapSettings.allowDarkMode ? themeManager.value : Theme.LIGHT);
|
||||||
const styleUrl = $derived(theme === Theme.DARK ? $serverConfig.mapDarkStyleUrl : $serverConfig.mapLightStyleUrl);
|
const styleUrl = $derived(theme === Theme.DARK ? $serverConfig.mapDarkStyleUrl : $serverConfig.mapLightStyleUrl);
|
||||||
|
|
||||||
export function addClipMapMarker(lng: number, lat: number) {
|
export function addClipMapMarker(lng: number, lat: number) {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import QRCode from 'qrcode';
|
|
||||||
import { colorTheme } from '$lib/stores/preferences.store';
|
|
||||||
import { Theme } from '$lib/constants';
|
import { Theme } from '$lib/constants';
|
||||||
|
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||||
|
import QRCode from 'qrcode';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
let promise = $derived(
|
let promise = $derived(
|
||||||
QRCode.toDataURL(value, {
|
QRCode.toDataURL(value, {
|
||||||
color: { dark: $colorTheme.value === Theme.DARK ? '#ffffffff' : '#000000ff', light: '#00000000' },
|
color: { dark: themeManager.value === Theme.DARK ? '#ffffffff' : '#000000ff', light: '#00000000' },
|
||||||
margin: 0,
|
margin: 0,
|
||||||
width,
|
width,
|
||||||
}),
|
}),
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
||||||
import CircleIconButton, { type Padding } from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton, { type Padding } from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import { Theme } from '$lib/constants';
|
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||||
import { colorTheme, handleToggleTheme } from '$lib/stores/preferences.store';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
let icon = $derived($colorTheme.value === Theme.LIGHT ? moonPath : sunPath);
|
let icon = $derived(themeManager.isDark ? sunPath : moonPath);
|
||||||
let viewBox = $derived($colorTheme.value === Theme.LIGHT ? moonViewBox : sunViewBox);
|
let viewBox = $derived(themeManager.isDark ? sunViewBox : moonViewBox);
|
||||||
let isDark = $derived($colorTheme.value === Theme.DARK);
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
padding?: Padding;
|
padding?: Padding;
|
||||||
@ -16,14 +14,14 @@
|
|||||||
let { padding = '3' }: Props = $props();
|
let { padding = '3' }: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !$colorTheme.system}
|
{#if !themeManager.theme.system}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
title={$t('toggle_theme')}
|
title={$t('toggle_theme')}
|
||||||
{icon}
|
{icon}
|
||||||
{viewBox}
|
{viewBox}
|
||||||
role="switch"
|
role="switch"
|
||||||
aria-checked={isDark ? 'true' : 'false'}
|
aria-checked={themeManager.isDark ? 'true' : 'false'}
|
||||||
onclick={handleToggleTheme}
|
onclick={() => themeManager.toggleTheme()}
|
||||||
{padding}
|
{padding}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { invalidateAll } from '$app/navigation';
|
||||||
import type { ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
|
import type { ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
|
||||||
import SettingCombobox from '$lib/components/shared-components/settings/setting-combobox.svelte';
|
import SettingCombobox from '$lib/components/shared-components/settings/setting-combobox.svelte';
|
||||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
import { defaultLang, fallbackLocale, langs, locales } from '$lib/constants';
|
import { defaultLang, fallbackLocale, langs, locales } from '$lib/constants';
|
||||||
|
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||||
import {
|
import {
|
||||||
alwaysLoadOriginalFile,
|
alwaysLoadOriginalFile,
|
||||||
colorTheme,
|
|
||||||
lang,
|
lang,
|
||||||
locale,
|
locale,
|
||||||
loopVideo,
|
loopVideo,
|
||||||
@ -17,7 +18,6 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { locale as i18nLocale, t } from 'svelte-i18n';
|
import { locale as i18nLocale, t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { invalidateAll } from '$app/navigation';
|
|
||||||
|
|
||||||
let time = $state(new Date());
|
let time = $state(new Date());
|
||||||
|
|
||||||
@ -40,10 +40,6 @@
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleColorTheme = () => {
|
|
||||||
$colorTheme.system = !$colorTheme.system;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleToggleLocaleBrowser = () => {
|
const handleToggleLocaleBrowser = () => {
|
||||||
$locale = $locale ? undefined : fallbackLocale.code;
|
$locale = $locale ? undefined : fallbackLocale.code;
|
||||||
};
|
};
|
||||||
@ -101,8 +97,8 @@
|
|||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title={$t('theme_selection')}
|
title={$t('theme_selection')}
|
||||||
subtitle={$t('theme_selection_description')}
|
subtitle={$t('theme_selection_description')}
|
||||||
bind:checked={$colorTheme.system}
|
checked={themeManager.theme.system}
|
||||||
onToggle={handleToggleColorTheme}
|
onToggle={(isChecked) => themeManager.setSystem(isChecked)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { ThemeSetting } from '$lib/managers/theme-manager.svelte';
|
||||||
import type { LoginResponseDto } from '@immich/sdk';
|
import type { LoginResponseDto } from '@immich/sdk';
|
||||||
|
|
||||||
type Listener<EventMap extends Record<string, unknown[]>, K extends keyof EventMap> = (...params: EventMap[K]) => void;
|
type Listener<EventMap extends Record<string, unknown[]>, K extends keyof EventMap> = (...params: EventMap[K]) => void;
|
||||||
@ -56,4 +57,5 @@ export const eventManager = new EventManager<{
|
|||||||
'auth.login': [LoginResponseDto];
|
'auth.login': [LoginResponseDto];
|
||||||
'auth.logout': [];
|
'auth.logout': [];
|
||||||
'language.change': [{ name: string; code: string; rtl?: boolean }];
|
'language.change': [{ name: string; code: string; rtl?: boolean }];
|
||||||
|
'theme.change': [ThemeSetting];
|
||||||
}>();
|
}>();
|
||||||
|
78
web/src/lib/managers/theme-manager.svelte.ts
Normal file
78
web/src/lib/managers/theme-manager.svelte.ts
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { Theme } from '$lib/constants';
|
||||||
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
|
import { PersistedLocalStorage } from '$lib/utils/persisted';
|
||||||
|
|
||||||
|
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('app.init', () => 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() {
|
||||||
|
globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
|
if (this.theme.system) {
|
||||||
|
this.#update('system');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#update(value: Theme | 'system') {
|
||||||
|
const theme: ThemeSetting =
|
||||||
|
value === 'system' ? { system: true, value: getDefaultTheme() } : { system: false, value };
|
||||||
|
|
||||||
|
if (theme.value === Theme.LIGHT) {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#theme.current = theme;
|
||||||
|
|
||||||
|
eventManager.emit('theme.change', theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const themeManager = new ThemeManager();
|
@ -2,39 +2,12 @@ import { browser } from '$app/environment';
|
|||||||
import { Theme, defaultLang } from '$lib/constants';
|
import { Theme, defaultLang } from '$lib/constants';
|
||||||
import { getPreferredLocale } from '$lib/utils/i18n';
|
import { getPreferredLocale } from '$lib/utils/i18n';
|
||||||
import { persisted } from 'svelte-persisted-store';
|
import { persisted } from 'svelte-persisted-store';
|
||||||
import { get } from 'svelte/store';
|
|
||||||
|
|
||||||
export interface ThemeSetting {
|
export interface ThemeSetting {
|
||||||
value: Theme;
|
value: Theme;
|
||||||
system: boolean;
|
system: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleToggleTheme = () => {
|
|
||||||
const theme = get(colorTheme);
|
|
||||||
theme.value = theme.value === Theme.DARK ? Theme.LIGHT : Theme.DARK;
|
|
||||||
colorTheme.set(theme);
|
|
||||||
};
|
|
||||||
|
|
||||||
const initTheme = (): ThemeSetting => {
|
|
||||||
if (browser && globalThis.matchMedia && !globalThis.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
||||||
return { value: Theme.LIGHT, system: false };
|
|
||||||
}
|
|
||||||
return { value: Theme.DARK, system: false };
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialTheme = initTheme();
|
|
||||||
|
|
||||||
// The 'color-theme' key is also used by app.html to prevent FOUC on page load.
|
|
||||||
export const colorTheme = persisted<ThemeSetting>('color-theme', initialTheme, {
|
|
||||||
serializer: {
|
|
||||||
parse: (text: string): ThemeSetting => {
|
|
||||||
const parsedText: ThemeSetting = JSON.parse(text);
|
|
||||||
return Object.values(Theme).includes(parsedText.value) ? parsedText : initTheme();
|
|
||||||
},
|
|
||||||
stringify: (object) => JSON.stringify(object),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Locale to use for formatting dates, numbers, etc.
|
// Locale to use for formatting dates, numbers, etc.
|
||||||
export const locale = persisted<string | undefined>('locale', undefined, {
|
export const locale = persisted<string | undefined>('locale', undefined, {
|
||||||
serializer: {
|
serializer: {
|
||||||
|
81
web/src/lib/utils/persisted.ts
Normal file
81
web/src/lib/utils/persisted.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import { browser } from '$app/environment';
|
||||||
|
import { createSubscriber } from 'svelte/reactivity';
|
||||||
|
|
||||||
|
type PersistedBaseOptions<T> = {
|
||||||
|
read: (key: string) => T | undefined;
|
||||||
|
write: (key: string, value: T) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
class PersistedBase<T> {
|
||||||
|
#value: T;
|
||||||
|
#subscribe: () => void;
|
||||||
|
#update = () => {};
|
||||||
|
|
||||||
|
#write: (value: T) => void;
|
||||||
|
|
||||||
|
get current() {
|
||||||
|
this.#subscribe();
|
||||||
|
return this.#value as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
set current(value: T) {
|
||||||
|
this.#write(value);
|
||||||
|
this.#update();
|
||||||
|
this.#value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(key: string, defaultValue: T, options: PersistedBaseOptions<T>) {
|
||||||
|
const value = options.read(key);
|
||||||
|
|
||||||
|
this.#value = value === undefined ? defaultValue : value;
|
||||||
|
this.#write = (value: T) => options.write(key, value);
|
||||||
|
|
||||||
|
this.#subscribe = createSubscriber((update) => {
|
||||||
|
this.#update = update;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.#update = () => {};
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PersistedLocalStorageOptions<T> = {
|
||||||
|
serializer?: {
|
||||||
|
stringify(value: T): string;
|
||||||
|
parse(text: string): T;
|
||||||
|
};
|
||||||
|
valid?: (value: T | unknown) => value is T;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class PersistedLocalStorage<T> extends PersistedBase<T> {
|
||||||
|
constructor(key: string, defaultValue: T, options: PersistedLocalStorageOptions<T> = {}) {
|
||||||
|
const valid = options.valid || (() => true);
|
||||||
|
const serializer = options.serializer || JSON;
|
||||||
|
|
||||||
|
super(key, defaultValue, {
|
||||||
|
read: (key: string) => {
|
||||||
|
if (!browser) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item = localStorage.getItem(key) ?? undefined;
|
||||||
|
if (item === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = serializer.parse(item);
|
||||||
|
if (!valid(parsed)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
},
|
||||||
|
write: (key: string, value: T) => {
|
||||||
|
if (browser) {
|
||||||
|
localStorage.setItem(key, serializer.stringify(value));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -10,16 +10,14 @@
|
|||||||
import NotificationList from '$lib/components/shared-components/notification/notification-list.svelte';
|
import NotificationList from '$lib/components/shared-components/notification/notification-list.svelte';
|
||||||
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
|
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
|
||||||
import VersionAnnouncementBox from '$lib/components/shared-components/version-announcement-box.svelte';
|
import VersionAnnouncementBox from '$lib/components/shared-components/version-announcement-box.svelte';
|
||||||
import { Theme } from '$lib/constants';
|
|
||||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||||
import { colorTheme, handleToggleTheme, type ThemeSetting } from '$lib/stores/preferences.store';
|
|
||||||
import { serverConfig } from '$lib/stores/server-config.store';
|
import { serverConfig } from '$lib/stores/server-config.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket';
|
import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket';
|
||||||
import { copyToClipboard } from '$lib/utils';
|
import { copyToClipboard } from '$lib/utils';
|
||||||
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
||||||
import { setTranslations } from '@immich/ui';
|
import { setTranslations } from '@immich/ui';
|
||||||
import { onDestroy, onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { run } from 'svelte/legacy';
|
import { run } from 'svelte/legacy';
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
@ -40,24 +38,6 @@
|
|||||||
|
|
||||||
let showNavigationLoadingBar = $state(false);
|
let showNavigationLoadingBar = $state(false);
|
||||||
|
|
||||||
const changeTheme = (theme: ThemeSetting) => {
|
|
||||||
if (theme.system) {
|
|
||||||
theme.value = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (theme.value === Theme.LIGHT) {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeTheme = () => {
|
|
||||||
if ($colorTheme.system) {
|
|
||||||
handleToggleTheme();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMyImmichLink = () => {
|
const getMyImmichLink = () => {
|
||||||
return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
|
return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
|
||||||
};
|
};
|
||||||
@ -66,11 +46,6 @@
|
|||||||
const element = document.querySelector('#stencil');
|
const element = document.querySelector('#stencil');
|
||||||
element?.remove();
|
element?.remove();
|
||||||
// if the browser theme changes, changes the Immich theme too
|
// if the browser theme changes, changes the Immich theme too
|
||||||
globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handleChangeTheme);
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
document.removeEventListener('change', handleChangeTheme);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
eventManager.emit('app.init');
|
eventManager.emit('app.init');
|
||||||
@ -85,9 +60,6 @@
|
|||||||
afterNavigate(() => {
|
afterNavigate(() => {
|
||||||
showNavigationLoadingBar = false;
|
showNavigationLoadingBar = false;
|
||||||
});
|
});
|
||||||
run(() => {
|
|
||||||
changeTheme($colorTheme);
|
|
||||||
});
|
|
||||||
run(() => {
|
run(() => {
|
||||||
if ($user) {
|
if ($user) {
|
||||||
openWebsocketConnection();
|
openWebsocketConnection();
|
||||||
|
@ -4,3 +4,15 @@ import { init } from 'svelte-i18n';
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await init({ fallbackLocale: 'dev' });
|
await init({ fallbackLocale: 'dev' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(globalThis, 'matchMedia', {
|
||||||
|
writable: true,
|
||||||
|
value: vi.fn().mockImplementation((query) => ({
|
||||||
|
matches: false,
|
||||||
|
media: query,
|
||||||
|
onchange: null,
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn(),
|
||||||
|
dispatchEvent: vi.fn(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user