mirror of
https://github.com/immich-app/immich.git
synced 2024-12-29 15:11:58 +00:00
feat(web): theme/locale preferences and improve SSR (#1832)
This commit is contained in:
parent
a9a769d902
commit
10cb612fb1
20 changed files with 142 additions and 144 deletions
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(() => {
|
||||
document.removeEventListener('keydown', onKeyboardPress);
|
||||
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');
|
||||
$colorTheme = $colorTheme === 'dark' ? 'light' : 'dark';
|
||||
};
|
||||
|
||||
// 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 {
|
||||
$: {
|
||||
if (browser) {
|
||||
if ($colorTheme === 'light') {
|
||||
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');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</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"
|
||||
><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"
|
||||
><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 $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
|
||||
>
|
||||
{: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,32 +82,30 @@
|
|||
</svelte:head>
|
||||
|
||||
<main on:dragenter={() => (showUploadCover = true)}>
|
||||
{#if canShow}
|
||||
<div in:fade={{ duration: 100 }}>
|
||||
{#if showNavigationLoadingBar}
|
||||
<NavigationLoadingBar />
|
||||
{/if}
|
||||
<div in:fade={{ duration: 100 }}>
|
||||
{#if showNavigationLoadingBar}
|
||||
<NavigationLoadingBar />
|
||||
{/if}
|
||||
|
||||
<slot />
|
||||
<slot />
|
||||
|
||||
{#if showUploadCover}
|
||||
<UploadCover
|
||||
{dropHandler}
|
||||
{dragOverHandler}
|
||||
dragLeaveHandler={() => (showUploadCover = false)}
|
||||
/>
|
||||
{/if}
|
||||
{#if showUploadCover}
|
||||
<UploadCover
|
||||
{dropHandler}
|
||||
{dragOverHandler}
|
||||
dragLeaveHandler={() => (showUploadCover = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<DownloadPanel />
|
||||
<UploadPanel />
|
||||
<NotificationList />
|
||||
{#if shouldShowAnnouncement}
|
||||
<AnnouncementBox
|
||||
{localVersion}
|
||||
{remoteVersion}
|
||||
on:close={() => (shouldShowAnnouncement = false)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<DownloadPanel />
|
||||
<UploadPanel />
|
||||
<NotificationList />
|
||||
{#if shouldShowAnnouncement}
|
||||
<AnnouncementBox
|
||||
{localVersion}
|
||||
{remoteVersion}
|
||||
on:close={() => (shouldShowAnnouncement = false)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</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">
|
||||
|
|
Loading…
Reference in a new issue