feat: go to next song and song progress
This commit is contained in:
parent
8655de32d7
commit
5d82d199c3
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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>();
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using Avalonia;
|
||||
using Avalonia.ReactiveUI;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ocarina2;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue