mirror of
https://github.com/immich-app/immich.git
synced 2026-01-22 01:18:54 -08:00
Compare commits
4 Commits
feat/integ
...
fix/incorr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a797cd6cf9 | ||
|
|
1b032339aa | ||
|
|
dc82c13ddc | ||
|
|
417af66f30 |
@@ -118,7 +118,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
bool dragInProgress = false;
|
||||
bool shouldPopOnDrag = false;
|
||||
bool assetReloadRequested = false;
|
||||
double? initialScale;
|
||||
double previousExtent = _kBottomSheetMinimumExtent;
|
||||
Offset dragDownPosition = Offset.zero;
|
||||
int totalAssets = 0;
|
||||
@@ -264,7 +263,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
(context.height * bottomSheetController.size) - (context.height * _kBottomSheetMinimumExtent);
|
||||
controller.position = Offset(0, -verticalOffset);
|
||||
// Apply the zoom effect when the bottom sheet is showing
|
||||
initialScale = controller.scale;
|
||||
controller.scale = (controller.scale ?? 1.0) + 0.01;
|
||||
}
|
||||
}
|
||||
@@ -316,7 +314,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
hasDraggedDown = null;
|
||||
viewController?.animateMultiple(
|
||||
position: initialPhotoViewState.position,
|
||||
scale: initialPhotoViewState.scale,
|
||||
scale: viewController?.initialScale ?? initialPhotoViewState.scale,
|
||||
rotation: initialPhotoViewState.rotation,
|
||||
);
|
||||
ref.read(assetViewerProvider.notifier).setOpacity(255);
|
||||
@@ -366,8 +364,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
final maxScaleDistance = ctx.height * 0.5;
|
||||
final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio);
|
||||
double? updatedScale;
|
||||
if (initialPhotoViewState.scale != null) {
|
||||
updatedScale = initialPhotoViewState.scale! * (1.0 - scaleReduction);
|
||||
double? initialScale = viewController?.initialScale ?? initialPhotoViewState.scale;
|
||||
if (initialScale != null) {
|
||||
updatedScale = initialScale * (1.0 - scaleReduction);
|
||||
}
|
||||
|
||||
final backgroundOpacity = (255 * (1.0 - (scaleReduction / dragRatio))).round();
|
||||
@@ -481,8 +480,6 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
|
||||
void _openBottomSheet(BuildContext ctx, {double extent = _kBottomSheetMinimumExtent, bool activitiesMode = false}) {
|
||||
ref.read(assetViewerProvider.notifier).setBottomSheet(true);
|
||||
initialScale = viewController?.scale;
|
||||
// viewController?.updateMultiple(scale: (viewController?.scale ?? 1.0) + 0.01);
|
||||
previousExtent = _kBottomSheetMinimumExtent;
|
||||
sheetCloseController = showBottomSheet(
|
||||
context: ctx,
|
||||
@@ -504,7 +501,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
|
||||
void _handleSheetClose() {
|
||||
viewController?.animateMultiple(position: Offset.zero);
|
||||
viewController?.updateMultiple(scale: initialScale);
|
||||
viewController?.updateMultiple(scale: viewController?.initialScale);
|
||||
ref.read(assetViewerProvider.notifier).setBottomSheet(false);
|
||||
sheetCloseController = null;
|
||||
shouldPopOnDrag = false;
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/src/utils/ignorable_change_notifier.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_utils.dart';
|
||||
|
||||
/// The interface in which controllers will be implemented.
|
||||
///
|
||||
@@ -62,6 +63,9 @@ abstract class PhotoViewControllerBase<T extends PhotoViewControllerValue> {
|
||||
/// The scale factor to transform the child (image or a customChild).
|
||||
late double? scale;
|
||||
|
||||
double? get initialScale;
|
||||
ScaleBoundaries? scaleBoundaries;
|
||||
|
||||
/// Nevermind this method :D, look away
|
||||
void setScaleInvisibly(double? scale);
|
||||
|
||||
@@ -141,6 +145,9 @@ class PhotoViewController implements PhotoViewControllerBase<PhotoViewController
|
||||
|
||||
late StreamController<PhotoViewControllerValue> _outputCtrl;
|
||||
|
||||
@override
|
||||
ScaleBoundaries? scaleBoundaries;
|
||||
|
||||
late void Function(Offset)? _animatePosition;
|
||||
late void Function(double)? _animateScale;
|
||||
late void Function(double)? _animateRotation;
|
||||
@@ -311,4 +318,7 @@ class PhotoViewController implements PhotoViewControllerBase<PhotoViewController
|
||||
}
|
||||
_valueNotifier.value = newValue;
|
||||
}
|
||||
|
||||
@override
|
||||
double? get initialScale => scaleBoundaries?.initialScale ?? initial.scale;
|
||||
}
|
||||
|
||||
@@ -108,6 +108,17 @@ class _ImageWrapperState extends State<ImageWrapper> {
|
||||
}
|
||||
}
|
||||
|
||||
// Should be called only when _imageSize is not null
|
||||
ScaleBoundaries get scaleBoundaries {
|
||||
return ScaleBoundaries(
|
||||
widget.minScale ?? 0.0,
|
||||
widget.maxScale ?? double.infinity,
|
||||
widget.initialScale ?? PhotoViewComputedScale.contained,
|
||||
widget.outerSize,
|
||||
_imageSize!,
|
||||
);
|
||||
}
|
||||
|
||||
// retrieve image from the provider
|
||||
void _resolveImage() {
|
||||
final ImageStream newStream = widget.imageProvider.resolve(const ImageConfiguration());
|
||||
@@ -133,6 +144,7 @@ class _ImageWrapperState extends State<ImageWrapper> {
|
||||
_lastStack = null;
|
||||
|
||||
_didLoadSynchronously = synchronousCall;
|
||||
widget.controller.scaleBoundaries = scaleBoundaries;
|
||||
}
|
||||
|
||||
synchronousCall && !_didLoadSynchronously ? setupCB() : setState(setupCB);
|
||||
@@ -204,14 +216,6 @@ class _ImageWrapperState extends State<ImageWrapper> {
|
||||
);
|
||||
}
|
||||
|
||||
final scaleBoundaries = ScaleBoundaries(
|
||||
widget.minScale ?? 0.0,
|
||||
widget.maxScale ?? double.infinity,
|
||||
widget.initialScale ?? PhotoViewComputedScale.contained,
|
||||
widget.outerSize,
|
||||
_imageSize!,
|
||||
);
|
||||
|
||||
return PhotoViewCore(
|
||||
imageProvider: widget.imageProvider,
|
||||
backgroundDecoration: widget.backgroundDecoration,
|
||||
|
||||
@@ -16,7 +16,7 @@ describe('AssetViewerNavBar component', () => {
|
||||
preAction: () => {},
|
||||
onZoomImage: () => {},
|
||||
onAction: () => {},
|
||||
onRunJob: () => {},
|
||||
onEdit: () => {},
|
||||
onPlaySlideshow: () => {},
|
||||
onClose: () => {},
|
||||
playOriginalVideo: false,
|
||||
|
||||
@@ -28,12 +28,11 @@
|
||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import { getAssetJobName, getSharedLink, withoutIcons } from '$lib/utils';
|
||||
import { getSharedLink, withoutIcons } from '$lib/utils';
|
||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import {
|
||||
AssetJobName,
|
||||
AssetTypeEnum,
|
||||
AssetVisibility,
|
||||
type AlbumResponseDto,
|
||||
@@ -44,13 +43,9 @@
|
||||
import { CommandPaletteDefaultProvider, IconButton, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiArrowLeft,
|
||||
mdiCogRefreshOutline,
|
||||
mdiCompare,
|
||||
mdiContentCopy,
|
||||
mdiDatabaseRefreshOutline,
|
||||
mdiDotsVertical,
|
||||
mdiHeadSyncOutline,
|
||||
mdiImageRefreshOutline,
|
||||
mdiImageSearch,
|
||||
mdiMagnifyMinusOutline,
|
||||
mdiMagnifyPlusOutline,
|
||||
@@ -71,7 +66,6 @@
|
||||
preAction: PreAction;
|
||||
onAction: OnAction;
|
||||
onUndoDelete?: OnUndoDelete;
|
||||
onRunJob: (name: AssetJobName) => void;
|
||||
onPlaySlideshow: () => void;
|
||||
onEdit: () => void;
|
||||
onClose?: () => void;
|
||||
@@ -90,7 +84,6 @@
|
||||
preAction,
|
||||
onAction,
|
||||
onUndoDelete = undefined,
|
||||
onRunJob,
|
||||
onPlaySlideshow,
|
||||
onClose,
|
||||
onEdit,
|
||||
@@ -124,6 +117,10 @@
|
||||
PlayMotionPhoto,
|
||||
StopMotionPhoto,
|
||||
Info,
|
||||
RefreshFacesJob,
|
||||
RefreshMetadataJob,
|
||||
RegenerateThumbnailJob,
|
||||
TranscodeVideoJob,
|
||||
} = $derived(getAssetActions($t, asset));
|
||||
const sharedLink = getSharedLink();
|
||||
|
||||
@@ -140,7 +137,24 @@
|
||||
|
||||
<CommandPaletteDefaultProvider
|
||||
name={$t('assets')}
|
||||
actions={withoutIcons([Close, Share, Offline, Favorite, Unfavorite, PlayMotionPhoto, StopMotionPhoto, Info])}
|
||||
actions={withoutIcons([
|
||||
Close,
|
||||
Cast,
|
||||
Share,
|
||||
Download,
|
||||
DownloadOriginal,
|
||||
SharedLinkDownload,
|
||||
Offline,
|
||||
Favorite,
|
||||
Unfavorite,
|
||||
PlayMotionPhoto,
|
||||
StopMotionPhoto,
|
||||
Info,
|
||||
RefreshFacesJob,
|
||||
RefreshMetadataJob,
|
||||
RegenerateThumbnailJob,
|
||||
TranscodeVideoJob,
|
||||
])}
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -275,28 +289,10 @@
|
||||
{/if}
|
||||
{#if isOwner}
|
||||
<hr />
|
||||
<MenuOption
|
||||
icon={mdiHeadSyncOutline}
|
||||
onClick={() => onRunJob(AssetJobName.RefreshFaces)}
|
||||
text={$getAssetJobName(AssetJobName.RefreshFaces)}
|
||||
/>
|
||||
<MenuOption
|
||||
icon={mdiDatabaseRefreshOutline}
|
||||
onClick={() => onRunJob(AssetJobName.RefreshMetadata)}
|
||||
text={$getAssetJobName(AssetJobName.RefreshMetadata)}
|
||||
/>
|
||||
<MenuOption
|
||||
icon={mdiImageRefreshOutline}
|
||||
onClick={() => onRunJob(AssetJobName.RegenerateThumbnail)}
|
||||
text={$getAssetJobName(AssetJobName.RegenerateThumbnail)}
|
||||
/>
|
||||
{#if asset.type === AssetTypeEnum.Video}
|
||||
<MenuOption
|
||||
icon={mdiCogRefreshOutline}
|
||||
onClick={() => onRunJob(AssetJobName.TranscodeVideo)}
|
||||
text={$getAssetJobName(AssetJobName.TranscodeVideo)}
|
||||
/>
|
||||
{/if}
|
||||
<ActionMenuItem action={RefreshFacesJob} />
|
||||
<ActionMenuItem action={RefreshMetadataJob} />
|
||||
<ActionMenuItem action={RegenerateThumbnailJob} />
|
||||
<ActionMenuItem action={TranscodeVideoJob} />
|
||||
{/if}
|
||||
</ButtonContextMenu>
|
||||
{/if}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
import { alwaysLoadOriginalVideo } from '$lib/stores/preferences.store';
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { getAssetJobMessage, getAssetUrl, 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';
|
||||
@@ -28,18 +28,15 @@
|
||||
import { preloadImageUrl } from '$lib/utils/sw-messaging';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import {
|
||||
AssetJobName,
|
||||
AssetTypeEnum,
|
||||
getAllAlbums,
|
||||
getAssetInfo,
|
||||
getStack,
|
||||
runAssetJobs,
|
||||
type AlbumResponseDto,
|
||||
type AssetResponseDto,
|
||||
type PersonResponseDto,
|
||||
type StackResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { toastManager } from '@immich/ui';
|
||||
import { onDestroy, onMount, untrack } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fly } from 'svelte/transition';
|
||||
@@ -262,15 +259,6 @@
|
||||
isShowEditor = !isShowEditor;
|
||||
};
|
||||
|
||||
const handleRunJob = async (name: AssetJobName) => {
|
||||
try {
|
||||
await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } });
|
||||
toastManager.success($getAssetJobMessage(name));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_submit_job'));
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Slide show mode
|
||||
*/
|
||||
@@ -473,7 +461,6 @@
|
||||
onAction={handleAction}
|
||||
{onUndoDelete}
|
||||
onEdit={showEditor}
|
||||
onRunJob={handleRunJob}
|
||||
onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
|
||||
onClose={onClose ? () => onClose(asset) : undefined}
|
||||
{playOriginalVideo}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { zoomImageToBase64 } from '$lib/utils/people-utils';
|
||||
@@ -70,8 +70,8 @@
|
||||
isShowLoadingPeople = false;
|
||||
}
|
||||
|
||||
const onPersonThumbnail = (personId: string) => {
|
||||
assetFaceGenerated.push(personId);
|
||||
const onPersonThumbnailReady = ({ id }: { id: string }) => {
|
||||
assetFaceGenerated.push(id);
|
||||
if (
|
||||
isEqual(assetFaceGenerated, peopleToCreate) &&
|
||||
loaderLoadingDoneTimeout &&
|
||||
@@ -86,7 +86,6 @@
|
||||
|
||||
onMount(() => {
|
||||
handlePromiseError(loadPeople());
|
||||
return websocketEvents.on('on_person_thumbnail', onPersonThumbnail);
|
||||
});
|
||||
|
||||
const isEqual = (a: string[], b: string[]): boolean => {
|
||||
@@ -184,6 +183,8 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents {onPersonThumbnailReady} />
|
||||
|
||||
<section
|
||||
transition:fly={{ x: 360, duration: 100, easing: linear }}
|
||||
class="absolute top-0 h-full w-90 overflow-x-hidden p-2 dark:text-immich-dark-fg bg-light"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { getAssetJobIcon, getAssetJobMessage, getAssetJobName } from '$lib/utils';
|
||||
import { getAssetJobIcon, getAssetJobName } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AssetJobName, runAssetJobs } from '@immich/sdk';
|
||||
import { toastManager } from '@immich/ui';
|
||||
@@ -22,7 +22,7 @@
|
||||
try {
|
||||
const ids = [...getOwnedAssets()].map(({ id }) => id);
|
||||
await runAssetJobs({ assetJobsDto: { assetIds: ids, name } });
|
||||
toastManager.success($getAssetJobMessage(name));
|
||||
toastManager.success(getAssetJobName($t, name));
|
||||
clearSelect();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_submit_job'));
|
||||
@@ -32,6 +32,6 @@
|
||||
|
||||
{#each jobs as job (job)}
|
||||
{#if isAllVideos || job !== AssetJobName.TranscodeVideo}
|
||||
<MenuOption text={$getAssetJobName(job)} icon={getAssetJobIcon(job)} onClick={() => handleRunJob(job)} />
|
||||
<MenuOption text={getAssetJobName($t, job)} icon={getAssetJobIcon(job)} onClick={() => handleRunJob(job)} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
@@ -44,6 +44,7 @@ export type Events = {
|
||||
AlbumUserDelete: [{ albumId: string; userId: string }];
|
||||
|
||||
PersonUpdate: [PersonResponseDto];
|
||||
PersonThumbnailReady: [{ id: string }];
|
||||
|
||||
BackupDeleteStatus: [{ filename: string; isDeleting: boolean }];
|
||||
BackupDeleted: [{ filename: string }];
|
||||
|
||||
@@ -3,28 +3,36 @@ import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { user as authUser, preferences } from '$lib/stores/user.store';
|
||||
import { getSharedLink, sleep } from '$lib/utils';
|
||||
import { getAssetJobName, getSharedLink, sleep } from '$lib/utils';
|
||||
import { downloadUrl } from '$lib/utils/asset-utils';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { asQueryString } from '$lib/utils/shared-links';
|
||||
import {
|
||||
AssetJobName,
|
||||
AssetTypeEnum,
|
||||
AssetVisibility,
|
||||
copyAsset,
|
||||
deleteAssets,
|
||||
getAssetInfo,
|
||||
getBaseUrl,
|
||||
runAssetJobs,
|
||||
updateAsset,
|
||||
type AssetJobsDto,
|
||||
type AssetResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiAlertOutline,
|
||||
mdiCogRefreshOutline,
|
||||
mdiDatabaseRefreshOutline,
|
||||
mdiDownload,
|
||||
mdiDownloadBox,
|
||||
mdiHeadSyncOutline,
|
||||
mdiHeart,
|
||||
mdiHeartOutline,
|
||||
mdiImageRefreshOutline,
|
||||
mdiInformationOutline,
|
||||
mdiMotionPauseOutline,
|
||||
mdiMotionPlayOutline,
|
||||
@@ -124,6 +132,31 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
shortcuts: [{ key: 'i' }],
|
||||
};
|
||||
|
||||
const RefreshFacesJob: ActionItem = {
|
||||
title: getAssetJobName($t, AssetJobName.RefreshFaces),
|
||||
icon: mdiHeadSyncOutline,
|
||||
onAction: () => handleRunAssetJob({ name: AssetJobName.RefreshFaces, assetIds: [asset.id] }),
|
||||
};
|
||||
|
||||
const RefreshMetadataJob: ActionItem = {
|
||||
title: getAssetJobName($t, AssetJobName.RefreshMetadata),
|
||||
icon: mdiDatabaseRefreshOutline,
|
||||
onAction: () => handleRunAssetJob({ name: AssetJobName.RefreshMetadata, assetIds: [asset.id] }),
|
||||
};
|
||||
|
||||
const RegenerateThumbnailJob: ActionItem = {
|
||||
title: getAssetJobName($t, AssetJobName.RegenerateThumbnail),
|
||||
icon: mdiImageRefreshOutline,
|
||||
onAction: () => handleRunAssetJob({ name: AssetJobName.RegenerateThumbnail, assetIds: [asset.id] }),
|
||||
};
|
||||
|
||||
const TranscodeVideoJob: ActionItem = {
|
||||
title: getAssetJobName($t, AssetJobName.TranscodeVideo),
|
||||
icon: mdiCogRefreshOutline,
|
||||
onAction: () => handleRunAssetJob({ name: AssetJobName.TranscodeVideo, assetIds: [asset.id] }),
|
||||
$if: () => asset.type === AssetTypeEnum.Video,
|
||||
};
|
||||
|
||||
return {
|
||||
Share,
|
||||
Download,
|
||||
@@ -135,6 +168,10 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
||||
Unfavorite,
|
||||
PlayMotionPhoto,
|
||||
StopMotionPhoto,
|
||||
RefreshFacesJob,
|
||||
RefreshMetadataJob,
|
||||
RegenerateThumbnailJob,
|
||||
TranscodeVideoJob,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -217,3 +254,14 @@ export const handleReplaceAsset = async (oldAssetId: string) => {
|
||||
|
||||
eventManager.emit('AssetReplace', { oldAssetId, newAssetId });
|
||||
};
|
||||
|
||||
const handleRunAssetJob = async (dto: AssetJobsDto) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
await runAssetJobs({ assetJobsDto: dto });
|
||||
toastManager.success(getAssetJobName($t, dto.name));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_submit_job'));
|
||||
}
|
||||
};
|
||||
|
||||
14
web/src/lib/services/keyboard.service.ts
Normal file
14
web/src/lib/services/keyboard.service.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
import { modalManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiKeyboard } from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
export const getKeyboardActions = ($t: MessageFormatter) => {
|
||||
const KeyboardShortcuts: ActionItem = {
|
||||
title: $t('show_keyboard_shortcuts'),
|
||||
icon: mdiKeyboard,
|
||||
onAction: () => modalManager.show(ShortcutsModal, {}),
|
||||
};
|
||||
|
||||
return { KeyboardShortcuts };
|
||||
};
|
||||
@@ -76,6 +76,7 @@ websocket
|
||||
.on('on_new_release', (event) => eventManager.emit('ReleaseEvent', event))
|
||||
.on('on_session_delete', () => authManager.logout())
|
||||
.on('on_user_delete', (id) => eventManager.emit('UserAdminDeleted', { id }))
|
||||
.on('on_person_thumbnail', (id) => eventManager.emit('PersonThumbnailReady', { id }))
|
||||
.on('on_notification', () => notificationManager.refresh())
|
||||
.on('connect_error', (e) => console.log('Websocket Connect Error', e));
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
} from '@immich/sdk';
|
||||
import { toastManager, type ActionItem, type IfLike } from '@immich/ui';
|
||||
import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiHeadSyncOutline, mdiImageRefreshOutline } from '@mdi/js';
|
||||
import { init, register, t } from 'svelte-i18n';
|
||||
import { init, register, t, type MessageFormatter } from 'svelte-i18n';
|
||||
import { derived, get } from 'svelte/store';
|
||||
|
||||
interface DownloadRequestOptions<T = unknown> {
|
||||
@@ -259,31 +259,16 @@ export const getProfileImageUrl = (user: UserResponseDto) =>
|
||||
export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) =>
|
||||
createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt });
|
||||
|
||||
export const getAssetJobName = derived(t, ($t) => {
|
||||
return (job: AssetJobName) => {
|
||||
const names: Record<AssetJobName, string> = {
|
||||
[AssetJobName.RefreshFaces]: $t('refresh_faces'),
|
||||
[AssetJobName.RefreshMetadata]: $t('refresh_metadata'),
|
||||
[AssetJobName.RegenerateThumbnail]: $t('refresh_thumbnails'),
|
||||
[AssetJobName.TranscodeVideo]: $t('refresh_encoded_videos'),
|
||||
};
|
||||
|
||||
return names[job];
|
||||
export const getAssetJobName = ($t: MessageFormatter, job: AssetJobName) => {
|
||||
const messages: Record<AssetJobName, string> = {
|
||||
[AssetJobName.RefreshFaces]: $t('refreshing_faces'),
|
||||
[AssetJobName.RefreshMetadata]: $t('refreshing_metadata'),
|
||||
[AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'),
|
||||
[AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'),
|
||||
};
|
||||
});
|
||||
|
||||
export const getAssetJobMessage = derived(t, ($t) => {
|
||||
return (job: AssetJobName) => {
|
||||
const messages: Record<AssetJobName, string> = {
|
||||
[AssetJobName.RefreshFaces]: $t('refreshing_faces'),
|
||||
[AssetJobName.RefreshMetadata]: $t('refreshing_metadata'),
|
||||
[AssetJobName.RegenerateThumbnail]: $t('regenerating_thumbnails'),
|
||||
[AssetJobName.TranscodeVideo]: $t('refreshing_encoded_video'),
|
||||
};
|
||||
|
||||
return messages[job];
|
||||
};
|
||||
});
|
||||
return messages[job];
|
||||
};
|
||||
|
||||
export const getAssetJobIcon = (job: AssetJobName) => {
|
||||
const names: Record<AssetJobName, string> = {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<script lang="ts">
|
||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte';
|
||||
import { Route } from '$lib/route';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { AssetMediaSize, type SearchExploreResponseDto } from '@immich/sdk';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiHeart } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
@@ -29,17 +28,17 @@
|
||||
|
||||
let hasPeople = $derived(data.response.total > 0);
|
||||
|
||||
onMount(() => {
|
||||
return websocketEvents.on('on_person_thumbnail', (personId: string) => {
|
||||
people.map((person) => {
|
||||
if (person.id === personId) {
|
||||
person.updatedAt = Date.now().toString();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
const onPersonThumbnailReady = ({ id }: { id: string }) => {
|
||||
for (const person of people) {
|
||||
if (person.id === id) {
|
||||
person.updatedAt = new Date().toISOString();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<OnEvents {onPersonThumbnailReady} />
|
||||
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
{#if hasPeople}
|
||||
<div class="mb-6 mt-2">
|
||||
|
||||
@@ -1,30 +1,21 @@
|
||||
<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 ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||
import { Container, IconButton, modalManager } from '@immich/ui';
|
||||
import { mdiKeyboard } from '@mdi/js';
|
||||
import { getKeyboardActions } from '$lib/services/keyboard.service';
|
||||
import { Container } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
data: PageData;
|
||||
}
|
||||
};
|
||||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const { KeyboardShortcuts } = $derived(getKeyboardActions($t));
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={data.meta.title}>
|
||||
{#snippet buttons()}
|
||||
<IconButton
|
||||
shape="round"
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
icon={mdiKeyboard}
|
||||
aria-label={$t('show_keyboard_shortcuts')}
|
||||
onclick={() => modalManager.show(ShortcutsModal, {})}
|
||||
/>
|
||||
{/snippet}
|
||||
<UserPageLayout title={data.meta.title} actions={[KeyboardShortcuts]}>
|
||||
<Container size="medium" center>
|
||||
<UserSettingsList keys={data.keys} sessions={data.sessions} />
|
||||
</Container>
|
||||
|
||||
Reference in New Issue
Block a user