Merge branch 'main' into openvino-cpu-fix

This commit is contained in:
Aleksander Pejcic 2025-10-15 12:29:42 -07:00 committed by GitHub
commit 1b81b9b2fd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 264 additions and 245 deletions

View file

@ -333,7 +333,7 @@
"transcoding_max_b_frames": "Maximum B-frames", "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_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": "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": "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_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", "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": "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_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": "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": "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_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", "transcoding_tone_mapping": "Tone-mapping",
@ -1807,6 +1807,8 @@
"setting_notifications_subtitle": "Adjust your notification preferences", "setting_notifications_subtitle": "Adjust your notification preferences",
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress", "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_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_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", "setting_video_viewer_original_video_title": "Force original video",

View file

@ -133,11 +133,15 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFileSystemSynchronizedRootGroup section */
B231F52D2E93A44A00BC45D1 /* Core */ = { B231F52D2E93A44A00BC45D1 /* Core */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Core; path = Core;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup; isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync; path = Sync;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@ -526,14 +530,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Copy Pods Resources"; name = "[CP] Copy Pods Resources";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@ -562,14 +562,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; 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_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230; CURRENT_PROJECT_VERSION = 231;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@ -862,7 +858,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230; CURRENT_PROJECT_VERSION = 231;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@ -892,7 +888,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230; CURRENT_PROJECT_VERSION = 231;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@ -926,7 +922,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230; CURRENT_PROJECT_VERSION = 231;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@ -969,7 +965,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230; CURRENT_PROJECT_VERSION = 231;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@ -1009,7 +1005,7 @@
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230; CURRENT_PROJECT_VERSION = 231;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17; GCC_C_LANGUAGE_STANDARD = gnu17;
@ -1048,7 +1044,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230; CURRENT_PROJECT_VERSION = 231;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -1092,7 +1088,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230; CURRENT_PROJECT_VERSION = 231;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;
@ -1133,7 +1129,7 @@
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 230; CURRENT_PROJECT_VERSION = 231;
CUSTOM_GROUP_ID = group.app.immich.share; CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES;

View file

@ -80,7 +80,7 @@
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>2.0.1</string> <string>2.1.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
@ -107,7 +107,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>230</string> <string>231</string>
<key>FLTEnableImpeller</key> <key>FLTEnableImpeller</key>
<true/> <true/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>

View file

@ -103,7 +103,9 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
let options = PHFetchOptions() let options = PHFetchOptions()
options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)] options.sortDescriptors = [NSSortDescriptor(key: "modificationDate", ascending: false)]
options.includeHiddenAssets = 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 let isCloud = album.assetCollectionSubtype == .albumCloudShared || album.assetCollectionSubtype == .albumMyPhotoStream
var domainAlbum = PlatformAlbum( var domainAlbum = PlatformAlbum(
@ -201,7 +203,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
let options = PHFetchOptions() let options = PHFetchOptions()
options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id)) options.predicate = NSPredicate(format: "localIdentifier IN %@", assets.map(\.id))
options.includeHiddenAssets = false options.includeHiddenAssets = false
let result = PHAsset.fetchAssets(in: album, options: options) let result = self.getAssetsFromAlbum(in: album, options: options)
result.enumerateObjects { (asset, _, _) in result.enumerateObjects { (asset, _, _) in
albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier) albumAssets[asset.localIdentifier, default: []].append(album.localIdentifier)
} }
@ -219,7 +221,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
var ids: [String] = [] var ids: [String] = []
let options = PHFetchOptions() let options = PHFetchOptions()
options.includeHiddenAssets = false options.includeHiddenAssets = false
let assets = PHAsset.fetchAssets(in: album, options: options) let assets = getAssetsFromAlbum(in: album, options: options)
assets.enumerateObjects { (asset, _, _) in assets.enumerateObjects { (asset, _, _) in
ids.append(asset.localIdentifier) ids.append(asset.localIdentifier)
} }
@ -236,7 +238,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
let options = PHFetchOptions() let options = PHFetchOptions()
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date)
options.includeHiddenAssets = false options.includeHiddenAssets = false
let assets = PHAsset.fetchAssets(in: album, options: options) let assets = getAssetsFromAlbum(in: album, options: options)
return Int64(assets.count) return Int64(assets.count)
} }
@ -253,7 +255,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
options.predicate = NSPredicate(format: "creationDate > %@ OR modificationDate > %@", date, date) 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) { if(result.count == 0) {
return [] return []
} }
@ -375,4 +377,13 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
PHAssetResourceManager.default().cancelDataRequest(requestId) 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)
}
}
} }

View file

@ -6,6 +6,7 @@ enum Setting<T> {
showStorageIndicator<bool>(StoreKey.storageIndicator, true), showStorageIndicator<bool>(StoreKey.storageIndicator, true),
loadOriginal<bool>(StoreKey.loadOriginal, false), loadOriginal<bool>(StoreKey.loadOriginal, false),
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false), loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, false),
autoPlayVideo<bool>(StoreKey.autoPlayVideo, true),
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false), preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false), advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
enableBackup<bool>(StoreKey.enableBackup, false); enableBackup<bool>(StoreKey.enableBackup, false);

View file

