diff --git a/README.md b/README.md
index 5ed3642..6e1cebe 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
Stylophone
===========
-[**Music Player Daemon**](https://www.musicpd.org/) Client for UWP.
+[**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!)
@@ -15,7 +15,8 @@ Based on [MpcNET](https://github.com/Difegue/MpcNET), my own fork of the origina
* Full playback control
* Playlist management (Create, Add/Remove tracks, Delete)
* Picture-in-picture mode
-* Live tile
+* Live tile on Windows 10
+* Integration with native playback controls
* Browse library by albums, or directly by folders
* All data is pulled from your MPD Server only
* Support for both albumart and readpicture commands for maximum compatibility with your cover art library
@@ -53,17 +54,30 @@ You can easily contribute translations to Stylophone! To help translate, follow
## Screenshots
-![Screen1](Screenshots/Screen1.jpg)
-![Screen2](Screenshots/Screen2.jpg)
-![Screen3](Screenshots/Screen3.jpg)
-![Screen4](Screenshots/Screen4.jpg)
-![Screen5](Screenshots/Screen5.jpg)
-![Screen6](Screenshots/Screen6.jpg)
+|Queue, UWP | Queue, iOS |
+|---|---|
+| ![queue_win](Screenshots/Screen1.jpg) | ![queue_ios](Screenshots/Screen5.jpg) |
-## Privacy Policy
+|Library, UWP | Library, iOS |
+|---|---|
+| ![library_win](Screenshots/Screen2.jpg) | ![library_ios](Screenshots/Screen6.jpg) |
-Stylophone collects no data from your computer by default.
-The Windows Store version can send anonymized error reports related to crashes of the application back to me.
+|Playlist, UWP | Playlist, iOS |
+|---|---|
+| ![playlist_win](Screenshots/Screen3.jpg)| ![playlist_ios](Screenshots/Screen7.jpg) |
+
+|Now Playing, UWP | Now Playing, iOS |
+|---|---|
+| ![nowplaying_win](Screenshots/Screen4.jpg)| ![nowplaying_ios](Screenshots/Screen8.jpg) |
+
+|Xbox Integration | iOS Control Center Integration |
+|---|---|
+| ![xbox](Screenshots/ScreenXbox.jpg)| ![controlcenter](Screenshots/ScreenNowPlaying.jpg) |
-If you enable Telemetry in the app's settings, the application will send detailed crash reports using App Center.
+## Privacy Policy
+
+If Telemetry is enabled in the app's settings, the application will send detailed crash reports using [App Center](https://appcenter.ms).
Those reports can contain information about your hardware. (Motherboard type, etc)
+
+Stylophone collects no other data from your device.
+The Windows Store version can send anonymized error reports related to crashes of the application back to me.
diff --git a/Screenshots/Screen3.jpg b/Screenshots/Screen3.jpg
index a8d807a..0695086 100644
Binary files a/Screenshots/Screen3.jpg and b/Screenshots/Screen3.jpg differ
diff --git a/Screenshots/Screen4.jpg b/Screenshots/Screen4.jpg
index 0695086..c604847 100644
Binary files a/Screenshots/Screen4.jpg and b/Screenshots/Screen4.jpg differ
diff --git a/Screenshots/Screen5.jpg b/Screenshots/Screen5.jpg
index fbe02c1..c1078ef 100644
Binary files a/Screenshots/Screen5.jpg and b/Screenshots/Screen5.jpg differ
diff --git a/Screenshots/Screen6.jpg b/Screenshots/Screen6.jpg
index c604847..71e6cbb 100644
Binary files a/Screenshots/Screen6.jpg and b/Screenshots/Screen6.jpg differ
diff --git a/Screenshots/Screen7.jpg b/Screenshots/Screen7.jpg
new file mode 100644
index 0000000..eed3966
Binary files /dev/null and b/Screenshots/Screen7.jpg differ
diff --git a/Screenshots/Screen8.jpg b/Screenshots/Screen8.jpg
new file mode 100644
index 0000000..2ff2fcd
Binary files /dev/null and b/Screenshots/Screen8.jpg differ
diff --git a/Screenshots/ScreenNowPlaying.jpg b/Screenshots/ScreenNowPlaying.jpg
new file mode 100644
index 0000000..28d9639
Binary files /dev/null and b/Screenshots/ScreenNowPlaying.jpg differ
diff --git a/Sources/Stylophone.Common/Services/AlbumArtService.cs b/Sources/Stylophone.Common/Services/AlbumArtService.cs
index 414348d..25e9df9 100644
--- a/Sources/Stylophone.Common/Services/AlbumArtService.cs
+++ b/Sources/Stylophone.Common/Services/AlbumArtService.cs
@@ -191,6 +191,9 @@ private async Task GetAlbumBitmap(IMpdFile f, CancellationToken token
}
catch (Exception e)
{
+ if (token.IsCancellationRequested)
+ return null;
+
Debug.WriteLine("Exception caught while getting albumart: " + e);
_notificationService.ShowErrorNotification(e);
return null;
diff --git a/Sources/Stylophone.Common/Services/MPDConnectionService.cs b/Sources/Stylophone.Common/Services/MPDConnectionService.cs
index 26b6d01..4d0acc2 100644
--- a/Sources/Stylophone.Common/Services/MPDConnectionService.cs
+++ b/Sources/Stylophone.Common/Services/MPDConnectionService.cs
@@ -11,6 +11,11 @@
using Stylophone.Common.Interfaces;
using MpcNET.Commands.Reflection;
using Stylophone.Localization.Strings;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Stylophone.Common.ViewModels;
+using Microsoft.AppCenter.Analytics;
+using Microsoft.AppCenter;
+using System.Drawing;
namespace Stylophone.Common.Services
{
@@ -19,7 +24,7 @@ public class SongChangedEventArgs : EventArgs { public int NewSongId { get; set;
public class MPDConnectionService
{
private const int ConnectionPoolSize = 5;
- public static MpdStatus BOGUS_STATUS = new MpdStatus(0, false, false, false, false, -1, -1, -1, MpdState.Unknown, -1, -1, -1, -1, TimeSpan.Zero, TimeSpan.Zero, -1, -1, -1, -1, -1, "", "");
+ public static MpdStatus BOGUS_STATUS = new MpdStatus(-1, false, false, false, false, -1, -1, -1, MpdState.Unknown, -1, -1, -1, -1, TimeSpan.Zero, TimeSpan.Zero, -1, -1, -1, -1, -1, "", "");
private INotificationService _notificationService;
@@ -70,6 +75,7 @@ public void SetServerInfo(string host, int port, string pass)
public async Task InitializeAsync(bool withRetry = false)
{
IsConnecting = true;
+ CurrentStatus = BOGUS_STATUS; // Reset status
if (IsConnected)
{
@@ -89,6 +95,8 @@ public async Task InitializeAsync(bool withRetry = false)
{
System.Diagnostics.Debug.WriteLine($"Error while connecting: {e.Message}");
+ ConnectionChanged?.Invoke(this, new EventArgs());
+
if (withRetry && !cancelToken.IsCancellationRequested)
{
// The RetryAttempter will call TryConnect() in five seconds.
@@ -100,7 +108,6 @@ public async Task InitializeAsync(bool withRetry = false)
}
IsConnecting = false;
- ConnectionChanged?.Invoke(this, new EventArgs());
}
private void ClearResources()
@@ -135,7 +142,6 @@ private async Task TryConnecting(CancellationToken token)
_mpdEndpoint = new IPEndPoint(ipAddress, _port);
- _idleConnection = await GetConnectionInternalAsync(token);
_statusConnection = await GetConnectionInternalAsync(token);
ConnectionPool = new ObjectPool>(ConnectionPoolSize,
@@ -153,11 +159,14 @@ private async Task TryConnecting(CancellationToken token)
// Connected, initialize basic data
Version = _statusConnection.Version;
+
+ _idleConnection = await GetConnectionInternalAsync(_cancelIdle.Token);
await UpdatePlaylistsAsync();
InitializeStatusUpdater(_cancelIdle.Token);
- ConnectionChanged?.Invoke(this, new EventArgs());
+ IsConnecting = false;
IsConnected = true;
+ ConnectionChanged?.Invoke(this, new EventArgs());
}
///
@@ -176,8 +185,11 @@ public async Task> GetConnectionAsync(Cancell
/// Return type of the command
/// IMpcCommand to send
/// The command results, or default value.
- public async Task SafelySendCommandAsync(IMpcCommand command)
+ public async Task SafelySendCommandAsync(IMpcCommand command, bool showError = true)
{
+ if (!IsConnected)
+ return default(T);
+
try
{
using (var c = await GetConnectionAsync())
@@ -198,8 +210,23 @@ public async Task SafelySendCommandAsync(IMpcCommand command)
}
catch (Exception e)
{
- _notificationService.ShowInAppNotification(string.Format(Resources.ErrorSendingMPDCommand, command.GetType().Name),
- e.Message, NotificationType.Error);
+ System.Diagnostics.Debug.WriteLine($"MPD Error: {e.Message}");
+
+ if (showError)
+ _notificationService.ShowInAppNotification(string.Format(Resources.ErrorSendingMPDCommand, command.GetType().Name),
+ e.Message, NotificationType.Error);
+
+#if DEBUG
+#else
+ var enableAnalytics = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.EnableAnalytics), true);
+ if (enableAnalytics)
+ {
+ var dict = new Dictionary();
+ dict.Add("command", command.Serialize());
+ dict.Add("exception", e.ToString());
+ Analytics.TrackEvent("MPDError", dict);
+ }
+#endif
}
return default(T);
@@ -226,16 +253,21 @@ private async Task GetConnectionInternalAsync(CancellationToken t
private void InitializeStatusUpdater(CancellationToken token = default)
{
- // Update status every second
- _statusUpdater = new System.Timers.Timer(1000);
- _statusUpdater.Elapsed += async (s, e) => await UpdateStatusAsync(_statusConnection);
- _statusUpdater.Start();
-
// Run an idle loop in a spare thread to fire events when needed
Task.Run(async () =>
{
while (true)
{
+ if (_statusUpdater?.Enabled != true && _statusConnection.IsConnected)
+ {
+ // Update status every second
+ _statusUpdater?.Stop();
+ _statusUpdater?.Dispose();
+ _statusUpdater = new System.Timers.Timer(1000);
+ _statusUpdater.Elapsed += async (s, e) => await UpdateStatusAsync(_statusConnection);
+ _statusUpdater.Start();
+ }
+
try
{
if (token.IsCancellationRequested || _idleConnection == null || !_idleConnection.IsConnected)
@@ -250,11 +282,15 @@ private void InitializeStatusUpdater(CancellationToken token = default)
}
catch (Exception e)
{
- System.Diagnostics.Debug.WriteLine($"Error in Idle connection thread: {e.Message}");
- await InitializeAsync(true);
+ if (token.IsCancellationRequested)
+ System.Diagnostics.Debug.WriteLine($"Idle connection cancelled.");
+ else
+ {
+ System.Diagnostics.Debug.WriteLine($"Error in Idle connection thread: {e.Message}");
+ await InitializeAsync(true);
+ }
}
}
-
}).ConfigureAwait(false);
}
diff --git a/Sources/Stylophone.Common/Stylophone.Common.csproj b/Sources/Stylophone.Common/Stylophone.Common.csproj
index 4504847..84d56ce 100644
--- a/Sources/Stylophone.Common/Stylophone.Common.csproj
+++ b/Sources/Stylophone.Common/Stylophone.Common.csproj
@@ -14,10 +14,14 @@
+
+
+
+
diff --git a/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs b/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs
index 357a3d9..d717b56 100644
--- a/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs
+++ b/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs
@@ -12,6 +12,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Net.NetworkInformation;
using System.Threading;
using System.Threading.Tasks;
@@ -69,14 +70,14 @@ public PlaybackViewModelBase(INavigationService navigationService, INotification
_mpdService.ConnectionChanged += OnConnectionChanged;
if (_mpdService.IsConnected)
- Task.Run(() => InitializeAsync());
+ Initialize();
}
private void OnConnectionChanged(object sender, EventArgs e)
{
if (_mpdService.IsConnected)
{
- Task.Run(() => InitializeAsync());
+ Initialize();
}
else
{
@@ -84,19 +85,23 @@ private void OnConnectionChanged(object sender, EventArgs e)
}
}
- private async Task InitializeAsync()
+ private void Initialize()
{
- OnTrackChange(this, new SongChangedEventArgs { NewSongId = _mpdService.CurrentStatus.SongId });
+ OnTrackChange(this, new SongChangedEventArgs { NewSongId = -1 });
CurrentTimeValue = _mpdService.CurrentStatus.Elapsed.TotalSeconds;
OnStateChange(this, null);
- await UpdateUpNextAsync(_mpdService.CurrentStatus);
}
#region Getters and Setters
public bool HasNextTrack => NextTrack != null;
+ ///
+ /// "(deprecated: -1 if the volume cannot be determined)"
+ ///
+ public bool CanSetVolume => _mpdService.CurrentStatus.Volume != -1;
+
///
/// The current playing track
///
@@ -171,7 +176,7 @@ public double MediaVolume
set
{
// HACK: Abort if mpdService doesn't have updated volume from the server yet
- if (_mpdService.CurrentStatus == MPDConnectionService.BOGUS_STATUS)
+ if (_mpdService.CurrentStatus == MPDConnectionService.BOGUS_STATUS || !CanSetVolume)
return;
SetProperty(ref _internalVolume, value);
@@ -188,12 +193,14 @@ public double MediaVolume
volumeTasks.Add(Task.Run(async () =>
{
await _mpdService.SafelySendCommandAsync(new SetVolumeCommand((byte)value));
- Thread.Sleep(1000); // Wait for MPD to acknowledge the new volume in its status...
- MediaVolume = _mpdService.CurrentStatus.Volume; // Update the value to the current server volume
+ Thread.Sleep(500); // Wait for MPD to acknowledge the new volume in its status...
+
+ if (volumeTasks.Count == 1)
+ MediaVolume = _mpdService.CurrentStatus.Volume; // Update the value to the current server volume
}, cts.Token));
// Update the UI
- if ((int)value == 0)
+ if ((int)value <= 0)
{
VolumeIcon = _interop.GetIcon(PlaybackIcon.VolumeMute);
}
@@ -273,19 +280,12 @@ partial void OnIsSingleEnabledChanged(bool value)
protected async void UpdateInformation(object sender, EventArgs e)
{
var status = _mpdService.CurrentStatus;
+ OnPropertyChanged(nameof(CanSetVolume));
// Only call the following if the player exists and the time is greater then 0.
if (status.Elapsed.TotalMilliseconds <= 0)
return;
- // Update song in case we went out of sync
- if (status.SongId != CurrentTrack?.File?.Id)
- {
- OnTrackChange(this, new SongChangedEventArgs { NewSongId = status.SongId });
- OnStateChange(this, null);
- await UpdateUpNextAsync(_mpdService.CurrentStatus);
- }
-
if (!HasNextTrack)
await UpdateUpNextAsync(status);
@@ -356,7 +356,8 @@ public void ToggleRepeat()
{
await _mpdService.SafelySendCommandAsync(new RepeatCommand(IsRepeatEnabled));
await _mpdService.SafelySendCommandAsync(new SingleCommand(IsSingleEnabled));
- Thread.Sleep(1000); // Wait for MPD to acknowledge the new status...
+ Thread.Sleep(500); // Wait for MPD to acknowledge the new status...
+ await UpdateUpNextAsync(_mpdService.CurrentStatus);
}, cts.Token));
}
@@ -376,7 +377,7 @@ public void ToggleShuffle()
stateTasks.Add(Task.Run(async () =>
{
await _mpdService.SafelySendCommandAsync(new RandomCommand(IsShuffleEnabled));
- Thread.Sleep(1000); // Wait for MPD to acknowledge the new status...
+ Thread.Sleep(500); // Wait for MPD to acknowledge the new status...
await UpdateUpNextAsync(_mpdService.CurrentStatus);
}, cts.Token));
}
@@ -396,7 +397,7 @@ public void ToggleConsume()
stateTasks.Add(Task.Run(async () =>
{
await _mpdService.SafelySendCommandAsync(new ConsumeCommand(IsConsumeEnabled));
- Thread.Sleep(1000); // Wait for MPD to acknowledge the new status...
+ Thread.Sleep(500); // Wait for MPD to acknowledge the new status...
}, cts.Token));
}
@@ -431,9 +432,15 @@ public void NavigateNowPlaying()
///
/// Toggles the state between the track playing
- /// and not playing
+ /// and not playing. If playback is fully stopped, this will play index 0.
///
- public void ChangePlaybackState() => _ = _mpdService.SafelySendCommandAsync(new PauseResumeCommand());
+ public void ChangePlaybackState()
+ {
+ if (_mpdService.CurrentStatus.State == MpdState.Stop)
+ _mpdService.SafelySendCommandAsync(new PlayCommand(0));
+ else
+ _mpdService.SafelySendCommandAsync(new PauseResumeCommand());
+ }
///
/// Go forward one track
@@ -490,7 +497,8 @@ private async Task UpdateUpNextAsync(MpdStatus status)
var nextSongId = status.NextSongId;
if (nextSongId != -1)
{
- var response = await _mpdService.SafelySendCommandAsync(new PlaylistIdCommand(nextSongId));
+ // Don't show errors if we can't get the next track due to an old status or something, it's fairly minor
+ var response = await _mpdService.SafelySendCommandAsync(new PlaylistIdCommand(nextSongId), false);
if (response != null)
{
@@ -576,6 +584,10 @@ private void OnStateChange(object sender, EventArgs eventArgs)
// Ditto for shuffle/repeat/single
if (stateTasks.Count == 0)
{
+ if (status.Random != IsShuffleEnabled || IsRepeatEnabled != status.Repeat || IsSingleEnabled != status.Single)
+ {
+ UpdateUpNextAsync(status);
+ }
IsShuffleEnabled = status.Random;
IsRepeatEnabled = status.Repeat;
IsSingleEnabled = status.Single;
diff --git a/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs b/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs
index cfe7d4a..53ca64a 100644
--- a/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs
@@ -193,7 +193,6 @@ private async void MPDConnectionService_QueueChanged(object sender, EventArgs e)
{
_ = Task.Run(async () =>
{
-
var response = await _mpdService.SafelySendCommandAsync(new PlChangesCommand(PlaylistVersion));
if (response != null)
@@ -213,9 +212,11 @@ private async void MPDConnectionService_QueueChanged(object sender, EventArgs e)
while (Source.Count != initialPosition)
{
- // Make sure
- if (Source.Count != initialPosition)
- await _dispatcherService.ExecuteOnUIThreadAsync(() => Source.RemoveAt(initialPosition));
+ await _dispatcherService.ExecuteOnUIThreadAsync(() => {
+ // Make sure
+ if (Source.Count != initialPosition)
+ Source.RemoveAt(initialPosition);
+ });
}
var toAdd = new List();
@@ -240,6 +241,8 @@ private async void MPDConnectionService_QueueChanged(object sender, EventArgs e)
public async Task LoadInitialDataAsync()
{
+ Source.CollectionChanged -= Source_CollectionChanged;
+
var tracks = new List();
var response = await _mpdService.SafelySendCommandAsync(new PlaylistInfoCommand());
diff --git a/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs b/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs
index d1ed921..34d07aa 100644
--- a/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs
@@ -65,6 +65,7 @@ public SettingsViewModel(MPDConnectionService mpdService, IApplicationStorageSer
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsServerValid))]
+ [NotifyPropertyChangedFor(nameof(ServerStatus))]
private bool _isCheckingServer;
[ObservableProperty]
@@ -118,7 +119,7 @@ partial void OnEnableAnalyticsChanged(bool value)
}
public bool IsServerValid => _mpdService.IsConnected;
- public string ServerStatus => IsServerValid ? ServerInfo?.Split('\n')?.First() + (IsStreamingAvailable ? ", "+ Resources.SettingsLocalPlaybackAvailable : "") :
+ public string ServerStatus => IsCheckingServer ? "..." : IsServerValid ? ServerInfo?.Split('\n')?.First() + (IsStreamingAvailable ? ", "+ Resources.SettingsLocalPlaybackAvailable : "") :
Resources.SettingsNoServerError;
partial void OnIsLocalPlaybackEnabledChanged(bool value)
@@ -205,9 +206,11 @@ private string GetVersionDescription()
var appName = Resources.AppDisplayName;
Version version = _interop.GetAppVersion();
- return $"{version.Major}.{version.Minor}.{(version.Build > -1 ? version.Build : 0)}";
+ return $"{version.Major}.{version.Minor}.{(version.Revision > -1 ? version.Revision : 0)}";
}
+ public void RetryConnection() => TriggerServerConnection(ServerHost, ServerPort, ServerPassword);
+
private void TriggerServerConnection(string host, int port, string pass)
{
IsCheckingServer = true;
@@ -252,7 +255,7 @@ private async Task UpdateServerVersionAsync()
if (!IsStreamingAvailable)
IsLocalPlaybackEnabled = false;
- }
+ }
else
{
ServerInfo = $"MPD Protocol {_mpdService.Version}\n" +
diff --git a/Sources/Stylophone.iOS/AppDelegate.cs b/Sources/Stylophone.iOS/AppDelegate.cs
index 407af79..1d4d465 100644
--- a/Sources/Stylophone.iOS/AppDelegate.cs
+++ b/Sources/Stylophone.iOS/AppDelegate.cs
@@ -15,6 +15,7 @@
using Microsoft.AppCenter.Crashes;
using System.Threading;
using AVFoundation;
+using System.Collections.Generic;
namespace Stylophone.iOS
{
@@ -28,6 +29,9 @@ public class AppDelegate : UIResponder, IUIApplicationDelegate
[Export("window")]
public UIWindow Window { get; set; }
+ public event EventHandler ApplicationWillResign;
+ public event EventHandler ApplicationWillBecomeActive;
+
public UISplitViewController RootViewController { get; set; }
public UIColor AppColor => UIColor.FromDynamicProvider((traitCollection) =>
@@ -66,6 +70,18 @@ public bool FinishedLaunching(UIApplication application, NSDictionary launchOpti
return true;
}
+ [Export("applicationWillResignActive:")]
+ public void OnResignActivation(UIApplication application)
+ {
+ ApplicationWillResign?.Invoke(this, EventArgs.Empty);
+ }
+
+ [Export("applicationDidBecomeActive:")]
+ public void OnActivated(UIApplication application)
+ {
+ ApplicationWillBecomeActive?.Invoke(this, EventArgs.Empty);
+ }
+
private async Task InitializeApplicationAsync()
{
var storageService = Ioc.Default.GetRequiredService();
@@ -112,6 +128,12 @@ await Ioc.Default.GetRequiredService().ExecuteOnUIThreadAsyn
// Initialize AppCenter
AppCenter.Start("90b62f5a-2448-4ef1-81ca-3fb807a5b126",
typeof(Analytics), typeof(Crashes));
+
+ AppDomain.CurrentDomain.UnhandledException += (sender, args) => {
+ var dict = new Dictionary();
+ dict.Add("exception", args.ExceptionObject.ToString());
+ Analytics.TrackEvent("UnhandledCrash", dict);
+ };
}
#endif
}
diff --git a/Sources/Stylophone.iOS/Entitlements.plist b/Sources/Stylophone.iOS/Entitlements.plist
index 36a8706..0c67376 100644
--- a/Sources/Stylophone.iOS/Entitlements.plist
+++ b/Sources/Stylophone.iOS/Entitlements.plist
@@ -1,6 +1,5 @@
-
-
+
diff --git a/Sources/Stylophone.iOS/Info.plist b/Sources/Stylophone.iOS/Info.plist
index 805f027..a804213 100644
--- a/Sources/Stylophone.iOS/Info.plist
+++ b/Sources/Stylophone.iOS/Info.plist
@@ -2,16 +2,12 @@
+ CFBundleDevelopmentRegion
+ en
CFBundleDisplayName
Stylophone
CFBundleIdentifier
- com.tvc-16.Stylophone
- CFBundleShortVersionString
- 2.5
- CFBundleVersion
- 2.5
- CFBundleDevelopmentRegion
- en
+ com.tvc16.stylophone
CFBundleLocalizations
en
@@ -19,6 +15,10 @@
pt
zh
+ CFBundleShortVersionString
+ 2.5.4
+ CFBundleVersion
+ 2.5.4
LSRequiresIPhoneOS
MinimumOSVersion
@@ -45,6 +45,7 @@
UISupportedInterfaceOrientations
UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
diff --git a/Sources/Stylophone.iOS/Services/DialogService.cs b/Sources/Stylophone.iOS/Services/DialogService.cs
index 78a276e..aad8b33 100644
--- a/Sources/Stylophone.iOS/Services/DialogService.cs
+++ b/Sources/Stylophone.iOS/Services/DialogService.cs
@@ -7,6 +7,7 @@
using Strings = Stylophone.Localization.Strings.Resources;
using UIKit;
using StoreKit;
+using Stylophone.Common.ViewModels;
namespace Stylophone.iOS.Services
{
@@ -15,12 +16,14 @@ public class DialogService : IDialogService
private IDispatcherService _dispatcherService;
private IApplicationStorageService _storageService;
private IInteropService _interop;
+ private INavigationService _navigationService;
private MPDConnectionService _mpdService;
- public DialogService(IDispatcherService dispatcherService, IApplicationStorageService storageService, IInteropService interop, MPDConnectionService mpdService)
+ public DialogService(IDispatcherService dispatcherService, IApplicationStorageService storageService, IInteropService interop, INavigationService navigationService, MPDConnectionService mpdService)
{
_dispatcherService = dispatcherService;
_storageService = storageService;
+ _navigationService = navigationService;
_mpdService = mpdService;
_interop = interop;
}
@@ -59,6 +62,7 @@ await _dispatcherService.ExecuteOnUIThreadAsync(async () =>
_storageService.SetValue("HasLaunchedOnce", true);
await ShowConfirmDialogAsync(Strings.FirstRunTitle, Strings.FirstRunText, Strings.OKButtonText);
+ _navigationService.Navigate();
}
});
}
diff --git a/Sources/Stylophone.iOS/Services/NotificationService.cs b/Sources/Stylophone.iOS/Services/NotificationService.cs
index ae2592e..819567c 100644
--- a/Sources/Stylophone.iOS/Services/NotificationService.cs
+++ b/Sources/Stylophone.iOS/Services/NotificationService.cs
@@ -19,6 +19,9 @@ public override void ShowInAppNotification(InAppNotification notification)
{
UIApplication.SharedApplication.InvokeOnMainThread(() =>
{
+ if (UIApplication.SharedApplication.ApplicationState != UIApplicationState.Active)
+ return;
+
RMessageType type = notification.NotificationType switch
{
NotificationType.Info => RMessageType.Normal,
diff --git a/Sources/Stylophone.iOS/Stylophone.iOS.csproj b/Sources/Stylophone.iOS/Stylophone.iOS.csproj
index b3e6584..87fcb18 100644
--- a/Sources/Stylophone.iOS/Stylophone.iOS.csproj
+++ b/Sources/Stylophone.iOS/Stylophone.iOS.csproj
@@ -39,6 +39,9 @@
None
x86_64
9.0
+ Automatique
+ iPhone Developer
+ true
true
@@ -66,6 +69,7 @@
iPhone Developer
SdkOnly
9.0
+ true
diff --git a/Sources/Stylophone.iOS/ViewControllers/LibraryViewController.cs b/Sources/Stylophone.iOS/ViewControllers/LibraryViewController.cs
index af83f99..fd1ef81 100644
--- a/Sources/Stylophone.iOS/ViewControllers/LibraryViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/LibraryViewController.cs
@@ -64,6 +64,14 @@ private async Task InitializeLibraryAsync()
CollectionView.PrefetchingEnabled = true;
UpdateDataSource();
+
+ // Start a timer to load visible items every sec -- a bit jank but eh
+ var timer = NSTimer.CreateRepeatingScheduledTimer(TimeSpan.FromSeconds(1), (NSTimer obj) =>
+ {
+ // Make sure we load the displayed items -- We don't specify IndexPaths here, the method picks up visible items automatically
+ CollectionView.PrefetchDataSource.PrefetchItems(CollectionView, new NSIndexPath[] { });
+ });
+ NSRunLoop.Current.AddTimer(timer, NSRunLoopMode.Common);
}
private void UpdateDataSource()
@@ -74,9 +82,6 @@ private void UpdateDataSource()
snapshot.AppendItems(items);
_dataSource.ApplySnapshot(snapshot, new NSString("base"), true);
-
- // Make sure we load the displayed items
- CollectionView.PrefetchDataSource.PrefetchItems(CollectionView, new NSIndexPath[] { });
}
private UICollectionViewCell GetAlbumViewCell(UICollectionView collectionView, NSIndexPath indexPath, NSObject identifier)
@@ -89,12 +94,6 @@ private UICollectionViewCell GetAlbumViewCell(UICollectionView collectionView, N
return cell;
}
- public override void Scrolled(UIScrollView scrollView)
- {
- var visibleIndexes = CollectionView.IndexPathsForVisibleItems;
-
- }
-
public override void ItemSelected(UICollectionView collectionView, NSIndexPath indexPath)
{
var holder = _dataSource.GetItemIdentifier(indexPath);
@@ -149,7 +148,7 @@ public CGSize GetSizeForItem(UICollectionView collectionView, UICollectionViewLa
if (referenceSize.Width < 600)
size = new CGSize(148, 148);
- if (referenceSize.Width < 350)
+ if (referenceSize.Width < 350 || referenceSize.Width == 490)
size = new CGSize(120, 120);
return size;
diff --git a/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs b/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs
index 2c8db93..662b91a 100644
--- a/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs
@@ -73,6 +73,7 @@ public override void AwakeFromNib()
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);
}
diff --git a/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs b/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs
index 188d7a9..ec61bf5 100644
--- a/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs
@@ -11,6 +11,7 @@
using UIKit;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Threading.Tasks;
namespace Stylophone.iOS.ViewControllers
{
@@ -31,6 +32,14 @@ public override void AwakeFromNib()
base.AwakeFromNib();
ViewModel.PropertyChanged += UpdateListOnPlaylistVersionChange;
+
+ (UIApplication.SharedApplication.Delegate as AppDelegate).ApplicationWillBecomeActive += OnLeavingBackground;
+ }
+
+ private void OnLeavingBackground(object sender, EventArgs e)
+ {
+ if (_mpdService.IsConnected)
+ Task.Run(async () => await ViewModel.LoadInitialDataAsync());
}
public override void ViewDidLoad()
@@ -64,11 +73,22 @@ 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(() =>
- TableView.ScrollToRow(NSIndexPath.FromRowSection(ViewModel.Source.IndexOf(playing), 0),
- UITableViewScrollPosition.Middle, true));
+ {
+ 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}");
+ }
+ });
}
private UIMenu GetRowContextMenu(NSIndexPath indexPath)
diff --git a/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs b/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs
index 557957d..b81da0e 100644
--- a/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs
@@ -60,6 +60,11 @@ public override void ViewDidLoad()
{
base.ViewDidLoad();
+ // Trick iOS into showing us the local network access prompt,
+ // So that the user can accept/decline it before we actually try to connect to MPD servers.
+ // cf. https://stackoverflow.com/questions/63940427/ios-14-how-to-trigger-local-network-dialog-and-check-user-answer
+ var hostName = NSProcessInfo.ProcessInfo.HostName;
+
// Value Transformers
var negateBoolTransformer = NSValueTransformer.GetValueTransformer(nameof(ReverseBoolValueTransformer));
var intToStringTransformer = NSValueTransformer.GetValueTransformer(nameof(IntToStringValueTransformer));
@@ -93,6 +98,20 @@ public override void ViewDidLoad()
GithubButton.PrimaryActionTriggered += (s, e) =>
UIApplication.SharedApplication.OpenUrl(new NSUrl(Resources.SettingsGithubLink));
+ ServerHostnameField.PrimaryActionTriggered += (s, e) => {
+ ((UITextField)s).ResignFirstResponder();
+ ViewModel.RetryConnection();
+ };
+ ServerPortField.PrimaryActionTriggered += (s, e) =>
+ {
+ ((UITextField)s).ResignFirstResponder();
+ ViewModel.RetryConnection();
+ };
+ ServerPasswordField.PrimaryActionTriggered += (s, e) => {
+ ((UITextField)s).ResignFirstResponder();
+ ViewModel.RetryConnection();
+ };
+
}
}
diff --git a/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.cs b/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.cs
index 0d2b27f..47bf673 100644
--- a/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.cs
@@ -36,7 +36,18 @@ public override void AwakeFromNib()
Layer.ShadowOffset = new CGSize(0, 0);
Layer.ShadowRadius = 4;
+ // Add shadow to albumart
+ ShadowCaster.Layer.MasksToBounds = false;
+ ShadowCaster.Layer.CornerRadius = 8;
+ ShadowCaster.Layer.ShadowColor = UIColor.Black.CGColor;
+ ShadowCaster.Layer.ShadowOpacity = 0.5F;
+ ShadowCaster.Layer.ShadowOffset = new CGSize(0, 0);
+ ShadowCaster.Layer.ShadowRadius = 4;
+
AlbumArt.Layer.CornerRadius = 8;
+
+ // Fallback default
+ TrackTitle.Text = Localization.Strings.Resources.NotificationNoTrackPlaying;
}
public override void LayoutSubviews()
diff --git a/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.designer.cs b/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.designer.cs
index ed458e1..6b2ee3b 100644
--- a/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.designer.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.designer.cs
@@ -42,6 +42,9 @@ partial class CompactPlaybackView
[Outlet]
public UIKit.UIButton PrevButton { get; private set; }
+ [Outlet]
+ UIKit.UIView ShadowCaster { get; set; }
+
[Outlet]
public UIKit.UIButton ShuffleButton { get; private set; }
@@ -117,6 +120,11 @@ void ReleaseDesignerOutlets ()
VolumeButton.Dispose ();
VolumeButton = null;
}
+
+ if (ShadowCaster != null) {
+ ShadowCaster.Dispose ();
+ ShadowCaster = null;
+ }
}
}
}
diff --git a/Sources/Stylophone.iOS/ViewModels/PlaybackViewModel.cs b/Sources/Stylophone.iOS/ViewModels/PlaybackViewModel.cs
index 0a8be8f..e5e42f4 100644
--- a/Sources/Stylophone.iOS/ViewModels/PlaybackViewModel.cs
+++ b/Sources/Stylophone.iOS/ViewModels/PlaybackViewModel.cs
@@ -17,12 +17,14 @@ public class PlaybackViewModel : PlaybackViewModelBase
public PlaybackViewModel(INavigationService navigationService, INotificationService notificationService, IDispatcherService dispatcherService, IInteropService interop, MPDConnectionService mpdService, TrackViewModelFactory trackVmFactory, LocalPlaybackViewModel localPlayback) :
base(navigationService, notificationService, dispatcherService, interop, mpdService, trackVmFactory, localPlayback)
{
- //Application.Current.LeavingBackground += CurrentOnLeavingBackground;
+ (UIApplication.SharedApplication.Delegate as AppDelegate).ApplicationWillBecomeActive += OnLeavingBackground;
- ((NavigationService)_navigationService).Navigated += (s, e) =>
- _dispatcherService.ExecuteOnUIThreadAsync(() => {
- //ShowTrackName = _navigationService.CurrentPageViewModelType != typeof(PlaybackViewModelBase);
- });
+ }
+
+ private void OnLeavingBackground(object sender, EventArgs e)
+ {
+ // Refresh all
+ UpdateInformation(sender, null);
}
public override Task SwitchToCompactViewAsync(EventArgs obj)
diff --git a/Sources/Stylophone.iOS/Views/NowPlaying.storyboard b/Sources/Stylophone.iOS/Views/NowPlaying.storyboard
index 0efd25b..03699eb 100644
--- a/Sources/Stylophone.iOS/Views/NowPlaying.storyboard
+++ b/Sources/Stylophone.iOS/Views/NowPlaying.storyboard
@@ -21,42 +21,42 @@
-
+
-
+
-
+
-
+
-
+
-
-
-
+
+
@@ -497,7 +501,9 @@
+
+
@@ -523,7 +529,9 @@
+
+
@@ -546,11 +554,11 @@
+
-
@@ -575,8 +583,8 @@
-
+
@@ -639,6 +647,7 @@
+
diff --git a/Sources/Stylophone.iOS/Views/Queue.storyboard b/Sources/Stylophone.iOS/Views/Queue.storyboard
index 400e84e..ed9a204 100644
--- a/Sources/Stylophone.iOS/Views/Queue.storyboard
+++ b/Sources/Stylophone.iOS/Views/Queue.storyboard
@@ -1,6 +1,6 @@
-
+
@@ -12,21 +12,21 @@
-
+
-
+
-
+
-
+
@@ -35,7 +35,7 @@
-
+
@@ -56,17 +56,17 @@
-
+
-
+
-
+
-
+
@@ -76,14 +76,14 @@
-
+
-
+
@@ -94,7 +94,7 @@
-
+
diff --git a/Sources/Stylophone.iOS/Views/Settings.storyboard b/Sources/Stylophone.iOS/Views/Settings.storyboard
index f23c619..35f1c1b 100644
--- a/Sources/Stylophone.iOS/Views/Settings.storyboard
+++ b/Sources/Stylophone.iOS/Views/Settings.storyboard
@@ -16,17 +16,17 @@
-
+
-
+
-
+
@@ -35,7 +35,7 @@
-
+
@@ -59,10 +59,10 @@
-
+
-
+
@@ -83,7 +83,7 @@
-
+
@@ -95,10 +95,10 @@
-
+
-
+
@@ -119,7 +119,7 @@
-
+
@@ -131,7 +131,7 @@
-
+
@@ -141,7 +141,7 @@
-
+
@@ -184,6 +184,7 @@
+
@@ -193,7 +194,7 @@
-
+
@@ -231,7 +232,7 @@
-
+
@@ -263,7 +264,7 @@
-
+
@@ -283,7 +284,7 @@
-
+
@@ -307,7 +308,7 @@
-
+
@@ -343,7 +344,7 @@
-
+
@@ -395,13 +396,13 @@
-
+
-
+
-
-
+
-
+
diff --git a/Sources/Stylophone/Package.appxmanifest b/Sources/Stylophone/Package.appxmanifest
index 4f4a9d4..04666c7 100644
--- a/Sources/Stylophone/Package.appxmanifest
+++ b/Sources/Stylophone/Package.appxmanifest
@@ -12,7 +12,7 @@
+ Version="2.5.2.0" />
diff --git a/Sources/Stylophone/Package.tt b/Sources/Stylophone/Package.tt
index a2a9d64..f796b29 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.0.0";
+ string version = "2.5.2.0";
// Get configuration name at Build time
string configName = Host.ResolveParameterValue("-", "-", "BuildConfiguration");
diff --git a/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml b/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml
index 821a2d4..0265fe1 100644
--- a/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml
+++ b/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml
@@ -360,6 +360,7 @@
Height="46"
Margin="{StaticResource SmallRightMargin}"
Click="{x:Bind PlaybackViewModel.ToggleMute}"
+ IsEnabled="{x:Bind PlaybackViewModel.CanSetVolume, Mode=OneWay}"
Content="{x:Bind PlaybackViewModel.VolumeIcon, Mode=OneWay}"
FontSize="21"
Style="{StaticResource SVButtonStyle}"
@@ -373,6 +374,7 @@
Minimum="0"
Orientation="Horizontal"
PointerWheelChanged="Volume_PointerWheelChanged"
+ IsEnabled="{x:Bind PlaybackViewModel.CanSetVolume, Mode=OneWay}"
Value="{x:Bind PlaybackViewModel.MediaVolume, Mode=TwoWay}" />
@@ -438,6 +440,7 @@
Height="46"
Margin="0,0,8,0"
Click="{x:Bind PlaybackViewModel.ToggleMute}"
+ IsEnabled="{x:Bind PlaybackViewModel.CanSetVolume, Mode=OneWay}"
Content="{x:Bind PlaybackViewModel.VolumeIcon, Mode=OneWay}"
FontSize="21"
Style="{StaticResource SVButtonStyle}" />
@@ -449,6 +452,7 @@
Minimum="0"
Orientation="Horizontal"
PointerWheelChanged="Volume_PointerWheelChanged"
+ IsEnabled="{x:Bind PlaybackViewModel.CanSetVolume, Mode=OneWay}"
Value="{x:Bind PlaybackViewModel.MediaVolume, Mode=TwoWay}" />