From 63af44cfb8de49d0c1710451728113be4e402471 Mon Sep 17 00:00:00 2001 From: Tung Huynh Date: Sat, 21 Dec 2024 00:18:39 -0800 Subject: [PATCH 1/4] 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 648f3f189..0ec166456 100644 --- a/Screenbox.Core/Screenbox.Core.csproj +++ b/Screenbox.Core/Screenbox.Core.csproj @@ -272,6 +272,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 5ed5c426e..bd7cfe901 100644 --- a/Screenbox/App.xaml +++ b/Screenbox/App.xaml @@ -87,6 +87,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 7e237178d..ad99ba5c0 100644 --- a/Screenbox/App.xaml.cs +++ b/Screenbox/App.xaml.cs @@ -139,6 +139,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 b9df65dc3..842743542 100644 --- a/Screenbox/Pages/MainPage.xaml +++ b/Screenbox/Pages/MainPage.xaml @@ -14,6 +14,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:muxc="using:Microsoft.UI.Xaml.Controls" xmlns:strings="using:Screenbox.Strings" + xmlns:ui="using:CommunityToolkit.WinUI" muxc:BackdropMaterial.ApplyToRootOrPageBackground="True" Loaded="MainPage_Loaded" mc:Ignorable="d"> @@ -183,6 +184,9 @@ + + + diff --git a/Screenbox/Pages/MainPage.xaml.cs b/Screenbox/Pages/MainPage.xaml.cs index 13e518bcf..c52c950f7 100644 --- a/Screenbox/Pages/MainPage.xaml.cs +++ b/Screenbox/Pages/MainPage.xaml.cs @@ -62,6 +62,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 1bebb0f98..12388a569 100644 --- a/Screenbox/Screenbox.csproj +++ b/Screenbox/Screenbox.csproj @@ -237,6 +237,9 @@ AlbumDetailsPage.xaml + + PlaylistsPage.xaml + AlbumSearchResultPage.xaml @@ -602,6 +605,10 @@ Designer MSBuild:Compile + + Designer + MSBuild:Compile + Designer MSBuild:Compile From 494eb8dfc587c4f32d777230f426d3f336ce0f63 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 2/4] create model and service for persistent playlist --- Screenbox.Core/Models/PersistentPlaylist.cs | 13 +++ .../Services/PlaylistStorageService.cs | 104 ++++++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 Screenbox.Core/Models/PersistentPlaylist.cs create mode 100644 Screenbox.Core/Services/PlaylistStorageService.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/PlaylistStorageService.cs b/Screenbox.Core/Services/PlaylistStorageService.cs new file mode 100644 index 000000000..2480dea68 --- /dev/null +++ b/Screenbox.Core/Services/PlaylistStorageService.cs @@ -0,0 +1,104 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Windows.Storage; +using Screenbox.Core.ViewModels; +using Screenbox.Core.Models; + +namespace Screenbox.Core.Services; + +public sealed class PlaylistService +{ + private const string PlaylistsFolderName = "Playlists"; + private const string ThumbnailsFolderName = "Thumbnails"; + private readonly FilesService _filesService; + + public PlaylistService(FilesService filesService) + { + _filesService = filesService; + } + + 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 f1df0e9c1999f922741912249e6473504b143c2e Mon Sep 17 00:00:00 2001 From: Tung Huynh <31434093+huynhsontung@users.noreply.github.com> Date: Thu, 21 Aug 2025 02:11:56 +0000 Subject: [PATCH 3/4] rename --- .../Services/{PlaylistStorageService.cs => PlaylistService.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Screenbox.Core/Services/{PlaylistStorageService.cs => PlaylistService.cs} (100%) diff --git a/Screenbox.Core/Services/PlaylistStorageService.cs b/Screenbox.Core/Services/PlaylistService.cs similarity index 100% rename from Screenbox.Core/Services/PlaylistStorageService.cs rename to Screenbox.Core/Services/PlaylistService.cs From fc7bd3561b8e144bde692a61812b6ff87c07b30a 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 4/4] 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; + } }