diff --git a/Harmonia.Core/Playlists/IPlaylistManager.cs b/Harmonia.Core/Playlists/IPlaylistManager.cs index e12457b..74efa52 100644 --- a/Harmonia.Core/Playlists/IPlaylistManager.cs +++ b/Harmonia.Core/Playlists/IPlaylistManager.cs @@ -9,6 +9,7 @@ public interface IPlaylistManager Task AddPlaylistAsync(); void RemovePlaylist(Playlist playlist); Playlist? FindPlaylistContaining(PlaylistSong playlistSong); + Playlist? FindPlaylistContaining(string playlistSongUID); event EventHandler? CurrentPlaylistChanged; event EventHandler PlaylistAdded; diff --git a/Harmonia.Core/Playlists/PlaylistManager.cs b/Harmonia.Core/Playlists/PlaylistManager.cs index 2c76c38..1f9b03c 100644 --- a/Harmonia.Core/Playlists/PlaylistManager.cs +++ b/Harmonia.Core/Playlists/PlaylistManager.cs @@ -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) diff --git a/Harmonia.WinUI/App.xaml.cs b/Harmonia.WinUI/App.xaml.cs index 1e06b5e..8a49cc0 100644 --- a/Harmonia.WinUI/App.xaml.cs +++ b/Harmonia.WinUI/App.xaml.cs @@ -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(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService().Session.Window); + services.AddSingleton(sp => sp.GetRequiredService().Session.Player); + services.AddSingleton(sp => sp.GetRequiredService().Session.Playlist); + + services.AddSingleton(); + services.AddSingleton(); + services.AddHarmonia(); ServiceProvider = services.BuildServiceProvider(); @@ -45,6 +54,15 @@ public partial class App : Application { await ServiceProvider.InitializeHarmoniaAsync(); + ISessionService sessionService = ServiceProvider.GetRequiredService(); + await sessionService.LoadAsync(); + + ISessionRestorer sessionRestorer = ServiceProvider.GetRequiredService(); + sessionRestorer.Restore(); + + ISessionTracker sessionTracker = ServiceProvider.GetRequiredService(); + sessionTracker.Start(); + _mainWindow = ServiceProvider.GetRequiredService(); _mainWindow.Activate(); } diff --git a/Harmonia.WinUI/MainWindow.xaml b/Harmonia.WinUI/MainWindow.xaml index e346843..6c3d5f1 100644 --- a/Harmonia.WinUI/MainWindow.xaml +++ b/Harmonia.WinUI/MainWindow.xaml @@ -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"> diff --git a/Harmonia.WinUI/MainWindow.xaml.cs b/Harmonia.WinUI/MainWindow.xaml.cs index 2dd425c..30daf16 100644 --- a/Harmonia.WinUI/MainWindow.xaml.cs +++ b/Harmonia.WinUI/MainWindow.xaml.cs @@ -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(); + } } \ No newline at end of file diff --git a/Harmonia.WinUI/Session/ISessionRestorer.cs b/Harmonia.WinUI/Session/ISessionRestorer.cs new file mode 100644 index 0000000..83b0e6f --- /dev/null +++ b/Harmonia.WinUI/Session/ISessionRestorer.cs @@ -0,0 +1,6 @@ +namespace Harmonia.WinUI.Session; + +public interface ISessionRestorer +{ + void Restore(); +} \ No newline at end of file diff --git a/Harmonia.WinUI/Session/ISessionService.cs b/Harmonia.WinUI/Session/ISessionService.cs new file mode 100644 index 0000000..f80a761 --- /dev/null +++ b/Harmonia.WinUI/Session/ISessionService.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Harmonia.WinUI.Session; + +public interface ISessionService +{ + ApplicationSession Session { get; } + Task LoadAsync(); + Task SaveAsync(); +} \ No newline at end of file diff --git a/Harmonia.WinUI/Session/ISessionTracker.cs b/Harmonia.WinUI/Session/ISessionTracker.cs new file mode 100644 index 0000000..92808ef --- /dev/null +++ b/Harmonia.WinUI/Session/ISessionTracker.cs @@ -0,0 +1,6 @@ +namespace Harmonia.WinUI.Session; + +public interface ISessionTracker +{ + void Start(); +} \ No newline at end of file diff --git a/Harmonia.WinUI/Session/JsonSessionService.cs b/Harmonia.WinUI/Session/JsonSessionService.cs new file mode 100644 index 0000000..4b787dc --- /dev/null +++ b/Harmonia.WinUI/Session/JsonSessionService.cs @@ -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(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); + } +} \ No newline at end of file diff --git a/Harmonia.WinUI/Session/SessionRestorer.cs b/Harmonia.WinUI/Session/SessionRestorer.cs new file mode 100644 index 0000000..703a866 --- /dev/null +++ b/Harmonia.WinUI/Session/SessionRestorer.cs @@ -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); + } +} \ No newline at end of file diff --git a/Harmonia.WinUI/Session/SessionTracker.cs b/Harmonia.WinUI/Session/SessionTracker.cs new file mode 100644 index 0000000..76303b6 --- /dev/null +++ b/Harmonia.WinUI/Session/SessionTracker.cs @@ -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; + } +} diff --git a/Harmonia.WinUI/ViewModels/PlaylistsViewModel.cs b/Harmonia.WinUI/ViewModels/PlaylistsViewModel.cs index a86e008..7c16206 100644 --- a/Harmonia.WinUI/ViewModels/PlaylistsViewModel.cs +++ b/Harmonia.WinUI/ViewModels/PlaylistsViewModel.cs @@ -69,6 +69,7 @@ public partial class PlaylistsViewModel : ViewModelBase _playlists = new ObservableCollection(_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) diff --git a/Harmonia.WinUI/Views/PlaylistsView.xaml.cs b/Harmonia.WinUI/Views/PlaylistsView.xaml.cs index a0cd9bd..0ca3027 100644 --- a/Harmonia.WinUI/Views/PlaylistsView.xaml.cs +++ b/Harmonia.WinUI/Views/PlaylistsView.xaml.cs @@ -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)