mirror of
https://github.com/immich-app/immich.git
synced 2026-01-21 17:13:17 -08:00
Compare commits
2 Commits
push-zzmpz
...
chore-cont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9aac2a4d6 | ||
|
|
cd286717a2 |
@@ -12,19 +12,20 @@
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { editManager, EditToolType } from '$lib/managers/edit/edit-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { imageManager } from '$lib/managers/ImageManager.svelte';
|
||||
import { preloadManager } from '$lib/managers/PreloadManager.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { ocrManager } from '$lib/stores/ocr.svelte';
|
||||
import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store';
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getSharedLink, handlePromiseError } from '$lib/utils';
|
||||
import { getAssetUrl, getSharedLink, handlePromiseError } from '$lib/utils';
|
||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||
import { navigateToAsset } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { InvocationTracker } from '$lib/utils/invocationTracker';
|
||||
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
||||
import { preloadImageUrl } from '$lib/utils/sw-messaging';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import {
|
||||
AssetTypeEnum,
|
||||
@@ -132,7 +133,9 @@
|
||||
}
|
||||
|
||||
untrack(() => {
|
||||
imageManager.preload(stack?.assets[1]);
|
||||
if (stack && stack?.assets.length > 1) {
|
||||
preloadImageUrl(getAssetUrl({ asset: stack.assets[1] }));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -217,7 +220,7 @@
|
||||
}
|
||||
|
||||
e?.stopPropagation();
|
||||
imageManager.cancel(asset);
|
||||
preloadManager.cancel(asset);
|
||||
if (tracker.isActive()) {
|
||||
return;
|
||||
}
|
||||
@@ -377,8 +380,8 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||
asset;
|
||||
untrack(() => handlePromiseError(refresh()));
|
||||
imageManager.preload(cursor.nextAsset);
|
||||
imageManager.preload(cursor.previousAsset);
|
||||
preloadManager.preload(cursor.nextAsset);
|
||||
preloadManager.preload(cursor.previousAsset);
|
||||
});
|
||||
|
||||
const onAssetReplace = async ({ oldAssetId, newAssetId }: { oldAssetId: string; newAssetId: string }) => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||
import { assetViewerFadeDuration } from '$lib/constants';
|
||||
import { castManager } from '$lib/managers/cast-manager.svelte';
|
||||
import { imageManager } from '$lib/managers/ImageManager.svelte';
|
||||
import { preloadManager } from '$lib/managers/PreloadManager.svelte';
|
||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
import { ocrManager } from '$lib/stores/ocr.svelte';
|
||||
@@ -164,7 +164,7 @@
|
||||
imageError = imageLoaded = true;
|
||||
};
|
||||
|
||||
onDestroy(() => imageManager.cancelPreloadUrl(imageLoaderUrl));
|
||||
onDestroy(() => preloadManager.cancelPreloadUrl(imageLoaderUrl));
|
||||
|
||||
let imageLoaderUrl = $derived(
|
||||
getAssetUrl({ asset, sharedLink, forceOriginal: originalImageLoaded || $photoZoomState.currentZoom > 1 }),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||
import { imageManager } from '$lib/managers/ImageManager.svelte';
|
||||
import { preloadManager } from '$lib/managers/PreloadManager.svelte';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiEyeOffOutline } from '@mdi/js';
|
||||
import type { ActionReturn } from 'svelte/action';
|
||||
@@ -60,7 +60,7 @@
|
||||
onComplete?.(false);
|
||||
}
|
||||
return {
|
||||
destroy: () => imageManager.cancelPreloadUrl(url),
|
||||
destroy: () => preloadManager.cancelPreloadUrl(url),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { getAssetUrlForKind, ImageKinds, type ImageKind } from '$lib/utils';
|
||||
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
|
||||
class ImageManager {
|
||||
preload(asset: AssetResponseDto | undefined, kind: ImageKind = 'preview') {
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = getAssetUrlForKind(asset, kind);
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
cancel(asset: AssetResponseDto | undefined, kind: ImageKind | 'all' = 'preview') {
|
||||
if (!asset) {
|
||||
return;
|
||||
}
|
||||
|
||||
const kinds = kind === 'all' ? (Object.keys(ImageKinds) as ImageKind[]) : [kind];
|
||||
for (const kind of kinds) {
|
||||
const url = getAssetUrlForKind(asset, kind);
|
||||
if (url) {
|
||||
cancelImageUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cancelPreloadUrl(url: string | undefined) {
|
||||
if (url) {
|
||||
cancelImageUrl(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const imageManager = new ImageManager();
|
||||
38
web/src/lib/managers/PreloadManager.svelte.ts
Normal file
38
web/src/lib/managers/PreloadManager.svelte.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { getAssetUrl } from '$lib/utils';
|
||||
import { cancelImageUrl, preloadImageUrl } from '$lib/utils/sw-messaging';
|
||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
||||
|
||||
class PreloadManager {
|
||||
preload(asset: AssetResponseDto | undefined) {
|
||||
if (globalThis.isSecureContext) {
|
||||
preloadImageUrl(getAssetUrl({ asset }));
|
||||
return;
|
||||
}
|
||||
if (!asset || asset.type !== AssetTypeEnum.Image) {
|
||||
return;
|
||||
}
|
||||
const img = new Image();
|
||||
const url = getAssetUrl({ asset });
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
cancel(asset: AssetResponseDto | undefined) {
|
||||
if (!globalThis.isSecureContext || !asset) {
|
||||
return;
|
||||
}
|
||||
const url = getAssetUrl({ asset });
|
||||
cancelImageUrl(url);
|
||||
}
|
||||
|
||||
cancelPreloadUrl(url: string | undefined) {
|
||||
if (!globalThis.isSecureContext) {
|
||||
return;
|
||||
}
|
||||
cancelImageUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
export const preloadManager = new PreloadManager();
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
type UpdateLibraryDto,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiInformationOutline, mdiPencilOutline, mdiPlusBoxOutline, mdiSync, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { mdiPencilOutline, mdiPlusBoxOutline, mdiSync, mdiTrashCanOutline } from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResponseDto[]) => {
|
||||
@@ -45,13 +45,6 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
|
||||
};
|
||||
|
||||
export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponseDto) => {
|
||||
const Detail: ActionItem = {
|
||||
icon: mdiInformationOutline,
|
||||
type: $t('command'),
|
||||
title: $t('details'),
|
||||
onAction: () => goto(Route.viewLibrary(library)),
|
||||
};
|
||||
|
||||
const Edit: ActionItem = {
|
||||
icon: mdiPencilOutline,
|
||||
type: $t('command'),
|
||||
@@ -91,7 +84,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
||||
shortcuts: { shift: true, key: 'r' },
|
||||
};
|
||||
|
||||
return { Detail, Edit, Delete, AddFolder, AddExclusionPattern, Scan };
|
||||
return { Edit, Delete, AddFolder, AddExclusionPattern, Scan };
|
||||
};
|
||||
|
||||
export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryResponseDto, folder: string) => {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiDeleteRestore,
|
||||
mdiInformationOutline,
|
||||
mdiLockReset,
|
||||
mdiLockSmart,
|
||||
mdiPencilOutline,
|
||||
@@ -46,6 +47,12 @@ export const getUserAdminsActions = ($t: MessageFormatter) => {
|
||||
};
|
||||
|
||||
export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminResponseDto) => {
|
||||
const Detail: ActionItem = {
|
||||
icon: mdiInformationOutline,
|
||||
title: $t('details'),
|
||||
onAction: () => goto(Route.viewUser(user)),
|
||||
};
|
||||
|
||||
const Update: ActionItem = {
|
||||
icon: mdiPencilOutline,
|
||||
title: $t('edit'),
|
||||
@@ -92,7 +99,7 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
||||
onAction: () => handleResetPinCodeUserAdmin(user),
|
||||
};
|
||||
|
||||
return { Update, Delete, Restore, ResetPassword, ResetPinCode };
|
||||
return { Detail, Update, Delete, Restore, ResetPassword, ResetPinCode };
|
||||
};
|
||||
|
||||
export const handleCreateUserAdmin = async (dto: UserAdminCreateDto) => {
|
||||
|
||||
@@ -31,15 +31,6 @@ import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, md
|
||||
import { init, register, t, type MessageFormatter } from 'svelte-i18n';
|
||||
import { derived, get } from 'svelte/store';
|
||||
|
||||
export const ImageKinds = {
|
||||
thumbnail: true,
|
||||
preview: true,
|
||||
fullsize: true,
|
||||
original: true,
|
||||
} as const;
|
||||
|
||||
export type ImageKind = keyof typeof ImageKinds;
|
||||
|
||||
interface DownloadRequestOptions<T = unknown> {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
url: string;
|
||||
@@ -204,23 +195,6 @@ const createUrl = (path: string, parameters?: Record<string, unknown>) => {
|
||||
|
||||
type AssetUrlOptions = { id: string; cacheKey?: string | null; edited?: boolean };
|
||||
|
||||
export const getAssetUrlForKind = (asset: AssetResponseDto, kind: ImageKind) => {
|
||||
switch (kind) {
|
||||
case 'preview': {
|
||||
return getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Preview, cacheKey: asset.thumbhash });
|
||||
}
|
||||
case 'thumbnail': {
|
||||
return getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash });
|
||||
}
|
||||
case 'fullsize': {
|
||||
return getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Fullsize, cacheKey: asset.thumbhash });
|
||||
}
|
||||
case 'original': {
|
||||
return getAssetOriginalUrl({ id: asset.id, cacheKey: asset.thumbhash });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getAssetUrl = ({
|
||||
asset,
|
||||
sharedLink,
|
||||
|
||||
@@ -4,16 +4,14 @@
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { getLibrariesActions, getLibraryActions } from '$lib/services/library.service';
|
||||
import { getLibrariesActions } from '$lib/services/library.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getBytesWithUnit } from '$lib/utils/byte-units';
|
||||
import { getLibrary, getLibraryStatistics, type LibraryResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
Button,
|
||||
CommandPaletteDefaultProvider,
|
||||
Container,
|
||||
ContextMenuButton,
|
||||
Link,
|
||||
MenuItemType,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -60,18 +58,13 @@
|
||||
|
||||
const { Create, ScanAll } = $derived(getLibrariesActions($t, libraries));
|
||||
|
||||
const getActionsForLibrary = (library: LibraryResponseDto) => {
|
||||
const { Detail, Scan, Edit, Delete } = getLibraryActions($t, library);
|
||||
return [Detail, Scan, Edit, MenuItemType.Divider, Delete];
|
||||
};
|
||||
|
||||
const classes = {
|
||||
column1: 'w-4/12',
|
||||
column2: 'w-4/12',
|
||||
column3: 'w-1/12',
|
||||
column4: 'w-1/12',
|
||||
column5: 'w-1/12',
|
||||
column6: 'w-1/12 flex justify-end',
|
||||
column3: 'w-2/12',
|
||||
column4: 'w-2/12',
|
||||
column5: 'w-2/12',
|
||||
column6: 'w-2/12',
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -96,19 +89,14 @@
|
||||
{#each libraries as library (library.id + library.name)}
|
||||
{@const { photos, usage, videos } = statistics[library.id]}
|
||||
{@const [diskUsage, diskUsageUnit] = getBytesWithUnit(usage, 0)}
|
||||
{@const owner = owners[library.id]}
|
||||
<TableRow>
|
||||
<TableCell class={classes.column1}>
|
||||
<Link href={Route.viewLibrary(library)}>{library.name}</Link>
|
||||
</TableCell>
|
||||
<TableCell class={classes.column2}>
|
||||
<Link href={Route.viewUser(owner)}>{owner.name}</Link>
|
||||
</TableCell>
|
||||
<TableCell class={classes.column1}>{library.name}</TableCell>
|
||||
<TableCell class={classes.column2}>{owners[library.id].name}</TableCell>
|
||||
<TableCell class={classes.column3}>{photos.toLocaleString($locale)}</TableCell>
|
||||
<TableCell class={classes.column4}>{videos.toLocaleString($locale)}</TableCell>
|
||||
<TableCell class={classes.column5}>{diskUsage} {diskUsageUnit}</TableCell>
|
||||
<TableCell class={classes.column6}>
|
||||
<ContextMenuButton color="primary" aria-label={$t('open')} items={getActionsForLibrary(library)} />
|
||||
<Button size="small" href={Route.viewLibrary(library)}>{$t('view')}</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
<script lang="ts">
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { getUserAdminsActions, handleNavigateUserAdmin } from '$lib/services/user-admin.service';
|
||||
import { Route } from '$lib/route';
|
||||
import { getUserAdminActions, getUserAdminsActions } from '$lib/services/user-admin.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
Button,
|
||||
CommandPaletteDefaultProvider,
|
||||
Container,
|
||||
ContextMenuButton,
|
||||
Icon,
|
||||
Link,
|
||||
MenuItemType,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
@@ -46,11 +49,16 @@
|
||||
|
||||
const { Create } = $derived(getUserAdminsActions($t));
|
||||
|
||||
const getActionsForUser = (user: UserAdminResponseDto) => {
|
||||
const { Detail, Update, Delete, ResetPassword, ResetPinCode } = getUserAdminActions($t, user);
|
||||
return [Detail, Update, ResetPassword, ResetPinCode, MenuItemType.Divider, Delete];
|
||||
};
|
||||
|
||||
const classes = {
|
||||
column1: 'w-8/12 sm:w-5/12 lg:w-6/12 xl:w-4/12 2xl:w-5/12',
|
||||
column2: 'hidden sm:block w-3/12',
|
||||
column3: 'hidden xl:block w-3/12 2xl:w-2/12',
|
||||
column4: 'w-4/12 lg:w-3/12 xl:w-2/12',
|
||||
column1: 'w-8/12 md:w-5/12 lg:w-4/12',
|
||||
column2: 'hidden md:block md:w-5/12 lg:w-4/12',
|
||||
column3: 'hidden lg:block lg:w-2/12',
|
||||
column4: 'w-4/12 md:w-2/12 flex justify-end',
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -68,16 +76,18 @@
|
||||
<Container center size="large">
|
||||
<Table class="mt-4" striped spacing="small" size="small">
|
||||
<TableHeader>
|
||||
<TableHeading class={classes.column1}>{$t('email')}</TableHeading>
|
||||
<TableHeading class={classes.column2}>{$t('name')}</TableHeading>
|
||||
<TableHeading class={classes.column1}>{$t('name')}</TableHeading>
|
||||
<TableHeading class={classes.column2}>{$t('email')}</TableHeading>
|
||||
<TableHeading class={classes.column3}>{$t('has_quota')}</TableHeading>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{#each users as user (user.id)}
|
||||
<TableRow color={user.deletedAt ? 'danger' : undefined}>
|
||||
<TableCell class={classes.column1}>{user.email}</TableCell>
|
||||
<TableCell class={classes.column2}>{user.name}</TableCell>
|
||||
<TableCell class={classes.column1}>
|
||||
<Link href={Route.viewUser(user)}>{user.name}</Link>
|
||||
</TableCell>
|
||||
<TableCell class={classes.column2}>{user.email}</TableCell>
|
||||
<TableCell class={classes.column3}>
|
||||
<div class="container mx-auto flex flex-wrap justify-center">
|
||||
{#if user.quotaSizeInBytes !== null && user.quotaSizeInBytes >= 0}
|
||||
@@ -88,7 +98,7 @@
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell class={classes.column4}>
|
||||
<Button onclick={() => handleNavigateUserAdmin(user)}>{$t('view')}</Button>
|
||||
<ContextMenuButton color="primary" aria-label={$t('open')} items={getActionsForUser(user)} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/each}
|
||||
|
||||
@@ -198,8 +198,8 @@
|
||||
})}
|
||||
>
|
||||
<p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
|
||||
<div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="h-[7px] rounded-full {getUsageClass()}" style="width: {usedPercentage}%"></div>
|
||||
<div class="mt-4 h-1.75 w-full rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="h-1.75 rounded-full {getUsageClass()}" style="width: {usedPercentage}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user