diff --git a/i18n/en.json b/i18n/en.json index 2f98ab4bcb..0d9e52681c 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -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", diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index a6a7b42cca..3f00b6c6aa 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -133,11 +133,15 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ B231F52D2E93A44A00BC45D1 /* Core */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Core; sourceTree = ""; }; B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + ); path = Sync; sourceTree = ""; }; @@ -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; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index fb89490550..1dc55468da 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -80,7 +80,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2.0.1 + 2.1.0 CFBundleSignature ???? CFBundleURLTypes @@ -107,7 +107,7 @@ CFBundleVersion - 230 + 231 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift index c9f58398b6..75981fb7ea 100644 --- a/mobile/ios/Runner/Sync/MessagesImpl.swift +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -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 { + // 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) + } + } } diff --git a/mobile/lib/domain/models/setting.model.dart b/mobile/lib/domain/models/setting.model.dart index f427d93285..2c46507331 100644 --- a/mobile/lib/domain/models/setting.model.dart +++ b/mobile/lib/domain/models/setting.model.dart @@ -6,6 +6,7 @@ enum Setting { showStorageIndicator(StoreKey.storageIndicator, true), loadOriginal(StoreKey.loadOriginal, false), loadOriginalVideo(StoreKey.loadOriginalVideo, false), + autoPlayVideo(StoreKey.autoPlayVideo, true), preferRemoteImage(StoreKey.preferRemoteImage, false), advancedTroubleshooting(StoreKey.advancedTroubleshooting, false), enableBackup(StoreKey.enableBackup, false); diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index efccc9bccd..d8404db409 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -70,6 +70,8 @@ enum StoreKey { // Read-only Mode settings readonlyModeEnabled._(138), + autoPlayVideo._(139), + // Experimental stuff photoManagerCustomFilter._(1000), betaPromptShown._(1001), diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart index d8b6db2276..7f39d07ec0 100644 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ b/mobile/lib/pages/common/native_video_viewer.page.dart @@ -190,7 +190,10 @@ class NativeVideoViewerPage extends HookConsumerWidget { isVideoReady.value = true; try { - await videoController.play(); + final autoPlayVideo = ref.read(appSettingsServiceProvider).getSetting(AppSettingsEnum.autoPlayVideo); + if (autoPlayVideo) { + await videoController.play(); + } await videoController.setVolume(0.9); } catch (error) { log.severe('Error playing video: $error'); diff --git a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart index 2bab507e3f..3af31950ad 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/video_viewer.widget.dart @@ -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 { - await videoController.play(); + final autoPlayVideo = AppSetting.get(Setting.autoPlayVideo); + if (autoPlayVideo) { + await videoController.play(); + } await videoController.setVolume(0.9); } catch (error) { log.severe('Error playing video: $error'); diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 03d91328d1..7149408e8a 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -34,6 +34,7 @@ enum AppSettingsEnum { preferRemoteImage(StoreKey.preferRemoteImage, null, false), loopVideo(StoreKey.loopVideo, "loopVideo", true), loadOriginalVideo(StoreKey.loadOriginalVideo, "loadOriginalVideo", false), + autoPlayVideo(StoreKey.autoPlayVideo, "autoPlayVideo", true), mapThemeMode(StoreKey.mapThemeMode, null, 0), mapShowFavoriteOnly(StoreKey.mapShowFavoriteOnly, null, false), mapIncludeArchived(StoreKey.mapIncludeArchived, null, false), diff --git a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart index 1d8d9812be..9a89b7e1e3 100644 --- a/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart +++ b/mobile/lib/widgets/settings/asset_viewer_settings/video_viewer_settings.dart @@ -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(), diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 37ad260324..8d169fbc8f 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -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() as TimelineManager; const assetInteraction = new AssetInteraction(); @@ -61,7 +59,7 @@ />
- +

diff --git a/web/src/lib/components/asset-viewer/video-native-viewer.svelte b/web/src/lib/components/asset-viewer/video-native-viewer.svelte index e2dae3d195..fd3bc1c44f 100644 --- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte @@ -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 @@