@ -70,6 +70,8 @@ enum StoreKey<T> {
// Read-only Mode settings // Read-only Mode settings
readonlyModeEnabled<bool>._(138), readonlyModeEnabled<bool>._(138),
autoPlayVideo<bool>._(139),
// Experimental stuff // Experimental stuff
photoManagerCustomFilter<bool>._(1000), photoManagerCustomFilter<bool>._(1000),
betaPromptShown<bool>._(1001), betaPromptShown<bool>._(1001),

View file

@ -190,7 +190,10 @@ class NativeVideoViewerPage extends HookConsumerWidget {
isVideoReady.value = true; isVideoReady.value = true;
try { try {
await videoController.play(); final autoPlayVideo = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.autoPlayVideo);
if (autoPlayVideo) {
await videoController.play();
}
await videoController.setVolume(0.9); await videoController.setVolume(0.9);
} catch (error) { } catch (error) {
log.severe('Error playing video: $error'); log.severe('Error playing video: $error');

View file

@ -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/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.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/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/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
@ -218,7 +219,10 @@ class NativeVideoViewer extends HookConsumerWidget {
} }
try { try {
await videoController.play(); final autoPlayVideo = AppSetting.get(Setting.autoPlayVideo);
if (autoPlayVideo) {
await videoController.play();
}
await videoController.setVolume(0.9); await videoController.setVolume(0.9);
} catch (error) { } catch (error) {
log.severe('Error playing video: $error'); log.severe('Error playing video: $error');

View file

@ -34,6 +34,7 @@ enum AppSettingsEnum<T> {
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false), preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true), loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, "loadOriginalVideo", false), loadOriginalVideo<bool>(StoreKey.loadOriginalVideo, "loadOriginalVideo", false),
autoPlayVideo<bool>(StoreKey.autoPlayVideo, "autoPlayVideo", true),
mapThemeMode<int>(StoreKey.mapThemeMode, null, 0), mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false), mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false), mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),

View file

