Moved playlist commands to view model.
This commit is contained in:
@@ -95,11 +95,11 @@ public class BassAudioEngine : IAudioEngine, IDisposable
|
|||||||
_mediaPlayer.PropertyChanged += OnMediaPlayerPropertyChanged;
|
_mediaPlayer.PropertyChanged += OnMediaPlayerPropertyChanged;
|
||||||
|
|
||||||
List<string> supportedFormats = [.. Bass.SupportedFormats.Split(';')];
|
List<string> supportedFormats = [.. Bass.SupportedFormats.Split(';')];
|
||||||
supportedFormats.Add(".aac");
|
//supportedFormats.Add(".aac");
|
||||||
supportedFormats.Add(".m4a");
|
//supportedFormats.Add(".m4a");
|
||||||
supportedFormats.Add(".flac");
|
supportedFormats.Add("*.flac");
|
||||||
supportedFormats.Add(".opus");
|
//supportedFormats.Add(".opus");
|
||||||
supportedFormats.Add(".wma");
|
//supportedFormats.Add(".wma");
|
||||||
|
|
||||||
SupportedFormats = [.. supportedFormats];
|
SupportedFormats = [.. supportedFormats];
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ManagedBass" Version="3.1.1" />
|
<PackageReference Include="ManagedBass" Version="3.1.1" />
|
||||||
<PackageReference Include="ManagedBass.Flac" Version="3.1.1" />
|
<PackageReference Include="ManagedBass.Flac" Version="3.1.1" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.2" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="9.0.3" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.3" />
|
||||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ namespace Harmonia.Core.Playlists;
|
|||||||
|
|
||||||
public class PlaylistSong(Song song)
|
public class PlaylistSong(Song song)
|
||||||
{
|
{
|
||||||
public string UID { get; } = Guid.NewGuid().ToString();
|
public string UID { get; init; } = Guid.NewGuid().ToString();
|
||||||
public Song Song { get; init; } = song;
|
public Song Song { get; init; } = song;
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,32 @@
|
|||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<!--<FluentTheme />-->
|
<!--<FluentTheme />-->
|
||||||
<semi:SemiTheme Locale="en-US"/>
|
<semi:SemiTheme Locale="en-US"/>
|
||||||
|
|
||||||
|
<Style Selector="ContextMenu">
|
||||||
|
<!--<Setter Property="RenderTransform">
|
||||||
|
<Setter.Value>
|
||||||
|
<TranslateTransform Y="-10" />
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>-->
|
||||||
|
<!--<Setter Property="Opacity" Value="1" />-->
|
||||||
|
<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"/>
|
||||||
|
<!--<Setter Property="TranslateTransform.Y" Value="-100" />-->
|
||||||
|
</KeyFrame>
|
||||||
|
<KeyFrame Cue="100%">
|
||||||
|
<Setter Property="Opacity" Value="1" />
|
||||||
|
<!--<Setter Property="TranslateTransform.Y" Value="0" />-->
|
||||||
|
<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>
|
||||||
@@ -62,6 +88,20 @@
|
|||||||
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
|
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
|
M9 5.5a.5.5 0 0 0-.854-.354l-1.75 1.75a.5.5 0 1 0 .708.708L8 6.707V10.5a.5.5 0 0 0 1 0z
|
||||||
</StreamGeometry>
|
</StreamGeometry>
|
||||||
|
<StreamGeometry 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
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry 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
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry 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
|
||||||
|
</StreamGeometry>
|
||||||
|
<StreamGeometry 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
|
||||||
|
</StreamGeometry>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
|
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
ServiceCollection services = new();
|
ServiceCollection services = new();
|
||||||
|
|
||||||
services.AddSingleton<MainViewModel>();
|
|
||||||
services.AddSingleton<MainWindow>();
|
services.AddSingleton<MainWindow>();
|
||||||
|
|
||||||
|
services.AddSingleton<MainViewModel>();
|
||||||
services.AddSingleton<PlaybackBarViewModel>();
|
services.AddSingleton<PlaybackBarViewModel>();
|
||||||
services.AddSingleton<PlayingSongInfoViewModel>();
|
services.AddSingleton<PlayingSongInfoViewModel>();
|
||||||
services.AddSingleton<PlaylistViewModel>();
|
services.AddSingleton<PlaylistViewModel>();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" />
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.3" />
|
||||||
<PackageReference Include="Semi.Avalonia" Version="11.2.1.5" />
|
<PackageReference Include="Semi.Avalonia" Version="11.2.1.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,24 @@ using System.Threading;
|
|||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using Harmonia.UI.Caching;
|
using Harmonia.UI.Caching;
|
||||||
|
using Harmonia.Core.Scanner;
|
||||||
|
using Harmonia.Core.Models;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input.Platform;
|
||||||
|
using Avalonia.VisualTree;
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -14,8 +32,11 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
private readonly IAudioPlayer _audioPlayer;
|
private readonly IAudioPlayer _audioPlayer;
|
||||||
private readonly IAudioBitmapCache _audioBitmapImageCache;
|
private readonly IAudioBitmapCache _audioBitmapImageCache;
|
||||||
|
private readonly IAudioFileScanner _audioFileScanner;
|
||||||
|
private readonly IAudioEngine _audioEngine;
|
||||||
private readonly ConcurrentDictionary<string, Bitmap> _bitmapDictionary = [];
|
private readonly ConcurrentDictionary<string, Bitmap> _bitmapDictionary = [];
|
||||||
|
|
||||||
|
public Playlist? Playlist { get; private set; }
|
||||||
public PlaylistSong? PlayingSong => _audioPlayer.PlayingSong;
|
public PlaylistSong? PlayingSong => _audioPlayer.PlayingSong;
|
||||||
|
|
||||||
private ObservableCollection<PlaylistSong> _playlistSongs = [];
|
private ObservableCollection<PlaylistSong> _playlistSongs = [];
|
||||||
@@ -32,34 +53,371 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache)
|
private ObservableCollection<PlaylistSong> _selectedPlaylistSongs = [];
|
||||||
|
public ObservableCollection<PlaylistSong> SelectedPlaylistSongs
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _selectedPlaylistSongs;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_selectedPlaylistSongs = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 AsyncRelayCommand(CutSongsAsync, AreSongsSelected);
|
||||||
|
public ICommand CopySongsCommand => new AsyncRelayCommand(CopySongsAsync, AreSongsSelected);
|
||||||
|
public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs);
|
||||||
|
public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected);
|
||||||
|
|
||||||
|
public PlaylistViewModel(IAudioPlayer audioPlayer, IAudioBitmapCache audioBitmapImageCache, IAudioFileScanner audioFileScanner, IAudioEngine audioEngine)
|
||||||
{
|
{
|
||||||
_audioPlayer = audioPlayer;
|
_audioPlayer = audioPlayer;
|
||||||
_audioPlayer.PlaylistChanged += OnAudioPlayerPlaylistChanged;
|
_audioPlayer.PlaylistChanged += OnAudioPlayerPlaylistChanged;
|
||||||
_audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged;
|
_audioPlayer.PlayingSongChanged += OnAudioPlayerPlayingSongChanged;
|
||||||
|
|
||||||
_audioBitmapImageCache = audioBitmapImageCache;
|
_audioBitmapImageCache = audioBitmapImageCache;
|
||||||
|
_audioFileScanner = audioFileScanner;
|
||||||
|
_audioEngine = audioEngine;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnAudioPlayerPlaylistChanged(object? sender, EventArgs e)
|
private void OnAudioPlayerPlaylistChanged(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() ?? [];
|
PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? [];
|
||||||
|
|
||||||
PlaylistSongs = [.. playlistSongs];
|
PlaylistSongs = [.. playlistSongs];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
|
||||||
|
{
|
||||||
|
switch (e.Action)
|
||||||
|
{
|
||||||
|
case PlaylistUpdateAction.Add:
|
||||||
|
Dispatcher.UIThread.Invoke(() => AddSongs(e.Songs, e.Index));
|
||||||
|
break;
|
||||||
|
case PlaylistUpdateAction.Remove:
|
||||||
|
Dispatcher.UIThread.Invoke(() => 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs)
|
||||||
|
{
|
||||||
|
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||||
|
{
|
||||||
|
PlaylistSongs.Remove(playlistSong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
|
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(nameof(PlayingSong));
|
OnPropertyChanged(nameof(PlayingSong));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void PlaySong(PlaylistSong playlistSong)
|
public async Task PlaySongAsync(PlaylistSong playlistSong)
|
||||||
{
|
{
|
||||||
_audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay);
|
await _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Bitmap?> GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken)
|
public async Task<Bitmap?> GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken);
|
return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
|
||||||
|
IStorageProvider? storageProvider = StorageProvider.Get();
|
||||||
|
|
||||||
|
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)];
|
||||||
|
|
||||||
|
Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken);
|
||||||
|
Playlist.AddSongs(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FilePickerFileType GetAudioFileTypes()
|
||||||
|
{
|
||||||
|
return new("Audo Files")
|
||||||
|
{
|
||||||
|
Patterns = [.. _audioEngine.SupportedFormats]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AddFolderAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IStorageProvider? storageProvider = StorageProvider.Get();
|
||||||
|
|
||||||
|
if (storageProvider == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
FolderPickerOpenOptions options = new()
|
||||||
|
{
|
||||||
|
AllowMultiple = true
|
||||||
|
};
|
||||||
|
|
||||||
|
IReadOnlyList<IStorageFolder> folders = await storageProvider.OpenFolderPickerAsync(options);
|
||||||
|
|
||||||
|
if (folders.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string? path = folders[0].TryGetLocalPath();
|
||||||
|
|
||||||
|
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 async Task CutSongsAsync()
|
||||||
|
{
|
||||||
|
if (SelectedPlaylistSongs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await CopySelectedSongsToClipboardAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CopySongsAsync()
|
||||||
|
{
|
||||||
|
if (SelectedPlaylistSongs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await CopySelectedSongsToClipboardAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CopySelectedSongsToClipboardAsync()
|
||||||
|
{
|
||||||
|
IClipboard? clipboard = Clipboard.Get();
|
||||||
|
|
||||||
|
if (clipboard == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Song[] songs = [.. SelectedPlaylistSongs.Select(playlistSong => playlistSong.Song)];
|
||||||
|
|
||||||
|
await clipboard.SetTextAsync(JsonSerializer.Serialize(songs));
|
||||||
|
}
|
||||||
|
|
||||||
|
//private async Task CopySelectedSongsToClipboard2Async()
|
||||||
|
//{
|
||||||
|
// IClipboard? clipboard = Clipboard.Get();
|
||||||
|
|
||||||
|
// if (clipboard == null)
|
||||||
|
// return;
|
||||||
|
|
||||||
|
// Song[] songs = [.. SelectedPlaylistSongs.Select(playlistSong => playlistSong.Song)];
|
||||||
|
|
||||||
|
// DataObject dataObject = new();
|
||||||
|
// dataObject.Set(DataFormats.Text, JsonSerializer.Serialize(songs));
|
||||||
|
|
||||||
|
// await clipboard.SetDataObjectAsync(dataObject);
|
||||||
|
//}
|
||||||
|
|
||||||
|
private bool CanPasteSongs()
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
IClipboard? clipboard = Clipboard.Get();
|
||||||
|
|
||||||
|
if (clipboard == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
string? clipboardText = clipboard.GetTextAsync().Result;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(clipboardText))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Song[] songs = [];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
songs = JsonSerializer.Deserialize<Song[]>(clipboardText) ?? [];
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return songs.Length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task PasteSongsAsync()
|
||||||
|
{
|
||||||
|
if (Playlist == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
IClipboard? clipboard = Clipboard.Get();
|
||||||
|
|
||||||
|
if (clipboard == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string? clipboardText = await clipboard.GetTextAsync();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(clipboardText))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Song[] songs = [];
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
songs = JsonSerializer.Deserialize<Song[]>(clipboardText) ?? [];
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Playlist.AddSongs(songs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenFileLocation()
|
||||||
|
{
|
||||||
|
if (SelectedPlaylistSongs.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string argument = "/select, \"" + SelectedPlaylistSongs[0].Song.FileName + "\"";
|
||||||
|
Process.Start("explorer.exe", argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
xmlns:converter="clr-namespace:Harmonia.UI.Converters"
|
xmlns:converter="clr-namespace:Harmonia.UI.Converters"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
DataContext="{x:Static vm:ViewModelLocator.PlaylistViewModel}"
|
DataContext="{x:Static vm:ViewModelLocator.PlaylistViewModel}"
|
||||||
|
Loaded="OnLoaded"
|
||||||
x:Class="Harmonia.UI.Views.PlaylistView"
|
x:Class="Harmonia.UI.Views.PlaylistView"
|
||||||
x:DataType="vm:PlaylistViewModel">
|
x:DataType="vm:PlaylistViewModel">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
@@ -46,7 +47,12 @@
|
|||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
<Grid Margin="0">
|
<Grid Margin="0">
|
||||||
<Canvas Background="#99000000"></Canvas> <!-- Was 99 !-->
|
<Canvas Background="#99000000"></Canvas> <!-- Was 99 !-->
|
||||||
<ListBox ItemsSource="{Binding PlaylistSongs}" DoubleTapped="ListBox_DoubleTapped">
|
<ListBox
|
||||||
|
Name="PlaylistListBox"
|
||||||
|
ItemsSource="{Binding PlaylistSongs}"
|
||||||
|
SelectedItems="{Binding SelectedPlaylistSongs, Mode=OneWay}"
|
||||||
|
DoubleTapped="ListBox_DoubleTapped"
|
||||||
|
SelectionMode="Multiple">
|
||||||
<!--<ListBox.Styles>
|
<!--<ListBox.Styles>
|
||||||
<Style Selector="ListBoxItem:nth-child(odd):not(:pointerover):not(:selected)">
|
<Style Selector="ListBoxItem:nth-child(odd):not(:pointerover):not(:selected)">
|
||||||
<Setter Property="Background" Value="#00000000"/>
|
<Setter Property="Background" Value="#00000000"/>
|
||||||
@@ -87,6 +93,86 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
|
<ListBox.ContextMenu>
|
||||||
|
<ContextMenu Name="PlaylistContextMenu" Padding="10" Background="#EE444444">
|
||||||
|
<MenuItem Command="{Binding PlaySongCommand}" HotKey="Enter" InputGesture="Enter">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Play" FontWeight="Bold" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource SemiIconPlay}"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<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>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Command="{Binding RemoveSongsCommand}" HotKey="Delete" InputGesture="Delete">
|
||||||
|
<MenuItem.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="#ff99a4"/>
|
||||||
|
</Style>
|
||||||
|
</MenuItem.Styles>
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Remove" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource DeleteIcon}" Foreground="#ff99a4"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Command="{Binding CutSongsCommand}" HotKey="Ctrl+X" InputGesture="Ctrl+X">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Cut" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource CutIcon}"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Command="{Binding CopySongsCommand}" HotKey="Ctrl+C" InputGesture="Ctrl+C">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Copy" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource CopyIcon}"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem Command="{Binding PasteSongsCommand}" HotKey="Ctrl+V" InputGesture="Ctrl+V">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Paste" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource PasteIcon}"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
<Separator />
|
||||||
|
<MenuItem Command="{Binding OpenFileLocationCommand}" HotKey="Alt+O" InputGesture="Alt+O">
|
||||||
|
<MenuItem.Header>
|
||||||
|
<TextBlock Text="Open File Location" Margin="8 0 8 0"></TextBlock>
|
||||||
|
</MenuItem.Header>
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<PathIcon Data="{StaticResource SemiIconFolderOpen}"></PathIcon>
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
|
</ContextMenu>
|
||||||
|
</ListBox.ContextMenu>
|
||||||
|
<ListBox.KeyBindings>
|
||||||
|
<KeyBinding Gesture="Enter" Command="{Binding PlaySongCommand}" />
|
||||||
|
</ListBox.KeyBindings>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,17 @@
|
|||||||
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 Harmonia.Core.Caching;
|
using Avalonia.VisualTree;
|
||||||
using Harmonia.Core.Imaging;
|
using Harmonia.Core.Engine;
|
||||||
using Harmonia.Core.Playlists;
|
using Harmonia.Core.Playlists;
|
||||||
using Harmonia.UI.ViewModels;
|
using Harmonia.UI.ViewModels;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.IO;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -19,19 +21,29 @@ 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!;
|
_viewModel = (PlaylistViewModel)DataContext!;
|
||||||
|
|
||||||
|
_audioEngine = App.ServiceProvider.GetRequiredService<IAudioEngine>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ListBox_DoubleTapped(object? sender, TappedEventArgs e)
|
private void OnLoaded(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_storageProvider = TopLevel.GetTopLevel(this)?.StorageProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void ListBox_DoubleTapped(object? sender, TappedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is ListBox listBox && listBox.SelectedItem is PlaylistSong playlistSong)
|
if (sender is ListBox listBox && listBox.SelectedItem is PlaylistSong playlistSong)
|
||||||
{
|
{
|
||||||
_viewModel.PlaySong(playlistSong);
|
await _viewModel.PlaySongAsync(playlistSong);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,4 +100,49 @@ 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