mirror of
https://github.com/immich-app/immich.git
synced 2025-01-04 02:46:47 +01: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",
|
"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">
|
||||||
|
|
Loading…
Reference in a new issue