Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: improve folder support #517

Merged
merged 6 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Screenbox.Core/Services/FilesService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private FileOpenPicker GetFilePickerForFormats(IReadOnlyCollection<string> forma
FileOpenPicker picker = new()
{
ViewMode = PickerViewMode.Thumbnail,
SuggestedStartLocation = PickerLocationId.VideosLibrary
SuggestedStartLocation = PickerLocationId.ComputerFolder
};

IEnumerable<string> fileTypes = formats;
Expand Down
1 change: 1 addition & 0 deletions Screenbox.Core/Services/ISettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public interface ISettingsService
bool PlayerShowControls { get; set; }
int PersistentVolume { get; set; }
bool ShowRecent { get; set; }
bool EnqueueAllFilesInFolder { get; set; }
bool SearchRemovableStorage { get; set; }
int MaxVolume { get; set; }
string GlobalArguments { get; set; }
Expand Down
7 changes: 7 additions & 0 deletions Screenbox.Core/Services/SettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public sealed class SettingsService : ISettingsService
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 AdvancedModeKey = "Advanced/IsEnabled";
private const string AdvancedMultipleInstancesKey = "Advanced/MultipleInstances";
private const string GlobalArgumentsKey = "Values/GlobalArguments";
Expand Down Expand Up @@ -78,6 +79,12 @@ public bool ShowRecent
set => SetValue(GeneralShowRecent, value);
}

public bool EnqueueAllFilesInFolder
{
get => GetValue<bool>(GeneralEnqueueAllInFolder);
set => SetValue(GeneralEnqueueAllInFolder, value);
}

public bool PlayerShowControls
{
get => GetValue<bool>(PlayerShowControlsKey);
Expand Down
149 changes: 106 additions & 43 deletions Screenbox.Core/ViewModels/MediaListViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public sealed partial class MediaListViewModel : ObservableRecipient,
private StorageFileQueryResult? _neighboringFilesQuery;
private object? _lastUpdated;
private CancellationTokenSource? _cts;
private CancellationTokenSource? _playFilesCts;

private const int MediaBufferCapacity = 5;

Expand Down Expand Up @@ -154,21 +155,49 @@ public async void Receive(PlayFilesMessage message)
{
IReadOnlyList<IStorageItem> files = message.Value;
_neighboringFilesQuery = message.NeighboringFilesQuery;
if (_neighboringFilesQuery == null && files.Count == 1 && files[0] is StorageFile file)
if (files.Count == 1 && files[0] is StorageFile file)
{
_neighboringFilesQuery = await _filesService.GetNeighboringFilesQueryAsync(file);
}
var media = _mediaFactory.GetSingleton(file);
// The current play queue may already have the file already. Just play the file in this case.
if (Items.Contains(media))
{
PlaySingle(media);
return;
}

if (_mediaPlayer == null)
{
_delayPlay = files;
// If there is only 1 file, play it immediately
// Avoid waiting to get all the neighboring files then play, which may cause delay
ClearPlaylist();
await EnqueueAndPlay(file);
// If there are more than one item in the queue, file is already a playlist, no need to check for neighboring files
if (Items.Count > 1) return;

_neighboringFilesQuery ??= await _filesService.GetNeighboringFilesQueryAsync(file);
// Populate the play queue with neighboring media if needed
if (!_settingsService.EnqueueAllFilesInFolder || _neighboringFilesQuery == null) return;
_playFilesCts?.Cancel();
using CancellationTokenSource cts = new();
try
{
_playFilesCts = cts;
await EnqueueNeighboringFiles(_neighboringFilesQuery, file, cts.Token);
}
finally
{
_playFilesCts = null;
}
}
else
{
ClearPlaylist();
MediaViewModel? next = await EnqueueAsync(files);
if (next != null)
PlaySingle(next);
var playlist = await CreatePlaylistAsync(files);
if (_mediaPlayer == null)
{
_delayPlay = playlist;
}
else if (playlist != null)
{
await EnqueueAndPlay(playlist);
}
}
}

Expand Down Expand Up @@ -324,6 +353,26 @@ partial void OnShuffleModeChanged(bool value)
}
}

private async Task EnqueueNeighboringFiles(StorageFileQueryResult neighboringFilesQuery, StorageFile file, CancellationToken cancellationToken = default)
{
PlaylistCreateResult? playlist;
try
{
var neighboringFiles = await neighboringFilesQuery.GetFilesAsync();
playlist = await CreatePlaylistAsync(neighboringFiles, file);
cancellationToken.ThrowIfCancellationRequested();
}
catch (Exception)
{
return;
}

if (playlist == null || playlist.Playlist.Count == 0) return;
if (CurrentItem != null && !playlist.Playlist.Contains(CurrentItem))
CurrentItem = null;
Items.SyncItems((IReadOnlyList<MediaViewModel>)playlist.Playlist);
}

private void ShufflePlaylist()
{
_random ??= new Random();
Expand Down Expand Up @@ -451,33 +500,53 @@ private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs
}
}

private async Task<PlaylistCreateResult?> CreatePlaylistAsync(IReadOnlyList<IStorageItem> files)
private async Task<PlaylistCreateResult?> CreatePlaylistAsync(IReadOnlyList<IStorageItem> storageItems, StorageFile? playNext = null)
{
List<MediaViewModel> queue = new();
foreach (IStorageItem item in files.ToList()) // ToList() to avoid modifying the collection while iterating
List<IStorageItem> storageItemQueue = storageItems.ToList();
MediaViewModel? next = null;
// Max number of items in queue is 10k. Reevaluate if needed.
for (int i = 0; i < storageItemQueue.Count && queue.Count < 10000; i++)
{
if (item is not StorageFile storageFile) continue;
MediaViewModel vm = _mediaFactory.GetSingleton(storageFile);
if (storageFile.IsSupportedPlaylist() && await ParseSubMediaRecursiveAsync(vm) is { Count: > 0 } playlist)
IStorageItem item = storageItemQueue[i];
switch (item)
{
queue.AddRange(playlist);
}
else
{
queue.Add(vm);
case StorageFile storageFile when storageFile.IsSupported():
MediaViewModel vm = _mediaFactory.GetSingleton(storageFile);
if (playNext != null && storageFile.IsEqual(playNext))
{
next = vm;
}

if (storageFile.IsSupportedPlaylist() && await ParseSubMediaRecursiveAsync(vm) is
{ Count: > 0 } playlist)
{
queue.AddRange(playlist);
}
else
{
queue.Add(vm);
}
break;

case StorageFolder storageFolder:
// Max number of items in a folder is 10k. Reevaluate if needed.
var subItems = await storageFolder.GetItemsAsync(0, 10000);
storageItemQueue.AddRange(subItems);
break;
}
}

return queue.Count > 0 ? new PlaylistCreateResult(queue[0], queue) : null;
return queue.Count > 0 ? new PlaylistCreateResult(next ?? queue[0], queue) : null;
}

public async Task<MediaViewModel?> EnqueueAsync(IReadOnlyList<IStorageItem> files)
public async Task EnqueueAsync(IReadOnlyList<IStorageItem> files)
{
var result = await CreatePlaylistAsync(files);
var queue = result?.Playlist ?? Array.Empty<MediaViewModel>();
Enqueue(queue);

return queue.Count > 0 ? queue[0] : null;
if (result?.Playlist.Count > 0)
{
Enqueue(result.Playlist);
}
}

private void Enqueue(IEnumerable<MediaViewModel> list)
Expand Down Expand Up @@ -546,26 +615,20 @@ private async Task<PlaylistCreateResult> CreatePlaylistAsync(Uri uri)

private async Task EnqueueAndPlay(object value)
{
try
MediaViewModel? playNext = GetMedia(value);
if (playNext != null)
{
MediaViewModel? playNext = GetMedia(value);
if (playNext != null)
{
Enqueue(new[] { playNext });
PlaySingle(playNext);
}

PlaylistCreateResult? result = await CreatePlaylistAsync(playNext ?? value);
if (result != null && !result.PlayNext.Source.Equals(playNext?.Source))
{
ClearPlaylist();
Enqueue(result.Playlist);
PlaySingle(result.PlayNext);
}
Enqueue(new[] { playNext });
PlaySingle(playNext);
}
catch (OperationCanceledException)

// If playNext is a playlist file, recursively parse the playlist and enqueue the items
PlaylistCreateResult? result = (value as PlaylistCreateResult) ?? await CreatePlaylistAsync(playNext ?? value);
if (result != null && !result.PlayNext.Source.Equals(playNext?.Source))
{
// pass
ClearPlaylist();
Enqueue(result.Playlist);
PlaySingle(result.PlayNext);
}
}

Expand Down
8 changes: 8 additions & 0 deletions Screenbox.Core/ViewModels/SettingsPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public sealed partial class SettingsPageViewModel : ObservableRecipient
[ObservableProperty] private int _volumeBoost;
[ObservableProperty] private bool _useIndexer;
[ObservableProperty] private bool _showRecent;
[ObservableProperty] private bool _enqueueAllFilesInFolder;
[ObservableProperty] private bool _searchRemovableStorage;
[ObservableProperty] private bool _advancedMode;
[ObservableProperty] private bool _useMultipleInstances;
Expand Down Expand Up @@ -76,6 +77,7 @@ public SettingsPageViewModel(ISettingsService settingsService, ILibraryService l
_playerShowControls = _settingsService.PlayerShowControls;
_useIndexer = _settingsService.UseIndexer;
_showRecent = _settingsService.ShowRecent;
_enqueueAllFilesInFolder = _settingsService.EnqueueAllFilesInFolder;
_searchRemovableStorage = _settingsService.SearchRemovableStorage;
_advancedMode = _settingsService.AdvancedMode;
_useMultipleInstances = _settingsService.UseMultipleInstances;
Expand Down Expand Up @@ -137,6 +139,12 @@ partial void OnShowRecentChanged(bool value)
Messenger.Send(new SettingsChangedMessage(nameof(ShowRecent), typeof(SettingsPageViewModel)));
}

partial void OnEnqueueAllFilesInFolderChanged(bool value)
{
_settingsService.EnqueueAllFilesInFolder = value;
Messenger.Send(new SettingsChangedMessage(nameof(EnqueueAllFilesInFolder), typeof(SettingsPageViewModel)));
}

async partial void OnSearchRemovableStorageChanged(bool value)
{
_settingsService.SearchRemovableStorage = value;
Expand Down
9 changes: 9 additions & 0 deletions Screenbox/Pages/SettingsPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@
</ctc:SettingsExpander.Items>
</ctc:SettingsExpander>

<ctc:SettingsCard
Margin="{StaticResource SettingsCardMargin}"
Description="{strings:Resources Key=SettingsEnqueueAllFilesInFolderDescription}"
Header="{strings:Resources Key=SettingsEnqueueAllFilesInFolderHeader}"
HeaderIcon="{ui:FontIcon FontFamily={StaticResource ScreenboxSymbolThemeFontFamily},
Glyph=&#xF2CE;}">
<ToggleSwitch IsOn="{x:Bind ViewModel.EnqueueAllFilesInFolder, Mode=TwoWay}" />
</ctc:SettingsCard>

<!-- Player section -->
<TextBlock Style="{StaticResource SettingsSectionHeaderTextBlockStyle}" Text="{strings:Resources Key=SettingsCategoryPlayer}" />

Expand Down
28 changes: 28 additions & 0 deletions Screenbox/Strings/en-US/Resources.generated.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3066,6 +3066,32 @@ public static string FailedToLoadVisualNotificationTitle
}
}
#endregion

#region SettingsEnqueueAllFilesInFolderDescription
/// <summary>
/// Looks up a localized string similar to: When opening a single file, automatically add all other files in the folder to the play queue.
/// </summary>
public static string SettingsEnqueueAllFilesInFolderDescription
{
get
{
return _resourceLoader.GetString("SettingsEnqueueAllFilesInFolderDescription");
}
}
#endregion

#region SettingsEnqueueAllFilesInFolderHeader
/// <summary>
/// Looks up a localized string similar to: Add all files in folder to queue
/// </summary>
public static string SettingsEnqueueAllFilesInFolderHeader
{
get
{
return _resourceLoader.GetString("SettingsEnqueueAllFilesInFolderHeader");
}
}
#endregion
}

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("DotNetPlus.ReswPlus", "2.1.3")]
Expand Down Expand Up @@ -3311,6 +3337,8 @@ public enum KeyEnum
ShowAlbum,
ShowArtist,
FailedToLoadVisualNotificationTitle,
SettingsEnqueueAllFilesInFolderDescription,
SettingsEnqueueAllFilesInFolderHeader,
}

private static ResourceLoader _resourceLoader;
Expand Down
6 changes: 6 additions & 0 deletions Screenbox/Strings/en-US/Resources.resw
Original file line number Diff line number Diff line change
Expand Up @@ -887,4 +887,10 @@
<data name="FailedToLoadVisualNotificationTitle" xml:space="preserve">
<value>Failed to load visual</value>
</data>
<data name="SettingsEnqueueAllFilesInFolderDescription" xml:space="preserve">
<value>When opening a single file, automatically add all other files in the folder to the play queue.</value>
</data>
<data name="SettingsEnqueueAllFilesInFolderHeader" xml:space="preserve">
<value>Add all files in folder to queue</value>
</data>
</root>