From cbadcceae581f152ca8e9f0484a9c65d70064ece Mon Sep 17 00:00:00 2001 From: Tung Huynh Date: Sat, 21 Dec 2024 00:18:39 -0800 Subject: [PATCH 01/31] add playlist nav page --- Screenbox.Core/Screenbox.Core.csproj | 1 + .../ViewModels/PlaylistsPageViewModel.cs | 4 ++ Screenbox/App.xaml | 1 + Screenbox/App.xaml.cs | 1 + Screenbox/Pages/MainPage.xaml | 4 ++ Screenbox/Pages/MainPage.xaml.cs | 1 + Screenbox/Pages/PlaylistsPage.xaml | 44 +++++++++++++++++++ Screenbox/Pages/PlaylistsPage.xaml.cs | 28 ++++++++++++ Screenbox/Screenbox.csproj | 7 +++ 9 files changed, 91 insertions(+) create mode 100644 Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs create mode 100644 Screenbox/Pages/PlaylistsPage.xaml create mode 100644 Screenbox/Pages/PlaylistsPage.xaml.cs diff --git a/Screenbox.Core/Screenbox.Core.csproj b/Screenbox.Core/Screenbox.Core.csproj index 72c5db350..10e5c5752 100644 --- a/Screenbox.Core/Screenbox.Core.csproj +++ b/Screenbox.Core/Screenbox.Core.csproj @@ -283,6 +283,7 @@ + diff --git a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs new file mode 100644 index 000000000..73a92910c --- /dev/null +++ b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs @@ -0,0 +1,4 @@ +namespace Screenbox.Core.ViewModels; +public class PlaylistsPageViewModel +{ +} diff --git a/Screenbox/App.xaml b/Screenbox/App.xaml index 12891e0db..242cf1ed9 100644 --- a/Screenbox/App.xaml +++ b/Screenbox/App.xaml @@ -89,6 +89,7 @@ 0,12,0,0 0,16,0,0 0,0,0,12 + 0,0,0,16 0,0,0,100 0,0,0,106 diff --git a/Screenbox/App.xaml.cs b/Screenbox/App.xaml.cs index b233ebe2a..e8b1b33ac 100644 --- a/Screenbox/App.xaml.cs +++ b/Screenbox/App.xaml.cs @@ -119,6 +119,7 @@ private static IServiceProvider ConfigureServices() services.AddSingleton(); services.AddSingleton(_ => new NavigationService( new KeyValuePair(typeof(HomePageViewModel), typeof(HomePage)), + new KeyValuePair(typeof(PlaylistsPageViewModel), typeof(PlaylistsPage)), new KeyValuePair(typeof(VideosPageViewModel), typeof(VideosPage)), new KeyValuePair(typeof(AllVideosPageViewModel), typeof(AllVideosPage)), new KeyValuePair(typeof(MusicPageViewModel), typeof(MusicPage)), diff --git a/Screenbox/Pages/MainPage.xaml b/Screenbox/Pages/MainPage.xaml index ec907cbc2..09fed807e 100644 --- a/Screenbox/Pages/MainPage.xaml +++ b/Screenbox/Pages/MainPage.xaml @@ -16,6 +16,7 @@ xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:strings="using:Screenbox.Strings" xmlns:triggers="using:Screenbox.Triggers" + xmlns:ui="using:CommunityToolkit.WinUI" muxc:BackdropMaterial.ApplyToRootOrPageBackground="True" Loaded="MainPage_Loaded" mc:Ignorable="d"> @@ -184,6 +185,9 @@ + + + diff --git a/Screenbox/Pages/MainPage.xaml.cs b/Screenbox/Pages/MainPage.xaml.cs index e54ecbe53..f283cf053 100644 --- a/Screenbox/Pages/MainPage.xaml.cs +++ b/Screenbox/Pages/MainPage.xaml.cs @@ -55,6 +55,7 @@ public MainPage() { "music", typeof(MusicPage) }, { "queue", typeof(PlayQueuePage) }, { "network", typeof(NetworkPage) }, + { "playlists", typeof(PlaylistsPage) }, { "settings", typeof(SettingsPage) } }; diff --git a/Screenbox/Pages/PlaylistsPage.xaml b/Screenbox/Pages/PlaylistsPage.xaml new file mode 100644 index 000000000..c44486b4a --- /dev/null +++ b/Screenbox/Pages/PlaylistsPage.xaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + diff --git a/Screenbox/Pages/PlaylistsPage.xaml.cs b/Screenbox/Pages/PlaylistsPage.xaml.cs new file mode 100644 index 000000000..e12deab10 --- /dev/null +++ b/Screenbox/Pages/PlaylistsPage.xaml.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Controls.Primitives; +using Windows.UI.Xaml.Data; +using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; +using Windows.UI.Xaml.Navigation; + +// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 + +namespace Screenbox.Pages; +/// +/// An empty page that can be used on its own or navigated to within a Frame. +/// +public sealed partial class PlaylistsPage : Page +{ + public PlaylistsPage() + { + this.InitializeComponent(); + } +} diff --git a/Screenbox/Screenbox.csproj b/Screenbox/Screenbox.csproj index 58dea9920..4a635986f 100644 --- a/Screenbox/Screenbox.csproj +++ b/Screenbox/Screenbox.csproj @@ -241,6 +241,9 @@ AlbumDetailsPage.xaml + + PlaylistsPage.xaml + AlbumSearchResultPage.xaml @@ -611,6 +614,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile From 2b5c67363b0ddd65191f02d1dd457e6da5902ce5 Mon Sep 17 00:00:00 2001 From: Tung Huynh <31434093+huynhsontung@users.noreply.github.com> Date: Thu, 21 Aug 2025 02:07:44 +0000 Subject: [PATCH 02/31] create model and service for persistent playlist rename --- Screenbox.Core/Models/PersistentPlaylist.cs | 13 +++ Screenbox.Core/Services/PlaylistService.cs | 93 +++++++++++++++++++-- 2 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 Screenbox.Core/Models/PersistentPlaylist.cs diff --git a/Screenbox.Core/Models/PersistentPlaylist.cs b/Screenbox.Core/Models/PersistentPlaylist.cs new file mode 100644 index 000000000..474b43da8 --- /dev/null +++ b/Screenbox.Core/Models/PersistentPlaylist.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using Screenbox.Core.Models; + +namespace Screenbox.Core.Models; + +public class PersistentPlaylist +{ + public string Id { get; set; } = string.Empty; + public string DisplayName { get; set; } = string.Empty; + public DateTimeOffset Created { get; set; } + public List Items { get; set; } = new(); +} diff --git a/Screenbox.Core/Services/PlaylistService.cs b/Screenbox.Core/Services/PlaylistService.cs index 529c9969b..2fa3b0b60 100644 --- a/Screenbox.Core/Services/PlaylistService.cs +++ b/Screenbox.Core/Services/PlaylistService.cs @@ -9,20 +9,22 @@ using Screenbox.Core.Models; using Screenbox.Core.ViewModels; using Windows.Media; +using Windows.Storage; using Windows.Storage.Search; namespace Screenbox.Core.Services; -/// -/// Stateless service for playlist operations -/// -public sealed class PlaylistService : IPlaylistService +public sealed class PlaylistService { + private const string PlaylistsFolderName = "Playlists"; + private const string ThumbnailsFolderName = "Thumbnails"; + private readonly FilesService _filesService; private readonly IMediaListFactory _mediaListFactory; - public PlaylistService(IMediaListFactory mediaListFactory) + public PlaylistService(IMediaListFactory mediaListFactory, FilesService filesService) { _mediaListFactory = mediaListFactory; + _filesService = filesService; } public async Task AddNeighboringFilesAsync(Playlist playlist, StorageFileQueryResult neighboringFilesQuery, CancellationToken cancellationToken = default) @@ -121,4 +123,85 @@ private static void Shuffle(IList list, Random rng) (list[k], list[n]) = (list[n], list[k]); } } + + public async Task SavePlaylistAsync(PersistentPlaylist playlist) + { + StorageFolder playlistsFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(PlaylistsFolderName, CreationCollisionOption.OpenIfExists); + string fileName = playlist.Id + ".json"; + await _filesService.SaveToDiskAsync(playlistsFolder, fileName, playlist); + } + + public async Task LoadPlaylistAsync(string id) + { + StorageFolder playlistsFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(PlaylistsFolderName, CreationCollisionOption.OpenIfExists); + string fileName = id + ".json"; + try + { + return await _filesService.LoadFromDiskAsync(playlistsFolder, fileName); + } + catch + { + return null; + } + } + + public async Task> ListPlaylistsAsync() + { + StorageFolder playlistsFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(PlaylistsFolderName, CreationCollisionOption.OpenIfExists); + var files = await playlistsFolder.GetFilesAsync(); + var playlists = new List(); + foreach (var file in files) + { + try + { + var playlist = await _filesService.LoadFromDiskAsync(file); + if (playlist != null) + playlists.Add(playlist); + } + catch { } + } + return playlists; + } + + public async Task DeletePlaylistAsync(string id) + { + StorageFolder playlistsFolder = await ApplicationData.Current.LocalFolder.CreateFolderAsync(PlaylistsFolderName, CreationCollisionOption.OpenIfExists); + string fileName = id + ".json"; + try + { + StorageFile file = await playlistsFolder.GetFileAsync(fileName); + await file.DeleteAsync(); + } + catch { } + } + + public async Task SaveThumbnailAsync(string mediaLocation, byte[] imageBytes) + { + StorageFolder thumbnailsFolder = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync(ThumbnailsFolderName, CreationCollisionOption.OpenIfExists); + string hash = GetHash(mediaLocation); + StorageFile file = await thumbnailsFolder.CreateFileAsync(hash + ".png", CreationCollisionOption.ReplaceExisting); + await FileIO.WriteBytesAsync(file, imageBytes); + } + + public async Task GetThumbnailFileAsync(string mediaLocation) + { + StorageFolder thumbnailsFolder = await ApplicationData.Current.LocalCacheFolder.CreateFolderAsync(ThumbnailsFolderName, CreationCollisionOption.OpenIfExists); + string hash = GetHash(mediaLocation); + try + { + return await thumbnailsFolder.GetFileAsync(hash + ".png"); + } + catch + { + return null; + } + } + + private static string GetHash(string input) + { + using var sha256 = System.Security.Cryptography.SHA256.Create(); + byte[] bytes = System.Text.Encoding.UTF8.GetBytes(input.ToLowerInvariant()); + byte[] hashBytes = sha256.ComputeHash(bytes); + return BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + } } From 3d90237eb05be325bec4c3f999266f14e89df9d5 Mon Sep 17 00:00:00 2001 From: Tung Huynh <31434093+huynhsontung@users.noreply.github.com> Date: Thu, 21 Aug 2025 02:24:21 +0000 Subject: [PATCH 03/31] implement base viewmodel --- .../ViewModels/PlaylistsPageViewModel.cs | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs index 73a92910c..46d3befd8 100644 --- a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs +++ b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs @@ -1,4 +1,72 @@ -namespace Screenbox.Core.ViewModels; -public class PlaylistsPageViewModel +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Screenbox.Core.Models; +using Screenbox.Core.Services; +using System.Collections.ObjectModel; +using System.Threading.Tasks; + +namespace Screenbox.Core.ViewModels; + +public partial class PlaylistsPageViewModel : ObservableObject { + private readonly PlaylistService _playlistService; + + [ObservableProperty] + private ObservableCollection _playlists = new(); + + [ObservableProperty] + private PersistentPlaylist? _selectedPlaylist; + + public PlaylistsPageViewModel(PlaylistService playlistService) + { + _playlistService = playlistService; + } + + [RelayCommand] + public async Task LoadPlaylistsAsync() + { + var loaded = await _playlistService.ListPlaylistsAsync(); + Playlists.Clear(); + foreach (var p in loaded) + Playlists.Add(p); + } + + [RelayCommand] + public async Task CreatePlaylistAsync() + { + // UI modal should collect display name and items, then call this command + var playlist = new PersistentPlaylist + { + Id = System.Guid.NewGuid().ToString(), + DisplayName = string.Empty, // To be set by modal + Created = System.DateTimeOffset.Now, + Items = new() + }; + await _playlistService.SavePlaylistAsync(playlist); + Playlists.Add(playlist); + SelectedPlaylist = playlist; + } + + + [RelayCommand] + public async Task RenamePlaylistAsync(PersistentPlaylist playlist, string newName) + { + playlist.DisplayName = newName; + await _playlistService.SavePlaylistAsync(playlist); + } + + [RelayCommand] + public async Task DeletePlaylistAsync(PersistentPlaylist playlist) + { + await _playlistService.DeletePlaylistAsync(playlist.Id); + Playlists.Remove(playlist); + if (SelectedPlaylist == playlist) + SelectedPlaylist = null; + } + + [RelayCommand] + public void SelectPlaylist(PersistentPlaylist playlist) + { + SelectedPlaylist = playlist; + } } From 13fec6a2799d80e2674c739bb549acec24999ec1 Mon Sep 17 00:00:00 2001 From: Tung Huynh Date: Sun, 19 Oct 2025 17:07:35 -0700 Subject: [PATCH 04/31] fix playlistservice --- Screenbox.Core/Screenbox.Core.csproj | 1 + Screenbox.Core/Services/PlaylistService.cs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Screenbox.Core/Screenbox.Core.csproj b/Screenbox.Core/Screenbox.Core.csproj index 10e5c5752..a7a7bf7a9 100644 --- a/Screenbox.Core/Screenbox.Core.csproj +++ b/Screenbox.Core/Screenbox.Core.csproj @@ -201,6 +201,7 @@ + diff --git a/Screenbox.Core/Services/PlaylistService.cs b/Screenbox.Core/Services/PlaylistService.cs index 2fa3b0b60..cde411ab2 100644 --- a/Screenbox.Core/Services/PlaylistService.cs +++ b/Screenbox.Core/Services/PlaylistService.cs @@ -18,10 +18,11 @@ public sealed class PlaylistService { private const string PlaylistsFolderName = "Playlists"; private const string ThumbnailsFolderName = "Thumbnails"; - private readonly FilesService _filesService; + private readonly IMediaListFactory _mediaListFactory; + private readonly IFilesService _filesService; - public PlaylistService(IMediaListFactory mediaListFactory, FilesService filesService) + public PlaylistService(IFilesService filesService, IMediaListFactory mediaListFactory) { _mediaListFactory = mediaListFactory; _filesService = filesService; From c42b79eb8e797341b0277d4f5d72637b0442c9a1 Mon Sep 17 00:00:00 2001 From: Tung Huynh Date: Sun, 19 Oct 2025 17:43:24 -0700 Subject: [PATCH 05/31] fix more issues wip: temp fix PlaylistsPageViewModel --- .../ViewModels/PlaylistsPageViewModel.cs | 110 +++++++++--------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs index 46d3befd8..d4f4cac89 100644 --- a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs +++ b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs @@ -1,72 +1,74 @@ -using CommunityToolkit.Mvvm.ComponentModel; +#nullable enable + +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Screenbox.Core.Models; using Screenbox.Core.Services; -using System.Collections.ObjectModel; -using System.Threading.Tasks; namespace Screenbox.Core.ViewModels; public partial class PlaylistsPageViewModel : ObservableObject { - private readonly PlaylistService _playlistService; + private readonly PlaylistService _playlistService; - [ObservableProperty] - private ObservableCollection _playlists = new(); + [ObservableProperty] + private ObservableCollection _playlists = new(); - [ObservableProperty] - private PersistentPlaylist? _selectedPlaylist; + [ObservableProperty] + private PersistentPlaylist? _selectedPlaylist; - public PlaylistsPageViewModel(PlaylistService playlistService) - { - _playlistService = playlistService; - } + public PlaylistsPageViewModel(PlaylistService playlistService) + { + _playlistService = playlistService; + } - [RelayCommand] - public async Task LoadPlaylistsAsync() - { - var loaded = await _playlistService.ListPlaylistsAsync(); - Playlists.Clear(); - foreach (var p in loaded) - Playlists.Add(p); - } + [RelayCommand] + public async Task LoadPlaylistsAsync() + { + var loaded = await _playlistService.ListPlaylistsAsync(); + Playlists.Clear(); + foreach (var p in loaded) + Playlists.Add(p); + } - [RelayCommand] - public async Task CreatePlaylistAsync() - { - // UI modal should collect display name and items, then call this command - var playlist = new PersistentPlaylist - { - Id = System.Guid.NewGuid().ToString(), - DisplayName = string.Empty, // To be set by modal - Created = System.DateTimeOffset.Now, - Items = new() - }; - await _playlistService.SavePlaylistAsync(playlist); - Playlists.Add(playlist); - SelectedPlaylist = playlist; - } + [RelayCommand] + public async Task CreatePlaylistAsync() + { + // UI modal should collect display name and items, then call this command + var playlist = new PersistentPlaylist + { + Id = System.Guid.NewGuid().ToString(), + DisplayName = string.Empty, // To be set by modal + Created = System.DateTimeOffset.Now, + Items = new() + }; + await _playlistService.SavePlaylistAsync(playlist); + Playlists.Add(playlist); + SelectedPlaylist = playlist; + } - [RelayCommand] - public async Task RenamePlaylistAsync(PersistentPlaylist playlist, string newName) - { - playlist.DisplayName = newName; - await _playlistService.SavePlaylistAsync(playlist); - } + [RelayCommand] + public async Task RenamePlaylistAsync(PersistentPlaylist playlist) + { + //playlist.DisplayName = newName; + //await _playlistService.SavePlaylistAsync(playlist); + } - [RelayCommand] - public async Task DeletePlaylistAsync(PersistentPlaylist playlist) - { - await _playlistService.DeletePlaylistAsync(playlist.Id); - Playlists.Remove(playlist); - if (SelectedPlaylist == playlist) - SelectedPlaylist = null; - } + [RelayCommand] + public async Task DeletePlaylistAsync(PersistentPlaylist playlist) + { + await _playlistService.DeletePlaylistAsync(playlist.Id); + Playlists.Remove(playlist); + if (SelectedPlaylist == playlist) + SelectedPlaylist = null; + } - [RelayCommand] - public void SelectPlaylist(PersistentPlaylist playlist) - { - SelectedPlaylist = playlist; - } + [RelayCommand] + public void SelectPlaylist(PersistentPlaylist playlist) + { + SelectedPlaylist = playlist; + } } From 880a3c9050ba1bd7d340895f0500b90e01c77c66 Mon Sep 17 00:00:00 2001 From: Tung Huynh <31434093+huynhsontung@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:00:19 +0800 Subject: [PATCH 06/31] fix PlaylistService --- Screenbox.Core/Models/PersistentPlaylist.cs | 3 +-- Screenbox.Core/Services/PlaylistService.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Screenbox.Core/Models/PersistentPlaylist.cs b/Screenbox.Core/Models/PersistentPlaylist.cs index 474b43da8..6c593a2d8 100644 --- a/Screenbox.Core/Models/PersistentPlaylist.cs +++ b/Screenbox.Core/Models/PersistentPlaylist.cs @@ -1,6 +1,5 @@ -using System; +using System; using System.Collections.Generic; -using Screenbox.Core.Models; namespace Screenbox.Core.Models; diff --git a/Screenbox.Core/Services/PlaylistService.cs b/Screenbox.Core/Services/PlaylistService.cs index cde411ab2..8e45ca74f 100644 --- a/Screenbox.Core/Services/PlaylistService.cs +++ b/Screenbox.Core/Services/PlaylistService.cs @@ -14,7 +14,7 @@ namespace Screenbox.Core.Services; -public sealed class PlaylistService +public sealed class PlaylistService : IPlaylistService { private const string PlaylistsFolderName = "Playlists"; private const string ThumbnailsFolderName = "Thumbnails"; From 24ece2169d7fc2516d736da88d52ce9a957db631 Mon Sep 17 00:00:00 2001 From: Tung Huynh <31434093+huynhsontung@users.noreply.github.com> Date: Sat, 25 Oct 2025 00:35:16 +0800 Subject: [PATCH 07/31] create PlaylistViewModel --- Screenbox.Core/Common/ServiceHelpers.cs | 1 + Screenbox.Core/Models/PersistentPlaylist.cs | 2 +- Screenbox.Core/Screenbox.Core.csproj | 3 +- Screenbox.Core/Services/IPlaylistService.cs | 5 ++ Screenbox.Core/Services/LibraryService.cs | 20 +++---- .../ViewModels/PlaylistViewModel.cs | 59 +++++++++++++++++++ .../ViewModels/PlaylistsPageViewModel.cs | 58 +++++------------- 7 files changed, 92 insertions(+), 56 deletions(-) create mode 100644 Screenbox.Core/ViewModels/PlaylistViewModel.cs diff --git a/Screenbox.Core/Common/ServiceHelpers.cs b/Screenbox.Core/Common/ServiceHelpers.cs index 9e085cc3c..b7b351b46 100644 --- a/Screenbox.Core/Common/ServiceHelpers.cs +++ b/Screenbox.Core/Common/ServiceHelpers.cs @@ -38,6 +38,7 @@ public static void PopulateCoreServices(ServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); // Shared between many pages services.AddSingleton(); // Avoid thread lock services.AddSingleton(); // Global playlist diff --git a/Screenbox.Core/Models/PersistentPlaylist.cs b/Screenbox.Core/Models/PersistentPlaylist.cs index 6c593a2d8..db51810b7 100644 --- a/Screenbox.Core/Models/PersistentPlaylist.cs +++ b/Screenbox.Core/Models/PersistentPlaylist.cs @@ -7,6 +7,6 @@ public class PersistentPlaylist { public string Id { get; set; } = string.Empty; public string DisplayName { get; set; } = string.Empty; - public DateTimeOffset Created { get; set; } + public DateTimeOffset LastUpdated { get; set; } public List Items { get; set; } = new(); } diff --git a/Screenbox.Core/Screenbox.Core.csproj b/Screenbox.Core/Screenbox.Core.csproj index a7a7bf7a9..681110b83 100644 --- a/Screenbox.Core/Screenbox.Core.csproj +++ b/Screenbox.Core/Screenbox.Core.csproj @@ -201,7 +201,7 @@ - + @@ -283,6 +283,7 @@ + diff --git a/Screenbox.Core/Services/IPlaylistService.cs b/Screenbox.Core/Services/IPlaylistService.cs index 30a3e463a..a6b2a6397 100644 --- a/Screenbox.Core/Services/IPlaylistService.cs +++ b/Screenbox.Core/Services/IPlaylistService.cs @@ -33,4 +33,9 @@ public interface IPlaylistService /// Get media buffer indices around current position /// IReadOnlyList GetMediaBufferIndices(int currentIndex, int playlistCount, MediaPlaybackAutoRepeatMode repeatMode, int bufferSize = 5); + + /// + /// List persistent playlists from storage + /// + Task> ListPlaylistsAsync(); } diff --git a/Screenbox.Core/Services/LibraryService.cs b/Screenbox.Core/Services/LibraryService.cs index 2b7754eb2..2280275ba 100644 --- a/Screenbox.Core/Services/LibraryService.cs +++ b/Screenbox.Core/Services/LibraryService.cs @@ -1,15 +1,15 @@ #nullable enable -using CommunityToolkit.WinUI; -using Screenbox.Core.Factories; -using Screenbox.Core.Helpers; -using Screenbox.Core.Models; -using Screenbox.Core.ViewModels; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.WinUI; +using Screenbox.Core.Factories; +using Screenbox.Core.Helpers; +using Screenbox.Core.Models; +using Screenbox.Core.ViewModels; using Windows.Devices.Enumeration; using Windows.Foundation; using Windows.Foundation.Metadata; @@ -244,9 +244,8 @@ private async Task CacheVideosAsync(CancellationToken cancellationToken) } } - private List GetMediaFromCache(PersistentStorageLibrary libraryCache) + private List GetMediaFromCache(List records) { - var records = libraryCache.Records; List mediaList = records.Select(record => { MediaViewModel media = _mediaFactory.GetSingleton(new Uri(record.Path)); @@ -281,7 +280,7 @@ private async Task FetchMusicCancelableAsync(bool useCache, CancellationToken ca var libraryCache = await LoadStorageLibraryCacheAsync(SongsCacheFileName); if (libraryCache?.Records.Count > 0) { - songs = GetMediaFromCache(libraryCache); + songs = GetMediaFromCache(libraryCache.Records); hasCache = !AreLibraryPathsChanged(libraryCache.FolderPaths, MusicLibrary); // Update cache with changes from library tracker. Invalidate cache if needed. @@ -392,7 +391,7 @@ private async Task FetchVideosCancelableAsync(bool useCache, CancellationToken c var libraryCache = await LoadStorageLibraryCacheAsync(VideoCacheFileName); if (libraryCache?.Records.Count > 0) { - videos = GetMediaFromCache(libraryCache); + videos = GetMediaFromCache(libraryCache.Records); hasCache = !AreLibraryPathsChanged(libraryCache.FolderPaths, VideosLibrary); // Update cache with changes from library tracker. Invalidate cache if needed. @@ -691,7 +690,8 @@ async void FetchAction() { // pass } - }; + } + ; // Delay fetch due to query result not yet updated at this time _musicRefreshTimer.Debounce(FetchAction, TimeSpan.FromMilliseconds(1000)); } diff --git a/Screenbox.Core/ViewModels/PlaylistViewModel.cs b/Screenbox.Core/ViewModels/PlaylistViewModel.cs new file mode 100644 index 000000000..ee98224d5 --- /dev/null +++ b/Screenbox.Core/ViewModels/PlaylistViewModel.cs @@ -0,0 +1,59 @@ +#nullable enable + +using System; +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using Screenbox.Core.Factories; +using Screenbox.Core.Models; +using Screenbox.Core.Services; + +namespace Screenbox.Core.ViewModels; + +public partial class PlaylistViewModel : ObservableObject +{ + public ObservableCollection Items { get; } = new(); + + [ObservableProperty] private string _displayName = string.Empty; + [ObservableProperty] private DateTimeOffset _lastUpdated = DateTimeOffset.Now; + + private Guid _id; + + private readonly IPlaylistService _playlistService; + private readonly MediaViewModelFactory _mediaFactory; + + public PlaylistViewModel(IPlaylistService playlistService, MediaViewModelFactory mediaFactory) + { + _playlistService = playlistService; + _mediaFactory = mediaFactory; + } + + public Playlist GetPlaylist() + { + return new Playlist(Items); + } + + public void Load(PersistentPlaylist persistentPlaylist) + { + if (!Guid.TryParse(persistentPlaylist.Id, out _id)) return; + DisplayName = persistentPlaylist.DisplayName; + LastUpdated = persistentPlaylist.LastUpdated; + Items.Clear(); + foreach (var item in persistentPlaylist.Items) + { + Items.Add(ToMediaViewModel(item)); + } + } + + private MediaViewModel ToMediaViewModel(PersistentMediaRecord record) + { + MediaViewModel media = _mediaFactory.GetSingleton(new Uri(record.Path)); + if (!string.IsNullOrEmpty(record.Title)) media.Name = record.Title; + media.MediaInfo = new MediaInfo(record.Properties); + if (record.DateAdded != default) + { + DateTimeOffset utcTime = DateTime.SpecifyKind(record.DateAdded, DateTimeKind.Utc); + media.DateAdded = utcTime.ToLocalTime(); + } + return media; + } +} diff --git a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs index d4f4cac89..a43c714a7 100644 --- a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs +++ b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs @@ -3,72 +3,42 @@ using System.Collections.ObjectModel; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; -using Screenbox.Core.Models; +using CommunityToolkit.Mvvm.DependencyInjection; using Screenbox.Core.Services; namespace Screenbox.Core.ViewModels; public partial class PlaylistsPageViewModel : ObservableObject { - private readonly PlaylistService _playlistService; + private readonly IPlaylistService _playlistService; - [ObservableProperty] - private ObservableCollection _playlists = new(); + public ObservableCollection Playlists { get; } = new(); - [ObservableProperty] - private PersistentPlaylist? _selectedPlaylist; + [ObservableProperty] private PlaylistViewModel? _selectedPlaylist; - public PlaylistsPageViewModel(PlaylistService playlistService) + public PlaylistsPageViewModel(IPlaylistService playlistService) { _playlistService = playlistService; } - [RelayCommand] public async Task LoadPlaylistsAsync() { var loaded = await _playlistService.ListPlaylistsAsync(); Playlists.Clear(); foreach (var p in loaded) - Playlists.Add(p); - } - - [RelayCommand] - public async Task CreatePlaylistAsync() - { - // UI modal should collect display name and items, then call this command - var playlist = new PersistentPlaylist { - Id = System.Guid.NewGuid().ToString(), - DisplayName = string.Empty, // To be set by modal - Created = System.DateTimeOffset.Now, - Items = new() - }; - await _playlistService.SavePlaylistAsync(playlist); - Playlists.Add(playlist); - SelectedPlaylist = playlist; + var playlist = Ioc.Default.GetRequiredService(); + playlist.Load(p); + Playlists.Add(playlist); + } } - - [RelayCommand] - public async Task RenamePlaylistAsync(PersistentPlaylist playlist) + public void CreatePlaylist(string displayName) { - //playlist.DisplayName = newName; - //await _playlistService.SavePlaylistAsync(playlist); - } + var playlist = Ioc.Default.GetRequiredService(); + playlist.DisplayName = displayName; - [RelayCommand] - public async Task DeletePlaylistAsync(PersistentPlaylist playlist) - { - await _playlistService.DeletePlaylistAsync(playlist.Id); - Playlists.Remove(playlist); - if (SelectedPlaylist == playlist) - SelectedPlaylist = null; - } - - [RelayCommand] - public void SelectPlaylist(PersistentPlaylist playlist) - { - SelectedPlaylist = playlist; + // Assume sort by last updated + Playlists.Insert(0, playlist); } } From e004fea85e503ca728da421bcd15fa1cda92ffb9 Mon Sep 17 00:00:00 2001 From: Tung Huynh <31434093+huynhsontung@users.noreply.github.com> Date: Sat, 25 Oct 2025 17:45:38 -0700 Subject: [PATCH 08/31] Connect playlists page with view models --- Screenbox.Core/Common/ServiceHelpers.cs | 1 + .../ViewModels/PlaylistViewModel.cs | 6 +- .../ViewModels/PlaylistsPageViewModel.cs | 2 +- Screenbox/Pages/PlaylistsPage.xaml | 64 +++++++++++++++---- Screenbox/Pages/PlaylistsPage.xaml.cs | 23 +++---- 5 files changed, 69 insertions(+), 27 deletions(-) diff --git a/Screenbox.Core/Common/ServiceHelpers.cs b/Screenbox.Core/Common/ServiceHelpers.cs index b7b351b46..7fb8a1fa8 100644 --- a/Screenbox.Core/Common/ServiceHelpers.cs +++ b/Screenbox.Core/Common/ServiceHelpers.cs @@ -39,6 +39,7 @@ public static void PopulateCoreServices(ServiceCollection services) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddSingleton(); // Shared between many pages services.AddSingleton(); // Avoid thread lock services.AddSingleton(); // Global playlist diff --git a/Screenbox.Core/ViewModels/PlaylistViewModel.cs b/Screenbox.Core/ViewModels/PlaylistViewModel.cs index ee98224d5..a5f91f990 100644 --- a/Screenbox.Core/ViewModels/PlaylistViewModel.cs +++ b/Screenbox.Core/ViewModels/PlaylistViewModel.cs @@ -13,7 +13,9 @@ public partial class PlaylistViewModel : ObservableObject { public ObservableCollection Items { get; } = new(); - [ObservableProperty] private string _displayName = string.Empty; + [ObservableProperty] private string _caption = string.Empty; + [ObservableProperty] private bool _isPlaying; + [ObservableProperty] private object? _thumbnail; [ObservableProperty] private DateTimeOffset _lastUpdated = DateTimeOffset.Now; private Guid _id; @@ -35,7 +37,7 @@ public Playlist GetPlaylist() public void Load(PersistentPlaylist persistentPlaylist) { if (!Guid.TryParse(persistentPlaylist.Id, out _id)) return; - DisplayName = persistentPlaylist.DisplayName; + Caption = persistentPlaylist.DisplayName; LastUpdated = persistentPlaylist.LastUpdated; Items.Clear(); foreach (var item in persistentPlaylist.Items) diff --git a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs index a43c714a7..a79c7d4c5 100644 --- a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs +++ b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs @@ -36,7 +36,7 @@ public async Task LoadPlaylistsAsync() public void CreatePlaylist(string displayName) { var playlist = Ioc.Default.GetRequiredService(); - playlist.DisplayName = displayName; + playlist.Caption = displayName; // Assume sort by last updated Playlists.Insert(0, playlist); diff --git a/Screenbox/Pages/PlaylistsPage.xaml b/Screenbox/Pages/PlaylistsPage.xaml index c44486b4a..916519e62 100644 --- a/Screenbox/Pages/PlaylistsPage.xaml +++ b/Screenbox/Pages/PlaylistsPage.xaml @@ -8,8 +8,18 @@ xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:local="using:Screenbox.Pages" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:strings="using:Screenbox.Strings" + xmlns:triggers="using:CommunityToolkit.WinUI" + xmlns:ui="using:CommunityToolkit.WinUI" mc:Ignorable="d"> + + + + + + + @@ -22,23 +32,55 @@ x:Name="HeaderGrid" Grid.Row="0" MinHeight="{StaticResource PageHeaderMinHeight}" - Margin="{StaticResource BottomLargeMargin}" + Margin="{StaticResource BottomMediumMargin}" Padding="{StaticResource ContentPagePadding}"> + - + + + + + + + + + + + Minimal + + + + + + + + + diff --git a/Screenbox/Pages/PlaylistsPage.xaml.cs b/Screenbox/Pages/PlaylistsPage.xaml.cs index e12deab10..9afd8b536 100644 --- a/Screenbox/Pages/PlaylistsPage.xaml.cs +++ b/Screenbox/Pages/PlaylistsPage.xaml.cs @@ -1,17 +1,8 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices.WindowsRuntime; -using Windows.Foundation; -using Windows.Foundation.Collections; -using Windows.UI.Xaml; +#nullable enable + +using CommunityToolkit.Mvvm.DependencyInjection; +using Screenbox.Core.ViewModels; using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml.Controls.Primitives; -using Windows.UI.Xaml.Data; -using Windows.UI.Xaml.Input; -using Windows.UI.Xaml.Media; -using Windows.UI.Xaml.Navigation; // The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 @@ -21,8 +12,14 @@ namespace Screenbox.Pages; /// public sealed partial class PlaylistsPage : Page { + internal PlaylistsPageViewModel ViewModel => (PlaylistsPageViewModel)DataContext; + + internal CommonViewModel Common { get; } + public PlaylistsPage() { this.InitializeComponent(); + DataContext = Ioc.Default.GetRequiredService(); + Common = Ioc.Default.GetRequiredService(); } } From b7e4eb42783764d6439485eb646782bed5bccc82 Mon Sep 17 00:00:00 2001 From: Tung Huynh <31434093+huynhsontung@users.noreply.github.com> Date: Wed, 5 Nov 2025 23:49:54 -0800 Subject: [PATCH 09/31] wip: create playlist dialog --- Screenbox.Core/Services/IPlaylistService.cs | 26 +++++++++++++ .../ViewModels/PlaylistsPageViewModel.cs | 22 ++++++++++- Screenbox/Controls/CreatePlaylistDialog.xaml | 23 +++++++++++ .../Controls/CreatePlaylistDialog.xaml.cs | 39 +++++++++++++++++++ Screenbox/Pages/PlaylistsPage.xaml | 7 ++-- Screenbox/Pages/PlaylistsPage.xaml.cs | 11 ++++++ Screenbox/Screenbox.csproj | 7 ++++ 7 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 Screenbox/Controls/CreatePlaylistDialog.xaml create mode 100644 Screenbox/Controls/CreatePlaylistDialog.xaml.cs diff --git a/Screenbox.Core/Services/IPlaylistService.cs b/Screenbox.Core/Services/IPlaylistService.cs index a6b2a6397..24d589637 100644 --- a/Screenbox.Core/Services/IPlaylistService.cs +++ b/Screenbox.Core/Services/IPlaylistService.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Screenbox.Core.Models; using Windows.Media; +using Windows.Storage; using Windows.Storage.Search; namespace Screenbox.Core.Services; @@ -34,8 +35,33 @@ public interface IPlaylistService /// IReadOnlyList GetMediaBufferIndices(int currentIndex, int playlistCount, MediaPlaybackAutoRepeatMode repeatMode, int bufferSize = 5); + /// + /// Save a persistent playlist to storage + /// + Task SavePlaylistAsync(PersistentPlaylist playlist); + + /// + /// Load a persistent playlist from storage + /// + Task LoadPlaylistAsync(string id); + /// /// List persistent playlists from storage /// Task> ListPlaylistsAsync(); + + /// + /// Delete a persistent playlist from storage + /// + Task DeletePlaylistAsync(string id); + + /// + /// Save a thumbnail for a media item + /// + Task SaveThumbnailAsync(string mediaLocation, byte[] imageBytes); + + /// + /// Get a thumbnail file for a media item + /// + Task GetThumbnailFileAsync(string mediaLocation); } diff --git a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs index a79c7d4c5..fa7a3e920 100644 --- a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs +++ b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs @@ -1,9 +1,11 @@ #nullable enable +using System; using System.Collections.ObjectModel; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.DependencyInjection; +using Screenbox.Core.Models; using Screenbox.Core.Services; namespace Screenbox.Core.ViewModels; @@ -33,10 +35,26 @@ public async Task LoadPlaylistsAsync() } } - public void CreatePlaylist(string displayName) + public async Task CreatePlaylistAsync(string displayName) { + // Generate a new unique ID + var id = Guid.NewGuid().ToString(); + + // Create the persistent playlist + var persistentPlaylist = new PersistentPlaylist + { + Id = id, + DisplayName = displayName, + LastUpdated = DateTimeOffset.Now, + Items = new() + }; + + // Save to disk + await _playlistService.SavePlaylistAsync(persistentPlaylist); + + // Create view model and add to collection var playlist = Ioc.Default.GetRequiredService(); - playlist.Caption = displayName; + playlist.Load(persistentPlaylist); // Assume sort by last updated Playlists.Insert(0, playlist); diff --git a/Screenbox/Controls/CreatePlaylistDialog.xaml b/Screenbox/Controls/CreatePlaylistDialog.xaml new file mode 100644 index 000000000..18a52bd4c --- /dev/null +++ b/Screenbox/Controls/CreatePlaylistDialog.xaml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/Screenbox/Controls/CreatePlaylistDialog.xaml.cs b/Screenbox/Controls/CreatePlaylistDialog.xaml.cs new file mode 100644 index 000000000..ed1b8a228 --- /dev/null +++ b/Screenbox/Controls/CreatePlaylistDialog.xaml.cs @@ -0,0 +1,39 @@ +#nullable enable + +using System; +using System.Threading.Tasks; +using Screenbox.Helpers; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +// The Content Dialog item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 + +namespace Screenbox.Controls; + +public sealed partial class CreatePlaylistDialog : ContentDialog +{ + private const int MaxPlaylistNameLength = 100; + + public CreatePlaylistDialog() + { + this.InitializeComponent(); + FlowDirection = GlobalizationHelper.GetFlowDirection(); + RequestedTheme = ((FrameworkElement)Window.Current.Content).RequestedTheme; + } + + public static async Task GetPlaylistNameAsync() + { + CreatePlaylistDialog dialog = new(); + ContentDialogResult result = await dialog.ShowAsync(); + string playlistName = dialog.PlaylistNameTextBox.Text.Trim(); + return result == ContentDialogResult.Primary && !string.IsNullOrWhiteSpace(playlistName) + ? playlistName + : null; + } + + private void PlaylistNameTextBox_OnTextChanged(object sender, TextChangedEventArgs e) + { + string text = PlaylistNameTextBox.Text.Trim(); + IsPrimaryButtonEnabled = !string.IsNullOrWhiteSpace(text) && text.Length <= MaxPlaylistNameLength; + } +} diff --git a/Screenbox/Pages/PlaylistsPage.xaml b/Screenbox/Pages/PlaylistsPage.xaml index 916519e62..f5d69b1eb 100644 --- a/Screenbox/Pages/PlaylistsPage.xaml +++ b/Screenbox/Pages/PlaylistsPage.xaml @@ -39,16 +39,17 @@ Style="{StaticResource TitleMediumTextBlockStyle}" Text="Playlists" /> diff --git a/Screenbox/Strings/en-US/Resources.resw b/Screenbox/Strings/en-US/Resources.resw index 7e2924c11..c0ecb9d9c 100644 --- a/Screenbox/Strings/en-US/Resources.resw +++ b/Screenbox/Strings/en-US/Resources.resw @@ -1008,4 +1008,19 @@ Enter a name for this playlist + + Playlists + + + Delete + + + Rename + + + Enter a new name for this playlist + + + Rename playlist + \ No newline at end of file From d8738a8e7d12b0064e23198f37d9bfa9669a8718 Mon Sep 17 00:00:00 2001 From: Tung Huynh <31434093+huynhsontung@users.noreply.github.com> Date: Sun, 8 Feb 2026 10:30:09 +0800 Subject: [PATCH 30/31] use commands instead of onclick handlers for playlist rename and delete --- .../ViewModels/PlaylistsPageViewModel.cs | 3 +- Screenbox/Controls/DeletePlaylistDialog.xaml | 17 +++++++++++ .../Controls/DeletePlaylistDialog.xaml.cs | 21 ++++++++++++++ Screenbox/Pages/PlaylistDetailsPage.xaml | 10 +++---- Screenbox/Pages/PlaylistDetailsPage.xaml.cs | 27 +++++++----------- Screenbox/Pages/PlaylistsPage.xaml | 13 ++++----- Screenbox/Pages/PlaylistsPage.xaml.cs | 28 ++++++------------- Screenbox/Screenbox.csproj | 7 +++++ Screenbox/Strings/en-US/Resources.resw | 7 +++++ 9 files changed, 82 insertions(+), 51 deletions(-) create mode 100644 Screenbox/Controls/DeletePlaylistDialog.xaml create mode 100644 Screenbox/Controls/DeletePlaylistDialog.xaml.cs diff --git a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs index 41e56dec5..a7c6981c7 100644 --- a/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs +++ b/Screenbox.Core/ViewModels/PlaylistsPageViewModel.cs @@ -37,8 +37,7 @@ public async Task CreatePlaylistAsync(string displayName) public async Task RenamePlaylistAsync(PlaylistViewModel playlist, string newDisplayName) { - playlist.Name = newDisplayName; - await playlist.SaveAsync(); + await playlist.RenameAsync(newDisplayName); } public async Task DeletePlaylistAsync(PlaylistViewModel playlist) diff --git a/Screenbox/Controls/DeletePlaylistDialog.xaml b/Screenbox/Controls/DeletePlaylistDialog.xaml new file mode 100644 index 000000000..3755ecc2d --- /dev/null +++ b/Screenbox/Controls/DeletePlaylistDialog.xaml @@ -0,0 +1,17 @@ + + + + diff --git a/Screenbox/Controls/DeletePlaylistDialog.xaml.cs b/Screenbox/Controls/DeletePlaylistDialog.xaml.cs new file mode 100644 index 000000000..55b4b87c6 --- /dev/null +++ b/Screenbox/Controls/DeletePlaylistDialog.xaml.cs @@ -0,0 +1,21 @@ +using Screenbox.Helpers; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +// The Content Dialog item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238 + +namespace Screenbox.Controls; + +public sealed partial class DeletePlaylistDialog : ContentDialog +{ + private string PlaylistName { get; } + + public DeletePlaylistDialog(string playlistName) + { + this.DefaultStyleKey = typeof(ContentDialog); + this.InitializeComponent(); + FlowDirection = GlobalizationHelper.GetFlowDirection(); + RequestedTheme = ((FrameworkElement)Window.Current.Content).RequestedTheme; + PlaylistName = playlistName; + } +} diff --git a/Screenbox/Pages/PlaylistDetailsPage.xaml b/Screenbox/Pages/PlaylistDetailsPage.xaml index e87cf3c11..e5a400e9e 100644 --- a/Screenbox/Pages/PlaylistDetailsPage.xaml +++ b/Screenbox/Pages/PlaylistDetailsPage.xaml @@ -213,18 +213,18 @@ + Label="{strings:Resources Key=Rename}" /> + Label="{strings:Resources Key=Delete}" /> diff --git a/Screenbox/Pages/PlaylistDetailsPage.xaml.cs b/Screenbox/Pages/PlaylistDetailsPage.xaml.cs index ad8a51cbf..c5afeb606 100644 --- a/Screenbox/Pages/PlaylistDetailsPage.xaml.cs +++ b/Screenbox/Pages/PlaylistDetailsPage.xaml.cs @@ -3,9 +3,12 @@ using System; using System.Collections.Generic; using System.Numerics; +using System.Threading.Tasks; using CommunityToolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.Input; using CommunityToolkit.WinUI; using CommunityToolkit.WinUI.Animations.Expressions; +using Screenbox.Controls; using Screenbox.Core; using Screenbox.Core.ViewModels; using Windows.UI.Composition; @@ -198,11 +201,11 @@ public static TimeSpan GetTotalDuration(IReadOnlyCollection? ite return duration; } - private async void RenamePlaylistButton_OnClick(object sender, RoutedEventArgs e) + [RelayCommand] + private async Task RenamePlaylistAsync() { if (ViewModel.Source == null) return; - - Controls.RenamePlaylistDialog dialog = new(ViewModel.Source.Name); + RenamePlaylistDialog dialog = new(ViewModel.Source.Name); string? newName = await dialog.GetPlaylistNameAsync(); if (!string.IsNullOrWhiteSpace(newName) && newName != ViewModel.Source.Name) { @@ -210,22 +213,12 @@ private async void RenamePlaylistButton_OnClick(object sender, RoutedEventArgs e } } - private async void DeletePlaylistButton_OnClick(object sender, RoutedEventArgs e) + [RelayCommand] + private async Task DeletePlaylistAsync() { if (ViewModel.Source == null) return; - - ContentDialog dialog = new() - { - Title = "Delete Playlist", - Content = $"Are you sure you want to delete '{ViewModel.Source.Name}'?", - PrimaryButtonText = "Delete", - CloseButtonText = "Cancel", - DefaultButton = ContentDialogButton.Close - }; - dialog.FlowDirection = Helpers.GlobalizationHelper.GetFlowDirection(); - dialog.RequestedTheme = ((FrameworkElement)Window.Current.Content).RequestedTheme; - - ContentDialogResult result = await dialog.ShowAsync(); + var deleteConfirmation = new DeletePlaylistDialog(ViewModel.Source.Name); + var result = await deleteConfirmation.ShowAsync(); if (result == ContentDialogResult.Primary) { bool deleted = await ViewModel.DeletePlaylistAsync(); diff --git a/Screenbox/Pages/PlaylistsPage.xaml b/Screenbox/Pages/PlaylistsPage.xaml index 80b341c91..608eaa90a 100644 --- a/Screenbox/Pages/PlaylistsPage.xaml +++ b/Screenbox/Pages/PlaylistsPage.xaml @@ -16,16 +16,15 @@ mc:Ignorable="d"> - + - @@ -89,7 +88,7 @@ ItemsSource="{x:Bind ViewModel.Playlists}" SelectionMode="None"> - + diff --git a/Screenbox/Pages/PlaylistsPage.xaml.cs b/Screenbox/Pages/PlaylistsPage.xaml.cs index f531206c5..328e7d1d6 100644 --- a/Screenbox/Pages/PlaylistsPage.xaml.cs +++ b/Screenbox/Pages/PlaylistsPage.xaml.cs @@ -1,7 +1,9 @@ #nullable enable using System; +using System.Threading.Tasks; using CommunityToolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.Input; using Screenbox.Controls; using Screenbox.Core.ViewModels; using Windows.UI.Xaml; @@ -35,10 +37,9 @@ private async void HeaderCreateButton_OnClick(object sender, RoutedEventArgs e) } } - private async void RenamePlaylistMenuItem_OnClick(object sender, RoutedEventArgs e) + [RelayCommand] + private async Task RenamePlaylistAsync(PlaylistViewModel playlist) { - if (sender is not FrameworkElement { DataContext: PlaylistViewModel playlist }) return; - RenamePlaylistDialog dialog = new(playlist.Name); string? newName = await dialog.GetPlaylistNameAsync(); if (!string.IsNullOrWhiteSpace(newName) && newName != playlist.Name) @@ -47,25 +48,12 @@ private async void RenamePlaylistMenuItem_OnClick(object sender, RoutedEventArgs } } - private async void DeletePlaylistMenuItem_OnClick(object sender, RoutedEventArgs e) + [RelayCommand] + private async Task DeletePlaylistAsync(PlaylistViewModel playlist) { - if (sender is not FrameworkElement { DataContext: PlaylistViewModel playlist }) return; - - ContentDialog dialog = new() - { - Title = "Delete Playlist", - Content = $"Are you sure you want to delete '{playlist.Name}'?", - PrimaryButtonText = "Delete", - CloseButtonText = "Cancel", - DefaultButton = ContentDialogButton.Close - }; - dialog.FlowDirection = Helpers.GlobalizationHelper.GetFlowDirection(); - dialog.RequestedTheme = ((FrameworkElement)Windows.UI.Xaml.Window.Current.Content).RequestedTheme; - - ContentDialogResult result = await dialog.ShowAsync(); + var deleteConfirmation = new DeletePlaylistDialog(playlist.Name); + var result = await deleteConfirmation.ShowAsync(); if (result == ContentDialogResult.Primary) - { await ViewModel.DeletePlaylistAsync(playlist); - } } } diff --git a/Screenbox/Screenbox.csproj b/Screenbox/Screenbox.csproj index 38c55d166..6f2ca214c 100644 --- a/Screenbox/Screenbox.csproj +++ b/Screenbox/Screenbox.csproj @@ -172,6 +172,9 @@ CreatePlaylistDialog.xaml + + DeletePlaylistDialog.xaml + ErrorInfo.xaml @@ -558,6 +561,10 @@ MSBuild:Compile Designer + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/Screenbox/Strings/en-US/Resources.resw b/Screenbox/Strings/en-US/Resources.resw index c0ecb9d9c..be0691094 100644 --- a/Screenbox/Strings/en-US/Resources.resw +++ b/Screenbox/Strings/en-US/Resources.resw @@ -1023,4 +1023,11 @@ Rename playlist + + Delete playlist + + + Are you sure you want to delete '{0}' playlist? This action cannot be undone. + #Format[String playlistName] + \ No newline at end of file From 752df5a2b0d7c4a347d99ae4ef78396a7eaa667f Mon Sep 17 00:00:00 2001 From: Tung Huynh <31434093+huynhsontung@users.noreply.github.com> Date: Sun, 8 Feb 2026 10:33:42 +0800 Subject: [PATCH 31/31] Undo resources VS lint --- Screenbox/Strings/ar-SA/Resources.resw | 36 ++++++++++++------------ Screenbox/Strings/be-BY/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/de-DE/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/el-GR/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/es-ES/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/fr-FR/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/he-IL/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/hu-HU/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/id-ID/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/it-IT/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/ja-JP/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/ms-MY/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/nl-NL/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/pl-PL/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/pt-BR/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/pt-PT/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/ro-RO/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/ru-RU/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/si-LK/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/tr-TR/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/uk-UA/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/vi/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/zh-Hans/Resources.resw | 34 +++++++++++----------- Screenbox/Strings/zh-Hant/Resources.resw | 34 +++++++++++----------- 24 files changed, 409 insertions(+), 409 deletions(-) diff --git a/Screenbox/Strings/ar-SA/Resources.resw b/Screenbox/Strings/ar-SA/Resources.resw index 785518154..033ed1c2f 100644 --- a/Screenbox/Strings/ar-SA/Resources.resw +++ b/Screenbox/Strings/ar-SA/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + @@ -946,7 +946,7 @@ نقطة - + عرض اسم الفصل عندما يكون متاحًا diff --git a/Screenbox/Strings/be-BY/Resources.resw b/Screenbox/Strings/be-BY/Resources.resw index 28066ddf2..f1fbbf42f 100644 --- a/Screenbox/Strings/be-BY/Resources.resw +++ b/Screenbox/Strings/be-BY/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/de-DE/Resources.resw b/Screenbox/Strings/de-DE/Resources.resw index bc938d8f0..53b7b120f 100644 --- a/Screenbox/Strings/de-DE/Resources.resw +++ b/Screenbox/Strings/de-DE/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/el-GR/Resources.resw b/Screenbox/Strings/el-GR/Resources.resw index c47afe024..ee31be2ef 100644 --- a/Screenbox/Strings/el-GR/Resources.resw +++ b/Screenbox/Strings/el-GR/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/es-ES/Resources.resw b/Screenbox/Strings/es-ES/Resources.resw index 4dbb693fc..af6f7f55f 100644 --- a/Screenbox/Strings/es-ES/Resources.resw +++ b/Screenbox/Strings/es-ES/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/fr-FR/Resources.resw b/Screenbox/Strings/fr-FR/Resources.resw index f79e5f7cb..75cb46781 100644 --- a/Screenbox/Strings/fr-FR/Resources.resw +++ b/Screenbox/Strings/fr-FR/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/he-IL/Resources.resw b/Screenbox/Strings/he-IL/Resources.resw index ab6570725..3ee2dcca5 100644 --- a/Screenbox/Strings/he-IL/Resources.resw +++ b/Screenbox/Strings/he-IL/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/hu-HU/Resources.resw b/Screenbox/Strings/hu-HU/Resources.resw index d31bab9b0..92fcbe052 100644 --- a/Screenbox/Strings/hu-HU/Resources.resw +++ b/Screenbox/Strings/hu-HU/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/id-ID/Resources.resw b/Screenbox/Strings/id-ID/Resources.resw index 4317decfc..67201abf5 100644 --- a/Screenbox/Strings/id-ID/Resources.resw +++ b/Screenbox/Strings/id-ID/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/it-IT/Resources.resw b/Screenbox/Strings/it-IT/Resources.resw index 459b79aed..8b60bdf33 100644 --- a/Screenbox/Strings/it-IT/Resources.resw +++ b/Screenbox/Strings/it-IT/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/ja-JP/Resources.resw b/Screenbox/Strings/ja-JP/Resources.resw index 7a9493d1a..ef5d4f569 100644 --- a/Screenbox/Strings/ja-JP/Resources.resw +++ b/Screenbox/Strings/ja-JP/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/ms-MY/Resources.resw b/Screenbox/Strings/ms-MY/Resources.resw index 233d9d2cb..15b2a9bd3 100644 --- a/Screenbox/Strings/ms-MY/Resources.resw +++ b/Screenbox/Strings/ms-MY/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/nl-NL/Resources.resw b/Screenbox/Strings/nl-NL/Resources.resw index 3a06ea65e..ade6def7b 100644 --- a/Screenbox/Strings/nl-NL/Resources.resw +++ b/Screenbox/Strings/nl-NL/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/pl-PL/Resources.resw b/Screenbox/Strings/pl-PL/Resources.resw index ecc9e2db1..734c835bc 100644 --- a/Screenbox/Strings/pl-PL/Resources.resw +++ b/Screenbox/Strings/pl-PL/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/pt-BR/Resources.resw b/Screenbox/Strings/pt-BR/Resources.resw index 723fcef37..eda61a075 100644 --- a/Screenbox/Strings/pt-BR/Resources.resw +++ b/Screenbox/Strings/pt-BR/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/pt-PT/Resources.resw b/Screenbox/Strings/pt-PT/Resources.resw index 6cd19d683..442179821 100644 --- a/Screenbox/Strings/pt-PT/Resources.resw +++ b/Screenbox/Strings/pt-PT/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/ro-RO/Resources.resw b/Screenbox/Strings/ro-RO/Resources.resw index 74adebc3e..afee99bb3 100644 --- a/Screenbox/Strings/ro-RO/Resources.resw +++ b/Screenbox/Strings/ro-RO/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/ru-RU/Resources.resw b/Screenbox/Strings/ru-RU/Resources.resw index 4547a3fdc..10218fef5 100644 --- a/Screenbox/Strings/ru-RU/Resources.resw +++ b/Screenbox/Strings/ru-RU/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/si-LK/Resources.resw b/Screenbox/Strings/si-LK/Resources.resw index 446c8f285..81d5dc46c 100644 --- a/Screenbox/Strings/si-LK/Resources.resw +++ b/Screenbox/Strings/si-LK/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/tr-TR/Resources.resw b/Screenbox/Strings/tr-TR/Resources.resw index ff2193ebd..ec5d7e973 100644 --- a/Screenbox/Strings/tr-TR/Resources.resw +++ b/Screenbox/Strings/tr-TR/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/uk-UA/Resources.resw b/Screenbox/Strings/uk-UA/Resources.resw index 60e2a3357..60946ef65 100644 --- a/Screenbox/Strings/uk-UA/Resources.resw +++ b/Screenbox/Strings/uk-UA/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/vi/Resources.resw b/Screenbox/Strings/vi/Resources.resw index 3abd3aa2f..5580705e8 100644 --- a/Screenbox/Strings/vi/Resources.resw +++ b/Screenbox/Strings/vi/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/zh-Hans/Resources.resw b/Screenbox/Strings/zh-Hans/Resources.resw index 7f5c7f994..39b9b16fc 100644 --- a/Screenbox/Strings/zh-Hans/Resources.resw +++ b/Screenbox/Strings/zh-Hans/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - + diff --git a/Screenbox/Strings/zh-Hant/Resources.resw b/Screenbox/Strings/zh-Hant/Resources.resw index 1c19aba9f..2c3ca9937 100644 --- a/Screenbox/Strings/zh-Hant/Resources.resw +++ b/Screenbox/Strings/zh-Hant/Resources.resw @@ -59,46 +59,46 @@ : using a System.ComponentModel.TypeConverter : and then encoded with base64 encoding. --> - - + + - + - - - - + + + + - - + + - - + + - - - - + + + + - + - +