mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
Merge branch 'main' into feat/toggle-video-auto-play
This commit is contained in:
commit
31156b9669
22 changed files with 2763 additions and 2600 deletions
|
|
@ -17,9 +17,9 @@
|
||||||
"write-heading-ids": "docusaurus write-heading-ids"
|
"write-heading-ids": "docusaurus write-heading-ids"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "~3.8.0",
|
"@docusaurus/core": "~3.9.0",
|
||||||
"@docusaurus/preset-classic": "~3.8.0",
|
"@docusaurus/preset-classic": "~3.9.0",
|
||||||
"@docusaurus/theme-common": "~3.8.0",
|
"@docusaurus/theme-common": "~3.9.0",
|
||||||
"@mdi/js": "^7.3.67",
|
"@mdi/js": "^7.3.67",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mdx-js/react": "^3.0.0",
|
"@mdx-js/react": "^3.0.0",
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "~3.8.0",
|
"@docusaurus/module-type-aliases": "~3.9.0",
|
||||||
"@docusaurus/tsconfig": "^3.7.0",
|
"@docusaurus/tsconfig": "^3.7.0",
|
||||||
"@docusaurus/types": "^3.7.0",
|
"@docusaurus/types": "^3.7.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.4",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
[tools]
|
[tools]
|
||||||
node = "22.20.0"
|
node = "22.20.0"
|
||||||
flutter = "3.35.5"
|
flutter = "3.35.5"
|
||||||
pnpm = "10.15.1"
|
pnpm = "10.18.0"
|
||||||
|
|
||||||
[tools."github:CQLabs/homebrew-dcm"]
|
[tools."github:CQLabs/homebrew-dcm"]
|
||||||
version = "1.30.0"
|
version = "1.30.0"
|
||||||
|
|
|
||||||
|
|
@ -221,10 +221,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
context.scaffoldMessenger.hideCurrentSnackBar();
|
context.scaffoldMessenger.hideCurrentSnackBar();
|
||||||
|
|
||||||
// send image to casting if the server has it
|
// send image to casting if the server has it
|
||||||
if (asset.hasRemote) {
|
if (asset is RemoteAsset) {
|
||||||
final remoteAsset = asset as RemoteAsset;
|
ref.read(castProvider.notifier).loadMedia(asset, false);
|
||||||
|
|
||||||
ref.read(castProvider.notifier).loadMedia(remoteAsset, false);
|
|
||||||
} else {
|
} else {
|
||||||
// casting cannot show local assets
|
// casting cannot show local assets
|
||||||
context.scaffoldMessenger.clearSnackBars();
|
context.scaffoldMessenger.clearSnackBars();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"description": "Monorepo for Immich",
|
"description": "Monorepo for Immich",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67",
|
"packageManager": "pnpm@10.18.0+sha512.e804f889f1cecc40d572db084eec3e4881739f8dec69c0ff10d2d1beff9a4e309383ba27b5b750059d7f4c149535b6cd0d2cb1ed3aeb739239a4284a68f40cfa",
|
||||||
"engines": {
|
"engines": {
|
||||||
"pnpm": ">=10.0.0"
|
"pnpm": ">=10.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5155
pnpm-lock.yaml
generated
5155
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -27,7 +27,7 @@ onlyBuiltDependencies:
|
||||||
- '@tailwindcss/oxide'
|
- '@tailwindcss/oxide'
|
||||||
overrides:
|
overrides:
|
||||||
canvas: 2.11.2
|
canvas: 2.11.2
|
||||||
sharp: ^0.34.3
|
sharp: ^0.34.4
|
||||||
packageExtensions:
|
packageExtensions:
|
||||||
nestjs-kysely:
|
nestjs-kysely:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
FROM ghcr.io/immich-app/base-server-dev:202509210934@sha256:b5ce2d7eaf379d4cf15efd4bab180d8afc8a80d20b36c9800f4091aca6ae267e AS builder
|
FROM ghcr.io/immich-app/base-server-dev:202510092146@sha256:124ec9659cba4a206924de5e3691f84acde16d75fa2b10b7007542424b696b96 AS builder
|
||||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||||
CI=1 \
|
CI=1 \
|
||||||
COREPACK_HOME=/tmp \
|
COREPACK_HOME=/tmp \
|
||||||
|
|
@ -48,7 +48,7 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
|
||||||
pnpm --filter @immich/sdk --filter @immich/cli build && \
|
pnpm --filter @immich/sdk --filter @immich/cli build && \
|
||||||
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
pnpm --filter @immich/cli --prod --no-optional deploy /output/cli-pruned
|
||||||
|
|
||||||
FROM ghcr.io/immich-app/base-server-prod:202509210934@sha256:0c7eacf0ba88ca52e1a267cfc62d20d07792ea2c604818c2cbd37dc7dcefdac9
|
FROM ghcr.io/immich-app/base-server-prod:202510092146@sha256:c39b9ad949e7777bce415e6931334aeff7331e04cb7f9df93f9ae44f6ff36b9e
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# dev build
|
# dev build
|
||||||
FROM ghcr.io/immich-app/base-server-dev:202509210934@sha256:b5ce2d7eaf379d4cf15efd4bab180d8afc8a80d20b36c9800f4091aca6ae267e AS dev
|
FROM ghcr.io/immich-app/base-server-dev:202510092146@sha256:124ec9659cba4a206924de5e3691f84acde16d75fa2b10b7007542424b696b96 AS dev
|
||||||
|
|
||||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||||
CI=1 \
|
CI=1 \
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@
|
||||||
"@opentelemetry/exporter-prometheus": "^0.205.0",
|
"@opentelemetry/exporter-prometheus": "^0.205.0",
|
||||||
"@opentelemetry/instrumentation-http": "^0.205.0",
|
"@opentelemetry/instrumentation-http": "^0.205.0",
|
||||||
"@opentelemetry/instrumentation-ioredis": "^0.53.0",
|
"@opentelemetry/instrumentation-ioredis": "^0.53.0",
|
||||||
"@opentelemetry/instrumentation-nestjs-core": "^0.51.0",
|
"@opentelemetry/instrumentation-nestjs-core": "^0.52.0",
|
||||||
"@opentelemetry/instrumentation-pg": "^0.58.0",
|
"@opentelemetry/instrumentation-pg": "^0.58.0",
|
||||||
"@opentelemetry/resources": "^2.0.1",
|
"@opentelemetry/resources": "^2.0.1",
|
||||||
"@opentelemetry/sdk-metrics": "^2.0.1",
|
"@opentelemetry/sdk-metrics": "^2.0.1",
|
||||||
|
|
@ -67,7 +67,7 @@
|
||||||
"compression": "^1.8.0",
|
"compression": "^1.8.0",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.7",
|
||||||
"cron": "4.3.0",
|
"cron": "4.3.3",
|
||||||
"exiftool-vendored": "^28.8.0",
|
"exiftool-vendored": "^28.8.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
|
|
@ -85,7 +85,7 @@
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"nest-commander": "^3.16.0",
|
"nest-commander": "^3.16.0",
|
||||||
"nestjs-cls": "^5.0.0",
|
"nestjs-cls": "^5.0.0",
|
||||||
"nestjs-kysely": "^3.0.0",
|
"nestjs-kysely": "3.0.0",
|
||||||
"nestjs-otel": "^7.0.0",
|
"nestjs-otel": "^7.0.0",
|
||||||
"nodemailer": "^7.0.0",
|
"nodemailer": "^7.0.0",
|
||||||
"openid-client": "^6.3.3",
|
"openid-client": "^6.3.3",
|
||||||
|
|
@ -101,7 +101,7 @@
|
||||||
"sanitize-filename": "^1.6.3",
|
"sanitize-filename": "^1.6.3",
|
||||||
"sanitize-html": "^2.14.0",
|
"sanitize-html": "^2.14.0",
|
||||||
"semver": "^7.6.2",
|
"semver": "^7.6.2",
|
||||||
"sharp": "^0.34.3",
|
"sharp": "^0.34.4",
|
||||||
"sirv": "^3.0.0",
|
"sirv": "^3.0.0",
|
||||||
"socket.io": "^4.8.1",
|
"socket.io": "^4.8.1",
|
||||||
"tailwindcss-preset-email": "^1.4.0",
|
"tailwindcss-preset-email": "^1.4.0",
|
||||||
|
|
@ -164,6 +164,6 @@
|
||||||
"node": "22.20.0"
|
"node": "22.20.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"sharp": "^0.34.3"
|
"sharp": "^0.34.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@immich/ui": "^0.29.0",
|
"@immich/ui": "^0.34.0",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.11.5",
|
"@photo-sphere-viewer/core": "^5.11.5",
|
||||||
|
|
@ -44,7 +44,7 @@
|
||||||
"geo-coordinates-parser": "^1.7.4",
|
"geo-coordinates-parser": "^1.7.4",
|
||||||
"geojson": "^0.5.0",
|
"geojson": "^0.5.0",
|
||||||
"handlebars": "^4.7.8",
|
"handlebars": "^4.7.8",
|
||||||
"happy-dom": "^18.0.1",
|
"happy-dom": "^20.0.0",
|
||||||
"intl-messageformat": "^10.7.11",
|
"intl-messageformat": "^10.7.11",
|
||||||
"justified-layout": "^4.1.0",
|
"justified-layout": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
|
@ -69,7 +69,7 @@
|
||||||
"@sveltejs/adapter-static": "^3.0.8",
|
"@sveltejs/adapter-static": "^3.0.8",
|
||||||
"@sveltejs/enhanced-img": "^0.8.0",
|
"@sveltejs/enhanced-img": "^0.8.0",
|
||||||
"@sveltejs/kit": "^2.27.1",
|
"@sveltejs/kit": "^2.27.1",
|
||||||
"@sveltejs/vite-plugin-svelte": "6.2.0",
|
"@sveltejs/vite-plugin-svelte": "6.2.1",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"@testing-library/jest-dom": "^6.4.2",
|
"@testing-library/jest-dom": "^6.4.2",
|
||||||
"@testing-library/svelte": "^5.2.8",
|
"@testing-library/svelte": "^5.2.8",
|
||||||
|
|
@ -89,13 +89,13 @@
|
||||||
"eslint-plugin-unicorn": "^61.0.2",
|
"eslint-plugin-unicorn": "^61.0.2",
|
||||||
"factory.ts": "^1.4.1",
|
"factory.ts": "^1.4.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"happy-dom": "^18.0.1",
|
"happy-dom": "^20.0.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"prettier-plugin-sort-json": "^4.1.1",
|
"prettier-plugin-sort-json": "^4.1.1",
|
||||||
"prettier-plugin-svelte": "^3.3.3",
|
"prettier-plugin-svelte": "^3.3.3",
|
||||||
"rollup-plugin-visualizer": "^6.0.0",
|
"rollup-plugin-visualizer": "^6.0.0",
|
||||||
"svelte": "5.38.10",
|
"svelte": "5.39.11",
|
||||||
"svelte-check": "^4.1.5",
|
"svelte-check": "^4.1.5",
|
||||||
"svelte-eslint-parser": "^1.3.3",
|
"svelte-eslint-parser": "^1.3.3",
|
||||||
"tailwindcss": "^4.1.7",
|
"tailwindcss": "^4.1.7",
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
<Label size="small" color="primary" for={id}>{title}</Label>
|
<Label size="small" color="primary" for={id}>{title}</Label>
|
||||||
<Text size="small" color="muted" {id}>
|
<Text size="small" color="muted" {id}>
|
||||||
{#if versionHref}
|
{#if versionHref}
|
||||||
<Link external href={versionHref}>{version}</Link>
|
<Link href={versionHref}>{version}</Link>
|
||||||
{:else}
|
{:else}
|
||||||
{version}
|
{version}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@
|
||||||
import {
|
import {
|
||||||
AssetJobName,
|
AssetJobName,
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
getAssetInfo,
|
|
||||||
getAllAlbums,
|
getAllAlbums,
|
||||||
|
getAssetInfo,
|
||||||
getStack,
|
getStack,
|
||||||
runAssetJobs,
|
runAssetJobs,
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
|
|
@ -303,10 +303,8 @@
|
||||||
|
|
||||||
const handleStopSlideshow = async () => {
|
const handleStopSlideshow = async () => {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line tscompat/tscompat
|
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
document.body.style.cursor = '';
|
document.body.style.cursor = '';
|
||||||
// eslint-disable-next-line tscompat/tscompat
|
|
||||||
await document.exitFullscreen();
|
await document.exitFullscreen();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -100,9 +100,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
const onShowSettings = async () => {
|
const onShowSettings = async () => {
|
||||||
// eslint-disable-next-line tscompat/tscompat
|
|
||||||
if (document.fullscreenElement) {
|
if (document.fullscreenElement) {
|
||||||
// eslint-disable-next-line tscompat/tscompat
|
|
||||||
await document.exitFullscreen();
|
await document.exitFullscreen();
|
||||||
}
|
}
|
||||||
await modalManager.show(SlideshowSettingsModal);
|
await modalManager.show(SlideshowSettingsModal);
|
||||||
|
|
@ -114,11 +112,7 @@
|
||||||
webkitIsFullScreen?: boolean;
|
webkitIsFullScreen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (!document.fullscreenElement && !doc.webkitIsFullScreen) {
|
||||||
// eslint-disable-next-line tscompat/tscompat
|
|
||||||
!document.fullscreenElement &&
|
|
||||||
!doc.webkitIsFullScreen
|
|
||||||
) {
|
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -341,7 +341,6 @@
|
||||||
scrubberMonthScrollPercent: monthGroupPercentY,
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
/* eslint-disable tscompat/tscompat */
|
|
||||||
const getTouch = (event: TouchEvent) => {
|
const getTouch = (event: TouchEvent) => {
|
||||||
if (event.touches.length === 1) {
|
if (event.touches.length === 1) {
|
||||||
return event.touches[0];
|
return event.touches[0];
|
||||||
|
|
@ -386,7 +385,6 @@
|
||||||
isHover = false;
|
isHover = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
/* eslint-enable tscompat/tscompat */
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
document.addEventListener('touchmove', onTouchMove, { capture: true, passive: true });
|
document.addEventListener('touchmove', onTouchMove, { capture: true, passive: true });
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
||||||
|
|
@ -134,26 +134,12 @@
|
||||||
element.scrollTop = top;
|
element.scrollTop = top;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const scrollBy = (y: number) => {
|
|
||||||
if (element) {
|
|
||||||
element.scrollBy(0, y);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
scrollTo(0);
|
scrollTo(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAssetHeight = (assetId: string, monthGroup: MonthGroup) => {
|
const getAssetHeight = (assetId: string, monthGroup: MonthGroup) => monthGroup.findAssetAbsolutePosition(assetId);
|
||||||
// the following method may trigger any layouts, so need to
|
|
||||||
// handle any scroll compensation that may have been set
|
|
||||||
const height = monthGroup!.findAssetAbsolutePosition(assetId);
|
|
||||||
|
|
||||||
while (timelineManager.scrollCompensation.monthGroup) {
|
|
||||||
handleScrollCompensation(timelineManager.scrollCompensation);
|
|
||||||
timelineManager.clearScrollCompensation();
|
|
||||||
}
|
|
||||||
return height;
|
|
||||||
};
|
|
||||||
|
|
||||||
const assetIsVisible = (assetTop: number): boolean => {
|
const assetIsVisible = (assetTop: number): boolean => {
|
||||||
if (!element) {
|
if (!element) {
|
||||||
|
|
@ -246,19 +232,6 @@
|
||||||
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker
|
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker
|
||||||
const updateSlidingWindow = () => timelineManager.updateSlidingWindow(element?.scrollTop || 0);
|
const updateSlidingWindow = () => timelineManager.updateSlidingWindow(element?.scrollTop || 0);
|
||||||
|
|
||||||
const handleScrollCompensation = ({ heightDelta, scrollTop }: { heightDelta?: number; scrollTop?: number }) => {
|
|
||||||
if (heightDelta !== undefined) {
|
|
||||||
scrollBy(heightDelta);
|
|
||||||
} else if (scrollTop !== undefined) {
|
|
||||||
scrollTo(scrollTop);
|
|
||||||
}
|
|
||||||
// Yes, updateSlideWindow() is called by the onScroll event triggered as a result of
|
|
||||||
// the above calls. However, this delay is enough time to set the intersecting property
|
|
||||||
// of the monthGroup to false, then true, which causes the DOM nodes to be recreated,
|
|
||||||
// causing bad perf, and also, disrupting focus of those elements.
|
|
||||||
updateSlidingWindow();
|
|
||||||
};
|
|
||||||
|
|
||||||
const topSectionResizeObserver: OnResizeCallback = ({ height }) => (timelineManager.topSectionHeight = height);
|
const topSectionResizeObserver: OnResizeCallback = ({ height }) => (timelineManager.topSectionHeight = height);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
@ -666,7 +639,6 @@
|
||||||
onSelect={({ title, assets }) => handleGroupSelect(timelineManager, title, assets)}
|
onSelect={({ title, assets }) => handleGroupSelect(timelineManager, title, assets)}
|
||||||
onSelectAssetCandidates={handleSelectAssetCandidates}
|
onSelectAssetCandidates={handleSelectAssetCandidates}
|
||||||
onSelectAssets={handleSelectAssets}
|
onSelectAssets={handleSelectAssets}
|
||||||
onScrollCompensation={handleScrollCompensation}
|
|
||||||
{customLayout}
|
{customLayout}
|
||||||
{onThumbnailClick}
|
{onThumbnailClick}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@
|
||||||
onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void;
|
onSelect: ({ title, assets }: { title: string; assets: TimelineAsset[] }) => void;
|
||||||
onSelectAssets: (asset: TimelineAsset) => void;
|
onSelectAssets: (asset: TimelineAsset) => void;
|
||||||
onSelectAssetCandidates: (asset: TimelineAsset | null) => void;
|
onSelectAssetCandidates: (asset: TimelineAsset | null) => void;
|
||||||
onScrollCompensation: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
|
|
||||||
onThumbnailClick?: (
|
onThumbnailClick?: (
|
||||||
asset: TimelineAsset,
|
asset: TimelineAsset,
|
||||||
timelineManager: TimelineManager,
|
timelineManager: TimelineManager,
|
||||||
|
|
@ -59,7 +58,7 @@
|
||||||
onSelect,
|
onSelect,
|
||||||
onSelectAssets,
|
onSelectAssets,
|
||||||
onSelectAssetCandidates,
|
onSelectAssetCandidates,
|
||||||
onScrollCompensation,
|
|
||||||
onThumbnailClick,
|
onThumbnailClick,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
|
@ -134,13 +133,6 @@
|
||||||
});
|
});
|
||||||
return getDateLocaleString(date);
|
return getDateLocaleString(date);
|
||||||
};
|
};
|
||||||
|
|
||||||
$effect.root(() => {
|
|
||||||
if (timelineManager.scrollCompensation.monthGroup === monthGroup) {
|
|
||||||
onScrollCompensation(timelineManager.scrollCompensation);
|
|
||||||
timelineManager.clearScrollCompensation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
|
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ export function calculateMonthGroupIntersecting(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate intersection for viewer assets with additional parameters like header height and scroll compensation
|
* Calculate intersection for viewer assets with additional parameters like header height
|
||||||
*/
|
*/
|
||||||
export function calculateViewerAssetIntersecting(
|
export function calculateViewerAssetIntersecting(
|
||||||
timelineManager: TimelineManager,
|
timelineManager: TimelineManager,
|
||||||
|
|
@ -64,12 +64,8 @@ export function calculateViewerAssetIntersecting(
|
||||||
expandTop: number = INTERSECTION_EXPAND_TOP,
|
expandTop: number = INTERSECTION_EXPAND_TOP,
|
||||||
expandBottom: number = INTERSECTION_EXPAND_BOTTOM,
|
expandBottom: number = INTERSECTION_EXPAND_BOTTOM,
|
||||||
) {
|
) {
|
||||||
const scrollCompensationHeightDelta = timelineManager.scrollCompensation?.heightDelta ?? 0;
|
const topWindow = timelineManager.visibleWindow.top - timelineManager.headerHeight - expandTop;
|
||||||
|
const bottomWindow = timelineManager.visibleWindow.bottom + timelineManager.headerHeight + expandBottom;
|
||||||
const topWindow =
|
|
||||||
timelineManager.visibleWindow.top - timelineManager.headerHeight - expandTop + scrollCompensationHeightDelta;
|
|
||||||
const bottomWindow =
|
|
||||||
timelineManager.visibleWindow.bottom + timelineManager.headerHeight + expandBottom + scrollCompensationHeightDelta;
|
|
||||||
|
|
||||||
const positionBottom = positionTop + positionHeight;
|
const positionBottom = positionTop + positionHeight;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,6 @@ export class MonthGroup {
|
||||||
|
|
||||||
#initialCount: number = 0;
|
#initialCount: number = 0;
|
||||||
#sortOrder: AssetOrder = AssetOrder.Desc;
|
#sortOrder: AssetOrder = AssetOrder.Desc;
|
||||||
percent: number = $state(0);
|
|
||||||
|
|
||||||
assetsCount: number = $derived(
|
assetsCount: number = $derived(
|
||||||
this.isLoaded
|
this.isLoaded
|
||||||
|
|
@ -242,42 +241,31 @@ export class MonthGroup {
|
||||||
if (this.#height === height) {
|
if (this.#height === height) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { timelineManager: store, percent } = this;
|
let needsIntersectionUpdate = false;
|
||||||
const index = store.months.indexOf(this);
|
const timelineManager = this.timelineManager;
|
||||||
|
const index = timelineManager.months.indexOf(this);
|
||||||
const heightDelta = height - this.#height;
|
const heightDelta = height - this.#height;
|
||||||
this.#height = height;
|
this.#height = height;
|
||||||
const prevMonthGroup = store.months[index - 1];
|
const prevMonthGroup = timelineManager.months[index - 1];
|
||||||
if (prevMonthGroup) {
|
if (prevMonthGroup) {
|
||||||
const newTop = prevMonthGroup.#top + prevMonthGroup.#height;
|
const newTop = prevMonthGroup.#top + prevMonthGroup.#height;
|
||||||
if (this.#top !== newTop) {
|
if (this.#top !== newTop) {
|
||||||
this.#top = newTop;
|
this.#top = newTop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let cursor = index + 1; cursor < store.months.length; cursor++) {
|
if (heightDelta === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let cursor = index + 1; cursor < timelineManager.months.length; cursor++) {
|
||||||
const monthGroup = this.timelineManager.months[cursor];
|
const monthGroup = this.timelineManager.months[cursor];
|
||||||
const newTop = monthGroup.#top + heightDelta;
|
const newTop = monthGroup.#top + heightDelta;
|
||||||
if (monthGroup.#top !== newTop) {
|
if (monthGroup.#top !== newTop) {
|
||||||
monthGroup.#top = newTop;
|
monthGroup.#top = newTop;
|
||||||
|
needsIntersectionUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (store.topIntersectingMonthGroup) {
|
if (needsIntersectionUpdate) {
|
||||||
const currentIndex = store.months.indexOf(store.topIntersectingMonthGroup);
|
timelineManager.updateIntersections();
|
||||||
if (currentIndex > 0) {
|
|
||||||
if (index < currentIndex) {
|
|
||||||
store.scrollCompensation = {
|
|
||||||
heightDelta,
|
|
||||||
scrollTop: undefined,
|
|
||||||
monthGroup: this,
|
|
||||||
};
|
|
||||||
} else if (percent > 0) {
|
|
||||||
const top = this.top + height * percent;
|
|
||||||
store.scrollCompensation = {
|
|
||||||
heightDelta: undefined,
|
|
||||||
scrollTop: top,
|
|
||||||
monthGroup: this,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,7 +68,7 @@ describe('TimelineManager', () => {
|
||||||
|
|
||||||
it('should load months in viewport', () => {
|
it('should load months in viewport', () => {
|
||||||
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
|
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
|
||||||
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
|
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates month height', () => {
|
it('calculates month height', () => {
|
||||||
|
|
@ -82,13 +82,13 @@ describe('TimelineManager', () => {
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ year: 2024, month: 3, height: 165.5 }),
|
expect.objectContaining({ year: 2024, month: 3, height: 165.5 }),
|
||||||
expect.objectContaining({ year: 2024, month: 2, height: 11_996 }),
|
expect.objectContaining({ year: 2024, month: 2, height: 11_996 }),
|
||||||
expect.objectContaining({ year: 2024, month: 1, height: 286 }),
|
expect.objectContaining({ year: 2024, month: 1, height: 48 }),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates timeline height', () => {
|
it('calculates timeline height', () => {
|
||||||
expect(timelineManager.timelineHeight).toBe(12_447.5);
|
expect(timelineManager.timelineHeight).toBe(12_209.5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { CancellableTask } from '$lib/utils/cancellable-task';
|
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||||
import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
|
|
||||||
import { clamp, debounce, isEqual } from 'lodash-es';
|
import { debounce, isEqual } from 'lodash-es';
|
||||||
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
|
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
|
||||||
|
|
||||||
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
|
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
|
||||||
|
|
@ -49,8 +49,6 @@ export class TimelineManager {
|
||||||
scrubberMonths: ScrubberMonth[] = $state([]);
|
scrubberMonths: ScrubberMonth[] = $state([]);
|
||||||
scrubberTimelineHeight: number = $state(0);
|
scrubberTimelineHeight: number = $state(0);
|
||||||
|
|
||||||
topIntersectingMonthGroup: MonthGroup | undefined = $state();
|
|
||||||
|
|
||||||
visibleWindow = $derived.by(() => ({
|
visibleWindow = $derived.by(() => ({
|
||||||
top: this.#scrollTop,
|
top: this.#scrollTop,
|
||||||
bottom: this.#scrollTop + this.viewportHeight,
|
bottom: this.#scrollTop + this.viewportHeight,
|
||||||
|
|
@ -87,15 +85,6 @@ export class TimelineManager {
|
||||||
#suspendTransitions = $state(false);
|
#suspendTransitions = $state(false);
|
||||||
#resetScrolling = debounce(() => (this.#scrolling = false), 1000);
|
#resetScrolling = debounce(() => (this.#scrolling = false), 1000);
|
||||||
#resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
|
#resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
|
||||||
scrollCompensation: {
|
|
||||||
heightDelta: number | undefined;
|
|
||||||
scrollTop: number | undefined;
|
|
||||||
monthGroup: MonthGroup | undefined;
|
|
||||||
} = $state({
|
|
||||||
heightDelta: 0,
|
|
||||||
scrollTop: 0,
|
|
||||||
monthGroup: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor() {}
|
constructor() {}
|
||||||
|
|
||||||
|
|
@ -241,38 +230,12 @@ export class TimelineManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearScrollCompensation() {
|
|
||||||
this.scrollCompensation = {
|
|
||||||
heightDelta: undefined,
|
|
||||||
scrollTop: undefined,
|
|
||||||
monthGroup: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIntersections() {
|
updateIntersections() {
|
||||||
if (!this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
|
if (!this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let topIntersectingMonthGroup = undefined;
|
|
||||||
for (const month of this.months) {
|
for (const month of this.months) {
|
||||||
updateIntersectionMonthGroup(this, month);
|
updateIntersectionMonthGroup(this, month);
|
||||||
if (!topIntersectingMonthGroup && month.actuallyIntersecting) {
|
|
||||||
topIntersectingMonthGroup = month;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (topIntersectingMonthGroup !== undefined && this.topIntersectingMonthGroup !== topIntersectingMonthGroup) {
|
|
||||||
this.topIntersectingMonthGroup = topIntersectingMonthGroup;
|
|
||||||
}
|
|
||||||
for (const month of this.months) {
|
|
||||||
if (month === this.topIntersectingMonthGroup) {
|
|
||||||
this.topIntersectingMonthGroup.percent = clamp(
|
|
||||||
(this.visibleWindow.top - this.topIntersectingMonthGroup.top) / this.topIntersectingMonthGroup.height,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
month.percent = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -401,10 +364,10 @@ export class TimelineManager {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await monthGroup.loader?.execute(async (signal: AbortSignal) => {
|
const executionStatus = await monthGroup.loader?.execute(async (signal: AbortSignal) => {
|
||||||
await loadFromTimeBuckets(this, monthGroup, this.#options, signal);
|
await loadFromTimeBuckets(this, monthGroup, this.#options, signal);
|
||||||
}, cancelable);
|
}, cancelable);
|
||||||
if (result === 'LOADED') {
|
if (executionStatus === 'LOADED') {
|
||||||
updateIntersectionMonthGroup(this, monthGroup);
|
updateIntersectionMonthGroup(this, monthGroup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { init } from 'svelte-i18n';
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await init({ fallbackLocale: 'dev' });
|
await init({ fallbackLocale: 'dev' });
|
||||||
|
Element.prototype.animate = vi.fn().mockImplementation(() => ({ cancel: () => {}, finished: Promise.resolve() }));
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.defineProperty(globalThis, 'matchMedia', {
|
Object.defineProperty(globalThis, 'matchMedia', {
|
||||||
|
|
@ -16,3 +17,11 @@ Object.defineProperty(globalThis, 'matchMedia', {
|
||||||
dispatchEvent: vi.fn(),
|
dispatchEvent: vi.fn(),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock('$env/dynamic/public', () => {
|
||||||
|
return {
|
||||||
|
env: {
|
||||||
|
PUBLIC_IMMICH_HOSTNAME: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue