diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index 8ff558de..176291f4 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -65,6 +65,10 @@ jobs:
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v1.0.2
+ - name: Add Toolkit Labs nuget feed
+ run: |
+ nuget sources add -name "CommunityToolkit-Labs" -source "https://pkgs.dev.azure.com/dotnet/CommunityToolkit/_packaging/CommunityToolkit-Labs/nuget/v3/index.json"
+
# Restore the application
- name: Restore the application
working-directory: ./Sources
diff --git a/README.md b/README.md
index c04b8de4..71c440b6 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,12 @@
Stylophone
===========
-[**Music Player Daemon**](https://www.musicpd.org/) Client for UWP and iOS/iPadOS.
+[**Music Player Daemon**](https://www.musicpd.org/) Client for Windows, Xbox, macOS 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)
+[](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)
+
+Get the macOS version for free from the [Releases!](https://github.com/Difegue/Stylophone/releases)
[Buy a sticker if you want!](https://ko-fi.com/s/9fcf421b6e)
@@ -16,6 +18,7 @@ Based on [MpcNET](https://github.com/Difegue/MpcNET), my own fork of the origina
* Playlist management (Create, Add/Remove tracks, Delete)
* Picture-in-picture mode
* Live tile on Windows 10
+* Local playback support if your MPD server has `httpd` as an output
* Integration with native playback controls
* Browse library by albums, or directly by folders
* All data is pulled from your MPD Server only
@@ -31,6 +34,23 @@ There is a workaround you can use with checknetisolation which should work:
checknetisolation loopbackexempt -a -n="13459Difegue.Stylophone_zd7bwy3j4yjfy"
```
+## Protocol usage (Windows only)
+
+Stylophone can be launched through the `stylophone://` protocol on Windows devices; This feature also makes it so you can control some features of the app through protocol invocations.
+
+The following URLs are supported:
+
+- `stylophone://?verb=stylophone_play` or `stylophone://?verb=stylophone_pause` : Toggle playback status
+- `stylophone://?verb=stylophone_stop` : Stop playback
+- `stylophone://?verb=stylophone_next` : Go to next track
+- `stylophone://?verb=stylophone_prev` : Go to previous track
+- `stylophone://?verb=stylophone_shuffle` : Toggle shuffle
+- `stylophone://?verb=stylophone_volume_up` : Raise volume
+- `stylophone://?verb=stylophone_volume_down` : Lower volume
+- `stylophone://?verb=stylophone_volume_set&volume=50` : Set volume to desired value
+- `stylophone://?verb=stylophone_seek&seek=50` : Seek to desired position in current track, in seconds
+- `stylophone://?verb=stylophone_load_playlistt&playlist=YourPlaylistName` : Load the desired playlist in queue
+
## Translation
You can easily contribute translations to Stylophone! To help translate, follow these instructions.
diff --git a/Sources/Stylophone.Common/Services/AlbumArtService.cs b/Sources/Stylophone.Common/Services/AlbumArtService.cs
index 0866df0f..5f627e56 100644
--- a/Sources/Stylophone.Common/Services/AlbumArtService.cs
+++ b/Sources/Stylophone.Common/Services/AlbumArtService.cs
@@ -246,14 +246,26 @@ internal async Task LoadImageFromFile(string fileName)
private SKBitmap ImageFromBytes(byte[] bytes)
{
- SKBitmap image = SKBitmap.Decode(bytes);
+ try
+ {
+ SKBitmap image = SKBitmap.Decode(bytes);
- // Resize overly large images to reduce OOM risk. Is 2048 too small ?
- if (image.Width > 2048)
+ if (image == null)
+ return null;
+
+ // Resize overly large images to reduce OOM risk. Is 2048 too small ?
+ if (image.Width > 2048)
+ {
+ image.Resize(new SKImageInfo(2048, 2048 * image.Height / image.Width), SKFilterQuality.High);
+ }
+ return image;
+ }
+ catch (Exception e)
{
- image.Resize(new SKImageInfo(2048, 2048 * image.Height / image.Width), SKFilterQuality.High);
+ Debug.WriteLine("Exception caught while loading albumart from MPD response: " + e);
+ _notificationService.ShowErrorNotification(e);
+ return null;
}
- return image;
}
}
diff --git a/Sources/Stylophone.Common/Stylophone.Common.csproj b/Sources/Stylophone.Common/Stylophone.Common.csproj
index 0c2ffb0a..269cef3a 100644
--- a/Sources/Stylophone.Common/Stylophone.Common.csproj
+++ b/Sources/Stylophone.Common/Stylophone.Common.csproj
@@ -7,13 +7,13 @@
-
+
-
-
-
-
-
+
+
+
+
+
diff --git a/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs b/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs
index d852b8a2..9c3bf3bc 100644
--- a/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs
@@ -128,7 +128,8 @@ private void CreateTrackViewModels()
Source.Add(_trackVmFactory.GetTrackViewModel(file));
}
- var totalTime = Source.Select(vm => vm.File.Time).Aggregate((t1, t2) => t1 + t2);
+ var times = Source.Select(vm => vm.File.Time);
+ var totalTime = times.Count() > 0 ? times.Aggregate((t1, t2) => t1 + t2) : 0;
TimeSpan t = TimeSpan.FromSeconds(totalTime);
PlaylistInfo = $"{Source.Count} Tracks, Total Time: {t.ToReadableString()}";
diff --git a/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs b/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs
index 2d7fec34..256e884a 100644
--- a/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs
+++ b/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs
@@ -50,11 +50,12 @@ public async Task LoadDataAsync()
var albumList = await _mpdService.SafelySendCommandAsync(new ListCommand(MpdTags.Album));
var albumSortList = await _mpdService.SafelySendCommandAsync(new ListCommand(MpdTags.AlbumSort));
- // Create a list of tuples
- var response = albumList.Zip(albumSortList, (album, albumSort) => new Album{ Name = album, SortName = albumSort });
-
- if (albumSortList != null)
+ if (albumList != null && albumSortList != null)
+ {
+ // Create a list of tuples
+ var response = albumList.Zip(albumSortList, (album, albumSort) => new Album { Name = album, SortName = albumSort });
GroupAlbumsByName(response);
+ }
if (Source.Count > 0)
FilteredSource.AddRange(Source);
diff --git a/Sources/Stylophone.Common/ViewModels/Items/OutputViewModel.cs b/Sources/Stylophone.Common/ViewModels/Items/OutputViewModel.cs
new file mode 100644
index 00000000..93413a79
--- /dev/null
+++ b/Sources/Stylophone.Common/ViewModels/Items/OutputViewModel.cs
@@ -0,0 +1,42 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using MpcNET;
+using MpcNET.Commands.Output;
+using MpcNET.Types;
+using Stylophone.Common.Interfaces;
+using Stylophone.Common.Services;
+
+namespace Stylophone.Common.ViewModels.Items
+{
+ public partial class OutputViewModel: ObservableObject
+ {
+ [ObservableProperty]
+ private string _name;
+
+ [ObservableProperty]
+ private string _plugin;
+
+ [ObservableProperty]
+ private bool _isEnabled;
+
+ private int _id;
+
+ public OutputViewModel() { }
+
+ public OutputViewModel(MpdOutput o)
+ {
+ _id = o.Id;
+ _name = o.Name;
+ _plugin = o.Plugin;
+ _isEnabled = o.IsEnabled;
+ }
+
+ partial void OnIsEnabledChanged(bool value)
+ {
+ IMpcCommand command = value ? new EnableOutputCommand(_id) : new DisableOutputCommand(_id);
+
+ Ioc.Default.GetRequiredService().SafelySendCommandAsync(command);
+ }
+
+ }
+}
diff --git a/Sources/Stylophone.Common/ViewModels/LocalPlaybackViewModel.cs b/Sources/Stylophone.Common/ViewModels/LocalPlaybackViewModel.cs
index 4b07cedb..5fefbec9 100644
--- a/Sources/Stylophone.Common/ViewModels/LocalPlaybackViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/LocalPlaybackViewModel.cs
@@ -19,6 +19,7 @@ public partial class LocalPlaybackViewModel : ViewModelBase
private LibVLC _vlcCore;
private MediaPlayer _mediaPlayer;
private string _serverHost;
+ private int _serverPort;
public LocalPlaybackViewModel(SettingsViewModel settingsVm, MPDConnectionService mpdService, IInteropService interopService, INotificationService notificationService, IDispatcherService dispatcherService) : base(dispatcherService)
{
@@ -37,6 +38,9 @@ public LocalPlaybackViewModel(SettingsViewModel settingsVm, MPDConnectionService
if (e.PropertyName == nameof(_settingsVm.ServerHost))
_serverHost = _settingsVm.ServerHost;
+
+ if (e.PropertyName == nameof(_settingsVm.LocalPlaybackPort))
+ _serverPort = _settingsVm.LocalPlaybackPort;
};
// Run an idle loop in a spare thread to make sure the libVLC volume is always accurate
@@ -57,9 +61,10 @@ public LocalPlaybackViewModel(SettingsViewModel settingsVm, MPDConnectionService
});
}
- public void Initialize(string host, bool isEnabled)
+ public void Initialize(string host, int port, bool isEnabled)
{
_serverHost = host;
+ _serverPort = port;
IsEnabled = isEnabled;
}
@@ -147,7 +152,7 @@ partial void OnIsPlayingChanged(bool value)
{
if (value && _serverHost != null && _mpdService.IsConnected)
{
- var urlString = "http://" + _serverHost + ":8000";
+ var urlString = "http://" + _serverHost + ":" + _serverPort;
var streamUrl = new Uri(urlString);
var media = new Media(_vlcCore, streamUrl);
diff --git a/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs b/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs
index 85f28524..e43a85b5 100644
--- a/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs
+++ b/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs
@@ -1,4 +1,6 @@
using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -7,8 +9,10 @@
using CommunityToolkit.Mvvm.Input;
using MpcNET.Commands.Output;
using MpcNET.Commands.Status;
+using MpcNET.Types;
using Stylophone.Common.Interfaces;
using Stylophone.Common.Services;
+using Stylophone.Common.ViewModels.Items;
using Stylophone.Localization.Strings;
namespace Stylophone.Common.ViewModels
@@ -75,6 +79,12 @@ public SettingsViewModel(MPDConnectionService mpdService, IApplicationStorageSer
[ObservableProperty]
private bool _isLocalPlaybackEnabled;
+ [ObservableProperty]
+ private int _localPlaybackPort;
+
+ [ObservableProperty]
+ private ObservableCollection _outputs = new();
+
partial void OnElementThemeChanged(Theme value)
{
Task.Run (async () => await _interop.SetThemeAsync(value));
@@ -123,6 +133,11 @@ partial void OnIsLocalPlaybackEnabledChanged(bool value)
_applicationStorageService.SetValue(nameof(IsLocalPlaybackEnabled), value);
}
+ partial void OnLocalPlaybackPortChanged(int value)
+ {
+ _applicationStorageService.SetValue(nameof(LocalPlaybackPort), value);
+ }
+
[RelayCommand]
private async Task ClearCacheAsync()
@@ -176,6 +191,7 @@ public async Task EnsureInstanceInitializedAsync()
_enableAnalytics = _applicationStorageService.GetValue(nameof(EnableAnalytics), true);
_isAlbumArtFetchingEnabled = _applicationStorageService.GetValue(nameof(IsAlbumArtFetchingEnabled), true);
_isLocalPlaybackEnabled = _applicationStorageService.GetValue(nameof(IsLocalPlaybackEnabled));
+ _localPlaybackPort = _applicationStorageService.GetValue(nameof(LocalPlaybackPort), 8000);
Enum.TryParse(_applicationStorageService.GetValue(nameof(ElementTheme)), out _elementTheme);
@@ -199,7 +215,6 @@ private async Task CheckUpdatingDbAsync()
private string GetVersionDescription()
{
- var appName = Resources.AppDisplayName;
Version version = _interop.GetAppVersion();
return $"{version.Major}.{version.Minor}.{(version.Revision > -1 ? version.Revision : 0)}";
@@ -232,13 +247,18 @@ private async Task UpdateServerVersionAsync()
lastUpdatedDb = DateTimeOffset.FromUnixTimeSeconds(db_update).UtcDateTime;
}
- // Build info string
+ // Get server outputs
var outputs = await _mpdService.SafelySendCommandAsync(new OutputsCommand());
+ Outputs.Clear();
+
+ foreach (var o in outputs)
+ Outputs.Add(new OutputViewModel(o));
var songs = response.ContainsKey("songs") ? response["songs"] : "??";
var albums = response.ContainsKey("albums") ? response["albums"] : "??";
- if (outputs != null && outputs.Count() > 0)
+ // Build info string
+ if (outputs?.Count() > 0)
{
var outputString = outputs.Select(o => o.Plugin).Aggregate((s, s2) => $"{s}, {s2}");
@@ -247,7 +267,7 @@ private async Task UpdateServerVersionAsync()
$"Database last updated {lastUpdatedDb}\n" +
$"Outputs available: {outputString}";
- IsStreamingAvailable = outputs.Select(o => o.Plugin).Contains("httpd");
+ IsStreamingAvailable = outputs.Any(o => o.Plugin.Contains("httpd"));
if (!IsStreamingAvailable)
IsLocalPlaybackEnabled = false;
diff --git a/Sources/Stylophone.Localization/Strings/Resources.Designer.cs b/Sources/Stylophone.Localization/Strings/Resources.Designer.cs
index dd3195d1..6d5feebe 100644
--- a/Sources/Stylophone.Localization/Strings/Resources.Designer.cs
+++ b/Sources/Stylophone.Localization/Strings/Resources.Designer.cs
@@ -974,6 +974,24 @@ public static string SettingsLocalPlaybackHeader {
}
}
+ ///
+ /// Looks up a localized string similar to Stream port.
+ ///
+ public static string SettingsLocalPlaybackPortHeader {
+ get {
+ return ResourceManager.GetString("SettingsLocalPlaybackPortHeader", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Set this to the port of your server's httpd stream..
+ ///
+ public static string SettingsLocalPlaybackPortText {
+ get {
+ return ResourceManager.GetString("SettingsLocalPlaybackPortText", resourceCulture);
+ }
+ }
+
///
/// 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..
@@ -993,6 +1011,24 @@ public static string SettingsNoServerError {
}
}
+ ///
+ /// Looks up a localized string similar to Server Outputs.
+ ///
+ public static string SettingsOutputsHeader {
+ get {
+ return ResourceManager.GetString("SettingsOutputsHeader", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Enable or disable the server's music outputs. .
+ ///
+ public static string SettingsOutputsText {
+ get {
+ return ResourceManager.GetString("SettingsOutputsText", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to MPD Server.
///
diff --git a/Sources/Stylophone.Localization/Strings/Resources.en-US.resx b/Sources/Stylophone.Localization/Strings/Resources.en-US.resx
index 50ac5315..f208923d 100644
--- a/Sources/Stylophone.Localization/Strings/Resources.en-US.resx
+++ b/Sources/Stylophone.Localization/Strings/Resources.en-US.resx
@@ -488,4 +488,16 @@ Enabling this option will show a second volume slider to control local volume.
Song playback
+
+ Stream port
+
+
+ Set this to the port of your server's httpd stream.
+
+
+ Server Outputs
+
+
+ Enable or disable the server's music outputs.
+
\ 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 2820b28c..4b67747d 100644
--- a/Sources/Stylophone.Localization/Strings/Resources.fr-FR.resx
+++ b/Sources/Stylophone.Localization/Strings/Resources.fr-FR.resx
@@ -464,7 +464,7 @@ L'activation de cette option affichera un second slider pour contrôler le volum
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)".
- Récupérer les pochettes d'album depuis le serveur MPD
+ Télécharger les pochettes d'album depuis le serveur MPD
Stylophone stocke les pochettes d'album sur votre ordinateur pour éviter de surcharger votre serveur MPD.
@@ -487,4 +487,16 @@ L'activation de cette option affichera un second slider pour contrôler le volum
Lecture de la piste
+
+ Port du stream
+
+
+ Ajustez ce paramètre pour correspondre au port de votre stream httpd sur le serveur.
+
+
+ Sorties du serveur
+
+
+ Activez ou désactivez les sorties audio du serveur.
+
\ 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 135d070a..4ca659db 100644
--- a/Sources/Stylophone.Localization/Strings/Resources.pt-PT.resx
+++ b/Sources/Stylophone.Localization/Strings/Resources.pt-PT.resx
@@ -484,4 +484,16 @@ Ativando esta opção mostrará um segundo deslizador de volume para controlar o
Leitura da faixa
+
+ Porta do fluxo
+
+
+ Introduze a porta do fluxo httpd do seu servidor MPD aqui.
+
+
+ Saidas do servidor
+
+
+ Active o deactive as saidas audio do servidor MPD.
+
\ No newline at end of file
diff --git a/Sources/Stylophone.Localization/Strings/Resources.resx b/Sources/Stylophone.Localization/Strings/Resources.resx
index b18cecb7..b0f22881 100644
--- a/Sources/Stylophone.Localization/Strings/Resources.resx
+++ b/Sources/Stylophone.Localization/Strings/Resources.resx
@@ -488,4 +488,16 @@ Enabling this option will show a second volume slider to control local volume.
Song playback
+
+ Stream port
+
+
+ Set this to the port of your server's httpd stream.
+
+
+ Server Outputs
+
+
+ Enable or disable the server's music outputs.
+
\ No newline at end of file
diff --git a/Sources/Stylophone.iOS/AppDelegate.cs b/Sources/Stylophone.iOS/AppDelegate.cs
index 1d4d4658..711fad24 100644
--- a/Sources/Stylophone.iOS/AppDelegate.cs
+++ b/Sources/Stylophone.iOS/AppDelegate.cs
@@ -60,7 +60,8 @@ public bool FinishedLaunching(UIApplication application, NSDictionary launchOpti
// Enable Now Playing integration
application.BeginReceivingRemoteControlEvents();
AVAudioSession.SharedInstance().SetCategory(AVAudioSessionCategory.Playback);
-
+ //AVAudioSession.SharedInstance().SetPrefersNoInterruptionsFromSystemAlerts(true, out _);
+
// Override point for customization after application launch
Task.Run(async () => await InitializeApplicationAsync());
@@ -82,6 +83,22 @@ public void OnActivated(UIApplication application)
ApplicationWillBecomeActive?.Invoke(this, EventArgs.Empty);
}
+ public override void BuildMenu(IUIMenuBuilder builder)
+ {
+ base.BuildMenu(builder);
+
+ builder.RemoveMenu(UIMenuIdentifier.Edit.GetConstant());
+ builder.RemoveMenu(UIMenuIdentifier.Font.GetConstant());
+ builder.RemoveMenu(UIMenuIdentifier.Format.GetConstant());
+ builder.RemoveMenu(UIMenuIdentifier.Services.GetConstant());
+ builder.RemoveMenu(UIMenuIdentifier.Hide.GetConstant());
+
+ builder.RemoveMenu(UIMenuIdentifier.File.GetConstant());
+ builder.RemoveMenu(UIMenuIdentifier.Document.GetConstant());
+
+ builder.System.SetNeedsRebuild();
+ }
+
private async Task InitializeApplicationAsync()
{
var storageService = Ioc.Default.GetRequiredService();
@@ -89,10 +106,11 @@ private async Task InitializeApplicationAsync()
var host = storageService.GetValue(nameof(SettingsViewModel.ServerHost));
var port = storageService.GetValue(nameof(SettingsViewModel.ServerPort), 6600);
var pass = storageService.GetValue(nameof(SettingsViewModel.ServerPassword));
- var localPlaybackEnabled = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.IsLocalPlaybackEnabled));
+ var localPlaybackEnabled = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.IsLocalPlaybackEnabled));
+ var localPlaybackPort = storageService.GetValue(nameof(SettingsViewModel.LocalPlaybackPort), 8000);
var localPlaybackVm = Ioc.Default.GetRequiredService();
- localPlaybackVm.Initialize(host, localPlaybackEnabled);
+ localPlaybackVm.Initialize(host, localPlaybackPort, localPlaybackEnabled);
var mpdService = Ioc.Default.GetRequiredService();
mpdService.SetServerInfo(host, port, pass);
diff --git a/Sources/Stylophone.iOS/Helpers/TrackTableViewDataSource.cs b/Sources/Stylophone.iOS/Helpers/TrackTableViewDataSource.cs
index 3bd07597..861a1c64 100644
--- a/Sources/Stylophone.iOS/Helpers/TrackTableViewDataSource.cs
+++ b/Sources/Stylophone.iOS/Helpers/TrackTableViewDataSource.cs
@@ -135,6 +135,14 @@ public void MoveRow(UITableView tableView, NSIndexPath sourceIndexPath, NSIndexP
#region Delegate
+ public override bool ShouldBeginMultipleSelectionInteraction(UITableView tableView, NSIndexPath indexPath)
+ => _canReorder;
+
+ public override void DidBeginMultipleSelectionInteraction(UITableView tableView, NSIndexPath indexPath)
+ {
+ tableView.SetEditing(true, true);
+ }
+
// 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;
diff --git a/Sources/Stylophone.iOS/Info.plist b/Sources/Stylophone.iOS/Info.plist
index ca943028..cd0472bd 100644
--- a/Sources/Stylophone.iOS/Info.plist
+++ b/Sources/Stylophone.iOS/Info.plist
@@ -16,11 +16,11 @@
zh
CFBundleShortVersionString
- 2.6.1
+ 2.7.0
CFBundleVersion
- 2.6.1
+ 2.7.0.0
LSRequiresIPhoneOS
-
+
MinimumOSVersion
16.0
UIBackgroundModes
diff --git a/Sources/Stylophone.iOS/Resources/PrivacyInfo.xcprivacy b/Sources/Stylophone.iOS/Resources/PrivacyInfo.xcprivacy
new file mode 100644
index 00000000..dd61db72
--- /dev/null
+++ b/Sources/Stylophone.iOS/Resources/PrivacyInfo.xcprivacy
@@ -0,0 +1,43 @@
+
+
+
+
+ NSPrivacyAccessedAPITypes
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryFileTimestamp
+ NSPrivacyAccessedAPITypeReasons
+
+ C617.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategorySystemBootTime
+ NSPrivacyAccessedAPITypeReasons
+
+ 35F9.1
+
+
+
+ NSPrivacyAccessedAPIType
+ NSPrivacyAccessedAPICategoryDiskSpace
+ NSPrivacyAccessedAPITypeReasons
+
+ E174.1
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Sources/Stylophone.iOS/Services/InteropService.cs b/Sources/Stylophone.iOS/Services/InteropService.cs
index 78f401f8..2d275d57 100644
--- a/Sources/Stylophone.iOS/Services/InteropService.cs
+++ b/Sources/Stylophone.iOS/Services/InteropService.cs
@@ -7,16 +7,14 @@
using UIKit;
using SkiaSharp.Views.iOS;
using Foundation;
+using CommunityToolkit.Mvvm.DependencyInjection;
namespace Stylophone.iOS.Services
{
public class InteropService: IInteropService
{
- private NowPlayingService _nowPlayingService;
-
- public InteropService(NowPlayingService nowPlayingService)
+ public InteropService()
{
- _nowPlayingService = nowPlayingService;
}
public Task SetThemeAsync(Theme theme)
@@ -61,8 +59,9 @@ public async Task GetPlaceholderImageAsync()
public async Task UpdateOperatingSystemIntegrationsAsync(TrackViewModel currentTrack)
{
- await _nowPlayingService.UpdateMetadataAsync(currentTrack);
- //await LiveTileHelper.UpdatePlayingSongAsync(currentTrack);
+ // This needs to be instantiated lazily since NPService depends on LocalPlayback, which depends on Interop already..
+ var nowPlayingService = Ioc.Default.GetRequiredService();
+ await nowPlayingService.UpdateMetadataAsync(currentTrack);
}
public Version GetAppVersion()
diff --git a/Sources/Stylophone.iOS/Services/NowPlayingService.cs b/Sources/Stylophone.iOS/Services/NowPlayingService.cs
index 2dbd4fca..c25067ed 100644
--- a/Sources/Stylophone.iOS/Services/NowPlayingService.cs
+++ b/Sources/Stylophone.iOS/Services/NowPlayingService.cs
@@ -21,17 +21,41 @@ public class NowPlayingService
private MPDConnectionService _mpdService;
private IApplicationStorageService _storageService;
- private AVAudioPlayer _silencePlayer;
+ private AVAudioPlayer? _silencePlayer;
- public NowPlayingService(MPDConnectionService mpdService, IApplicationStorageService storageService)
+ private bool _localPlaybackWasInterrupted;
+
+ public NowPlayingService(MPDConnectionService mpdService, LocalPlaybackViewModel localPlaybackVm, IApplicationStorageService storageService)
{
_mpdService = mpdService;
_storageService = storageService;
// https://stackoverflow.com/questions/48289037/using-mpnowplayinginfocenter-without-actually-playing-audio
// TODO This breaks when LibVLC playback stops
+ if (new NSProcessInfo().IsMacCatalystApplication)
+ return;
+
_silencePlayer = new AVAudioPlayer(new NSUrl("silence.wav",false,NSBundle.MainBundle.ResourceUrl), null, out var error);
_silencePlayer.NumberOfLoops = -1;
+
+ // Listen for AVAudio interruptions (eg phone calls)
+ AVAudioSession.Notifications.ObserveInterruption((s, e) =>
+ {
+ Task.Run(() =>
+ {
+ // The interruption always seems to be marked as ended, but using a bool of our own to track interrupting works well enough.
+ if (localPlaybackVm.IsPlaying)
+ {
+ localPlaybackVm.Stop();
+ _localPlaybackWasInterrupted = true;
+ }
+ else if (_localPlaybackWasInterrupted)
+ {
+ localPlaybackVm.Resume();
+ _localPlaybackWasInterrupted = false;
+ }
+ });
+ });
}
public void Initialize()
@@ -111,19 +135,19 @@ private void UpdateState(MpdStatus status)
switch (status.State)
{
case MpdState.Play:
- _silencePlayer.Play();
+ _silencePlayer?.Play();
_nowPlayingInfo.PlaybackRate = 1;
break;
case MpdState.Pause:
- _silencePlayer.Stop();
+ _silencePlayer?.Stop();
_nowPlayingInfo.PlaybackRate = 0;
break;
case MpdState.Stop:
- _silencePlayer.Stop();
+ _silencePlayer?.Stop();
_nowPlayingInfo.PlaybackRate = 0;
break;
case MpdState.Unknown:
- _silencePlayer.Stop();
+ _silencePlayer?.Stop();
_nowPlayingInfo.PlaybackRate = 0;
break;
default:
diff --git a/Sources/Stylophone.iOS/Stylophone.iOS.csproj b/Sources/Stylophone.iOS/Stylophone.iOS.csproj
index 6277b647..b39ba299 100644
--- a/Sources/Stylophone.iOS/Stylophone.iOS.csproj
+++ b/Sources/Stylophone.iOS/Stylophone.iOS.csproj
@@ -1,6 +1,7 @@
- net7.0-ios;net7.0-maccatalyst
+ net8.0-ios;net8.0-maccatalyst
+
Exe
enable
true
@@ -58,15 +59,18 @@
SymbolUIButton.cs
+
+ ServerOutputCell.cs
+
-
-
-
-
-
+
+
+
+
+
-
+
diff --git a/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs b/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs
index b81da0e3..bcc5214d 100644
--- a/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs
@@ -8,13 +8,17 @@
using Stylophone.iOS.Helpers;
using Stylophone.Localization.Strings;
using UIKit;
+using System.Collections.ObjectModel;
+using Stylophone.Common.ViewModels.Items;
+using System.Collections.Specialized;
+using Stylophone.iOS.Views;
namespace Stylophone.iOS.ViewControllers
{
public partial class SettingsViewController : UITableViewController, IViewController
{
- public SettingsViewController(IntPtr handle) : base(handle)
+ public SettingsViewController(ObjCRuntime.NativeHandle handle) : base(handle)
{
}
@@ -38,9 +42,10 @@ public override string TitleForHeader(UITableView tableView, nint section)
{
0 => Resources.SettingsServer,
1 => Resources.SettingsLocalPlaybackHeader,
- 2 => Resources.SettingsDatabase,
- 3 => Resources.SettingsAnalytics,
- 4 => Resources.SettingsAbout,
+ 2 => Resources.SettingsOutputsHeader,
+ 3 => Resources.SettingsDatabase,
+ 4 => Resources.SettingsAnalytics,
+ 5 => Resources.SettingsAbout,
_ => "",
};
}
@@ -50,8 +55,9 @@ public override string TitleForFooter(UITableView tableView, nint section)
return (int)section switch
{
1 => Resources.SettingsLocalPlaybackText,
- 2 => Resources.SettingsAlbumArtText,
- 3 => Resources.SettingsApplyOnRestart,
+ 2 => Resources.SettingsOutputsText,
+ 3 => Resources.SettingsAlbumArtText,
+ 4 => Resources.SettingsApplyOnRestart,
_ => "",
};
}
@@ -85,6 +91,10 @@ public override void ViewDidLoad()
Binder.Bind(LocalPlaybackToggle, "enabled", nameof(ViewModel.IsStreamingAvailable));
Binder.Bind(LocalPlaybackToggle, "on", nameof(ViewModel.IsLocalPlaybackEnabled), true);
+
+ Binder.Bind(LocalPlaybackPortField, "text", nameof(ViewModel.LocalPlaybackPort), true,
+ valueTransformer: intToStringTransformer);
+
Binder.Bind(AnalyticsToggle, "on", nameof(ViewModel.EnableAnalytics), true);
Binder.Bind(AlbumArtToggle, "on", nameof(ViewModel.IsAlbumArtFetchingEnabled), true);
@@ -112,7 +122,83 @@ public override void ViewDidLoad()
ViewModel.RetryConnection();
};
+ var outputsDataSource = new ServerOutputsDataSource(ServerOutputsTable, ViewModel.Outputs);
+
+ ServerOutputsTable.DataSource = outputsDataSource;
+ ServerOutputsTable.Delegate = outputsDataSource;
+
+ }
+ }
+
+ public class ServerOutputsDataSource : UITableViewDelegate, IUITableViewDataSource
+ {
+ private UITableView _tableView;
+ private ObservableCollection _sourceCollection;
+
+ public ServerOutputsDataSource(UITableView tableView, ObservableCollection source)
+ {
+ _tableView = tableView;
+ _sourceCollection = source;
+
+ _sourceCollection.CollectionChanged += (s, e) => UIApplication.SharedApplication.InvokeOnMainThread(
+ () => UpdateUITableView(e));
}
+
+ public nint RowsInSection(UITableView tableView, nint section)
+ {
+ return _sourceCollection.Count;
+ }
+
+ public UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
+ {
+ var cell = tableView.DequeueReusableCell("outputCell") as ServerOutputCell;
+
+ if (_sourceCollection.Count <= indexPath.Row)
+ return cell; // Safety check
+
+ var outputViewModel = _sourceCollection[indexPath.Row];
+
+ cell.Configure(indexPath.Row, outputViewModel);
+
+ return cell;
+ }
+
+ // TODO make an extension of UITableView?
+ private void UpdateUITableView(NotifyCollectionChangedEventArgs e)
+ {
+ if (e.Action == NotifyCollectionChangedAction.Reset)
+ {
+ _tableView.ReloadData();
+ }
+ else
+ {
+ _tableView.BeginUpdates();
+
+ //Build a NSIndexPath array that matches the changes from the ObservableCollection.
+ var indexPaths = new List();
+
+ if (e.Action == NotifyCollectionChangedAction.Add)
+ {
+ for (var i = e.NewStartingIndex; i < e.NewStartingIndex + e.NewItems.Count; i++)
+ indexPaths.Add(NSIndexPath.FromItemSection(i, 0));
+
+ _tableView.InsertRows(indexPaths.ToArray(), UITableViewRowAnimation.Left);
+ }
+
+ if (e.Action == NotifyCollectionChangedAction.Remove)
+ {
+ 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);
+ }
+
+ _tableView.EndUpdates();
+ }
+ }
+
}
}
diff --git a/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.designer.cs b/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.designer.cs
index da6b3a74..8913c1a8 100644
--- a/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.designer.cs
+++ b/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.designer.cs
@@ -24,6 +24,9 @@ partial class SettingsViewController
[Outlet]
UIKit.UIButton GithubButton { get; set; }
+ [Outlet]
+ UIKit.UITextField LocalPlaybackPortField { get; set; }
+
[Outlet]
UIKit.UISwitch LocalPlaybackToggle { get; set; }
@@ -48,6 +51,9 @@ partial class SettingsViewController
[Outlet]
UIKit.UILabel ServerInfoLabel { get; set; }
+ [Outlet]
+ UIKit.UITableView ServerOutputsTable { get; set; }
+
[Outlet]
UIKit.UITextField ServerPasswordField { get; set; }
@@ -62,6 +68,11 @@ partial class SettingsViewController
void ReleaseDesignerOutlets ()
{
+ if (AlbumArtToggle != null) {
+ AlbumArtToggle.Dispose ();
+ AlbumArtToggle = null;
+ }
+
if (AnalyticsToggle != null) {
AnalyticsToggle.Dispose ();
AnalyticsToggle = null;
@@ -77,16 +88,16 @@ void ReleaseDesignerOutlets ()
GithubButton = null;
}
+ if (LocalPlaybackPortField != null) {
+ LocalPlaybackPortField.Dispose ();
+ LocalPlaybackPortField = null;
+ }
+
if (LocalPlaybackToggle != null) {
LocalPlaybackToggle.Dispose ();
LocalPlaybackToggle = null;
}
- if (AlbumArtToggle != null) {
- AlbumArtToggle.Dispose ();
- AlbumArtToggle = null;
- }
-
if (RateButton != null) {
RateButton.Dispose ();
RateButton = null;
@@ -141,6 +152,11 @@ void ReleaseDesignerOutlets ()
VersionLabel.Dispose ();
VersionLabel = null;
}
+
+ if (ServerOutputsTable != null) {
+ ServerOutputsTable.Dispose ();
+ ServerOutputsTable = null;
+ }
}
}
}
diff --git a/Sources/Stylophone.iOS/ViewControllers/SubViews/ServerOutputCell.cs b/Sources/Stylophone.iOS/ViewControllers/SubViews/ServerOutputCell.cs
new file mode 100644
index 00000000..ef56a9c2
--- /dev/null
+++ b/Sources/Stylophone.iOS/ViewControllers/SubViews/ServerOutputCell.cs
@@ -0,0 +1,39 @@
+// This file has been autogenerated from a class added in the UI designer.
+
+using System;
+using Stylophone.Common.Helpers;
+using Stylophone.Common.ViewModels;
+using Stylophone.iOS.Helpers;
+using Foundation;
+using UIKit;
+using Stylophone.Common.ViewModels.Items;
+
+namespace Stylophone.iOS.Views
+{
+ public partial class ServerOutputCell : UITableViewCell
+ {
+ public ServerOutputCell (IntPtr handle) : base (handle)
+ {
+ }
+
+ private PropertyBinder _propertyBinder;
+ private OutputViewModel _outputViewModel;
+
+ internal void Configure(int row, OutputViewModel outputViewModel)
+ {
+ // Bind output
+ _outputViewModel = outputViewModel;
+ _propertyBinder?.Dispose();
+ _propertyBinder = new PropertyBinder(outputViewModel);
+ var negateBoolTransformer = NSValueTransformer.GetValueTransformer(nameof(ReverseBoolValueTransformer));
+
+ if (outputViewModel != null)
+ {
+ OutputLabel.Text = outputViewModel.Name + " (" + outputViewModel.Plugin + ")";
+
+ _propertyBinder.Bind(OutputToggle, "on", nameof(outputViewModel.IsEnabled), true);
+ }
+
+ }
+ }
+}
diff --git a/Sources/Stylophone.iOS/ViewControllers/SubViews/ServerOutputCell.designer.cs b/Sources/Stylophone.iOS/ViewControllers/SubViews/ServerOutputCell.designer.cs
new file mode 100644
index 00000000..ae1b51b5
--- /dev/null
+++ b/Sources/Stylophone.iOS/ViewControllers/SubViews/ServerOutputCell.designer.cs
@@ -0,0 +1,34 @@
+// WARNING
+//
+// This file has been generated automatically by Visual Studio to store outlets and
+// actions made in the UI designer. If it is removed, they will be lost.
+// Manual changes to this file may not be handled correctly.
+//
+using Foundation;
+using System.CodeDom.Compiler;
+
+namespace Stylophone.iOS.Views
+{
+ [Register ("ServerOutputCell")]
+ partial class ServerOutputCell
+ {
+ [Outlet]
+ UIKit.UILabel OutputLabel { get; set; }
+
+ [Outlet]
+ Stylophone.iOS.Helpers.KvoUISwitch OutputToggle { get; set; }
+
+ void ReleaseDesignerOutlets ()
+ {
+ if (OutputLabel != null) {
+ OutputLabel.Dispose ();
+ OutputLabel = null;
+ }
+
+ if (OutputToggle != null) {
+ OutputToggle.Dispose ();
+ OutputToggle = null;
+ }
+ }
+ }
+}
diff --git a/Sources/Stylophone.iOS/Views/Settings.storyboard b/Sources/Stylophone.iOS/Views/Settings.storyboard
index 35f1c1be..9de7127e 100644
--- a/Sources/Stylophone.iOS/Views/Settings.storyboard
+++ b/Sources/Stylophone.iOS/Views/Settings.storyboard
@@ -1,8 +1,9 @@
-
-
+
+
-
+
+
@@ -13,24 +14,24 @@
-
+
-
+
-
+
-
+
-
+
@@ -38,7 +39,7 @@
-
+
-
+
-
+
@@ -95,14 +96,14 @@
-
+
-
+
-
+
@@ -131,26 +132,26 @@
-
+
-
+
-
+
-
+
-
+
@@ -159,7 +160,7 @@
-
+
@@ -191,17 +192,17 @@
-
+
-
+
-
+
-
+
@@ -227,19 +228,116 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
@@ -264,14 +362,14 @@
-
+
-
+
@@ -284,14 +382,14 @@
-
+
-
+
@@ -308,20 +406,20 @@
-
+
-
+
-
+
-
+
+
+
+
diff --git a/Sources/Stylophone/Activation/DefaultActivationHandler.cs b/Sources/Stylophone/Activation/DefaultActivationHandler.cs
index 882f88e7..59591600 100644
--- a/Sources/Stylophone/Activation/DefaultActivationHandler.cs
+++ b/Sources/Stylophone/Activation/DefaultActivationHandler.cs
@@ -3,6 +3,12 @@
using Stylophone.Services;
using Stylophone.Common.Interfaces;
using Windows.ApplicationModel.Activation;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Stylophone.Common.Services;
+using Stylophone.Common.ViewModels;
+using System.Threading;
namespace Stylophone.Activation
{
@@ -29,13 +35,50 @@ protected override async Task HandleInternalAsync(IActivatedEventArgs args)
}
_navigationService.Navigate(_navElement, arguments);
- await Task.CompletedTask;
+
+ // Ensure the current window is active
+ Window.Current.Activate();
+
+ // Tasks after activation
+ await StartupAsync();
+ }
+
+ private async Task StartupAsync()
+ {
+ var theme = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ElementTheme));
+ Enum.TryParse(theme, out Theme elementTheme);
+ await Ioc.Default.GetRequiredService().SetThemeAsync(elementTheme);
+
+ await Ioc.Default.GetRequiredService().ShowFirstRunDialogIfAppropriateAsync();
+
+ _ = Task.Run(async () =>
+ {
+ Thread.Sleep(60000);
+ await Ioc.Default.GetRequiredService().ShowRateAppDialogIfAppropriateAsync();
+ });
+
+ var host = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ServerHost));
+ host = host?.Replace("\"", ""); // TODO: This is a quickfix for 1.x updates
+ var port = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ServerPort), 6600);
+ var pass = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ServerPassword));
+ var localPlaybackEnabled = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.IsLocalPlaybackEnabled));
+ var localPlaybackPort = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.LocalPlaybackPort), 8000);
+
+ var localPlaybackVm = Ioc.Default.GetRequiredService();
+ localPlaybackVm.Initialize(host, localPlaybackPort, localPlaybackEnabled);
+
+ var mpdService = Ioc.Default.GetRequiredService();
+ mpdService.SetServerInfo(host, port, pass);
+ await mpdService.InitializeAsync(true);
+
+ Ioc.Default.GetRequiredService().Initialize();
+ Ioc.Default.GetRequiredService().Initialize();
}
protected override bool CanHandleInternal(IActivatedEventArgs args)
{
// None of the ActivationHandlers has handled the app activation
- return ((NavigationService)_navigationService).Frame.Content == null && _navElement != null;
+ return ((NavigationService)_navigationService).Frame?.Content == null && _navElement != null;
}
}
}
diff --git a/Sources/Stylophone/Activation/ProtocolActivationHandler.cs b/Sources/Stylophone/Activation/ProtocolActivationHandler.cs
new file mode 100644
index 00000000..960d7140
--- /dev/null
+++ b/Sources/Stylophone/Activation/ProtocolActivationHandler.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Threading.Tasks;
+using Stylophone.Common.Interfaces;
+using Windows.ApplicationModel.Activation;
+using CommunityToolkit.Mvvm.DependencyInjection;
+using Stylophone.Common.Services;
+using MpcNET.Commands.Playback;
+using MpcNET.Commands.Status;
+using MpcNET.Commands.Playlist;
+using Stylophone.Common.ViewModels;
+using Windows.UI.Popups;
+
+namespace Stylophone.Activation
+{
+ internal class ProtocolActivationHandler : ActivationHandler
+ {
+ private MPDConnectionService _mpdService;
+
+ public ProtocolActivationHandler(MPDConnectionService connectionService)
+ {
+ _mpdService = connectionService;
+ }
+
+ protected override async Task HandleInternalAsync(IActivatedEventArgs args)
+ {
+ ProtocolActivatedEventArgs eventArgs = args as ProtocolActivatedEventArgs;
+
+ if (!_mpdService.IsConnected)
+ {
+ var host = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ServerHost));
+ host = host?.Replace("\"", ""); // TODO: This is a quickfix for 1.x updates
+ var port = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ServerPort), 6600);
+ var pass = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ServerPassword));
+ _mpdService.SetServerInfo(host, port, pass);
+ await _mpdService.InitializeAsync(false);
+ }
+
+ if (!_mpdService.IsConnected)
+ {
+ var dlg = new MessageDialog("Please open Stylophone and configure a MPD server.", "Couldn't connect to MPD server");
+ await dlg.ShowAsync();
+ return;
+ }
+
+ var status = _mpdService.CurrentStatus == MPDConnectionService.BOGUS_STATUS ? await _mpdService.SafelySendCommandAsync(new StatusCommand()) : _mpdService.CurrentStatus;
+
+ // Protocol launches can do basic operations on the MPD server based on the verb
+ // eg stylophone://?verb=stylophone_play
+ var uri = new Uri(eventArgs.Uri.AbsoluteUri);
+ var queryDictionary = System.Web.HttpUtility.ParseQueryString(uri.Query);
+
+ switch (queryDictionary["verb"])
+ {
+ case "stylophone_play":
+ case "stylophone_pause":
+ await _mpdService.SafelySendCommandAsync(new PauseResumeCommand());
+ break;
+ case "stylophone_stop":
+ await _mpdService.SafelySendCommandAsync(new StopCommand());
+ break;
+ case "stylophone_next":
+ await _mpdService.SafelySendCommandAsync(new NextCommand());
+ break;
+ case "stylophone_prev":
+ await _mpdService.SafelySendCommandAsync(new PreviousCommand());
+ break;
+ case "stylophone_shuffle":
+ await _mpdService.SafelySendCommandAsync(new RandomCommand(!status.Random));
+ break;
+ case "stylophone_volume_up":
+ await _mpdService.SafelySendCommandAsync(new SetVolumeCommand((byte)(status.Volume + 5)));
+ break;
+ case "stylophone_volume_down":
+ await _mpdService.SafelySendCommandAsync(new SetVolumeCommand((byte)(status.Volume - 5)));
+ break;
+ case "stylophone_volume_set":
+ var volume = queryDictionary["volume"] ?? "0";
+ await _mpdService.SafelySendCommandAsync(new SetVolumeCommand((byte)(int.Parse(volume))));
+ break;
+ case "stylophone_seek":
+ var seek = int.Parse(queryDictionary["seek"] ?? "0");
+ await _mpdService.SafelySendCommandAsync(new SeekCurCommand(seek));
+ break;
+ case "stylophone_load_playlist":
+ var playlist = queryDictionary["playlist"] ?? "";
+ await _mpdService.SafelySendCommandAsync(new LoadCommand(playlist));
+ break;
+ default:
+ break;
+ }
+ }
+
+ protected override bool CanHandleInternal(IActivatedEventArgs args)
+ {
+ return args.Kind == ActivationKind.Protocol;
+ }
+ }
+}
diff --git a/Sources/Stylophone/App.xaml.cs b/Sources/Stylophone/App.xaml.cs
index 4e675b10..ea82f55b 100644
--- a/Sources/Stylophone/App.xaml.cs
+++ b/Sources/Stylophone/App.xaml.cs
@@ -136,12 +136,7 @@ private void OnAppSuspending(object sender, SuspendingEventArgs e)
private ActivationService CreateActivationService()
{
- return new ActivationService(this, typeof(QueueViewModel), new Lazy(CreateShell));
- }
-
- private UIElement CreateShell()
- {
- return new Views.ShellPage();
+ return new ActivationService(this, typeof(QueueViewModel));
}
protected override async void OnBackgroundActivated(BackgroundActivatedEventArgs args)
diff --git a/Sources/Stylophone/Assets/widgetTemplate.json b/Sources/Stylophone/Assets/widgetTemplate.json
new file mode 100644
index 00000000..2a2e4f13
--- /dev/null
+++ b/Sources/Stylophone/Assets/widgetTemplate.json
@@ -0,0 +1,110 @@
+{
+ "type": "AdaptiveCard",
+ "body": [
+ {
+ "type": "Image",
+ "url": "${stylophone_albumArt}",
+ "altText": "${stylophone_album}",
+ "size": "Large",
+ "horizontalAlignment": "Left",
+ "$when": "${$host.widgetSize==\"medium\"}"
+ },
+ {
+ "type": "Image",
+ "url": "${stylophone_albumArt}",
+ "altText": "${stylophone_album}",
+ "$when": "${$host.widgetSize==\"large\"}"
+ },
+ {
+ "type": "ColumnSet",
+ "columns": [
+ {
+ "type": "Column",
+ "items": [
+ {
+ "type": "Image",
+ "url": "${stylophone_albumArt}",
+ "altText": "${stylophone_album}",
+ "size": "Medium",
+ "width": "80px"
+ }
+ ],
+ "$when": "${$host.widgetSize==\"small\"}",
+ "width": "auto"
+ },
+ {
+ "type": "Column",
+ "items": [
+ {
+ "type": "TextBlock",
+ "size": "Medium",
+ "weight": "Bolder",
+ "text": "${stylophone_title}",
+ "wrap": true,
+ "maxLines": 2
+ },
+ {
+ "type": "TextBlock",
+ "text": "${stylophone_artist}",
+ "weight": "Lighter",
+ "isSubtle": true,
+ "spacing": "None"
+ },
+ {
+ "type": "TextBlock",
+ "spacing": "None",
+ "text": "${stylophone_album}",
+ "isSubtle": true
+ }
+ ],
+ "width": "stretch",
+ "height": "stretch"
+ }
+ ]
+ },
+ {
+ "type": "ActionSet",
+ "actions": [
+ {
+ "type": "Action.Execute",
+ "iconUrl": "https://learn.microsoft.com/en-us/windows/apps/design/style/images/segoe-mdl/e8b1.png",
+ "tooltip": "Shuffle",
+ "verb": "stylophone_shuffle",
+ "$when": "${$host.widgetSize!=\"small\"}"
+ },
+ {
+ "type": "Action.Execute",
+ "iconUrl": "https://learn.microsoft.com/en-us/windows/apps/design/style/images/segoe-mdl/e892.png",
+ "tooltip": "Previous",
+ "verb": "stylophone_prev",
+ "$when": "${$host.widgetSize!=\"small\"}"
+ },
+ {
+ "type": "Action.Execute",
+ "iconUrl": "https://learn.microsoft.com/en-us/windows/apps/design/style/images/segoe-mdl/edb4.png",
+ "tooltip": "Pause",
+ "verb": "stylophone_pause",
+ "$when": "${$host.widgetSize!=\"small\"}"
+ },
+ {
+ "type": "Action.Execute",
+ "iconUrl": "https://learn.microsoft.com/en-us/windows/apps/design/style/images/segoe-mdl/e893.png",
+ "tooltip": "Next",
+ "verb": "stylophone_next",
+ "$when": "${$host.widgetSize!=\"small\"}"
+ },
+ {
+ "type": "Action.Execute",
+ "iconUrl": "https://learn.microsoft.com/en-us/windows/apps/design/style/images/segoe-mdl/e8ee.png",
+ "tooltip": "Repeat",
+ "verb": "stylophone_repeat",
+ "$when": "${$host.widgetSize!=\"small\"}"
+ }
+ ],
+ "spacing": "Small",
+ "horizontalAlignment": "Center"
+ }
+ ],
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
+ "version": "1.6"
+}
\ No newline at end of file
diff --git a/Sources/Stylophone/Package.appxmanifest b/Sources/Stylophone/Package.appxmanifest
index 93e2f24b..607e16a8 100644
--- a/Sources/Stylophone/Package.appxmanifest
+++ b/Sources/Stylophone/Package.appxmanifest
@@ -9,15 +9,15 @@
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
IgnorableNamespaces="uap mp genTemplate uap3">
-
+ Version="2.7.1.0" />
- Stylophone (Dev)
+ Stylophone
Difegue
Assets\StoreLogo.png
@@ -35,7 +35,7 @@
Executable="$targetnametoken$.exe"
EntryPoint="Stylophone.App">
+
+
+ Assets\Square44x44Logo.png
+ Stylophone URI Scheme
+
+
diff --git a/Sources/Stylophone/Package.tt b/Sources/Stylophone/Package.tt
index 70ff8db3..e878f94b 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.6.2.0";
+ string version = "2.7.1.0";
// Get configuration name at Build time
string configName = Host.ResolveParameterValue("-", "-", "BuildConfiguration");
@@ -101,6 +101,12 @@
+
+
+ Assets\Square44x44Logo.png
+ Stylophone URI Scheme
+
+
diff --git a/Sources/Stylophone/Services/ActivationService.cs b/Sources/Stylophone/Services/ActivationService.cs
index 9bac2ba9..beb0a228 100644
--- a/Sources/Stylophone/Services/ActivationService.cs
+++ b/Sources/Stylophone/Services/ActivationService.cs
@@ -10,6 +10,7 @@
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using System.Threading;
+using System.Diagnostics;
namespace Stylophone.Services
{
@@ -19,88 +20,42 @@ internal class ActivationService
{
private readonly App _app;
private readonly Type _defaultNavItem;
- private Lazy _shell;
- private object _lastActivationArgs;
-
- public ActivationService(App app, Type defaultNavItem, Lazy shell = null)
+ public ActivationService(App app, Type defaultNavItem)
{
_app = app;
- _shell = shell;
_defaultNavItem = defaultNavItem;
}
public async Task ActivateAsync(object activationArgs)
{
- if (IsInteractive(activationArgs))
+ // Do not repeat app initialization when the Window already has content,
+ // just ensure that the window is active
+ if (Window.Current.Content == null)
{
- // Do not repeat app initialization when the Window already has content,
- // just ensure that the window is active
- if (Window.Current.Content == null)
- {
- // Create a Shell or Frame to act as the navigation context
- Window.Current.Content = _shell?.Value ?? new Frame();
- }
+ // Create a Shell to act as the navigation context
+ Window.Current.Content = new Views.ShellPage();
}
- // Depending on activationArgs one of ActivationHandlers or DefaultActivationHandler
- // will navigate to the first page
+ // Depending on activationArgs, ProtocolActivationHandler and DefaultActivationHandler will trigger
await HandleActivationAsync(activationArgs);
- _lastActivationArgs = activationArgs;
-
- if (IsInteractive(activationArgs))
- {
- // Ensure the current window is active
- Window.Current.Activate();
-
- // Tasks after activation
- await StartupAsync();
- }
}
private async Task HandleActivationAsync(object activationArgs)
{
+ var defaultHandler = new DefaultActivationHandler(_defaultNavItem, Ioc.Default.GetRequiredService());
+ var protocolHandler = new ProtocolActivationHandler(Ioc.Default.GetRequiredService());
+
if (IsInteractive(activationArgs))
{
- var defaultHandler = new DefaultActivationHandler(_defaultNavItem, Ioc.Default.GetRequiredService());
+ if (protocolHandler.CanHandle(activationArgs))
+ await protocolHandler.HandleAsync(activationArgs);
+
if (defaultHandler.CanHandle(activationArgs))
- {
await defaultHandler.HandleAsync(activationArgs);
- }
}
}
- private async Task StartupAsync()
- {
- var theme = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ElementTheme));
- Enum.TryParse(theme, out Theme elementTheme);
- await Ioc.Default.GetRequiredService().SetThemeAsync(elementTheme);
-
- await Ioc.Default.GetRequiredService().ShowFirstRunDialogIfAppropriateAsync();
-
- _ = Task.Run(async () =>
- {
- Thread.Sleep(60000);
- await Ioc.Default.GetRequiredService().ShowRateAppDialogIfAppropriateAsync();
- });
-
- var host = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ServerHost));
- host = host?.Replace("\"", ""); // TODO: This is a quickfix for 1.x updates
- var port = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ServerPort), 6600);
- var pass = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.ServerPassword));
- var localPlaybackEnabled = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.IsLocalPlaybackEnabled));
-
- var localPlaybackVm = Ioc.Default.GetRequiredService();
- localPlaybackVm.Initialize(host, localPlaybackEnabled);
-
- var mpdService = Ioc.Default.GetRequiredService();
- mpdService.SetServerInfo(host, port, pass);
- await mpdService.InitializeAsync(true);
-
- Ioc.Default.GetRequiredService().Initialize();
- Ioc.Default.GetRequiredService().Initialize();
- }
-
private bool IsInteractive(object args)
{
return args is IActivatedEventArgs;
diff --git a/Sources/Stylophone/Styles/StyloResources.xaml b/Sources/Stylophone/Styles/StyloResources.xaml
index ed16f683..a052495f 100644
--- a/Sources/Stylophone/Styles/StyloResources.xaml
+++ b/Sources/Stylophone/Styles/StyloResources.xaml
@@ -8,6 +8,8 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Stylophone.Common.ViewModels"
xmlns:media="using:CommunityToolkit.WinUI.Media"
+ xmlns:labs="using:CommunityToolkit.Labs.WinUI.MarqueeTextRns"
+ xmlns:strings="using:Stylophone.Localization.Strings"
mc:Ignorable="d">
@@ -73,26 +75,45 @@
-
+
+
+
+
+
+
+ ToolTipService.ToolTip="{x:Bind Name}"
+ Visibility="{x:Bind IsPlaying, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"/>
-
+
+ ToolTipService.ToolTip="{x:Bind File.Artist}"
+ Visibility="{x:Bind IsPlaying, Mode=OneWay, Converter={StaticResource ReverseBoolToVisibilityConverter}}"/>
+
+
PackageReference
-
-
-
+
+ 0.1.240906-build.1745
+
+
+
+
6.2.14
- 10.1901.28001
+ 10.2307.3001
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
+
+
1.1.0
@@ -174,6 +176,7 @@
+
@@ -357,12 +360,6 @@
-
- True
- True
- Package.tt
- Designer
-
Designer
@@ -414,6 +411,14 @@
Microsoft Engagement Framework
+
+
+ Designer
+ True
+ True
+ Package.tt
+
+