feat: go to next song and song progress

This commit is contained in:
Matyáš Caras 2023-04-27 20:41:07 +02:00
parent 8655de32d7
commit 5d82d199c3
10 changed files with 244 additions and 24 deletions

View File

@ -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",

View File

@ -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();
}
}

22
Ocarina2/Models/Artist.cs Normal file
View File

@ -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<MusicFile> Songs { get; }
public MusicFile? Selected
{
get => _selected;
set => this.RaiseAndSetIfChanged(ref _selected, value);
}
public Artist(string name)
{
Name = name.Trim();
Songs = new List<MusicFile>();
}
}

View File

@ -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)

View File

@ -1,7 +1,6 @@
using Avalonia;
using Avalonia.ReactiveUI;
using System;
using System.Runtime.InteropServices;
namespace Ocarina2;

View File

@ -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<MusicFile> MusicFiles {get;}
/// <summary>
/// Contains all music files as Artist instances for each unique artist
/// </summary>
public ObservableCollection<Artist> SongCollection {get;}
public ReactiveCommand<Unit,Unit> PlayPauseMusic { get; }
/// <summary>
/// Finds music files in the music library folder
/// </summary>
/// <returns>ObservableCollection of found files</returns>
private ObservableCollection<MusicFile> LoadFiles()
private ObservableCollection<Artist> LoadFiles()
{
var musicfiles = new ObservableCollection<MusicFile>();
var musicfiles = new ObservableCollection<Artist>();
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);
}
/// <summary>
/// Runs when a song is selected in the listbox to start playing it
/// </summary>
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
/// </summary>
private MediaPlayer? _player;
public ReactiveCommand<Unit,Unit> StopMusic { get; }
/// <summary>
/// Stops and disposes of the player
@ -159,45 +277,61 @@ public class MainWindowViewModel : ViewModelBase
{
_player?.Stop();
_player?.Dispose();
_positionTimer?.Dispose();
}
private TimeSpan? _position;
/// <summary>
/// Used to play/pause playback
/// </summary>
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);
}
}

View File

@ -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">
<Design.DataContext>
@ -10,13 +11,29 @@
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
<viewModels:MainWindowViewModel />
</Design.DataContext> <!-- TODO: Handle empty library -->
<ListBox x:Name="MusicBox" Grid.Row="1" Grid.Column="1" IsEnabled="{Binding MusicFiles.Count}"
Items="{Binding MusicFiles}" SelectedItem="{Binding SelectedFile}" SelectionChanged="MusicBox_OnSelectionChanged">
<UserControl.DataTemplates>
<DataTemplate DataType="{x:Type models:Artist}">
<StackPanel>
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
<ListBox x:Name="MusicBox" IsEnabled="{Binding Songs.Count}"
Items="{Binding Songs}" SelectedItem="{Binding Selected }" SelectionChanged="MusicBox_OnSelectionChanged">
<ListBox.ItemTemplate>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Metadata.Tag.Title}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</UserControl.DataTemplates>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto" Margin="5">
<ItemsControl Items="{Binding SongCollection}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Metadata.Tag.Title}" />
<ContentControl Content="{Binding}" Margin="0,5,0,0"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl>

View File

@ -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);
}
}

View File

@ -33,6 +33,7 @@
<Window.Resources>
<converter:SongMetadataConverter x:Key="SongMeta"/>
<converter:AlbumArtConverter x:Key="AlbumArt"/>
<converter:PositionConverter x:Key="Position"/>
</Window.Resources>
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
@ -48,6 +49,10 @@
<Separator />
<MenuItem Header="{Binding MenuExit}" />
</MenuItem>
<MenuItem Header="{Binding MenuSort}">
<MenuItem Header="{Binding MenuSortAlpha}" />
<MenuItem Header="{Binding MenuSortInOrder}" />
</MenuItem>
</Menu>
</DockPanel>
<Grid Grid.Row="1" Grid.Column="0" Grid.RowSpan="2" RowDefinitions="*,2*" Margin="0">
@ -64,11 +69,16 @@
<Image Source="{Binding SelectedFile.Metadata.Tag.Pictures, FallbackValue={materialIcons:MaterialIconExt Kind=Music} , Converter={StaticResource AlbumArt}}"/>
<TextBlock Text="{Binding SelectedFile, Converter={StaticResource SongMeta}, ConverterParameter=1}" FontWeight="Bold"/>
<TextBlock Text="{Binding SelectedFile, Converter={StaticResource SongMeta}, ConverterParameter=2}" />
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding CurrentPosition, Converter={StaticResource Position}, FallbackValue='00:00'}" />
<TextBlock Text=" / " />
<TextBlock Text="{Binding Duration, Converter={StaticResource Position}, FallbackValue='00:00'}" />
</StackPanel>
</StackPanel>
<!-- Playback Controls -->
<StackPanel Orientation="Horizontal" Grid.Row="2" Grid.Column="0">
<Button Content="{materialIcons:MaterialIconExt Kind=PlayPause}" Padding="5" Margin="5,0,0,0" Command="{Binding StopMusic}" />
<Button Content="{materialIcons:MaterialIconExt Kind=PlayPause}" Padding="5" Margin="5,0,0,0" Command="{Binding PlayPauseMusic}" />
<Button Content="{materialIcons:MaterialIconExt Kind=Stop}" Padding="5" Margin="5,0,0,0" Command="{Binding StopMusic}" />
</StackPanel>
</Grid>

View File

@ -1,6 +1,8 @@
using System;
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Ocarina2.ViewModels;
using ReactiveUI;
@ -8,12 +10,14 @@ namespace Ocarina2.Views;
public partial class MainWindow : Window
{
public static WindowNotificationManager WindowNotificationManager;
public MainWindow()
{
InitializeComponent();
WindowNotificationManager = new WindowNotificationManager(this);
Closing += (sender, args) =>
{
// dispose on closea
// dispose on close
if (sender == null) return;
if (sender.ToString() != "Ocarina2.Views.MainWindow") return;
App.Client.Dispose();