Compare commits
10 Commits
9b80bf4a98
...
e1338563ed
| Author | SHA1 | Date | |
|---|---|---|---|
| e1338563ed | |||
| f459e0e6e6 | |||
| 2b2cb9d942 | |||
| f7cacf0bbb | |||
| b7ba8341a1 | |||
| 97f6040122 | |||
| 9245e9c4cc | |||
| c3a87dee6a | |||
| d988961513 | |||
| 107a20b713 |
@@ -32,30 +32,39 @@ public abstract class Cache<TKey, TValue> : ICache<TKey, TValue> where TKey : no
|
|||||||
|
|
||||||
SemaphoreSlim lockObject = _locks.GetOrAdd(actualKey, (key) => new SemaphoreSlim(1, 1));
|
SemaphoreSlim lockObject = _locks.GetOrAdd(actualKey, (key) => new SemaphoreSlim(1, 1));
|
||||||
|
|
||||||
|
bool throttlerAcquired = false;
|
||||||
|
bool lockAcquired = false;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _throttler.WaitAsync(cancellationToken);
|
await _throttler.WaitAsync(cancellationToken);
|
||||||
|
throttlerAcquired = true;
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
await lockObject.WaitAsync(cancellationToken);
|
await lockObject.WaitAsync(cancellationToken);
|
||||||
|
lockAcquired = true;
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
return default;
|
return default;
|
||||||
|
|
||||||
return TryGetValue(actualKey) ?? await FetchAsync2(key, cancellationToken);
|
return TryGetValue(actualKey) ?? await FetchAsync2(key, cancellationToken);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
if (lockAcquired)
|
||||||
lockObject.Release();
|
lockObject.Release();
|
||||||
|
|
||||||
_locks.TryRemove(lockObject, out _);
|
_locks.TryRemove(lockObject, out _);
|
||||||
|
|
||||||
|
if (throttlerAcquired)
|
||||||
_throttler.Release();
|
_throttler.Release();
|
||||||
}
|
}
|
||||||
|
|
||||||
//return TryGetValue(actualKey) ?? Refresh(key);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TValue?> FetchAsync2(TKey key, CancellationToken cancellationToken)
|
private async Task<TValue?> FetchAsync2(TKey key, CancellationToken cancellationToken)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
namespace Harmonia.Core.Caching;
|
namespace Harmonia.Core.Caching;
|
||||||
|
|
||||||
@@ -27,10 +28,16 @@ public abstract class MemoryCache<TKey, TValue> : Cache<TKey, TValue> where TKey
|
|||||||
|
|
||||||
var cacheEntryOptions = new MemoryCacheEntryOptions()
|
var cacheEntryOptions = new MemoryCacheEntryOptions()
|
||||||
.SetSize(entrySize)
|
.SetSize(entrySize)
|
||||||
.SetSlidingExpiration(SlidingExpiration);
|
.SetSlidingExpiration(SlidingExpiration)
|
||||||
|
.RegisterPostEvictionCallback(PostEvictionCallback);
|
||||||
|
|
||||||
_memoryCache.Set(key, entry, cacheEntryOptions);
|
_memoryCache.Set(key, entry, cacheEntryOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual void PostEvictionCallback(object? cacheKey, object? cacheValue, EvictionReason evictionReason, object? state)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
protected abstract long GetEntrySize(TValue entry);
|
protected abstract long GetEntrySize(TValue entry);
|
||||||
}
|
}
|
||||||
@@ -7,12 +7,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ManagedBass" Version="3.1.1" />
|
<PackageReference Include="ManagedBass" Version="4.0.2" />
|
||||||
<PackageReference Include="ManagedBass.Flac" Version="3.1.1" />
|
<PackageReference Include="ManagedBass.Flac" Version="4.0.2" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.3" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
|
||||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -23,4 +23,26 @@ public class Song
|
|||||||
public string? FileDirectory => Directory.GetParent(FileName)?.Name;
|
public string? FileDirectory => Directory.GetParent(FileName)?.Name;
|
||||||
public string? FileType => Path.GetExtension(FileName)?.Replace(".", "").ToUpper();
|
public string? FileType => Path.GetExtension(FileName)?.Replace(".", "").ToUpper();
|
||||||
public string ShortFileName => Path.GetFileNameWithoutExtension(FileName);
|
public string ShortFileName => Path.GetFileNameWithoutExtension(FileName);
|
||||||
|
|
||||||
|
public void Update(Song song)
|
||||||
|
{
|
||||||
|
if (string.Equals(song.FileName, FileName, StringComparison.OrdinalIgnoreCase) == false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Size = song.Size;
|
||||||
|
LastModified = song.LastModified;
|
||||||
|
Title = song.Title;
|
||||||
|
Album = song.Album;
|
||||||
|
Artists = song.Artists;
|
||||||
|
AlbumArtists = song.AlbumArtists;
|
||||||
|
DiscNumber = song.DiscNumber;
|
||||||
|
TrackNumber = song.TrackNumber;
|
||||||
|
Length = song.Length;
|
||||||
|
Year = song.Year;
|
||||||
|
Genre = song.Genre;
|
||||||
|
BitRate = song.BitRate;
|
||||||
|
SampleRate = song.SampleRate;
|
||||||
|
ImageName = song?.ImageName;
|
||||||
|
ImageHash = song?.ImageHash;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -172,6 +172,7 @@ public class Playlist
|
|||||||
foreach (PlaylistSong playlistSong in playlistSongs)
|
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||||
{
|
{
|
||||||
//playlistSong.Song = song;
|
//playlistSong.Song = song;
|
||||||
|
//playlistSong.Song.Update(song);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,14 +16,14 @@
|
|||||||
<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="Microsoft.NET.Test.Sdk" Version="17.13.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
<PackageReference Include="NSubstitute" Version="5.3.0" />
|
||||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.2">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
<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="2.0.0" />
|
<PackageReference Include="xunit.v3" Version="3.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.3.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -11,15 +11,15 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" Version="11.2.5" />
|
<PackageReference Include="Avalonia" Version="11.3.7" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.7" />
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.5" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.7" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
|
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" />
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.7" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
|
||||||
<PackageReference Include="Semi.Avalonia" Version="11.2.1.5" />
|
<PackageReference Include="Semi.Avalonia" Version="11.3.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
20
Harmonia.WinUI/App.xaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Application
|
||||||
|
x:Class="Harmonia.WinUI.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:Harmonia.WinUI">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||||
|
<!-- Other merged dictionaries here -->
|
||||||
|
<ResourceDictionary Source="/Resources/Converters.xaml"/>
|
||||||
|
<ResourceDictionary Source="/Resources/Geometry.xaml"/>
|
||||||
|
<ResourceDictionary Source="/Resources/Styles.xaml"/>
|
||||||
|
<ResourceDictionary Source="/Resources/ViewModels.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- Other app resources here -->
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
46
Harmonia.WinUI/App.xaml.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
using Harmonia.Core.Extensions;
|
||||||
|
using Harmonia.WinUI.Caching;
|
||||||
|
using Harmonia.WinUI.Storage;
|
||||||
|
using Harmonia.WinUI.ViewModels;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private Window? _mainWindow;
|
||||||
|
|
||||||
|
public static IServiceProvider ServiceProvider { get; private set; }
|
||||||
|
|
||||||
|
static App()
|
||||||
|
{
|
||||||
|
ServiceCollection services = new();
|
||||||
|
|
||||||
|
services.AddSingleton<MainWindow>();
|
||||||
|
|
||||||
|
//services.AddSingleton<MainViewModel>();
|
||||||
|
services.AddSingleton<PlayerViewModel>();
|
||||||
|
services.AddSingleton<PlayingSongViewModel>();
|
||||||
|
services.AddSingleton<PlaylistViewModel>();
|
||||||
|
|
||||||
|
services.AddSingleton<IAudioBitmapImageCache, AudioBitmapImageCache>();
|
||||||
|
services.AddSingleton<IStorageProvider, WindowsStorageProvider>();
|
||||||
|
|
||||||
|
services.AddHarmonia();
|
||||||
|
|
||||||
|
ServiceProvider = services.BuildServiceProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||||
|
{
|
||||||
|
_mainWindow = ServiceProvider.GetRequiredService<MainWindow>();
|
||||||
|
_mainWindow.Activate();
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Harmonia.WinUI/Assets/Default.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
Harmonia.WinUI/Assets/LockScreenLogo.scale-200.png
Normal file
|
After Width: | Height: | Size: 432 B |
BIN
Harmonia.WinUI/Assets/SplashScreen.scale-200.png
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
Harmonia.WinUI/Assets/Square150x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
Harmonia.WinUI/Assets/Square44x44Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 637 B |
|
After Width: | Height: | Size: 283 B |
BIN
Harmonia.WinUI/Assets/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 456 B |
BIN
Harmonia.WinUI/Assets/Wide310x150Logo.scale-200.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
228
Harmonia.WinUI/Caching/AudioBitmapImageCache.cs
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
using Harmonia.Core.Caching;
|
||||||
|
using Harmonia.Core.Imaging;
|
||||||
|
using Harmonia.Core.Models;
|
||||||
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Caching;
|
||||||
|
|
||||||
|
public class AudioBitmapImageCache(IAudioImageExtractor audioImageExtractor) : MemoryCache<Song, BitmapImage>, IAudioBitmapImageCache
|
||||||
|
{
|
||||||
|
protected override MemoryCacheOptions Options => new()
|
||||||
|
{
|
||||||
|
SizeLimit = 200_000_000,
|
||||||
|
CompactionPercentage = 0.2,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override TimeSpan SlidingExpiration => TimeSpan.FromSeconds(600);
|
||||||
|
protected override int MaxConcurrentRequests => 8;
|
||||||
|
protected virtual int MaxImageWidthOrHeight => 1000;
|
||||||
|
|
||||||
|
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 "Default";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async ValueTask<BitmapImage?> FetchAsync(Song key, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
SongPictureInfo? songPictureInfo = await audioImageExtractor.ExtractImageAsync(key.FileName, cancellationToken);
|
||||||
|
|
||||||
|
if (songPictureInfo == null)
|
||||||
|
return GetDefaultBitmapImage();
|
||||||
|
|
||||||
|
using MemoryStream stream = new(songPictureInfo.Data);
|
||||||
|
|
||||||
|
BitmapImage bitmapImage = new();
|
||||||
|
|
||||||
|
await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream());
|
||||||
|
|
||||||
|
bitmapImage.DecodePixelWidth = GetDecodePixelWidth(bitmapImage);
|
||||||
|
bitmapImage.DecodePixelHeight = GetDecodePixelHeight(bitmapImage);
|
||||||
|
|
||||||
|
return bitmapImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private BitmapImage GetDefaultBitmapImage()
|
||||||
|
{
|
||||||
|
Uri uri = new("ms-appx:///Assets/Default.png", UriKind.Absolute);
|
||||||
|
|
||||||
|
BitmapImage bitmapImage = new();
|
||||||
|
bitmapImage.DecodePixelWidth = GetDecodePixelWidth(bitmapImage);
|
||||||
|
bitmapImage.DecodePixelHeight = GetDecodePixelHeight(bitmapImage);
|
||||||
|
bitmapImage.UriSource = uri;
|
||||||
|
|
||||||
|
return bitmapImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetDecodePixelWidth(BitmapImage bitmapImage)
|
||||||
|
{
|
||||||
|
int originalImageWidth = bitmapImage.PixelWidth;
|
||||||
|
int orignalImageHeight = bitmapImage.PixelHeight;
|
||||||
|
|
||||||
|
if (originalImageWidth <= MaxImageWidthOrHeight && orignalImageHeight <= MaxImageWidthOrHeight)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (orignalImageHeight > originalImageWidth)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return MaxImageWidthOrHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetDecodePixelHeight(BitmapImage bitmapImage)
|
||||||
|
{
|
||||||
|
int originalImageWidth = bitmapImage.PixelWidth;
|
||||||
|
int orignalImageHeight = bitmapImage.PixelHeight;
|
||||||
|
|
||||||
|
if (originalImageWidth <= MaxImageWidthOrHeight && orignalImageHeight <= MaxImageWidthOrHeight)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (originalImageWidth > orignalImageHeight)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return MaxImageWidthOrHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override long GetEntrySize(BitmapImage entry)
|
||||||
|
{
|
||||||
|
return entry.DecodePixelWidth * entry.DecodePixelHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class AudioBitmapImageCache2 : MemoryCache<Song, BitmapImage>, IAudioBitmapImageCache
|
||||||
|
{
|
||||||
|
private readonly IAudioImageExtractor _audioImageExtractor;
|
||||||
|
private readonly BitmapImage[] _bitmapPool;
|
||||||
|
private int _nextIndex = 0;
|
||||||
|
private ConcurrentDictionary<object, BitmapImage> _test = [];
|
||||||
|
|
||||||
|
protected override MemoryCacheOptions Options => new()
|
||||||
|
{
|
||||||
|
SizeLimit = 200_000_000,
|
||||||
|
CompactionPercentage = 0.2,
|
||||||
|
};
|
||||||
|
|
||||||
|
protected override TimeSpan SlidingExpiration => TimeSpan.FromSeconds(600);
|
||||||
|
protected override int MaxConcurrentRequests => 8;
|
||||||
|
protected virtual int MaxImageWidthOrHeight => 1000;
|
||||||
|
protected virtual int BitmapPoolSize => 64;
|
||||||
|
|
||||||
|
public AudioBitmapImageCache2(IAudioImageExtractor audioImageExtractor)
|
||||||
|
{
|
||||||
|
_audioImageExtractor = audioImageExtractor;
|
||||||
|
_bitmapPool = new BitmapImage[BitmapPoolSize];
|
||||||
|
|
||||||
|
InitializeBitmapPool();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeBitmapPool()
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _bitmapPool.Length; i++)
|
||||||
|
_bitmapPool[i] = new BitmapImage();
|
||||||
|
|
||||||
|
//DispatcherQueue dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
|
|
||||||
|
//TaskCompletionSource taskCompletionSource = new();
|
||||||
|
|
||||||
|
//dispatcherQueue.TryEnqueue(() =>
|
||||||
|
//{
|
||||||
|
// for (int i = 0; i < _bitmapPool.Length; i++)
|
||||||
|
// _bitmapPool[i] = new BitmapImage();
|
||||||
|
|
||||||
|
// taskCompletionSource.SetResult();
|
||||||
|
//});
|
||||||
|
|
||||||
|
//taskCompletionSource.Task.GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "Default";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async ValueTask<BitmapImage?> FetchAsync(Song key, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
int index = Interlocked.Increment(ref _nextIndex);
|
||||||
|
BitmapImage bitmapImage = _bitmapPool[index % _bitmapPool.Length];
|
||||||
|
//_test.AddOrUpdate(index, bitmapImage);
|
||||||
|
|
||||||
|
SongPictureInfo? songPictureInfo = await _audioImageExtractor.ExtractImageAsync(key.FileName, cancellationToken);
|
||||||
|
|
||||||
|
if (songPictureInfo == null)
|
||||||
|
{
|
||||||
|
bitmapImage.UriSource = new("ms-appx:///Assets/Default.png", UriKind.Absolute);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using MemoryStream stream = new(songPictureInfo.Data);
|
||||||
|
await bitmapImage.SetSourceAsync(stream.AsRandomAccessStream());
|
||||||
|
}
|
||||||
|
|
||||||
|
bitmapImage.DecodePixelWidth = GetDecodePixelWidth(bitmapImage);
|
||||||
|
bitmapImage.DecodePixelHeight = GetDecodePixelHeight(bitmapImage);
|
||||||
|
|
||||||
|
return bitmapImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetDecodePixelWidth(BitmapImage bitmapImage)
|
||||||
|
{
|
||||||
|
int originalImageWidth = bitmapImage.PixelWidth;
|
||||||
|
int orignalImageHeight = bitmapImage.PixelHeight;
|
||||||
|
|
||||||
|
if (originalImageWidth <= MaxImageWidthOrHeight && orignalImageHeight <= MaxImageWidthOrHeight)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (orignalImageHeight > originalImageWidth)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return MaxImageWidthOrHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetDecodePixelHeight(BitmapImage bitmapImage)
|
||||||
|
{
|
||||||
|
int originalImageWidth = bitmapImage.PixelWidth;
|
||||||
|
int orignalImageHeight = bitmapImage.PixelHeight;
|
||||||
|
|
||||||
|
if (originalImageWidth <= MaxImageWidthOrHeight && orignalImageHeight <= MaxImageWidthOrHeight)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
if (originalImageWidth > orignalImageHeight)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return MaxImageWidthOrHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override long GetEntrySize(BitmapImage entry)
|
||||||
|
{
|
||||||
|
return entry.DecodePixelWidth * entry.DecodePixelHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PostEvictionCallback(object? cacheKey, object? cacheValue, EvictionReason evictionReason, object? state)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Harmonia.WinUI/Caching/IAudioBitmapImageCache.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Harmonia.Core.Caching;
|
||||||
|
using Harmonia.Core.Models;
|
||||||
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Caching;
|
||||||
|
|
||||||
|
public interface IAudioBitmapImageCache : ICache<Song, BitmapImage>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
26
Harmonia.WinUI/Converters/ArtistsToStringConverter.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Converters;
|
||||||
|
|
||||||
|
public partial class ArtistsToStringConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public ArtistsToStringConverter()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is not ICollection<string> artists)
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
return string.Join(" / ", artists);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
25
Harmonia.WinUI/Converters/DoubleToPercentConverter.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Converters;
|
||||||
|
|
||||||
|
public sealed partial class DoubleToPercentConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public DoubleToPercentConverter()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is not double doubleValue)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return Math.Round(doubleValue * 100) + "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
27
Harmonia.WinUI/Converters/NullVisibilityConverter.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Converters;
|
||||||
|
|
||||||
|
public sealed partial class NullVisibilityConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public NullVisibilityConverter()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is not IList list)
|
||||||
|
return value == null ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
|
||||||
|
return list.Count == 0 ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Harmonia.WinUI/Converters/RepeatStateConverter.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Harmonia.Core.Player;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Converters;
|
||||||
|
|
||||||
|
public sealed partial class RepeatStateConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public RepeatStateConverter()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is not RepeatState repeatState)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return repeatState.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Harmonia.WinUI/Converters/SecondsToStringConverter.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Converters;
|
||||||
|
|
||||||
|
public sealed partial class SecondsToStringConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public SecondsToStringConverter()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is not double doubleValue)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
TimeSpan timeSpan = TimeSpan.FromSeconds(doubleValue);
|
||||||
|
|
||||||
|
if (timeSpan.Hours >= 1)
|
||||||
|
return timeSpan.ToString(@"%h\:mm\:ss");
|
||||||
|
|
||||||
|
return timeSpan.ToString(@"%m\:ss");
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
return TimeSpan.Parse((string)value);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Harmonia.WinUI/Converters/SongTitleConverter.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Harmonia.Core.Models;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Converters;
|
||||||
|
|
||||||
|
public sealed partial class SongTitleConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public SongTitleConverter()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is not Song song)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(song.Title) ? song.ShortFileName : song.Title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
26
Harmonia.WinUI/Converters/VolumeStateToIconConverter.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using Harmonia.WinUI.Models;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Converters;
|
||||||
|
|
||||||
|
public sealed partial class VolumeStateConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public VolumeStateConverter()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
if (value is not VolumeState volumeState)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return volumeState.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
76
Harmonia.WinUI/Flex/IFlexView.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Flex;
|
||||||
|
|
||||||
|
public interface IFlexView
|
||||||
|
{
|
||||||
|
FlexLayout Layout { get; }
|
||||||
|
int ImageWidth { get; }
|
||||||
|
FlexOrientation ListOrientation { get; }
|
||||||
|
int RowSpacing { get; }
|
||||||
|
int ColumnSpacing { get; }
|
||||||
|
int TextLineHeight { get; }
|
||||||
|
FlexOrientation SubtitleFooterOrientation { get; }
|
||||||
|
int SubtitleFooterMargin { get; }
|
||||||
|
FlexOrientation ImageToTextOrientation { get; }
|
||||||
|
int TitleFontSize { get; }
|
||||||
|
int SubtitleFontSize { get; }
|
||||||
|
int FooterFontSize { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class GridFlexViewBase : IFlexView
|
||||||
|
{
|
||||||
|
public FlexLayout Layout => FlexLayout.Grid;
|
||||||
|
public FlexOrientation ListOrientation => FlexOrientation.Horizontal;
|
||||||
|
public FlexOrientation SubtitleFooterOrientation => FlexOrientation.Vertical;
|
||||||
|
public FlexOrientation ImageToTextOrientation => FlexOrientation.Vertical;
|
||||||
|
|
||||||
|
public abstract int ImageWidth { get; }
|
||||||
|
public abstract int RowSpacing { get; }
|
||||||
|
public abstract int ColumnSpacing { get; }
|
||||||
|
public abstract int TextLineHeight { get; }
|
||||||
|
public abstract int SubtitleFooterMargin { get; }
|
||||||
|
public abstract int TitleFontSize { get; }
|
||||||
|
public abstract int SubtitleFontSize { get; }
|
||||||
|
public abstract int FooterFontSize { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class ListFlexViewBase : IFlexView
|
||||||
|
{
|
||||||
|
public FlexLayout Layout => FlexLayout.List;
|
||||||
|
public FlexOrientation ListOrientation => FlexOrientation.Vertical;
|
||||||
|
public FlexOrientation SubtitleFooterOrientation => FlexOrientation.Vertical;
|
||||||
|
public FlexOrientation ImageToTextOrientation => FlexOrientation.Vertical;
|
||||||
|
|
||||||
|
public abstract int ImageWidth { get; }
|
||||||
|
|
||||||
|
public abstract int RowSpacing { get; }
|
||||||
|
|
||||||
|
public abstract int ColumnSpacing { get; }
|
||||||
|
|
||||||
|
public abstract int TextLineHeight { get; }
|
||||||
|
|
||||||
|
public abstract int SubtitleFooterMargin { get; }
|
||||||
|
|
||||||
|
public abstract int TitleFontSize { get; }
|
||||||
|
|
||||||
|
public abstract int SubtitleFontSize { get; }
|
||||||
|
|
||||||
|
public abstract int FooterFontSize { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FlexLayout
|
||||||
|
{
|
||||||
|
List,
|
||||||
|
Grid
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum FlexOrientation
|
||||||
|
{
|
||||||
|
Horizontal,
|
||||||
|
Vertical
|
||||||
|
}
|
||||||
100
Harmonia.WinUI/Harmonia.WinUI.csproj
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net9.0-windows10.0.26100.0</TargetFramework>
|
||||||
|
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||||
|
<RootNamespace>Harmonia.WinUI</RootNamespace>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<Platforms>x86;x64;ARM64</Platforms>
|
||||||
|
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
|
||||||
|
<PublishProfile>win-$(Platform).pubxml</PublishProfile>
|
||||||
|
<UseWinUI>true</UseWinUI>
|
||||||
|
<EnableMsixTooling>true</EnableMsixTooling>
|
||||||
|
<WindowsPackageType>None</WindowsPackageType>
|
||||||
|
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Resources\Styles.xaml" />
|
||||||
|
<None Remove="Views\PlayerView.xaml" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Assets\SplashScreen.scale-200.png" />
|
||||||
|
<Content Include="Assets\LockScreenLogo.scale-200.png" />
|
||||||
|
<Content Include="Assets\Square150x150Logo.scale-200.png" />
|
||||||
|
<Content Include="Assets\Square44x44Logo.scale-200.png" />
|
||||||
|
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
|
||||||
|
<Content Include="Assets\StoreLogo.png" />
|
||||||
|
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Manifest Include="$(ApplicationManifest)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
|
||||||
|
Tools extension to be activated for this project even if the Windows App SDK Nuget
|
||||||
|
package has not yet been restored.
|
||||||
|
-->
|
||||||
|
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||||
|
<ProjectCapability Include="Msix" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
|
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.250402" />
|
||||||
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
|
||||||
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.250916003" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Harmonia.Core\Harmonia.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Update="Assets\Default.png">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</Content>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Resources\Geometry.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Update="Resources\ViewModels.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Update="Resources\Converters.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Update="Views\PlayingSongView.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Update="Views\PlaylistView.xaml">
|
||||||
|
<SubType>Designer</SubType>
|
||||||
|
</Page>
|
||||||
|
<Page Update="Views\PlayerView.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Page Update="Resources\Styles.xaml">
|
||||||
|
<Generator>MSBuild:Compile</Generator>
|
||||||
|
</Page>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
|
||||||
|
Explorer "Package and Publish" context menu entry to be enabled for this project even if
|
||||||
|
the Windows App SDK Nuget package has not yet been restored.
|
||||||
|
-->
|
||||||
|
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
|
||||||
|
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Publish Properties -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
|
||||||
|
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||||
|
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
||||||
|
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
42
Harmonia.WinUI/MainWindow.xaml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Window
|
||||||
|
x:Class="Harmonia.WinUI.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:Harmonia.WinUI"
|
||||||
|
xmlns:views="using:Harmonia.WinUI.Views"
|
||||||
|
xmlns:Media="using:CommunityToolkit.WinUI.Media"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
Title="Harmonia.WinUI">
|
||||||
|
|
||||||
|
<!--<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
<Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
|
||||||
|
</StackPanel>-->
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid Grid.RowSpan="2">
|
||||||
|
<Image Source="/Assets/Default.png" Stretch="UniformToFill" VerticalAlignment="Center" HorizontalAlignment="Center"></Image>
|
||||||
|
<Canvas Background="#99000000"></Canvas>
|
||||||
|
<Border>
|
||||||
|
<Border.Background>
|
||||||
|
<Media:BackdropBlurBrush Amount="20"></Media:BackdropBlurBrush>
|
||||||
|
</Border.Background>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
<Grid Grid.Row="0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<views:PlayingSongView Grid.Column="0"></views:PlayingSongView>
|
||||||
|
<views:PlaylistView Grid.Column="1"></views:PlaylistView>
|
||||||
|
</Grid>
|
||||||
|
<views:PlayerView Grid.Row="1"></views:PlayerView>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</Window>
|
||||||
11
Harmonia.WinUI/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI;
|
||||||
|
|
||||||
|
public sealed partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
10
Harmonia.WinUI/Models/VolumeState.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Harmonia.WinUI.Models;
|
||||||
|
|
||||||
|
public enum VolumeState
|
||||||
|
{
|
||||||
|
Muted,
|
||||||
|
Off,
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High
|
||||||
|
}
|
||||||
51
Harmonia.WinUI/Package.appxmanifest
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<Package
|
||||||
|
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||||
|
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||||
|
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||||
|
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||||
|
IgnorableNamespaces="uap rescap">
|
||||||
|
|
||||||
|
<Identity
|
||||||
|
Name="52b8577e-aca4-4ab5-b180-69e74b1b5b49"
|
||||||
|
Publisher="CN=Brian"
|
||||||
|
Version="1.0.0.0" />
|
||||||
|
|
||||||
|
<mp:PhoneIdentity PhoneProductId="52b8577e-aca4-4ab5-b180-69e74b1b5b49" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
|
||||||
|
|
||||||
|
<Properties>
|
||||||
|
<DisplayName>Harmonia.WinUI</DisplayName>
|
||||||
|
<PublisherDisplayName>Brian</PublisherDisplayName>
|
||||||
|
<Logo>Assets\StoreLogo.png</Logo>
|
||||||
|
</Properties>
|
||||||
|
|
||||||
|
<Dependencies>
|
||||||
|
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||||
|
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
|
||||||
|
</Dependencies>
|
||||||
|
|
||||||
|
<Resources>
|
||||||
|
<Resource Language="x-generate"/>
|
||||||
|
</Resources>
|
||||||
|
|
||||||
|
<Applications>
|
||||||
|
<Application Id="App"
|
||||||
|
Executable="$targetnametoken$.exe"
|
||||||
|
EntryPoint="$targetentrypoint$">
|
||||||
|
<uap:VisualElements
|
||||||
|
DisplayName="Harmonia.WinUI"
|
||||||
|
Description="Harmonia.WinUI"
|
||||||
|
BackgroundColor="transparent"
|
||||||
|
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||||
|
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||||
|
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||||
|
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
||||||
|
</uap:VisualElements>
|
||||||
|
</Application>
|
||||||
|
</Applications>
|
||||||
|
|
||||||
|
<Capabilities>
|
||||||
|
<rescap:Capability Name="runFullTrust" />
|
||||||
|
</Capabilities>
|
||||||
|
</Package>
|
||||||
10
Harmonia.WinUI/Properties/launchSettings.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"profiles": {
|
||||||
|
"Harmonia.WinUI (Package)": {
|
||||||
|
"commandName": "MsixPackage"
|
||||||
|
},
|
||||||
|
"Harmonia.WinUI (Unpackaged)": {
|
||||||
|
"commandName": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Harmonia.WinUI/Resources/Converters.xaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:converters="using:Harmonia.WinUI.Converters">
|
||||||
|
|
||||||
|
<converters:SecondsToStringConverter x:Key="SecondsToString" />
|
||||||
|
<converters:ArtistsToStringConverter x:Key="ArtistsToString" />
|
||||||
|
<converters:NullVisibilityConverter x:Key="NullVisibility" />
|
||||||
|
<converters:SongTitleConverter x:Key="SongTitle" />
|
||||||
|
<converters:DoubleToPercentConverter x:Key="DoubleToPercent" />
|
||||||
|
<converters:RepeatStateConverter x:Key="RepeatState" />
|
||||||
|
<converters:VolumeStateConverter x:Key="VolumeState" />
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
145
Harmonia.WinUI/Resources/Geometry.xaml
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<x:String x:Key="PlayIcon">
|
||||||
|
M10.804 8 5 4.633v6.734zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="PlayFillIcon">
|
||||||
|
m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.39
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="StopIcon">
|
||||||
|
M3.5 5A1.5 1.5 0 0 1 5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11zM5 4.5a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="StopFillIcon">
|
||||||
|
M5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11V5A1.5 1.5 0 0 1 5 3.5
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="PauseIcon">
|
||||||
|
M6 3.5a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V4a.5.5 0 0 1 .5-.5m4 0a.5.5 0 0 1 .5.5v8a.5.5 0 0 1-1 0V4a.5.5 0 0 1 .5-.5
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="PauseFillIcon">
|
||||||
|
M5.5 3.5A1.5 1.5 0 0 1 7 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5m5 0A1.5 1.5 0 0 1 12 5v6a1.5 1.5 0 0 1-3 0V5a1.5 1.5 0 0 1 1.5-1.5
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="SkipStartIcon">
|
||||||
|
M4 4a.5.5 0 0 1 1 0v3.248l6.267-3.636c.52-.302 1.233.043 1.233.696v7.384c0 .653-.713.998-1.233.696L5 8.752V12a.5.5 0 0 1-1 0zm7.5.633L5.696 8l5.804 3.367z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="SkipStartFillIcon">
|
||||||
|
M4 4a.5.5 0 0 1 1 0v3.248l6.267-3.636c.54-.313 1.232.066 1.232.696v7.384c0 .63-.692 1.01-1.232.697L5 8.753V12a.5.5 0 0 1-1 0z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="SkipEndIcon">
|
||||||
|
M12.5 4a.5.5 0 0 0-1 0v3.248L5.233 3.612C4.713 3.31 4 3.655 4 4.308v7.384c0 .653.713.998 1.233.696L11.5 8.752V12a.5.5 0 0 0 1 0zM5 4.633 10.804 8 5 11.367z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="SkipEndFillIcon">
|
||||||
|
M12.5 4a.5.5 0 0 0-1 0v3.248L5.233 3.612C4.693 3.3 4 3.678 4 4.308v7.384c0 .63.692 1.01 1.233.697L11.5 8.753V12a.5.5 0 0 0 1 0z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String 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
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String 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
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String 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
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String 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
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String 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
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String 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
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String 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
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String 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
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="DeleteIcon">
|
||||||
|
M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5m3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0z
|
||||||
|
M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4zM2.5 3h11V2h-11z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="CutIcon">
|
||||||
|
M3.5 3.5c-.614-.884-.074-1.962.858-2.5L8 7.226 11.642 1c.932.538 1.472 1.616.858 2.5L8.81 8.61l1.556 2.661a2.5 2.5 0 1 1-.794.637L8 9.73l-1.572 2.177a2.5 2.5 0 1 1-.794-.637L7.19 8.61zm2.5 10a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0m7 0a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="CopyIcon">
|
||||||
|
M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1zM2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="PasteIcon">
|
||||||
|
M4 1.5H3a2 2 0 0 0-2 2V14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V3.5a2 2 0 0 0-2-2h-1v1h1a1 1 0 0 1 1 1V14a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3.5a1 1 0 0 1 1-1h1z
|
||||||
|
M9.5 1a.5.5 0 0 1 .5.5v1a.5.5 0 0 1-.5.5h-3a.5.5 0 0 1-.5-.5v-1a.5.5 0 0 1 .5-.5zm-3-1A1.5 1.5 0 0 0 5 1.5v1A1.5 1.5 0 0 0 6.5 4h3A1.5 1.5 0 0 0 11 2.5v-1A1.5 1.5 0 0 0 9.5 0z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="RefreshIcon">
|
||||||
|
M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2z
|
||||||
|
M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="SettingsIcon">
|
||||||
|
M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492M5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0
|
||||||
|
M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.377l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="LockIcon">
|
||||||
|
M8 1a2 2 0 0 1 2 2v4H6V3a2 2 0 0 1 2-2m3 6V3a3 3 0 0 0-6 0v4a2 2 0 0 0-2 2v5a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2M5 8h6a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="UnlockIcon">
|
||||||
|
M11 1a2 2 0 0 0-2 2v4a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2h5V3a3 3 0 0 1 6 0v4a.5.5 0 0 1-1 0V3a2 2 0 0 0-2-2M3 8a1 1 0 0 0-1 1v5a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="AddIcon">
|
||||||
|
M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="AddFileIcon">
|
||||||
|
M8 6.5a.5.5 0 0 1 .5.5v1.5H10a.5.5 0 0 1 0 1H8.5V11a.5.5 0 0 1-1 0V9.5H6a.5.5 0 0 1 0-1h1.5V7a.5.5 0 0 1 .5-.5
|
||||||
|
M14 4.5V14a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h5.5zm-3 0A1.5 1.5 0 0 1 9.5 3V1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V4.5z
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
<x:String x:Key="AddFolderIcon">
|
||||||
|
m.5 3 .04.87a2 2 0 0 0-.342 1.311l.637 7A2 2 0 0 0 2.826 14H9v-1H2.826a1 1 0 0 1-.995-.91l-.637-7A1 1 0 0 1 2.19 4h11.62a1 1 0 0 1 .996 1.09L14.54 8h1.005l.256-2.819A2 2 0 0 0 13.81 3H9.828a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 6.172 1H2.5a2 2 0 0 0-2 2m5.672-1a1 1 0 0 1 .707.293L7.586 3H2.19q-.362.002-.683.12L1.5 2.98a1 1 0 0 1 1-.98z
|
||||||
|
M13.5 9a.5.5 0 0 1 .5.5V11h1.5a.5.5 0 1 1 0 1H14v1.5a.5.5 0 1 1-1 0V12h-1.5a.5.5 0 0 1 0-1H13V9.5a.5.5 0 0 1 .5-.5
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
|
||||||
|
<x:String x:Key="MoreIcon">
|
||||||
|
M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3
|
||||||
|
</x:String>
|
||||||
|
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
79
Harmonia.WinUI/Resources/Styles.xaml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<!-- Global Font -->
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="TextBox" BasedOn="{StaticResource DefaultTextBoxStyle}">
|
||||||
|
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="MenuFlyoutItem" BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}">
|
||||||
|
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="ListViewItem" BasedOn="{StaticResource DefaultListViewItemStyle}">
|
||||||
|
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Flat Button -->
|
||||||
|
<Style x:Key="FlatButton" TargetType="Button">
|
||||||
|
<Setter Property="Padding" Value="10"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Flat Round Button -->
|
||||||
|
<Style x:Key="FlatRoundButton" TargetType="Button" BasedOn="{StaticResource FlatButton}">
|
||||||
|
<Setter Property="CornerRadius" Value="32"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Flat Button Path Icon -->
|
||||||
|
<Style x:Key="FlatButtonIcon" TargetType="PathIcon">
|
||||||
|
<Setter Property="Width" Value="18"/>
|
||||||
|
<Setter Property="Height" Value="18"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Flat Button Path -->
|
||||||
|
<Style x:Key="FlatButtonPath" TargetType="Path">
|
||||||
|
<Setter Property="Width" Value="18"/>
|
||||||
|
<Setter Property="Height" Value="18"/>
|
||||||
|
<Setter Property="Stretch" Value="Uniform"/>
|
||||||
|
<Setter Property="Fill" Value="White"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Flat Button Path Icon (Medium) -->
|
||||||
|
<Style x:Key="FlatButtonPath-Medium" TargetType="Path" BasedOn="{StaticResource FlatButtonPath}">
|
||||||
|
<Setter Property="Width" Value="24"/>
|
||||||
|
<Setter Property="Height" Value="24"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Flat Button Path Icon (Large) -->
|
||||||
|
<Style x:Key="FlatButtonPath-Large" TargetType="Path" BasedOn="{StaticResource FlatButtonPath}">
|
||||||
|
<Setter Property="Width" Value="36"/>
|
||||||
|
<Setter Property="Height" Value="36"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Flat Button Path Icon (Medium) -->
|
||||||
|
<Style x:Key="FlatButtonIcon-Medium" TargetType="PathIcon" BasedOn="{StaticResource FlatButtonIcon}">
|
||||||
|
<Setter Property="Width" Value="24"/>
|
||||||
|
<Setter Property="Height" Value="24"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Flat Button Path Icon (Large) -->
|
||||||
|
<Style x:Key="FlatButtonIcon-Large" TargetType="PathIcon" BasedOn="{StaticResource FlatButtonIcon}">
|
||||||
|
<Setter Property="Width" Value="36"/>
|
||||||
|
<Setter Property="Height" Value="36"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Selected List View Item -->
|
||||||
|
<!--<Style x:Name="SelectedListViewItem" TargetType="ListViewItem">
|
||||||
|
<Setter Property="Foreground" Value="Red" />
|
||||||
|
</Style>-->
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
9
Harmonia.WinUI/Resources/ViewModels.xaml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:Harmonia.WinUI.ViewModels">
|
||||||
|
|
||||||
|
<vm:ViewModelLocator x:Key="Locator" />
|
||||||
|
|
||||||
|
</ResourceDictionary>
|
||||||
10
Harmonia.WinUI/Session/ApplicationSession.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Harmonia.WinUI.Session;
|
||||||
|
|
||||||
|
public partial class ApplicationSession
|
||||||
|
{
|
||||||
|
public WindowState Window { get; set; } = new();
|
||||||
|
public NavigationState Navigation { get; set; } = new();
|
||||||
|
public AudioLibraryState Library { get; set; } = new();
|
||||||
|
public AudioPlayerState Player { get; set; } = new();
|
||||||
|
public PlaylistState Playlist { get; set; } = new();
|
||||||
|
}
|
||||||
8
Harmonia.WinUI/Session/AudioLibraryState.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Harmonia.WinUI.Session;
|
||||||
|
|
||||||
|
public partial class AudioLibraryState
|
||||||
|
{
|
||||||
|
public string? FlexViewType { get; set; }
|
||||||
|
public string? LibraryViewType { get; set; }
|
||||||
|
public string? Breadcrumbs { get; set; }
|
||||||
|
}
|
||||||
13
Harmonia.WinUI/Session/AudioPlayerState.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
namespace Harmonia.WinUI.Session;
|
||||||
|
|
||||||
|
public partial class AudioPlayerState
|
||||||
|
{
|
||||||
|
public string? PlaylistUID { get; set; }
|
||||||
|
public string? PlaylistSongUID { get; set; }
|
||||||
|
public double Position { get; set; } = 0.0;
|
||||||
|
public string? PlaybackState { get; set; }
|
||||||
|
public double Volume { get; set; } = 1.0;
|
||||||
|
public bool IsMuted { get; set; }
|
||||||
|
public string? RepeatState { get; set; }
|
||||||
|
public bool IsRandom { get; set; }
|
||||||
|
}
|
||||||
6
Harmonia.WinUI/Session/NavigationState.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Harmonia.WinUI.Session;
|
||||||
|
|
||||||
|
public partial class NavigationState
|
||||||
|
{
|
||||||
|
public string? CurrentPage { get; set; }
|
||||||
|
}
|
||||||
6
Harmonia.WinUI/Session/PlaylistState.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Harmonia.WinUI.Session;
|
||||||
|
|
||||||
|
public partial class PlaylistState
|
||||||
|
{
|
||||||
|
public string? OpenPlaylistUID { get; set; }
|
||||||
|
}
|
||||||
6
Harmonia.WinUI/Session/WindowState.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Harmonia.WinUI.Session;
|
||||||
|
|
||||||
|
public partial class WindowState
|
||||||
|
{
|
||||||
|
public bool IsMaximized { get; set; }
|
||||||
|
}
|
||||||
9
Harmonia.WinUI/Storage/FilePickerFileType.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Storage;
|
||||||
|
|
||||||
|
public sealed class FilePickerFileType
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public IReadOnlyList<string> Patterns { get; set; } = [];
|
||||||
|
}
|
||||||
9
Harmonia.WinUI/Storage/FilePickerOptions.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Storage;
|
||||||
|
|
||||||
|
public class FilePickerOptions
|
||||||
|
{
|
||||||
|
public IReadOnlyList<FilePickerFileType> FileTypeFilter { get; set; } = [];
|
||||||
|
public StoragePickerViewMode ViewMode { get; set; }
|
||||||
|
}
|
||||||
6
Harmonia.WinUI/Storage/FolderPickerOptions.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Harmonia.WinUI.Storage;
|
||||||
|
|
||||||
|
public class FolderPickerOptions
|
||||||
|
{
|
||||||
|
public StoragePickerViewMode ViewMode { get; set; }
|
||||||
|
}
|
||||||
10
Harmonia.WinUI/Storage/IStorageProvider.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Storage;
|
||||||
|
|
||||||
|
public interface IStorageProvider
|
||||||
|
{
|
||||||
|
public Task<string?> GetSingleFileAsync(FilePickerOptions? options = null);
|
||||||
|
public Task<string[]> GetFilesAsync(FilePickerOptions? options = null);
|
||||||
|
public Task<string?> GetPathAsync();
|
||||||
|
}
|
||||||
7
Harmonia.WinUI/Storage/StoragePickerViewMode.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Harmonia.WinUI.Storage;
|
||||||
|
|
||||||
|
public enum StoragePickerViewMode
|
||||||
|
{
|
||||||
|
List,
|
||||||
|
Thumbnail
|
||||||
|
}
|
||||||
93
Harmonia.WinUI/Storage/WindowsStorageProvider.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Windows.Storage;
|
||||||
|
using Windows.Storage.Pickers;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Storage;
|
||||||
|
|
||||||
|
public class WindowsStorageProvider : IStorageProvider
|
||||||
|
{
|
||||||
|
private static MainWindow MainWindow => App.ServiceProvider.GetRequiredService<MainWindow>();
|
||||||
|
|
||||||
|
public async Task<string?> GetSingleFileAsync(FilePickerOptions? options = null)
|
||||||
|
{
|
||||||
|
FileOpenPicker fileOpenPicker = GetFileOpenPicker(options);
|
||||||
|
InitializePicker(fileOpenPicker);
|
||||||
|
|
||||||
|
StorageFile? storageFile = await fileOpenPicker.PickSingleFileAsync();
|
||||||
|
|
||||||
|
return storageFile?.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string[]> GetFilesAsync(FilePickerOptions? options = null)
|
||||||
|
{
|
||||||
|
FileOpenPicker fileOpenPicker = GetFileOpenPicker(options);
|
||||||
|
InitializePicker(fileOpenPicker);
|
||||||
|
|
||||||
|
var storageFiles = await fileOpenPicker.PickMultipleFilesAsync();
|
||||||
|
|
||||||
|
return [.. storageFiles.Select(storageFile => storageFile.Path)];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static FileOpenPicker GetFileOpenPicker(FilePickerOptions? options = null)
|
||||||
|
{
|
||||||
|
FilePickerOptions filePickerOptions = options ?? new();
|
||||||
|
|
||||||
|
FileOpenPicker fileOpenPicker = new()
|
||||||
|
{
|
||||||
|
ViewMode = GetViewMode(filePickerOptions.ViewMode)
|
||||||
|
};
|
||||||
|
|
||||||
|
string[] filters = GetFilters(filePickerOptions.FileTypeFilter);
|
||||||
|
|
||||||
|
foreach (string filter in filters)
|
||||||
|
{
|
||||||
|
fileOpenPicker.FileTypeFilter.Add(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filePickerOptions.FileTypeFilter.Count == 0)
|
||||||
|
{
|
||||||
|
fileOpenPicker.FileTypeFilter.Add("*");
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileOpenPicker;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PickerViewMode GetViewMode(StoragePickerViewMode viewMode)
|
||||||
|
{
|
||||||
|
return viewMode switch
|
||||||
|
{
|
||||||
|
StoragePickerViewMode.Thumbnail => PickerViewMode.Thumbnail,
|
||||||
|
_ => PickerViewMode.List,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] GetFilters(IReadOnlyList<FilePickerFileType> fileTypes)
|
||||||
|
{
|
||||||
|
return [.. fileTypes.SelectMany(fileType => fileType.Patterns)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> GetPathAsync()
|
||||||
|
{
|
||||||
|
var folderPicker = new FolderPicker
|
||||||
|
{
|
||||||
|
ViewMode = PickerViewMode.Thumbnail
|
||||||
|
};
|
||||||
|
folderPicker.FileTypeFilter.Add("*");
|
||||||
|
|
||||||
|
InitializePicker(folderPicker);
|
||||||
|
|
||||||
|
StorageFolder? folder = await folderPicker.PickSingleFolderAsync();
|
||||||
|
|
||||||
|
return folder?.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InitializePicker(object target)
|
||||||
|
{
|
||||||
|
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(MainWindow);
|
||||||
|
WinRT.Interop.InitializeWithWindow.Initialize(target, hWnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
350
Harmonia.WinUI/ViewModels/PlayerViewModel.cs
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Harmonia.Core.Engine;
|
||||||
|
using Harmonia.Core.Models;
|
||||||
|
using Harmonia.Core.Player;
|
||||||
|
using Harmonia.WinUI.Caching;
|
||||||
|
using Harmonia.WinUI.Models;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Windows.System;
|
||||||
|
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.ViewModels;
|
||||||
|
|
||||||
|
public partial class PlayerViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IAudioPlayer _audioPlayer;
|
||||||
|
private readonly IAudioBitmapImageCache _audioBitmapImageCache;
|
||||||
|
private readonly DispatcherTimer _timer;
|
||||||
|
private readonly DispatcherQueue _dispatcherQueue;
|
||||||
|
|
||||||
|
private CancellationTokenSource? _songImageCancellationTokenSource;
|
||||||
|
|
||||||
|
private Song? _song;
|
||||||
|
public Song? Song
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _song;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
SetProperty(ref _song, value);
|
||||||
|
|
||||||
|
CurrentPosition = 0; // What if player is being loaded and returning to save state?
|
||||||
|
Position = 0;
|
||||||
|
MaxPosition = value?.Length.TotalSeconds ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BitmapImage? _songImageSource;
|
||||||
|
public BitmapImage? SongImageSource
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _songImageSource;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
SetProperty(ref _songImageSource, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _currentPosition;
|
||||||
|
public double CurrentPosition
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _currentPosition;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _currentPosition, value);
|
||||||
|
|
||||||
|
if (_isPositionChangeInProgress)
|
||||||
|
_audioPlayer.Position = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _position;
|
||||||
|
public double Position
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _position;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
SetProperty(ref _position, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _maxPosition;
|
||||||
|
public double MaxPosition
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _maxPosition;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _maxPosition, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _isPositionChangeInProgress;
|
||||||
|
public bool IsPositionChangeInProgress
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _isPositionChangeInProgress;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _isPositionChangeInProgress, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _audioPlayer.IsRandom;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_audioPlayer.IsRandom = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RepeatState RepeatState
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _audioPlayer.RepeatState;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_audioPlayer.RepeatState = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanUpdatePosition
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _audioPlayer.State != AudioPlaybackState.Stopped;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand PlaySongCommand => new RelayCommand(Play);
|
||||||
|
public ICommand PauseSongCommand => new RelayCommand(Pause);
|
||||||
|
public ICommand StopSongCommand => new RelayCommand(Stop);
|
||||||
|
public ICommand PreviousSongCommand => new RelayCommand(Previous);
|
||||||
|
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 PlayerViewModel(IAudioPlayer audioPlayer, IAudioBitmapImageCache audioBitmapCache)
|
||||||
|
{
|
||||||
|
_audioPlayer = audioPlayer;
|
||||||
|
_audioPlayer.PlayingSongChanged += OnPlayingSongChanged;
|
||||||
|
_audioPlayer.PropertyChanged += OnAudioPlayerPropertyChanged;
|
||||||
|
|
||||||
|
_audioBitmapImageCache = audioBitmapCache;
|
||||||
|
|
||||||
|
_timer = new()
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromMilliseconds(100)
|
||||||
|
};
|
||||||
|
|
||||||
|
_timer.Tick += TickTock;
|
||||||
|
|
||||||
|
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Event Handlers
|
||||||
|
|
||||||
|
private void OnPlayingSongChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Song = _audioPlayer.PlayingSong?.Song;
|
||||||
|
Task.Run(UpdateImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateImage()
|
||||||
|
{
|
||||||
|
// TODO: Show default picture
|
||||||
|
if (Song == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_songImageCancellationTokenSource != null)
|
||||||
|
await _songImageCancellationTokenSource.CancelAsync();
|
||||||
|
|
||||||
|
_songImageCancellationTokenSource = new();
|
||||||
|
CancellationToken cancellationToken = _songImageCancellationTokenSource.Token;
|
||||||
|
|
||||||
|
//BitmapImage? bitmapImage = await _audioBitmapImageCache.GetAsync(Song, cancellationToken);
|
||||||
|
|
||||||
|
_dispatcherQueue.TryEnqueue(async () =>
|
||||||
|
{
|
||||||
|
BitmapImage? bitmapImage = await _audioBitmapImageCache.GetAsync(Song, cancellationToken);
|
||||||
|
SongImageSource = bitmapImage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAudioPlayerPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.PropertyName)
|
||||||
|
{
|
||||||
|
case nameof(_audioPlayer.State):
|
||||||
|
UpdateTimer();
|
||||||
|
OnPropertyChanged(nameof(CanUpdatePosition));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateTimer()
|
||||||
|
{
|
||||||
|
if (_audioPlayer.State == AudioPlaybackState.Playing)
|
||||||
|
{
|
||||||
|
_timer.Start();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_timer.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TickTock(object? sender, object e)
|
||||||
|
{
|
||||||
|
Position = _audioPlayer.Position;
|
||||||
|
|
||||||
|
if (IsPositionChangeInProgress)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CurrentPosition = _audioPlayer.Position;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
|
||||||
|
public void Play()
|
||||||
|
{
|
||||||
|
_audioPlayer.Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Pause()
|
||||||
|
{
|
||||||
|
_audioPlayer.Pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
_audioPlayer.Stop();
|
||||||
|
CurrentPosition = 0;
|
||||||
|
Position = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Previous()
|
||||||
|
{
|
||||||
|
_audioPlayer.PreviousAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Next()
|
||||||
|
{
|
||||||
|
_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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
79
Harmonia.WinUI/ViewModels/PlayingSongViewModel.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
using Harmonia.Core.Models;
|
||||||
|
using Harmonia.Core.Player;
|
||||||
|
using Harmonia.WinUI.Caching;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.ViewModels;
|
||||||
|
|
||||||
|
public partial class PlayingSongViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IAudioPlayer _audioPlayer;
|
||||||
|
private readonly IAudioBitmapImageCache _audioBitmapImageCache;
|
||||||
|
private readonly DispatcherQueue _dispatcherQueue;
|
||||||
|
|
||||||
|
private CancellationTokenSource? _songImageCancellationTokenSource;
|
||||||
|
|
||||||
|
private Song? _song;
|
||||||
|
public Song? Song
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _song;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
SetProperty(ref _song, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BitmapImage? _songImageSource;
|
||||||
|
public BitmapImage? SongImageSource
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _songImageSource;
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
SetProperty(ref _songImageSource, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayingSongViewModel(IAudioPlayer audioPlayer, IAudioBitmapImageCache audioBitmapImageCache)
|
||||||
|
{
|
||||||
|
_audioPlayer = audioPlayer;
|
||||||
|
_audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged;
|
||||||
|
|
||||||
|
_audioBitmapImageCache = audioBitmapImageCache;
|
||||||
|
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Song = _audioPlayer.PlayingSong?.Song;
|
||||||
|
Task.Run(UpdateImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateImage()
|
||||||
|
{
|
||||||
|
// TODO: Show default picture
|
||||||
|
if (Song == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_songImageCancellationTokenSource != null)
|
||||||
|
await _songImageCancellationTokenSource.CancelAsync();
|
||||||
|
|
||||||
|
_songImageCancellationTokenSource = new();
|
||||||
|
CancellationToken cancellationToken = _songImageCancellationTokenSource.Token;
|
||||||
|
|
||||||
|
_dispatcherQueue.TryEnqueue(async () =>
|
||||||
|
{
|
||||||
|
BitmapImage? bitmapImage = await _audioBitmapImageCache.GetAsync(Song, cancellationToken);
|
||||||
|
SongImageSource = bitmapImage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
559
Harmonia.WinUI/ViewModels/PlaylistViewModel.cs
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Harmonia.Core.Caching;
|
||||||
|
using Harmonia.Core.Engine;
|
||||||
|
using Harmonia.Core.Imaging;
|
||||||
|
using Harmonia.Core.Models;
|
||||||
|
using Harmonia.Core.Player;
|
||||||
|
using Harmonia.Core.Playlists;
|
||||||
|
using Harmonia.Core.Scanner;
|
||||||
|
using Harmonia.WinUI.Caching;
|
||||||
|
using Harmonia.WinUI.Storage;
|
||||||
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Timers;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
|
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||||
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.ViewModels;
|
||||||
|
|
||||||
|
public partial class PlaylistViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IAudioPlayer _audioPlayer;
|
||||||
|
private readonly IAudioImageCache _audioImageCache;
|
||||||
|
private readonly IAudioBitmapImageCache _audioBitmapImageCache;
|
||||||
|
private readonly IAudioFileScanner _audioFileScanner;
|
||||||
|
private readonly IAudioEngine _audioEngine;
|
||||||
|
private readonly IStorageProvider _storageProvider;
|
||||||
|
private readonly DispatcherQueue _dispatcherQueue;
|
||||||
|
private readonly ConcurrentDictionary<int, CancellationTokenSource> _imageCancellationTokens = [];
|
||||||
|
|
||||||
|
private Timer? _filterTimer;
|
||||||
|
|
||||||
|
public Playlist? Playlist { get; private set; }
|
||||||
|
|
||||||
|
private PlaylistSong? _playingSong;
|
||||||
|
public PlaylistSong? PlayingSong
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _playingSong;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _playingSong, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObservableCollection<PlaylistSong> _playlistSongs = [];
|
||||||
|
public ObservableCollection<PlaylistSong> PlaylistSongs
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _playlistSongs;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _playlistSongs, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? _filter;
|
||||||
|
public string? Filter
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _filter;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _filter, value);
|
||||||
|
RestartFilterTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObservableCollection<PlaylistSong> _filteredPlaylistSongs = [];
|
||||||
|
public ObservableCollection<PlaylistSong> FilteredPlaylistSongs
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _filteredPlaylistSongs;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _filteredPlaylistSongs, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObservableCollection<PlaylistSong> _selectedPlaylistSongs = [];
|
||||||
|
public ObservableCollection<PlaylistSong> SelectedPlaylistSongs
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _selectedPlaylistSongs;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _selectedPlaylistSongs, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ICommand PlaySongCommand => new AsyncRelayCommand(PlaySongAsync, AreSongsSelected);
|
||||||
|
public ICommand AddFilesCommand => new AsyncRelayCommand(AddFilesAsync);
|
||||||
|
public ICommand AddFolderCommand => new AsyncRelayCommand(AddFolderAsync);
|
||||||
|
public ICommand RemoveSongsCommand => new RelayCommand(RemoveSongs, AreSongsSelected);
|
||||||
|
public ICommand CutSongsCommand => new RelayCommand(CutSongs, AreSongsSelected);
|
||||||
|
public ICommand CopySongsCommand => new RelayCommand(CopySongs, AreSongsSelected);
|
||||||
|
public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs);
|
||||||
|
public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected);
|
||||||
|
public ICommand RefreshTagsCommand => new RelayCommand(RefreshTags);
|
||||||
|
public ICommand RemoveMissingSongsCommand => new RelayCommand(RemoveMissingSongs);
|
||||||
|
public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs);
|
||||||
|
|
||||||
|
public bool IsUserUpdating { get; set; }
|
||||||
|
private bool _isUserInitiatingSongChange;
|
||||||
|
|
||||||
|
public event EventHandler? PlayingSongChangedAutomatically;
|
||||||
|
|
||||||
|
public PlaylistViewModel(
|
||||||
|
IAudioPlayer audioPlayer,
|
||||||
|
IAudioImageCache audioImageCache,
|
||||||
|
IAudioBitmapImageCache audioBitmapImageCache,
|
||||||
|
IAudioFileScanner audioFileScanner,
|
||||||
|
IAudioEngine audioEngine,
|
||||||
|
IStorageProvider storageProvider,
|
||||||
|
IPlaylistRepository playlistRepository)
|
||||||
|
{
|
||||||
|
_audioPlayer = audioPlayer;
|
||||||
|
_audioPlayer.PlaylistChanged += OnPlaylistChanged;
|
||||||
|
_audioPlayer.PlayingSongChanged += OnPlayingSongChanged;
|
||||||
|
|
||||||
|
_audioImageCache = audioImageCache;
|
||||||
|
_audioBitmapImageCache = audioBitmapImageCache;
|
||||||
|
_audioFileScanner = audioFileScanner;
|
||||||
|
_audioEngine = audioEngine;
|
||||||
|
_storageProvider = storageProvider;
|
||||||
|
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
|
|
||||||
|
FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged;
|
||||||
|
|
||||||
|
// Testing
|
||||||
|
Task.Run(() => PlayDemoSong(playlistRepository));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFilteredPlaylistSongsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (IsUserUpdating == false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int x = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PlayDemoSong(IPlaylistRepository playlistRepository)
|
||||||
|
{
|
||||||
|
if (playlistRepository.Get().Count == 0)
|
||||||
|
{
|
||||||
|
playlistRepository.AddPlaylist();
|
||||||
|
}
|
||||||
|
|
||||||
|
Playlist playlist = playlistRepository.Get().First();
|
||||||
|
|
||||||
|
if (playlist.Songs.Count > 0)
|
||||||
|
await _audioPlayer.LoadAsync(playlist.Songs[0], PlaybackMode.LoadOnly);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlaylistChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (Playlist != null)
|
||||||
|
{
|
||||||
|
Playlist.PlaylistUpdated -= OnPlaylistUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
Playlist = _audioPlayer.Playlist;
|
||||||
|
|
||||||
|
if (Playlist != null)
|
||||||
|
{
|
||||||
|
Playlist.PlaylistUpdated += OnPlaylistUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? [];
|
||||||
|
|
||||||
|
PlaylistSongs = [.. playlistSongs];
|
||||||
|
UpdateFilteredSongs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
|
||||||
|
{
|
||||||
|
if (IsUserUpdating)
|
||||||
|
return;
|
||||||
|
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case PlaylistUpdateAction.Add:
|
||||||
|
_dispatcherQueue.TryEnqueue(() => AddSongs(e.Songs, e.Index));
|
||||||
|
break;
|
||||||
|
case PlaylistUpdateAction.Remove:
|
||||||
|
_dispatcherQueue.TryEnqueue(() => RemoveSongsFromCollection(e.Songs));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddSongs(PlaylistSong[] playlistSongs, int index = 0)
|
||||||
|
{
|
||||||
|
// TODO: Performance improvements
|
||||||
|
int currentIndex = index;
|
||||||
|
|
||||||
|
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||||
|
{
|
||||||
|
PlaylistSongs.Insert(currentIndex++, playlistSong);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateFilteredSongs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs)
|
||||||
|
{
|
||||||
|
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||||
|
{
|
||||||
|
PlaylistSongs.Remove(playlistSong);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateFilteredSongs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayingSongChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
PlayingSong = _audioPlayer.PlayingSong;
|
||||||
|
|
||||||
|
if (_isUserInitiatingSongChange)
|
||||||
|
{
|
||||||
|
_isUserInitiatingSongChange = false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PlayingSongChangedAutomatically?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task PlaySongAsync(PlaylistSong playlistSong)
|
||||||
|
{
|
||||||
|
_isUserInitiatingSongChange = true;
|
||||||
|
await _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SongPictureInfo?> GetSongPictureInfoAsync(int hashCode, PlaylistSong playlistSong)
|
||||||
|
{
|
||||||
|
_imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
|
||||||
|
cancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
|
cancellationTokenSource = new();
|
||||||
|
_imageCancellationTokens.AddOrUpdate(hashCode, cancellationTokenSource, (_, _) => cancellationTokenSource);
|
||||||
|
|
||||||
|
return await _audioImageCache.GetAsync(playlistSong.Song, cancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BitmapImage?> GetBitmapImageAsync(int hashCode, PlaylistSong playlistSong)
|
||||||
|
{
|
||||||
|
_imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
|
||||||
|
cancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
|
cancellationTokenSource = new();
|
||||||
|
_imageCancellationTokens.AddOrUpdate(hashCode, cancellationTokenSource, (_, _) => cancellationTokenSource);
|
||||||
|
|
||||||
|
return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationTokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<BitmapImage?> GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Filtering
|
||||||
|
|
||||||
|
private void RestartFilterTimer()
|
||||||
|
{
|
||||||
|
if (_filterTimer == null)
|
||||||
|
{
|
||||||
|
_filterTimer = new Timer(300);
|
||||||
|
_filterTimer.Elapsed += OnFilterTimerElapsed;
|
||||||
|
_filterTimer.Start();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_filterTimer.Interval = 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFilterTimerElapsed(object? sender, ElapsedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_filterTimer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_filterTimer.Stop();
|
||||||
|
_filterTimer.Dispose();
|
||||||
|
_filterTimer = null;
|
||||||
|
|
||||||
|
_dispatcherQueue.TryEnqueue(UpdateFilteredSongs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFilteredSongs()
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<PlaylistSong> filteredPlaylistSongs = [.. Playlist.Songs.Where(playlistSong => IsFiltered(playlistSong.Song))];
|
||||||
|
//FilteredPlaylistSongs = [.. filteredPlaylistSongs];
|
||||||
|
|
||||||
|
for (int i = FilteredPlaylistSongs.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
PlaylistSong playlistSong = FilteredPlaylistSongs[i];
|
||||||
|
|
||||||
|
bool inPlaylist = Playlist.Songs.Contains(playlistSong);
|
||||||
|
bool inFilter = filteredPlaylistSongs.Contains(playlistSong);
|
||||||
|
|
||||||
|
if (!inPlaylist || !inFilter)
|
||||||
|
{
|
||||||
|
FilteredPlaylistSongs.Remove(playlistSong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int insertionIndex = 0;
|
||||||
|
|
||||||
|
foreach (PlaylistSong playlistSong in Playlist.Songs)
|
||||||
|
{
|
||||||
|
bool inFilter = filteredPlaylistSongs.Contains(playlistSong);
|
||||||
|
bool inCurrentFilteredList = FilteredPlaylistSongs.Contains(playlistSong);
|
||||||
|
|
||||||
|
if (inFilter)
|
||||||
|
{
|
||||||
|
if (!inCurrentFilteredList)
|
||||||
|
{
|
||||||
|
FilteredPlaylistSongs.Insert(insertionIndex, playlistSong);
|
||||||
|
}
|
||||||
|
|
||||||
|
insertionIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsFiltered(Song song)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Filter))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var shortFileName = Path.GetFileName(song.FileName);
|
||||||
|
|
||||||
|
if (shortFileName.Contains(Filter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(song.Title) == false && song.Title.Contains(Filter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(song.Album) == false && song.Album.Contains(Filter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (song.AlbumArtists.Any(x => x.Contains(Filter, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (song.Artists.Any(x => x.Contains(Filter, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Commands
|
||||||
|
|
||||||
|
private async Task PlaySongAsync()
|
||||||
|
{
|
||||||
|
if (SelectedPlaylistSongs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _audioPlayer.LoadAsync(SelectedPlaylistSongs[0], PlaybackMode.LoadAndPlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool AreSongsSelected()
|
||||||
|
{
|
||||||
|
return SelectedPlaylistSongs.Count > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddFilesAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
FilePickerOptions filePickerOptions = new()
|
||||||
|
{
|
||||||
|
FileTypeFilter = [GetAudioFileTypes()],
|
||||||
|
};
|
||||||
|
|
||||||
|
string[] fileNames = await _storageProvider.GetFilesAsync(filePickerOptions);
|
||||||
|
Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken);
|
||||||
|
|
||||||
|
Playlist.AddSongs(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FilePickerFileType GetAudioFileTypes()
|
||||||
|
{
|
||||||
|
string[] patterns = [.. _audioEngine.SupportedFormats.Select(format => format.Replace("*", ""))];
|
||||||
|
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Name = "Audio Files",
|
||||||
|
Patterns = patterns
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddFolderAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string? path = await _storageProvider.GetPathAsync();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Song[] songs = await _audioFileScanner.GetSongsFromPathAsync(path, cancellationToken);
|
||||||
|
Playlist.AddSongs(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddFilesAsync(string[] fileNames, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken);
|
||||||
|
Playlist.AddSongs(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddFolderAsync(string path, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Song[] songs = await _audioFileScanner.GetSongsFromPathAsync(path, cancellationToken);
|
||||||
|
Playlist.AddSongs(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSongs()
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (SelectedPlaylistSongs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PlaylistSong[] playlistSongs = [.. SelectedPlaylistSongs];
|
||||||
|
|
||||||
|
Playlist.RemoveSongs(playlistSongs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CutSongs()
|
||||||
|
{
|
||||||
|
if (SelectedPlaylistSongs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CopySelectedSongsToClipboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopySongs()
|
||||||
|
{
|
||||||
|
if (SelectedPlaylistSongs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CopySelectedSongsToClipboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CopySelectedSongsToClipboard()
|
||||||
|
{
|
||||||
|
Song[] songs = [.. SelectedPlaylistSongs.Select(playlistSong => playlistSong.Song)];
|
||||||
|
|
||||||
|
DataPackage dataPackage = new()
|
||||||
|
{
|
||||||
|
RequestedOperation = DataPackageOperation.Copy
|
||||||
|
};
|
||||||
|
|
||||||
|
dataPackage.Properties.Add("Type", "SongList");
|
||||||
|
dataPackage.SetData(StandardDataFormats.Text, JsonSerializer.Serialize(songs));
|
||||||
|
|
||||||
|
Clipboard.SetContent(dataPackage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanPasteSongs()
|
||||||
|
{
|
||||||
|
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
DataPackageView dataPackageView = Clipboard.GetContent();
|
||||||
|
|
||||||
|
if (dataPackageView == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (dataPackageView.Properties.ContainsKey("Type") == false)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return dataPackageView.Properties["Type"].ToString() == "SongList";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PasteSongsAsync()
|
||||||
|
{
|
||||||
|
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
int selectedPlaylistSongIndex = Playlist.Songs.IndexOf(SelectedPlaylistSongs[0]);
|
||||||
|
|
||||||
|
if (selectedPlaylistSongIndex == -1)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Song[] songs = await GetSongsFromClipboardAsync();
|
||||||
|
|
||||||
|
Playlist.AddSongs(songs, selectedPlaylistSongIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<Song[]> GetSongsFromClipboardAsync()
|
||||||
|
{
|
||||||
|
DataPackageView dataPackageView = Clipboard.GetContent();
|
||||||
|
string data = await dataPackageView.GetTextAsync(StandardDataFormats.Text);
|
||||||
|
|
||||||
|
return JsonSerializer.Deserialize<Song[]>(data) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenFileLocation()
|
||||||
|
{
|
||||||
|
if (SelectedPlaylistSongs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string argument = "/select, \"" + SelectedPlaylistSongs[0].Song.FileName + "\"";
|
||||||
|
Process.Start("explorer.exe", argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshTags()
|
||||||
|
{
|
||||||
|
//Playlist?.RefreshTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveMissingSongs()
|
||||||
|
{
|
||||||
|
Playlist?.RemoveMissingSongs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDuplicateSongs()
|
||||||
|
{
|
||||||
|
Playlist?.RemoveDuplicateSongs();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
8
Harmonia.WinUI/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.ViewModels;
|
||||||
|
|
||||||
|
public partial class ViewModelBase : ObservableObject
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
18
Harmonia.WinUI/ViewModels/ViewModelLocator.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.ViewModels;
|
||||||
|
|
||||||
|
public class ViewModelLocator
|
||||||
|
{
|
||||||
|
//public static MainViewModel MainViewModel
|
||||||
|
// => App.ServiceProvider.GetRequiredService<MainViewModel>();
|
||||||
|
|
||||||
|
public static PlayerViewModel PlayerViewModel
|
||||||
|
=> App.ServiceProvider.GetRequiredService<PlayerViewModel>();
|
||||||
|
|
||||||
|
public static PlayingSongViewModel PlayingSongViewModel
|
||||||
|
=> App.ServiceProvider.GetRequiredService<PlayingSongViewModel>();
|
||||||
|
|
||||||
|
public static PlaylistViewModel PlaylistViewModel
|
||||||
|
=> App.ServiceProvider.GetRequiredService<PlaylistViewModel>();
|
||||||
|
}
|
||||||
128
Harmonia.WinUI/Views/PlayerView.xaml
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Harmonia.WinUI.Views.PlayerView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:Harmonia.WinUI.Views"
|
||||||
|
xmlns:vm="using:Harmonia.WinUI.ViewModels"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
DataContext="{Binding Source={StaticResource Locator}, Path=PlayerViewModel}"
|
||||||
|
d:DataContext="{d:DesignInstance Type=vm:PlayerViewModel, IsDesignTimeCreatable=True}"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<SolidColorBrush x:Key="SongItemTitleBrush" Color="#dddddd"/>
|
||||||
|
<SolidColorBrush x:Key="SongItemSubtitleBrush" Color="#aaaaaa"/>
|
||||||
|
|
||||||
|
<Style x:Key="PlayerGrid" TargetType="Grid">
|
||||||
|
<Setter Property="Background" Value="#1a1a1a"/>
|
||||||
|
<Setter Property="Padding" Value="10"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="SongTitleTextBlock" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="SongImage" TargetType="Image">
|
||||||
|
<Setter Property="Width" Value="80"/>
|
||||||
|
<Setter Property="Height" Value="80"/>
|
||||||
|
</Style>
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid Style="{StaticResource PlayerGrid}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<!-- Position Slider -->
|
||||||
|
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Center" Margin="0 0 0 10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||||
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Border Grid.Column="0" Background="Transparent" Padding="0 8" Width="60" CornerRadius="4" VerticalAlignment="Center" Margin="0 0 6 0">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="#aaaaaa"
|
||||||
|
Text="{Binding Position, Converter={StaticResource SecondsToString}}"
|
||||||
|
FontSize="13"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Slider
|
||||||
|
Name="PositionSlider"
|
||||||
|
Loaded="PositionSlider_Loaded"
|
||||||
|
Grid.Column="1"
|
||||||
|
Value="{Binding CurrentPosition, Mode=TwoWay, UpdateSourceTrigger=Explicit}"
|
||||||
|
Minimum="0"
|
||||||
|
Maximum="{Binding MaxPosition, Mode=OneWay}"
|
||||||
|
IsEnabled="{Binding CanUpdatePosition, Mode=OneWay}"
|
||||||
|
ThumbToolTipValueConverter="{StaticResource SecondsToString}"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center" />
|
||||||
|
|
||||||
|
<Border Grid.Column="2" Background="Transparent" Padding="0 8" Width="60" CornerRadius="4" VerticalAlignment="Center" Margin="6 0 0 0">
|
||||||
|
<TextBlock
|
||||||
|
Foreground="#aaaaaa"
|
||||||
|
Text="{Binding MaxPosition, Converter={StaticResource SecondsToString}}"
|
||||||
|
FontSize="13"
|
||||||
|
VerticalAlignment="Center">
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Song Info -->
|
||||||
|
<Grid Grid.Row="1" Grid.Column="0" Name="PlayingSongGrid">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Grid Margin="0 0 10 0">
|
||||||
|
<Image Source="{Binding SongImageSource, Mode=OneWay}" Style="{StaticResource SongImage}"></Image>
|
||||||
|
<Canvas Background="#19000000"></Canvas>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Foreground="#dddddd" FontWeight="SemiBold" FontSize="16" Text="{Binding Song, Converter={StaticResource SongTitle}}"></TextBlock>
|
||||||
|
<TextBlock Foreground="#aaaaaa" FontSize="14" Text="{Binding Song.Artists, Converter={StaticResource ArtistsToString}}"></TextBlock>
|
||||||
|
<TextBlock Foreground="#aaaaaa" FontSize="14" Text="{Binding Song.Album}"></TextBlock>
|
||||||
|
<TextBlock Foreground="#777" FontSize="12" Visibility="{Binding Song, Converter={StaticResource NullVisibility}}">
|
||||||
|
<Run Text="{Binding Song.FileType}"></Run><Run Text=" - "></Run><Run Text="{Binding Song.BitRate}"></Run><Run Text=" kbps - "></Run><Run Text="{Binding Song.SampleRate}"></Run><Run Text=" Hz"></Run>
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Playback Actions -->
|
||||||
|
<Grid Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||||
|
<Button Style="{StaticResource FlatButton}" Command="{Binding PreviousSongCommand}">
|
||||||
|
<Path Style="{StaticResource FlatButtonPath}" Data="{StaticResource SkipStartFillIcon}"></Path>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource FlatButton}" Command="{Binding StopSongCommand}">
|
||||||
|
<Path Style="{StaticResource FlatButtonPath}" Data="{StaticResource StopFillIcon}"></Path>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource FlatButton}" Command="{Binding PlaySongCommand}">
|
||||||
|
<Path Style="{StaticResource FlatButtonPath-Large}" Data="{StaticResource PlayFillIcon}"></Path>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource FlatButton}" Command="{Binding PauseSongCommand}">
|
||||||
|
<Path Style="{StaticResource FlatButtonPath}" Data="{StaticResource PauseFillIcon}"></Path>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource FlatButton}" Command="{Binding NextSongCommand}">
|
||||||
|
<Path Style="{StaticResource FlatButtonPath}" Data="{StaticResource SkipEndFillIcon}"></Path>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
68
Harmonia.WinUI/Views/PlayerView.xaml.cs
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
using Harmonia.WinUI.ViewModels;
|
||||||
|
using Microsoft.UI.Input;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Input;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Views;
|
||||||
|
|
||||||
|
public sealed partial class PlayerView : UserControl
|
||||||
|
{
|
||||||
|
private readonly PlayerViewModel _viewModel;
|
||||||
|
|
||||||
|
public PlayerView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_viewModel = (PlayerViewModel)DataContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PositionSlider_Loaded(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Slider slider)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PointerEventHandler pointerPressedHandler = new(OnSliderPointerPressed);
|
||||||
|
slider.AddHandler(PointerPressedEvent, pointerPressedHandler, true);
|
||||||
|
|
||||||
|
PointerEventHandler pointerReleasedHandler = new(OnSliderPointerReleased);
|
||||||
|
slider.AddHandler(PointerReleasedEvent, pointerReleasedHandler, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSliderPointerPressed(object? sender, PointerRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Slider slider)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PointerPointProperties pointerProperties = e.GetCurrentPoint(slider).Properties;
|
||||||
|
|
||||||
|
if (pointerProperties.IsLeftButtonPressed == false)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_viewModel.IsPositionChangeInProgress = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSliderPointerReleased(object? sender, PointerRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Slider slider)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_viewModel.CurrentPosition = slider.Value;
|
||||||
|
_viewModel.IsPositionChangeInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void VolumeSlider_PointerWheelChanged(object? sender, PointerRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Slider slider)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var pointerProperties = e.GetCurrentPoint(slider).Properties;
|
||||||
|
var mouseWheelDelta = pointerProperties.MouseWheelDelta;
|
||||||
|
|
||||||
|
if (mouseWheelDelta == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
double delta = mouseWheelDelta > 0 ? .02 : -.02;
|
||||||
|
|
||||||
|
_viewModel.Volume += delta;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Harmonia.WinUI/Views/PlayingSongView.xaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Harmonia.WinUI.Views.PlayingSongView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:Harmonia.WinUI.Views"
|
||||||
|
xmlns:vm="using:Harmonia.WinUI.ViewModels"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
DataContext="{Binding Source={StaticResource Locator}, Path=PlayingSongViewModel}"
|
||||||
|
d:DataContext="{d:DesignInstance Type=vm:PlayingSongViewModel, IsDesignTimeCreatable=True}"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<Grid HorizontalAlignment="Center" VerticalAlignment="Center" Margin="20">
|
||||||
|
<Image Source="{Binding SongImageSource, Mode=OneWay}" Stretch="Uniform">
|
||||||
|
</Image>
|
||||||
|
<Canvas Background="#19000000"></Canvas>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
15
Harmonia.WinUI/Views/PlayingSongView.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using Harmonia.WinUI.ViewModels;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Views;
|
||||||
|
|
||||||
|
public sealed partial class PlayingSongView : UserControl
|
||||||
|
{
|
||||||
|
private readonly PlayingSongViewModel _viewModel;
|
||||||
|
|
||||||
|
public PlayingSongView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_viewModel = (PlayingSongViewModel)DataContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
254
Harmonia.WinUI/Views/PlaylistView.xaml
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
<UserControl
|
||||||
|
x:Class="Harmonia.WinUI.Views.PlaylistView"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:local="using:Harmonia.WinUI.Views"
|
||||||
|
xmlns:vm="using:Harmonia.WinUI.ViewModels"
|
||||||
|
xmlns:playlists="using:Harmonia.Core.Playlists"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
DataContext="{Binding Source={StaticResource Locator}, Path=PlaylistViewModel}"
|
||||||
|
d:DataContext="{d:DesignInstance Type=vm:PlaylistViewModel, IsDesignTimeCreatable=True}"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<!--<SolidColorBrush x:Key="PlaylistBackground" Color="#393a45"/>-->
|
||||||
|
<SolidColorBrush x:Key="PlaylistBackground" Color="#292a35"/>
|
||||||
|
<SolidColorBrush x:Key="PlaylistItemHighlightColor" Color="#494a55"/>
|
||||||
|
<!--<SolidColorBrush x:Key="PlaylistBackground" Color="#222"/>-->
|
||||||
|
<SolidColorBrush x:Key="SongItemTitleBrush" Color="#dddddd"/>
|
||||||
|
<SolidColorBrush x:Key="SongItemTitleBrushHighlighted" Color="#A2D2F6"/>
|
||||||
|
<SolidColorBrush x:Key="SongItemSubtitleBrush" Color="#aaaaaa"/>
|
||||||
|
<!--<SolidColorBrush x:Key="SongItemSubtitleBrushHighlighted" Color="#76b9ed"/>-->
|
||||||
|
<SolidColorBrush x:Key="SongItemSubtitleBrushHighlighted" Color="#ddA2D2F6"/>
|
||||||
|
|
||||||
|
<SolidColorBrush x:Key="SongItemFooterBrush" Color="#888"/>
|
||||||
|
<SolidColorBrush x:Key="SongItemFooterBrushHighlighted" Color="#aaA2D2F6"/>
|
||||||
|
|
||||||
|
<!-- Image Border -->
|
||||||
|
<Style x:Key="PlaylistSongImageBorder" TargetType="Border">
|
||||||
|
<Setter Property="Width" Value="60"/> <!-- as 75 -->
|
||||||
|
<Setter Property="Height" Value="60"/> <!-- as 75 -->
|
||||||
|
<Setter Property="CornerRadius" Value="8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Song Item Title Text Block -->
|
||||||
|
<Style x:Key="SongTitleTextBlock" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
|
||||||
|
<Setter Property="FontSize" Value="15"/>
|
||||||
|
<Setter Property="FontWeight" Value="Medium"/>
|
||||||
|
<Setter Property="LineStackingStrategy" Value="BlockLineHeight"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||||
|
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||||
|
<Setter Property="LineHeight" Value="0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="PlayingSongTitleTextBlock" TargetType="TextBlock" BasedOn="{StaticResource SongTitleTextBlock}">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource AccentTextFillColorPrimaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Song Item Subtitle Text Block -->
|
||||||
|
<Style x:Key="SongSubtitleTextBlock" TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource SongItemSubtitleBrush}"/>
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<Setter Property="LineStackingStrategy" Value="BlockLineHeight"/>
|
||||||
|
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||||
|
<Setter Property="LineHeight" Value="0"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="PlayingSongSubtitleTextBlock" TargetType="TextBlock" BasedOn="{StaticResource SongSubtitleTextBlock}">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource AccentTextFillColorSecondaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<DataTemplate x:Key="SongTemplate" x:DataType="playlists:PlaylistSong">
|
||||||
|
<!-- Background was formerly transparent -->
|
||||||
|
<Border x:Name="PlaylistSongListViewItem" DataContextChanged="PlaylistSongListViewItem_DataContextChanged" DoubleTapped="PlaylistListViewItem_DoubleTapped" RightTapped="PlaylistListViewItem_RightTapped" Background="Transparent">
|
||||||
|
<Grid Padding="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Border Grid.Column="0" Style="{StaticResource PlaylistSongImageBorder}">
|
||||||
|
<Grid>
|
||||||
|
<Image Loaded="Image_Loaded" Unloaded="Image_Unloaded"></Image>
|
||||||
|
<Canvas Background="#19000000"></Canvas>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<Grid Grid.Column="1" Margin="10 0 0 0" VerticalAlignment="Center">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="60"></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition></RowDefinition>
|
||||||
|
<RowDefinition></RowDefinition>
|
||||||
|
<RowDefinition></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBlock Grid.Column="0" Grid.Row="0" x:Name="SongTitleTextBlock" Text="{x:Bind Song, Converter={StaticResource SongTitle}, Mode=OneWay}" Style="{StaticResource SongTitleTextBlock}" >
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Grid.Row="1" x:Name="SongArtistsTextBlock" Text="{Binding Song.Artists, Converter={StaticResource ArtistsToString}}" Style="{StaticResource SongSubtitleTextBlock}">
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Grid.Row="2" x:Name="SongAlbumTextBlock" Text="{Binding Song.Album}" Style="{StaticResource SongSubtitleTextBlock}">
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Grid.Row="0" x:Name="SongLengthTextBlock" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" TextAlignment="Right" HorizontalTextAlignment="Right" Foreground="{StaticResource SongItemSubtitleBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="13" Text="{Binding Song.Length.TotalSeconds, Converter={StaticResource SecondsToString}}"></TextBlock>
|
||||||
|
<TextBlock Grid.Row="1" x:Name="SongFormatTextBlock" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{StaticResource SongItemFooterBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="12" Text="{Binding Song.FileType}"></TextBlock>
|
||||||
|
<TextBlock Grid.Row="2" x:Name="SongBitRateTextBlock" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{StaticResource SongItemFooterBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="12">
|
||||||
|
<Run Text="{Binding Song.BitRate}"></Run><Run Text=" kbps"></Run>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</UserControl.Resources>
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid Grid.Row="0" Margin="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0" Text="{Binding Filter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Center" Margin="10 0" HorizontalAlignment="Stretch" PlaceholderText="Filter"></TextBox>
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
|
||||||
|
<Button Style="{StaticResource FlatRoundButton}" Foreground="#7FD184">
|
||||||
|
<Button.Content>
|
||||||
|
<Path Style="{StaticResource FlatButtonPath}" Fill="#7FD184" Data="{StaticResource AddIcon}"></Path>
|
||||||
|
</Button.Content>
|
||||||
|
<Button.Flyout>
|
||||||
|
<MenuFlyout Placement="Bottom" ShouldConstrainToRootBounds="False" SystemBackdrop="{StaticResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||||
|
<MenuFlyout.MenuFlyoutPresenterStyle>
|
||||||
|
<Style TargetType="MenuFlyoutPresenter">
|
||||||
|
<Setter Property="Padding" Value="10"></Setter>
|
||||||
|
<Setter Property="Background" Value="Transparent"></Setter>
|
||||||
|
</Style>
|
||||||
|
</MenuFlyout.MenuFlyoutPresenterStyle>
|
||||||
|
<MenuFlyoutItem Text="Add Files..." Command="{Binding AddFilesCommand}">
|
||||||
|
<MenuFlyoutItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource AddFileIcon}"></PathIcon>
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
<MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="Insert"/>
|
||||||
|
</MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutItem Text="Add Folder..." Command="{Binding AddFolderCommand}">
|
||||||
|
<MenuFlyoutItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource AddFolderIcon}"></PathIcon>
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
<MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="Insert" Modifiers="Control" />
|
||||||
|
</MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
</MenuFlyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource FlatRoundButton}">
|
||||||
|
<Button.Content>
|
||||||
|
<Path Style="{StaticResource FlatButtonPath}" Data="{StaticResource MoreIcon}"></Path>
|
||||||
|
</Button.Content>
|
||||||
|
<Button.Flyout>
|
||||||
|
<MenuFlyout Placement="Bottom" ShouldConstrainToRootBounds="False" SystemBackdrop="{StaticResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||||
|
<MenuFlyout.MenuFlyoutPresenterStyle>
|
||||||
|
<Style TargetType="MenuFlyoutPresenter">
|
||||||
|
<Setter Property="Padding" Value="10"></Setter>
|
||||||
|
<Setter Property="Background" Value="Transparent"></Setter>
|
||||||
|
</Style>
|
||||||
|
</MenuFlyout.MenuFlyoutPresenterStyle>
|
||||||
|
<MenuFlyoutItem Text="Refresh Tags" Command="{Binding RefreshTagsCommand}">
|
||||||
|
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutItem Text="Remove Duplicates" Command="{Binding RemoveDuplicateSongsCommand}">
|
||||||
|
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutItem Text="Remove Missing" Command="{Binding RemoveMissingSongsCommand}">
|
||||||
|
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutItem Text="Lock Playlist">
|
||||||
|
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||||
|
<MenuFlyoutItem Text="Remove Playlist" Foreground="#ff99a4">
|
||||||
|
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||||
|
<MenuFlyoutItem Text="Settings">
|
||||||
|
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
</MenuFlyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<ListView
|
||||||
|
Grid.Row="1"
|
||||||
|
Name="PlaylistListView"
|
||||||
|
ItemsSource="{Binding FilteredPlaylistSongs}"
|
||||||
|
ItemTemplate="{StaticResource SongTemplate}"
|
||||||
|
CanReorderItems="True"
|
||||||
|
CanDragItems="True"
|
||||||
|
DragItemsStarting="PlaylistListView_DragItemsStarting"
|
||||||
|
DragItemsCompleted="PlaylistListView_DragItemsCompleted"
|
||||||
|
AllowDrop="True"
|
||||||
|
SelectionMode="Extended"
|
||||||
|
SelectionChanged="PlaylistListView_SelectionChanged">
|
||||||
|
<ListView.ContextFlyout>
|
||||||
|
<MenuFlyout x:Name="PlaylistListViewMenuFlyout" Opening="MenuFlyout_Opening" ShouldConstrainToRootBounds="False" SystemBackdrop="{StaticResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||||
|
<MenuFlyout.MenuFlyoutPresenterStyle>
|
||||||
|
<Style TargetType="MenuFlyoutPresenter">
|
||||||
|
<Setter Property="Padding" Value="10"></Setter>
|
||||||
|
<Setter Property="Background" Value="Transparent"></Setter>
|
||||||
|
</Style>
|
||||||
|
</MenuFlyout.MenuFlyoutPresenterStyle>
|
||||||
|
<MenuFlyoutItem Text="Play" FontWeight="SemiBold" Command="{Binding PlaySongCommand}">
|
||||||
|
<MenuFlyoutItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource PlayIcon}"></PathIcon>
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
<MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="Enter"/>
|
||||||
|
</MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||||
|
<MenuFlyoutItem Text="Remove" Command="{Binding RemoveSongsCommand}" Foreground="#ff99a4">
|
||||||
|
<MenuFlyoutItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource DeleteIcon}" Foreground="#ff99a4"></PathIcon>
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
<MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="Delete"/>
|
||||||
|
</MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||||
|
<MenuFlyoutItem Text="Cut" Command="{Binding CutSongsCommand}">
|
||||||
|
<MenuFlyoutItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource CutIcon}"></PathIcon>
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
<MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="X" Modifiers="Control" />
|
||||||
|
</MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutItem Text="Copy" Command="{Binding CopySongsCommand}">
|
||||||
|
<MenuFlyoutItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource CopyIcon}"></PathIcon>
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
<MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="C" Modifiers="Control" />
|
||||||
|
</MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutItem Text="Paste" Command="{Binding PasteSongsCommand}">
|
||||||
|
<MenuFlyoutItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource PasteIcon}"></PathIcon>
|
||||||
|
</MenuFlyoutItem.Icon>
|
||||||
|
<MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="V" Modifiers="Control" />
|
||||||
|
</MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||||
|
<MenuFlyoutItem Text="Open File Location" Command="{Binding OpenFileLocationCommand}">
|
||||||
|
<MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
<KeyboardAccelerator Key="O" Modifiers="Menu" />
|
||||||
|
</MenuFlyoutItem.KeyboardAccelerators>
|
||||||
|
</MenuFlyoutItem>
|
||||||
|
</MenuFlyout>
|
||||||
|
</ListView.ContextFlyout>
|
||||||
|
</ListView>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
264
Harmonia.WinUI/Views/PlaylistView.xaml.cs
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
using CommunityToolkit.WinUI;
|
||||||
|
using Harmonia.Core.Imaging;
|
||||||
|
using Harmonia.Core.Playlists;
|
||||||
|
using Harmonia.WinUI.ViewModels;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using Microsoft.UI.Xaml.Input;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Windows.UI.Popups;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Views;
|
||||||
|
|
||||||
|
public sealed partial class PlaylistView : UserControl
|
||||||
|
{
|
||||||
|
private readonly PlaylistViewModel _viewModel;
|
||||||
|
|
||||||
|
public PlaylistView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
_viewModel = (PlaylistViewModel)DataContext;
|
||||||
|
_viewModel.PropertyChanging += OnViewModelPropertyChanging;
|
||||||
|
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||||
|
_viewModel.PlayingSongChangedAutomatically += OnPlayingSongChangedAutomatically;
|
||||||
|
|
||||||
|
foreach (MenuFlyoutItemBase item in PlaylistListViewMenuFlyout.Items)
|
||||||
|
{
|
||||||
|
item.DataContextChanged += Item_DataContextChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnViewModelPropertyChanging(object? sender, PropertyChangingEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(_viewModel.PlayingSong))
|
||||||
|
{
|
||||||
|
ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(_viewModel.PlayingSong);
|
||||||
|
|
||||||
|
if (listViewItem != null)
|
||||||
|
{
|
||||||
|
UpdateListViewItemStyle(listViewItem, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(_viewModel.PlayingSong))
|
||||||
|
{
|
||||||
|
ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(_viewModel.PlayingSong);
|
||||||
|
|
||||||
|
UpdateListViewItemStyle(listViewItem, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateListViewItemStyle(FrameworkElement listViewItem, bool isPlaying)
|
||||||
|
{
|
||||||
|
if (listViewItem == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
FrameworkElement? frameworkElement;
|
||||||
|
|
||||||
|
if (listViewItem.Name == "PlaylistSongListViewItem")
|
||||||
|
{
|
||||||
|
frameworkElement = listViewItem;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (listViewItem.FindDescendant("PlaylistSongListViewItem") is not FrameworkElement frameworkElement2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
frameworkElement = frameworkElement2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frameworkElement == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string songItemTitleBrushName = isPlaying ? "PlayingSongTitleTextBlock" : "SongTitleTextBlock";
|
||||||
|
string songItemSubtitleBrushName = isPlaying ? "PlayingSongSubtitleTextBlock" : "SongSubtitleTextBlock";
|
||||||
|
|
||||||
|
UpdateElementStyle(frameworkElement, "SongTitleTextBlock", songItemTitleBrushName);
|
||||||
|
UpdateElementStyle(frameworkElement, "SongArtistsTextBlock", songItemSubtitleBrushName);
|
||||||
|
UpdateElementStyle(frameworkElement, "SongAlbumTextBlock", songItemSubtitleBrushName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateElementStyle(FrameworkElement dependencyObject, string elementName, string resourceName)
|
||||||
|
{
|
||||||
|
if (dependencyObject.FindDescendant(elementName) is not FrameworkElement frameworkElement)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Resources.TryGetValue(resourceName, out object? resource);
|
||||||
|
|
||||||
|
if (resource == null)
|
||||||
|
Application.Current.Resources.TryGetValue(resourceName, out resource);
|
||||||
|
|
||||||
|
if (resource is not Style style)
|
||||||
|
return;
|
||||||
|
|
||||||
|
frameworkElement.Style = style;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayingSongChangedAutomatically(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
BringPlayingSongIntoView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BringPlayingSongIntoView()
|
||||||
|
{
|
||||||
|
PlaylistSong? playingSong = _viewModel.PlayingSong;
|
||||||
|
|
||||||
|
if (playingSong == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(playingSong);
|
||||||
|
|
||||||
|
if (listViewItem != null)
|
||||||
|
{
|
||||||
|
listViewItem.UpdateLayout();
|
||||||
|
listViewItem.StartBringIntoView(new BringIntoViewOptions() { VerticalAlignmentRatio = .5, AnimationDesired = true });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PlaylistListView.UpdateLayout();
|
||||||
|
PlaylistListView.ScrollIntoView(playingSong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Item_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is not MenuFlyoutItemBase item)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.NewValue == _viewModel)
|
||||||
|
return;
|
||||||
|
|
||||||
|
item.DataContext = _viewModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Image_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Image image)
|
||||||
|
return;
|
||||||
|
|
||||||
|
image.DataContextChanged += Image_DataContextChanged;
|
||||||
|
|
||||||
|
if (image.DataContext is not PlaylistSong playlistSong)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//Task.Run(async () => await UpdateImage(image, playlistSong));
|
||||||
|
UpdateImage(image, playlistSong);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Image_Unloaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not Image image)
|
||||||
|
return;
|
||||||
|
|
||||||
|
image.DataContextChanged -= Image_DataContextChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Image_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (sender is not Image image)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (args.NewValue is not PlaylistSong playlistSong)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//Task.Run(async () => await UpdateImage(image, playlistSong));
|
||||||
|
UpdateImage(image, playlistSong);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateImage(Image image, PlaylistSong playlistSong)
|
||||||
|
{
|
||||||
|
int hashCode = image.GetHashCode();
|
||||||
|
//BitmapImage? bitmapImage = await _viewModel.GetBitmapImageAsync(hashCode, playlistSong);
|
||||||
|
//SongPictureInfo? songPictureInfo = await _viewModel.GetSongPictureInfoAsync(hashCode, playlistSong);
|
||||||
|
|
||||||
|
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
|
||||||
|
{
|
||||||
|
BitmapImage? bitmapImage = await _viewModel.GetBitmapImageAsync(hashCode, playlistSong);
|
||||||
|
image.Source = bitmapImage;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlaylistSongListViewItem_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.NewValue is not PlaylistSong playlistSong)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool isPlaying = playlistSong == _viewModel.PlayingSong;
|
||||||
|
|
||||||
|
UpdateListViewItemStyle(sender, isPlaying);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void PlaylistListViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not FrameworkElement element)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (element == null || element.DataContext is not PlaylistSong playlistSong)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await _viewModel.PlaySongAsync(playlistSong);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlaylistListViewItem_RightTapped(object sender, RightTappedRoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not FrameworkElement element)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (element == null || element.DataContext is not PlaylistSong playlistSong)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (PlaylistListView.SelectedItems.Contains(playlistSong))
|
||||||
|
return;
|
||||||
|
|
||||||
|
int index = PlaylistListView.Items.IndexOf(playlistSong);
|
||||||
|
|
||||||
|
PlaylistListView.DeselectAll();
|
||||||
|
PlaylistListView.SelectRange(new ItemIndexRange(index, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlaylistListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not ListView listView)
|
||||||
|
return;
|
||||||
|
|
||||||
|
PlaylistSong[] selectedPlaylistSongs = [.. listView.SelectedItems.Cast<PlaylistSong>()];
|
||||||
|
|
||||||
|
_viewModel.SelectedPlaylistSongs = [.. selectedPlaylistSongs];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MenuFlyout_Opening(object sender, object e)
|
||||||
|
{
|
||||||
|
if (sender is not MenuFlyout menuFlyout)
|
||||||
|
return;
|
||||||
|
|
||||||
|
foreach (MenuFlyoutItemBase item in menuFlyout.Items)
|
||||||
|
{
|
||||||
|
if (item.DataContext != _viewModel)
|
||||||
|
{
|
||||||
|
item.DataContext = _viewModel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlaylistListView_DragItemsStarting(object sender, DragItemsStartingEventArgs e)
|
||||||
|
{
|
||||||
|
_viewModel.IsUserUpdating = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PlaylistListView_DragItemsCompleted(ListViewBase sender, DragItemsCompletedEventArgs args)
|
||||||
|
{
|
||||||
|
_viewModel.IsUserUpdating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
Harmonia.WinUI/app.manifest
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="Harmonia.WinUI.app"/>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
|
||||||
|
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
|
||||||
|
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
|
||||||
|
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
</assembly>
|
||||||
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.UI", "Harmonia.UI\
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.UI.Desktop", "Harmonia.UI.Desktop\Harmonia.UI.Desktop.csproj", "{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.UI.Desktop", "Harmonia.UI.Desktop\Harmonia.UI.Desktop.csproj", "{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.WinUI", "Harmonia.WinUI\Harmonia.WinUI.csproj", "{5972FBF1-E81A-4A80-9153-13F45FA0E2AA}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -33,6 +35,12 @@ Global
|
|||||||
{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}.Release|Any CPU.Build.0 = Release|Any CPU
|
{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{5972FBF1-E81A-4A80-9153-13F45FA0E2AA}.Debug|Any CPU.ActiveCfg = Debug|x64
|
||||||
|
{5972FBF1-E81A-4A80-9153-13F45FA0E2AA}.Debug|Any CPU.Build.0 = Debug|x64
|
||||||
|
{5972FBF1-E81A-4A80-9153-13F45FA0E2AA}.Debug|Any CPU.Deploy.0 = Debug|x64
|
||||||
|
{5972FBF1-E81A-4A80-9153-13F45FA0E2AA}.Release|Any CPU.ActiveCfg = Release|x64
|
||||||
|
{5972FBF1-E81A-4A80-9153-13F45FA0E2AA}.Release|Any CPU.Build.0 = Release|x64
|
||||||
|
{5972FBF1-E81A-4A80-9153-13F45FA0E2AA}.Release|Any CPU.Deploy.0 = Release|x64
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||