mirror of
https://github.com/immich-app/immich.git
synced 2026-05-25 09:02:31 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 89e45e8a3c |
@@ -1,8 +1,8 @@
|
||||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:970c99f886b839fc8829289040c1845dadaf2cae46b37acc7710333158ec29b4 AS builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:121d86b6d08752968a7dddbc708849e5f3a839bbff47f32212b46d2a1d842bab AS builder-cpu
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS builder-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS builder-openvino
|
||||
|
||||
FROM builder-cpu AS builder-cuda
|
||||
|
||||
@@ -39,12 +39,12 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --frozen --extra ${DEVICE} --no-dev --no-editable --no-install-project --compile-bytecode --no-progress --active --link-mode copy
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:9c6f90801e6b68e772b7c0ca74260cbf7af9f320acec894e26fccdaccfbe3b47 AS prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:8dca233de9f3d9bb410665f00a4da6dd06f331083137e0e98ccf227236fcc438 AS prod-cpu
|
||||
|
||||
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
|
||||
MACHINE_LEARNING_MODEL_ARENA=false
|
||||
|
||||
FROM python:3.13-slim-trixie@sha256:d168b8d9eb761f4d3fe305ebd04aeb7e7f2de0297cec5fb2f8f6403244621664 AS prod-openvino
|
||||
FROM python:3.13-slim-trixie@sha256:b04b5d7233d2ad9c379e22ea8927cd1378cd15c60d4ef876c065b25ea8fb3bf3 AS prod-openvino
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \
|
||||
|
||||
Generated
+491
-444
File diff suppressed because it is too large
Load Diff
+1
-145
@@ -1,35 +1,17 @@
|
||||
import { defaultProvider, screencastManager, themeManager, ThemePreference, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountMultipleOutline,
|
||||
mdiAccountOutline,
|
||||
mdiArchiveArrowDownOutline,
|
||||
mdiBookshelf,
|
||||
mdiCog,
|
||||
mdiContentDuplicate,
|
||||
mdiCrosshairsGps,
|
||||
mdiFolderOutline,
|
||||
mdiHeartOutline,
|
||||
mdiImageAlbum,
|
||||
mdiImageMultipleOutline,
|
||||
mdiImageSizeSelectLarge,
|
||||
mdiKeyboard,
|
||||
mdiLink,
|
||||
mdiLockOutline,
|
||||
mdiMagnify,
|
||||
mdiMapOutline,
|
||||
mdiServer,
|
||||
mdiStateMachine,
|
||||
mdiSync,
|
||||
mdiTagMultipleOutline,
|
||||
mdiThemeLightDark,
|
||||
mdiToolboxOutline,
|
||||
mdiTrashCanOutline,
|
||||
} from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
|
||||
@@ -67,133 +49,7 @@ export const getPagesProvider = ($t: MessageFormatter) => {
|
||||
},
|
||||
].map((route) => ({ ...route, $if: () => authManager.authenticated && authManager.user.isAdmin }));
|
||||
|
||||
const userPages: ActionItem[] = [
|
||||
{
|
||||
title: $t('photos'),
|
||||
icon: mdiImageMultipleOutline,
|
||||
onAction: () => goto(Route.photos()),
|
||||
},
|
||||
{
|
||||
title: $t('explore'),
|
||||
icon: mdiMagnify,
|
||||
onAction: () => goto(Route.explore()),
|
||||
$if: () => authManager.authenticated && featureFlagsManager.value.search,
|
||||
},
|
||||
|
||||
{
|
||||
title: $t('map'),
|
||||
icon: mdiMapOutline,
|
||||
onAction: () => goto(Route.map()),
|
||||
$if: () => authManager.authenticated && featureFlagsManager.value.map,
|
||||
},
|
||||
{
|
||||
title: $t('people'),
|
||||
description: $t('people_feature_description'),
|
||||
icon: mdiAccountOutline,
|
||||
onAction: () => goto(Route.people()),
|
||||
$if: () => authManager.authenticated && authManager.preferences.people.enabled,
|
||||
},
|
||||
{
|
||||
title: $t('shared_links'),
|
||||
icon: mdiLink,
|
||||
onAction: () => goto(Route.sharedLinks()),
|
||||
$if: () => authManager.authenticated && authManager.preferences.sharedLinks.enabled,
|
||||
},
|
||||
{
|
||||
title: $t('recently_added'),
|
||||
icon: mdiMagnify,
|
||||
onAction: () => goto(Route.recentlyAdded()),
|
||||
$if: () => authManager.authenticated,
|
||||
},
|
||||
{
|
||||
title: $t('sharing'),
|
||||
icon: mdiAccountMultipleOutline,
|
||||
onAction: () => goto(Route.sharing()),
|
||||
$if: () => authManager.authenticated,
|
||||
},
|
||||
{
|
||||
title: $t('favorites'),
|
||||
icon: mdiHeartOutline,
|
||||
onAction: () => goto(Route.favorites()),
|
||||
$if: () => authManager.authenticated,
|
||||
},
|
||||
{
|
||||
title: $t('albums'),
|
||||
description: $t('albums_feature_description'),
|
||||
icon: mdiImageAlbum,
|
||||
onAction: () => goto(Route.albums()),
|
||||
$if: () => authManager.authenticated,
|
||||
},
|
||||
{
|
||||
title: $t('tags'),
|
||||
description: $t('tag_feature_description'),
|
||||
icon: mdiTagMultipleOutline,
|
||||
onAction: () => goto(Route.tags()),
|
||||
$if: () => authManager.authenticated && authManager.preferences.tags.enabled,
|
||||
},
|
||||
{
|
||||
title: $t('folders'),
|
||||
description: $t('folders_feature_description'),
|
||||
icon: mdiFolderOutline,
|
||||
onAction: () => goto(Route.folders()),
|
||||
$if: () => authManager.authenticated && authManager.preferences.folders.enabled,
|
||||
},
|
||||
{
|
||||
title: $t('utilities'),
|
||||
icon: mdiToolboxOutline,
|
||||
onAction: () => goto(Route.utilities()),
|
||||
$if: () => authManager.authenticated,
|
||||
},
|
||||
{
|
||||
title: $t('archive'),
|
||||
icon: mdiArchiveArrowDownOutline,
|
||||
onAction: () => goto(Route.archive()),
|
||||
$if: () => authManager.authenticated,
|
||||
},
|
||||
{
|
||||
title: $t('locked_folder'),
|
||||
icon: mdiLockOutline,
|
||||
onAction: () => goto(Route.locked()),
|
||||
$if: () => authManager.authenticated,
|
||||
},
|
||||
{
|
||||
title: $t('trash'),
|
||||
icon: mdiTrashCanOutline,
|
||||
onAction: () => goto(Route.trash()),
|
||||
$if: () => authManager.authenticated && featureFlagsManager.value.trash,
|
||||
},
|
||||
{
|
||||
title: $t('admin.user_settings'),
|
||||
icon: mdiCog,
|
||||
onAction: () => goto(Route.userSettings()),
|
||||
$if: () => authManager.authenticated,
|
||||
},
|
||||
].map((route) => ({ $if: () => authManager.authenticated, ...route }));
|
||||
|
||||
const utilityPages: ActionItem[] = [
|
||||
{
|
||||
title: $t('review_duplicates'),
|
||||
icon: mdiContentDuplicate,
|
||||
onAction: () => goto(Route.duplicatesUtility()),
|
||||
},
|
||||
{
|
||||
title: $t('review_large_files'),
|
||||
icon: mdiImageSizeSelectLarge,
|
||||
onAction: () => goto(Route.largeFileUtility()),
|
||||
},
|
||||
{
|
||||
title: $t('manage_geolocation'),
|
||||
icon: mdiCrosshairsGps,
|
||||
onAction: () => goto(Route.geolocationUtility()),
|
||||
},
|
||||
{
|
||||
title: $t('workflows'),
|
||||
icon: mdiStateMachine,
|
||||
onAction: () => goto(Route.workflows()),
|
||||
},
|
||||
].map((route) => ({ ...route, $if: () => authManager.authenticated }));
|
||||
|
||||
return defaultProvider({ name: $t('page'), actions: [...userPages, ...utilityPages, ...adminPages] });
|
||||
return defaultProvider({ name: $t('page'), actions: adminPages });
|
||||
};
|
||||
|
||||
const getMyImmichLink = () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { filterIsInOrNearViewport } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||
import type { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
@@ -12,8 +13,6 @@
|
||||
|
||||
type Props = {
|
||||
viewerAssets: ViewerAsset[];
|
||||
firstActiveIndex: number;
|
||||
lastActiveIndex: number;
|
||||
width: number;
|
||||
height: number;
|
||||
manager: VirtualScrollManager;
|
||||
@@ -28,27 +27,15 @@
|
||||
customThumbnailLayout?: Snippet<[asset: TimelineAsset]>;
|
||||
};
|
||||
|
||||
const {
|
||||
viewerAssets,
|
||||
firstActiveIndex,
|
||||
lastActiveIndex,
|
||||
width,
|
||||
height,
|
||||
manager,
|
||||
thumbnail,
|
||||
customThumbnailLayout,
|
||||
}: Props = $props();
|
||||
const { viewerAssets, width, height, manager, thumbnail, customThumbnailLayout }: Props = $props();
|
||||
|
||||
const transitionDuration = $derived(manager.suspendTransitions && !$isUploading ? 0 : 150);
|
||||
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
||||
const visibleViewerAssets = $derived(
|
||||
firstActiveIndex === -1 ? [] : viewerAssets.slice(firstActiveIndex, lastActiveIndex + 1),
|
||||
);
|
||||
</script>
|
||||
|
||||
<!-- Image grid -->
|
||||
<div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}>
|
||||
{#each visibleViewerAssets as viewerAsset (viewerAsset.id)}
|
||||
{#each filterIsInOrNearViewport(viewerAssets) as viewerAsset (viewerAsset.id)}
|
||||
{@const position = viewerAsset.position!}
|
||||
{@const asset = viewerAsset.asset!}
|
||||
|
||||
|
||||
@@ -101,8 +101,6 @@
|
||||
<AssetLayout
|
||||
{manager}
|
||||
viewerAssets={timelineDay.viewerAssets}
|
||||
firstActiveIndex={timelineDay.firstActiveIndex}
|
||||
lastActiveIndex={timelineDay.lastActiveIndex}
|
||||
height={timelineDay.height}
|
||||
width={timelineDay.width}
|
||||
{customThumbnailLayout}
|
||||
|
||||
@@ -53,3 +53,17 @@ export function updateTimelineMonthViewportProximity(timelineManager: TimelineMa
|
||||
timelineManager.clearDeferredLayout(month);
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateViewerAssetViewportProximity(
|
||||
timelineManager: TimelineManager,
|
||||
positionTop: number,
|
||||
positionHeight: number,
|
||||
) {
|
||||
const headerHeight = timelineManager.headerHeight;
|
||||
return calculateViewportProximity(
|
||||
positionTop,
|
||||
positionTop + positionHeight,
|
||||
timelineManager.visibleWindow.top - headerHeight,
|
||||
timelineManager.visibleWindow.bottom + headerHeight,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,31 +1,12 @@
|
||||
import { AssetOrder, AssetOrderBy } from '@immich/sdk';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import type { CommonLayoutOptions, CommonPosition } from '$lib/utils/layout-utils';
|
||||
import type { CommonLayoutOptions } from '$lib/utils/layout-utils';
|
||||
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
||||
import { getOrderingDate, plainDateTimeCompare } from '$lib/utils/timeline-util';
|
||||
import { TUNABLES } from '$lib/utils/tunables';
|
||||
import type { TimelineMonth } from './timeline-month.svelte';
|
||||
import type { Direction, MoveAsset, TimelineAsset } from './types';
|
||||
import { ViewerAsset } from './viewer-asset.svelte';
|
||||
|
||||
const {
|
||||
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
|
||||
} = TUNABLES;
|
||||
|
||||
function lowerBound(assets: ViewerAsset[], target: number, key: (pos: CommonPosition) => number): number {
|
||||
let lo = 0;
|
||||
let hi = assets.length;
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >>> 1;
|
||||
if (key(assets[mid].position!) < target) {
|
||||
lo = mid + 1;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
|
||||
export class TimelineDay {
|
||||
readonly timelineMonth: TimelineMonth;
|
||||
readonly index: number;
|
||||
@@ -36,11 +17,7 @@ export class TimelineDay {
|
||||
|
||||
height = $state(0);
|
||||
width = $state(0);
|
||||
|
||||
// Assets in or near the viewport; active assets should be added to the DOM.
|
||||
firstActiveIndex = $state(-1);
|
||||
lastActiveIndex = $state(-1);
|
||||
isInOrNearViewport = $derived(this.firstActiveIndex !== -1);
|
||||
isInOrNearViewport = $derived.by(() => this.viewerAssets.some((viewAsset) => viewAsset.isInOrNearViewport));
|
||||
|
||||
#top: number = $state(0);
|
||||
#start: number = $state(0);
|
||||
@@ -172,29 +149,6 @@ export class TimelineDay {
|
||||
for (let i = 0; i < this.viewerAssets.length; i++) {
|
||||
this.viewerAssets[i].position = geometry.getPosition(i);
|
||||
}
|
||||
this.updateAssetBoundaries();
|
||||
}
|
||||
|
||||
updateAssetBoundaries() {
|
||||
const manager = this.timelineMonth.timelineManager;
|
||||
const visibleWindow = manager.visibleWindow;
|
||||
if (this.viewerAssets.length === 0 || !this.viewerAssets[0].position) {
|
||||
this.firstActiveIndex = -1;
|
||||
this.lastActiveIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const dayOffset = this.absoluteTimelineDayTop;
|
||||
const headerHeight = manager.headerHeight;
|
||||
const expandedTop = visibleWindow.top - headerHeight - INTERSECTION_EXPAND_TOP - dayOffset;
|
||||
const expandedBottom = visibleWindow.bottom + headerHeight + INTERSECTION_EXPAND_BOTTOM - dayOffset;
|
||||
|
||||
const first = lowerBound(this.viewerAssets, expandedTop, (p) => p.top + p.height);
|
||||
const last = lowerBound(this.viewerAssets, expandedBottom, (p) => p.top) - 1;
|
||||
|
||||
const hasActive = last >= first && first < this.viewerAssets.length;
|
||||
this.firstActiveIndex = hasActive ? first : -1;
|
||||
this.lastActiveIndex = hasActive ? last : -1;
|
||||
}
|
||||
|
||||
get absoluteTimelineDayTop() {
|
||||
|
||||
@@ -214,11 +214,6 @@ export class TimelineManager extends VirtualScrollManager {
|
||||
|
||||
for (const month of this.months) {
|
||||
updateTimelineMonthViewportProximity(this, month);
|
||||
if (month.isInOrNearViewport && month.isLoaded) {
|
||||
for (const day of month.timelineDays) {
|
||||
day.updateAssetBoundaries();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const month = this.months.find((month) => month.isInViewport);
|
||||
|
||||
@@ -254,7 +254,7 @@ export class TimelineMonth {
|
||||
addContext.newTimelineDays.add(timelineDay);
|
||||
}
|
||||
|
||||
const viewerAsset = new ViewerAsset(timelineAsset);
|
||||
const viewerAsset = new ViewerAsset(timelineDay, timelineAsset);
|
||||
timelineDay.viewerAssets.push(viewerAsset);
|
||||
addContext.changedTimelineDays.add(timelineDay);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,36 @@
|
||||
import type { CommonPosition } from '$lib/utils/layout-utils';
|
||||
import {
|
||||
ViewportProximity,
|
||||
calculateViewerAssetViewportProximity,
|
||||
isInOrNearViewport,
|
||||
} from './internal/intersection-support.svelte';
|
||||
import type { TimelineDay } from './timeline-day.svelte';
|
||||
import type { TimelineAsset } from './types';
|
||||
|
||||
export class ViewerAsset {
|
||||
readonly #group: TimelineDay;
|
||||
|
||||
#viewportProximity = $derived.by(() => {
|
||||
if (!this.position) {
|
||||
return ViewportProximity.FarFromViewport;
|
||||
}
|
||||
|
||||
const store = this.#group.timelineMonth.timelineManager;
|
||||
const positionTop = this.#group.absoluteTimelineDayTop + this.position.top;
|
||||
|
||||
return calculateViewerAssetViewportProximity(store, positionTop, this.position.height);
|
||||
});
|
||||
|
||||
get isInOrNearViewport() {
|
||||
return isInOrNearViewport(this.#viewportProximity);
|
||||
}
|
||||
|
||||
position: CommonPosition | undefined = $state.raw();
|
||||
asset: TimelineAsset = $state() as TimelineAsset;
|
||||
id: string = $derived(this.asset.id);
|
||||
|
||||
constructor(asset: TimelineAsset) {
|
||||
constructor(group: TimelineDay, asset: TimelineAsset) {
|
||||
this.#group = group;
|
||||
this.asset = asset;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user