Added application session services, along with initial restore and tracker implementations.

This commit is contained in:
2026-08-06 02:11:47 -04:00
parent 75f820ed7b
commit c262ee4caa
13 changed files with 245 additions and 5 deletions

View File

@@ -9,6 +9,7 @@ public interface IPlaylistManager
Task<Playlist> AddPlaylistAsync();
void RemovePlaylist(Playlist playlist);
Playlist? FindPlaylistContaining(PlaylistSong playlistSong);
Playlist? FindPlaylistContaining(string playlistSongUID);
event EventHandler? CurrentPlaylistChanged;
event EventHandler<PlaylistAddedEventArgs> PlaylistAdded;

View File

@@ -88,6 +88,11 @@ public class PlaylistManager : IPlaylistManager
return _playlistsBySongUid.GetValueOrDefault(playlistSong.UID);
}
public Playlist? FindPlaylistContaining(string playlistSongUID)
{
return _playlistsBySongUid.GetValueOrDefault(playlistSongUID);
}
private async void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
{
if (sender is not Playlist playlist)

View File

@@ -1,6 +1,7 @@
using Harmonia.Core.Extensions;
using Harmonia.WinUI.Caching;
using Harmonia.WinUI.Messaging;
using Harmonia.WinUI.Session;
using Harmonia.WinUI.Storage;
using Harmonia.WinUI.ViewModels;
using Microsoft.Extensions.DependencyInjection;
@@ -31,6 +32,14 @@ public partial class App : Application
services.AddSingleton<IStorageProvider, WindowsStorageProvider>();
services.AddSingleton<IMessageService, WindowsMessageService>();
services.AddSingleton<ISessionService, JsonSessionService>();
services.AddSingleton(sp => sp.GetRequiredService<ISessionService>().Session.Window);
services.AddSingleton(sp => sp.GetRequiredService<ISessionService>().Session.Player);
services.AddSingleton(sp => sp.GetRequiredService<ISessionService>().Session.Playlist);
services.AddSingleton<ISessionRestorer, SessionRestorer>();
services.AddSingleton<ISessionTracker, SessionTracker>();
services.AddHarmonia();
ServiceProvider = services.BuildServiceProvider();
@@ -45,6 +54,15 @@ public partial class App : Application
{
await ServiceProvider.InitializeHarmoniaAsync();
ISessionService sessionService = ServiceProvider.GetRequiredService<ISessionService>();
await sessionService.LoadAsync();
ISessionRestorer sessionRestorer = ServiceProvider.GetRequiredService<ISessionRestorer>();
sessionRestorer.Restore();
ISessionTracker sessionTracker = ServiceProvider.GetRequiredService<ISessionTracker>();
sessionTracker.Start();
_mainWindow = ServiceProvider.GetRequiredService<MainWindow>();
_mainWindow.Activate();
}

View File

@@ -9,7 +9,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="Harmonia.WinUI">
Title="Harmonia.WinUI"
Closed="OnMainWindowClosed">
<Window.SystemBackdrop>
<MicaBackdrop />

View File

@@ -1,3 +1,5 @@
using Harmonia.WinUI.Session;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
namespace Harmonia.WinUI;
@@ -6,10 +8,17 @@ public sealed partial class MainWindow : Window
{
private const string ApplicationTitle = "Harmonia";
public MainWindow()
private readonly ISessionService _sessionService;
private bool _isSaved;
public MainWindow(ISessionService sessionService)
{
_sessionService = sessionService;
InitializeComponent();
InitializeTitleBar();
InitializeWindowState();
}
private void InitializeTitleBar()
@@ -18,4 +27,39 @@ public sealed partial class MainWindow : Window
ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar);
}
private void InitializeWindowState()
{
if (AppWindow.Presenter is OverlappedPresenter presenter && _sessionService.Session.Window.IsMaximized)
{
presenter.Maximize();
}
AppWindow.Changed += OnAppWindowChanged;
}
private void OnAppWindowChanged(AppWindow sender, AppWindowChangedEventArgs args)
{
if (sender.Presenter is not OverlappedPresenter presenter)
return;
// Ignore the minimized state so a minimize-then-close doesn't lose the maximized flag.
if (presenter.State is OverlappedPresenterState.Minimized)
return;
_sessionService.Session.Window.IsMaximized = presenter.State == OverlappedPresenterState.Maximized;
}
private async void OnMainWindowClosed(object sender, WindowEventArgs args)
{
if (_isSaved)
return;
args.Handled = true;
await _sessionService.SaveAsync();
_isSaved = true;
Close();
}
}

View File

@@ -0,0 +1,6 @@
namespace Harmonia.WinUI.Session;
public interface ISessionRestorer
{
void Restore();
}

View File

@@ -0,0 +1,10 @@
using System.Threading.Tasks;
namespace Harmonia.WinUI.Session;
public interface ISessionService
{
ApplicationSession Session { get; }
Task LoadAsync();
Task SaveAsync();
}

View File

@@ -0,0 +1,6 @@
namespace Harmonia.WinUI.Session;
public interface ISessionTracker
{
void Start();
}

View File

