Compare commits
30 Commits
16b87d255e
...
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 |
@@ -97,7 +97,7 @@ public class BassAudioEngine : IAudioEngine, IDisposable
|
|||||||
|
|
||||||
List<string> supportedFormats = [.. Bass.SupportedFormats.Split(';')];
|
List<string> supportedFormats = [.. Bass.SupportedFormats.Split(';')];
|
||||||
//supportedFormats.Add(".aac");
|
//supportedFormats.Add(".aac");
|
||||||
//supportedFormats.Add(".m4a");
|
supportedFormats.Add(".m4a");
|
||||||
supportedFormats.Add("*.flac");
|
supportedFormats.Add("*.flac");
|
||||||
//supportedFormats.Add(".opus");
|
//supportedFormats.Add(".opus");
|
||||||
//supportedFormats.Add(".wma");
|
//supportedFormats.Add(".wma");
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ManagedBass" Version="4.0.2" />
|
<PackageReference Include="ManagedBass" Version="4.0.2" />
|
||||||
<PackageReference Include="ManagedBass.Flac" Version="4.0.2" />
|
<PackageReference Include="ManagedBass.Flac" Version="4.0.2" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.10" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.12" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="10.0.12" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="10.0.12" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.12" />
|
||||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,13 @@ public class AudioPlayer : IAudioPlayer
|
|||||||
}
|
}
|
||||||
protected set
|
protected set
|
||||||
{
|
{
|
||||||
|
Playlist? oldPlaylist = _playlist;
|
||||||
_playlist = value;
|
_playlist = value;
|
||||||
|
|
||||||
NotifyPropertyChanged(nameof(Playlist));
|
NotifyPropertyChanged(nameof(Playlist));
|
||||||
PlaylistChanged?.Invoke(this, new());
|
|
||||||
|
PlaylistChangedEventArgs eventArgs = new(oldPlaylist, value);
|
||||||
|
PlaylistChanged?.Invoke(this, eventArgs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,9 +41,13 @@ public class AudioPlayer : IAudioPlayer
|
|||||||
}
|
}
|
||||||
protected set
|
protected set
|
||||||
{
|
{
|
||||||
|
PlaylistSong? oldSong = _playingSong;
|
||||||
_playingSong = value;
|
_playingSong = value;
|
||||||
|
|
||||||
NotifyPropertyChanged(nameof(PlayingSong));
|
NotifyPropertyChanged(nameof(PlayingSong));
|
||||||
PlayingSongChanged?.Invoke(this, new());
|
|
||||||
|
PlayingSongChangedEventArgs eventArgs = new(oldSong, value);
|
||||||
|
PlayingSongChanged?.Invoke(this, eventArgs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +123,8 @@ public class AudioPlayer : IAudioPlayer
|
|||||||
|
|
||||||
protected virtual int PreviousSongSecondsThreshold => 5;
|
protected virtual int PreviousSongSecondsThreshold => 5;
|
||||||
|
|
||||||
public event EventHandler? PlaylistChanged;
|
public event EventHandler<PlaylistChangedEventArgs>? PlaylistChanged;
|
||||||
public event EventHandler? PlayingSongChanged;
|
public event EventHandler<PlayingSongChangedEventArgs>? PlayingSongChanged;
|
||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
public AudioPlayer(IAudioEngine audioEngine, IPlaylistManager playlistManager)
|
public AudioPlayer(IAudioEngine audioEngine, IPlaylistManager playlistManager)
|
||||||
@@ -173,7 +181,7 @@ public class AudioPlayer : IAudioPlayer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int currentIndex = Playlist.Songs.IndexOf(CurrentPlaylistSong);
|
int currentIndex = Playlist.IndexOf(CurrentPlaylistSong);
|
||||||
int nextIndex = currentIndex + 1;
|
int nextIndex = currentIndex + 1;
|
||||||
|
|
||||||
if (nextIndex > Playlist.Songs.Count - 1)
|
if (nextIndex > Playlist.Songs.Count - 1)
|
||||||
@@ -200,7 +208,7 @@ public class AudioPlayer : IAudioPlayer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int currentIndex = Playlist.Songs.IndexOf(CurrentPlaylistSong);
|
int currentIndex = Playlist.IndexOf(CurrentPlaylistSong);
|
||||||
int nextIndex = currentIndex - 1;
|
int nextIndex = currentIndex - 1;
|
||||||
|
|
||||||
if (nextIndex < 0)
|
if (nextIndex < 0)
|
||||||
@@ -233,7 +241,7 @@ public class AudioPlayer : IAudioPlayer
|
|||||||
{
|
{
|
||||||
if (Playlist == null || Playlist.Songs.Contains(song) == false)
|
if (Playlist == null || Playlist.Songs.Contains(song) == false)
|
||||||
{
|
{
|
||||||
Playlist? newPlaylist = _playlistManager.GetPlaylist(song);
|
Playlist? newPlaylist = _playlistManager.FindPlaylistContaining(song);
|
||||||
|
|
||||||
if (newPlaylist == null)
|
if (newPlaylist == null)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public interface IAudioPlayer
|
|||||||
Task PreviousAsync();
|
Task PreviousAsync();
|
||||||
Task NextAsync();
|
Task NextAsync();
|
||||||
|
|
||||||
event EventHandler PlaylistChanged;
|
event EventHandler<PlaylistChangedEventArgs> PlaylistChanged;
|
||||||
event EventHandler PlayingSongChanged;
|
event EventHandler<PlayingSongChangedEventArgs> PlayingSongChanged;
|
||||||
event PropertyChangedEventHandler PropertyChanged;
|
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 InitializeAsync();
|
||||||
Task<Playlist> AddPlaylistAsync();
|
Task<Playlist> AddPlaylistAsync();
|
||||||
void RemovePlaylist(Playlist playlist);
|
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<PlaylistAddedEventArgs> PlaylistAdded;
|
||||||
event EventHandler<PlaylistRemovedEventArgs> PlaylistRemoved;
|
event EventHandler<PlaylistRemovedEventArgs> PlaylistRemoved;
|
||||||
|
event EventHandler<PlaylistSaveFailedEventArgs>? PlaylistSaveFailed;
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,5 @@ namespace Harmonia.Core.Playlists;
|
|||||||
|
|
||||||
public interface IPlaylistRepository : IRepository<Playlist>
|
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
|
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 UID { get; init; } = Guid.NewGuid().ToString();
|
||||||
public string? Name { get; set; }
|
public string? Name { get; private set; }
|
||||||
public List<PlaylistSong> Songs { get; init; } = []; // TODO: Change to "private init" once deserialization is fixed
|
public IReadOnlyList<PlaylistSong> Songs => _songs;
|
||||||
public List<GroupOption> GroupOptions { get; set; } = [];
|
public IReadOnlyList<GroupOption> GroupOptions => _groupOptions;
|
||||||
public List<SortOption> SortOptions { get; set; } = [];
|
public IReadOnlyList<SortOption> SortOptions => _sortOptions;
|
||||||
public bool IsLocked { get; set; }
|
public bool IsLocked { get; private set; }
|
||||||
|
|
||||||
public event EventHandler<PlaylistUpdatedEventArgs>? PlaylistUpdated;
|
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)
|
public void AddSong(Song song, int? index = null)
|
||||||
{
|
{
|
||||||
AddSongs([song], index);
|
AddSongs([song], index);
|
||||||
@@ -34,9 +121,9 @@ public class Playlist
|
|||||||
if (playlistSongs.Length == 0)
|
if (playlistSongs.Length == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int insertIndex = index ?? Songs.Count;
|
int insertIndex = index ?? _songs.Count;
|
||||||
|
|
||||||
Songs.InsertRange(insertIndex, playlistSongs);
|
_songs.InsertRange(insertIndex, playlistSongs);
|
||||||
|
|
||||||
PlaylistUpdatedEventArgs eventArgs = new()
|
PlaylistUpdatedEventArgs eventArgs = new()
|
||||||
{
|
{
|
||||||
@@ -51,7 +138,7 @@ public class Playlist
|
|||||||
|
|
||||||
public void MoveSong(PlaylistSong playlistSong, int newIndex)
|
public void MoveSong(PlaylistSong playlistSong, int newIndex)
|
||||||
{
|
{
|
||||||
int currentIndex = Songs.IndexOf(playlistSong);
|
int currentIndex = _songs.IndexOf(playlistSong);
|
||||||
|
|
||||||
MoveSong(currentIndex, newIndex);
|
MoveSong(currentIndex, newIndex);
|
||||||
}
|
}
|
||||||
@@ -64,10 +151,10 @@ public class Playlist
|
|||||||
if (oldIndex == newIndex)
|
if (oldIndex == newIndex)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PlaylistSong playlistSong = Songs[oldIndex];
|
PlaylistSong playlistSong = _songs[oldIndex];
|
||||||
|
|
||||||
Songs.Remove(playlistSong);
|
_songs.Remove(playlistSong);
|
||||||
Songs.Insert(newIndex, playlistSong);
|
_songs.Insert(newIndex, playlistSong);
|
||||||
|
|
||||||
PlaylistUpdatedEventArgs eventArgs = new()
|
PlaylistUpdatedEventArgs eventArgs = new()
|
||||||
{
|
{
|
||||||
@@ -83,9 +170,12 @@ public class Playlist
|
|||||||
|
|
||||||
public void SortSongs(PlaylistSong[] playlistSongs, SortOption[] sortOptions)
|
public void SortSongs(PlaylistSong[] playlistSongs, SortOption[] sortOptions)
|
||||||
{
|
{
|
||||||
|
if (IsLocked)
|
||||||
|
return;
|
||||||
|
|
||||||
Dictionary<int, PlaylistSong> oldPlaylistSongs = playlistSongs
|
Dictionary<int, PlaylistSong> oldPlaylistSongs = playlistSongs
|
||||||
.OrderBy(Songs.IndexOf)
|
.OrderBy(_songs.IndexOf)
|
||||||
.ToDictionary(Songs.IndexOf, playlistSong => playlistSong);
|
.ToDictionary(_songs.IndexOf, playlistSong => playlistSong);
|
||||||
|
|
||||||
Song[] songs = [.. playlistSongs.Select(playlistSong => playlistSong.Song)];
|
Song[] songs = [.. playlistSongs.Select(playlistSong => playlistSong.Song)];
|
||||||
Song[] sortedSongs = [.. songs.SortBy(sortOptions)];
|
Song[] sortedSongs = [.. songs.SortBy(sortOptions)];
|
||||||
@@ -101,8 +191,8 @@ public class Playlist
|
|||||||
if (newPlaylistSong == playlistSong)
|
if (newPlaylistSong == playlistSong)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Songs.RemoveAt(index);
|
_songs.RemoveAt(index);
|
||||||
Songs.Insert(index, newPlaylistSong);
|
_songs.Insert(index, newPlaylistSong);
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaylistUpdatedEventArgs eventArgs = new()
|
PlaylistUpdatedEventArgs eventArgs = new()
|
||||||
@@ -116,6 +206,50 @@ public class Playlist
|
|||||||
PlaylistUpdated?.Invoke(this, eventArgs);
|
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)
|
public void RemoveSong(int index)
|
||||||
{
|
{
|
||||||
RemoveSongs(index, 1);
|
RemoveSongs(index, 1);
|
||||||
@@ -123,7 +257,7 @@ public class Playlist
|
|||||||
|
|
||||||
public void RemoveSongs(int index, int count)
|
public void RemoveSongs(int index, int count)
|
||||||
{
|
{
|
||||||
PlaylistSong[] playlistSongs = [.. Songs.GetRange(index, count)];
|
PlaylistSong[] playlistSongs = [.. _songs.GetRange(index, count)];
|
||||||
|
|
||||||
RemoveSongs(playlistSongs);
|
RemoveSongs(playlistSongs);
|
||||||
}
|
}
|
||||||
@@ -142,7 +276,7 @@ public class Playlist
|
|||||||
|
|
||||||
foreach (PlaylistSong playlistSong in playlistSongs)
|
foreach (PlaylistSong playlistSong in playlistSongs)
|
||||||
{
|
{
|
||||||
if (Songs.Remove(playlistSong))
|
if (_songs.Remove(playlistSong))
|
||||||
{
|
{
|
||||||
removedSongs.Add(playlistSong);
|
removedSongs.Add(playlistSong);
|
||||||
}
|
}
|
||||||
@@ -164,6 +298,8 @@ public class Playlist
|
|||||||
|
|
||||||
public void ImportTags(Song[] songs)
|
public void ImportTags(Song[] songs)
|
||||||
{
|
{
|
||||||
|
List<PlaylistSong> updatedPlaylistSongs = [];
|
||||||
|
|
||||||
foreach (Song song in songs)
|
foreach (Song song in songs)
|
||||||
{
|
{
|
||||||
PlaylistSong[] playlistSongs = [.. Songs.Where(playlistSong =>
|
PlaylistSong[] playlistSongs = [.. Songs.Where(playlistSong =>
|
||||||
@@ -171,10 +307,24 @@ public class Playlist
|
|||||||
|
|
||||||
foreach (PlaylistSong playlistSong in playlistSongs)
|
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()
|
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 IPlaylistRepository playlistRepository;
|
||||||
private readonly List<Playlist> _playlists = [];
|
private readonly List<Playlist> _playlists = [];
|
||||||
|
private readonly Dictionary<string, Playlist> _playlistsBySongUid = [];
|
||||||
|
|
||||||
public IReadOnlyList<Playlist> Playlists => _playlists;
|
public IReadOnlyList<Playlist> Playlists => _playlists;
|
||||||
|
|
||||||
@@ -16,14 +17,17 @@ public class PlaylistManager : IPlaylistManager
|
|||||||
}
|
}
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
|
Playlist? oldPlaylist = _currentPlaylist;
|
||||||
_currentPlaylist = value;
|
_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<PlaylistAddedEventArgs>? PlaylistAdded;
|
||||||
public event EventHandler<PlaylistRemovedEventArgs>? PlaylistRemoved;
|
public event EventHandler<PlaylistRemovedEventArgs>? PlaylistRemoved;
|
||||||
|
public event EventHandler<PlaylistSaveFailedEventArgs>? PlaylistSaveFailed;
|
||||||
|
|
||||||
public PlaylistManager(IPlaylistRepository playlistRepository)
|
public PlaylistManager(IPlaylistRepository playlistRepository)
|
||||||
{
|
{
|
||||||
@@ -37,6 +41,11 @@ public class PlaylistManager : IPlaylistManager
|
|||||||
foreach (Playlist playlist in _playlists)
|
foreach (Playlist playlist in _playlists)
|
||||||
{
|
{
|
||||||
playlist.PlaylistUpdated += OnPlaylistUpdated;
|
playlist.PlaylistUpdated += OnPlaylistUpdated;
|
||||||
|
|
||||||
|
foreach (PlaylistSong song in playlist.Songs)
|
||||||
|
{
|
||||||
|
_playlistsBySongUid[song.UID] = playlist;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CurrentPlaylist = _playlists.Count > 0 ? _playlists[0] : await AddPlaylistAsync();
|
CurrentPlaylist = _playlists.Count > 0 ? _playlists[0] : await AddPlaylistAsync();
|
||||||
@@ -44,15 +53,17 @@ public class PlaylistManager : IPlaylistManager
|
|||||||
|
|
||||||
public async Task<Playlist> AddPlaylistAsync()
|
public async Task<Playlist> AddPlaylistAsync()
|
||||||
{
|
{
|
||||||
Playlist playlist = new()
|
Playlist playlist = new("New Playlist");
|
||||||
{
|
|
||||||
Name = "New Playlist"
|
|
||||||
};
|
|
||||||
|
|
||||||
playlist.PlaylistUpdated += OnPlaylistUpdated;
|
playlist.PlaylistUpdated += OnPlaylistUpdated;
|
||||||
_playlists.Add(playlist);
|
_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));
|
PlaylistAdded?.Invoke(this, new(playlist));
|
||||||
|
|
||||||
@@ -64,14 +75,29 @@ public class PlaylistManager : IPlaylistManager
|
|||||||
playlist.PlaylistUpdated -= OnPlaylistUpdated;
|
playlist.PlaylistUpdated -= OnPlaylistUpdated;
|
||||||
_playlists.Remove(playlist);
|
_playlists.Remove(playlist);
|
||||||
|
|
||||||
|
foreach (PlaylistSong song in playlist.Songs)
|
||||||
|
{
|
||||||
|
_playlistsBySongUid.Remove(song.UID);
|
||||||
|
}
|
||||||
|
|
||||||
playlistRepository.Delete(playlist);
|
playlistRepository.Delete(playlist);
|
||||||
|
|
||||||
|
if (CurrentPlaylist == playlist)
|
||||||
|
{
|
||||||
|
CurrentPlaylist = _playlists.Count > 0 ? _playlists[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
PlaylistRemoved?.Invoke(this, new(playlist));
|
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)
|
private async void OnPlaylistUpdated(object? sender, PlaylistUpdatedEventArgs e)
|
||||||
@@ -79,6 +105,34 @@ public class PlaylistManager : IPlaylistManager
|
|||||||
if (sender is not Playlist playlist)
|
if (sender is not Playlist playlist)
|
||||||
return;
|
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,14 +1,32 @@
|
|||||||
using Harmonia.Core.Data;
|
using Harmonia.Core.Data;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Harmonia.Core.Playlists;
|
namespace Harmonia.Core.Playlists;
|
||||||
|
|
||||||
public class PlaylistRepository : JsonFileRepository<Playlist>, IPlaylistRepository
|
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");
|
protected override string DirectoryName => Path.Combine("Playlists");
|
||||||
|
|
||||||
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()
|
protected override string GetNewFileName()
|
||||||
|
|||||||
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>
|
/// <summary>
|
||||||
/// The contents of the collection changed dramatically.
|
/// The contents of the collection changed dramatically.
|
||||||
/// </summary>
|
/// </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)
|
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)
|
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)
|
return [.. songs.Where(song => song != null)!];
|
||||||
continue;
|
|
||||||
|
|
||||||
songs.Add(song);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [.. songs];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Song> GetSongAsync(string fileName, CancellationToken cancellationToken)
|
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)
|
if (Playlist == null || PlayingSong == null)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
return Playlist.Songs.IndexOf(PlayingSong);
|
return Playlist.IndexOf(PlayingSong);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,10 +54,7 @@ public class AudioPlayerTests
|
|||||||
new Song() { FileName = "Song7.mp3" }
|
new Song() { FileName = "Song7.mp3" }
|
||||||
];
|
];
|
||||||
|
|
||||||
Playlist playlist = new()
|
Playlist playlist = new("Playlist1");
|
||||||
{
|
|
||||||
Name = "Playlist1"
|
|
||||||
};
|
|
||||||
|
|
||||||
playlist.AddSongs(songs);
|
playlist.AddSongs(songs);
|
||||||
|
|
||||||
@@ -65,7 +62,7 @@ public class AudioPlayerTests
|
|||||||
|
|
||||||
_playlistManager = Substitute.For<IPlaylistManager>();
|
_playlistManager = Substitute.For<IPlaylistManager>();
|
||||||
_playlistManager.Playlists.Returns([playlist]);
|
_playlistManager.Playlists.Returns([playlist]);
|
||||||
_playlistManager.GetPlaylist(Arg.Any<PlaylistSong>()).Returns(playlist);
|
_playlistManager.FindPlaylistContaining(Arg.Any<PlaylistSong>()).Returns(playlist);
|
||||||
|
|
||||||
_audioPlayer = new TestAudioPlayer(_audioEngine, _playlistManager);
|
_audioPlayer = new TestAudioPlayer(_audioEngine, _playlistManager);
|
||||||
_audioPlayer.SetPlaylist(playlist);
|
_audioPlayer.SetPlaylist(playlist);
|
||||||
|
|||||||
@@ -16,14 +16,14 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.8.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="6.0.0" />
|
<PackageReference Include="NSubstitute" Version="6.2.0" />
|
||||||
<PackageReference Include="Shouldly" Version="4.3.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>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="xunit.v3" Version="3.2.2" />
|
<PackageReference Include="xunit.v3" Version="4.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -8,6 +8,32 @@ namespace Harmonia.Tests;
|
|||||||
|
|
||||||
public class PlaylistTests
|
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]
|
[Fact]
|
||||||
public void Add_Songs()
|
public void Add_Songs()
|
||||||
{
|
{
|
||||||
@@ -37,6 +63,70 @@ public class PlaylistTests
|
|||||||
playlist.Songs[1].Song.FileName.ShouldBe("Song5.mp3");
|
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]
|
[Fact]
|
||||||
public void Sort_Songs()
|
public void Sort_Songs()
|
||||||
{
|
{
|
||||||
@@ -74,6 +164,34 @@ public class PlaylistTests
|
|||||||
.ShouldBeEquivalentTo(expectedSortedFileNames);
|
.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]
|
[Fact]
|
||||||
public void Remove_Songs()
|
public void Remove_Songs()
|
||||||
{
|
{
|
||||||
@@ -101,7 +219,7 @@ public class PlaylistTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Lock_Playlist()
|
public void Lock_And_Unlock_Playlist()
|
||||||
{
|
{
|
||||||
Playlist playlist = new();
|
Playlist playlist = new();
|
||||||
|
|
||||||
@@ -114,7 +232,11 @@ public class PlaylistTests
|
|||||||
|
|
||||||
playlist.AddSongs(songs);
|
playlist.AddSongs(songs);
|
||||||
|
|
||||||
playlist.IsLocked = true;
|
playlist.IsLocked.ShouldBeFalse();
|
||||||
|
|
||||||
|
playlist.Lock();
|
||||||
|
|
||||||
|
playlist.IsLocked.ShouldBeTrue();
|
||||||
|
|
||||||
Song song = new() { FileName = "Song4.mp3" };
|
Song song = new() { FileName = "Song4.mp3" };
|
||||||
playlist.AddSong(song);
|
playlist.AddSong(song);
|
||||||
@@ -124,11 +246,21 @@ public class PlaylistTests
|
|||||||
playlist.RemoveSong(0);
|
playlist.RemoveSong(0);
|
||||||
|
|
||||||
playlist.Songs.Count.ShouldBe(3);
|
playlist.Songs.Count.ShouldBe(3);
|
||||||
}
|
|
||||||
|
|
||||||
//public void Get_Playlists()
|
playlist.MoveSong(0, 2);
|
||||||
//{
|
|
||||||
// //PlaylistRepository playlistRepository = new();
|
string[] expectedMovedFileNamesOnLockedPlaylist =
|
||||||
// //playlistRepository.Get().Returns()
|
[
|
||||||
//}
|
"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>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="12.1.0" />
|
<PackageReference Include="Avalonia.Desktop" Version="12.1.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -11,16 +11,16 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" Version="12.1.0" />
|
<PackageReference Include="Avalonia" Version="12.1.2" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.0" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="12.1.2" />
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.0" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="12.1.2" />
|
||||||
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
<PackageReference Include="AvaloniaUI.DiagnosticsSupport" Version="2.2.3" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
|
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Include="Egolds.Xaml.Behaviors.Interactions.Animated" Version="11.3.3" />
|
<PackageReference Include="Egolds.Xaml.Behaviors.Interactions.Animated" Version="11.4.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.10" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.12" />
|
||||||
<PackageReference Include="Semi.Avalonia" Version="12.1.0" />
|
<PackageReference Include="Semi.Avalonia" Version="12.1.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ public class PlaylistViewModel : ViewModelBase
|
|||||||
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
|
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int selectedPlaylistSongIndex = Playlist.Songs.IndexOf(SelectedPlaylistSongs[0]);
|
int selectedPlaylistSongIndex = Playlist.IndexOf(SelectedPlaylistSongs[0]);
|
||||||
|
|
||||||
if (selectedPlaylistSongIndex == -1)
|
if (selectedPlaylistSongIndex == -1)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Harmonia.Core.Extensions;
|
using Harmonia.Core.Extensions;
|
||||||
using Harmonia.WinUI.Caching;
|
using Harmonia.WinUI.Caching;
|
||||||
using Harmonia.WinUI.Messaging;
|
using Harmonia.WinUI.Messaging;
|
||||||
|
using Harmonia.WinUI.Session;
|
||||||
using Harmonia.WinUI.Storage;
|
using Harmonia.WinUI.Storage;
|
||||||
using Harmonia.WinUI.ViewModels;
|
using Harmonia.WinUI.ViewModels;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -31,6 +32,14 @@ public partial class App : Application
|
|||||||
services.AddSingleton<IStorageProvider, WindowsStorageProvider>();
|
services.AddSingleton<IStorageProvider, WindowsStorageProvider>();
|
||||||
services.AddSingleton<IMessageService, WindowsMessageService>();
|
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();
|
services.AddHarmonia();
|
||||||
|
|
||||||
ServiceProvider = services.BuildServiceProvider();
|
ServiceProvider = services.BuildServiceProvider();
|
||||||
@@ -45,6 +54,15 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
await ServiceProvider.InitializeHarmoniaAsync();
|
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 = ServiceProvider.GetRequiredService<MainWindow>();
|
||||||
_mainWindow.Activate();
|
_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 System;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Data;
|
using Microsoft.UI.Xaml.Data;
|
||||||
|
using Microsoft.UI.Xaml.Markup;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
using Harmonia.WinUI.Models;
|
using Harmonia.WinUI.Models;
|
||||||
|
|
||||||
namespace Harmonia.WinUI.Converters;
|
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 sealed partial class VolumeStateConverter : IValueConverter
|
||||||
{
|
{
|
||||||
public VolumeStateConverter()
|
public VolumeStateConverter()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="Resources\Styles.xaml" />
|
<None Remove="Resources\Styles.xaml" />
|
||||||
<None Remove="Views\PlayerView.xaml" />
|
<None Remove="Views\PlayerView.xaml" />
|
||||||
@@ -30,6 +31,10 @@
|
|||||||
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="Assets\Fonts\*.ttf" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Manifest Include="$(ApplicationManifest)" />
|
<Manifest Include="$(ApplicationManifest)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@@ -45,8 +50,8 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251219" />
|
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251219" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2526" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2705" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.3.1" />
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.4.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Harmonia.Core\Harmonia.Core.csproj" />
|
<ProjectReference Include="..\Harmonia.Core\Harmonia.Core.csproj" />
|
||||||
@@ -102,5 +107,6 @@
|
|||||||
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
|
||||||
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
|
||||||
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="Harmonia.WinUI">
|
Title="Harmonia.WinUI"
|
||||||
|
Closed="OnMainWindowClosed">
|
||||||
|
|
||||||
<Window.SystemBackdrop>
|
<Window.SystemBackdrop>
|
||||||
<MicaBackdrop />
|
<MicaBackdrop />
|
||||||
@@ -19,13 +20,19 @@
|
|||||||
<Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
|
<Button x:Name="myButton" Click="myButton_Click">Click Me</Button>
|
||||||
</StackPanel>-->
|
</StackPanel>-->
|
||||||
<Grid>
|
<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>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"></RowDefinition>
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
<RowDefinition Height="*"></RowDefinition>
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
<RowDefinition Height="Auto"></RowDefinition>
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<!-- Title Bar -->
|
<!-- Title Bar -->
|
||||||
<Grid x:Name="AppTitleBar" Grid.Row="0" VerticalAlignment="Center" Padding="10">
|
<Grid x:Name="AppTitleBar" Grid.Row="0" VerticalAlignment="Center" Padding="10" Background="{StaticResource DarkTitleBarBackgroundBrush}" BorderBrush="#3B332B" BorderThickness="0 0 0 1">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
@@ -33,7 +40,7 @@
|
|||||||
<!--<Image Grid.Column="0" x:Name="TitleBarIcon" Source="ms-appx:///Assets/Images/MangaReader.png" Width="20" Height="20" Margin="0 0 10 0" />-->
|
<!--<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" />
|
<TextBlock Grid.Column="1" x:Name="AppTitle" Text="{x:Bind Title, Mode=OneWay}" Style="{StaticResource CaptionTextBlockStyle}" VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid Grid.Row="1" Grid.RowSpan="2">
|
<!--<Grid Grid.Row="1" Grid.RowSpan="2">
|
||||||
<Image Source="/Assets/Default.png" Stretch="UniformToFill" VerticalAlignment="Center" HorizontalAlignment="Center"></Image>
|
<Image Source="/Assets/Default.png" Stretch="UniformToFill" VerticalAlignment="Center" HorizontalAlignment="Center"></Image>
|
||||||
<Canvas Background="#99000000"></Canvas>
|
<Canvas Background="#99000000"></Canvas>
|
||||||
<Border>
|
<Border>
|
||||||
@@ -41,7 +48,7 @@
|
|||||||
<Media:BackdropBlurBrush Amount="20"></Media:BackdropBlurBrush>
|
<Media:BackdropBlurBrush Amount="20"></Media:BackdropBlurBrush>
|
||||||
</Border.Background>
|
</Border.Background>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>-->
|
||||||
<Grid Grid.Row="1">
|
<Grid Grid.Row="1">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition></ColumnDefinition>
|
<ColumnDefinition></ColumnDefinition>
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
|
using Harmonia.Core.Player;
|
||||||
|
using Harmonia.WinUI.Session;
|
||||||
|
using Microsoft.UI.Windowing;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
|
using System;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Harmonia.WinUI;
|
namespace Harmonia.WinUI;
|
||||||
|
|
||||||
@@ -6,10 +11,26 @@ public sealed partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
private const string ApplicationTitle = "Harmonia";
|
private const string ApplicationTitle = "Harmonia";
|
||||||
|
|
||||||
public MainWindow()
|
[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();
|
InitializeComponent();
|
||||||
InitializeTitleBar();
|
InitializeTitleBar();
|
||||||
|
InitializeWindowState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeTitleBar()
|
private void InitializeTitleBar()
|
||||||
@@ -18,4 +39,91 @@ public sealed partial class MainWindow : Window
|
|||||||
ExtendsContentIntoTitleBar = true;
|
ExtendsContentIntoTitleBar = true;
|
||||||
SetTitleBar(AppTitleBar);
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,4 +5,5 @@ namespace Harmonia.WinUI.Messaging;
|
|||||||
public interface IMessageService
|
public interface IMessageService
|
||||||
{
|
{
|
||||||
Task<bool> ConfirmAsync(string title, string message);
|
Task<bool> ConfirmAsync(string title, string message);
|
||||||
|
Task<string> InputTextAsync(string title, string message, string defaultText = "");
|
||||||
}
|
}
|
||||||
@@ -27,4 +27,44 @@ public class WindowsMessageService : IMessageService
|
|||||||
|
|
||||||
return result == ContentDialogResult.Primary;
|
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:DoubleToPercentConverter x:Key="DoubleToPercent" />
|
||||||
<converters:RepeatStateConverter x:Key="RepeatState" />
|
<converters:RepeatStateConverter x:Key="RepeatState" />
|
||||||
<converters:VolumeStateConverter x:Key="VolumeState" />
|
<converters:VolumeStateConverter x:Key="VolumeState" />
|
||||||
|
<converters:VolumeStateToIconConverter x:Key="VolumeStateToIcon" />
|
||||||
|
<converters:VolumeStateToFillConverter x:Key="VolumeStateToFill" />
|
||||||
|
|
||||||
</ResourceDictionary>
|
</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
|
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: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">
|
<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
|
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>
|
</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
|
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: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">
|
<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
|
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
|
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
|
||||||
@@ -148,4 +187,8 @@
|
|||||||
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
|
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: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>
|
</ResourceDictionary>
|
||||||
|
|||||||
@@ -4,20 +4,34 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
<!-- Global Font -->
|
<!-- 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">
|
<Style TargetType="TextBlock">
|
||||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="TextBox" BasedOn="{StaticResource DefaultTextBoxStyle}">
|
<Style TargetType="TextBox" BasedOn="{StaticResource DefaultTextBoxStyle}">
|
||||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="MenuFlyoutItem" BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}">
|
<Style TargetType="MenuFlyoutItem" BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}">
|
||||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}" />
|
||||||
</Style>
|
</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}">
|
<Style TargetType="ListViewItem" BasedOn="{StaticResource DefaultListViewItemStyle}">
|
||||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP" />
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<!-- Premium Palette (from Harmonia icon set) -->
|
<!-- Premium Palette (from Harmonia icon set) -->
|
||||||
@@ -83,7 +97,7 @@
|
|||||||
<Setter Property="Background" Value="{StaticResource PremiumTileGradient}"/>
|
<Setter Property="Background" Value="{StaticResource PremiumTileGradient}"/>
|
||||||
<Setter Property="Padding" Value="14,8"/>
|
<Setter Property="Padding" Value="14,8"/>
|
||||||
<Setter Property="CornerRadius" Value="12"/>
|
<Setter Property="CornerRadius" Value="12"/>
|
||||||
<Setter Property="FontFamily" Value="Lexend,Noto Sans JP"/>
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamily}"/>
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
namespace Harmonia.WinUI.Session;
|
using Harmonia.Core.Engine;
|
||||||
|
|
||||||
|
namespace Harmonia.WinUI.Session;
|
||||||
|
|
||||||
public partial class AudioPlayerState
|
public partial class AudioPlayerState
|
||||||
{
|
{
|
||||||
public string? PlaylistUID { get; set; }
|
public string? PlaylistUID { get; set; }
|
||||||
public string? PlaylistSongUID { get; set; }
|
public string? PlaylistSongUID { get; set; }
|
||||||
public double Position { get; set; } = 0.0;
|
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 double Volume { get; set; } = 1.0;
|
||||||
public bool IsMuted { get; set; }
|
public bool IsMuted { get; set; }
|
||||||
public string? RepeatState { 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;
|
_timer.Tick += TickTock;
|
||||||
|
|
||||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
_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
|
#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);
|
Task.Run(UpdateImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,11 +50,22 @@ public partial class PlayingSongViewModel : ViewModelBase
|
|||||||
|
|
||||||
_audioBitmapImageCache = audioBitmapImageCache;
|
_audioBitmapImageCache = audioBitmapImageCache;
|
||||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
_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);
|
Task.Run(UpdateImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ using System.Collections.Concurrent;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
|
||||||
using Windows.ApplicationModel.DataTransfer;
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue;
|
||||||
using DispatcherQueueTimer = Microsoft.UI.Dispatching.DispatcherQueueTimer;
|
using DispatcherQueueTimer = Microsoft.UI.Dispatching.DispatcherQueueTimer;
|
||||||
@@ -47,7 +47,14 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
public Playlist? Playlist
|
public Playlist? Playlist
|
||||||
{
|
{
|
||||||
get => _playlist;
|
get => _playlist;
|
||||||
private set => SetProperty(ref _playlist, value);
|
private set
|
||||||
|
{
|
||||||
|
_playlist?.PlaylistUpdated -= OnPlaylistUpdated;
|
||||||
|
SetProperty(ref _playlist, value);
|
||||||
|
_playlist?.PlaylistUpdated += OnPlaylistUpdated;
|
||||||
|
|
||||||
|
IsPlaylistLocked = Playlist?.IsLocked ?? false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private PlaylistSong? _playingSong;
|
private PlaylistSong? _playingSong;
|
||||||
@@ -103,22 +110,64 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
SetProperty(ref _selectedPlaylistSongs, value);
|
SetProperty(ref _selectedPlaylistSongs, value);
|
||||||
|
NotifySelectionDependentCommands();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ICommand PlaySongCommand => new AsyncRelayCommand(PlaySongAsync, AreSongsSelected);
|
public SortItem[] SortItems { get; } =
|
||||||
public ICommand NewPlaylistCommand => new AsyncRelayCommand(NewPlaylistAsync);
|
[
|
||||||
public ICommand AddFilesCommand => new AsyncRelayCommand(AddFilesAsync);
|
new SortItem
|
||||||
public ICommand AddFolderCommand => new AsyncRelayCommand(AddFolderAsync);
|
{
|
||||||
public ICommand RemoveSongsCommand => new RelayCommand(RemoveSongs, AreSongsSelected);
|
Name = "Album - Disc # - Track # - Title - File Name",
|
||||||
public ICommand CutSongsCommand => new RelayCommand(CutSongs, AreSongsSelected);
|
SortOptions =
|
||||||
public ICommand CopySongsCommand => new RelayCommand(CopySongs, AreSongsSelected);
|
[
|
||||||
public ICommand PasteSongsCommand => new AsyncRelayCommand(PasteSongsAsync, CanPasteSongs);
|
new(nameof(Song.Album), ListSortDirection.Ascending),
|
||||||
public ICommand OpenFileLocationCommand => new RelayCommand(OpenFileLocation, AreSongsSelected);
|
new(nameof(Song.DiscNumber), ListSortDirection.Ascending),
|
||||||
public ICommand RefreshTagsCommand => new RelayCommand(RefreshTags);
|
new(nameof(Song.TrackNumber), ListSortDirection.Ascending),
|
||||||
public ICommand RemoveMissingSongsCommand => new RelayCommand(RemoveMissingSongs);
|
new(nameof(Song.Title), ListSortDirection.Ascending),
|
||||||
public ICommand RemoveDuplicateSongsCommand => new RelayCommand(RemoveDuplicateSongs);
|
new(nameof(Song.FileName), ListSortDirection.Ascending)
|
||||||
public ICommand DeletePlaylistCommand => new AsyncRelayCommand(DeletePlaylistAsync);
|
]
|
||||||
|
},
|
||||||
|
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; }
|
public bool IsUserUpdating { get; set; }
|
||||||
private bool _isUserInitiatingSongChange;
|
private bool _isUserInitiatingSongChange;
|
||||||
@@ -152,12 +201,50 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
_messageService = messageService;
|
_messageService = messageService;
|
||||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
_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;
|
FilteredPlaylistSongs.CollectionChanged += OnFilteredPlaylistSongsCollectionChanged;
|
||||||
|
|
||||||
Playlist = _playlistManager.CurrentPlaylist; // Testing
|
Playlist = _playlistManager.CurrentPlaylist; // Testing
|
||||||
|
PlayingSong = _audioPlayer.PlayingSong; // Testing
|
||||||
|
|
||||||
UpdateFilteredSongs();
|
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)
|
private void OnFilteredPlaylistSongsCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (IsUserUpdating == false)
|
if (IsUserUpdating == false)
|
||||||
@@ -193,13 +280,9 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
Playlist.MoveSong(oldIndex, newIndex);
|
Playlist.MoveSong(oldIndex, newIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaylistChanged(object? sender, EventArgs e)
|
private void OnPlaylistChanged(object? sender, PlaylistChangedEventArgs e)
|
||||||
{
|
{
|
||||||
Playlist?.PlaylistUpdated -= OnPlaylistUpdated;
|
Playlist = e.NewPlaylist;
|
||||||
//Playlist = _audioPlayer.Playlist;
|
|
||||||
Playlist = _playlistManager.CurrentPlaylist;
|
|
||||||
Playlist?.PlaylistUpdated += OnPlaylistUpdated;
|
|
||||||
|
|
||||||
UpdateFilteredSongs();
|
UpdateFilteredSongs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,12 +295,17 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
case PlaylistUpdateAction.Add:
|
case PlaylistUpdateAction.Add:
|
||||||
case PlaylistUpdateAction.Remove:
|
case PlaylistUpdateAction.Remove:
|
||||||
|
case PlaylistUpdateAction.Refresh:
|
||||||
_dispatcherQueue.TryEnqueue(UpdateFilteredSongs);
|
_dispatcherQueue.TryEnqueue(UpdateFilteredSongs);
|
||||||
break;
|
break;
|
||||||
case PlaylistUpdateAction.Move:
|
case PlaylistUpdateAction.Move:
|
||||||
case PlaylistUpdateAction.Reset:
|
case PlaylistUpdateAction.Reset:
|
||||||
_dispatcherQueue.TryEnqueue(() => ApplyReorderedSongs(e.Songs));
|
_dispatcherQueue.TryEnqueue(() => ApplyReorderedSongs(e.Songs));
|
||||||
break;
|
break;
|
||||||
|
case PlaylistUpdateAction.Lock:
|
||||||
|
case PlaylistUpdateAction.Unlock:
|
||||||
|
IsPlaylistLocked = Playlist?.IsLocked ?? false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,9 +321,9 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
UpdateFilteredSongs();
|
UpdateFilteredSongs();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlayingSongChanged(object? sender, EventArgs e)
|
private void OnPlayingSongChanged(object? sender, PlayingSongChangedEventArgs e)
|
||||||
{
|
{
|
||||||
PlayingSong = _audioPlayer.PlayingSong;
|
PlayingSong = e.NewSong;
|
||||||
|
|
||||||
if (_isUserInitiatingSongChange)
|
if (_isUserInitiatingSongChange)
|
||||||
{
|
{
|
||||||
@@ -381,9 +469,24 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
return SelectedPlaylistSongs.Count > 0;
|
return SelectedPlaylistSongs.Count > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool AreMultipleSongsSelected()
|
||||||
|
{
|
||||||
|
return SelectedPlaylistSongs.Count > 1;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task NewPlaylistAsync(CancellationToken cancellationToken)
|
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 playlist = await _playlistManager.AddPlaylistAsync();
|
||||||
|
playlist.SetName(playlistName);
|
||||||
|
|
||||||
_playlistManager.CurrentPlaylist = playlist;
|
_playlistManager.CurrentPlaylist = playlist;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,7 +595,7 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
|
|
||||||
private bool CanPasteSongs()
|
private bool CanPasteSongs()
|
||||||
{
|
{
|
||||||
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
|
if (Playlist == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
DataPackageView dataPackageView = Clipboard.GetContent();
|
DataPackageView dataPackageView = Clipboard.GetContent();
|
||||||
@@ -508,17 +611,21 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
|
|
||||||
private async Task PasteSongsAsync()
|
private async Task PasteSongsAsync()
|
||||||
{
|
{
|
||||||
if (Playlist == null || SelectedPlaylistSongs.Count == 0)
|
if (Playlist == null)
|
||||||
return;
|
|
||||||
|
|
||||||
int selectedPlaylistSongIndex = Playlist.Songs.IndexOf(SelectedPlaylistSongs[0]);
|
|
||||||
|
|
||||||
if (selectedPlaylistSongIndex == -1)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Song[] songs = await GetSongsFromClipboardAsync();
|
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()
|
private static async Task<Song[]> GetSongsFromClipboardAsync()
|
||||||
@@ -538,9 +645,23 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
Process.Start("explorer.exe", argument);
|
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()
|
private void RemoveMissingSongs()
|
||||||
@@ -564,19 +685,111 @@ public partial class PlaylistDetailViewModel : ViewModelBase
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
_playlistManager.RemovePlaylist(Playlist);
|
_playlistManager.RemovePlaylist(Playlist);
|
||||||
|
}
|
||||||
|
|
||||||
if (_playlistManager.CurrentPlaylist == 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(() =>
|
||||||
{
|
{
|
||||||
if (_playlistManager.Playlists.Count > 0)
|
PlaylistSong[] selectedPlaylistSongs = [.. Playlist?.Songs.Where(x => playlistSongIds.Contains(x.UID)) ?? []];
|
||||||
{
|
SelectedPlaylistSongs = new ObservableCollection<PlaylistSong>(selectedPlaylistSongs);
|
||||||
_playlistManager.CurrentPlaylist = _playlistManager.Playlists.ElementAt(0);
|
});
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
private void RandomizeAllSongs()
|
||||||
_playlistManager.CurrentPlaylist = null;
|
{
|
||||||
}
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SortItem
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public IReadOnlyCollection<SortOption> SortOptions { get; set; } = [];
|
||||||
|
}
|
||||||
@@ -9,6 +9,19 @@ public partial class PlaylistItemViewModel : ObservableObject
|
|||||||
{
|
{
|
||||||
public Playlist Playlist { get; private set; }
|
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;
|
private string _summary = string.Empty;
|
||||||
public string Summary
|
public string Summary
|
||||||
{
|
{
|
||||||
@@ -22,6 +35,19 @@ public partial class PlaylistItemViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _isLocked;
|
||||||
|
public bool IsLocked
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _isLocked;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
SetProperty(ref _isLocked, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public PlaylistItemViewModel(Playlist playlist)
|
public PlaylistItemViewModel(Playlist playlist)
|
||||||
{
|
{
|
||||||
Playlist = playlist;
|
Playlist = playlist;
|
||||||
@@ -41,6 +67,9 @@ public partial class PlaylistItemViewModel : ObservableObject
|
|||||||
|
|
||||||
private void UpdateMetaData()
|
private void UpdateMetaData()
|
||||||
{
|
{
|
||||||
|
Name = Playlist.Name ?? "Untitled Playlist";
|
||||||
|
IsLocked = Playlist.IsLocked;
|
||||||
|
|
||||||
TimeSpan total = TimeSpan.FromSeconds(Playlist.Songs.Sum(s => s.Song.Length.TotalSeconds));
|
TimeSpan total = TimeSpan.FromSeconds(Playlist.Songs.Sum(s => s.Song.Length.TotalSeconds));
|
||||||
Summary = $"{Playlist.Songs.Count} songs / {total:h\\:mm\\:ss}";
|
Summary = $"{Playlist.Songs.Count} songs / {total:h\\:mm\\:ss}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using Harmonia.Core.Player;
|
||||||
using Harmonia.Core.Playlists;
|
using Harmonia.Core.Playlists;
|
||||||
|
using Harmonia.WinUI.Messaging;
|
||||||
|
using Microsoft.UI.Dispatching;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Harmonia.WinUI.ViewModels;
|
namespace Harmonia.WinUI.ViewModels;
|
||||||
|
|
||||||
public partial class PlaylistsViewModel : ViewModelBase
|
public partial class PlaylistsViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly IPlaylistManager _playlistManager;
|
private readonly IPlaylistManager _playlistManager;
|
||||||
|
private readonly IAudioPlayer _audioPlayer;
|
||||||
|
private readonly IMessageService _messageService;
|
||||||
|
private readonly DispatcherQueue _dispatcherQueue;
|
||||||
|
|
||||||
private ObservableCollection<PlaylistItemViewModel> _playlists = [];
|
private ObservableCollection<PlaylistItemViewModel> _playlists = [];
|
||||||
public ObservableCollection<PlaylistItemViewModel> Playlists
|
public ObservableCollection<PlaylistItemViewModel> Playlists
|
||||||
@@ -39,15 +46,58 @@ public partial class PlaylistsViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public PlaylistsViewModel(IPlaylistManager playlistManager)
|
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 = playlistManager;
|
||||||
_playlistManager.PlaylistAdded += OnPlaylistAdded;
|
_playlistManager.PlaylistAdded += OnPlaylistAdded;
|
||||||
_playlistManager.PlaylistRemoved += OnPlaylistRemoved;
|
_playlistManager.PlaylistRemoved += OnPlaylistRemoved;
|
||||||
_playlistManager.CurrentPlaylistChanged += OnCurrentPlaylistChanged;
|
_playlistManager.CurrentPlaylistChanged += OnCurrentPlaylistChanged;
|
||||||
|
_messageService = messageService;
|
||||||
|
|
||||||
|
_audioPlayer = audioPlayer;
|
||||||
|
_audioPlayer.PlayingSongChanged += OnPlayingSongChanged;
|
||||||
|
|
||||||
|
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
|
|
||||||
_playlists = new ObservableCollection<PlaylistItemViewModel>(_playlistManager.Playlists.Select(p => new PlaylistItemViewModel(p)));
|
_playlists = new ObservableCollection<PlaylistItemViewModel>(_playlistManager.Playlists.Select(p => new PlaylistItemViewModel(p)));
|
||||||
_selectedPlaylist = _playlists.FirstOrDefault(pv => pv.Playlist == _playlistManager.CurrentPlaylist);
|
_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)
|
private void OnCurrentPlaylistChanged(object? sender, EventArgs e)
|
||||||
@@ -70,4 +120,67 @@ public partial class PlaylistsViewModel : ViewModelBase
|
|||||||
Playlists.Remove(playlistView);
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -14,13 +14,45 @@
|
|||||||
<SolidColorBrush x:Key="SongItemTitleBrush" Color="#dddddd"/>
|
<SolidColorBrush x:Key="SongItemTitleBrush" Color="#dddddd"/>
|
||||||
<SolidColorBrush x:Key="SongItemSubtitleBrush" Color="#aaaaaa"/>
|
<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">
|
<Style x:Key="PlayerGrid" TargetType="Grid">
|
||||||
<Setter Property="Background" Value="#1a1a1a"/>
|
|
||||||
<Setter Property="Padding" Value="10"/>
|
<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>
|
||||||
<Style x:Key="SongTitleTextBlock" TargetType="TextBlock">
|
<Style x:Key="SongTitleTextBlock" TargetType="TextBlock">
|
||||||
<Setter Property="FontSize" Value="16"/>
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamilySemiBold}"/>
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||||
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
|
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
|
||||||
@@ -68,11 +100,24 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Value="{Binding CurrentPosition, Mode=TwoWay, UpdateSourceTrigger=Explicit}"
|
Value="{Binding CurrentPosition, Mode=TwoWay, UpdateSourceTrigger=Explicit}"
|
||||||
Minimum="0"
|
Minimum="0"
|
||||||
|
Foreground="{StaticResource SliderGradientBrush}"
|
||||||
Maximum="{Binding MaxPosition, Mode=OneWay}"
|
Maximum="{Binding MaxPosition, Mode=OneWay}"
|
||||||
IsEnabled="{Binding CanUpdatePosition, Mode=OneWay}"
|
IsEnabled="{Binding CanUpdatePosition, Mode=OneWay}"
|
||||||
ThumbToolTipValueConverter="{StaticResource SecondsToString}"
|
ThumbToolTipValueConverter="{StaticResource SecondsToString}"
|
||||||
VerticalAlignment="Center"
|
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">
|
<Border Grid.Column="2" Background="Transparent" Padding="0 8" Width="60" CornerRadius="4" VerticalAlignment="Center" Margin="6 0 0 0">
|
||||||
<TextBlock
|
<TextBlock
|
||||||
@@ -88,14 +133,14 @@
|
|||||||
<!-- Song Info -->
|
<!-- Song Info -->
|
||||||
<Grid Grid.Row="1" Grid.Column="0" Name="PlayingSongGrid">
|
<Grid Grid.Row="1" Grid.Column="0" Name="PlayingSongGrid">
|
||||||
<StackPanel Orientation="Horizontal">
|
<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>
|
<Image Source="{Binding SongImageSource, Mode=OneWay}" Style="{StaticResource SongImage}"></Image>
|
||||||
<Canvas Background="#19000000"></Canvas>
|
<Canvas Background="#19000000"></Canvas>
|
||||||
</Grid>
|
</Grid>
|
||||||
<StackPanel VerticalAlignment="Center">
|
<StackPanel VerticalAlignment="Center">
|
||||||
<TextBlock Foreground="#dddddd" FontWeight="SemiBold" FontSize="16" Text="{Binding Song, Converter={StaticResource SongTitle}}"></TextBlock>
|
<TextBlock Foreground="{StaticResource SongItemTitleBrush2}" FontFamily="{StaticResource AppFontFamilySemiBold}" 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="{StaticResource SongItemSubtitleBrush2}" FontSize="14" Text="{Binding Song.Artists, Converter={StaticResource ArtistsToString}}"></TextBlock>
|
||||||
<TextBlock Foreground="#aaaaaa" FontSize="14" Text="{Binding Song.Album}"></TextBlock>
|
<TextBlock Foreground="{StaticResource SongItemSubtitleBrush2}" FontSize="14" Text="{Binding Song.Album}"></TextBlock>
|
||||||
<TextBlock Foreground="#777" FontSize="12" Visibility="{Binding Song, Converter={StaticResource NullVisibility}}">
|
<TextBlock Foreground="#777" FontSize="12" Visibility="{Binding Song, Converter={StaticResource NullVisibility}}">
|
||||||
<Run Text="{Binding Song.FileType}"></Run><Run Text=" - "></Run><Run Text="{Binding Song.BitRate}"></Run><Run Text=" kbps - "></Run><Run Text="{Binding Song.SampleRate}"></Run><Run Text=" Hz"></Run>
|
<Run Text="{Binding Song.FileType}"></Run><Run Text=" - "></Run><Run Text="{Binding Song.BitRate}"></Run><Run Text=" kbps - "></Run><Run Text="{Binding Song.SampleRate}"></Run><Run Text=" Hz"></Run>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
@@ -106,23 +151,103 @@
|
|||||||
<!-- Playback Actions -->
|
<!-- Playback Actions -->
|
||||||
<Grid Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center">
|
<Grid Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="16">
|
<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}">
|
<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>
|
||||||
<Button Style="{StaticResource FlatButton}" Command="{Binding StopSongCommand}">
|
<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>
|
||||||
<Button Style="{StaticResource FlatButton}" Command="{Binding PlaySongCommand}">
|
<Button Style="{StaticResource FlatButton}" Command="{Binding PlaySongCommand}" Padding="0"
|
||||||
<Path Style="{StaticResource FlatButtonPath-Large}" Data="{StaticResource PlayFillIcon}"></Path>
|
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>
|
||||||
<Button Style="{StaticResource FlatButton}" Command="{Binding PauseSongCommand}">
|
<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>
|
||||||
<Button Style="{StaticResource FlatButton}" Command="{Binding NextSongCommand}">
|
<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>
|
||||||
|
<!--<Button Style="{StaticResource FlatButton}" Command="{Binding NextSongCommand}">
|
||||||
|
<Path Style="{StaticResource FlatButtonPath}" Fill="{StaticResource LightGradientBrush}" Data="{StaticResource RepeatIcon}"></Path>
|
||||||
|
</Button>-->
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</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>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ public sealed partial class PlayerView : UserControl
|
|||||||
_viewModel.IsPositionChangeInProgress = false;
|
_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)
|
private void VolumeSlider_PointerWheelChanged(object? sender, PointerRoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (sender is not Slider slider)
|
if (sender is not Slider slider)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="using:Harmonia.WinUI.Views"
|
xmlns:local="using:Harmonia.WinUI.Views"
|
||||||
xmlns:vm="using:Harmonia.WinUI.ViewModels"
|
xmlns:vm="using:Harmonia.WinUI.ViewModels"
|
||||||
|
xmlns:converter="using:Harmonia.WinUI.Converters"
|
||||||
xmlns:playlists="using:Harmonia.Core.Playlists"
|
xmlns:playlists="using:Harmonia.Core.Playlists"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
@@ -11,6 +12,9 @@
|
|||||||
d:DataContext="{d:DesignInstance Type=vm:PlaylistDetailViewModel, IsDesignTimeCreatable=True}"
|
d:DataContext="{d:DesignInstance Type=vm:PlaylistDetailViewModel, IsDesignTimeCreatable=True}"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<UserControl.Resources>
|
<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="#393a45"/>-->
|
||||||
<SolidColorBrush x:Key="PlaylistBackground" Color="#292a35"/>
|
<SolidColorBrush x:Key="PlaylistBackground" Color="#292a35"/>
|
||||||
<SolidColorBrush x:Key="PlaylistItemHighlightColor" Color="#494a55"/>
|
<SolidColorBrush x:Key="PlaylistItemHighlightColor" Color="#494a55"/>
|
||||||
@@ -24,6 +28,11 @@
|
|||||||
<SolidColorBrush x:Key="SongItemFooterBrush" Color="#888"/>
|
<SolidColorBrush x:Key="SongItemFooterBrush" Color="#888"/>
|
||||||
<SolidColorBrush x:Key="SongItemFooterBrushHighlighted" Color="#aaA2D2F6"/>
|
<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 -->
|
<!-- Image Border -->
|
||||||
<Style x:Key="PlaylistSongImageBorder" TargetType="Border">
|
<Style x:Key="PlaylistSongImageBorder" TargetType="Border">
|
||||||
<Setter Property="Width" Value="60"/> <!-- as 75 -->
|
<Setter Property="Width" Value="60"/> <!-- as 75 -->
|
||||||
@@ -34,6 +43,7 @@
|
|||||||
<!-- Song Item Title Text Block -->
|
<!-- Song Item Title Text Block -->
|
||||||
<Style x:Key="SongTitleTextBlock" TargetType="TextBlock">
|
<Style x:Key="SongTitleTextBlock" TargetType="TextBlock">
|
||||||
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
|
<Setter Property="Foreground" Value="{StaticResource SongItemTitleBrush}"/>
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource AppFontFamilyMedium}"/>
|
||||||
<Setter Property="FontSize" Value="15"/>
|
<Setter Property="FontSize" Value="15"/>
|
||||||
<Setter Property="FontWeight" Value="Medium"/>
|
<Setter Property="FontWeight" Value="Medium"/>
|
||||||
<Setter Property="LineStackingStrategy" Value="BlockLineHeight"/>
|
<Setter Property="LineStackingStrategy" Value="BlockLineHeight"/>
|
||||||
@@ -67,10 +77,11 @@
|
|||||||
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
<ColumnDefinition Width="Auto"></ColumnDefinition>
|
||||||
<ColumnDefinition Width="*"></ColumnDefinition>
|
<ColumnDefinition Width="*"></ColumnDefinition>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<Border Grid.Column="0" Style="{StaticResource PlaylistSongImageBorder}">
|
<Border Grid.Column="0" Style="{StaticResource PlaylistSongImageBorder}" BorderBrush="#2D2930" BorderThickness="1">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Image Loaded="Image_Loaded" Unloaded="Image_Unloaded"></Image>
|
<Image Loaded="Image_Loaded" Unloaded="Image_Unloaded"></Image>
|
||||||
<Canvas Background="#19000000"></Canvas>
|
<Canvas Background="#19000000"></Canvas>
|
||||||
|
<!--<Canvas Background="#30000000"></Canvas>-->
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
<Grid Grid.Column="1" Margin="10 0 0 0" VerticalAlignment="Center">
|
<Grid Grid.Column="1" Margin="10 0 0 0" VerticalAlignment="Center">
|
||||||
@@ -99,7 +110,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<Grid>
|
<Grid Background="{StaticResource DarkBackgroundBrush}">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto"></RowDefinition>
|
<RowDefinition Height="Auto"></RowDefinition>
|
||||||
<RowDefinition Height="*"></RowDefinition>
|
<RowDefinition Height="*"></RowDefinition>
|
||||||
@@ -160,24 +171,41 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</MenuFlyout.MenuFlyoutPresenterStyle>
|
</MenuFlyout.MenuFlyoutPresenterStyle>
|
||||||
<MenuFlyoutItem Text="Refresh Tags" Command="{Binding RefreshTagsCommand}">
|
<MenuFlyoutItem Text="Refresh Tags" Command="{Binding RefreshTagsCommand}">
|
||||||
|
|
||||||
</MenuFlyoutItem>
|
</MenuFlyoutItem>
|
||||||
|
|
||||||
<MenuFlyoutItem Text="Remove Duplicates" Command="{Binding RemoveDuplicateSongsCommand}">
|
<MenuFlyoutItem Text="Remove Duplicates" Command="{Binding RemoveDuplicateSongsCommand}">
|
||||||
|
|
||||||
</MenuFlyoutItem>
|
</MenuFlyoutItem>
|
||||||
|
|
||||||
<MenuFlyoutItem Text="Remove Missing" Command="{Binding RemoveMissingSongsCommand}">
|
<MenuFlyoutItem Text="Remove Missing" Command="{Binding RemoveMissingSongsCommand}">
|
||||||
|
|
||||||
</MenuFlyoutItem>
|
</MenuFlyoutItem>
|
||||||
<MenuFlyoutItem Text="Lock Playlist">
|
|
||||||
|
|
||||||
|
<MenuFlyoutItem Text="Rename Playlist" Command="{Binding RenamePlaylistCommand}">
|
||||||
</MenuFlyoutItem>
|
</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>
|
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
||||||
|
|
||||||
<MenuFlyoutItem Text="Remove Playlist" Foreground="#ff99a4" Command="{Binding DeletePlaylistCommand}">
|
<MenuFlyoutItem Text="Remove Playlist" Foreground="#ff99a4" Command="{Binding DeletePlaylistCommand}">
|
||||||
|
|
||||||
</MenuFlyoutItem>
|
</MenuFlyoutItem>
|
||||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
|
||||||
<MenuFlyoutItem Text="Settings">
|
|
||||||
|
|
||||||
|
<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>
|
</MenuFlyoutItem>
|
||||||
</MenuFlyout>
|
</MenuFlyout>
|
||||||
</Button.Flyout>
|
</Button.Flyout>
|
||||||
@@ -210,7 +238,7 @@
|
|||||||
<Setter Property="Background" Value="Transparent"></Setter>
|
<Setter Property="Background" Value="Transparent"></Setter>
|
||||||
</Style>
|
</Style>
|
||||||
</MenuFlyout.MenuFlyoutPresenterStyle>
|
</MenuFlyout.MenuFlyoutPresenterStyle>
|
||||||
<MenuFlyoutItem Text="Play" FontWeight="SemiBold" Command="{Binding PlaySongCommand}">
|
<MenuFlyoutItem Text="Play" FontFamily="{StaticResource AppFontFamilySemiBold}" FontWeight="SemiBold" Command="{Binding PlaySongCommand}">
|
||||||
<MenuFlyoutItem.Icon>
|
<MenuFlyoutItem.Icon>
|
||||||
<PathIcon Data="{StaticResource PlayIcon}"></PathIcon>
|
<PathIcon Data="{StaticResource PlayIcon}"></PathIcon>
|
||||||
</MenuFlyoutItem.Icon>
|
</MenuFlyoutItem.Icon>
|
||||||
@@ -228,6 +256,17 @@
|
|||||||
</MenuFlyoutItem.KeyboardAccelerators>
|
</MenuFlyoutItem.KeyboardAccelerators>
|
||||||
</MenuFlyoutItem>
|
</MenuFlyoutItem>
|
||||||
<MenuFlyoutSeparator></MenuFlyoutSeparator>
|
<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 Text="Cut" Command="{Binding CutSongsCommand}">
|
||||||
<MenuFlyoutItem.Icon>
|
<MenuFlyoutItem.Icon>
|
||||||
<PathIcon Data="{StaticResource CutIcon}"></PathIcon>
|
<PathIcon Data="{StaticResource CutIcon}"></PathIcon>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using CommunityToolkit.WinUI;
|
using CommunityToolkit.WinUI;
|
||||||
using Harmonia.Core.Imaging;
|
|
||||||
using Harmonia.Core.Playlists;
|
using Harmonia.Core.Playlists;
|
||||||
using Harmonia.WinUI.ViewModels;
|
using Harmonia.WinUI.ViewModels;
|
||||||
using Microsoft.UI.Dispatching;
|
using Microsoft.UI.Dispatching;
|
||||||
@@ -7,14 +6,10 @@ using Microsoft.UI.Xaml;
|
|||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.UI.Xaml.Data;
|
using Microsoft.UI.Xaml.Data;
|
||||||
using Microsoft.UI.Xaml.Input;
|
using Microsoft.UI.Xaml.Input;
|
||||||
using Microsoft.UI.Xaml.Media;
|
|
||||||
using Microsoft.UI.Xaml.Media.Imaging;
|
using Microsoft.UI.Xaml.Media.Imaging;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Windows.UI.Popups;
|
|
||||||
|
|
||||||
namespace Harmonia.WinUI.Views;
|
namespace Harmonia.WinUI.Views;
|
||||||
|
|
||||||
@@ -33,8 +28,44 @@ public sealed partial class PlaylistDetailView : UserControl
|
|||||||
|
|
||||||
foreach (MenuFlyoutItemBase item in PlaylistListViewMenuFlyout.Items)
|
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)
|
private void OnViewModelPropertyChanging(object? sender, PropertyChangingEventArgs e)
|
||||||
@@ -52,15 +83,44 @@ public sealed partial class PlaylistDetailView : UserControl
|
|||||||
|
|
||||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
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);
|
UpdateListViewItemStyle(listViewItem, true);
|
||||||
|
break;
|
||||||
|
case nameof(_viewModel.Playlist):
|
||||||
|
ScrollToTop();
|
||||||
|
break;
|
||||||
|
case nameof(_viewModel.SelectedPlaylistSongs):
|
||||||
|
SyncListViewSelection();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else if (e.PropertyName == nameof(_viewModel.Playlist))
|
}
|
||||||
|
|
||||||
|
private bool _isSyncingSelection;
|
||||||
|
|
||||||
|
private void SyncListViewSelection()
|
||||||
|
{
|
||||||
|
_isSyncingSelection = true;
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
ScrollToTop();
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +208,19 @@ public sealed partial class PlaylistDetailView : 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)
|
private void Item_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||||
{
|
{
|
||||||
if (sender is not MenuFlyoutItemBase item)
|
if (sender is not MenuFlyoutItemBase item)
|
||||||
@@ -211,7 +284,7 @@ public sealed partial class PlaylistDetailView : UserControl
|
|||||||
if (args.NewValue is not PlaylistSong playlistSong)
|
if (args.NewValue is not PlaylistSong playlistSong)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool isPlaying = playlistSong == _viewModel.PlayingSong;
|
bool isPlaying = playlistSong.UID == _viewModel.PlayingSong?.UID;
|
||||||
|
|
||||||
UpdateListViewItemStyle(sender, isPlaying);
|
UpdateListViewItemStyle(sender, isPlaying);
|
||||||
}
|
}
|
||||||
@@ -246,6 +319,9 @@ public sealed partial class PlaylistDetailView : UserControl
|
|||||||
|
|
||||||
private void PlaylistListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
private void PlaylistListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
|
if (_isSyncingSelection)
|
||||||
|
return;
|
||||||
|
|
||||||
if (sender is not ListView listView)
|
if (sender is not ListView listView)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,63 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:vm="using:Harmonia.WinUI.ViewModels"
|
xmlns:vm="using:Harmonia.WinUI.ViewModels"
|
||||||
|
xmlns:converter="using:Harmonia.WinUI.Converters"
|
||||||
xmlns:playlists="using:Harmonia.Core.Playlists"
|
xmlns:playlists="using:Harmonia.Core.Playlists"
|
||||||
DataContext="{Binding Source={StaticResource Locator}, Path=PlaylistsViewModel}"
|
DataContext="{Binding Source={StaticResource Locator}, Path=PlaylistsViewModel}"
|
||||||
d:DataContext="{d:DesignInstance Type=vm:PlaylistsViewModel, IsDesignTimeCreatable=True}"
|
d:DataContext="{d:DesignInstance Type=vm:PlaylistsViewModel, IsDesignTimeCreatable=True}"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<UserControl.Resources>
|
<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"/>
|
<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 -->
|
<!-- Playlist Item Subtitle Text Block -->
|
||||||
<Style x:Key="PlaylistSubtitleTextBlock" TargetType="TextBlock">
|
<Style x:Key="PlaylistSubtitleTextBlock" TargetType="TextBlock">
|
||||||
<Setter Property="Foreground" Value="{StaticResource PlaylistItemSubtitleBrush}"/>
|
<Setter Property="Foreground" Value="{StaticResource PlaylistItemSubtitleBrush}"/>
|
||||||
@@ -23,29 +73,71 @@
|
|||||||
<Setter Property="LineHeight" Value="0"/>
|
<Setter Property="LineHeight" Value="0"/>
|
||||||
</Style>
|
</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">
|
<DataTemplate x:Key="PlaylistTemplate" x:DataType="vm:PlaylistItemViewModel">
|
||||||
<Grid>
|
<Grid x:Name="PlaylistListViewItem">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="*" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="200" />
|
<ColumnDefinition Width="200" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||||
<Viewbox Height="24" Width="24">
|
<Viewbox Height="24" Width="24">
|
||||||
<PathIcon Data="{StaticResource MusicNoteList}" />
|
<PathIcon x:Name="MusicNoteListPathIcon" Data="{StaticResource MusicNoteList}" />
|
||||||
</Viewbox>
|
</Viewbox>
|
||||||
<TextBlock Text="{x:Bind Playlist.Name}" FontSize="16" VerticalAlignment="Center" />
|
<TextBlock x:Name="PlaylistTitleTextBlock" Text="{x:Bind Name, Mode=OneWay}" Style="{StaticResource PlaylistTitleTextBlock}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center" Spacing="8">
|
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center" Spacing="8">
|
||||||
<TextBlock Text="{x:Bind Summary, Mode=OneWay}" Style="{StaticResource PlaylistSubtitleTextBlock}" />
|
<TextBlock x:Name="PlaylistSubtitleTextBlock" Text="{x:Bind Summary, Mode=OneWay}" Style="{StaticResource PlaylistSubtitleTextBlock}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<Grid Margin="10">
|
<Grid Background="{StaticResource DarkBackgroundBrush}" BorderBrush="#3C342F" BorderThickness="1 0 1 0">
|
||||||
<ListView
|
<ListView
|
||||||
|
Name="PlaylistsListView"
|
||||||
ItemsSource="{Binding Playlists, Mode=OneWay}"
|
ItemsSource="{Binding Playlists, Mode=OneWay}"
|
||||||
SelectedItem="{Binding SelectedPlaylist, Mode=TwoWay}"
|
SelectedItem="{Binding SelectedPlaylist, Mode=TwoWay}"
|
||||||
ItemTemplate="{StaticResource PlaylistTemplate}">
|
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>
|
</ListView>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -1,12 +1,126 @@
|
|||||||
|
using CommunityToolkit.WinUI;
|
||||||
|
using Harmonia.WinUI.ViewModels;
|
||||||
using Microsoft.UI.Xaml;
|
using Microsoft.UI.Xaml;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace Harmonia.WinUI.Views;
|
namespace Harmonia.WinUI.Views;
|
||||||
|
|
||||||
public sealed partial class PlaylistsView : UserControl
|
public sealed partial class PlaylistsView : UserControl
|
||||||
{
|
{
|
||||||
|
private readonly PlaylistsViewModel _viewModel;
|
||||||
|
|
||||||
public PlaylistsView()
|
public PlaylistsView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
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