Various updates.

This commit is contained in:
2025-03-30 23:18:40 -04:00
parent b7ba8341a1
commit f7cacf0bbb
11 changed files with 322 additions and 57 deletions

View File

@@ -12,8 +12,6 @@ namespace Harmonia.WinUI.Caching;
public class AudioBitmapImageCache(IAudioImageExtractor audioImageExtractor) : MemoryCache<Song, BitmapImage>, IAudioBitmapImageCache public class AudioBitmapImageCache(IAudioImageExtractor audioImageExtractor) : MemoryCache<Song, BitmapImage>, IAudioBitmapImageCache
{ {
protected virtual int MaxImageWidthOrHeight => 1000;
protected override MemoryCacheOptions Options => new() protected override MemoryCacheOptions Options => new()
{ {
SizeLimit = 200_000_000, SizeLimit = 200_000_000,
@@ -21,8 +19,8 @@ public class AudioBitmapImageCache(IAudioImageExtractor audioImageExtractor) : M
}; };
protected override TimeSpan SlidingExpiration => TimeSpan.FromSeconds(600); protected override TimeSpan SlidingExpiration => TimeSpan.FromSeconds(600);
protected override int MaxConcurrentRequests => 8; protected override int MaxConcurrentRequests => 8;
protected virtual int MaxImageWidthOrHeight => 1000;
protected override object? GetKey(Song key) protected override object? GetKey(Song key)
{ {
@@ -35,7 +33,7 @@ public class AudioBitmapImageCache(IAudioImageExtractor audioImageExtractor) : M
return key.ImageName; return key.ImageName;
} }
return null; return "Default";
} }
protected override async ValueTask<BitmapImage?> FetchAsync(Song key, CancellationToken cancellationToken) protected override async ValueTask<BitmapImage?> FetchAsync(Song key, CancellationToken cancellationToken)
@@ -43,7 +41,7 @@ public class AudioBitmapImageCache(IAudioImageExtractor audioImageExtractor) : M
SongPictureInfo? songPictureInfo = await audioImageExtractor.ExtractImageAsync(key.FileName, cancellationToken); SongPictureInfo? songPictureInfo = await audioImageExtractor.ExtractImageAsync(key.FileName, cancellationToken);
if (songPictureInfo == null) if (songPictureInfo == null)
return null; return GetDefaultBitmapImage();
using MemoryStream stream = new(songPictureInfo.Data); using MemoryStream stream = new(songPictureInfo.Data);
@@ -57,6 +55,18 @@ public class AudioBitmapImageCache(IAudioImageExtractor audioImageExtractor) : M
return bitmapImage; return bitmapImage;
} }
private BitmapImage GetDefaultBitmapImage()
{
Uri uri = new("ms-appx:///Assets/Default.png", UriKind.Absolute);
BitmapImage bitmapImage = new();
bitmapImage.DecodePixelWidth = GetDecodePixelWidth(bitmapImage);
bitmapImage.DecodePixelHeight = GetDecodePixelHeight(bitmapImage);
bitmapImage.UriSource = uri;
return bitmapImage;
}
private int GetDecodePixelWidth(BitmapImage bitmapImage) private int GetDecodePixelWidth(BitmapImage bitmapImage)
{ {
int originalImageWidth = bitmapImage.PixelWidth; int originalImageWidth = bitmapImage.PixelWidth;

View File

@@ -1,5 +1,6 @@
using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Data;
using System; using System;
using System.Collections.Generic;
namespace Harmonia.WinUI.Converters; namespace Harmonia.WinUI.Converters;
@@ -12,7 +13,7 @@ public partial class ArtistsToStringConverter : IValueConverter
public object Convert(object value, Type targetType, object parameter, string language) public object Convert(object value, Type targetType, object parameter, string language)
{ {
if (value is not string[] artists) if (value is not ICollection<string> artists)
return string.Empty; return string.Empty;
return string.Join(" / ", artists); return string.Join(" / ", artists);

View File

@@ -15,7 +15,7 @@ public sealed partial class NullVisibilityConverter : IValueConverter
public object Convert(object value, Type targetType, object parameter, string language) public object Convert(object value, Type targetType, object parameter, string language)
{ {
if (value is not IList list) if (value is not IList list)
return Visibility.Collapsed; return value == null ? Visibility.Collapsed : Visibility.Visible;
return list.Count == 0 ? Visibility.Collapsed : Visibility.Visible; return list.Count == 0 ? Visibility.Collapsed : Visibility.Visible;
} }

View File

@@ -50,6 +50,11 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Harmonia.Core\Harmonia.Core.csproj" /> <ProjectReference Include="..\Harmonia.Core\Harmonia.Core.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Update="Assets\Default.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Resources\Geometry.xaml"> <Page Update="Resources\Geometry.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>

View File

@@ -3,6 +3,23 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Global Font -->
<Style TargetType="TextBlock">
<Setter Property="FontFamily" Value="Lexend" />
</Style>
<Style TargetType="TextBox" BasedOn="{StaticResource DefaultTextBoxStyle}">
<Setter Property="FontFamily" Value="Lexend" />
</Style>
<Style TargetType="MenuFlyoutItem" BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}">
<Setter Property="FontFamily" Value="Lexend" />
</Style>
<Style TargetType="ListViewItem" BasedOn="{StaticResource DefaultListViewItemStyle}">
<Setter Property="FontFamily" Value="Lexend" />
</Style>
<!-- Flat Button --> <!-- Flat Button -->
<Style x:Key="FlatButton" TargetType="Button"> <Style x:Key="FlatButton" TargetType="Button">
<Setter Property="Padding" Value="10"/> <Setter Property="Padding" Value="10"/>

View File

@@ -12,6 +12,8 @@ using System.ComponentModel;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Windows.System;
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
namespace Harmonia.WinUI.ViewModels; namespace Harmonia.WinUI.ViewModels;
@@ -20,6 +22,7 @@ public partial class PlayerViewModel : ViewModelBase
private readonly IAudioPlayer _audioPlayer; private readonly IAudioPlayer _audioPlayer;
private readonly IAudioBitmapImageCache _audioBitmapImageCache; private readonly IAudioBitmapImageCache _audioBitmapImageCache;
private readonly DispatcherTimer _timer; private readonly DispatcherTimer _timer;
private readonly DispatcherQueue _dispatcherQueue;
private CancellationTokenSource? _songImageCancellationTokenSource; private CancellationTokenSource? _songImageCancellationTokenSource;
@@ -185,6 +188,18 @@ public partial class PlayerViewModel : ViewModelBase
} }
} }
public bool CanUpdatePosition
{
get
{
return _audioPlayer.State != AudioPlaybackState.Stopped;
}
private set
{
OnPropertyChanged();
}
}
public ICommand PlaySongCommand => new RelayCommand(Play); public ICommand PlaySongCommand => new RelayCommand(Play);
public ICommand PauseSongCommand => new RelayCommand(Pause); public ICommand PauseSongCommand => new RelayCommand(Pause);
public ICommand StopSongCommand => new RelayCommand(Stop); public ICommand StopSongCommand => new RelayCommand(Stop);
@@ -208,6 +223,8 @@ public partial class PlayerViewModel : ViewModelBase
}; };
_timer.Tick += TickTock; _timer.Tick += TickTock;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
} }
#region Event Handlers #region Event Handlers
@@ -230,9 +247,13 @@ public partial class PlayerViewModel : ViewModelBase
_songImageCancellationTokenSource = new(); _songImageCancellationTokenSource = new();
CancellationToken cancellationToken = _songImageCancellationTokenSource.Token; CancellationToken cancellationToken = _songImageCancellationTokenSource.Token;
BitmapImage? bitmapImage = await _audioBitmapImageCache.GetAsync(Song, cancellationToken); //BitmapImage? bitmapImage = await _audioBitmapImageCache.GetAsync(Song, cancellationToken);
DispatcherQueue.GetForCurrentThread().TryEnqueue(() => SongImageSource = bitmapImage); _dispatcherQueue.TryEnqueue(async () =>
{
BitmapImage? bitmapImage = await _audioBitmapImageCache.GetAsync(Song, cancellationToken);
SongImageSource = bitmapImage;
});
} }
private void OnAudioPlayerPropertyChanged(object? sender, PropertyChangedEventArgs e) private void OnAudioPlayerPropertyChanged(object? sender, PropertyChangedEventArgs e)
@@ -241,6 +262,7 @@ public partial class PlayerViewModel : ViewModelBase
{ {
case nameof(_audioPlayer.State): case nameof(_audioPlayer.State):
UpdateTimer(); UpdateTimer();
OnPropertyChanged(nameof(CanUpdatePosition));
break; break;
} }
} }

