mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge remote-tracking branch 'origin/main' into feat/yucca-integration
This commit is contained in:
+2
-1
@@ -30,6 +30,7 @@
|
||||
"@immich/ui": "^0.76.0",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.3.0",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@noble/hashes": "^2.2.0",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.0",
|
||||
"@photo-sphere-viewer/markers-plugin": "^5.14.0",
|
||||
@@ -102,7 +103,7 @@
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^7.0.0",
|
||||
"svelte": "5.55.1",
|
||||
"svelte-check": "^4.1.5",
|
||||
"svelte-check": "^4.4.6",
|
||||
"svelte-eslint-parser": "^1.3.3",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^6.0.0",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
@import 'tailwindcss';
|
||||
@import '@immich/ui/theme/default.css';
|
||||
@source "../node_modules/@immich/ui";
|
||||
/* @import '/usr/ui/dist/theme/default.css'; */
|
||||
/* @import '../../../ui/packages/ui/dist/theme/default.css'; */
|
||||
|
||||
@utility immich-form-input {
|
||||
@apply bg-gray-100 ring-1 ring-gray-200 transition outline-none focus-within:ring-1 disabled:cursor-not-allowed dark:bg-gray-800 dark:ring-neutral-900 flex w-full items-center rounded-lg disabled:bg-gray-300 disabled:text-dark dark:disabled:bg-gray-900 dark:disabled:text-gray-200 flex-1 py-2.5 text-base pl-4 pr-4;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
<script lang="ts">
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import AssetChangeDateModal from '$lib/modals/AssetChangeDateModal.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { Icon, modalManager } from '@immich/ui';
|
||||
import { mdiCalendar, mdiPencil } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
asset: AssetResponseDto;
|
||||
};
|
||||
|
||||
const { asset }: Props = $props();
|
||||
|
||||
const timeZone = $derived(asset.exifInfo?.timeZone ?? undefined);
|
||||
const dateTime = $derived(
|
||||
timeZone && asset.exifInfo?.dateTimeOriginal
|
||||
? fromISODateTime(asset.exifInfo.dateTimeOriginal, timeZone)
|
||||
: fromISODateTimeUTC(asset.localDateTime),
|
||||
);
|
||||
const isOwner = $derived(authManager.authenticated && asset.ownerId === authManager.user.id);
|
||||
|
||||
const handleChangeDate = async () => {
|
||||
if (!isOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
await modalManager.show(AssetChangeDateModal, {
|
||||
asset: toTimelineAsset(asset),
|
||||
initialDate: dateTime,
|
||||
initialTimeZone: timeZone,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if dateTime}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full text-start justify-between place-items-start gap-4 py-4"
|
||||
onclick={handleChangeDate}
|
||||
title={isOwner ? $t('edit_date') : ''}
|
||||
class:hover:text-primary={isOwner}
|
||||
data-testid="detail-panel-edit-date-button"
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<Icon icon={mdiCalendar} size="24" />
|
||||
|
||||
<div>
|
||||
<p>
|
||||
{dateTime.toLocaleString({ month: 'short', day: 'numeric', year: 'numeric' }, { locale: $locale })}
|
||||
</p>
|
||||
<div class="flex gap-2 text-sm">
|
||||
<p>
|
||||
{dateTime.toLocaleString(
|
||||
{
|
||||
weekday: 'short',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZoneName: timeZone ? 'longOffset' : undefined,
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isOwner}
|
||||
<div class="p-1">
|
||||
<Icon icon={mdiPencil} size="20" />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{:else if !dateTime && isOwner}
|
||||
<div class="flex justify-between place-items-start gap-4 py-4">
|
||||
<div class="flex gap-4">
|
||||
<Icon icon={mdiCalendar} size="24" />
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<Icon icon={mdiPencil} size="20" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import DetailPanelDate from '$lib/components/asset-viewer/detail-panel-date.svelte';
|
||||
import DetailPanelDescription from '$lib/components/asset-viewer/detail-panel-description.svelte';
|
||||
import DetailPanelLocation from '$lib/components/asset-viewer/detail-panel-location.svelte';
|
||||
import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte';
|
||||
@@ -8,7 +9,6 @@
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import AssetChangeDateModal from '$lib/modals/AssetChangeDateModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
@@ -16,7 +16,6 @@
|
||||
import { delay, getDimensions } from '$lib/utils/asset-utils';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { fromISODateTime, fromISODateTimeUTC, toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { getParentPath } from '$lib/utils/tree-utils';
|
||||
import {
|
||||
AssetMediaSize,
|
||||
@@ -25,9 +24,8 @@
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { Icon, IconButton, LoadingSpinner, modalManager, Text } from '@immich/ui';
|
||||
import { Icon, IconButton, LoadingSpinner, Text } from '@immich/ui';
|
||||
import {
|
||||
mdiCalendar,
|
||||
mdiCamera,
|
||||
mdiCameraIris,
|
||||
mdiClose,
|
||||
@@ -59,12 +57,6 @@
|
||||
let people = $derived(asset.people || []);
|
||||
let unassignedFaces = $derived(asset.unassignedFaces || []);
|
||||
let showingHiddenPeople = $state(false);
|
||||
let timeZone = $derived(asset.exifInfo?.timeZone ?? undefined);
|
||||
let dateTime = $derived(
|
||||
timeZone && asset.exifInfo?.dateTimeOriginal
|
||||
? fromISODateTime(asset.exifInfo.dateTimeOriginal, timeZone)
|
||||
: fromISODateTimeUTC(asset.localDateTime),
|
||||
);
|
||||
let latlng = $derived(
|
||||
(() => {
|
||||
const lat = asset.exifInfo?.latitude;
|
||||
@@ -126,18 +118,6 @@
|
||||
// Remove the last part of the path to get the parent path
|
||||
return Route.folders({ path: getParentPath(asset.originalPath) });
|
||||
};
|
||||
|
||||
const handleChangeDate = async () => {
|
||||
if (!isOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
await modalManager.show(AssetChangeDateModal, {
|
||||
asset: toTimelineAsset(asset),
|
||||
initialDate: dateTime,
|
||||
initialTimeZone: timeZone,
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents onAlbumAddAssets={() => (albums = refreshAlbums())} />
|
||||
@@ -291,65 +271,7 @@
|
||||
<Text size="small" color="muted">{$t('no_exif_info_available')}</Text>
|
||||
{/if}
|
||||
|
||||
{#if dateTime}
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full text-start justify-between place-items-start gap-4 py-4"
|
||||
onclick={handleChangeDate}
|
||||
title={isOwner ? $t('edit_date') : ''}
|
||||
class:hover:text-primary={isOwner}
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div>
|
||||
<Icon icon={mdiCalendar} size="24" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>
|
||||
{dateTime.toLocaleString(
|
||||
{
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
</p>
|
||||
<div class="flex gap-2 text-sm">
|
||||
<p>
|
||||
{dateTime.toLocaleString(
|
||||
{
|
||||
weekday: 'short',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
timeZoneName: timeZone ? 'longOffset' : undefined,
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if isOwner}
|
||||
<div class="p-1">
|
||||
<Icon icon={mdiPencil} size="20" />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
{:else if !dateTime && isOwner}
|
||||
<div class="flex justify-between place-items-start gap-4 py-4">
|
||||
<div class="flex gap-4">
|
||||
<div>
|
||||
<Icon icon={mdiCalendar} size="24" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-1">
|
||||
<Icon icon={mdiPencil} size="20" />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<DetailPanelDate {asset} />
|
||||
|
||||
<div class="flex gap-4 py-4">
|
||||
<div><Icon icon={mdiImageOutline} size="24" /></div>
|
||||
|
||||
@@ -106,13 +106,13 @@
|
||||
assetViewerManager.animatedZoom(targetZoom);
|
||||
};
|
||||
|
||||
const onPlaySlideshow = () => ($slideshowState = SlideshowState.PlaySlideshow);
|
||||
|
||||
$effect(() => {
|
||||
if (assetViewerManager.isFaceEditMode && assetViewerManager.zoom > 1) {
|
||||
const onFaceEditModeChange = (isFaceEditMode: boolean) => {
|
||||
if (isFaceEditMode && assetViewerManager.zoom > 1) {
|
||||
onZoom();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const onPlaySlideshow = () => ($slideshowState = SlideshowState.PlaySlideshow);
|
||||
|
||||
// TODO move to action + command palette
|
||||
const onCopyShortcut = (event: KeyboardEvent) => {
|
||||
@@ -200,7 +200,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<AssetViewerEvents {onCopy} {onZoom} />
|
||||
<AssetViewerEvents {onCopy} {onZoom} {onFaceEditModeChange} />
|
||||
|
||||
<svelte:document
|
||||
use:shortcuts={[
|
||||
|
||||
@@ -291,7 +291,7 @@
|
||||
playbackOnIconHover={!$playVideoThumbnailOnHover}
|
||||
/>
|
||||
</div>
|
||||
{:else if asset.isImage && asset.duration && !asset.duration.includes('0:00:00.000') && mouseOver}
|
||||
{:else if asset.isImage && asset.duration && mouseOver}
|
||||
<!-- GIF -->
|
||||
<div class="absolute h-full w-full pointer-events-none">
|
||||
<ImageThumbnail
|
||||
@@ -369,7 +369,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if asset.isImage && asset.duration && !asset.duration.includes('0:00:00.000')}
|
||||
{#if asset.isImage && asset.duration}
|
||||
<div class="z-2 absolute inset-e-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white">
|
||||
<span class="pe-2 pt-2">
|
||||
<Icon icon={mouseOver ? mdiMotionPauseOutline : mdiFileGifBox} size="24" />
|
||||
|
||||
+3
-10
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { invalidateAll } from '$app/navigation';
|
||||
import Combobox from '$lib/components/shared-components/combobox.svelte';
|
||||
import { defaultLang, langs } from '$lib/constants';
|
||||
import { defaultLang } from '$lib/constants';
|
||||
import { lang } from '$lib/stores/preferences.store';
|
||||
import { getClosestAvailableLocale, langCodes } from '$lib/utils/i18n';
|
||||
import { getClosestAvailableLocale, langCodes, langs } from '$lib/utils/i18n';
|
||||
import { Label, Text } from '@immich/ui';
|
||||
import { locale as i18nLocale, t } from 'svelte-i18n';
|
||||
|
||||
@@ -13,14 +13,7 @@
|
||||
|
||||
let { showSettingDescription = false }: Props = $props();
|
||||
|
||||
const langOptions = langs
|
||||
.map((lang) => ({ label: lang.name, value: lang.code }))
|
||||
.sort((a, b) => {
|
||||
if (b.label.startsWith('Development')) {
|
||||
return -1;
|
||||
}
|
||||
return a.label.localeCompare(b.label);
|
||||
});
|
||||
const langOptions = langs.map((lang) => ({ label: lang.name, value: lang.code }));
|
||||
|
||||
const defaultLangOption = { label: defaultLang.name, value: defaultLang.code };
|
||||
|
||||
|
||||
+1
-100
@@ -229,114 +229,15 @@ export const locales = [
|
||||
{ code: 'zu-ZA', name: 'Zulu (South Africa)' },
|
||||
];
|
||||
|
||||
interface Lang {
|
||||
export interface Lang {
|
||||
name: string;
|
||||
code: string;
|
||||
loader: () => Promise<{ default: object }>;
|
||||
rtl?: boolean;
|
||||
weblateCode?: string;
|
||||
}
|
||||
|
||||
export const defaultLang: Lang = { name: 'English', code: 'en', loader: () => import('$i18n/en.json') };
|
||||
|
||||
export const langs: Lang[] = [
|
||||
{ name: 'Afrikaans', code: 'af', loader: () => import('$i18n/af.json') },
|
||||
{ name: 'Arabic', code: 'ar', loader: () => import('$i18n/ar.json'), rtl: true },
|
||||
{ name: 'Azerbaijani', code: 'az', loader: () => import('$i18n/az.json'), rtl: true },
|
||||
{ name: 'Belarusian', code: 'be', loader: () => import('$i18n/be.json') },
|
||||
{ name: 'Bulgarian', code: 'bg', loader: () => import('$i18n/bg.json') },
|
||||
{ name: 'Bislama', code: 'bi', loader: () => import('$i18n/bi.json') },
|
||||
{ name: 'Bengali', code: 'bn', loader: () => import('$i18n/bn.json') },
|
||||
{ name: 'Breton', code: 'br', loader: () => import('$i18n/br.json') },
|
||||
{ name: 'Catalan', code: 'ca', loader: () => import('$i18n/ca.json') },
|
||||
{ name: 'Czech', code: 'cs', loader: () => import('$i18n/cs.json') },
|
||||
{ name: 'Chuvash', code: 'cv', loader: () => import('$i18n/cv.json') },
|
||||
{ name: 'Danish', code: 'da', loader: () => import('$i18n/da.json') },
|
||||
{ name: 'German', code: 'de', loader: () => import('$i18n/de.json') },
|
||||
{ name: 'German (Switzerland)', code: 'de-CH', weblateCode: 'de_CH', loader: () => import('$i18n/de_CH.json') },
|
||||
defaultLang,
|
||||
{ name: 'Greek', code: 'el', loader: () => import('$i18n/el.json') },
|
||||
{ name: 'Esperanto', code: 'eo', loader: () => import('$i18n/eo.json') },
|
||||
{ name: 'Spanish', code: 'es', loader: () => import('$i18n/es.json') },
|
||||
{ name: 'Estonian', code: 'et', loader: () => import('$i18n/et.json') },
|
||||
{ name: 'Basque', code: 'eu', loader: () => import('$i18n/eu.json') },
|
||||
{ name: 'Persian', code: 'fa', loader: () => import('$i18n/fa.json'), rtl: true },
|
||||
{ name: 'Finnish', code: 'fi', loader: () => import('$i18n/fi.json') },
|
||||
{ name: 'Filipino', code: 'fil', loader: () => import('$i18n/fil.json') },
|
||||
{ name: 'French', code: 'fr', loader: () => import('$i18n/fr.json') },
|
||||
{ name: 'Irish', code: 'ga', loader: () => import('$i18n/ga.json') },
|
||||
{ name: 'Galician', code: 'gl', loader: () => import('$i18n/gl.json') },
|
||||
{ name: 'Alemannic', code: 'gsw', loader: () => import('$i18n/gsw.json') },
|
||||
{ name: 'Gujarati', code: 'gu', loader: () => import('$i18n/gu.json') },
|
||||
{ name: 'Hebrew', code: 'he', loader: () => import('$i18n/he.json'), rtl: true },
|
||||
{ name: 'Hindi', code: 'hi', loader: () => import('$i18n/hi.json') },
|
||||
{ name: 'Croatian', code: 'hr', loader: () => import('$i18n/hr.json') },
|
||||
{ name: 'Hungarian', code: 'hu', loader: () => import('$i18n/hu.json') },
|
||||
{ name: 'Armenian', code: 'hy', loader: () => import('$i18n/hy.json') },
|
||||
{ name: 'Indonesian', code: 'id', loader: () => import('$i18n/id.json') },
|
||||
{ name: 'Icelandic', code: 'is', loader: () => import('$i18n/is.json') },
|
||||
{ name: 'Italian', code: 'it', loader: () => import('$i18n/it.json') },
|
||||
{ name: 'Japanese', code: 'ja', loader: () => import('$i18n/ja.json') },
|
||||
{ name: 'Georgian', code: 'ka', loader: () => import('$i18n/ka.json') },
|
||||
{ name: 'Kazakh', code: 'kk', loader: () => import('$i18n/kk.json') },
|
||||
{ name: 'Khmer (Central)', code: 'km', loader: () => import('$i18n/km.json') },
|
||||
{ name: 'Kurdish (Northern)', code: 'kmr', loader: () => import('$i18n/kmr.json'), rtl: true },
|
||||
{ name: 'Kannada', code: 'kn', loader: () => import('$i18n/kn.json') },
|
||||
{ name: 'Korean', code: 'ko', loader: () => import('$i18n/ko.json') },
|
||||
{ name: 'Luxembourgish', code: 'lb', loader: () => import('$i18n/lb.json') },
|
||||
{ name: 'Lithuanian', code: 'lt', loader: () => import('$i18n/lt.json') },
|
||||
{ name: 'Latvian', code: 'lv', loader: () => import('$i18n/lv.json') },
|
||||
{ name: 'Malay (Pattani)', code: 'mfa', loader: () => import('$i18n/mfa.json') },
|
||||
{ name: 'Macedonian', code: 'mk', loader: () => import('$i18n/mk.json') },
|
||||
{ name: 'Malayalam', code: 'ml', loader: () => import('$i18n/ml.json') },
|
||||
{ name: 'Mongolian', code: 'mn', loader: () => import('$i18n/mn.json') },
|
||||
{ name: 'Marathi', code: 'mr', loader: () => import('$i18n/mr.json') },
|
||||
{ name: 'Malay', code: 'ms', loader: () => import('$i18n/ms.json') },
|
||||
{ name: 'Norwegian Bokmål', code: 'nb-NO', weblateCode: 'nb_NO', loader: () => import('$i18n/nb_NO.json') },
|
||||
{ name: 'Dutch', code: 'nl', loader: () => import('$i18n/nl.json') },
|
||||
{ name: 'Norwegian Nynorsk', code: 'nn', loader: () => import('$i18n/nn.json') },
|
||||
{ name: 'Punjabi', code: 'pa', loader: () => import('$i18n/pa.json') },
|
||||
{ name: 'Polish', code: 'pl', loader: () => import('$i18n/pl.json') },
|
||||
{ name: 'Portuguese', code: 'pt', loader: () => import('$i18n/pt.json') },
|
||||
{ name: 'Portuguese (Brazil) ', code: 'pt-BR', weblateCode: 'pt_BR', loader: () => import('$i18n/pt_BR.json') },
|
||||
{ name: 'Romanian', code: 'ro', loader: () => import('$i18n/ro.json') },
|
||||
{ name: 'Russian', code: 'ru', loader: () => import('$i18n/ru.json') },
|
||||
{ name: 'Sinhala', code: 'si', loader: () => import('$i18n/si.json') },
|
||||
{ name: 'Slovak', code: 'sk', loader: () => import('$i18n/sk.json') },
|
||||
{ name: 'Slovenian', code: 'sl', loader: () => import('$i18n/sl.json') },
|
||||
{ name: 'Albanian', code: 'sq', loader: () => import('$i18n/sq.json') },
|
||||
{
|
||||
name: 'Serbian (Cyrillic)',
|
||||
code: 'sr-Cyrl',
|
||||
weblateCode: 'sr_Cyrl',
|
||||
loader: () => import('$i18n/sr_Cyrl.json'),
|
||||
},
|
||||
{ name: 'Serbian (Latin)', code: 'sr-Latn', weblateCode: 'sr_Latn', loader: () => import('$i18n/sr_Latn.json') },
|
||||
{ name: 'Swedish', code: 'sv', loader: () => import('$i18n/sv.json') },
|
||||
{ name: 'Tamil', code: 'ta', loader: () => import('$i18n/ta.json') },
|
||||
{ name: 'Telugu', code: 'te', loader: () => import('$i18n/te.json') },
|
||||
{ name: 'Thai', code: 'th', loader: () => import('$i18n/th.json') },
|
||||
{ name: 'Turkish', code: 'tr', loader: () => import('$i18n/tr.json') },
|
||||
{ name: 'Ukrainian', code: 'uk', loader: () => import('$i18n/uk.json') },
|
||||
{ name: 'Urdu', code: 'ur', loader: () => import('$i18n/ur.json'), rtl: true },
|
||||
{ name: 'Uzbek', code: 'uz', loader: () => import('$i18n/uz.json') },
|
||||
{ name: 'Vietnamese', code: 'vi', loader: () => import('$i18n/vi.json') },
|
||||
{ name: 'Cantonese (Traditional Han script)', code: 'yue_Hant', loader: () => import('$i18n/yue_Hant.json') },
|
||||
{
|
||||
name: 'Chinese (Traditional)',
|
||||
code: 'zh-TW',
|
||||
weblateCode: 'zh_Hant',
|
||||
loader: () => import('$i18n/zh_Hant.json'),
|
||||
},
|
||||
{
|
||||
name: 'Chinese (Simplified)',
|
||||
code: 'zh-CN',
|
||||
weblateCode: 'zh_Hans',
|
||||
loader: () => import('$i18n/zh_Hans.json'),
|
||||
},
|
||||
{ name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({ default: {} }) },
|
||||
];
|
||||
|
||||
export enum ImmichProduct {
|
||||
Client = 'immich-client',
|
||||
Server = 'immich-server',
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { langs } from '$lib/constants';
|
||||
import { getClosestAvailableLocale } from '$lib/utils/i18n';
|
||||
import { getClosestAvailableLocale, langs } from '$lib/utils/i18n';
|
||||
import { readFileSync, readdirSync } from 'node:fs';
|
||||
|
||||
describe('i18n', () => {
|
||||
@@ -12,7 +11,7 @@ describe('i18n', () => {
|
||||
}
|
||||
|
||||
const code = filename.replaceAll('.json', '');
|
||||
const item = langs.find((lang) => lang.weblateCode === code || lang.code === code);
|
||||
const item = langs.find((lang) => lang.code === code);
|
||||
expect(item, `${filename} has no loader`).toBeDefined();
|
||||
if (!item) {
|
||||
return;
|
||||
|
||||
@@ -23,6 +23,7 @@ export type Events = {
|
||||
Zoom: [];
|
||||
ZoomChange: [ZoomImageWheelState];
|
||||
Copy: [];
|
||||
FaceEditModeChange: [boolean];
|
||||
};
|
||||
|
||||
class AssetViewerManager extends BaseEventManager<Events> {
|
||||
@@ -185,9 +186,13 @@ class AssetViewerManager extends BaseEventManager<Events> {
|
||||
|
||||
toggleFaceEditMode() {
|
||||
this.#isFaceEditMode = !this.#isFaceEditMode;
|
||||
this.emit('FaceEditModeChange', this.#isFaceEditMode);
|
||||
}
|
||||
|
||||
closeFaceEditMode() {
|
||||
if (this.#isFaceEditMode) {
|
||||
this.emit('FaceEditModeChange', false);
|
||||
}
|
||||
this.#isFaceEditMode = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { langs } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { lang } from '$lib/stores/preferences.store';
|
||||
import { langs } from '$lib/utils/i18n';
|
||||
|
||||
class LanguageManager {
|
||||
constructor() {
|
||||
|
||||
@@ -25,7 +25,7 @@ export async function loadFromTimeBuckets(
|
||||
{ signal },
|
||||
);
|
||||
|
||||
if (!bucketResponse) {
|
||||
if (!bucketResponse || signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ export async function loadFromTimeBuckets(
|
||||
},
|
||||
{ signal },
|
||||
);
|
||||
if (!albumAssets) {
|
||||
if (!albumAssets || signal.aborted) {
|
||||
return;
|
||||
}
|
||||
for (const id of albumAssets.id) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { defaultLang, langs, locales } from '$lib/constants';
|
||||
import { defaultLang, locales } from '$lib/constants';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { alwaysLoadOriginalFile, lang } from '$lib/stores/preferences.store';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { langs } from '$lib/utils/i18n';
|
||||
import {
|
||||
AssetMediaSize,
|
||||
AssetTypeEnum,
|
||||
@@ -218,7 +219,7 @@ export function getAssetUrls(asset: AssetResponseDto, sharedLink?: SharedLinkRes
|
||||
}
|
||||
|
||||
const forceUseOriginal = (asset: AssetResponseDto) => {
|
||||
return asset.type === AssetTypeEnum.Image && asset.duration && !asset.duration.includes('0:00:00.000');
|
||||
return asset.type === AssetTypeEnum.Image && asset.duration;
|
||||
};
|
||||
|
||||
export const targetImageSize = (asset: AssetResponseDto, forceOriginal: boolean) => {
|
||||
|
||||
@@ -20,9 +20,6 @@ describe('fileUploader error handling', () => {
|
||||
vi.spyOn(uploadManager, 'getExtensions').mockReturnValue(['.jpg']);
|
||||
uploadAssetsStore.reset();
|
||||
authManager.reset();
|
||||
|
||||
// Stub out crypto to avoid that branch
|
||||
vi.stubGlobal('crypto', undefined);
|
||||
});
|
||||
|
||||
for (const [name, mockUser] of [
|
||||
|
||||
@@ -127,6 +127,30 @@ function getDeviceAssetId(asset: File) {
|
||||
return 'web' + '-' + asset.name + '-' + asset.lastModified;
|
||||
}
|
||||
|
||||
function hashFile(file: File): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const worker = new Worker(new URL('$lib/workers/hash-file.ts', import.meta.url), { type: 'module' });
|
||||
|
||||
worker.addEventListener('message', ({ data }: MessageEvent<{ result?: string; error?: string }>) => {
|
||||
worker.terminate();
|
||||
|
||||
if (data.error) {
|
||||
reject(new Error(data.error));
|
||||
} else {
|
||||
resolve(data.result!);
|
||||
}
|
||||
});
|
||||
|
||||
worker.addEventListener('error', (event) => {
|
||||
worker.terminate();
|
||||
|
||||
reject(new Error(event.message));
|
||||
});
|
||||
|
||||
worker.postMessage(file);
|
||||
});
|
||||
}
|
||||
|
||||
type FileUploaderParams = {
|
||||
assetFile: File;
|
||||
albumId?: string;
|
||||
@@ -155,7 +179,6 @@ async function fileUploader({
|
||||
fileCreatedAt,
|
||||
fileModifiedAt: new Date(assetFile.lastModified).toISOString(),
|
||||
isFavorite: 'false',
|
||||
duration: '0:00:00.000000',
|
||||
assetData: new File([assetFile], assetFile.name),
|
||||
})) {
|
||||
formData.append(key, value);
|
||||
@@ -166,15 +189,11 @@ async function fileUploader({
|
||||
}
|
||||
|
||||
let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined;
|
||||
if (crypto?.subtle?.digest && !authManager.isSharedLink) {
|
||||
if (!authManager.isSharedLink) {
|
||||
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
|
||||
await tick();
|
||||
try {
|
||||
const bytes = await assetFile.arrayBuffer();
|
||||
const hash = await crypto.subtle.digest('SHA-1', bytes);
|
||||
const checksum = Array.from(new Uint8Array(hash))
|
||||
.map((b) => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
const checksum = await hashFile(assetFile);
|
||||
|
||||
const {
|
||||
results: [checkUploadResult],
|
||||
|
||||
+111
-12
@@ -1,8 +1,8 @@
|
||||
import { langs } from '$lib/constants';
|
||||
import type { Lang } from '$lib/constants';
|
||||
import { locale, t, waitLocale } from 'svelte-i18n';
|
||||
import { get, type Unsubscriber } from 'svelte/store';
|
||||
|
||||
export async function getFormatter() {
|
||||
export const getFormatter = async () => {
|
||||
let unsubscribe: Unsubscriber | undefined;
|
||||
await new Promise((resolve) => {
|
||||
unsubscribe = locale.subscribe((value) => value && resolve(value));
|
||||
@@ -11,23 +11,122 @@ export async function getFormatter() {
|
||||
|
||||
await waitLocale();
|
||||
return get(t);
|
||||
}
|
||||
};
|
||||
|
||||
const modules = import.meta.glob('$i18n/*.json');
|
||||
|
||||
const fileCodes = Object.keys(modules)
|
||||
.map((path) => path.match(/\/(\w+)\.json$/)?.[1])
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
const convertBCP47 = (code: string) => code.replace('_', '-');
|
||||
|
||||
export const langCodes = fileCodes.map((code) => convertBCP47(code));
|
||||
|
||||
// https://github.com/kaisermann/svelte-i18n/blob/780932a3e1270d521d348aac8ba03be9df309f04/src/runtime/stores/locale.ts#L11
|
||||
function getSubLocales(refLocale: string) {
|
||||
return refLocale
|
||||
const getSubLocales = (locale: string) => {
|
||||
return locale
|
||||
.split('-')
|
||||
.map((_, i, arr) => arr.slice(0, i + 1).join('-'))
|
||||
.reverse();
|
||||
}
|
||||
};
|
||||
|
||||
export function getClosestAvailableLocale(locales: readonly string[], allLocales: readonly string[]) {
|
||||
export const getClosestAvailableLocale = (locales: readonly string[], allLocales: readonly string[]) => {
|
||||
const allLocalesSet = new Set(allLocales);
|
||||
return locales.find((locale) => getSubLocales(locale).some((subLocale) => allLocalesSet.has(subLocale)));
|
||||
}
|
||||
};
|
||||
|
||||
export const langCodes = langs.map((lang) => lang.code);
|
||||
export const getPreferredLocale = () => getClosestAvailableLocale(navigator.languages, langCodes);
|
||||
|
||||
export function getPreferredLocale() {
|
||||
return getClosestAvailableLocale(navigator.languages, langCodes);
|
||||
}
|
||||
const rtlCodes = new Set([
|
||||
'ae',
|
||||
'aeb',
|
||||
'aii',
|
||||
'ajp',
|
||||
'apc',
|
||||
'apd',
|
||||
'ar',
|
||||
'ar_BH',
|
||||
'ar_DZ',
|
||||
'ar_EG',
|
||||
'ar_KW',
|
||||
'ar_LY',
|
||||
'ar_MA',
|
||||
'ar_SA',
|
||||
'ar_YE',
|
||||
'ara',
|
||||
'arc',
|
||||
'arq',
|
||||
'ars',
|
||||
'arz',
|
||||
'ave',
|
||||
'bal',
|
||||
'bcc',
|
||||
'bgn',
|
||||
'bqi',
|
||||
'ckb',
|
||||
'ckb_IR',
|
||||
'dv',
|
||||
'egy',
|
||||
'fa',
|
||||
'fa_AF',
|
||||
'fas',
|
||||
'glk',
|
||||
'ha',
|
||||
'he',
|
||||
'heb',
|
||||
'khw',
|
||||
'kmr',
|
||||
'ks',
|
||||
'ku',
|
||||
'lrc',
|
||||
'luz',
|
||||
'ms_Arab',
|
||||
'mzn',
|
||||
'nqo',
|
||||
'pa_PK',
|
||||
'pal',
|
||||
'per',
|
||||
'phn',
|
||||
'pnb',
|
||||
'prs',
|
||||
'ps',
|
||||
'rhg',
|
||||
'sam',
|
||||
'sd',
|
||||
'sdh',
|
||||
'skr',
|
||||
'syc',
|
||||
'syr',
|
||||
'ug',
|
||||
'ur',
|
||||
'ur_IN',
|
||||
'urd',
|
||||
'yi',
|
||||
]);
|
||||
|
||||
const capitalize = (string: string) =>
|
||||
string
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
|
||||
const nonIntlNames: Record<string, string> = {
|
||||
mfa: 'Malay (Pattani)',
|
||||
bi: 'Bislama',
|
||||
};
|
||||
|
||||
const getLanguageName = (code: string) =>
|
||||
nonIntlNames[code] ?? capitalize(new Intl.DisplayNames([code], { type: 'language' }).of(code) ?? code);
|
||||
|
||||
export const langs: Lang[] = [
|
||||
...fileCodes
|
||||
.map((code) => ({
|
||||
name: getLanguageName(convertBCP47(code)),
|
||||
code,
|
||||
loader: () => import(`$i18n/${code}.json`),
|
||||
rtl: rtlCodes.has(code),
|
||||
}))
|
||||
.sort((a, b) => a.code.localeCompare(b.code)),
|
||||
{ name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({ default: {} }) },
|
||||
];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAssetMediaUrl, setSharedLink } from '$lib/utils';
|
||||
import { getAssetMediaUrl, getSharedLink as getCachedSharedLink, setSharedLink } from '$lib/utils';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
||||
@@ -28,11 +28,16 @@ export const loadSharedLink = async ({
|
||||
await authenticate(url, { public: true });
|
||||
|
||||
const common = { key, slug };
|
||||
|
||||
const $t = await getFormatter();
|
||||
|
||||
const cachedSharedLink = getCachedSharedLink();
|
||||
const sharedLinkPromise =
|
||||
cachedSharedLink && (key === cachedSharedLink.key || slug === cachedSharedLink.slug)
|
||||
? Promise.resolve(cachedSharedLink)
|
||||
: getMySharedLink({ key, slug });
|
||||
|
||||
try {
|
||||
const [sharedLink, asset] = await Promise.all([getMySharedLink({ key, slug }), getAssetInfoFromParam(params)]);
|
||||
const [sharedLink, asset] = await Promise.all([sharedLinkPromise, getAssetInfoFromParam(params)]);
|
||||
setSharedLink(sharedLink);
|
||||
const assetCount = sharedLink.assets.length;
|
||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { sha1 } from '@noble/hashes/legacy.js';
|
||||
import { bytesToHex } from '@noble/hashes/utils.js';
|
||||
|
||||
const HASH_CHUNK_SIZE = 5 * 1024 * 1024;
|
||||
|
||||
async function hashFile(file: File): Promise<string> {
|
||||
const hasher = sha1.create();
|
||||
|
||||
for (let offset = 0; offset < file.size; offset += HASH_CHUNK_SIZE) {
|
||||
const slice = file.slice(offset, Math.min(offset + HASH_CHUNK_SIZE, file.size));
|
||||
|
||||
const buffer = await slice.arrayBuffer();
|
||||
|
||||
hasher.update(new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
return bytesToHex(hasher.digest());
|
||||
}
|
||||
|
||||
addEventListener('message', (event: MessageEvent<File>) => {
|
||||
void hashFile(event.data)
|
||||
.then((result) => postMessage({ result }))
|
||||
.catch((error: unknown) => postMessage({ error: error instanceof Error ? error.message : String(error) }));
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import UploadCover from '$lib/components/shared-components/drag-and-drop-upload-overlay.svelte';
|
||||
import UploadCover from './drag-and-drop-upload-overlay.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
interface Props {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { scrollMemory } from '$lib/actions/scroll-memory';
|
||||
import AlbumsControls from '$lib/components/album-page/albums-controls.svelte';
|
||||
import AlbumsControls from './albums-controls.svelte';
|
||||
import Albums from '$lib/components/album-page/albums-list.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
|
||||
+10
-4
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { goto, onNavigate } from '$app/navigation';
|
||||
import { goto, invalidate, onNavigate } from '$app/navigation';
|
||||
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
|
||||
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
|
||||
import AlbumDescription from './album-description.svelte';
|
||||
import AlbumMap from '$lib/components/album-page/album-map.svelte';
|
||||
import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
|
||||
import AlbumTitle from '$lib/components/album-page/album-title.svelte';
|
||||
import AlbumTitle from './album-title.svelte';
|
||||
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
|
||||
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte';
|
||||
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
|
||||
@@ -309,6 +309,12 @@
|
||||
album = { ...album, albumUsers };
|
||||
};
|
||||
|
||||
const onAlbumUpdate = async (newAlbum: AlbumResponseDto) => {
|
||||
album = newAlbum;
|
||||
|
||||
await invalidate('album:data');
|
||||
};
|
||||
|
||||
const { Cast } = $derived(getGlobalActions($t));
|
||||
const { Share } = $derived(getAlbumActions($t, album));
|
||||
const { AddAssets, Upload } = $derived(getAlbumAssetsActions($t, album, timelineMultiSelectManager.assets));
|
||||
@@ -330,7 +336,7 @@
|
||||
{onAlbumShare}
|
||||
{onAlbumUserUpdate}
|
||||
onAlbumUserDelete={refreshAlbum}
|
||||
onAlbumUpdate={(newAlbum) => (album = newAlbum)}
|
||||
{onAlbumUpdate}
|
||||
/>
|
||||
<CommandPaletteDefaultProvider name={$t('album')} actions={[AddAssets, Upload, Close]} />
|
||||
|
||||
|
||||
@@ -2,8 +2,11 @@ import { authenticate } from '$lib/utils/auth';
|
||||
import { getAlbumInfo } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params, url }) => {
|
||||
export const load = (async ({ params, url, depends }) => {
|
||||
await authenticate(url);
|
||||
|
||||
depends('album:data');
|
||||
|
||||
const album = await getAlbumInfo({ id: params.albumId });
|
||||
|
||||
return {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import AlbumDescription from '$lib/components/album-page/album-description.svelte';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import { describe } from 'vitest';
|
||||
import AlbumDescription from './album-description.svelte';
|
||||
|
||||
describe('AlbumDescription component', () => {
|
||||
it('shows an AutogrowTextarea component when isOwned is true', () => {
|
||||
@@ -3,7 +3,7 @@
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte';
|
||||
import LicenseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
||||
import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte';
|
||||
import SupporterBadge from './supporter-badge.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { Alert, Container, Stack } from '@immich/ui';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import MapTimelinePanel from '$lib/components/shared-components/map/MapTimelinePanel.svelte';
|
||||
import MapTimelinePanel from './MapTimelinePanel.svelte';
|
||||
import type { SelectionBBox } from '$lib/components/shared-components/map/types';
|
||||
import { timeToLoadTheMap } from '$lib/constants';
|
||||
import Portal from '$lib/elements/Portal.svelte';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import MemoryViewer from '$lib/components/memory-page/memory-viewer.svelte';
|
||||
import MemoryViewer from './memory-viewer.svelte';
|
||||
</script>
|
||||
|
||||
<MemoryViewer />
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import MemoryPhotoViewer from '$lib/components/memory-page/memory-photo-viewer.svelte';
|
||||
import MemoryVideoViewer from '$lib/components/memory-page/memory-video-viewer.svelte';
|
||||
import MemoryPhotoViewer from './memory-photo-viewer.svelte';
|
||||
import MemoryVideoViewer from './memory-video-viewer.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
@@ -3,9 +3,9 @@
|
||||
import { page } from '$app/stores';
|
||||
import { scrollMemory } from '$lib/actions/scroll-memory';
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte';
|
||||
import PeopleCard from '$lib/components/faces-page/people-card.svelte';
|
||||
import PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte';
|
||||
import ManagePeopleVisibility from './manage-people-visibility.svelte';
|
||||
import PeopleCard from './people-card.svelte';
|
||||
import PeopleInfiniteScroll from './people-infinite-scroll.svelte';
|
||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
|
||||
+3
-3
@@ -5,9 +5,9 @@
|
||||
import { listNavigation } from '$lib/actions/list-navigation';
|
||||
import { scrollMemoryClearer } from '$lib/actions/scroll-memory';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
|
||||
import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte';
|
||||
import UnMergeFaceSelector from '$lib/components/faces-page/unmerge-face-selector.svelte';
|
||||
import EditNameInput from './edit-name-input.svelte';
|
||||
import MergeFaceSelector from './merge-face-selector.svelte';
|
||||
import UnMergeFaceSelector from './unmerge-face-selector.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
import { type PersonResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
|
||||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { type PersonResponseDto } from '@immich/sdk';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
|
||||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
import { flip } from 'svelte/animate';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import FaceThumbnail from './face-thumbnail.svelte';
|
||||
import PeopleList from './people-list.svelte';
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import FaceThumbnail from './face-thumbnail.svelte';
|
||||
import PeopleList from './people-list.svelte';
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import PeopleInfiniteScroll from '$lib/components/faces-page/people-infinite-scroll.svelte';
|
||||
import PeopleInfiniteScroll from './people-infinite-scroll.svelte';
|
||||
import { ToggleVisibility } from '$lib/constants';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
+2
-2
@@ -16,8 +16,8 @@
|
||||
mdiHeartOutline,
|
||||
} from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
|
||||
type Props = {
|
||||
person: PersonResponseDto;
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import PlacesControls from '$lib/components/places-page/places-controls.svelte';
|
||||
import PlacesControls from './places-controls.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import Places from '$lib/components/places-page/places-list.svelte';
|
||||
import Places from './places-list.svelte';
|
||||
import { placesViewSettings } from '$lib/stores/preferences.store';
|
||||
|
||||
interface Props {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { page } from '$app/state';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/SharedLinkCard.svelte';
|
||||
import SharedLinkCard from './SharedLinkCard.svelte';
|
||||
import { type SharedLinkTab } from '$lib/constants';
|
||||
import GroupTab from '$lib/elements/GroupTab.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
|
||||
import ShareCover from './share-cover.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getSharedLinkActions } from '$lib/services/shared-link.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
+1
-1
@@ -1,9 +1,9 @@
|
||||
import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte';
|
||||
import { getAssetMediaUrl } from '$lib/utils';
|
||||
import { albumFactory } from '@test-data/factories/album-factory';
|
||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||
import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
|
||||
import { render, screen } from '@testing-library/svelte';
|
||||
import ShareCover from './share-cover.svelte';
|
||||
|
||||
vi.mock('$lib/utils');
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import DeleteAssets from '$lib/components/timeline/actions/DeleteAssetsAction.svelte';
|
||||
import RestoreAssets from '$lib/components/timeline/actions/RestoreAction.svelte';
|
||||
import RestoreAssets from './RestoreAction.svelte';
|
||||
import SelectAllAssets from '$lib/components/timeline/actions/SelectAllAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import UserSettingsList from '$lib/components/user-settings-page/user-settings-list.svelte';
|
||||
import UserSettingsList from './user-settings-list.svelte';
|
||||
import { getKeyboardActions } from '$lib/services/keyboard.service';
|
||||
import { Container } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import PinCodeChangeForm from '$lib/components/user-settings-page/PinCodeChangeForm.svelte';
|
||||
import PinCodeChangeForm from './PinCodeChangeForm.svelte';
|
||||
import PinCodeCreateForm from '$lib/components/user-settings-page/PinCodeCreateForm.svelte';
|
||||
import { getAuthStatus } from '@immich/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { ComboBoxOption } from '$lib/components/shared-components/combobox.svelte';
|
||||
import SettingCombobox from '$lib/components/shared-components/settings/setting-combobox.svelte';
|
||||
import SettingCombobox from './setting-combobox.svelte';
|
||||
import SettingsLanguageSelector from '$lib/components/shared-components/settings/settings-language-selector.svelte';
|
||||
import { fallbackLocale, locales } from '$lib/constants';
|
||||
import {
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
import { Button, modalManager, Text, toastManager } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
import DeviceCard from './device-card.svelte';
|
||||
import DeviceCard from '$lib/components/user-settings-page/device-card.svelte';
|
||||
|
||||
interface Props {
|
||||
devices: SessionResponseDto[];
|
||||
+8
-8
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import ChangePinCodeSettings from '$lib/components/user-settings-page/PinCodeSettings.svelte';
|
||||
import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte';
|
||||
import FeatureSettings from '$lib/components/user-settings-page/feature-settings.svelte';
|
||||
import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte';
|
||||
import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte';
|
||||
import UserUsageStatistic from '$lib/components/user-settings-page/user-usage-statistic.svelte';
|
||||
import ChangePinCodeSettings from './PinCodeSettings.svelte';
|
||||
import DownloadSettings from './download-settings.svelte';
|
||||
import FeatureSettings from './feature-settings.svelte';
|
||||
import NotificationsSettings from './notifications-settings.svelte';
|
||||
import UserPurchaseSettings from './user-purchase-settings.svelte';
|
||||
import UserUsageStatistic from './user-usage-statistic.svelte';
|
||||
import { OpenQueryParam, QueryParameter } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { oauth } from '$lib/utils';
|
||||
@@ -26,8 +26,8 @@
|
||||
mdiTwoFactorAuthentication,
|
||||
} from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import SettingAccordionState from '../shared-components/settings/setting-accordion-state.svelte';
|
||||
import SettingAccordion from '../shared-components/settings/setting-accordion.svelte';
|
||||
import SettingAccordionState from '$lib/components/shared-components/settings/setting-accordion-state.svelte';
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||
import AppSettings from './app-settings.svelte';
|
||||
import ChangePasswordSettings from './change-password-settings.svelte';
|
||||
import DeviceList from './device-list.svelte';
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import UtilitiesMenu from '$lib/components/utilities-page/utilities-menu.svelte';
|
||||
import UtilitiesMenu from './utilities-menu.svelte';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@
|
||||
import { page } from '$app/state';
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import LinkToDocs from '$lib/components/LinkToDocs.svelte';
|
||||
import DuplicatesCompareControl from '$lib/components/utilities-page/duplicates/duplicates-compare-control.svelte';
|
||||
import LinkToDocs from './LinkToDocs.svelte';
|
||||
import DuplicatesCompareControl from './duplicates-compare-control.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
|
||||
-2
@@ -33,7 +33,6 @@
|
||||
let { assets, asset, isSelected, onSelectAsset, onViewAsset }: Props = $props();
|
||||
|
||||
let isFromExternalLibrary = $derived(!!asset.libraryId);
|
||||
let assetData = $derived(JSON.stringify(asset, null, 2));
|
||||
|
||||
let locationParts = $derived([asset.exifInfo?.city, asset.exifInfo?.state, asset.exifInfo?.country].filter(Boolean));
|
||||
|
||||
@@ -114,7 +113,6 @@
|
||||
<img
|
||||
src={getAssetMediaUrl({ id: asset.id })}
|
||||
alt={$getAltText(toTimelineAsset(asset))}
|
||||
title={assetData}
|
||||
class="h-60 object-cover w-full rounded-t-md"
|
||||
draggable="false"
|
||||
/>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { shortcuts } from '$lib/actions/shortcut';
|
||||
import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte';
|
||||
import DuplicateAsset from './duplicate-asset.svelte';
|
||||
import Portal from '$lib/elements/Portal.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import LargeAssetData from '$lib/components/utilities-page/large-assets/large-asset-data.svelte';
|
||||
import LargeAssetData from './large-asset-data.svelte';
|
||||
import Portal from '$lib/elements/Portal.svelte';
|
||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
import { beforeNavigate, goto } from '$app/navigation';
|
||||
import { dragAndDrop } from '$lib/attachments/drag-and-drop.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import SchemaFormFields from '$lib/components/workflows/SchemaFormFields.svelte';
|
||||
import WorkflowCardConnector from '$lib/components/workflows/WorkflowCardConnector.svelte';
|
||||
import WorkflowJsonEditor from '$lib/components/workflows/WorkflowJsonEditor.svelte';
|
||||
import WorkflowSummarySidebar from '$lib/components/workflows/WorkflowSummary.svelte';
|
||||
import WorkflowTriggerCard from '$lib/components/workflows/WorkflowTriggerCard.svelte';
|
||||
import SchemaFormFields from './SchemaFormFields.svelte';
|
||||
import WorkflowCardConnector from './WorkflowCardConnector.svelte';
|
||||
import WorkflowJsonEditor from './WorkflowJsonEditor.svelte';
|
||||
import WorkflowSummarySidebar from './WorkflowSummary.svelte';
|
||||
import WorkflowTriggerCard from './WorkflowTriggerCard.svelte';
|
||||
import AddWorkflowStepModal from '$lib/modals/AddWorkflowStepModal.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import WorkflowPickerItemCard from '$lib/components/workflows/WorkflowPickerItemCard.svelte';
|
||||
import WorkflowPickerItemCard from './WorkflowPickerItemCard.svelte';
|
||||
import AlbumPickerModal from '$lib/modals/AlbumPickerModal.svelte';
|
||||
import PeoplePickerModal from '$lib/modals/PeoplePickerModal.svelte';
|
||||
import { fetchPickerMetadata, type PickerMetadata } from '$lib/services/workflow.service';
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import { page } from '$app/state';
|
||||
import ErrorLayout from '$lib/components/layouts/ErrorLayout.svelte';
|
||||
import ErrorLayout from './ErrorLayout.svelte';
|
||||
</script>
|
||||
|
||||
<ErrorLayout error={page.error}></ErrorLayout>
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { getPagesProvider, getSettingsProvider } from '$lib/commands';
|
||||
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
|
||||
import ErrorLayout from '$lib/components/layouts/ErrorLayout.svelte';
|
||||
import DownloadPanel from './download-panel.svelte';
|
||||
import ErrorLayout from './ErrorLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import NavigationLoadingBar from '$lib/components/shared-components/navigation-loading-bar.svelte';
|
||||
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
|
||||
import VersionAnnouncement from '$lib/components/VersionAnnouncement.svelte';
|
||||
import NavigationLoadingBar from './navigation-loading-bar.svelte';
|
||||
import UploadPanel from './upload-panel.svelte';
|
||||
import VersionAnnouncement from './VersionAnnouncement.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import JobsPanel from '$lib/components/QueuePanel.svelte';
|
||||
import JobsPanel from './QueuePanel.svelte';
|
||||
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||
import { getQueuesActions } from '$lib/services/queue.service';
|
||||
import { type QueueResponseDto } from '@immich/sdk';
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { cleanClass } from '$lib';
|
||||
import QueueCardBadge from '$lib/components/QueueCardBadge.svelte';
|
||||
import QueueCardButton from '$lib/components/QueueCardButton.svelte';
|
||||
import QueueCardBadge from './QueueCardBadge.svelte';
|
||||
import QueueCardButton from './QueueCardButton.svelte';
|
||||
import Badge from '$lib/elements/Badge.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { asQueueItem } from '$lib/services/queue.service';
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import QueueCard from '$lib/components/QueueCard.svelte';
|
||||
import QueueStorageMigrationDescription from '$lib/components/QueueStorageMigrationDescription.svelte';
|
||||
import QueueCard from './QueueCard.svelte';
|
||||
import QueueStorageMigrationDescription from './QueueStorageMigrationDescription.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||
import { asQueueItem } from '$lib/services/queue.service';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user