@ -14,11 +14,18 @@ class VideoViewerSettings extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final useLoopVideo = useAppSettingsState(AppSettingsEnum.loopVideo); final useLoopVideo = useAppSettingsState(AppSettingsEnum.loopVideo);
final useOriginalVideo = useAppSettingsState(AppSettingsEnum.loadOriginalVideo); final useOriginalVideo = useAppSettingsState(AppSettingsEnum.loadOriginalVideo);
final useAutoPlayVideo = useAppSettingsState(AppSettingsEnum.autoPlayVideo);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
SettingsSubTitle(title: "videos".tr()), 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( SettingsSwitchListTile(
valueNotifier: useLoopVideo, valueNotifier: useLoopVideo,
title: "setting_video_viewer_looping_title".tr(), title: "setting_video_viewer_looping_title".tr(),

View file

@ -17,7 +17,6 @@
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk'; import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
import { IconButton } from '@immich/ui'; import { IconButton } from '@immich/ui';
import { mdiDownload, mdiFileImagePlusOutline } from '@mdi/js'; import { mdiDownload, mdiFileImagePlusOutline } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import ControlAppBar from '../shared-components/control-app-bar.svelte'; import ControlAppBar from '../shared-components/control-app-bar.svelte';
import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte'; import ImmichLogoSmallLink from '../shared-components/immich-logo-small-link.svelte';
@ -35,9 +34,8 @@
let { isViewing: showAssetViewer } = assetViewingStore; let { isViewing: showAssetViewer } = assetViewingStore;
const timelineManager = new TimelineManager(); const options = $derived({ albumId: album.id, order: album.order });
$effect(() => void timelineManager.updateOptions({ albumId: album.id, order: album.order })); let timelineManager = $state<TimelineManager>() as TimelineManager;
onDestroy(() => timelineManager.destroy());
const assetInteraction = new AssetInteraction(); 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)"> <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"> <section class="pt-8 md:pt-24 px-2 md:px-0">
<!-- ALBUM TITLE --> <!-- ALBUM TITLE -->
<h1 class="text-2xl md:text-4xl lg:text-6xl text-primary outline-none transition-all"> <h1 class="text-2xl md:text-4xl lg:text-6xl text-primary outline-none transition-all">

View file

@ -4,7 +4,12 @@
import { assetViewerFadeDuration } from '$lib/constants'; import { assetViewerFadeDuration } from '$lib/constants';
import { castManager } from '$lib/managers/cast-manager.svelte'; import { castManager } from '$lib/managers/cast-manager.svelte';
import { isFaceEditMode } from '$lib/stores/face-edit.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 { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
import { AssetMediaSize } from '@immich/sdk'; import { AssetMediaSize } from '@immich/sdk';
import { LoadingSpinner } from '@immich/ui'; import { LoadingSpinner } from '@immich/ui';
@ -125,7 +130,7 @@
<video <video
bind:this={videoPlayer} bind:this={videoPlayer}
loop={$loopVideoPreference && loopVideo} loop={$loopVideoPreference && loopVideo}
autoplay autoplay={$autoPlayVideo}
playsinline playsinline
controls controls
class="h-full object-contain" class="h-full object-contain"

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { assetViewerFadeDuration } from '$lib/constants'; import { assetViewerFadeDuration } from '$lib/constants';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { autoPlayVideo } from '$lib/stores/preferences.store';
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils'; import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
import { AssetMediaSize } from '@immich/sdk'; import { AssetMediaSize } from '@immich/sdk';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -27,7 +28,7 @@
<div class="h-full w-full bg-pink-9000" transition:fade={{ duration: assetViewerFadeDuration }}> <div class="h-full w-full bg-pink-9000" transition:fade={{ duration: assetViewerFadeDuration }}>
<video <video
bind:this={videoPlayer} bind:this={videoPlayer}
autoplay autoplay={$autoPlayVideo}
playsinline playsinline
class="h-full w-full rounded-2xl object-contain transition-all" class="h-full w-full rounded-2xl object-contain transition-all"
src={getAssetPlaybackUrl({ id: asset.id })} src={getAssetPlaybackUrl({ id: asset.id })}

View file

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; 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 { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { getTabbable } from '$lib/utils/focus-util'; 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 { Icon } from '@immich/ui';
import { mdiPlay } from '@mdi/js'; import { mdiPlay } from '@mdi/js';
import { clamp } from 'lodash-es'; 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 */ /** The percentage of scroll through the month that is currently intersecting the top boundary of the viewport */
viewportTopMonthScrollPercent?: number; viewportTopMonthScrollPercent?: number;
/** The year/month of the timeline month at the top of the viewport */ /** The year/month of the timeline month at the top of the viewport */
viewportTopMonth?: TimelineYearMonth; viewportTopMonth?: ViewportTopMonth;
/** Indicates whether the viewport is currently in the lead-out section (after all months) */
isInLeadOutSection?: boolean;
/** Width of the scrubber component in pixels (bindable for parent component margin adjustments) */ /** Width of the scrubber component in pixels (bindable for parent component margin adjustments) */
scrubberWidth?: number; scrubberWidth?: number;
/** Callback fired when user interacts with the scrubber to navigate */ /** Callback fired when user interacts with the scrubber to navigate */
@ -47,7 +46,6 @@
timelineScrollPercent = 0, timelineScrollPercent = 0,
viewportTopMonthScrollPercent = 0, viewportTopMonthScrollPercent = 0,
viewportTopMonth = undefined, viewportTopMonth = undefined,
isInLeadOutSection = false,
onScrub = undefined, onScrub = undefined,
onScrubKeyDown = undefined, onScrubKeyDown = undefined,
startScrub = undefined, startScrub = undefined,
@ -94,11 +92,19 @@
}); });
const toScrollFromMonthGroupPercentage = ( const toScrollFromMonthGroupPercentage = (
scrubberMonth: { year: number; month: number } | undefined, scrubberMonth: ViewportTopMonth,
scrubberMonthPercent: number, scrubberMonthPercent: number,
scrubOverallPercent: 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 offset = relativeTopOffset;
let match = false; let match = false;
for (const segment of segments) { for (const segment of segments) {
@ -113,23 +119,16 @@
offset += scrubberMonthPercent * relativeBottomOffset; offset += scrubberMonthPercent * relativeBottomOffset;
} }
return offset; return offset;
} else if (isInLeadOutSection) {
let offset = relativeTopOffset;
for (const segment of segments) {
offset += segment.height;
}
offset += scrubOverallPercent * relativeBottomOffset;
return offset;
} else { } else {
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM)); return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
} }
}; };
let scrollY = $derived( const scrollY = $derived(
toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent), toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
); );
let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset); const timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight);
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight)); const relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight)); const relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
type Segment = { type Segment = {
count: number; count: number;
@ -173,14 +172,13 @@
segment.hasLabel = true; segment.hasLabel = true;
previousLabeledSegment = segment; previousLabeledSegment = segment;
} }
if (i !== 1 && segment.height > 5 && dotHeight > MIN_DOT_DISTANCE) { if (segment.height > 5 && dotHeight > MIN_DOT_DISTANCE) {
segment.hasDot = true; segment.hasDot = true;
dotHeight = 0; dotHeight = 0;
} }
height += segment.height; height += segment.height;
dotHeight += segment.height;
} }
dotHeight += segment.height;
segments.push(segment); segments.push(segment);
} }
@ -197,7 +195,13 @@
} }
return activeSegment?.dataset.label; 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) { if (!activeSegment?.dataset.segmentYearMonth) {
return undefined; return undefined;
} }
@ -215,7 +219,22 @@
} }
return null; 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[]) => { const findElementBestY = (elements: Element[], y: number, ...ids: string[]) => {
if (ids.length === 0) { if (ids.length === 0) {
@ -308,38 +327,23 @@
isHoverOnPaddingTop = isOnPaddingTop; isHoverOnPaddingTop = isOnPaddingTop;
isHoverOnPaddingBottom = isOnPaddingBottom; isHoverOnPaddingBottom = isOnPaddingBottom;
const scrollPercent = toTimelineY(hoverY); const scrubData = {
scrubberMonth: segmentDate,
overallScrollPercent: toTimelineY(hoverY),
scrubberMonthScrollPercent: monthGroupPercentY,
};
if (wasDragging === false && isDragging) { if (wasDragging === false && isDragging) {
void startScrub?.({ void startScrub?.(scrubData);
scrubberMonth: segmentDate!, void onScrub?.(scrubData);
overallScrollPercent: scrollPercent,
scrubberMonthScrollPercent: monthGroupPercentY,
});
void onScrub?.({
scrubberMonth: segmentDate!,
overallScrollPercent: scrollPercent,
scrubberMonthScrollPercent: monthGroupPercentY,
});
} }
if (wasDragging && !isDragging) { if (wasDragging && !isDragging) {
void stopScrub?.({ void stopScrub?.(scrubData);
scrubberMonth: segmentDate!,
overallScrollPercent: scrollPercent,
scrubberMonthScrollPercent: monthGroupPercentY,
});
return; return;
} }
if (!isDragging) { if (!isDragging) {
return; return;
} }
void onScrub?.(scrubData);
void onScrub?.({
scrubberMonth: segmentDate!,
overallScrollPercent: scrollPercent,
scrubberMonthScrollPercent: monthGroupPercentY,
});
}; };
const getTouch = (event: TouchEvent) => { const getTouch = (event: TouchEvent) => {
if (event.touches.length === 1) { if (event.touches.length === 1) {
@ -559,13 +563,8 @@
class="relative" class="relative"
style:height={relativeTopOffset + 'px'} style:height={relativeTopOffset + 'px'}
data-id="lead-in" data-id="lead-in"
data-segment-year-month={segments.at(0)?.year + '-' + segments.at(0)?.month}
data-label={segments.at(0)?.dateFormatted} data-label={segments.at(0)?.dateFormatted}
> ></div>
{#if relativeTopOffset > 6}
<div class="absolute end-3 h-[4px] w-[4px] rounded-full bg-gray-300"></div>
{/if}
</div>
<!-- Time Segment --> <!-- Time Segment -->
{#each segments as segment (segment.year + '-' + segment.month)} {#each segments as segment (segment.year + '-' + segment.month)}
<div <div
@ -587,5 +586,10 @@
{/if} {/if}
</div> </div>
{/each} {/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> </div>

View file

@ -12,17 +12,17 @@
import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte'; import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte'; import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.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 { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte'; import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { navigate } from '$lib/utils/navigation'; 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 { type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { onMount, type Snippet } from 'svelte'; import { onDestroy, onMount, type Snippet } from 'svelte';
import type { UpdatePayload } from 'vite'; import type { UpdatePayload } from 'vite';
import TimelineDateGroup from './TimelineDateGroup.svelte'; import TimelineDateGroup from './TimelineDateGroup.svelte';
@ -33,7 +33,8 @@
`AssetViewingStore.gridScrollTarget` and load and scroll to the asset specified, and `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 */ additionally, update the page location/url with the asset as the timeline is scrolled */
enableRouting: boolean; enableRouting: boolean;
timelineManager: TimelineManager; timelineManager?: TimelineManager;
options?: TimelineManagerOptions;
assetInteraction: AssetInteraction; assetInteraction: AssetInteraction;
removeAction?: removeAction?:
| AssetAction.UNARCHIVE | AssetAction.UNARCHIVE
@ -71,6 +72,7 @@
singleSelect = false, singleSelect = false,
enableRouting, enableRouting,
timelineManager = $bindable(), timelineManager = $bindable(),
options,
assetInteraction, assetInteraction,
removeAction = null, removeAction = null,
withStacked = false, withStacked = false,
@ -87,6 +89,10 @@
onThumbnailClick, onThumbnailClick,
}: Props = $props(); }: Props = $props();
timelineManager = new TimelineManager();
onDestroy(() => timelineManager.destroy());
$effect(() => options && void timelineManager.updateOptions(options));
let { isViewing: showAssetViewer, asset: viewingAsset, gridScrollTarget } = assetViewingStore; let { isViewing: showAssetViewer, asset: viewingAsset, gridScrollTarget } = assetViewingStore;
let scrollableElement: HTMLElement | undefined = $state(); let scrollableElement: HTMLElement | undefined = $state();
@ -97,16 +103,11 @@
// Note: There may be multiple months visible within the viewport at any given time. // Note: There may be multiple months visible within the viewport at any given time.
let viewportTopMonthScrollPercent = $state(0); let viewportTopMonthScrollPercent = $state(0);
// The timeline month intersecting the top position of the viewport // 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) // Overall scroll percentage through the entire timeline (0-1)
let timelineScrollPercent: number = $state(0); let timelineScrollPercent: number = $state(0);
let scrubberWidth = $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 isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
const maxMd = $derived(mobileDevice.maxMd); const maxMd = $derived(mobileDevice.maxMd);
const usingMobileDevice = $derived(mobileDevice.pointerCoarse); 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); const updateIsScrolling = () => (timelineManager.scrolling = true);
// note: don't throttle, debounch, or otherwise do this function async - it causes flicker // note: don't throttle, debounch, or otherwise do this function async - it causes flicker
@ -230,41 +224,36 @@
} }
}); });
const getMaxScrollPercent = () => { const scrollToSegmentPercentage = (segmentTop: number, segmentHeight: number, monthGroupScrollPercent: number) => {
const totalHeight = timelineManager.timelineHeight + bottomSectionHeight + timelineManager.topSectionHeight; const topOffset = segmentTop;
return (totalHeight - timelineManager.viewportHeight) / totalHeight; const maxScrollPercent = timelineManager.maxScrollPercent;
}; const delta = segmentHeight * monthGroupScrollPercent;
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 scrollToTop = (topOffset + delta) * maxScrollPercent; const scrollToTop = (topOffset + delta) * maxScrollPercent;
timelineManager.scrollTo(scrollToTop); timelineManager.scrollTo(scrollToTop);
}; };
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker // 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 // this function scrolls the timeline to the specified month group and offset, based on scrubber interaction
const onScrub: ScrubberListener = (scrubberData) => { const onScrub: ScrubberListener = (scrubberData) => {
const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent } = 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 // edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
const maxScroll = getMaxScroll(); const maxScroll = timelineManager.maxScrollPercent;
const offset = maxScroll * overallScrollPercent; const offset = maxScroll * overallScrollPercent * timelineManager.totalViewerHeight;
timelineManager.scrollTo(offset); timelineManager.scrollTo(offset);
} else if (leadIn) {
scrollToSegmentPercentage(0, timelineManager.topSectionHeight, scrubberMonthScrollPercent);
} else if (leadOut) {
scrollToSegmentPercentage(
timelineManager.topSectionHeight + timelineManager.assetsHeight,
timelineManager.bottomSectionHeight,
scrubberMonthScrollPercent,
);
} else { } else {
const monthGroup = timelineManager.months.find( const monthGroup = timelineManager.months.find(
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month, ({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
@ -272,50 +261,41 @@
if (!monthGroup) { if (!monthGroup) {
return; return;
} }
scrollToMonthGroupAndOffset(monthGroup, scrubberMonthScrollPercent); scrollToSegmentPercentage(monthGroup.top, monthGroup.height, scrubberMonthScrollPercent);
} }
}; };
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker // note: don't throttle, debounch, or otherwise make this function async - it causes flicker
const handleTimelineScroll = () => { const handleTimelineScroll = () => {
isInLeadOutSection = false;
if (!scrollableElement) { if (!scrollableElement) {
return; 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 // edge case - scroll limited due to size of content, must adjust - use the overall percent instead
const maxScroll = getMaxScroll(); const maxScroll = timelineManager.maxScroll;
timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll);
timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll);
viewportTopMonth = undefined; viewportTopMonth = undefined;
viewportTopMonthScrollPercent = 0; viewportTopMonthScrollPercent = 0;
} else { } else {
timelineScrollPercent = 0;
let top = scrollableElement.scrollTop; let top = scrollableElement.scrollTop;
if (top < timelineManager.topSectionHeight) { let maxScrollPercent = timelineManager.maxScrollPercent;
// 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;
const monthsLength = timelineManager.months.length; const monthsLength = timelineManager.months.length;
for (let i = -1; i < monthsLength + 1; i++) { for (let i = -1; i < monthsLength + 1; i++) {
let monthGroup: TimelineYearMonth | undefined; let monthGroup: ViewportTopMonth;
let monthGroupHeight = 0; let monthGroupHeight = 0;
if (i === -1) { if (i === -1) {
// lead-in // lead-in
monthGroup = 'lead-in';
monthGroupHeight = timelineManager.topSectionHeight; monthGroupHeight = timelineManager.topSectionHeight;
} else if (i === monthsLength) { } else if (i === monthsLength) {
// lead-out // lead-out
monthGroupHeight = bottomSectionHeight; monthGroup = 'lead-out';
monthGroupHeight = timelineManager.bottomSectionHeight;
} else { } else {
monthGroup = timelineManager.months[i].yearMonth; monthGroup = timelineManager.months[i].yearMonth;
monthGroupHeight = timelineManager.months[i].height; monthGroupHeight = timelineManager.months[i].height;
@ -334,18 +314,10 @@
viewportTopMonth = timelineManager.months[i + 1].yearMonth; viewportTopMonth = timelineManager.months[i + 1].yearMonth;
viewportTopMonthScrollPercent = 0; viewportTopMonthScrollPercent = 0;
} }
found = true;
break; break;
} }
top = next; top = next;
} }
if (!found) {
isInLeadOutSection = true;
viewportTopMonth = undefined;
viewportTopMonthScrollPercent = 0;
timelineScrollPercent = 1;
}
} }
}; };
@ -525,7 +497,7 @@
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} /> <svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
<HotModuleReload onAfterUpdate={handleAfterUpdate} onBeforeUpdate={handleBeforeUpdate} /> <HotModuleReload onAfterUpdate={handleAfterUpdate} onBeforeUpdate={() => timelineManager.destroy()} />
<TimelineKeyboardActions <TimelineKeyboardActions
scrollToAsset={(asset) => scrollToAsset(asset) ?? false} scrollToAsset={(asset) => scrollToAsset(asset) ?? false}
@ -540,8 +512,7 @@
{timelineManager} {timelineManager}
height={timelineManager.viewportHeight} height={timelineManager.viewportHeight}
timelineTopOffset={timelineManager.topSectionHeight} timelineTopOffset={timelineManager.topSectionHeight}
timelineBottomOffset={bottomSectionHeight} timelineBottomOffset={timelineManager.bottomSectionHeight}
{isInLeadOutSection}
{timelineScrollPercent} {timelineScrollPercent}
{viewportTopMonthScrollPercent} {viewportTopMonthScrollPercent}
{viewportTopMonth} {viewportTopMonth}
@ -580,7 +551,7 @@
bind:this={timelineElement} bind:this={timelineElement}
id="virtual-timeline" id="virtual-timeline"
class:invisible class:invisible
style:height={timelineManager.timelineHeight + 'px'} style:height={timelineManager.totalViewerHeight + 'px'}
> >
<section <section
use:resizeObserver={topSectionResizeObserver} use:resizeObserver={topSectionResizeObserver}
@ -636,11 +607,11 @@
{/each} {/each}
<!-- spacer for leadout --> <!-- spacer for leadout -->
<div <div
class="h-[60px]" style:height={timelineManager.bottomSectionHeight + 'px'}
style:position="absolute" style:position="absolute"
style:left="0" style:left="0"
style:right="0" style:right="0"
style:transform={`translate3d(0,${timelineManager.timelineHeight}px,0)`} style:transform={`translate3d(0,${timelineManager.topSectionHeight + timelineManager.assetsHeight}px,0)`}
></div> ></div>
</section> </section>
</section> </section>

View file

@ -7,6 +7,7 @@
import { themeManager } from '$lib/managers/theme-manager.svelte'; import { themeManager } from '$lib/managers/theme-manager.svelte';
import { import {
alwaysLoadOriginalFile, alwaysLoadOriginalFile,
autoPlayVideo,
locale, locale,
loopVideo, loopVideo,
playVideoThumbnailOnHover, playVideoThumbnailOnHover,
@ -108,6 +109,13 @@
bind:checked={$playVideoThumbnailOnHover} bind:checked={$playVideoThumbnailOnHover}
/> />
</div> </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"> <div class="ms-4">
<SettingSwitch title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} /> <SettingSwitch title={$t('loop_videos')} subtitle={$t('loop_videos_description')} bind:checked={$loopVideo} />
</div> </div>

View file

@ -90,7 +90,7 @@ describe('TimelineManager', () => {
}); });
it('calculates timeline height', () => { it('calculates timeline height', () => {
expect(timelineManager.timelineHeight).toBe(12_447.5); expect(timelineManager.totalViewerHeight).toBe(12_507.5);
}); });
}); });

View file

@ -48,7 +48,9 @@ export class TimelineManager {
isInitialized = $state(false); isInitialized = $state(false);
months: MonthGroup[] = $state([]); months: MonthGroup[] = $state([]);
topSectionHeight = $state(0); 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)); assetCount = $derived(this.months.reduce((accumulator, b) => accumulator + b.assetsCount, 0));
albumAssets: Set<string> = new SvelteSet(); albumAssets: Set<string> = new SvelteSet();
@ -62,6 +64,7 @@ export class TimelineManager {
top: this.#scrollTop, top: this.#scrollTop,
bottom: this.#scrollTop + this.viewportHeight, bottom: this.#scrollTop + this.viewportHeight,
})); }));
limitedScroll = $derived(this.maxScrollPercent < 0.5);
initTask = new CancellableTask( initTask = new CancellableTask(
() => { () => {
@ -97,8 +100,6 @@ export class TimelineManager {
#updatingIntersections = false; #updatingIntersections = false;
#scrollableElement: HTMLElement | undefined = $state(); #scrollableElement: HTMLElement | undefined = $state();
constructor() {}
setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: TimelineManagerLayoutOptions) { setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: TimelineManagerLayoutOptions) {
let changed = false; let changed = false;
changed ||= this.#setHeaderHeight(headerHeight); changed ||= this.#setHeaderHeight(headerHeight);
@ -383,7 +384,9 @@ export class TimelineManager {
updateGeometry(this, month, { invalidateHeight: changedWidth }); updateGeometry(this, month, { invalidateHeight: changedWidth });
} }
this.updateIntersections(); this.updateIntersections();
this.#createScrubberMonths(); if (changedWidth) {
this.#createScrubberMonths();
}
} }
#createScrubberMonths() { #createScrubberMonths() {
@ -394,7 +397,7 @@ export class TimelineManager {
title: month.monthGroupTitle, title: month.monthGroupTitle,
height: month.height, height: month.height,
})); }));
this.scrubberTimelineHeight = this.timelineHeight; this.scrubberTimelineHeight = this.totalViewerHeight;
} }
createLayoutOptions() { 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> { async loadMonthGroup(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise<void> {
let cancelable = true; let cancelable = true;
if (options) { if (options) {

View file

@ -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'; 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 AssetApiGetTimeBucketsRequest = Parameters<typeof import('@immich/sdk').getTimeBuckets>[0];
export type TimelineManagerOptions = Omit<AssetApiGetTimeBucketsRequest, 'size'> & { export type TimelineManagerOptions = Omit<AssetApiGetTimeBucketsRequest, 'size'> & {

View file

@ -146,4 +146,6 @@ export const playVideoThumbnailOnHover = persisted<boolean>('play-video-thumbnai
export const loopVideo = persisted<boolean>('loop-video', true, {}); 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, {}); export const recentAlbumsDropdown = persisted<boolean>('recent-albums-open', true, {});

View file

@ -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 { locale } from '$lib/stores/preferences.store';
import { getAssetRatio } from '$lib/utils/asset-utils'; import { getAssetRatio } from '$lib/utils/asset-utils';
import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
@ -24,7 +24,7 @@ export type TimelineDateTime = TimelineDate & {
}; };
export type ScrubberListener = (scrubberData: { export type ScrubberListener = (scrubberData: {
scrubberMonth: { year: number; month: number }; scrubberMonth: ViewportTopMonth;
overallScrollPercent: number; overallScrollPercent: number;
scrubberMonthScrollPercent: number; scrubberMonthScrollPercent: number;
}) => void | Promise<void>; }) => void | Promise<void>;

View file

@ -328,18 +328,19 @@
} }
}); });
let timelineManager = new TimelineManager(); let timelineManager = $state<TimelineManager>() as TimelineManager;
const options = $derived.by(() => {
$effect(() => {
if (viewMode === AlbumPageViewMode.VIEW) { if (viewMode === AlbumPageViewMode.VIEW) {
void timelineManager.updateOptions({ albumId, order: albumOrder }); return { albumId, order: albumOrder };
} else if (viewMode === AlbumPageViewMode.SELECT_ASSETS) { }
void timelineManager.updateOptions({ if (viewMode === AlbumPageViewMode.SELECT_ASSETS) {
return {
visibility: AssetVisibility.Timeline, visibility: AssetVisibility.Timeline,
withPartners: true, withPartners: true,
timelineAlbumId: albumId, timelineAlbumId: albumId,
}); };
} }
return {};
}); });
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0); const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0);
@ -352,10 +353,7 @@
handlePromiseError(activityManager.init(album.id)); handlePromiseError(activityManager.init(album.id));
}); });
onDestroy(() => { onDestroy(() => activityManager.reset());
activityManager.reset();
timelineManager.destroy();
});
let isOwned = $derived($user.id == album.ownerId); let isOwned = $derived($user.id == album.ownerId);
@ -446,7 +444,8 @@
<Timeline <Timeline
enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true} enableRouting={viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : true}
{album} {album}
{timelineManager} bind:timelineManager
{options}
assetInteraction={currentAssetIntersection} assetInteraction={currentAssetIntersection}
{isShared} {isShared}
{isSelectionMode} {isSelectionMode}

View file

@ -18,7 +18,6 @@
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility } from '@immich/sdk';
import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { mdiDotsVertical, mdiPlus } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -27,9 +26,8 @@
} }
let { data }: Props = $props(); let { data }: Props = $props();
const timelineManager = new TimelineManager(); let timelineManager = $state<TimelineManager>() as TimelineManager;
void timelineManager.updateOptions({ visibility: AssetVisibility.Archive }); const options = { visibility: AssetVisibility.Archive };
onDestroy(() => timelineManager.destroy());
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
@ -49,7 +47,8 @@
<UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}> <UserPageLayout hideNavbar={assetInteraction.selectionActive} title={data.meta.title} scrollbar={false}>
<Timeline <Timeline
enableRouting={true} enableRouting={true}
{timelineManager} bind:timelineManager
{options}
{assetInteraction} {assetInteraction}
removeAction={AssetAction.UNARCHIVE} removeAction={AssetAction.UNARCHIVE}
onEscape={handleEscape} onEscape={handleEscape}

View file

@ -21,7 +21,6 @@
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { mdiDotsVertical, mdiPlus } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -31,9 +30,8 @@
let { data }: Props = $props(); let { data }: Props = $props();
const timelineManager = new TimelineManager(); let timelineManager = $state<TimelineManager>() as TimelineManager;
void timelineManager.updateOptions({ isFavorite: true, withStacked: true }); const options = { isFavorite: true, withStacked: true };
onDestroy(() => timelineManager.destroy());
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
@ -54,7 +52,8 @@
<Timeline <Timeline
enableRouting={true} enableRouting={true}
withStacked={true} withStacked={true}
{timelineManager} bind:timelineManager
{options}
{assetInteraction} {assetInteraction}
removeAction={AssetAction.UNFAVORITE} removeAction={AssetAction.UNFAVORITE}
onEscape={handleEscape} onEscape={handleEscape}

View file

@ -17,7 +17,6 @@
import { AssetVisibility, lockAuthSession } from '@immich/sdk'; import { AssetVisibility, lockAuthSession } from '@immich/sdk';
import { Button } from '@immich/ui'; import { Button } from '@immich/ui';
import { mdiDotsVertical, mdiLockOutline } from '@mdi/js'; import { mdiDotsVertical, mdiLockOutline } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -27,9 +26,8 @@
let { data }: Props = $props(); let { data }: Props = $props();
const timelineManager = new TimelineManager(); let timelineManager = $state<TimelineManager>() as TimelineManager;
void timelineManager.updateOptions({ visibility: AssetVisibility.Locked }); const options = { visibility: AssetVisibility.Locked };
onDestroy(() => timelineManager.destroy());
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
@ -60,7 +58,8 @@
<Timeline <Timeline
enableRouting={true} enableRouting={true}
{timelineManager} bind:timelineManager
{options}
{assetInteraction} {assetInteraction}
onEscape={handleEscape} onEscape={handleEscape}
removeAction={AssetAction.SET_VISIBILITY_TIMELINE} removeAction={AssetAction.SET_VISIBILITY_TIMELINE}

View file

@ -8,11 +8,9 @@
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
import Timeline from '$lib/components/timeline/Timeline.svelte'; import Timeline from '$lib/components/timeline/Timeline.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility } from '@immich/sdk';
import { mdiArrowLeft, mdiPlus } from '@mdi/js'; import { mdiArrowLeft, mdiPlus } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -22,16 +20,12 @@
let { data }: Props = $props(); let { data }: Props = $props();
const timelineManager = new TimelineManager(); const options = $derived({
$effect( userId: data.partner.id,
() => visibility: AssetVisibility.Timeline,
void timelineManager.updateOptions({ withStacked: true,
userId: data.partner.id, });
visibility: AssetVisibility.Timeline,
withStacked: true,
}),
);
onDestroy(() => timelineManager.destroy());
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
const handleEscape = () => { const handleEscape = () => {
@ -43,7 +37,7 @@
</script> </script>
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)"> <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> </main>
{#if assetInteraction.selectionActive} {#if assetInteraction.selectionActive}

View file

@ -63,7 +63,7 @@
mdiPlus, mdiPlus,
} from '@mdi/js'; } from '@mdi/js';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { onDestroy, onMount } from 'svelte'; import { onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -76,10 +76,8 @@
let numberOfAssets = $state(data.statistics.assets); let numberOfAssets = $state(data.statistics.assets);
let { isViewing: showAssetViewer } = assetViewingStore; let { isViewing: showAssetViewer } = assetViewingStore;
const timelineManager = new TimelineManager(); let timelineManager = $state<TimelineManager>() as TimelineManager;
$effect(() => void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, personId: data.person.id })); const options = $derived({ visibility: AssetVisibility.Timeline, personId: data.person.id });
onDestroy(() => timelineManager.destroy());
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS); let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
@ -388,7 +386,8 @@
<Timeline <Timeline
enableRouting={true} enableRouting={true}
{person} {person}
{timelineManager} bind:timelineManager
{options}
{assetInteraction} {assetInteraction}
isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON} isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON}
singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON} singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON}

View file

@ -37,13 +37,12 @@
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility } from '@immich/sdk';
import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { mdiDotsVertical, mdiPlus } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
let { isViewing: showAssetViewer } = assetViewingStore; let { isViewing: showAssetViewer } = assetViewingStore;
const timelineManager = new TimelineManager(); let timelineManager = $state<TimelineManager>() as TimelineManager;
void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true }); const options = { visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true };
onDestroy(() => timelineManager.destroy());
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
@ -91,7 +90,8 @@
<UserPageLayout hideNavbar={assetInteraction.selectionActive} showUploadButton scrollbar={false}> <UserPageLayout hideNavbar={assetInteraction.selectionActive} showUploadButton scrollbar={false}>
<Timeline <Timeline
enableRouting={true} enableRouting={true}
{timelineManager} bind:timelineManager
{options}
{assetInteraction} {assetInteraction}
removeAction={AssetAction.ARCHIVE} removeAction={AssetAction.ARCHIVE}
onEscape={handleEscape} onEscape={handleEscape}

View file

@ -21,7 +21,6 @@
import TagAction from '$lib/components/timeline/actions/TagAction.svelte'; import TagAction from '$lib/components/timeline/actions/TagAction.svelte';
import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte'; import AssetSelectControlBar from '$lib/components/timeline/AssetSelectControlBar.svelte';
import { AppRoute, QueryParameter } from '$lib/constants'; 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 type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
@ -79,8 +78,6 @@
}); });
}); });
let timelineManager = new TimelineManager();
const onEscape = () => { const onEscape = () => {
if ($showAssetViewer) { if ($showAssetViewer) {
return; return;
@ -129,7 +126,6 @@
}; };
const handleSetVisibility = (assetIds: string[]) => { const handleSetVisibility = (assetIds: string[]) => {
timelineManager.removeAssets(assetIds);
assetInteraction.clearMultiselect(); assetInteraction.clearMultiselect();
onAssetDelete(assetIds); onAssetDelete(assetIds);
}; };

View file

@ -16,7 +16,6 @@
import { deleteTag, getAllTags, type TagResponseDto } from '@immich/sdk'; import { deleteTag, getAllTags, type TagResponseDto } from '@immich/sdk';
import { Button, HStack, modalManager, Text } from '@immich/ui'; import { Button, HStack, modalManager, Text } from '@immich/ui';
import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js'; import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -28,14 +27,13 @@
const assetInteraction = new AssetInteraction(); 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); let tags = $derived<TagResponseDto[]>(data.tags);
const tree = $derived(TreeNode.fromTags(tags)); const tree = $derived(TreeNode.fromTags(tags));
const tag = $derived(tree.traverse(data.path)); 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 handleNavigation = (tag: string) => navigateToView(joinPaths(data.path, tag));
const getLink = (path: string) => { const getLink = (path: string) => {
@ -117,7 +115,13 @@
<section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar"> <section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar">
{#if tag.hasAssets} {#if tag.hasAssets}
<Timeline enableRouting={true} {timelineManager} {assetInteraction} removeAction={AssetAction.UNARCHIVE}> <Timeline
enableRouting={true}
bind:timelineManager
{options}
{assetInteraction}
removeAction={AssetAction.UNARCHIVE}
>
{#snippet empty()} {#snippet empty()}
<TreeItemThumbnails items={tag.children} icon={mdiTag} onClick={handleNavigation} /> <TreeItemThumbnails items={tag.children} icon={mdiTag} onClick={handleNavigation} />
{/snippet} {/snippet}

View file

@ -21,7 +21,6 @@
import { emptyTrash, restoreTrash } from '@immich/sdk'; import { emptyTrash, restoreTrash } from '@immich/sdk';
import { Button, HStack, modalManager, Text } from '@immich/ui'; import { Button, HStack, modalManager, Text } from '@immich/ui';
import { mdiDeleteForeverOutline, mdiHistory } from '@mdi/js'; import { mdiDeleteForeverOutline, mdiHistory } from '@mdi/js';
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -35,9 +34,8 @@
handlePromiseError(goto(AppRoute.PHOTOS)); handlePromiseError(goto(AppRoute.PHOTOS));
} }
const timelineManager = new TimelineManager(); let timelineManager = $state<TimelineManager>() as TimelineManager;
void timelineManager.updateOptions({ isTrashed: true }); const options = { isTrashed: true };
onDestroy(() => timelineManager.destroy());
const assetInteraction = new AssetInteraction(); const assetInteraction = new AssetInteraction();
@ -116,7 +114,7 @@
</HStack> </HStack>
{/snippet} {/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"> <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 } })} {$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}
</p> </p>

View file

@ -30,13 +30,13 @@
let location = $state<{ latitude: number; longitude: number }>({ latitude: 0, longitude: 0 }); let location = $state<{ latitude: number; longitude: number }>({ latitude: 0, longitude: 0 });
let locationUpdated = $state(false); let locationUpdated = $state(false);
const timelineManager = new TimelineManager(); let timelineManager = $state<TimelineManager>() as TimelineManager;
void timelineManager.updateOptions({ const options = {
visibility: AssetVisibility.Timeline, visibility: AssetVisibility.Timeline,
withStacked: true, withStacked: true,
withPartners: true, withPartners: true,
withCoordinates: true, withCoordinates: true,
}); };
const handleUpdate = async () => { const handleUpdate = async () => {
const confirmed = await modalManager.show(GeolocationUpdateConfirmModal, { const confirmed = await modalManager.show(GeolocationUpdateConfirmModal, {
@ -188,7 +188,8 @@
<Timeline <Timeline
isSelectionMode={true} isSelectionMode={true}
enableRouting={true} enableRouting={true}
{timelineManager} bind:timelineManager
{options}
{assetInteraction} {assetInteraction}
removeAction={AssetAction.ARCHIVE} removeAction={AssetAction.ARCHIVE}
onEscape={handleEscape} onEscape={handleEscape}