View File

@@ -9,6 +9,7 @@ using Harmonia.WinUI.Storage;
using Microsoft.UI.Dispatching; using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
@@ -34,6 +35,7 @@ public partial class PlaylistViewModel : ViewModelBase
private readonly IAudioEngine _audioEngine; private readonly IAudioEngine _audioEngine;
private readonly IStorageProvider _storageProvider; private readonly IStorageProvider _storageProvider;
private readonly DispatcherQueue _dispatcherQueue; private readonly DispatcherQueue _dispatcherQueue;
private readonly ConcurrentDictionary<int, CancellationTokenSource> _imageCancellationTokens = [];
private Timer? _filterTimer; private Timer? _filterTimer;
@@ -118,6 +120,9 @@ public partial class PlaylistViewModel : ViewModelBase
public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs); public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs);
public bool IsUserUpdating { get; set; } public bool IsUserUpdating { get; set; }
private bool _isUserInitiatingSongChange;
public event EventHandler? PlayingSongChangedAutomatically;
public PlaylistViewModel( public PlaylistViewModel(
IAudioPlayer audioPlayer, IAudioPlayer audioPlayer,
@@ -216,13 +221,34 @@ public partial class PlaylistViewModel : ViewModelBase
private void OnPlayingSongChanged(object? sender, EventArgs e) private void OnPlayingSongChanged(object? sender, EventArgs e)
{ {
PlayingSong = _audioPlayer.PlayingSong; PlayingSong = _audioPlayer.PlayingSong;
if (_isUserInitiatingSongChange)
{
_isUserInitiatingSongChange = false;
}
else
{
PlayingSongChangedAutomatically?.Invoke(this, EventArgs.Empty);
}
} }
public async Task PlaySongAsync(PlaylistSong playlistSong) public async Task PlaySongAsync(PlaylistSong playlistSong)
{ {
_isUserInitiatingSongChange = true;
await _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay); await _audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadAndPlay);
} }
public async Task<BitmapImage?> GetBitmapImageAsync(int hashCode, PlaylistSong playlistSong)
{
_imageCancellationTokens.TryGetValue(hashCode, out CancellationTokenSource? cancellationTokenSource);
cancellationTokenSource?.Cancel();
cancellationTokenSource = new();
_imageCancellationTokens.AddOrUpdate(hashCode, cancellationTokenSource, (_,_) => cancellationTokenSource);
return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationTokenSource.Token);
}
public async Task<BitmapImage?> GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken) public async Task<BitmapImage?> GetBitmapAsync(PlaylistSong playlistSong, CancellationToken cancellationToken)
{ {
return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken); return await _audioBitmapImageCache.GetAsync(playlistSong.Song, cancellationToken);
@@ -412,6 +438,9 @@ public partial class PlaylistViewModel : ViewModelBase
private bool CanPasteSongs() private bool CanPasteSongs()
{ {
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
return false;
DataPackageView dataPackageView = Clipboard.GetContent(); DataPackageView dataPackageView = Clipboard.GetContent();
if (dataPackageView == null) if (dataPackageView == null)

View File

@@ -11,11 +11,20 @@
mc:Ignorable="d"> mc:Ignorable="d">
<UserControl.Resources> <UserControl.Resources>
<SolidColorBrush x:Key="SongItemTitleBrush" Color="#dddddd"/>
<SolidColorBrush x:Key="SongItemSubtitleBrush" Color="#aaaaaa"/>
<Style x:Key="PlayerGrid" TargetType="Grid"> <Style x:Key="PlayerGrid" TargetType="Grid">
<Setter Property="Background" Value="#1a1a1a"/> <Setter Property="Background" Value="#1a1a1a"/>
<Setter Property="Padding" Value="10"/> <Setter Property="Padding" Value="10"/>
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent"/>
</Style> </Style>
<Style x:Key="SongTitleTextBlock" TargetType="TextBlock">
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
</Style>
<Style x:Key="SongImage" TargetType="Image"> <Style x:Key="SongImage" TargetType="Image">
<Setter Property="Width" Value="80"/> <Setter Property="Width" Value="80"/>
<Setter Property="Height" Value="80"/> <Setter Property="Height" Value="80"/>
@@ -43,15 +52,16 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock <Border Grid.Column="0" Background="Transparent" Padding="0 8" Width="60" CornerRadius="4" VerticalAlignment="Center" Margin="0 0 6 0">
Grid.Column="0" <TextBlock
Foreground="#aaaaaa" Foreground="#aaaaaa"
Text="{Binding Position, Converter={StaticResource SecondsToString}}" Text="{Binding Position, Converter={StaticResource SecondsToString}}"
FontSize="13" FontSize="13"
Margin="0 0 6 0" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center">
LineHeight="0"> </TextBlock>
</TextBlock> </Border>
<Slider <Slider
Name="PositionSlider" Name="PositionSlider"
Loaded="PositionSlider_Loaded" Loaded="PositionSlider_Loaded"
@@ -59,17 +69,20 @@
Value="{Binding CurrentPosition, Mode=TwoWay, UpdateSourceTrigger=Explicit}" Value="{Binding CurrentPosition, Mode=TwoWay, UpdateSourceTrigger=Explicit}"
Minimum="0" Minimum="0"
Maximum="{Binding MaxPosition, Mode=OneWay}" Maximum="{Binding MaxPosition, Mode=OneWay}"
IsEnabled="{Binding CanUpdatePosition, Mode=OneWay}"
ThumbToolTipValueConverter="{StaticResource SecondsToString}" ThumbToolTipValueConverter="{StaticResource SecondsToString}"
VerticalAlignment="Center" VerticalAlignment="Center"
VerticalContentAlignment="Center" /> VerticalContentAlignment="Center" />
<TextBlock
Grid.Column="2" <Border Grid.Column="2" Background="Transparent" Padding="0 8" Width="60" CornerRadius="4" VerticalAlignment="Center" Margin="6 0 0 0">
Foreground="#aaaaaa" <TextBlock
Text="{Binding MaxPosition, Converter={StaticResource SecondsToString}}" Foreground="#aaaaaa"
FontSize="13" Text="{Binding MaxPosition, Converter={StaticResource SecondsToString}}"
Margin="6 0 0 0" FontSize="13"
VerticalAlignment="Center"> VerticalAlignment="Center">
</TextBlock> </TextBlock>
</Border>
</Grid> </Grid>
<!-- Song Info --> <!-- Song Info -->
@@ -80,10 +93,10 @@
<Canvas Background="#19000000"></Canvas> <Canvas Background="#19000000"></Canvas>
</Grid> </Grid>
<StackPanel VerticalAlignment="Center"> <StackPanel VerticalAlignment="Center">
<TextBlock Foreground="#dddddd" FontWeight="SemiBold" FontSize="16" LineHeight="20" Text="{Binding Song, Converter={StaticResource SongTitle}}"></TextBlock> <TextBlock Foreground="#dddddd" FontWeight="SemiBold" FontSize="16" Text="{Binding Song, Converter={StaticResource SongTitle}}"></TextBlock>
<TextBlock Foreground="#aaaaaa" FontSize="14" LineHeight="20" Text="{Binding Song.Artists, Converter={StaticResource ArtistsToString}}"></TextBlock> <TextBlock Foreground="#aaaaaa" FontSize="14" Text="{Binding Song.Artists, Converter={StaticResource ArtistsToString}}"></TextBlock>
<TextBlock Foreground="#aaaaaa" FontSize="14" LineHeight="20" Text="{Binding Song.Album}"></TextBlock> <TextBlock Foreground="#aaaaaa" FontSize="14" Text="{Binding Song.Album}"></TextBlock>
<TextBlock Foreground="#777" FontSize="12" Visibility="{Binding Song, Converter={StaticResource NullVisibility}}" FontWeight="Normal" Opacity="1.0"> <TextBlock Foreground="#777" FontSize="12" Visibility="{Binding Song, Converter={StaticResource NullVisibility}}">
<Run Text="{Binding Song.FileType}"></Run><Run Text=" - "></Run><Run Text="{Binding Song.BitRate}"></Run><Run Text=" kbps - "></Run><Run Text="{Binding Song.SampleRate}"></Run><Run Text=" Hz"></Run> <Run Text="{Binding Song.FileType}"></Run><Run Text=" - "></Run><Run Text="{Binding Song.BitRate}"></Run><Run Text=" kbps - "></Run><Run Text="{Binding Song.SampleRate}"></Run><Run Text=" Hz"></Run>
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>

View File

@@ -26,14 +26,14 @@
<!-- Image Border --> <!-- Image Border -->
<Style x:Key="PlaylistSongImageBorder" TargetType="Border"> <Style x:Key="PlaylistSongImageBorder" TargetType="Border">
<Setter Property="Width" Value="75"/> <Setter Property="Width" Value="60"/> <!-- as 75 -->
<Setter Property="Height" Value="75"/> <Setter Property="Height" Value="60"/> <!-- as 75 -->
<Setter Property="CornerRadius" Value="8"/> <Setter Property="CornerRadius" Value="8"/>
</Style> </Style>
<DataTemplate x:Key="SongTemplate" x:DataType="playlists:PlaylistSong"> <DataTemplate x:Key="SongTemplate" x:DataType="playlists:PlaylistSong">
<!-- Background was formerly transparent --> <!-- Background was formerly transparent -->
<Border DoubleTapped="PlaylistListViewItem_DoubleTapped" Background="Transparent"> <Border DoubleTapped="PlaylistListViewItem_DoubleTapped" RightTapped="PlaylistListViewItem_RightTapped" Background="Transparent">
<Grid Padding="10"> <Grid Padding="10">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition>
@@ -55,11 +55,11 @@
<RowDefinition></RowDefinition> <RowDefinition></RowDefinition>
<RowDefinition></RowDefinition> <RowDefinition></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Column="0" Grid.Row="0" Foreground="{StaticResource SongItemTitleBrush}" Text="{x:Bind Song, Converter={StaticResource SongTitle}, Mode=OneWay}" FontSize="15" FontWeight="SemiBold" LineStackingStrategy="BlockLineHeight" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" LineHeight="0"> <TextBlock Grid.Column="0" Grid.Row="0" Foreground="{StaticResource SongItemTitleBrush}" Text="{x:Bind Song, Converter={StaticResource SongTitle}, Mode=OneWay}" FontSize="15" FontWeight="Medium" LineStackingStrategy="BlockLineHeight" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" LineHeight="0">
</TextBlock> </TextBlock>
<TextBlock Grid.Row="1" Text="{Binding Song.Artists, Converter={StaticResource ArtistsToString}}" Foreground="{StaticResource SongItemSubtitleBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="14" Margin="0 0 0 0"> <TextBlock Grid.Row="1" Text="{Binding Song.Artists, Converter={StaticResource ArtistsToString}}" Foreground="{StaticResource SongItemSubtitleBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="13" Margin="0 0 0 0">
</TextBlock> </TextBlock>
<TextBlock Grid.Row="2" Text="{Binding Song.Album}" Foreground="{StaticResource SongItemSubtitleBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="14" Margin="0 0 0 0"> <TextBlock Grid.Row="2" Text="{Binding Song.Album}" Foreground="{StaticResource SongItemSubtitleBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="13" Margin="0 0 0 0">
</TextBlock> </TextBlock>
<TextBlock Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" TextAlignment="Right" HorizontalTextAlignment="Right" Foreground="{StaticResource SongItemSubtitleBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="13" Text="{Binding Song.Length.TotalSeconds, Converter={StaticResource SecondsToString}}"></TextBlock> <TextBlock Grid.Row="0" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" TextAlignment="Right" HorizontalTextAlignment="Right" Foreground="{StaticResource SongItemSubtitleBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="13" Text="{Binding Song.Length.TotalSeconds, Converter={StaticResource SecondsToString}}"></TextBlock>
<TextBlock Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{StaticResource SongItemFooterBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="12" Text="{Binding Song.FileType}"></TextBlock> <TextBlock Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Foreground="{StaticResource SongItemFooterBrush}" TextTrimming="CharacterEllipsis" LineStackingStrategy="BlockLineHeight" LineHeight="0" FontSize="12" Text="{Binding Song.FileType}"></TextBlock>
@@ -128,7 +128,66 @@
Name="PlaylistListView" Name="PlaylistListView"
ItemsSource="{Binding FilteredPlaylistSongs}" ItemsSource="{Binding FilteredPlaylistSongs}"
ItemTemplate="{StaticResource SongTemplate}" ItemTemplate="{StaticResource SongTemplate}"
SelectionMode="Extended"> SelectionMode="Extended"
SelectionChanged="PlaylistListView_SelectionChanged">
<ListView.ContextFlyout>
<MenuFlyout x:Name="PlaylistListViewMenuFlyout" Opening="MenuFlyout_Opening" ShouldConstrainToRootBounds="False" SystemBackdrop="{StaticResource AcrylicBackgroundFillColorDefaultBackdrop}">
<MenuFlyout.MenuFlyoutPresenterStyle>
<Style TargetType="MenuFlyoutPresenter">
<Setter Property="Padding" Value="10"></Setter>
<Setter Property="Background" Value="Transparent"></Setter>
</Style>
</MenuFlyout.MenuFlyoutPresenterStyle>
<MenuFlyoutItem Text="Play" FontWeight="SemiBold" Command="{Binding PlaySongCommand}">
<MenuFlyoutItem.Icon>
<PathIcon Data="{StaticResource PlayIcon}"></PathIcon>
</MenuFlyoutItem.Icon>
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="Enter"/>
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
<MenuFlyoutSeparator></MenuFlyoutSeparator>
<MenuFlyoutItem Text="Remove" Command="{Binding RemoveSongsCommand}">
<MenuFlyoutItem.Icon>
<PathIcon Data="{StaticResource DeleteIcon}"></PathIcon>
</MenuFlyoutItem.Icon>
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="Delete"/>
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
<MenuFlyoutSeparator></MenuFlyoutSeparator>
<MenuFlyoutItem Text="Cut" Command="{Binding CutSongsCommand}">
<MenuFlyoutItem.Icon>
<PathIcon Data="{StaticResource CutIcon}"></PathIcon>
</MenuFlyoutItem.Icon>
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="X" Modifiers="Control" />
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
<MenuFlyoutItem Text="Copy" Command="{Binding CopySongsCommand}">
<MenuFlyoutItem.Icon>
<PathIcon Data="{StaticResource CopyIcon}"></PathIcon>
</MenuFlyoutItem.Icon>
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="C" Modifiers="Control" />
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
<MenuFlyoutItem Text="Paste" Command="{Binding PasteSongsCommand}">
<MenuFlyoutItem.Icon>
<PathIcon Data="{StaticResource PasteIcon}"></PathIcon>
</MenuFlyoutItem.Icon>
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="V" Modifiers="Control" />
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
<MenuFlyoutSeparator></MenuFlyoutSeparator>
<MenuFlyoutItem Text="Open File Location" Command="{Binding OpenFileLocationCommand}">
<MenuFlyoutItem.KeyboardAccelerators>
<KeyboardAccelerator Key="O" Modifiers="Menu" />
</MenuFlyoutItem.KeyboardAccelerators>
</MenuFlyoutItem>
</MenuFlyout>
</ListView.ContextFlyout>
</ListView> </ListView>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@@ -1,9 +1,14 @@
using Harmonia.Core.Playlists; using CommunityToolkit.WinUI;
using Harmonia.Core.Playlists;
using Harmonia.WinUI.ViewModels; using Harmonia.WinUI.ViewModels;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Input;
using System.Threading.Tasks; using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.Linq;
namespace Harmonia.WinUI.Views; namespace Harmonia.WinUI.Views;
@@ -14,34 +19,97 @@ public sealed partial class PlaylistView : UserControl
public PlaylistView() public PlaylistView()
{ {
InitializeComponent(); InitializeComponent();
_viewModel = (PlaylistViewModel)DataContext; _viewModel = (PlaylistViewModel)DataContext;
_viewModel.PlayingSongChangedAutomatically += OnPlayingSongChangedAutomatically;
foreach (MenuFlyoutItemBase item in PlaylistListViewMenuFlyout.Items)
{
item.DataContextChanged += Item_DataContextChanged;
}
}
private void OnPlayingSongChangedAutomatically(object? sender, EventArgs e)
{
BringPlayingSongIntoView();
}
private void BringPlayingSongIntoView()
{
PlaylistSong? playingSong = _viewModel.PlayingSong;
if (playingSong == null)
return;
ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(playingSong);
if (listViewItem != null)
{
listViewItem.UpdateLayout();
listViewItem.StartBringIntoView(new BringIntoViewOptions() { VerticalAlignmentRatio = .5, AnimationDesired = true });
}
else
{
PlaylistListView.UpdateLayout();
PlaylistListView.ScrollIntoView(playingSong);
}
}
private void Item_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if (sender is not MenuFlyoutItemBase item)
return;
if (args.NewValue == _viewModel)
return;
item.DataContext = _viewModel;
} }
private void Image_Loaded(object sender, RoutedEventArgs e) private void Image_Loaded(object sender, RoutedEventArgs e)
{ {
//Image? image = sender as Image; if (sender is not Image image)
return;
//if (image == null) image.DataContextChanged += Image_DataContextChanged;
// return;
//image.DataContextChanged += Image_DataContextChanged; if (image.DataContext is not PlaylistSong playlistSong)
return;
//var song = (PlaylistSong)image.DataContext; //Task.Run(async () => await UpdateImage(image, playlistSong));
UpdateImage(image, playlistSong);
//if (song == null)
// return;
//Task.Run(async () => await FetchImage(song.Song, image));
} }
private void Image_Unloaded(object sender, RoutedEventArgs e) private void Image_Unloaded(object sender, RoutedEventArgs e)
{ {
//Image? image = sender as Image; if (sender is not Image image)
return;
//if (image == null) image.DataContextChanged -= Image_DataContextChanged;
// return; }
//image.DataContextChanged -= Image_DataContextChanged; private void Image_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if (sender is not Image image)
return;
if (args.NewValue is not PlaylistSong playlistSong)
return;
//Task.Run(async () => await UpdateImage(image, playlistSong));
UpdateImage(image, playlistSong);
}
private void UpdateImage(Image image, PlaylistSong playlistSong)
{
int hashCode = image.GetHashCode();
//BitmapImage? bitmapImage = await _viewModel.GetBitmapImageAsync(hashCode, playlistSong);
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, async () =>
{
BitmapImage? bitmapImage = await _viewModel.GetBitmapImageAsync(hashCode, playlistSong);
image.Source = bitmapImage;
});
} }
private async void PlaylistListViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e) private async void PlaylistListViewItem_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
@@ -54,4 +122,45 @@ public sealed partial class PlaylistView : UserControl
await _viewModel.PlaySongAsync(playlistSong); await _viewModel.PlaySongAsync(playlistSong);
} }
private void PlaylistListViewItem_RightTapped(object sender, RightTappedRoutedEventArgs e)
{
if (sender is not FrameworkElement element)
return;
if (element == null || element.DataContext is not PlaylistSong playlistSong)
return;
if (PlaylistListView.SelectedItems.Contains(playlistSong))
return;
int index = PlaylistListView.Items.IndexOf(playlistSong);
PlaylistListView.DeselectAll();
PlaylistListView.SelectRange(new ItemIndexRange(index, 1));
}
private void PlaylistListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is not ListView listView)
return;
PlaylistSong[] selectedPlaylistSongs = [.. listView.SelectedItems.Cast<PlaylistSong>()];
_viewModel.SelectedPlaylistSongs = [.. selectedPlaylistSongs];
}
private void MenuFlyout_Opening(object sender, object e)
{
if (sender is not MenuFlyout menuFlyout)
return;
foreach (MenuFlyoutItemBase item in menuFlyout.Items)
{
if (item.DataContext != _viewModel)
{
item.DataContext = _viewModel;
}
}
}
} }