diff --git a/README.md b/README.md
index 6e1cebeb..c04b8de4 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@ Stylophone
[**Music Player Daemon**](https://www.musicpd.org/) Client for UWP and iOS/iPadOS.
Based on [MpcNET](https://github.com/Difegue/MpcNET), my own fork of the original .NET Client Library for MPD. (now on NuGet!)
-
+[ ](https://www.microsoft.com/store/apps/9NCB693428T8?cid=storebadge&ocid=badge) [ ](https://apps.apple.com/us/app/stylophone/id1644672889?itsct=apps_box_link&itscg=30200)
[Buy a sticker if you want!](https://ko-fi.com/s/9fcf421b6e)
diff --git a/Sources/Stylophone.Common/Helpers/ColorThief.Skia.cs b/Sources/Stylophone.Common/Helpers/ColorThief.Skia.cs
index c1ca7f72..72e3e67c 100644
--- a/Sources/Stylophone.Common/Helpers/ColorThief.Skia.cs
+++ b/Sources/Stylophone.Common/Helpers/ColorThief.Skia.cs
@@ -93,6 +93,12 @@ public QuantizedColor GetColor(SKBitmap sourceImage, int quality = DefaultQualit
{
var palette = GetPalette(sourceImage, 3, quality, ignoreWhite);
+ // Handle case where GetPalette returns an empty list (because GetColorMap failed?)
+ if (palette.Count == 0)
+ {
+ return new QuantizedColor(SKColors.Black, 1);
+ }
+
var avgR = Convert.ToByte(palette.Average(a => a.Color.Red));
var avgG = Convert.ToByte(palette.Average(a => a.Color.Green));
var avgB = Convert.ToByte(palette.Average(a => a.Color.Blue));
diff --git a/Sources/Stylophone.Common/Helpers/Miscellaneous.cs b/Sources/Stylophone.Common/Helpers/Miscellaneous.cs
index d1f021fb..c4b0530c 100644
--- a/Sources/Stylophone.Common/Helpers/Miscellaneous.cs
+++ b/Sources/Stylophone.Common/Helpers/Miscellaneous.cs
@@ -2,6 +2,7 @@
using SkiaSharp;
using System;
using System.IO;
+using System.Net;
namespace Stylophone.Common.Helpers
{
@@ -83,5 +84,25 @@ public static string EscapeFilename(string fileName)
}
return fileName.Replace(".", "002E");
}
+
+ public static IPEndPoint GetIPEndPointFromHostName(string hostName, int port, bool throwIfMoreThanOneIP)
+ {
+ var addresses = System.Net.Dns.GetHostAddresses(hostName);
+ if (addresses.Length == 0)
+ {
+ throw new ArgumentException(
+ "Unable to retrieve address from specified host name.",
+ "hostName"
+ );
+ }
+ else if (throwIfMoreThanOneIP && addresses.Length > 1)
+ {
+ throw new ArgumentException(
+ "There is more that one IP address to the specified host.",
+ "hostName"
+ );
+ }
+ return new IPEndPoint(addresses[0], port); // Port gets validated here.
+ }
}
}
diff --git a/Sources/Stylophone.Common/Helpers/RangedObservableCollection.cs b/Sources/Stylophone.Common/Helpers/RangedObservableCollection.cs
new file mode 100644
index 00000000..97bb61e8
--- /dev/null
+++ b/Sources/Stylophone.Common/Helpers/RangedObservableCollection.cs
@@ -0,0 +1,170 @@
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.ComponentModel;
+using System.Linq;
+
+namespace System.Collections.ObjectModel
+{
+ ///
+ /// Implementation of a dynamic data collection based on generic Collection<T>,
+ /// implementing INotifyCollectionChanged to notify listeners
+ /// when items get added, removed or the whole list is refreshed.
+ ///
+ public class RangedObservableCollection : ObservableCollection
+ {
+ private const string CountName = "Count";
+ private const string IndexerName = "Item[]";
+
+ ///
+ /// Initializes a new instance of RangedObservableCollection that is empty and has default initial capacity.
+ ///
+ public RangedObservableCollection()
+ : base()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the RangedObservableCollection class that contains
+ /// elements copied from the specified collection and has sufficient capacity
+ /// to accommodate the number of elements copied.
+ ///
+ /// The collection whose elements are copied to the new list.
+ ///
+ /// The elements are copied onto the RangedObservableCollection in the
+ /// same order they are read by the enumerator of the collection.
+ ///
+ /// collection is a null reference
+ public RangedObservableCollection(IEnumerable collection)
+ : base(collection)
+ {
+ }
+
+ ///
+ /// Adds the elements of the specified collection to the end of the RangedObservableCollection.
+ ///
+ /// The collection whose elements are added to the list.
+ ///
+ /// The elements are copied onto the RangedObservableCollection in the
+ /// same order they are read by the enumerator of the collection.
+ ///
+ /// collection is a null reference
+ public void AddRange(IEnumerable collection)
+ {
+ if (collection == null)
+ throw new ArgumentNullException(nameof(collection));
+
+ CheckReentrancy();
+
+ var startIndex = Count;
+ var changedItems = new List(collection);
+
+ foreach (var i in changedItems)
+ Items.Add(i);
+
+ OnCountPropertyChanged();
+ OnIndexerPropertyChanged();
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, changedItems, startIndex));
+ }
+
+ ///
+ /// Clears the current RangedObservableCollection and replaces the elements with the elements of specified collection.
+ ///
+ /// The collection whose elements are added to the list.
+ ///
+ /// The elements are copied onto the RangedObservableCollection in the
+ /// same order they are read by the enumerator of the collection.
+ ///
+ /// collection is a null reference
+ public void ReplaceRange(IEnumerable collection)
+ {
+ if (collection == null)
+ throw new ArgumentNullException(nameof(collection));
+
+ CheckReentrancy();
+
+ Items.Clear();
+ foreach (var i in collection)
+ Items.Add(i);
+
+ OnCountPropertyChanged();
+ OnIndexerPropertyChanged();
+ OnCollectionReset();
+ }
+
+ ///
+ /// Removes the first occurence of each item in the specified collection.
+ ///
+ /// The collection whose elements are removed from list.
+ ///
+ /// The elements are copied onto the RangedObservableCollection in the
+ /// same order they are read by the enumerator of the collection.
+ ///
+ /// collection is a null reference
+ public void RemoveRange(IEnumerable collection)
+ {
+ if (collection == null)
+ throw new ArgumentNullException(nameof(collection));
+
+ CheckReentrancy();
+
+ // HACK ATTACK: normally, since this method can remove items from multiple different spaces in the collection, this'd be incorrect...
+ // but since we only use it for ranged deletions of queue items, which are always sequential, it should be fine(tm)
+ var index = Items.IndexOf(collection.FirstOrDefault());
+
+ var changedItems = new List(collection);
+ for (int i = 0; i < changedItems.Count; i++)
+ {
+ if (!Items.Remove(changedItems[i]))
+ {
+ changedItems.RemoveAt(i);
+ i--;
+ }
+ }
+
+ if (changedItems.Count > 0)
+ {
+ OnCountPropertyChanged();
+ OnIndexerPropertyChanged();
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, changedItems, index));
+ }
+ }
+
+ ///
+ /// Clears the current RangedObservableCollection and replaces the elements with the specified element.
+ ///
+ /// The element which is added to the list.
+ public void Replace(T item)
+ {
+ CheckReentrancy();
+
+ Items.Clear();
+ Items.Add(item);
+
+ OnCountPropertyChanged();
+ OnIndexerPropertyChanged();
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+ }
+
+ private void OnCountPropertyChanged()
+ {
+ OnPropertyChanged(EventArgsCache.CountPropertyChanged);
+ }
+
+ private void OnIndexerPropertyChanged()
+ {
+ OnPropertyChanged(EventArgsCache.IndexerPropertyChanged);
+ }
+
+ private void OnCollectionReset()
+ {
+ OnCollectionChanged(EventArgsCache.ResetCollectionChanged);
+ }
+
+ private static class EventArgsCache
+ {
+ internal static readonly PropertyChangedEventArgs CountPropertyChanged = new PropertyChangedEventArgs(CountName);
+ internal static readonly PropertyChangedEventArgs IndexerPropertyChanged = new PropertyChangedEventArgs(IndexerName);
+ internal static readonly NotifyCollectionChangedEventArgs ResetCollectionChanged = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Sources/Stylophone.Common/Services/AlbumArtService.cs b/Sources/Stylophone.Common/Services/AlbumArtService.cs
index 25e9df96..7e88da06 100644
--- a/Sources/Stylophone.Common/Services/AlbumArtService.cs
+++ b/Sources/Stylophone.Common/Services/AlbumArtService.cs
@@ -40,7 +40,6 @@ public void Initialize()
{
_queueCanceller?.Cancel();
_queueCanceller = new CancellationTokenSource();
-
var token = _queueCanceller.Token;
_albumArtQueue = new Stack();
@@ -81,6 +80,11 @@ public void Initialize()
}).ConfigureAwait(false);
}
+ public void Stop()
+ {
+ _queueCanceller?.Cancel();
+ }
+
///
/// Check if this file's album art is already stored in the internal Album Art cache.
///
diff --git a/Sources/Stylophone.Common/Services/MPDConnectionService.cs b/Sources/Stylophone.Common/Services/MPDConnectionService.cs
index 4d0acc26..1f6f6db7 100644
--- a/Sources/Stylophone.Common/Services/MPDConnectionService.cs
+++ b/Sources/Stylophone.Common/Services/MPDConnectionService.cs
@@ -11,11 +11,10 @@
using Stylophone.Common.Interfaces;
using MpcNET.Commands.Reflection;
using Stylophone.Localization.Strings;
+using Stylophone.Common.Helpers;
using CommunityToolkit.Mvvm.DependencyInjection;
-using Stylophone.Common.ViewModels;
using Microsoft.AppCenter.Analytics;
-using Microsoft.AppCenter;
-using System.Drawing;
+using Stylophone.Common.ViewModels;
namespace Stylophone.Common.Services
{
@@ -77,13 +76,7 @@ public async Task InitializeAsync(bool withRetry = false)
IsConnecting = true;
CurrentStatus = BOGUS_STATUS; // Reset status
- if (IsConnected)
- {
- IsConnected = false;
- ConnectionChanged?.Invoke(this, new EventArgs());
- }
-
- ClearResources();
+ Disconnect();
var cancelToken = _cancelConnect.Token;
@@ -106,23 +99,31 @@ public async Task InitializeAsync(bool withRetry = false)
_connectionRetryAttempter.Start();
}
}
-
+
IsConnecting = false;
}
- private void ClearResources()
+ public void Disconnect()
{
- _idleConnection?.SendAsync(new NoIdleCommand());
- _idleConnection?.DisconnectAsync();
- _statusConnection?.DisconnectAsync();
+
+ if (IsConnected)
+ {
+ System.Diagnostics.Debug.WriteLine($"Terminating MPD connections");
+ IsConnected = false;
+ ConnectionChanged?.Invoke(this, new EventArgs());
+ }
+
+ // Stop the idle connection first
+ _cancelIdle?.Cancel();
_connectionRetryAttempter?.Stop();
_connectionRetryAttempter?.Dispose();
+ // Stop the status timer before killing the matching connection
_statusUpdater?.Stop();
_statusUpdater?.Dispose();
+ _statusConnection?.DisconnectAsync();
- _cancelIdle?.Cancel();
_cancelIdle = new CancellationTokenSource();
_cancelConnect?.Cancel();
@@ -137,10 +138,16 @@ private void ClearResources()
private async Task TryConnecting(CancellationToken token)
{
if (token.IsCancellationRequested) return;
- if (!IPAddress.TryParse(_host, out var ipAddress))
- throw new Exception("Invalid IP address");
- _mpdEndpoint = new IPEndPoint(ipAddress, _port);
+ if (!IPAddress.TryParse(_host, out var ipAddress))
+ {
+ // Maybe it's a hostname? Try getting an IP from it nonetheless
+ _mpdEndpoint = Miscellaneous.GetIPEndPointFromHostName(_host, _port, false);
+ }
+ else
+ {
+ _mpdEndpoint = new IPEndPoint(ipAddress, _port);
+ }
_statusConnection = await GetConnectionInternalAsync(token);
@@ -223,8 +230,12 @@ public async Task SafelySendCommandAsync(IMpcCommand command, bool show
{
var dict = new Dictionary();
dict.Add("command", command.Serialize());
- dict.Add("exception", e.ToString());
- Analytics.TrackEvent("MPDError", dict);
+ dict.Add("exception", e.InnerException?.ToString());
+ dict.Add("source", e.Source);
+ dict.Add("message", e.Message);
+ dict.Add("stacktrace", e.StackTrace);
+
+ Analytics.TrackEvent("MPDError", dict);
}
#endif
}
@@ -270,15 +281,25 @@ private void InitializeStatusUpdater(CancellationToken token = default)
try
{
+ // Run the idleConnection in a wrapper task since MpcNET isn't fully async and will block here
+ var idleChangesTask = Task.Run(async () => await _idleConnection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options update")));
+
+ // Wait for the idle command to finish or for the token to be cancelled
+ await Task.WhenAny(idleChangesTask, Task.Delay(-1, token));
+
if (token.IsCancellationRequested || _idleConnection == null || !_idleConnection.IsConnected)
+ {
+ //_idleConnection?.SendAsync(new NoIdleCommand());
+ _idleConnection?.DisconnectAsync();
break;
+ }
+
+ var message = idleChangesTask.Result;
- var idleChanges = await _idleConnection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options update"));
-
- if (idleChanges.IsResponseValid)
- await HandleIdleResponseAsync(idleChanges.Response.Content);
+ if (message.IsResponseValid)
+ await HandleIdleResponseAsync(message.Response.Content);
else
- throw new Exception(idleChanges.Response?.Content);
+ throw new Exception(message.Response?.Content);
}
catch (Exception e)
{
diff --git a/Sources/Stylophone.Common/Stylophone.Common.csproj b/Sources/Stylophone.Common/Stylophone.Common.csproj
index 84d56cee..082c9c60 100644
--- a/Sources/Stylophone.Common/Stylophone.Common.csproj
+++ b/Sources/Stylophone.Common/Stylophone.Common.csproj
@@ -7,14 +7,13 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs b/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs
index f67add3a..d852b8a2 100644
--- a/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs
@@ -46,7 +46,7 @@ public AlbumDetailViewModel(IDialogService dialogService, INotificationService n
public bool IsSourceEmpty => Source.Count == 0;
[RelayCommand]
- private async void AddToQueue(object list)
+ private async Task AddToQueue(object list)
{
var selectedTracks = (IList)list;
@@ -67,7 +67,7 @@ private async void AddToQueue(object list)
}
[RelayCommand]
- private async void AddToPlaylist(object list)
+ private async Task AddToPlaylist(object list)
{
var playlistName = await _dialogService.ShowAddToPlaylistDialog();
if (playlistName == null) return;
diff --git a/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs b/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs
index 0cc0c545..078c979e 100644
--- a/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs
+++ b/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs
@@ -40,7 +40,7 @@ public async Task LoadDataAsync()
FilteredSource.CollectionChanged += (s, e) => OnPropertyChanged(nameof(IsSourceEmpty));
Source.Clear();
- var response = await _mpdService.SafelySendCommandAsync(new ListCommand(MpdTags.Album));
+ var response = await _mpdService.SafelySendCommandAsync(new ListCommand(MpdTags.AlbumSort));
if (response != null)
GroupAlbumsByName(response);
diff --git a/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs b/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs
index d717b565..17b1a2f3 100644
--- a/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs
+++ b/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs
@@ -61,10 +61,9 @@ public PlaybackViewModelBase(INavigationService navigationService, INotification
_internalVolume = _mpdService.CurrentStatus.Volume;
- // Bind timer methods and start it
+ // Bind timer methods
_updateInformationTimer = new System.Timers.Timer(500);
_updateInformationTimer.Elapsed += UpdateInformation;
- _updateInformationTimer.Start();
// Update info to current track
_mpdService.ConnectionChanged += OnConnectionChanged;
@@ -82,6 +81,7 @@ private void OnConnectionChanged(object sender, EventArgs e)
else
{
IsTrackInfoAvailable = false;
+ _updateInformationTimer?.Stop();
}
}
@@ -90,6 +90,7 @@ private void Initialize()
OnTrackChange(this, new SongChangedEventArgs { NewSongId = -1 });
CurrentTimeValue = _mpdService.CurrentStatus.Elapsed.TotalSeconds;
+ _updateInformationTimer.Start();
OnStateChange(this, null);
}
@@ -638,7 +639,6 @@ public virtual void Dispose()
_mpdService.SongChanged -= OnTrackChange;
_updateInformationTimer.Stop();
- _updateInformationTimer.Dispose();
}
#region Commands
diff --git a/Sources/Stylophone.Common/ViewModels/Items/AlbumViewModel.cs b/Sources/Stylophone.Common/ViewModels/Items/AlbumViewModel.cs
index b3115af1..0a85e31f 100644
--- a/Sources/Stylophone.Common/ViewModels/Items/AlbumViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/Items/AlbumViewModel.cs
@@ -113,7 +113,7 @@ internal void SetAlbumArt(AlbumArt art)
}
[RelayCommand]
- private async void AddToPlaylist()
+ private async Task AddToPlaylist()
{
var playlistName = await _dialogService.ShowAddToPlaylistDialog();
if (playlistName == null || Files.Count == 0) return;
@@ -132,7 +132,7 @@ private async void AddToPlaylist()
}
[RelayCommand]
- private async void AddAlbum()
+ private async Task AddAlbum()
{
var commandList = new CommandList();
@@ -152,7 +152,7 @@ private async void AddAlbum()
}
[RelayCommand]
- private async void PlayAlbum()
+ private async Task PlayAlbum()
{
if (Files.Count == 0)
{
@@ -208,7 +208,8 @@ public async Task LoadAlbumDataAsync(MpcConnection c)
if (Files.Count == 0)
Files.AddRange(findReq.Response.Content);
- Artist = Files.Select(f => f.Artist).Distinct().Where(f => f != "").Aggregate((f1, f2) => $"{f1}, {f2}");
+ Artist = Files.Any(f => f.HasAlbumArtist) ? Files.First(f => f.HasAlbumArtist).AlbumArtist :
+ Files.Select(f => f.Artist).Distinct().Where(f => f != "").Aggregate((f1, f2) => $"{f1}, {f2}");
// If we've already generated album art, don't use the queue and directly grab it
if (await _albumArtService.IsAlbumArtCachedAsync(Files[0]))
diff --git a/Sources/Stylophone.Common/ViewModels/Items/FilePathViewModel.cs b/Sources/Stylophone.Common/ViewModels/Items/FilePathViewModel.cs
index b323e154..b6a3cb3e 100644
--- a/Sources/Stylophone.Common/ViewModels/Items/FilePathViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/Items/FilePathViewModel.cs
@@ -66,10 +66,11 @@ internal FilePathViewModel(FilePathViewModelFactory factory, IMpdFilePath file,
if (file is MpdDirectory)
{
IsDirectory = true;
- _children = new RangedObservableCollection();
-
- // Add a bogus child that'll be replaced when the list is loaded
- _children.Add(new FilePathViewModel(Resources.FoldersLoadingTreeItem, this, _dispatcherService));
+ _children = new RangedObservableCollection
+ {
+ // Add a bogus child that'll be replaced when the list is loaded
+ new FilePathViewModel(Resources.FoldersLoadingTreeItem, this, _dispatcherService)
+ };
}
}
@@ -99,7 +100,7 @@ public FilePathViewModel(string name, FilePathViewModel parent, IDispatcherServi
private bool _isLoadingChildren;
public async Task LoadChildrenAsync()
{
- if (IsLoaded || _isLoadingChildren || IsDirectory == false || _children == null || Path == null) return;
+ if (IsLoaded || _isLoadingChildren || IsDirectory == false || Children == null || Path == null) return;
_isLoadingChildren = true;
try
@@ -118,8 +119,8 @@ public async Task LoadChildrenAsync()
await _dispatcherService.ExecuteOnUIThreadAsync(() =>
{
- _children.AddRange(newChildren);
- _children.RemoveAt(0); // Remove the placeholder after adding the new items, otherwise the treeitem can close back up
+ Children.AddRange(newChildren);
+ Children.RemoveAt(0); // Remove the placeholder after adding the new items, otherwise the treeitem can close back up
IsLoaded = true;
});
}
@@ -131,7 +132,7 @@ await _dispatcherService.ExecuteOnUIThreadAsync(() =>
[RelayCommand]
- private async void Play()
+ private async Task Play()
{
// Clear queue, add path and play
var commandList = new CommandList(new IMpcCommand[] { new ClearCommand(), new AddCommand(Path), new PlayCommand(0) });
@@ -143,7 +144,7 @@ private async void Play()
}
[RelayCommand]
- private async void AddToQueue()
+ private async Task AddToQueue()
{
// AddCommand adds either the full directory or the song, depending on the path given.
var response = await _mpdService.SafelySendCommandAsync(new AddCommand(Path));
@@ -153,7 +154,7 @@ private async void AddToQueue()
}
[RelayCommand]
- private async void AddToPlaylist()
+ private async Task AddToPlaylist()
{
var playlistName = await _dialogService.ShowAddToPlaylistDialog();
if (playlistName == null) return;
diff --git a/Sources/Stylophone.Common/ViewModels/Items/TrackViewModel.cs b/Sources/Stylophone.Common/ViewModels/Items/TrackViewModel.cs
index 76371ee8..907e7465 100644
--- a/Sources/Stylophone.Common/ViewModels/Items/TrackViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/Items/TrackViewModel.cs
@@ -90,13 +90,13 @@ internal TrackViewModel(TrackViewModelFactory factory, IMpdFile file): base(fact
private bool _isLight;
[RelayCommand]
- private async void PlayTrack(IMpdFile file) => await _mpdService.SafelySendCommandAsync(new PlayIdCommand(file.Id));
+ private async Task PlayTrack(IMpdFile file) => await _mpdService.SafelySendCommandAsync(new PlayIdCommand(file.Id));
[RelayCommand]
- private async void RemoveFromQueue(IMpdFile file) => await _mpdService.SafelySendCommandAsync(new DeleteIdCommand(file.Id));
+ private async Task RemoveFromQueue(IMpdFile file) => await _mpdService.SafelySendCommandAsync(new DeleteIdCommand(file.Id));
[RelayCommand]
- private async void AddToQueue(IMpdFile file)
+ private async Task AddToQueue(IMpdFile file)
{
var response = await _mpdService.SafelySendCommandAsync(new AddIdCommand(file.Path));
@@ -105,7 +105,7 @@ private async void AddToQueue(IMpdFile file)
}
[RelayCommand]
- private async void AddToPlaylist(IMpdFile file)
+ private async Task AddToPlaylist(IMpdFile file)
{
var playlistName = await _dialogService.ShowAddToPlaylistDialog();
if (playlistName == null) return;
@@ -168,5 +168,9 @@ public void Dispose()
AlbumArt?.Dispose();
}
+ public override string ToString()
+ {
+ return $"{Name} - {File.Artist} - {File.Album} - {Miscellaneous.FormatTimeString(File.Time*1000)}";
+ }
}
}
diff --git a/Sources/Stylophone.Common/ViewModels/PlaylistViewModel.cs b/Sources/Stylophone.Common/ViewModels/PlaylistViewModel.cs
index 917e1a18..9f4be259 100644
--- a/Sources/Stylophone.Common/ViewModels/PlaylistViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/PlaylistViewModel.cs
@@ -95,7 +95,7 @@ private bool IsSingleTrackSelected(object list)
}
[RelayCommand]
- private async void RemovePlaylist()
+ private async Task RemovePlaylist()
{
var result = await _dialogService.ShowConfirmDialogAsync(Resources.DeletePlaylistContentDialog, "", Resources.OKButtonText, Resources.CancelButtonText);
@@ -112,7 +112,7 @@ private async void RemovePlaylist()
}
[RelayCommand]
- private async void LoadPlaylist()
+ private async Task LoadPlaylist()
{
var res = await _mpdService.SafelySendCommandAsync(new LoadCommand(Name));
@@ -121,7 +121,7 @@ private async void LoadPlaylist()
}
[RelayCommand]
- private async void PlayPlaylist()
+ private async Task PlayPlaylist()
{
// Clear queue, add playlist and play
var commandList = new CommandList(new IMpcCommand[] { new ClearCommand() , new LoadCommand(Name), new PlayCommand(0) });
@@ -135,7 +135,7 @@ private async void PlayPlaylist()
[RelayCommand]
- private async void AddToQueue(object list)
+ private async Task AddToQueue(object list)
{
// Cast the received __ComObject
var selectedTracks = (IList)list;
@@ -171,27 +171,20 @@ private void ViewAlbum(object list)
[RelayCommand]
- private async void RemoveTrackFromPlaylist(object list)
+ private async Task RemoveTrackFromPlaylist(object list)
{
// Cast the received __ComObject
var selectedTracks = (IList)list;
-
- if (selectedTracks?.Count > 0)
+ var trackCount = selectedTracks?.Count;
+ if (trackCount > 0)
{
- var commandList = new CommandList();
-
- // We can't batch PlaylistDeleteCommands cleanly, since they're index-based and logically, said indexes will shift as we remove stuff from the playlist.
- // To simulate this behavior, we copy our Source list and incrementally remove the affected tracks from it to get the valid indexes as we move down the commandList.
- IList copy = Source.ToList();
+ var command = new PlaylistDeleteCommand(Name, Source.IndexOf(selectedTracks.First() as TrackViewModel));
+
+ // Use new ranged variant if necessary
+ if (trackCount > 1)
+ command = new PlaylistDeleteCommand(Name, Source.IndexOf(selectedTracks.First() as TrackViewModel), Source.IndexOf(selectedTracks.Last() as TrackViewModel));
- foreach (var f in selectedTracks)
- {
- var trackVM = f as TrackViewModel;
- commandList.Add(new PlaylistDeleteCommand(Name, copy.IndexOf(trackVM)));
- copy.Remove(trackVM);
- }
-
- var r = await _mpdService.SafelySendCommandAsync(commandList);
+ var r = await _mpdService.SafelySendCommandAsync(command);
if (r != null) // Reload playlist
await LoadDataAsync(Name);
}
@@ -237,7 +230,7 @@ public async Task LoadDataAsync(string playlistName)
await Task.Run(async () =>
{
// Get album art for three albums to display in the playlist view
- Random r = new Random();
+ Random r = new();
var distinctAlbums = Source.GroupBy(tr => tr.File.Album).Select(tr => tr.First()).OrderBy((item) => r.Next()).ToList();
if (distinctAlbums.Count > 1)
@@ -247,8 +240,13 @@ await Task.Run(async () =>
DominantColor = (art?.DominantColor?.Color).GetValueOrDefault();
- if (DominantColor == default(SKColor))
- DominantColor = _interop.GetAccentColor();
+ if (DominantColor == default)
+ {
+ await _dispatcherService.ExecuteOnUIThreadAsync(() =>
+ {
+ DominantColor = _interop.GetAccentColor();
+ });
+ }
IsLight = (!art?.DominantColor?.IsDark).GetValueOrDefault();
}
@@ -274,6 +272,13 @@ await Task.Run(async () =>
private async void Source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
+ // iOS uses move
+ if (e.Action == NotifyCollectionChangedAction.Move)
+ {
+ await _mpdService.SafelySendCommandAsync(new PlaylistMoveCommand(Name, e.OldStartingIndex, e.NewStartingIndex));
+ return;
+ }
+
if (e.Action == NotifyCollectionChangedAction.Add && _previousAction == NotifyCollectionChangedAction.Remove)
{
// User reordered tracks, send matching command
diff --git a/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs b/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs
index 53ca64ae..6ec853ee 100644
--- a/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs
@@ -162,12 +162,21 @@ private async Task ClearQueue()
///
private async void Source_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
+ // iOS uses move
+ if (e.Action == NotifyCollectionChangedAction.Move)
+ {
+ _oldId = e.OldItems.Count > 0 ? (e.OldItems[0] as TrackViewModel).File.Id : -1;
+ await _mpdService.SafelySendCommandAsync(new MoveIdCommand(_oldId, e.NewStartingIndex));
+ return;
+ }
+
if (e.Action == NotifyCollectionChangedAction.Add && _previousAction == NotifyCollectionChangedAction.Remove)
{
// One Remove->Add Pair means one MoveIdCommand
- await _mpdService.SafelySendCommandAsync(new MoveIdCommand(_oldId, e.NewStartingIndex));
_previousAction = NotifyCollectionChangedAction.Move;
+ await _mpdService.SafelySendCommandAsync(new MoveIdCommand(_oldId, e.NewStartingIndex));
}
+
if (e.Action == NotifyCollectionChangedAction.Remove)
{
_previousAction = e.Action;
@@ -199,37 +208,39 @@ private async void MPDConnectionService_QueueChanged(object sender, EventArgs e)
{
Source.CollectionChanged -= Source_CollectionChanged;
- // If the queue was cleared, PlaylistLength is 0.
- if (status.PlaylistLength == 0)
+ // PlChanges gives the full list of files affected by the change.
+ foreach (var f in response)
{
- await _dispatcherService.ExecuteOnUIThreadAsync(() => Source.Clear());
- }
- else
- {
- // PlChanges gives the full list of files starting from the change, so we delete all existing tracks from the source after that change, and swap the new ones in.
- // If the response is empty, that means the last file in the source was removed.
- var initialPosition = response.Count() == 0 ? Source.Count - 1 : response.First().Position;
+ var trackVm = Source.Where(tvm => tvm.File.Id == f.Id).FirstOrDefault();
- while (Source.Count != initialPosition)
- {
- await _dispatcherService.ExecuteOnUIThreadAsync(() => {
- // Make sure
- if (Source.Count != initialPosition)
- Source.RemoveAt(initialPosition);
- });
- }
+ // We might already be up to date
+ if (Source.IndexOf(trackVm) == f.Position)
+ continue;
- var toAdd = new List();
- foreach (var item in response)
+ // Nw track
+ if (trackVm == default)
{
- var trackVm = _trackVmFactory.GetTrackViewModel(item);
- toAdd.Add(trackVm);
+ trackVm = _trackVmFactory.GetTrackViewModel(f);
}
+ else await _dispatcherService.ExecuteOnUIThreadAsync(() =>
+ {
+ Source.Remove(trackVm);
+ });
- if (toAdd.Count > 0)
- await _dispatcherService.ExecuteOnUIThreadAsync(() => Source.AddRange(toAdd));
+ await _dispatcherService.ExecuteOnUIThreadAsync(() =>
+ {
+ Source.Insert(f.Position, trackVm);
+ });
}
+ // To detect songs that were deleted at the end of the playlist, use playlistlength returned by status command.
+ // (If the queue was cleared, PlaylistLength is 0.)
+ var tracksToRemove = Source.Skip(status.PlaylistLength).ToList();
+
+ await _dispatcherService.ExecuteOnUIThreadAsync(() => {
+ Source.RemoveRange(tracksToRemove);
+ });
+
Source.CollectionChanged += Source_CollectionChanged;
// Update internal playlist version
diff --git a/Sources/Stylophone.Common/ViewModels/SearchResultsViewModel.cs b/Sources/Stylophone.Common/ViewModels/SearchResultsViewModel.cs
index 6b382472..e5768e84 100644
--- a/Sources/Stylophone.Common/ViewModels/SearchResultsViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/SearchResultsViewModel.cs
@@ -105,7 +105,7 @@ private bool IsSingleTrackSelected(object list)
}
[RelayCommand]
- private async void AddToQueue(object list)
+ private async Task AddToQueue(object list)
{
var selectedTracks = (IList)list;
@@ -126,7 +126,7 @@ private async void AddToQueue(object list)
}
[RelayCommand]
- private async void AddToPlaylist(object list)
+ private async Task AddToPlaylist(object list)
{
var playlistName = await _dialogService.ShowAddToPlaylistDialog();
if (playlistName == null) return;
diff --git a/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs b/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs
index 34d07aa7..85f28524 100644
--- a/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs
@@ -78,11 +78,7 @@ public SettingsViewModel(MPDConnectionService mpdService, IApplicationStorageSer
partial void OnElementThemeChanged(Theme value)
{
Task.Run (async () => await _interop.SetThemeAsync(value));
-
- if (value != _elementTheme)
- {
- _applicationStorageService.SetValue(nameof(ElementTheme), value.ToString());
- }
+ _applicationStorageService.SetValue(nameof(ElementTheme), value.ToString());
}
partial void OnServerHostChanged(string value)
@@ -242,7 +238,7 @@ private async Task UpdateServerVersionAsync()
var songs = response.ContainsKey("songs") ? response["songs"] : "??";
var albums = response.ContainsKey("albums") ? response["albums"] : "??";
- if (outputs != null)
+ if (outputs != null && outputs.Count() > 0)
{
var outputString = outputs.Select(o => o.Plugin).Aggregate((s, s2) => $"{s}, {s2}");
diff --git a/Sources/Stylophone.Localization/Strings/Resources.Designer.cs b/Sources/Stylophone.Localization/Strings/Resources.Designer.cs
index 589217cc..dd3195d1 100644
--- a/Sources/Stylophone.Localization/Strings/Resources.Designer.cs
+++ b/Sources/Stylophone.Localization/Strings/Resources.Designer.cs
@@ -1,10 +1,10 @@
//------------------------------------------------------------------------------
//
-// Ce code a été généré par un outil.
-// Version du runtime :4.0.30319.42000
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
//
-// Les modifications apportées à ce fichier peuvent provoquer un comportement incorrect et seront perdues si
-// le code est régénéré.
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
//
//------------------------------------------------------------------------------
@@ -13,12 +13,12 @@ namespace Stylophone.Localization.Strings {
///
- /// Une classe de ressource fortement typée destinée, entre autres, à la consultation des chaînes localisées.
+ /// A strongly-typed resource class, for looking up localized strings, etc.
///
- // Cette classe a été générée automatiquement par la classe StronglyTypedResourceBuilder
- // à l'aide d'un outil, tel que ResGen ou Visual Studio.
- // Pour ajouter ou supprimer un membre, modifiez votre fichier .ResX, puis réexécutez ResGen
- // avec l'option /str ou régénérez votre projet VS.
+ // This class was auto-generated by the StronglyTypedResourceBuilder
+ // class via a tool like ResGen or Visual Studio.
+ // To add or remove a member, edit your .ResX file then rerun ResGen
+ // with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
@@ -33,7 +33,7 @@ internal Resources() {
}
///
- /// Retourne l'instance ResourceManager mise en cache utilisée par cette classe.
+ /// Returns the cached ResourceManager instance used by this class.
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Resources.ResourceManager ResourceManager {
@@ -47,8 +47,8 @@ internal Resources() {
}
///
- /// Remplace la propriété CurrentUICulture du thread actuel pour toutes
- /// les recherches de ressources à l'aide de cette classe de ressource fortement typée.
+ /// Overrides the current thread's CurrentUICulture property for all
+ /// resource lookups using this strongly typed resource class.
///
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
public static global::System.Globalization.CultureInfo Culture {
@@ -61,7 +61,7 @@ internal Resources() {
}
///
- /// Recherche une chaîne localisée semblable à Change Volume.
+ /// Looks up a localized string similar to Change Volume.
///
public static string ActionChangeVolume {
get {
@@ -70,7 +70,7 @@ public static string ActionChangeVolume {
}
///
- /// Recherche une chaîne localisée semblable à Show Mini Player.
+ /// Looks up a localized string similar to Show Mini Player.
///
public static string ActionCompactOverlay {
get {
@@ -79,7 +79,16 @@ public static string ActionCompactOverlay {
}
///
- /// Recherche une chaîne localisée semblable à More Actions.
+ /// Looks up a localized string similar to Open Fullscreen Playback.
+ ///
+ public static string ActionFullscreenPlayback {
+ get {
+ return ResourceManager.GetString("ActionFullscreenPlayback", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to More Actions.
///
public static string ActionMore {
get {
@@ -88,7 +97,7 @@ public static string ActionMore {
}
///
- /// Recherche une chaîne localisée semblable à Play/Pause.
+ /// Looks up a localized string similar to Play/Pause.
///
public static string ActionPlayPause {
get {
@@ -97,7 +106,7 @@ public static string ActionPlayPause {
}
///
- /// Recherche une chaîne localisée semblable à Next Track.
+ /// Looks up a localized string similar to Next Track.
///
public static string ActionSkipNext {
get {
@@ -106,7 +115,7 @@ public static string ActionSkipNext {
}
///
- /// Recherche une chaîne localisée semblable à Previous Track.
+ /// Looks up a localized string similar to Previous Track.
///
public static string ActionSkipPrevious {
get {
@@ -115,7 +124,7 @@ public static string ActionSkipPrevious {
}
///
- /// Recherche une chaîne localisée semblable à Toggle Consume.
+ /// Looks up a localized string similar to Toggle Consume.
///
public static string ActionToggleConsume {
get {
@@ -124,7 +133,7 @@ public static string ActionToggleConsume {
}
///
- /// Recherche une chaîne localisée semblable à Toggle Repeat.
+ /// Looks up a localized string similar to Toggle Repeat.
///
public static string ActionToggleRepeat {
get {
@@ -133,7 +142,7 @@ public static string ActionToggleRepeat {
}
///
- /// Recherche une chaîne localisée semblable à Toggle Shuffle.
+ /// Looks up a localized string similar to Toggle Shuffle.
///
public static string ActionToggleShuffle {
get {
@@ -142,7 +151,7 @@ public static string ActionToggleShuffle {
}
///
- /// Recherche une chaîne localisée semblable à Create New Playlist.
+ /// Looks up a localized string similar to Create New Playlist.
///
public static string AddToPlaylistCreateNewPlaylist {
get {
@@ -151,7 +160,7 @@ public static string AddToPlaylistCreateNewPlaylist {
}
///
- /// Recherche une chaîne localisée semblable à Playlist Name.
+ /// Looks up a localized string similar to Playlist Name.
///
public static string AddToPlaylistNewPlaylistName {
get {
@@ -160,7 +169,7 @@ public static string AddToPlaylistNewPlaylistName {
}
///
- /// Recherche une chaîne localisée semblable à Select a Playlist.
+ /// Looks up a localized string similar to Select a Playlist.
///
public static string AddToPlaylistPlaceholder {
get {
@@ -169,7 +178,7 @@ public static string AddToPlaylistPlaceholder {
}
///
- /// Recherche une chaîne localisée semblable à Add.
+ /// Looks up a localized string similar to Add.
///
public static string AddToPlaylistPrimaryButtonText {
get {
@@ -178,7 +187,7 @@ public static string AddToPlaylistPrimaryButtonText {
}
///
- /// Recherche une chaîne localisée semblable à Add track(s) to the following Playlist:.
+ /// Looks up a localized string similar to Add track(s) to the following Playlist:.
///
public static string AddToPlaylistText {
get {
@@ -187,7 +196,7 @@ public static string AddToPlaylistText {
}
///
- /// Recherche une chaîne localisée semblable à Add to Playlist.
+ /// Looks up a localized string similar to Add to Playlist.
///
public static string AddToPlaylistTitle {
get {
@@ -196,7 +205,7 @@ public static string AddToPlaylistTitle {
}
///
- /// Recherche une chaîne localisée semblable à Stylophone.
+ /// Looks up a localized string similar to Stylophone.
///
public static string AppDisplayName {
get {
@@ -205,7 +214,7 @@ public static string AppDisplayName {
}
///
- /// Recherche une chaîne localisée semblable à Cancel.
+ /// Looks up a localized string similar to Cancel.
///
public static string CancelButtonText {
get {
@@ -214,7 +223,7 @@ public static string CancelButtonText {
}
///
- /// Recherche une chaîne localisée semblable à Save Queue as Playlist.
+ /// Looks up a localized string similar to Save Queue as Playlist.
///
public static string ContextMenuAddQueueToPlaylist {
get {
@@ -223,7 +232,7 @@ public static string ContextMenuAddQueueToPlaylist {
}
///
- /// Recherche une chaîne localisée semblable à Add to Playlist.
+ /// Looks up a localized string similar to Add to Playlist.
///
public static string ContextMenuAddToPlaylist {
get {
@@ -232,7 +241,7 @@ public static string ContextMenuAddToPlaylist {
}
///
- /// Recherche une chaîne localisée semblable à Add to Queue.
+ /// Looks up a localized string similar to Add to Queue.
///
public static string ContextMenuAddToQueue {
get {
@@ -241,7 +250,7 @@ public static string ContextMenuAddToQueue {
}
///
- /// Recherche une chaîne localisée semblable à Clear Queue.
+ /// Looks up a localized string similar to Clear Queue.
///
public static string ContextMenuClearQueue {
get {
@@ -250,7 +259,7 @@ public static string ContextMenuClearQueue {
}
///
- /// Recherche une chaîne localisée semblable à Delete Playlist.
+ /// Looks up a localized string similar to Delete Playlist.
///
public static string ContextMenuDeletePlaylist {
get {
@@ -259,7 +268,7 @@ public static string ContextMenuDeletePlaylist {
}
///
- /// Recherche une chaîne localisée semblable à Play.
+ /// Looks up a localized string similar to Play.
///
public static string ContextMenuPlay {
get {
@@ -268,7 +277,7 @@ public static string ContextMenuPlay {
}
///
- /// Recherche une chaîne localisée semblable à Remove from Playlist.
+ /// Looks up a localized string similar to Remove from Playlist.
///
public static string ContextMenuRemoveFromPlaylist {
get {
@@ -277,7 +286,7 @@ public static string ContextMenuRemoveFromPlaylist {
}
///
- /// Recherche une chaîne localisée semblable à Remove from Queue.
+ /// Looks up a localized string similar to Remove from Queue.
///
public static string ContextMenuRemoveFromQueue {
get {
@@ -286,7 +295,7 @@ public static string ContextMenuRemoveFromQueue {
}
///
- /// Recherche une chaîne localisée semblable à View Album.
+ /// Looks up a localized string similar to View Album.
///
public static string ContextMenuViewAlbum {
get {
@@ -295,7 +304,7 @@ public static string ContextMenuViewAlbum {
}
///
- /// Recherche une chaîne localisée semblable à Server Database is Updating.
+ /// Looks up a localized string similar to Server Database is Updating.
///
public static string DatabaseUpdateHeader {
get {
@@ -304,7 +313,7 @@ public static string DatabaseUpdateHeader {
}
///
- /// Recherche une chaîne localisée semblable à Delete Playlist?.
+ /// Looks up a localized string similar to Delete Playlist?.
///
public static string DeletePlaylistContentDialog {
get {
@@ -313,7 +322,7 @@ public static string DeletePlaylistContentDialog {
}
///
- /// Recherche une chaîne localisée semblable à Is your MPD server connected?.
+ /// Looks up a localized string similar to Is your MPD server connected?.
///
public static string EmptyFoldersDesc {
get {
@@ -322,7 +331,7 @@ public static string EmptyFoldersDesc {
}
///
- /// Recherche une chaîne localisée semblable à No folders found on server..
+ /// Looks up a localized string similar to No folders found on server..
///
public static string EmptyFoldersTitle {
get {
@@ -331,7 +340,7 @@ public static string EmptyFoldersTitle {
}
///
- /// Recherche une chaîne localisée semblable à Ain't nothin' like an empty library..
+ /// Looks up a localized string similar to Ain't nothin' like an empty library..
///
public static string EmptyLibraryTitle {
get {
@@ -340,7 +349,7 @@ public static string EmptyLibraryTitle {
}
///
- /// Recherche une chaîne localisée semblable à Get some tracks in there!.
+ /// Looks up a localized string similar to Get some tracks in there!.
///
public static string EmptyPlaylistDesc {
get {
@@ -349,7 +358,7 @@ public static string EmptyPlaylistDesc {
}
///
- /// Recherche une chaîne localisée semblable à I can't jam to this..
+ /// Looks up a localized string similar to I can't jam to this..
///
public static string EmptyPlaylistTitle {
get {
@@ -358,7 +367,7 @@ public static string EmptyPlaylistTitle {
}
///
- /// Recherche une chaîne localisée semblable à Why don't you add some music?.
+ /// Looks up a localized string similar to Why don't you add some music?.
///
public static string EmptyQueueDesc {
get {
@@ -367,7 +376,7 @@ public static string EmptyQueueDesc {
}
///
- /// Recherche une chaîne localisée semblable à All is quiet now..
+ /// Looks up a localized string similar to All is quiet now..
///
public static string EmptyQueueTitle {
get {
@@ -376,7 +385,7 @@ public static string EmptyQueueTitle {
}
///
- /// Recherche une chaîne localisée semblable à No Tracks found on Server..
+ /// Looks up a localized string similar to No Tracks found on Server..
///
public static string EmptySearchDesc {
get {
@@ -385,7 +394,7 @@ public static string EmptySearchDesc {
}
///
- /// Recherche une chaîne localisée semblable à Funky Fresh Search!.
+ /// Looks up a localized string similar to Funky Fresh Search!.
///
public static string EmptySearchTitle {
get {
@@ -394,7 +403,7 @@ public static string EmptySearchTitle {
}
///
- /// Recherche une chaîne localisée semblable à Couldn't add Album: {0}.
+ /// Looks up a localized string similar to Couldn't add Album: {0}.
///
public static string ErrorAddingAlbum {
get {
@@ -403,7 +412,7 @@ public static string ErrorAddingAlbum {
}
///
- /// Recherche une chaîne localisée semblable à Couldn't clear queue!.
+ /// Looks up a localized string similar to Couldn't clear queue!.
///
public static string ErrorCantClearCache {
get {
@@ -412,7 +421,7 @@ public static string ErrorCantClearCache {
}
///
- /// Recherche une chaîne localisée semblable à Error: {0}.
+ /// Looks up a localized string similar to Error: {0}.
///
public static string ErrorGeneric {
get {
@@ -421,7 +430,7 @@ public static string ErrorGeneric {
}
///
- /// Recherche une chaîne localisée semblable à Invalid MPD Response..
+ /// Looks up a localized string similar to Invalid MPD Response..
///
public static string ErrorInvalidMPDResponse {
get {
@@ -430,7 +439,7 @@ public static string ErrorInvalidMPDResponse {
}
///
- /// Recherche une chaîne localisée semblable à This track doesn't have a matching Album..
+ /// Looks up a localized string similar to This track doesn't have a matching Album..
///
public static string ErrorNoMatchingAlbum {
get {
@@ -439,7 +448,7 @@ public static string ErrorNoMatchingAlbum {
}
///
- /// Recherche une chaîne localisée semblable à Invalid password.
+ /// Looks up a localized string similar to Invalid password.
///
public static string ErrorPassword {
get {
@@ -448,7 +457,7 @@ public static string ErrorPassword {
}
///
- /// Recherche une chaîne localisée semblable à Error trying to play the MPD server's httpd stream.
+ /// Looks up a localized string similar to Error trying to play the MPD server's httpd stream.
///
public static string ErrorPlayingMPDStream {
get {
@@ -457,7 +466,7 @@ public static string ErrorPlayingMPDStream {
}
///
- /// Recherche une chaîne localisée semblable à Couldn't play content: {0}.
+ /// Looks up a localized string similar to Couldn't play content: {0}.
///
public static string ErrorPlayingTrack {
get {
@@ -466,7 +475,7 @@ public static string ErrorPlayingTrack {
}
///
- /// Recherche une chaîne localisée semblable à Sending {0} failed.
+ /// Looks up a localized string similar to Sending {0} failed.
///
public static string ErrorSendingMPDCommand {
get {
@@ -475,7 +484,7 @@ public static string ErrorSendingMPDCommand {
}
///
- /// Recherche une chaîne localisée semblable à Updating Playlists failed.
+ /// Looks up a localized string similar to Updating Playlists failed.
///
public static string ErrorUpdatingPlaylist {
get {
@@ -484,7 +493,7 @@ public static string ErrorUpdatingPlaylist {
}
///
- /// Recherche une chaîne localisée semblable à David Bowie is credited with playing the Stylophone on his 1969 debut hit song "Space Oddity" and also for his 2002 album Heathen track titled "Slip Away," as well as on the song "Heathen (The Rays)"..
+ /// Looks up a localized string similar to David Bowie is credited with playing the Stylophone on his 1969 debut hit song "Space Oddity" and also for his 2002 album Heathen track titled "Slip Away," as well as on the song "Heathen (The Rays)"..
///
public static string FirstRunFlavorText {
get {
@@ -493,7 +502,7 @@ public static string FirstRunFlavorText {
}
///
- /// Recherche une chaîne localisée semblable à To get started, please visit the Settings page to key in your MPD server's URL and port.
+ /// Looks up a localized string similar to To get started, please visit the Settings page to key in your MPD server's URL and port.
///Keep in mind Stylophone can send usage data to help diagnose bugs.
///If you're not okay with this, you can disable telemetry in the Settings.
///Hope you enjoy using the application!
@@ -506,7 +515,7 @@ public static string FirstRunText {
}
///
- /// Recherche une chaîne localisée semblable à Welcome to Stylophone!.
+ /// Looks up a localized string similar to Welcome to Stylophone!.
///
public static string FirstRunTitle {
get {
@@ -515,7 +524,7 @@ public static string FirstRunTitle {
}
///
- /// Recherche une chaîne localisée semblable à Close all open folders.
+ /// Looks up a localized string similar to Close all open folders.
///
public static string FoldersCollapseAll {
get {
@@ -524,7 +533,7 @@ public static string FoldersCollapseAll {
}
///
- /// Recherche une chaîne localisée semblable à Folders.
+ /// Looks up a localized string similar to Folders.
///
public static string FoldersHeader {
get {
@@ -533,7 +542,7 @@ public static string FoldersHeader {
}
///
- /// Recherche une chaîne localisée semblable à Loading....
+ /// Looks up a localized string similar to Loading....
///
public static string FoldersLoadingTreeItem {
get {
@@ -542,7 +551,7 @@ public static string FoldersLoadingTreeItem {
}
///
- /// Recherche une chaîne localisée semblable à MPD Filesystem.
+ /// Looks up a localized string similar to MPD Filesystem.
///
public static string FoldersRoot {
get {
@@ -551,7 +560,7 @@ public static string FoldersRoot {
}
///
- /// Recherche une chaîne localisée semblable à Library.
+ /// Looks up a localized string similar to Library.
///
public static string LibraryHeader {
get {
@@ -560,7 +569,7 @@ public static string LibraryHeader {
}
///
- /// Recherche une chaîne localisée semblable à Search.
+ /// Looks up a localized string similar to Search.
///
public static string LibrarySearchPlaceholder {
get {
@@ -569,7 +578,7 @@ public static string LibrarySearchPlaceholder {
}
///
- /// Recherche une chaîne localisée semblable à Local Volume.
+ /// Looks up a localized string similar to Local Volume.
///
public static string LocalVolumeHeader {
get {
@@ -578,7 +587,7 @@ public static string LocalVolumeHeader {
}
///
- /// Recherche une chaîne localisée semblable à No.
+ /// Looks up a localized string similar to No.
///
public static string NoButtonText {
get {
@@ -587,7 +596,7 @@ public static string NoButtonText {
}
///
- /// Recherche une chaîne localisée semblable à Added to Playlist {0}!.
+ /// Looks up a localized string similar to Added to Playlist {0}!.
///
public static string NotificationAddedToPlaylist {
get {
@@ -596,7 +605,7 @@ public static string NotificationAddedToPlaylist {
}
///
- /// Recherche une chaîne localisée semblable à Added to Queue!.
+ /// Looks up a localized string similar to Added to Queue!.
///
public static string NotificationAddedToQueue {
get {
@@ -605,7 +614,7 @@ public static string NotificationAddedToQueue {
}
///
- /// Recherche une chaîne localisée semblable à Album art cache has been deleted..
+ /// Looks up a localized string similar to Album art cache has been deleted..
///
public static string NotificationCacheDeleted {
get {
@@ -614,7 +623,7 @@ public static string NotificationCacheDeleted {
}
///
- /// Recherche une chaîne localisée semblable à The database is already being updated..
+ /// Looks up a localized string similar to The database is already being updated..
///
public static string NotificationDbAlreadyUpdating {
get {
@@ -623,7 +632,7 @@ public static string NotificationDbAlreadyUpdating {
}
///
- /// Recherche une chaîne localisée semblable à Database update started..
+ /// Looks up a localized string similar to Database update started..
///
public static string NotificationDbUpdateStarted {
get {
@@ -632,7 +641,7 @@ public static string NotificationDbUpdateStarted {
}
///
- /// Recherche une chaîne localisée semblable à No Track is currently playing..
+ /// Looks up a localized string similar to No Track is currently playing..
///
public static string NotificationNoTrackPlaying {
get {
@@ -641,7 +650,7 @@ public static string NotificationNoTrackPlaying {
}
///
- /// Recherche une chaîne localisée semblable à No tracks loaded yet..
+ /// Looks up a localized string similar to No tracks loaded yet..
///
public static string NotificationNoTracksLoaded {
get {
@@ -650,7 +659,7 @@ public static string NotificationNoTracksLoaded {
}
///
- /// Recherche une chaîne localisée semblable à Now Playing {0}.
+ /// Looks up a localized string similar to Now Playing {0}.
///
public static string NotificationNowPlayingTrack {
get {
@@ -659,7 +668,7 @@ public static string NotificationNowPlayingTrack {
}
///
- /// Recherche une chaîne localisée semblable à The Playlist has been removed..
+ /// Looks up a localized string similar to The Playlist has been removed..
///
public static string NotificationPlaylistRemoved {
get {
@@ -668,7 +677,7 @@ public static string NotificationPlaylistRemoved {
}
///
- /// Recherche une chaîne localisée semblable à Off.
+ /// Looks up a localized string similar to Off.
///
public static string OffText {
get {
@@ -677,7 +686,7 @@ public static string OffText {
}
///
- /// Recherche une chaîne localisée semblable à OK.
+ /// Looks up a localized string similar to OK.
///
public static string OKButtonText {
get {
@@ -686,7 +695,7 @@ public static string OKButtonText {
}
///
- /// Recherche une chaîne localisée semblable à On.
+ /// Looks up a localized string similar to On.
///
public static string OnText {
get {
@@ -695,7 +704,7 @@ public static string OnText {
}
///
- /// Recherche une chaîne localisée semblable à Up Next:.
+ /// Looks up a localized string similar to Up Next:.
///
public static string PlaybackUpNext {
get {
@@ -704,7 +713,7 @@ public static string PlaybackUpNext {
}
///
- /// Recherche une chaîne localisée semblable à Playlists.
+ /// Looks up a localized string similar to Playlists.
///
public static string PlaylistsHeader {
get {
@@ -713,7 +722,7 @@ public static string PlaylistsHeader {
}
///
- /// Recherche une chaîne localisée semblable à Queue.
+ /// Looks up a localized string similar to Queue.
///
public static string QueueHeader {
get {
@@ -722,7 +731,7 @@ public static string QueueHeader {
}
///
- /// Recherche une chaîne localisée semblable à Pick some Songs.
+ /// Looks up a localized string similar to Pick some Songs.
///
public static string RandomTracksHeader {
get {
@@ -731,7 +740,7 @@ public static string RandomTracksHeader {
}
///
- /// Recherche une chaîne localisée semblable à Shuffling through your library....
+ /// Looks up a localized string similar to Shuffling through your library....
///
public static string RandomTracksInProgress {
get {
@@ -740,7 +749,7 @@ public static string RandomTracksInProgress {
}
///
- /// Recherche une chaîne localisée semblable à Thanks for using Stylophone! Would you like to rate the app on the Store?
+ /// Looks up a localized string similar to Thanks for using Stylophone! Would you like to rate the app on the Store?
///(We won't ask again. 🙏).
///
public static string RateAppPromptText {
@@ -750,7 +759,7 @@ public static string RateAppPromptText {
}
///
- /// Recherche une chaîne localisée semblable à Rate the Application.
+ /// Looks up a localized string similar to Rate the Application.
///
public static string RateAppPromptTitle {
get {
@@ -759,7 +768,7 @@ public static string RateAppPromptTitle {
}
///
- /// Recherche une chaîne localisée semblable à Albums.
+ /// Looks up a localized string similar to Albums.
///
public static string SearchAlbumsToggle {
get {
@@ -768,7 +777,7 @@ public static string SearchAlbumsToggle {
}
///
- /// Recherche une chaîne localisée semblable à Artists.
+ /// Looks up a localized string similar to Artists.
///
public static string SearchArtistsToggle {
get {
@@ -777,7 +786,7 @@ public static string SearchArtistsToggle {
}
///
- /// Recherche une chaîne localisée semblable à Search for "{0}" on the server....
+ /// Looks up a localized string similar to Search for "{0}" on the server....
///
public static string SearchGoToDetail {
get {
@@ -786,7 +795,7 @@ public static string SearchGoToDetail {
}
///
- /// Recherche une chaîne localisée semblable à No Results have been found....
+ /// Looks up a localized string similar to No Results have been found....
///
public static string SearchNoResultsTitle_Text {
get {
@@ -795,7 +804,7 @@ public static string SearchNoResultsTitle_Text {
}
///
- /// Recherche une chaîne localisée semblable à Search Tracks....
+ /// Looks up a localized string similar to Search Tracks....
///
public static string SearchPlaceholderText {
get {
@@ -804,7 +813,7 @@ public static string SearchPlaceholderText {
}
///
- /// Recherche une chaîne localisée semblable à Search Results for "{0}".
+ /// Looks up a localized string similar to Search Results for "{0}".
///
public static string SearchResultsFor {
get {
@@ -813,7 +822,7 @@ public static string SearchResultsFor {
}
///
- /// Recherche une chaîne localisée semblable à Tracks.
+ /// Looks up a localized string similar to Tracks.
///
public static string SearchTracksToggle {
get {
@@ -822,7 +831,7 @@ public static string SearchTracksToggle {
}
///
- /// Recherche une chaîne localisée semblable à About this application.
+ /// Looks up a localized string similar to About this application.
///
public static string SettingsAbout {
get {
@@ -831,7 +840,7 @@ public static string SettingsAbout {
}
///
- /// Recherche une chaîne localisée semblable à A pretty cool MPD Client. Uses MpcNET..
+ /// Looks up a localized string similar to A pretty cool MPD Client. Uses MpcNET..
///
public static string SettingsAboutText {
get {
@@ -840,7 +849,7 @@ public static string SettingsAboutText {
}
///
- /// Recherche une chaîne localisée semblable à Download Album Art from the MPD Server.
+ /// Looks up a localized string similar to Download Album Art from the MPD Server.
///
public static string SettingsAlbumArt {
get {
@@ -849,7 +858,7 @@ public static string SettingsAlbumArt {
}
///
- /// Recherche une chaîne localisée semblable à Stylophone stores album art locally to avoid overloading your MPD Server..
+ /// Looks up a localized string similar to Stylophone stores album art locally to avoid overloading your MPD Server..
///
public static string SettingsAlbumArtText {
get {
@@ -858,7 +867,7 @@ public static string SettingsAlbumArtText {
}
///
- /// Recherche une chaîne localisée semblable à Analytics.
+ /// Looks up a localized string similar to Analytics.
///
public static string SettingsAnalytics {
get {
@@ -867,7 +876,7 @@ public static string SettingsAnalytics {
}
///
- /// Recherche une chaîne localisée semblable à Allow Stylophone to send crash and analytics reports.
+ /// Looks up a localized string similar to Allow Stylophone to send crash and analytics reports.
///
public static string SettingsAnalyticsText {
get {
@@ -876,7 +885,7 @@ public static string SettingsAnalyticsText {
}
///
- /// Recherche une chaîne localisée semblable à This setting will apply after restarting the app..
+ /// Looks up a localized string similar to This setting will apply after restarting the app..
///
public static string SettingsApplyOnRestart {
get {
@@ -885,7 +894,7 @@ public static string SettingsApplyOnRestart {
}
///
- /// Recherche une chaîne localisée semblable à Clear Cache.
+ /// Looks up a localized string similar to Clear Cache.
///
public static string SettingsClearCache {
get {
@@ -894,7 +903,7 @@ public static string SettingsClearCache {
}
///
- /// Recherche une chaîne localisée semblable à Clear the local album art cache.
+ /// Looks up a localized string similar to Clear the local album art cache.
///
public static string SettingsClearCacheDescription {
get {
@@ -903,7 +912,7 @@ public static string SettingsClearCacheDescription {
}
///
- /// Recherche une chaîne localisée semblable à Personalization.
+ /// Looks up a localized string similar to Personalization.
///
public static string SettingsCustomization {
get {
@@ -912,7 +921,7 @@ public static string SettingsCustomization {
}
///
- /// Recherche une chaîne localisée semblable à Database and Album Art.
+ /// Looks up a localized string similar to Database and Album Art.
///
public static string SettingsDatabase {
get {
@@ -921,7 +930,7 @@ public static string SettingsDatabase {
}
///
- /// Recherche une chaîne localisée semblable à Source Code, License and Privacy Statement.
+ /// Looks up a localized string similar to Source Code, License and Privacy Statement.
///
public static string SettingsGithub {
get {
@@ -930,7 +939,7 @@ public static string SettingsGithub {
}
///
- /// Recherche une chaîne localisée semblable à https://github.com/Difegue/Stylophone.
+ /// Looks up a localized string similar to https://github.com/Difegue/Stylophone.
///
public static string SettingsGithubLink {
get {
@@ -939,7 +948,7 @@ public static string SettingsGithubLink {
}
///
- /// Recherche une chaîne localisée semblable à Settings.
+ /// Looks up a localized string similar to Settings.
///
public static string SettingsHeader {
get {
@@ -948,7 +957,7 @@ public static string SettingsHeader {
}
///
- /// Recherche une chaîne localisée semblable à Local Playback available.
+ /// Looks up a localized string similar to Local Playback available.
///
public static string SettingsLocalPlaybackAvailable {
get {
@@ -957,7 +966,7 @@ public static string SettingsLocalPlaybackAvailable {
}
///
- /// Recherche une chaîne localisée semblable à Local Playback.
+ /// Looks up a localized string similar to Local Playback.
///
public static string SettingsLocalPlaybackHeader {
get {
@@ -966,7 +975,7 @@ public static string SettingsLocalPlaybackHeader {
}
///
- /// Recherche une chaîne localisée semblable à Stylophone can play your MPD Server's music stream.
+ /// Looks up a localized string similar to Stylophone can play your MPD Server's music stream.
///Enabling this option will show a second volume slider to control local volume..
///
public static string SettingsLocalPlaybackText {
@@ -976,7 +985,7 @@ public static string SettingsLocalPlaybackText {
}
///
- /// Recherche une chaîne localisée semblable à Couldn't find an MPD server at this address!.
+ /// Looks up a localized string similar to Couldn't find an MPD server at this address!.
///
public static string SettingsNoServerError {
get {
@@ -985,7 +994,7 @@ public static string SettingsNoServerError {
}
///
- /// Recherche une chaîne localisée semblable à MPD Server.
+ /// Looks up a localized string similar to MPD Server.
///
public static string SettingsServer {
get {
@@ -994,7 +1003,7 @@ public static string SettingsServer {
}
///
- /// Recherche une chaîne localisée semblable à Hostname.
+ /// Looks up a localized string similar to Hostname.
///
public static string SettingsServerHost {
get {
@@ -1003,7 +1012,7 @@ public static string SettingsServerHost {
}
///
- /// Recherche une chaîne localisée semblable à Password.
+ /// Looks up a localized string similar to Password.
///
public static string SettingsServerPassword {
get {
@@ -1012,7 +1021,7 @@ public static string SettingsServerPassword {
}
///
- /// Recherche une chaîne localisée semblable à Port.
+ /// Looks up a localized string similar to Port.
///
public static string SettingsServerPort {
get {
@@ -1021,7 +1030,7 @@ public static string SettingsServerPort {
}
///
- /// Recherche une chaîne localisée semblable à Theme.
+ /// Looks up a localized string similar to Theme.
///
public static string SettingsTheme {
get {
@@ -1030,7 +1039,7 @@ public static string SettingsTheme {
}
///
- /// Recherche une chaîne localisée semblable à Dark.
+ /// Looks up a localized string similar to Dark.
///
public static string SettingsThemeDark {
get {
@@ -1039,7 +1048,7 @@ public static string SettingsThemeDark {
}
///
- /// Recherche une chaîne localisée semblable à System default.
+ /// Looks up a localized string similar to System default.
///
public static string SettingsThemeDefault {
get {
@@ -1048,7 +1057,7 @@ public static string SettingsThemeDefault {
}
///
- /// Recherche une chaîne localisée semblable à Light.
+ /// Looks up a localized string similar to Light.
///
public static string SettingsThemeLight {
get {
@@ -1057,7 +1066,7 @@ public static string SettingsThemeLight {
}
///
- /// Recherche une chaîne localisée semblable à UI Density.
+ /// Looks up a localized string similar to UI Density.
///
public static string SettingsUIDensity {
get {
@@ -1066,7 +1075,7 @@ public static string SettingsUIDensity {
}
///
- /// Recherche une chaîne localisée semblable à Compact.
+ /// Looks up a localized string similar to Compact.
///
public static string SettingsUIDensityCompact {
get {
@@ -1075,7 +1084,7 @@ public static string SettingsUIDensityCompact {
}
///
- /// Recherche une chaîne localisée semblable à Normal.
+ /// Looks up a localized string similar to Normal.
///
public static string SettingsUIDensityNormal {
get {
@@ -1084,7 +1093,7 @@ public static string SettingsUIDensityNormal {
}
///
- /// Recherche une chaîne localisée semblable à Update.
+ /// Looks up a localized string similar to Update.
///
public static string SettingsUpdateDatabase {
get {
@@ -1093,7 +1102,7 @@ public static string SettingsUpdateDatabase {
}
///
- /// Recherche une chaîne localisée semblable à This operation might take the server some time to complete..
+ /// Looks up a localized string similar to This operation might take the server some time to complete..
///
public static string SettingsUpdateDbDesc {
get {
@@ -1102,7 +1111,7 @@ public static string SettingsUpdateDbDesc {
}
///
- /// Recherche une chaîne localisée semblable à Update the MPD Server Database.
+ /// Looks up a localized string similar to Update the MPD Server Database.
///
public static string SettingsUpdateDbTitle {
get {
@@ -1111,7 +1120,16 @@ public static string SettingsUpdateDbTitle {
}
///
- /// Recherche une chaîne localisée semblable à Yes.
+ /// Looks up a localized string similar to Song playback.
+ ///
+ public static string SongPlaybackLabel {
+ get {
+ return ResourceManager.GetString("SongPlaybackLabel", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Yes.
///
public static string YesButtonText {
get {
diff --git a/Sources/Stylophone.Localization/Strings/Resources.en-US.resx b/Sources/Stylophone.Localization/Strings/Resources.en-US.resx
index b3419349..50ac5315 100644
--- a/Sources/Stylophone.Localization/Strings/Resources.en-US.resx
+++ b/Sources/Stylophone.Localization/Strings/Resources.en-US.resx
@@ -482,4 +482,10 @@ Enabling this option will show a second volume slider to control local volume.
Updating Playlists failed
+
+ Open Fullscreen Playback
+
+
+ Song playback
+
\ No newline at end of file
diff --git a/Sources/Stylophone.Localization/Strings/Resources.fr-FR.resx b/Sources/Stylophone.Localization/Strings/Resources.fr-FR.resx
index eb9d656e..2820b28c 100644
--- a/Sources/Stylophone.Localization/Strings/Resources.fr-FR.resx
+++ b/Sources/Stylophone.Localization/Strings/Resources.fr-FR.resx
@@ -481,4 +481,10 @@ L'activation de cette option affichera un second slider pour contrôler le volum
La mise à jour des playlists a échoué
+
+ Ouvrir le lecteur plein écran
+
+
+ Lecture de la piste
+
\ No newline at end of file
diff --git a/Sources/Stylophone.Localization/Strings/Resources.pt-PT.resx b/Sources/Stylophone.Localization/Strings/Resources.pt-PT.resx
index 92c13b3b..135d070a 100644
--- a/Sources/Stylophone.Localization/Strings/Resources.pt-PT.resx
+++ b/Sources/Stylophone.Localization/Strings/Resources.pt-PT.resx
@@ -478,4 +478,10 @@ Ativando esta opção mostrará um segundo deslizador de volume para controlar o
Não foi possível enviar {0}
+
+ Mostrar Leitor
+
+
+ Leitura da faixa
+
\ No newline at end of file
diff --git a/Sources/Stylophone.Localization/Strings/Resources.resx b/Sources/Stylophone.Localization/Strings/Resources.resx
index a5fe00b4..b18cecb7 100644
--- a/Sources/Stylophone.Localization/Strings/Resources.resx
+++ b/Sources/Stylophone.Localization/Strings/Resources.resx
@@ -482,4 +482,10 @@ Enabling this option will show a second volume slider to control local volume.
Updating Playlists failed
+
+ Open Fullscreen Playback
+
+
+ Song playback
+
\ No newline at end of file
diff --git a/Sources/Stylophone.iOS/Helpers/LocalizedLabel.cs b/Sources/Stylophone.iOS/Helpers/LocalizedLabel.cs
index 34151f95..9825ae35 100644
--- a/Sources/Stylophone.iOS/Helpers/LocalizedLabel.cs
+++ b/Sources/Stylophone.iOS/Helpers/LocalizedLabel.cs
@@ -26,10 +26,10 @@ public override void AwakeFromNib()
// Use the text set in IB to find the matching property.
// Set the identifier in "User Defined Runtime Attributes".
- var prop = typeof(Resources).GetProperty(stringIdentifier ?? "AppDisplayName");
+ var identifier = stringIdentifier ?? "AppDisplayName";
// Get the property value to have the localized string.
- Text = prop.GetValue(null, null).ToString();
+ Text = Resources.ResourceManager.GetString(identifier);
}
}
}
diff --git a/Sources/Stylophone.iOS/Helpers/NSValueConverters.cs b/Sources/Stylophone.iOS/Helpers/NSValueConverters.cs
index fd850591..039e324b 100644
--- a/Sources/Stylophone.iOS/Helpers/NSValueConverters.cs
+++ b/Sources/Stylophone.iOS/Helpers/NSValueConverters.cs
@@ -147,4 +147,34 @@ public override NSObject ReverseTransformedValue(NSObject value)
return NSNumber.FromInt32(result);
}
}
+
+ [Register(nameof(TrackToStringValueTransformer))]
+ public class TrackToStringValueTransformer : NSValueTransformer
+ {
+ public static new Class TransformedValueClass => new NSString().Class;
+ public static new bool AllowsReverseTransformation => false;
+
+ public override NSObject TransformedValue(NSObject value)
+ {
+ var text = "";
+
+ if (value is NSWrapper wrap)
+ {
+ var trackVm = (TrackViewModel)wrap.ManagedObject;
+ text = trackVm.ToString();
+ }
+
+ return new NSString(text);
+ }
+
+ public override NSObject ReverseTransformedValue(NSObject value)
+ {
+ int result = 0;
+
+ if (value is NSString s)
+ int.TryParse(s, out result);
+
+ return NSNumber.FromInt32(result);
+ }
+ }
}
diff --git a/Sources/Stylophone.iOS/Helpers/PropertyBinder.cs b/Sources/Stylophone.iOS/Helpers/PropertyBinder.cs
index 2d11df33..4ab0e174 100644
--- a/Sources/Stylophone.iOS/Helpers/PropertyBinder.cs
+++ b/Sources/Stylophone.iOS/Helpers/PropertyBinder.cs
@@ -13,8 +13,9 @@ namespace Stylophone.iOS.Helpers
public class PropertyBinder: IDisposable
where TObservable : ObservableObject
{
- private readonly Dictionary> _bindings = new Dictionary>();
- private readonly Dictionary _observers = new Dictionary();
+ private readonly Dictionary> _bindings = new();
+ private readonly Dictionary _observers = new();
+ private readonly Dictionary _buttonBindings = new();
private TObservable _observableObject;
public PropertyBinder(TObservable viewModel)
@@ -31,13 +32,22 @@ public void Dispose()
// Dispose our observers
foreach (IDisposable observer in _observers.Values)
observer.Dispose();
+
+ // Unregister our button bindings
+ foreach (var kvp in _buttonBindings)
+ kvp.Key.PrimaryActionTriggered -= kvp.Value;
}
// Shorthand method to attach a command to a UIButton.
public void BindButton(UIButton button, string buttonText, ICommand command, object parameter = null)
{
button.SetTitle(buttonText, UIControlState.Normal);
- button.PrimaryActionTriggered += (s, e) => command.Execute(parameter);
+
+ // Record button/eventhandler association so we can unregister them when the binder is disposed
+ var evtHandler = new EventHandler((s, e) => command.Execute(parameter));
+ button.PrimaryActionTriggered += evtHandler;
+
+ _buttonBindings.Add(button, evtHandler);
}
public UIAction GetCommandAction(string actionText, string systemImage, ICommand command, object parameter = null)
diff --git a/Sources/Stylophone.iOS/Helpers/TrackTableViewDataSource.cs b/Sources/Stylophone.iOS/Helpers/TrackTableViewDataSource.cs
index d0a8de7f..3bd07597 100644
--- a/Sources/Stylophone.iOS/Helpers/TrackTableViewDataSource.cs
+++ b/Sources/Stylophone.iOS/Helpers/TrackTableViewDataSource.cs
@@ -21,7 +21,9 @@ public class TrackTableViewDataSource : UITableViewDelegate, IUITableViewDataSou
private Func _menuFactory;
private Func _swipeFactory;
private Action _scrollHandler;
+ private Action _primaryAction;
private ObservableCollection _sourceCollection;
+ private bool _canReorder;
public TrackTableViewDataSource(IntPtr handle) : base(handle)
{
@@ -34,21 +36,26 @@ public TrackTableViewDataSource(IntPtr handle) : base(handle)
/// The source TrackViewModels
/// A factory for row context menus
/// A factory for row swipe actions
- /// Whether you can select multiple rows
+ /// Whether you can reorder items
/// Optional scrollHandler
+ /// Optional primary action
public TrackTableViewDataSource(UITableView tableView, ObservableCollection source,
Func contextMenuFactory, Func swipeActionFactory,
- bool canSelectRows = false, Action scrollHandler = null)
+ bool canReorder = false, Action scrollHandler = null, Action primaryAction = null)
{
_tableView = tableView;
_sourceCollection = source;
_menuFactory = contextMenuFactory;
_swipeFactory = swipeActionFactory;
+ _canReorder = canReorder;
_scrollHandler = scrollHandler;
+ _primaryAction = primaryAction;
_sourceCollection.CollectionChanged += (s,e) => UIApplication.SharedApplication.InvokeOnMainThread(
() => UpdateUITableView(s,e));
- _tableView.AllowsMultipleSelection = canSelectRows;
+
+ //_tableView.AllowsMultipleSelectionDuringEditing = _canReorder;
+ //_tableView.AllowsMultipleSelection = canSelectRows;
}
private void UpdateUITableView(object sender, NotifyCollectionChangedEventArgs e)
@@ -74,7 +81,9 @@ private void UpdateUITableView(object sender, NotifyCollectionChangedEventArgs e
if (e.Action == NotifyCollectionChangedAction.Remove)
{
- for (var i = e.OldStartingIndex; i < e.OldStartingIndex + e.OldItems.Count; i++)
+ var startIndex = e.OldStartingIndex;
+
+ for (var i = startIndex; i < startIndex + e.OldItems.Count; i++)
indexPaths.Add(NSIndexPath.FromItemSection(i, 0));
_tableView.DeleteRows(indexPaths.ToArray(), UITableViewRowAnimation.Right);
@@ -86,6 +95,9 @@ private void UpdateUITableView(object sender, NotifyCollectionChangedEventArgs e
#region DataSource
+ [Export("tableView:canMoveRowAtIndexPath:")]
+ public bool CanMoveRow(UITableView tableView, NSIndexPath indexPath) => _canReorder;
+
public nint RowsInSection(UITableView tableView, nint section)
{
return _sourceCollection.Count;
@@ -113,10 +125,20 @@ public UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
return cell;
}
+ [Export("tableView:moveRowAtIndexPath:toIndexPath:")]
+ public void MoveRow(UITableView tableView, NSIndexPath sourceIndexPath, NSIndexPath destinationIndexPath)
+ {
+ _sourceCollection.Move(sourceIndexPath.Row, destinationIndexPath.Row);
+ }
+
#endregion
#region Delegate
+ // If multiselect isn't enabled, this will show a delete icon on the left side of the cells
+ public override UITableViewCellEditingStyle EditingStyleForRow(UITableView tableView, NSIndexPath indexPath)
+ => UITableViewCellEditingStyle.Delete;
+
public override void Scrolled(UIScrollView scrollView)
{
_scrollHandler?.Invoke(scrollView);
@@ -124,14 +146,20 @@ public override void Scrolled(UIScrollView scrollView)
public override void RowSelected(UITableView tableView, NSIndexPath indexPath)
{
- if (tableView.AllowsMultipleSelection)
- tableView.CellAt(indexPath).Accessory = UITableViewCellAccessory.Checkmark;
+ //if (tableView.Editing)
+ // tableView.CellAt(indexPath).Accessory = UITableViewCellAccessory.Checkmark;
}
public override void RowDeselected(UITableView tableView, NSIndexPath indexPath)
{
- if (tableView.AllowsMultipleSelection)
- tableView.CellAt(indexPath).Accessory = UITableViewCellAccessory.None;
+ //if (tableView.Editing)
+ // tableView.CellAt(indexPath).Accessory = UITableViewCellAccessory.None;
+ }
+
+ public override void PerformPrimaryAction(UITableView tableView, NSIndexPath rowIndexPath)
+ {
+ if (!tableView.Editing)
+ _primaryAction?.Invoke(rowIndexPath);
}
public override UIContextMenuConfiguration GetContextMenuConfiguration(UITableView tableView, NSIndexPath indexPath, CoreGraphics.CGPoint point)
diff --git a/Sources/Stylophone.iOS/Helpers/UICircularProgressView.cs b/Sources/Stylophone.iOS/Helpers/UICircularProgressView.cs
index d248dccd..b77f1134 100644
--- a/Sources/Stylophone.iOS/Helpers/UICircularProgressView.cs
+++ b/Sources/Stylophone.iOS/Helpers/UICircularProgressView.cs
@@ -20,7 +20,7 @@ public UICircularProgressView (IntPtr handle) : base (handle)
private CAShapeLayer _backgroundCircle = new CAShapeLayer();
private CAShapeLayer _progressCircle = new CAShapeLayer();
- private UIColor _backgroundCircleColor = UIColor.SystemGray4Color;
+ private UIColor _backgroundCircleColor = UIColor.SystemGray4;
[Export(nameof(BackgroundCircleColor))]
public UIColor BackgroundCircleColor
{
diff --git a/Sources/Stylophone.iOS/Info.plist b/Sources/Stylophone.iOS/Info.plist
index a8042133..eeebfbeb 100644
--- a/Sources/Stylophone.iOS/Info.plist
+++ b/Sources/Stylophone.iOS/Info.plist
@@ -16,13 +16,13 @@
zh
CFBundleShortVersionString
- 2.5.4
+ 2.6.0
CFBundleVersion
- 2.5.4
+ 2.6.0
LSRequiresIPhoneOS
MinimumOSVersion
- 15.0
+ 16.0
UIBackgroundModes
audio
diff --git a/Sources/Stylophone.iOS/Properties/AssemblyInfo.cs b/Sources/Stylophone.iOS/Properties/AssemblyInfo.cs
deleted file mode 100644
index 63e95fbd..00000000
--- a/Sources/Stylophone.iOS/Properties/AssemblyInfo.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-using System.Reflection;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-
-// General Information about an assembly is controlled through the following
-// set of attributes. Change these attribute values to modify the information
-// associated with an assembly.
-[assembly: AssemblyTitle("Stylophone.iOS")]
-[assembly: AssemblyDescription("")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("Stylophone.iOS")]
-[assembly: AssemblyCopyright("Copyright © 2017")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
-
-// Setting ComVisible to false makes the types in this assembly not visible
-// to COM components. If you need to access a type in this assembly from
-// COM, set the ComVisible attribute to true on that type.
-[assembly: ComVisible(false)]
-
-// The following GUID is for the ID of the typelib if this project is exposed to COM
-[assembly: Guid("50c7b8c9-e664-45af-b88e-0c9b8b9c1be1")]
-
-// Version information for an assembly consists of the following four values:
-//
-// Major Version
-// Minor Version
-// Build Number
-// Revision
-//
-// You can specify all the values or you can default the Build and Revision Numbers
-// by using the '*' as shown below:
-// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.0.0")]
-[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Sources/Stylophone.iOS/Services/DialogService.cs b/Sources/Stylophone.iOS/Services/DialogService.cs
index aad8b33a..55d3ee35 100644
--- a/Sources/Stylophone.iOS/Services/DialogService.cs
+++ b/Sources/Stylophone.iOS/Services/DialogService.cs
@@ -39,9 +39,11 @@ public async Task ShowAddToPlaylistDialog(bool allowExistingPlaylists =
var dialog = new AddToPlaylistViewController(_mpdService, allowExistingPlaylists);
var navigationController = new UINavigationController(dialog);
- // Specify medium detent for the NavigationController's presentation
+ // Custom size detent since the addToPlaylist view is quite small
UISheetPresentationController uspc = (UISheetPresentationController)navigationController.PresentationController;
- uspc.Detents = new [] { UISheetPresentationControllerDetent.CreateMediumDetent() };
+
+ var detent = UISheetPresentationControllerDetent.Create(null, (context) => 240);
+ uspc.Detents = new [] { detent };
UIApplication.SharedApplication.KeyWindow.RootViewController.PresentViewController(navigationController, true, null);
diff --git a/Sources/Stylophone.iOS/Services/NavigationService.cs b/Sources/Stylophone.iOS/Services/NavigationService.cs
index 91c00bd0..3b7a4db9 100644
--- a/Sources/Stylophone.iOS/Services/NavigationService.cs
+++ b/Sources/Stylophone.iOS/Services/NavigationService.cs
@@ -29,7 +29,7 @@ public NavigationService()
_viewControllers = new List();
}
- public override bool CanGoBack => NavigationController.ViewControllers?.Count() > 1;
+ public override bool CanGoBack => NavigationController.ViewControllers?.Length > 1;
public override Type CurrentPageViewModelType => _viewModelToStoryboardDictionary.Keys.Where(
k => _viewModelToStoryboardDictionary[k] == NavigationController.VisibleViewController.Storyboard).FirstOrDefault();
@@ -73,14 +73,22 @@ public override void NavigateImplementation(Type viewmodelType, object parameter
{
viewController = viewControllerLoaded.First();
(viewController as IPreparableViewController)?.Prepare(parameter);
- NavigationController.PushViewController(viewController, true);
+
+ if (NavigationController.ViewControllers.Length == 0)
+ NavigationController.ViewControllers = new UIViewController[] { viewController };
+ else
+ NavigationController.PushViewController(viewController, true);
}
else // This is truly new, load the VC from scratch
{
viewController = storyboard.InstantiateInitialViewController();
(viewController as IPreparableViewController)?.Prepare(parameter);
- NavigationController.PushViewController(viewController, true);
_viewControllers.Add(viewController);
+
+ if (NavigationController.ViewControllers.Length == 0)
+ NavigationController.ViewControllers = new UIViewController[] { viewController };
+ else
+ NavigationController.PushViewController(viewController, true);
}
_lastParamUsed = parameter;
diff --git a/Sources/Stylophone.iOS/Services/NotificationService.cs b/Sources/Stylophone.iOS/Services/NotificationService.cs
index 819567c5..3e83d5b4 100644
--- a/Sources/Stylophone.iOS/Services/NotificationService.cs
+++ b/Sources/Stylophone.iOS/Services/NotificationService.cs
@@ -1,14 +1,13 @@
using System;
using UserNotifications;
using Stylophone.Common.Interfaces;
-using UIKit;
-using Xam.RMessage;
namespace Stylophone.iOS.Services
{
public class NotificationService : NotificationServiceBase
{
private IDispatcherService _dispatcherService;
+ private Timer _notificationTimer;
public NotificationService(IDispatcherService dispatcherService)
{
@@ -17,22 +16,30 @@ public NotificationService(IDispatcherService dispatcherService)
public override void ShowInAppNotification(InAppNotification notification)
{
+ if (notification.NotificationType == NotificationType.Error)
+ {
+ // Let's just use alerts until TipKit is available... This is cheap but w/e
+ var alert = new UIAlertView(notification.NotificationTitle, notification.NotificationText, null, "Ok");
+ alert.Show();
+ return;
+ }
+
+ var rootVc = (UIApplication.SharedApplication.Delegate as AppDelegate).RootViewController;
+
UIApplication.SharedApplication.InvokeOnMainThread(() =>
{
if (UIApplication.SharedApplication.ApplicationState != UIApplicationState.Active)
return;
- RMessageType type = notification.NotificationType switch
- {
- NotificationType.Info => RMessageType.Normal,
- NotificationType.Warning => RMessageType.Warning,
- NotificationType.Error => RMessageType.Error,
- _ => RMessageType.Normal
- };
-
- RMessage.ShowNotificationWithTitle(notification.NotificationTitle, notification.NotificationText, type, "",
- notification.NotificationType == NotificationType.Error ? 300000 : 2, () => { });
+ var popover = new NotificationPopoverViewController(notification.NotificationTitle, rootVc);
+
+ rootVc.PresentViewController(popover, true, null);
});
+
+ _notificationTimer?.Dispose();
+ _notificationTimer = new Timer((_) => UIApplication.SharedApplication.InvokeOnMainThread(() =>
+ rootVc.DismissViewController(true, null)),
+ null, 2000, Timeout.Infinite);
}
public override void ShowBasicToastNotification(string title, string description)
@@ -51,4 +58,58 @@ public override void ShowBasicToastNotification(string title, string description
});
}
}
+
+ public class NotificationPopoverViewController : UIViewController, IUIPopoverPresentationControllerDelegate
+ {
+ private string _text;
+
+ public NotificationPopoverViewController(string text, UISplitViewController rootVc) //, CGRect sourceBounds, CGSize contentSize)
+ {
+ _text = text;
+
+ ModalPresentationStyle = UIModalPresentationStyle.Popover;
+
+ PopoverPresentationController.SourceView = rootVc.View;
+ PopoverPresentationController.Delegate = this;
+ PopoverPresentationController.PermittedArrowDirections = UIPopoverArrowDirection.Left;
+ PopoverPresentationController.SourceRect = new CGRect(16, 64, 1, 1);
+ }
+
+ public override void LoadView()
+ {
+ base.LoadView();
+
+ PreferredContentSize = new CGSize(196, 54);
+
+ var stackView = new UIStackView
+ {
+ Axis = UILayoutConstraintAxis.Horizontal,
+ Distribution = UIStackViewDistribution.FillProportionally,
+ Spacing = 32
+ };
+
+ stackView.AddArrangedSubview(new UIView());
+ stackView.AddArrangedSubview(new UILabel
+ {
+ Text = _text,
+ Font = UIFont.PreferredHeadline,
+ AdjustsFontSizeToFitWidth = true,
+ Lines = 2,
+ });
+ stackView.AddArrangedSubview(new UIView());
+
+ View = stackView;
+
+ }
+
+ [Export("adaptivePresentationStyleForPresentationController:traitCollection:")]
+ public UIModalPresentationStyle GetAdaptivePresentationStyle(UIPresentationController controller, UITraitCollection traitCollection)
+ {
+ // Prevent popover from being adaptive fullscreen on phones
+ // (https://pspdfkit.com/blog/2022/presenting-popovers-on-iphone-with-swiftui/)
+ return UIModalPresentationStyle.None;
+ }
+
+ }
+
}
diff --git a/Sources/Stylophone.iOS/Stylophone.iOS.csproj b/Sources/Stylophone.iOS/Stylophone.iOS.csproj
index 87fcb187..3889d5c1 100644
--- a/Sources/Stylophone.iOS/Stylophone.iOS.csproj
+++ b/Sources/Stylophone.iOS/Stylophone.iOS.csproj
@@ -1,264 +1,80 @@
-
-
+
- Debug
- iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}
- {FEACFBD2-3405-455C-9665-78FE426C6842};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
- {e1087329-5912-47eb-bd6a-19b74ccb7863}
+ net7.0-ios
Exe
- Stylophone.iOS
- Resources
- Stylophone.iOS
- true
- NSUrlSessionHandler
- PackageReference
- automatic
+ enable
+ true
+ 16.0
-
- true
- full
- false
- bin\iPhoneSimulator\Debug
- DEBUG
- prompt
- 4
- x86_64
- None
- true
- 9.0
- iOS Team Provisioning Profile: com.tvc-16.Stylophone
- iPhone Developer
+
+ false
-
- none
- true
- bin\iPhoneSimulator\Release
- prompt
- 4
- None
- x86_64
- 9.0
- Automatique
- iPhone Developer
- true
-
-
- true
- full
- false
- bin\iPhone\Debug
- DEBUG
- prompt
- 4
- ARM64
- Entitlements.plist
- iPhone Developer
- true
- SdkOnly
- 9.0
-
-
- none
- true
- bin\iPhone\Release
- prompt
- 4
- Entitlements.plist
- ARM64
- iPhone Developer
- SdkOnly
- 9.0
- true
+
+ false
-
-
-
-
-
-
-
+
SidebarViewController.cs
-
-
-
-
-
-
-
-
-
+
SettingsViewController.cs
-
-
-
-
-
-
-
-
+
QueueViewController.cs
-
-
+
PlaybackViewController.cs
-
-
+
TrackViewCell.cs
-
-
+
CompactPlaybackView.cs
-
-
-
+
UICircularProgressView.cs
-
-
-
+
AlbumDetailViewController.cs
-
-
+
FoldersViewController.cs
-
-
+
FilePathCell.cs
-
-
+
AlbumCollectionViewCell.cs
-
-
+
LibraryViewController.cs
-
-
-
+
SearchResultsViewController.cs
-
-
-
-
+
PlaylistViewController.cs
-
-
-
+
SymbolUIButton.cs
-
-
-
-
-
-
-
-
-
-
-
- false
-
-
- false
-
-
- false
-
-
- false
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 2.88.1
-
-
- 6.0.0
-
-
- 4.5.3
-
-
- 4.5.3
-
-
- 0.0.1
-
-
- 1.0.0
-
-
- 3.3.17
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
- {83B15D0E-C82F-4438-8290-0DFC23A6498F}
- Stylophone.Common
- {3DEDF3EF-8DD2-4E20-B057-21DF5E342230}
- Stylophone.Localization
-
\ No newline at end of file
diff --git a/Sources/Stylophone.iOS/ViewControllers/AddToPlaylistViewController.cs b/Sources/Stylophone.iOS/ViewControllers/AddToPlaylistViewController.cs
index 53fb9daa..c96149bc 100644
--- a/Sources/Stylophone.iOS/ViewControllers/AddToPlaylistViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/AddToPlaylistViewController.cs
@@ -27,7 +27,6 @@ public AddToPlaylistViewController(MPDConnectionService mpdService, bool allowEx
Playlists = new ObservableCollection(mpdService.Playlists);
- Title = Strings.AddToPlaylistTitle;
ModalPresentationStyle = UIModalPresentationStyle.FormSheet;
ModalTransitionStyle = UIModalTransitionStyle.CoverVertical;
@@ -58,10 +57,10 @@ public override void LoadView()
{
base.LoadView();
- PreferredContentSize = new CGSize(512, 368);
+ PreferredContentSize = new CGSize(512, 196);
var stackView = new UIStackView {
- BackgroundColor = UIColor.SystemBackgroundColor,
+ BackgroundColor = UIColor.SystemBackground,
Axis = UILayoutConstraintAxis.Vertical,
Alignment = UIStackViewAlignment.Center,
Distribution = UIStackViewDistribution.Fill,
@@ -70,7 +69,10 @@ public override void LoadView()
var playlistPicker = new UIPickerView();
playlistPicker.DataSource = this;
- playlistPicker.Delegate = this;
+ playlistPicker.Delegate = this;
+
+ var newPlaylistLabel = new UILabel { Text = Strings.AddToPlaylistText, Font = UIFont.PreferredTitle2 };
+ newPlaylistLabel.Hidden = AllowExistingPlaylists;
var newPlaylistTextField = new UITextField { Placeholder = Strings.AddToPlaylistNewPlaylistName, BorderStyle = UITextBorderStyle.RoundedRect };
newPlaylistTextField.EditingChanged += (s, e) => PlaylistName = newPlaylistTextField.Text;
@@ -85,16 +87,18 @@ public override void LoadView()
AddNewPlaylist = playlistSwitch.SelectedSegment == 1;
playlistPicker.Hidden = AddNewPlaylist;
newPlaylistTextField.Hidden = !AddNewPlaylist;
+ newPlaylistLabel.Hidden = !AddNewPlaylist;
};
if (AllowExistingPlaylists)
- stackView.AddArrangedSubview(playlistSwitch);
-
- //stackView.AddArrangedSubview(new UILabel { Text = Strings.AddToPlaylistText, Font = UIFont.PreferredTitle2 });
+ NavigationItem.TitleView = playlistSwitch;
+ else
+ Title = Strings.AddToPlaylistCreateNewPlaylist;
if (AllowExistingPlaylists)
stackView.AddArrangedSubview(playlistPicker);
-
+
+ stackView.AddArrangedSubview(newPlaylistLabel);
stackView.AddArrangedSubview(newPlaylistTextField);
var spacerView = new UIView();
@@ -104,6 +108,7 @@ public override void LoadView()
var constraints = new List();
constraints.Add(newPlaylistTextField.WidthAnchor.ConstraintEqualTo(View.WidthAnchor, 0.8F));
+ constraints.Add(playlistPicker.HeightAnchor.ConstraintLessThanOrEqualTo(196));
NSLayoutConstraint.ActivateConstraints(constraints.ToArray());
}
diff --git a/Sources/Stylophone.iOS/ViewControllers/AlbumDetailViewController.cs b/Sources/Stylophone.iOS/ViewControllers/AlbumDetailViewController.cs
index 0f19babd..1c49a151 100644
--- a/Sources/Stylophone.iOS/ViewControllers/AlbumDetailViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/AlbumDetailViewController.cs
@@ -34,16 +34,21 @@ public override void AwakeFromNib()
TraitCollectionDidChange(null);
NavigationItem.LargeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Never;
- var trackDataSource = new TrackTableViewDataSource(TableView, ViewModel.Source, GetRowContextMenu, GetRowSwipeActions, true, OnScroll);
+ var trackDataSource = new TrackTableViewDataSource(TableView, ViewModel.Source,
+ GetRowContextMenu, GetRowSwipeActions, false, OnScroll, OnTap);
TableView.DataSource = trackDataSource;
TableView.Delegate = trackDataSource;
+ TableView.SelfSizingInvalidation = UITableViewSelfSizingInvalidation.EnabledIncludingConstraints;
- Binder.Bind(EmptyView, "hidden", nameof(ViewModel.IsSourceEmpty),
+ Binder.Bind(EmptyView, "hidden", nameof(ViewModel.IsSourceEmpty),
valueTransformer: NSValueTransformer.GetValueTransformer(nameof(ReverseBoolValueTransformer)));
Binder.Bind(AlbumTrackInfo, "text", nameof(ViewModel.PlaylistInfo));
}
- private void OnScroll(UIScrollView scrollView)
+ private void OnTap(NSIndexPath indexPath) =>
+ ViewModel.AddToQueueCommand.Execute(new List { ViewModel?.Source[indexPath.Row] });
+
+ private void OnScroll(UIScrollView scrollView)
{
if (scrollView.ContentOffset.Y > 192)
{
diff --git a/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs b/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs
index 662b91ab..571e9c59 100644
--- a/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs
@@ -10,11 +10,9 @@
using Stylophone.Common.ViewModels;
using Stylophone.iOS.Helpers;
using Stylophone.iOS.ViewModels;
-using UIKit;
-using Pop = ARSPopover.iOS;
-using static Xamarin.Essentials.Permissions;
using CommunityToolkit.Mvvm.Input;
using CoreGraphics;
+using UIKit;
namespace Stylophone.iOS.ViewControllers
{
@@ -53,6 +51,7 @@ public override void AwakeFromNib()
// Bind
var negateBoolTransformer = NSValueTransformer.GetValueTransformer(nameof(ReverseBoolValueTransformer));
var intToStringTransformer = NSValueTransformer.GetValueTransformer(nameof(IntToStringValueTransformer));
+ var trackToStringTransformer = NSValueTransformer.GetValueTransformer(nameof(TrackToStringValueTransformer));
// Compact View Binding
Binder.Bind(CompactView, "hidden", nameof(ViewModel.IsTrackInfoAvailable), valueTransformer: negateBoolTransformer);
@@ -62,7 +61,12 @@ public override void AwakeFromNib()
CompactView.PlayPauseButton.PrimaryActionTriggered += (s, e) => ViewModel.ChangePlaybackState();
CompactView.ShuffleButton.PrimaryActionTriggered += (s, e) => ViewModel.ToggleShuffle();
+ CompactView.VolumeButton.AccessibilityLabel = Strings.ActionChangeVolume;
+ CompactView.ShuffleButton.AccessibilityLabel = Strings.ActionToggleShuffle;
+
CompactView.OpenFullScreenButton.PrimaryActionTriggered += (s, e) => ViewModel.NavigateNowPlaying();
+ CompactView.OpenFullScreenButton.AccessibilityLabel = Strings.ActionFullscreenPlayback;
+ Binder.Bind(CompactView.OpenFullScreenButton, "accessibilityValue", nameof(ViewModel.CurrentTrack), valueTransformer: trackToStringTransformer);
// Volume Popover Binding
LocalPlaybackBinder.Bind(LocalPlaybackView, "hidden", nameof(ViewModel.LocalPlayback.IsEnabled), valueTransformer: negateBoolTransformer);
@@ -70,12 +74,13 @@ public override void AwakeFromNib()
LocalMuteButton.PrimaryActionTriggered += (s, e) => ViewModel.LocalPlayback.ToggleMute();
LocalPlaybackBinder.Bind(LocalVolumeSlider, "value", nameof(ViewModel.LocalPlayback.Volume), true);
LocalPlaybackBinder.Bind(LocalVolume, "text", nameof(ViewModel.LocalPlayback.Volume), valueTransformer: intToStringTransformer);
+ LocalVolumeSlider.AccessibilityLabel = Strings.LocalVolumeHeader;
ServerMuteButton.PrimaryActionTriggered += (s, e) => ViewModel.ToggleMute();
Binder.Bind(ServerVolumeSlider, "value", nameof(ViewModel.MediaVolume), true);
Binder.Bind(ServerVolumeSlider, "enabled", nameof(ViewModel.CanSetVolume));
Binder.Bind(ServerVolume, "text", nameof(ViewModel.MediaVolume), valueTransformer: intToStringTransformer);
-
+ ServerVolumeSlider.AccessibilityLabel = Strings.ActionChangeVolume;
}
public override void ViewWillAppear(bool animated)
@@ -98,23 +103,17 @@ public override void ViewDidLayoutSubviews()
if (!ViewModel.IsFullScreen) return;
- // Don't multi-line for small phones in vertical orientation
+ // Different multi-line behavior depending on size classes
if (TraitCollection.HorizontalSizeClass == UIUserInterfaceSizeClass.Compact &&
TraitCollection.VerticalSizeClass == UIUserInterfaceSizeClass.Regular &&
UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone)
{
TrackTitle.Lines = 2;
- AlbumName.Lines = 1;
-
}
else
{
- TrackTitle.Lines = 3;
-
- if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone)
- AlbumName.Lines = 1;
- else
- AlbumName.Lines = 2;
+ if (UIDevice.CurrentDevice.UserInterfaceIdiom != UIUserInterfaceIdiom.Phone)
+ TrackTitle.Lines = 3;
}
// On compact widths, change the application tintcolor, as that's what is used instead of the navigation bar's
@@ -136,6 +135,7 @@ public override void ViewDidLoad()
{
base.ViewDidLoad();
+ TrackSlider.AccessibilityLabel = Strings.SongPlaybackLabel;
TrackSlider.TouchDragInside += (s, e) =>
{
ViewModel.TimeListened = Miscellaneous.FormatTimeString(TrackSlider.Value * 1000);
@@ -144,6 +144,7 @@ public override void ViewDidLoad()
TrackSlider.ValueChanged += (s, e) =>
{
ViewModel.OnPlayingSliderChange();
+ TrackSlider.AccessibilityValue = ViewModel.TimeListened;
};
var upNextTransformer = NSValueTransformer.GetValueTransformer(nameof(NextTrackToStringValueTransformer));
@@ -153,6 +154,8 @@ public override void ViewDidLoad()
Binder.Bind(RemainingTime, "text", nameof(ViewModel.TimeRemaining));
Binder.Bind(TrackSlider, "value", nameof(ViewModel.CurrentTimeValue), true);
Binder.Bind(TrackSlider, "maximumValue", nameof(ViewModel.MaxTimeValue));
+ Binder.Bind(TrackSlider, "accessibilityValue", nameof(ViewModel.TimeListened));
+
Binder.Bind(upNextView, "text", nameof(ViewModel.NextTrack), valueTransformer:upNextTransformer);
UpdateFullView(ViewModel.CurrentTrack);
@@ -162,6 +165,10 @@ public override void ViewDidLoad()
UpdateButton(RepeatButton, ViewModel.RepeatIcon);
UpdateButton(ShuffleButton, ViewModel.IsShuffleEnabled ? "shuffle.circle.fill" : "shuffle.circle");
+ VolumeButton.AccessibilityLabel = Strings.ActionChangeVolume;
+ ShuffleButton.AccessibilityLabel = Strings.ActionToggleShuffle;
+ RepeatButton.AccessibilityLabel = Strings.ActionToggleRepeat;
+
SkipPrevButton.PrimaryActionTriggered += (s, e) => ViewModel.SkipPrevious();
SkipNextButton.PrimaryActionTriggered += (s, e) => ViewModel.SkipNext();
PlayPauseButton.PrimaryActionTriggered += (s, e) => ViewModel.ChangePlaybackState();
@@ -271,17 +278,9 @@ private UIBarButtonItem CreateSettingsButton()
public void ShowVolumePopover(UIButton sourceButton, UIViewController sourceVc = null)
{
var sourceBounds = sourceButton.ImageView.Bounds;
+ var size = ViewModel.LocalPlayback.IsEnabled ? new CGSize(276, 176) : new CGSize(276, 96);
- var popover = new Pop.ARSPopover
- {
- SourceView = sourceButton.ImageView,
- SourceRect = new CoreGraphics.CGRect(sourceBounds.Width/2, -4, 0, 0),
- ContentSize = ViewModel.LocalPlayback.IsEnabled ?
- new CoreGraphics.CGSize(276, 176) : new CoreGraphics.CGSize(276, 96),
- ArrowDirection = UIPopoverArrowDirection.Down
- };
-
- popover.View.AddSubview(VolumePopover);
+ var popover = new VolumePopoverViewController(VolumePopover, sourceButton, sourceBounds, size);
if (sourceVc == null)
sourceVc = this;
@@ -289,4 +288,36 @@ public void ShowVolumePopover(UIButton sourceButton, UIViewController sourceVc =
sourceVc.PresentViewController(popover, true, null);
}
}
+
+ public class VolumePopoverViewController: UIViewController, IUIPopoverPresentationControllerDelegate
+ {
+ private UIView _volumeView;
+
+ public VolumePopoverViewController(UIView view, UIButton sourceButton, CGRect sourceBounds, CGSize contentSize)
+ {
+ _volumeView = view;
+
+ ModalPresentationStyle = UIModalPresentationStyle.Popover;
+ PopoverPresentationController.SourceItem = sourceButton.ImageView;
+ PopoverPresentationController.Delegate = this;
+ PopoverPresentationController.PermittedArrowDirections = UIPopoverArrowDirection.Down;
+ PopoverPresentationController.SourceRect = new CGRect(sourceBounds.Width / 2, -4, 0, 0);
+
+ PreferredContentSize = contentSize;
+ }
+
+ [Export("adaptivePresentationStyleForPresentationController:traitCollection:")]
+ public UIModalPresentationStyle GetAdaptivePresentationStyle(UIPresentationController controller, UITraitCollection traitCollection)
+ {
+ // Prevent popover from being adaptive fullscreen on phones
+ // (https://pspdfkit.com/blog/2022/presenting-popovers-on-iphone-with-swiftui/)
+ return UIModalPresentationStyle.None;
+ }
+
+ public override void ViewDidLoad()
+ {
+ base.ViewDidLoad();
+ View.AddSubview(_volumeView);
+ }
+ }
}
diff --git a/Sources/Stylophone.iOS/ViewControllers/PlaylistViewController.cs b/Sources/Stylophone.iOS/ViewControllers/PlaylistViewController.cs
index d05e7858..3ddbfde3 100644
--- a/Sources/Stylophone.iOS/ViewControllers/PlaylistViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/PlaylistViewController.cs
@@ -59,7 +59,8 @@ public override void AwakeFromNib()
_settingsBtn = CreateSettingsButton();
- var trackDataSource = new TrackTableViewDataSource(TableView, ViewModel.Source, GetRowContextMenu, GetRowSwipeActions, true, OnScroll);
+ var trackDataSource = new TrackTableViewDataSource(TableView, ViewModel.Source,
+ GetRowContextMenu, GetRowSwipeActions, true, OnScroll, OnTap);
TableView.DataSource = trackDataSource;
TableView.Delegate = trackDataSource;
@@ -95,6 +96,8 @@ public override void AwakeFromNib()
ArtContainer.Layer.ShadowOpacity = 0.5F;
ArtContainer.Layer.ShadowOffset = new CGSize(0, 0);
ArtContainer.Layer.ShadowRadius = 4;
+
+ NavigationItem.RightBarButtonItems = new UIBarButtonItem[] { EditButtonItem };
}
public override void ViewWillDisappear(bool animated)
@@ -103,17 +106,20 @@ public override void ViewWillDisappear(bool animated)
ViewModel.Dispose();
}
+ private void OnTap(NSIndexPath indexPath) =>
+ ViewModel.AddToQueueCommand.Execute(new List { ViewModel?.Source[indexPath.Row] });
+
private void OnScroll(UIScrollView scrollView)
{
if (scrollView.ContentOffset.Y > 192)
{
Title = ViewModel?.Name;
- NavigationItem.RightBarButtonItem = _settingsBtn;
+ NavigationItem.RightBarButtonItems = new UIBarButtonItem[] { _settingsBtn, EditButtonItem };
}
else
{
Title = "";
- NavigationItem.RightBarButtonItem = null;
+ NavigationItem.RightBarButtonItems = new UIBarButtonItem[] { EditButtonItem };
}
}
diff --git a/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs b/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs
index ec61bf55..e979284e 100644
--- a/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs
@@ -11,7 +11,6 @@
using UIKit;
using System.Collections.Generic;
using System.ComponentModel;
-using System.Threading.Tasks;
namespace Stylophone.iOS.ViewControllers
{
@@ -39,7 +38,7 @@ public override void AwakeFromNib()
private void OnLeavingBackground(object sender, EventArgs e)
{
if (_mpdService.IsConnected)
- Task.Run(async () => await ViewModel.LoadInitialDataAsync());
+ Task.Run(ViewModel.LoadInitialDataAsync);
}
public override void ViewDidLoad()
@@ -52,15 +51,20 @@ public override void ViewDidLoad()
Binder.Bind(EmptyView, "hidden", nameof(ViewModel.IsSourceEmpty),
valueTransformer: negateBoolTransformer);
- NavigationItem.RightBarButtonItem = CreateSettingsButton();
+ NavigationItem.RightBarButtonItems = new UIBarButtonItem[] { CreateSettingsButton(), EditButtonItem };
- var trackDataSource = new TrackTableViewDataSource(TableView, ViewModel.Source, GetRowContextMenu, GetRowSwipeActions);
+ var trackDataSource = new TrackTableViewDataSource(TableView, ViewModel.Source,
+ GetRowContextMenu, GetRowSwipeActions, true, primaryAction:OnTap);
TableView.DataSource = trackDataSource;
TableView.Delegate = trackDataSource;
+ TableView.SelfSizingInvalidation = UITableViewSelfSizingInvalidation.EnabledIncludingConstraints;
_mpdService.SongChanged += ScrollToPlayingSong;
}
+ private void OnTap(NSIndexPath indexPath) =>
+ ViewModel.PlayTrackCommand.Execute(new List { ViewModel?.Source[indexPath.Row] });
+
private void UpdateListOnPlaylistVersionChange(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(ViewModel.PlaylistVersion))
@@ -74,21 +78,24 @@ private void ScrollToPlayingSong(object sender = null, SongChangedEventArgs e =
// Scroll to currently playing song
var playing = ViewModel.Source.Where(t => t.IsPlaying).FirstOrDefault();
- if (playing != null)
- UIApplication.SharedApplication.BeginInvokeOnMainThread(() =>
+ if (playing == null)
+ return;
+
+ UIApplication.SharedApplication.BeginInvokeOnMainThread(() =>
+ {
+ try
+ {
+ var indexPath = NSIndexPath.FromRowSection(ViewModel.Source.IndexOf(playing), 0);
+ var tableViewRows = TableView.NumberOfRowsInSection(0);
+
+ if (tableViewRows >= indexPath.Row)
+ TableView.ScrollToRow(indexPath, UITableViewScrollPosition.Middle, true);
+ }
+ catch (Exception e)
{
- try
- {
- var indexPath = NSIndexPath.FromRowSection(ViewModel.Source.IndexOf(playing), 0);
- var tableViewRows = TableView.NumberOfRowsInSection(0);
-
- if (tableViewRows >= indexPath.Row)
- TableView.ScrollToRow(indexPath, UITableViewScrollPosition.Middle, true);
- } catch (Exception e)
- {
- System.Diagnostics.Debug.WriteLine($"Error while scrolling to row: {e}");
- }
- });
+ System.Diagnostics.Debug.WriteLine($"Error while scrolling to row: {e}");
+ }
+ });
}
private UIMenu GetRowContextMenu(NSIndexPath indexPath)
diff --git a/Sources/Stylophone.iOS/ViewControllers/SearchResultsViewController.cs b/Sources/Stylophone.iOS/ViewControllers/SearchResultsViewController.cs
index 65e773bc..9034ea7a 100644
--- a/Sources/Stylophone.iOS/ViewControllers/SearchResultsViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/SearchResultsViewController.cs
@@ -46,7 +46,8 @@ public override void ViewDidLoad()
valueTransformer: negateBoolTransformer);
- var trackDataSource = new TrackTableViewDataSource(TableView, ViewModel.Source, GetRowContextMenu, GetRowSwipeActions);
+ var trackDataSource = new TrackTableViewDataSource(TableView, ViewModel.Source,
+ GetRowContextMenu, GetRowSwipeActions, primaryAction: OnTap);
TableView.DataSource = trackDataSource;
TableView.Delegate = trackDataSource;
@@ -58,6 +59,9 @@ public override void ViewDidLoad()
SearchSegmentedControl.PrimaryActionTriggered += SearchSegmentedControl_PrimaryActionTriggered;
}
+ private void OnTap(NSIndexPath indexPath) =>
+ ViewModel.AddToQueueCommand.Execute(new List { ViewModel?.Source[indexPath.Row] });
+
private void SearchSegmentedControl_PrimaryActionTriggered(object sender, EventArgs e)
{
switch (SearchSegmentedControl.SelectedSegment)
diff --git a/Sources/Stylophone.iOS/ViewControllers/SubViews/TrackViewCell.cs b/Sources/Stylophone.iOS/ViewControllers/SubViews/TrackViewCell.cs
index a3308e66..217a8083 100644
--- a/Sources/Stylophone.iOS/ViewControllers/SubViews/TrackViewCell.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/SubViews/TrackViewCell.cs
@@ -35,7 +35,7 @@ public override void LayoutSubviews()
internal void Configure(int row, TrackViewModel trackViewModel)
{
// Set background depending on the row number
- BackgroundColor = (row % 2 == 0) ? UIColor.SystemGray6Color : UIColor.Clear;
+ BackgroundColor = (row % 2 == 0) ? UIColor.SystemGray6 : UIColor.Clear;
// Bind trackData
_trackViewModel = trackViewModel;
diff --git a/Sources/Stylophone.iOS/ViewModels/ShellViewModel.cs b/Sources/Stylophone.iOS/ViewModels/ShellViewModel.cs
index dd497c62..9e562345 100644
--- a/Sources/Stylophone.iOS/ViewModels/ShellViewModel.cs
+++ b/Sources/Stylophone.iOS/ViewModels/ShellViewModel.cs
@@ -97,7 +97,7 @@ protected override void Navigate(object itemInvoked)
{
if (itemInvoked is string s)
{
- _navigationService.Navigate();
+ Navigate(typeof(SettingsViewModel));
return;
}
@@ -114,11 +114,21 @@ protected override void Navigate(object itemInvoked)
// Playlist items navigate with their name as parameter
if (pageType == typeof(PlaylistViewModel))
- _navigationService.Navigate(pageType, sidebarItem.Title);
+ Navigate(pageType, sidebarItem.Title);
else
- _navigationService.Navigate(pageType);
+ Navigate(pageType);
}
}
}
+
+ private void Navigate(Type viewModel, object parameter = null)
+ {
+ // On phones, since the sidebar is its own screen,
+ // we don't want to keep the root view controller(usually the queue) when navigating from the sidebar.
+ if (UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Phone)
+ ((NavigationService)_navigationService).NavigationController.SetViewControllers(new UIViewController[0], false);
+
+ _navigationService.Navigate(viewModel, parameter);
+ }
}
}
diff --git a/Sources/Stylophone.iOS/Views/AlbumDetail.storyboard b/Sources/Stylophone.iOS/Views/AlbumDetail.storyboard
index 2b5092c5..122955b1 100644
--- a/Sources/Stylophone.iOS/Views/AlbumDetail.storyboard
+++ b/Sources/Stylophone.iOS/Views/AlbumDetail.storyboard
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,25 +12,25 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
-
+
+
@@ -42,13 +42,13 @@
-
+
-
-
+
+
@@ -72,7 +72,7 @@
-
+
@@ -85,7 +85,7 @@
-
+
@@ -98,10 +98,10 @@
-
+
-
+
@@ -124,14 +124,14 @@
-
-
+
+
-
+
@@ -145,7 +145,7 @@
-
+
@@ -163,10 +163,11 @@
-
+
+
@@ -174,19 +175,17 @@
+
-
-
+
+
-
-
-
@@ -247,11 +246,11 @@
-
+
-
+
@@ -262,17 +261,17 @@
-
+
-
+
-
+
-
-
+
+
@@ -282,23 +281,35 @@
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
@@ -309,8 +320,18 @@
-
+
+
+
+
+
+
+
+
+
+
+
@@ -357,9 +378,9 @@
-
+
-
+
diff --git a/Sources/Stylophone.iOS/Views/NowPlaying.storyboard b/Sources/Stylophone.iOS/Views/NowPlaying.storyboard
index 03699eb8..0cf141e2 100644
--- a/Sources/Stylophone.iOS/Views/NowPlaying.storyboard
+++ b/Sources/Stylophone.iOS/Views/NowPlaying.storyboard
@@ -1,8 +1,10 @@
-
-
+
+
+
+
-
+
@@ -16,332 +18,366 @@
-
-
+
+
-
+
-
+
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
+
+
-
+
+
-
+
+
-
+
-
-
+
+
-
+
-
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
@@ -383,20 +419,20 @@
-
-
+
+
-
+
-
+
@@ -455,6 +491,7 @@
+
@@ -493,7 +530,7 @@
-
+
@@ -511,7 +548,7 @@
-
+
@@ -540,7 +577,9 @@
-
+
+
+
@@ -667,7 +706,7 @@
-
+
@@ -676,10 +715,10 @@
-
+
-
+
@@ -689,7 +728,7 @@
-
+
@@ -712,10 +751,10 @@
-
+
-
+
@@ -724,10 +763,10 @@
-
+
-
+
@@ -737,7 +776,7 @@
-
+
@@ -760,7 +799,7 @@
-
+
@@ -768,22 +807,19 @@
-
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
+
diff --git a/Sources/Stylophone.iOS/Views/Playlist.storyboard b/Sources/Stylophone.iOS/Views/Playlist.storyboard
index 4ead2beb..6a9ebd64 100644
--- a/Sources/Stylophone.iOS/Views/Playlist.storyboard
+++ b/Sources/Stylophone.iOS/Views/Playlist.storyboard
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,25 +12,25 @@
-
-
+
+
-
+
-
+
-
+
-
+
-
-
+
+
@@ -42,13 +42,13 @@
-
+
-
-
+
+
@@ -72,7 +72,7 @@
-
+
@@ -85,7 +85,7 @@
-
+
@@ -98,11 +98,13 @@
-
+
-
+
+
+
-
+
@@ -122,14 +124,14 @@
-
-
+
+
-
+
@@ -143,7 +145,9 @@
-
+
+
+
@@ -157,11 +161,10 @@
-
-
-
-
+
+
+
@@ -178,19 +181,15 @@
-
-
+
+
-
-
-
-
@@ -199,7 +198,6 @@
-
@@ -219,7 +217,6 @@
-
@@ -236,7 +233,7 @@
-
+
@@ -247,11 +244,11 @@
-
+
-
+
@@ -262,17 +259,17 @@
-
+
-
+
-
+
-
-
+
+
@@ -282,31 +279,35 @@
-
+
-
+
-
+
+
+
-
+
-
+
+
+
-
+
@@ -317,8 +318,18 @@
-
+
+
+
+
+
+
+
+
+
+
+
@@ -359,16 +370,16 @@
-
+
-
+
-
-
+
+
diff --git a/Sources/Stylophone.iOS/Views/Queue.storyboard b/Sources/Stylophone.iOS/Views/Queue.storyboard
index ed9a2041..42036a5e 100644
--- a/Sources/Stylophone.iOS/Views/Queue.storyboard
+++ b/Sources/Stylophone.iOS/Views/Queue.storyboard
@@ -1,8 +1,8 @@
-
-
+
+
-
+
@@ -12,21 +12,21 @@
-
+
-
+
-
+
-
+
@@ -35,7 +35,7 @@
-
+
@@ -55,18 +55,18 @@
-
-
+
+
-
+
-
+
-
-
+
+
@@ -76,14 +76,14 @@
-
+
-
+
@@ -94,15 +94,17 @@
-
+
-
+
+
+
-
+
@@ -113,8 +115,18 @@
-
+
+
+
+
+
+
+
+
+
+
+
@@ -149,8 +161,8 @@
-
-
+
+
diff --git a/Sources/Stylophone.iOS/Views/SearchResults.storyboard b/Sources/Stylophone.iOS/Views/SearchResults.storyboard
index fe48b876..098df16e 100644
--- a/Sources/Stylophone.iOS/Views/SearchResults.storyboard
+++ b/Sources/Stylophone.iOS/Views/SearchResults.storyboard
@@ -1,8 +1,8 @@
-
+
-
+
@@ -37,14 +37,14 @@
-
+
-
+
@@ -107,7 +107,7 @@
-
+
@@ -127,7 +127,7 @@
-
+
@@ -147,7 +147,9 @@
-
+
+
+
@@ -164,8 +166,18 @@
-
+
+
+
+
+
+
+
+
+
+
+
@@ -202,8 +214,8 @@
-
-
+
+
diff --git a/Sources/Stylophone.sln b/Sources/Stylophone.sln
index 14a645d8..6f9bc7fb 100644
--- a/Sources/Stylophone.sln
+++ b/Sources/Stylophone.sln
@@ -9,7 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stylophone.Common", "Stylop
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stylophone.Localization", "Stylophone.Localization\Stylophone.Localization.csproj", "{3DEDF3EF-8DD2-4E20-B057-21DF5E342230}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylophone.iOS", "Stylophone.iOS\Stylophone.iOS.csproj", "{A437E83D-67F2-410A-99DB-6B6D03AA4130}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stylophone.iOS", "Stylophone.iOS\Stylophone.iOS.csproj", "{EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -179,48 +179,48 @@ Global
{3DEDF3EF-8DD2-4E20-B057-21DF5E342230}.Release-Stable|x64.Build.0 = Release|Any CPU
{3DEDF3EF-8DD2-4E20-B057-21DF5E342230}.Release-Stable|x86.ActiveCfg = Release|Any CPU
{3DEDF3EF-8DD2-4E20-B057-21DF5E342230}.Release-Stable|x86.Build.0 = Release|Any CPU
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|Any CPU.Build.0 = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|ARM.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|ARM.Build.0 = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|ARM64.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|ARM64.Build.0 = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|iPhone.ActiveCfg = Debug|iPhone
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|iPhone.Build.0 = Debug|iPhone
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|x64.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|x64.Build.0 = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|x86.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Debug|x86.Build.0 = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|Any CPU.ActiveCfg = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|Any CPU.Build.0 = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|ARM.ActiveCfg = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|ARM.Build.0 = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|ARM64.ActiveCfg = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|ARM64.Build.0 = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|iPhone.ActiveCfg = Release|iPhone
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|iPhone.Build.0 = Release|iPhone
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|x64.ActiveCfg = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|x64.Build.0 = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|x86.ActiveCfg = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release|x86.Build.0 = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|Any CPU.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|Any CPU.Build.0 = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|ARM.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|ARM.Build.0 = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|ARM64.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|ARM64.Build.0 = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|iPhone.ActiveCfg = Release|iPhone
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|iPhone.Build.0 = Release|iPhone
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|iPhoneSimulator.Build.0 = Release|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|x64.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|x64.Build.0 = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|x86.ActiveCfg = Debug|iPhoneSimulator
- {A437E83D-67F2-410A-99DB-6B6D03AA4130}.Release-Stable|x86.Build.0 = Debug|iPhoneSimulator
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|ARM.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|ARM.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|ARM64.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|ARM64.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|iPhone.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|iPhone.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|x64.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Debug|x86.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|ARM.ActiveCfg = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|ARM.Build.0 = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|ARM64.ActiveCfg = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|ARM64.Build.0 = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|iPhone.ActiveCfg = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|iPhone.Build.0 = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|iPhoneSimulator.Build.0 = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|x64.ActiveCfg = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|x64.Build.0 = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|x86.ActiveCfg = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release|x86.Build.0 = Release|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|Any CPU.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|ARM.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|ARM.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|ARM64.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|ARM64.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|iPhone.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|iPhone.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|iPhoneSimulator.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|iPhoneSimulator.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|x64.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|x64.Build.0 = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|x86.ActiveCfg = Debug|Any CPU
+ {EF817BEC-A839-4BAD-8E48-75B42A3EDAC4}.Release-Stable|x86.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Sources/Stylophone/App.xaml.cs b/Sources/Stylophone/App.xaml.cs
index 04fafed2..4e675b10 100644
--- a/Sources/Stylophone/App.xaml.cs
+++ b/Sources/Stylophone/App.xaml.cs
@@ -16,6 +16,7 @@
using Microsoft.Toolkit.Uwp.Helpers;
using Windows.Foundation;
using Microsoft.Services.Store.Engagement;
+using Windows.ApplicationModel;
#if DEBUG
#else
using System.Collections.Generic;
@@ -40,6 +41,8 @@ public App()
InitializeComponent();
UnhandledException += OnAppUnhandledException;
+ Suspending += OnAppSuspending;
+ Resuming += OnAppResuming;
// Deferred execution until used. Check https://msdn.microsoft.com/library/dd642331(v=vs.110).aspx for further info on Lazy class.
_activationService = new Lazy(CreateActivationService);
@@ -119,6 +122,18 @@ private void OnAppUnhandledException(object sender, Windows.UI.Xaml.UnhandledExc
e.Handled = true;
}
+ private async void OnAppResuming(object sender, object e)
+ {
+ await Ioc.Default.GetRequiredService().InitializeAsync(true);
+ Ioc.Default.GetRequiredService().Initialize();
+ }
+
+ private void OnAppSuspending(object sender, SuspendingEventArgs e)
+ {
+ Ioc.Default.GetRequiredService().Disconnect();
+ Ioc.Default.GetRequiredService().Stop();
+ }
+
private ActivationService CreateActivationService()
{
return new ActivationService(this, typeof(QueueViewModel), new Lazy(CreateShell));
diff --git a/Sources/Stylophone/Assets/silence.wav b/Sources/Stylophone/Assets/silence.wav
new file mode 100644
index 00000000..7d2e290d
Binary files /dev/null and b/Sources/Stylophone/Assets/silence.wav differ
diff --git a/Sources/Stylophone/Behaviors/AlternatingListViewBehavior.cs b/Sources/Stylophone/Behaviors/AlternatingListViewBehavior.cs
new file mode 100644
index 00000000..30f0cbab
--- /dev/null
+++ b/Sources/Stylophone/Behaviors/AlternatingListViewBehavior.cs
@@ -0,0 +1,134 @@
+using Microsoft.Xaml.Interactivity;
+using System;
+using Windows.Foundation.Collections;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Controls.Primitives;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Media;
+using CommunityToolkit.WinUI;
+
+namespace Stylophone.Behaviors
+{
+ internal class AlternatingListViewBehavior : Behavior
+ {
+ public static readonly DependencyProperty AlternateBackgroundProperty = DependencyProperty.Register(
+ nameof(AlternateBackground),
+ typeof(Brush),
+ typeof(AlternatingListViewBehavior),
+ new PropertyMetadata(default(Brush)));
+
+ public static readonly DependencyProperty AlternateBorderThicknessProperty = DependencyProperty.Register(
+ nameof(AlternateBorderThickness),
+ typeof(Thickness),
+ typeof(AlternatingListViewBehavior),
+ new PropertyMetadata(default(Thickness)));
+
+ public static readonly DependencyProperty AlternateBorderBrushProperty = DependencyProperty.Register(
+ nameof(AlternateBorderBrush),
+ typeof(Brush),
+ typeof(AlternatingListViewBehavior),
+ new PropertyMetadata(default(Brush?)));
+
+ public Brush? AlternateBorderBrush
+ {
+ get => (Brush?)GetValue(AlternateBorderBrushProperty);
+ set => SetValue(AlternateBorderBrushProperty, value);
+ }
+
+ public Thickness AlternateBorderThickness
+ {
+ get => (Thickness)GetValue(AlternateBorderThicknessProperty);
+ set => SetValue(AlternateBorderThicknessProperty, value);
+ }
+
+ public Brush? AlternateBackground
+ {
+ get => (Brush?)GetValue(AlternateBackgroundProperty);
+ set => SetValue(AlternateBackgroundProperty, value);
+ }
+
+ protected override void OnAttached()
+ {
+ base.OnAttached();
+
+ AssociatedObject.ActualThemeChanged += OnActualThemeChanged;
+ AssociatedObject.ContainerContentChanging += OnContainerContentChanging;
+ if (AssociatedObject.Items != null)
+ {
+ AssociatedObject.Items.VectorChanged += ItemsOnVectorChanged;
+ }
+ }
+
+ protected override void OnDetaching()
+ {
+ base.OnDetaching();
+
+ AssociatedObject.ActualThemeChanged -= OnActualThemeChanged;
+ AssociatedObject.ContainerContentChanging -= OnContainerContentChanging;
+ if (AssociatedObject.Items != null)
+ {
+ AssociatedObject.Items.VectorChanged -= ItemsOnVectorChanged;
+ }
+ }
+
+ private void OnActualThemeChanged(FrameworkElement sender, object args)
+ {
+ if (AssociatedObject.Items == null) return;
+ for (int i = 0; i < AssociatedObject.Items.Count; i++)
+ {
+ if (AssociatedObject.ContainerFromIndex(i) is SelectorItem itemContainer)
+ {
+ UpdateAlternateLayout(itemContainer, i);
+ }
+ }
+ }
+
+ private void ItemsOnVectorChanged(IObservableVector sender, IVectorChangedEventArgs args)
+ {
+ // If the index is at the end we can ignore
+ if (args.Index == (sender.Count - 1))
+ {
+ return;
+ }
+
+ // Only need to handle Inserted and Removed because we'll handle everything else in the
+ // OnContainerContentChanging method
+ if (args.CollectionChange is CollectionChange.ItemInserted or CollectionChange.ItemRemoved)
+ {
+ for (int i = (int)args.Index; i < sender.Count; i++)
+ {
+ if (AssociatedObject.ContainerFromIndex(i) is SelectorItem itemContainer)
+ {
+ UpdateAlternateLayout(itemContainer, i);
+ }
+ }
+ }
+ }
+
+ private void OnContainerContentChanging(ListViewBase sender, ContainerContentChangingEventArgs args)
+ {
+ if (args.Phase > 0 || args.InRecycleQueue) return;
+ UpdateAlternateLayout(args.ItemContainer, args.ItemIndex);
+ }
+
+ private void UpdateAlternateLayout(SelectorItem itemContainer, int itemIndex)
+ {
+ if (itemIndex < 0 || AlternateBackground == null) return;
+ Brush evenBackground = AlternateBackground;
+ itemContainer.Background = itemIndex % 2 == 0 ? evenBackground : null;
+ if (itemContainer.FindDescendant() is not { } border) return;
+ if (itemIndex % 2 == 0)
+ {
+ border.Background = evenBackground;
+ border.BorderBrush = AlternateBorderBrush;
+ border.BorderThickness = AlternateBorderThickness;
+ }
+ else
+ {
+ border.Background = null;
+ border.BorderThickness = default;
+ }
+ }
+ }
+}
+
diff --git a/Sources/Stylophone/Behaviors/StackedNotificationsBehavior.cs b/Sources/Stylophone/Behaviors/StackedNotificationsBehavior.cs
deleted file mode 100644
index af7efe39..00000000
--- a/Sources/Stylophone/Behaviors/StackedNotificationsBehavior.cs
+++ /dev/null
@@ -1,322 +0,0 @@
-// Licensed to the .NET Foundation under one or more agreements.
-// The .NET Foundation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-// TODO: Remove this when StackedNotificationsBehavior lands in Toolkit
-
-using Microsoft.Toolkit.Uwp.UI.Behaviors;
-using Microsoft.UI.Xaml.Controls;
-using System;
-using System.Collections.Generic;
-using Windows.System;
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls.Primitives;
-using Windows.UI.Xaml.Input;
-using DQ = Windows.System.DispatcherQueue;
-
-namespace CommunityToolkit.Labs.WinUI
-{
- ///
- /// The content of a notification to display in .
- /// The , , and values will
- /// always be applied to the targeted .
- /// The , , and values
- /// will be applied only if set.
- ///
- public class Notification
- {
- private NotificationOverrides _overrides;
- private bool _isIconVisible;
- private object? _content;
- private DataTemplate? _contentTemplate;
- private ButtonBase? _actionButton;
-
- ///
- /// Gets or sets the notification title.
- ///
- public string? Title { get; set; }
-
- ///
- /// Gets or sets the notification message.
- ///
- public string? Message { get; set; }
-
- ///
- /// Gets or sets the duration of the notification.
- /// Set to null for persistent notification.
- ///
- public TimeSpan? Duration { get; set; }
-
- ///
- /// Gets or sets the type of the to apply consistent status color, icon,
- /// and assistive technology settings dependent on the criticality of the notification.
- ///
- public InfoBarSeverity Severity { get; set; }
-
- ///
- /// Gets or sets a value indicating whether the icon is visible or not.
- /// True if the icon is visible; otherwise, false. The default is true.
- ///
- public bool IsIconVisible
- {
- get => _isIconVisible;
- set
- {
- _isIconVisible = value;
- _overrides |= NotificationOverrides.Icon;
- }
- }
-
- ///
- /// Gets or sets the XAML Content that is displayed below the title and message in
- /// the InfoBar.
- ///
- public object? Content
- {
- get => _content;
- set
- {
- _content = value;
- _overrides |= NotificationOverrides.Content;
- }
- }
-
- ///
- /// Gets or sets the data template for the .
- ///
- public DataTemplate? ContentTemplate
- {
- get => _contentTemplate;
- set
- {
- _contentTemplate = value;
- _overrides |= NotificationOverrides.ContentTemplate;
- }
- }
-
- ///
- /// Gets or sets the action button of the InfoBar.
- ///
- public ButtonBase? ActionButton
- {
- get => _actionButton;
- set
- {
- _actionButton = value;
- _overrides |= NotificationOverrides.ActionButton;
- }
- }
-
- internal NotificationOverrides Overrides => _overrides;
- }
-
- ///
- /// The overrides which should be set on the notification.
- ///
- [Flags]
- internal enum NotificationOverrides
- {
- None,
- Icon,
- Content,
- ContentTemplate,
- ActionButton,
- }
-
- ///
- /// A behavior to add the stacked notification support to .
- ///
- public class StackedNotificationsBehavior : BehaviorBase
- {
- private readonly LinkedList _stackedNotifications;
- private readonly DispatcherQueueTimer _dismissTimer;
- private Notification? _currentNotification;
- private bool _initialIconVisible;
- private object? _initialContent;
- private DataTemplate? _initialContentTemplate;
- private ButtonBase? _initialActionButton;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public StackedNotificationsBehavior()
- {
- _stackedNotifications = new LinkedList();
-
- // TODO: On WinUI 3 we can use the local DispatcherQueue, so we need to abstract better for UWP
- var dispatcherQueue = DQ.GetForCurrentThread();
- _dismissTimer = dispatcherQueue.CreateTimer();
- _dismissTimer.Tick += OnTimerTick;
- }
-
- ///
- /// Show .
- ///
- /// The notification to display.
- public void Show(Notification notification)
- {
- if (notification is null)
- {
- throw new ArgumentNullException(nameof(notification));
- }
-
- _stackedNotifications.AddLast(notification);
- ShowNext();
- }
-
- ///
- /// Remove the .
- /// If the notification is displayed, it will be closed.
- /// If the notification is still in the queue, it will be removed.
- ///
- /// The notification to remove.
- public void Remove(Notification notification)
- {
- if (notification is null)
- {
- throw new ArgumentNullException(nameof(notification));
- }
-
- if (notification == _currentNotification)
- {
- // We close the notification. This will trigger the display of the next one.
- // See OnInfoBarClosed.
- AssociatedObject.IsOpen = false;
- return;
- }
-
- _stackedNotifications.Remove(notification);
- }
-
- ///
- protected override bool Initialize()
- {
- AssociatedObject.Closed += OnInfoBarClosed;
- AssociatedObject.PointerEntered += OnPointerEntered;
- AssociatedObject.PointerExited += OnPointedExited;
- return true;
- }
-
- ///
- protected override bool Uninitialize()
- {
- AssociatedObject.Closed -= OnInfoBarClosed;
- AssociatedObject.PointerEntered -= OnPointerEntered;
- AssociatedObject.PointerExited -= OnPointedExited;
- return true;
- }
-
- private void OnInfoBarClosed(InfoBar sender, InfoBarClosedEventArgs args)
- {
- // We display the next notification.
- ShowNext();
- }
-
- private void ShowNext()
- {
- if (AssociatedObject.IsOpen)
- {
- // One notification is already displayed. We wait for it to close
- return;
- }
-
- StopTimer();
- AssociatedObject.IsOpen = false;
- RestoreOverridenProperties();
-
- if (_stackedNotifications.Count == 0)
- {
- _currentNotification = null;
- return;
- }
-
- var notificationToDisplay = _stackedNotifications!.First!.Value;
- _stackedNotifications.RemoveFirst();
-
- _currentNotification = notificationToDisplay;
- SetNotification(notificationToDisplay);
- AssociatedObject.IsOpen = true;
-
- StartTimer(notificationToDisplay.Duration);
- }
-
- private void SetNotification(Notification notification)
- {
- AssociatedObject.Title = notification.Title ?? string.Empty;
- AssociatedObject.Message = notification.Message ?? string.Empty;
- AssociatedObject.Severity = notification.Severity;
-
- if (notification.Overrides.HasFlag(NotificationOverrides.Icon))
- {
- _initialIconVisible = AssociatedObject.IsIconVisible;
- AssociatedObject.IsIconVisible = notification.IsIconVisible;
- }
-
- if (notification.Overrides.HasFlag(NotificationOverrides.Content))
- {
- _initialContent = AssociatedObject.Content;
- AssociatedObject.Content = notification.Content!;
- }
-
- if (notification.Overrides.HasFlag(NotificationOverrides.ContentTemplate))
- {
- _initialContentTemplate = AssociatedObject.ContentTemplate;
- AssociatedObject.ContentTemplate = notification.ContentTemplate!;
- }
-
- if (notification.Overrides.HasFlag(NotificationOverrides.ActionButton))
- {
- _initialActionButton = AssociatedObject.ActionButton;
- AssociatedObject.ActionButton = notification.ActionButton!;
- }
- }
-
- private void RestoreOverridenProperties()
- {
- if (_currentNotification is null)
- {
- return;
- }
-
- if (_currentNotification.Overrides.HasFlag(NotificationOverrides.Icon))
- {
- AssociatedObject.IsIconVisible = _initialIconVisible;
- }
-
- if (_currentNotification.Overrides.HasFlag(NotificationOverrides.Content))
- {
- AssociatedObject.Content = _initialContent!;
- }
-
- if (_currentNotification.Overrides.HasFlag(NotificationOverrides.ContentTemplate))
- {
- AssociatedObject.ContentTemplate = _initialContentTemplate!;
- }
-
- if (_currentNotification.Overrides.HasFlag(NotificationOverrides.ActionButton))
- {
- AssociatedObject.ActionButton = _initialActionButton!;
- }
- }
-
- private void StartTimer(TimeSpan? duration)
- {
- if (duration is null)
- {
- return;
- }
-
- _dismissTimer.Interval = duration.Value;
- _dismissTimer.Start();
- }
-
- private void StopTimer() => _dismissTimer.Stop();
-
- private void OnTimerTick(DispatcherQueueTimer sender, object args) => AssociatedObject.IsOpen = false;
-
- private void OnPointedExited(object sender, PointerRoutedEventArgs e) => StartTimer(_currentNotification?.Duration);
-
- private void OnPointerEntered(object sender, PointerRoutedEventArgs e) => StopTimer();
- }
-
-}
-
diff --git a/Sources/Stylophone/Controls/SettingsBlockControl.xaml b/Sources/Stylophone/Controls/SettingsBlockControl.xaml
deleted file mode 100644
index 1f58b7ec..00000000
--- a/Sources/Stylophone/Controls/SettingsBlockControl.xaml
+++ /dev/null
@@ -1,124 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Sources/Stylophone/Controls/SettingsBlockControl.xaml.cs b/Sources/Stylophone/Controls/SettingsBlockControl.xaml.cs
deleted file mode 100644
index ccfb0a93..00000000
--- a/Sources/Stylophone/Controls/SettingsBlockControl.xaml.cs
+++ /dev/null
@@ -1,115 +0,0 @@
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Windows.UI.Xaml.Markup;
-
-namespace Stylophone.Controls
-{
- [ContentProperty(Name = nameof(SettingsActionableElement))]
- public sealed partial class SettingsBlockControl : UserControl
- {
- public FrameworkElement SettingsActionableElement { get; set; }
-
- public static readonly DependencyProperty ExpandableContentProperty = DependencyProperty.Register(
- "ExpandableContent",
- typeof(FrameworkElement),
- typeof(SettingsBlockControl),
- new PropertyMetadata(null)
- );
-
- public FrameworkElement ExpandableContent
- {
- get => (FrameworkElement)GetValue(ExpandableContentProperty);
- set => SetValue(ExpandableContentProperty, value);
- }
-
- public static readonly DependencyProperty AdditionalDescriptionContentProperty = DependencyProperty.Register(
- "AdditionalDescriptionContent",
- typeof(FrameworkElement),
- typeof(SettingsBlockControl),
- new PropertyMetadata(null)
- );
-
- public FrameworkElement AdditionalDescriptionContent
- {
- get => (FrameworkElement)GetValue(AdditionalDescriptionContentProperty);
- set => SetValue(AdditionalDescriptionContentProperty, value);
- }
-
- public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(
- "Title",
- typeof(string),
- typeof(SettingsBlockControl),
- new PropertyMetadata(null)
- );
-
- public string Title
- {
- get => (string)GetValue(TitleProperty);
- set => SetValue(TitleProperty, value);
- }
-
- public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
- "Description",
- typeof(string),
- typeof(SettingsBlockControl),
- new PropertyMetadata(null)
- );
-
- public string Description
- {
- get => (string)GetValue(DescriptionProperty);
- set => SetValue(DescriptionProperty, value);
- }
-
- public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
- "Icon",
- typeof(IconElement),
- typeof(SettingsBlockControl),
- new PropertyMetadata(null)
- );
-
- public IconElement Icon
- {
- get => (IconElement)GetValue(IconProperty);
- set => SetValue(IconProperty, value);
- }
-
- public static readonly DependencyProperty IsClickableProperty = DependencyProperty.Register(
- "IsClickable",
- typeof(bool),
- typeof(SettingsBlockControl),
- new PropertyMetadata(false)
- );
-
- public bool IsClickable
- {
- get => (bool)GetValue(IsClickableProperty);
- set => SetValue(IsClickableProperty, value);
- }
-
- //
- // Summary:
- // Occurs when a button control is clicked.
- public event RoutedEventHandler Click;
-
- public SettingsBlockControl()
- {
- this.InitializeComponent();
- }
-
- private void ActionableButton_Click(object sender, RoutedEventArgs e)
- {
- Click?.Invoke(this, e);
- }
-
- private void Expander_Expanding(Microsoft.UI.Xaml.Controls.Expander sender, Microsoft.UI.Xaml.Controls.ExpanderExpandingEventArgs args)
- {
- Click?.Invoke(this, new RoutedEventArgs());
- }
-
- private void Expander_Collapsed(Microsoft.UI.Xaml.Controls.Expander sender, Microsoft.UI.Xaml.Controls.ExpanderCollapsedEventArgs args)
- {
- Click?.Invoke(this, new RoutedEventArgs());
- }
- }
-}
diff --git a/Sources/Stylophone/Controls/SettingsDisplayControl.xaml b/Sources/Stylophone/Controls/SettingsDisplayControl.xaml
deleted file mode 100644
index b8d4a266..00000000
--- a/Sources/Stylophone/Controls/SettingsDisplayControl.xaml
+++ /dev/null
@@ -1,118 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Sources/Stylophone/Controls/SettingsDisplayControl.xaml.cs b/Sources/Stylophone/Controls/SettingsDisplayControl.xaml.cs
deleted file mode 100644
index 549f5bbc..00000000
--- a/Sources/Stylophone/Controls/SettingsDisplayControl.xaml.cs
+++ /dev/null
@@ -1,85 +0,0 @@
-using Windows.UI.Xaml;
-using Windows.UI.Xaml.Controls;
-using Windows.UI.Xaml.Markup;
-
-namespace Stylophone.Controls
-{
- [ContentProperty(Name = nameof(SettingsActionableElement))]
- public sealed partial class SettingsDisplayControl : UserControl
- {
- public FrameworkElement SettingsActionableElement { get; set; }
-
- public static readonly DependencyProperty AdditionalDescriptionContentProperty = DependencyProperty.Register(
- "AdditionalDescriptionContent",
- typeof(FrameworkElement),
- typeof(SettingsDisplayControl),
- new PropertyMetadata(null)
- );
-
- public FrameworkElement AdditionalDescriptionContent
- {
- get => (FrameworkElement)GetValue(AdditionalDescriptionContentProperty);
- set => SetValue(AdditionalDescriptionContentProperty, value);
- }
-
- public static readonly DependencyProperty TitleProperty = DependencyProperty.Register(
- "Title",
- typeof(string),
- typeof(SettingsDisplayControl),
- new PropertyMetadata(null)
- );
-
- public string Title
- {
- get => (string)GetValue(TitleProperty);
- set => SetValue(TitleProperty, value);
- }
-
- public static readonly DependencyProperty DescriptionProperty = DependencyProperty.Register(
- "Description",
- typeof(string),
- typeof(SettingsDisplayControl),
- new PropertyMetadata(null)
- );
-
- public string Description
- {
- get => (string)GetValue(DescriptionProperty);
- set => SetValue(DescriptionProperty, value);
- }
-
- public static readonly DependencyProperty IconProperty = DependencyProperty.Register(
- "Icon",
- typeof(IconElement),
- typeof(SettingsDisplayControl),
- new PropertyMetadata(null)
- );
-
- public IconElement Icon
- {
- get => (IconElement)GetValue(IconProperty);
- set => SetValue(IconProperty, value);
- }
-
- public SettingsDisplayControl()
- {
- this.InitializeComponent();
- VisualStateManager.GoToState(this, "NormalState", false);
- }
-
- private void MainPanel_SizeChanged(object sender, SizeChangedEventArgs e)
- {
- if (e.NewSize.Width == e.PreviousSize.Width || ActionableElement == null)
- return;
-
- if (ActionableElement.ActualWidth > e.NewSize.Width / 3)
- {
- VisualStateManager.GoToState(this, "CompactState", false);
- }
- else
- {
- VisualStateManager.GoToState(this, "NormalState", false);
- }
- }
- }
-}
diff --git a/Sources/Stylophone/Converters/SkiaConverters.cs b/Sources/Stylophone/Converters/SkiaConverters.cs
index 4f96a2ee..8f58898d 100644
--- a/Sources/Stylophone/Converters/SkiaConverters.cs
+++ b/Sources/Stylophone/Converters/SkiaConverters.cs
@@ -37,7 +37,7 @@ public object ConvertBack(object value, Type targetType, object parameter, strin
}
///
- /// Converts an SKImage to a WriteableBitmap for use in XAML.
+ /// Converts an SKColor to a Windows.UI.Color for use in XAML.
///
public class SKColorToUWPConverter : IValueConverter
{
diff --git a/Sources/Stylophone/Helpers/ScrollViewerExtensions.MiddleClickScrolling.cs b/Sources/Stylophone/Helpers/ScrollViewerExtensions.MiddleClickScrolling.cs
new file mode 100644
index 00000000..84e272af
--- /dev/null
+++ b/Sources/Stylophone/Helpers/ScrollViewerExtensions.MiddleClickScrolling.cs
@@ -0,0 +1,400 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using CommunityToolkit.WinUI;
+using System;
+using System.Threading;
+using Windows.Devices.Input;
+using Windows.Foundation;
+using Windows.System;
+using Windows.UI.Core;
+using Windows.UI.Input;
+using Windows.UI.Xaml;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Input;
+
+namespace Microsoft.Toolkit.Uwp.UI
+{
+
+ ///
+ /// Provides attached dependency properties and methods for the control.
+ ///
+ public partial class ScrollViewerExtensions
+ {
+#pragma warning disable CS0419 // Ambiguous reference in cref attribute
+
+ ///
+ /// Attached for enabling middle click scrolling
+ ///
+ public static readonly DependencyProperty EnableMiddleClickScrollingProperty =
+ DependencyProperty.RegisterAttached("EnableMiddleClickScrolling", typeof(bool), typeof(ScrollViewerExtensions), new PropertyMetadata(false, OnEnableMiddleClickScrollingChanged));
+
+ ///
+ /// Get . Returns `true` if middle click scrolling is enabled else return `false`
+ ///
+ /// The to get the associated `bool`
+ /// The `bool` associated with the
+ public static bool GetEnableMiddleClickScrolling(DependencyObject obj)
+ {
+ return (bool)obj.GetValue(EnableMiddleClickScrollingProperty);
+ }
+
+ ///
+ /// Set . `true` to enable middle click scrolling
+ ///
+ /// The to associate the `bool` with
+ /// The `bool` for binding to the
+ public static void SetEnableMiddleClickScrolling(DependencyObject obj, bool value)
+ {
+ obj.SetValue(EnableMiddleClickScrollingProperty, value);
+ }
+#pragma warning restore CS0419 // Ambiguous reference in cref attribute
+ }
+
+ ///
+ /// Provides attached dependency properties and methods for the control.
+ ///
+ public static partial class ScrollViewerExtensions
+ {
+ private static double _threshold = 50;
+ private static bool _isPressed = false;
+ private static bool _isMoved = false;
+ private static Point _startPosition;
+ private static bool _isDeferredMovingStarted = false;
+ private static double _factor = 50;
+ private static Point _currentPosition;
+ private static Timer _timer;
+ private static ScrollViewer _scrollViewer;
+ private static uint _oldCursorID = 100;
+ private static uint _maxSpeed = 200;
+ private static bool _isCursorAvailable = false;
+
+ ///
+ /// Function will be called when is updated
+ ///
+ /// Holds the dependency object
+ /// Holds the dependency object args
+ private static void OnEnableMiddleClickScrollingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ if (d is ScrollViewer scrollViewer)
+ {
+ _scrollViewer = scrollViewer;
+ }
+ else
+ {
+ _scrollViewer = (d as FrameworkElement).FindDescendant();
+
+ if (_scrollViewer == null)
+ {
+ (d as FrameworkElement).Loaded += (sender, arg) =>
+ {
+ _scrollViewer = (sender as FrameworkElement).FindDescendant();
+
+ if (_scrollViewer != null)
+ {
+ UpdateChange((bool)e.NewValue);
+ }
+ };
+ }
+ }
+
+ if (_scrollViewer == null)
+ {
+ return;
+ }
+
+ UpdateChange((bool)e.NewValue);
+ }
+
+ ///
+ /// Function to update changes in
+ ///
+ /// New value from the
+ private static void UpdateChange(bool newValue)
+ {
+ if (newValue)
+ {
+ _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed;
+ _scrollViewer.PointerPressed += ScrollViewer_PointerPressed;
+ }
+ else
+ {
+ _scrollViewer.PointerPressed -= ScrollViewer_PointerPressed;
+ UnsubscribeMiddleClickScrolling();
+ }
+ }
+
+ ///
+ /// Function to set default value and subscribe to events
+ ///
+ private static void SubscribeMiddleClickScrolling(DispatcherQueue dispatcherQueue)
+ {
+ _isPressed = true;
+ _isMoved = false;
+ _startPosition = default(Point);
+ _currentPosition = default(Point);
+ _isDeferredMovingStarted = false;
+ _oldCursorID = 100;
+ _isCursorAvailable = IsCursorResourceAvailable();
+
+ _timer?.Dispose();
+ _timer = new Timer(Scroll, dispatcherQueue, 5, 5);
+
+ Window.Current.CoreWindow.PointerMoved -= CoreWindow_PointerMoved;
+ Window.Current.CoreWindow.PointerReleased -= CoreWindow_PointerReleased;
+
+ Window.Current.CoreWindow.PointerMoved += CoreWindow_PointerMoved;
+ Window.Current.CoreWindow.PointerReleased += CoreWindow_PointerReleased;
+ }
+
+ ///
+ /// Function to set default value and unsubscribe to events
+ ///
+ private static void UnsubscribeMiddleClickScrolling()
+ {
+ _isPressed = false;
+ _isMoved = false;
+ _startPosition = default(Point);
+ _currentPosition = default(Point);
+ _isDeferredMovingStarted = false;
+ _oldCursorID = 100;
+ _timer?.Dispose();
+
+ Window.Current.CoreWindow.PointerMoved -= CoreWindow_PointerMoved;
+ Window.Current.CoreWindow.PointerReleased -= CoreWindow_PointerReleased;
+
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Arrow, 0);
+ }
+
+ ///
+ /// This function will be called for every small interval by
+ ///
+ /// Default param for . In this function it will be `null`
+ private static void Scroll(object state)
+ {
+ var dispatcherQueue = state as DispatcherQueue;
+ if (dispatcherQueue == null)
+ {
+ return;
+ }
+
+ var offsetX = _currentPosition.X - _startPosition.X;
+ var offsetY = _currentPosition.Y - _startPosition.Y;
+
+ SetCursorType(dispatcherQueue, offsetX, offsetY);
+
+ if (Math.Abs(offsetX) > _threshold || Math.Abs(offsetY) > _threshold)
+ {
+ offsetX = Math.Abs(offsetX) < _threshold ? 0 : offsetX;
+ offsetY = Math.Abs(offsetY) < _threshold ? 0 : offsetY;
+
+ offsetX /= _factor;
+ offsetY /= _factor;
+
+ offsetX = offsetX > 0 ? Math.Pow(offsetX, 2) : -Math.Pow(offsetX, 2);
+ offsetY = offsetY > 0 ? Math.Pow(offsetY, 2) : -Math.Pow(offsetY, 2);
+
+ offsetX = offsetX > _maxSpeed ? _maxSpeed : offsetX;
+ offsetY = offsetY > _maxSpeed ? _maxSpeed : offsetY;
+
+ dispatcherQueue.EnqueueAsync(() => _scrollViewer?.ChangeView(_scrollViewer.HorizontalOffset + offsetX, _scrollViewer.VerticalOffset + offsetY, null, true));
+ }
+ }
+
+ ///
+ /// Function to check the status of scrolling
+ ///
+ /// Return true if the scrolling is started
+ private static bool CanScroll()
+ {
+ return _isDeferredMovingStarted || (_isPressed && !_isDeferredMovingStarted);
+ }
+
+ private static void ScrollViewer_PointerPressed(object sender, PointerRoutedEventArgs e)
+ {
+ // Unsubscribe if deferred moving is started
+ if (_isDeferredMovingStarted)
+ {
+ UnsubscribeMiddleClickScrolling();
+ return;
+ }
+
+ Pointer pointer = e.Pointer;
+
+ if (pointer.PointerDeviceType == PointerDeviceType.Mouse)
+ {
+ _scrollViewer = sender as ScrollViewer;
+
+ PointerPoint pointerPoint = e.GetCurrentPoint(_scrollViewer);
+
+ // SubscribeMiddle if middle button is pressed
+ if (pointerPoint.Properties.IsMiddleButtonPressed)
+ {
+ SubscribeMiddleClickScrolling(DispatcherQueue.GetForCurrentThread());
+
+ _startPosition = Window.Current.CoreWindow.PointerPosition;
+ _currentPosition = Window.Current.CoreWindow.PointerPosition;
+ }
+ }
+ }
+
+ private static void CoreWindow_PointerMoved(CoreWindow sender, PointerEventArgs args)
+ {
+ // If condition that occurs before scrolling begins
+ if (_isPressed && !_isMoved)
+ {
+ PointerPoint pointerPoint = args.CurrentPoint;
+
+ if (pointerPoint.Properties.IsMiddleButtonPressed)
+ {
+ _currentPosition = Window.Current.CoreWindow.PointerPosition;
+
+ var offsetX = _currentPosition.X - _startPosition.X;
+ var offsetY = _currentPosition.Y - _startPosition.Y;
+
+ // Setting _isMoved if pointer goes out of threshold value
+ if (Math.Abs(offsetX) > _threshold || Math.Abs(offsetY) > _threshold)
+ {
+ _isMoved = true;
+ }
+ }
+ }
+
+ // Update current position of the pointer if scrolling started
+ if (CanScroll())
+ {
+ _currentPosition = Window.Current.CoreWindow.PointerPosition;
+ }
+ }
+
+ private static void CoreWindow_PointerReleased(CoreWindow sender, PointerEventArgs args)
+ {
+ // Start deferred moving if the pointer is pressed and not moved
+ if (_isPressed && !_isMoved)
+ {
+ _isDeferredMovingStarted = true;
+
+ // Event to stop deferred scrolling if pointer exited
+ Window.Current.CoreWindow.PointerExited -= CoreWindow_PointerExited;
+ Window.Current.CoreWindow.PointerExited += CoreWindow_PointerExited;
+
+ // Event to stop deferred scrolling if pointer pressed
+ Window.Current.CoreWindow.PointerPressed -= CoreWindow_PointerPressed;
+ Window.Current.CoreWindow.PointerPressed += CoreWindow_PointerPressed;
+
+ SetCursorType(DispatcherQueue.GetForCurrentThread(), 0, 0);
+ }
+ else
+ {
+ _isDeferredMovingStarted = false;
+ }
+
+ // Unsubscribe if the pointer is pressed and not DeferredMoving
+ if (_isPressed && !_isDeferredMovingStarted)
+ {
+ UnsubscribeMiddleClickScrolling();
+ }
+ }
+
+ private static void CoreWindow_PointerPressed(CoreWindow sender, PointerEventArgs args)
+ {
+ Window.Current.CoreWindow.PointerPressed -= CoreWindow_PointerPressed;
+ Window.Current.CoreWindow.PointerExited -= CoreWindow_PointerExited;
+ UnsubscribeMiddleClickScrolling();
+ }
+
+ private static void CoreWindow_PointerExited(CoreWindow sender, PointerEventArgs args)
+ {
+ Window.Current.CoreWindow.PointerPressed -= CoreWindow_PointerPressed;
+ Window.Current.CoreWindow.PointerExited -= CoreWindow_PointerExited;
+ UnsubscribeMiddleClickScrolling();
+ }
+
+ private static void SetCursorType(DispatcherQueue dispatcherQueue, double offsetX, double offsetY)
+ {
+ if (!_isCursorAvailable)
+ {
+ return;
+ }
+
+ uint cursorID = 101;
+
+ if (Math.Abs(offsetX) < _threshold && Math.Abs(offsetY) < _threshold)
+ {
+ cursorID = 101;
+ }
+ else if (Math.Abs(offsetX) < _threshold && offsetY < -_threshold)
+ {
+ cursorID = 102;
+ }
+ else if (offsetX > _threshold && offsetY < -_threshold)
+ {
+ cursorID = 103;
+ }
+ else if (offsetX > _threshold && Math.Abs(offsetY) < _threshold)
+ {
+ cursorID = 104;
+ }
+ else if (offsetX > _threshold && offsetY > _threshold)
+ {
+ cursorID = 105;
+ }
+ else if (Math.Abs(offsetX) < _threshold && offsetY > _threshold)
+ {
+ cursorID = 106;
+ }
+ else if (offsetX < -_threshold && offsetY > _threshold)
+ {
+ cursorID = 107;
+ }
+ else if (offsetX < -_threshold && Math.Abs(offsetY) < _threshold)
+ {
+ cursorID = 108;
+ }
+ else if (offsetX < -_threshold && offsetY < -_threshold)
+ {
+ cursorID = 109;
+ }
+
+ if (_oldCursorID != cursorID)
+ {
+ dispatcherQueue.EnqueueAsync(() => Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Custom, cursorID));
+
+ _oldCursorID = cursorID;
+ }
+ }
+
+ ///
+ /// Function to check the availability of cursor resource
+ ///
+ /// Returns `true` if the cursor resource is available
+ private static bool IsCursorResourceAvailable()
+ {
+ var isCursorAvailable = true;
+
+ try
+ {
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Custom, 101);
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Custom, 102);
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Custom, 103);
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Custom, 104);
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Custom, 105);
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Custom, 106);
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Custom, 107);
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Custom, 108);
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Custom, 109);
+ }
+ catch (Exception)
+ {
+ isCursorAvailable = false;
+ }
+ finally
+ {
+ Window.Current.CoreWindow.PointerCursor = new CoreCursor(CoreCursorType.Arrow, 0);
+ }
+
+ return isCursorAvailable;
+ }
+ }
+}
diff --git a/Sources/Stylophone/Package.appxmanifest b/Sources/Stylophone/Package.appxmanifest
index 04666c7f..16a58082 100644
--- a/Sources/Stylophone/Package.appxmanifest
+++ b/Sources/Stylophone/Package.appxmanifest
@@ -12,7 +12,7 @@
+ Version="2.6.0.0" />
diff --git a/Sources/Stylophone/Package.tt b/Sources/Stylophone/Package.tt
index f796b294..9dcdfd2d 100644
--- a/Sources/Stylophone/Package.tt
+++ b/Sources/Stylophone/Package.tt
@@ -2,7 +2,7 @@
<#@ output extension=".appxmanifest" #>
<#@ parameter type="System.String" name="BuildConfiguration" #>
<#
- string version = "2.5.2.0";
+ string version = "2.6.0.0";
// Get configuration name at Build time
string configName = Host.ResolveParameterValue("-", "-", "BuildConfiguration");
diff --git a/Sources/Stylophone/Services/DialogService.cs b/Sources/Stylophone/Services/DialogService.cs
index fd78844b..ffbf5a1b 100644
--- a/Sources/Stylophone/Services/DialogService.cs
+++ b/Sources/Stylophone/Services/DialogService.cs
@@ -2,7 +2,6 @@
using System.Threading.Tasks;
using Stylophone.Views;
-using Microsoft.Toolkit.Uwp.Helpers;
using Stylophone.Common.Interfaces;
using Stylophone.Common.Services;
using Stylophone.Common.ViewModels;
@@ -12,6 +11,7 @@
using Windows.Services.Store;
using Windows.UI.Xaml.Media;
using System.Linq;
+using Microsoft.Toolkit.Uwp.Helpers;
namespace Stylophone.Services
{
diff --git a/Sources/Stylophone/Services/DispatcherService.cs b/Sources/Stylophone/Services/DispatcherService.cs
index fa5c0767..054b100e 100644
--- a/Sources/Stylophone/Services/DispatcherService.cs
+++ b/Sources/Stylophone/Services/DispatcherService.cs
@@ -1,4 +1,5 @@
-using Microsoft.Toolkit.Uwp;
+using CommunityToolkit;
+using CommunityToolkit.WinUI;
using Stylophone.Common.Interfaces;
using System;
using System.Threading.Tasks;
diff --git a/Sources/Stylophone/Services/NavigationService.cs b/Sources/Stylophone/Services/NavigationService.cs
index 43e49767..5bf17379 100644
--- a/Sources/Stylophone/Services/NavigationService.cs
+++ b/Sources/Stylophone/Services/NavigationService.cs
@@ -1,7 +1,7 @@
using Stylophone.ViewModels;
using Stylophone.Views;
using CommunityToolkit.Mvvm.ComponentModel;
-using Microsoft.Toolkit.Uwp.UI.Animations;
+using CommunityToolkit.WinUI.Animations;
using Stylophone.Common.Interfaces;
using Stylophone.Common.ViewModels;
using System;
diff --git a/Sources/Stylophone/Services/NotificationService.cs b/Sources/Stylophone/Services/NotificationService.cs
index ddec9317..311e6523 100644
--- a/Sources/Stylophone/Services/NotificationService.cs
+++ b/Sources/Stylophone/Services/NotificationService.cs
@@ -1,5 +1,4 @@
using System;
-using CommunityToolkit.Mvvm.Messaging.Messages;
using Microsoft.Toolkit.Uwp.Notifications;
using Windows.UI.Notifications;
using Stylophone.Common.Interfaces;
@@ -21,7 +20,7 @@ public override void ShowBasicToastNotification(string title, string description
// Create the toast content
var content = new ToastContent()
{
- // More about the Launch property at https://docs.microsoft.com/dotnet/api/microsoft.toolkit.uwp.notifications.toastcontent
+ // More about the Launch property at https://docs.microsoft.com/dotnet/api/CommunityToolkit.notifications.toastcontent
Launch = "ToastContentActivationParams",
Visual = new ToastVisual()
@@ -47,7 +46,7 @@ public override void ShowBasicToastNotification(string title, string description
{
Buttons =
{
- // More about Toast Buttons at https://docs.microsoft.com/dotnet/api/microsoft.toolkit.uwp.notifications.toastbutton
+ // More about Toast Buttons at https://docs.microsoft.com/dotnet/api/CommunityToolkit.notifications.toastbutton
new ToastButton("OK", "ToastButtonActivationArguments")
{
ActivationType = ToastActivationType.Foreground
diff --git a/Sources/Stylophone/Services/SystemMediaControlsService.cs b/Sources/Stylophone/Services/SystemMediaControlsService.cs
index 2d501e16..1b9e507c 100644
--- a/Sources/Stylophone/Services/SystemMediaControlsService.cs
+++ b/Sources/Stylophone/Services/SystemMediaControlsService.cs
@@ -1,7 +1,5 @@
using System;
using System.Threading.Tasks;
-using Stylophone.Helpers;
-using Microsoft.Toolkit.Uwp.Helpers;
using MpcNET;
using MpcNET.Commands.Playback;
using Stylophone.Common.Helpers;
@@ -10,6 +8,9 @@
using Windows.Media;
using Windows.Storage;
using Windows.Storage.Streams;
+using Microsoft.Toolkit.Uwp.Helpers;
+using Windows.Media.Core;
+using Windows.Media.Playback;
namespace Stylophone.Services
{
@@ -18,11 +19,19 @@ public class SystemMediaControlsService
private SystemMediaTransportControls _smtc;
private MPDConnectionService _mpdService;
+ private MediaPlayer _silencePlayer;
+
public SystemMediaControlsService(MPDConnectionService mpdService)
{
_mpdService = mpdService;
- }
+ // https://stackoverflow.com/questions/47339719/how-to-disable-uwp-app-suspension
+ _silencePlayer = new MediaPlayer();
+ _silencePlayer.Source = MediaSource.CreateFromUri(new Uri("ms-appx:///Assets/silence.wav"));
+ _silencePlayer.CommandManager.IsEnabled = false; // Hide silencePlayer from SMTC
+ _silencePlayer.IsLoopingEnabled = true;
+ }
+
public void Initialize()
{
@@ -72,6 +81,8 @@ private async void SystemControls_ButtonPressed(SystemMediaTransportControls sen
private void UpdateState(MpdStatus status)
{
+ _silencePlayer.Play();
+
switch (status.State)
{
case MpdState.Play:
diff --git a/Sources/Stylophone/Styles/StyloResources.xaml b/Sources/Stylophone/Styles/StyloResources.xaml
index aa34e0c3..ed16f683 100644
--- a/Sources/Stylophone/Styles/StyloResources.xaml
+++ b/Sources/Stylophone/Styles/StyloResources.xaml
@@ -3,10 +3,11 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:Stylophone="using:Stylophone.Helpers"
- xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters"
+ xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Stylophone.Common.ViewModels"
+ xmlns:media="using:CommunityToolkit.WinUI.Media"
mc:Ignorable="d">
@@ -30,7 +31,7 @@
-
+
diff --git a/Sources/Stylophone/Stylophone.csproj b/Sources/Stylophone/Stylophone.csproj
index 89186d51..0ad75108 100644
--- a/Sources/Stylophone/Stylophone.csproj
+++ b/Sources/Stylophone/Stylophone.csproj
@@ -18,8 +18,7 @@
512
{A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
true
-
-
+ Stylophone_TemporaryKey.pfx
MiddleClickScrolling-CursorType.res
False
SHA256
@@ -28,7 +27,8 @@
Always
x86|x64|arm
0
- AC6B4D78B6FC1C9719183572B36F08C049AFFB1E
+
+
True
DXFeatureLevel
@@ -139,51 +139,32 @@
PackageReference
-
- 4.5.3
-
-
- 4.5.3
-
-
- 6.0.0
-
+
+
+
6.2.14
10.1901.28001
-
- 7.1.2
-
-
- 7.1.2
-
-
- 7.1.2
-
-
- 7.1.2
-
-
- 7.1.2
-
-
- 2.7.3
-
-
- 2.0.1
-
-
- 1.4.0
-
-
- 2.88.1
-
-
- 3.3.2
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
1.1.0
@@ -193,20 +174,15 @@
+
-
-
- SettingsBlockControl.xaml
-
-
- SettingsDisplayControl.xaml
-
+
@@ -272,14 +248,6 @@
-
- MSBuild:Compile
- Designer
-
-
- MSBuild:Compile
- Designer
-
Designer
MSBuild:Compile
@@ -348,6 +316,7 @@
+
diff --git a/Sources/Stylophone/ViewModels/PlaybackViewModel.cs b/Sources/Stylophone/ViewModels/PlaybackViewModel.cs
index c6eecac9..2d7b62ca 100644
--- a/Sources/Stylophone/ViewModels/PlaybackViewModel.cs
+++ b/Sources/Stylophone/ViewModels/PlaybackViewModel.cs
@@ -1,6 +1,6 @@
using Stylophone.Services;
using Stylophone.Views;
-using Microsoft.Toolkit.Uwp;
+using CommunityToolkit;
using Stylophone.Common.Interfaces;
using Stylophone.Common.Services;
using Stylophone.Common.ViewModels;
diff --git a/Sources/Stylophone/ViewModels/ShellViewModel.cs b/Sources/Stylophone/ViewModels/ShellViewModel.cs
index 3f7ec303..15d0e321 100644
--- a/Sources/Stylophone/ViewModels/ShellViewModel.cs
+++ b/Sources/Stylophone/ViewModels/ShellViewModel.cs
@@ -14,9 +14,9 @@
using Stylophone.Services;
using Windows.Foundation;
using MpcNET.Commands.Playback;
-using CommunityToolkit.Labs.WinUI;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI.Xaml.Controls;
+using CommunityToolkit.WinUI.Behaviors;
namespace Stylophone.ViewModels
{
diff --git a/Sources/Stylophone/Views/FoldersPage.xaml b/Sources/Stylophone/Views/FoldersPage.xaml
index 01cc0c8f..804a7f67 100644
--- a/Sources/Stylophone/Views/FoldersPage.xaml
+++ b/Sources/Stylophone/Views/FoldersPage.xaml
@@ -3,12 +3,12 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:behaviors="using:Stylophone.Behaviors"
- xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters"
+ xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="using:Microsoft.Xaml.Interactivity"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:strings="using:Stylophone.Localization.Strings"
- xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
+ xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:vm="using:Stylophone.Common.ViewModels"
xmlns:winui="using:Microsoft.UI.Xaml.Controls"
mc:Ignorable="d">
diff --git a/Sources/Stylophone/Views/LibraryDetailPage.xaml b/Sources/Stylophone/Views/LibraryDetailPage.xaml
index 4506db23..b82e67be 100644
--- a/Sources/Stylophone/Views/LibraryDetailPage.xaml
+++ b/Sources/Stylophone/Views/LibraryDetailPage.xaml
@@ -2,13 +2,15 @@
x:Class="Stylophone.Views.LibraryDetailPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
+ xmlns:controls="using:CommunityToolkit.WinUI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:strings="using:Stylophone.Localization.Strings"
- xmlns:stylophone="using:Stylophone.Helpers"
- xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
+ xmlns:ui="using:CommunityToolkit.WinUI"
+ xmlns:ui7="using:Microsoft.Toolkit.Uwp.UI"
xmlns:winui="using:Microsoft.UI.Xaml.Controls"
+ xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
+ xmlns:behaviors="using:Stylophone.Behaviors"
mc:Ignorable="d">
@@ -57,18 +59,15 @@
-
+ ui:Effects.Shadow="{StaticResource CommonShadow}"
+ >
-
+
-
+
+
+
+
diff --git a/Sources/Stylophone/Views/LibraryDetailPage.xaml.cs b/Sources/Stylophone/Views/LibraryDetailPage.xaml.cs
index 401ab77f..674e3e8e 100644
--- a/Sources/Stylophone/Views/LibraryDetailPage.xaml.cs
+++ b/Sources/Stylophone/Views/LibraryDetailPage.xaml.cs
@@ -1,5 +1,5 @@
using Stylophone.Helpers;
-using Microsoft.Toolkit.Uwp.UI.Animations;
+using CommunityToolkit.WinUI.Animations;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
diff --git a/Sources/Stylophone/Views/LibraryPage.xaml b/Sources/Stylophone/Views/LibraryPage.xaml
index c101bd1e..06e1cfc8 100644
--- a/Sources/Stylophone/Views/LibraryPage.xaml
+++ b/Sources/Stylophone/Views/LibraryPage.xaml
@@ -2,17 +2,16 @@
x:Class="Stylophone.Views.LibraryPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:animations="using:Microsoft.Toolkit.Uwp.UI.Animations"
+ xmlns:animations="using:CommunityToolkit.WinUI.Animations"
xmlns:behaviors="using:Stylophone.Behaviors"
- xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
- xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:strings="using:Stylophone.Localization.Strings"
- xmlns:stylophone="using:Stylophone.Helpers"
- xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
+ xmlns:ui="using:CommunityToolkit.WinUI"
+ xmlns:ui7="using:Microsoft.Toolkit.Uwp.UI"
xmlns:vm="using:Stylophone.Common.ViewModels"
xmlns:winui="using:Microsoft.UI.Xaml.Controls"
+ xmlns:media="using:CommunityToolkit.WinUI.Media"
NavigationCacheMode="Required"
mc:Ignorable="d">
@@ -33,7 +32,7 @@
animations:Connected.ListItemKey="animationKeyLibrary"
behaviors:ListViewBehavior.FillBeforeWrap="True"
behaviors:ListViewBehavior.MinItemWidth="204"
- ui:ScrollViewerExtensions.EnableMiddleClickScrolling="True"
+ ui7:ScrollViewerExtensions.EnableMiddleClickScrolling="True"
IsItemClickEnabled="True"
ItemClick="AlbumClicked"
ItemsSource="{x:Bind ViewModel.FilteredSource, Mode=OneWay}"
@@ -45,43 +44,40 @@
-
-
+ VerticalAlignment="Center">
+
+
+
+
-
+
-
-
+
-
@@ -96,7 +92,7 @@
-
+
@@ -108,7 +104,7 @@
+ Height="180" >
@@ -137,8 +133,6 @@
x:Name="Album"
Width="180"
Height="180"
- BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
- BorderThickness="1"
CornerRadius="4">
diff --git a/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml b/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml
index 0265fe16..503ff618 100644
--- a/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml
+++ b/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml
@@ -3,11 +3,11 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:WindowsStateTriggers="using:WindowsStateTriggers"
- xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
- xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters"
+ xmlns:controls="using:CommunityToolkit.WinUI.Converters"
+ xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
+ xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:strings="using:Stylophone.Localization.Strings"
d:DesignHeight="96"
mc:Ignorable="d">
@@ -110,21 +110,20 @@
Canvas.ZIndex="10"
Click="{x:Bind PlaybackViewModel.NavigateNowPlaying}"
CornerRadius="4"
+ AutomationProperties.Name="{x:Bind strings:Resources.ActionFullscreenPlayback}"
+ AutomationProperties.FullDescription="{x:Bind PlaybackViewModel.CurrentTrack.Name, Mode=OneWay}"
IsEnabled="{x:Bind PlaybackViewModel.IsTrackInfoAvailable, Mode=OneWay}"
Visibility="{x:Bind PlaybackViewModel.ShowTrackName, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
-
+ ui:Effects.Shadow="{StaticResource CommonShadow}">
-
+
@@ -220,6 +221,7 @@
Click="{x:Bind PlaybackViewModel.SkipPrevious}"
FontSize="22"
Style="{StaticResource SVButtonStyle}"
+ AutomationProperties.Name="{x:Bind strings:Resources.ActionSkipPrevious}"
ToolTipService.ToolTip="{x:Bind strings:Resources.ActionSkipPrevious}">
@@ -235,6 +237,7 @@
Content="{x:Bind PlaybackViewModel.PlayButtonContent, Mode=OneWay}"
FontSize="28"
Style="{StaticResource SVButtonStyle}"
+ AutomationProperties.Name="{x:Bind strings:Resources.ActionPlayPause}"
ToolTipService.ToolTip="{x:Bind strings:Resources.ActionPlayPause}" />
@@ -245,6 +248,7 @@
Click="{x:Bind PlaybackViewModel.SkipNext}"
FontSize="22"
Style="{StaticResource SVButtonStyle}"
+ AutomationProperties.Name="{x:Bind strings:Resources.ActionSkipNext}"
ToolTipService.ToolTip="{x:Bind strings:Resources.ActionSkipNext}">
@@ -269,6 +273,8 @@
Content="{x:Bind PlaybackViewModel.RepeatIcon, Mode=OneWay}"
FontSize="18"
Style="{StaticResource SVButtonStyle}"
+ AutomationProperties.Name="{x:Bind strings:Resources.ActionToggleRepeat}"
+ AutomationProperties.ItemStatus="{x:Bind PlaybackViewModel.IsRepeatEnabled}"
ToolTipService.ToolTip="{x:Bind strings:Resources.ActionToggleRepeat}" />
@@ -304,11 +310,15 @@
Margin="16,8,16,0"
ManipulationMode="All"
ManipulationStarting="{x:Bind PlaybackViewModel.OnPlayingSliderMoving}"
- Maximum="{x:Bind PlaybackViewModel.MaxTimeValue, Mode=OneWay}"
+ GettingFocus="{x:Bind PlaybackViewModel.OnPlayingSliderMoving}"
PointerCaptureLost="{x:Bind PlaybackViewModel.OnPlayingSliderChange}"
+ LosingFocus="{x:Bind PlaybackViewModel.OnPlayingSliderChange}"
ThumbToolTipValueConverter="{StaticResource SliderValueConverter}"
+ AutomationProperties.Name="{x:Bind strings:Resources.SongPlaybackLabel}"
+ AutomationProperties.FullDescription="{x:Bind PlaybackViewModel.CurrentTimeValue, Converter={StaticResource SliderValueConverter}}"
Visibility="{x:Bind PlaybackViewModel.IsTrackInfoAvailable, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}"
- Value="{x:Bind PlaybackViewModel.CurrentTimeValue, Mode=TwoWay}" />
+ Value="{x:Bind PlaybackViewModel.CurrentTimeValue, Mode=TwoWay}"
+ Maximum="{x:Bind PlaybackViewModel.MaxTimeValue, Mode=OneWay}" />
@@ -491,6 +503,7 @@
Margin="{StaticResource XSmallLeftMargin}"
FontSize="22"
Style="{StaticResource SVButtonStyle}"
+ AutomationProperties.Name="{x:Bind strings:Resources.ActionMore}"
ToolTipService.ToolTip="{x:Bind strings:Resources.ActionMore}">
diff --git a/Sources/Stylophone/Views/Playback/OverlayView.xaml b/Sources/Stylophone/Views/Playback/OverlayView.xaml
index 197e9525..398beb5b 100644
--- a/Sources/Stylophone/Views/Playback/OverlayView.xaml
+++ b/Sources/Stylophone/Views/Playback/OverlayView.xaml
@@ -2,7 +2,7 @@
x:Class="Stylophone.Views.OverlayView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:converters1="using:Microsoft.Toolkit.Uwp.UI.Converters"
+ xmlns:converters1="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:strings="using:Stylophone.Localization.Strings"
diff --git a/Sources/Stylophone/Views/Playback/OverlayView.xaml.cs b/Sources/Stylophone/Views/Playback/OverlayView.xaml.cs
index 738c6d10..e461e82d 100644
--- a/Sources/Stylophone/Views/Playback/OverlayView.xaml.cs
+++ b/Sources/Stylophone/Views/Playback/OverlayView.xaml.cs
@@ -1,6 +1,6 @@
using Stylophone.ViewModels;
using CommunityToolkit.Mvvm.DependencyInjection;
-using Microsoft.Toolkit.Uwp;
+using CommunityToolkit;
using Stylophone.Common.Helpers;
using System;
using System.Threading.Tasks;
diff --git a/Sources/Stylophone/Views/Playback/PlaybackView.xaml b/Sources/Stylophone/Views/Playback/PlaybackView.xaml
index 22418df0..caad4e3c 100644
--- a/Sources/Stylophone/Views/Playback/PlaybackView.xaml
+++ b/Sources/Stylophone/Views/Playback/PlaybackView.xaml
@@ -2,9 +2,9 @@
x:Class="Stylophone.Views.PlaybackView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
+ xmlns:ui="using:CommunityToolkit.WinUI"
xmlns:converters="using:Stylophone.Helpers"
- xmlns:converters1="using:Microsoft.Toolkit.Uwp.UI.Converters"
+ xmlns:converters1="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:strings="using:Stylophone.Localization.Strings"
@@ -61,24 +61,19 @@
-
+ ui:Effects.Shadow="{StaticResource CommonShadow}">
-
+
@@ -57,22 +59,16 @@
-
+ ui:Effects.Shadow="{StaticResource CommonShadow}">
-
+
-
+ ui:Effects.Shadow="{StaticResource CommonShadow}">
-
+
-
+ ui:Effects.Shadow="{StaticResource CommonShadow}">
-
+
+
+
+
@@ -324,11 +315,6 @@
-
-
-
-
-
diff --git a/Sources/Stylophone/Views/SearchResultsPage.xaml b/Sources/Stylophone/Views/SearchResultsPage.xaml
index 08eb1c45..21391f98 100644
--- a/Sources/Stylophone/Views/SearchResultsPage.xaml
+++ b/Sources/Stylophone/Views/SearchResultsPage.xaml
@@ -2,12 +2,15 @@
x:Class="Stylophone.Views.SearchResultsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:Stylophone="using:Stylophone.Helpers"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:strings="using:Stylophone.Localization.Strings"
- xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
+ xmlns:ui="using:CommunityToolkit.WinUI"
+ xmlns:ui7="using:Microsoft.Toolkit.Uwp.UI"
xmlns:winui="using:Microsoft.UI.Xaml.Controls"
+ xmlns:controls="using:CommunityToolkit.WinUI.Controls"
+ xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
+ xmlns:behaviors="using:Stylophone.Behaviors"
mc:Ignorable="d">
@@ -16,24 +19,22 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/Sources/Stylophone/Views/ServerQueuePage.xaml b/Sources/Stylophone/Views/ServerQueuePage.xaml
index c17b8271..093ffc41 100644
--- a/Sources/Stylophone/Views/ServerQueuePage.xaml
+++ b/Sources/Stylophone/Views/ServerQueuePage.xaml
@@ -2,13 +2,13 @@
x:Class="Stylophone.Views.ServerQueuePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:Stylophone="using:Stylophone.Helpers"
- xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:strings="using:Stylophone.Localization.Strings"
- xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
- xmlns:vm="using:Stylophone.Common.ViewModels"
+ xmlns:ui="using:CommunityToolkit.WinUI"
+ xmlns:ui7="using:Microsoft.Toolkit.Uwp.UI"
+ xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
+ xmlns:behaviors="using:Stylophone.Behaviors"
NavigationCacheMode="Required"
mc:Ignorable="d">
@@ -23,7 +23,7 @@
+
+
+
diff --git a/Sources/Stylophone/Views/ServerQueuePage.xaml.cs b/Sources/Stylophone/Views/ServerQueuePage.xaml.cs
index 3004bb06..4b27f81a 100644
--- a/Sources/Stylophone/Views/ServerQueuePage.xaml.cs
+++ b/Sources/Stylophone/Views/ServerQueuePage.xaml.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System;
+using System.Linq;
using Stylophone.Helpers;
using CommunityToolkit.Mvvm.DependencyInjection;
using Stylophone.Common.Interfaces;
@@ -7,6 +8,7 @@
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;
+using System.Collections.Specialized;
namespace Stylophone.Views
{
@@ -31,7 +33,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
- ViewModel.PropertyChanged += ViewModel_PropertyChanged;
+ ViewModel.Source.CollectionChanged += ScrollToPlayingSong;
_mpdService.SongChanged += MPDConnectionService_SongChanged;
@@ -58,31 +60,35 @@ private void MPDConnectionService_SongChanged(object sender, SongChangedEventArg
});
}
- private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
+ private void ScrollToPlayingSong(object sender, NotifyCollectionChangedEventArgs e)
{
- if (e.PropertyName == nameof(ViewModel.Source))
- {
- if (QueueList.Items.Count == 0)
- return;
+ if (QueueList.Items.Count == 0)
+ return;
- var playing = ViewModel.Source.Where(t => t.IsPlaying && t.File.Id != manualSongId).FirstOrDefault();
- if (playing != null)
- {
- playing.UpdatePlayingStatus();
- QueueList.ScrollIntoView(playing, ScrollIntoViewAlignment.Leading);
- }
+ var playing = ViewModel.Source.Where(t => t.IsPlaying && t.File.Id != manualSongId).FirstOrDefault();
+ if (playing != null)
+ {
+ playing.UpdatePlayingStatus();
+ QueueList.ScrollIntoView(playing, ScrollIntoViewAlignment.Leading);
}
}
private void Play_Track(object sender, RoutedEventArgs e)
{
- var listView = sender as ListView;
- var trackVm = listView.SelectedItem as TrackViewModel;
- // Set this ID as manually played by the user to prevent unnecessary autoscrolling.
- // Kind of a duct tape fix for now
- // TODO: Apply to context menu as well, maybe main playbar buttons if the queue is showing?
- manualSongId = trackVm.File.Id;
- trackVm.PlayTrackCommand.Execute(trackVm.File);
+ try
+ {
+ var listView = sender as ListView;
+ var trackVm = listView.SelectedItem as TrackViewModel;
+ // Set this ID as manually played by the user to prevent unnecessary autoscrolling.
+ // Kind of a duct tape fix for now
+ // TODO: Apply to context menu as well, maybe main playbar buttons if the queue is showing?
+ manualSongId = trackVm.File.Id;
+ trackVm.PlayTrackCommand.Execute(trackVm.File);
+ }
+ catch (Exception ex)
+ {
+ Ioc.Default.GetRequiredService().ShowErrorNotification(ex);
+ }
}
private void Select_Item(object sender, Windows.UI.Xaml.Input.RightTappedRoutedEventArgs e) => UWPHelpers.SelectItemOnFlyoutRightClick(QueueList, e);
diff --git a/Sources/Stylophone/Views/SettingsPage.xaml b/Sources/Stylophone/Views/SettingsPage.xaml
index a6e1384a..0e4057c0 100644
--- a/Sources/Stylophone/Views/SettingsPage.xaml
+++ b/Sources/Stylophone/Views/SettingsPage.xaml
@@ -3,12 +3,13 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.UI.Xaml.Controls"
- xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters"
+ xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:Stylophone.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:strings="using:Stylophone.Localization.Strings"
xmlns:helpers="using:Stylophone.Helpers"
+ xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
mc:Ignorable="d">
@@ -33,37 +34,25 @@
-
+ HeaderIcon="{x:Bind ViewModel.IsServerValid, Converter={StaticResource IconConverter}, Mode=OneWay}">
-
-
-
+
-
-
-
-
-
+
+
-
-
-
-
+
+
+
-
-
-
-
+
+
+
-
+
-
+
-
-
-
-
+
+
-
-
-
+
+
+
+
+
+
-
-
+
-
-
-
+
+
-
-
+
@@ -123,42 +105,40 @@
Style="{ThemeResource BaseTextBlockStyle}"
Text="{x:Bind strings:Resources.SettingsDatabase}" />
-
-
+
-
+
-
+
-
-
+
-
-
-
+
-
-
-
-
+
+
+
-
+
-
-
-
+
+
+
@@ -167,34 +147,31 @@
Style="{ThemeResource BaseTextBlockStyle}"
Text="{x:Bind strings:Resources.SettingsCustomization}" />
-
-
+
+
-
+
-
-
-
-
-
-
-
+
+
+
+
+
-
+
-
-
+
+
-
+
-
-
-
-
-
-
+
+
+
+
-
+
@@ -203,21 +180,18 @@
Style="{ThemeResource BaseTextBlockStyle}"
Text="{x:Bind strings:Resources.SettingsAnalytics}" />
-
-
+
-
-
-
+
-
-
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Sources/Stylophone/Views/ShellPage.xaml b/Sources/Stylophone/Views/ShellPage.xaml
index 5ab3dd5f..cc3de41c 100644
--- a/Sources/Stylophone/Views/ShellPage.xaml
+++ b/Sources/Stylophone/Views/ShellPage.xaml
@@ -2,8 +2,8 @@
x:Class="Stylophone.Views.ShellPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:controls="using:Microsoft.Toolkit.Uwp.UI.Controls"
- xmlns:converters="using:Microsoft.Toolkit.Uwp.UI.Converters"
+ xmlns:controls="using:CommunityToolkit.WinUI.Converters"
+ xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="using:Stylophone.Helpers"
xmlns:i="using:Microsoft.Xaml.Interactivity"
@@ -14,7 +14,7 @@
xmlns:strings="using:Stylophone.Localization.Strings"
xmlns:stylophone="using:Stylophone.Common.ViewModels"
xmlns:viewmodels="using:Stylophone.ViewModels"
- xmlns:views="using:Stylophone.Views" xmlns:behaviors="using:CommunityToolkit.Labs.WinUI"
+ xmlns:views="using:Stylophone.Views" xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"
muxc:BackdropMaterial.ApplyToRootOrPageBackground="True"
PreviewKeyDown="GlobalPlayPauseShortcut"
mc:Ignorable="d">