Moved playlist commands to view model.

This commit is contained in:
2025-03-14 22:59:15 -04:00
parent bd9b30abbd
commit 7c70eb3814
9 changed files with 565 additions and 23 deletions

View File

@@ -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];

View File

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

View File

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

View File

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

View File

@@ -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>();

View File

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

View File

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

View File

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

View File

@@ -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);
//}
} }