Compare commits

..

2 Commits

15 changed files with 397 additions and 18 deletions

View File

@@ -6,6 +6,7 @@ namespace Harmonia.Core.Engine;
public class BassAudioEngine : IAudioEngine, IDisposable public class BassAudioEngine : IAudioEngine, IDisposable
{ {
private readonly BaseMediaPlayer _mediaPlayer; private readonly BaseMediaPlayer _mediaPlayer;
private readonly SemaphoreSlim _loadLock = new(1, 1);
private CancellationTokenSource? _cancellationTokenSource; private CancellationTokenSource? _cancellationTokenSource;
@@ -136,9 +137,20 @@ public class BassAudioEngine : IAudioEngine, IDisposable
private async Task<bool> LoadWaveSourceAsync(string fileName) private async Task<bool> LoadWaveSourceAsync(string fileName)
{ {
_cancellationTokenSource?.Cancel(); _cancellationTokenSource?.Cancel();
_cancellationTokenSource = new CancellationTokenSource();
CancellationToken token = _cancellationTokenSource.Token; CancellationTokenSource cancellationTokenSource = new();
_cancellationTokenSource = cancellationTokenSource;
CancellationToken token = cancellationTokenSource.Token;
// Serialize loads so overlapping MediaPlayer.LoadAsync calls can't
// race and leave orphaned streams playing.
await _loadLock.WaitAsync(CancellationToken.None);
try
{
if (token.IsCancellationRequested)
return false;
try try
{ {
@@ -154,10 +166,20 @@ public class BassAudioEngine : IAudioEngine, IDisposable
} }
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
{
// A newer load superseded this one; make sure this stream
// doesn't keep playing in the background.
_mediaPlayer.Stop();
return false; return false;
}
return true; return true;
} }
finally
{
_loadLock.Release();
}
}
private void UpdateSource(string fileName) private void UpdateSource(string fileName)
{ {

View File

@@ -9,6 +9,8 @@ public class AudioPlayer : IAudioPlayer
private readonly IAudioEngine _audioEngine; private readonly IAudioEngine _audioEngine;
private readonly IPlaylistManager _playlistManager; private readonly IPlaylistManager _playlistManager;
private int _loadVersion;
private Playlist? _playlist; private Playlist? _playlist;
public Playlist? Playlist public Playlist? Playlist
{ {
@@ -241,8 +243,15 @@ public class AudioPlayer : IAudioPlayer
CurrentPlaylistSong = song; CurrentPlaylistSong = song;
int loadVersion = Interlocked.Increment(ref _loadVersion);
bool isLoaded = await TryLoadAsync(song); bool isLoaded = await TryLoadAsync(song);
// A newer load request was started while this one was in flight;
// abandon this one so only the latest request controls playback.
if (loadVersion != Volatile.Read(ref _loadVersion))
return false;
if (isLoaded == false) if (isLoaded == false)
{ {
if (mode == PlaybackMode.LoadAndPlay) if (mode == PlaybackMode.LoadAndPlay)

View File

@@ -1,5 +1,6 @@
using Harmonia.Core.Extensions; using Harmonia.Core.Extensions;
using Harmonia.WinUI.Caching; using Harmonia.WinUI.Caching;
using Harmonia.WinUI.Messaging;
using Harmonia.WinUI.Storage; using Harmonia.WinUI.Storage;
using Harmonia.WinUI.ViewModels; using Harmonia.WinUI.ViewModels;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@@ -23,10 +24,12 @@ public partial class App : Application
//services.AddSingleton<MainViewModel>(); //services.AddSingleton<MainViewModel>();
services.AddSingleton<PlayerViewModel>(); services.AddSingleton<PlayerViewModel>();
services.AddSingleton<PlayingSongViewModel>(); services.AddSingleton<PlayingSongViewModel>();
services.AddSingleton<PlaylistsViewModel>();
services.AddSingleton<PlaylistDetailViewModel>(); services.AddSingleton<PlaylistDetailViewModel>();
services.AddSingleton<IAudioBitmapImageCache, AudioBitmapImageCache>(); services.AddSingleton<IAudioBitmapImageCache, AudioBitmapImageCache>();
services.AddSingleton<IStorageProvider, WindowsStorageProvider>(); services.AddSingleton<IStorageProvider, WindowsStorageProvider>();
services.AddSingleton<IMessageService, WindowsMessageService>();
services.AddHarmonia(); services.AddHarmonia();

View File

@@ -17,6 +17,7 @@
<ItemGroup> <ItemGroup>
<None Remove="Resources\Styles.xaml" /> <None Remove="Resources\Styles.xaml" />
<None Remove="Views\PlayerView.xaml" /> <None Remove="Views\PlayerView.xaml" />
<None Remove="Views\PlaylistsView.xaml" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -55,6 +56,11 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content> </Content>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Page Update="Views\PlaylistsView.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup> <ItemGroup>
<Page Update="Resources\Geometry.xaml"> <Page Update="Resources\Geometry.xaml">
<SubType>Designer</SubType> <SubType>Designer</SubType>

View File

@@ -46,9 +46,11 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition> <ColumnDefinition></ColumnDefinition>
<ColumnDefinition></ColumnDefinition>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<views:PlayingSongView Grid.Column="0"></views:PlayingSongView> <views:PlayingSongView Grid.Column="0"></views:PlayingSongView>
<views:PlaylistDetailView Grid.Column="1"></views:PlaylistDetailView> <views:PlaylistsView Grid.Column="1"></views:PlaylistsView>
<views:PlaylistDetailView Grid.Column="2"></views:PlaylistDetailView>
</Grid> </Grid>
<views:PlayerView Grid.Row="2"></views:PlayerView> <views:PlayerView Grid.Row="2"></views:PlayerView>
</Grid> </Grid>

View File

@@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Harmonia.WinUI.Messaging;
public interface IMessageService
{
Task<bool> ConfirmAsync(string title, string message);
}

View File

@@ -0,0 +1,30 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.Threading.Tasks;
namespace Harmonia.WinUI.Messaging;
public class WindowsMessageService : IMessageService
{
private static MainWindow MainWindow => App.ServiceProvider.GetRequiredService<MainWindow>();
public async Task<bool> ConfirmAsync(string title, string message)
{
ContentDialog dialog = new()
{
XamlRoot = MainWindow.Content.XamlRoot,
Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style,
Title = title,
Content = message,
PrimaryButtonText = "Yes",
CloseButtonText = "No",
DefaultButton = ContentDialogButton.Primary
};
var result = await dialog.ShowAsync();
return result == ContentDialogResult.Primary;
}
}

View File

@@ -141,5 +141,11 @@
M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3 M3 9.5a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3m5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3
</x:String> </x:String>
<x:String x:Key="MusicNoteList">
M12 13c0 1.105-1.12 2-2.5 2S7 14.105 7 13s1.12-2 2.5-2 2.5.895 2.5 2
M12 3v10h-1V3z
M11 2.82a1 1 0 0 1 .804-.98l3-.6A1 1 0 0 1 16 2.22V4l-5 1z
M0 11.5a.5.5 0 0 1 .5-.5H4a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5m0-4A.5.5 0 0 1 .5 7H8a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5m0-4A.5.5 0 0 1 .5 3H8a.5.5 0 0 1 0 1H.5a.5.5 0 0 1-.5-.5
</x:String>
</ResourceDictionary> </ResourceDictionary>

View File

@@ -20,6 +20,139 @@
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" /> <Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
</Style> </Style>
<!-- Premium Palette (from Harmonia icon set) -->
<LinearGradientBrush x:Key="PremiumGoldGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#E9CFA3" Offset="0"/>
<GradientStop Color="#C7A468" Offset="0.5"/>
<GradientStop Color="#8A6D42" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="PremiumTileGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#2B2830" Offset="0"/>
<GradientStop Color="#1E1B22" Offset="1"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="PremiumForegroundBrush" Color="#E9CFA3"/>
<!-- Accent: Teal (Shuffle / Repeat) -->
<LinearGradientBrush x:Key="PremiumTealGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#A8DFD3" Offset="0"/>
<GradientStop Color="#6FBFAE" Offset="0.5"/>
<GradientStop Color="#3E7A6C" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="PremiumTealTileGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#26302D" Offset="0"/>
<GradientStop Color="#1A211F" Offset="1"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="PremiumTealForegroundBrush" Color="#A8DFD3"/>
<!-- Accent: Lavender (Previous / Next) -->
<LinearGradientBrush x:Key="PremiumLavenderGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#CBB8EF" Offset="0"/>
<GradientStop Color="#A78FD6" Offset="0.5"/>
<GradientStop Color="#6C5799" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="PremiumLavenderTileGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#2A2733" Offset="0"/>
<GradientStop Color="#1D1B24" Offset="1"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="PremiumLavenderForegroundBrush" Color="#CBB8EF"/>
<!-- Accent: Pink (Radio / Podcasts) -->
<LinearGradientBrush x:Key="PremiumPinkGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#EFB8C8" Offset="0"/>
<GradientStop Color="#D68FA8" Offset="0.5"/>
<GradientStop Color="#99576E" Offset="1"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="PremiumPinkTileGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#332730" Offset="0"/>
<GradientStop Color="#241B21" Offset="1"/>
</LinearGradientBrush>
<SolidColorBrush x:Key="PremiumPinkForegroundBrush" Color="#EFB8C8"/>
<!-- Premium Button -->
<Style x:Key="PremiumButton" TargetType="Button">
<Setter Property="Foreground" Value="{StaticResource PremiumForegroundBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource PremiumGoldGradient}"/>
<Setter Property="Background" Value="{StaticResource PremiumTileGradient}"/>
<Setter Property="Padding" Value="14,8"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<!-- Outer gradient acts as the border ring -->
<Grid x:Name="Root"
Background="{TemplateBinding BorderBrush}"
CornerRadius="{TemplateBinding CornerRadius}"
Padding="1.5">
<Grid x:Name="Inner"
Background="{TemplateBinding Background}"
CornerRadius="10">
<ContentPresenter x:Name="ContentPresenter"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Padding="{TemplateBinding Padding}"
Foreground="{TemplateBinding Foreground}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="Inner.Opacity" Value="0.88"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="Inner.Opacity" Value="0.75"/>
<Setter Target="Root.Opacity" Value="0.9"/>
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="Root.Opacity" Value="0.4"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Premium Button (Teal) -->
<Style x:Key="PremiumButton-Teal" TargetType="Button" BasedOn="{StaticResource PremiumButton}">
<Setter Property="Foreground" Value="{StaticResource PremiumTealForegroundBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource PremiumTealGradient}"/>
<Setter Property="Background" Value="{StaticResource PremiumTealTileGradient}"/>
</Style>
<!-- Premium Button (Lavender) -->
<Style x:Key="PremiumButton-Lavender" TargetType="Button" BasedOn="{StaticResource PremiumButton}">
<Setter Property="Foreground" Value="{StaticResource PremiumLavenderForegroundBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource PremiumLavenderGradient}"/>
<Setter Property="Background" Value="{StaticResource PremiumLavenderTileGradient}"/>
</Style>
<!-- Premium Button (Pink) -->
<Style x:Key="PremiumButton-Pink" TargetType="Button" BasedOn="{StaticResource PremiumButton}">
<Setter Property="Foreground" Value="{StaticResource PremiumPinkForegroundBrush}"/>
<Setter Property="BorderBrush" Value="{StaticResource PremiumPinkGradient}"/>
<Setter Property="Background" Value="{StaticResource PremiumPinkTileGradient}"/>
</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

@@ -7,6 +7,7 @@ using Harmonia.Core.Player;
using Harmonia.Core.Playlists; using Harmonia.Core.Playlists;
using Harmonia.Core.Scanner; using Harmonia.Core.Scanner;
using Harmonia.WinUI.Caching; using Harmonia.WinUI.Caching;
using Harmonia.WinUI.Messaging;
using Harmonia.WinUI.Storage; using Harmonia.WinUI.Storage;
using Microsoft.UI.Xaml.Media.Imaging; using Microsoft.UI.Xaml.Media.Imaging;
using System; using System;
@@ -36,6 +37,7 @@ public partial class PlaylistDetailViewModel : ViewModelBase
private readonly IAudioFileScanner _audioFileScanner; private readonly IAudioFileScanner _audioFileScanner;
private readonly IAudioEngine _audioEngine; private readonly IAudioEngine _audioEngine;
private readonly IStorageProvider _storageProvider; private readonly IStorageProvider _storageProvider;
private readonly IMessageService _messageService;
private readonly DispatcherQueue _dispatcherQueue; private readonly DispatcherQueue _dispatcherQueue;
private readonly ConcurrentDictionary<int, CancellationTokenSource> _imageCancellationTokens = []; private readonly ConcurrentDictionary<int, CancellationTokenSource> _imageCancellationTokens = [];
@@ -100,6 +102,7 @@ public partial class PlaylistDetailViewModel : ViewModelBase
} }
public ICommand PlaySongCommand => new AsyncRelayCommand(PlaySongAsync, AreSongsSelected); public ICommand PlaySongCommand => new AsyncRelayCommand(PlaySongAsync, AreSongsSelected);
public ICommand NewPlaylistCommand => new AsyncRelayCommand(NewPlaylistAsync);
public ICommand AddFilesCommand => new AsyncRelayCommand(AddFilesAsync); public ICommand AddFilesCommand => new AsyncRelayCommand(AddFilesAsync);
public ICommand AddFolderCommand => new AsyncRelayCommand(AddFolderAsync); public ICommand AddFolderCommand => new AsyncRelayCommand(AddFolderAsync);
public ICommand RemoveSongsCommand => new RelayCommand(RemoveSongs, AreSongsSelected); public ICommand RemoveSongsCommand => new RelayCommand(RemoveSongs, AreSongsSelected);
@@ -110,6 +113,7 @@ public partial class PlaylistDetailViewModel : ViewModelBase
public ICommand RefreshTagsCommand => new RelayCommand(RefreshTags); public ICommand RefreshTagsCommand => new RelayCommand(RefreshTags);
public ICommand RemoveMissingSongsCommand => new RelayCommand(RemoveMissingSongs); public ICommand RemoveMissingSongsCommand => new RelayCommand(RemoveMissingSongs);
public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs); public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs);
public ICommand DeletePlaylistCommand => new AsyncRelayCommand(DeletePlaylistAsync);
public bool IsUserUpdating { get; set; } public bool IsUserUpdating { get; set; }
private bool _isUserInitiatingSongChange; private bool _isUserInitiatingSongChange;
@@ -125,10 +129,11 @@ public partial class PlaylistDetailViewModel : ViewModelBase
IAudioFileScanner audioFileScanner, IAudioFileScanner audioFileScanner,
IAudioEngine audioEngine, IAudioEngine audioEngine,
IStorageProvider storageProvider, IStorageProvider storageProvider,
IPlaylistManager playlistManager) IPlaylistManager playlistManager,
IMessageService messageService)
{ {
_playlistManager = playlistManager; _playlistManager = playlistManager;
//_playlistManager.CurrentPlaylistChanged += OnPlaylistChanged; _playlistManager.CurrentPlaylistChanged += OnPlaylistChanged;
_audioPlayer = audioPlayer; _audioPlayer = audioPlayer;
_audioPlayer.PlaylistChanged += OnPlaylistChanged; _audioPlayer.PlaylistChanged += OnPlaylistChanged;
@@ -139,6 +144,7 @@ public partial class PlaylistDetailViewModel : ViewModelBase
_audioFileScanner = audioFileScanner; _audioFileScanner = audioFileScanner;
_audioEngine = audioEngine; _audioEngine = audioEngine;
_storageProvider = storageProvider; _storageProvider = storageProvider;
_messageService = messageService;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread(); _dispatcherQueue = DispatcherQueue.GetForCurrentThread();
FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged; FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged;
@@ -185,7 +191,8 @@ public partial class PlaylistDetailViewModel : ViewModelBase
private void OnPlaylistChanged(object? sender, EventArgs e) private void OnPlaylistChanged(object? sender, EventArgs e)
{ {
Playlist?.PlaylistUpdated -= OnPlaylistUpdated; Playlist?.PlaylistUpdated -= OnPlaylistUpdated;
Playlist = _audioPlayer.Playlist; //Playlist = _audioPlayer.Playlist;
Playlist = _playlistManager.CurrentPlaylist;
Playlist?.PlaylistUpdated += OnPlaylistUpdated; Playlist?.PlaylistUpdated += OnPlaylistUpdated;
UpdateFilteredSongs(); UpdateFilteredSongs();
@@ -369,6 +376,12 @@ public partial class PlaylistDetailViewModel : ViewModelBase
return SelectedPlaylistSongs.Count > 0; return SelectedPlaylistSongs.Count > 0;
} }
private async Task NewPlaylistAsync(CancellationToken cancellationToken)
{
Playlist playlist = await _playlistManager.AddPlaylistAsync();
_playlistManager.CurrentPlaylist = playlist;
}
private async Task AddFilesAsync(CancellationToken cancellationToken) private async Task AddFilesAsync(CancellationToken cancellationToken)
{ {
if (Playlist == null) if (Playlist == null)
@@ -535,5 +548,30 @@ public partial class PlaylistDetailViewModel : ViewModelBase
Playlist?.RemoveDuplicateSongs(); Playlist?.RemoveDuplicateSongs();
} }
private async Task DeletePlaylistAsync()
{
if (Playlist == null)
return;
bool confirmed = await _messageService.ConfirmAsync($"Delete Playlist - {Playlist.Name}", "Are you sure you want to delete this playlist?");
if (!confirmed)
return;
_playlistManager.RemovePlaylist(Playlist);
if (_playlistManager.CurrentPlaylist == Playlist)
{
if (_playlistManager.Playlists.Count > 0)
{
_playlistManager.CurrentPlaylist = _playlistManager.Playlists.ElementAt(0);
}
else
{
_playlistManager.CurrentPlaylist = null;
}
}
}
#endregion #endregion
} }

View File

@@ -0,0 +1,65 @@
using Harmonia.Core.Playlists;
using System;
using System.Collections.ObjectModel;
namespace Harmonia.WinUI.ViewModels;
public partial class PlaylistsViewModel : ViewModelBase
{
private readonly IPlaylistManager _playlistManager;
private ObservableCollection<Playlist> _playlists = [];
public ObservableCollection<Playlist> Playlists
{
get
{
return _playlists;
}
set
{
SetProperty(ref _playlists, value);
}
}
private Playlist? _selectedPlaylist = null;
public Playlist? SelectedPlaylist
{
get
{
return _selectedPlaylist;
}
set
{
if (SetProperty(ref _selectedPlaylist, value))
{
_playlistManager.CurrentPlaylist = value;
}
}
}
public PlaylistsViewModel(IPlaylistManager playlistManager)
{
_playlistManager = playlistManager;
_playlistManager.PlaylistAdded += OnPlaylistAdded;
_playlistManager.PlaylistRemoved += OnPlaylistRemoved;
_playlistManager.CurrentPlaylistChanged += OnCurrentPlaylistChanged;
_playlists = new(_playlistManager.Playlists);
_selectedPlaylist = _playlistManager.CurrentPlaylist;
}
private void OnCurrentPlaylistChanged(object? sender, EventArgs e)
{
SelectedPlaylist = _playlistManager.CurrentPlaylist;
}
private void OnPlaylistAdded(object? sender, PlaylistAddedEventArgs e)
{
Playlists.Add(e.Playlist);
}
private void OnPlaylistRemoved(object? sender, PlaylistRemovedEventArgs e)
{
Playlists.Remove(e.Playlist);
}
}

View File

@@ -13,6 +13,9 @@ public class ViewModelLocator
public static PlayingSongViewModel PlayingSongViewModel public static PlayingSongViewModel PlayingSongViewModel
=> App.ServiceProvider.GetRequiredService<PlayingSongViewModel>(); => App.ServiceProvider.GetRequiredService<PlayingSongViewModel>();
public static PlaylistsViewModel PlaylistsViewModel
=> App.ServiceProvider.GetRequiredService<PlaylistsViewModel>();
public static PlaylistDetailViewModel PlaylistDetailViewModel public static PlaylistDetailViewModel PlaylistDetailViewModel
=> App.ServiceProvider.GetRequiredService<PlaylistDetailViewModel>(); => App.ServiceProvider.GetRequiredService<PlaylistDetailViewModel>();
} }

