Compare commits

..

10 Commits

63 changed files with 3068 additions and 21 deletions

View File

@@ -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));
bool throttlerAcquired = false;
bool lockAcquired = false;
try
{
await _throttler.WaitAsync(cancellationToken);
throttlerAcquired = true;
if (cancellationToken.IsCancellationRequested)
return default;
await lockObject.WaitAsync(cancellationToken);
lockAcquired = true;
if (cancellationToken.IsCancellationRequested)
return default;
return TryGetValue(actualKey) ?? await FetchAsync2(key, cancellationToken);
}
catch (OperationCanceledException)
{
return default;
}
finally
{
if (lockAcquired)
lockObject.Release();
_locks.TryRemove(lockObject, out _);
if (throttlerAcquired)
_throttler.Release();
}
//return TryGetValue(actualKey) ?? Refresh(key);
}
private async Task<TValue?> FetchAsync2(TKey key, CancellationToken cancellationToken)

View File

@@ -1,4 +1,5 @@
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace Harmonia.Core.Caching;
@@ -27,10 +28,16 @@ public abstract class MemoryCache<TKey, TValue> : Cache<TKey, TValue> where TKey
var cacheEntryOptions = new MemoryCacheEntryOptions()
.SetSize(entrySize)
.SetSlidingExpiration(SlidingExpiration);
.SetSlidingExpiration(SlidingExpiration)
.RegisterPostEvictionCallback(PostEvictionCallback);
_memoryCache.Set(key, entry, cacheEntryOptions);
}
protected virtual void PostEvictionCallback(object? cacheKey, object? cacheValue, EvictionReason evictionReason, object? state)
{
}
protected abstract long GetEntrySize(TValue entry);
}

View File

@@ -7,12 +7,12 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ManagedBass" Version="3.1.1" />
<PackageReference Include="ManagedBass.Flac" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.3" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.3" />
<PackageReference Include="ManagedBass" Version="4.0.2" />
<PackageReference Include="ManagedBass.Flac" Version="4.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.9" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.9" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />
</ItemGroup>

View File

@@ -23,4 +23,26 @@ public class Song
public string? FileDirectory => Directory.GetParent(FileName)?.Name;
public string? FileType => Path.GetExtension(FileName)?.Replace(".", "").ToUpper();
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;
}
}

View File

@@ -172,6 +172,7 @@ public class Playlist
foreach (PlaylistSong playlistSong in playlistSongs)
{
//playlistSong.Song = song;
//playlistSong.Song.Update(song);
}
}
}

View File

@@ -16,14 +16,14 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</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="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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="xunit.v3" Version="2.0.0" />
<PackageReference Include="xunit.v3" Version="3.1.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.7" />
</ItemGroup>
<ItemGroup>

View File

@@ -11,15 +11,15 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.5" />
<PackageReference Include="Avalonia" Version="11.3.7" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.7" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.7" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<!--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 Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
<PackageReference Include="Semi.Avalonia" Version="11.2.1.5" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.7" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.9" />
<PackageReference Include="Semi.Avalonia" Version="11.3.7" />
</ItemGroup>
<ItemGroup>

20
Harmonia.WinUI/App.xaml Normal file
View 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>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View 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)
{
}
}

View 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>
{
}

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

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

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

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

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

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

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

View 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
}

View 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>

View 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>

View File

@@ -0,0 +1,11 @@
using Microsoft.UI.Xaml;
namespace Harmonia.WinUI;
public sealed partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,10 @@
namespace Harmonia.WinUI.Models;
public enum VolumeState
{
Muted,
Off,
Low,
Medium,
High
}

View 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>

View File

@@ -0,0 +1,10 @@
{
"profiles": {
"Harmonia.WinUI (Package)": {
"commandName": "MsixPackage"
},
"Harmonia.WinUI (Unpackaged)": {
"commandName": "Project"
}
}
}

View 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>

View 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>

View 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>

View 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>

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

View 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; }
}

View 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; }
}

View File

@@ -0,0 +1,6 @@
namespace Harmonia.WinUI.Session;
public partial class NavigationState
{
public string? CurrentPage { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Harmonia.WinUI.Session;
public partial class PlaylistState
{
public string? OpenPlaylistUID { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Harmonia.WinUI.Session;
public partial class WindowState
{
public bool IsMaximized { get; set; }
}

View 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; } = [];
}

View 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; }
}

View File

@@ -0,0 +1,6 @@
namespace Harmonia.WinUI.Storage;
public class FolderPickerOptions
{
public StoragePickerViewMode ViewMode { get; set; }
}

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

View File

@@ -0,0 +1,7 @@
namespace Harmonia.WinUI.Storage;
public enum StoragePickerViewMode
{
List,
Thumbnail
}

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

View 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
}

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

View 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
}

View File

@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Harmonia.WinUI.ViewModels;
public partial class ViewModelBase : ObservableObject
{
}

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

View 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>

View 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;
}
}

View 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>

View 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;
}
}

View 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>

View 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;
}
}

View 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>

View File

@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.UI", "Harmonia.UI\
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.UI.Desktop", "Harmonia.UI.Desktop\Harmonia.UI.Desktop.csproj", "{61B86E6A-A7E9-4946-E488-40A7E12ED4FC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harmonia.WinUI", "Harmonia.WinUI\Harmonia.WinUI.csproj", "{5972FBF1-E81A-4A80-9153-13F45FA0E2AA}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE