feat: go to next song and song progress
This commit is contained in:
parent
8655de32d7
commit
5d82d199c3
|
@ -3,6 +3,9 @@
|
||||||
"MenuFile":"_File",
|
"MenuFile":"_File",
|
||||||
"MenuOpen":"_Open...",
|
"MenuOpen":"_Open...",
|
||||||
"MenuExit":"_Exit",
|
"MenuExit":"_Exit",
|
||||||
|
"MenuSort":"_Sort",
|
||||||
|
"MenuSortAlpha":"Sort alphabetically",
|
||||||
|
"MenuSortInOrder":"Sort in loaded order",
|
||||||
"Music":"Music",
|
"Music":"Music",
|
||||||
"NoMusic":"Your music library is empty...",
|
"NoMusic":"Your music library is empty...",
|
||||||
"RPSong":"Listening to $song",
|
"RPSong":"Listening to $song",
|
||||||
|
|
20
Ocarina2/Converter/PositionConverter.cs
Normal file
20
Ocarina2/Converter/PositionConverter.cs
Normal 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
22
Ocarina2/Models/Artist.cs
Normal 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>();
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,8 +17,11 @@ public class Language
|
||||||
public readonly string RPSong;
|
public readonly string RPSong;
|
||||||
public readonly string RPArtist;
|
public readonly string RPArtist;
|
||||||
public readonly string RPIconText;
|
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
|
OCARINA2 Open-source music player and library manager
|
||||||
|
@ -45,6 +48,9 @@ public class Language
|
||||||
RPSong = rpSong;
|
RPSong = rpSong;
|
||||||
RPArtist = rpArtist;
|
RPArtist = rpArtist;
|
||||||
RPIconText = rpIconText;
|
RPIconText = rpIconText;
|
||||||
|
MenuSort = menuSort;
|
||||||
|
MenuSortAlpha = menuSortAlpha;
|
||||||
|
MenuSortInOrder = menuSortInOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Language Load(string code)
|
public static Language Load(string code)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace Ocarina2;
|
namespace Ocarina2;
|
||||||
|
|
||||||
|
|
|
@ -4,10 +4,17 @@ using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.Notifications;
|
||||||
|
using Avalonia.Threading;
|
||||||
using DiscordRPC;
|
using DiscordRPC;
|
||||||
|
using DynamicData;
|
||||||
using ManagedBass;
|
using ManagedBass;
|
||||||
using Ocarina2.Models;
|
using Ocarina2.Models;
|
||||||
|
using Ocarina2.Views;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
using Notification = Avalonia.Controls.Notifications.Notification;
|
||||||
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace Ocarina2.ViewModels;
|
namespace Ocarina2.ViewModels;
|
||||||
|
|
||||||
|
@ -35,6 +42,11 @@ public class MainWindowViewModel : ViewModelBase
|
||||||
private string _menuExit;
|
private string _menuExit;
|
||||||
private string _music;
|
private string _music;
|
||||||
private string _nomusic;
|
private string _nomusic;
|
||||||
|
private string _menuSort;
|
||||||
|
private string _menuSortAlpha;
|
||||||
|
private string _menuSortInOrder;
|
||||||
|
private TimeSpan? _duration;
|
||||||
|
private Timer? _positionTimer;
|
||||||
private MusicFile? _selectedFile;
|
private MusicFile? _selectedFile;
|
||||||
public MusicFile? SelectedFile
|
public MusicFile? SelectedFile
|
||||||
{
|
{
|
||||||
|
@ -42,6 +54,36 @@ public class MainWindowViewModel : ViewModelBase
|
||||||
set => this.RaiseAndSetIfChanged(ref _selectedFile, value);
|
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
|
public string MenuOpen
|
||||||
{
|
{
|
||||||
get => _menuOpen;
|
get => _menuOpen;
|
||||||
|
@ -72,15 +114,20 @@ public class MainWindowViewModel : ViewModelBase
|
||||||
set => this.RaiseAndSetIfChanged(ref _nomusic, value);
|
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>
|
/// <summary>
|
||||||
/// Finds music files in the music library folder
|
/// Finds music files in the music library folder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>ObservableCollection of found files</returns>
|
/// <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)))
|
if (!Directory.Exists(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic)))
|
||||||
return musicfiles;
|
return musicfiles;
|
||||||
var subfolders = Directory.GetDirectories(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic));
|
var subfolders = Directory.GetDirectories(Environment.GetFolderPath(Environment.SpecialFolder.MyMusic));
|
||||||
|
@ -91,7 +138,20 @@ public class MainWindowViewModel : ViewModelBase
|
||||||
foreach (var file in f)
|
foreach (var file in f)
|
||||||
{
|
{
|
||||||
var tfile = TagLib.File.Create(file);
|
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
|
// get directory-level files
|
||||||
|
@ -99,21 +159,68 @@ public class MainWindowViewModel : ViewModelBase
|
||||||
foreach (var file in files)
|
foreach (var file in files)
|
||||||
{
|
{
|
||||||
var tfile = TagLib.File.Create(file);
|
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;
|
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>
|
/// <summary>
|
||||||
/// Runs when a song is selected in the listbox to start playing it
|
/// Runs when a song is selected in the listbox to start playing it
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void FileSelected()
|
public void FileSelected(MusicFile? f)
|
||||||
{
|
{
|
||||||
if (SelectedFile == null) return;
|
if (f == null) return;
|
||||||
|
|
||||||
Stop();
|
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
|
try
|
||||||
{
|
{
|
||||||
var t = new Thread(async () => // TODO: toto thread asi neni fajn?
|
var t = new Thread(async () => // TODO: toto thread asi neni fajn?
|
||||||
|
@ -121,9 +228,17 @@ public class MainWindowViewModel : ViewModelBase
|
||||||
_player = new MediaPlayer();
|
_player = new MediaPlayer();
|
||||||
await _player.LoadAsync(SelectedFile.Path);
|
await _player.LoadAsync(SelectedFile.Path);
|
||||||
_player.Play();
|
_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),
|
Details = _l.RPSong.Replace("$song",SelectedFile.Metadata.Tag.Title),
|
||||||
Timestamps = new Timestamps
|
Timestamps = new Timestamps
|
||||||
{
|
{
|
||||||
|
@ -151,6 +266,9 @@ public class MainWindowViewModel : ViewModelBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private MediaPlayer? _player;
|
private MediaPlayer? _player;
|
||||||
public ReactiveCommand<Unit,Unit> StopMusic { get; }
|
public ReactiveCommand<Unit,Unit> StopMusic { get; }
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Stops and disposes of the player
|
/// Stops and disposes of the player
|
||||||
|
@ -159,45 +277,61 @@ public class MainWindowViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
_player?.Stop();
|
_player?.Stop();
|
||||||
_player?.Dispose();
|
_player?.Dispose();
|
||||||
|
_positionTimer?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private TimeSpan? _position;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Used to play/pause playback
|
/// Used to play/pause playback
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void PlayPause()
|
public void PlayPause()
|
||||||
{
|
{
|
||||||
if (_player == null) return;
|
if (_player == null) return;
|
||||||
|
Console.WriteLine(_player.State);
|
||||||
if (_player.State == PlaybackState.Playing)
|
if (_player.State == PlaybackState.Playing)
|
||||||
{
|
{
|
||||||
_player.Stop();
|
_player.Stop();
|
||||||
|
_positionTimer?.Stop();
|
||||||
|
CurrentPosition = _player.Position;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_player.Play();
|
_player.Play();
|
||||||
|
_positionTimer?.Start();
|
||||||
|
_player.Position = CurrentPosition ?? TimeSpan.Zero;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public MainWindowViewModel()
|
public MainWindowViewModel()
|
||||||
{
|
{
|
||||||
|
_menuSort = _l.MenuSort;
|
||||||
|
_menuSortAlpha = _l.MenuSortAlpha;
|
||||||
|
_menuSortInOrder = _l.MenuSortInOrder;
|
||||||
_menuOpen = _l.MenuOpen;
|
_menuOpen = _l.MenuOpen;
|
||||||
_menuExit = _l.MenuExit;
|
_menuExit = _l.MenuExit;
|
||||||
_menuFile = _l.MenuFile;
|
_menuFile = _l.MenuFile;
|
||||||
_music = _l.Music;
|
_music = _l.Music;
|
||||||
MusicFiles = LoadFiles();
|
SongCollection = LoadFiles();
|
||||||
_nomusic = _l.NoMusic;
|
_nomusic = _l.NoMusic;
|
||||||
StopMusic = ReactiveCommand.Create(Stop);
|
StopMusic = ReactiveCommand.Create(Stop);
|
||||||
|
PlayPauseMusic = ReactiveCommand.Create(PlayPause);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MainWindowViewModel(Language l)
|
public MainWindowViewModel(Language l)
|
||||||
{
|
{
|
||||||
_l = l;
|
_l = l;
|
||||||
|
_menuSort = _l.MenuSort;
|
||||||
|
_menuSortAlpha = _l.MenuSortAlpha;
|
||||||
|
_menuSortInOrder = _l.MenuSortInOrder;
|
||||||
_menuOpen = _l.MenuOpen;
|
_menuOpen = _l.MenuOpen;
|
||||||
_menuExit = _l.MenuExit;
|
_menuExit = _l.MenuExit;
|
||||||
_menuFile = _l.MenuFile;
|
_menuFile = _l.MenuFile;
|
||||||
_music = _l.Music;
|
_music = _l.Music;
|
||||||
_nomusic = _l.NoMusic;
|
_nomusic = _l.NoMusic;
|
||||||
MusicFiles = LoadFiles();
|
SongCollection = LoadFiles();
|
||||||
StopMusic = ReactiveCommand.Create(Stop);
|
StopMusic = ReactiveCommand.Create(Stop);
|
||||||
|
PlayPauseMusic = ReactiveCommand.Create(PlayPause);
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:viewModels="clr-namespace:Ocarina2.ViewModels"
|
xmlns:viewModels="clr-namespace:Ocarina2.ViewModels"
|
||||||
|
xmlns:models="clr-namespace:Ocarina2.Models"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Ocarina2.Views.LibraryView">
|
x:Class="Ocarina2.Views.LibraryView">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
|
@ -10,13 +11,29 @@
|
||||||
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
|
to set the actual DataContext for runtime, set the DataContext property in code (look at App.axaml.cs) -->
|
||||||
<viewModels:MainWindowViewModel />
|
<viewModels:MainWindowViewModel />
|
||||||
</Design.DataContext> <!-- TODO: Handle empty library -->
|
</Design.DataContext> <!-- TODO: Handle empty library -->
|
||||||
<ListBox x:Name="MusicBox" Grid.Row="1" Grid.Column="1" IsEnabled="{Binding MusicFiles.Count}"
|
<UserControl.DataTemplates>
|
||||||
Items="{Binding MusicFiles}" SelectedItem="{Binding SelectedFile}" SelectionChanged="MusicBox_OnSelectionChanged">
|
<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>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding Metadata.Tag.Title}" />
|
<ContentControl Content="{Binding}" Margin="0,5,0,0"/>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ListBox>
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Ocarina2.Models;
|
||||||
using Ocarina2.ViewModels;
|
using Ocarina2.ViewModels;
|
||||||
|
|
||||||
namespace Ocarina2.Views;
|
namespace Ocarina2.Views;
|
||||||
|
@ -18,6 +21,8 @@ public partial class LibraryView : UserControl
|
||||||
|
|
||||||
private void MusicBox_OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
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>
|
<Window.Resources>
|
||||||
<converter:SongMetadataConverter x:Key="SongMeta"/>
|
<converter:SongMetadataConverter x:Key="SongMeta"/>
|
||||||
<converter:AlbumArtConverter x:Key="AlbumArt"/>
|
<converter:AlbumArtConverter x:Key="AlbumArt"/>
|
||||||
|
<converter:PositionConverter x:Key="Position"/>
|
||||||
</Window.Resources>
|
</Window.Resources>
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<!-- This only sets the DataContext for the previewer in an IDE,
|
<!-- This only sets the DataContext for the previewer in an IDE,
|
||||||
|
@ -48,6 +49,10 @@
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Header="{Binding MenuExit}" />
|
<MenuItem Header="{Binding MenuExit}" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<MenuItem Header="{Binding MenuSort}">
|
||||||
|
<MenuItem Header="{Binding MenuSortAlpha}" />
|
||||||
|
<MenuItem Header="{Binding MenuSortInOrder}" />
|
||||||
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
<Grid Grid.Row="1" Grid.Column="0" Grid.RowSpan="2" RowDefinitions="*,2*" Margin="0">
|
<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}}"/>
|
<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=1}" FontWeight="Bold"/>
|
||||||
<TextBlock Text="{Binding SelectedFile, Converter={StaticResource SongMeta}, ConverterParameter=2}" />
|
<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>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Playback Controls -->
|
<!-- Playback Controls -->
|
||||||
<StackPanel Orientation="Horizontal" Grid.Row="2" Grid.Column="0">
|
<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}" />
|
<Button Content="{materialIcons:MaterialIconExt Kind=Stop}" Padding="5" Margin="5,0,0,0" Command="{Binding StopMusic}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Notifications;
|
||||||
using Ocarina2.ViewModels;
|
using Ocarina2.ViewModels;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
|
@ -8,12 +10,14 @@ namespace Ocarina2.Views;
|
||||||
|
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
|
public static WindowNotificationManager WindowNotificationManager;
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
WindowNotificationManager = new WindowNotificationManager(this);
|
||||||
Closing += (sender, args) =>
|
Closing += (sender, args) =>
|
||||||
{
|
{
|
||||||
// dispose on closea
|
// dispose on close
|
||||||
if (sender == null) return;
|
if (sender == null) return;
|
||||||
if (sender.ToString() != "Ocarina2.Views.MainWindow") return;
|
if (sender.ToString() != "Ocarina2.Views.MainWindow") return;
|
||||||
App.Client.Dispose();
|
App.Client.Dispose();
|
||||||
|
|
Loading…
Reference in a new issue