@@ -0,0 +1,47 @@
using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
namespace Harmonia.WinUI.Session;
public sealed class JsonSessionService : ISessionService
{
private static readonly JsonSerializerOptions Options = new()
{
WriteIndented = true
};
private readonly string _filePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Harmonia",
"session.json"
);
public ApplicationSession Session { get; private set; } = new();
public async Task LoadAsync()
{
try
{
if (File.Exists(_filePath))
{
await using var stream = File.OpenRead(_filePath);
Session = await JsonSerializer.DeserializeAsync<ApplicationSession>(stream) ?? new();
}
}
catch
{
Session = new();
}
}
public async Task SaveAsync()
{
string directoryName = Path.GetDirectoryName(_filePath)!;
Directory.CreateDirectory(directoryName);
await using var stream = File.Create(_filePath);
await JsonSerializer.SerializeAsync(stream, Session, Options);
}
}

View File

@@ -0,0 +1,55 @@
using Harmonia.Core.Player;
using Harmonia.Core.Playlists;
using System;
using System.Linq;
namespace Harmonia.WinUI.Session;
public class SessionRestorer(
ISessionService sessionService,
IPlaylistManager playlistManager,
IAudioPlayer audioPlayer) : ISessionRestorer
{
public void Restore()
{
RestorePlaylist();
RestoreAudioPlayer();
}
private void RestorePlaylist()
{
PlaylistState playlistState = sessionService.Session.Playlist;
if (string.IsNullOrWhiteSpace(playlistState.OpenPlaylistUID))
return;
Playlist? playlist = playlistManager.Playlists.FirstOrDefault(p => p.UID == playlistState.OpenPlaylistUID);
if (playlist is null)
return;
playlistManager.CurrentPlaylist = playlist;
}
private void RestoreAudioPlayer()
{
AudioPlayerState audioPlayerState = sessionService.Session.Player;
if (string.IsNullOrWhiteSpace(audioPlayerState.PlaylistSongUID))
return;
Playlist? playlist = playlistManager.FindPlaylistContaining(audioPlayerState.PlaylistSongUID);
if (playlist is null)
return;
PlaylistSong? song = playlist.Songs.FirstOrDefault(s => s.UID == audioPlayerState.PlaylistSongUID);
if (song is null)
return;
//PlaybackMode playbackMode = audioPlayerState.PlaybackState
audioPlayer.LoadAsync(song, PlaybackMode.LoadOnly);
}
}

View File

@@ -0,0 +1,29 @@
using Harmonia.Core.Player;
using Harmonia.Core.Playlists;
using System;
namespace Harmonia.WinUI.Session;
public class SessionTracker(
ISessionService sessionService,
IPlaylistManager playlistManager,
IAudioPlayer audioPlayer) : ISessionTracker
{
public void Start()
{
playlistManager.CurrentPlaylistChanged += OnCurrentPlaylistChanged;
audioPlayer.PlayingSongChanged += OnPlayingSongChanged;
}
private void OnCurrentPlaylistChanged(object? sender, EventArgs e)
{
PlaylistState playlistState = sessionService.Session.Playlist;
playlistState.OpenPlaylistUID = playlistManager.CurrentPlaylist?.UID;
}
private void OnPlayingSongChanged(object? sender, PlayingSongChangedEventArgs e)
{
AudioPlayerState audioPlayerState = sessionService.Session.Player;
audioPlayerState.PlaylistSongUID = e.NewSong?.UID;
}
}

View File

@@ -69,6 +69,7 @@ public partial class PlaylistsViewModel : ViewModelBase
_playlists = new ObservableCollection<PlaylistItemViewModel>(_playlistManager.Playlists.Select(p => new PlaylistItemViewModel(p)));
_selectedPlaylist = _playlists.FirstOrDefault(pv => pv.Playlist == _playlistManager.CurrentPlaylist);
_activePlaylist = _playlists.FirstOrDefault(pv => pv.Playlist == _audioPlayer.Playlist);
}
private void OnCurrentPlaylistChanged(object? sender, EventArgs e)

View File

@@ -17,6 +17,20 @@ public sealed partial class PlaylistsView : UserControl
_viewModel = (PlaylistsViewModel)DataContext;
_viewModel.PropertyChanging += OnViewModelPropertyChanging;
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
PlaylistsListView.ContainerContentChanging += OnContainerContentChanging;
}
private void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
{
if (args.InRecycleQueue || args.ItemContainer is not ListViewItem listViewItem)
return;
PlaylistItemStyle style = ReferenceEquals(args.Item, _viewModel.ActivePlaylist)
? PlaylistItemStyle.Selected
: PlaylistItemStyle.Normal;
UpdateListViewItemStyle(listViewItem, style);
}
private void OnViewModelPropertyChanging(object? sender, PropertyChangingEventArgs e)
@@ -41,11 +55,14 @@ public sealed partial class PlaylistsView : UserControl
private void UpdateListViewItemStyle(PlaylistItemStyle style)
{
ListViewItem listViewItem = (ListViewItem)PlaylistsListView.ContainerFromItem(_viewModel.ActivePlaylist);
if (listViewItem is null)
if (PlaylistsListView.ContainerFromItem(_viewModel.ActivePlaylist) is not ListViewItem listViewItem)
return;
UpdateListViewItemStyle(listViewItem, style);
}
private void UpdateListViewItemStyle(ListViewItem listViewItem, PlaylistItemStyle style)
{
FrameworkElement? frameworkElement = TryGetFrameworkElement(listViewItem, "PlaylistListViewItem");
if (frameworkElement == null)