Added playlist view and initial playlist view logic.
This commit is contained in:
@@ -20,6 +20,7 @@ public class AudioPlayer : IAudioPlayer
|
|||||||
{
|
{
|
||||||
_playlist = value;
|
_playlist = value;
|
||||||
NotifyPropertyChanged(nameof(Playlist));
|
NotifyPropertyChanged(nameof(Playlist));
|
||||||
|
PlaylistChanged?.Invoke(this, new());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +113,7 @@ public class AudioPlayer : IAudioPlayer
|
|||||||
|
|
||||||
protected virtual int PreviousSongSecondsThreshold => 5;
|
protected virtual int PreviousSongSecondsThreshold => 5;
|
||||||
|
|
||||||
|
public event EventHandler? PlaylistChanged;
|
||||||
public event EventHandler? PlayingSongChanged;
|
public event EventHandler? PlayingSongChanged;
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public interface IAudioPlayer
|
|||||||
Task PreviousAsync();
|
Task PreviousAsync();
|
||||||
Task NextAsync();
|
Task NextAsync();
|
||||||
|
|
||||||
|
event EventHandler PlaylistChanged;
|
||||||
event EventHandler PlayingSongChanged;
|
event EventHandler PlayingSongChanged;
|
||||||
event PropertyChangedEventHandler PropertyChanged;
|
event PropertyChangedEventHandler PropertyChanged;
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="xunit.v3" Version="1.1.0" />
|
<PackageReference Include="xunit.v3" Version="2.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="clr-namespace:Harmonia.UI.ViewModels"
|
xmlns:vm="clr-namespace:Harmonia.UI.ViewModels"
|
||||||
xmlns:control="clr-namespace:Harmonia.UI.Controls"
|
xmlns:control="clr-namespace:Harmonia.UI.Controls"
|
||||||
|
xmlns:converters="clr-namespace:Harmonia.UI.Converters"
|
||||||
xmlns:semi="https://irihi.tech/semi"
|
xmlns:semi="https://irihi.tech/semi"
|
||||||
x:Class="Harmonia.UI.App"
|
x:Class="Harmonia.UI.App"
|
||||||
RequestedThemeVariant="Default">
|
RequestedThemeVariant="Default">
|
||||||
@@ -16,6 +17,51 @@
|
|||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<vm:ViewModelLocator x:Key="Locator" />
|
<vm:ViewModelLocator x:Key="Locator" />
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
<!-- Converters -->
|
||||||
|
<converters:SecondsToStringConverter x:Key="SecondsToString" />
|
||||||
|
<converters:ArtistsToStringConverter x:Key="ArtistsToString" />
|
||||||
|
<converters:NullVisibilityConverter x:Key="NullVisibility" />
|
||||||
|
<converters:SongTitleConverter x:Key="SongTitle" />
|
||||||
|
<converters:VolumeStateToIconConverter x:Key="VolumeStateToIconConverter" />
|
||||||
|
<converters:RepeatStateToIconConverter x:Key="RepeatStateToIconConverter" />
|
||||||
|
<converters:PlaylistSongEqualityConverter x:Key="PlaylistSongEquality" />
|
||||||
|
|
||||||
|
<!-- Geometry -->
|
||||||
|
<StreamGeometry x:Key="VolumeMutedIcon">
|
||||||
|
M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06
|
||||||
|
M13.854 5.646a.5.5 0 0 1 0 .708L12.207 8l1.647 1.646a.5.5 0 0 1-.708.708L11.5 8.707l-1.646 1.647a.5.5 0 0 1-.708-.708L10.793 8 9.146 6.354a.5.5 0 1 1 .708-.708L11.5 7.293l1.646-1.647a.5.5 0 0 1 .708 0
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry x:Key="VolumeOffIcon">
|
||||||
|
M8.707 11.182A4.5 4.5 0 0 0 10.025 8a4.5 4.5 0 0 0-1.318-3.182L8 5.525A3.5 3.5 0 0 1 9.025 8 3.5 3.5 0 0 1 8 10.475z
|
||||||
|
M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry x:Key="VolumeLowIcon">
|
||||||
|
M8.707 11.182A4.5 4.5 0 0 0 10.025 8a4.5 4.5 0 0 0-1.318-3.182L8 5.525A3.5 3.5 0 0 1 9.025 8 3.5 3.5 0 0 1 8 10.475z
|
||||||
|
M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry x:Key="VolumeMediumIcon">
|
||||||
|
M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.48 5.48 0 0 1 11.025 8a5.48 5.48 0 0 1-1.61 3.89z
|
||||||
|
M8.707 11.182A4.5 4.5 0 0 0 10.025 8a4.5 4.5 0 0 0-1.318-3.182L8 5.525A3.5 3.5 0 0 1 9.025 8 3.5 3.5 0 0 1 8 10.475z
|
||||||
|
M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry x:Key="VolumeHighIcon">
|
||||||
|
M11.536 14.01A8.47 8.47 0 0 0 14.026 8a8.47 8.47 0 0 0-2.49-6.01l-.708.707A7.48 7.48 0 0 1 13.025 8c0 2.071-.84 3.946-2.197 5.303z
|
||||||
|
M10.121 12.596A6.48 6.48 0 0 0 12.025 8a6.48 6.48 0 0 0-1.904-4.596l-.707.707A5.48 5.48 0 0 1 11.025 8a5.48 5.48 0 0 1-1.61 3.89z
|
||||||
|
M8.707 11.182A4.5 4.5 0 0 0 10.025 8a4.5 4.5 0 0 0-1.318-3.182L8 5.525A3.5 3.5 0 0 1 9.025 8 3.5 3.5 0 0 1 8 10.475z
|
||||||
|
M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry x:Key="ShuffleIcon">
|
||||||
|
M0 3.5A.5.5 0 0 1 .5 3H1c2.202 0 3.827 1.24 4.874 2.418.49.552.865 1.102 1.126 1.532.26-.43.636-.98 1.126-1.532C9.173 4.24 10.798 3 13 3v1c-1.798 0-3.173 1.01-4.126 2.082A9.6 9.6 0 0 0 7.556 8a9.6 9.6 0 0 0 1.317 1.918C9.828 10.99 11.204 12 13 12v1c-2.202 0-3.827-1.24-4.874-2.418A10.6 10.6 0 0 1 7 9.05c-.26.43-.636.98-1.126 1.532C4.827 11.76 3.202 13 1 13H.5a.5.5 0 0 1 0-1H1c1.798 0 3.173-1.01 4.126-2.082A9.6 9.6 0 0 0 6.444 8a9.6 9.6 0 0 0-1.317-1.918C4.172 5.01 2.796 4 1 4H.5a.5.5 0 0 1-.5-.5
|
||||||
|
M13 5.466V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192
|
||||||
|
M13 14.466v-3.932a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry x:Key="RepeatIcon">
|
||||||
|
M11 5.466V4H5a4 4 0 0 0-3.584 5.777.5.5 0 1 1-.896.446A5 5 0 0 1 5 3h6V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192m3.81.086a.5.5 0 0 1 .67.225A5 5 0 0 1 11 13H5v1.466a.25.25 0 0 1-.41.192l-2.36-1.966a.25.25 0 0 1 0-.384l2.36-1.966a.25.25 0 0 1 .41.192V12h6a4 4 0 0 0 3.585-5.777.5.5 0 0 1 .225-.67Z
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry x:Key="RepeatOneIcon">
|
||||||
|
M11 4v1.466a.25.25 0 0 0 .41.192l2.36-1.966a.25.25 0 0 0 0-.384l-2.36-1.966a.25.25 0 0 0-.41.192V3H5a5 5 0 0 0-4.48 7.223.5.5 0 0 0 .896-.446A4 4 0 0 1 5 4zm4.48 1.777a.5.5 0 0 0-.896.446A4 4 0 0 1 11 12H5.001v-1.466a.25.25 0 0 0-.41-.192l-2.36 1.966a.25.25 0 0 0 0 .384l2.36 1.966a.25.25 0 0 0 .41-.192V13h6a5 5 0 0 0 4.48-7.223Z
|
||||||
|
M9 5.5a.5.5 0 0 0-.854-.354l-1.75 1.75a.5.5 0 1 0 .708.708L8 6.707V10.5a.5.5 0 0 0 1 0z
|
||||||
|
</StreamGeometry>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
|
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes;
|
|||||||
using Avalonia.Data.Core.Plugins;
|
using Avalonia.Data.Core.Plugins;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Harmonia.Core.Extensions;
|
using Harmonia.Core.Extensions;
|
||||||
|
using Harmonia.UI.Caching;
|
||||||
using Harmonia.UI.ViewModels;
|
using Harmonia.UI.ViewModels;
|
||||||
using Harmonia.UI.Views;
|
using Harmonia.UI.Views;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -22,6 +23,9 @@ public partial class App : Application
|
|||||||
services.AddSingleton<MainWindow>();
|
services.AddSingleton<MainWindow>();
|
||||||
services.AddSingleton<PlaybackBarViewModel>();
|
services.AddSingleton<PlaybackBarViewModel>();
|
||||||
services.AddSingleton<PlayingSongInfoViewModel>();
|
services.AddSingleton<PlayingSongInfoViewModel>();
|
||||||
|
services.AddSingleton<PlaylistViewModel>();
|
||||||
|
|
||||||
|
services.AddSingleton<IAudioBitmapCache, AudioBitmapCache>();
|
||||||
|
|
||||||
services.AddHarmonia();
|
services.AddHarmonia();
|
||||||
|
|
||||||
|
|||||||
55
Harmonia.UI/Caching/AudioBitmapCache.cs
Normal file
55
Harmonia.UI/Caching/AudioBitmapCache.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Harmonia.Core.Caching;
|
||||||
|
using Harmonia.Core.Imaging;
|
||||||
|
using Harmonia.Core.Models;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Caching;
|
||||||
|
|
||||||
|
public class AudioBitmapCache(IAudioImageExtractor audioImageExtractor) : MemoryCache<Song, Bitmap>, IAudioBitmapCache
|
||||||
|
{
|
||||||
|
protected override MemoryCacheOptions Options => new()
|
||||||
|
{
|
||||||
|
SizeLimit = 40,
|
||||||
|
CompactionPercentage = 0.2,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override TimeSpan SlidingExpiration => TimeSpan.FromSeconds(600);
|
||||||
|
|
||||||
|
protected override int MaxConcurrentRequests => 8;
|
||||||
|
|
||||||
|
protected override object? GetKey(Song key)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key.ImageHash) == false)
|
||||||
|
{
|
||||||
|
return key.ImageHash;
|
||||||
|
}
|
||||||
|
else if (string.IsNullOrWhiteSpace(key.ImageName) == false)
|
||||||
|
{
|
||||||
|
return key.ImageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async ValueTask<Bitmap?> FetchAsync(Song key, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
SongPictureInfo? songPictureInfo = await audioImageExtractor.ExtractImageAsync(key.FileName, cancellationToken);
|
||||||
|
|
||||||
|
if (songPictureInfo == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
using MemoryStream stream = new(songPictureInfo.Data);
|
||||||
|
|
||||||
|
return new Bitmap(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override long GetEntrySize(Bitmap entry)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Harmonia.UI/Caching/IAudioBitmapCache.cs
Normal file
10
Harmonia.UI/Caching/IAudioBitmapCache.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Harmonia.Core.Caching;
|
||||||
|
using Harmonia.Core.Models;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Caching;
|
||||||
|
|
||||||
|
public interface IAudioBitmapCache : ICache<Song, Bitmap>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
28
Harmonia.UI/Converters/RepeatStateToIconConverter.cs
Normal file
28
Harmonia.UI/Converters/RepeatStateToIconConverter.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Harmonia.Core.Player;
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Converters;
|
||||||
|
|
||||||
|
public class RepeatStateToIconConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is not RepeatState repeatMode)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
string resourceName = repeatMode == RepeatState.RepeatOne
|
||||||
|
? "RepeatOneIcon"
|
||||||
|
: "RepeatIcon";
|
||||||
|
|
||||||
|
return Application.Current?.FindResource(resourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Harmonia.UI/Converters/VolumeStateToIconConverter.cs
Normal file
37
Harmonia.UI/Converters/VolumeStateToIconConverter.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Avalonia;
|
||||||
|
using Harmonia.UI.Models;
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Harmonia.Core.Playlists;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Converters;
|
||||||
|
|
||||||
|
public class VolumeStateToIconConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is not VolumeState volumeState)
|
||||||
|
return AvaloniaProperty.UnsetValue;
|
||||||
|
|
||||||
|
return Application.Current?.FindResource($"Volume{volumeState}Icon") ?? AvaloniaProperty.UnsetValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlaylistSongEqualityConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is PlaylistSong playingSong && parameter is PlaylistSong currentSong)
|
||||||
|
{
|
||||||
|
return playingSong == currentSong;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
@@ -27,6 +27,9 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Compile Update="Views\PlaylistView.axaml.cs">
|
||||||
|
<DependentUpon>PlaylistView.axaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
<Compile Update="Views\PlayingSongInfo.axaml.cs">
|
<Compile Update="Views\PlayingSongInfo.axaml.cs">
|
||||||
<DependentUpon>PlayingSongInfo.axaml</DependentUpon>
|
<DependentUpon>PlayingSongInfo.axaml</DependentUpon>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
|||||||
10
Harmonia.UI/Models/VolumeState.cs
Normal file
10
Harmonia.UI/Models/VolumeState.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Harmonia.UI.Models;
|
||||||
|
|
||||||
|
public enum VolumeState
|
||||||
|
{
|
||||||
|
Muted,
|
||||||
|
Off,
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ using Harmonia.Core.Models;
|
|||||||
using Harmonia.Core.Player;
|
using Harmonia.Core.Player;
|
||||||
using Harmonia.Core.Playlists;
|
using Harmonia.Core.Playlists;
|
||||||
using Harmonia.Core.Scanner;
|
using Harmonia.Core.Scanner;
|
||||||
|
using Harmonia.UI.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -118,6 +119,57 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public double Volume
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _audioPlayer.Volume;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (IsMuted)
|
||||||
|
IsMuted = false;
|
||||||
|
|
||||||
|
_audioPlayer.Volume = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(VolumeState));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsMuted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _audioPlayer.IsMuted;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_audioPlayer.IsMuted = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
OnPropertyChanged(nameof(VolumeState));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public VolumeState VolumeState
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (IsMuted)
|
||||||
|
return VolumeState.Muted;
|
||||||
|
|
||||||
|
if (Volume == 0)
|
||||||
|
return VolumeState.Off;
|
||||||
|
|
||||||
|
if (Volume < .33)
|
||||||
|
return VolumeState.Low;
|
||||||
|
|
||||||
|
if (Volume < .66)
|
||||||
|
return VolumeState.Medium;
|
||||||
|
|
||||||
|
return VolumeState.High;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsRandom
|
public bool IsRandom
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
@@ -131,11 +183,27 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RepeatState RepeatState
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _audioPlayer.RepeatState;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_audioPlayer.RepeatState = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public ICommand PlaySongCommand => new RelayCommand(Play);
|
public ICommand PlaySongCommand => new RelayCommand(Play);
|
||||||
public ICommand PauseSongCommand => new RelayCommand(Pause);
|
public ICommand PauseSongCommand => new RelayCommand(Pause);
|
||||||
public ICommand StopSongCommand => new RelayCommand(Stop);
|
public ICommand StopSongCommand => new RelayCommand(Stop);
|
||||||
public ICommand PreviousSongCommand => new RelayCommand(Previous);
|
public ICommand PreviousSongCommand => new RelayCommand(Previous);
|
||||||
public ICommand NextSongCommand => new RelayCommand(Next);
|
public ICommand NextSongCommand => new RelayCommand(Next);
|
||||||
|
public ICommand ToggleMuteCommand => new RelayCommand(ToggleMute);
|
||||||
|
public ICommand ToggleRandomizerCommand => new RelayCommand(ToggleRandomizer);
|
||||||
|
public ICommand ToggleRepeatCommand => new RelayCommand(ToggleRepeat);
|
||||||
|
|
||||||
public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache, IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner)
|
public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache, IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner)
|
||||||
{
|
{
|
||||||
@@ -219,6 +287,32 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
|
|||||||
_audioPlayer.NextAsync();
|
_audioPlayer.NextAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ToggleMute()
|
||||||
|
{
|
||||||
|
IsMuted = !IsMuted;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToggleRandomizer()
|
||||||
|
{
|
||||||
|
IsRandom = !IsRandom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToggleRepeat()
|
||||||
|
{
|
||||||
|
RepeatState = GetNextRepeatState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private RepeatState GetNextRepeatState()
|
||||||
|
{
|
||||||
|
return _audioPlayer.RepeatState switch
|
||||||
|
{
|
||||||
|
RepeatState.Off => RepeatState.RepeatAll,
|
||||||
|
RepeatState.RepeatAll => RepeatState.RepeatOne,
|
||||||
|
RepeatState.RepeatOne => RepeatState.Off,
|
||||||
|
_ => _audioPlayer.RepeatState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
SongImageSource?.Dispose();
|
SongImageSource?.Dispose();
|
||||||
|
|||||||
65
Harmonia.UI/ViewModels/PlaylistViewModel.cs
Normal file
65
Harmonia.UI/ViewModels/PlaylistViewModel.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
using Harmonia.Core.Player;
|
||||||
|
using Harmonia.Core.Playlists;
|
||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Threading;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using Harmonia.UI.Caching;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.ViewModels;
|
||||||
|
|
||||||
|
public class PlaylistViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IAudioPlayer _audioPlayer;
|
||||||
|
private readonly IAudioBitmapCache _audioBitmapImageCache;
|
||||||
|
private readonly ConcurrentDictionary<string, Bitmap> _bitmapDictionary = [];
|
||||||
|
|
||||||
|
public PlaylistSong? PlayingSong => _audioPlayer.PlayingSong;
|
||||||
|
|
||||||
|
private ObservableCollection<PlaylistSong> _playlistSongs = [];
|
||||||
|
public ObservableCollection<PlaylistSong> PlaylistSongs
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _playlistSongs;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_playlistSongs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache)
|
||||||
|
{
|
||||||
|
_audioPlayer = audioPlayer;
|
||||||
|
_audioPlayer.PlaylistChanged += OnAudioPlayerPlaylistChanged;
|
||||||
|
_audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged;
|
||||||
|
|
||||||
|
_audioBitmapImageCache = audioBitmapImageCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAudioPlayerPlaylistChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? [];
|
||||||
|
|
||||||
|
PlaylistSongs = [.. playlistSongs];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(PlayingSong));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PlaySong(PlaylistSong playlistSong)
|
||||||
|
{
|
||||||
|
_audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Bitmap?> GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,4 +12,7 @@ public class ViewModelLocator
|
|||||||
|
|
||||||
public static PlayingSongInfoViewModel PlayingSongInfoViewModel
|
public static PlayingSongInfoViewModel PlayingSongInfoViewModel
|
||||||
=> App.ServiceProvider.GetRequiredService<PlayingSongInfoViewModel>();
|
=> App.ServiceProvider.GetRequiredService<PlayingSongInfoViewModel>();
|
||||||
|
|
||||||
|
public static PlaylistViewModel PlaylistViewModel
|
||||||
|
=> App.ServiceProvider.GetRequiredService<PlaylistViewModel>();
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,12 @@
|
|||||||
<Setter Property="BorderThickness" Value="0"/>
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Flat Button ViewBox -->
|
||||||
|
<!--<Style Selector="Viewbox.FlatButtonViewbox">
|
||||||
|
<Setter Property="Width" Value="18"/>
|
||||||
|
<Setter Property="Height" Value="18"/>
|
||||||
|
</Style>-->
|
||||||
|
|
||||||
<!-- Flat Button Path Icon -->
|
<!-- Flat Button Path Icon -->
|
||||||
<Style Selector="PathIcon.FlatButtonIcon">
|
<Style Selector="PathIcon.FlatButtonIcon">
|
||||||
<Setter Property="Padding" Value="10"/>
|
<Setter Property="Padding" Value="10"/>
|
||||||
@@ -31,6 +37,12 @@
|
|||||||
<Setter Property="Foreground" Value="White"/>
|
<Setter Property="Foreground" Value="White"/>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Flat Button Path Icon (Large) -->
|
||||||
|
<Style Selector="PathIcon.FlatButtonIcon.Medium">
|
||||||
|
<Setter Property="Width" Value="24"/>
|
||||||
|
<Setter Property="Height" Value="24"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
<!-- Flat Button Path Icon (Large) -->
|
<!-- Flat Button Path Icon (Large) -->
|
||||||
<Style Selector="PathIcon.FlatButtonIcon.Large">
|
<Style Selector="PathIcon.FlatButtonIcon.Large">
|
||||||
<Setter Property="Width" Value="36"/>
|
<Setter Property="Width" Value="36"/>
|
||||||
@@ -106,14 +118,36 @@
|
|||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<Grid Grid.Row="1" Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right">
|
<Grid Grid.Row="1" Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right">
|
||||||
<StackPanel Orientation="Horizontal">
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Button Classes="Flat" Command="{Binding ToggleMuteCommand}">
|
||||||
|
<Grid>
|
||||||
|
<!--<PathIcon Classes="FlatButtonIcon Medium" Data="{StaticResource VolumeHighIcon}"></PathIcon>-->
|
||||||
|
<PathIcon Classes="FlatButtonIcon Medium" Data="{Binding VolumeState, Converter={StaticResource VolumeStateToIconConverter}}"></PathIcon>
|
||||||
|
</Grid>
|
||||||
|
</Button>
|
||||||
|
<Slider Value="{Binding Volume, Mode=TwoWay}" Minimum="0" Maximum="1" PointerWheelChanged="VolumeSlider_PointerWheelChanged" Width="200" VerticalAlignment="Center" />
|
||||||
|
<Button Classes="Flat" Command="{Binding ToggleRandomizerCommand}">
|
||||||
|
<PathIcon Classes="FlatButtonIcon Medium" Classes.ShuffleOn="{Binding IsRandom}" Data="{StaticResource ShuffleIcon}">
|
||||||
|
<PathIcon.Styles>
|
||||||
|
<Style Selector="PathIcon.ShuffleOn">
|
||||||
|
<Setter Property="Foreground" Value="#76b9ed"/>
|
||||||
|
</Style>
|
||||||
|
</PathIcon.Styles>
|
||||||
|
</PathIcon>
|
||||||
|
</Button>
|
||||||
|
<Button Classes="Flat" Command="{Binding ToggleRepeatCommand}">
|
||||||
|
<PathIcon Classes="FlatButtonIcon Medium" Data="{Binding RepeatState, Converter={StaticResource RepeatStateToIconConverter}}"></PathIcon>
|
||||||
|
</Button>
|
||||||
<Button Classes="Flat">
|
<Button Classes="Flat">
|
||||||
<PathIcon Classes="FlatButtonIcon" Data="{StaticResource SemiIconSetting}"></PathIcon>
|
<PathIcon Classes="FlatButtonIcon" Data="{StaticResource SemiIconSetting}"></PathIcon>
|
||||||
<Button.Flyout>
|
<Button.Flyout>
|
||||||
<MenuFlyout Placement="Top">
|
<Flyout Placement="Top">
|
||||||
<RadioButton Content="Compact" VerticalContentAlignment="Stretch" HorizontalAlignment="Stretch" GroupName="Density" />
|
<StackPanel>
|
||||||
<RadioButton Content="Comfortable" VerticalContentAlignment="Stretch" HorizontalAlignment="Stretch" GroupName="Density" />
|
<TextBlock Text="Density"></TextBlock>
|
||||||
<RadioButton Content="Cozy" VerticalContentAlignment="Stretch" HorizontalAlignment="Stretch" GroupName="Density" />
|
<RadioButton Content="Compact" Margin="10" HorizontalAlignment="Stretch" GroupName="Density" />
|
||||||
</MenuFlyout>
|
<RadioButton Content="Comfortable" Margin="10" HorizontalAlignment="Stretch" GroupName="Density" />
|
||||||
|
<RadioButton Content="Cozy" Margin="10" HorizontalAlignment="Stretch" GroupName="Density" />
|
||||||
|
</StackPanel>
|
||||||
|
</Flyout>
|
||||||
</Button.Flyout>
|
</Button.Flyout>
|
||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -45,4 +45,16 @@ public partial class PlaybackBar : UserControl
|
|||||||
_viewModel.CurrentPosition = slider.Value;
|
_viewModel.CurrentPosition = slider.Value;
|
||||||
_viewModel.IsPositionChangeInProgress = false;
|
_viewModel.IsPositionChangeInProgress = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void VolumeSlider_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
|
||||||
|
{
|
||||||
|
double mouseWheelDelta = e.Delta.Y;
|
||||||
|
|
||||||
|
if (mouseWheelDelta == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
double delta = mouseWheelDelta > 0 ? .02 : -.02;
|
||||||
|
|
||||||
|
_viewModel.Volume += delta;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -3,25 +3,36 @@
|
|||||||
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:vm="clr-namespace:Harmonia.UI.ViewModels"
|
xmlns:vm="clr-namespace:Harmonia.UI.ViewModels"
|
||||||
|
xmlns:views="clr-namespace:Harmonia.UI.Views"
|
||||||
xmlns:converter="clr-namespace:Harmonia.UI.Converters"
|
xmlns:converter="clr-namespace:Harmonia.UI.Converters"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
DataContext="{x:Static vm:ViewModelLocator.PlayingSongInfoViewModel}"
|
DataContext="{x:Static vm:ViewModelLocator.PlayingSongInfoViewModel}"
|
||||||
x:Class="Harmonia.UI.Views.PlayingSongInfo"
|
x:Class="Harmonia.UI.Views.PlayingSongInfo"
|
||||||
x:DataType="vm:PlayingSongInfoViewModel">
|
x:DataType="vm:PlayingSongInfoViewModel">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Image Source="{Binding SongImageSource, Mode=OneWay}" Stretch="UniformToFill" HorizontalAlignment="Center" VerticalAlignment="Center">
|
<Grid.ColumnDefinitions>
|
||||||
<Image.Effect>
|
<ColumnDefinition></ColumnDefinition>
|
||||||
<BlurEffect Radius="20" />
|
<ColumnDefinition></ColumnDefinition>
|
||||||
</Image.Effect>
|
</Grid.ColumnDefinitions>
|
||||||
<Image.RenderTransform>
|
<Grid Grid.ColumnSpan="2">
|
||||||
<ScaleTransform ScaleX="1.05" ScaleY="1.05" />
|
<Image Source="{Binding SongImageSource, Mode=OneWay}" Stretch="UniformToFill" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
</Image.RenderTransform>
|
<Image.Effect>
|
||||||
</Image>
|
<BlurEffect Radius="20" />
|
||||||
<Canvas Background="#99000000"></Canvas>
|
</Image.Effect>
|
||||||
<Image Source="{Binding SongImageSource}" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
|
<Image.RenderTransform>
|
||||||
<Image.Effect>
|
<ScaleTransform ScaleX="1.05" ScaleY="1.05" />
|
||||||
<DropShadowEffect BlurRadius="10" Opacity=".4" OffsetX="3" OffsetY="3" />
|
</Image.RenderTransform>
|
||||||
</Image.Effect>
|
</Image>
|
||||||
</Image>
|
<Canvas Background="#99000000"></Canvas>
|
||||||
|
</Grid>
|
||||||
|
<Grid Grid.Column="0">
|
||||||
|
<Image Source="{Binding SongImageSource}" Stretch="Uniform" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
|
||||||
|
<Image.Effect>
|
||||||
|
<DropShadowEffect BlurRadius="10" Opacity=".4" OffsetX="3" OffsetY="3" />
|
||||||
|
</Image.Effect>
|
||||||
|
</Image>
|
||||||
|
</Grid>
|
||||||
|
<views:PlaylistView Grid.Column="1"></views:PlaylistView>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
93
Harmonia.UI/Views/PlaylistView.axaml
Normal file
93
Harmonia.UI/Views/PlaylistView.axaml
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
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.PlaylistViewModel}"
|
||||||
|
x:Class="Harmonia.UI.Views.PlaylistView"
|
||||||
|
x:DataType="vm:PlaylistViewModel">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<SolidColorBrush x:Key="SongItemTitleBrush" Color="#dddddd"/>
|
||||||
|
<SolidColorBrush x:Key="SongItemSubtitleBrush" Color="#aaaaaa"/>
|
||||||
|
</UserControl.Resources>
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Style Selector="Border.SongImage">
|
||||||
|
<Setter Property="Width" Value="60"/>
|
||||||
|
<Setter Property="Height" Value="60"/>
|
||||||
|
<Setter Property="Margin" Value="0 0 10 0"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.SongTitle">
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.SongSubtitle">
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SongItemSubtitleBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.SongMetaData">
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.SongMetaData.Subtitle">
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SongItemSubtitleBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Grid.Playing TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#76b9ed"/>
|
||||||
|
<!-- Light Blue for Highlight -->
|
||||||
|
</Style>
|
||||||
|
</UserControl.Styles>
|
||||||
|
<Grid Margin="0">
|
||||||
|
<Canvas Background="#99000000"></Canvas> <!-- Was 99 !-->
|
||||||
|
<ListBox ItemsSource="{Binding PlaylistSongs}" DoubleTapped="ListBox_DoubleTapped">
|
||||||
|
<!--<ListBox.Styles>
|
||||||
|
<Style Selector="ListBoxItem:nth-child(odd):not(:pointerover):not(:selected)">
|
||||||
|
<Setter Property="Background" Value="#00000000"/>
|
||||||
|
--><!-- Light Gray --><!--
|
||||||
|
</Style>
|
||||||
|
<Style Selector="ListBoxItem:nth-child(even):not(:pointerover):not(:selected)">
|
||||||
|
<Setter Property="Background" Value="#22AAAAAA"/>
|
||||||
|
--><!-- Slightly Darker Gray --><!--
|
||||||
|
</Style>
|
||||||
|
</ListBox.Styles>-->
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Grid Classes.Playing="{Binding $parent[ListBox].((vm:PlaylistViewModel)DataContext).PlayingSong, Converter={StaticResource PlaylistSongEquality}, ConverterParameter={Binding .}}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="70"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="70"></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition></RowDefinition>
|
||||||
|
<RowDefinition></RowDefinition>
|
||||||
|
<RowDefinition></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Border Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" Classes="SongImage">
|
||||||
|
<Grid>
|
||||||
|
<Image Loaded="Image_Loaded" Unloaded="Image_Unloaded"></Image>
|
||||||
|
<Canvas Background="#19000000"></Canvas>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Song.Title, Mode=OneWay}" Classes="SongTitle" />
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Song.Artists, Mode=OneWay, Converter={StaticResource ArtistsToString}}" Classes="SongSubtitle" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Song.Album, Mode=OneWay}" Classes="SongSubtitle" />
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Song.Length.TotalSeconds, Mode=OneWay, Converter={StaticResource SecondsToString}}" Classes="SongMetaData" />
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="2" Text="{Binding Song.FileType, Mode=OneWay}" Classes="SongMetaData Subtitle" />
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="2" Classes="SongMetaData Subtitle">
|
||||||
|
<Run Text="{Binding Song.BitRate, Mode=OneWay}"></Run><Run Text=" kbps"></Run>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
91
Harmonia.UI/Views/PlaylistView.axaml.cs
Normal file
91
Harmonia.UI/Views/PlaylistView.axaml.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Harmonia.Core.Caching;
|
||||||
|
using Harmonia.Core.Imaging;
|
||||||
|
using Harmonia.Core.Playlists;
|
||||||
|
using Harmonia.UI.ViewModels;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Views;
|
||||||
|
|
||||||
|
public partial class PlaylistView : UserControl
|
||||||
|
{
|
||||||
|
private readonly PlaylistViewModel _viewModel;
|
||||||
|
private readonly ConcurrentDictionary<int, CancellationTokenSource> _imageCancellationTokens = [];
|
||||||
|
|
||||||
|
public PlaylistView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_viewModel = (PlaylistViewModel)DataContext!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ListBox_DoubleTapped(object? sender, TappedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is ListBox listBox && listBox.SelectedItem is PlaylistSong playlistSong)
|
||||||
|
{
|
||||||
|
_viewModel.PlaySong(playlistSong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Image_Loaded(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Image image)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (image.DataContext is not PlaylistSong playlistSong)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Task.Run(() => DoSomethingAsync(image, playlistSong));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task DoSomethingAsync(Image image, PlaylistSong playlistSong)
|
||||||
|
{
|
||||||
|
int hashCode = image.GetHashCode();
|
||||||
|
|
||||||
|
_imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
|
||||||
|
cancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
|
cancellationTokenSource = new();
|
||||||
|
|
||||||
|
Bitmap? bitmap = await _viewModel.GetBitmapAsync(playlistSong, cancellationTokenSource.Token);
|
||||||
|
|
||||||
|
if (bitmap == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(() => SetSongImageSource(image, bitmap));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetSongImageSource(Image image, Bitmap bitmap)
|
||||||
|
{
|
||||||
|
image.Source = bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Image_Unloaded(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Image image)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (image.DataContext is not PlaylistSong playlistSong)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (image.Source is Bitmap bitmap)
|
||||||
|
{
|
||||||
|
bitmap.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
image.Source = null;
|
||||||
|
|
||||||
|
int hashCode = image.GetHashCode();
|
||||||
|
|
||||||
|
_imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
|
||||||
|
cancellationTokenSource?.Cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user