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", | 				"luxon": "^3.1.1", | ||||||
| 				"rxjs": "^7.8.0", | 				"rxjs": "^7.8.0", | ||||||
| 				"socket.io-client": "^4.5.1", | 				"socket.io-client": "^4.5.1", | ||||||
|  | 				"svelte-local-storage-store": "^0.4.0", | ||||||
| 				"svelte-material-icons": "^2.0.2" | 				"svelte-material-icons": "^2.0.2" | ||||||
| 			}, | 			}, | ||||||
| 			"devDependencies": { | 			"devDependencies": { | ||||||
| @@ -10584,6 +10585,17 @@ | |||||||
| 				"svelte": ">= 3" | 				"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": { | 		"node_modules/svelte-material-icons": { | ||||||
| 			"version": "2.0.4", | 			"version": "2.0.4", | ||||||
| 			"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz", | 			"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz", | ||||||
| @@ -19014,6 +19026,12 @@ | |||||||
| 			"dev": true, | 			"dev": true, | ||||||
| 			"requires": {} | 			"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": { | 		"svelte-material-icons": { | ||||||
| 			"version": "2.0.4", | 			"version": "2.0.4", | ||||||
| 			"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz", | 			"resolved": "https://registry.npmjs.org/svelte-material-icons/-/svelte-material-icons-2.0.4.tgz", | ||||||
|   | |||||||
| @@ -68,6 +68,7 @@ | |||||||
| 		"luxon": "^3.1.1", | 		"luxon": "^3.1.1", | ||||||
| 		"rxjs": "^7.8.0", | 		"rxjs": "^7.8.0", | ||||||
| 		"socket.io-client": "^4.5.1", | 		"socket.io-client": "^4.5.1", | ||||||
|  | 		"svelte-local-storage-store": "^0.4.0", | ||||||
| 		"svelte-material-icons": "^2.0.2" | 		"svelte-material-icons": "^2.0.2" | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -4,6 +4,15 @@ | |||||||
| 		<meta charset="utf-8" /> | 		<meta charset="utf-8" /> | ||||||
| 		<meta name="viewport" content="width=device-width, initial-scale=1" /> | 		<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||||
| 		%sveltekit.head% | 		%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> | 	</head> | ||||||
|  |  | ||||||
| 	<body class="bg-immich-bg dark:bg-immich-dark-bg"> | 	<body class="bg-immich-bg dark:bg-immich-dark-bg"> | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ | |||||||
| 	import SelectionSearch from 'svelte-material-icons/SelectionSearch.svelte'; | 	import SelectionSearch from 'svelte-material-icons/SelectionSearch.svelte'; | ||||||
| 	import Play from 'svelte-material-icons/Play.svelte'; | 	import Play from 'svelte-material-icons/Play.svelte'; | ||||||
| 	import AllInclusive from 'svelte-material-icons/AllInclusive.svelte'; | 	import AllInclusive from 'svelte-material-icons/AllInclusive.svelte'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
| 	import { createEventDispatcher } from 'svelte'; | 	import { createEventDispatcher } from 'svelte'; | ||||||
| 	import { JobCounts } from '@api'; | 	import { JobCounts } from '@api'; | ||||||
|  |  | ||||||
| @@ -22,8 +22,6 @@ | |||||||
| 	const run = (includeAllAssets: boolean) => { | 	const run = (includeAllAssets: boolean) => { | ||||||
| 		dispatch('click', { includeAllAssets }); | 		dispatch('click', { includeAllAssets }); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <div class="flex justify-between rounded-3xl bg-gray-100 dark:bg-immich-dark-gray"> | <div class="flex justify-between rounded-3xl bg-gray-100 dark:bg-immich-dark-gray"> | ||||||
| @@ -45,7 +43,7 @@ | |||||||
| 					<p>Active</p> | 					<p>Active</p> | ||||||
| 					<p class="text-2xl"> | 					<p class="text-2xl"> | ||||||
| 						{#if jobCounts.active !== undefined} | 						{#if jobCounts.active !== undefined} | ||||||
| 							{jobCounts.active.toLocaleString(locale)} | 							{jobCounts.active.toLocaleString($locale)} | ||||||
| 						{:else} | 						{:else} | ||||||
| 							<LoadingSpinner /> | 							<LoadingSpinner /> | ||||||
| 						{/if} | 						{/if} | ||||||
| @@ -57,7 +55,7 @@ | |||||||
| 				> | 				> | ||||||
| 					<p class="text-2xl"> | 					<p class="text-2xl"> | ||||||
| 						{#if jobCounts.waiting !== undefined} | 						{#if jobCounts.waiting !== undefined} | ||||||
| 							{jobCounts.waiting.toLocaleString(locale)} | 							{jobCounts.waiting.toLocaleString($locale)} | ||||||
| 						{:else} | 						{:else} | ||||||
| 							<LoadingSpinner /> | 							<LoadingSpinner /> | ||||||
| 						{/if} | 						{/if} | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ | |||||||
| 	import { getBytesWithUnit, asByteUnitString } from '../../../utils/byte-units'; | 	import { getBytesWithUnit, asByteUnitString } from '../../../utils/byte-units'; | ||||||
| 	import { onMount, onDestroy } from 'svelte'; | 	import { onMount, onDestroy } from 'svelte'; | ||||||
| 	import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; | 	import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |  | ||||||
| 	export let allUsers: Array<UserResponseDto>; | 	export let allUsers: Array<UserResponseDto>; | ||||||
|  |  | ||||||
| @@ -37,8 +38,6 @@ | |||||||
|  |  | ||||||
| 	// Stats are unavailable if data is not loaded yet | 	// Stats are unavailable if data is not loaded yet | ||||||
| 	$: [spaceUsage, spaceUnit] = getBytesWithUnit(stats ? stats.usageRaw : 0); | 	$: [spaceUsage, spaceUnit] = getBytesWithUnit(stats ? stats.usageRaw : 0); | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <div class="flex flex-col gap-5"> | <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">{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.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.videos.toLocaleString($locale)}</td | ||||||
|  | 							> | ||||||
| 							<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usageRaw)}</td> | 							<td class="text-sm px-2 w-1/4 text-ellipsis">{asByteUnitString(user.usageRaw)}</td> | ||||||
| 						</tr> | 						</tr> | ||||||
| 					{/each} | 					{/each} | ||||||
|   | |||||||
| @@ -17,6 +17,7 @@ | |||||||
| 	import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; | 	import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; | ||||||
| 	import CircleIconButton from '../shared-components/circle-icon-button.svelte'; | 	import CircleIconButton from '../shared-components/circle-icon-button.svelte'; | ||||||
| 	import noThumbnailUrl from '$lib/assets/no-thumbnail.png'; | 	import noThumbnailUrl from '$lib/assets/no-thumbnail.png'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |  | ||||||
| 	export let album: AlbumResponseDto; | 	export let album: AlbumResponseDto; | ||||||
|  |  | ||||||
| @@ -52,8 +53,6 @@ | |||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
| 		imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || noThumbnailUrl; | 		imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || noThumbnailUrl; | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <div | <div | ||||||
| @@ -91,7 +90,10 @@ | |||||||
| 		</p> | 		</p> | ||||||
|  |  | ||||||
| 		<span class="text-xs flex gap-2 dark:text-immich-dark-fg" data-testid="album-details"> | 		<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} | 			{#if album.shared} | ||||||
| 				<p>·</p> | 				<p>·</p> | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ | |||||||
| 	import ThemeButton from '../shared-components/theme-button.svelte'; | 	import ThemeButton from '../shared-components/theme-button.svelte'; | ||||||
| 	import { openFileUploadDialog } from '$lib/utils/file-uploader'; | 	import { openFileUploadDialog } from '$lib/utils/file-uploader'; | ||||||
| 	import { bulkDownload } from '$lib/utils/asset-utils'; | 	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 GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||||
| 	import ImmichLogo from '../shared-components/immich-logo.svelte'; | 	import ImmichLogo from '../shared-components/immich-logo.svelte'; | ||||||
|  |  | ||||||
| @@ -88,7 +89,6 @@ | |||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| 	const albumDateFormat: Intl.DateTimeFormatOptions = { | 	const albumDateFormat: Intl.DateTimeFormatOptions = { | ||||||
| 		month: 'short', | 		month: 'short', | ||||||
| 		day: 'numeric', | 		day: 'numeric', | ||||||
| @@ -99,8 +99,8 @@ | |||||||
| 		const startDate = new Date(album.assets[0].fileCreatedAt); | 		const startDate = new Date(album.assets[0].fileCreatedAt); | ||||||
| 		const endDate = new Date(album.assets[album.assetCount - 1].fileCreatedAt); | 		const endDate = new Date(album.assets[album.assetCount - 1].fileCreatedAt); | ||||||
|  |  | ||||||
| 		const startDateString = startDate.toLocaleDateString(locale, albumDateFormat); | 		const startDateString = startDate.toLocaleDateString($locale, albumDateFormat); | ||||||
| 		const endDateString = endDate.toLocaleDateString(locale, albumDateFormat); | 		const endDateString = endDate.toLocaleDateString($locale, albumDateFormat); | ||||||
|  |  | ||||||
| 		// If the start and end date are the same, only show one date | 		// If the start and end date are the same, only show one date | ||||||
| 		return startDateString === endDateString | 		return startDateString === endDateString | ||||||
| @@ -380,7 +380,7 @@ | |||||||
| 		> | 		> | ||||||
| 			<svelte:fragment slot="leading"> | 			<svelte:fragment slot="leading"> | ||||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | ||||||
| 					Selected {multiSelectAsset.size.toLocaleString(locale)} | 					Selected {multiSelectAsset.size.toLocaleString($locale)} | ||||||
| 				</p> | 				</p> | ||||||
| 			</svelte:fragment> | 			</svelte:fragment> | ||||||
| 			<svelte:fragment slot="trailing"> | 			<svelte:fragment slot="trailing"> | ||||||
|   | |||||||
| @@ -11,12 +11,12 @@ | |||||||
| 		assetsInAlbumStoreState, | 		assetsInAlbumStoreState, | ||||||
| 		selectedAssets | 		selectedAssets | ||||||
| 	} from '$lib/stores/asset-interaction.store'; | 	} from '$lib/stores/asset-interaction.store'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |  | ||||||
| 	const dispatch = createEventDispatcher(); | 	const dispatch = createEventDispatcher(); | ||||||
|  |  | ||||||
| 	export let albumId: string; | 	export let albumId: string; | ||||||
| 	export let assetsInAlbum: AssetResponseDto[]; | 	export let assetsInAlbum: AssetResponseDto[]; | ||||||
| 	const locale = navigator.language; |  | ||||||
|  |  | ||||||
| 	onMount(() => { | 	onMount(() => { | ||||||
| 		$assetsInAlbumStoreState = assetsInAlbum; | 		$assetsInAlbumStoreState = assetsInAlbum; | ||||||
| @@ -51,7 +51,7 @@ | |||||||
| 				<p class="text-lg dark:text-immich-dark-fg">Add to album</p> | 				<p class="text-lg dark:text-immich-dark-fg">Add to album</p> | ||||||
| 			{:else} | 			{:else} | ||||||
| 				<p class="text-lg dark:text-immich-dark-fg"> | 				<p class="text-lg dark:text-immich-dark-fg"> | ||||||
| 					{$selectedAssets.size.toLocaleString(locale)} selected | 					{$selectedAssets.size.toLocaleString($locale)} selected | ||||||
| 				</p> | 				</p> | ||||||
| 			{/if} | 			{/if} | ||||||
| 		</svelte:fragment> | 		</svelte:fragment> | ||||||
|   | |||||||
| @@ -24,6 +24,7 @@ | |||||||
|  |  | ||||||
| 	import { assetStore } from '$lib/stores/assets.store'; | 	import { assetStore } from '$lib/stores/assets.store'; | ||||||
| 	import { addAssetsToAlbum } from '$lib/utils/asset-utils'; | 	import { addAssetsToAlbum } from '$lib/utils/asset-utils'; | ||||||
|  | 	import { browser } from '$app/environment'; | ||||||
|  |  | ||||||
| 	export let asset: AssetResponseDto; | 	export let asset: AssetResponseDto; | ||||||
| 	export let publicSharedKey = ''; | 	export let publicSharedKey = ''; | ||||||
| @@ -54,7 +55,9 @@ | |||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	onDestroy(() => { | 	onDestroy(() => { | ||||||
|  | 		if (browser) { | ||||||
| 			document.removeEventListener('keydown', onKeyboardPress); | 			document.removeEventListener('keydown', onKeyboardPress); | ||||||
|  | 		} | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
| 	$: asset.id && getAllAlbums(); // Update the album information when the asset ID changes | 	$: asset.id && getAllAlbums(); // Update the album information when the asset ID changes | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ | |||||||
| 	import { browser } from '$app/environment'; | 	import { browser } from '$app/environment'; | ||||||
| 	import { AssetResponseDto, AlbumResponseDto } from '@api'; | 	import { AssetResponseDto, AlbumResponseDto } from '@api'; | ||||||
| 	import { asByteUnitString } from '../../utils/byte-units'; | 	import { asByteUnitString } from '../../utils/byte-units'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |  | ||||||
| 	type Leaflet = typeof import('leaflet'); | 	type Leaflet = typeof import('leaflet'); | ||||||
| 	type LeafletMap = import('leaflet').Map; | 	type LeafletMap = import('leaflet').Map; | ||||||
| @@ -69,8 +70,6 @@ | |||||||
|  |  | ||||||
| 		return undefined; | 		return undefined; | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"> | <section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"> | ||||||
| @@ -101,7 +100,7 @@ | |||||||
|  |  | ||||||
| 				<div> | 				<div> | ||||||
| 					<p> | 					<p> | ||||||
| 						{assetDateTimeOriginal.toLocaleDateString(locale, { | 						{assetDateTimeOriginal.toLocaleDateString($locale, { | ||||||
| 							month: 'short', | 							month: 'short', | ||||||
| 							day: 'numeric', | 							day: 'numeric', | ||||||
| 							year: 'numeric' | 							year: 'numeric' | ||||||
| @@ -109,7 +108,7 @@ | |||||||
| 					</p> | 					</p> | ||||||
| 					<div class="flex gap-2 text-sm"> | 					<div class="flex gap-2 text-sm"> | ||||||
| 						<p> | 						<p> | ||||||
| 							{assetDateTimeOriginal.toLocaleString(locale, { | 							{assetDateTimeOriginal.toLocaleString($locale, { | ||||||
| 								weekday: 'short', | 								weekday: 'short', | ||||||
| 								hour: 'numeric', | 								hour: 'numeric', | ||||||
| 								minute: '2-digit', | 								minute: '2-digit', | ||||||
| @@ -149,14 +148,14 @@ | |||||||
| 				<div> | 				<div> | ||||||
| 					<p>{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}</p> | 					<p>{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}</p> | ||||||
| 					<div class="flex text-sm gap-2"> | 					<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} | 						{#if asset.exifInfo.exposureTime} | ||||||
| 							<p>{`${asset.exifInfo.exposureTime}`}</p> | 							<p>{`${asset.exifInfo.exposureTime}`}</p> | ||||||
| 						{/if} | 						{/if} | ||||||
|  |  | ||||||
| 						{#if asset.exifInfo.focalLength} | 						{#if asset.exifInfo.focalLength} | ||||||
| 							<p>{`${asset.exifInfo.focalLength.toLocaleString(locale)} mm`}</p> | 							<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p> | ||||||
| 						{/if} | 						{/if} | ||||||
|  |  | ||||||
| 						{#if asset.exifInfo.iso} | 						{#if asset.exifInfo.iso} | ||||||
|   | |||||||
| @@ -13,12 +13,13 @@ | |||||||
| 		selectedAssets, | 		selectedAssets, | ||||||
| 		selectedGroup | 		selectedGroup | ||||||
| 	} from '$lib/stores/asset-interaction.store'; | 	} from '$lib/stores/asset-interaction.store'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |  | ||||||
| 	export let assets: AssetResponseDto[]; | 	export let assets: AssetResponseDto[]; | ||||||
| 	export let bucketDate: string; | 	export let bucketDate: string; | ||||||
| 	export let bucketHeight: number; | 	export let bucketHeight: number; | ||||||
| 	export let isAlbumSelectionMode = false; | 	export let isAlbumSelectionMode = false; | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| 	const groupDateFormat: Intl.DateTimeFormatOptions = { | 	const groupDateFormat: Intl.DateTimeFormatOptions = { | ||||||
| 		weekday: 'short', | 		weekday: 'short', | ||||||
| 		month: 'short', | 		month: 'short', | ||||||
| @@ -31,7 +32,7 @@ | |||||||
| 	let hoveredDateGroup = ''; | 	let hoveredDateGroup = ''; | ||||||
| 	$: assetsGroupByDate = lodash | 	$: assetsGroupByDate = lodash | ||||||
| 		.chain(assets) | 		.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])) | 		.sortBy((group) => assets.indexOf(group[0])) | ||||||
| 		.value(); | 		.value(); | ||||||
|  |  | ||||||
| @@ -115,7 +116,7 @@ | |||||||
| > | > | ||||||
| 	{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)} | 	{#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)} | ||||||
| 		{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString( | 		{@const dateGroupTitle = new Date(assetsInDateGroup[0].fileCreatedAt).toLocaleDateString( | ||||||
| 			locale, | 			$locale, | ||||||
| 			groupDateFormat | 			groupDateFormat | ||||||
| 		)} | 		)} | ||||||
| 		<!-- Asset Group By Date --> | 		<!-- Asset Group By Date --> | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ | |||||||
| 		notificationController, | 		notificationController, | ||||||
| 		NotificationType | 		NotificationType | ||||||
| 	} from '../shared-components/notification/notification'; | 	} from '../shared-components/notification/notification'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |  | ||||||
| 	export let sharedLink: SharedLinkResponseDto; | 	export let sharedLink: SharedLinkResponseDto; | ||||||
| 	export let isOwned: boolean; | 	export let isOwned: boolean; | ||||||
| @@ -86,8 +87,6 @@ | |||||||
| 			clearMultiSelectAssetAssetHandler(); | 			clearMultiSelectAssetAssetHandler(); | ||||||
| 		} | 		} | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <section class="bg-immich-bg dark:bg-immich-dark-bg"> | <section class="bg-immich-bg dark:bg-immich-dark-bg"> | ||||||
| @@ -99,7 +98,7 @@ | |||||||
| 		> | 		> | ||||||
| 			<svelte:fragment slot="leading"> | 			<svelte:fragment slot="leading"> | ||||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | ||||||
| 					Selected {selectedAssets.size.toLocaleString(locale)} | 					Selected {selectedAssets.size.toLocaleString($locale)} | ||||||
| 				</p> | 				</p> | ||||||
| 			</svelte:fragment> | 			</svelte:fragment> | ||||||
| 			<svelte:fragment slot="trailing"> | 			<svelte:fragment slot="trailing"> | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ | |||||||
| 	import LoadingSpinner from '../loading-spinner.svelte'; | 	import LoadingSpinner from '../loading-spinner.svelte'; | ||||||
| 	import StatusBox from '../status-box.svelte'; | 	import StatusBox from '../status-box.svelte'; | ||||||
| 	import SideBarButton from './side-bar-button.svelte'; | 	import SideBarButton from './side-bar-button.svelte'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |  | ||||||
| 	const getAssetCount = async () => { | 	const getAssetCount = async () => { | ||||||
| 		const { data: assetCount } = await api.assetApi.getAssetCountByUserId(); | 		const { data: assetCount } = await api.assetApi.getAssetCountByUserId(); | ||||||
| @@ -35,8 +36,6 @@ | |||||||
| 			owned: albumCount.owned | 			owned: albumCount.owned | ||||||
| 		}; | 		}; | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <section id="sidebar" class="flex flex-col gap-1 pt-8 pr-6 bg-immich-bg dark:bg-immich-dark-bg"> | <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 /> | 					<LoadingSpinner /> | ||||||
| 				{:then data} | 				{:then data} | ||||||
| 					<div> | 					<div> | ||||||
| 						<p>{data.videos.toLocaleString(locale)} Videos</p> | 						<p>{data.videos.toLocaleString($locale)} Videos</p> | ||||||
| 						<p>{data.photos.toLocaleString(locale)} Photos</p> | 						<p>{data.photos.toLocaleString($locale)} Photos</p> | ||||||
| 					</div> | 					</div> | ||||||
| 				{/await} | 				{/await} | ||||||
| 			</svelte:fragment> | 			</svelte:fragment> | ||||||
| @@ -74,7 +73,7 @@ | |||||||
| 					<LoadingSpinner /> | 					<LoadingSpinner /> | ||||||
| 				{:then data} | 				{:then data} | ||||||
| 					<div> | 					<div> | ||||||
| 						<p>{(data.shared + data.sharing).toLocaleString(locale)} Albums</p> | 						<p>{(data.shared + data.sharing).toLocaleString($locale)} Albums</p> | ||||||
| 					</div> | 					</div> | ||||||
| 				{/await} | 				{/await} | ||||||
| 			</svelte:fragment> | 			</svelte:fragment> | ||||||
| @@ -108,7 +107,7 @@ | |||||||
| 					<LoadingSpinner /> | 					<LoadingSpinner /> | ||||||
| 				{:then data} | 				{:then data} | ||||||
| 					<div> | 					<div> | ||||||
| 						<p>{data.owned.toLocaleString(locale)} Albums</p> | 						<p>{data.owned.toLocaleString($locale)} Albums</p> | ||||||
| 					</div> | 					</div> | ||||||
| 				{/await} | 				{/await} | ||||||
| 			</svelte:fragment> | 			</svelte:fragment> | ||||||
|   | |||||||
| @@ -1,74 +1,38 @@ | |||||||
| <script lang="ts"> | <script lang="ts"> | ||||||
| 	import { onMount } from 'svelte'; | 	import { browser } from '$app/environment'; | ||||||
|  | 	import { colorTheme } from '$lib/stores/preferences.store'; | ||||||
| 	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'); |  | ||||||
| 		} |  | ||||||
| 	}); |  | ||||||
|  |  | ||||||
| 	const toggleTheme = () => { | 	const toggleTheme = () => { | ||||||
| 		var themeToggleDarkIcon = document.getElementById('theme-toggle-dark-icon'); | 		$colorTheme = $colorTheme === 'dark' ? 'light' : 'dark'; | ||||||
| 		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'); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
|  | 	$: { | ||||||
|  | 		if (browser) { | ||||||
|  | 			if ($colorTheme === 'light') { | ||||||
|  | 				document.documentElement.classList.remove('dark'); | ||||||
|  | 			} else { | ||||||
|  | 				document.documentElement.classList.add('dark'); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <button | <button | ||||||
| 	on:click={toggleTheme} | 	on:click={toggleTheme} | ||||||
| 	id="theme-toggle" |  | ||||||
| 	type="button" | 	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 | 	{#if $colorTheme === 'light'} | ||||||
| 		id="theme-toggle-dark-icon" | 		<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" | ||||||
| 		class="hidden 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 | 			><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" /></svg | ||||||
| 		> | 		> | ||||||
| 	<svg | 	{:else} | ||||||
| 		id="theme-toggle-light-icon" | 		<svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg" | ||||||
| 		class="hidden w-6 h-6" |  | ||||||
| 		fill="currentColor" |  | ||||||
| 		viewBox="0 0 20 20" |  | ||||||
| 		xmlns="http://www.w3.org/2000/svg" |  | ||||||
| 			><path | 			><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" | 				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" | 				fill-rule="evenodd" | ||||||
| 				clip-rule="evenodd" | 				clip-rule="evenodd" | ||||||
| 			/></svg | 			/></svg | ||||||
| 		> | 		> | ||||||
|  | 	{/if} | ||||||
| </button> | </button> | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ | |||||||
| 		notificationController, | 		notificationController, | ||||||
| 		NotificationType | 		NotificationType | ||||||
| 	} from '../shared-components/notification/notification'; | 	} from '../shared-components/notification/notification'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |  | ||||||
| 	let keys: APIKeyResponseDto[] = []; | 	let keys: APIKeyResponseDto[] = []; | ||||||
|  |  | ||||||
| @@ -20,7 +21,6 @@ | |||||||
| 	let deleteKey: APIKeyResponseDto | null = null; | 	let deleteKey: APIKeyResponseDto | null = null; | ||||||
| 	let secret = ''; | 	let secret = ''; | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| 	const format: Intl.DateTimeFormatOptions = { | 	const format: Intl.DateTimeFormatOptions = { | ||||||
| 		month: 'short', | 		month: 'short', | ||||||
| 		day: 'numeric', | 		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">{key.name}</td> | ||||||
| 								<td class="text-sm px-4 w-1/3 text-ellipsis" | 								<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> | ||||||
| 								<td class="text-sm px-4 w-1/3 text-ellipsis"> | 								<td class="text-sm px-4 w-1/3 text-ellipsis"> | ||||||
| 									<button | 									<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 localVersion: string; | ||||||
| 	let remoteVersion: string; | 	let remoteVersion: string; | ||||||
| 	let showNavigationLoadingBar = false; | 	let showNavigationLoadingBar = false; | ||||||
| 	let canShow = false; |  | ||||||
| 	let showUploadCover = false; | 	let showUploadCover = false; | ||||||
|  |  | ||||||
| 	onMount(async () => { | 	onMount(async () => { | ||||||
| 		checkUserTheme(); |  | ||||||
| 		const res = await checkAppVersion(); | 		const res = await checkAppVersion(); | ||||||
|  |  | ||||||
| 		shouldShowAnnouncement = res.shouldShowAnnouncement; | 		shouldShowAnnouncement = res.shouldShowAnnouncement; | ||||||
| @@ -31,21 +29,6 @@ | |||||||
| 		remoteVersion = res.remoteVersion ?? 'unknown'; | 		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(() => { | 	beforeNavigate(() => { | ||||||
| 		showNavigationLoadingBar = true; | 		showNavigationLoadingBar = true; | ||||||
| 	}); | 	}); | ||||||
| @@ -99,7 +82,6 @@ | |||||||
| </svelte:head> | </svelte:head> | ||||||
|  |  | ||||||
| <main on:dragenter={() => (showUploadCover = true)}> | <main on:dragenter={() => (showUploadCover = true)}> | ||||||
| 	{#if canShow} |  | ||||||
| 	<div in:fade={{ duration: 100 }}> | 	<div in:fade={{ duration: 100 }}> | ||||||
| 		{#if showNavigationLoadingBar} | 		{#if showNavigationLoadingBar} | ||||||
| 			<NavigationLoadingBar /> | 			<NavigationLoadingBar /> | ||||||
| @@ -126,5 +108,4 @@ | |||||||
| 			/> | 			/> | ||||||
| 		{/if} | 		{/if} | ||||||
| 	</div> | 	</div> | ||||||
| 	{/if} |  | ||||||
| </main> | </main> | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ | |||||||
| 	import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte'; | 	import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte'; | ||||||
| 	import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte'; | 	import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte'; | ||||||
| 	import { page } from '$app/stores'; | 	import { page } from '$app/stores'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |  | ||||||
| 	let allUsers: UserResponseDto[] = []; | 	let allUsers: UserResponseDto[] = []; | ||||||
| 	let shouldShowEditUserForm = false; | 	let shouldShowEditUserForm = false; | ||||||
| @@ -28,7 +29,6 @@ | |||||||
| 		return user.deletedAt != null; | 		return user.deletedAt != null; | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| 	const deleteDateFormat: Intl.DateTimeFormatOptions = { | 	const deleteDateFormat: Intl.DateTimeFormatOptions = { | ||||||
| 		month: 'long', | 		month: 'long', | ||||||
| 		day: 'numeric', | 		day: 'numeric', | ||||||
| @@ -38,7 +38,7 @@ | |||||||
| 	const getDeleteDate = (user: UserResponseDto): string => { | 	const getDeleteDate = (user: UserResponseDto): string => { | ||||||
| 		let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now()); | 		let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now()); | ||||||
| 		deletedAt.setDate(deletedAt.getDate() + 7); | 		deletedAt.setDate(deletedAt.getDate() + 7); | ||||||
| 		return deletedAt.toLocaleString(locale, deleteDateFormat); | 		return deletedAt.toLocaleString($locale, deleteDateFormat); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const onUserCreated = async () => { | 	const onUserCreated = async () => { | ||||||
|   | |||||||
| @@ -28,6 +28,7 @@ | |||||||
| 	import Plus from 'svelte-material-icons/Plus.svelte'; | 	import Plus from 'svelte-material-icons/Plus.svelte'; | ||||||
| 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; | 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; | ||||||
| 	import type { PageData } from './$types'; | 	import type { PageData } from './$types'; | ||||||
|  | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
|  |  | ||||||
| 	export let data: PageData; | 	export let data: PageData; | ||||||
| 	let isShowCreateSharedLinkModal = false; | 	let isShowCreateSharedLinkModal = false; | ||||||
| @@ -141,8 +142,6 @@ | |||||||
| 		assetInteractionStore.clearMultiselect(); | 		assetInteractionStore.clearMultiselect(); | ||||||
| 		isShowCreateSharedLinkModal = false; | 		isShowCreateSharedLinkModal = false; | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const locale = navigator.language; |  | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <section> | <section> | ||||||
| @@ -154,7 +153,7 @@ | |||||||
| 		> | 		> | ||||||
| 			<svelte:fragment slot="leading"> | 			<svelte:fragment slot="leading"> | ||||||
| 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | 				<p class="font-medium text-immich-primary dark:text-immich-dark-primary"> | ||||||
| 					Selected {$selectedAssets.size.toLocaleString(locale)} | 					Selected {$selectedAssets.size.toLocaleString($locale)} | ||||||
| 				</p> | 				</p> | ||||||
| 			</svelte:fragment> | 			</svelte:fragment> | ||||||
| 			<svelte:fragment slot="trailing"> | 			<svelte:fragment slot="trailing"> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user