mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
Merge branch 'main' into openvino-cpu-fix
This commit is contained in:
commit
1b81b9b2fd
32 changed files with 264 additions and 245 deletions
|
|
@ -333,7 +333,7 @@
|
|||
"transcoding_max_b_frames": "Maximum B-frames",
|
||||
"transcoding_max_b_frames_description": "Higher values improve compression efficiency, but slow down encoding. May not be compatible with hardware acceleration on older devices. 0 disables B-frames, while -1 sets this value automatically.",
|
||||
"transcoding_max_bitrate": "Maximum bitrate",
|
||||
"transcoding_max_bitrate_description": "Setting a max bitrate can make file sizes more predictable at a minor cost to quality. At 720p, typical values are 2600 kbit/s for VP9 or HEVC, or 4500 kbit/s for H.264. Disabled if set to 0.",
|
||||
"transcoding_max_bitrate_description": "Setting a max bitrate can make file sizes more predictable at a minor cost to quality. At 720p, typical values are 2600 kbit/s for VP9 or HEVC, or 4500 kbit/s for H.264. Disabled if set to 0. When no unit is specified, k (for kbit/s) is assumed; therefore 5000, 5000k, and 5M (for Mbit/s) are equivalent.",
|
||||
"transcoding_max_keyframe_interval": "Maximum keyframe interval",
|
||||
"transcoding_max_keyframe_interval_description": "Sets the maximum frame distance between keyframes. Lower values worsen compression efficiency, but improve seek times and may improve quality in scenes with fast movement. 0 sets this value automatically.",
|
||||
"transcoding_optimal_description": "Videos higher than target resolution or not in an accepted format",
|
||||
|
|
@ -351,7 +351,7 @@
|
|||
"transcoding_target_resolution": "Target resolution",
|
||||
"transcoding_target_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.",
|
||||
"transcoding_temporal_aq": "Temporal AQ",
|
||||
"transcoding_temporal_aq_description": "Applies only to NVENC. Increases quality of high-detail, low-motion scenes. May not be compatible with older devices.",
|
||||
"transcoding_temporal_aq_description": "Applies only to NVENC. Temporal Adaptive Quantization increases quality of high-detail, low-motion scenes. May not be compatible with older devices.",
|
||||
"transcoding_threads": "Threads",
|
||||
"transcoding_threads_description": "Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0.",
|
||||
"transcoding_tone_mapping": "Tone-mapping",
|
||||
|
|
@ -1807,6 +1807,8 @@
|
|||
"setting_notifications_subtitle": "Adjust your notification preferences",
|
||||
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
|
||||
"setting_notifications_total_progress_title": "Show background backup total progress",
|
||||
"setting_video_viewer_auto_play_subtitle": "Automatically start playing videos when they are opened",
|
||||
"setting_video_viewer_auto_play_title": "Auto play videos",
|
||||
"setting_video_viewer_looping_title": "Looping",
|
||||
"setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.",
|
||||
"setting_video_viewer_original_video_title": "Force original video",
|
||||
|
|
|
|||
|
|
@ -133,11 +133,15 @@
|
|||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
B231F52D2E93A44A00BC45D1 /* Core */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Core;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
);
|
||||
path = Sync;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
|
|
@ -526,14 +530,10 @@
|
|||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
|
|
@ -562,14 +562,10 @@
|
|||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
|
||||
|
|
@ -718,7 +714,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
|
@ -862,7 +858,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
|
@ -892,7 +888,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
|
@ -926,7 +922,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
|
@ -969,7 +965,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
|
@ -1009,7 +1005,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
|
|
@ -1048,7 +1044,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
|
@ -1092,7 +1088,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
|
@ -1133,7 +1129,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 230;
|
||||
CURRENT_PROJECT_VERSION = 231;
|
||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.0.1</string>
|
||||
<string>2.1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>230</string>
|
||||
<string>231</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
|
|
|||
|
|
@ -103,7 +103,9 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||
let options = PHFetchOptions()
|
||||
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
|
||||
options.includeHiddenAssets = false
|
||||
let assets = PHAsset.fetchAssets(in: album, options: options)
|
||||
|
||||
let assets = getAssetsFromAlbum(in: album, options: options)
|
||||
|
||||
let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
|
||||
|
||||
var domainAlbum = PlatformAlbum(
|
||||
|
|
@ -201,7 +203,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||
let options = PHFetchOptions()
|
||||
options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id))
|
||||
options.includeHiddenAssets = false
|
||||
let result = PHAsset.fetchAssets(in: album, options: options)
|
||||
let result = self.getAssetsFromAlbum(in: album, options: options)
|
||||
result.enumerateObjects { (asset, _, _) in
|
||||
albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier)
|
||||
}
|
||||
|
|
@ -219,7 +221,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||
var ids: [String] = []
|
||||
let options = PHFetchOptions()
|
||||
options.includeHiddenAssets = false
|
||||
let assets = PHAsset.fetchAssets(in: album, options: options)
|
||||
let assets = getAssetsFromAlbum(in: album, options: options)
|
||||
assets.enumerateObjects { (asset, _, _) in
|
||||
ids.append(asset.localIdentifier)
|
||||
}
|
||||
|
|
@ -236,7 +238,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||
let options = PHFetchOptions()
|
||||
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
||||
options.includeHiddenAssets = false
|
||||
let assets = PHAsset.fetchAssets(in: album, options: options)
|
||||
let assets = getAssetsFromAlbum(in: album, options: options)
|
||||
return Int64(assets.count)
|
||||
}
|
||||
|
||||
|
|
@ -253,7 +255,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
|
||||
}
|
||||
|
||||
let result = PHAsset.fetchAssets(in: album, options: options)
|
||||
let result = getAssetsFromAlbum(in: album, options: options)
|
||||
if(result.count == 0) {
|
||||
return []
|
||||
}
|
||||
|
|
@ -375,4 +377,13 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
|
|||
PHAssetResourceManager.default().cancelDataRequest(requestId)
|
||||
})
|
||||
}
|
||||
|
||||
private func getAssetsFromAlbum(in album: PHAssetCollection, options: PHFetchOptions) -> PHFetchResult<PHAsset> {
|
||||
// Ensure to actually getting all assets for the Recents album
|
||||
if (album.assetCollectionSubtype == .smartAlbumUserLibrary) {
|
||||
return PHAsset.fetchAssets(with: options)
|
||||
} else {
|
||||
return PHAsset.fetchAssets(in: album, options: options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ enum Setting<T> {
|
|||
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
|
||||
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
||||
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
|
||||
autoPlayVideo<bool>(StoreKey.autoPlayVideo, true),
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
||||
enableBackup<bool>(StoreKey.enableBackup, false);
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ enum StoreKey<T> {
|
|||
// Read-only Mode settings
|
||||
readonlyModeEnabled<bool>._(138),
|
||||
|
||||
autoPlayVideo<bool>._(139),
|
||||
|
||||
// Experimental stuff
|
||||
photoManagerCustomFilter<bool>._(1000),
|
||||
betaPromptShown<bool>._(1001),
|
||||
|
|
|
|||
|
|
@ -190,7 +190,10 @@ class NativeVideoViewerPage extends HookConsumerWidget {
|
|||
isVideoReady.value = true;
|
||||
|
||||
try {
|
||||
final autoPlayVideo = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.autoPlayVideo);
|
||||
if (autoPlayVideo) {
|
||||
await videoController.play();
|
||||
}
|
||||
await videoController.setVolume(0.9);
|
||||
} catch (error) {
|
||||
log.severe('Error playing video: $error');
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
|
|
@ -218,7 +219,10 @@ class NativeVideoViewer extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
try {
|
||||
final autoPlayVideo = AppSetting.get(Setting.autoPlayVideo);
|
||||
if (autoPlayVideo) {
|
||||
await videoController.play();
|
||||
}
|
||||
await videoController.setVolume(0.9);
|
||||
} catch (error) {
|
||||
log.severe('Error playing video: $error');
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ enum AppSettingsEnum<T> {
|
|||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, "loadOriginalVideo", false),
|
||||
autoPlayVideo<bool>(StoreKey.autoPlayVideo, "autoPlayVideo", true),
|
||||
mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
|
||||
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
|
||||
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
|
||||
|
|
|
|||
|
|
@ -14,11 +14,18 @@ class VideoViewerSettings extends HookConsumerWidget {
|
|||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final useLoopVideo = useAppSettingsState(AppSettingsEnum.loopVideo);
|
||||
final useOriginalVideo = useAppSettingsState(AppSettingsEnum.loadOriginalVideo);
|
||||
final useAutoPlayVideo = useAppSettingsState(AppSettingsEnum.autoPlayVideo);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "videos".tr()),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: useAutoPlayVideo,
|
||||
title: "setting_video_viewer_auto_play_title".tr(),
|
||||
subtitle: "setting_video_viewer_auto_play_subtitle".tr(),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: useLoopVideo,
|
||||
title: "setting_video_viewer_looping_title".tr(),
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
|
||||
import { IconButton } from '@immich/ui';
|
||||
import { mdiDownload, mdiFileImagePlusOutline } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte';
|
||||
|
|
@ -35,9 +34,8 @@
|
|||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
const timelineManager = new TimelineManager();
|
||||
$effect(() => void timelineManager.updateOptions({ albumId: album.id, order: album.order }));
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
const options = $derived({ albumId: album.id, order: album.order });
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
|
|
@ -61,7 +59,7 @@
|
|||
/>
|
||||
|
||||
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
|
||||
<Timeline enableRouting={true} {album} {timelineManager} {assetInteraction}>
|
||||
<Timeline enableRouting={true} {album} bind:timelineManager {options} {assetInteraction}>
|
||||
<section class="pt-8 md:pt-24 px-2 md:px-0">
|
||||
<!-- ALBUM TITLE -->
|
||||
<h1 class="text-2xl md:text-4xl lg:text-6xl text-primary outline-none transition-all">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,12 @@
|
|||
import { assetViewerFadeDuration } from '$lib/constants';
|
||||
import { castManager } from '$lib/managers/cast-manager.svelte';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
import { loopVideo as loopVideoPreference, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
||||
import {
|
||||
autoPlayVideo,
|
||||
loopVideo as loopVideoPreference,
|
||||
videoViewerMuted,
|
||||
videoViewerVolume,
|
||||
} from '$lib/stores/preferences.store';
|
||||
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { AssetMediaSize } from '@immich/sdk';
|
||||
import { LoadingSpinner } from '@immich/ui';
|
||||
|
|
@ -125,7 +130,7 @@
|
|||
<video
|
||||
bind:this={videoPlayer}
|
||||
loop={$loopVideoPreference && loopVideo}
|
||||
autoplay
|
||||
autoplay={$autoPlayVideo}
|
||||
playsinline
|
||||
controls
|
||||
class="h-full object-contain"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { assetViewerFadeDuration } from '$lib/constants';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { autoPlayVideo } from '$lib/stores/preferences.store';
|
||||
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { AssetMediaSize } from '@immich/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
|
|
@ -27,7 +28,7 @@
|
|||
<div class="h-full w-full bg-pink-9000" transition:fade={{ duration: assetViewerFadeDuration }}>
|
||||
<video
|
||||
bind:this={videoPlayer}
|
||||
autoplay
|
||||
autoplay={$autoPlayVideo}
|
||||
playsinline
|
||||
class="h-full w-full rounded-2xl object-contain transition-all"
|
||||
src={getAssetPlaybackUrl({ id: asset.id })}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
|
||||
import type { ScrubberMonth, ViewportTopMonth } from '$lib/managers/timeline-manager/types';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { getTabbable } from '$lib/utils/focus-util';
|
||||
import { type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
import { Icon } from '@immich/ui';
|
||||
import { mdiPlay } from '@mdi/js';
|
||||
import { clamp } from 'lodash-es';
|
||||
|
|
@ -24,9 +24,8 @@
|
|||
/** The percentage of scroll through the month that is currently intersecting the top boundary of the viewport */
|
||||
viewportTopMonthScrollPercent?: number;
|
||||
/** The year/month of the timeline month at the top of the viewport */
|
||||
viewportTopMonth?: TimelineYearMonth;
|
||||
/** Indicates whether the viewport is currently in the lead-out section (after all months) */
|
||||
isInLeadOutSection?: boolean;
|
||||
viewportTopMonth?: ViewportTopMonth;
|
||||
|
||||
/** Width of the scrubber component in pixels (bindable for parent component margin adjustments) */
|
||||
scrubberWidth?: number;
|
||||
/** Callback fired when user interacts with the scrubber to navigate */
|
||||
|
|
@ -47,7 +46,6 @@
|
|||
timelineScrollPercent = 0,
|
||||
viewportTopMonthScrollPercent = 0,
|
||||
viewportTopMonth = undefined,
|
||||
isInLeadOutSection = false,
|
||||
onScrub = undefined,
|
||||
onScrubKeyDown = undefined,
|
||||
startScrub = undefined,
|
||||
|
|
@ -94,11 +92,19 @@
|
|||
});
|
||||
|
||||
const toScrollFromMonthGroupPercentage = (
|
||||
scrubberMonth: { year: number; month: number } | undefined,
|
||||
scrubberMonth: ViewportTopMonth,
|
||||
scrubberMonthPercent: number,
|
||||
scrubOverallPercent: number,
|
||||
) => {
|
||||
if (scrubberMonth) {
|
||||
if (scrubberMonth === 'lead-in') {
|
||||
return relativeTopOffset * scrubberMonthPercent;
|
||||
} else if (scrubberMonth === 'lead-out') {
|
||||
let offset = relativeTopOffset;
|
||||
for (const segment of segments) {
|
||||
offset += segment.height;
|
||||
}
|
||||
return offset + relativeBottomOffset * scrubberMonthPercent;
|
||||
} else if (scrubberMonth) {
|
||||
let offset = relativeTopOffset;
|
||||
let match = false;
|
||||
for (const segment of segments) {
|
||||
|
|
@ -113,23 +119,16 @@
|
|||
offset += scrubberMonthPercent * relativeBottomOffset;
|
||||
}
|
||||
return offset;
|
||||
} else if (isInLeadOutSection) {
|
||||
let offset = relativeTopOffset;
|
||||
for (const segment of segments) {
|
||||
offset += segment.height;
|
||||
}
|
||||
offset += scrubOverallPercent * relativeBottomOffset;
|
||||
return offset;
|
||||
} else {
|
||||
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
||||
}
|
||||
};
|
||||
let scrollY = $derived(
|
||||
const scrollY = $derived(
|
||||
toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
|
||||
);
|
||||
let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
|
||||
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
||||
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
||||
const timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight);
|
||||
const relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
||||
const relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
||||
|
||||
type Segment = {
|
||||
count: number;
|
||||
|
|
@ -173,14 +172,13 @@
|
|||
segment.hasLabel = true;
|
||||
previousLabeledSegment = segment;
|
||||
}
|
||||
if (i !== 1 && segment.height > 5 && dotHeight > MIN_DOT_DISTANCE) {
|
||||
if (segment.height > 5 && dotHeight > MIN_DOT_DISTANCE) {
|
||||
segment.hasDot = true;
|
||||
dotHeight = 0;
|
||||
}
|
||||
|
||||
height += segment.height;
|
||||
dotHeight += segment.height;
|
||||
}
|
||||
dotHeight += segment.height;
|
||||
segments.push(segment);
|
||||
}
|
||||
|
||||
|
|
@ -197,7 +195,13 @@
|
|||
}
|
||||
return activeSegment?.dataset.label;
|
||||
});
|
||||
const segmentDate = $derived.by(() => {
|
||||
const segmentDate: ViewportTopMonth = $derived.by(() => {
|
||||
if (activeSegment?.dataset.id === 'lead-in') {
|
||||
return 'lead-in';
|
||||
}
|
||||
if (activeSegment?.dataset.id === 'lead-out') {
|
||||
return 'lead-out';
|
||||
}
|
||||
if (!activeSegment?.dataset.segmentYearMonth) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
@ -215,7 +219,22 @@
|
|||
}
|
||||
return null;
|
||||
});
|
||||
const scrollHoverLabel = $derived(scrollSegment?.dateFormatted || '');
|
||||
const scrollHoverLabel = $derived.by(() => {
|
||||
if (scrollY !== undefined) {
|
||||
if (scrollY < relativeTopOffset) {
|
||||
return segments.at(0)?.dateFormatted;
|
||||
} else {
|
||||
let offset = relativeTopOffset;
|
||||
for (const segment of segments) {
|
||||
offset += segment.height;
|
||||
}
|
||||
if (scrollY > offset) {
|
||||
return segments.at(-1)?.dateFormatted;
|
||||
}
|
||||
}
|
||||
}
|
||||
return scrollSegment?.dateFormatted || '';
|
||||
});
|
||||
|
||||
const findElementBestY = (elements: Element[], y: number, ...ids: string[]) => {
|
||||
if (ids.length === 0) {
|
||||
|
|
@ -308,38 +327,23 @@
|
|||
isHoverOnPaddingTop = isOnPaddingTop;
|
||||
isHoverOnPaddingBottom = isOnPaddingBottom;
|
||||
|
||||
const scrollPercent = toTimelineY(hoverY);
|
||||
const scrubData = {
|
||||
scrubberMonth: segmentDate,
|
||||
overallScrollPercent: toTimelineY(hoverY),
|
||||
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||
};
|
||||
if (wasDragging === false && isDragging) {
|
||||
void startScrub?.({
|
||||
scrubberMonth: segmentDate!,
|
||||
overallScrollPercent: scrollPercent,
|
||||
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||
});
|
||||
void onScrub?.({
|
||||
scrubberMonth: segmentDate!,
|
||||
overallScrollPercent: scrollPercent,
|
||||
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||
});
|
||||
void startScrub?.(scrubData);
|
||||
void onScrub?.(scrubData);
|
||||
}
|
||||
|
||||
if (wasDragging && !isDragging) {
|
||||
void stopScrub?.({
|
||||
scrubberMonth: segmentDate!,
|
||||
overallScrollPercent: scrollPercent,
|
||||
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||
});
|
||||
void stopScrub?.(scrubData);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDragging) {
|
||||
return;
|
||||
}
|
||||
|
||||
void onScrub?.({
|
||||
scrubberMonth: segmentDate!,
|
||||
overallScrollPercent: scrollPercent,
|
||||
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||
});
|
||||
void onScrub?.(scrubData);
|
||||
};
|
||||
const getTouch = (event: TouchEvent) => {
|
||||
if (event.touches.length === 1) {
|
||||
|
|
@ -559,13 +563,8 @@
|
|||
class="relative"
|
||||
style:height={relativeTopOffset + 'px'}
|
||||
data-id="lead-in"
|
||||
data-segment-year-month={segments.at(0)?.year + '-' + segments.at(0)?.month}
|
||||
data-label={segments.at(0)?.dateFormatted}
|
||||
>
|
||||
{#if relativeTopOffset > 6}
|
||||
<div class="absolute end-3 h-[4px] w-[4px] rounded-full bg-gray-300"></div>
|
||||
{/if}
|
||||
</div>
|
||||
></div>
|
||||
<!-- Time Segment -->
|
||||
{#each segments as segment (segment.year + '-' + segment.month)}
|
||||
<div
|
||||
|
|
@ -587,5 +586,10 @@
|
|||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
<div data-id="lead-out" class="relative" style:height={relativeBottomOffset + 'px'}></div>
|
||||
<div
|
||||
data-id="lead-out"
|
||||
class="relative"
|
||||
style:height={relativeBottomOffset + 'px'}
|
||||
data-label={segments.at(-1)?.dateFormatted}
|
||||
></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,17 +12,17 @@
|
|||
import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
|
||||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import type { TimelineAsset, TimelineManagerOptions, ViewportTopMonth } from '$lib/managers/timeline-manager/types';
|
||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { getTimes, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { getTimes, type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
import { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { onDestroy, onMount, type Snippet } from 'svelte';
|
||||
import type { UpdatePayload } from 'vite';
|
||||
import TimelineDateGroup from './TimelineDateGroup.svelte';
|
||||
|
||||
|
|
@ -33,7 +33,8 @@
|
|||
`AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and
|
||||
additionally, update the page location/url with the asset as the timeline is scrolled */
|
||||
enableRouting: boolean;
|
||||
timelineManager: TimelineManager;
|
||||
timelineManager?: TimelineManager;
|
||||
options?: TimelineManagerOptions;
|
||||
assetInteraction: AssetInteraction;
|
||||
removeAction?:
|
||||
| AssetAction.UNARCHIVE
|
||||
|
|
@ -71,6 +72,7 @@
|
|||
singleSelect = false,
|
||||
enableRouting,
|
||||
timelineManager = $bindable(),
|
||||
options,
|
||||
assetInteraction,
|
||||
removeAction = null,
|
||||
withStacked = false,
|
||||
|
|
@ -87,6 +89,10 @@
|
|||
onThumbnailClick,
|
||||
}: Props = $props();
|
||||
|
||||
timelineManager = new TimelineManager();
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
$effect(() => options && void timelineManager.updateOptions(options));
|
||||
|
||||
let { isViewing: showAssetViewer, asset: viewingAsset, gridScrollTarget } = assetViewingStore;
|
||||
|
||||
let scrollableElement: HTMLElement | undefined = $state();
|
||||
|
|
@ -97,16 +103,11 @@
|
|||
// Note: There may be multiple months visible within the viewport at any given time.
|
||||
let viewportTopMonthScrollPercent = $state(0);
|
||||
// The timeline month intersecting the top position of the viewport
|
||||
let viewportTopMonth: { year: number; month: number } | undefined = $state(undefined);
|
||||
let viewportTopMonth: ViewportTopMonth = $state(undefined);
|
||||
// Overall scroll percentage through the entire timeline (0-1)
|
||||
let timelineScrollPercent: number = $state(0);
|
||||
let scrubberWidth = $state(0);
|
||||
|
||||
// 60 is the bottom spacer element at 60px
|
||||
let bottomSectionHeight = 60;
|
||||
// Indicates whether the viewport is currently in the lead-out section (after all months)
|
||||
let isInLeadOutSection = $state(false);
|
||||
|
||||
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||
const maxMd = $derived(mobileDevice.maxMd);
|
||||
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
||||
|
|
@ -212,13 +213,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
const handleBeforeUpdate = (payload: UpdatePayload) => {
|
||||
const timelineUpdate = payload.updates.some((update) => update.path.endsWith('Timeline.svelte'));
|
||||
if (timelineUpdate) {
|
||||
timelineManager.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
const updateIsScrolling = () => (timelineManager.scrolling = true);
|
||||
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker
|
||||
|
||||
|
|
@ -230,41 +224,36 @@
|
|||
}
|
||||
});
|
||||
|
||||
const getMaxScrollPercent = () => {
|
||||
const totalHeight = timelineManager.timelineHeight + bottomSectionHeight + timelineManager.topSectionHeight;
|
||||
return (totalHeight - timelineManager.viewportHeight) / totalHeight;
|
||||
};
|
||||
|
||||
const getMaxScroll = () => {
|
||||
if (!scrollableElement || !timelineElement) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
timelineManager.topSectionHeight +
|
||||
bottomSectionHeight +
|
||||
(timelineElement.clientHeight - scrollableElement.clientHeight)
|
||||
);
|
||||
};
|
||||
|
||||
const scrollToMonthGroupAndOffset = (monthGroup: MonthGroup, monthGroupScrollPercent: number) => {
|
||||
const topOffset = monthGroup.top;
|
||||
const maxScrollPercent = getMaxScrollPercent();
|
||||
const delta = monthGroup.height * monthGroupScrollPercent;
|
||||
const scrollToSegmentPercentage = (segmentTop: number, segmentHeight: number, monthGroupScrollPercent: number) => {
|
||||
const topOffset = segmentTop;
|
||||
const maxScrollPercent = timelineManager.maxScrollPercent;
|
||||
const delta = segmentHeight * monthGroupScrollPercent;
|
||||
const scrollToTop = (topOffset + delta) * maxScrollPercent;
|
||||
|
||||
timelineManager.scrollTo(scrollToTop);
|
||||
};
|
||||
|
||||
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker
|
||||
// this function scrolls the timeline to the specified month group and offset, based on scrubber interaction
|
||||
const onScrub: ScrubberListener = (scrubberData) => {
|
||||
const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent } = scrubberData;
|
||||
|
||||
if (!scrubberMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||
const leadIn = scrubberMonth === 'lead-in';
|
||||
const leadOut = scrubberMonth === 'lead-out';
|
||||
const noMonth = !scrubberMonth;
|
||||
|
||||
if (noMonth || timelineManager.limitedScroll) {
|
||||
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
||||
const maxScroll = getMaxScroll();
|
||||
const offset = maxScroll * overallScrollPercent;
|
||||
const maxScroll = timelineManager.maxScrollPercent;
|
||||
const offset = maxScroll * overallScrollPercent * timelineManager.totalViewerHeight;
|
||||
timelineManager.scrollTo(offset);
|
||||
} else if (leadIn) {
|
||||
scrollToSegmentPercentage(0, timelineManager.topSectionHeight, scrubberMonthScrollPercent);
|
||||
} else if (leadOut) {
|
||||
scrollToSegmentPercentage(
|
||||
timelineManager.topSectionHeight + timelineManager.assetsHeight,
|
||||
timelineManager.bottomSectionHeight,
|
||||
scrubberMonthScrollPercent,
|
||||
);
|
||||
} else {
|
||||
const monthGroup = timelineManager.months.find(
|
||||
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
|
||||
|
|
@ -272,50 +261,41 @@
|
|||
if (!monthGroup) {
|
||||
return;
|
||||
}
|
||||
scrollToMonthGroupAndOffset(monthGroup, scrubberMonthScrollPercent);
|
||||
scrollToSegmentPercentage(monthGroup.top, monthGroup.height, scrubberMonthScrollPercent);
|
||||
}
|
||||
};
|
||||
|
||||
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
||||
const handleTimelineScroll = () => {
|
||||
isInLeadOutSection = false;
|
||||
|
||||
if (!scrollableElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||
if (timelineManager.limitedScroll) {
|
||||
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
|
||||
const maxScroll = getMaxScroll();
|
||||
timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll);
|
||||
const maxScroll = timelineManager.maxScroll;
|
||||
|
||||
timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll);
|
||||
viewportTopMonth = undefined;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
} else {
|
||||
timelineScrollPercent = 0;
|
||||
|
||||
let top = scrollableElement.scrollTop;
|
||||
if (top < timelineManager.topSectionHeight) {
|
||||
// in the lead-in area
|
||||
viewportTopMonth = undefined;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
const maxScroll = getMaxScroll();
|
||||
|
||||
timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll);
|
||||
return;
|
||||
}
|
||||
|
||||
let maxScrollPercent = getMaxScrollPercent();
|
||||
let found = false;
|
||||
let maxScrollPercent = timelineManager.maxScrollPercent;
|
||||
|
||||
const monthsLength = timelineManager.months.length;
|
||||
for (let i = -1; i < monthsLength + 1; i++) {
|
||||
let monthGroup: TimelineYearMonth | undefined;
|
||||
let monthGroup: ViewportTopMonth;
|
||||
let monthGroupHeight = 0;
|
||||
if (i === -1) {
|
||||
// lead-in
|
||||
monthGroup = 'lead-in';
|
||||
monthGroupHeight = timelineManager.topSectionHeight;
|
||||
} else if (i === monthsLength) {
|
||||
// lead-out
|
||||
monthGroupHeight = bottomSectionHeight;
|
||||
monthGroup = 'lead-out';
|
||||
monthGroupHeight = timelineManager.bottomSectionHeight;
|
||||
} else {
|
||||
monthGroup = timelineManager.months[i].yearMonth;
|
||||
monthGroupHeight = timelineManager.months[i].height;
|
||||
|
|
@ -334,18 +314,10 @@
|
|||
viewportTopMonth = timelineManager.months[i + 1].yearMonth;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
}
|
||||
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
top = next;
|
||||
}
|
||||
if (!found) {
|
||||
isInLeadOutSection = true;
|
||||
viewportTopMonth = undefined;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
timelineScrollPercent = 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -525,7 +497,7 @@
|
|||
|
||||
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
||||
|
||||
<HotModuleReload onAfterUpdate={handleAfterUpdate} onBeforeUpdate={handleBeforeUpdate} />
|
||||
<HotModuleReload onAfterUpdate={handleAfterUpdate} onBeforeUpdate={() => timelineManager.destroy()} />
|
||||
|
||||
<TimelineKeyboardActions
|
||||
scrollToAsset={(asset) => scrollToAsset(asset) ?? false}
|
||||
|
|
@ -540,8 +512,7 @@
|
|||
{timelineManager}
|
||||
height={timelineManager.viewportHeight}
|
||||
timelineTopOffset={timelineManager.topSectionHeight}
|
||||
timelineBottomOffset={bottomSectionHeight}
|
||||
{isInLeadOutSection}
|
||||
timelineBottomOffset={timelineManager.bottomSectionHeight}
|
||||
{timelineScrollPercent}
|
||||
{viewportTopMonthScrollPercent}
|
||||
{viewportTopMonth}
|
||||
|
|
@ -580,7 +551,7 @@
|
|||
bind:this={timelineElement}
|
||||
id="virtual-timeline"
|
||||
class:invisible
|
||||
style:height={timelineManager.timelineHeight + 'px'}
|
||||
style:height={timelineManager.totalViewerHeight + 'px'}
|
||||
>
|
||||
<section
|
||||
use:resizeObserver={topSectionResizeObserver}
|
||||
|
|
@ -636,11 +607,11 @@
|
|||
{/each}
|
||||
<!-- spacer for leadout -->
|
||||
<div
|
||||
class="h-[60px]"
|
||||
style:height={timelineManager.bottomSectionHeight + 'px'}
|
||||
style:position="absolute"
|
||||
style:left="0"
|
||||
style:right="0"
|
||||
style:transform={`translate3d(0,${timelineManager.timelineHeight}px,0)`}
|
||||
style:transform={`translate3d(0,${timelineManager.topSectionHeight + timelineManager.assetsHeight}px,0)`}
|
||||
></div>
|
||||
</section>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import {
|
||||
alwaysLoadOriginalFile,
|
||||
autoPlayVideo,
|
||||
locale,
|
||||
loopVideo,
|
||||
playVideoThumbnailOnHover,
|
||||
|
|
@ -108,6 +109,13 @@
|
|||
bind:checked={$playVideoThumbnailOnHover}
|
||||
/>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<SettingSwitch
|
||||
title={$t('setting_video_viewer_auto_play_title')}
|
||||
subtitle={$t('setting_video_viewer_auto_play_subtitle')}
|
||||
bind:checked={$autoPlayVideo}
|
||||
/>
|
||||
</div>
|
||||
<div class="ms-4">
|
||||
<SettingSwitch title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ describe('TimelineManager', () => {
|
|||
});
|
||||
|
||||
it('calculates timeline height', () => {
|
||||
expect(timelineManager.timelineHeight).toBe(12_447.5);
|
||||
expect(timelineManager.totalViewerHeight).toBe(12_507.5);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,9 @@ export class TimelineManager {
|
|||
isInitialized = $state(false);
|
||||
months: MonthGroup[] = $state([]);
|
||||
topSectionHeight = $state(0);
|
||||
timelineHeight = $derived(this.months.reduce((accumulator, b) => accumulator + b.height, 0) + this.topSectionHeight);
|
||||
bottomSectionHeight = $state(60);
|
||||
assetsHeight = $derived(this.months.reduce((accumulator, b) => accumulator + b.height, 0));
|
||||
totalViewerHeight = $derived(this.topSectionHeight + this.assetsHeight + this.bottomSectionHeight);
|
||||
assetCount = $derived(this.months.reduce((accumulator, b) => accumulator + b.assetsCount, 0));
|
||||
|
||||
albumAssets: Set<string> = new SvelteSet();
|
||||
|
|
@ -62,6 +64,7 @@ export class TimelineManager {
|
|||
top: this.#scrollTop,
|
||||
bottom: this.#scrollTop + this.viewportHeight,
|
||||
}));
|
||||
limitedScroll = $derived(this.maxScrollPercent < 0.5);
|
||||
|
||||
initTask = new CancellableTask(
|
||||
() => {
|
||||
|
|
@ -97,8 +100,6 @@ export class TimelineManager {
|
|||
#updatingIntersections = false;
|
||||
#scrollableElement: HTMLElement | undefined = $state();
|
||||
|
||||
constructor() {}
|
||||
|
||||
setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: TimelineManagerLayoutOptions) {
|
||||
let changed = false;
|
||||
changed ||= this.#setHeaderHeight(headerHeight);
|
||||
|
|
@ -383,8 +384,10 @@ export class TimelineManager {
|
|||
updateGeometry(this, month, { invalidateHeight: changedWidth });
|
||||
}
|
||||
this.updateIntersections();
|
||||
if (changedWidth) {
|
||||
this.#createScrubberMonths();
|
||||
}
|
||||
}
|
||||
|
||||
#createScrubberMonths() {
|
||||
this.scrubberMonths = this.months.map((month) => ({
|
||||
|
|
@ -394,7 +397,7 @@ export class TimelineManager {
|
|||
title: month.monthGroupTitle,
|
||||
height: month.height,
|
||||
}));
|
||||
this.scrubberTimelineHeight = this.timelineHeight;
|
||||
this.scrubberTimelineHeight = this.totalViewerHeight;
|
||||
}
|
||||
|
||||
createLayoutOptions() {
|
||||
|
|
@ -408,6 +411,16 @@ export class TimelineManager {
|
|||
};
|
||||
}
|
||||
|
||||
get maxScrollPercent() {
|
||||
const totalHeight = this.totalViewerHeight;
|
||||
const max = (totalHeight - this.viewportHeight) / totalHeight;
|
||||
return max;
|
||||
}
|
||||
|
||||
get maxScroll() {
|
||||
return this.totalViewerHeight - this.viewportHeight;
|
||||
}
|
||||
|
||||
async loadMonthGroup(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise<void> {
|
||||
let cancelable = true;
|
||||
if (options) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { TimelineDate, TimelineDateTime } from '$lib/utils/timeline-util';
|
||||
import type { TimelineDate, TimelineDateTime, TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import type { AssetStackResponseDto, AssetVisibility } from '@immich/sdk';
|
||||
|
||||
export type ViewportTopMonth = TimelineYearMonth | undefined | 'lead-in' | 'lead-out';
|
||||
|
||||
export type AssetApiGetTimeBucketsRequest = Parameters<typeof import('@immich/sdk').getTimeBuckets>[0];
|
||||
|
||||
export type TimelineManagerOptions = Omit<AssetApiGetTimeBucketsRequest, 'size'> & {
|
||||
|
|
|
|||
|
|
@ -146,4 +146,6 @@ export const playVideoThumbnailOnHover = persisted<boolean>('play-video-thumbnai
|
|||
|
||||
export const loopVideo = persisted<boolean>('loop-video', true, {});
|
||||
|
||||
export const autoPlayVideo = persisted<boolean>('auto-play-video', true, {});
|
||||
|
||||
export const recentAlbumsDropdown = persisted<boolean>('recent-albums-open', true, {});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import type { TimelineAsset, ViewportTopMonth } from '$lib/managers/timeline-manager/types';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
|
||||
|
|
@ -24,7 +24,7 @@ export type TimelineDateTime = TimelineDate & {
|
|||
};
|
||||
|
||||
export type ScrubberListener = (scrubberData: {
|
||||
scrubberMonth: { year: number; month: number };
|
||||
scrubberMonth: ViewportTopMonth;
|
||||
overallScrollPercent: number;
|
||||
scrubberMonthScrollPercent: number;
|
||||
}) => void | Promise<void>;
|
||||
|
|
|
|||
|
|
@ -328,18 +328,19 @@
|
|||
}
|
||||
});
|
||||
|
||||
let timelineManager = new TimelineManager();
|
||||
|
||||
$effect(() => {
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
const options = $derived.by(() => {
|
||||
if (viewMode === AlbumPageViewMode.VIEW) {
|
||||
void timelineManager.updateOptions({ albumId, order: albumOrder });
|
||||
} else if (viewMode === AlbumPageViewMode.SELECT_ASSETS) {
|
||||
void timelineManager.updateOptions({
|
||||
return { albumId, order: albumOrder };
|
||||
}
|
||||
if (viewMode === AlbumPageViewMode.SELECT_ASSETS) {
|
||||
return {
|
||||
visibility: AssetVisibility.Timeline,
|
||||
withPartners: true,
|
||||
timelineAlbumId: albumId,
|
||||
});
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
|
||||
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0);
|
||||
|
|
@ -352,10 +353,7 @@
|
|||
handlePromiseError(activityManager.init(album.id));
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
activityManager.reset();
|
||||
timelineManager.destroy();
|
||||
});
|
||||
onDestroy(() => activityManager.reset());
|
||||
|
||||
let isOwned = $derived($user.id == album.ownerId);
|
||||
|
||||
|
|
@ -446,7 +444,8 @@
|
|||
<Timeline
|
||||
enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true}
|
||||
{album}
|
||||
{timelineManager}
|
||||
bind:timelineManager
|
||||
{options}
|
||||
assetInteraction={currentAssetIntersection}
|
||||
{isShared}
|
||||
{isSelectionMode}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
|
|
@ -27,9 +26,8 @@
|
|||
}
|
||||
|
||||
let { data }: Props = $props();
|
||||
const timelineManager = new TimelineManager();
|
||||
void timelineManager.updateOptions({ visibility: AssetVisibility.Archive });
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
const options = { visibility: AssetVisibility.Archive };
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
|
|
@ -49,7 +47,8 @@
|
|||
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
|
||||
<Timeline
|
||||
enableRouting={true}
|
||||
{timelineManager}
|
||||
bind:timelineManager
|
||||
{options}
|
||||
{assetInteraction}
|
||||
removeAction={AssetAction.UNARCHIVE}
|
||||
onEscape={handleEscape}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
|
|
@ -31,9 +30,8 @@
|
|||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const timelineManager = new TimelineManager();
|
||||
void timelineManager.updateOptions({ isFavorite: true, withStacked: true });
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
const options = { isFavorite: true, withStacked: true };
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
|
|
@ -54,7 +52,8 @@
|
|||
<Timeline
|
||||
enableRouting={true}
|
||||
withStacked={true}
|
||||
{timelineManager}
|
||||
bind:timelineManager
|
||||
{options}
|
||||
{assetInteraction}
|
||||
removeAction={AssetAction.UNFAVORITE}
|
||||
onEscape={handleEscape}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
import { AssetVisibility, lockAuthSession } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { mdiDotsVertical, mdiLockOutline } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
|
|
@ -27,9 +26,8 @@
|
|||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const timelineManager = new TimelineManager();
|
||||
void timelineManager.updateOptions({ visibility: AssetVisibility.Locked });
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
const options = { visibility: AssetVisibility.Locked };
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
|
|
@ -60,7 +58,8 @@
|
|||
|
||||
<Timeline
|
||||
enableRouting={true}
|
||||
{timelineManager}
|
||||
bind:timelineManager
|
||||
{options}
|
||||
{assetInteraction}
|
||||
onEscape={handleEscape}
|
||||
removeAction={AssetAction.SET_VISIBILITY_TIMELINE}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,9 @@
|
|||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import Timeline from '$lib/components/timeline/Timeline.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
import { mdiArrowLeft, mdiPlus } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
|
|
@ -22,16 +20,12 @@
|
|||
|
||||
let { data }: Props = $props();
|
||||
|
||||
const timelineManager = new TimelineManager();
|
||||
$effect(
|
||||
() =>
|
||||
void timelineManager.updateOptions({
|
||||
const options = $derived({
|
||||
userId: data.partner.id,
|
||||
visibility: AssetVisibility.Timeline,
|
||||
withStacked: true,
|
||||
}),
|
||||
);
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
});
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
const handleEscape = () => {
|
||||
|
|
@ -43,7 +37,7 @@
|
|||
</script>
|
||||
|
||||
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
|
||||
<Timeline enableRouting={true} {timelineManager} {assetInteraction} onEscape={handleEscape} />
|
||||
<Timeline enableRouting={true} {options} {assetInteraction} onEscape={handleEscape} />
|
||||
</main>
|
||||
|
||||
{#if assetInteraction.selectionActive}
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@
|
|||
mdiPlus,
|
||||
} from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
|
|
@ -76,10 +76,8 @@
|
|||
let numberOfAssets = $state(data.statistics.assets);
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
|
||||
const timelineManager = new TimelineManager();
|
||||
$effect(() => void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, personId: data.person.id }));
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
const options = $derived({ visibility: AssetVisibility.Timeline, personId: data.person.id });
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
|
||||
|
|
@ -388,7 +386,8 @@
|
|||
<Timeline
|
||||
enableRouting={true}
|
||||
{person}
|
||||
{timelineManager}
|
||||
bind:timelineManager
|
||||
{options}
|
||||
{assetInteraction}
|
||||
isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON}
|
||||
singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON}
|
||||
|
|
|
|||
|
|
@ -37,13 +37,12 @@
|
|||
import { AssetVisibility } from '@immich/sdk';
|
||||
|
||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||
const timelineManager = new TimelineManager();
|
||||
void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true });
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
const options = { visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true };
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
|
|
@ -91,7 +90,8 @@
|
|||
<UserPageLayout hideNavbar={assetInteraction.selectionActive} showUploadButton scrollbar={false}>
|
||||
<Timeline
|
||||
enableRouting={true}
|
||||
{timelineManager}
|
||||
bind:timelineManager
|
||||
{options}
|
||||
{assetInteraction}
|
||||
removeAction={AssetAction.ARCHIVE}
|
||||
onEscape={handleEscape}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
|
||||
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
|
|
@ -79,8 +78,6 @@
|
|||
});
|
||||
});
|
||||
|
||||
let timelineManager = new TimelineManager();
|
||||
|
||||
const onEscape = () => {
|
||||
if ($showAssetViewer) {
|
||||
return;
|
||||
|
|
@ -129,7 +126,6 @@
|
|||
};
|
||||
|
||||
const handleSetVisibility = (assetIds: string[]) => {
|
||||
timelineManager.removeAssets(assetIds);
|
||||
assetInteraction.clearMultiselect();
|
||||
onAssetDelete(assetIds);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
import { deleteTag, getAllTags, type TagResponseDto } from '@immich/sdk';
|
||||
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
||||
import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
|
|
@ -28,14 +27,13 @@
|
|||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
const timelineManager = new TimelineManager();
|
||||
$effect(() => void timelineManager.updateOptions({ deferInit: !tag, tagId: tag?.id }));
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
|
||||
let tags = $derived<TagResponseDto[]>(data.tags);
|
||||
const tree = $derived(TreeNode.fromTags(tags));
|
||||
const tag = $derived(tree.traverse(data.path));
|
||||
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
const options = $derived({ deferInit: !tag, tagId: tag?.id });
|
||||
|
||||
const handleNavigation = (tag: string) => navigateToView(joinPaths(data.path, tag));
|
||||
|
||||
const getLink = (path: string) => {
|
||||
|
|
@ -117,7 +115,13 @@
|
|||
|
||||
<section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar">
|
||||
{#if tag.hasAssets}
|
||||
<Timeline enableRouting={true} {timelineManager} {assetInteraction} removeAction={AssetAction.UNARCHIVE}>
|
||||
<Timeline
|
||||
enableRouting={true}
|
||||
bind:timelineManager
|
||||
{options}
|
||||
{assetInteraction}
|
||||
removeAction={AssetAction.UNARCHIVE}
|
||||
>
|
||||
{#snippet empty()}
|
||||
<TreeItemThumbnails items={tag.children} icon={mdiTag} onClick={handleNavigation} />
|
||||
{/snippet}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
import { emptyTrash, restoreTrash } from '@immich/sdk';
|
||||
import { Button, HStack, modalManager, Text } from '@immich/ui';
|
||||
import { mdiDeleteForeverOutline, mdiHistory } from '@mdi/js';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
|
|
@ -35,9 +34,8 @@
|
|||
handlePromiseError(goto(AppRoute.PHOTOS));
|
||||
}
|
||||
|
||||
const timelineManager = new TimelineManager();
|
||||
void timelineManager.updateOptions({ isTrashed: true });
|
||||
onDestroy(() => timelineManager.destroy());
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
const options = { isTrashed: true };
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
||||
|
|
@ -116,7 +114,7 @@
|
|||
</HStack>
|
||||
{/snippet}
|
||||
|
||||
<Timeline enableRouting={true} {timelineManager} {assetInteraction} onEscape={handleEscape}>
|
||||
<Timeline enableRouting={true} {options} {assetInteraction} onEscape={handleEscape}>
|
||||
<p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4">
|
||||
{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -30,13 +30,13 @@
|
|||
let location = $state<{ latitude: number; longitude: number }>({ latitude: 0, longitude: 0 });
|
||||
let locationUpdated = $state(false);
|
||||
|
||||
const timelineManager = new TimelineManager();
|
||||
void timelineManager.updateOptions({
|
||||
let timelineManager = $state<TimelineManager>() as TimelineManager;
|
||||
const options = {
|
||||
visibility: AssetVisibility.Timeline,
|
||||
withStacked: true,
|
||||
withPartners: true,
|
||||
withCoordinates: true,
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdate = async () => {
|
||||
const confirmed = await modalManager.show(GeolocationUpdateConfirmModal, {
|
||||
|
|
@ -188,7 +188,8 @@
|
|||
<Timeline
|
||||
isSelectionMode={true}
|
||||
enableRouting={true}
|
||||
{timelineManager}
|
||||
bind:timelineManager
|
||||
{options}
|
||||
{assetInteraction}
|
||||
removeAction={AssetAction.ARCHIVE}
|
||||
onEscape={handleEscape}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue