Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
cbadcce
add playlist nav page
huynhsontung Dec 21, 2024
2b5c673
create model and service for persistent playlist
huynhsontung Aug 21, 2025
3d90237
implement base viewmodel
huynhsontung Aug 21, 2025
13fec6a
fix playlistservice
huynhsontung Oct 20, 2025
c42b79e
fix more issues
huynhsontung Oct 20, 2025
880a3c9
fix PlaylistService
huynhsontung Oct 24, 2025
24ece21
create PlaylistViewModel
huynhsontung Oct 24, 2025
e004fea
Connect playlists page with view models
huynhsontung Oct 26, 2025
b7e4eb4
wip: create playlist dialog
huynhsontung Nov 6, 2025
22d4872
Merge branch 'main' into playlist-support
huynhsontung Nov 23, 2025
70185d7
support json read and write
huynhsontung Nov 23, 2025
9656b0d
fix XAML files
huynhsontung Nov 23, 2025
c2e9ebd
fix create playlists dialog style
huynhsontung Dec 6, 2025
355f1fb
using FileIO to write JSON to file
huynhsontung Dec 6, 2025
c8d2e11
feat: add PlaylistDetailsPage
huynhsontung Dec 6, 2025
21799c0
use FileIO to read json from file
huynhsontung Dec 7, 2025
e46d91f
save files to playlist
huynhsontung Dec 7, 2025
5124b19
move save method into PlaylistViewModel
huynhsontung Dec 7, 2025
0998298
rename and delete playlist
huynhsontung Dec 7, 2025
a8c6056
directly bind details page to PlaylistViewModel
huynhsontung Dec 7, 2025
127ab8d
Merge branch 'main' into playlist-support
huynhsontung Dec 19, 2025
8adb340
load playlists in the MainPageViewModel
huynhsontung Dec 20, 2025
ccd5929
Add to playlist sub (SongsPage)
huynhsontung Dec 20, 2025
69f72bd
Merge branch 'main' into playlist-support
huynhsontung Feb 1, 2026
5cce6a5
refine playlist grid view to show items count
huynhsontung Feb 1, 2026
09e7b3b
create CreatePlaylistCommand to combine dialog and VM logic
huynhsontung Feb 2, 2026
04b8451
refine AddToPlaylistFlyoutSubmenuBehavior and delete CreatePlaylistCo…
huynhsontung Feb 7, 2026
66f20be
add "add to playlist" submenu to all relevant pages
huynhsontung Feb 7, 2026
d5d2813
minor clean up
huynhsontung Feb 7, 2026
0dce146
handle duplicated items in PlaylistViewModel
huynhsontung Feb 7, 2026
627db76
localize CreatePlaylistDialog
huynhsontung Feb 7, 2026
fe03a5c
more localization
huynhsontung Feb 8, 2026
d8738a8
use commands instead of onclick handlers for playlist rename and delete
huynhsontung Feb 8, 2026
752df5a
Undo resources VS lint
huynhsontung Feb 8, 2026
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
4 changes: 4 additions & 0 deletions Screenbox.Core/Common/ServiceHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static void PopulateCoreServices(ServiceCollection services)
services.AddTransient<PlayQueueViewModel>();
services.AddTransient<AlbumDetailsPageViewModel>();
services.AddTransient<ArtistDetailsPageViewModel>();
services.AddTransient<PlaylistDetailsPageViewModel>();
services.AddTransient<SongsPageViewModel>();
services.AddTransient<AlbumsPageViewModel>();
services.AddTransient<ArtistsPageViewModel>();
Expand All @@ -41,6 +42,8 @@ public static void PopulateCoreServices(ServiceCollection services)
services.AddTransient<LivelyWallpaperPlayerViewModel>();
services.AddTransient<LivelyWallpaperSelectorViewModel>();
services.AddTransient<HomePageViewModel>();
services.AddTransient<PlaylistViewModel>();
services.AddTransient<PlaylistsPageViewModel>();
services.AddSingleton<CommonViewModel>(); // Shared between many pages
services.AddSingleton<VolumeViewModel>(); // Avoid thread lock
services.AddSingleton<MediaListViewModel>(); // Global playlist
Expand All @@ -57,6 +60,7 @@ public static void PopulateCoreServices(ServiceCollection services)

// Contexts
services.AddSingleton<PlayerContext>();
services.AddSingleton<PlaylistsContext>();
services.AddSingleton<CastContext>();
services.AddSingleton<LibraryContext>();

Expand Down
18 changes: 18 additions & 0 deletions Screenbox.Core/Contexts/PlaylistsContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#nullable enable

using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using Screenbox.Core.ViewModels;

namespace Screenbox.Core.Contexts;

/// <summary>
/// Context for holding the application-wide playlists.
/// </summary>
public sealed partial class PlaylistsContext : ObservableObject
{
/// <summary>
/// Gets the collection of playlists.
/// </summary>
public ObservableCollection<PlaylistViewModel> Playlists { get; } = new();
}
12 changes: 10 additions & 2 deletions Screenbox.Core/Models/MediaInfo.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#nullable enable

using Screenbox.Core.Enums;
using System;
using Screenbox.Core.Enums;
using Windows.Storage.FileProperties;

namespace Screenbox.Core.Models;

public sealed class MediaInfo
{
public MediaPlaybackType MediaType { get; set; }
Expand All @@ -17,11 +18,18 @@ public sealed class MediaInfo

public DateTimeOffset DateModified { get; }

public MediaInfo(MediaPlaybackType mediaType)
public MediaInfo(MediaPlaybackType mediaType, string title = "", uint year = default, TimeSpan duration = default)
{
MediaType = mediaType;
VideoProperties = new VideoInfo();
MusicProperties = new MusicInfo();

VideoProperties.Title = title;
VideoProperties.Duration = duration;
VideoProperties.Year = year;
MusicProperties.Title = title;
MusicProperties.Duration = duration;
MusicProperties.Year = year;
}

internal MediaInfo(IMediaProperties properties)
Expand Down
24 changes: 22 additions & 2 deletions Screenbox.Core/Models/PersistentMediaRecord.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#nullable enable

using System;
using System.Text.Json.Serialization;
using ProtoBuf;
using Screenbox.Core.Enums;

namespace Screenbox.Core.Models;

Expand All @@ -14,12 +16,23 @@ public class PersistentMediaRecord
[ProtoMember(2)]
public string Path { get; set; }

[JsonIgnore]
[ProtoMember(3)]
public IMediaProperties Properties { get; set; }
public IMediaProperties? Properties { get; set; }

[ProtoMember(4)]
public DateTime DateAdded { get; set; } // Must be UTC

[ProtoMember(5)]
public TimeSpan Duration { get; set; }

[ProtoMember(6)]
public uint Year { get; set; }

[ProtoMember(7)]
public MediaPlaybackType MediaType { get; set; }


#pragma warning disable CS8618 // Non-nullable field must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring as nullable.
public PersistentMediaRecord()
{
Expand All @@ -31,7 +44,14 @@ public PersistentMediaRecord(string title, string path, IMediaProperties propert
{
Title = title;
Path = path;
Properties = properties;
DateAdded = dateAdded.UtcDateTime;
Duration = properties.Duration;
Year = properties.Year;
MediaType = properties switch
{
VideoInfo => MediaPlaybackType.Video,
MusicInfo => MediaPlaybackType.Music,
_ => MediaPlaybackType.Unknown,
};
}
}
12 changes: 12 additions & 0 deletions Screenbox.Core/Models/PersistentPlaylist.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;

namespace Screenbox.Core.Models;

public class PersistentPlaylist
{
public string Id { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
public DateTimeOffset LastUpdated { get; set; }
public List<PersistentMediaRecord> Items { get; set; } = new();
}
5 changes: 3 additions & 2 deletions Screenbox.Core/Models/VideoInfo.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using ProtoBuf;
using System;
using System;
using ProtoBuf;
using Windows.Storage.FileProperties;

namespace Screenbox.Core.Models;

[ProtoContract]
public sealed class VideoInfo : IMediaProperties
{
Expand Down
5 changes: 5 additions & 0 deletions Screenbox.Core/Screenbox.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
<Compile Include="Common\NavigationMetadata.cs" />
<Compile Include="Contexts\CastContext.cs" />
<Compile Include="Contexts\LibraryContext.cs" />
<Compile Include="Contexts\PlaylistsContext.cs" />
<Compile Include="Enums\LivelyWallpaperType.cs" />
<Compile Include="Enums\MediaPlaybackType.cs" />
<Compile Include="Enums\ResourceName.cs" />
Expand Down Expand Up @@ -203,6 +204,7 @@
<Compile Include="Models\MediaInfo.cs" />
<Compile Include="Models\MediaLastPosition.cs" />
<Compile Include="Models\MusicInfo.cs" />
<Compile Include="Models\PersistentPlaylist.cs" />
<Compile Include="Models\PersistentStorageLibrary.cs" />
<Compile Include="Models\PlaybackNavigationResult.cs" />
<Compile Include="Models\Playlist.cs" />
Expand Down Expand Up @@ -288,7 +290,10 @@
<Compile Include="ViewModels\PlayerControlsViewModel.cs" />
<Compile Include="ViewModels\PlayerElementViewModel.cs" />
<Compile Include="ViewModels\PlayerPageViewModel.cs" />
<Compile Include="ViewModels\PlaylistDetailsPageViewModel.cs" />
<Compile Include="ViewModels\PlaylistViewModel.cs" />
<Compile Include="ViewModels\PlayQueueViewModel.cs" />
<Compile Include="ViewModels\PlaylistsPageViewModel.cs" />
<Compile Include="ViewModels\PlayQueuePageViewModel.cs" />
<Compile Include="ViewModels\PropertyViewModel.cs" />
<Compile Include="ViewModels\SearchResultPageViewModel.cs" />
Expand Down
44 changes: 26 additions & 18 deletions Screenbox.Core/Services/FilesService.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#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.Text.Json;
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;
Expand Down Expand Up @@ -112,14 +113,19 @@ public async Task<StorageFile> SaveToDiskAsync<T>(StorageFolder folder, string f

public async Task SaveToDiskAsync<T>(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();
if (file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
var json = JsonSerializer.Serialize(source);
await FileIO.WriteTextAsync(file, json);
}
else
{
using var stream = await file.OpenAsync(FileAccessMode.ReadWrite);
var writeStream = stream.AsStreamForWrite();
Serializer.Serialize(writeStream, source);
writeStream.SetLength(writeStream.Position); // A weird quirk of protobuf-net
await stream.FlushAsync();
}
}

public async Task<T> LoadFromDiskAsync<T>(StorageFolder folder, string fileName)
Expand All @@ -130,12 +136,14 @@ public async Task<T> LoadFromDiskAsync<T>(StorageFolder folder, string fileName)

public async Task<T> LoadFromDiskAsync<T>(StorageFile file)
{
using Stream readStream = await file.OpenStreamForReadAsync();
return Serializer.Deserialize<T>(readStream);
// using var dataReader = new StreamReader(readStream);
// using var jsonReader = new JsonTextReader(dataReader);
// var serializer = JsonSerializer.Create();
// return serializer.Deserialize<T>(jsonReader) ?? throw new NullReferenceException();
if (file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
string json = await FileIO.ReadTextAsync(file);
return JsonSerializer.Deserialize<T>(json) ?? throw new InvalidOperationException("Failed to deserialize JSON");
}

using var readStream = await file.OpenReadAsync();
return Serializer.Deserialize<T>(readStream.AsStream());
}

public async Task OpenFileLocationAsync(string path)
Expand Down
37 changes: 37 additions & 0 deletions Screenbox.Core/Services/IPlaylistService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
using System.Threading;
using System.Threading.Tasks;
using Screenbox.Core.Models;
using Screenbox.Core.ViewModels;
using Windows.Media;
using Windows.Storage;
using Windows.Storage.Search;

namespace Screenbox.Core.Services;
Expand Down Expand Up @@ -33,4 +35,39 @@ public interface IPlaylistService
/// Get media buffer indices around current position
/// </summary>
IReadOnlyList<int> GetMediaBufferIndices(int currentIndex, int playlistCount, MediaPlaybackAutoRepeatMode repeatMode, int bufferSize = 5);

/// <summary>
/// Save a persistent playlist to storage
/// </summary>
Task SavePlaylistAsync(PersistentPlaylist playlist);

/// <summary>
/// Load a persistent playlist from storage
/// </summary>
Task<PersistentPlaylist?> LoadPlaylistAsync(string id);

/// <summary>
/// List persistent playlists from storage
/// </summary>
Task<IReadOnlyList<PersistentPlaylist>> ListPlaylistsAsync();

/// <summary>
/// Delete a persistent playlist from storage
/// </summary>
Task DeletePlaylistAsync(string id);

/// <summary>
/// Save a thumbnail for a media item
/// </summary>
Task SaveThumbnailAsync(string mediaLocation, byte[] imageBytes);

/// <summary>
/// Get a thumbnail file for a media item
/// </summary>
Task<StorageFile?> GetThumbnailFileAsync(string mediaLocation);

/// <summary>
/// Appends media items to an existing persistent playlist and persists the updated playlist.
/// </summary>
Task AddToPlaylistAsync(string playlistId, IReadOnlyList<MediaViewModel> items);
}
4 changes: 3 additions & 1 deletion Screenbox.Core/Services/LibraryService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ private List<MediaViewModel> GetMediaFromCache(PersistentStorageLibrary libraryC
MediaViewModel media = _mediaFactory.GetSingleton(new Uri(record.Path));
media.IsFromLibrary = true;
if (!string.IsNullOrEmpty(record.Title)) media.Name = record.Title;
media.MediaInfo = new MediaInfo(record.Properties);
media.MediaInfo = record.Properties != null
? new MediaInfo(record.Properties)
: new MediaInfo(record.MediaType, record.Title, record.Year, record.Duration);
if (record.DateAdded != default)
{
DateTimeOffset utcTime = DateTime.SpecifyKind(record.DateAdded, DateTimeKind.Utc);
Expand Down
Loading