From 1a9c1a54783e20b801ddb70361e5a233d70a2e8e Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Sun, 2 Mar 2025 13:46:33 -0500 Subject: [PATCH] Adding initial playback bar logic. --- Harmonia.Core/Data/FileRepository.cs | 7 +- Harmonia.Core/Imaging/AudioImageExtractor.cs | 7 +- .../Playlists/IPlaylistRepository.cs | 5 ++ Harmonia.Core/Playlists/Playlist.cs | 2 +- Harmonia.Core/Playlists/PlaylistManager.cs | 6 +- Harmonia.Core/Playlists/PlaylistRepository.cs | 46 +++++++++- Harmonia.Core/Playlists/PlaylistSong.cs | 2 +- Harmonia.UI/App.axaml | 5 +- .../Converters/ArtistsToStringConverter.cs | 32 +++++++ .../Converters/NullVisibilityConverter.cs | 29 ++++++ .../Converters/SecondsToStringConverter.cs | 37 ++++++++ Harmonia.UI/Converters/SongTitleConverter.cs | 27 ++++++ Harmonia.UI/Harmonia.UI.csproj | 11 +-- .../ViewModels/PlaybackBarViewModel.cs | 33 ++++++- Harmonia.UI/Views/PlaybackBar.axaml | 88 +++++++++++++++++-- Harmonia.UI/Views/PlaybackBar.axaml.cs | 62 +++++++++++++ 16 files changed, 374 insertions(+), 25 deletions(-) create mode 100644 Harmonia.UI/Converters/ArtistsToStringConverter.cs create mode 100644 Harmonia.UI/Converters/NullVisibilityConverter.cs create mode 100644 Harmonia.UI/Converters/SecondsToStringConverter.cs create mode 100644 Harmonia.UI/Converters/SongTitleConverter.cs diff --git a/Harmonia.Core/Data/FileRepository.cs b/Harmonia.Core/Data/FileRepository.cs index a2d7b0e..23e58e5 100644 --- a/Harmonia.Core/Data/FileRepository.cs +++ b/Harmonia.Core/Data/FileRepository.cs @@ -13,9 +13,12 @@ public abstract class FileRepository : IRepository where TObje public FileRepository() { - if (string.IsNullOrWhiteSpace(DirectoryName) || Directory.Exists(DirectoryName) == false) + if (string.IsNullOrWhiteSpace(DirectoryName)) return; + if (Directory.Exists(DirectoryName) == false) + Directory.CreateDirectory(DirectoryName); + if (string.IsNullOrWhiteSpace(Extension)) return; @@ -30,7 +33,7 @@ public abstract class FileRepository : IRepository where TObje var fileInfoList = directoryInfo.EnumerateFiles("*." + Extension, SearchOption.TopDirectoryOnly).Where(x => x.Attributes.HasFlag(FileAttributes.Hidden) == false); - return fileInfoList.Select(fileInfo => fileInfo.FullName).ToList(); + return [.. fileInfoList.Select(fileInfo => fileInfo.FullName)]; } private void LoadFileNamesIntoMap(List fileNames) diff --git a/Harmonia.Core/Imaging/AudioImageExtractor.cs b/Harmonia.Core/Imaging/AudioImageExtractor.cs index 3ade0dc..f9fc515 100644 --- a/Harmonia.Core/Imaging/AudioImageExtractor.cs +++ b/Harmonia.Core/Imaging/AudioImageExtractor.cs @@ -74,12 +74,17 @@ public class AudioImageExtractor : IAudioImageExtractor private string? GetImagePath(string path) { + string? directoryName = Path.GetDirectoryName(path); + + if (string.IsNullOrWhiteSpace(directoryName)) + return null; + string[] fileNames; string extension; foreach (string searchPattern in _searchPatterns) { - fileNames = GetFilesFromDirectory(path, searchPattern); + fileNames = GetFilesFromDirectory(directoryName, searchPattern); foreach (string fileName in fileNames) { diff --git a/Harmonia.Core/Playlists/IPlaylistRepository.cs b/Harmonia.Core/Playlists/IPlaylistRepository.cs index fd22cf6..af93dcd 100644 --- a/Harmonia.Core/Playlists/IPlaylistRepository.cs +++ b/Harmonia.Core/Playlists/IPlaylistRepository.cs @@ -5,4 +5,9 @@ namespace Harmonia.Core.Playlists; public interface IPlaylistRepository : IRepository { Playlist? GetPlaylist(PlaylistSong playlistSong); + void AddPlaylist(); + void RemovePlaylist(Playlist playlist); + + event EventHandler PlaylistAdded; + event EventHandler PlaylistRemoved; } \ No newline at end of file diff --git a/Harmonia.Core/Playlists/Playlist.cs b/Harmonia.Core/Playlists/Playlist.cs index b89a32f..10a0e1f 100644 --- a/Harmonia.Core/Playlists/Playlist.cs +++ b/Harmonia.Core/Playlists/Playlist.cs @@ -7,7 +7,7 @@ public class Playlist { public string UID { get; init; } = Guid.NewGuid().ToString(); public string? Name { get; set; } - public List Songs { get; } = []; + public List Songs { get; init; } = []; // TODO: Change to "private init" once deserialization is fixed public List GroupOptions { get; set; } = []; public List SortOptions { get; set; } = []; public bool IsLocked { get; set; } diff --git a/Harmonia.Core/Playlists/PlaylistManager.cs b/Harmonia.Core/Playlists/PlaylistManager.cs index b7dc6d6..84ea04f 100644 --- a/Harmonia.Core/Playlists/PlaylistManager.cs +++ b/Harmonia.Core/Playlists/PlaylistManager.cs @@ -1,8 +1,6 @@ -using Harmonia.Core.Data; +namespace Harmonia.Core.Playlists; -namespace Harmonia.Core.Playlists; - -public class PlaylistManager(IRepository playlistRepository) : IPlaylistManager +public class PlaylistManager(IPlaylistRepository playlistRepository) : IPlaylistManager { private Playlist? _currentPlaylist; public Playlist? CurrentPlaylist diff --git a/Harmonia.Core/Playlists/PlaylistRepository.cs b/Harmonia.Core/Playlists/PlaylistRepository.cs index 3e3ea05..623ac36 100644 --- a/Harmonia.Core/Playlists/PlaylistRepository.cs +++ b/Harmonia.Core/Playlists/PlaylistRepository.cs @@ -4,7 +4,26 @@ namespace Harmonia.Core.Playlists; public class PlaylistRepository : JsonFileRepository, IPlaylistRepository { - protected override string DirectoryName => string.Empty; + protected override string DirectoryName => Path.Combine("Playlists"); + + public PlaylistRepository() + { + List playlists = Get(); + + foreach (Playlist playlist in playlists) + { + playlist.PlaylistUpdated += OnPlaylistUpdated; + } + } + + private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e) + { + if (sender is not Playlist playlist) + return; + + Save(playlist); + //PlaylistUpdated?.Invoke(sender, e); + } public Playlist? GetPlaylist(PlaylistSong playlistSong) { @@ -24,4 +43,29 @@ public class PlaylistRepository : JsonFileRepository, IPlaylistReposit throw new Exception("Unable to determine new fileName"); } + + public event EventHandler? PlaylistAdded; + //public event EventHandler? PlaylistUpdated; + public event EventHandler? PlaylistRemoved; + + public void AddPlaylist() + { + Playlist playlist = new() + { + Name = "New Playlist" + }; + + playlist.PlaylistUpdated += OnPlaylistUpdated; + + Save(playlist); + PlaylistAdded?.Invoke(this, new(playlist)); + } + + public void RemovePlaylist(Playlist playlist) + { + playlist.PlaylistUpdated -= OnPlaylistUpdated; + + Delete(playlist); + PlaylistRemoved?.Invoke(this, new(playlist)); + } } \ No newline at end of file diff --git a/Harmonia.Core/Playlists/PlaylistSong.cs b/Harmonia.Core/Playlists/PlaylistSong.cs index e18bf64..488c23b 100644 --- a/Harmonia.Core/Playlists/PlaylistSong.cs +++ b/Harmonia.Core/Playlists/PlaylistSong.cs @@ -5,5 +5,5 @@ namespace Harmonia.Core.Playlists; public class PlaylistSong(Song song) { public string UID { get; } = Guid.NewGuid().ToString(); - public Song Song { get; } = song; + public Song Song { get; init; } = song; } \ No newline at end of file diff --git a/Harmonia.UI/App.axaml b/Harmonia.UI/App.axaml index 28ae225..9e9aa24 100644 --- a/Harmonia.UI/App.axaml +++ b/Harmonia.UI/App.axaml @@ -1,12 +1,15 @@ - + + diff --git a/Harmonia.UI/Converters/ArtistsToStringConverter.cs b/Harmonia.UI/Converters/ArtistsToStringConverter.cs new file mode 100644 index 0000000..3d7ae69 --- /dev/null +++ b/Harmonia.UI/Converters/ArtistsToStringConverter.cs @@ -0,0 +1,32 @@ +using Avalonia.Data.Converters; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Harmonia.UI.Converters; + +public class ArtistsToStringConverter : IValueConverter +{ + public ArtistsToStringConverter() + { + + } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not List) + return string.Empty; + + List artists = (List)value; + + if (artists == null) + return string.Empty; + + return string.Join(" / ", artists); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return new List(); + } +} \ No newline at end of file diff --git a/Harmonia.UI/Converters/NullVisibilityConverter.cs b/Harmonia.UI/Converters/NullVisibilityConverter.cs new file mode 100644 index 0000000..a805f1a --- /dev/null +++ b/Harmonia.UI/Converters/NullVisibilityConverter.cs @@ -0,0 +1,29 @@ +using Avalonia.Data.Converters; +using System; +using System.Collections; +using System.Globalization; + +namespace Harmonia.UI.Converters; + +public sealed class NullVisibilityConverter : IValueConverter +{ + public NullVisibilityConverter() + { + + } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is IList list) + { + return list.Count > 0; + } + + return value is not null; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Harmonia.UI/Converters/SecondsToStringConverter.cs b/Harmonia.UI/Converters/SecondsToStringConverter.cs new file mode 100644 index 0000000..2da5f88 --- /dev/null +++ b/Harmonia.UI/Converters/SecondsToStringConverter.cs @@ -0,0 +1,37 @@ +using Avalonia.Data.Converters; +using System; +using System.Globalization; + +namespace Harmonia.UI.Converters; + +public sealed class SecondsToStringConverter : IValueConverter +{ + public SecondsToStringConverter() + { + + } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not double doubleValue) + return null; + + TimeSpan timeSpan = TimeSpan.FromSeconds(doubleValue); + + if (timeSpan.Hours >= 1) + return timeSpan.ToString(@"%h\:mm\:ss"); + + return timeSpan.ToString(@"%m\:ss"); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value == null) + return null; + + if (value is not string stringValue) + return null; + + return TimeSpan.Parse(stringValue); + } +} \ No newline at end of file diff --git a/Harmonia.UI/Converters/SongTitleConverter.cs b/Harmonia.UI/Converters/SongTitleConverter.cs new file mode 100644 index 0000000..47a794a --- /dev/null +++ b/Harmonia.UI/Converters/SongTitleConverter.cs @@ -0,0 +1,27 @@ +using Avalonia.Data.Converters; +using Harmonia.Core.Models; +using System; +using System.Globalization; + +namespace Harmonia.UI.Converters; + +public sealed class SongTitleConverter : IValueConverter +{ + public SongTitleConverter() + { + + } + + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not Song song) + return null; + + return string.IsNullOrWhiteSpace(song.Title) ? song.ShortFileName : song.Title; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Harmonia.UI/Harmonia.UI.csproj b/Harmonia.UI/Harmonia.UI.csproj index 3e23391..6e55c98 100644 --- a/Harmonia.UI/Harmonia.UI.csproj +++ b/Harmonia.UI/Harmonia.UI.csproj @@ -11,14 +11,15 @@ - - - - + + + + - + + diff --git a/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs b/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs index a77d007..5229752 100644 --- a/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs +++ b/Harmonia.UI/ViewModels/PlaybackBarViewModel.cs @@ -1,10 +1,14 @@ -using Avalonia.Media.Imaging; +using Avalonia.Controls; +using Avalonia.Media.Imaging; using Avalonia.Threading; using Harmonia.Core.Caching; using Harmonia.Core.Imaging; using Harmonia.Core.Models; using Harmonia.Core.Player; +using Harmonia.Core.Playlists; +using Harmonia.Core.Scanner; using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -14,8 +18,8 @@ public partial class PlaybackBarViewModel : ViewModelBase { private readonly IAudioPlayer _audioPlayer; private readonly IAudioImageCache _audioImageCache; + private readonly DispatcherTimer _timer; - private bool _isPositionChangeInProgress; private CancellationTokenSource? _audioImageCancellationTokenSource; private Song? _song; @@ -95,6 +99,17 @@ public partial class PlaybackBarViewModel : ViewModelBase } } + private bool _isPositionChangeInProgress; + public bool IsPositionChangeInProgress + { + get { return _isPositionChangeInProgress; } + set + { + _isPositionChangeInProgress = value; + OnPropertyChanged(); + } + } + public bool IsRandom { get @@ -110,12 +125,14 @@ public partial class PlaybackBarViewModel : ViewModelBase public string Greeting => "Welcome to Harmonia!"; - public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache) + public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache, IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner) { _audioPlayer = audioPlayer; _audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged; _audioImageCache = audioImageCache; + + _timer = new(TimeSpan.FromMilliseconds(100), DispatcherPriority.Default, TickTock); } private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e) @@ -149,6 +166,16 @@ public partial class PlaybackBarViewModel : ViewModelBase SongImageSource = new(songPictureInfo.Stream); } + private void TickTock(object? sender, object e) + { + Position = _audioPlayer.Position; + + if (IsPositionChangeInProgress) + return; + + CurrentPosition = _audioPlayer.Position; + } + public void Play() { _audioPlayer.Play(); diff --git a/Harmonia.UI/Views/PlaybackBar.axaml b/Harmonia.UI/Views/PlaybackBar.axaml index 55f9d9b..24c75c0 100644 --- a/Harmonia.UI/Views/PlaybackBar.axaml +++ b/Harmonia.UI/Views/PlaybackBar.axaml @@ -3,15 +3,91 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:vm="clr-namespace:Harmonia.UI.ViewModels" + xmlns:converter="clr-namespace:Harmonia.UI.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" DataContext="{x:Static vm:ViewModelLocator.PlaybackBarViewModel}" x:Class="Harmonia.UI.Views.PlaybackBar" x:DataType="vm:PlaybackBarViewModel"> - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Harmonia.UI/Views/PlaybackBar.axaml.cs b/Harmonia.UI/Views/PlaybackBar.axaml.cs index cb3e0b9..6b40747 100644 --- a/Harmonia.UI/Views/PlaybackBar.axaml.cs +++ b/Harmonia.UI/Views/PlaybackBar.axaml.cs @@ -1,11 +1,73 @@ using Avalonia.Controls; +using Avalonia.Input; +using Avalonia.Interactivity; +using Harmonia.UI.ViewModels; namespace Harmonia.UI.Views; public partial class PlaybackBar : UserControl { + private readonly PlaybackBarViewModel _viewModel; + public PlaybackBar() { InitializeComponent(); + _viewModel = (PlaybackBarViewModel)DataContext!; + } + + private void Slider_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e) + { + if (sender is not Slider slider) + return; + + slider.AddHandler(PointerPressedEvent, OnSliderPointerPressed, RoutingStrategies.Tunnel); + slider.AddHandler(PointerReleasedEvent, OnSliderPointerReleased, RoutingStrategies.Tunnel); + } + + private void OnSliderPointerPressed(object? sender, PointerPressedEventArgs e) + { + if (sender is not Slider slider) + return; + + PointerPoint currentPoint = e.GetCurrentPoint(slider); + + if (currentPoint.Properties.IsLeftButtonPressed == false) + return; + + _viewModel.IsPositionChangeInProgress = true; + } + + private void OnSliderPointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (sender is not Slider slider) + return; + + _viewModel.CurrentPosition = slider.Value; + _viewModel.IsPositionChangeInProgress = false; + } + + private void PlayButton_Click(object? sender, RoutedEventArgs e) + { + _viewModel.Play(); + } + + private void StopButton_Click(object? sender, RoutedEventArgs e) + { + _viewModel.Stop(); + } + + private void PauseButton_Click(object? sender, RoutedEventArgs e) + { + _viewModel.Pause(); + } + + private void PreviousSongButton_Click(object? sender, RoutedEventArgs e) + { + _viewModel.Previous(); + } + + private void NextSongButton_Click(object sender, RoutedEventArgs e) + { + _viewModel.Next(); } } \ No newline at end of file