diff --git a/web/src/lib/components/user-settings-page/app-settings.svelte b/web/src/lib/components/user-settings-page/app-settings.svelte index ffb0263c94..138a5555d4 100644 --- a/web/src/lib/components/user-settings-page/app-settings.svelte +++ b/web/src/lib/components/user-settings-page/app-settings.svelte @@ -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} /> +
+ +
diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts index 7c448331ff..c9ac596e34 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts @@ -90,7 +90,7 @@ describe('TimelineManager', () => { }); it('calculates timeline height', () => { - expect(timelineManager.timelineHeight).toBe(12_447.5); + expect(timelineManager.totalViewerHeight).toBe(12_507.5); }); }); diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts index 23cf677b40..2938485bc1 100644 --- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts +++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts @@ -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 = 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,7 +384,9 @@ export class TimelineManager { updateGeometry(this, month, { invalidateHeight: changedWidth }); } this.updateIntersections(); - this.#createScrubberMonths(); + if (changedWidth) { + this.#createScrubberMonths(); + } } #createScrubberMonths() { @@ -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 { let cancelable = true; if (options) { diff --git a/web/src/lib/managers/timeline-manager/types.ts b/web/src/lib/managers/timeline-manager/types.ts index fea62084b2..27c27dcb63 100644 --- a/web/src/lib/managers/timeline-manager/types.ts +++ b/web/src/lib/managers/timeline-manager/types.ts @@ -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[0]; export type TimelineManagerOptions = Omit & { diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index f0689e5d6e..bd3fccf68f 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -146,4 +146,6 @@ export const playVideoThumbnailOnHover = persisted('play-video-thumbnai export const loopVideo = persisted('loop-video', true, {}); +export const autoPlayVideo = persisted('auto-play-video', true, {}); + export const recentAlbumsDropdown = persisted('recent-albums-open', true, {}); diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 80cac0b738..673b929a1c 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -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; diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8c8b84ce16..8441362b64 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -328,18 +328,19 @@ } }); - let timelineManager = new TimelineManager(); - - $effect(() => { + let timelineManager = $state() 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 @@ timelineManager.destroy()); + let timelineManager = $state() as TimelineManager; + const options = { visibility: AssetVisibility.Archive }; const assetInteraction = new AssetInteraction(); @@ -49,7 +47,8 @@ timelineManager.destroy()); + let timelineManager = $state() as TimelineManager; + const options = { isFavorite: true, withStacked: true }; const assetInteraction = new AssetInteraction(); @@ -54,7 +52,8 @@ timelineManager.destroy()); + let timelineManager = $state() as TimelineManager; + const options = { visibility: AssetVisibility.Locked }; const assetInteraction = new AssetInteraction(); @@ -60,7 +58,8 @@ - void timelineManager.updateOptions({ - userId: data.partner.id, - visibility: AssetVisibility.Timeline, - withStacked: true, - }), - ); - onDestroy(() => timelineManager.destroy()); + const options = $derived({ + userId: data.partner.id, + visibility: AssetVisibility.Timeline, + withStacked: true, + }); + const assetInteraction = new AssetInteraction(); const handleEscape = () => { @@ -43,7 +37,7 @@
- +
{#if assetInteraction.selectionActive} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5de52fc689..31c599f19b 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -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() 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 @@ timelineManager.destroy()); + let timelineManager = $state() as TimelineManager; + const options = { visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true }; const assetInteraction = new AssetInteraction(); @@ -91,7 +90,8 @@ { if ($showAssetViewer) { return; @@ -129,7 +126,6 @@ }; const handleSetVisibility = (assetIds: string[]) => { - timelineManager.removeAssets(assetIds); assetInteraction.clearMultiselect(); onAssetDelete(assetIds); }; diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index 20db2b23bb..442d3cef6c 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -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(data.tags); const tree = $derived(TreeNode.fromTags(tags)); const tag = $derived(tree.traverse(data.path)); + let timelineManager = $state() 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 @@
{#if tag.hasAssets} - + {#snippet empty()} {/snippet} diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 20a9746a4b..9615618445 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -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() as TimelineManager; + const options = { isTrashed: true }; const assetInteraction = new AssetInteraction(); @@ -116,7 +114,7 @@ {/snippet} - +

{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}

diff --git a/web/src/routes/(user)/utilities/geolocation/+page.svelte b/web/src/routes/(user)/utilities/geolocation/+page.svelte index 5441257df4..732e0625ab 100644 --- a/web/src/routes/(user)/utilities/geolocation/+page.svelte +++ b/web/src/routes/(user)/utilities/geolocation/+page.svelte @@ -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() 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 @@