Merge remote-tracking branch 'origin/main' into feat/yucca-integration

This commit is contained in:
izzy
2026-04-20 10:55:40 +01:00
338 changed files with 4761 additions and 5589 deletions
+2 -1
View File
@@ -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
View File
@@ -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" />
@@ -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
View File
@@ -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',
+2 -3
View File
@@ -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) {
+3 -2
View File
@@ -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) => {
-3
View File
@@ -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 [
+26 -7
View File
@@ -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
View File
@@ -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: {} }) },
];
+8 -3
View File
@@ -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;
+24
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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';
@@ -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,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', () => {
+1 -1
View File
@@ -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,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 -3
View File
@@ -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';
@@ -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';
@@ -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,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;
@@ -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';
@@ -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,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';
@@ -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;
+2 -2
View File
@@ -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,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,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,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,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 {
@@ -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[];
@@ -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 -1
View File
@@ -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;
@@ -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';
@@ -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,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,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,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 -1
View File
@@ -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>
+5 -5
View File
@@ -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 -1
View File
@@ -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';
@@ -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';
@@ -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