diff --git a/Screenbox.Core/Common/ServiceHelpers.cs b/Screenbox.Core/Common/ServiceHelpers.cs index bd8127e2e..0a9614349 100644 --- a/Screenbox.Core/Common/ServiceHelpers.cs +++ b/Screenbox.Core/Common/ServiceHelpers.cs @@ -2,7 +2,6 @@ using Screenbox.Core.Contexts; using Screenbox.Core.Controllers; using Screenbox.Core.Factories; -using Screenbox.Core.Helpers; using Screenbox.Core.Services; using Screenbox.Core.ViewModels; @@ -45,9 +44,6 @@ public static void PopulateCoreServices(ServiceCollection services) services.AddSingleton(); // Avoid thread lock services.AddSingleton(); // Global playlist - // Misc - services.AddTransient(); - // Factories services.AddSingleton(); services.AddSingleton(); @@ -62,6 +58,7 @@ public static void PopulateCoreServices(ServiceCollection services) // Controllers services.AddSingleton(); + services.AddSingleton(); // Services services.AddSingleton(); diff --git a/Screenbox.Core/Controllers/LastPositionTracker.cs b/Screenbox.Core/Controllers/LastPositionTracker.cs new file mode 100644 index 000000000..a52b5cb78 --- /dev/null +++ b/Screenbox.Core/Controllers/LastPositionTracker.cs @@ -0,0 +1,136 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Messaging; +using Screenbox.Core.Messages; +using Screenbox.Core.Models; +using Screenbox.Core.Services; +using Windows.Storage; + +namespace Screenbox.Core.Controllers; + +public sealed class LastPositionTracker : ObservableRecipient, + IRecipient +{ + 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; + private List _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 void UpdateLastPosition(string location, TimeSpan position) + { + LastUpdated = DateTimeOffset.Now; + _removeCache = null; + MediaLastPosition? item = _updateCache; + if (item?.Location == location) + { + item.Position = position; + if (_lastPositions.FirstOrDefault() != item) + { + int index = _lastPositions.IndexOf(item); + if (index >= 0) + { + _lastPositions.RemoveAt(index); + } + + _lastPositions.Insert(0, item); + } + } + else + { + item = _lastPositions.Find(x => x.Location == location); + if (item == null) + { + item = new MediaLastPosition(location, position); + _lastPositions.Insert(0, item); + if (_lastPositions.Count > Capacity) + { + _lastPositions.RemoveAt(Capacity); + } + } + else + { + item.Position = position; + } + } + + _updateCache = item; + } + + public TimeSpan GetPosition(string location) + { + return _lastPositions.Find(x => x.Location == location)?.Position ?? TimeSpan.Zero; + } + + public void RemovePosition(string location) + { + LastUpdated = DateTimeOffset.Now; + if (_removeCache == location) return; + _lastPositions.RemoveAll(x => x.Location == location); + _removeCache = location; + } + + public void ClearAll() + { + LastUpdated = DateTimeOffset.Now; + _lastPositions.Clear(); + _updateCache = null; + _removeCache = null; + } + + public async Task SaveToDiskAsync() + { + try + { + await _filesService.SaveToDiskAsync(ApplicationData.Current.TemporaryFolder, SaveFileName, _lastPositions); + } + catch (FileLoadException) + { + // File in use. Skipped + } + } + + public async Task LoadFromDiskAsync() + { + try + { + List lastPositions = + await _filesService.LoadFromDiskAsync>(ApplicationData.Current.TemporaryFolder, SaveFileName); + lastPositions.Capacity = Capacity; + _lastPositions = lastPositions; + LastUpdated = DateTimeOffset.UtcNow; + } + catch (FileNotFoundException) + { + // pass + } + catch (Exception) + { + // pass + } + } +} diff --git a/Screenbox.Core/Helpers/LastPositionTracker.cs b/Screenbox.Core/Helpers/LastPositionTracker.cs deleted file mode 100644 index e612b85db..000000000 --- a/Screenbox.Core/Helpers/LastPositionTracker.cs +++ /dev/null @@ -1,129 +0,0 @@ -#nullable enable - -using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Messaging; -using Screenbox.Core.Messages; -using Screenbox.Core.Models; -using Screenbox.Core.Services; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Windows.Storage; - -namespace Screenbox.Core.Helpers -{ - public sealed class LastPositionTracker : ObservableRecipient, - IRecipient - { - 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; - private List _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 void UpdateLastPosition(string location, TimeSpan position) - { - LastUpdated = DateTimeOffset.Now; - _removeCache = null; - MediaLastPosition? item = _updateCache; - if (item?.Location == location) - { - item.Position = position; - if (_lastPositions.FirstOrDefault() != item) - { - int index = _lastPositions.IndexOf(item); - if (index >= 0) - { - _lastPositions.RemoveAt(index); - } - - _lastPositions.Insert(0, item); - } - } - else - { - item = _lastPositions.Find(x => x.Location == location); - if (item == null) - { - item = new MediaLastPosition(location, position); - _lastPositions.Insert(0, item); - if (_lastPositions.Count > Capacity) - { - _lastPositions.RemoveAt(Capacity); - } - } - else - { - item.Position = position; - } - } - - _updateCache = item; - } - - public TimeSpan GetPosition(string location) - { - return _lastPositions.Find(x => x.Location == location)?.Position ?? TimeSpan.Zero; - } - - public void RemovePosition(string location) - { - LastUpdated = DateTimeOffset.Now; - if (_removeCache == location) return; - _lastPositions.RemoveAll(x => x.Location == location); - _removeCache = location; - } - - public async Task SaveToDiskAsync() - { - try - { - await _filesService.SaveToDiskAsync(ApplicationData.Current.TemporaryFolder, SaveFileName, _lastPositions.ToList()); - } - catch (FileLoadException) - { - // File in use. Skipped - } - } - - public async Task LoadFromDiskAsync() - { - try - { - List lastPositions = - await _filesService.LoadFromDiskAsync>(ApplicationData.Current.TemporaryFolder, SaveFileName); - lastPositions.Capacity = Capacity; - _lastPositions = lastPositions; - LastUpdated = DateTimeOffset.UtcNow; - } - catch (FileNotFoundException) - { - // pass - } - catch (Exception) - { - // pass - } - } - } -} diff --git a/Screenbox.Core/Screenbox.Core.csproj b/Screenbox.Core/Screenbox.Core.csproj index c51129997..c8ecdb631 100644 --- a/Screenbox.Core/Screenbox.Core.csproj +++ b/Screenbox.Core/Screenbox.Core.csproj @@ -152,7 +152,7 @@ - + diff --git a/Screenbox.Core/Services/FilesService.cs b/Screenbox.Core/Services/FilesService.cs index 59dfe4f4a..be6e487a0 100644 --- a/Screenbox.Core/Services/FilesService.cs +++ b/Screenbox.Core/Services/FilesService.cs @@ -1,14 +1,14 @@ #nullable enable -using ProtoBuf; -using Screenbox.Core.Enums; -using Screenbox.Core.Helpers; -using Screenbox.Core.Models; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; +using ProtoBuf; +using Screenbox.Core.Enums; +using Screenbox.Core.Helpers; +using Screenbox.Core.Models; using Windows.Foundation; using Windows.Storage; using Windows.Storage.AccessCache; @@ -17,212 +17,211 @@ using Windows.Storage.Search; using Windows.System; -namespace Screenbox.Core.Services +namespace Screenbox.Core.Services; + +public sealed class FilesService : IFilesService { - public sealed class FilesService : IFilesService + public async Task GetNeighboringFilesQueryAsync(StorageFile file, QueryOptions? options = null) { - public async Task GetNeighboringFilesQueryAsync(StorageFile file, QueryOptions? options = null) + try { - try - { - StorageFolder? parent = await file.GetParentAsync(); - options ??= new QueryOptions(CommonFileQuery.DefaultQuery, FilesHelpers.SupportedFormats); - StorageFileQueryResult? queryResult = parent?.CreateFileQueryWithOptions(options); - return queryResult; - } - catch (Exception) - { - return null; - } + StorageFolder? parent = await file.GetParentAsync(); + options ??= new QueryOptions(CommonFileQuery.DefaultQuery, FilesHelpers.SupportedFormats); + StorageFileQueryResult? queryResult = parent?.CreateFileQueryWithOptions(options); + return queryResult; } - - public async Task GetNextFileAsync(IStorageFile currentFile, StorageFileQueryResult neighboringFilesQuery) + catch (Exception) { - // Due to limitations with NeighboringFilesQuery, manually find the next supported file - uint startIndex = await neighboringFilesQuery.FindStartIndexAsync(currentFile); - if (startIndex == uint.MaxValue) return null; - startIndex += 1; - - // The following line return a native vector view. - // It does not fetch all the files in the directory at once. - // No need for manual paging! - IReadOnlyList files = await neighboringFilesQuery.GetFilesAsync(startIndex, uint.MaxValue); - return files.FirstOrDefault(x => x.IsSupported()); + return null; } + } - public async Task GetPreviousFileAsync(IStorageFile currentFile, StorageFileQueryResult neighboringFilesQuery) - { - // Due to limitations with NeighboringFilesQuery, manually find the previous supported file - uint startIndex = await neighboringFilesQuery.FindStartIndexAsync(currentFile); - if (startIndex == uint.MaxValue) return null; - - // The following line return a native vector view. - // It does not fetch all the files in the directory at once. - // No need for manual paging! - IReadOnlyList files = await neighboringFilesQuery.GetFilesAsync(0, startIndex); - return files.LastOrDefault(x => x.IsSupported()); - } + public async Task GetNextFileAsync(IStorageFile currentFile, StorageFileQueryResult neighboringFilesQuery) + { + // Due to limitations with NeighboringFilesQuery, manually find the next supported file + uint startIndex = await neighboringFilesQuery.FindStartIndexAsync(currentFile); + if (startIndex == uint.MaxValue) return null; + startIndex += 1; + + // The following line return a native vector view. + // It does not fetch all the files in the directory at once. + // No need for manual paging! + IReadOnlyList files = await neighboringFilesQuery.GetFilesAsync(startIndex, uint.MaxValue); + return files.FirstOrDefault(x => x.IsSupported()); + } - public StorageItemQueryResult GetSupportedItems(StorageFolder folder) - { - // Don't use indexer when querying. Potential incomplete result. - QueryOptions queryOptions = new(CommonFileQuery.DefaultQuery, FilesHelpers.SupportedFormats); - return folder.CreateItemQueryWithOptions(queryOptions); - } + public async Task GetPreviousFileAsync(IStorageFile currentFile, StorageFileQueryResult neighboringFilesQuery) + { + // Due to limitations with NeighboringFilesQuery, manually find the previous supported file + uint startIndex = await neighboringFilesQuery.FindStartIndexAsync(currentFile); + if (startIndex == uint.MaxValue) return null; + + // The following line return a native vector view. + // It does not fetch all the files in the directory at once. + // No need for manual paging! + IReadOnlyList files = await neighboringFilesQuery.GetFilesAsync(0, startIndex); + return files.LastOrDefault(x => x.IsSupported()); + } - public IAsyncOperation GetSupportedItemCountAsync(StorageFolder folder) - { - QueryOptions queryOptions = new(CommonFileQuery.DefaultQuery, FilesHelpers.SupportedFormats); - return folder.CreateItemQueryWithOptions(queryOptions).GetItemCountAsync(); - } + public StorageItemQueryResult GetSupportedItems(StorageFolder folder) + { + // Don't use indexer when querying. Potential incomplete result. + QueryOptions queryOptions = new(CommonFileQuery.DefaultQuery, FilesHelpers.SupportedFormats); + return folder.CreateItemQueryWithOptions(queryOptions); + } + + public IAsyncOperation GetSupportedItemCountAsync(StorageFolder folder) + { + QueryOptions queryOptions = new(CommonFileQuery.DefaultQuery, FilesHelpers.SupportedFormats); + return folder.CreateItemQueryWithOptions(queryOptions).GetItemCountAsync(); + } + + public IAsyncOperation PickFileAsync(params string[] formats) + { + FileOpenPicker picker = GetFilePickerForFormats(formats); + return picker.PickSingleFileAsync(); + } - public IAsyncOperation PickFileAsync(params string[] formats) + public IAsyncOperation> PickMultipleFilesAsync(params string[] formats) + { + FileOpenPicker picker = GetFilePickerForFormats(formats); + return picker.PickMultipleFilesAsync(); + } + + public IAsyncOperation PickFolderAsync() + { + FolderPicker picker = new() { - FileOpenPicker picker = GetFilePickerForFormats(formats); - return picker.PickSingleFileAsync(); - } + SuggestedStartLocation = PickerLocationId.ComputerFolder + }; - public IAsyncOperation> PickMultipleFilesAsync(params string[] formats) + foreach (string supportedFormat in FilesHelpers.SupportedFormats) { - FileOpenPicker picker = GetFilePickerForFormats(formats); - return picker.PickMultipleFilesAsync(); + picker.FileTypeFilter.Add(supportedFormat); } - public IAsyncOperation PickFolderAsync() - { - FolderPicker picker = new() - { - SuggestedStartLocation = PickerLocationId.ComputerFolder - }; + return picker.PickSingleFolderAsync(); + } - foreach (string supportedFormat in FilesHelpers.SupportedFormats) - { - picker.FileTypeFilter.Add(supportedFormat); - } + public async Task SaveToDiskAsync(StorageFolder folder, string fileName, T source) + { + StorageFile file = await folder.CreateFileAsync(fileName, CreationCollisionOption.OpenIfExists); + await SaveToDiskAsync(file, source); + return file; + } - return picker.PickSingleFolderAsync(); - } + public async Task SaveToDiskAsync(StorageFile file, T source) + { + using var stream = await file.OpenStreamForWriteAsync(); + // using var dataWriter = new StreamWriter(stream); + // using var jsonWriter = new JsonTextWriter(dataWriter); + // var serializer = JsonSerializer.Create(); + // serializer.Serialize(jsonWriter, source); + Serializer.Serialize(stream, source); + stream.SetLength(stream.Position); // A weird quirk of protobuf-net + await stream.FlushAsync(); + } - public async Task SaveToDiskAsync(StorageFolder folder, string fileName, T source) - { - StorageFile file = await folder.CreateFileAsync(fileName, CreationCollisionOption.OpenIfExists); - await SaveToDiskAsync(file, source); - return file; - } + public async Task LoadFromDiskAsync(StorageFolder folder, string fileName) + { + StorageFile file = await folder.GetFileAsync(fileName); + return await LoadFromDiskAsync(file); + } + + public async Task LoadFromDiskAsync(StorageFile file) + { + using Stream readStream = await file.OpenStreamForReadAsync(); + return Serializer.Deserialize(readStream); + // using var dataReader = new StreamReader(readStream); + // using var jsonReader = new JsonTextReader(dataReader); + // var serializer = JsonSerializer.Create(); + // return serializer.Deserialize(jsonReader) ?? throw new NullReferenceException(); + } + + public async Task OpenFileLocationAsync(string path) + { + string? folderPath = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(folderPath)) + await Launcher.LaunchFolderPathAsync(folderPath); + } - public async Task SaveToDiskAsync(StorageFile file, T source) + public async Task OpenFileLocationAsync(StorageFile file) + { + StorageFolder? folder = await file.GetParentAsync(); + if (folder == null) { - using var stream = await file.OpenStreamForWriteAsync(); - // using var dataWriter = new StreamWriter(stream); - // using var jsonWriter = new JsonTextWriter(dataWriter); - // var serializer = JsonSerializer.Create(); - // serializer.Serialize(jsonWriter, source); - Serializer.Serialize(stream, source); - stream.SetLength(stream.Position); // A weird quirk of protobuf-net - await stream.FlushAsync(); + await OpenFileLocationAsync(file.Path); } - - public async Task LoadFromDiskAsync(StorageFolder folder, string fileName) + else { - StorageFile file = await folder.GetFileAsync(fileName); - return await LoadFromDiskAsync(file); + FolderLauncherOptions options = new(); + options.ItemsToSelect.Add(file); + await Launcher.LaunchFolderAsync(folder, options); } + } - public async Task LoadFromDiskAsync(StorageFile file) + public void AddToRecent(IStorageItem item) + { + string metadata = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + try { - using Stream readStream = await file.OpenStreamForReadAsync(); - return Serializer.Deserialize(readStream); - // using var dataReader = new StreamReader(readStream); - // using var jsonReader = new JsonTextReader(dataReader); - // var serializer = JsonSerializer.Create(); - // return serializer.Deserialize(jsonReader) ?? throw new NullReferenceException(); + StorageApplicationPermissions.MostRecentlyUsedList.Add(item, metadata); } - - public async Task OpenFileLocationAsync(string path) + catch (Exception) { - string? folderPath = Path.GetDirectoryName(path); - if (!string.IsNullOrEmpty(folderPath)) - await Launcher.LaunchFolderPathAsync(folderPath); + // System.Exception: Element not found. (Exception from HRESULT: 0x80070490) + // Ownership issue? } + } - public async Task OpenFileLocationAsync(StorageFile file) + public async Task GetMediaInfoAsync(StorageFile file) + { + MediaPlaybackType mediaType = FilesHelpers.GetMediaTypeForFile(file); + if (!file.IsAvailable) return new MediaInfo(mediaType); + + try { - StorageFolder? folder = await file.GetParentAsync(); - if (folder == null) + BasicProperties basicProperties = await file.GetBasicPropertiesAsync(); + switch (mediaType) { - await OpenFileLocationAsync(file.Path); - } - else - { - FolderLauncherOptions options = new(); - options.ItemsToSelect.Add(file); - await Launcher.LaunchFolderAsync(folder, options); + case MediaPlaybackType.Video: + VideoProperties videoProperties = await file.Properties.GetVideoPropertiesAsync(); + return new MediaInfo(basicProperties, videoProperties); + case MediaPlaybackType.Music: + MusicProperties musicProperties = await file.Properties.GetMusicPropertiesAsync(); + return new MediaInfo(basicProperties, musicProperties); } } - - public void AddToRecent(IStorageItem item) + catch (Exception e) { - string metadata = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); - try - { - StorageApplicationPermissions.MostRecentlyUsedList.Add(item, metadata); - } - catch (Exception) - { - // System.Exception: Element not found. (Exception from HRESULT: 0x80070490) - // Ownership issue? - } + // System.Exception: The RPC server is unavailable. + if (e.HResult != unchecked((int)0x800706BA)) + LogService.Log(e); } - public async Task GetMediaInfoAsync(StorageFile file) - { - MediaPlaybackType mediaType = FilesHelpers.GetMediaTypeForFile(file); - if (!file.IsAvailable) return new MediaInfo(mediaType); + return new MediaInfo(mediaType); + } - try - { - BasicProperties basicProperties = await file.GetBasicPropertiesAsync(); - switch (mediaType) - { - case MediaPlaybackType.Video: - VideoProperties videoProperties = await file.Properties.GetVideoPropertiesAsync(); - return new MediaInfo(basicProperties, videoProperties); - case MediaPlaybackType.Music: - MusicProperties musicProperties = await file.Properties.GetMusicPropertiesAsync(); - return new MediaInfo(basicProperties, musicProperties); - } - } - catch (Exception e) - { - // System.Exception: The RPC server is unavailable. - if (e.HResult != unchecked((int)0x800706BA)) - LogService.Log(e); - } + private FileOpenPicker GetFilePickerForFormats(IReadOnlyCollection formats) + { + FileOpenPicker picker = new() + { + ViewMode = PickerViewMode.Thumbnail, + SuggestedStartLocation = PickerLocationId.ComputerFolder + }; - return new MediaInfo(mediaType); + IEnumerable fileTypes = formats; + if (formats.Count == 0) + { + fileTypes = FilesHelpers.SupportedFormats; + picker.FileTypeFilter.Add("*"); } - private FileOpenPicker GetFilePickerForFormats(IReadOnlyCollection formats) + foreach (string? fileType in fileTypes) { - FileOpenPicker picker = new() - { - ViewMode = PickerViewMode.Thumbnail, - SuggestedStartLocation = PickerLocationId.ComputerFolder - }; - - IEnumerable fileTypes = formats; - if (formats.Count == 0) - { - fileTypes = FilesHelpers.SupportedFormats; - picker.FileTypeFilter.Add("*"); - } - - foreach (string? fileType in fileTypes) - { - picker.FileTypeFilter.Add(fileType); - } - - return picker; + picker.FileTypeFilter.Add(fileType); } + + return picker; } } diff --git a/Screenbox.Core/Services/ISettingsService.cs b/Screenbox.Core/Services/ISettingsService.cs index beccce782..942cac4b4 100644 --- a/Screenbox.Core/Services/ISettingsService.cs +++ b/Screenbox.Core/Services/ISettingsService.cs @@ -1,31 +1,36 @@ using Screenbox.Core.Enums; using Windows.Media; -namespace Screenbox.Core.Services +namespace Screenbox.Core.Services; + +public interface ISettingsService { - public interface ISettingsService - { - PlayerAutoResizeOption PlayerAutoResize { get; set; } - bool UseIndexer { get; set; } - bool PlayerVolumeGesture { get; set; } - bool PlayerSeekGesture { get; set; } - bool PlayerTapGesture { get; set; } - bool PlayerShowControls { get; set; } - bool PlayerShowChapters { get; set; } - int PlayerControlsHideDelay { get; set; } - int PersistentVolume { get; set; } - string PersistentSubtitleLanguage { get; set; } - 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; } - bool AdvancedMode { get; set; } - VideoUpscaleOption VideoUpscale { get; set; } - bool UseMultipleInstances { get; set; } - string LivelyActivePath { get; set; } - MediaPlaybackAutoRepeatMode PersistentRepeatMode { get; set; } - } + PlayerAutoResizeOption PlayerAutoResize { get; set; } + bool UseIndexer { get; set; } + bool PlayerVolumeGesture { get; set; } + bool PlayerSeekGesture { get; set; } + bool PlayerTapGesture { get; set; } + bool PlayerShowControls { get; set; } + bool PlayerShowChapters { get; set; } + int PlayerControlsHideDelay { get; set; } + int PersistentVolume { get; set; } + string PersistentSubtitleLanguage { get; set; } + 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; } + bool AdvancedMode { get; set; } + VideoUpscaleOption VideoUpscale { get; set; } + bool UseMultipleInstances { get; set; } + string LivelyActivePath { get; set; } + MediaPlaybackAutoRepeatMode PersistentRepeatMode { get; set; } + + /// + /// Gets or sets a value that indicates whether the playback position should be saved + /// and restored between sessions. + /// + bool PersistPlaybackPosition { get; set; } } diff --git a/Screenbox.Core/Services/SettingsService.cs b/Screenbox.Core/Services/SettingsService.cs index 946eb5b0e..e19a55561 100644 --- a/Screenbox.Core/Services/SettingsService.cs +++ b/Screenbox.Core/Services/SettingsService.cs @@ -8,224 +8,231 @@ using Windows.Media; using Windows.Storage; -namespace Screenbox.Core.Services -{ - public sealed class SettingsService : ISettingsService - { - private static IPropertySet SettingsStorage => ApplicationData.Current.LocalSettings.Values; - - private const string GeneralThemeKey = "General/Theme"; - private const string PlayerAutoResizeKey = "Player/AutoResize"; - private const string PlayerVolumeGestureKey = "Player/Gesture/Volume"; - private const string PlayerSeekGestureKey = "Player/Gesture/Seek"; - private const string PlayerTapGestureKey = "Player/Gesture/Tap"; - private const string PlayerShowControlsKey = "Player/ShowControls"; - private const string PlayerControlsHideDelayKey = "Player/ControlsHideDelay"; - private const string PlayerLivelyPathKey = "Player/Lively/Path"; - private const string LibrariesUseIndexerKey = "Libraries/UseIndexer"; - 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 AdvancedVideoUpscaleKey = "Advanced/VideoUpscale"; - private const string AdvancedMultipleInstancesKey = "Advanced/MultipleInstances"; - private const string GlobalArgumentsKey = "Values/GlobalArguments"; - private const string PersistentVolumeKey = "Values/Volume"; - private const string MaxVolumeKey = "Values/MaxVolume"; - private const string PersistentRepeatModeKey = "Values/RepeatMode"; - private const string PersistentSubtitleLanguageKey = "Values/SubtitleLanguage"; - private const string PlayerShowChaptersKey = "Player/ShowChapters"; - - public bool UseIndexer - { - get => GetValue(LibrariesUseIndexerKey); - set => SetValue(LibrariesUseIndexerKey, value); - } +namespace Screenbox.Core.Services; - public ThemeOption Theme - { - get => (ThemeOption)GetValue(GeneralThemeKey); - set => SetValue(GeneralThemeKey, (int)value); - } +public sealed class SettingsService : ISettingsService +{ + private static IPropertySet SettingsStorage => ApplicationData.Current.LocalSettings.Values; + + private const string GeneralThemeKey = "General/Theme"; + private const string PlayerAutoResizeKey = "Player/AutoResize"; + private const string PlayerVolumeGestureKey = "Player/Gesture/Volume"; + private const string PlayerSeekGestureKey = "Player/Gesture/Seek"; + private const string PlayerTapGestureKey = "Player/Gesture/Tap"; + private const string PlayerShowControlsKey = "Player/ShowControls"; + private const string PlayerControlsHideDelayKey = "Player/ControlsHideDelay"; + private const string PlayerLivelyPathKey = "Player/Lively/Path"; + private const string LibrariesUseIndexerKey = "Libraries/UseIndexer"; + 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 AdvancedVideoUpscaleKey = "Advanced/VideoUpscale"; + private const string AdvancedMultipleInstancesKey = "Advanced/MultipleInstances"; + private const string GlobalArgumentsKey = "Values/GlobalArguments"; + private const string PersistentVolumeKey = "Values/Volume"; + private const string MaxVolumeKey = "Values/MaxVolume"; + private const string PersistentRepeatModeKey = "Values/RepeatMode"; + private const string PersistentSubtitleLanguageKey = "Values/SubtitleLanguage"; + private const string PlayerShowChaptersKey = "Player/ShowChapters"; + private const string PrivacyPersistPlaybackPosition = "Privacy/PersistPlaybackPosition"; + + public bool UseIndexer + { + get => GetValue(LibrariesUseIndexerKey); + set => SetValue(LibrariesUseIndexerKey, value); + } - public PlayerAutoResizeOption PlayerAutoResize - { - get => (PlayerAutoResizeOption)GetValue(PlayerAutoResizeKey); - set => SetValue(PlayerAutoResizeKey, (int)value); - } + public ThemeOption Theme + { + get => (ThemeOption)GetValue(GeneralThemeKey); + set => SetValue(GeneralThemeKey, (int)value); + } - public bool PlayerVolumeGesture - { - get => GetValue(PlayerVolumeGestureKey); - set => SetValue(PlayerVolumeGestureKey, value); - } + public PlayerAutoResizeOption PlayerAutoResize + { + get => (PlayerAutoResizeOption)GetValue(PlayerAutoResizeKey); + set => SetValue(PlayerAutoResizeKey, (int)value); + } - public bool PlayerSeekGesture - { - get => GetValue(PlayerSeekGestureKey); - set => SetValue(PlayerSeekGestureKey, value); - } + public bool PlayerVolumeGesture + { + get => GetValue(PlayerVolumeGestureKey); + set => SetValue(PlayerVolumeGestureKey, value); + } - public bool PlayerTapGesture - { - get => GetValue(PlayerTapGestureKey); - set => SetValue(PlayerTapGestureKey, value); - } + public bool PlayerSeekGesture + { + get => GetValue(PlayerSeekGestureKey); + set => SetValue(PlayerSeekGestureKey, value); + } - public int PersistentVolume - { - get => GetValue(PersistentVolumeKey); - set => SetValue(PersistentVolumeKey, value); - } + public bool PlayerTapGesture + { + get => GetValue(PlayerTapGestureKey); + set => SetValue(PlayerTapGestureKey, value); + } - public string PersistentSubtitleLanguage - { - get => GetValue(PersistentSubtitleLanguageKey) ?? string.Empty; - set => SetValue(PersistentSubtitleLanguageKey, value); - } + public int PersistentVolume + { + get => GetValue(PersistentVolumeKey); + set => SetValue(PersistentVolumeKey, value); + } - public int MaxVolume - { - get => GetValue(MaxVolumeKey); - set => SetValue(MaxVolumeKey, value); - } + public string PersistentSubtitleLanguage + { + get => GetValue(PersistentSubtitleLanguageKey) ?? string.Empty; + set => SetValue(PersistentSubtitleLanguageKey, value); + } - public bool ShowRecent - { - get => GetValue(GeneralShowRecent); - set => SetValue(GeneralShowRecent, value); - } + public int MaxVolume + { + get => GetValue(MaxVolumeKey); + set => SetValue(MaxVolumeKey, value); + } - public bool EnqueueAllFilesInFolder - { - get => GetValue(GeneralEnqueueAllInFolder); - set => SetValue(GeneralEnqueueAllInFolder, value); - } + public bool ShowRecent + { + get => GetValue(GeneralShowRecent); + set => SetValue(GeneralShowRecent, value); + } - public bool RestorePlaybackPosition - { - get => GetValue(GeneralRestorePlaybackPosition); - set => SetValue(GeneralRestorePlaybackPosition, value); - } + public bool EnqueueAllFilesInFolder + { + get => GetValue(GeneralEnqueueAllInFolder); + set => SetValue(GeneralEnqueueAllInFolder, value); + } - public bool PlayerShowControls - { - get => GetValue(PlayerShowControlsKey); - set => SetValue(PlayerShowControlsKey, value); - } + public bool RestorePlaybackPosition + { + get => GetValue(GeneralRestorePlaybackPosition); + set => SetValue(GeneralRestorePlaybackPosition, value); + } - public int PlayerControlsHideDelay - { - get => GetValue(PlayerControlsHideDelayKey); - set => SetValue(PlayerControlsHideDelayKey, value); - } + public bool PlayerShowControls + { + get => GetValue(PlayerShowControlsKey); + set => SetValue(PlayerShowControlsKey, value); + } - public bool SearchRemovableStorage - { - get => GetValue(LibrariesSearchRemovableStorageKey); - set => SetValue(LibrariesSearchRemovableStorageKey, value); - } + public int PlayerControlsHideDelay + { + get => GetValue(PlayerControlsHideDelayKey); + set => SetValue(PlayerControlsHideDelayKey, value); + } - public MediaPlaybackAutoRepeatMode PersistentRepeatMode - { - get => (MediaPlaybackAutoRepeatMode)GetValue(PersistentRepeatModeKey); - set => SetValue(PersistentRepeatModeKey, (int)value); - } + public bool SearchRemovableStorage + { + get => GetValue(LibrariesSearchRemovableStorageKey); + set => SetValue(LibrariesSearchRemovableStorageKey, value); + } - public string GlobalArguments - { - get => GetValue(GlobalArgumentsKey) ?? string.Empty; - set => SetValue(GlobalArgumentsKey, SanitizeArguments(value)); - } + public MediaPlaybackAutoRepeatMode PersistentRepeatMode + { + get => (MediaPlaybackAutoRepeatMode)GetValue(PersistentRepeatModeKey); + set => SetValue(PersistentRepeatModeKey, (int)value); + } - public bool AdvancedMode - { - get => GetValue(AdvancedModeKey); - set => SetValue(AdvancedModeKey, value); - } + public string GlobalArguments + { + get => GetValue(GlobalArgumentsKey) ?? string.Empty; + set => SetValue(GlobalArgumentsKey, SanitizeArguments(value)); + } - public VideoUpscaleOption VideoUpscale - { - get => (VideoUpscaleOption)GetValue(AdvancedVideoUpscaleKey); - set => SetValue(AdvancedVideoUpscaleKey, (int)value); - } + public bool AdvancedMode + { + get => GetValue(AdvancedModeKey); + set => SetValue(AdvancedModeKey, value); + } - public bool UseMultipleInstances - { - get => GetValue(AdvancedMultipleInstancesKey); - set => SetValue(AdvancedMultipleInstancesKey, value); - } + public VideoUpscaleOption VideoUpscale + { + get => (VideoUpscaleOption)GetValue(AdvancedVideoUpscaleKey); + set => SetValue(AdvancedVideoUpscaleKey, (int)value); + } - public string LivelyActivePath - { - get => GetValue(PlayerLivelyPathKey) ?? string.Empty; - set => SetValue(PlayerLivelyPathKey, value); - } + public bool UseMultipleInstances + { + get => GetValue(AdvancedMultipleInstancesKey); + set => SetValue(AdvancedMultipleInstancesKey, value); + } - public bool PlayerShowChapters - { - get => GetValue(PlayerShowChaptersKey); - set => SetValue(PlayerShowChaptersKey, value); - } + public string LivelyActivePath + { + get => GetValue(PlayerLivelyPathKey) ?? string.Empty; + set => SetValue(PlayerLivelyPathKey, value); + } - public SettingsService() - { - SetDefault(PlayerAutoResizeKey, (int)PlayerAutoResizeOption.Never); - SetDefault(PlayerVolumeGestureKey, true); - SetDefault(PlayerSeekGestureKey, true); - SetDefault(PlayerTapGestureKey, true); - SetDefault(PlayerShowControlsKey, true); - SetDefault(PlayerControlsHideDelayKey, 3); - SetDefault(PersistentVolumeKey, 100); - SetDefault(MaxVolumeKey, 100); - SetDefault(LibrariesUseIndexerKey, true); - SetDefault(LibrariesSearchRemovableStorageKey, true); - SetDefault(GeneralShowRecent, true); - SetDefault(PersistentRepeatModeKey, (int)MediaPlaybackAutoRepeatMode.None); - SetDefault(AdvancedModeKey, false); - SetDefault(AdvancedVideoUpscaleKey, (int)VideoUpscaleOption.Linear); - SetDefault(AdvancedMultipleInstancesKey, false); - SetDefault(GlobalArgumentsKey, string.Empty); - SetDefault(PlayerShowChaptersKey, true); - - // Device family specific overrides - if (SystemInformation.IsXbox) - { - SetValue(PlayerTapGestureKey, false); - SetValue(PlayerSeekGestureKey, false); - SetValue(PlayerVolumeGestureKey, false); - SetValue(PlayerAutoResizeKey, (int)PlayerAutoResizeOption.Never); - SetValue(PlayerShowControlsKey, true); - } - } + public bool PlayerShowChapters + { + get => GetValue(PlayerShowChaptersKey); + set => SetValue(PlayerShowChaptersKey, value); + } - private static T? GetValue(string key) - { - if (SettingsStorage.TryGetValue(key, out object value)) - { - return (T)value; - } + public bool PersistPlaybackPosition + { + get => GetValue(PrivacyPersistPlaybackPosition); + set => SetValue(PrivacyPersistPlaybackPosition, value); + } - return default; + public SettingsService() + { + SetDefault(PlayerAutoResizeKey, (int)PlayerAutoResizeOption.Never); + SetDefault(PlayerVolumeGestureKey, true); + SetDefault(PlayerSeekGestureKey, true); + SetDefault(PlayerTapGestureKey, true); + SetDefault(PlayerShowControlsKey, true); + SetDefault(PlayerControlsHideDelayKey, 3); + SetDefault(PersistentVolumeKey, 100); + SetDefault(MaxVolumeKey, 100); + SetDefault(LibrariesUseIndexerKey, true); + SetDefault(LibrariesSearchRemovableStorageKey, true); + SetDefault(GeneralShowRecent, true); + SetDefault(PersistentRepeatModeKey, (int)MediaPlaybackAutoRepeatMode.None); + SetDefault(AdvancedModeKey, false); + SetDefault(AdvancedVideoUpscaleKey, (int)VideoUpscaleOption.Linear); + SetDefault(AdvancedMultipleInstancesKey, false); + SetDefault(GlobalArgumentsKey, string.Empty); + SetDefault(PlayerShowChaptersKey, true); + SetDefault(PrivacyPersistPlaybackPosition, true); + + // Device family specific overrides + if (SystemInformation.IsXbox) + { + SetValue(PlayerTapGestureKey, false); + SetValue(PlayerSeekGestureKey, false); + SetValue(PlayerVolumeGestureKey, false); + SetValue(PlayerAutoResizeKey, (int)PlayerAutoResizeOption.Never); + SetValue(PlayerShowControlsKey, true); } + } - private static void SetValue(string key, T value) + private static T? GetValue(string key) + { + if (SettingsStorage.TryGetValue(key, out object value)) { - SettingsStorage[key] = value; + return (T)value; } - private static void SetDefault(string key, T value) - { - if (SettingsStorage.ContainsKey(key) && SettingsStorage[key] is T) return; - SettingsStorage[key] = value; - } + return default; + } - private static string SanitizeArguments(string raw) - { - string[] args = raw.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries) - .Where(s => s.StartsWith('-') && s != "--").ToArray(); - return string.Join(' ', args); - } + private static void SetValue(string key, T value) + { + SettingsStorage[key] = value; + } + + private static void SetDefault(string key, T value) + { + if (SettingsStorage.ContainsKey(key) && SettingsStorage[key] is T) return; + SettingsStorage[key] = value; + } + + private static string SanitizeArguments(string raw) + { + string[] args = raw.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Where(s => s.StartsWith('-') && s != "--").ToArray(); + return string.Join(' ', args); } } diff --git a/Screenbox.Core/ViewModels/MediaListViewModel.cs b/Screenbox.Core/ViewModels/MediaListViewModel.cs index 365e12c0e..7a0f4f363 100644 --- a/Screenbox.Core/ViewModels/MediaListViewModel.cs +++ b/Screenbox.Core/ViewModels/MediaListViewModel.cs @@ -288,7 +288,7 @@ async partial void OnCurrentItemChanged(MediaViewModel? value) // Async updates await Task.WhenAll( - AddToRecent(value?.Source), + _settingsService.ShowRecent ? AddToRecent(value?.Source) : Task.CompletedTask, _transportControlsService.UpdateTransportControlsDisplayAsync(value), UpdateMediaBufferAsync() ); diff --git a/Screenbox.Core/ViewModels/SeekBarViewModel.cs b/Screenbox.Core/ViewModels/SeekBarViewModel.cs index fedad0e68..30fabe2e9 100644 --- a/Screenbox.Core/ViewModels/SeekBarViewModel.cs +++ b/Screenbox.Core/ViewModels/SeekBarViewModel.cs @@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.Messaging.Messages; using CommunityToolkit.WinUI; using Screenbox.Core.Contexts; +using Screenbox.Core.Controllers; using Screenbox.Core.Enums; using Screenbox.Core.Events; using Screenbox.Core.Helpers; @@ -21,388 +22,390 @@ using Windows.UI.Xaml; using Windows.UI.Xaml.Controls.Primitives; -namespace Screenbox.Core.ViewModels +namespace Screenbox.Core.ViewModels; + +public sealed partial class SeekBarViewModel : + ObservableRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient, + IRecipient>, + IRecipient> { - public sealed partial class SeekBarViewModel : - ObservableRecipient, - IRecipient, - IRecipient, - IRecipient, - IRecipient, - IRecipient>, - IRecipient> - { - [ObservableProperty] private double _length; + [ObservableProperty] private double _length; - [ObservableProperty] private double _time; + [ObservableProperty] private double _time; - [ObservableProperty] private bool _isSeekable; + [ObservableProperty] private bool _isSeekable; - [ObservableProperty] private bool _bufferingVisible; + [ObservableProperty] private bool _bufferingVisible; - [ObservableProperty] private double _previewTime; + [ObservableProperty] private double _previewTime; - [ObservableProperty] private bool _shouldShowPreview; + [ObservableProperty] private bool _shouldShowPreview; - [ObservableProperty] private bool _shouldHandleKeyDown; + [ObservableProperty] private bool _shouldHandleKeyDown; - public ObservableCollection Chapters { get; } + public ObservableCollection Chapters { get; } - private TimeSpan NaturalDuration => TimeSpan.FromMilliseconds(Length); + private TimeSpan NaturalDuration => TimeSpan.FromMilliseconds(Length); - private TimeSpan Position - { - get => TimeSpan.FromMilliseconds(Time); - set => Time = value.TotalMilliseconds; - } + private TimeSpan Position + { + get => TimeSpan.FromMilliseconds(Time); + set => Time = value.TotalMilliseconds; + } - private IMediaPlayer? MediaPlayer => _playerContext.MediaPlayer; - - private readonly ISettingsService _settingsService; - private readonly PlayerContext _playerContext; - 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(ISettingsService settingsService, LastPositionTracker lastPositionTracker, - PlayerContext playerContext) + private IMediaPlayer? MediaPlayer => _playerContext.MediaPlayer; + + private readonly ISettingsService _settingsService; + private readonly PlayerContext _playerContext; + 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(ISettingsService settingsService, LastPositionTracker lastPositionTracker, + PlayerContext playerContext) + { + _settingsService = settingsService; + _lastPositionTracker = lastPositionTracker; + _playerContext = playerContext; + _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); + _bufferingTimer = _dispatcherQueue.CreateTimer(); + _seekTimer = _dispatcherQueue.CreateTimer(); + _originalPositionTimer = _dispatcherQueue.CreateTimer(); + _originalPositionTimer.IsRepeating = false; + _shouldShowPreview = true; + _shouldHandleKeyDown = true; + Chapters = new ObservableCollection(); + + if (MediaPlayer != null) { - _settingsService = settingsService; - _lastPositionTracker = lastPositionTracker; - _playerContext = playerContext; - _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); - _bufferingTimer = _dispatcherQueue.CreateTimer(); - _seekTimer = _dispatcherQueue.CreateTimer(); - _originalPositionTimer = _dispatcherQueue.CreateTimer(); - _originalPositionTimer.IsRepeating = false; - _shouldShowPreview = true; - _shouldHandleKeyDown = true; - Chapters = new ObservableCollection(); - - 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; - } - - // Activate the view model's messenger - IsActive = true; + MediaPlayer.PlaybackStateChanged += OnPlaybackStateChanged; + MediaPlayer.NaturalDurationChanged += OnNaturalDurationChanged; + MediaPlayer.PositionChanged += OnPositionChanged; + MediaPlayer.MediaEnded += OnEndReached; + MediaPlayer.BufferingStarted += OnBufferingStarted; + MediaPlayer.BufferingEnded += OnBufferingEnded; + MediaPlayer.PlaybackItemChanged += OnPlaybackItemChanged; + MediaPlayer.CanSeekChanged += OnCanSeekChanged; } - public void Receive(PlaylistCurrentItemChangedMessage message) + // Activate the view model's messenger + IsActive = true; + } + + public void Receive(PlaylistCurrentItemChangedMessage message) + { + _lastTrackedPosition = TimeSpan.Zero; + _currentItem = message.Value; + if (message.Value != null && _lastPositionTracker.IsLoaded) { - _lastTrackedPosition = TimeSpan.Zero; - _currentItem = message.Value; - if (message.Value != null && _lastPositionTracker.IsLoaded) - { - RestoreLastPosition(message.Value); - } + RestoreLastPosition(message.Value); } + } + + public void Receive(PropertyChangedMessage message) + { + ShouldHandleKeyDown = message.NewValue != PlayerVisibilityState.Visible; + } - public void Receive(PropertyChangedMessage message) + public void Receive(PlayerControlsVisibilityChangedMessage message) + { + if (!message.Value && ShouldShowPreview) { - ShouldHandleKeyDown = message.NewValue != PlayerVisibilityState.Visible; + ShouldShowPreview = false; } + } + + public async void Receive(PropertyChangedMessage message) + { + if (message.Sender is not PlayerContext) return; - public void Receive(PlayerControlsVisibilityChangedMessage message) + if (message.OldValue is { } oldPlayer) { - if (!message.Value && ShouldShowPreview) - { - ShouldShowPreview = false; - } + oldPlayer.PlaybackStateChanged -= OnPlaybackStateChanged; + oldPlayer.NaturalDurationChanged -= OnNaturalDurationChanged; + oldPlayer.PositionChanged -= OnPositionChanged; + oldPlayer.MediaEnded -= OnEndReached; + oldPlayer.BufferingStarted -= OnBufferingStarted; + oldPlayer.BufferingEnded -= OnBufferingEnded; + oldPlayer.PlaybackItemChanged -= OnPlaybackItemChanged; + oldPlayer.CanSeekChanged -= OnCanSeekChanged; } - public async void Receive(PropertyChangedMessage message) + if (MediaPlayer != null) { - if (message.Sender is not PlayerContext) return; - - if (message.OldValue is { } oldPlayer) - { - oldPlayer.PlaybackStateChanged -= OnPlaybackStateChanged; - oldPlayer.NaturalDurationChanged -= OnNaturalDurationChanged; - oldPlayer.PositionChanged -= OnPositionChanged; - oldPlayer.MediaEnded -= OnEndReached; - oldPlayer.BufferingStarted -= OnBufferingStarted; - oldPlayer.BufferingEnded -= OnBufferingEnded; - oldPlayer.PlaybackItemChanged -= OnPlaybackItemChanged; - oldPlayer.CanSeekChanged -= OnCanSeekChanged; - } - - 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; + + if (!_lastPositionTracker.IsLoaded) { - MediaPlayer.PlaybackStateChanged += OnPlaybackStateChanged; - MediaPlayer.NaturalDurationChanged += OnNaturalDurationChanged; - MediaPlayer.PositionChanged += OnPositionChanged; - MediaPlayer.MediaEnded += OnEndReached; - MediaPlayer.BufferingStarted += OnBufferingStarted; - MediaPlayer.BufferingEnded += OnBufferingEnded; - MediaPlayer.PlaybackItemChanged += OnPlaybackItemChanged; - MediaPlayer.CanSeekChanged += OnCanSeekChanged; - - if (!_lastPositionTracker.IsLoaded) + await _lastPositionTracker.LoadFromDiskAsync(); + if (_currentItem != null) { - await _lastPositionTracker.LoadFromDiskAsync(); - if (_currentItem != null) - { - RestoreLastPosition(_currentItem); - } + RestoreLastPosition(_currentItem); } } } + } - public void Receive(TimeChangeOverrideMessage message) - { - _timeChangeOverride = message.Value; - } + public void Receive(TimeChangeOverrideMessage message) + { + _timeChangeOverride = message.Value; + } - public void Receive(ChangeTimeRequestMessage message) - { - var result = UpdatePosition(message.Value, message.IsOffset, message.Debounce); - message.Reply(result); - } + public void Receive(ChangeTimeRequestMessage message) + { + var result = UpdatePosition(message.Value, message.IsOffset, message.Debounce); + message.Reply(result); + } - public void OnSeekBarPointerEvent(bool pressed) - { - _timeChangeOverride = pressed; - } + public void OnSeekBarPointerEvent(bool pressed) + { + _timeChangeOverride = pressed; + } - public void UpdatePreviewTime(double normalizedPosition) - { - normalizedPosition = Math.Clamp(normalizedPosition, 0, 1); - PreviewTime = (long)(normalizedPosition * Length); - } + public void UpdatePreviewTime(double normalizedPosition) + { + normalizedPosition = Math.Clamp(normalizedPosition, 0, 1); + PreviewTime = (long)(normalizedPosition * Length); + } - public void OnSeekBarPointerWheelChanged(double pointerWheelDelta) - { - if (!IsSeekable || MediaPlayer == null) return; - var controlPressed = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control) == CoreVirtualKeyStates.Down; - var shiftPressed = Window.Current.CoreWindow.GetKeyState(VirtualKey.Shift) == CoreVirtualKeyStates.Down; - var delta = 5000; - if (controlPressed) delta = 10000; - if (shiftPressed) delta = 2000; - var result = UpdatePosition(TimeSpan.FromMilliseconds(pointerWheelDelta > 0 ? delta : -delta), true, true); - TimeSpan offset = result.NewPosition - result.OriginalPosition; - string extra = $"{(offset > TimeSpan.Zero ? '+' : string.Empty)}{Humanizer.ToDuration(offset)}"; - Messenger.SendPositionStatus(result.NewPosition, result.NaturalDuration, extra); - } + public void OnSeekBarPointerWheelChanged(double pointerWheelDelta) + { + if (!IsSeekable || MediaPlayer == null) return; + var controlPressed = Window.Current.CoreWindow.GetKeyState(VirtualKey.Control) == CoreVirtualKeyStates.Down; + var shiftPressed = Window.Current.CoreWindow.GetKeyState(VirtualKey.Shift) == CoreVirtualKeyStates.Down; + var delta = 5000; + if (controlPressed) delta = 10000; + if (shiftPressed) delta = 2000; + var result = UpdatePosition(TimeSpan.FromMilliseconds(pointerWheelDelta > 0 ? delta : -delta), true, true); + TimeSpan offset = result.NewPosition - result.OriginalPosition; + string extra = $"{(offset > TimeSpan.Zero ? '+' : string.Empty)}{Humanizer.ToDuration(offset)}"; + Messenger.SendPositionStatus(result.NewPosition, result.NaturalDuration, extra); + } - public void OnSeekBarValueChanged(object sender, RangeBaseValueChangedEventArgs args) + 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 + // coming from user. + // We can detect user interaction by checking if Time != args.NewValue + if (IsSeekable && MediaPlayer != null && Math.Abs(Time - args.NewValue) > 50) { - 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 - // coming from user. - // We can detect user interaction by checking if Time != args.NewValue - if (IsSeekable && MediaPlayer != null && Math.Abs(Time - args.NewValue) > 50) + Time = args.NewValue; + double currentMs = MediaPlayer.Position.TotalMilliseconds; + double newDiffMs = Math.Abs(args.NewValue - currentMs); + bool shouldUpdate = newDiffMs > 400; + bool shouldOverride = _timeChangeOverride && newDiffMs > 100; + bool paused = MediaPlayer.PlaybackState is MediaPlaybackState.Paused or MediaPlaybackState.Buffering; + if (shouldUpdate || paused || shouldOverride) { - Time = args.NewValue; - double currentMs = MediaPlayer.Position.TotalMilliseconds; - double newDiffMs = Math.Abs(args.NewValue - currentMs); - bool shouldUpdate = newDiffMs > 400; - bool shouldOverride = _timeChangeOverride && newDiffMs > 100; - bool paused = MediaPlayer.PlaybackState is MediaPlaybackState.Paused or MediaPlaybackState.Buffering; - if (shouldUpdate || paused || shouldOverride) - { - SetPlayerPosition(newPosition, true); - } + SetPlayerPosition(newPosition, true); } - - UpdateLastPosition(newPosition); } - private void RestoreLastPosition(MediaViewModel media) + UpdateLastPosition(newPosition); + } + + private void RestoreLastPosition(MediaViewModel media) + { + if (!_settingsService.PersistPlaybackPosition) return; + + TimeSpan lastPosition = _lastPositionTracker.GetPosition(media.Location); + if (lastPosition <= TimeSpan.Zero) return; + if (_settingsService.RestorePlaybackPosition) { - TimeSpan lastPosition = _lastPositionTracker.GetPosition(media.Location); - if (lastPosition <= TimeSpan.Zero) return; - if (_settingsService.RestorePlaybackPosition) + if (media.IsPlaying ?? false) { - if (media.IsPlaying ?? false) - { - _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => SetPlayerPosition(lastPosition, false)); - _lastTrackedPosition = TimeSpan.Zero; - } - else - { - // Media is not seekable yet, so we need to wait for the PlaybackStateChanged event - _lastTrackedPosition = lastPosition; - } + _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => SetPlayerPosition(lastPosition, false)); + _lastTrackedPosition = TimeSpan.Zero; } else { - Messenger.Send(new RaiseResumePositionNotificationMessage(lastPosition)); + // 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; + _originalPositionTimer.Debounce(() => _originalPosition = currentPosition, TimeSpan.FromSeconds(1), true); - private PositionChangedResult UpdatePosition(TimeSpan position, bool isOffset, bool debounce) + // Assume UI thread + Position = isOffset ? (currentPosition + position) switch { - TimeSpan currentPosition = Position; - _originalPositionTimer.Debounce(() => _originalPosition = currentPosition, TimeSpan.FromSeconds(1), true); + var newPosition when newPosition < TimeSpan.Zero => TimeSpan.Zero, + var newPosition when newPosition > NaturalDuration => NaturalDuration, + var newPosition => newPosition + } : position; + SetPlayerPosition(Position, debounce); - // Assume UI thread - 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, Position, _originalPosition, NaturalDuration); + } - return new PositionChangedResult(currentPosition, Position, _originalPosition, NaturalDuration); + private void SetPlayerPosition(TimeSpan position, bool debounce) + { + if (!IsSeekable || MediaPlayer == null) return; + if (debounce) + { + _seekTimer.Debounce(() => MediaPlayer.Position = position, TimeSpan.FromMilliseconds(50)); } - - private void SetPlayerPosition(TimeSpan position, bool debounce) + else { - if (!IsSeekable || MediaPlayer == null) return; - if (debounce) - { - _seekTimer.Debounce(() => MediaPlayer.Position = position, TimeSpan.FromMilliseconds(50)); - } - else - { - _seekTimer.Stop(); - MediaPlayer.Position = position; - } + _seekTimer.Stop(); + MediaPlayer.Position = position; } + } + + private void OnCanSeekChanged(IMediaPlayer sender, EventArgs args) + { + _dispatcherQueue.TryEnqueue(() => + { + IsSeekable = sender.CanSeek; + }); + } - private void OnCanSeekChanged(IMediaPlayer sender, EventArgs args) + private void OnPlaybackStateChanged(IMediaPlayer sender, ValueChangedEventArgs args) + { + if (args.NewValue is not (MediaPlaybackState.None or MediaPlaybackState.Opening) && + _lastTrackedPosition > TimeSpan.Zero) { - _dispatcherQueue.TryEnqueue(() => + _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => { - IsSeekable = sender.CanSeek; + SetPlayerPosition(_lastTrackedPosition, false); + _lastTrackedPosition = TimeSpan.Zero; }); } + } - private void OnPlaybackStateChanged(IMediaPlayer sender, ValueChangedEventArgs args) + private void OnPlaybackItemChanged(IMediaPlayer sender, object? args) + { + _seekTimer.Stop(); + if (sender.PlaybackItem == null) { - if (args.NewValue is not (MediaPlaybackState.None or MediaPlaybackState.Opening) && - _lastTrackedPosition > TimeSpan.Zero) + _dispatcherQueue.TryEnqueue(() => { - _dispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => - { - SetPlayerPosition(_lastTrackedPosition, false); - _lastTrackedPosition = TimeSpan.Zero; - }); - } + IsSeekable = false; + Time = 0; + Chapters.Clear(); + }); } - - private void OnPlaybackItemChanged(IMediaPlayer sender, object? args) + else { - _seekTimer.Stop(); - if (sender.PlaybackItem == null) - { - _dispatcherQueue.TryEnqueue(() => - { - IsSeekable = false; - Time = 0; - Chapters.Clear(); - }); - } - else + _dispatcherQueue.TryEnqueue(() => { - _dispatcherQueue.TryEnqueue(() => - { - Time = 0; - Chapters.Clear(); - }); - } + Time = 0; + Chapters.Clear(); + }); } + } - private void OnBufferingEnded(IMediaPlayer sender, object? args) - { - _bufferingTimer.Stop(); - _dispatcherQueue.TryEnqueue(() => BufferingVisible = false); - } + private void OnBufferingEnded(IMediaPlayer sender, object? args) + { + _bufferingTimer.Stop(); + _dispatcherQueue.TryEnqueue(() => BufferingVisible = false); + } - private void OnBufferingStarted(IMediaPlayer sender, object? args) + private void OnBufferingStarted(IMediaPlayer sender, object? args) + { + // When the player is paused, the following still triggers a buffering + if (sender.Position == sender.NaturalDuration) + return; + + // Only show buffering if it takes more than 0.5s + _bufferingTimer.Debounce(() => BufferingVisible = true, TimeSpan.FromSeconds(0.5)); + } + + private void OnPositionChanged(IMediaPlayer sender, object? args) + { + if (_seekTimer.IsRunning || _timeChangeOverride) return; + _dispatcherQueue.TryEnqueue(() => { - // When the player is paused, the following still triggers a buffering - if (sender.Position == sender.NaturalDuration) - return; + Time = sender.Position.TotalMilliseconds; + }); + } - // Only show buffering if it takes more than 0.5s - _bufferingTimer.Debounce(() => BufferingVisible = true, TimeSpan.FromSeconds(0.5)); - } + private void OnNaturalDurationChanged(IMediaPlayer sender, object? args) + { + // Natural duration can fluctuate during playback + // Do not rely on this event to detect media changes + _dispatcherQueue.TryEnqueue(() => + { + Length = sender.NaturalDuration.TotalMilliseconds; + IsSeekable = sender.CanSeek; + UpdateChapters(sender.PlaybackItem?.Chapters); + }); + } - private void OnPositionChanged(IMediaPlayer sender, object? args) + private void OnEndReached(IMediaPlayer sender, object? args) + { + if (!_timeChangeOverride) { - if (_seekTimer.IsRunning || _timeChangeOverride) return; _dispatcherQueue.TryEnqueue(() => { - Time = sender.Position.TotalMilliseconds; + // Check if Time is close enough to Length. Sometimes a new file is already loaded at this point. + if (Length - Time is > 0 and < 400) + { + // Round Time to Length to avoid gap at the end + Time = Length; + } }); } + } - private void OnNaturalDurationChanged(IMediaPlayer sender, object? args) + private void UpdateChapters(PlaybackChapterList? chapterList) + { + Chapters.Clear(); + if (chapterList == null) return; + if (MediaPlayer != null) { - // Natural duration can fluctuate during playback - // Do not rely on this event to detect media changes - _dispatcherQueue.TryEnqueue(() => - { - Length = sender.NaturalDuration.TotalMilliseconds; - IsSeekable = sender.CanSeek; - UpdateChapters(sender.PlaybackItem?.Chapters); - }); + chapterList.Load(MediaPlayer); } - private void OnEndReached(IMediaPlayer sender, object? args) + foreach (ChapterCue chapterCue in chapterList) { - if (!_timeChangeOverride) - { - _dispatcherQueue.TryEnqueue(() => - { - // Check if Time is close enough to Length. Sometimes a new file is already loaded at this point. - if (Length - Time is > 0 and < 400) - { - // Round Time to Length to avoid gap at the end - Time = Length; - } - }); - } + Chapters.Add(chapterCue); } + // Chapters.SyncItems(chapterList); + } - private void UpdateChapters(PlaybackChapterList? chapterList) - { - Chapters.Clear(); - if (chapterList == null) return; - if (MediaPlayer != null) - { - chapterList.Load(MediaPlayer); - } + private void UpdateLastPosition(TimeSpan position) + { + if (!_settingsService.PersistPlaybackPosition || + _currentItem == null || NaturalDuration <= TimeSpan.FromMinutes(1) || + DateTimeOffset.Now - _lastPositionTracker.LastUpdated <= TimeSpan.FromSeconds(3)) + return; - foreach (ChapterCue chapterCue in chapterList) - { - Chapters.Add(chapterCue); - } - // Chapters.SyncItems(chapterList); + if (position > TimeSpan.FromSeconds(30) && position + TimeSpan.FromSeconds(10) < NaturalDuration) + { + _lastPositionTracker.UpdateLastPosition(_currentItem.Location, position); } - - private void UpdateLastPosition(TimeSpan position) + else if (position > TimeSpan.FromSeconds(5)) { - 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); - } + _lastPositionTracker.RemovePosition(_currentItem.Location); } } } diff --git a/Screenbox.Core/ViewModels/SettingsPageViewModel.cs b/Screenbox.Core/ViewModels/SettingsPageViewModel.cs index a13dd7846..9399c2b33 100644 --- a/Screenbox.Core/ViewModels/SettingsPageViewModel.cs +++ b/Screenbox.Core/ViewModels/SettingsPageViewModel.cs @@ -45,6 +45,7 @@ public sealed partial class SettingsPageViewModel : ObservableRecipient [ObservableProperty] private string _globalArguments; [ObservableProperty] private bool _isRelaunchRequired; [ObservableProperty] private int _selectedLanguage; + [ObservableProperty] private bool _persistPlaybackPosition; public ObservableCollection MusicLocations { get; } @@ -63,6 +64,7 @@ public sealed partial class SettingsPageViewModel : ObservableRecipient private readonly DispatcherQueue _dispatcherQueue; private readonly DispatcherQueueTimer _storageDeviceRefreshTimer; private readonly DeviceWatcher? _portableStorageDeviceWatcher; + private readonly LastPositionTracker _lastPositionTracker; private static InitialValues? _initialValues; private StorageLibrary? _videosLibrary; private StorageLibrary? _musicLibrary; @@ -79,12 +81,14 @@ public SettingsPageViewModel( ISettingsService settingsService, LibraryContext libraryContext, ILibraryService libraryService, - LibraryController libraryController) + LibraryController libraryController, + LastPositionTracker lastPositionTracker) { _settingsService = settingsService; _libraryContext = libraryContext; _libraryService = libraryService; _libraryController = libraryController; + _lastPositionTracker = lastPositionTracker; _dispatcherQueue = DispatcherQueue.GetForCurrentThread(); _storageDeviceRefreshTimer = _dispatcherQueue.CreateTimer(); MusicLocations = new ObservableCollection(); @@ -115,6 +119,7 @@ public SettingsPageViewModel( _playerControlsHideDelay = _settingsService.PlayerControlsHideDelay; _useIndexer = _settingsService.UseIndexer; _showRecent = _settingsService.ShowRecent; + _persistPlaybackPosition = _settingsService.PersistPlaybackPosition; _theme = ((int)_settingsService.Theme + 2) % 3; _enqueueAllFilesInFolder = _settingsService.EnqueueAllFilesInFolder; _restorePlaybackPosition = _settingsService.RestorePlaybackPosition; @@ -295,6 +300,12 @@ partial void OnGlobalArgumentsChanged(string value) CheckForRelaunch(); } + partial void OnPersistPlaybackPositionChanged(bool value) + { + _settingsService.PersistPlaybackPosition = value; + Messenger.Send(new SettingsChangedMessage(nameof(PersistPlaybackPosition), typeof(SettingsPageViewModel))); + } + [RelayCommand] private async Task RefreshLibrariesAsync() { @@ -351,6 +362,20 @@ private void ClearRecentHistory() StorageApplicationPermissions.MostRecentlyUsedList.Clear(); } + [RelayCommand] + private async Task ClearPlaybackPositionHistoryAsync() + { + try + { + _lastPositionTracker.ClearAll(); + await _lastPositionTracker.SaveToDiskAsync(); + } + catch (Exception) + { + // pass + } + } + public void OnNavigatedFrom() { if (SystemInformation.IsXbox) diff --git a/Screenbox/Pages/SettingsPage.xaml b/Screenbox/Pages/SettingsPage.xaml index 72436b691..a0d88deff 100644 --- a/Screenbox/Pages/SettingsPage.xaml +++ b/Screenbox/Pages/SettingsPage.xaml @@ -28,6 +28,7 @@ 0,0,0,4 + 120 @@ -316,33 +317,6 @@ - - - - - - - - - - -