Added ContextMenu/Flyout animations. Added platform services. Use bitmap cache for all views.
This commit is contained in:
@@ -68,7 +68,7 @@ public class AudioImageExtractor : IAudioImageExtractor
|
|||||||
if (string.IsNullOrWhiteSpace(imagePath))
|
if (string.IsNullOrWhiteSpace(imagePath))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return await SongPictureInfo.FromFileAsync(path, cancellationToken);
|
return await SongPictureInfo.FromFileAsync(imagePath, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ public class Playlist
|
|||||||
if (IsLocked)
|
if (IsLocked)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
if (playlistSongs.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
int insertIndex = index ?? Songs.Count;
|
int insertIndex = index ?? Songs.Count;
|
||||||
|
|
||||||
Songs.InsertRange(insertIndex, playlistSongs);
|
Songs.InsertRange(insertIndex, playlistSongs);
|
||||||
@@ -158,4 +161,54 @@ public class Playlist
|
|||||||
|
|
||||||
PlaylistUpdated?.Invoke(this, eventArgs);
|
PlaylistUpdated?.Invoke(this, eventArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ImportTags(Song[] songs)
|
||||||
|
{
|
||||||
|
foreach (Song song in songs)
|
||||||
|
{
|
||||||
|
PlaylistSong[] playlistSongs = [.. Songs.Where(playlistSong =>
|
||||||
|
string.Equals(playlistSong.Song.FileName, song.FileName, StringComparison.OrdinalIgnoreCase))];
|
||||||
|
|
||||||
|
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||||
|
{
|
||||||
|
//playlistSong.Song = song;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveMissingSongs()
|
||||||
|
{
|
||||||
|
PlaylistSong[] missingSongs = [.. Songs.Where(playlistSong =>
|
||||||
|
File.Exists(playlistSong.Song.FileName) == false)];
|
||||||
|
|
||||||
|
if (missingSongs.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RemoveSongs(missingSongs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveDuplicateSongs()
|
||||||
|
{
|
||||||
|
List<PlaylistSong> songsToRemove = [];
|
||||||
|
|
||||||
|
string[] fileNames = Songs
|
||||||
|
.GroupBy(x => x.Song.FileName)
|
||||||
|
.Where(x => x.Count() > 1)
|
||||||
|
.Select(x => x.Key)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
foreach (string fileName in fileNames)
|
||||||
|
{
|
||||||
|
List<PlaylistSong> songs = Songs.Where(x =>
|
||||||
|
string.Equals(x.Song.FileName, fileName, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
|
||||||
|
for (int i = songs.Count - 1; i > 0; i--)
|
||||||
|
songsToRemove.Add(songs[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songsToRemove.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
RemoveSongs(songsToRemove.ToArray());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,9 @@ public class PlaylistRepository : JsonFileRepository<Playlist>, IPlaylistReposit
|
|||||||
{
|
{
|
||||||
playlist.PlaylistUpdated += OnPlaylistUpdated;
|
playlist.PlaylistUpdated += OnPlaylistUpdated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (playlists.Count == 0)
|
||||||
|
AddPlaylist();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
|
private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
|
||||||
@@ -22,7 +25,6 @@ public class PlaylistRepository : JsonFileRepository<Playlist>, IPlaylistReposit
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
Save(playlist);
|
Save(playlist);
|
||||||
//PlaylistUpdated?.Invoke(sender, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Playlist? GetPlaylist(PlaylistSong playlistSong)
|
public Playlist? GetPlaylist(PlaylistSong playlistSong)
|
||||||
@@ -45,7 +47,6 @@ public class PlaylistRepository : JsonFileRepository<Playlist>, IPlaylistReposit
|
|||||||
}
|
}
|
||||||
|
|
||||||
public event EventHandler<PlaylistAddedEventArgs>? PlaylistAdded;
|
public event EventHandler<PlaylistAddedEventArgs>? PlaylistAdded;
|
||||||
//public event EventHandler<PlaylistUpdatedEventArgs>? PlaylistUpdated;
|
|
||||||
public event EventHandler<PlaylistRemovedEventArgs>? PlaylistRemoved;
|
public event EventHandler<PlaylistRemovedEventArgs>? PlaylistRemoved;
|
||||||
|
|
||||||
public void AddPlaylist()
|
public void AddPlaylist()
|
||||||
|
|||||||
58
Harmonia.UI/Animations/Animations.cs
Normal file
58
Harmonia.UI/Animations/Animations.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Animation;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Styling;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Animations
|
||||||
|
{
|
||||||
|
public static class Animations
|
||||||
|
{
|
||||||
|
public static Animation SlideIn(Point start, Point end, long duration = 200)
|
||||||
|
{
|
||||||
|
return new()
|
||||||
|
{
|
||||||
|
Duration = TimeSpan.FromMilliseconds(duration),
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new KeyFrame
|
||||||
|
{
|
||||||
|
Cue = new Cue(0f),
|
||||||
|
Setters =
|
||||||
|
{
|
||||||
|
new Setter(Visual.OpacityProperty, 0.0),
|
||||||
|
new Setter(TranslateTransform.XProperty, start.X),
|
||||||
|
new Setter(TranslateTransform.YProperty, start.Y)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new KeyFrame
|
||||||
|
{
|
||||||
|
Cue = new Cue(1f),
|
||||||
|
Setters =
|
||||||
|
{
|
||||||
|
new Setter(Visual.OpacityProperty, 1.0),
|
||||||
|
new Setter(TranslateTransform.XProperty, end.X),
|
||||||
|
new Setter(TranslateTransform.YProperty, end.Y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Animation SlideFromRight(double offSet, long duration = 200)
|
||||||
|
{
|
||||||
|
Point start = new(-offSet, 0);
|
||||||
|
Point end = new(0, 0);
|
||||||
|
|
||||||
|
return SlideIn(start, end, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Animation SlideToRight(double offSet, long duration = 200)
|
||||||
|
{
|
||||||
|
Point start = new(0, 0);
|
||||||
|
Point end = new(offSet, 0);
|
||||||
|
|
||||||
|
return SlideIn(start, end, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,13 +12,8 @@
|
|||||||
<!--<FluentTheme />-->
|
<!--<FluentTheme />-->
|
||||||
<semi:SemiTheme Locale="en-US"/>
|
<semi:SemiTheme Locale="en-US"/>
|
||||||
|
|
||||||
|
<!-- Context Menu Global Style -->
|
||||||
<Style Selector="ContextMenu">
|
<Style Selector="ContextMenu">
|
||||||
<!--<Setter Property="RenderTransform">
|
|
||||||
<Setter.Value>
|
|
||||||
<TranslateTransform Y="-10" />
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>-->
|
|
||||||
<!--<Setter Property="Opacity" Value="1" />-->
|
|
||||||
<Setter Property="FontSize" Value="13"/>
|
<Setter Property="FontSize" Value="13"/>
|
||||||
<Style.Animations>
|
<Style.Animations>
|
||||||
<Animation Duration="0.2">
|
<Animation Duration="0.2">
|
||||||
@@ -37,6 +32,25 @@
|
|||||||
</Animation>
|
</Animation>
|
||||||
</Style.Animations>
|
</Style.Animations>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- Menu Flyout Global Style -->
|
||||||
|
<Style Selector="MenuFlyoutPresenter">
|
||||||
|
<Setter Property="FontSize" Value="13"/>
|
||||||
|
<!--<Style.Animations>
|
||||||
|
<Animation Duration="0.2">
|
||||||
|
<KeyFrame Cue="0%">
|
||||||
|
<Setter Property="Opacity" Value="0" />
|
||||||
|
<Setter Property="ScaleTransform.ScaleX" Value="0.8"/>
|
||||||
|
<Setter Property="ScaleTransform.ScaleY" Value="0.8"/>
|
||||||
|
</KeyFrame>
|
||||||
|
<KeyFrame Cue="100%">
|
||||||
|
<Setter Property="Opacity" Value="1" />
|
||||||
|
<Setter Property="ScaleTransform.ScaleX" Value="1"/>
|
||||||
|
<Setter Property="ScaleTransform.ScaleY" Value="1"/>
|
||||||
|
</KeyFrame>
|
||||||
|
</Animation>
|
||||||
|
</Style.Animations>-->
|
||||||
|
</Style>
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
|
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
@@ -102,6 +116,20 @@
|
|||||||
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
|
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
|
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
|
||||||
</StreamGeometry>
|
</StreamGeometry>
|
||||||
|
<StreamGeometry 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
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry 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
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry 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
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry 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
|
||||||
|
</StreamGeometry>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
|
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Avalonia.Data.Core.Plugins;
|
|||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Harmonia.Core.Extensions;
|
using Harmonia.Core.Extensions;
|
||||||
using Harmonia.UI.Caching;
|
using Harmonia.UI.Caching;
|
||||||
|
using Harmonia.UI.Platform;
|
||||||
using Harmonia.UI.ViewModels;
|
using Harmonia.UI.ViewModels;
|
||||||
using Harmonia.UI.Views;
|
using Harmonia.UI.Views;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -27,6 +28,8 @@ public partial class App : Application
|
|||||||
services.AddSingleton<PlaylistViewModel>();
|
services.AddSingleton<PlaylistViewModel>();
|
||||||
|
|
||||||
services.AddSingleton<IAudioBitmapCache, AudioBitmapCache>();
|
services.AddSingleton<IAudioBitmapCache, AudioBitmapCache>();
|
||||||
|
services.AddSingleton<IStorageProviderLocator, StorageProviderLocator>();
|
||||||
|
services.AddSingleton<IClipboardLocator, ClipboardLocator>();
|
||||||
|
|
||||||
services.AddHarmonia();
|
services.AddHarmonia();
|
||||||
|
|
||||||
|
|||||||
103
Harmonia.UI/Controls/AnimatedFlyout.cs
Normal file
103
Harmonia.UI/Controls/AnimatedFlyout.cs
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Animation;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Rendering;
|
||||||
|
using Avalonia.Styling;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Controls;
|
||||||
|
|
||||||
|
public class AnimatedMenuFlyout : MenuFlyout
|
||||||
|
{
|
||||||
|
protected override Control CreatePresenter()
|
||||||
|
{
|
||||||
|
Control presenter = base.CreatePresenter();
|
||||||
|
presenter.AttachedToVisualTree += OnPresenterAttachedToVisualTree;
|
||||||
|
|
||||||
|
return presenter;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnPresenterAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not MenuFlyoutPresenter menuFlyoutPresenter)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await ApplyAnimationAsync(menuFlyoutPresenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ApplyAnimationAsync(MenuFlyoutPresenter presenter)
|
||||||
|
{
|
||||||
|
double translateYStart = ShouldSlideDown(presenter) ? - 100 : 100;
|
||||||
|
|
||||||
|
Animation animation = new()
|
||||||
|
{
|
||||||
|
Duration = TimeSpan.FromMilliseconds(200),
|
||||||
|
Children =
|
||||||
|
{
|
||||||
|
new KeyFrame
|
||||||
|
{
|
||||||
|
Cue = new Cue(0f),
|
||||||
|
Setters =
|
||||||
|
{
|
||||||
|
new Setter(Visual.OpacityProperty, 0.0),
|
||||||
|
new Setter(TranslateTransform.YProperty, translateYStart)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new KeyFrame
|
||||||
|
{
|
||||||
|
Cue = new Cue(1f),
|
||||||
|
Setters =
|
||||||
|
{
|
||||||
|
new Setter(Visual.OpacityProperty, 1.0),
|
||||||
|
new Setter(TranslateTransform.YProperty, 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await animation.RunAsync(presenter);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldSlideDown(MenuFlyoutPresenter presenter)
|
||||||
|
{
|
||||||
|
if (presenter.Parent is not Control targetControl)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Rect? topLevelBounds = GetTopLevelBounds();
|
||||||
|
|
||||||
|
if (topLevelBounds == null)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
Rect targetBounds = targetControl.Bounds;
|
||||||
|
|
||||||
|
double availableSpaceBelow = topLevelBounds.Value.Height - (targetBounds.Y + targetBounds.Height);
|
||||||
|
double availableSpaceAbove = targetBounds.Y;
|
||||||
|
|
||||||
|
return availableSpaceBelow >= availableSpaceAbove; // Slide down if more space below
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Rect? GetTopLevelBounds()
|
||||||
|
{
|
||||||
|
//Desktop
|
||||||
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
return desktop.MainWindow?.Bounds;
|
||||||
|
}
|
||||||
|
//Android (and iOS?)
|
||||||
|
else if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
|
||||||
|
{
|
||||||
|
IRenderRoot? visualRoot = singleViewPlatform.MainView?.GetVisualRoot();
|
||||||
|
|
||||||
|
if (visualRoot is TopLevel topLevel)
|
||||||
|
{
|
||||||
|
return topLevel.Bounds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Harmonia.UI/Platform/ClipboardLocator.cs
Normal file
17
Harmonia.UI/Platform/ClipboardLocator.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input.Platform;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Platform;
|
||||||
|
|
||||||
|
public class ClipboardLocator : PlatformServiceLocator<IClipboard>, IClipboardLocator
|
||||||
|
{
|
||||||
|
protected override IClipboard? GetFromWindow(Window mainWindow)
|
||||||
|
{
|
||||||
|
return mainWindow.Clipboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IClipboard? GetFromTopLevel(TopLevel topLevel)
|
||||||
|
{
|
||||||
|
return topLevel.Clipboard;
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Harmonia.UI/Platform/IClipboardLocator.cs
Normal file
8
Harmonia.UI/Platform/IClipboardLocator.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Avalonia.Input.Platform;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Platform;
|
||||||
|
|
||||||
|
public interface IClipboardLocator : IPlatformServiceLocator<IClipboard>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
6
Harmonia.UI/Platform/IPlatformServiceLocator.cs
Normal file
6
Harmonia.UI/Platform/IPlatformServiceLocator.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Harmonia.UI.Platform;
|
||||||
|
|
||||||
|
public interface IPlatformServiceLocator<T>
|
||||||
|
{
|
||||||
|
T? Get();
|
||||||
|
}
|
||||||
8
Harmonia.UI/Platform/IStorageProviderLocator.cs
Normal file
8
Harmonia.UI/Platform/IStorageProviderLocator.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Platform;
|
||||||
|
|
||||||
|
public interface IStorageProviderLocator : IPlatformServiceLocator<IStorageProvider>
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
40
Harmonia.UI/Platform/PlatformServiceLocator.cs
Normal file
40
Harmonia.UI/Platform/PlatformServiceLocator.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Rendering;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Platform;
|
||||||
|
|
||||||
|
public abstract class PlatformServiceLocator<T> : IPlatformServiceLocator<T>
|
||||||
|
{
|
||||||
|
public T? Get()
|
||||||
|
{
|
||||||
|
//Desktop
|
||||||
|
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
{
|
||||||
|
if (desktop.MainWindow == null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
return GetFromWindow(desktop.MainWindow);
|
||||||
|
}
|
||||||
|
//Android (and iOS?)
|
||||||
|
else if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
|
||||||
|
{
|
||||||
|
if (singleViewPlatform.MainView == null)
|
||||||
|
return default;
|
||||||
|
|
||||||
|
IRenderRoot? visualRoot = singleViewPlatform.MainView.GetVisualRoot();
|
||||||
|
|
||||||
|
if (visualRoot is TopLevel topLevel)
|
||||||
|
{
|
||||||
|
return GetFromTopLevel(topLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract T? GetFromWindow(Window mainWindow);
|
||||||
|
protected abstract T? GetFromTopLevel(TopLevel topLevel);
|
||||||
|
}
|
||||||
17
Harmonia.UI/Platform/StorageProviderLocator.cs
Normal file
17
Harmonia.UI/Platform/StorageProviderLocator.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Platform.Storage;
|
||||||
|
|
||||||
|
namespace Harmonia.UI.Platform;
|
||||||
|
|
||||||
|
public class StorageProviderLocator : PlatformServiceLocator<IStorageProvider>, IStorageProviderLocator
|
||||||
|
{
|
||||||
|
protected override IStorageProvider? GetFromWindow(Window mainWindow)
|
||||||
|
{
|
||||||
|
return mainWindow.StorageProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IStorageProvider? GetFromTopLevel(TopLevel topLevel)
|
||||||
|
{
|
||||||
|
return topLevel.StorageProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using Harmonia.Core.Caching;
|
|
||||||
using Harmonia.Core.Imaging;
|
|
||||||
using Harmonia.Core.Models;
|
using Harmonia.Core.Models;
|
||||||
using Harmonia.Core.Player;
|
using Harmonia.Core.Player;
|
||||||
using Harmonia.Core.Playlists;
|
using Harmonia.Core.Playlists;
|
||||||
using Harmonia.Core.Scanner;
|
using Harmonia.Core.Scanner;
|
||||||
|
using Harmonia.UI.Caching;
|
||||||
using Harmonia.UI.Models;
|
using Harmonia.UI.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -20,7 +18,7 @@ namespace Harmonia.UI.ViewModels;
|
|||||||
public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
|
public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IAudioPlayer _audioPlayer;
|
private readonly IAudioPlayer _audioPlayer;
|
||||||
private readonly IAudioImageCache _audioImageCache;
|
private readonly IAudioBitmapCache _audioBitmapCache;
|
||||||
private readonly DispatcherTimer _timer;
|
private readonly DispatcherTimer _timer;
|
||||||
|
|
||||||
private CancellationTokenSource? _audioImageCancellationTokenSource;
|
private CancellationTokenSource? _audioImageCancellationTokenSource;
|
||||||
@@ -53,7 +51,6 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
|
|||||||
}
|
}
|
||||||
private set
|
private set
|
||||||
{
|
{
|
||||||
_songImageSource?.Dispose();
|
|
||||||
_songImageSource = value;
|
_songImageSource = value;
|
||||||
OnPropertyChanged();
|
OnPropertyChanged();
|
||||||
}
|
}
|
||||||
@@ -205,14 +202,29 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
|
|||||||
public ICommand ToggleRandomizerCommand => new RelayCommand(ToggleRandomizer);
|
public ICommand ToggleRandomizerCommand => new RelayCommand(ToggleRandomizer);
|
||||||
public ICommand ToggleRepeatCommand => new RelayCommand(ToggleRepeat);
|
public ICommand ToggleRepeatCommand => new RelayCommand(ToggleRepeat);
|
||||||
|
|
||||||
public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioImageCache audioImageCache, IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner)
|
public PlaybackBarViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapCache, IPlaylistRepository playlistRepository, IAudioFileScanner audioFileScanner)
|
||||||
{
|
{
|
||||||
_audioPlayer = audioPlayer;
|
_audioPlayer = audioPlayer;
|
||||||
_audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged;
|
_audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged;
|
||||||
|
|
||||||
_audioImageCache = audioImageCache;
|
_audioBitmapCache = audioBitmapCache;
|
||||||
|
|
||||||
_timer = new(TimeSpan.FromMilliseconds(100), DispatcherPriority.Default, TickTock);
|
_timer = new(TimeSpan.FromMilliseconds(100), DispatcherPriority.Default, TickTock);
|
||||||
|
|
||||||
|
Task.Run(() => PlayDemoSong(playlistRepository));
|
||||||
|
}
|
||||||
|
|
||||||
|
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 OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
|
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
|
||||||
@@ -233,21 +245,14 @@ public partial class PlaybackBarViewModel : ViewModelBase, IDisposable
|
|||||||
_audioImageCancellationTokenSource = new();
|
_audioImageCancellationTokenSource = new();
|
||||||
CancellationToken cancellationToken = _audioImageCancellationTokenSource.Token;
|
CancellationToken cancellationToken = _audioImageCancellationTokenSource.Token;
|
||||||
|
|
||||||
SongPictureInfo? songPictureInfo = await _audioImageCache.GetAsync(Song, cancellationToken);
|
Bitmap? bitmap = await _audioBitmapCache.GetAsync(Song, cancellationToken);
|
||||||
|
|
||||||
if (songPictureInfo == null)
|
await Dispatcher.UIThread.InvokeAsync(() => SetSongImageSource(bitmap));
|
||||||
return;
|
|
||||||
|
|
||||||
await Dispatcher.UIThread.InvokeAsync(() => SetSongImageSource(songPictureInfo));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetSongImageSource(SongPictureInfo songPictureInfo)
|
private void SetSongImageSource(Bitmap? bitmap)
|
||||||
{
|
{
|
||||||
if (songPictureInfo.Data.Length == 0)
|
SongImageSource = bitmap;
|
||||||
return;
|
|
||||||
|
|
||||||
using MemoryStream stream = new(songPictureInfo.Data);
|
|
||||||
SongImageSource = new(stream);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TickTock(object? sender, object e)
|
private void TickTock(object? sender, object e)
|
||||||
|
|||||||
@@ -1,30 +1,28 @@
|
|||||||
using Harmonia.Core.Player;
|
using Avalonia.Input.Platform;
|
||||||
using Harmonia.Core.Playlists;
|
|
||||||
using System;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Threading;
|
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using System.Collections.Concurrent;
|
using Avalonia.Platform.Storage;
|
||||||
using Harmonia.UI.Caching;
|
|
||||||
using Harmonia.Core.Scanner;
|
|
||||||
using Harmonia.Core.Models;
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using System.Windows.Input;
|
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Harmonia.Core.Engine;
|
||||||
|
using Harmonia.Core.Models;
|
||||||
|
using Harmonia.Core.Player;
|
||||||
|
using Harmonia.Core.Playlists;
|
||||||
|
using Harmonia.Core.Scanner;
|
||||||
|
using Harmonia.UI.Caching;
|
||||||
|
using Harmonia.UI.Platform;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Avalonia;
|
using System.Threading;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using System.Timers;
|
||||||
using Avalonia.Input.Platform;
|
using System.Windows.Input;
|
||||||
using Avalonia.VisualTree;
|
using Timer = System.Timers.Timer;
|
||||||
using Avalonia.Rendering;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using Avalonia.Platform.Storage;
|
|
||||||
using Harmonia.Core.Engine;
|
|
||||||
using Avalonia.Input;
|
|
||||||
|
|
||||||
namespace Harmonia.UI.ViewModels;
|
namespace Harmonia.UI.ViewModels;
|
||||||
|
|
||||||
@@ -34,8 +32,12 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
private readonly IAudioBitmapCache _audioBitmapImageCache;
|
private readonly IAudioBitmapCache _audioBitmapImageCache;
|
||||||
private readonly IAudioFileScanner _audioFileScanner;
|
private readonly IAudioFileScanner _audioFileScanner;
|
||||||
private readonly IAudioEngine _audioEngine;
|
private readonly IAudioEngine _audioEngine;
|
||||||
|
private readonly IStorageProviderLocator _storageProviderLocator;
|
||||||
|
private readonly IClipboardLocator _clipboardLocator;
|
||||||
private readonly ConcurrentDictionary<string, Bitmap> _bitmapDictionary = [];
|
private readonly ConcurrentDictionary<string, Bitmap> _bitmapDictionary = [];
|
||||||
|
|
||||||
|
private Timer? _filterTimer;
|
||||||
|
|
||||||
public Playlist? Playlist { get; private set; }
|
public Playlist? Playlist { get; private set; }
|
||||||
public PlaylistSong? PlayingSong => _audioPlayer.PlayingSong;
|
public PlaylistSong? PlayingSong => _audioPlayer.PlayingSong;
|
||||||
|
|
||||||
@@ -53,6 +55,35 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string? _filter;
|
||||||
|
public string? Filter
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _filter;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_filter = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
RestartFilterTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObservableCollection<PlaylistSong> _filteredPlaylistSongs = [];
|
||||||
|
public ObservableCollection<PlaylistSong> FilteredPlaylistSongs
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _filteredPlaylistSongs;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_filteredPlaylistSongs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ObservableCollection<PlaylistSong> _selectedPlaylistSongs = [];
|
private ObservableCollection<PlaylistSong> _selectedPlaylistSongs = [];
|
||||||
public ObservableCollection<PlaylistSong> SelectedPlaylistSongs
|
public ObservableCollection<PlaylistSong> SelectedPlaylistSongs
|
||||||
{
|
{
|
||||||
@@ -76,7 +107,15 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs);
|
public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs);
|
||||||
public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected);
|
public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected);
|
||||||
|
|
||||||
public PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache, IAudioFileScanner audioFileScanner, IAudioEngine audioEngine)
|
public ICommand RefreshTagsCommand => new RelayCommand(RefreshTags);
|
||||||
|
public ICommand RemoveMissingSongsCommand => new RelayCommand(RemoveMissingSongs);
|
||||||
|
public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs);
|
||||||
|
|
||||||
|
public PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache,
|
||||||
|
IAudioFileScanner audioFileScanner,
|
||||||
|
IAudioEngine audioEngine,
|
||||||
|
IStorageProviderLocator storageProviderLocator,
|
||||||
|
IClipboardLocator clipboardLocator)
|
||||||
{
|
{
|
||||||
_audioPlayer = audioPlayer;
|
_audioPlayer = audioPlayer;
|
||||||
_audioPlayer.PlaylistChanged += OnAudioPlayerPlaylistChanged;
|
_audioPlayer.PlaylistChanged += OnAudioPlayerPlaylistChanged;
|
||||||
@@ -85,6 +124,8 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
_audioBitmapImageCache = audioBitmapImageCache;
|
_audioBitmapImageCache = audioBitmapImageCache;
|
||||||
_audioFileScanner = audioFileScanner;
|
_audioFileScanner = audioFileScanner;
|
||||||
_audioEngine = audioEngine;
|
_audioEngine = audioEngine;
|
||||||
|
_storageProviderLocator = storageProviderLocator;
|
||||||
|
_clipboardLocator = clipboardLocator;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAudioPlayerPlaylistChanged(object? sender, EventArgs e)
|
private void OnAudioPlayerPlaylistChanged(object? sender, EventArgs e)
|
||||||
@@ -104,6 +145,7 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? [];
|
PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? [];
|
||||||
|
|
||||||
PlaylistSongs = [.. playlistSongs];
|
PlaylistSongs = [.. playlistSongs];
|
||||||
|
UpdateFilteredSongs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
|
private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
|
||||||
@@ -128,6 +170,8 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
PlaylistSongs.Insert(currentIndex++, playlistSong);
|
PlaylistSongs.Insert(currentIndex++, playlistSong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateFilteredSongs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs)
|
private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs)
|
||||||
@@ -136,6 +180,8 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
PlaylistSongs.Remove(playlistSong);
|
PlaylistSongs.Remove(playlistSong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateFilteredSongs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
|
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
|
||||||
@@ -153,6 +199,70 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
return await _audioBitmapImageCache.GetAsync(playlistSong.Song, 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;
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Invoke(UpdateFilteredSongs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFilteredSongs()
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<PlaylistSong> filteredPlaylistSongs = [.. Playlist.Songs.Where(playlistSong => IsFiltered(playlistSong.Song))];
|
||||||
|
FilteredPlaylistSongs = [.. filteredPlaylistSongs];
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
#region Commands
|
||||||
|
|
||||||
private async Task PlaySongAsync()
|
private async Task PlaySongAsync()
|
||||||
@@ -173,7 +283,7 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
if (Playlist == null)
|
if (Playlist == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IStorageProvider? storageProvider = StorageProvider.Get();
|
IStorageProvider? storageProvider = _storageProviderLocator.Get();
|
||||||
|
|
||||||
if (storageProvider == null)
|
if (storageProvider == null)
|
||||||
return;
|
return;
|
||||||
@@ -204,7 +314,7 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
if (Playlist == null)
|
if (Playlist == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IStorageProvider? storageProvider = StorageProvider.Get();
|
IStorageProvider? storageProvider = _storageProviderLocator.Get();
|
||||||
|
|
||||||
if (storageProvider == null)
|
if (storageProvider == null)
|
||||||
return;
|
return;
|
||||||
@@ -277,7 +387,7 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
|
|
||||||
private async Task CopySelectedSongsToClipboardAsync()
|
private async Task CopySelectedSongsToClipboardAsync()
|
||||||
{
|
{
|
||||||
IClipboard? clipboard = Clipboard.Get();
|
IClipboard? clipboard = _clipboardLocator.Get();
|
||||||
|
|
||||||
if (clipboard == null)
|
if (clipboard == null)
|
||||||
return;
|
return;
|
||||||
@@ -307,7 +417,7 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
if (Playlist == null)
|
if (Playlist == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
IClipboard? clipboard = Clipboard.Get();
|
IClipboard? clipboard = _clipboardLocator.Get();
|
||||||
|
|
||||||
if (clipboard == null)
|
if (clipboard == null)
|
||||||
return false;
|
return false;
|
||||||
@@ -336,7 +446,7 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
if (Playlist == null)
|
if (Playlist == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IClipboard? clipboard = Clipboard.Get();
|
IClipboard? clipboard = _clipboardLocator.Get();
|
||||||
|
|
||||||
if (clipboard == null)
|
if (clipboard == null)
|
||||||
return;
|
return;
|
||||||
@@ -369,55 +479,20 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
Process.Start("explorer.exe", argument);
|
Process.Start("explorer.exe", argument);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshTags()
|
||||||
|
{
|
||||||
|
//Playlist?.RefreshTags();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveMissingSongs()
|
||||||
|
{
|
||||||
|
Playlist?.RemoveMissingSongs();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDuplicateSongs()
|
||||||
|
{
|
||||||
|
Playlist?.RemoveDuplicateSongs();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Clipboard
|
|
||||||
{
|
|
||||||
public static IClipboard? Get()
|
|
||||||
{
|
|
||||||
|
|
||||||
//Desktop
|
|
||||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
||||||
{
|
|
||||||
return desktop.MainWindow?.Clipboard;
|
|
||||||
}
|
|
||||||
//Android (and iOS?)
|
|
||||||
else if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
|
|
||||||
{
|
|
||||||
IRenderRoot? visualRoot = singleViewPlatform.MainView?.GetVisualRoot();
|
|
||||||
|
|
||||||
if (visualRoot is TopLevel topLevel)
|
|
||||||
{
|
|
||||||
return topLevel.Clipboard;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class StorageProvider
|
|
||||||
{
|
|
||||||
public static IStorageProvider? Get()
|
|
||||||
{
|
|
||||||
|
|
||||||
//Desktop
|
|
||||||
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
|
||||||
{
|
|
||||||
return desktop.MainWindow?.StorageProvider;
|
|
||||||
}
|
|
||||||
//Android (and iOS?)
|
|
||||||
else if (Application.Current?.ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform)
|
|
||||||
{
|
|
||||||
IRenderRoot? visualRoot = singleViewPlatform.MainView?.GetVisualRoot();
|
|
||||||
|
|
||||||
if (visualRoot is TopLevel topLevel)
|
|
||||||
{
|
|
||||||
return topLevel.StorageProvider;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="clr-namespace:Harmonia.UI.ViewModels"
|
xmlns:vm="clr-namespace:Harmonia.UI.ViewModels"
|
||||||
xmlns:converter="clr-namespace:Harmonia.UI.Converters"
|
xmlns:converter="clr-namespace:Harmonia.UI.Converters"
|
||||||
|
xmlns:controls="clr-namespace:Harmonia.UI.Controls"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
DataContext="{x:Static vm:ViewModelLocator.PlaylistViewModel}"
|
DataContext="{x:Static vm:ViewModelLocator.PlaylistViewModel}"
|
||||||
Loaded="OnLoaded"
|
Loaded="OnLoaded"
|
||||||
@@ -46,11 +47,109 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
<Grid Margin="0">
|
<Grid Margin="0">
|
||||||
<Canvas Background="#99000000"></Canvas> <!-- Was 99 !-->
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Canvas Background="#99000000" Grid.RowSpan="2"></Canvas> <!-- Was 99 !-->
|
||||||
|
<Grid Grid.Row="0" Margin="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
|
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBox Grid.Column="0" HorizontalAlignment="Stretch" Watermark="Filter" Text="{Binding Filter, Mode=TwoWay}"></TextBox>
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal">
|
||||||
|
<Button Foreground="#7FD184" Margin="10 0 0 0">
|
||||||
|
<Button.Content>
|
||||||
|
<PathIcon Data="{StaticResource SemiIconPlus}"></PathIcon>
|
||||||
|
</Button.Content>
|
||||||
|
<Button.Flyout>
|
||||||
|
<controls:AnimatedMenuFlyout Placement="Bottom">
|
||||||
|
<MenuItem Command="{Binding AddFilesCommand}" HotKey="Insert" InputGesture="Insert">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Add Files..." Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource SemiIconFile}"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Command="{Binding AddFolderCommand}" HotKey="Ctrl+Insert" InputGesture="Ctrl+Insert">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Add Folder..." Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource SemiIconFolder}"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
</controls:AnimatedMenuFlyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
</Button>
|
||||||
|
<Button Foreground="#F9F9F9" Margin="10 0 0 0">
|
||||||
|
<Button.Content>
|
||||||
|
<PathIcon Data="{StaticResource SemiIconMore}"></PathIcon>
|
||||||
|
</Button.Content>
|
||||||
|
<Button.Flyout>
|
||||||
|
<controls:AnimatedMenuFlyout Placement="Bottom">
|
||||||
|
<MenuItem Command="{Binding RefreshTagsCommand}">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Refresh Tags" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource RefreshIcon}"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Command="{Binding RemoveDuplicateSongsCommand}">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Remove Duplicates" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Command="{Binding RemoveMissingSongsCommand}">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Remove Missing" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem>
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Lock Playlist" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource LockIcon}"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem>
|
||||||
|
<MenuItem.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#ff99a4"/>
|
||||||
|
</Style>
|
||||||
|
</MenuItem.Styles>
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Remove Playlist" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource DeleteIcon}" Foreground="#ff99a4"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem>
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Settings" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource SettingsIcon}"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
</controls:AnimatedMenuFlyout>
|
||||||
|
</Button.Flyout>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
<ListBox
|
<ListBox
|
||||||
Name="PlaylistListBox"
|
Name="PlaylistListBox"
|
||||||
ItemsSource="{Binding PlaylistSongs}"
|
Grid.Row="1"
|
||||||
|
ItemsSource="{Binding FilteredPlaylistSongs, Mode=OneWay}"
|
||||||
SelectedItems="{Binding SelectedPlaylistSongs, Mode=OneWay}"
|
SelectedItems="{Binding SelectedPlaylistSongs, Mode=OneWay}"
|
||||||
|
DragDrop.AllowDrop="True"
|
||||||
DoubleTapped="ListBox_DoubleTapped"
|
DoubleTapped="ListBox_DoubleTapped"
|
||||||
SelectionMode="Multiple">
|
SelectionMode="Multiple">
|
||||||
<!--<ListBox.Styles>
|
<!--<ListBox.Styles>
|
||||||
@@ -82,7 +181,7 @@
|
|||||||
<Canvas Background="#19000000"></Canvas>
|
<Canvas Background="#19000000"></Canvas>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Song.Title, Mode=OneWay}" Classes="SongTitle" />
|
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Song, Mode=OneWay, Converter={StaticResource SongTitle}}" Classes="SongTitle" />
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Song.Artists, Mode=OneWay, Converter={StaticResource ArtistsToString}}" Classes="SongSubtitle" />
|
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Song.Artists, Mode=OneWay, Converter={StaticResource ArtistsToString}}" Classes="SongSubtitle" />
|
||||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Song.Album, Mode=OneWay}" Classes="SongSubtitle" />
|
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Song.Album, Mode=OneWay}" Classes="SongSubtitle" />
|
||||||
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Song.Length.TotalSeconds, Mode=OneWay, Converter={StaticResource SecondsToString}}" Classes="SongMetaData" />
|
<TextBlock Grid.Row="0" Grid.Column="2" Text="{Binding Song.Length.TotalSeconds, Mode=OneWay, Converter={StaticResource SecondsToString}}" Classes="SongMetaData" />
|
||||||
@@ -104,7 +203,7 @@
|
|||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />
|
||||||
<MenuItem Command="{Binding AddFilesCommand}" HotKey="Insert" InputGesture="Insert">
|
<!--<MenuItem Command="{Binding AddFilesCommand}" HotKey="Insert" InputGesture="Insert">
|
||||||
<MenuItem.Header>
|
<MenuItem.Header>
|
||||||
<TextBlock Text="Add Files..." Margin="8 0 8 0"></TextBlock>
|
<TextBlock Text="Add Files..." Margin="8 0 8 0"></TextBlock>
|
||||||
</MenuItem.Header>
|
</MenuItem.Header>
|
||||||
@@ -120,7 +219,7 @@
|
|||||||
<PathIcon Data="{StaticResource SemiIconFolder}"></PathIcon>
|
<PathIcon Data="{StaticResource SemiIconFolder}"></PathIcon>
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<Separator />
|
<Separator />-->
|
||||||
<MenuItem Command="{Binding RemoveSongsCommand}" HotKey="Delete" InputGesture="Delete">
|
<MenuItem Command="{Binding RemoveSongsCommand}" HotKey="Delete" InputGesture="Delete">
|
||||||
<MenuItem.Styles>
|
<MenuItem.Styles>
|
||||||
<Style Selector="TextBlock">
|
<Style Selector="TextBlock">
|
||||||
|
|||||||
@@ -2,17 +2,11 @@
|
|||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using Avalonia.Platform.Storage;
|
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Avalonia.VisualTree;
|
|
||||||
using Harmonia.Core.Engine;
|
|
||||||
using Harmonia.Core.Playlists;
|
using Harmonia.Core.Playlists;
|
||||||
using Harmonia.UI.ViewModels;
|
using Harmonia.UI.ViewModels;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -21,22 +15,32 @@ namespace Harmonia.UI.Views;
|
|||||||
public partial class PlaylistView : UserControl
|
public partial class PlaylistView : UserControl
|
||||||
{
|
{
|
||||||
private readonly PlaylistViewModel _viewModel;
|
private readonly PlaylistViewModel _viewModel;
|
||||||
private readonly IAudioEngine _audioEngine;
|
|
||||||
private readonly ConcurrentDictionary<int, CancellationTokenSource> _imageCancellationTokens = [];
|
private readonly ConcurrentDictionary<int, CancellationTokenSource> _imageCancellationTokens = [];
|
||||||
|
|
||||||
private IStorageProvider? _storageProvider;
|
|
||||||
|
|
||||||
public PlaylistView()
|
public PlaylistView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_viewModel = (PlaylistViewModel)DataContext!;
|
|
||||||
|
|
||||||
_audioEngine = App.ServiceProvider.GetRequiredService<IAudioEngine>();
|
_viewModel = (PlaylistViewModel)DataContext!;
|
||||||
|
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(_viewModel.FilteredPlaylistSongs))
|
||||||
|
{
|
||||||
|
await Dispatcher.UIThread.InvokeAsync(SlideInSongs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SlideInSongs()
|
||||||
|
{
|
||||||
|
await Animations.Animations.SlideFromRight(100, duration: 300).RunAsync(PlaylistListBox);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLoaded(object? sender, RoutedEventArgs e)
|
private void OnLoaded(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
_storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider;
|
//_storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ListBox_DoubleTapped(object? sender, TappedEventArgs e)
|
private async void ListBox_DoubleTapped(object? sender, TappedEventArgs e)
|
||||||
@@ -85,7 +89,7 @@ public partial class PlaylistView : UserControl
|
|||||||
if (sender is not Image image)
|
if (sender is not Image image)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (image.DataContext is not PlaylistSong playlistSong)
|
if (image.DataContext is not PlaylistSong)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (image.Source is Bitmap bitmap)
|
if (image.Source is Bitmap bitmap)
|
||||||
@@ -100,49 +104,4 @@ public partial class PlaylistView : UserControl
|
|||||||
_imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
|
_imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
|
||||||
cancellationTokenSource?.Cancel();
|
cancellationTokenSource?.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
//private async void AddFiles_Click(object? sender, RoutedEventArgs e)
|
|
||||||
//{
|
|
||||||
// if (_storageProvider == null)
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// FilePickerOpenOptions openOptions = new()
|
|
||||||
// {
|
|
||||||
// FileTypeFilter = [GetAudioFileTypes()],
|
|
||||||
// AllowMultiple = true
|
|
||||||
// };
|
|
||||||
|
|
||||||
// IReadOnlyList<IStorageFile> result = await _storageProvider.OpenFilePickerAsync(openOptions);
|
|
||||||
// string[] fileNames = [.. result.Select(file => file.TryGetLocalPath() ?? string.Empty)];
|
|
||||||
|
|
||||||
// CancellationToken cancellationToken = default;
|
|
||||||
// await _viewModel.AddFilesAsync(fileNames, cancellationToken);
|
|
||||||
//}
|
|
||||||
|
|
||||||
//private FilePickerFileType GetAudioFileTypes()
|
|
||||||
//{
|
|
||||||
// return new("Audo Files")
|
|
||||||
// {
|
|
||||||
// Patterns = [.. _audioEngine.SupportedFormats]
|
|
||||||
// };
|
|
||||||
//}
|
|
||||||
|
|
||||||
//private async void AddFolder_Click(object? sender, RoutedEventArgs e)
|
|
||||||
//{
|
|
||||||
// if (_storageProvider == null)
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// FolderPickerOpenOptions options = new()
|
|
||||||
// {
|
|
||||||
// AllowMultiple = true
|
|
||||||
// };
|
|
||||||
|
|
||||||
// IReadOnlyList<IStorageFolder> folders = await _storageProvider.OpenFolderPickerAsync(options);
|
|
||||||
|
|
||||||
// if (folders.Count == 0)
|
|
||||||
// return;
|
|
||||||
|
|
||||||
// CancellationToken cancellationToken = default;
|
|
||||||
// await _viewModel.AddFolderAsync(folders[0].TryGetLocalPath() ?? string.Empty, cancellationToken);
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user