View File

@@ -123,6 +123,11 @@
<Setter Property="Background" Value="Transparent"></Setter> <Setter Property="Background" Value="Transparent"></Setter>
</Style> </Style>
</MenuFlyout.MenuFlyoutPresenterStyle> </MenuFlyout.MenuFlyoutPresenterStyle>
<MenuFlyoutItem Text="New Playlist" Command="{Binding NewPlaylistCommand}">
<MenuFlyoutItem.Icon>
<PathIcon Data="{StaticResource AddFileIcon}"></PathIcon>
</MenuFlyoutItem.Icon>
</MenuFlyoutItem>
<MenuFlyoutItem Text="Add Files..." Command="{Binding AddFilesCommand}"> <MenuFlyoutItem Text="Add Files..." Command="{Binding AddFilesCommand}">
<MenuFlyoutItem.Icon> <MenuFlyoutItem.Icon>
<PathIcon Data="{StaticResource AddFileIcon}"></PathIcon> <PathIcon Data="{StaticResource AddFileIcon}"></PathIcon>
@@ -167,7 +172,7 @@
</MenuFlyoutItem> </MenuFlyoutItem>
<MenuFlyoutSeparator></MenuFlyoutSeparator> <MenuFlyoutSeparator></MenuFlyoutSeparator>
<MenuFlyoutItem Text="Remove Playlist" Foreground="#ff99a4"> <MenuFlyoutItem Text="Remove Playlist" Foreground="#ff99a4" Command="{Binding DeletePlaylistCommand}">
</MenuFlyoutItem> </MenuFlyoutItem>
<MenuFlyoutSeparator></MenuFlyoutSeparator> <MenuFlyoutSeparator></MenuFlyoutSeparator>
@@ -177,6 +182,12 @@
</MenuFlyout> </MenuFlyout>
</Button.Flyout> </Button.Flyout>
</Button> </Button>
<!--<Button Style="{StaticResource PremiumButton-Teal}">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Style="{StaticResource FlatButtonIcon}" Foreground="{StaticResource PremiumTeaForegroundBrush}" Data="{StaticResource AddIcon}" />
<TextBlock Text="Go Premium"/>
</StackPanel>
</Button>-->
</StackPanel> </StackPanel>
</Grid> </Grid>
<ListView <ListView

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<UserControl
x:Class="Harmonia.WinUI.Views.PlaylistsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Harmonia.WinUI.Views"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Harmonia.WinUI.ViewModels"
xmlns:playlists="using:Harmonia.Core.Playlists"
DataContext="{Binding Source={StaticResource Locator}, Path=PlaylistsViewModel}"
d:DataContext="{d:DesignInstance Type=vm:PlaylistsViewModel, IsDesignTimeCreatable=True}"
mc:Ignorable="d">
<UserControl.Resources>
<DataTemplate x:Key="PlaylistTemplate" x:DataType="playlists:Playlist">
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Bottom">
<Viewbox Height="24" Width="24">
<PathIcon Data="{StaticResource MusicNoteList}" />
</Viewbox>
<TextBlock Text="{x:Bind Name}" FontSize="16" VerticalAlignment="Center" />
</StackPanel>
</DataTemplate>
</UserControl.Resources>
<Grid Margin="10">
<ListView
ItemsSource="{Binding Playlists, Mode=OneWay}"
SelectedItem="{Binding SelectedPlaylist, Mode=TwoWay}"
ItemTemplate="{StaticResource PlaylistTemplate}">
</ListView>
</Grid>
</UserControl>

View File

@@ -0,0 +1,12 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Harmonia.WinUI.Views;
public sealed partial class PlaylistsView : UserControl
{
public PlaylistsView()
{
InitializeComponent();
}
}