Compare commits
38 Commits
140d4ea49a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 27fd79c392 | |||
| e92ef0e28b | |||
| 8e56ac05f8 | |||
| 4fd43c5c18 | |||
| b48184ddac | |||
| ccea149df0 | |||
| 32547c4f4c | |||
| 92f04a4fd3 | |||
| b9ebd0430d | |||
| 1bf49b6809 | |||
| 5302a3c955 | |||
| bc807e3540 | |||
| c8ce4ca7f0 | |||
| 321db65154 | |||
| 23aff91ba0 | |||
| 91e4d88643 | |||
| cf8a40ee52 | |||
| 895cfdc5f7 | |||
| 2be865d445 | |||
| afcae0761b | |||
| 0942e3e5ad | |||
| c262ee4caa | |||
| 75f820ed7b | |||
| 1d45826c38 | |||
| 3c3cf37cd5 | |||
| cd6f9c382e | |||
| de16ac6240 | |||
| 2b78ac5667 | |||
| 99978c62c4 | |||
| 7aaf36f7b2 | |||
| 16b87d255e | |||
| ea5b428a65 | |||
| 8864da7a75 | |||
| 6484f2120e | |||
| 62e53a29d4 | |||
| 7cfcbf26cc | |||
| a85c65e833 | |||
| c334a4166f |
@@ -6,6 +6,7 @@ namespace Harmonia.Core.Engine;
|
||||
public class BassAudioEngine : IAudioEngine, IDisposable
|
||||
{
|
||||
private readonly BaseMediaPlayer _mediaPlayer;
|
||||
private readonly SemaphoreSlim _loadLock = new(1, 1);
|
||||
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
@@ -96,7 +97,7 @@ public class BassAudioEngine : IAudioEngine, IDisposable
|
||||
|
||||
List<string> supportedFormats = [.. Bass.SupportedFormats.Split(';')];
|
||||
//supportedFormats.Add(".aac");
|
||||
//supportedFormats.Add(".m4a");
|
||||
supportedFormats.Add(".m4a");
|
||||
supportedFormats.Add("*.flac");
|
||||
//supportedFormats.Add(".opus");
|
||||
//supportedFormats.Add(".wma");
|
||||
@@ -136,27 +137,48 @@ public class BassAudioEngine : IAudioEngine, IDisposable
|
||||
private async Task<bool> LoadWaveSourceAsync(string fileName)
|
||||
{
|
||||
_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
|
||||
{
|
||||
await _mediaPlayer.LoadAsync(fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
//return new Result(State.Exception, ex.Message);
|
||||
throw new Exception("An error occurred - " + fileName, ex);
|
||||
try
|
||||
{
|
||||
await _mediaPlayer.LoadAsync(fileName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (token.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
//return new Result(State.Exception, ex.Message);
|
||||
throw new Exception("An error occurred - " + fileName, ex);
|
||||
}
|
||||
|
||||
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 true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loadLock.Release();
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateSource(string fileName)
|
||||
|
||||
@@ -9,10 +9,10 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ManagedBass" Version="4.0.2" />
|
||||
<PackageReference Include="ManagedBass.Flac" Version="4.0.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.12" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.12" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ public class AudioPlayer : IAudioPlayer
|
||||
private readonly IAudioEngine _audioEngine;
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
|
||||
private int _loadVersion;
|
||||
|
||||
private Playlist? _playlist;
|
||||
public Playlist? Playlist
|
||||
{
|
||||
@@ -18,9 +20,13 @@ public class AudioPlayer : IAudioPlayer
|
||||
}
|
||||
protected set
|
||||
{
|
||||
Playlist? oldPlaylist = _playlist;
|
||||
_playlist = value;
|
||||
|
||||
NotifyPropertyChanged(nameof(Playlist));
|
||||
PlaylistChanged?.Invoke(this, new());
|
||||
|
||||
PlaylistChangedEventArgs eventArgs = new(oldPlaylist, value);
|
||||
PlaylistChanged?.Invoke(this, eventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,9 +41,13 @@ public class AudioPlayer : IAudioPlayer
|
||||
}
|
||||
protected set
|
||||
{
|
||||
PlaylistSong? oldSong = _playingSong;
|
||||
_playingSong = value;
|
||||
|
||||
NotifyPropertyChanged(nameof(PlayingSong));
|
||||
PlayingSongChanged?.Invoke(this, new());
|
||||
|
||||
PlayingSongChangedEventArgs eventArgs = new(oldSong, value);
|
||||
PlayingSongChanged?.Invoke(this, eventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,8 +123,8 @@ public class AudioPlayer : IAudioPlayer
|
||||
|
||||
protected virtual int PreviousSongSecondsThreshold => 5;
|
||||
|
||||
public event EventHandler? PlaylistChanged;
|
||||
public event EventHandler? PlayingSongChanged;
|
||||
public event EventHandler<PlaylistChangedEventArgs>? PlaylistChanged;
|
||||
public event EventHandler<PlayingSongChangedEventArgs>? PlayingSongChanged;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public AudioPlayer(IAudioEngine audioEngine, IPlaylistManager playlistManager)
|
||||
@@ -171,7 +181,7 @@ public class AudioPlayer : IAudioPlayer
|
||||
return;
|
||||
}
|
||||
|
||||
int currentIndex = Playlist.Songs.IndexOf(CurrentPlaylistSong);
|
||||
int currentIndex = Playlist.IndexOf(CurrentPlaylistSong);
|
||||
int nextIndex = currentIndex + 1;
|
||||
|
||||
if (nextIndex > Playlist.Songs.Count - 1)
|
||||
@@ -198,7 +208,7 @@ public class AudioPlayer : IAudioPlayer
|
||||
return;
|
||||
}
|
||||
|
||||
int currentIndex = Playlist.Songs.IndexOf(CurrentPlaylistSong);
|
||||
int currentIndex = Playlist.IndexOf(CurrentPlaylistSong);
|
||||
int nextIndex = currentIndex - 1;
|
||||
|
||||
if (nextIndex < 0)
|
||||
@@ -231,7 +241,7 @@ public class AudioPlayer : IAudioPlayer
|
||||
{
|
||||
if (Playlist == null || Playlist.Songs.Contains(song) == false)
|
||||
{
|
||||
Playlist? newPlaylist = _playlistManager.GetPlaylist(song);
|
||||
Playlist? newPlaylist = _playlistManager.FindPlaylistContaining(song);
|
||||
|
||||
if (newPlaylist == null)
|
||||
return false;
|
||||
@@ -241,8 +251,15 @@ public class AudioPlayer : IAudioPlayer
|
||||
|
||||
CurrentPlaylistSong = song;
|
||||
|
||||
int loadVersion = Interlocked.Increment(ref _loadVersion);
|
||||
|
||||
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 (mode == PlaybackMode.LoadAndPlay)
|
||||
|
||||
@@ -24,7 +24,7 @@ public interface IAudioPlayer
|
||||
Task PreviousAsync();
|
||||
Task NextAsync();
|
||||
|
||||
event EventHandler PlaylistChanged;
|
||||
event EventHandler PlayingSongChanged;
|
||||
event EventHandler<PlaylistChangedEventArgs> PlaylistChanged;
|
||||
event EventHandler<PlayingSongChangedEventArgs> PlayingSongChanged;
|
||||
event PropertyChangedEventHandler PropertyChanged;
|
||||
}
|
||||
9
Harmonia.Core/Player/PlayingSongChangedEventArgs.cs
Normal file
9
Harmonia.Core/Player/PlayingSongChangedEventArgs.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Harmonia.Core.Playlists;
|
||||
|
||||
namespace Harmonia.Core.Player;
|
||||
|
||||
public class PlayingSongChangedEventArgs(PlaylistSong? oldSong, PlaylistSong? newSong) : EventArgs
|
||||
{
|
||||
public PlaylistSong? OldSong { get; } = oldSong;
|
||||
public PlaylistSong? NewSong { get; } = newSong;
|
||||
}
|
||||
9
Harmonia.Core/Player/PlaylistChangedEventArgs.cs
Normal file
9
Harmonia.Core/Player/PlaylistChangedEventArgs.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Harmonia.Core.Playlists;
|
||||
|
||||
namespace Harmonia.Core.Player;
|
||||
|
||||
//public class PlaylistChangedEventArgs(Playlist? oldPlaylist, Playlist? newPlaylist) : EventArgs
|
||||
//{
|
||||
// public Playlist? OldPlaylist { get; } = oldPlaylist;
|
||||
// public Playlist? NewPlaylist { get; } = newPlaylist;
|
||||
//}
|
||||
@@ -8,9 +8,11 @@ public interface IPlaylistManager
|
||||
Task InitializeAsync();
|
||||
Task<Playlist> AddPlaylistAsync();
|
||||
void RemovePlaylist(Playlist playlist);
|
||||
Playlist? GetPlaylist(PlaylistSong playlistSong);
|
||||
Playlist? FindPlaylistContaining(PlaylistSong playlistSong);
|
||||
Playlist? FindPlaylistContaining(string playlistSongUID);
|
||||
|
||||
event EventHandler? CurrentPlaylistChanged;
|
||||
event EventHandler<PlaylistChangedEventArgs>? CurrentPlaylistChanged;
|
||||
event EventHandler<PlaylistAddedEventArgs> PlaylistAdded;
|
||||
event EventHandler<PlaylistRemovedEventArgs> PlaylistRemoved;
|
||||
event EventHandler<PlaylistSaveFailedEventArgs>? PlaylistSaveFailed;
|
||||
}
|
||||
@@ -4,10 +4,5 @@ namespace Harmonia.Core.Playlists;
|
||||
|
||||
public interface IPlaylistRepository : IRepository<Playlist>
|
||||
{
|
||||
Playlist? GetPlaylist(PlaylistSong playlistSong);
|
||||
//void AddPlaylist();
|
||||
//void RemovePlaylist(Playlist playlist);
|
||||
|
||||
//event EventHandler<PlaylistAddedEventArgs> PlaylistAdded;
|
||||
//event EventHandler<PlaylistRemovedEventArgs> PlaylistRemoved;
|
||||
}
|
||||
@@ -5,15 +5,102 @@ namespace Harmonia.Core.Playlists;
|
||||
|
||||
public class Playlist
|
||||
{
|
||||
private readonly List<PlaylistSong> _songs = [];
|
||||
private readonly List<GroupOption> _groupOptions = [];
|
||||
private readonly List<SortOption> _sortOptions = [];
|
||||
|
||||
public string UID { get; init; } = Guid.NewGuid().ToString();
|
||||
public string? Name { get; set; }
|
||||
public List<PlaylistSong> Songs { get; init; } = []; // TODO: Change to "private init" once deserialization is fixed
|
||||
public List<GroupOption> GroupOptions { get; set; } = [];
|
||||
public List<SortOption> SortOptions { get; set; } = [];
|
||||
public bool IsLocked { get; set; }
|
||||
public string? Name { get; private set; }
|
||||
public IReadOnlyList<PlaylistSong> Songs => _songs;
|
||||
public IReadOnlyList<GroupOption> GroupOptions => _groupOptions;
|
||||
public IReadOnlyList<SortOption> SortOptions => _sortOptions;
|
||||
public bool IsLocked { get; private set; }
|
||||
|
||||
public event EventHandler<PlaylistUpdatedEventArgs>? PlaylistUpdated;
|
||||
|
||||
public Playlist()
|
||||
{
|
||||
}
|
||||
|
||||
public Playlist(string? name)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
internal static Playlist Restore(string uid, string? name, IEnumerable<PlaylistSong> songs, IEnumerable<GroupOption> groupOptions, IEnumerable<SortOption> sortOptions, bool isLocked)
|
||||
{
|
||||
Playlist playlist = new(name)
|
||||
{
|
||||
UID = uid,
|
||||
IsLocked = isLocked
|
||||
};
|
||||
|
||||
playlist._songs.AddRange(songs);
|
||||
playlist._groupOptions.AddRange(groupOptions);
|
||||
playlist._sortOptions.AddRange(sortOptions);
|
||||
|
||||
return playlist;
|
||||
}
|
||||
|
||||
public int IndexOf(PlaylistSong playlistSong)
|
||||
{
|
||||
return _songs.IndexOf(playlistSong);
|
||||
}
|
||||
|
||||
public void SetName(string name)
|
||||
{
|
||||
if (string.Equals(Name, name, StringComparison.Ordinal))
|
||||
return;
|
||||
|
||||
Name = name;
|
||||
|
||||
PlaylistUpdatedEventArgs eventArgs = new()
|
||||
{
|
||||
Action = PlaylistUpdateAction.Rename,
|
||||
Index = -1,
|
||||
Count = 0,
|
||||
Songs = []
|
||||
};
|
||||
|
||||
PlaylistUpdated?.Invoke(this, eventArgs);
|
||||
}
|
||||
|
||||
public void Lock()
|
||||
{
|
||||
if (IsLocked)
|
||||
return;
|
||||
|
||||
IsLocked = true;
|
||||
|
||||
PlaylistUpdatedEventArgs eventArgs = new()
|
||||
{
|
||||
Action = PlaylistUpdateAction.Lock,
|
||||
Index = -1,
|
||||
Count = 0,
|
||||
Songs = []
|
||||
};
|
||||
|
||||
PlaylistUpdated?.Invoke(this, eventArgs);
|
||||
}
|
||||
|
||||
public void Unlock()
|
||||
{
|
||||
if (IsLocked == false)
|
||||
return;
|
||||
|
||||
IsLocked = false;
|
||||
|
||||
PlaylistUpdatedEventArgs eventArgs = new()
|
||||
{
|
||||
Action = PlaylistUpdateAction.Unlock,
|
||||
Index = -1,
|
||||
Count = 0,
|
||||
Songs = []
|
||||
};
|
||||
|
||||
PlaylistUpdated?.Invoke(this, eventArgs);
|
||||
}
|
||||
|
||||
public void AddSong(Song song, int? index = null)
|
||||
{
|
||||
AddSongs([song], index);
|
||||
@@ -34,9 +121,9 @@ public class Playlist
|
||||
if (playlistSongs.Length == 0)
|
||||
return;
|
||||
|
||||
int insertIndex = index ?? Songs.Count;
|
||||
int insertIndex = index ?? _songs.Count;
|
||||
|
||||
Songs.InsertRange(insertIndex, playlistSongs);
|
||||
_songs.InsertRange(insertIndex, playlistSongs);
|
||||
|
||||
PlaylistUpdatedEventArgs eventArgs = new()
|
||||
{
|
||||
@@ -51,7 +138,7 @@ public class Playlist
|
||||
|
||||
public void MoveSong(PlaylistSong playlistSong, int newIndex)
|
||||
{
|
||||
int currentIndex = Songs.IndexOf(playlistSong);
|
||||
int currentIndex = _songs.IndexOf(playlistSong);
|
||||
|
||||
MoveSong(currentIndex, newIndex);
|
||||
}
|
||||
@@ -64,10 +151,10 @@ public class Playlist
|
||||
if (oldIndex == newIndex)
|
||||
return;
|
||||
|
||||
PlaylistSong playlistSong = Songs[oldIndex];
|
||||
PlaylistSong playlistSong = _songs[oldIndex];
|
||||
|
||||
Songs.Remove(playlistSong);
|
||||
Songs.Insert(newIndex, playlistSong);
|
||||
_songs.Remove(playlistSong);
|
||||
_songs.Insert(newIndex, playlistSong);
|
||||
|
||||
PlaylistUpdatedEventArgs eventArgs = new()
|
||||
{
|
||||
@@ -83,9 +170,12 @@ public class Playlist
|
||||
|
||||
public void SortSongs(PlaylistSong[] playlistSongs, SortOption[] sortOptions)
|
||||
{
|
||||
if (IsLocked)
|
||||
return;
|
||||
|
||||
Dictionary<int, PlaylistSong> oldPlaylistSongs = playlistSongs
|
||||
.OrderBy(Songs.IndexOf)
|
||||
.ToDictionary(Songs.IndexOf, playlistSong => playlistSong);
|
||||
.OrderBy(_songs.IndexOf)
|
||||
.ToDictionary(_songs.IndexOf, playlistSong => playlistSong);
|
||||
|
||||
Song[] songs = [.. playlistSongs.Select(playlistSong => playlistSong.Song)];
|
||||
Song[] sortedSongs = [.. songs.SortBy(sortOptions)];
|
||||
@@ -101,8 +191,8 @@ public class Playlist
|
||||
if (newPlaylistSong == playlistSong)
|
||||
continue;
|
||||
|
||||
Songs.RemoveAt(index);
|
||||
Songs.Insert(index, newPlaylistSong);
|
||||
_songs.RemoveAt(index);
|
||||
_songs.Insert(index, newPlaylistSong);
|
||||
}
|
||||
|
||||
PlaylistUpdatedEventArgs eventArgs = new()
|
||||
@@ -116,6 +206,50 @@ public class Playlist
|
||||
PlaylistUpdated?.Invoke(this, eventArgs);
|
||||
}
|
||||
|
||||
public void Randomize(PlaylistSong[] playlistSongs)
|
||||
{
|
||||
if (IsLocked)
|
||||
return;
|
||||
|
||||
int[] originalIndexes = [.. playlistSongs.Select(playlistSong => _songs.IndexOf(playlistSong))];
|
||||
PlaylistSong[] shuffledSongs = [.. playlistSongs.Shuffle()];
|
||||
|
||||
for (int i = 0; i < originalIndexes.Length; i++)
|
||||
_songs[originalIndexes[i]] = shuffledSongs[i];
|
||||
|
||||
PlaylistUpdatedEventArgs eventArgs = new()
|
||||
{
|
||||
Action = PlaylistUpdateAction.Reset,
|
||||
Index = -1,
|
||||
Count = playlistSongs.Length,
|
||||
Songs = playlistSongs
|
||||
};
|
||||
|
||||
PlaylistUpdated?.Invoke(this, eventArgs);
|
||||
}
|
||||
|
||||
public void Reverse(PlaylistSong[] playlistSongs)
|
||||
{
|
||||
if (IsLocked)
|
||||
return;
|
||||
|
||||
int[] originalIndexes = [.. playlistSongs.Select(playlistSong => _songs.IndexOf(playlistSong))];
|
||||
PlaylistSong[] reversedSongs = [.. originalIndexes.Select(i => _songs[i]).Reverse()];
|
||||
|
||||
for (int i = 0; i < originalIndexes.Length; i++)
|
||||
_songs[originalIndexes[i]] = reversedSongs[i];
|
||||
|
||||
PlaylistUpdatedEventArgs eventArgs = new()
|
||||
{
|
||||
Action = PlaylistUpdateAction.Reset,
|
||||
Index = -1,
|
||||
Count = playlistSongs.Length,
|
||||
Songs = playlistSongs
|
||||
};
|
||||
|
||||
PlaylistUpdated?.Invoke(this, eventArgs);
|
||||
}
|
||||
|
||||
public void RemoveSong(int index)
|
||||
{
|
||||
RemoveSongs(index, 1);
|
||||
@@ -123,7 +257,7 @@ public class Playlist
|
||||
|
||||
public void RemoveSongs(int index, int count)
|
||||
{
|
||||
PlaylistSong[] playlistSongs = [.. Songs.GetRange(index, count)];
|
||||
PlaylistSong[] playlistSongs = [.. _songs.GetRange(index, count)];
|
||||
|
||||
RemoveSongs(playlistSongs);
|
||||
}
|
||||
@@ -142,7 +276,7 @@ public class Playlist
|
||||
|
||||
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||
{
|
||||
if (Songs.Remove(playlistSong))
|
||||
if (_songs.Remove(playlistSong))
|
||||
{
|
||||
removedSongs.Add(playlistSong);
|
||||
}
|
||||
@@ -164,6 +298,8 @@ public class Playlist
|
||||
|
||||
public void ImportTags(Song[] songs)
|
||||
{
|
||||
List<PlaylistSong> updatedPlaylistSongs = [];
|
||||
|
||||
foreach (Song song in songs)
|
||||
{
|
||||
PlaylistSong[] playlistSongs = [.. Songs.Where(playlistSong =>
|
||||
@@ -171,10 +307,24 @@ public class Playlist
|
||||
|
||||
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||
{
|
||||
//playlistSong.Song = song;
|
||||
//playlistSong.Song.Update(song);
|
||||
playlistSong.Song.Update(song);
|
||||
}
|
||||
|
||||
updatedPlaylistSongs.AddRange(playlistSongs);
|
||||
}
|
||||
|
||||
if (updatedPlaylistSongs.Count == 0)
|
||||
return;
|
||||
|
||||
PlaylistUpdatedEventArgs eventArgs = new()
|
||||
{
|
||||
Action = PlaylistUpdateAction.Refresh,
|
||||
Index = -1,
|
||||
Count = updatedPlaylistSongs.Count,
|
||||
Songs = [.. updatedPlaylistSongs]
|
||||
};
|
||||
|
||||
PlaylistUpdated?.Invoke(this, eventArgs);
|
||||
}
|
||||
|
||||
public void RemoveMissingSongs()
|
||||
|
||||
7
Harmonia.Core/Playlists/PlaylistChangedEventArgs.cs
Normal file
7
Harmonia.Core/Playlists/PlaylistChangedEventArgs.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Harmonia.Core.Playlists;
|
||||
|
||||
public class PlaylistChangedEventArgs(Playlist? oldPlaylist, Playlist? newPlaylist) : EventArgs
|
||||
{
|
||||
public readonly Playlist? OldPlaylist = oldPlaylist;
|
||||
public readonly Playlist? NewPlaylist = newPlaylist;
|
||||
}
|
||||
60
Harmonia.Core/Playlists/PlaylistDto.cs
Normal file
60
Harmonia.Core/Playlists/PlaylistDto.cs
Normal file
@@ -0,0 +1,60 @@
|
||||
using Harmonia.Core.Models;
|
||||
|
||||
namespace Harmonia.Core.Playlists;
|
||||
|
||||
internal sealed record PlaylistDto
|
||||
{
|
||||
public string UID { get; init; } = Guid.NewGuid().ToString();
|
||||
public string? Name { get; init; }
|
||||
public List<PlaylistSongDto> Songs { get; init; } = [];
|
||||
public List<GroupOption> GroupOptions { get; init; } = [];
|
||||
public List<SortOption> SortOptions { get; init; } = [];
|
||||
public bool IsLocked { get; init; }
|
||||
|
||||
public static PlaylistDto FromPlaylist(Playlist playlist)
|
||||
{
|
||||
return new PlaylistDto
|
||||
{
|
||||
UID = playlist.UID,
|
||||
Name = playlist.Name,
|
||||
Songs = [.. playlist.Songs.Select(PlaylistSongDto.FromPlaylistSong)],
|
||||
GroupOptions = [.. playlist.GroupOptions],
|
||||
SortOptions = [.. playlist.SortOptions],
|
||||
IsLocked = playlist.IsLocked
|
||||
};
|
||||
}
|
||||
|
||||
public Playlist ToPlaylist()
|
||||
{
|
||||
return Playlist.Restore(
|
||||
UID,
|
||||
Name,
|
||||
Songs.Select(song => song.ToPlaylistSong()),
|
||||
GroupOptions,
|
||||
SortOptions,
|
||||
IsLocked);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record PlaylistSongDto
|
||||
{
|
||||
public string UID { get; init; } = Guid.NewGuid().ToString();
|
||||
public required Song Song { get; init; }
|
||||
|
||||
public static PlaylistSongDto FromPlaylistSong(PlaylistSong playlistSong)
|
||||
{
|
||||
return new PlaylistSongDto
|
||||
{
|
||||
UID = playlistSong.UID,
|
||||
Song = playlistSong.Song
|
||||
};
|
||||
}
|
||||
|
||||
public PlaylistSong ToPlaylistSong()
|
||||
{
|
||||
return new PlaylistSong(Song)
|
||||
{
|
||||
UID = UID
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ public class PlaylistManager : IPlaylistManager
|
||||
{
|
||||
private readonly IPlaylistRepository playlistRepository;
|
||||
private readonly List<Playlist> _playlists = [];
|
||||
private readonly Dictionary<string, Playlist> _playlistsBySongUid = [];
|
||||
|
||||
public IReadOnlyList<Playlist> Playlists => _playlists;
|
||||
|
||||
@@ -16,14 +17,17 @@ public class PlaylistManager : IPlaylistManager
|
||||
}
|
||||
set
|
||||
{
|
||||
Playlist? oldPlaylist = _currentPlaylist;
|
||||
_currentPlaylist = value;
|
||||
CurrentPlaylistChanged?.Invoke(this, new());
|
||||
|
||||
CurrentPlaylistChanged?.Invoke(this, new(oldPlaylist, _currentPlaylist));
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler? CurrentPlaylistChanged;
|
||||
public event EventHandler<PlaylistChangedEventArgs>? CurrentPlaylistChanged;
|
||||
public event EventHandler<PlaylistAddedEventArgs>? PlaylistAdded;
|
||||
public event EventHandler<PlaylistRemovedEventArgs>? PlaylistRemoved;
|
||||
public event EventHandler<PlaylistSaveFailedEventArgs>? PlaylistSaveFailed;
|
||||
|
||||
public PlaylistManager(IPlaylistRepository playlistRepository)
|
||||
{
|
||||
@@ -37,6 +41,11 @@ public class PlaylistManager : IPlaylistManager
|
||||
foreach (Playlist playlist in _playlists)
|
||||
{
|
||||
playlist.PlaylistUpdated += OnPlaylistUpdated;
|
||||
|
||||
foreach (PlaylistSong song in playlist.Songs)
|
||||
{
|
||||
_playlistsBySongUid[song.UID] = playlist;
|
||||
}
|
||||
}
|
||||
|
||||
CurrentPlaylist = _playlists.Count > 0 ? _playlists[0] : await AddPlaylistAsync();
|
||||
@@ -44,15 +53,17 @@ public class PlaylistManager : IPlaylistManager
|
||||
|
||||
public async Task<Playlist> AddPlaylistAsync()
|
||||
{
|
||||
Playlist playlist = new()
|
||||
{
|
||||
Name = "New Playlist"
|
||||
};
|
||||
Playlist playlist = new("New Playlist");
|
||||
|
||||
playlist.PlaylistUpdated += OnPlaylistUpdated;
|
||||
_playlists.Add(playlist);
|
||||
|
||||
await playlistRepository.SaveAsync(playlist);
|
||||
foreach (PlaylistSong song in playlist.Songs)
|
||||
{
|
||||
_playlistsBySongUid[song.UID] = playlist;
|
||||
}
|
||||
|
||||
await SavePlaylistAsync(playlist);
|
||||
|
||||
PlaylistAdded?.Invoke(this, new(playlist));
|
||||
|
||||
@@ -64,14 +75,29 @@ public class PlaylistManager : IPlaylistManager
|
||||
playlist.PlaylistUpdated -= OnPlaylistUpdated;
|
||||
_playlists.Remove(playlist);
|
||||
|
||||
foreach (PlaylistSong song in playlist.Songs)
|
||||
{
|
||||
_playlistsBySongUid.Remove(song.UID);
|
||||
}
|
||||
|
||||
playlistRepository.Delete(playlist);
|
||||
|
||||
if (CurrentPlaylist == playlist)
|
||||
{
|
||||
CurrentPlaylist = _playlists.Count > 0 ? _playlists[0] : null;
|
||||
}
|
||||
|
||||
PlaylistRemoved?.Invoke(this, new(playlist));
|
||||
}
|
||||
|
||||
public Playlist? GetPlaylist(PlaylistSong playlistSong)
|
||||
public Playlist? FindPlaylistContaining(PlaylistSong playlistSong)
|
||||
{
|
||||
return _playlists.FirstOrDefault(playlist => playlist.Songs.Any(song => song.UID == playlistSong.UID));
|
||||
return _playlistsBySongUid.GetValueOrDefault(playlistSong.UID);
|
||||
}
|
||||
|
||||
public Playlist? FindPlaylistContaining(string playlistSongUID)
|
||||
{
|
||||
return _playlistsBySongUid.GetValueOrDefault(playlistSongUID);
|
||||
}
|
||||
|
||||
private async void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
|
||||
@@ -79,6 +105,34 @@ public class PlaylistManager : IPlaylistManager
|
||||
if (sender is not Playlist playlist)
|
||||
return;
|
||||
|
||||
await playlistRepository.SaveAsync(playlist);
|
||||
switch (e.Action)
|
||||
{
|
||||
case PlaylistUpdateAction.Add:
|
||||
foreach (PlaylistSong song in e.Songs)
|
||||
{
|
||||
_playlistsBySongUid[song.UID] = playlist;
|
||||
}
|
||||
break;
|
||||
case PlaylistUpdateAction.Remove:
|
||||
foreach (PlaylistSong song in e.Songs)
|
||||
{
|
||||
_playlistsBySongUid.Remove(song.UID);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await SavePlaylistAsync(playlist);
|
||||
}
|
||||
|
||||
private async Task SavePlaylistAsync(Playlist playlist)
|
||||
{
|
||||
try
|
||||
{
|
||||
await playlistRepository.SaveAsync(playlist);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
PlaylistSaveFailed?.Invoke(this, new(playlist, ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,32 @@
|
||||
using Harmonia.Core.Data;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Harmonia.Core.Playlists;
|
||||
|
||||
public class PlaylistRepository : JsonFileRepository<Playlist>, IPlaylistRepository
|
||||
{
|
||||
private static readonly JsonSerializerOptions _options = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
IgnoreReadOnlyProperties = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
protected override string DirectoryName => Path.Combine("Playlists");
|
||||
|
||||
//public PlaylistRepository()
|
||||
//{
|
||||
// List<Playlist> playlists = Get();
|
||||
|
||||
// foreach (Playlist playlist in playlists)
|
||||
// {
|
||||
// playlist.PlaylistUpdated += OnPlaylistUpdated;
|
||||
// }
|
||||
|
||||
// if (playlists.Count == 0)
|
||||
// AddPlaylist();
|
||||
//}
|
||||
|
||||
//private void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
|
||||
//{
|
||||
// if (sender is not Playlist playlist)
|
||||
// return;
|
||||
|
||||
// Save(playlist);
|
||||
//}
|
||||
|
||||
public Playlist? GetPlaylist(PlaylistSong playlistSong)
|
||||
protected override async Task<Playlist> DeserializeAsync(Stream stream)
|
||||
{
|
||||
return Get().FirstOrDefault(playlist => playlist.Songs.Contains(playlistSong));
|
||||
PlaylistDto dto = await JsonSerializer.DeserializeAsync<PlaylistDto>(stream, _options) ?? new();
|
||||
|
||||
return dto.ToPlaylist();
|
||||
}
|
||||
|
||||
protected override Task<string> SerializeAsync(Playlist playlist)
|
||||
{
|
||||
PlaylistDto dto = PlaylistDto.FromPlaylist(playlist);
|
||||
|
||||
return Task.FromResult(JsonSerializer.Serialize(dto, _options));
|
||||
}
|
||||
|
||||
protected override string GetNewFileName()
|
||||
@@ -45,28 +42,4 @@ public class PlaylistRepository : JsonFileRepository<Playlist>, IPlaylistReposit
|
||||
|
||||
throw new Exception("Unable to determine new fileName");
|
||||
}
|
||||
|
||||
//public event EventHandler<PlaylistAddedEventArgs>? PlaylistAdded;
|
||||
//public event EventHandler<PlaylistRemovedEventArgs>? PlaylistRemoved;
|
||||
|
||||
//public void AddPlaylist()
|
||||
//{
|
||||
// Playlist playlist = new()
|
||||
// {
|
||||
// Name = "New Playlist"
|
||||
// };
|
||||
|
||||
// playlist.PlaylistUpdated += OnPlaylistUpdated;
|
||||
|
||||
// Save(playlist);
|
||||
// PlaylistAdded?.Invoke(this, new(playlist));
|
||||
//}
|
||||
|
||||
//public void RemovePlaylist(Playlist playlist)
|
||||
//{
|
||||
// playlist.PlaylistUpdated -= OnPlaylistUpdated;
|
||||
|
||||
// Delete(playlist);
|
||||
// PlaylistRemoved?.Invoke(this, new(playlist));
|
||||
//}
|
||||
}
|
||||
7
Harmonia.Core/Playlists/PlaylistSaveFailedEventArgs.cs
Normal file
7
Harmonia.Core/Playlists/PlaylistSaveFailedEventArgs.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Harmonia.Core.Playlists;
|
||||
|
||||
public class PlaylistSaveFailedEventArgs(Playlist playlist, Exception exception) : EventArgs
|
||||
{
|
||||
public Playlist Playlist { get; } = playlist;
|
||||
public Exception Exception { get; } = exception;
|
||||
}
|
||||
@@ -17,5 +17,12 @@ public enum PlaylistUpdateAction
|
||||
/// <summary>
|
||||
/// The contents of the collection changed dramatically.
|
||||
/// </summary>
|
||||
Reset
|
||||
Reset,
|
||||
/// <summary>
|
||||
/// The tags of the songs in the collection were refreshed.
|
||||
/// </summary>
|
||||
Refresh,
|
||||
Rename,
|
||||
Lock,
|
||||
Unlock
|
||||
}
|
||||
@@ -9,22 +9,19 @@ public class AudioFileScanner(IAudioEngine audioEngine, ITagResolver tagResolver
|
||||
{
|
||||
public async Task<Song[]> GetSongsAsync(string[] fileNames, CancellationToken cancellationToken)
|
||||
{
|
||||
List<Song> songs = [];
|
||||
Song?[] songs = new Song?[fileNames.Length];
|
||||
|
||||
foreach (string fileName in fileNames)
|
||||
await Parallel.ForEachAsync(Enumerable.Range(0, fileNames.Length), cancellationToken, async (index, token) =>
|
||||
{
|
||||
string fileName = fileNames[index];
|
||||
|
||||
if (string.IsNullOrWhiteSpace(fileName) || File.Exists(fileName) == false)
|
||||
continue;
|
||||
return;
|
||||
|
||||
Song? song = await GetSongAsync(fileName, cancellationToken);
|
||||
songs[index] = await GetSongAsync(fileName, token);
|
||||
});
|
||||
|
||||
if (song == null)
|
||||
continue;
|
||||
|
||||
songs.Add(song);
|
||||
}
|
||||
|
||||
return [.. songs];
|
||||
return [.. songs.Where(song => song != null)!];
|
||||
}
|
||||
|
||||
private async Task<Song> GetSongAsync(string fileName, CancellationToken cancellationToken)
|
||||
|
||||
@@ -21,7 +21,7 @@ internal class TestAudioPlayer(IAudioEngine audioEngine, IPlaylistManager playli
|
||||
if (Playlist == null || PlayingSong == null)
|
||||
return -1;
|
||||
|
||||
return Playlist.Songs.IndexOf(PlayingSong);
|
||||
return Playlist.IndexOf(PlayingSong);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,7 @@ public class AudioPlayerTests
|
||||
new Song() { FileName = "Song7.mp3" }
|
||||
];
|
||||
|
||||
Playlist playlist = new()
|
||||
{
|
||||
Name = "Playlist1"
|
||||
};
|
||||
Playlist playlist = new("Playlist1");
|
||||
|
||||
playlist.AddSongs(songs);
|
||||
|
||||
@@ -65,7 +62,7 @@ public class AudioPlayerTests
|
||||
|
||||
_playlistManager = Substitute.For<IPlaylistManager>();
|
||||
_playlistManager.Playlists.Returns([playlist]);
|
||||
_playlistManager.GetPlaylist(Arg.Any<PlaylistSong>()).Returns(playlist);
|
||||
_playlistManager.FindPlaylistContaining(Arg.Any<PlaylistSong>()).Returns(playlist);
|
||||
|
||||
_audioPlayer = new TestAudioPlayer(_audioEngine, _playlistManager);
|
||||
_audioPlayer.SetPlaylist(playlist);
|
||||
|
||||
@@ -16,14 +16,14 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
||||
<PackageReference Include="NSubstitute" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.0" />
|
||||
<PackageReference Include="NSubstitute" Version="6.2.0" />
|
||||
<PackageReference Include="Shouldly" Version="4.3.0" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="xunit.v3" Version="3.2.2" />
|
||||
<PackageReference Include="xunit.v3" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -8,6 +8,32 @@ namespace Harmonia.Tests;
|
||||
|
||||
public class PlaylistTests
|
||||
{
|
||||
[Fact]
|
||||
public void Create_Playlist()
|
||||
{
|
||||
Playlist playlist = new();
|
||||
playlist.Name.ShouldBeNull();
|
||||
playlist.Songs.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create_Playlist_With_Name()
|
||||
{
|
||||
Playlist playlist = new("My Playlist");
|
||||
playlist.Name.ShouldBe("My Playlist");
|
||||
playlist.Songs.ShouldBeEmpty();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Set_Playlist_Name()
|
||||
{
|
||||
Playlist playlist = new();
|
||||
playlist.Name.ShouldBeNull();
|
||||
|
||||
playlist.SetName("My Playlist");
|
||||
playlist.Name.ShouldBe("My Playlist");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Add_Songs()
|
||||
{
|
||||
@@ -37,6 +63,70 @@ public class PlaylistTests
|
||||
playlist.Songs[1].Song.FileName.ShouldBe("Song5.mp3");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Move_Songs()
|
||||
{
|
||||
Playlist playlist = new();
|
||||
|
||||
Song[] songs =
|
||||
[
|
||||
new Song() { FileName = "Song1.mp3" },
|
||||
new Song() { FileName = "Song2.mp3" },
|
||||
new Song() { FileName = "Song3.mp3" },
|
||||
new Song() { FileName = "Song4.mp3" },
|
||||
new Song() { FileName = "Song5.mp3" },
|
||||
];
|
||||
|
||||
playlist.AddSongs(songs);
|
||||
|
||||
playlist.MoveSong(2, 3);
|
||||
|
||||
string[] expectedFileNames =
|
||||
[
|
||||
"Song1.mp3",
|
||||
"Song2.mp3",
|
||||
"Song4.mp3",
|
||||
"Song3.mp3",
|
||||
"Song5.mp3"
|
||||
];
|
||||
|
||||
playlist.Songs.Select(x => x.Song.FileName).ToArray()
|
||||
.ShouldBeEquivalentTo(expectedFileNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Duplicate_Songs()
|
||||
{
|
||||
Playlist playlist = new();
|
||||
|
||||
Song[] songs =
|
||||
[
|
||||
new Song() { FileName = "Song1.mp3" },
|
||||
new Song() { FileName = "Song2.mp3" },
|
||||
new Song() { FileName = "Song3.mp3" },
|
||||
new Song() { FileName = "Song4.mp3" },
|
||||
new Song() { FileName = "Song5.mp3" },
|
||||
new Song() { FileName = "Song2.mp3" },
|
||||
new Song() { FileName = "Song3.mp3" },
|
||||
];
|
||||
|
||||
playlist.AddSongs(songs);
|
||||
|
||||
playlist.RemoveDuplicateSongs();
|
||||
|
||||
string[] expectedFileNames =
|
||||
[
|
||||
"Song1.mp3",
|
||||
"Song2.mp3",
|
||||
"Song3.mp3",
|
||||
"Song4.mp3",
|
||||
"Song5.mp3"
|
||||
];
|
||||
|
||||
playlist.Songs.Select(x => x.Song.FileName).ToArray()
|
||||
.ShouldBeEquivalentTo(expectedFileNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sort_Songs()
|
||||
{
|
||||
@@ -74,6 +164,34 @@ public class PlaylistTests
|
||||
.ShouldBeEquivalentTo(expectedSortedFileNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reverse_Songs()
|
||||
{
|
||||
Playlist playlist = new();
|
||||
|
||||
Song[] songs =
|
||||
[
|
||||
new Song() { FileName = "Song1.mp3" },
|
||||
new Song() { FileName = "Song2.mp3" },
|
||||
new Song() { FileName = "Song3.mp3" }
|
||||
];
|
||||
|
||||
playlist.AddSongs(songs);
|
||||
|
||||
PlaylistSong[] playlistSongs = [.. playlist.Songs];
|
||||
|
||||
playlist.Reverse(playlistSongs);
|
||||
|
||||
string[] expectedReversedFileNames =
|
||||
[
|
||||
"Song3.mp3",
|
||||
"Song2.mp3",
|
||||
"Song1.mp3"
|
||||
];
|
||||
playlist.Songs.Select(x => x.Song.FileName).ToArray()
|
||||
.ShouldBeEquivalentTo(expectedReversedFileNames);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_Songs()
|
||||
{
|
||||
@@ -101,7 +219,7 @@ public class PlaylistTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Lock_Playlist()
|
||||
public void Lock_And_Unlock_Playlist()
|
||||
{
|
||||
Playlist playlist = new();
|
||||
|
||||
@@ -114,7 +232,11 @@ public class PlaylistTests
|
||||
|
||||
playlist.AddSongs(songs);
|
||||
|
||||
playlist.IsLocked = true;
|
||||
playlist.IsLocked.ShouldBeFalse();
|
||||
|
||||
playlist.Lock();
|
||||
|
||||
playlist.IsLocked.ShouldBeTrue();
|
||||
|
||||
Song song = new() { FileName = "Song4.mp3" };
|
||||
playlist.AddSong(song);
|
||||
@@ -124,11 +246,21 @@ public class PlaylistTests
|
||||
playlist.RemoveSong(0);
|
||||
|
||||
playlist.Songs.Count.ShouldBe(3);
|
||||
}
|
||||
|
||||
//public void Get_Playlists()
|
||||
//{
|
||||
// //PlaylistRepository playlistRepository = new();
|
||||
// //playlistRepository.Get().Returns()
|
||||
//}
|
||||
playlist.MoveSong(0, 2);
|
||||
|
||||
string[] expectedMovedFileNamesOnLockedPlaylist =
|
||||
[
|
||||
"Song1.mp3",
|
||||
"Song2.mp3",
|
||||
"Song3.mp3"
|
||||
];
|
||||
|
||||
playlist.Songs.Select(x => x.Song.FileName).ToArray()
|
||||
.ShouldBeEquivalentTo(expectedMovedFileNamesOnLockedPlaylist);
|
||||
|
||||
playlist.Unlock();
|
||||
|
||||
playlist.IsLocked.ShouldBeFalse();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.1.0" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="12.1.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -11,16 +11,16 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="12.1.0" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.0" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.0" />
|
||||
<PackageReference Include="Avalonia" Version="12.1.2" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.2" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.2" />
|
||||
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Include="Egolds.Xaml.Behaviors.Interactions.Animated" Version="11.3.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.10" />
|
||||
<PackageReference Include="Semi.Avalonia" Version="12.1.0" />
|
||||
<PackageReference Include="Egolds.Xaml.Behaviors.Interactions.Animated" Version="11.4.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.12" />
|
||||
<PackageReference Include="Semi.Avalonia" Version="12.1.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -433,7 +433,7 @@ public class PlaylistViewModel : ViewModelBase
|
||||
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
|
||||
return;
|
||||
|
||||
int selectedPlaylistSongIndex = Playlist.Songs.IndexOf(SelectedPlaylistSongs[0]);
|
||||
int selectedPlaylistSongIndex = Playlist.IndexOf(SelectedPlaylistSongs[0]);
|
||||
|
||||
if (selectedPlaylistSongIndex == -1)
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Harmonia.Core.Extensions;
|
||||
using Harmonia.Core.Extensions;
|
||||
using Harmonia.WinUI.Caching;
|
||||
using Harmonia.WinUI.Messaging;
|
||||
using Harmonia.WinUI.Session;
|
||||
using Harmonia.WinUI.Storage;
|
||||
using Harmonia.WinUI.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -23,10 +25,20 @@ public partial class App : Application
|
||||
//services.AddSingleton<MainViewModel>();
|
||||
services.AddSingleton<PlayerViewModel>();
|
||||
services.AddSingleton<PlayingSongViewModel>();
|
||||
services.AddSingleton<PlaylistViewModel>();
|
||||
services.AddSingleton<PlaylistsViewModel>();
|
||||
services.AddSingleton<PlaylistDetailViewModel>();
|
||||
|
||||
services.AddSingleton<IAudioBitmapImageCache, AudioBitmapImageCache>();
|
||||
services.AddSingleton<IStorageProvider, WindowsStorageProvider>();
|
||||
services.AddSingleton<IMessageService, WindowsMessageService>();
|
||||
|
||||
services.AddSingleton<ISessionService, JsonSessionService>();
|
||||
services.AddSingleton(sp => sp.GetRequiredService<ISessionService>().Session.Window);
|
||||
services.AddSingleton(sp => sp.GetRequiredService<ISessionService>().Session.Player);
|
||||
services.AddSingleton(sp => sp.GetRequiredService<ISessionService>().Session.Playlist);
|
||||
|
||||
services.AddSingleton<ISessionRestorer, SessionRestorer>();
|
||||
services.AddSingleton<ISessionTracker, SessionTracker>();
|
||||
|
||||
services.AddHarmonia();
|
||||
|
||||
@@ -42,6 +54,15 @@ public partial class App : Application
|
||||
{
|
||||
await ServiceProvider.InitializeHarmoniaAsync();
|
||||
|
||||
ISessionService sessionService = ServiceProvider.GetRequiredService<ISessionService>();
|
||||
await sessionService.LoadAsync();
|
||||
|
||||
ISessionRestorer sessionRestorer = ServiceProvider.GetRequiredService<ISessionRestorer>();
|
||||
await sessionRestorer.RestoreAsync();
|
||||
|
||||
ISessionTracker sessionTracker = ServiceProvider.GetRequiredService<ISessionTracker>();
|
||||
sessionTracker.Start();
|
||||
|
||||
_mainWindow = ServiceProvider.GetRequiredService<MainWindow>();
|
||||
_mainWindow.Activate();
|
||||
}
|
||||
|
||||
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Black.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Black.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Bold.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Bold.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-ExtraBold.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-ExtraLight.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Light.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Light.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Medium.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Medium.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Regular.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Regular.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-SemiBold.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-SemiBold.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Thin.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/Lexend-Thin.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Black.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Black.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Bold.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Bold.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-ExtraBold.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-ExtraBold.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-ExtraLight.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Light.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Light.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Medium.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Medium.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Regular.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Regular.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-SemiBold.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-SemiBold.ttf
Normal file
Binary file not shown.
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Thin.ttf
Normal file
BIN
Harmonia.WinUI/Assets/Fonts/NotoSansJP-Thin.ttf
Normal file
Binary file not shown.
38
Harmonia.WinUI/Converters/BooleanToVisibilityConverter.cs
Normal file
38
Harmonia.WinUI/Converters/BooleanToVisibilityConverter.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Harmonia.WinUI.Converters;
|
||||
|
||||
public class BooleanToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public Visibility TrueValue { get; set; }
|
||||
public Visibility FalseValue { get; set; }
|
||||
|
||||
public BooleanToVisibilityConverter()
|
||||
{
|
||||
TrueValue = Visibility.Visible;
|
||||
FalseValue = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (!(value is bool))
|
||||
return null;
|
||||
|
||||
return (bool)value ? TrueValue : FalseValue;
|
||||
}
|
||||
|
||||
public object? ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (Equals(value, TrueValue))
|
||||
return true;
|
||||
|
||||
if (Equals(value, FalseValue))
|
||||
return false;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,44 @@
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Markup;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Harmonia.WinUI.Models;
|
||||
|
||||
namespace Harmonia.WinUI.Converters;
|
||||
|
||||
public sealed partial class VolumeStateToIconConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is not VolumeState volumeState)
|
||||
return null;
|
||||
|
||||
if (Application.Current.Resources[$"Volume{volumeState}Icon2"] is not string pathData)
|
||||
return null;
|
||||
|
||||
return XamlBindingHelper.ConvertValue(typeof(Geometry), pathData.Trim());
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class VolumeStateToFillConverter : IValueConverter
|
||||
{
|
||||
public object? Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return value is VolumeState.Muted ? parameter : null;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed partial class VolumeStateConverter : IValueConverter
|
||||
{
|
||||
public VolumeStateConverter()
|
||||
|
||||
@@ -14,9 +14,11 @@
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\Styles.xaml" />
|
||||
<None Remove="Views\PlayerView.xaml" />
|
||||
<None Remove="Views\PlaylistsView.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -29,6 +31,10 @@
|
||||
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\Fonts\*.ttf" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</ItemGroup>
|
||||
@@ -44,8 +50,8 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251219" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2526" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.3.1" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2705" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.4.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Harmonia.Core\Harmonia.Core.csproj" />
|
||||
@@ -55,6 +61,11 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Views\PlaylistsView.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Update="Resources\Geometry.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
@@ -68,7 +79,7 @@
|
||||
<Page Update="Views\PlayingSongView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Views\PlaylistView.xaml">
|
||||
<Page Update="Views\PlaylistDetailView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Views\PlayerView.xaml">
|
||||
@@ -96,5 +107,6 @@
|
||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
||||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -9,17 +9,38 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
Title="Harmonia.WinUI">
|
||||
Title="Harmonia.WinUI"
|
||||
Closed="OnMainWindowClosed">
|
||||
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
|
||||
<!--<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
|
||||
</StackPanel>-->
|
||||
<Grid>
|
||||
<Grid.Resources>
|
||||
<LinearGradientBrush x:Key="DarkTitleBarBackgroundBrush" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#0C0E12" Offset="0"/>
|
||||
<GradientStop Color="#0A0B0E" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Grid.Resources>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.RowSpan="2">
|
||||
<!-- Title Bar -->
|
||||
<Grid x:Name="AppTitleBar" Grid.Row="0" VerticalAlignment="Center" Padding="10" Background="{StaticResource DarkTitleBarBackgroundBrush}" BorderBrush="#3B332B" BorderThickness="0 0 0 1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<!--<Image Grid.Column="0" x:Name="TitleBarIcon" Source="ms-appx:///Assets/Images/MangaReader.png" Width="20" Height="20" Margin="0 0 10 0" />-->
|
||||
<TextBlock Grid.Column="1" x:Name="AppTitle" Text="{x:Bind Title, Mode=OneWay}" Style="{StaticResource CaptionTextBlockStyle}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
<!--<Grid Grid.Row="1" Grid.RowSpan="2">
|
||||
<Image Source="/Assets/Default.png" Stretch="UniformToFill" VerticalAlignment="Center" HorizontalAlignment="Center"></Image>
|
||||
<Canvas Background="#99000000"></Canvas>
|
||||
<Border>
|
||||
@@ -27,16 +48,18 @@
|
||||
<Media:BackdropBlurBrush Amount="20"></Media:BackdropBlurBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</Grid>
|
||||
<Grid Grid.Row="0">
|
||||
</Grid>-->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
<ColumnDefinition></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<views:PlayingSongView Grid.Column="0"></views:PlayingSongView>
|
||||
<views:PlaylistView Grid.Column="1"></views:PlaylistView>
|
||||
<views:PlaylistsView Grid.Column="1"></views:PlaylistsView>
|
||||
<views:PlaylistDetailView Grid.Column="2"></views:PlaylistDetailView>
|
||||
</Grid>
|
||||
<views:PlayerView Grid.Row="1"></views:PlayerView>
|
||||
<views:PlayerView Grid.Row="2"></views:PlayerView>
|
||||
</Grid>
|
||||
|
||||
</Window>
|
||||
|
||||
@@ -1,11 +1,129 @@
|
||||
using Harmonia.Core.Player;
|
||||
using Harmonia.WinUI.Session;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Harmonia.WinUI;
|
||||
|
||||
public sealed partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
private const string ApplicationTitle = "Harmonia";
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
private static partial EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);
|
||||
|
||||
private readonly ISessionService _sessionService;
|
||||
private readonly ISessionTracker _sessionTracker;
|
||||
private readonly IAudioPlayer _audioPlayer;
|
||||
|
||||
private bool _isSaved;
|
||||
|
||||
public MainWindow(ISessionService sessionService, ISessionTracker sessionTracker, IAudioPlayer audioPlayer)
|
||||
{
|
||||
_sessionService = sessionService;
|
||||
_sessionTracker = sessionTracker;
|
||||
|
||||
_audioPlayer = audioPlayer;
|
||||
_audioPlayer.PropertyChanged += OnAudioPlayerPropertyChanged;
|
||||
|
||||
InitializeComponent();
|
||||
InitializeTitleBar();
|
||||
InitializeWindowState();
|
||||
}
|
||||
|
||||
private void InitializeTitleBar()
|
||||
{
|
||||
Title = ApplicationTitle;
|
||||
ExtendsContentIntoTitleBar = true;
|
||||
SetTitleBar(AppTitleBar);
|
||||
}
|
||||
|
||||
private void InitializeWindowState()
|
||||
{
|
||||
if (AppWindow.Presenter is OverlappedPresenter presenter && _sessionService.Session.Window.IsMaximized)
|
||||
{
|
||||
presenter.Maximize();
|
||||
}
|
||||
|
||||
AppWindow.Changed += OnAppWindowChanged;
|
||||
}
|
||||
|
||||
private void OnAppWindowChanged(AppWindow sender, AppWindowChangedEventArgs args)
|
||||
{
|
||||
if (sender.Presenter is not OverlappedPresenter presenter)
|
||||
return;
|
||||
|
||||
// Ignore the minimized state so a minimize-then-close doesn't lose the maximized flag.
|
||||
if (presenter.State is OverlappedPresenterState.Minimized)
|
||||
return;
|
||||
|
||||
_sessionService.Session.Window.IsMaximized = presenter.State == OverlappedPresenterState.Maximized;
|
||||
}
|
||||
|
||||
private async void OnMainWindowClosed(object sender, WindowEventArgs args)
|
||||
{
|
||||
EnableSleepIdleTimeout();
|
||||
|
||||
if (_isSaved)
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
|
||||
_sessionTracker.Capture();
|
||||
await _sessionService.SaveAsync();
|
||||
|
||||
_isSaved = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnAudioPlayerPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(_audioPlayer.State):
|
||||
OnAudioPlayerStateChanged(sender, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAudioPlayerStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// SetThreadExecutionState is per-thread, so always marshal to the UI thread to
|
||||
// guarantee the ES_CONTINUOUS flags are set and cleared on the same thread.
|
||||
DispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
switch (_audioPlayer.State)
|
||||
{
|
||||
case Core.Engine.AudioPlaybackState.Playing:
|
||||
DisableSleepIdleTimeout();
|
||||
break;
|
||||
default:
|
||||
EnableSleepIdleTimeout();
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void EnableSleepIdleTimeout()
|
||||
{
|
||||
// Clear EXECUTION_STATE flags to allow the system to idle to sleep normally.
|
||||
SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
|
||||
}
|
||||
|
||||
private static void DisableSleepIdleTimeout()
|
||||
{
|
||||
// Prevent the system sleep idle time-out while still allowing the display to sleep.
|
||||
SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS | EXECUTION_STATE.ES_SYSTEM_REQUIRED);
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum EXECUTION_STATE : uint
|
||||
{
|
||||
ES_CONTINUOUS = 0x80000000,
|
||||
ES_SYSTEM_REQUIRED = 0x00000001,
|
||||
ES_DISPLAY_REQUIRED = 0x00000002,
|
||||
ES_AWAYMODE_REQUIRED = 0x00000040
|
||||
}
|
||||
}
|
||||
9
Harmonia.WinUI/Messaging/IMessageService.cs
Normal file
9
Harmonia.WinUI/Messaging/IMessageService.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Harmonia.WinUI.Messaging;
|
||||
|
||||
public interface IMessageService
|
||||
{
|
||||
Task<bool> ConfirmAsync(string title, string message);
|
||||
Task<string> InputTextAsync(string title, string message, string defaultText = "");
|
||||
}
|
||||
70
Harmonia.WinUI/Messaging/WindowsMessageService.cs
Normal file
70
Harmonia.WinUI/Messaging/WindowsMessageService.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
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;
|
||||
}
|
||||
|
||||
public async Task<string> InputTextAsync(string title, string message, string defaultText = "")
|
||||
{
|
||||
TextBox inputTextBox = new()
|
||||
{
|
||||
Text = defaultText,
|
||||
Margin = new Thickness(0, 10, 0, 0)
|
||||
};
|
||||
|
||||
inputTextBox.SelectAll();
|
||||
|
||||
ContentDialog dialog = new()
|
||||
{
|
||||
XamlRoot = MainWindow.Content.XamlRoot,
|
||||
Style = Application.Current.Resources["DefaultContentDialogStyle"] as Style,
|
||||
Title = title,
|
||||
Content = new StackPanel
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new TextBlock { Text = message },
|
||||
inputTextBox
|
||||
}
|
||||
},
|
||||
PrimaryButtonText = "OK",
|
||||
CloseButtonText = "Cancel",
|
||||
DefaultButton = ContentDialogButton.Primary
|
||||
};
|
||||
|
||||
ContentDialogResult result = await dialog.ShowAsync();
|
||||
|
||||
if (result == ContentDialogResult.Primary)
|
||||
{
|
||||
return inputTextBox.Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,7 @@
|
||||
<converters:DoubleToPercentConverter x:Key="DoubleToPercent" />
|
||||
<converters:RepeatStateConverter x:Key="RepeatState" />
|
||||
<converters:VolumeStateConverter x:Key="VolumeState" />
|
||||
<converters:VolumeStateToIconConverter x:Key="VolumeStateToIcon" />
|
||||
<converters:VolumeStateToFillConverter x:Key="VolumeStateToFill" />
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -11,6 +11,14 @@
|
||||
m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.39
|
||||
</x:String>
|
||||
|
||||
<x:String x:Key="PlayFillIcon2">
|
||||
m11.596 8.697-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393
|
||||
</x:String>
|
||||
|
||||
<x:String x:Key="PlayFillIcon3">
|
||||
M440.415,583.554 L421.418,571.311 C420.291,570.704 419,570.767 419,572.946 L419,597.054 C419,599.046 420.385,599.36 421.418,598.689 L440.415,586.446 C441.197,585.647 441.197,584.353 440.415,583.554
|
||||
</x:String>
|
||||
|
||||
<x:String x:Key="StopIcon">
|
||||
M3.5 5A1.5 1.5 0 0 1 5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11zM5 4.5a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5z
|
||||
</x:String>
|
||||
@@ -71,6 +79,37 @@
|
||||
M6.717 3.55A.5.5 0 0 1 7 4v8a.5.5 0 0 1-.812.39L3.825 10.5H1.5A.5.5 0 0 1 1 10V6a.5.5 0 0 1 .5-.5h2.325l2.363-1.89a.5.5 0 0 1 .529-.06
|
||||
</x:String>
|
||||
|
||||
<x:String x:Key="VolumeHighIcon2">
|
||||
M126,192H56a8,8,0,0,0-8,8V312a8,8,0,0,0,8,8h69.65a15.93,15.93,0,0,1,10.14,3.54l91.47,74.89A8,8,0,0,0,240,392V120a8,8,0,0,0-12.74-6.43l-91.47,74.89A15,15,0,0,1,126,192Z
|
||||
M320,320c9.74-19.38,16-40.84,16-64,0-23.48-6-44.42-16-64
|
||||
M368,368c19.48-33.92,32-64.06,32-112s-12-77.74-32-112
|
||||
M416,416c30-46,48-91.43,48-160S446,143,416,96
|
||||
</x:String>
|
||||
|
||||
<x:String x:Key="VolumeMediumIcon2">
|
||||
M157.65,192H88a8,8,0,0,0-8,8V312a8,8,0,0,0,8,8h69.65a16,16,0,0,1,10.14,3.63l91.47,75A8,8,0,0,0,272,392.17V119.83a8,8,0,0,0-12.74-6.44l-91.47,75A16,16,0,0,1,157.65,192Z
|
||||
M352,320c9.74-19.41,16-40.81,16-64,0-23.51-6-44.4-16-64
|
||||
M400,368c19.48-34,32-64,32-112s-12-77.7-32-112
|
||||
</x:String>
|
||||
|
||||
<x:String x:Key="VolumeLowIcon2">
|
||||
M189.65,192H120a8,8,0,0,0-8,8V312a8,8,0,0,0,8,8h69.65a16,16,0,0,1,10.14,3.63l91.47,75A8,8,0,0,0,304,392.17V119.83a8,8,0,0,0-12.74-6.44l-91.47,75A16,16,0,0,1,189.65,192Z
|
||||
M384,320c9.74-19.41,16-40.81,16-64,0-23.51-6-44.4-16-64
|
||||
</x:String>
|
||||
|
||||
<x:String x:Key="VolumeOffIcon2">
|
||||
M237.65,192H168a8,8,0,0,0-8,8V312a8,8,0,0,0,8,8h69.65a16,16,0,0,1,10.14,3.63l91.47,75A8,8,0,0,0,352,392.17V119.83a8,8,0,0,0-12.74-6.44l-91.47,75A16,16,0,0,1,237.65,192Z
|
||||
</x:String>
|
||||
|
||||
<x:String x:Key="VolumeMutedIcon2">
|
||||
M416,432 64,80
|
||||
M224,136.92v33.8a4,4,0,0,0,1.17,2.82l24,24a4,4,0,0,0,6.83-2.82V120.57a24.53,24.53,0,0,0-12.67-21.72,23.91,23.91,0,0,0-25.55,1.83,8.27,8.27,0,0,0-.66.51l-31.94,26.15a4,4,0,0,0-.29,5.92l17.05,17.06a4,4,0,0,0,5.37.26Z
|
||||
M224,375.08l-78.07-63.92A32,32,0,0,0,125.65,304H64V208h50.72a4,4,0,0,0,2.82-6.83l-24-24A4,4,0,0,0,90.72,176H56a24,24,0,0,0-24,24V312a24,24,0,0,0,24,24h69.76l91.36,74.8a8.27,8.27,0,0,0,.66.51A23.93,23.93,0,0,0,243.63,413,24.49,24.49,0,0,0,256,391.45V341.28a4,4,0,0,0-1.17-2.82l-24-24a4,4,0,0,0-6.83,2.82ZM125.82,336Z
|
||||
M352,256c0-24.56-5.81-47.88-17.75-71.27a16,16,0,0,0-28.5,14.54C315.34,218.06,320,236.62,320,256q0,4-.31,8.13a8,8,0,0,0,2.32,6.25l19.66,19.67a4,4,0,0,0,6.75-2A146.89,146.89,0,0,0,352,256Z
|
||||
M416,256c0-51.19-13.08-83.89-34.18-120.06a16,16,0,0,0-27.64,16.12C373.07,184.44,384,211.83,384,256c0,23.83-3.29,42.88-9.37,60.65a8,8,0,0,0,1.9,8.26l16.77,16.76a4,4,0,0,0,6.52-1.27C410.09,315.88,416,289.91,416,256Z
|
||||
M480,256c0-74.26-20.19-121.11-50.51-168.61a16,16,0,1,0-27,17.22C429.82,147.38,448,189.5,448,256c0,47.45-8.9,82.12-23.59,113a4,4,0,0,0,.77,4.55L443,391.39a4,4,0,0,0,6.4-1C470.88,348.22,480,307,480,256Z
|
||||
</x:String>
|
||||
|
||||
<x:String x:Key="ShuffleIcon">
|
||||
M0 3.5A.5.5 0 0 1 .5 3H1c2.202 0 3.827 1.24 4.874 2.418.49.552.865 1.102 1.126 1.532.26-.43.636-.98 1.126-1.532C9.173 4.24 10.798 3 13 3v1c-1.798 0-3.173 1.01-4.126 2.082A9.6 9.6 0 0 0 7.556 8a9.6 9.6 0 0 0 1.317 1.918C9.828 10.99 11.204 12 13 12v1c-2.202 0-3.827-1.24-4.874-2.418A10.6 10.6 0 0 1 7 9.05c-.26.43-.636.98-1.126 1.532C4.827 11.76 3.202 13 1 13H.5a.5.5 0 0 1 0-1H1c1.798 0 3.173-1.01 4.126-2.082A9.6 9.6 0 0 0 6.444 8a9.6 9.6 0 0 0-1.317-1.918C4.172 5.01 2.796 4 1 4H.5a.5.5 0 0 1-.5-.5
|
||||
M13 5.466V1.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384l-2.36 1.966a.25.25 0 0 1-.41-.192
|
||||
@@ -141,5 +180,15 @@
|
||||
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: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>
|
||||
|
||||
<x:String x:Key="SortIcon">
|
||||
M7 5C7.55228 5 8 5.44772 8 6V15.5858L10.2929 13.2929C10.6834 12.9024 11.3166 12.9024 11.7071 13.2929C12.0976 13.6834 12.0976 14.3166 11.7071 14.7071L7.70711 18.7071C7.31658 19.0976 6.68342 19.0976 6.29289 18.7071L2.29289 14.7071C1.90237 14.3166 1.90237 13.6834 2.29289 13.2929C2.68342 12.9024 3.31658 12.9024 3.70711 13.2929L6 15.5858V6C6 5.44772 6.44772 5 7 5ZM16.2929 5.29289C16.6834 4.90237 17.3166 4.90237 17.7071 5.29289L21.7071 9.29289C22.0976 9.68342 22.0976 10.3166 21.7071 10.7071C21.3166 11.0976 20.6834 11.0976 20.2929 10.7071L18 8.41421V18C18 18.5523 17.5523 19 17 19C16.4477 19 16 18.5523 16 18V8.41421L13.7071 10.7071C13.3166 11.0976 12.6834 11.0976 12.2929 10.7071C11.9024 10.3166 11.9024 9.68342 12.2929 9.29289L16.2929 5.29289Z
|
||||
</x:String>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -4,20 +4,167 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- Global Font -->
|
||||
<FontFamily x:Key="AppFontFamily">ms-appx:///Assets/Fonts/Lexend-Regular.ttf#Lexend, ms-appx:///Assets/Fonts/NotoSansJP-Regular.ttf#Noto Sans JP</FontFamily>
|
||||
<FontFamily x:Key="AppFontFamilyMedium">ms-appx:///Assets/Fonts/Lexend-Medium.ttf#Lexend, ms-appx:///Assets/Fonts/NotoSansJP-Medium.ttf#Noto Sans JP</FontFamily>
|
||||
<FontFamily x:Key="AppFontFamilySemiBold">ms-appx:///Assets/Fonts/Lexend-SemiBold.ttf#Lexend, ms-appx:///Assets/Fonts/NotoSansJP-SemiBold.ttf#Noto Sans JP</FontFamily>
|
||||
<FontFamily x:Key="AppFontFamilyBold">ms-appx:///Assets/Fonts/Lexend-Bold.ttf#Lexend, ms-appx:///Assets/Fonts/NotoSansJP-Bold.ttf#Noto Sans JP</FontFamily>
|
||||
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TextBox" BasedOn="{StaticResource DefaultTextBoxStyle}">
|
||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="MenuFlyoutItem" BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}">
|
||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="MenuFlyoutSubItem" BasedOn="{StaticResource DefaultMenuFlyoutSubItemStyle}">
|
||||
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ToggleMenuFlyoutItem" BasedOn="{StaticResource DefaultToggleMenuFlyoutItemStyle}">
|
||||
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}" />
|
||||
</Style>
|
||||
|
||||
|
||||
<Style TargetType="ListViewItem" BasedOn="{StaticResource DefaultListViewItemStyle}">
|
||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
||||
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}" />
|
||||
</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="{StaticResource AppFontFamily}"/>
|
||||
<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 -->
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
namespace Harmonia.WinUI.Session;
|
||||
using Harmonia.Core.Engine;
|
||||
|
||||
namespace Harmonia.WinUI.Session;
|
||||
|
||||
public partial class AudioPlayerState
|
||||
{
|
||||
public string? PlaylistUID { get; set; }
|
||||
public string? PlaylistSongUID { get; set; }
|
||||
public double Position { get; set; } = 0.0;
|
||||
public string? PlaybackState { get; set; }
|
||||
public AudioPlaybackState PlaybackState { get; set; } = AudioPlaybackState.Stopped;
|
||||
public double Volume { get; set; } = 1.0;
|
||||
public bool IsMuted { get; set; }
|
||||
public string? RepeatState { get; set; }
|
||||
|
||||
8
Harmonia.WinUI/Session/ISessionRestorer.cs
Normal file
8
Harmonia.WinUI/Session/ISessionRestorer.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Harmonia.WinUI.Session;
|
||||
|
||||
public interface ISessionRestorer
|
||||
{
|
||||
Task RestoreAsync();
|
||||
}
|
||||
10
Harmonia.WinUI/Session/ISessionService.cs
Normal file
10
Harmonia.WinUI/Session/ISessionService.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Harmonia.WinUI.Session;
|
||||
|
||||
public interface ISessionService
|
||||
{
|
||||
ApplicationSession Session { get; }
|
||||
Task LoadAsync();
|
||||
Task SaveAsync();
|
||||
}
|
||||
7
Harmonia.WinUI/Session/ISessionTracker.cs
Normal file
7
Harmonia.WinUI/Session/ISessionTracker.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Harmonia.WinUI.Session;
|
||||
|
||||
public interface ISessionTracker
|
||||
{
|
||||
void Start();
|
||||
void Capture();
|
||||
}
|
||||
47
Harmonia.WinUI/Session/JsonSessionService.cs
Normal file
47
Harmonia.WinUI/Session/JsonSessionService.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Harmonia.WinUI.Session;
|
||||
|
||||
public sealed class JsonSessionService : ISessionService
|
||||
{
|
||||
private static readonly JsonSerializerOptions Options = new()
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private readonly string _filePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Harmonia",
|
||||
"session.json"
|
||||
);
|
||||
|
||||
public ApplicationSession Session { get; private set; } = new();
|
||||
|
||||
public async Task LoadAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(_filePath))
|
||||
{
|
||||
await using var stream = File.OpenRead(_filePath);
|
||||
Session = await JsonSerializer.DeserializeAsync<ApplicationSession>(stream) ?? new();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Session = new();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SaveAsync()
|
||||
{
|
||||
string directoryName = Path.GetDirectoryName(_filePath)!;
|
||||
Directory.CreateDirectory(directoryName);
|
||||
|
||||
await using var stream = File.Create(_filePath);
|
||||
await JsonSerializer.SerializeAsync(stream, Session, Options);
|
||||
}
|
||||
}
|
||||
62
Harmonia.WinUI/Session/SessionRestorer.cs
Normal file
62
Harmonia.WinUI/Session/SessionRestorer.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using Harmonia.Core.Engine;
|
||||
using Harmonia.Core.Player;
|
||||
using Harmonia.Core.Playlists;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Harmonia.WinUI.Session;
|
||||
|
||||
public class SessionRestorer(
|
||||
ISessionService sessionService,
|
||||
IPlaylistManager playlistManager,
|
||||
IAudioPlayer audioPlayer) : ISessionRestorer
|
||||
{
|
||||
public async Task RestoreAsync()
|
||||
{
|
||||
RestorePlaylist();
|
||||
await RestoreAudioPlayerAsync();
|
||||
}
|
||||
|
||||
private void RestorePlaylist()
|
||||
{
|
||||
PlaylistState playlistState = sessionService.Session.Playlist;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(playlistState.OpenPlaylistUID))
|
||||
return;
|
||||
|
||||
Playlist? playlist = playlistManager.Playlists.FirstOrDefault(p => p.UID == playlistState.OpenPlaylistUID);
|
||||
|
||||
if (playlist is null)
|
||||
return;
|
||||
|
||||
playlistManager.CurrentPlaylist = playlist;
|
||||
}
|
||||
|
||||
private async Task RestoreAudioPlayerAsync()
|
||||
{
|
||||
AudioPlayerState audioPlayerState = sessionService.Session.Player;
|
||||
audioPlayer.Volume = audioPlayerState.Volume;
|
||||
audioPlayer.IsMuted = audioPlayerState.IsMuted;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(audioPlayerState.PlaylistSongUID))
|
||||
return;
|
||||
|
||||
Playlist? playlist = playlistManager.FindPlaylistContaining(audioPlayerState.PlaylistSongUID);
|
||||
|
||||
if (playlist is null)
|
||||
return;
|
||||
|
||||
PlaylistSong? playlistSong = playlist.Songs.FirstOrDefault(s => s.UID == audioPlayerState.PlaylistSongUID);
|
||||
|
||||
if (playlistSong is null)
|
||||
return;
|
||||
|
||||
await audioPlayer.LoadAsync(playlistSong, PlaybackMode.LoadOnly);
|
||||
|
||||
if (audioPlayerState.Position > 0 && audioPlayerState.Position <= playlistSong.Song.Length.TotalSeconds)
|
||||
audioPlayer.Position = audioPlayerState.Position;
|
||||
|
||||
if (audioPlayerState.PlaybackState == AudioPlaybackState.Playing)
|
||||
audioPlayer.Play();
|
||||
}
|
||||
}
|
||||
50
Harmonia.WinUI/Session/SessionTracker.cs
Normal file
50
Harmonia.WinUI/Session/SessionTracker.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Harmonia.Core.Engine;
|
||||
using Harmonia.Core.Player;
|
||||
using Harmonia.Core.Playlists;
|
||||
using System;
|
||||
|
||||
namespace Harmonia.WinUI.Session;
|
||||
|
||||
public class SessionTracker(
|
||||
ISessionService sessionService,
|
||||
IPlaylistManager playlistManager,
|
||||
IAudioPlayer audioPlayer) : ISessionTracker
|
||||
{
|
||||
public void Start()
|
||||
{
|
||||
playlistManager.CurrentPlaylistChanged += OnCurrentPlaylistChanged;
|
||||
audioPlayer.PlayingSongChanged += OnPlayingSongChanged;
|
||||
}
|
||||
|
||||
private void OnCurrentPlaylistChanged(object? sender, EventArgs e)
|
||||
{
|
||||
PlaylistState playlistState = sessionService.Session.Playlist;
|
||||
playlistState.OpenPlaylistUID = playlistManager.CurrentPlaylist?.UID;
|
||||
}
|
||||
|
||||
private void OnPlayingSongChanged(object? sender, PlayingSongChangedEventArgs e)
|
||||
{
|
||||
AudioPlayerState audioPlayerState = sessionService.Session.Player;
|
||||
audioPlayerState.PlaylistSongUID = e.NewSong?.UID;
|
||||
}
|
||||
|
||||
public void Capture()
|
||||
{
|
||||
AudioPlayerState audioPlayerState = sessionService.Session.Player;
|
||||
|
||||
audioPlayerState.Position = audioPlayer.Position;
|
||||
audioPlayerState.PlaybackState = GetSemanticPlaybackState();
|
||||
audioPlayerState.Volume = audioPlayer.Volume;
|
||||
audioPlayerState.IsMuted = audioPlayer.IsMuted;
|
||||
}
|
||||
|
||||
private AudioPlaybackState GetSemanticPlaybackState()
|
||||
{
|
||||
if (audioPlayer.PlayingSong is null)
|
||||
return AudioPlaybackState.Stopped;
|
||||
|
||||
return audioPlayer.State == AudioPlaybackState.Playing
|
||||
? AudioPlaybackState.Playing
|
||||
: AudioPlaybackState.Paused;
|
||||
}
|
||||
}
|
||||
@@ -225,13 +225,27 @@ public partial class PlayerViewModel : ViewModelBase
|
||||
_timer.Tick += TickTock;
|
||||
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
TryRestore();
|
||||
}
|
||||
|
||||
private void TryRestore()
|
||||
{
|
||||
if (_audioPlayer.PlayingSong is null)
|
||||
return;
|
||||
|
||||
Song = _audioPlayer.PlayingSong.Song;
|
||||
Position = _audioPlayer.Position;
|
||||
CurrentPosition = _audioPlayer.Position;
|
||||
|
||||
Task.Run(UpdateImage);
|
||||
}
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void OnPlayingSongChanged(object? sender, EventArgs e)
|
||||
private void OnPlayingSongChanged(object? sender, PlayingSongChangedEventArgs e)
|
||||
{
|
||||
Song = _audioPlayer.PlayingSong?.Song;
|
||||
Song = e.NewSong?.Song;
|
||||
Task.Run(UpdateImage);
|
||||
}
|
||||
|
||||
|
||||
@@ -50,11 +50,22 @@ public partial class PlayingSongViewModel : ViewModelBase
|
||||
|
||||
_audioBitmapImageCache = audioBitmapImageCache;
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
TryRestore();
|
||||
}
|
||||
|
||||
private void OnAudioPlayerPlayingSongChanged(object? sender, EventArgs e)
|
||||
private void TryRestore()
|
||||
{
|
||||
Song = _audioPlayer.PlayingSong?.Song;
|
||||
if (_audioPlayer.PlayingSong is null)
|
||||
return;
|
||||
|
||||
Song = _audioPlayer.PlayingSong.Song;
|
||||
Task.Run(UpdateImage);
|
||||
}
|
||||
|
||||
private void OnAudioPlayerPlayingSongChanged(object? sender, PlayingSongChangedEventArgs e)
|
||||
{
|
||||
Song = e.NewSong?.Song;
|
||||
Task.Run(UpdateImage);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Harmonia.Core.Caching;
|
||||
using Harmonia.Core.Engine;
|
||||
using Harmonia.Core.Imaging;
|
||||
@@ -7,6 +7,7 @@ using Harmonia.Core.Player;
|
||||
using Harmonia.Core.Playlists;
|
||||
using Harmonia.Core.Scanner;
|
||||
using Harmonia.WinUI.Caching;
|
||||
using Harmonia.WinUI.Messaging;
|
||||
using Harmonia.WinUI.Storage;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using System;
|
||||
@@ -14,20 +15,20 @@ using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Windows.ApplicationModel.DataTransfer;
|
||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||
using DispatcherQueueTimer = Microsoft.UI.Dispatching.DispatcherQueueTimer;
|
||||
|
||||
namespace Harmonia.WinUI.ViewModels;
|
||||
|
||||
public partial class PlaylistViewModel : ViewModelBase
|
||||
public partial class PlaylistDetailViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
private readonly IAudioPlayer _audioPlayer;
|
||||
@@ -36,12 +37,25 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
private readonly IAudioFileScanner _audioFileScanner;
|
||||
private readonly IAudioEngine _audioEngine;
|
||||
private readonly IStorageProvider _storageProvider;
|
||||
private readonly IMessageService _messageService;
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
private readonly ConcurrentDictionary<int, CancellationTokenSource> _imageCancellationTokens = [];
|
||||
|
||||
private DispatcherQueueTimer? _filterTimer;
|
||||
|
||||
public Playlist? Playlist { get; private set; }
|
||||
private Playlist? _playlist;
|
||||
public Playlist? Playlist
|
||||
{
|
||||
get => _playlist;
|
||||
private set
|
||||
{
|
||||
_playlist?.PlaylistUpdated -= OnPlaylistUpdated;
|
||||
SetProperty(ref _playlist, value);
|
||||
_playlist?.PlaylistUpdated += OnPlaylistUpdated;
|
||||
|
||||
IsPlaylistLocked = Playlist?.IsLocked ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
private PlaylistSong? _playingSong;
|
||||
public PlaylistSong? PlayingSong
|
||||
@@ -56,19 +70,6 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
private ObservableCollection<PlaylistSong> _playlistSongs = [];
|
||||
public ObservableCollection<PlaylistSong> PlaylistSongs
|
||||
{
|
||||
get
|
||||
{
|
||||
return _playlistSongs;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _playlistSongs, value);
|
||||
}
|
||||
}
|
||||
|
||||
private string? _filter;
|
||||
public string? Filter
|
||||
{
|
||||
@@ -79,10 +80,13 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
set
|
||||
{
|
||||
SetProperty(ref _filter, value);
|
||||
OnPropertyChanged(nameof(CanReorderSongs));
|
||||
RestartFilterTimer();
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanReorderSongs => string.IsNullOrWhiteSpace(Filter);
|
||||
|
||||
private ObservableCollection<PlaylistSong> _filteredPlaylistSongs = [];
|
||||
public ObservableCollection<PlaylistSong> FilteredPlaylistSongs
|
||||
{
|
||||
@@ -106,37 +110,84 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
set
|
||||
{
|
||||
SetProperty(ref _selectedPlaylistSongs, value);
|
||||
NotifySelectionDependentCommands();
|
||||
}
|
||||
}
|
||||
|
||||
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 RelayCommand(CutSongs, AreSongsSelected);
|
||||
public ICommand CopySongsCommand => new RelayCommand(CopySongs, AreSongsSelected);
|
||||
public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs);
|
||||
public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected);
|
||||
public ICommand RefreshTagsCommand => new RelayCommand(RefreshTags);
|
||||
public ICommand RemoveMissingSongsCommand => new RelayCommand(RemoveMissingSongs);
|
||||
public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs);
|
||||
public SortItem[] SortItems { get; } =
|
||||
[
|
||||
new SortItem
|
||||
{
|
||||
Name = "Album - Disc # - Track # - Title - File Name",
|
||||
SortOptions =
|
||||
[
|
||||
new(nameof(Song.Album), ListSortDirection.Ascending),
|
||||
new(nameof(Song.DiscNumber), ListSortDirection.Ascending),
|
||||
new(nameof(Song.TrackNumber), ListSortDirection.Ascending),
|
||||
new(nameof(Song.Title), ListSortDirection.Ascending),
|
||||
new(nameof(Song.FileName), ListSortDirection.Ascending)
|
||||
]
|
||||
},
|
||||
new SortItem
|
||||
{
|
||||
Name = "Title - File Name",
|
||||
SortOptions =
|
||||
[
|
||||
new(nameof(Song.Title), ListSortDirection.Ascending),
|
||||
new(nameof(Song.FileName), ListSortDirection.Ascending)
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
private bool _isPlaylistLocked;
|
||||
public bool IsPlaylistLocked
|
||||
{
|
||||
get => _isPlaylistLocked;
|
||||
private set => SetProperty(ref _isPlaylistLocked, value);
|
||||
}
|
||||
|
||||
public IAsyncRelayCommand PlaySongCommand { get; }
|
||||
public IAsyncRelayCommand NewPlaylistCommand { get; }
|
||||
public IAsyncRelayCommand AddFilesCommand { get; }
|
||||
public IAsyncRelayCommand AddFolderCommand { get; }
|
||||
public IRelayCommand RemoveSongsCommand { get; }
|
||||
public IRelayCommand CutSongsCommand { get; }
|
||||
public IRelayCommand CopySongsCommand { get; }
|
||||
public IAsyncRelayCommand PasteSongsCommand { get; }
|
||||
public IRelayCommand OpenFileLocationCommand { get; }
|
||||
public IAsyncRelayCommand RefreshTagsCommand { get; }
|
||||
public IRelayCommand RemoveMissingSongsCommand { get; }
|
||||
public IRelayCommand RemoveDuplicateSongsCommand { get; }
|
||||
public IAsyncRelayCommand DeletePlaylistCommand { get; }
|
||||
public IRelayCommand<SortItem> SortAllCommand { get; }
|
||||
public IRelayCommand<SortItem> SortSelectedCommand { get; }
|
||||
public IRelayCommand RandomizeAllCommand { get; }
|
||||
public IRelayCommand RandomizeSelectedCommand { get; }
|
||||
public IRelayCommand ReverseAllCommand { get; }
|
||||
public IRelayCommand ReverseSelectedCommand { get; }
|
||||
public IAsyncRelayCommand RenamePlaylistCommand { get; }
|
||||
public IRelayCommand LockPlaylistCommand { get; }
|
||||
public IRelayCommand UnlockPlaylistCommand { get; }
|
||||
|
||||
public bool IsUserUpdating { get; set; }
|
||||
private bool _isUserInitiatingSongChange;
|
||||
private PlaylistSong? _pendingMovedSong;
|
||||
private int _pendingMovedSongIndex = -1;
|
||||
|
||||
public event EventHandler? PlayingSongChangedAutomatically;
|
||||
|
||||
public PlaylistViewModel(
|
||||
public PlaylistDetailViewModel(
|
||||
IAudioPlayer audioPlayer,
|
||||
IAudioImageCache audioImageCache,
|
||||
IAudioBitmapImageCache audioBitmapImageCache,
|
||||
IAudioFileScanner audioFileScanner,
|
||||
IAudioEngine audioEngine,
|
||||
IStorageProvider storageProvider,
|
||||
IPlaylistManager playlistManager)
|
||||
IPlaylistManager playlistManager,
|
||||
IMessageService messageService)
|
||||
{
|
||||
_playlistManager = playlistManager;
|
||||
//_playlistManager.CurrentPlaylistChanged += OnPlaylistChanged;
|
||||
_playlistManager.CurrentPlaylistChanged += OnPlaylistChanged;
|
||||
|
||||
_audioPlayer = audioPlayer;
|
||||
_audioPlayer.PlaylistChanged += OnPlaylistChanged;
|
||||
@@ -147,15 +198,51 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
_audioFileScanner = audioFileScanner;
|
||||
_audioEngine = audioEngine;
|
||||
_storageProvider = storageProvider;
|
||||
_messageService = messageService;
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
PlaySongCommand = new AsyncRelayCommand(PlaySongAsync, AreSongsSelected);
|
||||
NewPlaylistCommand = new AsyncRelayCommand(NewPlaylistAsync);
|
||||
AddFilesCommand = new AsyncRelayCommand(AddFilesAsync);
|
||||
AddFolderCommand = new AsyncRelayCommand(AddFolderAsync);
|
||||
RemoveSongsCommand = new RelayCommand(RemoveSongs, AreSongsSelected);
|
||||
CutSongsCommand = new RelayCommand(CutSongs, AreSongsSelected);
|
||||
CopySongsCommand = new RelayCommand(CopySongs, AreSongsSelected);
|
||||
PasteSongsCommand = new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs);
|
||||
OpenFileLocationCommand = new RelayCommand(OpenFileLocation, AreSongsSelected);
|
||||
RefreshTagsCommand = new AsyncRelayCommand(RefreshTagsAsync);
|
||||
RemoveMissingSongsCommand = new RelayCommand(RemoveMissingSongs);
|
||||
RemoveDuplicateSongsCommand = new RelayCommand(RemoveDuplicateSongs);
|
||||
DeletePlaylistCommand = new AsyncRelayCommand(DeletePlaylistAsync);
|
||||
SortAllCommand = new RelayCommand<SortItem>(SortAllSongs);
|
||||
SortSelectedCommand = new RelayCommand<SortItem>(SortSelectedSongs, _ => AreMultipleSongsSelected());
|
||||
RandomizeAllCommand = new RelayCommand(RandomizeAllSongs);
|
||||
RandomizeSelectedCommand = new RelayCommand(RandomizeSelectedSongs, AreMultipleSongsSelected);
|
||||
ReverseAllCommand = new RelayCommand(ReverseAllSongs);
|
||||
ReverseSelectedCommand = new RelayCommand(ReverseSelectedSongs, AreMultipleSongsSelected);
|
||||
RenamePlaylistCommand = new AsyncRelayCommand(RenamePlaylist);
|
||||
LockPlaylistCommand = new RelayCommand(LockPlaylist);
|
||||
UnlockPlaylistCommand = new RelayCommand(UnlockPlaylist);
|
||||
|
||||
FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged;
|
||||
|
||||
Playlist = _playlistManager.CurrentPlaylist; // Testing
|
||||
UpdatePlaylistSongs(Playlist);
|
||||
PlayingSong = _audioPlayer.PlayingSong; // Testing
|
||||
|
||||
// Testing
|
||||
//Task.Run(() => PlayDemoSong(playlistRepository));
|
||||
UpdateFilteredSongs();
|
||||
}
|
||||
|
||||
private void NotifySelectionDependentCommands()
|
||||
{
|
||||
PlaySongCommand.NotifyCanExecuteChanged();
|
||||
RemoveSongsCommand.NotifyCanExecuteChanged();
|
||||
CutSongsCommand.NotifyCanExecuteChanged();
|
||||
CopySongsCommand.NotifyCanExecuteChanged();
|
||||
PasteSongsCommand.NotifyCanExecuteChanged();
|
||||
OpenFileLocationCommand.NotifyCanExecuteChanged();
|
||||
SortSelectedCommand.NotifyCanExecuteChanged();
|
||||
RandomizeSelectedCommand.NotifyCanExecuteChanged();
|
||||
ReverseSelectedCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
|
||||
private void OnFilteredPlaylistSongsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
@@ -163,47 +250,39 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
if (IsUserUpdating == false)
|
||||
return;
|
||||
|
||||
int x = 1;
|
||||
// A drag-reorder in the ListView raises a Remove followed by an Add for the same item.
|
||||
// Reordering is only enabled while unfiltered, so view indices map 1:1 to Playlist.Songs.
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
_pendingMovedSong = e.OldItems?.Cast<PlaylistSong>().FirstOrDefault();
|
||||
_pendingMovedSongIndex = e.OldStartingIndex;
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
PlaylistSong? addedSong = e.NewItems?.Cast<PlaylistSong>().FirstOrDefault();
|
||||
|
||||
if (addedSong != null && addedSong == _pendingMovedSong)
|
||||
{
|
||||
MoveSongInPlaylist(_pendingMovedSongIndex, e.NewStartingIndex);
|
||||
}
|
||||
|
||||
_pendingMovedSong = null;
|
||||
_pendingMovedSongIndex = -1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
//private async Task PlayDemoSong(IPlaylistRepository playlistRepository)
|
||||
//{
|
||||
// if (playlistRepository.Get().Count == 0)
|
||||
// {
|
||||
// playlistRepository.AddPlaylist();
|
||||
// }
|
||||
|
||||
// Playlist playlist = playlistRepository.Get().First();
|
||||
|
||||
// if (playlist.Songs.Count > 0)
|
||||
// await _audioPlayer.LoadAsync(playlist.Songs[0], PlaybackMode.LoadOnly);
|
||||
//}
|
||||
|
||||
private void UpdatePlaylistSongs(Playlist? playlist)
|
||||
private void MoveSongInPlaylist(int oldIndex, int newIndex)
|
||||
{
|
||||
PlaylistSong[] playlistSongs = playlist?.Songs.ToArray() ?? [];
|
||||
if (Playlist == null || oldIndex < 0 || oldIndex == newIndex)
|
||||
return;
|
||||
|
||||
PlaylistSongs = [.. playlistSongs];
|
||||
UpdateFilteredSongs();
|
||||
Playlist.MoveSong(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
private void OnPlaylistChanged(object? sender, EventArgs e)
|
||||
private void OnPlaylistChanged(object? sender, PlaylistChangedEventArgs e)
|
||||
{
|
||||
if (Playlist != null)
|
||||
{
|
||||
Playlist.PlaylistUpdated -= OnPlaylistUpdated;
|
||||
}
|
||||
|
||||
Playlist = _audioPlayer.Playlist;
|
||||
|
||||
if (Playlist != null)
|
||||
{
|
||||
Playlist.PlaylistUpdated += OnPlaylistUpdated;
|
||||
}
|
||||
|
||||
PlaylistSong[] playlistSongs = _audioPlayer.Playlist?.Songs.ToArray() ?? [];
|
||||
|
||||
PlaylistSongs = [.. playlistSongs];
|
||||
Playlist = e.NewPlaylist;
|
||||
UpdateFilteredSongs();
|
||||
}
|
||||
|
||||
@@ -215,40 +294,36 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
switch (e.Action)
|
||||
{
|
||||
case PlaylistUpdateAction.Add:
|
||||
_dispatcherQueue.TryEnqueue(() => AddSongs(e.Songs, e.Index));
|
||||
break;
|
||||
case PlaylistUpdateAction.Remove:
|
||||
_dispatcherQueue.TryEnqueue(() => RemoveSongsFromCollection(e.Songs));
|
||||
case PlaylistUpdateAction.Refresh:
|
||||
_dispatcherQueue.TryEnqueue(UpdateFilteredSongs);
|
||||
break;
|
||||
case PlaylistUpdateAction.Move:
|
||||
case PlaylistUpdateAction.Reset:
|
||||
_dispatcherQueue.TryEnqueue(() => ApplyReorderedSongs(e.Songs));
|
||||
break;
|
||||
case PlaylistUpdateAction.Lock:
|
||||
case PlaylistUpdateAction.Unlock:
|
||||
IsPlaylistLocked = Playlist?.IsLocked ?? false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSongs(PlaylistSong[] playlistSongs, int index = 0)
|
||||
private void ApplyReorderedSongs(PlaylistSong[] playlistSongs)
|
||||
{
|
||||
// TODO: Performance improvements
|
||||
int currentIndex = index;
|
||||
|
||||
// UpdateFilteredSongs assumes the projection order matches Playlist.Songs order,
|
||||
// so remove reordered songs first to let them be reinserted at their new positions.
|
||||
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||
{
|
||||
PlaylistSongs.Insert(currentIndex++, playlistSong);
|
||||
FilteredPlaylistSongs.Remove(playlistSong);
|
||||
}
|
||||
|
||||
UpdateFilteredSongs();
|
||||
}
|
||||
|
||||
private void RemoveSongsFromCollection(PlaylistSong[] playlistSongs)
|
||||
private void OnPlayingSongChanged(object? sender, PlayingSongChangedEventArgs e)
|
||||
{
|
||||
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||
{
|
||||
PlaylistSongs.Remove(playlistSong);
|
||||
}
|
||||
|
||||
UpdateFilteredSongs();
|
||||
}
|
||||
|
||||
private void OnPlayingSongChanged(object? sender, EventArgs e)
|
||||
{
|
||||
PlayingSong = _audioPlayer.PlayingSong;
|
||||
PlayingSong = e.NewSong;
|
||||
|
||||
if (_isUserInitiatingSongChange)
|
||||
{
|
||||
@@ -394,6 +469,27 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
return SelectedPlaylistSongs.Count > 0;
|
||||
}
|
||||
|
||||
private bool AreMultipleSongsSelected()
|
||||
{
|
||||
return SelectedPlaylistSongs.Count > 1;
|
||||
}
|
||||
|
||||
private async Task NewPlaylistAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
string playlistName = await _messageService.InputTextAsync(
|
||||
$"New Playlist",
|
||||
"Enter a new name for the playlist:",
|
||||
"New Playlist");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(playlistName))
|
||||
return;
|
||||
|
||||
Playlist playlist = await _playlistManager.AddPlaylistAsync();
|
||||
playlist.SetName(playlistName);
|
||||
|
||||
_playlistManager.CurrentPlaylist = playlist;
|
||||
}
|
||||
|
||||
private async Task AddFilesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Playlist == null)
|
||||
@@ -499,7 +595,7 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
|
||||
private bool CanPasteSongs()
|
||||
{
|
||||
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
|
||||
if (Playlist == null)
|
||||
return false;
|
||||
|
||||
DataPackageView dataPackageView = Clipboard.GetContent();
|
||||
@@ -515,17 +611,21 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
|
||||
private async Task PasteSongsAsync()
|
||||
{
|
||||
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
|
||||
return;
|
||||
|
||||
int selectedPlaylistSongIndex = Playlist.Songs.IndexOf(SelectedPlaylistSongs[0]);
|
||||
|
||||
if (selectedPlaylistSongIndex == -1)
|
||||
if (Playlist == null)
|
||||
return;
|
||||
|
||||
Song[] songs = await GetSongsFromClipboardAsync();
|
||||
int insertIndex = Playlist.Songs.Count;
|
||||
|
||||
Playlist.AddSongs(songs, selectedPlaylistSongIndex + 1);
|
||||
if (SelectedPlaylistSongs.Count > 0)
|
||||
{
|
||||
int selectedPlaylistSongIndex = Playlist.IndexOf(SelectedPlaylistSongs[0]);
|
||||
|
||||
if (selectedPlaylistSongIndex >= 0)
|
||||
insertIndex = selectedPlaylistSongIndex + 1;
|
||||
}
|
||||
|
||||
Playlist.AddSongs(songs, insertIndex);
|
||||
}
|
||||
|
||||
private static async Task<Song[]> GetSongsFromClipboardAsync()
|
||||
@@ -545,9 +645,23 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
Process.Start("explorer.exe", argument);
|
||||
}
|
||||
|
||||
private void RefreshTags()
|
||||
private async Task RefreshTagsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
//Playlist?.RefreshTags();
|
||||
if (Playlist is null)
|
||||
return;
|
||||
|
||||
// TODO: Have different method for selected songs versus all songs in the playlist
|
||||
PlaylistSong[] targets = SelectedPlaylistSongs.Count > 0
|
||||
? [.. SelectedPlaylistSongs]
|
||||
: [.. Playlist.Songs];
|
||||
|
||||
string[] fileNames = [.. targets
|
||||
.Select(ps => ps.Song.FileName)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)];
|
||||
|
||||
Song[] songs = await _audioFileScanner.GetSongsAsync(fileNames, cancellationToken);
|
||||
|
||||
Playlist.ImportTags(songs);
|
||||
}
|
||||
|
||||
private void RemoveMissingSongs()
|
||||
@@ -560,5 +674,122 @@ public partial class PlaylistViewModel : ViewModelBase
|
||||
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);
|
||||
}
|
||||
|
||||
private void SortAllSongs(SortItem? sortItem)
|
||||
{
|
||||
if (Playlist == null || sortItem == null)
|
||||
return;
|
||||
|
||||
Playlist.SortSongs([.. Playlist.Songs], [.. sortItem.SortOptions]);
|
||||
}
|
||||
|
||||
private void SortSelectedSongs(SortItem? sortItem)
|
||||
{
|
||||
if (Playlist == null || sortItem == null)
|
||||
return;
|
||||
|
||||
if (SelectedPlaylistSongs.Count < 2)
|
||||
return;
|
||||
|
||||
string[] selectedPlaylistSongIds = [.. SelectedPlaylistSongs.Select(x => x.UID)];
|
||||
|
||||
Playlist.SortSongs([.. SelectedPlaylistSongs], [.. sortItem.SortOptions]);
|
||||
|
||||
RestoreSelectedSongs(selectedPlaylistSongIds);
|
||||
}
|
||||
|
||||
private void RestoreSelectedSongs(string[] playlistSongIds)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(() =>
|
||||
{
|
||||
PlaylistSong[] selectedPlaylistSongs = [.. Playlist?.Songs.Where(x => playlistSongIds.Contains(x.UID)) ?? []];
|
||||
SelectedPlaylistSongs = new ObservableCollection<PlaylistSong>(selectedPlaylistSongs);
|
||||
});
|
||||
}
|
||||
|
||||
private void RandomizeAllSongs()
|
||||
{
|
||||
if (Playlist == null || Playlist.Songs.Count < 2)
|
||||
return;
|
||||
|
||||
Playlist.Randomize([.. Playlist.Songs]);
|
||||
}
|
||||
|
||||
private void RandomizeSelectedSongs()
|
||||
{
|
||||
if (Playlist == null || SelectedPlaylistSongs.Count < 2)
|
||||
return;
|
||||
|
||||
string[] selectedPlaylistSongIds = [.. SelectedPlaylistSongs.Select(x => x.UID)];
|
||||
|
||||
Playlist.Randomize([.. SelectedPlaylistSongs]);
|
||||
|
||||
RestoreSelectedSongs(selectedPlaylistSongIds);
|
||||
}
|
||||
|
||||
private void ReverseAllSongs()
|
||||
{
|
||||
if (Playlist == null || Playlist.Songs.Count < 2)
|
||||
return;
|
||||
|
||||
Playlist.Reverse([.. Playlist.Songs]);
|
||||
}
|
||||
|
||||
private void ReverseSelectedSongs()
|
||||
{
|
||||
if (Playlist == null || SelectedPlaylistSongs.Count < 2)
|
||||
return;
|
||||
|
||||
string[] selectedPlaylistSongIds = [.. SelectedPlaylistSongs.Select(x => x.UID)];
|
||||
|
||||
Playlist.Reverse([.. SelectedPlaylistSongs]);
|
||||
|
||||
RestoreSelectedSongs(selectedPlaylistSongIds);
|
||||
}
|
||||
|
||||
private void LockPlaylist()
|
||||
{
|
||||
Playlist?.Lock();
|
||||
}
|
||||
|
||||
private void UnlockPlaylist()
|
||||
{
|
||||
Playlist?.Unlock();
|
||||
}
|
||||
|
||||
private async Task RenamePlaylist()
|
||||
{
|
||||
if (Playlist == null)
|
||||
return;
|
||||
|
||||
string newName = await _messageService.InputTextAsync(
|
||||
$"Rename Playlist - {Playlist.Name}",
|
||||
"Enter a new name for the playlist:",
|
||||
Playlist.Name ?? string.Empty);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newName))
|
||||
return;
|
||||
|
||||
Playlist.SetName(newName);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class SortItem
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public IReadOnlyCollection<SortOption> SortOptions { get; set; } = [];
|
||||
}
|
||||
76
Harmonia.WinUI/ViewModels/PlaylistItemViewModel.cs
Normal file
76
Harmonia.WinUI/ViewModels/PlaylistItemViewModel.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Harmonia.Core.Playlists;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Harmonia.WinUI.ViewModels;
|
||||
|
||||
public partial class PlaylistItemViewModel : ObservableObject
|
||||
{
|
||||
public Playlist Playlist { get; private set; }
|
||||
|
||||
private string _name = string.Empty;
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return _name;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _name, value);
|
||||
}
|
||||
}
|
||||
|
||||
private string _summary = string.Empty;
|
||||
public string Summary
|
||||
{
|
||||
get
|
||||
{
|
||||
return _summary;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _summary, value);
|
||||
}
|
||||
}
|
||||
|
||||
private bool _isLocked;
|
||||
public bool IsLocked
|
||||
{
|
||||
get
|
||||
{
|
||||
return _isLocked;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _isLocked, value);
|
||||
}
|
||||
}
|
||||
|
||||
public PlaylistItemViewModel(Playlist playlist)
|
||||
{
|
||||
Playlist = playlist;
|
||||
Playlist.PlaylistUpdated += OnPlaylistUpdated;
|
||||
UpdateMetaData();
|
||||
}
|
||||
|
||||
public void Unsubscribe()
|
||||
{
|
||||
Playlist.PlaylistUpdated -= OnPlaylistUpdated;
|
||||
}
|
||||
|
||||
private void OnPlaylistUpdated(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateMetaData();
|
||||
}
|
||||
|
||||
private void UpdateMetaData()
|
||||
{
|
||||
Name = Playlist.Name ?? "Untitled Playlist";
|
||||
IsLocked = Playlist.IsLocked;
|
||||
|
||||
TimeSpan total = TimeSpan.FromSeconds(Playlist.Songs.Sum(s => s.Song.Length.TotalSeconds));
|
||||
Summary = $"{Playlist.Songs.Count} songs / {total:h\\:mm\\:ss}";
|
||||
}
|
||||
}
|
||||
186
Harmonia.WinUI/ViewModels/PlaylistsViewModel.cs
Normal file
186
Harmonia.WinUI/ViewModels/PlaylistsViewModel.cs
Normal file
@@ -0,0 +1,186 @@
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Harmonia.Core.Player;
|
||||
using Harmonia.Core.Playlists;
|
||||
using Harmonia.WinUI.Messaging;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Harmonia.WinUI.ViewModels;
|
||||
|
||||
public partial class PlaylistsViewModel : ViewModelBase
|
||||
{
|
||||
private readonly IPlaylistManager _playlistManager;
|
||||
private readonly IAudioPlayer _audioPlayer;
|
||||
private readonly IMessageService _messageService;
|
||||
private readonly DispatcherQueue _dispatcherQueue;
|
||||
|
||||
private ObservableCollection<PlaylistItemViewModel> _playlists = [];
|
||||
public ObservableCollection<PlaylistItemViewModel> Playlists
|
||||
{
|
||||
get
|
||||
{
|
||||
return _playlists;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _playlists, value);
|
||||
}
|
||||
}
|
||||
|
||||
private PlaylistItemViewModel? _selectedPlaylist = null;
|
||||
public PlaylistItemViewModel? SelectedPlaylist
|
||||
{
|
||||
get
|
||||
{
|
||||
return _selectedPlaylist;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (SetProperty(ref _selectedPlaylist, value))
|
||||
{
|
||||
_playlistManager.CurrentPlaylist = value?.Playlist;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private PlaylistItemViewModel? _activePlaylist = null;
|
||||
public PlaylistItemViewModel? ActivePlaylist
|
||||
{
|
||||
get
|
||||
{
|
||||
return _activePlaylist;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _activePlaylist, value);
|
||||
}
|
||||
}
|
||||
|
||||
private PlaylistItemViewModel? _flyoutPlaylist = null;
|
||||
public PlaylistItemViewModel? FlyoutPlaylist
|
||||
{
|
||||
get
|
||||
{
|
||||
return _flyoutPlaylist;
|
||||
}
|
||||
set
|
||||
{
|
||||
SetProperty(ref _flyoutPlaylist, value);
|
||||
}
|
||||
}
|
||||
|
||||
public IAsyncRelayCommand<PlaylistItemViewModel?> RenamePlaylistCommand { get; }
|
||||
public IRelayCommand<PlaylistItemViewModel?> LockPlaylistCommand { get; }
|
||||
public IRelayCommand<PlaylistItemViewModel?> UnlockPlaylistCommand { get; }
|
||||
public IAsyncRelayCommand<PlaylistItemViewModel?> DeletePlaylistCommand { get; }
|
||||
|
||||
public PlaylistsViewModel(IPlaylistManager playlistManager, IAudioPlayer audioPlayer, IMessageService messageService)
|
||||
{
|
||||
_playlistManager = playlistManager;
|
||||
_playlistManager.PlaylistAdded += OnPlaylistAdded;
|
||||
_playlistManager.PlaylistRemoved += OnPlaylistRemoved;
|
||||
_playlistManager.CurrentPlaylistChanged += OnCurrentPlaylistChanged;
|
||||
_messageService = messageService;
|
||||
|
||||
_audioPlayer = audioPlayer;
|
||||
_audioPlayer.PlayingSongChanged += OnPlayingSongChanged;
|
||||
|
||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
|
||||
_playlists = new ObservableCollection<PlaylistItemViewModel>(_playlistManager.Playlists.Select(p => new PlaylistItemViewModel(p)));
|
||||
_selectedPlaylist = _playlists.FirstOrDefault(pv => pv.Playlist == _playlistManager.CurrentPlaylist);
|
||||
_activePlaylist = _playlists.FirstOrDefault(pv => pv.Playlist == _audioPlayer.Playlist);
|
||||
|
||||
RenamePlaylistCommand = new AsyncRelayCommand<PlaylistItemViewModel?>(RenamePlaylist);
|
||||
LockPlaylistCommand = new RelayCommand<PlaylistItemViewModel?>(LockPlaylist);
|
||||
UnlockPlaylistCommand = new RelayCommand<PlaylistItemViewModel?>(UnlockPlaylist);
|
||||
DeletePlaylistCommand = new AsyncRelayCommand<PlaylistItemViewModel?>(DeletePlaylistAsync);
|
||||
}
|
||||
|
||||
private void OnCurrentPlaylistChanged(object? sender, EventArgs e)
|
||||
{
|
||||
SelectedPlaylist = _playlists.FirstOrDefault(pv => pv.Playlist == _playlistManager.CurrentPlaylist);
|
||||
}
|
||||
|
||||
private void OnPlaylistAdded(object? sender, PlaylistAddedEventArgs e)
|
||||
{
|
||||
Playlists.Add(new PlaylistItemViewModel(e.Playlist));
|
||||
}
|
||||
|
||||
private void OnPlaylistRemoved(object? sender, PlaylistRemovedEventArgs e)
|
||||
{
|
||||
var playlistView = Playlists.FirstOrDefault(pv => pv.Playlist == e.Playlist);
|
||||
|
||||
if (playlistView != null)
|
||||
{
|
||||
playlistView.Unsubscribe();
|
||||
Playlists.Remove(playlistView);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayingSongChanged(object? sender, PlayingSongChangedEventArgs e)
|
||||
{
|
||||
_dispatcherQueue.TryEnqueue(UpdateActivePlaylist);
|
||||
}
|
||||
|
||||
private void UpdateActivePlaylist()
|
||||
{
|
||||
ActivePlaylist = Playlists.FirstOrDefault(pv => pv.Playlist == _audioPlayer.Playlist);
|
||||
}
|
||||
|
||||
private async Task RenamePlaylist(PlaylistItemViewModel? playlistItem)
|
||||
{
|
||||
if (playlistItem is null || playlistItem.Playlist is null)
|
||||
return;
|
||||
|
||||
Playlist playlist = playlistItem.Playlist;
|
||||
|
||||
string newName = await _messageService.InputTextAsync(
|
||||
$"Rename Playlist - {playlist.Name}",
|
||||
"Enter a new name for the playlist:",
|
||||
playlist.Name ?? string.Empty);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(newName))
|
||||
return;
|
||||
|
||||
playlist.SetName(newName);
|
||||
}
|
||||
|
||||
private void LockPlaylist(PlaylistItemViewModel? playlistItem)
|
||||
{
|
||||
if (playlistItem is null || playlistItem.Playlist is null)
|
||||
return;
|
||||
|
||||
Playlist playlist = playlistItem.Playlist;
|
||||
|
||||
playlist.Lock();
|
||||
}
|
||||
|
||||
private void UnlockPlaylist(PlaylistItemViewModel? playlistItem)
|
||||
{
|
||||
if (playlistItem is null || playlistItem.Playlist is null)
|
||||
return;
|
||||
|
||||
Playlist playlist = playlistItem.Playlist;
|
||||
|
||||
playlist.Unlock();
|
||||
}
|
||||
|
||||
private async Task DeletePlaylistAsync(PlaylistItemViewModel? playlistItem)
|
||||
{
|
||||
if (playlistItem is null || playlistItem.Playlist is null)
|
||||
return;
|
||||
|
||||
Playlist playlist = playlistItem.Playlist;
|
||||
|
||||
bool confirmed = await _messageService.ConfirmAsync($"Delete Playlist - {playlist.Name}", "Are you sure you want to delete this playlist?");
|
||||
|
||||
if (!confirmed)
|
||||
return;
|
||||
|
||||
_playlistManager.RemovePlaylist(playlist);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Harmonia.WinUI.ViewModels;
|
||||
|
||||
@@ -13,6 +13,9 @@ public class ViewModelLocator
|
||||
public static PlayingSongViewModel PlayingSongViewModel
|
||||
=> App.ServiceProvider.GetRequiredService<PlayingSongViewModel>();
|
||||
|
||||
public static PlaylistViewModel PlaylistViewModel
|
||||
=> App.ServiceProvider.GetRequiredService<PlaylistViewModel>();
|
||||
public static PlaylistsViewModel PlaylistsViewModel
|
||||
=> App.ServiceProvider.GetRequiredService<PlaylistsViewModel>();
|
||||
|
||||
public static PlaylistDetailViewModel PlaylistDetailViewModel
|
||||
=> App.ServiceProvider.GetRequiredService<PlaylistDetailViewModel>();
|
||||
}
|
||||
@@ -14,13 +14,45 @@
|
||||
<SolidColorBrush x:Key="SongItemTitleBrush" Color="#dddddd"/>
|
||||
<SolidColorBrush x:Key="SongItemSubtitleBrush" Color="#aaaaaa"/>
|
||||
|
||||
<SolidColorBrush x:Key="SongItemTitleBrush2" Color="#FCF8ED" />
|
||||
<SolidColorBrush x:Key="SongItemSubtitleBrush2" Color="#B5B8B8" />
|
||||
|
||||
<LinearGradientBrush x:Key="LightGradientBrush" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#F6F2E3" Offset="0"/>
|
||||
<GradientStop Color="#C9B59F" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<LinearGradientBrush x:Key="DarkGradientBrush" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#F9EBC6" Offset="0"/>
|
||||
<GradientStop Color="#A66831" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<LinearGradientBrush x:Key="PurpleGradientBrush" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#EFD6F5" Offset="0"/>
|
||||
<GradientStop Color="#AC8CC4" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<LinearGradientBrush x:Key="SliderGradientBrush" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#E1B88A" Offset="0"/>
|
||||
<GradientStop Color="#E9B578" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<Style x:Key="PlayerGrid" TargetType="Grid">
|
||||
<Setter Property="Background" Value="#1a1a1a"/>
|
||||
<Setter Property="Padding" Value="10"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#101215" Offset="0"/>
|
||||
<GradientStop Color="#101214" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="BorderBrush" Value="#3B332B" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
</Style>
|
||||
<Style x:Key="SongTitleTextBlock" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource AppFontFamilySemiBold}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
|
||||
@@ -68,11 +100,24 @@
|
||||
Grid.Column="1"
|
||||
Value="{Binding CurrentPosition, Mode=TwoWay, UpdateSourceTrigger=Explicit}"
|
||||
Minimum="0"
|
||||
Foreground="{StaticResource SliderGradientBrush}"
|
||||
Maximum="{Binding MaxPosition, Mode=OneWay}"
|
||||
IsEnabled="{Binding CanUpdatePosition, Mode=OneWay}"
|
||||
ThumbToolTipValueConverter="{StaticResource SecondsToString}"
|
||||
VerticalAlignment="Center"
|
||||
VerticalContentAlignment="Center" />
|
||||
VerticalContentAlignment="Center">
|
||||
<Slider.Resources>
|
||||
<!-- Thumb (the "ball") -->
|
||||
<StaticResource x:Key="SliderThumbBackground" ResourceKey="DarkGradientBrush"/>
|
||||
<StaticResource x:Key="SliderThumbBackgroundPointerOver" ResourceKey="DarkGradientBrush"/>
|
||||
<StaticResource x:Key="SliderThumbBackgroundPressed" ResourceKey="DarkGradientBrush"/>
|
||||
|
||||
<!-- Filled portion of the track (defaults to system accent on hover/press) -->
|
||||
<StaticResource x:Key="SliderTrackValueFill" ResourceKey="SliderGradientBrush"/>
|
||||
<StaticResource x:Key="SliderTrackValueFillPointerOver" ResourceKey="SliderGradientBrush"/>
|
||||
<StaticResource x:Key="SliderTrackValueFillPressed" ResourceKey="SliderGradientBrush"/>
|
||||
</Slider.Resources>
|
||||
</Slider>
|
||||
|
||||
<Border Grid.Column="2" Background="Transparent" Padding="0 8" Width="60" CornerRadius="4" VerticalAlignment="Center" Margin="6 0 0 0">
|
||||
<TextBlock
|
||||
@@ -88,14 +133,14 @@
|
||||
<!-- Song Info -->
|
||||
<Grid Grid.Row="1" Grid.Column="0" Name="PlayingSongGrid">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Grid Margin="0 0 10 0">
|
||||
<Grid Margin="0 0 10 0" BorderBrush="#2D2930" BorderThickness="1" CornerRadius="4">
|
||||
<Image Source="{Binding SongImageSource, Mode=OneWay}" Style="{StaticResource SongImage}"></Image>
|
||||
<Canvas Background="#19000000"></Canvas>
|
||||
</Grid>
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock Foreground="#dddddd" FontWeight="SemiBold" FontSize="16" Text="{Binding Song, Converter={StaticResource SongTitle}}"></TextBlock>
|
||||
<TextBlock Foreground="#aaaaaa" FontSize="14" Text="{Binding Song.Artists, Converter={StaticResource ArtistsToString}}"></TextBlock>
|
||||
<TextBlock Foreground="#aaaaaa" FontSize="14" Text="{Binding Song.Album}"></TextBlock>
|
||||
<TextBlock Foreground="{StaticResource SongItemTitleBrush2}" FontFamily="{StaticResource AppFontFamilySemiBold}" FontWeight="SemiBold" FontSize="16" Text="{Binding Song, Converter={StaticResource SongTitle}}"></TextBlock>
|
||||
<TextBlock Foreground="{StaticResource SongItemSubtitleBrush2}" FontSize="14" Text="{Binding Song.Artists, Converter={StaticResource ArtistsToString}}"></TextBlock>
|
||||
<TextBlock Foreground="{StaticResource SongItemSubtitleBrush2}" FontSize="14" Text="{Binding Song.Album}"></TextBlock>
|
||||
<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>
|
||||
</TextBlock>
|
||||
@@ -106,23 +151,103 @@
|
||||
<!-- Playback Actions -->
|
||||
<Grid Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
||||
<!--<Button Style="{StaticResource FlatButton}" Command="{Binding PreviousSongCommand}">
|
||||
<Path Style="{StaticResource FlatButtonPath}" Fill="{StaticResource LightGradientBrush}" Data="{StaticResource ShuffleIcon}"></Path>
|
||||
</Button>-->
|
||||
<Button Style="{StaticResource FlatButton}" Command="{Binding PreviousSongCommand}">
|
||||
<Path Style="{StaticResource FlatButtonPath}" Data="{StaticResource SkipStartFillIcon}"></Path>
|
||||
<Path Style="{StaticResource FlatButtonPath}" Fill="{StaticResource LightGradientBrush}" Data="{StaticResource SkipStartFillIcon}"></Path>
|
||||
</Button>
|
||||
<Button Style="{StaticResource FlatButton}" Command="{Binding StopSongCommand}">
|
||||
<Path Style="{StaticResource FlatButtonPath}" Data="{StaticResource StopFillIcon}"></Path>
|
||||
<Path Style="{StaticResource FlatButtonPath}" Fill="{StaticResource LightGradientBrush}" Data="{StaticResource StopFillIcon}"></Path>
|
||||
</Button>
|
||||
<Button Style="{StaticResource FlatButton}" Command="{Binding PlaySongCommand}">
|
||||
<Path Style="{StaticResource FlatButtonPath-Large}" Data="{StaticResource PlayFillIcon}"></Path>
|
||||
<Button Style="{StaticResource FlatButton}" Command="{Binding PlaySongCommand}" Padding="0"
|
||||
PointerEntered="PlayButton_PointerEntered" PointerExited="PlayButton_PointerExited">
|
||||
<Button.Resources>
|
||||
<!-- Keep background transparent on hover/press -->
|
||||
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="Transparent"/>
|
||||
<SolidColorBrush x:Key="ButtonBackgroundPressed" Color="Transparent"/>
|
||||
|
||||
<Storyboard x:Name="PlayHoverEnterStoryboard">
|
||||
<ColorAnimation Storyboard.TargetName="PlayGradientStopTop" Storyboard.TargetProperty="Color"
|
||||
To="#FDF6E2" Duration="0:0:0.15" EnableDependentAnimation="True"/>
|
||||
<ColorAnimation Storyboard.TargetName="PlayGradientStopBottom" Storyboard.TargetProperty="Color"
|
||||
To="#C98B4E" Duration="0:0:0.15" EnableDependentAnimation="True"/>
|
||||
<ColorAnimation Storyboard.TargetName="PlayIconGradientStopTop" Storyboard.TargetProperty="Color"
|
||||
To="#FDF6E2" Duration="0:0:0.15" EnableDependentAnimation="True"/>
|
||||
<ColorAnimation Storyboard.TargetName="PlayIconGradientStopBottom" Storyboard.TargetProperty="Color"
|
||||
To="#C98B4E" Duration="0:0:0.15" EnableDependentAnimation="True"/>
|
||||
</Storyboard>
|
||||
<Storyboard x:Name="PlayHoverExitStoryboard">
|
||||
<ColorAnimation Storyboard.TargetName="PlayGradientStopTop" Storyboard.TargetProperty="Color"
|
||||
To="#F9EBC6" Duration="0:0:0.15" EnableDependentAnimation="True"/>
|
||||
<ColorAnimation Storyboard.TargetName="PlayGradientStopBottom" Storyboard.TargetProperty="Color"
|
||||
To="#A66831" Duration="0:0:0.15" EnableDependentAnimation="True"/>
|
||||
<ColorAnimation Storyboard.TargetName="PlayIconGradientStopTop" Storyboard.TargetProperty="Color"
|
||||
To="#F9EBC6" Duration="0:0:0.15" EnableDependentAnimation="True"/>
|
||||
<ColorAnimation Storyboard.TargetName="PlayIconGradientStopBottom" Storyboard.TargetProperty="Color"
|
||||
To="#A66831" Duration="0:0:0.15" EnableDependentAnimation="True"/>
|
||||
</Storyboard>
|
||||
</Button.Resources>
|
||||
<Border BorderThickness="2" CornerRadius="48" Padding="16">
|
||||
<Border.BorderBrush>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop x:Name="PlayGradientStopTop" Color="#F9EBC6" Offset="0"/>
|
||||
<GradientStop x:Name="PlayGradientStopBottom" Color="#A66831" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Border.BorderBrush>
|
||||
<Path Style="{StaticResource FlatButtonPath}" Data="{StaticResource PlayFillIcon}" Margin="1,0,-1,0">
|
||||
<Path.Fill>
|
||||
<LinearGradientBrush StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop x:Name="PlayIconGradientStopTop" Color="#F9EBC6" Offset="0"/>
|
||||
<GradientStop x:Name="PlayIconGradientStopBottom" Color="#A66831" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Path.Fill>
|
||||
</Path>
|
||||
</Border>
|
||||
</Button>
|
||||
<Button Style="{StaticResource FlatButton}" Command="{Binding PauseSongCommand}">
|
||||
<Path Style="{StaticResource FlatButtonPath}" Data="{StaticResource PauseFillIcon}"></Path>
|
||||
<Path Style="{StaticResource FlatButtonPath}" Fill="{StaticResource LightGradientBrush}" Data="{StaticResource PauseFillIcon}"></Path>
|
||||
</Button>
|
||||
<Button Style="{StaticResource FlatButton}" Command="{Binding NextSongCommand}">
|
||||
<Path Style="{StaticResource FlatButtonPath}" Data="{StaticResource SkipEndFillIcon}"></Path>
|
||||
<Path Style="{StaticResource FlatButtonPath}" Fill="{StaticResource LightGradientBrush}" Data="{StaticResource SkipEndFillIcon}"></Path>
|
||||
</Button>
|
||||
<!--<Button Style="{StaticResource FlatButton}" Command="{Binding NextSongCommand}">
|
||||
<Path Style="{StaticResource FlatButtonPath}" Fill="{StaticResource LightGradientBrush}" Data="{StaticResource RepeatIcon}"></Path>
|
||||
</Button>-->
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Volume Slider -->
|
||||
<Grid Grid.Row="1" Grid.Column="2" VerticalAlignment="Center" HorizontalAlignment="Right">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Button Style="{StaticResource FlatButton}" BorderBrush="{StaticResource DarkGradientBrush}" Command="{Binding ToggleMuteCommand}">
|
||||
<Viewbox Width="24" Height="24">
|
||||
<Canvas Width="512" Height="512">
|
||||
<Path Stretch="None" Fill="{Binding VolumeState, Converter={StaticResource VolumeStateToFill}, ConverterParameter={StaticResource DarkGradientBrush}}" Stroke="{StaticResource DarkGradientBrush}" StrokeThickness="32" StrokeLineJoin="Round" StrokeStartLineCap="Round" StrokeEndLineCap="Round" Data="{Binding VolumeState, Converter={StaticResource VolumeStateToIcon}}"></Path>
|
||||
</Canvas>
|
||||
</Viewbox>
|
||||
</Button>
|
||||
<Slider
|
||||
Width="200"
|
||||
Minimum="0.0"
|
||||
Maximum="1.0"
|
||||
StepFrequency="0.01"
|
||||
VerticalAlignment="Center"
|
||||
PointerWheelChanged="VolumeSlider_PointerWheelChanged"
|
||||
Value="{Binding Volume, Mode=TwoWay}">
|
||||
<Slider.Resources>
|
||||
<!-- Thumb (the "ball") -->
|
||||
<StaticResource x:Key="SliderThumbBackground" ResourceKey="DarkGradientBrush"/>
|
||||
<StaticResource x:Key="SliderThumbBackgroundPointerOver" ResourceKey="DarkGradientBrush"/>
|
||||
<StaticResource x:Key="SliderThumbBackgroundPressed" ResourceKey="DarkGradientBrush"/>
|
||||
|
||||
<!-- Filled portion of the track (defaults to system accent on hover/press) -->
|
||||
<StaticResource x:Key="SliderTrackValueFill" ResourceKey="SliderGradientBrush"/>
|
||||
<StaticResource x:Key="SliderTrackValueFillPointerOver" ResourceKey="SliderGradientBrush"/>
|
||||
<StaticResource x:Key="SliderTrackValueFillPressed" ResourceKey="SliderGradientBrush"/>
|
||||
</Slider.Resources>
|
||||
</Slider>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -50,6 +50,18 @@ public sealed partial class PlayerView : UserControl
|
||||
_viewModel.IsPositionChangeInProgress = false;
|
||||
}
|
||||
|
||||
private void PlayButton_PointerEntered(object? sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
PlayHoverExitStoryboard.Stop();
|
||||
PlayHoverEnterStoryboard.Begin();
|
||||
}
|
||||
|
||||
private void PlayButton_PointerExited(object? sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
PlayHoverEnterStoryboard.Stop();
|
||||
PlayHoverExitStoryboard.Begin();
|
||||
}
|
||||
|
||||
private void VolumeSlider_PointerWheelChanged(object? sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Slider slider)
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<UserControl
|
||||
x:Class="Harmonia.WinUI.Views.PlaylistView"
|
||||
<UserControl
|
||||
x:Class="Harmonia.WinUI.Views.PlaylistDetailView"
|
||||
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:vm="using:Harmonia.WinUI.ViewModels"
|
||||
xmlns:converter="using:Harmonia.WinUI.Converters"
|
||||
xmlns:playlists="using:Harmonia.Core.Playlists"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
DataContext="{Binding Source={StaticResource Locator}, Path=PlaylistViewModel}"
|
||||
d:DataContext="{d:DesignInstance Type=vm:PlaylistViewModel, IsDesignTimeCreatable=True}"
|
||||
DataContext="{Binding Source={StaticResource Locator}, Path=PlaylistDetailViewModel}"
|
||||
d:DataContext="{d:DesignInstance Type=vm:PlaylistDetailViewModel, IsDesignTimeCreatable=True}"
|
||||
mc:Ignorable="d">
|
||||
<UserControl.Resources>
|
||||
<converter:BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
<converter:BooleanToVisibilityConverter x:Key="InverseBoolToVis" TrueValue="Collapsed" FalseValue="Visible"/>
|
||||
|
||||
<!--<SolidColorBrush x:Key="PlaylistBackground" Color="#393a45"/>-->
|
||||
<SolidColorBrush x:Key="PlaylistBackground" Color="#292a35"/>
|
||||
<SolidColorBrush x:Key="PlaylistItemHighlightColor" Color="#494a55"/>
|
||||
@@ -24,6 +28,11 @@
|
||||
<SolidColorBrush x:Key="SongItemFooterBrush" Color="#888"/>
|
||||
<SolidColorBrush x:Key="SongItemFooterBrushHighlighted" Color="#aaA2D2F6"/>
|
||||
|
||||
<LinearGradientBrush x:Key="DarkBackgroundBrush" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#101114" Offset="0"/>
|
||||
<GradientStop Color="#090B0F" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<!-- Image Border -->
|
||||
<Style x:Key="PlaylistSongImageBorder" TargetType="Border">
|
||||
<Setter Property="Width" Value="60"/> <!-- as 75 -->
|
||||
@@ -34,6 +43,7 @@
|
||||
<!-- Song Item Title Text Block -->
|
||||
<Style x:Key="SongTitleTextBlock" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource AppFontFamilyMedium}"/>
|
||||
<Setter Property="FontSize" Value="15"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="LineStackingStrategy" Value="BlockLineHeight"/>
|
||||
@@ -67,10 +77,11 @@
|
||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Border Grid.Column="0" Style="{StaticResource PlaylistSongImageBorder}">
|
||||
<Border Grid.Column="0" Style="{StaticResource PlaylistSongImageBorder}" BorderBrush="#2D2930" BorderThickness="1">
|
||||
<Grid>
|
||||
<Image Loaded="Image_Loaded" Unloaded="Image_Unloaded"></Image>
|
||||
<Canvas Background="#19000000"></Canvas>
|
||||
<!--<Canvas Background="#30000000"></Canvas>-->
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid Grid.Column="1" Margin="10 0 0 0" VerticalAlignment="Center">
|
||||
@@ -99,7 +110,7 @@
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</UserControl.Resources>
|
||||
<Grid>
|
||||
<Grid Background="{StaticResource DarkBackgroundBrush}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"></RowDefinition>
|
||||
<RowDefinition Height="*"></RowDefinition>
|
||||
@@ -123,6 +134,11 @@
|
||||
<Setter Property="Background" Value="Transparent"></Setter>
|
||||
</Style>
|
||||
</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.Icon>
|
||||
<PathIcon Data="{StaticResource AddFileIcon}"></PathIcon>
|
||||
@@ -155,28 +171,51 @@
|
||||
</Style>
|
||||
</MenuFlyout.MenuFlyoutPresenterStyle>
|
||||
<MenuFlyoutItem Text="Refresh Tags" Command="{Binding RefreshTagsCommand}">
|
||||
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem Text="Remove Duplicates" Command="{Binding RemoveDuplicateSongsCommand}">
|
||||
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem Text="Remove Missing" Command="{Binding RemoveMissingSongsCommand}">
|
||||
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem Text="Lock Playlist">
|
||||
|
||||
<MenuFlyoutItem Text="Rename Playlist" Command="{Binding RenamePlaylistCommand}">
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem Text="Lock Playlist" Command="{Binding LockPlaylistCommand}" Visibility="{Binding IsPlaylistLocked, Converter={StaticResource InverseBoolToVis}}">
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem Text="Unlock Playlist" Command="{Binding UnlockPlaylistCommand}" Visibility="{Binding IsPlaylistLocked, Converter={StaticResource BoolToVis}}">
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutItem Text="Remove Playlist" Foreground="#ff99a4">
|
||||
|
||||
<MenuFlyoutItem Text="Remove Playlist" Foreground="#ff99a4" Command="{Binding DeletePlaylistCommand}">
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
|
||||
<MenuFlyoutSubItem x:Name="SortByMenuFlyoutSubItem" Text="Sort By...">
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutItem Text="Randomize" Command="{Binding RandomizeAllCommand}">
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem Text="Reverse" Command="{Binding ReverseAllCommand}">
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyoutSubItem>
|
||||
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
|
||||
<MenuFlyoutItem Text="Settings">
|
||||
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</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>
|
||||
</Grid>
|
||||
<ListView
|
||||
@@ -184,11 +223,11 @@
|
||||
Name="PlaylistListView"
|
||||
ItemsSource="{Binding FilteredPlaylistSongs}"
|
||||
ItemTemplate="{StaticResource SongTemplate}"
|
||||
CanReorderItems="True"
|
||||
CanDragItems="True"
|
||||
CanReorderItems="{Binding CanReorderSongs, Mode=OneWay}"
|
||||
CanDragItems="{Binding CanReorderSongs, Mode=OneWay}"
|
||||
DragItemsStarting="PlaylistListView_DragItemsStarting"
|
||||
DragItemsCompleted="PlaylistListView_DragItemsCompleted"
|
||||
AllowDrop="True"
|
||||
AllowDrop="{Binding CanReorderSongs, Mode=OneWay}"
|
||||
SelectionMode="Extended"
|
||||
SelectionChanged="PlaylistListView_SelectionChanged">
|
||||
<ListView.ContextFlyout>
|
||||
@@ -199,7 +238,7 @@
|
||||
<Setter Property="Background" Value="Transparent"></Setter>
|
||||
</Style>
|
||||
</MenuFlyout.MenuFlyoutPresenterStyle>
|
||||
<MenuFlyoutItem Text="Play" FontWeight="SemiBold" Command="{Binding PlaySongCommand}">
|
||||
<MenuFlyoutItem Text="Play" FontFamily="{StaticResource AppFontFamilySemiBold}" FontWeight="SemiBold" Command="{Binding PlaySongCommand}">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<PathIcon Data="{StaticResource PlayIcon}"></PathIcon>
|
||||
</MenuFlyoutItem.Icon>
|
||||
@@ -217,6 +256,17 @@
|
||||
</MenuFlyoutItem.KeyboardAccelerators>
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutSubItem x:Name="SortSelectedByMenuFlyoutSubItem" Text="Sort By...">
|
||||
<MenuFlyoutSubItem.Icon>
|
||||
<PathIcon Data="{StaticResource SortIcon}"></PathIcon>
|
||||
</MenuFlyoutSubItem.Icon>
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutItem Text="Randomize" Command="{Binding RandomizeSelectedCommand}">
|
||||
</MenuFlyoutItem>
|
||||
<MenuFlyoutItem Text="Reverse" Command="{Binding ReverseSelectedCommand}">
|
||||
</MenuFlyoutItem>
|
||||
</MenuFlyoutSubItem>
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
<MenuFlyoutItem Text="Cut" Command="{Binding CutSongsCommand}">
|
||||
<MenuFlyoutItem.Icon>
|
||||
<PathIcon Data="{StaticResource CutIcon}"></PathIcon>
|
||||
@@ -1,5 +1,4 @@
|
||||
using CommunityToolkit.WinUI;
|
||||
using Harmonia.Core.Imaging;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Harmonia.Core.Playlists;
|
||||
using Harmonia.WinUI.ViewModels;
|
||||
using Microsoft.UI.Dispatching;
|
||||
@@ -7,34 +6,66 @@ using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Windows.UI.Popups;
|
||||
|
||||
namespace Harmonia.WinUI.Views;
|
||||
|
||||
public sealed partial class PlaylistView : UserControl
|
||||
public sealed partial class PlaylistDetailView : UserControl
|
||||
{
|
||||
private readonly PlaylistViewModel _viewModel;
|
||||
private readonly PlaylistDetailViewModel _viewModel;
|
||||
|
||||
public PlaylistView()
|
||||
public PlaylistDetailView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_viewModel = (PlaylistViewModel)DataContext;
|
||||
_viewModel = (PlaylistDetailViewModel)DataContext;
|
||||
_viewModel.PropertyChanging += OnViewModelPropertyChanging;
|
||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
_viewModel.PlayingSongChangedAutomatically += OnPlayingSongChangedAutomatically;
|
||||
|
||||
foreach (MenuFlyoutItemBase item in PlaylistListViewMenuFlyout.Items)
|
||||
{
|
||||
item.DataContextChanged += Item_DataContextChanged;
|
||||
HookDataContextChanged(item);
|
||||
}
|
||||
|
||||
int currentSortItemIndex = 0;
|
||||
|
||||
foreach (SortItem sortItem in _viewModel.SortItems)
|
||||
{
|
||||
SortByMenuFlyoutSubItem.Items.Insert(currentSortItemIndex, new MenuFlyoutItem
|
||||
{
|
||||
Text = sortItem.Name,
|
||||
Command = _viewModel.SortAllCommand,
|
||||
CommandParameter = sortItem
|
||||
});
|
||||
|
||||
SortSelectedByMenuFlyoutSubItem.Items.Insert(currentSortItemIndex, new MenuFlyoutItem
|
||||
{
|
||||
Text = sortItem.Name,
|
||||
Command = _viewModel.SortSelectedCommand,
|
||||
CommandParameter = sortItem
|
||||
});
|
||||
|
||||
currentSortItemIndex++;
|
||||
}
|
||||
|
||||
_viewModel.SortSelectedCommand.CanExecuteChanged += (_, _) =>
|
||||
SortSelectedByMenuFlyoutSubItem.IsEnabled = _viewModel.SortSelectedCommand.CanExecute(null);
|
||||
|
||||
SortSelectedByMenuFlyoutSubItem.IsEnabled = _viewModel.SortSelectedCommand.CanExecute(null);
|
||||
|
||||
Loaded += OnLoaded;
|
||||
}
|
||||
|
||||
private void OnLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Loaded -= OnLoaded;
|
||||
|
||||
// Defer until the ListView has completed its first layout/container realization pass.
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, BringPlayingSongIntoView);
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanging(object? sender, PropertyChangingEventArgs e)
|
||||
@@ -52,11 +83,56 @@ public sealed partial class PlaylistView : UserControl
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(_viewModel.PlayingSong))
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(_viewModel.PlayingSong);
|
||||
case nameof(_viewModel.PlayingSong):
|
||||
ListViewItem listViewItem = (ListViewItem)PlaylistListView.ContainerFromItem(_viewModel.PlayingSong);
|
||||
UpdateListViewItemStyle(listViewItem, true);
|
||||
break;
|
||||
case nameof(_viewModel.Playlist):
|
||||
ScrollToTop();
|
||||
break;
|
||||
case nameof(_viewModel.SelectedPlaylistSongs):
|
||||
SyncListViewSelection();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateListViewItemStyle(listViewItem, true);
|
||||
private bool _isSyncingSelection;
|
||||
|
||||
private void SyncListViewSelection()
|
||||
{
|
||||
_isSyncingSelection = true;
|
||||
|
||||
try
|
||||
{
|
||||
PlaylistListView.DeselectAll();
|
||||
|
||||
foreach (PlaylistSong playlistSong in _viewModel.SelectedPlaylistSongs)
|
||||
{
|
||||
int index = PlaylistListView.Items.IndexOf(playlistSong);
|
||||
|
||||
if (index >= 0)
|
||||
{
|
||||
PlaylistListView.SelectRange(new ItemIndexRange(index, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSyncingSelection = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ScrollToTop()
|
||||
{
|
||||
if (PlaylistListView.FindDescendant<ScrollViewer>() is ScrollViewer scrollViewer)
|
||||
{
|
||||
scrollViewer.ChangeView(null, 0, null, disableAnimation: false);
|
||||
}
|
||||
else if (_viewModel.FilteredPlaylistSongs.FirstOrDefault() is PlaylistSong firstSong)
|
||||
{
|
||||
PlaylistListView.ScrollIntoView(firstSong);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +208,19 @@ public sealed partial class PlaylistView : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private void HookDataContextChanged(MenuFlyoutItemBase item)
|
||||
{
|
||||
item.DataContextChanged += Item_DataContextChanged;
|
||||
|
||||
if (item is MenuFlyoutSubItem subItem)
|
||||
{
|
||||
foreach (MenuFlyoutItemBase childItem in subItem.Items)
|
||||
{
|
||||
HookDataContextChanged(childItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Item_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||
{
|
||||
if (sender is not MenuFlyoutItemBase item)
|
||||
@@ -195,7 +284,7 @@ public sealed partial class PlaylistView : UserControl
|
||||
if (args.NewValue is not PlaylistSong playlistSong)
|
||||
return;
|
||||
|
||||
bool isPlaying = playlistSong == _viewModel.PlayingSong;
|
||||
bool isPlaying = playlistSong.UID == _viewModel.PlayingSong?.UID;
|
||||
|
||||
UpdateListViewItemStyle(sender, isPlaying);
|
||||
}
|
||||
@@ -230,6 +319,9 @@ public sealed partial class PlaylistView : UserControl
|
||||
|
||||
private void PlaylistListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_isSyncingSelection)
|
||||
return;
|
||||
|
||||
if (sender is not ListView listView)
|
||||
return;
|
||||
|
||||
143
Harmonia.WinUI/Views/PlaylistsView.xaml
Normal file
143
Harmonia.WinUI/Views/PlaylistsView.xaml
Normal file
@@ -0,0 +1,143 @@
|
||||
<?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:converter="using:Harmonia.WinUI.Converters"
|
||||
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>
|
||||
<converter:BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||
<converter:BooleanToVisibilityConverter x:Key="InverseBoolToVis" TrueValue="Collapsed" FalseValue="Visible"/>
|
||||
|
||||
<SolidColorBrush x:Key="PlaylistItemIconBrush" Color="#dddddd"/>
|
||||
<SolidColorBrush x:Key="PlaylistItemTitleBrush" Color="#dddddd"/>
|
||||
<SolidColorBrush x:Key="PlaylistItemSubtitleBrush" Color="#aaaaaa"/>
|
||||
|
||||
<LinearGradientBrush x:Key="DarkBackgroundBrush" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#161719" Offset="0"/>
|
||||
<GradientStop Color="#0B0D0F" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<LinearGradientBrush x:Key="ActiveGradientBrush" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#E1B88A" Offset="0"/>
|
||||
<GradientStop Color="#E9B578" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<LinearGradientBrush x:Key="ActiveDarkGradientBrush" StartPoint="0,0" EndPoint="0,1">
|
||||
<GradientStop Color="#F9EBC6" Offset="0"/>
|
||||
<GradientStop Color="#A66831" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
|
||||
<!-- Playlist Icon Path -->
|
||||
<Style x:Key="PlaylistIconPath" TargetType="PathIcon">
|
||||
<Setter Property="Foreground" Value="{StaticResource PlaylistItemIconBrush}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource ActiveDarkGradientBrush}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SelectedPlaylistIconPath" TargetType="PathIcon" BasedOn="{StaticResource PlaylistIconPath}">
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentTextFillColorPrimaryBrush}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource ActiveGradientBrush}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource ActiveDarkGradientBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- Playlist Title Text Block -->
|
||||
<Style x:Key="PlaylistTitleTextBlock" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource PlaylistItemTitleBrush}"/>
|
||||
<Setter Property="FontSize" Value="15"/>
|
||||
<Setter Property="FontWeight" Value="Normal"/>
|
||||
<Setter Property="LineStackingStrategy" Value="BlockLineHeight"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||
<Setter Property="LineHeight" Value="0"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SelectedPlaylistTitleTextBlock" TargetType="TextBlock" BasedOn="{StaticResource PlaylistTitleTextBlock}">
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentTextFillColorPrimaryBrush}"/>
|
||||
<Setter Property="FontFamily" Value="{StaticResource AppFontFamilyMedium}"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
</Style>
|
||||
|
||||
<!-- Playlist Item Subtitle Text Block -->
|
||||
<Style x:Key="PlaylistSubtitleTextBlock" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource PlaylistItemSubtitleBrush}"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="LineStackingStrategy" Value="BlockLineHeight"/>
|
||||
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||
<Setter Property="LineHeight" Value="0"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="SelectedPlaylistSubtitleTextBlock" TargetType="TextBlock" BasedOn="{StaticResource PlaylistSubtitleTextBlock}">
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentTextFillColorSecondaryBrush}"/>
|
||||
</Style>
|
||||
|
||||
<DataTemplate x:Key="PlaylistTemplate" x:DataType="vm:PlaylistItemViewModel">
|
||||
<Grid x:Name="PlaylistListViewItem">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="200" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<Viewbox Height="24" Width="24">
|
||||
<PathIcon x:Name="MusicNoteListPathIcon" Data="{StaticResource MusicNoteList}" />
|
||||
</Viewbox>
|
||||
<TextBlock x:Name="PlaylistTitleTextBlock" Text="{x:Bind Name, Mode=OneWay}" Style="{StaticResource PlaylistTitleTextBlock}" />
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center" Spacing="8">
|
||||
<TextBlock x:Name="PlaylistSubtitleTextBlock" Text="{x:Bind Summary, Mode=OneWay}" Style="{StaticResource PlaylistSubtitleTextBlock}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</UserControl.Resources>
|
||||
<Grid Background="{StaticResource DarkBackgroundBrush}" BorderBrush="#3C342F" BorderThickness="1 0 1 0">
|
||||
<ListView
|
||||
Name="PlaylistsListView"
|
||||
ItemsSource="{Binding Playlists, Mode=OneWay}"
|
||||
SelectedItem="{Binding SelectedPlaylist, Mode=TwoWay}"
|
||||
ItemTemplate="{StaticResource PlaylistTemplate}"
|
||||
RightTapped="PlaylistsListView_RightTapped">
|
||||
<ListView.ContextFlyout>
|
||||
<MenuFlyout
|
||||
x:Name="PlaylistListViewMenuFlyout"
|
||||
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="{Binding FlyoutPlaylist.Name}" IsEnabled="False" FontWeight="SemiLight">
|
||||
<MenuFlyoutItem.Resources>
|
||||
<SolidColorBrush x:Key="MenuFlyoutItemForegroundDisabled" Color="#aaaaaa"/>
|
||||
</MenuFlyoutItem.Resources>
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
|
||||
<MenuFlyoutItem Text="Rename" Command="{Binding RenamePlaylistCommand}" CommandParameter="{Binding FlyoutPlaylist}">
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem Text="Lock" Command="{Binding LockPlaylistCommand}" CommandParameter="{Binding FlyoutPlaylist}" Visibility="{Binding FlyoutPlaylist.IsLocked, Converter={StaticResource InverseBoolToVis}}">
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutItem Text="Unlock" Command="{Binding UnlockPlaylistCommand}" CommandParameter="{Binding FlyoutPlaylist}" Visibility="{Binding FlyoutPlaylist.IsLocked, Converter={StaticResource BoolToVis}}">
|
||||
</MenuFlyoutItem>
|
||||
|
||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||
|
||||
<MenuFlyoutItem Text="Remove" Foreground="#ff99a4" Command="{Binding DeletePlaylistCommand}" CommandParameter="{Binding FlyoutPlaylist}">
|
||||
</MenuFlyoutItem>
|
||||
|
||||
</MenuFlyout>
|
||||
</ListView.ContextFlyout>
|
||||
</ListView>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
126
Harmonia.WinUI/Views/PlaylistsView.xaml.cs
Normal file
126
Harmonia.WinUI/Views/PlaylistsView.xaml.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using CommunityToolkit.WinUI;
|
||||
using Harmonia.WinUI.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace Harmonia.WinUI.Views;
|
||||
|
||||
public sealed partial class PlaylistsView : UserControl
|
||||
{
|
||||
private readonly PlaylistsViewModel _viewModel;
|
||||
|
||||
public PlaylistsView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
_viewModel = (PlaylistsViewModel)DataContext;
|
||||
_viewModel.PropertyChanging += OnViewModelPropertyChanging;
|
||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
|
||||
PlaylistsListView.ContainerContentChanging += OnContainerContentChanging;
|
||||
|
||||
foreach (MenuFlyoutItemBase item in PlaylistListViewMenuFlyout.Items)
|
||||
{
|
||||
item.DataContext = _viewModel;
|
||||
}
|
||||
}
|
||||
|
||||
private void PlaylistsListView_RightTapped(object sender, Microsoft.UI.Xaml.Input.RightTappedRoutedEventArgs e)
|
||||
{
|
||||
_viewModel.FlyoutPlaylist = (e.OriginalSource as FrameworkElement)?.DataContext as PlaylistItemViewModel;
|
||||
}
|
||||
|
||||
private void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
|
||||
{
|
||||
if (args.InRecycleQueue || args.ItemContainer is not ListViewItem listViewItem)
|
||||
return;
|
||||
|
||||
PlaylistItemStyle style = ReferenceEquals(args.Item, _viewModel.ActivePlaylist)
|
||||
? PlaylistItemStyle.Selected
|
||||
: PlaylistItemStyle.Normal;
|
||||
|
||||
UpdateListViewItemStyle(listViewItem, style);
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanging(object? sender, PropertyChangingEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(_viewModel.ActivePlaylist):
|
||||
UpdateListViewItemStyle(PlaylistItemStyle.Normal);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(_viewModel.ActivePlaylist):
|
||||
UpdateListViewItemStyle(PlaylistItemStyle.Selected);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateListViewItemStyle(PlaylistItemStyle style)
|
||||
{
|
||||
if (PlaylistsListView.ContainerFromItem(_viewModel.ActivePlaylist) is not ListViewItem listViewItem)
|
||||
return;
|
||||
|
||||
UpdateListViewItemStyle(listViewItem, style);
|
||||
}
|
||||
|
||||
private void UpdateListViewItemStyle(ListViewItem listViewItem, PlaylistItemStyle style)
|
||||
{
|
||||
FrameworkElement? frameworkElement = TryGetFrameworkElement(listViewItem, "PlaylistListViewItem");
|
||||
|
||||
if (frameworkElement == null)
|
||||
return;
|
||||
|
||||
string playlistItemIconBrushName = style == PlaylistItemStyle.Selected ? "SelectedPlaylistIconPath" : "PlaylistIconPath";
|
||||
string playlistItemTitleBrushName = style == PlaylistItemStyle.Selected ? "SelectedPlaylistTitleTextBlock" : "PlaylistTitleTextBlock";
|
||||
string playlistItemSubtitleBrushName = style == PlaylistItemStyle.Selected ? "SelectedPlaylistSubtitleTextBlock" : "PlaylistSubtitleTextBlock";
|
||||
|
||||
UpdateElementStyle(frameworkElement, "MusicNoteListPathIcon", playlistItemIconBrushName);
|
||||
UpdateElementStyle(frameworkElement, "PlaylistTitleTextBlock", playlistItemTitleBrushName);
|
||||
UpdateElementStyle(frameworkElement, "PlaylistSubtitleTextBlock", playlistItemSubtitleBrushName);
|
||||
}
|
||||
|
||||
private static FrameworkElement? TryGetFrameworkElement(ListViewItem listViewItem, string elementName)
|
||||
{
|
||||
if (listViewItem.Name == elementName)
|
||||
{
|
||||
return listViewItem;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (listViewItem.FindDescendant(elementName) is not FrameworkElement frameworkElement)
|
||||
return null;
|
||||
|
||||
return frameworkElement;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateElementStyle(FrameworkElement dependencyObject, string elementName, string resourceName)
|
||||
{
|
||||
if (dependencyObject.FindDescendant(elementName) is not FrameworkElement frameworkElement)
|
||||
return;
|
||||
|
||||
Resources.TryGetValue(resourceName, out object? resource);
|
||||
|
||||
if (resource == null)
|
||||
Application.Current.Resources.TryGetValue(resourceName, out resource);
|
||||
|
||||
if (resource is not Style style)
|
||||
return;
|
||||
|
||||
frameworkElement.Style = style;
|
||||
}
|
||||
}
|
||||
|
||||
public enum PlaylistItemStyle
|
||||
{
|
||||
Normal,
|
||||
Selected
|
||||
}
|
||||
Reference in New Issue
Block a user