From f50f4fe5c4a4e7a440e5d2727fc5c6af824fa774 Mon Sep 17 00:00:00 2001 From: Tung Huynh <tung75605@gmail.com> Date: Sat, 4 Jan 2025 22:07:00 -0800 Subject: [PATCH 1/6] feat: add option to always resume from the last position --- Screenbox.Core/Services/ISettingsService.cs | 1 + Screenbox.Core/Services/SettingsService.cs | 7 +++++++ Screenbox.Core/ViewModels/PlayerPageViewModel.cs | 11 ++++++++++- Screenbox.Core/ViewModels/SettingsPageViewModel.cs | 8 ++++++++ Screenbox/Pages/SettingsPage.xaml | 7 +++++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Screenbox.Core/Services/ISettingsService.cs b/Screenbox.Core/Services/ISettingsService.cs index 3513dd59f..009c2a5c8 100644 --- a/Screenbox.Core/Services/ISettingsService.cs +++ b/Screenbox.Core/Services/ISettingsService.cs @@ -16,6 +16,7 @@ public interface ISettingsService bool ShowRecent { get; set; } ThemeOption Theme { get; set; } bool EnqueueAllFilesInFolder { get; set; } + bool RestorePlaybackPosition { get; set; } bool SearchRemovableStorage { get; set; } int MaxVolume { get; set; } string GlobalArguments { get; set; } diff --git a/Screenbox.Core/Services/SettingsService.cs b/Screenbox.Core/Services/SettingsService.cs index 8bdba9b20..c1888f144 100644 --- a/Screenbox.Core/Services/SettingsService.cs +++ b/Screenbox.Core/Services/SettingsService.cs @@ -25,6 +25,7 @@ public sealed class SettingsService : ISettingsService private const string LibrariesSearchRemovableStorageKey = "Libraries/SearchRemovableStorage"; private const string GeneralShowRecent = "General/ShowRecent"; private const string GeneralEnqueueAllInFolder = "General/EnqueueAllInFolder"; + private const string GeneralRestorePlaybackPosition = "General/RestorePlaybackPosition"; private const string AdvancedModeKey = "Advanced/IsEnabled"; private const string AdvancedMultipleInstancesKey = "Advanced/MultipleInstances"; private const string GlobalArgumentsKey = "Values/GlobalArguments"; @@ -99,6 +100,12 @@ public bool EnqueueAllFilesInFolder set => SetValue(GeneralEnqueueAllInFolder, value); } + public bool RestorePlaybackPosition + { + get => GetValue<bool>(GeneralRestorePlaybackPosition); + set => SetValue(GeneralRestorePlaybackPosition, value); + } + public bool PlayerShowControls { get => GetValue<bool>(PlayerShowControlsKey); diff --git a/Screenbox.Core/ViewModels/PlayerPageViewModel.cs b/Screenbox.Core/ViewModels/PlayerPageViewModel.cs index 8faadc404..a27a360dc 100644 --- a/Screenbox.Core/ViewModels/PlayerPageViewModel.cs +++ b/Screenbox.Core/ViewModels/PlayerPageViewModel.cs @@ -177,7 +177,16 @@ public void Receive(PlaylistCurrentItemChangedMessage message) if (message.Value != null) { TimeSpan lastPosition = _lastPositionTracker.GetPosition(message.Value.Location); - Messenger.Send(new RaiseResumePositionNotificationMessage(lastPosition)); + if (lastPosition <= TimeSpan.Zero) return; + if (_settingsService.RestorePlaybackPosition) + { + // TODO: Wait until playback starts then send time change message + Messenger.Send(new ChangeTimeRequestMessage(lastPosition, debounce: false)); + } + else + { + Messenger.Send(new RaiseResumePositionNotificationMessage(lastPosition)); + } } } diff --git a/Screenbox.Core/ViewModels/SettingsPageViewModel.cs b/Screenbox.Core/ViewModels/SettingsPageViewModel.cs index a2ccff135..da050f063 100644 --- a/Screenbox.Core/ViewModels/SettingsPageViewModel.cs +++ b/Screenbox.Core/ViewModels/SettingsPageViewModel.cs @@ -30,6 +30,7 @@ public sealed partial class SettingsPageViewModel : ObservableRecipient [ObservableProperty] private bool _showRecent; [ObservableProperty] private int _theme; [ObservableProperty] private bool _enqueueAllFilesInFolder; + [ObservableProperty] private bool _restorePlaybackPosition; [ObservableProperty] private bool _searchRemovableStorage; [ObservableProperty] private bool _advancedMode; [ObservableProperty] private bool _useMultipleInstances; @@ -80,6 +81,7 @@ public SettingsPageViewModel(ISettingsService settingsService, ILibraryService l _showRecent = _settingsService.ShowRecent; _theme = ((int)_settingsService.Theme + 2) % 3; _enqueueAllFilesInFolder = _settingsService.EnqueueAllFilesInFolder; + _restorePlaybackPosition = _settingsService.RestorePlaybackPosition; _searchRemovableStorage = _settingsService.SearchRemovableStorage; _advancedMode = _settingsService.AdvancedMode; _useMultipleInstances = _settingsService.UseMultipleInstances; @@ -154,6 +156,12 @@ partial void OnEnqueueAllFilesInFolderChanged(bool value) Messenger.Send(new SettingsChangedMessage(nameof(EnqueueAllFilesInFolder), typeof(SettingsPageViewModel))); } + partial void OnRestorePlaybackPositionChanged(bool value) + { + _settingsService.RestorePlaybackPosition = value; + Messenger.Send(new SettingsChangedMessage(nameof(RestorePlaybackPosition), typeof(SettingsPageViewModel))); + } + async partial void OnSearchRemovableStorageChanged(bool value) { _settingsService.SearchRemovableStorage = value; diff --git a/Screenbox/Pages/SettingsPage.xaml b/Screenbox/Pages/SettingsPage.xaml index c41842637..6a638bf9e 100644 --- a/Screenbox/Pages/SettingsPage.xaml +++ b/Screenbox/Pages/SettingsPage.xaml @@ -315,6 +315,13 @@ <ToggleSwitch AutomationProperties.HelpText="{x:Bind SettingsEnqueueAllFilesInFolderCard.Description}" IsOn="{x:Bind ViewModel.EnqueueAllFilesInFolder, Mode=TwoWay}" /> </ctc:SettingsCard> + <ctc:SettingsCard + Margin="{StaticResource SettingsCardMargin}" + Description="Continue playing from where you last stopped when opening a file" + Header="Always resume from last position"> + <ToggleSwitch IsOn="{x:Bind ViewModel.RestorePlaybackPosition, Mode=TwoWay}" /> + </ctc:SettingsCard> + <!-- Player section --> <TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{strings:Resources Key=SettingsCategoryPlayer}" /> From 5072c2b3afef36a72edf70850d38ac5d5524aa28 Mon Sep 17 00:00:00 2001 From: Tung Huynh <tung75605@gmail.com> Date: Fri, 14 Feb 2025 00:36:26 -0800 Subject: [PATCH 2/6] move LastPositionTracker to SeekBarViewModel LastPositionTracker now participates in messaging and dependency injection --- Screenbox.Core/Common/ServiceHelpers.cs | 4 + Screenbox.Core/Helpers/LastPositionTracker.cs | 38 ++++++- .../ViewModels/PlayerPageViewModel.cs | 48 +-------- Screenbox.Core/ViewModels/SeekBarViewModel.cs | 100 ++++++++++++++++-- 4 files changed, 131 insertions(+), 59 deletions(-) diff --git a/Screenbox.Core/Common/ServiceHelpers.cs b/Screenbox.Core/Common/ServiceHelpers.cs index 51bd7a135..5bc9082f1 100644 --- a/Screenbox.Core/Common/ServiceHelpers.cs +++ b/Screenbox.Core/Common/ServiceHelpers.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Screenbox.Core.Factories; +using Screenbox.Core.Helpers; using Screenbox.Core.Services; using Screenbox.Core.ViewModels; @@ -41,6 +42,9 @@ public static void PopulateCoreServices(ServiceCollection services) services.AddSingleton<VolumeViewModel>(); // Avoid thread lock services.AddSingleton<MediaListViewModel>(); // Global playlist + // Misc + services.AddTransient<LastPositionTracker>(); + // Factories services.AddSingleton<MediaViewModelFactory>(); services.AddSingleton<StorageItemViewModelFactory>(); diff --git a/Screenbox.Core/Helpers/LastPositionTracker.cs b/Screenbox.Core/Helpers/LastPositionTracker.cs index 0da06d0fc..2e2bfcfe8 100644 --- a/Screenbox.Core/Helpers/LastPositionTracker.cs +++ b/Screenbox.Core/Helpers/LastPositionTracker.cs @@ -1,5 +1,8 @@ #nullable enable +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Screenbox.Core.Messages; using Screenbox.Core.Models; using Screenbox.Core.Services; using System; @@ -11,17 +14,41 @@ namespace Screenbox.Core.Helpers { - internal class LastPositionTracker + public sealed class LastPositionTracker : ObservableRecipient, + IRecipient<SuspendingMessage>, + IRecipient<MediaPlayerChangedMessage> { private const int Capacity = 64; private const string SaveFileName = "last_positions.bin"; + public DateTimeOffset LastUpdated { get; private set; } + + private readonly IFilesService _filesService; private List<MediaLastPosition> _lastPositions = new(Capacity + 1); private MediaLastPosition? _updateCache; private string? _removeCache; + public LastPositionTracker(IFilesService filesService) + { + _filesService = filesService; + + IsActive = true; + } + + public void Receive(SuspendingMessage message) + { + message.Reply(SaveToDiskAsync()); + } + + public async void Receive(MediaPlayerChangedMessage message) + { + if (_lastPositions.Count > 0) return; + await LoadFromDiskAsync(); + } + public void UpdateLastPosition(string location, TimeSpan position) { + LastUpdated = DateTimeOffset.Now; _removeCache = null; MediaLastPosition? item = _updateCache; if (item?.Location == location) @@ -66,16 +93,17 @@ public TimeSpan GetPosition(string location) public void RemovePosition(string location) { + LastUpdated = DateTimeOffset.Now; if (_removeCache == location) return; _lastPositions.RemoveAll(x => x.Location == location); _removeCache = location; } - public async Task SaveToDiskAsync(IFilesService filesService) + public async Task SaveToDiskAsync() { try { - await filesService.SaveToDiskAsync(ApplicationData.Current.TemporaryFolder, SaveFileName, _lastPositions.ToList()); + await _filesService.SaveToDiskAsync(ApplicationData.Current.TemporaryFolder, SaveFileName, _lastPositions.ToList()); } catch (FileLoadException) { @@ -83,12 +111,12 @@ public async Task SaveToDiskAsync(IFilesService filesService) } } - public async Task LoadFromDiskAsync(IFilesService filesService) + public async Task LoadFromDiskAsync() { try { List<MediaLastPosition> lastPositions = - await filesService.LoadFromDiskAsync<List<MediaLastPosition>>(ApplicationData.Current.TemporaryFolder, SaveFileName); + await _filesService.LoadFromDiskAsync<List<MediaLastPosition>>(ApplicationData.Current.TemporaryFolder, SaveFileName); lastPositions.Capacity = Capacity; _lastPositions = lastPositions; } diff --git a/Screenbox.Core/ViewModels/PlayerPageViewModel.cs b/Screenbox.Core/ViewModels/PlayerPageViewModel.cs index a27a360dc..91dc9ee46 100644 --- a/Screenbox.Core/ViewModels/PlayerPageViewModel.cs +++ b/Screenbox.Core/ViewModels/PlayerPageViewModel.cs @@ -32,7 +32,6 @@ public sealed partial class PlayerPageViewModel : ObservableRecipient, IRecipient<UpdateStatusMessage>, IRecipient<UpdateVolumeStatusMessage>, IRecipient<TogglePlayerVisibilityMessage>, - IRecipient<SuspendingMessage>, IRecipient<MediaPlayerChangedMessage>, IRecipient<PlaylistCurrentItemChangedMessage>, IRecipient<ShowPlayPauseBadgeMessage>, @@ -71,7 +70,6 @@ public sealed partial class PlayerPageViewModel : ObservableRecipient, private readonly ISettingsService _settingsService; private readonly IResourceService _resourceService; private readonly IFilesService _filesService; - private readonly LastPositionTracker _lastPositionTracker; private IMediaPlayer? _mediaPlayer; private bool _visibilityOverride; private bool _resizeNext; @@ -90,7 +88,6 @@ public PlayerPageViewModel(IWindowService windowService, IResourceService resour _playPauseBadgeTimer = _dispatcherQueue.CreateTimer(); _navigationViewDisplayMode = Messenger.Send<NavigationViewDisplayModeRequestMessage>(); _playerVisibility = PlayerVisibilityState.Hidden; - _lastPositionTracker = new LastPositionTracker(); _lastUpdated = DateTimeOffset.MinValue; FocusManager.GotFocus += FocusManagerOnFocusChanged; @@ -132,19 +129,11 @@ private void WindowServiceOnViewModeChanged(object sender, ViewModeChangedEventA }); } - public void Receive(SuspendingMessage message) - { - message.Reply(_lastPositionTracker.SaveToDiskAsync(_filesService)); - } - - public async void Receive(MediaPlayerChangedMessage message) + public void Receive(MediaPlayerChangedMessage message) { _mediaPlayer = message.Value; _mediaPlayer.PlaybackStateChanged += OnStateChanged; - _mediaPlayer.PositionChanged += OnPositionChanged; _mediaPlayer.NaturalVideoSizeChanged += OnNaturalVideoSizeChanged; - - await _lastPositionTracker.LoadFromDiskAsync(_filesService); } public void Receive(UpdateVolumeStatusMessage message) @@ -174,20 +163,6 @@ public void Receive(UpdateStatusMessage message) public void Receive(PlaylistCurrentItemChangedMessage message) { _dispatcherQueue.TryEnqueue(() => ProcessOpeningMedia(message.Value)); - if (message.Value != null) - { - TimeSpan lastPosition = _lastPositionTracker.GetPosition(message.Value.Location); - if (lastPosition <= TimeSpan.Zero) return; - if (_settingsService.RestorePlaybackPosition) - { - // TODO: Wait until playback starts then send time change message - Messenger.Send(new ChangeTimeRequestMessage(lastPosition, debounce: false)); - } - else - { - Messenger.Send(new RaiseResumePositionNotificationMessage(lastPosition)); - } - } } public void Receive(ShowPlayPauseBadgeMessage message) @@ -673,27 +648,6 @@ private void OnStateChanged(IMediaPlayer sender, object? args) }); } - private void OnPositionChanged(IMediaPlayer sender, object? args) - { - // Only record position for media over 1 minute - // Update every 3 seconds - TimeSpan position = sender.Position; - if (Media == null || sender.NaturalDuration <= TimeSpan.FromMinutes(1) || - DateTimeOffset.Now - _lastUpdated <= TimeSpan.FromSeconds(3)) - return; - - if (position > TimeSpan.FromSeconds(30) && position + TimeSpan.FromSeconds(10) < sender.NaturalDuration) - { - _lastUpdated = DateTimeOffset.Now; - _lastPositionTracker.UpdateLastPosition(Media.Location, position); - } - else if (position > TimeSpan.FromSeconds(5)) - { - _lastUpdated = DateTimeOffset.Now; - _lastPositionTracker.RemovePosition(Media.Location); - } - } - private void OnNaturalVideoSizeChanged(IMediaPlayer sender, EventArgs args) { if (!_resizeNext && _settingsService.PlayerAutoResize != PlayerAutoResizeOption.Always) return; diff --git a/Screenbox.Core/ViewModels/SeekBarViewModel.cs b/Screenbox.Core/ViewModels/SeekBarViewModel.cs index 4f5571e1c..b35101fc7 100644 --- a/Screenbox.Core/ViewModels/SeekBarViewModel.cs +++ b/Screenbox.Core/ViewModels/SeekBarViewModel.cs @@ -5,10 +5,12 @@ using CommunityToolkit.Mvvm.Messaging.Messages; using CommunityToolkit.WinUI; using Screenbox.Core.Enums; +using Screenbox.Core.Events; using Screenbox.Core.Helpers; using Screenbox.Core.Messages; using Screenbox.Core.Models; using Screenbox.Core.Playback; +using Screenbox.Core.Services; using System; using System.Collections.ObjectModel; using Windows.Media.Core; @@ -25,6 +27,7 @@ public sealed partial class SeekBarViewModel : IRecipient<TimeChangeOverrideMessage>, IRecipient<ChangeTimeRequestMessage>, IRecipient<PlayerControlsVisibilityChangedMessage>, + IRecipient<PlaylistCurrentItemChangedMessage>, IRecipient<PropertyChangedMessage<PlayerVisibilityState>>, IRecipient<MediaPlayerChangedMessage> { @@ -44,17 +47,31 @@ public sealed partial class SeekBarViewModel : public ObservableCollection<ChapterCue> Chapters { get; } + private TimeSpan NaturalDuration => TimeSpan.FromMilliseconds(Length); + + private TimeSpan Position + { + get => TimeSpan.FromMilliseconds(Time); + set => Time = value.TotalMilliseconds; + } + private IMediaPlayer? _mediaPlayer; + private readonly ISettingsService _settingsService; private readonly DispatcherQueue _dispatcherQueue; private readonly DispatcherQueueTimer _bufferingTimer; private readonly DispatcherQueueTimer _seekTimer; private readonly DispatcherQueueTimer _originalPositionTimer; + private readonly LastPositionTracker _lastPositionTracker; private TimeSpan _originalPosition; + private TimeSpan _lastTrackedPosition; private bool _timeChangeOverride; + private MediaViewModel? _currentItem; - public SeekBarViewModel() + public SeekBarViewModel(ISettingsService settingsService, LastPositionTracker lastPositionTracker) { + _settingsService = settingsService; + _lastPositionTracker = lastPositionTracker; _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); _bufferingTimer = _dispatcherQueue.CreateTimer(); _seekTimer = _dispatcherQueue.CreateTimer(); @@ -68,6 +85,26 @@ public SeekBarViewModel() IsActive = true; } + public void Receive(PlaylistCurrentItemChangedMessage message) + { + _lastTrackedPosition = TimeSpan.Zero; + _currentItem = message.Value; + if (message.Value != null) + { + TimeSpan lastPosition = _lastPositionTracker.GetPosition(message.Value.Location); + if (lastPosition <= TimeSpan.Zero) return; + if (_settingsService.RestorePlaybackPosition) + { + // Media is not seekable yet, so we need to wait for the PlaybackStateChanged event + _lastTrackedPosition = lastPosition; + } + else + { + Messenger.Send(new RaiseResumePositionNotificationMessage(lastPosition)); + } + } + } + public void Receive(PropertyChangedMessage<PlayerVisibilityState> message) { ShouldHandleKeyDown = message.NewValue != PlayerVisibilityState.Visible; @@ -83,7 +120,20 @@ public void Receive(PlayerControlsVisibilityChangedMessage message) public void Receive(MediaPlayerChangedMessage message) { + if (_mediaPlayer != null) + { + _mediaPlayer.PlaybackStateChanged -= OnPlaybackStateChanged; + _mediaPlayer.NaturalDurationChanged -= OnNaturalDurationChanged; + _mediaPlayer.PositionChanged -= OnPositionChanged; + _mediaPlayer.MediaEnded -= OnEndReached; + _mediaPlayer.BufferingStarted -= OnBufferingStarted; + _mediaPlayer.BufferingEnded -= OnBufferingEnded; + _mediaPlayer.PlaybackItemChanged -= OnPlaybackItemChanged; + _mediaPlayer.CanSeekChanged -= OnCanSeekChanged; + } + _mediaPlayer = message.Value; + _mediaPlayer.PlaybackStateChanged += OnPlaybackStateChanged; _mediaPlayer.NaturalDurationChanged += OnNaturalDurationChanged; _mediaPlayer.PositionChanged += OnPositionChanged; _mediaPlayer.MediaEnded += OnEndReached; @@ -131,6 +181,7 @@ public void OnSeekBarPointerWheelChanged(double pointerWheelDelta) public void OnSeekBarValueChanged(object sender, RangeBaseValueChangedEventArgs args) { + var newPosition = TimeSpan.FromMilliseconds(args.NewValue); // Only update player position when there is a user interaction. // SeekBar should have OneWay binding to Time, so when Time changes and invokes // this handler, Time = args.NewValue. The only exception is when the change is @@ -146,22 +197,28 @@ public void OnSeekBarValueChanged(object sender, RangeBaseValueChangedEventArgs bool paused = _mediaPlayer.PlaybackState is MediaPlaybackState.Paused or MediaPlaybackState.Buffering; if (shouldUpdate || paused || shouldOverride) { - SetPlayerPosition(TimeSpan.FromMilliseconds(args.NewValue), true); + SetPlayerPosition(newPosition, true); } } + + UpdateLastPosition(newPosition); } private PositionChangedResult UpdatePosition(TimeSpan position, bool isOffset, bool debounce) { - TimeSpan currentPosition = TimeSpan.FromMilliseconds(Time); + TimeSpan currentPosition = Position; _originalPositionTimer.Debounce(() => _originalPosition = currentPosition, TimeSpan.FromSeconds(1), true); // Assume UI thread - Time = isOffset ? Math.Clamp(Time + position.TotalMilliseconds, 0, Length) : position.TotalMilliseconds; - SetPlayerPosition(TimeSpan.FromMilliseconds(Time), debounce); + Position = isOffset ? (currentPosition + position) switch + { + var newPosition when newPosition < TimeSpan.Zero => TimeSpan.Zero, + var newPosition when newPosition > NaturalDuration => NaturalDuration, + var newPosition => newPosition + } : position; + SetPlayerPosition(Position, debounce); - return new PositionChangedResult(currentPosition, TimeSpan.FromMilliseconds(Time), - _originalPosition, TimeSpan.FromMilliseconds(Length)); + return new PositionChangedResult(currentPosition, Position, _originalPosition, NaturalDuration); } private void SetPlayerPosition(TimeSpan position, bool debounce) @@ -186,6 +243,19 @@ private void OnCanSeekChanged(IMediaPlayer sender, EventArgs args) }); } + private void OnPlaybackStateChanged(IMediaPlayer sender, ValueChangedEventArgs<MediaPlaybackState> args) + { + if (args.NewValue is not (MediaPlaybackState.None or MediaPlaybackState.Opening) && + _lastTrackedPosition > TimeSpan.Zero) + { + _dispatcherQueue.TryEnqueue(() => + { + SetPlayerPosition(_lastTrackedPosition, false); + _lastTrackedPosition = TimeSpan.Zero; + }); + } + } + private void OnPlaybackItemChanged(IMediaPlayer sender, object? args) { _seekTimer.Stop(); @@ -276,5 +346,21 @@ private void UpdateChapters(PlaybackChapterList? chapterList) } // Chapters.SyncItems(chapterList); } + + private void UpdateLastPosition(TimeSpan position) + { + if (_currentItem == null || NaturalDuration <= TimeSpan.FromMinutes(1) || + DateTimeOffset.Now - _lastPositionTracker.LastUpdated <= TimeSpan.FromSeconds(3)) + return; + + if (position > TimeSpan.FromSeconds(30) && position + TimeSpan.FromSeconds(10) < NaturalDuration) + { + _lastPositionTracker.UpdateLastPosition(_currentItem.Location, position); + } + else if (position > TimeSpan.FromSeconds(5)) + { + _lastPositionTracker.RemovePosition(_currentItem.Location); + } + } } } \ No newline at end of file From 5c153071877d77bf333a4865bb570aa04a22b1cd Mon Sep 17 00:00:00 2001 From: Tung Huynh <tung75605@gmail.com> Date: Fri, 21 Feb 2025 00:43:46 -0800 Subject: [PATCH 3/6] localize raw strings --- Screenbox/Pages/SettingsPage.xaml | 4 +-- .../Strings/en-US/Resources.generated.cs | 28 +++++++++++++++++++ Screenbox/Strings/en-US/Resources.resw | 6 ++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Screenbox/Pages/SettingsPage.xaml b/Screenbox/Pages/SettingsPage.xaml index 6a638bf9e..2993402aa 100644 --- a/Screenbox/Pages/SettingsPage.xaml +++ b/Screenbox/Pages/SettingsPage.xaml @@ -317,8 +317,8 @@ <ctc:SettingsCard Margin="{StaticResource SettingsCardMargin}" - Description="Continue playing from where you last stopped when opening a file" - Header="Always resume from last position"> + Description="{strings:Resources Key=SettingsRestorePlaybackPositionDescription}" + Header="{strings:Resources Key=SettingsRestorePlaybackPositionHeader}"> <ToggleSwitch IsOn="{x:Bind ViewModel.RestorePlaybackPosition, Mode=TwoWay}" /> </ctc:SettingsCard> diff --git a/Screenbox/Strings/en-US/Resources.generated.cs b/Screenbox/Strings/en-US/Resources.generated.cs index 661b2a92d..f35092c89 100644 --- a/Screenbox/Strings/en-US/Resources.generated.cs +++ b/Screenbox/Strings/en-US/Resources.generated.cs @@ -3170,6 +3170,32 @@ public static string SettingsCategoryPersonalization } } #endregion + + #region SettingsRestorePlaybackPositionHeader + /// <summary> + /// Looks up a localized string similar to: Always resume from last position + /// </summary> + public static string SettingsRestorePlaybackPositionHeader + { + get + { + return _resourceLoader.GetString("SettingsRestorePlaybackPositionHeader"); + } + } + #endregion + + #region SettingsRestorePlaybackPositionDescription + /// <summary> + /// Looks up a localized string similar to: Continue playing from where you last stopped when opening a file + /// </summary> + public static string SettingsRestorePlaybackPositionDescription + { + get + { + return _resourceLoader.GetString("SettingsRestorePlaybackPositionDescription"); + } + } + #endregion } [global::System.CodeDom.Compiler.GeneratedCodeAttribute("DotNetPlus.ReswPlus", "2.1.3")] @@ -3423,6 +3449,8 @@ public enum KeyEnum SettingsThemeSelectionDescription, SettingsThemeSelectionHeader, SettingsCategoryPersonalization, + SettingsRestorePlaybackPositionHeader, + SettingsRestorePlaybackPositionDescription, } private static ResourceLoader _resourceLoader; diff --git a/Screenbox/Strings/en-US/Resources.resw b/Screenbox/Strings/en-US/Resources.resw index a5e711c04..645e5b918 100644 --- a/Screenbox/Strings/en-US/Resources.resw +++ b/Screenbox/Strings/en-US/Resources.resw @@ -911,4 +911,10 @@ <data name="SettingsCategoryPersonalization" xml:space="preserve"> <value>Personalization</value> </data> + <data name="SettingsRestorePlaybackPositionHeader" xml:space="preserve"> + <value>Always resume from last position</value> + </data> + <data name="SettingsRestorePlaybackPositionDescription" xml:space="preserve"> + <value>Continue playing from where you last stopped when opening a file</value> + </data> </root> \ No newline at end of file From 16042999695d996f4167b33a095b214e23b1201a Mon Sep 17 00:00:00 2001 From: Tung Huynh <31434093+huynhsontung@users.noreply.github.com> Date: Mon, 24 Feb 2025 00:13:36 -0800 Subject: [PATCH 4/6] style: update icon for the restore playback position option Co-authored-by: United600 <United600@users.noreply.github.com> --- Screenbox/Pages/SettingsPage.xaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Screenbox/Pages/SettingsPage.xaml b/Screenbox/Pages/SettingsPage.xaml index 2993402aa..93d0a02e1 100644 --- a/Screenbox/Pages/SettingsPage.xaml +++ b/Screenbox/Pages/SettingsPage.xaml @@ -318,7 +318,8 @@ <ctc:SettingsCard Margin="{StaticResource SettingsCardMargin}" Description="{strings:Resources Key=SettingsRestorePlaybackPositionDescription}" - Header="{strings:Resources Key=SettingsRestorePlaybackPositionHeader}"> + Header="{strings:Resources Key=SettingsRestorePlaybackPositionHeader}" + HeaderIcon="{ui:FontIcon Glyph=}"> <ToggleSwitch IsOn="{x:Bind ViewModel.RestorePlaybackPosition, Mode=TwoWay}" /> </ctc:SettingsCard> From ca75d51668f5bb8d862d572e08575ce3d3ec1c00 Mon Sep 17 00:00:00 2001 From: Tung Huynh <tung75605@gmail.com> Date: Mon, 24 Feb 2025 00:18:22 -0800 Subject: [PATCH 5/6] a11y: add automation help text for the restore playback position option adjust other help text on the SettingsPage to avoid using x:Bind since they are static values --- Screenbox/Pages/SettingsPage.xaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Screenbox/Pages/SettingsPage.xaml b/Screenbox/Pages/SettingsPage.xaml index 93d0a02e1..56b3874a9 100644 --- a/Screenbox/Pages/SettingsPage.xaml +++ b/Screenbox/Pages/SettingsPage.xaml @@ -172,7 +172,7 @@ </interactivity:Interaction.Behaviors> <Button x:Name="AddMusicFolderButton" - AutomationProperties.HelpText="{x:Bind AddMusicFolderButton.(ToolTipService.ToolTip)}" + AutomationProperties.HelpText="{strings:Resources Key=AddMusicFolderToolTip}" AutomationProperties.Name="{strings:Resources Key=AddFolder}" Command="{x:Bind ViewModel.AddMusicFolderCommand}" ToolTipService.ToolTip="{strings:Resources Key=AddMusicFolderToolTip}"> @@ -201,7 +201,7 @@ </interactivity:Interaction.Behaviors> <Button x:Name="AddVideoFolderButton" - AutomationProperties.HelpText="{x:Bind AddVideoFolderButton.(ToolTipService.ToolTip)}" + AutomationProperties.HelpText="{strings:Resources Key=AddVideoFolderToolTip}" AutomationProperties.Name="{strings:Resources Key=AddFolder}" Command="{x:Bind ViewModel.AddVideosFolderCommand}" ToolTipService.ToolTip="{strings:Resources Key=AddVideoFolderToolTip}"> @@ -312,7 +312,7 @@ Header="{strings:Resources Key=SettingsEnqueueAllFilesInFolderHeader}" HeaderIcon="{ui:FontIcon FontFamily={StaticResource ScreenboxSymbolThemeFontFamily}, Glyph={StaticResource FolderListGlyph}}"> - <ToggleSwitch AutomationProperties.HelpText="{x:Bind SettingsEnqueueAllFilesInFolderCard.Description}" IsOn="{x:Bind ViewModel.EnqueueAllFilesInFolder, Mode=TwoWay}" /> + <ToggleSwitch AutomationProperties.HelpText="{strings:Resources Key=SettingsEnqueueAllFilesInFolderDescription}" IsOn="{x:Bind ViewModel.EnqueueAllFilesInFolder, Mode=TwoWay}" /> </ctc:SettingsCard> <ctc:SettingsCard @@ -320,7 +320,7 @@ Description="{strings:Resources Key=SettingsRestorePlaybackPositionDescription}" Header="{strings:Resources Key=SettingsRestorePlaybackPositionHeader}" HeaderIcon="{ui:FontIcon Glyph=}"> - <ToggleSwitch IsOn="{x:Bind ViewModel.RestorePlaybackPosition, Mode=TwoWay}" /> + <ToggleSwitch AutomationProperties.HelpText="{strings:Resources Key=SettingsRestorePlaybackPositionDescription}" IsOn="{x:Bind ViewModel.RestorePlaybackPosition, Mode=TwoWay}" /> </ctc:SettingsCard> <!-- Player section --> From 31a089772a7dd0f9c383d9b0fa012ad5c90a15a2 Mon Sep 17 00:00:00 2001 From: Tung Huynh <tung75605@gmail.com> Date: Tue, 25 Feb 2025 00:56:36 -0800 Subject: [PATCH 6/6] fix: last postion doesn't restore on file launch --- Screenbox.Core/Helpers/LastPositionTracker.cs | 12 ++--- Screenbox.Core/ViewModels/SeekBarViewModel.cs | 47 ++++++++++++++----- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/Screenbox.Core/Helpers/LastPositionTracker.cs b/Screenbox.Core/Helpers/LastPositionTracker.cs index 2e2bfcfe8..e612b85db 100644 --- a/Screenbox.Core/Helpers/LastPositionTracker.cs +++ b/Screenbox.Core/Helpers/LastPositionTracker.cs @@ -15,12 +15,13 @@ namespace Screenbox.Core.Helpers { public sealed class LastPositionTracker : ObservableRecipient, - IRecipient<SuspendingMessage>, - IRecipient<MediaPlayerChangedMessage> + IRecipient<SuspendingMessage> { private const int Capacity = 64; private const string SaveFileName = "last_positions.bin"; + public bool IsLoaded => LastUpdated != default; + public DateTimeOffset LastUpdated { get; private set; } private readonly IFilesService _filesService; @@ -40,12 +41,6 @@ public void Receive(SuspendingMessage message) message.Reply(SaveToDiskAsync()); } - public async void Receive(MediaPlayerChangedMessage message) - { - if (_lastPositions.Count > 0) return; - await LoadFromDiskAsync(); - } - public void UpdateLastPosition(string location, TimeSpan position) { LastUpdated = DateTimeOffset.Now; @@ -119,6 +114,7 @@ public async Task LoadFromDiskAsync() await _filesService.LoadFromDiskAsync<List<MediaLastPosition>>(ApplicationData.Current.TemporaryFolder, SaveFileName); lastPositions.Capacity = Capacity; _lastPositions = lastPositions; + LastUpdated = DateTimeOffset.UtcNow; } catch (FileNotFoundException) { diff --git a/Screenbox.Core/ViewModels/SeekBarViewModel.cs b/Screenbox.Core/ViewModels/SeekBarViewModel.cs index b35101fc7..9f5e695c7 100644 --- a/Screenbox.Core/ViewModels/SeekBarViewModel.cs +++ b/Screenbox.Core/ViewModels/SeekBarViewModel.cs @@ -89,19 +89,9 @@ public void Receive(PlaylistCurrentItemChangedMessage message) { _lastTrackedPosition = TimeSpan.Zero; _currentItem = message.Value; - if (message.Value != null) + if (message.Value != null && _lastPositionTracker.IsLoaded) { - TimeSpan lastPosition = _lastPositionTracker.GetPosition(message.Value.Location); - if (lastPosition <= TimeSpan.Zero) return; - if (_settingsService.RestorePlaybackPosition) - { - // Media is not seekable yet, so we need to wait for the PlaybackStateChanged event - _lastTrackedPosition = lastPosition; - } - else - { - Messenger.Send(new RaiseResumePositionNotificationMessage(lastPosition)); - } + RestoreLastPosition(message.Value); } } @@ -118,7 +108,7 @@ public void Receive(PlayerControlsVisibilityChangedMessage message) } } - public void Receive(MediaPlayerChangedMessage message) + public async void Receive(MediaPlayerChangedMessage message) { if (_mediaPlayer != null) { @@ -141,6 +131,15 @@ public void Receive(MediaPlayerChangedMessage message) _mediaPlayer.BufferingEnded += OnBufferingEnded; _mediaPlayer.PlaybackItemChanged += OnPlaybackItemChanged; _mediaPlayer.CanSeekChanged += OnCanSeekChanged; + + if (!_lastPositionTracker.IsLoaded) + { + await _lastPositionTracker.LoadFromDiskAsync(); + if (_currentItem != null) + { + RestoreLastPosition(_currentItem); + } + } } public void Receive(TimeChangeOverrideMessage message) @@ -204,6 +203,28 @@ public void OnSeekBarValueChanged(object sender, RangeBaseValueChangedEventArgs UpdateLastPosition(newPosition); } + private void RestoreLastPosition(MediaViewModel media) + { + TimeSpan lastPosition = _lastPositionTracker.GetPosition(media.Location); + if (lastPosition <= TimeSpan.Zero) return; + if (_settingsService.RestorePlaybackPosition) + { + if (_mediaPlayer?.PlaybackState is { } and not (MediaPlaybackState.None or MediaPlaybackState.Opening)) + { + UpdatePosition(lastPosition, false, false); + } + else + { + // Media is not seekable yet, so we need to wait for the PlaybackStateChanged event + _lastTrackedPosition = lastPosition; + } + } + else + { + Messenger.Send(new RaiseResumePositionNotificationMessage(lastPosition)); + } + } + private PositionChangedResult UpdatePosition(TimeSpan position, bool isOffset, bool debounce) { TimeSpan currentPosition = Position;