From 5d82d199c331317330ea17ae62eb67c932daae99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maty=C3=A1=C5=A1=20Caras?= Date: Thu, 27 Apr 2023 20:41:07 +0200 Subject: [PATCH] feat: go to next song and song progress --- Ocarina2/Assets/Languages/en_US.json | 3 + Ocarina2/Converter/PositionConverter.cs | 20 +++ Ocarina2/Models/Artist.cs | 22 +++ Ocarina2/Models/Language.cs | 8 +- Ocarina2/Program.cs | 1 - Ocarina2/ViewModels/MainWindowViewModel.cs | 160 +++++++++++++++++++-- Ocarina2/Views/LibraryView.axaml | 29 +++- Ocarina2/Views/LibraryView.axaml.cs | 7 +- Ocarina2/Views/MainWindow.axaml | 12 +- Ocarina2/Views/MainWindow.axaml.cs | 6 +- 10 files changed, 244 insertions(+), 24 deletions(-) create mode 100644 Ocarina2/Converter/PositionConverter.cs create mode 100644 Ocarina2/Models/Artist.cs diff --git a/Ocarina2/Assets/Languages/en_US.json b/Ocarina2/Assets/Languages/en_US.json index 14044c7..3dee798 100644 --- a/Ocarina2/Assets/Languages/en_US.json +++ b/Ocarina2/Assets/Languages/en_US.json @@ -3,6 +3,9 @@ "MenuFile":"_File", "MenuOpen":"_Open...", "MenuExit":"_Exit", + "MenuSort":"_Sort", + "MenuSortAlpha":"Sort alphabetically", + "MenuSortInOrder":"Sort in loaded order", "Music":"Music", "NoMusic":"Your music library is empty...", "RPSong":"Listening to $song", diff --git a/Ocarina2/Converter/PositionConverter.cs b/Ocarina2/Converter/PositionConverter.cs new file mode 100644 index 0000000..a00083b --- /dev/null +++ b/Ocarina2/Converter/PositionConverter.cs @@ -0,0 +1,20 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace Ocarina2.Converter; + +public class PositionConverter:IValueConverter +{ + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is not TimeSpan t) return new BindingNotification(new InvalidCastException(), BindingErrorType.Error); + return $"{(t.Minutes<10?"0"+t.Minutes:t.Minutes)}:{(t.Seconds<10?"0"+t.Seconds:t.Seconds)}"; + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/Ocarina2/Models/Artist.cs b/Ocarina2/Models/Artist.cs new file mode 100644 index 0000000..d9e2ff9 --- /dev/null +++ b/Ocarina2/Models/Artist.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using ReactiveUI; + +namespace Ocarina2.Models; + +public class Artist:ReactiveObject +{ + private MusicFile? _selected; + public string Name { get; } + public List Songs { get; } + public MusicFile? Selected + { + get => _selected; + set => this.RaiseAndSetIfChanged(ref _selected, value); + } + + public Artist(string name) + { + Name = name.Trim(); + Songs = new List(); + } +} \ No newline at end of file diff --git a/Ocarina2/Models/Language.cs b/Ocarina2/Models/Language.cs index 6254a4b..41da05f 100644 --- a/Ocarina2/Models/Language.cs +++ b/Ocarina2/Models/Language.cs @@ -17,8 +17,11 @@ public class Language public readonly string RPSong; public readonly string RPArtist; public readonly string RPIconText; + public readonly string MenuSort; + public readonly string MenuSortAlpha; + public readonly string MenuSortInOrder; - public Language(string code, string menuFile, string menuOpen, string menuExit, string music, string noMusic, string rpSong, string rpArtist, string rpIconText) + public Language(string code, string menuFile, string menuOpen, string menuExit, string music, string noMusic, string rpSong, string rpArtist, string rpIconText, string menuSort, string menuSortAlpha, string menuSortInOrder) { /* OCARINA2 Open-source music player and library manager @@ -45,6 +48,9 @@ public class Language RPSong = rpSong; RPArtist = rpArtist; RPIconText = rpIconText; + MenuSort = menuSort; + MenuSortAlpha = menuSortAlpha; + MenuSortInOrder = menuSortInOrder; } public static Language Load(string code) diff --git a/Ocarina2/Program.cs b/Ocarina2/Program.cs index 4c89431..5ca61b3 100644 --- a/Ocarina2/Program.cs +++ b/Ocarina2/Program.cs @@ -1,7 +1,6 @@ using Avalonia; using Avalonia.ReactiveUI; using System; -using System.Runtime.InteropServices; namespace Ocarina2; diff --git a/Ocarina2/ViewModels/MainWindowViewModel.cs b/Ocarina2/ViewModels/MainWindowViewModel.cs index f22bccf..93d014f 100644 --- a/Ocarina2/ViewModels/MainWindowViewModel.cs +++ b/Ocarina2/ViewModels/MainWindowViewModel.cs @@ -4,10 +4,17 @@ using System.IO; using System.Linq; using System.Reactive; using System.Threading; +using Avalonia; +using Avalonia.Controls.Notifications; +using Avalonia.Threading; using DiscordRPC; +using DynamicData; using ManagedBass; using Ocarina2.Models; +using Ocarina2.Views; using ReactiveUI; +using Notification = Avalonia.Controls.Notifications.Notification; +using Timer = System.Timers.Timer; namespace Ocarina2.ViewModels; @@ -35,6 +42,11 @@ public class MainWindowViewModel : ViewModelBase private string _menuExit; private string _music; private string _nomusic; + private string _menuSort; + private string _menuSortAlpha; + private string _menuSortInOrder; + private TimeSpan? _duration; + private Timer? _positionTimer; private MusicFile? _selectedFile; public MusicFile? SelectedFile { @@ -42,6 +54,36 @@ public class MainWindowViewModel : ViewModelBase set => this.RaiseAndSetIfChanged(ref _selectedFile, value); } + public string? MenuSort + { + get => _menuSort; + set => this.RaiseAndSetIfChanged(ref _menuSort, value); + } + + public string? MenuSortAlpha + { + get => _menuSortAlpha; + set => this.RaiseAndSetIfChanged(ref _menuSortAlpha, value); + } + + public string? MenuSortInOrder + { + get => _menuSortInOrder; + set => this.RaiseAndSetIfChanged(ref _menuSortInOrder, value); + } + + public TimeSpan? CurrentPosition + { + get => _position; + set => this.RaiseAndSetIfChanged(ref _position, value); + } + + public TimeSpan? Duration + { + get => _duration; + set => this.RaiseAndSetIfChanged(ref _duration, value); + } + public string MenuOpen { get => _menuOpen; @@ -72,15 +114,20 @@ public class MainWindowViewModel : ViewModelBase set => this.RaiseAndSetIfChanged(ref _nomusic, value); } - public ObservableCollection MusicFiles {get;} + /// + /// Contains all music files as Artist instances for each unique artist + /// + public ObservableCollection SongCollection {get;} + + public ReactiveCommand PlayPauseMusic { get; } /// /// Finds music files in the music library folder /// /// ObservableCollection of found files - private ObservableCollection LoadFiles() + private ObservableCollection LoadFiles() { - var musicfiles = new ObservableCollection(); + var musicfiles = new ObservableCollection(); if (!Directory.Exists(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic))) return musicfiles; var subfolders = Directory.GetDirectories(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic)); @@ -91,7 +138,20 @@ public class MainWindowViewModel : ViewModelBase foreach (var file in f) { var tfile = TagLib.File.Create(file); - musicfiles.Add(new MusicFile(tfile,file)); + var knownArtist = musicfiles.Where(x => x.Name == tfile.Tag.FirstPerformer.Trim()).ToList(); + // look for artist in our list + if (knownArtist.Any()) + { + musicfiles[musicfiles.IndexOf(knownArtist.First())].Songs.Add(new MusicFile(tfile,file)); + } + else + { + var newArtist = new Artist(tfile.Tag.FirstPerformer); + newArtist.Songs.Add(new MusicFile(tfile,file)); + musicfiles.Add(newArtist); + } + musicfiles[musicfiles.IndexOf(musicfiles.First(x => x.Name == tfile.Tag.FirstPerformer.Trim()))].Songs.Sort( + (x, y) => x.Metadata.Tag.Track.CompareTo(y.Metadata.Tag.Track)); // sort per Track tag } } // get directory-level files @@ -99,21 +159,68 @@ public class MainWindowViewModel : ViewModelBase foreach (var file in files) { var tfile = TagLib.File.Create(file); - musicfiles.Add(new MusicFile(tfile,file)); + var knownArtist = musicfiles.Where(x => x.Name == tfile.Tag.FirstPerformer.Trim()).ToList(); + // look for artist in our list + if (knownArtist.Any()) + { + musicfiles[musicfiles.IndexOf(knownArtist.First())].Songs.Add(new MusicFile(tfile,file)); + } + else + { + var newArtist = new Artist(tfile.Tag.FirstPerformer); + newArtist.Songs.Add(new MusicFile(tfile,file)); + musicfiles.Add(newArtist); + } + musicfiles[musicfiles.IndexOf(musicfiles.First(x => x.Name == tfile.Tag.FirstPerformer.Trim()))].Songs.Sort( + (x, y) => x.Metadata.Tag.Track.CompareTo(y.Metadata.Tag.Track)); + } return musicfiles; } - + public void PlayNext() + { + if (SelectedFile == null) return; + var currentArtist = SongCollection.Where(x => x.Name == SelectedFile.Metadata.Tag.FirstPerformer.Trim()).ToList(); + if (!currentArtist.Any()) return; + if (currentArtist[0].Songs.IndexOf(SelectedFile) == currentArtist[0].Songs.Count - 1)// last song of current artist + { + if (SongCollection.IndexOf(currentArtist[0]) == SongCollection.Count - 1) //if last artist go to first TODO: loop setting + { + FileSelected(SongCollection.First().Songs.First()); + return; + } + FileSelected(SongCollection[currentArtist[0].Songs.IndexOf(SelectedFile)+1].Songs.First()); // next artists first song + return; + } + + var np = currentArtist[0].Songs[currentArtist[0].Songs.IndexOf(SelectedFile) + 1]; + + Dispatcher.UIThread.Post(()=>MainWindow.WindowNotificationManager.Show(new Notification("Now playing",$"{np.Metadata.Tag.Title} by {np.Metadata.Tag.FirstPerformer}"))); + FileSelected(np); + } + /// /// Runs when a song is selected in the listbox to start playing it /// - public void FileSelected() + public void FileSelected(MusicFile? f) { - if (SelectedFile == null) return; + if (f == null) return; + Stop(); + if(SelectedFile != null && SelectedFile.Metadata.Tag.FirstPerformer.Trim() != f.Metadata.Tag.FirstPerformer.Trim()) + { + var a = SongCollection.Where(x => x.Name == SelectedFile.Metadata.Tag.FirstPerformer.Trim()).ToList(); + if (!a.Any()) return; + SongCollection[SongCollection.IndexOf(a[0])].Selected = null; + } + SelectedFile = f; + var b = SongCollection.Where(x => x.Name == SelectedFile.Metadata.Tag.FirstPerformer.Trim()).ToList(); + if (!b.Any()) return; + SongCollection[SongCollection.IndexOf(b[0])].Selected = f; + try { var t = new Thread(async () => // TODO: toto thread asi neni fajn? @@ -121,9 +228,17 @@ public class MainWindowViewModel : ViewModelBase _player = new MediaPlayer(); await _player.LoadAsync(SelectedFile.Path); _player.Play(); - App.Client.SetPresence(new RichPresence() + _player.MediaEnded += (_, _) => PlayNext(); + _positionTimer = new Timer(interval:1000); + _positionTimer.Elapsed += (_, _) => { - State = _l.RPArtist.Replace("$artist",SelectedFile.Metadata.Tag.FirstAlbumArtist), + CurrentPosition = _player.Position; + }; + _positionTimer.Start(); + Duration = _player.Duration; + App.Client.SetPresence(new RichPresence + { + State = _l.RPArtist.Replace("$artist",SelectedFile.Metadata.Tag.FirstPerformer), Details = _l.RPSong.Replace("$song",SelectedFile.Metadata.Tag.Title), Timestamps = new Timestamps { @@ -151,6 +266,9 @@ public class MainWindowViewModel : ViewModelBase /// private MediaPlayer? _player; public ReactiveCommand StopMusic { get; } + + + /// /// Stops and disposes of the player @@ -159,45 +277,61 @@ public class MainWindowViewModel : ViewModelBase { _player?.Stop(); _player?.Dispose(); + _positionTimer?.Dispose(); } + private TimeSpan? _position; + /// /// Used to play/pause playback /// public void PlayPause() { if (_player == null) return; + Console.WriteLine(_player.State); if (_player.State == PlaybackState.Playing) { _player.Stop(); + _positionTimer?.Stop(); + CurrentPosition = _player.Position; } else { _player.Play(); + _positionTimer?.Start(); + _player.Position = CurrentPosition ?? TimeSpan.Zero; + } } public MainWindowViewModel() { + _menuSort = _l.MenuSort; + _menuSortAlpha = _l.MenuSortAlpha; + _menuSortInOrder = _l.MenuSortInOrder; _menuOpen = _l.MenuOpen; _menuExit = _l.MenuExit; _menuFile = _l.MenuFile; _music = _l.Music; - MusicFiles = LoadFiles(); + SongCollection = LoadFiles(); _nomusic = _l.NoMusic; StopMusic = ReactiveCommand.Create(Stop); + PlayPauseMusic = ReactiveCommand.Create(PlayPause); } public MainWindowViewModel(Language l) { _l = l; + _menuSort = _l.MenuSort; + _menuSortAlpha = _l.MenuSortAlpha; + _menuSortInOrder = _l.MenuSortInOrder; _menuOpen = _l.MenuOpen; _menuExit = _l.MenuExit; _menuFile = _l.MenuFile; _music = _l.Music; _nomusic = _l.NoMusic; - MusicFiles = LoadFiles(); + SongCollection = LoadFiles(); StopMusic = ReactiveCommand.Create(Stop); - + PlayPauseMusic = ReactiveCommand.Create(PlayPause); } } \ No newline at end of file diff --git a/Ocarina2/Views/LibraryView.axaml b/Ocarina2/Views/LibraryView.axaml index 6a53f9c..8e4e791 100644 --- a/Ocarina2/Views/LibraryView.axaml +++ b/Ocarina2/Views/LibraryView.axaml @@ -3,6 +3,7 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:viewModels="clr-namespace:Ocarina2.ViewModels" + xmlns:models="clr-namespace:Ocarina2.Models" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ocarina2.Views.LibraryView"> @@ -10,13 +11,29 @@ to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) --> - + + + + + - + + + + + + + + + + + + - + - - + + + diff --git a/Ocarina2/Views/LibraryView.axaml.cs b/Ocarina2/Views/LibraryView.axaml.cs index 98491e3..19afdb0 100644 --- a/Ocarina2/Views/LibraryView.axaml.cs +++ b/Ocarina2/Views/LibraryView.axaml.cs @@ -1,5 +1,8 @@ +using System; +using System.IO; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Ocarina2.Models; using Ocarina2.ViewModels; namespace Ocarina2.Views; @@ -18,6 +21,8 @@ public partial class LibraryView : UserControl private void MusicBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) { - (DataContext as MainWindowViewModel)?.FileSelected(); + if (sender is not ListBox b) return; + (DataContext as MainWindowViewModel)?.FileSelected(b.SelectedItem as MusicFile); + } } \ No newline at end of file diff --git a/Ocarina2/Views/MainWindow.axaml b/Ocarina2/Views/MainWindow.axaml index 5014531..f05c6e4 100644 --- a/Ocarina2/Views/MainWindow.axaml +++ b/Ocarina2/Views/MainWindow.axaml @@ -33,6 +33,7 @@ + -