mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web): theme/locale preferences and improve SSR (#1832)
This commit is contained in:
		
							
								
								
									
										3
									
								
								web/__mocks__/$app/environment.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								web/__mocks__/$app/environment.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| module.exports = { | ||||
| 	browser: false | ||||
| }; | ||||
							
								
								
									
										18
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										18
									
								
								web/package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -16,6 +16,7 @@ | ||||
| 				"luxon": "^3.1.1", | ||||
| 				"rxjs": "^7.8.0", | ||||
| 				"socket.io-client": "^4.5.1", | ||||
| 				"svelte-local-storage-store": "^0.4.0", | ||||
| 				"svelte-material-icons": "^2.0.2" | ||||
| 			}, | ||||
| 			"devDependencies": { | ||||
| @@ -10584,6 +10585,17 @@ | ||||
| 				"svelte": ">= 3" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/svelte-local-storage-store": { | ||||
| 			"version": "0.4.0", | ||||
| 			"resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.4.0.tgz", | ||||
| 			"integrity": "sha512-ctPykTt4S3BE5bF0mfV0jKiUR1qlmqLvnAkQvYHLeb9wRyO1MdIFDVI23X+TZEFleATHkTaOpYZswIvf3b2tWA==", | ||||
| 			"engines": { | ||||
| 				"node": ">=0.14" | ||||
| 			}, | ||||
| 			"peerDependencies": { | ||||
| 				"svelte": "^3.48.0" | ||||
| 			} | ||||
| 		}, | ||||
| 		"node_modules/svelte-material-icons": { | ||||
| 			"version": "2.0.4", | ||||
| 			"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz", | ||||
| @@ -19014,6 +19026,12 @@ | ||||
| 			"dev": true, | ||||
| 			"requires": {} | ||||
| 		}, | ||||
| 		"svelte-local-storage-store": { | ||||
| 			"version": "0.4.0", | ||||
| 			"resolved": "https://registry.npmjs.org/svelte-local-storage-store/-/svelte-local-storage-store-0.4.0.tgz", | ||||
| 			"integrity": "sha512-ctPykTt4S3BE5bF0mfV0jKiUR1qlmqLvnAkQvYHLeb9wRyO1MdIFDVI23X+TZEFleATHkTaOpYZswIvf3b2tWA==", | ||||
| 			"requires": {} | ||||
| 		}, | ||||
| 		"svelte-material-icons": { | ||||
| 			"version": "2.0.4", | ||||
| 			"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz", | ||||
|   | ||||
| @@ -68,6 +68,7 @@ | ||||
| 		"luxon": "^3.1.1", | ||||
| 		"rxjs": "^7.8.0", | ||||
| 		"socket.io-client": "^4.5.1", | ||||
| 		"svelte-local-storage-store": "^0.4.0", | ||||
| 		"svelte-material-icons": "^2.0.2" | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -4,6 +4,15 @@ | ||||
| 		<meta charset="utf-8" /> | ||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||
| 		%sveltekit.head% | ||||
| 		<script> | ||||
| 			/** | ||||
| 			 * Prevent FOUC on page load. | ||||
| 			 */ | ||||
| 			const theme = localStorage.getItem('color-theme') || 'dark'; | ||||
| 			if (theme === 'light') { | ||||
| 				document.documentElement.classList.remove('dark'); | ||||
| 			} | ||||
| 		</script> | ||||
| 	</head> | ||||
|  | ||||
| 	<body class="bg-immich-bg dark:bg-immich-dark-bg"> | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| 	import SelectionSearch from 'svelte-material-icons/SelectionSearch.svelte'; | ||||
| 	import Play from 'svelte-material-icons/Play.svelte'; | ||||
| 	import AllInclusive from 'svelte-material-icons/AllInclusive.svelte'; | ||||
|  | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
| 	import { createEventDispatcher } from 'svelte'; | ||||
| 	import { JobCounts } from '@api'; | ||||
|  | ||||
| @@ -22,8 +22,6 @@ | ||||
| 	const run = (includeAllAssets: boolean) => { | ||||
| 		dispatch('click', { includeAllAssets }); | ||||
| 	}; | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| </script> | ||||
|  | ||||
| <div class="flex justify-between rounded-3xl bg-gray-100 dark:bg-immich-dark-gray"> | ||||
| @@ -45,7 +43,7 @@ | ||||
| 					<p>Active</p> | ||||
| 					<p class="text-2xl"> | ||||
| 						{#if jobCounts.active !== undefined} | ||||
| 							{jobCounts.active.toLocaleString(locale)} | ||||
| 							{jobCounts.active.toLocaleString($locale)} | ||||
| 						{:else} | ||||
| 							<LoadingSpinner /> | ||||
| 						{/if} | ||||
| @@ -57,7 +55,7 @@ | ||||
| 				> | ||||
| 					<p class="text-2xl"> | ||||
| 						{#if jobCounts.waiting !== undefined} | ||||
| 							{jobCounts.waiting.toLocaleString(locale)} | ||||
| 							{jobCounts.waiting.toLocaleString($locale)} | ||||
| 						{:else} | ||||
| 							<LoadingSpinner /> | ||||
| 						{/if} | ||||
|   | ||||
| @@ -7,6 +7,7 @@ | ||||
| 	import { getBytesWithUnit, asByteUnitString } from '../../../utils/byte-units'; | ||||
| 	import { onMount, onDestroy } from 'svelte'; | ||||
| 	import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	export let allUsers: Array<UserResponseDto>; | ||||
|  | ||||
| @@ -37,8 +38,6 @@ | ||||
|  | ||||
| 	// Stats are unavailable if data is not loaded yet | ||||
| 	$: [spaceUsage, spaceUnit] = getBytesWithUnit(stats ? stats.usageRaw : 0); | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| </script> | ||||
|  | ||||
| <div class="flex flex-col gap-5"> | ||||
| @@ -83,8 +82,10 @@ | ||||
| 							}`} | ||||
| 						> | ||||
| 							<td class="text-sm px-2 w-1/4 text-ellipsis">{getFullName(user.userId)}</td> | ||||
| 							<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString(locale)}</td> | ||||
| 							<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString(locale)}</td> | ||||
| 							<td class="text-sm px-2 w-1/4 text-ellipsis">{user.photos.toLocaleString($locale)}</td | ||||
| 							> | ||||
| 							<td class="text-sm px-2 w-1/4 text-ellipsis">{user.videos.toLocaleString($locale)}</td | ||||
| 							> | ||||
| 							<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usageRaw)}</td> | ||||
| 						</tr> | ||||
| 					{/each} | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
| 	import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; | ||||
| 	import CircleIconButton from '../shared-components/circle-icon-button.svelte'; | ||||
| 	import noThumbnailUrl from '$lib/assets/no-thumbnail.png'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	export let album: AlbumResponseDto; | ||||
|  | ||||
| @@ -52,8 +53,6 @@ | ||||
| 	onMount(async () => { | ||||
| 		imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || noThumbnailUrl; | ||||
| 	}); | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| </script> | ||||
|  | ||||
| <div | ||||
| @@ -91,7 +90,10 @@ | ||||
| 		</p> | ||||
|  | ||||
| 		<span class="text-xs flex gap-2 dark:text-immich-dark-fg" data-testid="album-details"> | ||||
| 			<p>{album.assetCount.toLocaleString(locale)} {album.assetCount == 1 ? `item` : `items`}</p> | ||||
| 			<p> | ||||
| 				{album.assetCount.toLocaleString($locale)} | ||||
| 				{album.assetCount == 1 ? `item` : `items`} | ||||
| 			</p> | ||||
|  | ||||
| 			{#if album.shared} | ||||
| 				<p>·</p> | ||||
|   | ||||
| @@ -39,6 +39,7 @@ | ||||
| 	import ThemeButton from '../shared-components/theme-button.svelte'; | ||||
| 	import { openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||
| 	import { bulkDownload } from '$lib/utils/asset-utils'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
| 	import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||
| 	import ImmichLogo from '../shared-components/immich-logo.svelte'; | ||||
|  | ||||
| @@ -88,7 +89,6 @@ | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| 	const albumDateFormat: Intl.DateTimeFormatOptions = { | ||||
| 		month: 'short', | ||||
| 		day: 'numeric', | ||||
| @@ -99,8 +99,8 @@ | ||||
| 		const startDate = new Date(album.assets[0].fileCreatedAt); | ||||
| 		const endDate = new Date(album.assets[album.assetCount - 1].fileCreatedAt); | ||||
|  | ||||
| 		const startDateString = startDate.toLocaleDateString(locale, albumDateFormat); | ||||
| 		const endDateString = endDate.toLocaleDateString(locale, albumDateFormat); | ||||
| 		const startDateString = startDate.toLocaleDateString($locale, albumDateFormat); | ||||
| 		const endDateString = endDate.toLocaleDateString($locale, albumDateFormat); | ||||
|  | ||||
| 		// If the start and end date are the same, only show one date | ||||
| 		return startDateString === endDateString | ||||
| @@ -380,7 +380,7 @@ | ||||
| 		> | ||||
| 			<svelte:fragment slot="leading"> | ||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | ||||
| 					Selected {multiSelectAsset.size.toLocaleString(locale)} | ||||
| 					Selected {multiSelectAsset.size.toLocaleString($locale)} | ||||
| 				</p> | ||||
| 			</svelte:fragment> | ||||
| 			<svelte:fragment slot="trailing"> | ||||
|   | ||||
| @@ -11,12 +11,12 @@ | ||||
| 		assetsInAlbumStoreState, | ||||
| 		selectedAssets | ||||
| 	} from '$lib/stores/asset-interaction.store'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	const dispatch = createEventDispatcher(); | ||||
|  | ||||
| 	export let albumId: string; | ||||
| 	export let assetsInAlbum: AssetResponseDto[]; | ||||
| 	const locale = navigator.language; | ||||
|  | ||||
| 	onMount(() => { | ||||
| 		$assetsInAlbumStoreState = assetsInAlbum; | ||||
| @@ -51,7 +51,7 @@ | ||||
| 				<p class="text-lg dark:text-immich-dark-fg">Add to album</p> | ||||
| 			{:else} | ||||
| 				<p class="text-lg dark:text-immich-dark-fg"> | ||||
| 					{$selectedAssets.size.toLocaleString(locale)} selected | ||||
| 					{$selectedAssets.size.toLocaleString($locale)} selected | ||||
| 				</p> | ||||
| 			{/if} | ||||
| 		</svelte:fragment> | ||||
|   | ||||
| @@ -24,6 +24,7 @@ | ||||
|  | ||||
| 	import { assetStore } from '$lib/stores/assets.store'; | ||||
| 	import { addAssetsToAlbum } from '$lib/utils/asset-utils'; | ||||
| 	import { browser } from '$app/environment'; | ||||
|  | ||||
| 	export let asset: AssetResponseDto; | ||||
| 	export let publicSharedKey = ''; | ||||
| @@ -54,7 +55,9 @@ | ||||
| 	}); | ||||
|  | ||||
| 	onDestroy(() => { | ||||
| 		if (browser) { | ||||
| 			document.removeEventListener('keydown', onKeyboardPress); | ||||
| 		} | ||||
| 	}); | ||||
|  | ||||
| 	$: asset.id && getAllAlbums(); // Update the album information when the asset ID changes | ||||
|   | ||||
| @@ -8,6 +8,7 @@ | ||||
| 	import { browser } from '$app/environment'; | ||||
| 	import { AssetResponseDto, AlbumResponseDto } from '@api'; | ||||
| 	import { asByteUnitString } from '../../utils/byte-units'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	type Leaflet = typeof import('leaflet'); | ||||
| 	type LeafletMap = import('leaflet').Map; | ||||
| @@ -69,8 +70,6 @@ | ||||
|  | ||||
| 		return undefined; | ||||
| 	}; | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| </script> | ||||
|  | ||||
| <section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"> | ||||
| @@ -101,7 +100,7 @@ | ||||
|  | ||||
| 				<div> | ||||
| 					<p> | ||||
| 						{assetDateTimeOriginal.toLocaleDateString(locale, { | ||||
| 						{assetDateTimeOriginal.toLocaleDateString($locale, { | ||||
| 							month: 'short', | ||||
| 							day: 'numeric', | ||||
| 							year: 'numeric' | ||||
| @@ -109,7 +108,7 @@ | ||||
| 					</p> | ||||
| 					<div class="flex gap-2 text-sm"> | ||||
| 						<p> | ||||
| 							{assetDateTimeOriginal.toLocaleString(locale, { | ||||
| 							{assetDateTimeOriginal.toLocaleString($locale, { | ||||
| 								weekday: 'short', | ||||
| 								hour: 'numeric', | ||||
| 								minute: '2-digit', | ||||
| @@ -149,14 +148,14 @@ | ||||
| 				<div> | ||||
| 					<p>{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}</p> | ||||
| 					<div class="flex text-sm gap-2"> | ||||
| 						<p>{`ƒ/${asset.exifInfo.fNumber.toLocaleString(locale)}` || ''}</p> | ||||
| 						<p>{`ƒ/${asset.exifInfo.fNumber.toLocaleString($locale)}` || ''}</p> | ||||
|  | ||||
| 						{#if asset.exifInfo.exposureTime} | ||||
| 							<p>{`${asset.exifInfo.exposureTime}`}</p> | ||||
| 						{/if} | ||||
|  | ||||
| 						{#if asset.exifInfo.focalLength} | ||||
| 							<p>{`${asset.exifInfo.focalLength.toLocaleString(locale)} mm`}</p> | ||||
| 							<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p> | ||||
| 						{/if} | ||||
|  | ||||
| 						{#if asset.exifInfo.iso} | ||||
|   | ||||
| @@ -13,12 +13,13 @@ | ||||
| 		selectedAssets, | ||||
| 		selectedGroup | ||||
| 	} from '$lib/stores/asset-interaction.store'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	export let assets: AssetResponseDto[]; | ||||
| 	export let bucketDate: string; | ||||
| 	export let bucketHeight: number; | ||||
| 	export let isAlbumSelectionMode = false; | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| 	const groupDateFormat: Intl.DateTimeFormatOptions = { | ||||
| 		weekday: 'short', | ||||
| 		month: 'short', | ||||
| @@ -31,7 +32,7 @@ | ||||
| 	let hoveredDateGroup = ''; | ||||
| 	$: assetsGroupByDate = lodash | ||||
| 		.chain(assets) | ||||
| 		.groupBy((a) => new Date(a.fileCreatedAt).toLocaleDateString(locale, groupDateFormat)) | ||||
| 		.groupBy((a) => new Date(a.fileCreatedAt).toLocaleDateString($locale, groupDateFormat)) | ||||
| 		.sortBy((group) => assets.indexOf(group[0])) | ||||
| 		.value(); | ||||
|  | ||||
| @@ -115,7 +116,7 @@ | ||||
| > | ||||
| 	{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)} | ||||
| 		{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString( | ||||
| 			locale, | ||||
| 			$locale, | ||||
| 			groupDateFormat | ||||
| 		)} | ||||
| 		<!-- Asset Group By Date --> | ||||
|   | ||||
| @@ -18,6 +18,7 @@ | ||||
| 		notificationController, | ||||
| 		NotificationType | ||||
| 	} from '../shared-components/notification/notification'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	export let sharedLink: SharedLinkResponseDto; | ||||
| 	export let isOwned: boolean; | ||||
| @@ -86,8 +87,6 @@ | ||||
| 			clearMultiSelectAssetAssetHandler(); | ||||
| 		} | ||||
| 	}; | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| </script> | ||||
|  | ||||
| <section class="bg-immich-bg dark:bg-immich-dark-bg"> | ||||
| @@ -99,7 +98,7 @@ | ||||
| 		> | ||||
| 			<svelte:fragment slot="leading"> | ||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | ||||
| 					Selected {selectedAssets.size.toLocaleString(locale)} | ||||
| 					Selected {selectedAssets.size.toLocaleString($locale)} | ||||
| 				</p> | ||||
| 			</svelte:fragment> | ||||
| 			<svelte:fragment slot="trailing"> | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
| 	import LoadingSpinner from '../loading-spinner.svelte'; | ||||
| 	import StatusBox from '../status-box.svelte'; | ||||
| 	import SideBarButton from './side-bar-button.svelte'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	const getAssetCount = async () => { | ||||
| 		const { data: assetCount } = await api.assetApi.getAssetCountByUserId(); | ||||
| @@ -35,8 +36,6 @@ | ||||
| 			owned: albumCount.owned | ||||
| 		}; | ||||
| 	}; | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| </script> | ||||
|  | ||||
| <section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6 bg-immich-bg dark:bg-immich-dark-bg"> | ||||
| @@ -56,8 +55,8 @@ | ||||
| 					<LoadingSpinner /> | ||||
| 				{:then data} | ||||
| 					<div> | ||||
| 						<p>{data.videos.toLocaleString(locale)} Videos</p> | ||||
| 						<p>{data.photos.toLocaleString(locale)} Photos</p> | ||||
| 						<p>{data.videos.toLocaleString($locale)} Videos</p> | ||||
| 						<p>{data.photos.toLocaleString($locale)} Photos</p> | ||||
| 					</div> | ||||
| 				{/await} | ||||
| 			</svelte:fragment> | ||||
| @@ -74,7 +73,7 @@ | ||||
| 					<LoadingSpinner /> | ||||
| 				{:then data} | ||||
| 					<div> | ||||
| 						<p>{(data.shared + data.sharing).toLocaleString(locale)} Albums</p> | ||||
| 						<p>{(data.shared + data.sharing).toLocaleString($locale)} Albums</p> | ||||
| 					</div> | ||||
| 				{/await} | ||||
| 			</svelte:fragment> | ||||
| @@ -108,7 +107,7 @@ | ||||
| 					<LoadingSpinner /> | ||||
| 				{:then data} | ||||
| 					<div> | ||||
| 						<p>{data.owned.toLocaleString(locale)} Albums</p> | ||||
| 						<p>{data.owned.toLocaleString($locale)} Albums</p> | ||||
| 					</div> | ||||
| 				{/await} | ||||
| 			</svelte:fragment> | ||||
|   | ||||
| @@ -1,74 +1,38 @@ | ||||
| <script lang="ts"> | ||||
| 	import { onMount } from 'svelte'; | ||||
|  | ||||
| 	onMount(() => { | ||||
| 		var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); | ||||
| 		var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); | ||||
|  | ||||
| 		// Change the icons inside the button based on previous settings | ||||
| 		if ( | ||||
| 			localStorage.getItem('color-theme') === 'dark' || | ||||
| 			(!('color-theme' in localStorage) && | ||||
| 				window.matchMedia('(prefers-color-scheme: dark)').matches) | ||||
| 		) { | ||||
| 			themeToggleLightIcon?.classList.remove('hidden'); | ||||
| 		} else { | ||||
| 			themeToggleDarkIcon?.classList.remove('hidden'); | ||||
| 		} | ||||
| 	}); | ||||
| 	import { browser } from '$app/environment'; | ||||
| 	import { colorTheme } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	const toggleTheme = () => { | ||||
| 		var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); | ||||
| 		var themeToggleLightIcon = document.getElementById('theme-toggle-light-icon'); | ||||
| 		// toggle icons inside button | ||||
| 		themeToggleDarkIcon?.classList.toggle('hidden'); | ||||
| 		themeToggleLightIcon?.classList.toggle('hidden'); | ||||
|  | ||||
| 		// if set via local storage previously | ||||
| 		if (localStorage.getItem('color-theme')) { | ||||
| 			if (localStorage.getItem('color-theme') === 'light') { | ||||
| 				document.documentElement.classList.add('dark'); | ||||
| 				localStorage.setItem('color-theme', 'dark'); | ||||
| 			} else { | ||||
| 				document.documentElement.classList.remove('dark'); | ||||
| 				localStorage.setItem('color-theme', 'light'); | ||||
| 			} | ||||
| 		} else { | ||||
| 			if (document.documentElement.classList.contains('dark')) { | ||||
| 				document.documentElement.classList.remove('dark'); | ||||
| 				localStorage.setItem('color-theme', 'light'); | ||||
| 			} else { | ||||
| 				document.documentElement.classList.add('dark'); | ||||
| 				localStorage.setItem('color-theme', 'dark'); | ||||
| 			} | ||||
| 		} | ||||
| 		$colorTheme = $colorTheme === 'dark' ? 'light' : 'dark'; | ||||
| 	}; | ||||
|  | ||||
| 	$: { | ||||
| 		if (browser) { | ||||
| 			if ($colorTheme === 'light') { | ||||
| 				document.documentElement.classList.remove('dark'); | ||||
| 			} else { | ||||
| 				document.documentElement.classList.add('dark'); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| </script> | ||||
|  | ||||
| <button | ||||
| 	on:click={toggleTheme} | ||||
| 	id="theme-toggle" | ||||
| 	type="button" | ||||
| 	class="text-gray-500 dark:text-immich-dark-primary hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none  rounded-full text-sm p-2.5" | ||||
| 	class="text-gray-500 dark:text-immich-dark-primary hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-full p-2.5" | ||||
| > | ||||
| 	<svg | ||||
| 		id="theme-toggle-dark-icon" | ||||
| 		class="hidden w-6 h-6" | ||||
| 		fill="currentColor" | ||||
| 		viewBox="0 0 20 20" | ||||
| 		xmlns="http://www.w3.org/2000/svg" | ||||
| 	{#if $colorTheme === 'light'} | ||||
| 		<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" | ||||
| 			><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /></svg | ||||
| 		> | ||||
| 	<svg | ||||
| 		id="theme-toggle-light-icon" | ||||
| 		class="hidden w-6 h-6" | ||||
| 		fill="currentColor" | ||||
| 		viewBox="0 0 20 20" | ||||
| 		xmlns="http://www.w3.org/2000/svg" | ||||
| 	{:else} | ||||
| 		<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" | ||||
| 			><path | ||||
| 				d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" | ||||
| 				fill-rule="evenodd" | ||||
| 				clip-rule="evenodd" | ||||
| 			/></svg | ||||
| 		> | ||||
| 	{/if} | ||||
| </button> | ||||
|   | ||||
| @@ -12,6 +12,7 @@ | ||||
| 		notificationController, | ||||
| 		NotificationType | ||||
| 	} from '../shared-components/notification/notification'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	let keys: APIKeyResponseDto[] = []; | ||||
|  | ||||
| @@ -20,7 +21,6 @@ | ||||
| 	let deleteKey: APIKeyResponseDto | null = null; | ||||
| 	let secret = ''; | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| 	const format: Intl.DateTimeFormatOptions = { | ||||
| 		month: 'short', | ||||
| 		day: 'numeric', | ||||
| @@ -154,7 +154,7 @@ | ||||
| 							> | ||||
| 								<td class="text-sm px-4 w-1/3 text-ellipsis">{key.name}</td> | ||||
| 								<td class="text-sm px-4 w-1/3 text-ellipsis" | ||||
| 									>{new Date(key.createdAt).toLocaleDateString(locale, format)} | ||||
| 									>{new Date(key.createdAt).toLocaleDateString($locale, format)} | ||||
| 								</td> | ||||
| 								<td class="text-sm px-4 w-1/3 text-ellipsis"> | ||||
| 									<button | ||||
|   | ||||
							
								
								
									
										21
									
								
								web/src/lib/stores/preferences.store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								web/src/lib/stores/preferences.store.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import { browser } from '$app/environment'; | ||||
| import { persisted } from 'svelte-local-storage-store'; | ||||
|  | ||||
| const initialTheme = | ||||
| 	browser && !window.matchMedia('(prefers-color-scheme: dark)').matches ? 'light' : 'dark'; | ||||
|  | ||||
| // 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, { | ||||
| 	serializer: { | ||||
| 		parse: (text) => (text === 'light' ? text : 'dark'), | ||||
| 		stringify: (obj) => obj | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| // Locale to use for formatting dates, numbers, etc. | ||||
| export const locale = persisted<string | undefined>('locale', undefined, { | ||||
| 	serializer: { | ||||
| 		parse: (text) => text, | ||||
| 		stringify: (obj) => obj ?? '' | ||||
| 	} | ||||
| }); | ||||
| @@ -19,11 +19,9 @@ | ||||
| 	let localVersion: string; | ||||
| 	let remoteVersion: string; | ||||
| 	let showNavigationLoadingBar = false; | ||||
| 	let canShow = false; | ||||
| 	let showUploadCover = false; | ||||
|  | ||||
| 	onMount(async () => { | ||||
| 		checkUserTheme(); | ||||
| 		const res = await checkAppVersion(); | ||||
|  | ||||
| 		shouldShowAnnouncement = res.shouldShowAnnouncement; | ||||
| @@ -31,21 +29,6 @@ | ||||
| 		remoteVersion = res.remoteVersion ?? 'unknown'; | ||||
| 	}); | ||||
|  | ||||
| 	const checkUserTheme = () => { | ||||
| 		// On page load or when changing themes, best to add inline in `head` to avoid FOUC | ||||
| 		if ( | ||||
| 			localStorage.getItem('color-theme') === 'dark' || | ||||
| 			(!('color-theme' in localStorage) && | ||||
| 				window.matchMedia('(prefers-color-scheme: dark)').matches) | ||||
| 		) { | ||||
| 			document.documentElement.classList.add('dark'); | ||||
| 		} else { | ||||
| 			document.documentElement.classList.remove('dark'); | ||||
| 		} | ||||
|  | ||||
| 		canShow = true; | ||||
| 	}; | ||||
|  | ||||
| 	beforeNavigate(() => { | ||||
| 		showNavigationLoadingBar = true; | ||||
| 	}); | ||||
| @@ -99,7 +82,6 @@ | ||||
| </svelte:head> | ||||
|  | ||||
| <main on:dragenter={() => (showUploadCover = true)}> | ||||
| 	{#if canShow} | ||||
| 	<div in:fade={{ duration: 100 }}> | ||||
| 		{#if showNavigationLoadingBar} | ||||
| 			<NavigationLoadingBar /> | ||||
| @@ -126,5 +108,4 @@ | ||||
| 			/> | ||||
| 		{/if} | ||||
| 	</div> | ||||
| 	{/if} | ||||
| </main> | ||||
|   | ||||
| @@ -11,6 +11,7 @@ | ||||
| 	import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte'; | ||||
| 	import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte'; | ||||
| 	import { page } from '$app/stores'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	let allUsers: UserResponseDto[] = []; | ||||
| 	let shouldShowEditUserForm = false; | ||||
| @@ -28,7 +29,6 @@ | ||||
| 		return user.deletedAt != null; | ||||
| 	}; | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| 	const deleteDateFormat: Intl.DateTimeFormatOptions = { | ||||
| 		month: 'long', | ||||
| 		day: 'numeric', | ||||
| @@ -38,7 +38,7 @@ | ||||
| 	const getDeleteDate = (user: UserResponseDto): string => { | ||||
| 		let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now()); | ||||
| 		deletedAt.setDate(deletedAt.getDate() + 7); | ||||
| 		return deletedAt.toLocaleString(locale, deleteDateFormat); | ||||
| 		return deletedAt.toLocaleString($locale, deleteDateFormat); | ||||
| 	}; | ||||
|  | ||||
| 	const onUserCreated = async () => { | ||||
|   | ||||
| @@ -28,6 +28,7 @@ | ||||
| 	import Plus from 'svelte-material-icons/Plus.svelte'; | ||||
| 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; | ||||
| 	import type { PageData } from './$types'; | ||||
| 	import { locale } from '$lib/stores/preferences.store'; | ||||
|  | ||||
| 	export let data: PageData; | ||||
| 	let isShowCreateSharedLinkModal = false; | ||||
| @@ -141,8 +142,6 @@ | ||||
| 		assetInteractionStore.clearMultiselect(); | ||||
| 		isShowCreateSharedLinkModal = false; | ||||
| 	}; | ||||
|  | ||||
| 	const locale = navigator.language; | ||||
| </script> | ||||
|  | ||||
| <section> | ||||
| @@ -154,7 +153,7 @@ | ||||
| 		> | ||||
| 			<svelte:fragment slot="leading"> | ||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | ||||
| 					Selected {$selectedAssets.size.toLocaleString(locale)} | ||||
| 					Selected {$selectedAssets.size.toLocaleString($locale)} | ||||
| 				</p> | ||||
| 			</svelte:fragment> | ||||
| 			<svelte:fragment slot="trailing"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user