diff --git a/web/package-lock.json b/web/package-lock.json index 86743132c7..e081c55349 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -15,7 +15,6 @@ "leaflet": "^1.8.0", "lodash": "^4.17.21", "lodash-es": "^4.17.21", - "moment": "^2.29.3", "socket.io-client": "^4.5.1", "svelte-keydown": "^0.5.0", "svelte-material-icons": "^2.0.2" @@ -8873,14 +8872,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", - "engines": { - "node": "*" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -17603,11 +17594,6 @@ "minimist": "^1.2.6" } }, - "moment": { - "version": "2.29.4", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", - "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" - }, "mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", diff --git a/web/package.json b/web/package.json index f9b63ac70f..2aea1179ff 100644 --- a/web/package.json +++ b/web/package.json @@ -65,7 +65,6 @@ "leaflet": "^1.8.0", "lodash": "^4.17.21", "lodash-es": "^4.17.21", - "moment": "^2.29.3", "socket.io-client": "^4.5.1", "svelte-keydown": "^0.5.0", "svelte-material-icons": "^2.0.2" diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index 0ef9184312..0c53a542ad 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -4,6 +4,7 @@ import PlayCircle from 'svelte-material-icons/PlayCircle.svelte'; import Memory from 'svelte-material-icons/Memory.svelte'; import StatsCard from './stats-card.svelte'; + import { getBytesWithUnit, asByteUnitString } from '../../../utils/byte-units'; export let stats: ServerStatsResponseDto; export let allUsers: Array; @@ -15,8 +16,9 @@ return name; }; - $: spaceUnit = stats.usage.split(' ')[1]; - $: spaceUsage = stats.usage.split(' ')[0]; + $: [spaceUsage, spaceUnit] = getBytesWithUnit(stats.usageRaw); + + const locale = navigator.languages;
@@ -55,9 +57,9 @@ }`} > {getFullName(user.userId)} - {user.photos} - {user.videos} - {user.usage} + {user.photos.toLocaleString(locale)} + {user.videos.toLocaleString(locale)} + {asByteUnitString(user.usageRaw)} {/each} diff --git a/web/src/lib/components/admin-page/user-management.svelte b/web/src/lib/components/admin-page/user-management.svelte index 0b8a3cda74..59db1aeaf5 100644 --- a/web/src/lib/components/admin-page/user-management.svelte +++ b/web/src/lib/components/admin-page/user-management.svelte @@ -5,7 +5,6 @@ import PencilOutline from 'svelte-material-icons/PencilOutline.svelte'; import TrashCanOutline from 'svelte-material-icons/TrashCanOutline.svelte'; import DeleteRestore from 'svelte-material-icons/DeleteRestore.svelte'; - import moment from 'moment'; export let allUsers: Array; @@ -15,8 +14,15 @@ return user.deletedAt != null; }; + const locale = navigator.languages; + const deleteDateFormat: Intl.DateTimeFormatOptions = { + month: 'long', day: 'numeric', year: 'numeric' + }; + const getDeleteDate = (user: UserResponseDto): string => { - return moment(user.deletedAt).add(7, 'days').format('LL'); + let deletedAt = new Date(user.deletedAt ? user.deletedAt : Date.now()); + deletedAt.setDate(deletedAt.getDate() + 7); + return deletedAt.toLocaleString(locale, deleteDateFormat); }; diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index 0398a99b3d..bb4770c4b3 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -53,6 +53,8 @@ onMount(async () => { imageData = (await loadHighQualityThumbnail(album.albumThumbnailAssetId)) || NO_THUMBNAIL; }); + + const locale = navigator.languages;
-

{album.assetCount} items

+

{album.assetCount.toLocaleString(locale)} {album.assetCount == 1 ? `item` : `items`}

{#if album.shared}

·

diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index ca8b3355b9..fbebccacaf 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -86,19 +86,22 @@ } } - const getDateRange = () => { - const startDate = new Date(album.assets[0].createdAt); - const endDate = new Date(album.assets[album.assetCount - 1].createdAt); - - const timeFormatOption: Intl.DateTimeFormatOptions = { + const locale = navigator.languages; + const albumDateFormat: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', year: 'numeric' }; - const startDateString = startDate.toLocaleDateString('us-EN', timeFormatOption); - const endDateString = endDate.toLocaleDateString('us-EN', timeFormatOption); - return `${startDateString} - ${endDateString}`; + const getDateRange = () => { + const startDate = new Date(album.assets[0].createdAt); + const endDate = new Date(album.assets[album.assetCount - 1].createdAt); + + 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 ? startDateString : `${startDateString} - ${endDateString}`; }; onMount(async () => { diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index c427443392..65568f2b26 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -4,11 +4,10 @@ import ImageOutline from 'svelte-material-icons/ImageOutline.svelte'; import CameraIris from 'svelte-material-icons/CameraIris.svelte'; import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte'; - import moment from 'moment'; import { createEventDispatcher, onMount } from 'svelte'; import { browser } from '$app/environment'; import { AssetResponseDto, AlbumResponseDto } from '@api'; - import { getHumanReadableBytes } from '../../utils/byte-units'; + import { asByteUnitString } from '../../utils/byte-units'; type Leaflet = typeof import('leaflet'); type LeafletMap = import('leaflet').Map; @@ -70,6 +69,8 @@ return undefined; }; + + const locale = navigator.languages;
@@ -92,18 +93,18 @@ {/if} {#if asset.exifInfo?.dateTimeOriginal} + {@const assetDateTimeOriginal = new Date(asset.exifInfo.dateTimeOriginal)}
-

{moment(asset.exifInfo.dateTimeOriginal).format('MMM DD, YYYY')}

+

{assetDateTimeOriginal.toLocaleDateString(locale, {month:'short', day:'numeric', year: 'numeric'})}

- {moment(asset.exifInfo.dateTimeOriginal).format('ddd, hh:mm A')} + {assetDateTimeOriginal.toLocaleString(locale, {weekday:'short', hour: 'numeric', minute: '2-digit', timeZoneName:'longOffset'})}

-

GMT{moment(asset.exifInfo.dateTimeOriginal).format('Z')}

{/if} @@ -124,7 +125,7 @@

{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}

{/if} -

{getHumanReadableBytes(asset.exifInfo.fileSizeInByte)}

+

{asByteUnitString(asset.exifInfo.fileSizeInByte)}

@@ -137,14 +138,14 @@

{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}

-

{`ƒ/${asset.exifInfo.fNumber}` || ''}

+

{`ƒ/${asset.exifInfo.fNumber.toLocaleString(locale)}` || ''}

{#if asset.exifInfo.exposureTime}

{`1/${Math.floor(1 / asset.exifInfo.exposureTime)}`}

{/if} {#if asset.exifInfo.focalLength} -

{`${asset.exifInfo.focalLength} mm`}

+

{`${asset.exifInfo.focalLength.toLocaleString(locale)} mm`}

{/if} {#if asset.exifInfo.iso} @@ -164,7 +165,9 @@

{asset.exifInfo.city}

-

{asset.exifInfo.state},

+

{asset.exifInfo.state}

+
+

{asset.exifInfo.country}

diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index 11176856e1..6da41f3183 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -5,7 +5,6 @@ import { fly } from 'svelte/transition'; import { AssetResponseDto } from '@api'; import lodash from 'lodash-es'; - import moment from 'moment'; import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte'; import { assetInteractionStore, @@ -19,12 +18,20 @@ export let bucketHeight: number; export let isAlbumSelectionMode = false; + const locale = navigator.languages; + const groupDateFormat: Intl.DateTimeFormatOptions = { + weekday: 'short', + month: 'short', + day: 'numeric', + year: 'numeric' + }; + let isMouseOverGroup = false; let actualBucketHeight: number; let hoveredDateGroup = ''; $: assetsGroupByDate = lodash .chain(assets) - .groupBy((a) => moment(a.createdAt).format('ddd, MMM DD YYYY')) + .groupBy((a) => new Date(a.createdAt).toLocaleDateString(locale, groupDateFormat)) .sortBy((group) => assets.indexOf(group[0])) .value(); @@ -107,7 +114,7 @@ bind:clientHeight={actualBucketHeight} > {#each assetsGroupByDate as assetsInDateGroup, groupIndex (assetsInDateGroup[0].id)} - {@const dateGroupTitle = moment(assetsInDateGroup[0].createdAt).format('ddd, MMM DD YYYY')} + {@const dateGroupTitle = new Date(assetsInDateGroup[0].createdAt).toLocaleDateString(locale, groupDateFormat)}
-

{serverInfo?.diskUse} of {serverInfo?.diskSize} used

+

{asByteUnitString(serverInfo?.diskUseRaw)} of {asByteUnitString(serverInfo?.diskSizeRaw)} used

{:else}
diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index 0901cc008c..e393f61dd1 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -6,7 +6,7 @@ import WindowMinimize from 'svelte-material-icons/WindowMinimize.svelte'; import type { UploadAsset } from '$lib/models/upload-asset'; import { notificationController, NotificationType } from './notification/notification'; - import { getHumanReadableBytes } from '../../utils/byte-units'; + import { getBytesWithUnit } from '../../utils/byte-units'; let showDetail = true; @@ -115,7 +115,7 @@ diff --git a/web/src/lib/utils/byte-units.ts b/web/src/lib/utils/byte-units.ts index f8fa139fbe..6c3cd882f7 100644 --- a/web/src/lib/utils/byte-units.ts +++ b/web/src/lib/utils/byte-units.ts @@ -1,4 +1,14 @@ -export function getHumanReadableBytes(bytes: number): string { +/** + * Convert bytes to best human readable unit and number of that unit. + * + * * For `1024` bytes, returns `1` and `KiB`. + * * For `1536` bytes, returns `1.5` and `KiB`. + * + * @param bytes number of bytes + * @param maxPrecision maximum number of decimal places, default is `1` + * @returns size (number) and unit (string) + */ +export function getBytesWithUnit(bytes: number, maxPrecision = 1): [number, string] { const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB']; let magnitude = 0; @@ -12,5 +22,24 @@ export function getHumanReadableBytes(bytes: number): string { } } - return `${remainder.toFixed(magnitude == 0 ? 0 : 1)} ${units[magnitude]}`; + remainder = parseFloat(remainder.toFixed(maxPrecision)); + + return [remainder, units[magnitude]]; +} + +/** + * Localized number of bytes with a unit. + * + * For `1536` bytes: + * * en: `1.5 KiB` + * * de: `1,5 KiB` + * + * @param bytes number of bytes + * @param maxPrecision maximum number of decimal places, default is `1` + * @returns localized bytes with unit as string + */ +export function asByteUnitString(bytes: number, maxPrecision = 1): string { + const locale = Array.from(navigator.languages); + const [size, unit] = getBytesWithUnit(bytes, maxPrecision); + return `${size.toLocaleString(locale)} ${unit}`; }