mirror of
https://github.com/immich-app/immich.git
synced 2025-12-12 15:50:43 -08:00
chore: refactor svelte reactivity (#24072)
This commit is contained in:
@@ -62,10 +62,7 @@ export const setupTimelineMockApiRoutes = async (
|
||||
return route.continue();
|
||||
});
|
||||
|
||||
await context.route('**/api/assets/**', async (route, request) => {
|
||||
const pattern = /\/api\/assets\/(?<assetId>[^/]+)\/thumbnail\?size=(?<size>preview|thumbnail)/;
|
||||
const match = request.url().match(pattern);
|
||||
if (!match) {
|
||||
await context.route('**/api/assets/*', async (route, request) => {
|
||||
const url = new URL(request.url());
|
||||
const pathname = url.pathname;
|
||||
const assetId = basename(pathname);
|
||||
@@ -75,37 +72,50 @@ export const setupTimelineMockApiRoutes = async (
|
||||
contentType: 'application/json',
|
||||
json: asset,
|
||||
});
|
||||
});
|
||||
|
||||
await context.route('**/api/assets/*/ocr', async (route) => {
|
||||
return route.fulfill({ status: 200, contentType: 'application/json', json: [] });
|
||||
});
|
||||
|
||||
await context.route('**/api/assets/*/thumbnail?size=*', async (route, request) => {
|
||||
const pattern = /\/api\/assets\/(?<assetId>[^/]+)\/thumbnail\?size=(?<size>preview|thumbnail)/;
|
||||
const match = request.url().match(pattern);
|
||||
if (!match?.groups) {
|
||||
throw new Error(`Invalid URL for thumbnail endpoint: ${request.url()}`);
|
||||
}
|
||||
if (match.groups?.size === 'preview') {
|
||||
|
||||
if (match.groups.size === 'preview') {
|
||||
if (!route.request().serviceWorker()) {
|
||||
return route.continue();
|
||||
}
|
||||
const asset = getAsset(timelineRestData, match.groups?.assetId);
|
||||
const asset = getAsset(timelineRestData, match.groups.assetId);
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'content-type': 'image/jpeg', ETag: 'abc123', 'Cache-Control': 'public, max-age=3600' },
|
||||
body: await randomPreview(
|
||||
match.groups?.assetId,
|
||||
match.groups.assetId,
|
||||
(asset?.exifInfo?.exifImageWidth ?? 0) / (asset?.exifInfo?.exifImageHeight ?? 1),
|
||||
),
|
||||
});
|
||||
}
|
||||
if (match.groups?.size === 'thumbnail') {
|
||||
if (match.groups.size === 'thumbnail') {
|
||||
if (!route.request().serviceWorker()) {
|
||||
return route.continue();
|
||||
}
|
||||
const asset = getAsset(timelineRestData, match.groups?.assetId);
|
||||
const asset = getAsset(timelineRestData, match.groups.assetId);
|
||||
return route.fulfill({
|
||||
status: 200,
|
||||
headers: { 'content-type': 'image/jpeg' },
|
||||
body: await randomThumbnail(
|
||||
match.groups?.assetId,
|
||||
match.groups.assetId,
|
||||
(asset?.exifInfo?.exifImageWidth ?? 0) / (asset?.exifInfo?.exifImageHeight ?? 1),
|
||||
),
|
||||
});
|
||||
}
|
||||
return route.continue();
|
||||
});
|
||||
|
||||
await context.route('**/api/albums/**', async (route, request) => {
|
||||
const pattern = /\/api\/albums\/(?<albumId>[^/?]+)/;
|
||||
const match = request.url().match(pattern);
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
let innerHeight: number = $state(0);
|
||||
let activityHeight: number = $state(0);
|
||||
let chatHeight: number = $state(0);
|
||||
let divHeight: number = $state(0);
|
||||
let divHeight = $derived(innerHeight - activityHeight);
|
||||
let previousAssetId: string | undefined = $state(assetId);
|
||||
let message = $state('');
|
||||
let isSendingMessage = $state(false);
|
||||
@@ -96,11 +96,7 @@
|
||||
}
|
||||
isSendingMessage = false;
|
||||
};
|
||||
$effect(() => {
|
||||
if (innerHeight && activityHeight) {
|
||||
divHeight = innerHeight - activityHeight;
|
||||
}
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (assetId && previousAssetId != assetId) {
|
||||
previousAssetId = assetId;
|
||||
|
||||
@@ -35,15 +35,13 @@
|
||||
});
|
||||
};
|
||||
|
||||
let albumNameArray: string[] = $state(['', '', '']);
|
||||
|
||||
// This part of the code is responsible for splitting album name into 3 parts where part 2 is the search query
|
||||
// It is used to highlight the search query in the album name
|
||||
$effect(() => {
|
||||
const albumNameArray: string[] = $derived.by(() => {
|
||||
let { albumName } = album;
|
||||
let findIndex = normalizeSearchString(albumName).indexOf(normalizeSearchString(searchQuery));
|
||||
let findLength = searchQuery.length;
|
||||
albumNameArray = [
|
||||
return [
|
||||
albumName.slice(0, findIndex),
|
||||
albumName.slice(findIndex, findIndex + findLength),
|
||||
albumName.slice(findIndex + findLength),
|
||||
|
||||
@@ -395,13 +395,11 @@
|
||||
}
|
||||
});
|
||||
|
||||
let currentAssetId = $derived(asset.id);
|
||||
// primarily, this is reactive on `asset`
|
||||
$effect(() => {
|
||||
if (currentAssetId) {
|
||||
untrack(() => handlePromiseError(handleGetAllAlbums()));
|
||||
handlePromiseError(handleGetAllAlbums());
|
||||
ocrManager.clear();
|
||||
handlePromiseError(ocrManager.getAssetOcr(currentAssetId));
|
||||
}
|
||||
handlePromiseError(ocrManager.getAssetOcr(asset.id));
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -171,7 +171,6 @@
|
||||
|
||||
$effect(() => {
|
||||
if (assetFileUrl) {
|
||||
// this can't be in an async context with $effect
|
||||
void cast(assetFileUrl);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -43,7 +43,9 @@
|
||||
|
||||
let videoPlayer: HTMLVideoElement | undefined = $state();
|
||||
let isLoading = $state(true);
|
||||
let assetFileUrl = $state('');
|
||||
let assetFileUrl = $derived(
|
||||
playOriginalVideo ? getAssetOriginalUrl({ id: assetId, cacheKey }) : getAssetPlaybackUrl({ id: assetId, cacheKey }),
|
||||
);
|
||||
let isScrubbing = $state(false);
|
||||
let showVideo = $state(false);
|
||||
|
||||
@@ -53,11 +55,9 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
assetFileUrl = playOriginalVideo
|
||||
? getAssetOriginalUrl({ id: assetId, cacheKey })
|
||||
: getAssetPlaybackUrl({ id: assetId, cacheKey });
|
||||
if (videoPlayer) {
|
||||
videoPlayer.load();
|
||||
// reactive on `assetFileUrl` changes
|
||||
if (assetFileUrl) {
|
||||
videoPlayer?.load();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
|
||||
$effect(() => {
|
||||
if (assetFileUrl) {
|
||||
// this can't be in an async context with $effect
|
||||
void cast(assetFileUrl);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
import { type PlacesGroup, getSelectedPlacesGroupOption } from '$lib/utils/places-utils';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
interface Props {
|
||||
places?: AssetResponseDto[];
|
||||
@@ -70,39 +69,27 @@
|
||||
},
|
||||
};
|
||||
|
||||
let filteredPlaces: AssetResponseDto[] = $state([]);
|
||||
let groupedPlaces: PlacesGroup[] = $state([]);
|
||||
|
||||
let placesGroupOption: string = $state(PlacesGroupBy.None);
|
||||
|
||||
let hasPlaces = $derived(places.length > 0);
|
||||
|
||||
// Step 1: Filter using the given search query.
|
||||
run(() => {
|
||||
if (searchQuery) {
|
||||
const filteredPlaces = $derived.by(() => {
|
||||
const searchQueryNormalized = normalizeSearchString(searchQuery);
|
||||
|
||||
filteredPlaces = places.filter((place) => {
|
||||
return normalizeSearchString(place.exifInfo?.city ?? '').includes(searchQueryNormalized);
|
||||
return searchQueryNormalized
|
||||
? places.filter((place) => normalizeSearchString(place.exifInfo?.city ?? '').includes(searchQueryNormalized))
|
||||
: places;
|
||||
});
|
||||
} else {
|
||||
filteredPlaces = places;
|
||||
}
|
||||
|
||||
const placesGroupOption: string = $derived(getSelectedPlacesGroupOption(userSettings));
|
||||
const groupingFunction = $derived(groupOptions[placesGroupOption] ?? groupOptions[PlacesGroupBy.None]);
|
||||
const groupedPlaces: PlacesGroup[] = $derived(groupingFunction(filteredPlaces));
|
||||
|
||||
$effect(() => {
|
||||
searchResultCount = filteredPlaces.length;
|
||||
});
|
||||
|
||||
// Step 2: Group places.
|
||||
run(() => {
|
||||
placesGroupOption = getSelectedPlacesGroupOption(userSettings);
|
||||
const groupFunc = groupOptions[placesGroupOption] ?? groupOptions[PlacesGroupBy.None];
|
||||
groupedPlaces = groupFunc(filteredPlaces);
|
||||
|
||||
$effect(() => {
|
||||
placesGroupIds = groupedPlaces.map(({ id }) => id);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if hasPlaces}
|
||||
{#if places.length > 0}
|
||||
<!-- Album Cards -->
|
||||
{#if placesGroupOption === PlacesGroupBy.None}
|
||||
<PlacesCardGroup places={groupedPlaces[0].places} />
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
let { asset = undefined, point: initialPoint, onClose }: Props = $props();
|
||||
|
||||
let places: PlacesResponseDto[] = $state([]);
|
||||
let suggestedPlaces: PlacesResponseDto[] = $state([]);
|
||||
let suggestedPlaces: PlacesResponseDto[] = $derived(places.slice(0, 5));
|
||||
let searchWord: string = $state('');
|
||||
let latestSearchTimeout: number;
|
||||
let showLoadingSpinner = $state(false);
|
||||
@@ -52,9 +52,6 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (places) {
|
||||
suggestedPlaces = places.slice(0, 5);
|
||||
}
|
||||
if (searchWord === '') {
|
||||
suggestedPlaces = [];
|
||||
}
|
||||
|
||||
@@ -33,37 +33,36 @@
|
||||
children,
|
||||
}: Props = $props();
|
||||
|
||||
let left: number = $state(0);
|
||||
let top: number = $state(0);
|
||||
const swap = (direction: string) => (direction === 'left' ? 'right' : 'left');
|
||||
|
||||
// We need to bind clientHeight since the bounding box may return a height
|
||||
// of zero when starting the 'slide' animation.
|
||||
let height: number = $state(0);
|
||||
|
||||
let isTransitioned = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (menuElement) {
|
||||
let layoutDirection = direction;
|
||||
if (languageManager.rtl) {
|
||||
layoutDirection = direction === 'left' ? 'right' : 'left';
|
||||
const layoutDirection = $derived(languageManager.rtl ? swap(direction) : direction);
|
||||
const position = $derived.by(() => {
|
||||
if (!menuElement) {
|
||||
return { left: 0, top: 0 };
|
||||
}
|
||||
|
||||
const rect = menuElement.getBoundingClientRect();
|
||||
const directionWidth = layoutDirection === 'left' ? rect.width : 0;
|
||||
const menuHeight = Math.min(menuElement.clientHeight, height) || 0;
|
||||
|
||||
left = Math.max(8, Math.min(window.innerWidth - rect.width, x - directionWidth));
|
||||
top = Math.max(8, Math.min(window.innerHeight - menuHeight, y));
|
||||
}
|
||||
const left = Math.max(8, Math.min(window.innerWidth - rect.width, x - directionWidth));
|
||||
const top = Math.max(8, Math.min(window.innerHeight - menuHeight, y));
|
||||
|
||||
return { left, top };
|
||||
});
|
||||
|
||||
// We need to bind clientHeight since the bounding box may return a height
|
||||
// of zero when starting the 'slide' animation.
|
||||
let height: number = $state(0);
|
||||
|
||||
let isTransitioned = $state(false);
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:clientHeight={height}
|
||||
class="fixed min-w-50 w-max max-w-75 overflow-hidden rounded-lg shadow-lg z-1"
|
||||
style:left="{left}px"
|
||||
style:top="{top}px"
|
||||
style:left="{position.left}px"
|
||||
style:top="{position.top}px"
|
||||
transition:slide={{ duration: 250, easing: quintOut }}
|
||||
use:clickOutside={{ onOutclick: onClose }}
|
||||
onintroend={() => {
|
||||
|
||||
@@ -64,10 +64,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
let makeFilter = $derived(filters.make);
|
||||
let modelFilter = $derived(filters.model);
|
||||
let lensModelFilter = $derived(filters.lensModel);
|
||||
const makeFilter = $derived(filters.make);
|
||||
const modelFilter = $derived(filters.model);
|
||||
const lensModelFilter = $derived(filters.lensModel);
|
||||
|
||||
// TODO replace by async $derived, at the latest when it's in stable https://svelte.dev/docs/svelte/await-expressions
|
||||
$effect(() => {
|
||||
handlePromiseError(updateMakes());
|
||||
});
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import Combobox, { asComboboxOptions, asSelectedOption } from '$lib/components/shared-components/combobox.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { getSearchSuggestions, SearchSuggestionType } from '@immich/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
interface Props {
|
||||
@@ -66,15 +65,12 @@
|
||||
}
|
||||
let countryFilter = $derived(filters.country);
|
||||
let stateFilter = $derived(filters.state);
|
||||
run(() => {
|
||||
handlePromiseError(updateCountries());
|
||||
});
|
||||
run(() => {
|
||||
handlePromiseError(updateStates(countryFilter));
|
||||
});
|
||||
run(() => {
|
||||
handlePromiseError(updateCities(countryFilter, stateFilter));
|
||||
});
|
||||
|
||||
// TODO replace by async $derived, at the latest when it's in stable https://svelte.dev/docs/svelte/await-expressions
|
||||
$effect(() => handlePromiseError(updateStates(countryFilter)));
|
||||
$effect(() => handlePromiseError(updateCities(countryFilter, stateFilter)));
|
||||
|
||||
onMount(() => updateCountries());
|
||||
</script>
|
||||
|
||||
<div id="location-selection">
|
||||
|
||||
@@ -19,21 +19,23 @@ export type OcrBoundingBox = {
|
||||
class OcrManager {
|
||||
#data = $state<OcrBoundingBox[]>([]);
|
||||
showOverlay = $state(false);
|
||||
hasOcrData = $state(false);
|
||||
#hasOcrData = $derived(this.#data.length > 0);
|
||||
|
||||
get data() {
|
||||
return this.#data;
|
||||
}
|
||||
|
||||
get hasOcrData() {
|
||||
return this.#hasOcrData;
|
||||
}
|
||||
|
||||
async getAssetOcr(id: string) {
|
||||
this.#data = await getAssetOcr({ id });
|
||||
this.hasOcrData = this.#data.length > 0;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.#data = [];
|
||||
this.showOverlay = false;
|
||||
this.hasOcrData = false;
|
||||
}
|
||||
|
||||
toggleOcrBoundingBox() {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
import { page } from '$app/state';
|
||||
import UploadCover from '$lib/components/shared-components/drag-and-drop-upload-overlay.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import type { Snippet } from 'svelte';
|
||||
@@ -15,13 +13,13 @@
|
||||
|
||||
// $page.data.asset is loaded by route specific +page.ts loaders if that
|
||||
// route contains the assetId path.
|
||||
run(() => {
|
||||
if ($page.data.asset) {
|
||||
setAsset($page.data.asset);
|
||||
$effect.pre(() => {
|
||||
if (page.data.asset) {
|
||||
setAsset(page.data.asset);
|
||||
} else {
|
||||
$showAssetViewer = false;
|
||||
}
|
||||
const asset = $page.url.searchParams.get('at');
|
||||
const asset = page.url.searchParams.get('at');
|
||||
$gridScrollTarget = { at: asset };
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
} from '@immich/sdk';
|
||||
import { Icon, IconButton, LoadingSpinner } from '@immich/ui';
|
||||
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||
import { tick } from 'svelte';
|
||||
import { tick, untrack } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
@@ -71,11 +71,10 @@
|
||||
let terms = $derived(searchQuery ? JSON.parse(searchQuery) : {});
|
||||
|
||||
$effect(() => {
|
||||
// we want this to *only* be reactive on `terms`
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
terms;
|
||||
setTimeout(() => {
|
||||
handlePromiseError(onSearchQueryUpdate());
|
||||
});
|
||||
untrack(() => handlePromiseError(onSearchQueryUpdate()));
|
||||
});
|
||||
|
||||
const onEscape = () => {
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import { modalManager, setTranslations } from '@immich/ui';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { run } from 'svelte/legacy';
|
||||
import '../app.css';
|
||||
|
||||
interface Props {
|
||||
@@ -69,7 +68,8 @@
|
||||
afterNavigate(() => {
|
||||
showNavigationLoadingBar = false;
|
||||
});
|
||||
run(() => {
|
||||
|
||||
$effect.pre(() => {
|
||||
if ($user || page.url.pathname.startsWith(AppRoute.MAINTENANCE)) {
|
||||
openWebsocketConnection();
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import OnboardingBackup from '$lib/components/onboarding-page/onboarding-backup.svelte';
|
||||
import OnboardingCard from '$lib/components/onboarding-page/onboarding-card.svelte';
|
||||
import OnboardingHello from '$lib/components/onboarding-page/onboarding-hello.svelte';
|
||||
@@ -95,7 +95,11 @@
|
||||
},
|
||||
]);
|
||||
|
||||
let index = $state(0);
|
||||
const index = $derived.by(() => {
|
||||
const stepState = page.url.searchParams.get('step');
|
||||
const temporaryIndex = onboardingSteps.findIndex((step) => step.name === stepState);
|
||||
return temporaryIndex === -1 ? 0 : temporaryIndex;
|
||||
});
|
||||
let userRole = $derived(
|
||||
$user.isAdmin && !serverConfigManager.value.isOnboarded ? OnboardingRole.SERVER : OnboardingRole.USER,
|
||||
);
|
||||
@@ -114,12 +118,6 @@
|
||||
);
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
const stepState = $page.url.searchParams.get('step');
|
||||
const temporaryIndex = onboardingSteps.findIndex((step) => step.name === stepState);
|
||||
index = temporaryIndex === -1 ? 0 : temporaryIndex;
|
||||
});
|
||||
|
||||
const previousStepIndex = $derived(
|
||||
onboardingSteps.findLastIndex((step, i) => shouldRunStep(step.role, userRole) && i < index),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user