mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
feat(web): auto switch theme (#6176)
* move from app.html to user-page-layout.svelte * fix: use layout.svelte * simplify * fix: map style don't change * fix: auto switch theme map * use constants * simplify * rename * rename settign * fix: remove * pr feedback * fix: tests * fix: migration * fix: migration * pr feedback * simplify * simplify * pr feedback * fix: merge * chore: set insetad of toggle on click --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
41a32b4e6b
commit
84e60ea155
9 changed files with 144 additions and 52 deletions
|
@ -18,11 +18,37 @@
|
|||
/**
|
||||
* Prevent FOUC on page load.
|
||||
*/
|
||||
const theme = localStorage.getItem('color-theme') || 'dark';
|
||||
if (theme === 'light') {
|
||||
const colorThemeKeyName = 'color-theme';
|
||||
let theme;
|
||||
const fallbackTheme = { value: 'dark', system: false };
|
||||
let item = localStorage.getItem(colorThemeKeyName);
|
||||
if (item === 'dark' || item === 'light') {
|
||||
fallbackTheme.value = item;
|
||||
item = JSON.stringify(fallbackTheme);
|
||||
localStorage.setItem(colorThemeKeyName, item);
|
||||
}
|
||||
|
||||
theme = JSON.parse(localStorage.getItem(colorThemeKeyName));
|
||||
|
||||
if (theme) {
|
||||
if (theme.system) {
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
theme.value = 'dark';
|
||||
} else {
|
||||
theme.value = 'light';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
theme = fallbackTheme;
|
||||
}
|
||||
|
||||
if (theme.value === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" href="/custom.css" />
|
||||
</head>
|
||||
|
||||
|
|
|
@ -6,19 +6,12 @@
|
|||
import { createEventDispatcher } from 'svelte';
|
||||
import { colorTheme } from '$lib/stores/preferences.store';
|
||||
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
||||
import { Theme } from '$lib/constants';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
done: void;
|
||||
previous: void;
|
||||
}>();
|
||||
|
||||
const toggleLightTheme = () => {
|
||||
$colorTheme = 'light';
|
||||
};
|
||||
|
||||
const toggleDarkTheme = () => {
|
||||
$colorTheme = 'dark';
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnboardingCard>
|
||||
|
@ -31,7 +24,7 @@
|
|||
<div class="flex gap-4 mb-6">
|
||||
<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"
|
||||
on:click={toggleLightTheme}
|
||||
on:click={() => ($colorTheme.value = Theme.LIGHT)}
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-primary"
|
||||
|
@ -42,7 +35,7 @@
|
|||
</button>
|
||||
<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"
|
||||
on:click={toggleDarkTheme}
|
||||
on:click={() => ($colorTheme.value = Theme.DARK)}
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-items-center place-content-center justify-around h-full w-full text-immich-dark-primary"
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiCog, mdiMapMarker } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { Theme } from '$lib/constants';
|
||||
|
||||
export let mapMarkers: MapMarkerResponseDto[];
|
||||
export let showSettingsModal: boolean | undefined = undefined;
|
||||
|
@ -36,7 +37,7 @@
|
|||
|
||||
$: style = (async () => {
|
||||
const { data } = await api.systemConfigApi.getMapStyle({
|
||||
theme: $mapSettings.allowDarkMode ? $colorTheme : 'light',
|
||||
theme: $mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT,
|
||||
});
|
||||
return data as StyleSpecification;
|
||||
})();
|
||||
|
|
|
@ -1,29 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { moonPath, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
||||
import { colorTheme } from '$lib/stores/preferences.store';
|
||||
import { moonPath, moonViewBox, sunPath, sunViewBox } from '$lib/assets/svg-paths';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { colorTheme, handleToggleTheme } from '$lib/stores/preferences.store';
|
||||
import IconButton from '../elements/buttons/icon-button.svelte';
|
||||
import Icon from '../elements/icon.svelte';
|
||||
|
||||
const toggleTheme = () => {
|
||||
$colorTheme = $colorTheme === 'dark' ? 'light' : 'dark';
|
||||
};
|
||||
|
||||
$: {
|
||||
if (browser) {
|
||||
if ($colorTheme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<IconButton on:click={toggleTheme} title="Toggle theme">
|
||||
{#if $colorTheme === 'light'}
|
||||
<Icon path={moonPath} viewBox={sunViewBox} class="h-6 w-6" />
|
||||
{:else}
|
||||
<Icon path={sunPath} viewBox={sunViewBox} class="h-6 w-6" />
|
||||
{/if}
|
||||
</IconButton>
|
||||
{#if !$colorTheme.system}
|
||||
<IconButton on:click={handleToggleTheme} title="Toggle theme">
|
||||
{#if $colorTheme.value === Theme.LIGHT}
|
||||
<Icon path={moonPath} viewBox={sunViewBox} class="h-6 w-6" />
|
||||
{:else}
|
||||
<Icon path={sunPath} viewBox={moonViewBox} class="h-6 w-6" />
|
||||
{/if}
|
||||
</IconButton>
|
||||
{/if}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { colorTheme } from '../../stores/preferences.store';
|
||||
import SettingSwitch from '../admin-page/settings/setting-switch.svelte';
|
||||
|
||||
export const handleToggle = () => {
|
||||
$colorTheme.system = !$colorTheme.system;
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ml-4">
|
||||
<SettingSwitch
|
||||
title="Theme selection"
|
||||
subtitle="Automatically set the theme to light or dark based on your browser's system preference"
|
||||
bind:checked={$colorTheme.system}
|
||||
on:toggle={handleToggle}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -14,6 +14,7 @@
|
|||
import UserAPIKeyList from './user-api-key-list.svelte';
|
||||
import UserProfileSettings from './user-profile-settings.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import AppearanceSettings from './appearance-settings.svelte';
|
||||
|
||||
export let keys: APIKeyResponseDto[] = [];
|
||||
export let devices: AuthDeviceResponseDto[] = [];
|
||||
|
@ -24,6 +25,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<SettingAccordion title="Appearance" subtitle="Manage your Immich appearance">
|
||||
<AppearanceSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Account" subtitle="Manage your account">
|
||||
<UserProfileSettings user={$user} />
|
||||
</SettingAccordion>
|
||||
|
|
|
@ -57,3 +57,9 @@ export const dateFormats = {
|
|||
year: 'numeric',
|
||||
},
|
||||
};
|
||||
|
||||
// should be the same values as the ones in the app.html
|
||||
export enum Theme {
|
||||
LIGHT = 'light',
|
||||
DARK = 'dark',
|
||||
}
|
||||
|
|
|
@ -1,13 +1,42 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { Theme } from '$lib/constants';
|
||||
import { persisted } from 'svelte-local-storage-store';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
const initialTheme = browser && !window.matchMedia('(prefers-color-scheme: dark)').matches ? 'light' : 'dark';
|
||||
export interface ThemeSetting {
|
||||
value: Theme;
|
||||
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) {
|
||||
if (!window.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<'dark' | 'light'>('color-theme', initialTheme, {
|
||||
export const colorTheme = persisted<ThemeSetting>('color-theme', initialTheme, {
|
||||
serializer: {
|
||||
parse: (text) => (text === 'light' ? text : 'dark'),
|
||||
stringify: (obj) => obj,
|
||||
parse: (text: string): ThemeSetting => {
|
||||
const parsedText: ThemeSetting = JSON.parse(text);
|
||||
if (Object.values(Theme).includes(parsedText.value)) {
|
||||
return parsedText;
|
||||
} else {
|
||||
return initTheme();
|
||||
}
|
||||
},
|
||||
stringify: (obj) => JSON.stringify(obj),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -11,15 +11,15 @@
|
|||
import UploadCover from '$lib/components/shared-components/drag-and-drop-upload-overlay.svelte';
|
||||
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
|
||||
import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { loadConfig } from '$lib/stores/server-config.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { api } from '@api';
|
||||
import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { browser } from '$app/environment';
|
||||
import { colorTheme } from '$lib/stores/preferences.store';
|
||||
import { ThemeSetting, colorTheme, handleToggleTheme } from '$lib/stores/preferences.store';
|
||||
import { Theme } from '$lib/constants';
|
||||
|
||||
let showNavigationLoadingBar = false;
|
||||
let albumId: string | undefined;
|
||||
|
@ -27,15 +27,35 @@
|
|||
const isSharedLinkRoute = (route: string | null) => route?.startsWith('/(user)/share/[key]');
|
||||
const isAuthRoute = (route?: string) => route?.startsWith('/auth');
|
||||
|
||||
$: {
|
||||
if (browser) {
|
||||
if ($colorTheme === 'light') {
|
||||
document.documentElement.classList.remove('dark');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
$: changeTheme($colorTheme);
|
||||
|
||||
const changeTheme = (theme: ThemeSetting) => {
|
||||
if (theme.system) {
|
||||
theme.value =
|
||||
window.matchMedia && window.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();
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
// if the browser theme changes, changes the Immich theme too
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handleChangeTheme);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
document.removeEventListener('change', handleChangeTheme);
|
||||
});
|
||||
|
||||
if (isSharedLinkRoute($page.route?.id)) {
|
||||
api.setKey($page.params.key);
|
||||
|
|
Loading…
Reference in a new issue