diff --git a/Screenshots/Screen1.jpg b/Screenshots/Screen1.jpg index 99d6c21f..ab75be61 100644 Binary files a/Screenshots/Screen1.jpg and b/Screenshots/Screen1.jpg differ diff --git a/Screenshots/Screen2.jpg b/Screenshots/Screen2.jpg index 536e063c..04599fbc 100644 Binary files a/Screenshots/Screen2.jpg and b/Screenshots/Screen2.jpg differ diff --git a/Screenshots/Screen3.jpg b/Screenshots/Screen3.jpg index 3a1e5bad..a8d807a4 100644 Binary files a/Screenshots/Screen3.jpg and b/Screenshots/Screen3.jpg differ diff --git a/Screenshots/Screen4.jpg b/Screenshots/Screen4.jpg index e62a5d67..0695086f 100644 Binary files a/Screenshots/Screen4.jpg and b/Screenshots/Screen4.jpg differ diff --git a/Screenshots/Screen5.jpg b/Screenshots/Screen5.jpg index 07ebf063..fbe02c17 100644 Binary files a/Screenshots/Screen5.jpg and b/Screenshots/Screen5.jpg differ diff --git a/Screenshots/Screen6.jpg b/Screenshots/Screen6.jpg index 5641063a..c6048476 100644 Binary files a/Screenshots/Screen6.jpg and b/Screenshots/Screen6.jpg differ diff --git a/Screenshots/ScreenXbox.jpg b/Screenshots/ScreenXbox.jpg index 0360c8cb..f953e2ac 100644 Binary files a/Screenshots/ScreenXbox.jpg and b/Screenshots/ScreenXbox.jpg differ diff --git a/Sources/Stylophone.Common/Interfaces/INavigationService.cs b/Sources/Stylophone.Common/Interfaces/INavigationService.cs index 9dc2a647..6d412569 100644 --- a/Sources/Stylophone.Common/Interfaces/INavigationService.cs +++ b/Sources/Stylophone.Common/Interfaces/INavigationService.cs @@ -1,10 +1,10 @@ -using Microsoft.Toolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using System; namespace Stylophone.Common.Interfaces { public class CoreNavigationEventArgs : EventArgs { public Type NavigationTarget { get; set; } public object Parameter { get; set; } } - + public interface INavigationService { Type CurrentPageViewModelType { get; } diff --git a/Sources/Stylophone.Common/Interfaces/INotificationService.cs b/Sources/Stylophone.Common/Interfaces/INotificationService.cs index ba9216b5..83aba52b 100644 --- a/Sources/Stylophone.Common/Interfaces/INotificationService.cs +++ b/Sources/Stylophone.Common/Interfaces/INotificationService.cs @@ -5,29 +5,55 @@ namespace Stylophone.Common.Interfaces { - public class InAppNotificationRequestedEventArgs : EventArgs { public string NotificationText { get; set; } public int NotificationTime { get; set; } } + public class InAppNotification + { + public string NotificationTitle { get; set; } + public string NotificationText { get; set; } + public NotificationType NotificationType { get; set; } + } + + public enum NotificationType + { + Info, + Warning, + Error + } public interface INotificationService { void ShowBasicToastNotification(string title, string description); - void ShowInAppNotification(string notification, bool autoHide = true); + void ShowInAppNotification(string text, string description = "", NotificationType type = NotificationType.Info); + void ShowInAppNotification(InAppNotification notification); void ShowErrorNotification(Exception ex); } public abstract class NotificationServiceBase: INotificationService { - public event EventHandler InAppNotificationRequested; - - public void InvokeInAppNotificationRequested(InAppNotificationRequestedEventArgs args) + public void ShowInAppNotification(string text, string description = "", NotificationType type = NotificationType.Info) { - InAppNotificationRequested?.Invoke(this, args); + var notification = new InAppNotification + { + NotificationTitle = text, + NotificationText = description, + NotificationType = type + }; + ShowInAppNotification(notification); } - public void ShowErrorNotification(Exception ex) => ShowInAppNotification(string.Format(Resources.ErrorGeneric, ex), false); - + public void ShowErrorNotification(Exception ex) + { + var notification = new InAppNotification + { + NotificationTitle = string.Format(Resources.ErrorGeneric, ex.Message), + NotificationText = ex.StackTrace, + NotificationType = NotificationType.Error + }; + ShowInAppNotification(notification); + } + public abstract void ShowBasicToastNotification(string title, string description); - public abstract void ShowInAppNotification(string notification, bool autoHide = true); + public abstract void ShowInAppNotification(InAppNotification notification); } } diff --git a/Sources/Stylophone.Common/Services/AlbumArtService.cs b/Sources/Stylophone.Common/Services/AlbumArtService.cs index c065c756..414348d5 100644 --- a/Sources/Stylophone.Common/Services/AlbumArtService.cs +++ b/Sources/Stylophone.Common/Services/AlbumArtService.cs @@ -1,5 +1,4 @@ using ColorThiefDotNet; -using Microsoft.Toolkit.Mvvm.DependencyInjection; using MpcNET.Commands.Database; using MpcNET.Types; using SkiaSharp; @@ -9,6 +8,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Drawing; using System.Threading; using System.Threading.Tasks; @@ -26,12 +26,14 @@ public class AlbumArtService private CancellationTokenSource _queueCanceller; private IApplicationStorageService _applicationStorageService; + private INotificationService _notificationService; private MPDConnectionService _mpdService; - public AlbumArtService(MPDConnectionService mpdService, IApplicationStorageService appStorage) + public AlbumArtService(MPDConnectionService mpdService, IApplicationStorageService appStorage, INotificationService notificationService) { _mpdService = mpdService; _applicationStorageService = appStorage; + _notificationService = notificationService; } public void Initialize() @@ -73,6 +75,7 @@ public void Initialize() catch (Exception e) { Debug.WriteLine("Exception while processing albumart queue: " + e); + _notificationService.ShowErrorNotification(e); } } }).ConfigureAwait(false); @@ -189,6 +192,7 @@ private async Task GetAlbumBitmap(IMpdFile f, CancellationToken token catch (Exception e) { Debug.WriteLine("Exception caught while getting albumart: " + e); + _notificationService.ShowErrorNotification(e); return null; } @@ -217,13 +221,17 @@ private async Task LoadImageFromFile(string fileName) { try { - var fileStream = await _applicationStorageService.OpenFileAsync(fileName, "AlbumArt"); - SKBitmap image = SKBitmap.Decode(fileStream); - fileStream.Dispose(); - return image; + // Go through a SKData object to sidestep https://github.com/mono/SkiaSharp/issues/1551 + using (var fileStream = await _applicationStorageService.OpenFileAsync(fileName, "AlbumArt")) + using (var skData = SKData.Create(fileStream)) + { + return SKBitmap.Decode(skData); + } } - catch (Exception) + catch (Exception e) { + Debug.WriteLine("Exception caught while loading albumart from file: " + e); + _notificationService.ShowErrorNotification(e); return null; } } diff --git a/Sources/Stylophone.Common/Services/MPDConnectionService.cs b/Sources/Stylophone.Common/Services/MPDConnectionService.cs index 924bf96b..26b6d016 100644 --- a/Sources/Stylophone.Common/Services/MPDConnectionService.cs +++ b/Sources/Stylophone.Common/Services/MPDConnectionService.cs @@ -10,6 +10,7 @@ using MpcNET.Commands.Status; using Stylophone.Common.Interfaces; using MpcNET.Commands.Reflection; +using Stylophone.Localization.Strings; namespace Stylophone.Common.Services { @@ -129,7 +130,8 @@ private void ClearResources() private async Task TryConnecting(CancellationToken token) { if (token.IsCancellationRequested) return; - if (!IPAddress.TryParse(_host, out var ipAddress)) return; + if (!IPAddress.TryParse(_host, out var ipAddress)) + throw new Exception("Invalid IP address"); _mpdEndpoint = new IPEndPoint(ipAddress, _port); @@ -196,7 +198,8 @@ public async Task SafelySendCommandAsync(IMpcCommand command) } catch (Exception e) { - _notificationService.ShowInAppNotification($"Sending {command.GetType().Name} failed: {e.Message}", false); + _notificationService.ShowInAppNotification(string.Format(Resources.ErrorSendingMPDCommand, command.GetType().Name), + e.Message, NotificationType.Error); } return default(T); @@ -214,7 +217,7 @@ private async Task GetConnectionInternalAsync(CancellationToken t if (!r.IsResponseValid) { var mpdError = r.Response?.Result?.MpdError; - _notificationService.ShowInAppNotification($"Invalid password: {mpdError ?? r.ToString()}", false); + _notificationService.ShowInAppNotification(Resources.ErrorPassword, $"{mpdError ?? r.ToString()}", NotificationType.Error); } } @@ -235,7 +238,7 @@ private void InitializeStatusUpdater(CancellationToken token = default) { try { - if (token.IsCancellationRequested || _idleConnection == null) + if (token.IsCancellationRequested || _idleConnection == null || !_idleConnection.IsConnected) break; var idleChanges = await _idleConnection.SendAsync(new IdleCommand("stored_playlist playlist player mixer output options update")); diff --git a/Sources/Stylophone.Common/Stylophone.Common.csproj b/Sources/Stylophone.Common/Stylophone.Common.csproj index c2fcc6d1..4504847a 100644 --- a/Sources/Stylophone.Common/Stylophone.Common.csproj +++ b/Sources/Stylophone.Common/Stylophone.Common.csproj @@ -1,18 +1,19 @@  + 9.0 netstandard2.0 - - - - - + + + + + - - + + diff --git a/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs b/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs index 8eefe5b8..f67add3a 100644 --- a/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs +++ b/Sources/Stylophone.Common/ViewModels/AlbumDetailViewModel.cs @@ -3,9 +3,8 @@ using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; -using System.Windows.Input; -using Microsoft.Toolkit.Mvvm.ComponentModel; -using Microsoft.Toolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using MpcNET.Commands.Playlist; using MpcNET.Commands.Queue; using MpcNET.Commands.Reflection; @@ -17,7 +16,7 @@ namespace Stylophone.Common.ViewModels { - public class AlbumDetailViewModel : ViewModelBase + public partial class AlbumDetailViewModel : ViewModelBase { private IDialogService _dialogService; @@ -36,27 +35,18 @@ public AlbumDetailViewModel(IDialogService dialogService, INotificationService n Source.CollectionChanged += (s, e) => OnPropertyChanged(nameof(IsSourceEmpty)); } + [ObservableProperty] private AlbumViewModel _item; - public AlbumViewModel Item - { - get { return _item; } - set { Set(ref _item, value); } - } - private string _info; - public string PlaylistInfo - { - get => _info; - private set => Set(ref _info, value); - } + [ObservableProperty] + private string _playlistInfo; public ObservableCollection Source { get; } = new ObservableCollection(); + public bool IsSourceEmpty => Source.Count == 0; - private ICommand _addToQueueCommand; - public ICommand AddToQueueCommand => _addToQueueCommand ?? (_addToQueueCommand = new RelayCommand>(QueueTrack)); - - private async void QueueTrack(object list) + [RelayCommand] + private async void AddToQueue(object list) { var selectedTracks = (IList)list; @@ -76,9 +66,7 @@ private async void QueueTrack(object list) } } - private ICommand _addToPlaylistCommand; - public ICommand AddToPlayListCommand => _addToPlaylistCommand ?? (_addToPlaylistCommand = new RelayCommand>(AddToPlaylist)); - + [RelayCommand] private async void AddToPlaylist(object list) { var playlistName = await _dialogService.ShowAddToPlaylistDialog(); @@ -131,7 +119,6 @@ public void Initialize(AlbumViewModel album) await _dispatcherService.ExecuteOnUIThreadAsync(() => CreateTrackViewModels()); }).ConfigureAwait(false); } - } private void CreateTrackViewModels() diff --git a/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs b/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs index 5c56a9a1..0cc0c545 100644 --- a/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs +++ b/Sources/Stylophone.Common/ViewModels/Bases/LibraryViewModelBase.cs @@ -1,14 +1,9 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Collections.Specialized; -using System.ComponentModel; using System.Linq; -using System.Threading; using System.Threading.Tasks; -using System.Windows.Input; -using Microsoft.Toolkit.Mvvm.ComponentModel; -using Microsoft.Toolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Input; using MpcNET.Commands.Database; using MpcNET.Tags; using Stylophone.Common.Helpers; @@ -18,7 +13,7 @@ namespace Stylophone.Common.ViewModels { - public abstract class LibraryViewModelBase : ViewModelBase + public abstract partial class LibraryViewModelBase : ViewModelBase { private INavigationService _navigationService; private MPDConnectionService _mpdService; @@ -36,9 +31,6 @@ public LibraryViewModelBase(INavigationService navigationService, IDispatcherSer public static new string GetHeader() => Resources.LibraryHeader; - private ICommand _itemClickCommand; - public ICommand ItemClickCommand => _itemClickCommand ?? (_itemClickCommand = new RelayCommand(OnItemClick)); - public List Source { get; } = new List(); public bool IsSourceEmpty => FilteredSource.Count == 0; @@ -97,7 +89,8 @@ private string GetGroupHeader(string title) return char.IsLetter(c) ? c.ToString() : char.IsDigit(c) ? "#" : "&"; } - private void OnItemClick(AlbumViewModel clickedItem) + [RelayCommand] + private void ItemClick(AlbumViewModel clickedItem) { if (clickedItem != null) { diff --git a/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs b/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs index be602a79..357a3d91 100644 --- a/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs +++ b/Sources/Stylophone.Common/ViewModels/Bases/PlaybackViewModelBase.cs @@ -1,9 +1,8 @@ -using Microsoft.Toolkit.Mvvm.ComponentModel; -using Microsoft.Toolkit.Mvvm.DependencyInjection; -using Microsoft.Toolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.Input; using MpcNET; using MpcNET.Commands.Playback; -using MpcNET.Commands.Playlist; using MpcNET.Commands.Queue; using MpcNET.Commands.Status; using Stylophone.Common.Helpers; @@ -15,11 +14,10 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; -using System.Windows.Input; namespace Stylophone.Common.ViewModels { - public abstract class PlaybackViewModelBase : ViewModelBase + public abstract partial class PlaybackViewModelBase : ViewModelBase { private CancellationTokenSource _albumArtCancellationSource = new CancellationTokenSource(); @@ -35,8 +33,8 @@ public abstract class PlaybackViewModelBase : ViewModelBase public LocalPlaybackViewModel LocalPlayback; - public PlaybackViewModelBase(INavigationService navigationService, INotificationService notificationService, IDispatcherService dispatcherService, IInteropService interop, - MPDConnectionService mpdService, TrackViewModelFactory trackVmFactory, LocalPlaybackViewModel localPlayback): + public PlaybackViewModelBase(INavigationService navigationService, INotificationService notificationService, IDispatcherService dispatcherService, IInteropService interop, + MPDConnectionService mpdService, TrackViewModelFactory trackVmFactory, LocalPlaybackViewModel localPlayback) : base(dispatcherService) { _navigationService = navigationService; @@ -49,7 +47,7 @@ public PlaybackViewModelBase(INavigationService navigationService, INotification // Default to NowPlayingBar _hostType = VisualizationType.NowPlayingBar; - _trackInfoAvailable = false; + _isTrackInfoAvailable = false; // Initialize icons _volumeIcon = _interop.GetIcon(PlaybackIcon.VolumeFull); @@ -79,7 +77,7 @@ private void OnConnectionChanged(object sender, EventArgs e) if (_mpdService.IsConnected) { Task.Run(() => InitializeAsync()); - } + } else { IsTrackInfoAvailable = false; @@ -95,124 +93,73 @@ private async Task InitializeAsync() await UpdateUpNextAsync(_mpdService.CurrentStatus); } - #region Getters and Setters - private TrackViewModel _currentTrack; + public bool HasNextTrack => NextTrack != null; + /// /// The current playing track /// - public TrackViewModel CurrentTrack - { - get => _currentTrack; - private set => Set(ref _currentTrack, value); - } + [ObservableProperty] + private TrackViewModel _currentTrack; - private TrackViewModel _nextTrack; /// /// The next track in queue /// - public TrackViewModel NextTrack - { - get => _nextTrack; - private set - { - Set(ref _nextTrack, value); - OnPropertyChanged(nameof(HasNextTrack)); - } - } - public bool HasNextTrack => NextTrack != null; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(HasNextTrack))] + private TrackViewModel _nextTrack; + [ObservableProperty] private bool _showTrackName; - public bool ShowTrackName - { - get => _showTrackName; - set => Set(ref _showTrackName, value); - } - private bool _trackInfoAvailable; - public bool IsTrackInfoAvailable - { - get => _trackInfoAvailable; - set => Set(ref _trackInfoAvailable, value); - } + [ObservableProperty] + private bool _isTrackInfoAvailable; + [ObservableProperty] private VisualizationType _hostType; - public VisualizationType HostType - { - get => _hostType; - set => Set(ref _hostType, value); - } - private string _timeListened = "00:00"; /// /// The amount of time spent listening to the track /// - public string TimeListened - { - get => _timeListened; - set => Set(ref _timeListened, value); - } + [ObservableProperty] + private string _timeListened = "00:00"; - private string _timeRemaining = "-00:00"; /// /// The amount of time remaining /// - public string TimeRemaining - { - get => _timeRemaining; - set => Set(ref _timeRemaining, value); - } + [ObservableProperty] + private string _timeRemaining = "-00:00"; - private double _currentTimeValue; /// /// The current slider value /// - public double CurrentTimeValue - { - get => _currentTimeValue; - set => Set(ref _currentTimeValue, value); - } + [ObservableProperty] + private double _currentTimeValue; - private double _maxTimeValue = 100; /// /// The max slider value /// - public double MaxTimeValue - { - get => _maxTimeValue; - private set => Set(ref _maxTimeValue, value); - } + [ObservableProperty] + private double _maxTimeValue = 100; - private string _volumeIcon; /// /// The current text for the volume icon /// - public string VolumeIcon - { - get => _volumeIcon; - private set => Set(ref _volumeIcon, value); - } + [ObservableProperty] + private string _volumeIcon; - private string _repeatIcon; /// /// The current text for the repeat icon /// - public string RepeatIcon - { - get => _repeatIcon; - private set => Set(ref _repeatIcon, value); - } + [ObservableProperty] + private string _repeatIcon; - private string _playButtonContent; /// /// The content on the play_pause button /// - public string PlayButtonContent - { - get => _playButtonContent; - set => Set(ref _playButtonContent, value); - } + [ObservableProperty] + private string _playButtonContent; private double _internalVolume; /// @@ -227,7 +174,7 @@ public double MediaVolume if (_mpdService.CurrentStatus == MPDConnectionService.BOGUS_STATUS) return; - Set(ref _internalVolume, value); + SetProperty(ref _internalVolume, value); if (value > 0) // _previousVolume is only used to keep track of volume when muting, if the volume has changed from zero due to another client, the value becomes meaningless _previousVolume = -1; @@ -236,12 +183,14 @@ public double MediaVolume cts.Cancel(); cts = new CancellationTokenSource(); - // Set the volume - 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... - }, cts.Token)); + // Set the volume + if (_mpdService.CurrentStatus.Volume != value) + 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 + }, cts.Token)); // Update the UI if ((int)value == 0) @@ -267,62 +216,45 @@ public double MediaVolume } } - - private bool _isShuffledEnabled; /// /// Are tracks shuffled /// - public bool IsShuffleEnabled - { - get => _isShuffledEnabled; - set => Set(ref _isShuffledEnabled, value); - } + [ObservableProperty] + private bool _isShuffleEnabled; - private bool _isConsumeEnabled; /// /// Are tracks removed upon playback /// - public bool IsConsumeEnabled - { - get => _isConsumeEnabled; - set => Set(ref _isConsumeEnabled, value); - } + [ObservableProperty] + private bool _isConsumeEnabled; - private bool _isRepeatEnabled; /// /// Is the song going to repeat when finished /// - public bool IsRepeatEnabled - { - get => _isRepeatEnabled; - set - { - Set(ref _isRepeatEnabled, value); - - if (value) - RepeatIcon = _interop.GetIcon(PlaybackIcon.Repeat); - else - RepeatIcon = _interop.GetIcon(PlaybackIcon.RepeatOff); - } - } + [ObservableProperty] + private bool _isRepeatEnabled; - private bool _isSingleEnabled; /// /// Is the song going to loop when finished /// - public bool IsSingleEnabled + [ObservableProperty] + private bool _isSingleEnabled; + + partial void OnIsRepeatEnabledChanged(bool value) { - get => _isSingleEnabled; - set - { - Set(ref _isSingleEnabled, value); + if (value) + RepeatIcon = _interop.GetIcon(PlaybackIcon.Repeat); + else + RepeatIcon = _interop.GetIcon(PlaybackIcon.RepeatOff); + } - // Update UI icon - if (value) - RepeatIcon = _interop.GetIcon(PlaybackIcon.RepeatSingle); - else - RepeatIcon = _interop.GetIcon(PlaybackIcon.RepeatOff); - } + partial void OnIsSingleEnabledChanged(bool value) + { + // Update UI icon + if (value) + RepeatIcon = _interop.GetIcon(PlaybackIcon.RepeatSingle); + else + RepeatIcon = _interop.GetIcon(PlaybackIcon.RepeatOff); } #endregion Getters and Setters @@ -340,38 +272,43 @@ public bool IsSingleEnabled /// protected async void UpdateInformation(object sender, EventArgs e) { + var status = _mpdService.CurrentStatus; + // Only call the following if the player exists and the time is greater then 0. - if (_mpdService.CurrentStatus.Elapsed.TotalMilliseconds <= 0) + if (status.Elapsed.TotalMilliseconds <= 0) return; - if (CurrentTrack == null) - 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(_mpdService.CurrentStatus); + await UpdateUpNextAsync(status); - await _dispatcherService.ExecuteOnUIThreadAsync(() => - { - var status = _mpdService.CurrentStatus; + if (CurrentTrack == null) + return; - // Set the current time value - if the user isn't scrobbling the slider - if (!_isUserMovingSlider) - { - CurrentTimeValue = status.Elapsed.TotalSeconds; + // Set the current time value - if the user isn't scrobbling the slider + if (!_isUserMovingSlider) + { + CurrentTimeValue = status.Elapsed.TotalSeconds; - // Set the time listened text - TimeListened = Miscellaneous.FormatTimeString(status.Elapsed.TotalMilliseconds); - } + // Set the time listened text + TimeListened = Miscellaneous.FormatTimeString(status.Elapsed.TotalMilliseconds); + } - // Get the remaining time for the track - var remainingTime = _mpdService.CurrentStatus.Duration.Subtract(status.Elapsed); + // Get the remaining time for the track + var remainingTime = _mpdService.CurrentStatus.Duration.Subtract(status.Elapsed); - // Set the time remaining text - TimeRemaining = "-" + Miscellaneous.FormatTimeString(remainingTime.TotalMilliseconds); + // Set the time remaining text + TimeRemaining = "-" + Miscellaneous.FormatTimeString(remainingTime.TotalMilliseconds); - // Set the maximum value - MaxTimeValue = status.Duration.TotalSeconds; - }); + // Set the maximum value + MaxTimeValue = status.Duration.TotalSeconds; } #endregion Timer Methods @@ -440,7 +377,7 @@ public void ToggleShuffle() { await _mpdService.SafelySendCommandAsync(new RandomCommand(IsShuffleEnabled)); Thread.Sleep(1000); // Wait for MPD to acknowledge the new status... - await _dispatcherService.ExecuteOnUIThreadAsync(async () => await UpdateUpNextAsync(_mpdService.CurrentStatus)); + await UpdateUpNextAsync(_mpdService.CurrentStatus); }, cts.Token)); } @@ -486,6 +423,8 @@ public void NavigateNowPlaying() _navigationService.Navigate(this); } + public bool IsFullScreen => _navigationService.CurrentPageViewModelType == typeof(PlaybackViewModelBase); + #endregion Track Control Methods #region Track Playback State @@ -537,6 +476,7 @@ public async void OnPlayingSliderChange() TimeRemaining = "-" + Miscellaneous.FormatTimeString(remainingTime.TotalMilliseconds); // Set the track position + CurrentTimeValue = Math.Round(CurrentTimeValue); // Fractional values don't seem to work well on iOS await _mpdService.SafelySendCommandAsync(new SeekCurCommand(CurrentTimeValue)); // Wait for MPD Status to catch up before we start auto-updating the slider again @@ -580,6 +520,10 @@ private async void OnTrackChange(object sender, SongChangedEventArgs eventArgs) if (response != null) { IsTrackInfoAvailable = true; + + // Dispose previous track + CurrentTrack?.Dispose(); + // Set the new current track CurrentTrack = _trackVmFactory.GetTrackViewModel(response); } @@ -596,16 +540,17 @@ private async void OnTrackChange(object sender, SongChangedEventArgs eventArgs) CurrentTimeValue = 0; MaxTimeValue = CurrentTrack.File.Time; - _ = Task.Run(async () => { + _ = Task.Run(async () => + { await _interop.UpdateOperatingSystemIntegrationsAsync(CurrentTrack); await CurrentTrack.GetAlbumArtAsync(HostType, _albumArtCancellationSource.Token); // Re-update OS integrations, as we have album art now if (!_albumArtCancellationSource.Token.IsCancellationRequested) - await _interop.UpdateOperatingSystemIntegrationsAsync(CurrentTrack); + await _interop.UpdateOperatingSystemIntegrationsAsync(CurrentTrack); }).ConfigureAwait(false); - await _dispatcherService.ExecuteOnUIThreadAsync(async () => await UpdateUpNextAsync(_mpdService.CurrentStatus)); + await UpdateUpNextAsync(_mpdService.CurrentStatus); } else { @@ -614,66 +559,62 @@ private async void OnTrackChange(object sender, SongChangedEventArgs eventArgs) } } - private async void OnStateChange(object sender, EventArgs eventArgs) + private void OnStateChange(object sender, EventArgs eventArgs) { var status = _mpdService.CurrentStatus; - await _dispatcherService.ExecuteOnUIThreadAsync(() => + // Remove completed requests + volumeTasks.RemoveAll(t => t.IsCompleted); + stateTasks.RemoveAll(t => t.IsCompleted); + + // Update volume to match the server value -- If we're not setting it ourselves + if (volumeTasks.Count == 0) { - // Remove completed requests - volumeTasks.RemoveAll(t => t.IsCompleted); - stateTasks.RemoveAll(t => t.IsCompleted); + MediaVolume = status.Volume; + } - // Update volume to match the server value -- If we're not setting it ourselves - if (volumeTasks.Count == 0) - { - _internalVolume = status.Volume; - OnPropertyChanged(nameof(MediaVolume)); - } + // Ditto for shuffle/repeat/single + if (stateTasks.Count == 0) + { + IsShuffleEnabled = status.Random; + IsRepeatEnabled = status.Repeat; + IsSingleEnabled = status.Single; + IsConsumeEnabled = status.Consume; - // Ditto for shuffle/repeat/single - if (stateTasks.Count == 0) - { - _isShuffledEnabled = status.Random; - _isRepeatEnabled = status.Repeat; - _isSingleEnabled = status.Single; - _isConsumeEnabled = status.Consume; - - if (_isSingleEnabled) - RepeatIcon = _interop.GetIcon(PlaybackIcon.RepeatSingle); - else if (_isRepeatEnabled) - RepeatIcon = _interop.GetIcon(PlaybackIcon.Repeat); - else - RepeatIcon = _interop.GetIcon(PlaybackIcon.RepeatOff); - - OnPropertyChanged(nameof(IsRepeatEnabled)); - OnPropertyChanged(nameof(IsShuffleEnabled)); - OnPropertyChanged(nameof(IsConsumeEnabled)); - } + if (status.Single) + RepeatIcon = _interop.GetIcon(PlaybackIcon.RepeatSingle); + else if (status.Repeat) + RepeatIcon = _interop.GetIcon(PlaybackIcon.Repeat); + else + RepeatIcon = _interop.GetIcon(PlaybackIcon.RepeatOff); + } - switch (status.State) - { - case MpdState.Play: - IsTrackInfoAvailable = true; - PlayButtonContent = _interop.GetIcon(PlaybackIcon.Pause); - break; - - case MpdState.Stop: - IsTrackInfoAvailable = true; - PlayButtonContent = _interop.GetIcon(PlaybackIcon.Play); - break; - - case MpdState.Pause: - IsTrackInfoAvailable = true; - PlayButtonContent = _interop.GetIcon(PlaybackIcon.Play); - break; - - default: - IsTrackInfoAvailable = false; - PlayButtonContent = _interop.GetIcon(PlaybackIcon.Play); - break; - } - }); + switch (status.State) + { + case MpdState.Play: + IsTrackInfoAvailable = true; + PlayButtonContent = _interop.GetIcon(PlaybackIcon.Pause); + LocalPlayback.Resume(); + break; + + case MpdState.Stop: + IsTrackInfoAvailable = true; + PlayButtonContent = _interop.GetIcon(PlaybackIcon.Play); + LocalPlayback.Stop(); + break; + + case MpdState.Pause: + IsTrackInfoAvailable = true; + PlayButtonContent = _interop.GetIcon(PlaybackIcon.Play); + LocalPlayback.Stop(); + break; + + default: + IsTrackInfoAvailable = false; + PlayButtonContent = _interop.GetIcon(PlaybackIcon.Play); + LocalPlayback.Stop(); + break; + } } #endregion Methods @@ -690,41 +631,37 @@ public virtual void Dispose() #region Commands - private ICommand _addToPlaylistCommand; - public ICommand AddToPlaylistCommand => _addToPlaylistCommand ?? (_addToPlaylistCommand = new RelayCommand(AddToPlaylist)); + [RelayCommand] private void AddToPlaylist(EventArgs obj) { // Track must exist if (CurrentTrack == null) { - _notificationService.ShowInAppNotification(Resources.NotificationNoTrackPlaying); + _notificationService.ShowInAppNotification(Resources.NotificationNoTrackPlaying, "", NotificationType.Warning); } - CurrentTrack?.AddToPlayListCommand.Execute(CurrentTrack.File); + CurrentTrack?.AddToPlaylistCommand.Execute(CurrentTrack.File); } - private ICommand _showAlbumCommand; - public ICommand ShowAlbumCommand => _showAlbumCommand ?? (_showAlbumCommand = new RelayCommand(ShowAlbum)); + [RelayCommand] private void ShowAlbum(EventArgs obj) { // Track must exist if (CurrentTrack == null) { - _notificationService.ShowInAppNotification(Resources.NotificationNoTrackPlaying); + _notificationService.ShowInAppNotification(Resources.NotificationNoTrackPlaying, "", NotificationType.Warning); return; } CurrentTrack.ViewAlbumCommand.Execute(CurrentTrack.File); } - private ICommand _compactViewCommand; - public ICommand SwitchToCompactViewCommand => _compactViewCommand ?? (_compactViewCommand = new AsyncRelayCommand(SwitchToCompactViewAsync)); - /// /// Switch to compact overlay mode /// + [RelayCommand] public abstract Task SwitchToCompactViewAsync(EventArgs obj); - + #endregion Method Bindings } diff --git a/Sources/Stylophone.Common/ViewModels/Bases/ShellViewModelBase.cs b/Sources/Stylophone.Common/ViewModels/Bases/ShellViewModelBase.cs index e36fea38..aeec3a62 100644 --- a/Sources/Stylophone.Common/ViewModels/Bases/ShellViewModelBase.cs +++ b/Sources/Stylophone.Common/ViewModels/Bases/ShellViewModelBase.cs @@ -4,9 +4,8 @@ using System.Reflection; using System.Threading.Tasks; using System.Windows.Input; - -using Microsoft.Toolkit.Mvvm.ComponentModel; -using Microsoft.Toolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using MpcNET; using MpcNET.Commands.Database; using MpcNET.Commands.Queue; @@ -14,13 +13,14 @@ using MpcNET.Commands.Status; using MpcNET.Tags; using MpcNET.Types; +using MpcNET.Types.Filters; using Stylophone.Common.Interfaces; using Stylophone.Common.Services; using Stylophone.Localization.Strings; namespace Stylophone.Common.ViewModels { - public abstract class ShellViewModelBase : ViewModelBase + public abstract partial class ShellViewModelBase : ViewModelBase { protected INavigationService _navigationService; protected INotificationService _notificationService; @@ -35,8 +35,7 @@ public ShellViewModelBase(INavigationService navigationService, INotificationSer // First View, use that to initialize our DispatcherService _dispatcherService.Initialize(); - - ((NotificationServiceBase)_notificationService).InAppNotificationRequested += ShowInAppNotification; + ((NavigationServiceBase)_navigationService).Navigated += OnFrameNavigated; TryUpdatePlaylists(); @@ -47,34 +46,21 @@ public ShellViewModelBase(INavigationService navigationService, INotificationSer }; } - private bool _isBackEnabled; - public bool IsBackEnabled - { - get { return _isBackEnabled; } - set { Set(ref _isBackEnabled, value); } - } - public bool IsServerUpdating => _mpdService.CurrentStatus.UpdatingDb != -1; - private string _shellHeader; - public string HeaderText - { - get { return _shellHeader; } - set { Set(ref _shellHeader, value); } - } + [ObservableProperty] + private string _headerText; - private ICommand _loadedCommand; - public ICommand LoadedCommand => _loadedCommand ?? (_loadedCommand = new RelayCommand(OnLoaded)); - - private ICommand _navigateCommand; - public ICommand NavigateCommand => _navigateCommand ?? (_navigateCommand = new RelayCommand(OnItemInvoked)); + [ObservableProperty] + private bool _isBackEnabled; - private ICommand _shuffleTracksCommand; - public ICommand AddRandomTracksCommand => _shuffleTracksCommand ?? (_shuffleTracksCommand = new RelayCommand(() => QueueRandomTracks(5))); + [RelayCommand] + protected abstract void Loaded(); + [RelayCommand] + protected abstract void Navigate(object item); + [RelayCommand] + private void AddRandomTracks() => QueueRandomTracks(5); - protected abstract void ShowInAppNotification(object sender, InAppNotificationRequestedEventArgs e); - protected abstract void OnLoaded(); - protected abstract void OnItemInvoked(object item); protected abstract void UpdatePlaylistNavigation(); private void OnFrameNavigated(object sender, CoreNavigationEventArgs e) @@ -98,7 +84,8 @@ public async Task> SearchAsync(string text) if (text.Length > 2) { - var response = await _mpdService.SafelySendCommandAsync(new SearchCommand(FindTags.Title, text)); + var filter = new FilterTag(FindTags.Title, text, FilterOperator.Contains); + var response = await _mpdService.SafelySendCommandAsync(new SearchCommand(filter)); if (response != null) { @@ -112,7 +99,7 @@ public async Task> SearchAsync(string text) private void QueueRandomTracks(int count) { - _notificationService.ShowInAppNotification(Resources.RandomTracksInProgress, false); + _notificationService.ShowInAppNotification(Resources.RandomTracksInProgress); _ = Task.Run(async () => { var response = await _mpdService.SafelySendCommandAsync(new StatsCommand()); @@ -124,7 +111,7 @@ private void QueueRandomTracks(int count) count--; // Pick a song n°, and queue it directly with searchadd var r = new Random().Next(0, songs-1); - commandList.Add(new SearchAddCommand(MpdTags.Title, "", r, r + 1)); + commandList.Add(new SearchAddCommand(new FilterTag(MpdTags.Title, "", FilterOperator.Contains), r, r + 1)); } await _mpdService.SafelySendCommandAsync(commandList); @@ -158,8 +145,7 @@ private void TryUpdatePlaylists() } catch (Exception e) { - //TODO localize - _notificationService.ShowInAppNotification($"Updating Playlist Navigation failed: {e.Message}", false); + _notificationService.ShowInAppNotification(Resources.ErrorUpdatingPlaylist, e.Message, NotificationType.Error); } }); } diff --git a/Sources/Stylophone.Common/ViewModels/Bases/ViewModelBase.cs b/Sources/Stylophone.Common/ViewModels/Bases/ViewModelBase.cs index 3b80f599..3250e15a 100644 --- a/Sources/Stylophone.Common/ViewModels/Bases/ViewModelBase.cs +++ b/Sources/Stylophone.Common/ViewModels/Bases/ViewModelBase.cs @@ -1,8 +1,9 @@ -using Microsoft.Toolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using Stylophone.Common.Interfaces; using Stylophone.Localization.Strings; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; @@ -27,25 +28,12 @@ public ViewModelBase(IDispatcherService dispatcherService) } /// - /// Compares the current and new values for a given property.If the value has changed, - /// raises the Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject.PropertyChanging - /// event, updates the property with the new value, then raises the Microsoft.Toolkit.Mvvm.ComponentModel.ObservableObject.PropertyChanged - /// event. - /// - /// This wrapper method calls SetProperty on the UI Thread. + /// This wrapper method calls OnPropertyChanged on the UI Thread. Thanks WinRT! /// - /// - protected bool Set(ref T field, T newValue, [CallerMemberName] string propertyName = null) + /// + protected override void OnPropertyChanged(PropertyChangedEventArgs e) { - var oldValue = field; - field = newValue; - - _dispatcherService.ExecuteOnUIThreadAsync(() => SetProperty(ref oldValue, newValue, propertyName)); - - if (newValue != null) - return !newValue.Equals(oldValue); - else - return false; + _dispatcherService.ExecuteOnUIThreadAsync(() => base.OnPropertyChanged(e)); } } } diff --git a/Sources/Stylophone.Common/ViewModels/FoldersViewModel.cs b/Sources/Stylophone.Common/ViewModels/FoldersViewModel.cs index ebb02442..fec62657 100644 --- a/Sources/Stylophone.Common/ViewModels/FoldersViewModel.cs +++ b/Sources/Stylophone.Common/ViewModels/FoldersViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.ObjectModel; using System.Threading.Tasks; -using Microsoft.Toolkit.Mvvm.ComponentModel; using MpcNET.Commands.Database; using Stylophone.Common.Interfaces; using Stylophone.Common.Services; diff --git a/Sources/Stylophone.Common/ViewModels/Items/AlbumViewModel.cs b/Sources/Stylophone.Common/ViewModels/Items/AlbumViewModel.cs index a991117b..b3115af1 100644 --- a/Sources/Stylophone.Common/ViewModels/Items/AlbumViewModel.cs +++ b/Sources/Stylophone.Common/ViewModels/Items/AlbumViewModel.cs @@ -1,5 +1,5 @@ -using Microsoft.Toolkit.Mvvm.ComponentModel; -using Microsoft.Toolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using MpcNET; using MpcNET.Commands.Database; using MpcNET.Commands.Playback; @@ -12,6 +12,7 @@ using Stylophone.Common.Interfaces; using Stylophone.Common.Services; using Stylophone.Localization.Strings; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -47,7 +48,7 @@ public AlbumViewModel GetAlbumViewModel(string albumName) } } - public class AlbumViewModel : ViewModelBase + public partial class AlbumViewModel : ViewModelBase, IDisposable { private INotificationService _notificationService; private IInteropService _interop; @@ -72,48 +73,32 @@ internal AlbumViewModel(AlbumViewModelFactory factory, string albumName) : base( DominantColor = _interop.GetAccentColor(); } - public string Name - { - get => _name; - set => Set(ref _name, value); - } + [ObservableProperty] private string _name; + [ObservableProperty] private string _artist; - public string Artist - { - get => _artist; - private set => Set(ref _artist, value); - - } - public List Files - { - get => _files; - set => Set(ref _files, value); - } + [ObservableProperty] private List _files; - private bool _detailLoading; - public bool IsDetailLoading - { - get => _detailLoading; - set => Set(ref _detailLoading, value); - } + [ObservableProperty] + private bool _isDetailLoading; - private bool _artLoaded; - public bool AlbumArtLoaded - { - get => _artLoaded; - private set => Set(ref _artLoaded, value); - } + [ObservableProperty] + private bool _albumArtLoaded; + [ObservableProperty] private SKImage _albumArt; - public SKImage AlbumArt - { - get => _albumArt; - private set => Set(ref _albumArt, value); - } + + [ObservableProperty] + private SKColor _dominantColor; + + /// + /// If the dominant color of the album is too light to show white text on top of, this boolean will be true. + /// + [ObservableProperty] + private bool _isLight; internal void SetAlbumArt(AlbumArt art) { @@ -127,27 +112,7 @@ internal void SetAlbumArt(AlbumArt art) AlbumArtLoaded = true; } - - private SKColor _albumColor; - public SKColor DominantColor - { - get => _albumColor; - set => Set(ref _albumColor, value); - } - - - private bool _isLight; - /// - /// If the dominant color of the album is too light to show white text on top of, this boolean will be true. - /// - public bool IsLight - { - get => _isLight; - private set => Set(ref _isLight, value); - } - - private ICommand _addToPlaylistCommand; - public ICommand AddToPlaylistCommand => _addToPlaylistCommand ?? (_addToPlaylistCommand = new RelayCommand(AddToPlaylist)); + [RelayCommand] private async void AddToPlaylist() { var playlistName = await _dialogService.ShowAddToPlaylistDialog(); @@ -166,15 +131,14 @@ private async void AddToPlaylist() } } - private ICommand _addToQueueCommand; - public ICommand AddAlbumCommand => _addToQueueCommand ?? (_addToQueueCommand = new RelayCommand(AddToQueue)); - private async void AddToQueue() + [RelayCommand] + private async void AddAlbum() { var commandList = new CommandList(); if (Files.Count == 0) { - _notificationService.ShowInAppNotification(string.Format(Resources.ErrorAddingAlbum, Resources.NotificationNoTracksLoaded), false); + _notificationService.ShowInAppNotification(Resources.NotificationNoTracksLoaded, "", NotificationType.Warning); return; } @@ -187,13 +151,12 @@ private async void AddToQueue() _notificationService.ShowInAppNotification(Resources.NotificationAddedToQueue); } - private ICommand _playCommand; - public ICommand PlayAlbumCommand => _playCommand ?? (_playCommand = new RelayCommand(PlayAlbum)); + [RelayCommand] private async void PlayAlbum() { if (Files.Count == 0) { - _notificationService.ShowInAppNotification(string.Format(Resources.ErrorPlayingTrack, Resources.NotificationNoTracksLoaded), false); + _notificationService.ShowInAppNotification(Resources.NotificationNoTracksLoaded, "", NotificationType.Warning); return; } @@ -264,5 +227,12 @@ public async Task LoadAlbumDataAsync(MpcConnection c) IsDetailLoading = false; } } + + public void Dispose() + { + AlbumArt?.Dispose(); + AlbumArt = null; + AlbumArtLoaded = false; + } } } diff --git a/Sources/Stylophone.Common/ViewModels/Items/FilePathViewModel.cs b/Sources/Stylophone.Common/ViewModels/Items/FilePathViewModel.cs index df18e124..b323e154 100644 --- a/Sources/Stylophone.Common/ViewModels/Items/FilePathViewModel.cs +++ b/Sources/Stylophone.Common/ViewModels/Items/FilePathViewModel.cs @@ -12,8 +12,9 @@ using MpcNET; using Stylophone.Common.Interfaces; using Stylophone.Common.Services; -using Microsoft.Toolkit.Mvvm.Input; using Stylophone.Localization.Strings; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; namespace Stylophone.Common.ViewModels { @@ -42,7 +43,7 @@ public FilePathViewModel GetFilePathViewModel(IMpdFilePath file, FilePathViewMod } } - public class FilePathViewModel : ViewModelBase + public partial class FilePathViewModel : ViewModelBase { private INotificationService _notificationService; private IDialogService _dialogService; @@ -65,10 +66,10 @@ internal FilePathViewModel(FilePathViewModelFactory factory, IMpdFilePath file, if (file is MpdDirectory) { IsDirectory = true; - _childPaths = new RangedObservableCollection(); + _children = new RangedObservableCollection(); // Add a bogus child that'll be replaced when the list is loaded - _childPaths.Add(new FilePathViewModel(Resources.FoldersLoadingTreeItem, this, _dispatcherService)); + _children.Add(new FilePathViewModel(Resources.FoldersLoadingTreeItem, this, _dispatcherService)); } } @@ -77,45 +78,28 @@ public FilePathViewModel(string name, FilePathViewModel parent, IDispatcherServi { Name = name; Parent = parent; - _childPaths = new RangedObservableCollection(); + _children = new RangedObservableCollection(); } + [ObservableProperty] private string _path; - public string Path - { - get => _path; - private set => Set(ref _path, value); - } + [ObservableProperty] private string _name; - public string Name - { - get => _name; - private set => Set(ref _name, value); - } - - public bool IsDirectory { get; set; } + [ObservableProperty] private bool _isLoaded; - public bool IsLoaded - { - get => _isLoaded; - private set => Set(ref _isLoaded, value); - } - public FilePathViewModel Parent { get; } + [ObservableProperty] + private RangedObservableCollection _children; - private RangedObservableCollection _childPaths; - public RangedObservableCollection Children - { - get => _childPaths; - set => Set(ref _childPaths, value); - } + public bool IsDirectory { get; set; } + public FilePathViewModel Parent { get; } private bool _isLoadingChildren; public async Task LoadChildrenAsync() { - if (IsLoaded || _isLoadingChildren || IsDirectory == false || _childPaths == null || Path == null) return; + if (IsLoaded || _isLoadingChildren || IsDirectory == false || _children == null || Path == null) return; _isLoadingChildren = true; try @@ -134,8 +118,8 @@ public async Task LoadChildrenAsync() await _dispatcherService.ExecuteOnUIThreadAsync(() => { - _childPaths.AddRange(newChildren); - _childPaths.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; }); } @@ -145,10 +129,9 @@ await _dispatcherService.ExecuteOnUIThreadAsync(() => } } - private ICommand _playCommand; - public ICommand PlayCommand => _playCommand ?? (_playCommand = new RelayCommand(PlayPath)); - private async void PlayPath() + [RelayCommand] + private async void Play() { // Clear queue, add path and play var commandList = new CommandList(new IMpcCommand[] { new ClearCommand(), new AddCommand(Path), new PlayCommand(0) }); @@ -159,9 +142,7 @@ private async void PlayPath() } } - private ICommand _addToQueueCommand; - public ICommand AddToQueueCommand => _addToQueueCommand ?? (_addToQueueCommand = new RelayCommand(AddToQueue)); - + [RelayCommand] private async void AddToQueue() { // AddCommand adds either the full directory or the song, depending on the path given. @@ -171,8 +152,7 @@ private async void AddToQueue() _notificationService.ShowInAppNotification(Resources.NotificationAddedToQueue); } - private ICommand _addToPlaylistCommand; - public ICommand AddToPlaylistCommand => _addToPlaylistCommand ?? (_addToPlaylistCommand = new RelayCommand(AddToPlaylist)); + [RelayCommand] private async void AddToPlaylist() { var playlistName = await _dialogService.ShowAddToPlaylistDialog(); diff --git a/Sources/Stylophone.Common/ViewModels/Items/TrackViewModel.cs b/Sources/Stylophone.Common/ViewModels/Items/TrackViewModel.cs index bb321d42..76371ee8 100644 --- a/Sources/Stylophone.Common/ViewModels/Items/TrackViewModel.cs +++ b/Sources/Stylophone.Common/ViewModels/Items/TrackViewModel.cs @@ -3,16 +3,16 @@ using MpcNET.Types; using System; using System.Threading.Tasks; -using System.Windows.Input; using System.Linq; using MpcNET.Commands.Queue; using System.Threading; using Stylophone.Common.Services; using SkiaSharp; -using Microsoft.Toolkit.Mvvm.Input; using Stylophone.Common.Interfaces; using Stylophone.Localization.Strings; using Stylophone.Common.Helpers; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; namespace Stylophone.Common.ViewModels { @@ -48,7 +48,7 @@ public TrackViewModel GetTrackViewModel(IMpdFile file) } } - public class TrackViewModel : ViewModelBase + public partial class TrackViewModel : ViewModelBase, IDisposable { private INotificationService _notificationService; private INavigationService _navigationService; @@ -71,52 +71,31 @@ internal TrackViewModel(TrackViewModelFactory factory, IMpdFile file): base(fact _mpdService.SongChanged += (s, e) => UpdatePlayingStatus(); File = file; - DominantColor = new SKColor(); + + _dispatcherService.ExecuteOnUIThreadAsync(() => DominantColor = _interop.GetAccentColor()); } public IMpdFile File { get; } - public string Name => File.HasTitle ? File.Title : File.Path.Split('/').Last(); - public bool IsPlaying => _mpdService.CurrentStatus.SongId != -1 && _mpdService.CurrentStatus.SongId == File.Id; - public void UpdatePlayingStatus() => _dispatcherService.ExecuteOnUIThreadAsync(() => OnPropertyChanged(nameof(IsPlaying))); + [ObservableProperty] private SKImage _albumArt; - public SKImage AlbumArt - { - get => _albumArt; - private set => Set(ref _albumArt, value); - } - - private SKColor _albumColor; - public SKColor DominantColor - { - get => _albumColor; - private set => Set(ref _albumColor, value); - } + + [ObservableProperty] + private SKColor _dominantColor; + [ObservableProperty] private bool _isLight; - public bool IsLight - { - get => _isLight; - private set => Set(ref _isLight, value); - } - - - private ICommand _playCommand; - public ICommand PlayTrackCommand => _playCommand ?? (_playCommand = new RelayCommand(PlayTrack)); + [RelayCommand] private async void PlayTrack(IMpdFile file) => await _mpdService.SafelySendCommandAsync(new PlayIdCommand(file.Id)); - private ICommand _removeCommand; - public ICommand RemoveFromQueueCommand => _removeCommand ?? (_removeCommand = new RelayCommand(RemoveTrack)); - - private async void RemoveTrack(IMpdFile file) => await _mpdService.SafelySendCommandAsync(new DeleteIdCommand(file.Id)); - - private ICommand _addToQueueCommand; - public ICommand AddToQueueCommand => _addToQueueCommand ?? (_addToQueueCommand = new RelayCommand(AddToQueue)); + [RelayCommand] + private async void RemoveFromQueue(IMpdFile file) => await _mpdService.SafelySendCommandAsync(new DeleteIdCommand(file.Id)); + [RelayCommand] private async void AddToQueue(IMpdFile file) { var response = await _mpdService.SafelySendCommandAsync(new AddIdCommand(file.Path)); @@ -125,10 +104,8 @@ private async void AddToQueue(IMpdFile file) _notificationService.ShowInAppNotification(Resources.NotificationAddedToQueue); } - private ICommand _addToPlaylistCommand; - public ICommand AddToPlayListCommand => _addToPlaylistCommand ?? (_addToPlaylistCommand = new RelayCommand(AddToPlaylist)); - - private async void AddToPlaylist(IMpdFile file) + [RelayCommand] + private async void AddToPlaylist(IMpdFile file) { var playlistName = await _dialogService.ShowAddToPlaylistDialog(); if (playlistName == null) return; @@ -139,8 +116,26 @@ private async void AddToPlaylist(IMpdFile file) _notificationService.ShowInAppNotification(string.Format(Resources.NotificationAddedToPlaylist, playlistName)); } - private ICommand _viewAlbumCommand; - public ICommand ViewAlbumCommand => _viewAlbumCommand ?? (_viewAlbumCommand = new RelayCommand(GoToMatchingAlbum)); + [RelayCommand] + private void ViewAlbum(IMpdFile file) + { + try + { + if (!file.HasAlbum) + { + _notificationService.ShowInAppNotification(Resources.ErrorNoMatchingAlbum, "", NotificationType.Warning); + return; + } + + // Build an AlbumViewModel from the album name and navigate to it + var album = _albumVmFactory.GetAlbumViewModel(file.Album); + _navigationService.Navigate(album); + } + catch (Exception e) + { + _notificationService.ShowErrorNotification(e); + } + } /// /// Fires off an async request to get the album art from MPD. @@ -168,24 +163,9 @@ public async Task GetAlbumArtAsync(VisualizationType hostType = VisualizationTyp } } - private void GoToMatchingAlbum(IMpdFile file) + public void Dispose() { - try - { - if (!file.HasAlbum) - { - _notificationService.ShowInAppNotification(Resources.ErrorNoMatchingAlbum, false); - return; - } - - // Build an AlbumViewModel from the album name and navigate to it - var album = _albumVmFactory.GetAlbumViewModel(file.Album); - _navigationService.Navigate(album); - } - catch (Exception e) - { - _notificationService.ShowErrorNotification(e); - } + AlbumArt?.Dispose(); } } diff --git a/Sources/Stylophone.Common/ViewModels/LocalPlaybackViewModel.cs b/Sources/Stylophone.Common/ViewModels/LocalPlaybackViewModel.cs index 75f22ca0..4b07cedb 100644 --- a/Sources/Stylophone.Common/ViewModels/LocalPlaybackViewModel.cs +++ b/Sources/Stylophone.Common/ViewModels/LocalPlaybackViewModel.cs @@ -1,4 +1,7 @@ using System; +using System.Threading; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; using LibVLCSharp.Shared; using Stylophone.Common.Interfaces; using Stylophone.Common.Services; @@ -6,7 +9,7 @@ namespace Stylophone.Common.ViewModels { - public class LocalPlaybackViewModel : ViewModelBase + public partial class LocalPlaybackViewModel : ViewModelBase { private IInteropService _interopService; private INotificationService _notificationService; @@ -17,7 +20,7 @@ public class LocalPlaybackViewModel : ViewModelBase private MediaPlayer _mediaPlayer; private string _serverHost; - public LocalPlaybackViewModel(SettingsViewModel settingsVm, MPDConnectionService mpdService, IInteropService interopService, INotificationService notificationService, IDispatcherService dispatcherService): base(dispatcherService) + public LocalPlaybackViewModel(SettingsViewModel settingsVm, MPDConnectionService mpdService, IInteropService interopService, INotificationService notificationService, IDispatcherService dispatcherService) : base(dispatcherService) { _interopService = interopService; _notificationService = notificationService; @@ -26,6 +29,7 @@ public LocalPlaybackViewModel(SettingsViewModel settingsVm, MPDConnectionService _volumeIcon = _interopService.GetIcon(PlaybackIcon.VolumeMute); + // TODO this'd be better with an IMessenger + [NotifyPropertyChangedRecipients] in SettingsViewModel _settingsVm.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(_settingsVm.IsLocalPlaybackEnabled)) @@ -34,6 +38,23 @@ public LocalPlaybackViewModel(SettingsViewModel settingsVm, MPDConnectionService if (e.PropertyName == nameof(_settingsVm.ServerHost)) _serverHost = _settingsVm.ServerHost; }; + + // Run an idle loop in a spare thread to make sure the libVLC volume is always accurate + // Workaround for UWP, see https://code.videolan.org/videolan/vlc/-/commit/6ea058bf2d0813dab247f973b2d7bc9804486d81 + Task.Run(() => + { + while (true) + { + try + { + if (IsPlaying && _mediaPlayer != null && _mediaPlayer.Volume != _volume) + _mediaPlayer.Volume = _volume; + + Thread.Sleep(500); + } + catch (Exception) { } + } + }); } public void Initialize(string host, bool isEnabled) @@ -42,127 +63,95 @@ public void Initialize(string host, bool isEnabled) IsEnabled = isEnabled; } - private bool _isEnabled; - public bool IsEnabled + public void Stop() { - get => _isEnabled; - private set - { - Set(ref _isEnabled, value); - - if (value) - { - if (_vlcCore == null) - _vlcCore = new LibVLC(); - - _mediaPlayer?.Dispose(); - _mediaPlayer = new MediaPlayer(_vlcCore); - } - else - { - // Reset - IsPlaying = false; - Volume = 0; - _previousVolume = 10; - - _vlcCore?.Dispose(); - _vlcCore = null; - } - - } + if (IsEnabled && Volume != 0) + IsPlaying = false; } - private string _volumeIcon; - /// - /// The current text for the volume icon - /// - public string VolumeIcon + public void Resume() { - get => _volumeIcon; - private set => Set(ref _volumeIcon, value); + if (IsEnabled && Volume != 0) + IsPlaying = true; } + [ObservableProperty] + private bool _isEnabled; + + [ObservableProperty] + private string _volumeIcon; + + [ObservableProperty] private int _volume = 0; - public int Volume + + [ObservableProperty] + private bool _isPlaying; + + partial void OnIsEnabledChanged(bool value) { - get => _volume; - set + if (value) { - Set(ref _volume, value); - - // If the user changed the volume, play the stream back - if (!IsPlaying && value != 0) - IsPlaying = true; + if (_vlcCore == null) + _vlcCore = new LibVLC(); - if (_mediaPlayer != null) - _mediaPlayer.Volume = value; + _mediaPlayer?.Dispose(); + + _mediaPlayer = new MediaPlayer(_vlcCore); + } + else + { + // Reset + IsPlaying = false; + Volume = 0; + _previousVolume = 10; - if (value == 0) - { - VolumeIcon = _interopService.GetIcon(PlaybackIcon.VolumeMute); - } - else if (value < 25) - { - VolumeIcon = _interopService.GetIcon(PlaybackIcon.Volume25); - } - else if (value < 50) - { - VolumeIcon = _interopService.GetIcon(PlaybackIcon.Volume50); - } - else if (value < 75) - { - VolumeIcon = _interopService.GetIcon(PlaybackIcon.Volume75); - } - else - { - VolumeIcon = _interopService.GetIcon(PlaybackIcon.VolumeFull); - } + _vlcCore?.Dispose(); + _vlcCore = null; } } - private bool _isPlaying; - public bool IsPlaying + partial void OnVolumeChanged(int value) { - get => _isPlaying; - private set + // If the user changed the volume, play the stream back + if (!IsPlaying && value != 0) + IsPlaying = true; + + if (_mediaPlayer != null) + _mediaPlayer.Volume = value; + + if (value == 0) { - Set(ref _isPlaying, value); - UpdatePlayback(); + VolumeIcon = _interopService.GetIcon(PlaybackIcon.VolumeMute); } - } - - private int _previousVolume = 25; - /// - /// Toggle if we should mute - /// - public void ToggleMute() - { - if (Volume > 0) + else if (value < 25) { - _previousVolume = Volume; - IsPlaying = false; - Volume = 0; + VolumeIcon = _interopService.GetIcon(PlaybackIcon.Volume25); + } + else if (value < 50) + { + VolumeIcon = _interopService.GetIcon(PlaybackIcon.Volume50); + } + else if (value < 75) + { + VolumeIcon = _interopService.GetIcon(PlaybackIcon.Volume75); } else { - Volume = _previousVolume; // Setting MediaVolume automatically starts playback + VolumeIcon = _interopService.GetIcon(PlaybackIcon.VolumeFull); } } - private void UpdatePlayback() + partial void OnIsPlayingChanged(bool value) { try { - if (IsPlaying && _serverHost != null && _mpdService.IsConnected) + if (value && _serverHost != null && _mpdService.IsConnected) { var urlString = "http://" + _serverHost + ":8000"; var streamUrl = new Uri(urlString); var media = new Media(_vlcCore, streamUrl); _mediaPlayer.Play(media); - - // This set won't work on UWP, see https://code.videolan.org/videolan/LibVLCSharp/-/issues/423 - _mediaPlayer.Volume = _volume; } else { @@ -171,8 +160,27 @@ private void UpdatePlayback() } catch (Exception e) { - _notificationService.ShowInAppNotification(string.Format(Resources.ErrorPlayingMPDStream, e.Message), false); + _notificationService.ShowInAppNotification(Resources.ErrorPlayingMPDStream, e.Message, NotificationType.Error); + } + } + + private int _previousVolume = 25; + /// + /// Toggle if we should mute + /// + public void ToggleMute() + { + if (Volume > 0) + { + _previousVolume = Volume; + IsPlaying = false; + Volume = 0; + } + else + { + Volume = _previousVolume; // Setting MediaVolume automatically starts playback } } + } } diff --git a/Sources/Stylophone.Common/ViewModels/PlaylistViewModel.cs b/Sources/Stylophone.Common/ViewModels/PlaylistViewModel.cs index 41ec0b17..917e1a18 100644 --- a/Sources/Stylophone.Common/ViewModels/PlaylistViewModel.cs +++ b/Sources/Stylophone.Common/ViewModels/PlaylistViewModel.cs @@ -3,10 +3,11 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Input; -using Microsoft.Toolkit.Mvvm.ComponentModel; -using Microsoft.Toolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using MpcNET; using MpcNET.Commands.Playback; using MpcNET.Commands.Playlist; @@ -20,7 +21,7 @@ namespace Stylophone.Common.ViewModels { - public class PlaylistViewModel : ViewModelBase + public partial class PlaylistViewModel : ViewModelBase, IDisposable { private INotificationService _notificationService; private INavigationService _navigationService; @@ -43,81 +44,47 @@ public PlaylistViewModel(INotificationService notificationService, INavigationSe _trackVmFactory = trackVmFactory; Source.CollectionChanged += (s, e) => OnPropertyChanged(nameof(IsSourceEmpty)); + DominantColor = _interop.GetAccentColor(); } private NotifyCollectionChangedAction _previousAction; private int _oldId; + private CancellationTokenSource _albumArtCts; public ObservableCollection Source { get; private set; } = new ObservableCollection(); public bool IsSourceEmpty => Source.Count == 0; + [ObservableProperty] private string _name; - public string Name - { - get => _name; - set => Set(ref _name, value); - } + [ObservableProperty] private string _artists; - public string Artists - { - get => _artists; - private set => Set(ref _artists, value); - } - private string _info; - public string PlaylistInfo - { - get => _info; - private set => Set(ref _info, value); - } + [ObservableProperty] + private string _playlistInfo; + [ObservableProperty] private bool _artLoaded; - public bool ArtLoaded - { - get => _artLoaded; - set => Set(ref _artLoaded, value); - } + [ObservableProperty] private SKImage _playlistArt; - public SKImage PlaylistArt - { - get => _playlistArt; - private set => Set(ref _playlistArt, value); - } + [ObservableProperty] private SKImage _playlistArt2; - public SKImage PlaylistArt2 - { - get => _playlistArt2; - private set => Set(ref _playlistArt2, value); - } + [ObservableProperty] private SKImage _playlistArt3; - public SKImage PlaylistArt3 - { - get => _playlistArt3; - private set => Set(ref _playlistArt3, value); - } - private SKColor _albumColor; - public SKColor DominantColor - { - get => _albumColor; - set => Set(ref _albumColor, value); - } + [ObservableProperty] + private SKColor _dominantColor; - private bool _isLight; /// /// If the dominant color of the album is too light to show white text on top of, this boolean will be true. /// - public bool IsLight - { - get => _isLight; - private set => Set(ref _isLight, value); - } - + [ObservableProperty] + private bool _isLight; + #region Commands private bool IsSingleTrackSelected(object list) { @@ -127,9 +94,8 @@ private bool IsSingleTrackSelected(object list) return (selectedTracks?.Count == 1); } - private ICommand _deletePlaylistCommand; - public ICommand RemovePlaylistCommand => _deletePlaylistCommand ?? (_deletePlaylistCommand = new RelayCommand(DeletePlaylist)); - private async void DeletePlaylist() + [RelayCommand] + private async void RemovePlaylist() { var result = await _dialogService.ShowConfirmDialogAsync(Resources.DeletePlaylistContentDialog, "", Resources.OKButtonText, Resources.CancelButtonText); @@ -145,8 +111,7 @@ private async void DeletePlaylist() } } - private ICommand _loadPlaylistCommand; - public ICommand LoadPlaylistCommand => _loadPlaylistCommand ?? (_loadPlaylistCommand = new RelayCommand(LoadPlaylist)); + [RelayCommand] private async void LoadPlaylist() { var res = await _mpdService.SafelySendCommandAsync(new LoadCommand(Name)); @@ -154,8 +119,8 @@ private async void LoadPlaylist() if (res != null) _notificationService.ShowInAppNotification(Resources.NotificationAddedToQueue); } - private ICommand _playCommand; - public ICommand PlayPlaylistCommand => _playCommand ?? (_playCommand = new RelayCommand(PlayPlaylist)); + + [RelayCommand] private async void PlayPlaylist() { // Clear queue, add playlist and play @@ -168,11 +133,11 @@ private async void PlayPlaylist() } } - private ICommand _addToQueueCommand; - public ICommand AddToQueueCommand => _addToQueueCommand ?? (_addToQueueCommand = new RelayCommand>(QueueTrack)); + [RelayCommand] - private async void QueueTrack(object list) + private async void AddToQueue(object list) { + // Cast the received __ComObject var selectedTracks = (IList)list; if (selectedTracks?.Count > 0) @@ -191,9 +156,7 @@ private async void QueueTrack(object list) } } - private ICommand _viewAlbumCommand; - public ICommand ViewAlbumCommand => _viewAlbumCommand ?? (_viewAlbumCommand = new RelayCommand>(ViewAlbum, IsSingleTrackSelected)); - + [RelayCommand(CanExecute = nameof(IsSingleTrackSelected))] private void ViewAlbum(object list) { // Cast the received __ComObject @@ -206,11 +169,11 @@ private void ViewAlbum(object list) } } - private ICommand _removeTrackCommand; - public ICommand RemoveTrackFromPlaylistCommand => _removeTrackCommand ?? (_removeTrackCommand = new RelayCommand> (RemoveTrack)); - private async void RemoveTrack(object list) + [RelayCommand] + private async void RemoveTrackFromPlaylist(object list) { + // Cast the received __ComObject var selectedTracks = (IList)list; if (selectedTracks?.Count > 0) @@ -239,12 +202,14 @@ private async void RemoveTrack(object list) public async Task LoadDataAsync(string playlistName) { var placeholder = await _interop.GetPlaceholderImageAsync(); + _albumArtCts = new CancellationTokenSource(); ArtLoaded = false; + PlaylistArt = placeholder; PlaylistArt2 = placeholder; PlaylistArt3 = placeholder; - + Name = playlistName; Source.CollectionChanged -= Source_CollectionChanged; Source.Clear(); @@ -267,7 +232,6 @@ public async Task LoadDataAsync(string playlistName) PlaylistInfo = $"{Source.Count} Tracks, Total Time: {Miscellaneous.ToReadableString(t)}"; - if (Source.Count > 0) { await Task.Run(async () => @@ -278,23 +242,27 @@ await Task.Run(async () => if (distinctAlbums.Count > 1) { - var art = await _albumArtService.GetAlbumArtAsync(distinctAlbums[0].File, true); + var art = await _albumArtService.GetAlbumArtAsync(distinctAlbums[0].File, true, _albumArtCts.Token); PlaylistArt = art != null ? art.ArtBitmap : PlaylistArt; DominantColor = (art?.DominantColor?.Color).GetValueOrDefault(); + + if (DominantColor == default(SKColor)) + DominantColor = _interop.GetAccentColor(); + IsLight = (!art?.DominantColor?.IsDark).GetValueOrDefault(); } if (distinctAlbums.Count > 2) { - var art = await _albumArtService.GetAlbumArtAsync(distinctAlbums[1].File, false); + var art = await _albumArtService.GetAlbumArtAsync(distinctAlbums[1].File, false, _albumArtCts.Token); PlaylistArt2 = art != null ? art.ArtBitmap : PlaylistArt2; } else PlaylistArt2 = PlaylistArt; if (distinctAlbums.Count > 3) { - var art = await _albumArtService.GetAlbumArtAsync(distinctAlbums[2].File, false); + var art = await _albumArtService.GetAlbumArtAsync(distinctAlbums[2].File, false, _albumArtCts.Token); PlaylistArt3 = art != null ? art.ArtBitmap : PlaylistArt3; } else PlaylistArt3 = PlaylistArt2; @@ -318,5 +286,19 @@ private async void Source_CollectionChanged(object sender, NotifyCollectionChang _oldId = e.OldStartingIndex; } } + + public void Dispose() + { + _albumArtCts?.Cancel(); + + PlaylistArt?.Dispose(); + PlaylistArt = null; + PlaylistArt2?.Dispose(); + PlaylistArt2 = null; + PlaylistArt3?.Dispose(); + PlaylistArt3 = null; + + ArtLoaded = false; + } } } diff --git a/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs b/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs index 12b48b8c..cfe7d4a9 100644 --- a/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs +++ b/Sources/Stylophone.Common/ViewModels/QueueViewModel.cs @@ -5,8 +5,8 @@ using System.Linq; using System.Threading.Tasks; using System.Windows.Input; -using Microsoft.Toolkit.Mvvm.ComponentModel; -using Microsoft.Toolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using MpcNET.Commands.Playback; using MpcNET.Commands.Playlist; using MpcNET.Commands.Queue; @@ -18,7 +18,7 @@ namespace Stylophone.Common.ViewModels { - public class QueueViewModel : ViewModelBase + public partial class QueueViewModel : ViewModelBase { private NotifyCollectionChangedAction _previousAction; private int _oldId; @@ -48,6 +48,12 @@ public QueueViewModel(IDialogService dialogService, INotificationService notific } public static new string GetHeader() => Resources.QueueHeader; + public RangedObservableCollection Source { get; } = new RangedObservableCollection(); + public bool IsSourceEmpty => Source.Count == 0; + + [ObservableProperty] + private int _playlistVersion; + private bool IsSingleTrackSelected(object list) { // Cast the received __ComObject @@ -56,9 +62,7 @@ private bool IsSingleTrackSelected(object list) return (selectedTracks?.Count == 1); } - private ICommand _playCommand; - public ICommand PlayTrackCommand => _playCommand ?? (_playCommand = new AsyncRelayCommand>(PlayTrackAsync, IsSingleTrackSelected)); - + [RelayCommand(CanExecute = nameof(IsSingleTrackSelected))] private async Task PlayTrackAsync(object list) { // Cast the received __ComObject @@ -71,9 +75,7 @@ private async Task PlayTrackAsync(object list) } } - private ICommand _viewAlbumCommand; - public ICommand ViewAlbumCommand => _viewAlbumCommand ?? (_viewAlbumCommand = new RelayCommand>(ViewAlbum, IsSingleTrackSelected)); - + [RelayCommand(CanExecute = nameof(IsSingleTrackSelected))] private void ViewAlbum(object list) { // Cast the received __ComObject @@ -85,12 +87,11 @@ private void ViewAlbum(object list) trackVM.ViewAlbumCommand.Execute(trackVM.File); } } - - private ICommand _removeCommand; - public ICommand RemoveFromQueueCommand => _removeCommand ?? (_removeCommand = new AsyncRelayCommand>(RemoveTrackAsync)); - - private async Task RemoveTrackAsync(object list) + + [RelayCommand] + private async Task RemoveFromQueue(object list) { + // Cast the received __ComObject var selectedTracks = (IList)list; if (selectedTracks?.Count > 0) @@ -109,16 +110,15 @@ private async Task RemoveTrackAsync(object list) } } - private ICommand _addToPlaylistCommand; - public ICommand AddToPlayListCommand => _addToPlaylistCommand ?? (_addToPlaylistCommand = new AsyncRelayCommand>(AddToPlaylist)); - + [RelayCommand] private async Task AddToPlaylist(object list) { + // Cast the received __ComObject + var selectedTracks = (IList)list; + var playlistName = await _dialogService.ShowAddToPlaylistDialog(); if (playlistName == null) return; - var selectedTracks = (IList)list; - if (selectedTracks?.Count > 0) { var commandList = new CommandList(); @@ -136,13 +136,8 @@ private async Task AddToPlaylist(object list) } } - private ICommand _saveQueueCommand; - /// - /// Write the current queue to a playlist. - /// - public ICommand SaveQueueCommand => _saveQueueCommand ?? (_saveQueueCommand = new AsyncRelayCommand(SaveQueueAsync)); - - private async Task SaveQueueAsync() + [RelayCommand] + private async Task SaveQueue() { var playlistName = await _dialogService.ShowAddToPlaylistDialog(false); if (playlistName == null) return; @@ -153,13 +148,9 @@ private async Task SaveQueueAsync() _notificationService.ShowInAppNotification(string.Format(Resources.NotificationAddedToPlaylist, playlistName)); } - private ICommand _clearQueueCommand; - /// - /// Clear the MPD queue. - /// - public ICommand ClearQueueCommand => _clearQueueCommand ?? (_clearQueueCommand = new AsyncRelayCommand(ClearQueueAsync)); - private async Task ClearQueueAsync() + [RelayCommand] + private async Task ClearQueue() { await _mpdService.SafelySendCommandAsync(new ClearCommand()); } @@ -191,7 +182,6 @@ private void MPDConnectionService_ConnectionChanged(object sender, EventArgs e) else _dispatcherService.ExecuteOnUIThreadAsync(() => Source.Clear()); } - private async void MPDConnectionService_QueueChanged(object sender, EventArgs e) { // Ask for a new status ourselves as the shared ConnectionService one might not be up to date yet @@ -248,17 +238,6 @@ private async void MPDConnectionService_QueueChanged(object sender, EventArgs e) } } - public RangedObservableCollection Source { get; } = new RangedObservableCollection(); - - public bool IsSourceEmpty => Source.Count == 0; - - private int _playlistVersion; - public int PlaylistVersion - { - get => _playlistVersion; - private set => Set(ref _playlistVersion, value); - } - public async Task LoadInitialDataAsync() { var tracks = new List(); diff --git a/Sources/Stylophone.Common/ViewModels/SearchResultsViewModel.cs b/Sources/Stylophone.Common/ViewModels/SearchResultsViewModel.cs index b81d1db3..6b382472 100644 --- a/Sources/Stylophone.Common/ViewModels/SearchResultsViewModel.cs +++ b/Sources/Stylophone.Common/ViewModels/SearchResultsViewModel.cs @@ -3,20 +3,22 @@ using System.Linq; using System.Threading.Tasks; using System.Windows.Input; -using Microsoft.Toolkit.Mvvm.ComponentModel; -using Microsoft.Toolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using MpcNET.Commands.Database; using MpcNET.Commands.Playlist; using MpcNET.Commands.Queue; using MpcNET.Commands.Reflection; using MpcNET.Tags; +using MpcNET.Types; +using MpcNET.Types.Filters; using Stylophone.Common.Interfaces; using Stylophone.Common.Services; using Stylophone.Localization.Strings; namespace Stylophone.Common.ViewModels { - public class SearchResultsViewModel : ViewModelBase + public partial class SearchResultsViewModel : ViewModelBase { private INotificationService _notificationService; @@ -37,80 +39,60 @@ public SearchResultsViewModel(IDispatcherService dispatcherService, INotificatio _searchTracks = true; } - private string _search; - public string QueryText - { - get { return _search; } - set { Set(ref _search, value); } - } + public static new string GetHeader() => string.Format(Resources.SearchResultsFor, "..."); // Fallback when we return to this page via navigation - private bool _isSearching; - public bool IsSearchInProgress - { - get { return _isSearching; } - set { - Set(ref _isSearching, value); - OnPropertyChanged(nameof(IsSourceEmpty)); - } - } + public ObservableCollection Source { get; } = new ObservableCollection(); + public bool IsSourceEmpty => !IsSearchInProgress && Source.Count == 0; - private bool _searchTracks; - public bool SearchTracks - { - get { return _searchTracks; } - set { - Set(ref _searchTracks, value); + [ObservableProperty] + private string _queryText; - if (value) - { - SearchAlbums = false; - SearchArtists = false; - } + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsSourceEmpty))] + private bool _isSearchInProgress; - if (value || (!SearchArtists && !SearchAlbums && !SearchTracks)) - UpdateSource(); - } - } + [ObservableProperty] + private bool _searchTracks; + [ObservableProperty] private bool _searchAlbums; - public bool SearchAlbums - { - get { return _searchAlbums; } - set { - Set(ref _searchAlbums, value); - if (value) - { - SearchTracks = false; - SearchArtists = false; - } + [ObservableProperty] + private bool _searchArtists; - if (value || (!SearchArtists && !SearchAlbums && !SearchTracks)) - UpdateSource(); + partial void OnSearchTracksChanged(bool value) + { + if (value) + { + SearchAlbums = false; + SearchArtists = false; } + + if (value || (!SearchArtists && !SearchAlbums && !SearchTracks)) + UpdateSource(); } - private bool _searchArtists; - public bool SearchArtists + partial void OnSearchAlbumsChanged(bool value) { - get { return _searchArtists; } - set { - Set(ref _searchArtists, value); - - if (value) - { - SearchTracks = false; - SearchAlbums = false; - } - - if (value || (!SearchArtists && !SearchAlbums && !SearchTracks)) - UpdateSource(); + if (value) + { + SearchTracks = false; + SearchArtists = false; } + if (value || (!SearchArtists && !SearchAlbums && !SearchTracks)) + UpdateSource(); } - public ObservableCollection Source { get; } = new ObservableCollection(); - - public bool IsSourceEmpty => !IsSearchInProgress && Source.Count == 0; + partial void OnSearchArtistsChanged(bool value) + { + if (value) + { + SearchTracks = false; + SearchAlbums = false; + } + if (value || (!SearchArtists && !SearchAlbums && !SearchTracks)) + UpdateSource(); + } #region Commands @@ -122,10 +104,8 @@ private bool IsSingleTrackSelected(object list) return (selectedTracks?.Count == 1); } - private ICommand _addToQueueCommand; - public ICommand AddToQueueCommand => _addToQueueCommand ?? (_addToQueueCommand = new RelayCommand>(QueueTrack)); - - private async void QueueTrack(object list) + [RelayCommand] + private async void AddToQueue(object list) { var selectedTracks = (IList)list; @@ -145,9 +125,7 @@ private async void QueueTrack(object list) } } - private ICommand _addToPlaylistCommand; - public ICommand AddToPlayListCommand => _addToPlaylistCommand ?? (_addToPlaylistCommand = new RelayCommand>(AddToPlaylist)); - + [RelayCommand] private async void AddToPlaylist(object list) { var playlistName = await _dialogService.ShowAddToPlaylistDialog(); @@ -172,9 +150,7 @@ private async void AddToPlaylist(object list) } } - private ICommand _viewAlbumCommand; - public ICommand ViewAlbumCommand => _viewAlbumCommand ?? (_viewAlbumCommand = new RelayCommand>(ViewAlbum, IsSingleTrackSelected)); - + [RelayCommand(CanExecute=nameof(IsSingleTrackSelected))] private void ViewAlbum(object list) { // Cast the received __ComObject @@ -205,13 +181,14 @@ private void UpdateSource() if (SearchAlbums) await DoSearchAsync(FindTags.Album); if (SearchArtists) await DoSearchAsync(FindTags.Artist); - await _dispatcherService.ExecuteOnUIThreadAsync(() => IsSearchInProgress = false); + IsSearchInProgress = false; }); } private async Task DoSearchAsync(ITag tag) { - var response = await _mpdService.SafelySendCommandAsync(new SearchCommand(tag, QueryText)); + var filter = new FilterTag(tag, QueryText, FilterOperator.Contains); + var response = await _mpdService.SafelySendCommandAsync(new SearchCommand(filter)); if (response != null) { diff --git a/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs b/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs index bea744f4..d1ed921c 100644 --- a/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs +++ b/Sources/Stylophone.Common/ViewModels/SettingsViewModel.cs @@ -3,9 +3,8 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Input; - -using Microsoft.Toolkit.Mvvm.ComponentModel; -using Microsoft.Toolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; using MpcNET.Commands.Output; using MpcNET.Commands.Status; using Stylophone.Common.Interfaces; @@ -15,7 +14,7 @@ namespace Stylophone.Common.ViewModels { - public class SettingsViewModel : ViewModelBase + public partial class SettingsViewModel : ViewModelBase { private IApplicationStorageService _applicationStorageService; private INotificationService _notificationService; @@ -36,179 +35,99 @@ public SettingsViewModel(MPDConnectionService mpdService, IApplicationStorageSer private bool _hasInstanceBeenInitialized = false; private int _previousUpdatingDb = 0; + [ObservableProperty] private Theme _elementTheme; - public Theme ElementTheme - { - get { return _elementTheme; } - set - { - if (value != _elementTheme) - { - _applicationStorageService.SetValue(nameof(ElementTheme), value.ToString()); - } - Set(ref _elementTheme, value); - } - } + [ObservableProperty] private string _versionDescription; - public string VersionDescription - { - get { return _versionDescription; } - set { Set(ref _versionDescription, value); } - } + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ServerStatus))] private string _serverInfo; - public string ServerInfo - { - get { return _serverInfo; } - set { Set(ref _serverInfo, value); } - } + [ObservableProperty] private string _serverHost; - public string ServerHost - { - get { return _serverHost; } - set - { - if (value != _serverHost) - { - _applicationStorageService.SetValue(nameof(ServerHost), value ?? "localhost"); - TriggerServerConnection(value, ServerPort, ServerPassword); - } - Set(ref _serverHost, value); - } - } + [ObservableProperty] private int _serverPort; - public int ServerPort - { - get { return _serverPort; } - set - { - if (value != _serverPort) - { - _applicationStorageService.SetValue(nameof(ServerPort), value); - TriggerServerConnection(ServerHost, value, ServerPassword); - } - Set(ref _serverPort, value); - } - } - private string _serverPass; - public string ServerPassword - { - get { return _serverPass; } - set - { - if (value != _serverPass) - { - _applicationStorageService.SetValue(nameof(ServerPassword), value ?? ""); - TriggerServerConnection(ServerHost, ServerPort, value); - } - Set(ref _serverPass, value); - } - } + [ObservableProperty] + private string _serverPassword; + + [ObservableProperty] + private bool _isCompactSizing; + + [ObservableProperty] + private bool _isAlbumArtFetchingEnabled; + + [ObservableProperty] + private bool _enableAnalytics; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(IsServerValid))] + private bool _isCheckingServer; - private bool _compactEnabled; - public bool IsCompactSizing + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ServerStatus))] + private bool _isStreamingAvailable; + + [ObservableProperty] + private bool _isLocalPlaybackEnabled; + + partial void OnElementThemeChanged(Theme value) { - get { return _compactEnabled; } - set + Task.Run (async () => await _interop.SetThemeAsync(value)); + + if (value != _elementTheme) { - if (value != _compactEnabled) - { - _applicationStorageService.SetValue(nameof(IsCompactSizing), value); - } - Set(ref _compactEnabled, value); + _applicationStorageService.SetValue(nameof(ElementTheme), value.ToString()); } } - private bool _albumArtEnabled; - public bool IsAlbumArtFetchingEnabled + partial void OnServerHostChanged(string value) { - get { return _albumArtEnabled; } - set - { - if (value != _albumArtEnabled) - { - _applicationStorageService.SetValue(nameof(IsAlbumArtFetchingEnabled), value); - } - Set(ref _albumArtEnabled, value); - } + _applicationStorageService.SetValue(nameof(ServerHost), value ?? "localhost"); + TriggerServerConnection(value, ServerPort, ServerPassword); } - private bool _enableAnalytics; - public bool EnableAnalytics + partial void OnServerPortChanged(int value) { - get { return _enableAnalytics; } - set - { - if (value != _enableAnalytics) - { - _applicationStorageService.SetValue(nameof(EnableAnalytics), value); - } - Set(ref _enableAnalytics, value); - } + _applicationStorageService.SetValue(nameof(ServerPort), value); + TriggerServerConnection(ServerHost, value, ServerPassword); } - private bool _isCheckingServer; - public bool IsCheckingServer + partial void OnServerPasswordChanged(string value) { - get { return _isCheckingServer; } - set => Set(ref _isCheckingServer, value); + _applicationStorageService.SetValue(nameof(ServerPassword), value); + TriggerServerConnection(ServerHost, ServerPort, value); } - public bool IsServerValid => _mpdService.IsConnected; - - public string ServerStatus => IsServerValid ? ServerInfo?.Split('\n')?.First() + (IsStreamingAvailable ? ", "+ Resources.SettingsLocalPlaybackAvailable : "") : Resources.SettingsNoServerError; - - private bool _httpdAvailable; - public bool IsStreamingAvailable + partial void OnIsCompactSizingChanged(bool value) { - get { return _httpdAvailable; } - set => Set(ref _httpdAvailable, value); + _applicationStorageService.SetValue(nameof(IsCompactSizing), value); } - private bool _localPlaybackEnabled; - public bool IsLocalPlaybackEnabled + partial void OnIsAlbumArtFetchingEnabledChanged(bool value) { - get { return _localPlaybackEnabled; } - set - { - if (value != _localPlaybackEnabled) - { - _applicationStorageService.SetValue(nameof(IsLocalPlaybackEnabled), value); - } - Set(ref _localPlaybackEnabled, value); - } + _applicationStorageService.SetValue(nameof(IsAlbumArtFetchingEnabled), value); } - private ICommand _switchThemeCommand; - public ICommand SwitchThemeCommand => _switchThemeCommand ?? (_switchThemeCommand = new AsyncRelayCommand(SwitchThemeAsync)); - - private async Task SwitchThemeAsync(Theme param) + partial void OnEnableAnalyticsChanged(bool value) { - if (_hasInstanceBeenInitialized) - { - ElementTheme = param; - await _interop.SetThemeAsync(param); - } + _applicationStorageService.SetValue(nameof(EnableAnalytics), value); } - private ICommand _switchSizingCommand; - public ICommand SwitchSizingCommand => _switchSizingCommand ?? (_switchSizingCommand = new RelayCommand(SwitchSizing)); + public bool IsServerValid => _mpdService.IsConnected; + public string ServerStatus => IsServerValid ? ServerInfo?.Split('\n')?.First() + (IsStreamingAvailable ? ", "+ Resources.SettingsLocalPlaybackAvailable : "") : + Resources.SettingsNoServerError; - private void SwitchSizing(string param) + partial void OnIsLocalPlaybackEnabledChanged(bool value) { - if (_hasInstanceBeenInitialized) - { - IsCompactSizing = bool.Parse(param); - } + _applicationStorageService.SetValue(nameof(IsLocalPlaybackEnabled), value); } - private ICommand _clearCacheCommand; - public ICommand ClearCacheCommand => _clearCacheCommand ?? (_clearCacheCommand = new AsyncRelayCommand(ClearCacheAsync)); + [RelayCommand] private async Task ClearCacheAsync() { try @@ -222,14 +141,12 @@ private async Task ClearCacheAsync() } } - private ICommand _rescanDbCommand; - public ICommand RescanDbCommand => _rescanDbCommand ?? (_rescanDbCommand = new AsyncRelayCommand(RescanDbAsync)); - + [RelayCommand] private async Task RescanDbAsync() { if (_mpdService.CurrentStatus.UpdatingDb > 0) { - _notificationService.ShowInAppNotification(Resources.NotificationDbAlreadyUpdating); + _notificationService.ShowInAppNotification(Resources.NotificationDbAlreadyUpdating, "", NotificationType.Warning); return; } @@ -239,8 +156,12 @@ private async Task RescanDbAsync() _notificationService.ShowInAppNotification(Resources.NotificationDbUpdateStarted); } - private ICommand _rateAppCommand; - public ICommand RateAppCommand => _rateAppCommand ?? (_rateAppCommand = new RelayCommand(() => _interop.OpenStoreReviewUrlAsync())); + + [RelayCommand] + private async Task RateAppAsync() + { + await _interop.OpenStoreReviewUrlAsync(); + } public async Task EnsureInstanceInitializedAsync() { @@ -250,14 +171,14 @@ public async Task EnsureInstanceInitializedAsync() _mpdService.StatusChanged += async (s, e) => await CheckUpdatingDbAsync(); // Initialize values directly to avoid calling CheckServerAddressAsync twice - _compactEnabled = _applicationStorageService.GetValue(nameof(IsCompactSizing)); + _isCompactSizing = _applicationStorageService.GetValue(nameof(IsCompactSizing)); _serverHost = _applicationStorageService.GetValue(nameof(ServerHost)); _serverHost = _serverHost?.Replace("\"", ""); // TODO: This is a quickfix for 1.x updates _serverPort = _applicationStorageService.GetValue(nameof(ServerPort), 6600); _enableAnalytics = _applicationStorageService.GetValue(nameof(EnableAnalytics), true); - _albumArtEnabled = _applicationStorageService.GetValue(nameof(IsAlbumArtFetchingEnabled), true); - _localPlaybackEnabled = _applicationStorageService.GetValue(nameof(IsLocalPlaybackEnabled)); + _isAlbumArtFetchingEnabled = _applicationStorageService.GetValue(nameof(IsAlbumArtFetchingEnabled), true); + _isLocalPlaybackEnabled = _applicationStorageService.GetValue(nameof(IsLocalPlaybackEnabled)); Enum.TryParse(_applicationStorageService.GetValue(nameof(ElementTheme)), out _elementTheme); @@ -284,7 +205,7 @@ private string GetVersionDescription() var appName = Resources.AppDisplayName; Version version = _interop.GetAppVersion(); - return $"{appName} - {version.Major}.{version.Minor}.{(version.Build > -1 ? version.Build : 0)}.{(version.Revision > -1 ? version.Revision : 0)}"; + return $"{version.Major}.{version.Minor}.{(version.Build > -1 ? version.Build : 0)}"; } private void TriggerServerConnection(string host, int port, string pass) @@ -298,9 +219,6 @@ private void TriggerServerConnection(string host, int port, string pass) private async Task UpdateServerVersionAsync() { IsCheckingServer = _mpdService.IsConnecting; - - await _dispatcherService.ExecuteOnUIThreadAsync(() => { OnPropertyChanged(nameof(IsServerValid)); OnPropertyChanged(nameof(ServerStatus)); }); - if (!_mpdService.IsConnected) return; var response = await _mpdService.SafelySendCommandAsync(new StatsCommand()); @@ -341,8 +259,6 @@ private async Task UpdateServerVersionAsync() $"{songs} Songs, {albums} Albums\n" + $"Database last updated {lastUpdatedDb}"; } - - await _dispatcherService.ExecuteOnUIThreadAsync(() => { OnPropertyChanged(nameof(IsServerValid)); OnPropertyChanged(nameof(ServerStatus)); }); } } } diff --git a/Sources/Stylophone.Localization/Strings/Resources.Designer.cs b/Sources/Stylophone.Localization/Strings/Resources.Designer.cs index e38a596d..589217cc 100644 --- a/Sources/Stylophone.Localization/Strings/Resources.Designer.cs +++ b/Sources/Stylophone.Localization/Strings/Resources.Designer.cs @@ -448,7 +448,7 @@ public static string ErrorPassword { } /// - /// Recherche une chaîne localisée semblable à Error trying to play the MPD server's httpd stream: {0}. + /// Recherche une chaîne localisée semblable à Error trying to play the MPD server's httpd stream. /// public static string ErrorPlayingMPDStream { get { @@ -466,47 +466,20 @@ public static string ErrorPlayingTrack { } /// - /// Recherche une chaîne localisée semblable à parameter must be an Enum name!. + /// Recherche une chaîne localisée semblable à Sending {0} failed. /// - public static string ExceptionEnumToBooleanConverterParameterMustBeAnEnumName { + public static string ErrorSendingMPDCommand { get { - return ResourceManager.GetString("ExceptionEnumToBooleanConverterParameterMustBeAnEnumName", resourceCulture); + return ResourceManager.GetString("ErrorSendingMPDCommand", resourceCulture); } } /// - /// Recherche une chaîne localisée semblable à value must be an Enum!. + /// Recherche une chaîne localisée semblable à Updating Playlists failed. /// - public static string ExceptionEnumToBooleanConverterValueMustBeAnEnum { + public static string ErrorUpdatingPlaylist { get { - return ResourceManager.GetString("ExceptionEnumToBooleanConverterValueMustBeAnEnum", resourceCulture); - } - } - - /// - /// Recherche une chaîne localisée semblable à File name is null or empty. Specify a valid file name. - /// - public static string ExceptionSettingsStorageExtensionsFileNameIsNullOrEmpty { - get { - return ResourceManager.GetString("ExceptionSettingsStorageExtensionsFileNameIsNullOrEmpty", resourceCulture); - } - } - - /// - /// Recherche une chaîne localisée semblable à All pages opened in a new window must subscribe to the Released Event.. - /// - public static string ExceptionViewLifeTimeControlMissingReleasedSubscription { - get { - return ResourceManager.GetString("ExceptionViewLifeTimeControlMissingReleasedSubscription", resourceCulture); - } - } - - /// - /// Recherche une chaîne localisée semblable à This view is being disposed.. - /// - public static string ExceptionViewLifeTimeControlViewDisposal { - get { - return ResourceManager.GetString("ExceptionViewLifeTimeControlViewDisposal", resourceCulture); + return ResourceManager.GetString("ErrorUpdatingPlaylist", resourceCulture); } } @@ -858,7 +831,7 @@ public static string SettingsAbout { } /// - /// Recherche une chaîne localisée semblable à MPD Client for the Universal Windows Platform, based on MpcNET.. + /// Recherche une chaîne localisée semblable à A pretty cool MPD Client. Uses MpcNET.. /// public static string SettingsAboutText { get { diff --git a/Sources/Stylophone.Localization/Strings/Resources.en-US.resx b/Sources/Stylophone.Localization/Strings/Resources.en-US.resx index a11ca6b3..b3419349 100644 --- a/Sources/Stylophone.Localization/Strings/Resources.en-US.resx +++ b/Sources/Stylophone.Localization/Strings/Resources.en-US.resx @@ -183,26 +183,6 @@ OK - - File name is null or empty. Specify a valid file name - File name is null or empty to save file in settings storage extensions - - - value must be an Enum! - Value must be an Enum in enum to boolean converter - - - parameter must be an Enum name! - Parameter must be an Enum name in enum to boolean converter - - - This view is being disposed. - View disposed - - - All pages opened in a new window must subscribe to the Released Event. - The page is not subscribed to the Released event. - Added to Playlist {0}! @@ -234,7 +214,7 @@ About this application - MPD Client for the Universal Windows Platform, based on MpcNET. + A pretty cool MPD Client. Uses MpcNET. Analytics @@ -457,7 +437,7 @@ Hope you enjoy using the application! Enabling this option will show a second volume slider to control local volume. - Error trying to play the MPD server's httpd stream: {0} + Error trying to play the MPD server's httpd stream Local Volume @@ -496,4 +476,10 @@ Enabling this option will show a second volume slider to control local volume. Server Database is Updating + + Sending {0} failed + + + Updating Playlists failed + \ 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 65b51ab6..eb9d656e 100644 --- a/Sources/Stylophone.Localization/Strings/Resources.fr-FR.resx +++ b/Sources/Stylophone.Localization/Strings/Resources.fr-FR.resx @@ -129,26 +129,6 @@ Bibliothèque Navigation view item name for Library - - File name is null or empty. Specify a valid file name - File name is null or empty to save file in settings storage extensions - - - value must be an Enum! - Value must be an Enum in enum to boolean converter - - - parameter must be an Enum name! - Parameter must be an Enum name in enum to boolean converter - - - This view is being disposed. - View disposed - - - All pages opened in a new window must subscribe to the Released Event. - The page is not subscribed to the Released event. - Ajouté à la Playlist {0} ! @@ -233,7 +213,7 @@ A propos de cette application - Client MPD pour UWP, utilisant MpcNET. + Un client MPD plutôt cool. Utilise MpcNET. Télémétrie @@ -456,7 +436,7 @@ J'espère que vous apprécierez l'application! L'activation de cette option affichera un second slider pour contrôler le volume local. - Erreur lors de la lecture du stream httpd du serveur MPD: {0} + Erreur lors de la lecture du stream httpd du serveur MPD Volume Local @@ -495,4 +475,10 @@ L'activation de cette option affichera un second slider pour contrôler le volum Base de données en cours de MàJ + + L'envoi de {0} a échoué + + + La mise à jour des playlists a échoué + \ 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 817206e1..92c13b3b 100644 --- a/Sources/Stylophone.Localization/Strings/Resources.pt-PT.resx +++ b/Sources/Stylophone.Localization/Strings/Resources.pt-PT.resx @@ -182,26 +182,6 @@ OK - - File name is null or empty. Specify a valid file name - File name is null or empty to save file in settings storage extensions - - - value must be an Enum! - Value must be an Enum in enum to boolean converter - - - parameter must be an Enum name! - Parameter must be an Enum name in enum to boolean converter - - - This view is being disposed. - View disposed - - - All pages opened in a new window must subscribe to the Released Event. - The page is not subscribed to the Released event. - Adicionado na Lista de reprodução {0}! @@ -233,7 +213,7 @@ Acerca desta aplicação - Cliente MPD para a Plataforma Universal Windows, baseado em MpcNET. + Cliente MPD, baseado em MpcNET. Analíticos @@ -456,7 +436,7 @@ Espero que goste de utilizar a aplicação! Ativando esta opção mostrará um segundo deslizador de volume para controlar o volume local. - Erro ao tentar reproduzir o fluxo httpd do servidor MPD: {0} + Erro ao tentar reproduzir o fluxo httpd do servidor MPD Volume Local @@ -495,4 +475,7 @@ Ativando esta opção mostrará um segundo deslizador de volume para controlar o Base de dados do servidor actualizando + + Não foi possível enviar {0} + \ No newline at end of file diff --git a/Sources/Stylophone.Localization/Strings/Resources.resx b/Sources/Stylophone.Localization/Strings/Resources.resx index cd8d6f23..a5fe00b4 100644 --- a/Sources/Stylophone.Localization/Strings/Resources.resx +++ b/Sources/Stylophone.Localization/Strings/Resources.resx @@ -183,26 +183,6 @@ OK - - File name is null or empty. Specify a valid file name - File name is null or empty to save file in settings storage extensions - - - value must be an Enum! - Value must be an Enum in enum to boolean converter - - - parameter must be an Enum name! - Parameter must be an Enum name in enum to boolean converter - - - This view is being disposed. - View disposed - - - All pages opened in a new window must subscribe to the Released Event. - The page is not subscribed to the Released event. - Added to Playlist {0}! @@ -234,7 +214,7 @@ About this application - MPD Client for the Universal Windows Platform, based on MpcNET. + A pretty cool MPD Client. Uses MpcNET. Analytics @@ -457,7 +437,7 @@ Hope you enjoy using the application! Enabling this option will show a second volume slider to control local volume. - Error trying to play the MPD server's httpd stream: {0} + Error trying to play the MPD server's httpd stream Local Volume @@ -496,4 +476,10 @@ Enabling this option will show a second volume slider to control local volume. Server Database is Updating + + Sending {0} failed + + + Updating Playlists failed + \ No newline at end of file diff --git a/Sources/Stylophone.Localization/Strings/Resources.zh-CN.resx b/Sources/Stylophone.Localization/Strings/Resources.zh-CN.resx new file mode 100644 index 00000000..462f4b6a --- /dev/null +++ b/Sources/Stylophone.Localization/Strings/Resources.zh-CN.resx @@ -0,0 +1,478 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Stylophone + Application display name + + + 播放队列 + Navigation view item name for NowPlaying + + + 音乐库 + Navigation view item name for Library + + + 文件夹 + Navigation view item name for Folders + + + 播放列表 + Navigation view item name for Playlists + + + 设置 + Navigation view item name for Settings + + + 该曲目没有与之匹配的专辑。 + + + 已删除播放列表。 + + + 正在播放:{0} + + + 未加载曲目。 + + + 没有播放中的曲目。 + + + 删除播放列表? + + + 无法清除队列。 + + + 开始更新数据库。 + + + 已在更新数据库。 + + + 取消 + + + 无法在该地址下找到 MPD 服务器。 + + + 在服务器上搜索 “{0}”…… + + + “{0}”的搜索结果 + + + + + + 已添加到播放列表“{0}”。 + + + 已添加到队列。 + + + 已清除专辑图片缓存。 + + + 无法添加专辑:{0} + + + 无法播放内容:{0} + + + 错误:{0} + + + 无效 MPD 响应。 + + + 该操作可能需要服务器花一些时间才能完成。 + + + 更新 MPD 服务器数据库 + + + 关于此应用 + + + 基于 MpcNET 的 MPD 客户端。 + + + 分析 + + + 允许 Stylophone 发送崩溃与分析报告 + + + 该设置将在重启应用后生效。 + + + 清除缓存 + + + 清除本地专辑图片缓存 + + + 个性化 + + + 数据库与专辑图片 + + + 源代码、许可证及隐私声明 + + + https://github.com/Difegue/Stylophone + + + MPD 服务器 + + + 主机名 + + + 端口 + + + 主题 + + + 深色 + + + 系统默认 + + + 浅色 + + + 界面密度 + + + 紧凑 + + + 标准 + + + 更新 + + + 搜索曲目…… + + + 专辑 + + + 艺术家 + + + 曲目 + + + 未找到结果。 + + + 要开始,请打开设置页面,输入 MPD 服务器的 URL 和端口。 +请注意,Stylophone 会发送使用数据来帮助诊断错误。 +如果您不同意,可以在设置中禁用遥测。 +祝您使用愉快! + First use prompt message body + + + 欢迎使用 Stylophone! + First use prompt message title + + + 播放 + + + 查看专辑 + + + 从播放列表移除 + + + 从播放队列移除 + + + 添加到播放列表 + + + 将播放队列存为播放列表 + + + 清除播放队列 + + + 添加到播放队列 + + + 删除播放列表 + + + 是否已连接到 MPD 服务器? + + + 未在服务器上找到文件夹。 + + + 这里空空如也。 + + + 添加曲目进来吧! + + + 巧妇难为无米之炊。 + + + 何不添加一些音乐? + + + 一切都静悄悄。 + + + 酷毙了! + + + 然而未在服务器上找到任何曲目。 + + + 搜索…… + + + MPD 文件系统 + + + 正在加载…… + + + 折叠所有已打开文件夹 + + + 接下来播放: + + + 随机播放 + + + 播放/暂停 + + + 上一曲目 + + + 循环播放 + + + 下一曲目 + + + 改变音量 + + + 显示迷你播放器 + + + 更多操作 + + + 添加到播放列表 + + + 将曲目添加到以下播放列表: + + + 选择播放列表 + + + 添加 + + + 创建新播放列表 + + + 播放列表名称 + + + 密码 + + + 密码无效 + + + 本地回放可用 + + + + + + + + + 本地回放 + + + Stylophone 可以播放来自 MPD 服务器的音频流。 +启用此选项后,将显示另一个音量滑块,用来控制本地音量。 + + + 试图播放 MPD 服务器的 httpd 流时出现错误 + + + 本地音量 + + + 给应用评分 + + + 感谢使用 Stylophone!您愿意在商店打个分吗? +(我们只问这一次🙏) + + + + + + + + + 随机添加曲目 + + + 正在音乐库中随机添加曲目…… + + + 大卫·鲍伊在其 1969 年首支热门单曲《Space Oddity》,以及 2002 年专辑《Heathen》里的两支单曲《Slip Away》和《Heathen (The Rays)》中均演奏了 Stylophone,广受赞誉。 + + + 从 MPD 服务器下载专辑图片 + + + 为避免 MPD 服务器过载,Stylophone 会将专辑图片本地保存。 + + + 自动从播放队列中移除已播放曲目 + + + 正在更新服务器数据库 + + \ No newline at end of file diff --git a/Sources/Stylophone.Localization/Stylophone.Localization.csproj b/Sources/Stylophone.Localization/Stylophone.Localization.csproj index 5e1dc5d9..3d2cc419 100644 --- a/Sources/Stylophone.Localization/Stylophone.Localization.csproj +++ b/Sources/Stylophone.Localization/Stylophone.Localization.csproj @@ -22,7 +22,10 @@ Designer - + + Designer + + PublicResXFileCodeGenerator Resources.Designer.cs diff --git a/Sources/Stylophone.iOS/AppDelegate.cs b/Sources/Stylophone.iOS/AppDelegate.cs index ac0da911..407af79d 100644 --- a/Sources/Stylophone.iOS/AppDelegate.cs +++ b/Sources/Stylophone.iOS/AppDelegate.cs @@ -2,7 +2,7 @@ using UIKit; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using System; using Stylophone.Common.Interfaces; using Stylophone.Common.Services; @@ -14,6 +14,7 @@ using Microsoft.AppCenter.Analytics; using Microsoft.AppCenter.Crashes; using System.Threading; +using AVFoundation; namespace Stylophone.iOS { @@ -29,6 +30,14 @@ public class AppDelegate : UIResponder, IUIApplicationDelegate public UISplitViewController RootViewController { get; set; } + public UIColor AppColor => UIColor.FromDynamicProvider((traitCollection) => + { + var darkColor = UIColor.FromRGB(204, 172, 128); + var lightColor = UIColor.FromRGB(135, 114, 85); + + return traitCollection.UserInterfaceStyle == UIUserInterfaceStyle.Dark ? darkColor : lightColor; + }); + public AppDelegate() { Services = ConfigureServices(); @@ -44,9 +53,16 @@ public void ShowDetailView() [Export("application:didFinishLaunchingWithOptions:")] public bool FinishedLaunching(UIApplication application, NSDictionary launchOptions) { + // Enable Now Playing integration + application.BeginReceivingRemoteControlEvents(); + AVAudioSession.SharedInstance().SetCategory(AVAudioSessionCategory.Playback); + // Override point for customization after application launch Task.Run(async () => await InitializeApplicationAsync()); + RootViewController = Window.RootViewController as UISplitViewController; + Window.TintColor = AppColor; + return true; } @@ -70,6 +86,7 @@ private async Task InitializeApplicationAsync() storageService.SetValue("LaunchCount", launchCount + 1); Ioc.Default.GetRequiredService().Initialize(); + Ioc.Default.GetRequiredService().Initialize(); await Ioc.Default.GetRequiredService().ExecuteOnUIThreadAsync(async () => { @@ -99,24 +116,6 @@ await Ioc.Default.GetRequiredService().ExecuteOnUIThreadAsyn #endif } - // UISceneSession Lifecycle - - [Export("application:configurationForConnectingSceneSession:options:")] - public UISceneConfiguration GetConfiguration(UIApplication application, UISceneSession connectingSceneSession, UISceneConnectionOptions options) - { - // Called when a new scene session is being created. - // Use this method to select a configuration to create the new scene with. - return UISceneConfiguration.Create("Default Configuration", connectingSceneSession.Role); - } - - [Export("application:didDiscardSceneSessions:")] - public void DidDiscardSceneSessions(UIApplication application, NSSet sceneSessions) - { - // Called when the user discards a scene session. - // If any sessions were discarded while the application was not running, this will be called shortly after `FinishedLaunching`. - // Use this method to release any resources that were specific to the discarded scenes, as they will not return. - } - /// /// Configures the services for the application. /// @@ -132,7 +131,7 @@ private static IServiceProvider ConfigureServices() services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - //services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); // Viewmodel Factories diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AlbumPlaceholder.imageset/AlbumPlaceholderApple.png b/Sources/Stylophone.iOS/Assets.xcassets/AlbumPlaceholder.imageset/AlbumPlaceholderApple.png index d0f88509..b24ac388 100644 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AlbumPlaceholder.imageset/AlbumPlaceholderApple.png and b/Sources/Stylophone.iOS/Assets.xcassets/AlbumPlaceholder.imageset/AlbumPlaceholderApple.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/appstore1024.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/appstore1024.png deleted file mode 100644 index 87c8ff51..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/appstore1024.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipad152.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipad152.png deleted file mode 100644 index f971f59f..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipad152.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipad76.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipad76.png deleted file mode 100644 index d3d7e3cd..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipad76.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadNotification20.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadNotification20.png deleted file mode 100644 index 0a8a4337..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadNotification20.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadNotification40.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadNotification40.png deleted file mode 100644 index ac07f19c..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadNotification40.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadPro167.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadPro167.png deleted file mode 100644 index 837150a7..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadPro167.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSettings29.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSettings29.png deleted file mode 100644 index f134480c..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSettings29.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSettings58.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSettings58.png deleted file mode 100644 index 9acbf60a..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSettings58.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSpotlight40.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSpotlight40.png deleted file mode 100644 index ac07f19c..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSpotlight40.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSpotlight80.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSpotlight80.png deleted file mode 100644 index 69921711..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/ipadSpotlight80.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/iphone120.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/iphone120.png deleted file mode 100644 index 9bf1acd3..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/iphone120.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/iphone180.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/iphone180.png deleted file mode 100644 index 0c264d28..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/iphone180.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac1024.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac1024.png deleted file mode 100644 index 3bb5cee8..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac1024.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac128.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac128.png deleted file mode 100644 index 0bac707f..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac128.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac16.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac16.png deleted file mode 100644 index b361056c..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac16.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac256.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac256.png deleted file mode 100644 index f4dee53d..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac256.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac32.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac32.png deleted file mode 100644 index 2d44269f..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac32.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac512.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac512.png deleted file mode 100644 index 9077cb5c..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac512.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac64.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac64.png deleted file mode 100644 index 891fb523..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/mac64.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/notification40.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/notification40.png deleted file mode 100644 index ac07f19c..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/notification40.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/notification60.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/notification60.png deleted file mode 100644 index d493f2bc..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/notification60.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/settings58.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/settings58.png deleted file mode 100644 index 9acbf60a..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/settings58.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/settings87.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/settings87.png deleted file mode 100644 index 04f36ce6..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/settings87.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/spotlight120.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/spotlight120.png deleted file mode 100644 index 9bf1acd3..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/spotlight120.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/spotlight80.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/spotlight80.png deleted file mode 100644 index 69921711..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/spotlight80.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/100.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/100.png new file mode 100644 index 00000000..d8d0d8ca Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/100.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/1024.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/1024.png new file mode 100644 index 00000000..a802d8d5 Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/1024.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/114.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 00000000..e1d87c7f Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/114.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/120.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/120.png new file mode 100644 index 00000000..e9939523 Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/120.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/144.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/144.png new file mode 100644 index 00000000..1485c8fa Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/144.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/152.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/152.png new file mode 100644 index 00000000..5feb8752 Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/152.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/167.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/167.png new file mode 100644 index 00000000..f7b71506 Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/167.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/180.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/180.png new file mode 100644 index 00000000..d757907b Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/180.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/20.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/20.png new file mode 100644 index 00000000..ea6fddad Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/20.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/29.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/29.png new file mode 100644 index 00000000..2c656fbc Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/29.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/40.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/40.png new file mode 100644 index 00000000..9e7bc0eb Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/40.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/50.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/50.png new file mode 100644 index 00000000..3942512a Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/50.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/57.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/57.png new file mode 100644 index 00000000..7a09648f Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/57.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/58.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/58.png new file mode 100644 index 00000000..330e4e34 Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/58.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/60.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/60.png new file mode 100644 index 00000000..652748a0 Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/60.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/72.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/72.png new file mode 100644 index 00000000..e961c5bc Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/72.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/76.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 00000000..678aba10 Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/76.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/80.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/80.png new file mode 100644 index 00000000..39cb23e3 Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/80.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/87.png b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/87.png new file mode 100644 index 00000000..02cfba6d Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/87.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/Contents.json b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 53% rename from Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/Contents.json rename to Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json index dfcf4661..4fdf8826 100644 --- a/Sources/Stylophone.iOS/Assets.xcassets/AppIcon-1.appiconset/Contents.json +++ b/Sources/Stylophone.iOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,172 +1,154 @@ { "images" : [ { - "filename" : "notification40.png", + "filename" : "40.png", "idiom" : "iphone", "scale" : "2x", "size" : "20x20" }, { - "filename" : "notification60.png", + "filename" : "60.png", "idiom" : "iphone", "scale" : "3x", "size" : "20x20" }, { - "filename" : "settings58.png", + "filename" : "29.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "58.png", "idiom" : "iphone", "scale" : "2x", "size" : "29x29" }, { - "filename" : "settings87.png", + "filename" : "87.png", "idiom" : "iphone", "scale" : "3x", "size" : "29x29" }, { - "filename" : "spotlight80.png", + "filename" : "80.png", "idiom" : "iphone", "scale" : "2x", "size" : "40x40" }, { - "filename" : "spotlight120.png", + "filename" : "120.png", "idiom" : "iphone", "scale" : "3x", "size" : "40x40" }, { - "filename" : "iphone120.png", + "filename" : "57.png", + "idiom" : "iphone", + "scale" : "1x", + "size" : "57x57" + }, + { + "filename" : "114.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "57x57" + }, + { + "filename" : "120.png", "idiom" : "iphone", "scale" : "2x", "size" : "60x60" }, { - "filename" : "iphone180.png", + "filename" : "180.png", "idiom" : "iphone", "scale" : "3x", "size" : "60x60" }, { - "filename" : "ipadNotification20.png", + "filename" : "20.png", "idiom" : "ipad", "scale" : "1x", "size" : "20x20" }, { - "filename" : "ipadNotification40.png", + "filename" : "40.png", "idiom" : "ipad", "scale" : "2x", "size" : "20x20" }, { - "filename" : "ipadSettings29.png", + "filename" : "29.png", "idiom" : "ipad", "scale" : "1x", "size" : "29x29" }, { - "filename" : "ipadSettings58.png", + "filename" : "58.png", "idiom" : "ipad", "scale" : "2x", "size" : "29x29" }, { - "filename" : "ipadSpotlight40.png", + "filename" : "40.png", "idiom" : "ipad", "scale" : "1x", "size" : "40x40" }, { - "filename" : "ipadSpotlight80.png", + "filename" : "80.png", "idiom" : "ipad", "scale" : "2x", "size" : "40x40" }, { - "filename" : "ipad76.png", + "filename" : "50.png", "idiom" : "ipad", "scale" : "1x", - "size" : "76x76" + "size" : "50x50" }, { - "filename" : "ipad152.png", + "filename" : "100.png", "idiom" : "ipad", "scale" : "2x", - "size" : "76x76" + "size" : "50x50" }, { - "filename" : "ipadPro167.png", + "filename" : "72.png", "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "filename" : "appstore1024.png", - "idiom" : "ios-marketing", "scale" : "1x", - "size" : "1024x1024" + "size" : "72x72" }, { - "filename" : "mac16.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "mac32.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "mac32.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "mac64.png", - "idiom" : "mac", + "filename" : "144.png", + "idiom" : "ipad", "scale" : "2x", - "size" : "32x32" + "size" : "72x72" }, { - "filename" : "mac128.png", - "idiom" : "mac", + "filename" : "76.png", + "idiom" : "ipad", "scale" : "1x", - "size" : "128x128" + "size" : "76x76" }, { - "filename" : "mac256.png", - "idiom" : "mac", + "filename" : "152.png", + "idiom" : "ipad", "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "mac256.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" + "size" : "76x76" }, { - "filename" : "mac512.png", - "idiom" : "mac", + "filename" : "167.png", + "idiom" : "ipad", "scale" : "2x", - "size" : "256x256" + "size" : "83.5x83.5" }, { - "filename" : "mac512.png", - "idiom" : "mac", + "filename" : "1024.png", + "idiom" : "ios-marketing", "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "mac1024.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" + "size" : "1024x1024" } ], "info" : { diff --git a/Sources/Stylophone.iOS/Assets.xcassets/icon.imageset/Contents.json b/Sources/Stylophone.iOS/Assets.xcassets/icon.imageset/Contents.json index 2945b36b..b849022f 100644 --- a/Sources/Stylophone.iOS/Assets.xcassets/icon.imageset/Contents.json +++ b/Sources/Stylophone.iOS/Assets.xcassets/icon.imageset/Contents.json @@ -1,17 +1,8 @@ { "images" : [ { - "filename" : "icon.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" + "filename" : "Square44x44Logo.altform-unplated_targetsize-256.png", + "idiom" : "universal" } ], "info" : { diff --git a/Sources/Stylophone.iOS/Assets.xcassets/icon.imageset/Square44x44Logo.altform-unplated_targetsize-256.png b/Sources/Stylophone.iOS/Assets.xcassets/icon.imageset/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 00000000..65c7c1fa Binary files /dev/null and b/Sources/Stylophone.iOS/Assets.xcassets/icon.imageset/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/Sources/Stylophone.iOS/Assets.xcassets/icon.imageset/icon.png b/Sources/Stylophone.iOS/Assets.xcassets/icon.imageset/icon.png deleted file mode 100644 index 4b5d9ebd..00000000 Binary files a/Sources/Stylophone.iOS/Assets.xcassets/icon.imageset/icon.png and /dev/null differ diff --git a/Sources/Stylophone.iOS/Helpers/Binding.cs b/Sources/Stylophone.iOS/Helpers/Binding.cs index 445396e2..ed7c665a 100644 --- a/Sources/Stylophone.iOS/Helpers/Binding.cs +++ b/Sources/Stylophone.iOS/Helpers/Binding.cs @@ -2,7 +2,7 @@ using System.Diagnostics; using System.Reflection; using Foundation; -using Microsoft.Toolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using UIKit; namespace Stylophone.iOS.Helpers diff --git a/Sources/Stylophone.iOS/Helpers/NSValueConverters.cs b/Sources/Stylophone.iOS/Helpers/NSValueConverters.cs index 480a151c..fd850591 100644 --- a/Sources/Stylophone.iOS/Helpers/NSValueConverters.cs +++ b/Sources/Stylophone.iOS/Helpers/NSValueConverters.cs @@ -68,7 +68,7 @@ public override NSObject TransformedValue(NSObject value) col = skiaColor.ToUIColor(); } else - col = UIColor.SystemBlueColor; + col = UIColor.SystemBlue; return col; } diff --git a/Sources/Stylophone.iOS/Helpers/PropertyBinder.cs b/Sources/Stylophone.iOS/Helpers/PropertyBinder.cs index f4193acc..2d11df33 100644 --- a/Sources/Stylophone.iOS/Helpers/PropertyBinder.cs +++ b/Sources/Stylophone.iOS/Helpers/PropertyBinder.cs @@ -5,7 +5,7 @@ using System.Reflection; using System.Windows.Input; using Foundation; -using Microsoft.Toolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using UIKit; namespace Stylophone.iOS.Helpers diff --git a/Sources/Stylophone.iOS/Helpers/SymbolUIButton.cs b/Sources/Stylophone.iOS/Helpers/SymbolUIButton.cs new file mode 100644 index 00000000..302667b2 --- /dev/null +++ b/Sources/Stylophone.iOS/Helpers/SymbolUIButton.cs @@ -0,0 +1,41 @@ +// This file has been autogenerated from a class added in the UI designer. + +using System; +using CoreGraphics; +using Foundation; +using UIKit; + +namespace Stylophone.iOS.Views +{ + public partial class SymbolUIButton : UIButton + { + public SymbolUIButton (IntPtr handle) : base (handle) + { + + } + + public override void AwakeFromNib() + { + // https://stackoverflow.com/questions/58338420/sf-symbol-looks-distorted + ImageView.ContentMode = UIViewContentMode.ScaleAspectFit; + + // https://www.roryba.in/programming/swift/2018/03/24/animating-uibutton.html + var identity = CGAffineTransform.MakeIdentity(); + var scaled = CGAffineTransform.MakeIdentity(); + scaled.Scale((nfloat)0.95, (nfloat)0.95, MatrixOrder.Prepend); + + AddTarget((s,e) => Animate(scaled), UIControlEvent.TouchDown | UIControlEvent.TouchDragEnter); + AddTarget((s, e) => Animate(identity), UIControlEvent.TouchDragExit | UIControlEvent.TouchCancel | UIControlEvent.TouchUpInside | UIControlEvent.TouchUpOutside); + + } + + private void Animate(CGAffineTransform transform) + { + Animate(0.4, 0, UIViewAnimationOptions.CurveEaseInOut, + () => { Transform = transform; }, + null); + } + + + } +} diff --git a/Sources/Stylophone.iOS/Helpers/SymbolUIButton.designer.cs b/Sources/Stylophone.iOS/Helpers/SymbolUIButton.designer.cs new file mode 100644 index 00000000..17feafe8 --- /dev/null +++ b/Sources/Stylophone.iOS/Helpers/SymbolUIButton.designer.cs @@ -0,0 +1,20 @@ +// 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 ("SymbolUIButton")] + partial class SymbolUIButton + { + + void ReleaseDesignerOutlets () + { + } + } +} diff --git a/Sources/Stylophone.iOS/Info.plist b/Sources/Stylophone.iOS/Info.plist index a5297755..805f0273 100644 --- a/Sources/Stylophone.iOS/Info.plist +++ b/Sources/Stylophone.iOS/Info.plist @@ -7,32 +7,26 @@ CFBundleIdentifier com.tvc-16.Stylophone CFBundleShortVersionString - 1.0 + 2.5 CFBundleVersion - 1.0 + 2.5 + CFBundleDevelopmentRegion + en + CFBundleLocalizations + + en + fr + pt + zh + LSRequiresIPhoneOS - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - UISceneConfigurations - - UIWindowSceneSessionRoleApplication - - - UISceneConfigurationName - Default Configuration - UISceneDelegateClassName - SceneDelegate - UISceneStoryboardFile - Main - - - - MinimumOSVersion 15.0 + UIBackgroundModes + + audio + UIDeviceFamily 1 @@ -62,6 +56,6 @@ UIInterfaceOrientationLandscapeRight XSAppIconAssets - Assets.xcassets/AppIcon-1.appiconset + Assets.xcassets/AppIcon.appiconset diff --git a/Sources/Stylophone.iOS/NavigationController.cs b/Sources/Stylophone.iOS/NavigationController.cs index 4cf94840..e449e799 100644 --- a/Sources/Stylophone.iOS/NavigationController.cs +++ b/Sources/Stylophone.iOS/NavigationController.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Linq; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using ObjCRuntime; using Stylophone.Common.Interfaces; using Stylophone.Common.ViewModels; diff --git a/Sources/Stylophone.iOS/Resources/LaunchScreen.xib b/Sources/Stylophone.iOS/Resources/LaunchScreen.xib index 2f763e8f..df292044 100644 --- a/Sources/Stylophone.iOS/Resources/LaunchScreen.xib +++ b/Sources/Stylophone.iOS/Resources/LaunchScreen.xib @@ -1,8 +1,8 @@ - + - + @@ -12,27 +12,18 @@ - - + + + + + + + - - - - - - + + @@ -40,8 +31,6 @@ - - - + diff --git a/Sources/Stylophone.iOS/Resources/silence.wav b/Sources/Stylophone.iOS/Resources/silence.wav new file mode 100644 index 00000000..7d2e290d Binary files /dev/null and b/Sources/Stylophone.iOS/Resources/silence.wav differ diff --git a/Sources/Stylophone.iOS/SceneDelegate.cs b/Sources/Stylophone.iOS/SceneDelegate.cs deleted file mode 100644 index 411dbd3b..00000000 --- a/Sources/Stylophone.iOS/SceneDelegate.cs +++ /dev/null @@ -1,72 +0,0 @@ -using System; -using Foundation; -using Stylophone.iOS; -using UIKit; - -namespace NewSingleViewTemplate -{ - [Register("SceneDelegate")] - public class SceneDelegate : UIResponder, IUIWindowSceneDelegate - { - - [Export("window")] - public UIWindow Window { get; set; } - - [Export("scene:willConnectToSession:options:")] - public void WillConnect(UIScene scene, UISceneSession session, UISceneConnectionOptions connectionOptions) - { - // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. - // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. - // This delegate does not imply the connecting scene or session are new (see UIApplicationDelegate `GetConfiguration` instead). - } - - [Export("sceneDidDisconnect:")] - public void DidDisconnect(UIScene scene) - { - // Called as the scene is being released by the system. - // This occurs shortly after the scene enters the background, or when its session is discarded. - // Release any resources associated with this scene that can be re-created the next time the scene connects. - // The scene may re-connect later, as its session was not neccessarily discarded (see UIApplicationDelegate `DidDiscardSceneSessions` instead). - } - - [Export("sceneDidBecomeActive:")] - public void DidBecomeActive(UIScene scene) - { - // Called when the scene has moved from an inactive state to an active state. - // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. - - (UIApplication.SharedApplication.Delegate as AppDelegate).RootViewController = - Window.RootViewController as UISplitViewController; - - Window.TintColor = UIColor.FromDynamicProvider((traitCollection) => - { - var darkColor = UIColor.FromRGB(204,172,128); - var lightColor = UIColor.FromRGB(135,114,85); - - return traitCollection.UserInterfaceStyle == UIUserInterfaceStyle.Dark ? darkColor : lightColor; - }); - } - - [Export("sceneWillResignActive:")] - public void WillResignActive(UIScene scene) - { - // Called when the scene will move from an active state to an inactive state. - // This may occur due to temporary interruptions (ex. an incoming phone call). - } - - [Export("sceneWillEnterForeground:")] - public void WillEnterForeground(UIScene scene) - { - // Called as the scene transitions from the background to the foreground. - // Use this method to undo the changes made on entering the background. - } - - [Export("sceneDidEnterBackground:")] - public void DidEnterBackground(UIScene scene) - { - // Called as the scene transitions from the foreground to the background. - // Use this method to save data, release shared resources, and store enough scene-specific state information - // to restore the scene back to its current state. - } - } -} diff --git a/Sources/Stylophone.iOS/Services/InteropService.cs b/Sources/Stylophone.iOS/Services/InteropService.cs index 6a6dc94c..78f401f8 100644 --- a/Sources/Stylophone.iOS/Services/InteropService.cs +++ b/Sources/Stylophone.iOS/Services/InteropService.cs @@ -12,9 +12,11 @@ namespace Stylophone.iOS.Services { public class InteropService: IInteropService { + private NowPlayingService _nowPlayingService; - public InteropService( ) + public InteropService(NowPlayingService nowPlayingService) { + _nowPlayingService = nowPlayingService; } public Task SetThemeAsync(Theme theme) @@ -35,7 +37,11 @@ public Task SetThemeAsync(Theme theme) public SKColor GetAccentColor() { - var accent = UIApplication.SharedApplication.KeyWindow.TintColor; + var accent = UIColor.SystemBlue; + UIApplication.SharedApplication.InvokeOnMainThread(() => + { + accent = UIApplication.SharedApplication.KeyWindow.TintColor; + }); return accent.ToSKColor(); } @@ -53,11 +59,10 @@ public async Task GetPlaceholderImageAsync() return skImage; } - public Task UpdateOperatingSystemIntegrationsAsync(TrackViewModel currentTrack) + public async Task UpdateOperatingSystemIntegrationsAsync(TrackViewModel currentTrack) { - //await _smtcService.UpdateMetadataAsync(currentTrack); + await _nowPlayingService.UpdateMetadataAsync(currentTrack); //await LiveTileHelper.UpdatePlayingSongAsync(currentTrack); - return Task.CompletedTask; } public Version GetAppVersion() diff --git a/Sources/Stylophone.iOS/Services/NotificationService.cs b/Sources/Stylophone.iOS/Services/NotificationService.cs index 5683aa4f..ae2592e6 100644 --- a/Sources/Stylophone.iOS/Services/NotificationService.cs +++ b/Sources/Stylophone.iOS/Services/NotificationService.cs @@ -15,14 +15,20 @@ public NotificationService(IDispatcherService dispatcherService) _dispatcherService = dispatcherService; } - public override void ShowInAppNotification(string notification, bool autoHide) + public override void ShowInAppNotification(InAppNotification notification) { UIApplication.SharedApplication.InvokeOnMainThread(() => { - if (autoHide) - RMessage.ShowNotificationWithTitle(notification, "", RMessageType.Normal, "", 2, () => { }, true); - else - RMessage.ShowNotificationWithTitle(notification, RMessageType.Normal, "", () => { }); + 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, () => { }); }); } diff --git a/Sources/Stylophone.iOS/Services/NowPlayingService.cs b/Sources/Stylophone.iOS/Services/NowPlayingService.cs new file mode 100644 index 00000000..2dbd4fca --- /dev/null +++ b/Sources/Stylophone.iOS/Services/NowPlayingService.cs @@ -0,0 +1,179 @@ +using System; +using System.Threading.Tasks; +using AVFoundation; +using Foundation; +using MediaPlayer; +using MpcNET; +using MpcNET.Commands.Playback; +using SkiaSharp; +using SkiaSharp.Views.iOS; +using Stylophone.Common.Helpers; +using Stylophone.Common.Interfaces; +using Stylophone.Common.Services; +using Stylophone.Common.ViewModels; +using UIKit; + +namespace Stylophone.iOS.Services +{ + public class NowPlayingService + { + private MPNowPlayingInfo _nowPlayingInfo; + private MPDConnectionService _mpdService; + private IApplicationStorageService _storageService; + + private AVAudioPlayer _silencePlayer; + + public NowPlayingService(MPDConnectionService mpdService, IApplicationStorageService storageService) + { + _mpdService = mpdService; + _storageService = storageService; + + // https://stackoverflow.com/questions/48289037/using-mpnowplayinginfocenter-without-actually-playing-audio + // TODO This breaks when LibVLC playback stops + _silencePlayer = new AVAudioPlayer(new NSUrl("silence.wav",false,NSBundle.MainBundle.ResourceUrl), null, out var error); + _silencePlayer.NumberOfLoops = -1; + } + + public void Initialize() + { + _nowPlayingInfo = MPNowPlayingInfoCenter.DefaultCenter.NowPlaying; + + _nowPlayingInfo.MediaType = MPNowPlayingInfoMediaType.Audio; + _nowPlayingInfo.IsLiveStream = false; + + EnableCommands(_mpdService.IsConnected); + RegisterCommands(); + + // Hook up to the MPDConnectionService for status updates. + _mpdService.ConnectionChanged += (s, e) => EnableCommands(_mpdService.IsConnected); + _mpdService.StatusChanged += (s, e) => UpdateState(_mpdService.CurrentStatus); + } + + private void EnableCommands(bool isConnected) + { + AVAudioSession.SharedInstance().SetActive(isConnected, AVAudioSessionSetActiveOptions.NotifyOthersOnDeactivation); + + MPRemoteCommandCenter.Shared.PreviousTrackCommand.Enabled = isConnected; + MPRemoteCommandCenter.Shared.NextTrackCommand.Enabled = isConnected; + MPRemoteCommandCenter.Shared.ChangePlaybackPositionCommand.Enabled = isConnected; + MPRemoteCommandCenter.Shared.TogglePlayPauseCommand.Enabled = isConnected; + MPRemoteCommandCenter.Shared.ChangeRepeatModeCommand.Enabled = isConnected; + MPRemoteCommandCenter.Shared.ChangeShuffleModeCommand.Enabled = isConnected; + MPRemoteCommandCenter.Shared.StopCommand.Enabled = isConnected; + } + + private void RegisterCommands() + { + MPRemoteCommandCenter.Shared.TogglePlayPauseCommand.AddTarget((evt) => { + _mpdService.SafelySendCommandAsync(new PauseResumeCommand()); + return MPRemoteCommandHandlerStatus.Success; + }); + + MPRemoteCommandCenter.Shared.PreviousTrackCommand.AddTarget((evt) => { + _mpdService.SafelySendCommandAsync(new PreviousCommand()); + return MPRemoteCommandHandlerStatus.Success; + }); + + MPRemoteCommandCenter.Shared.NextTrackCommand.AddTarget((evt) => { + _mpdService.SafelySendCommandAsync(new NextCommand()); + return MPRemoteCommandHandlerStatus.Success; + }); + + MPRemoteCommandCenter.Shared.ChangeRepeatModeCommand.AddTarget((evt) => { + var isRepeat = (evt.Command as MPChangeRepeatModeCommand).CurrentRepeatType; + _mpdService.SafelySendCommandAsync(new RepeatCommand(isRepeat != MPRepeatType.Off)); + _mpdService.SafelySendCommandAsync(new SingleCommand(isRepeat == MPRepeatType.One)); + return MPRemoteCommandHandlerStatus.Success; + }); + + MPRemoteCommandCenter.Shared.ChangeShuffleModeCommand.AddTarget((evt) => { + var isShuffle = (evt.Command as MPChangeShuffleModeCommand).CurrentShuffleType; + _mpdService.SafelySendCommandAsync(new RandomCommand(isShuffle == MPShuffleType.Items)); + return MPRemoteCommandHandlerStatus.Success; + }); + + MPRemoteCommandCenter.Shared.ChangePlaybackPositionCommand.AddTarget((evt) => { + var position = (evt as MPChangePlaybackPositionCommandEvent).PositionTime; + + position = Math.Round(position); // Fractional values don't seem to work well on iOS + _mpdService.SafelySendCommandAsync(new SeekCurCommand(position)); + return MPRemoteCommandHandlerStatus.Success; + }); + + MPRemoteCommandCenter.Shared.StopCommand.AddTarget((evt) => { + _mpdService.SafelySendCommandAsync(new StopCommand()); + return MPRemoteCommandHandlerStatus.Success; + }); + } + + private void UpdateState(MpdStatus status) + { + switch (status.State) + { + case MpdState.Play: + _silencePlayer.Play(); + _nowPlayingInfo.PlaybackRate = 1; + break; + case MpdState.Pause: + _silencePlayer.Stop(); + _nowPlayingInfo.PlaybackRate = 0; + break; + case MpdState.Stop: + _silencePlayer.Stop(); + _nowPlayingInfo.PlaybackRate = 0; + break; + case MpdState.Unknown: + _silencePlayer.Stop(); + _nowPlayingInfo.PlaybackRate = 0; + break; + default: + break; + } + + _nowPlayingInfo.ElapsedPlaybackTime = status.Elapsed.TotalSeconds; + _nowPlayingInfo.PlaybackDuration = status.Duration.TotalSeconds; + + MPNowPlayingInfoCenter.DefaultCenter.NowPlaying = _nowPlayingInfo; + } + + public async Task UpdateMetadataAsync(TrackViewModel track) + { + try + { + _nowPlayingInfo.Artist = track.File.Artist ?? ""; + _nowPlayingInfo.Title = track.File.Title ?? ""; + _nowPlayingInfo.AlbumTitle = track.File.Album ?? ""; + + var thumbnail = UIImage.FromBundle("AlbumPlaceholder"); + + // Set the album art thumbnail. + var uniqueIdentifier = Miscellaneous.GetFileIdentifier(track.File); + + // Use the cached albumart if it exists + if (await _storageService.DoesFileExistAsync(uniqueIdentifier, "AlbumArt")) + { + using (var fileStream = await _storageService.OpenFileAsync(uniqueIdentifier, "AlbumArt")) + using (var data = NSData.FromStream(fileStream)) + thumbnail = UIImage.LoadFromData(data); + } + + _nowPlayingInfo.Artwork = new MPMediaItemArtwork(thumbnail); + + MPNowPlayingInfoCenter.DefaultCenter.NowPlaying = _nowPlayingInfo; + } + catch (Exception e) + { + System.Diagnostics.Debug.WriteLine("Error while updating NowPlayingInfo: " + e); + } + } + + private void UpdateTimeline(TimeSpan current, TimeSpan length) + { + _nowPlayingInfo.ElapsedPlaybackTime = current.TotalSeconds; + _nowPlayingInfo.PlaybackDuration = length.TotalSeconds; + _nowPlayingInfo.PlaybackRate = 1; + + MPNowPlayingInfoCenter.DefaultCenter.NowPlaying = _nowPlayingInfo; + } + } +} diff --git a/Sources/Stylophone.iOS/Stylophone.iOS.csproj b/Sources/Stylophone.iOS/Stylophone.iOS.csproj index 1831517c..b3e6584f 100644 --- a/Sources/Stylophone.iOS/Stylophone.iOS.csproj +++ b/Sources/Stylophone.iOS/Stylophone.iOS.csproj @@ -27,6 +27,8 @@ None true 9.0 + iOS Team Provisioning Profile: com.tvc-16.Stylophone + iPhone Developer none @@ -68,7 +70,6 @@ - @@ -146,13 +147,15 @@ PlaylistViewController.cs + + + + SymbolUIButton.cs + - - false - @@ -167,39 +170,33 @@ false - + false - + false - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + @@ -210,18 +207,18 @@ - + - 2.80.2 + 2.88.1 - 5.0.1 + 6.0.0 - 4.2.0 + 4.5.3 - 4.2.0 + 4.5.3 0.0.1 @@ -230,9 +227,9 @@ 1.0.0 - 3.3.10 + 3.3.17 - + @@ -256,5 +253,8 @@ 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 3a25f3f3..53fb9daa 100644 --- a/Sources/Stylophone.iOS/ViewControllers/AddToPlaylistViewController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/AddToPlaylistViewController.cs @@ -90,7 +90,7 @@ public override void LoadView() if (AllowExistingPlaylists) stackView.AddArrangedSubview(playlistSwitch); - stackView.AddArrangedSubview(new UILabel { Text = Strings.AddToPlaylistText, Font = UIFont.PreferredTitle2 }); + //stackView.AddArrangedSubview(new UILabel { Text = Strings.AddToPlaylistText, Font = UIFont.PreferredTitle2 }); if (AllowExistingPlaylists) stackView.AddArrangedSubview(playlistPicker); diff --git a/Sources/Stylophone.iOS/ViewControllers/AlbumDetailViewController.cs b/Sources/Stylophone.iOS/ViewControllers/AlbumDetailViewController.cs index 472f269e..0f19babd 100644 --- a/Sources/Stylophone.iOS/ViewControllers/AlbumDetailViewController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/AlbumDetailViewController.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Linq; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Strings = Stylophone.Localization.Strings.Resources; using Stylophone.Common.ViewModels; using Stylophone.iOS.Helpers; @@ -138,7 +138,7 @@ private UIMenu GetRowContextMenu(NSIndexPath indexPath) } var queueAction = Binder.GetCommandAction(Strings.ContextMenuAddToQueue, "plus", ViewModel.AddToQueueCommand, trackList); - var playlistAction = Binder.GetCommandAction(Strings.ContextMenuAddToPlaylist, "music.note.list", ViewModel.AddToPlayListCommand, trackList); + var playlistAction = Binder.GetCommandAction(Strings.ContextMenuAddToPlaylist, "music.note.list", ViewModel.AddToPlaylistCommand, trackList); return UIMenu.Create(new[] { queueAction, playlistAction }); } @@ -150,7 +150,7 @@ private UISwipeActionsConfiguration GetRowSwipeActions(NSIndexPath indexPath, bo trackList.Add(ViewModel?.Source[indexPath.Row]); var action = isLeadingSwipe ? Binder.GetContextualAction(UIContextualActionStyle.Normal, Strings.ContextMenuAddToQueue, ViewModel.AddToQueueCommand, trackList) - : Binder.GetContextualAction(UIContextualActionStyle.Normal, Strings.ContextMenuAddToPlaylist, ViewModel.AddToPlayListCommand, trackList); + : Binder.GetContextualAction(UIContextualActionStyle.Normal, Strings.ContextMenuAddToPlaylist, ViewModel.AddToPlaylistCommand, trackList); return UISwipeActionsConfiguration.FromActions(new[] { action }); } diff --git a/Sources/Stylophone.iOS/ViewControllers/FoldersViewController.cs b/Sources/Stylophone.iOS/ViewControllers/FoldersViewController.cs index 6293ec33..beeba663 100644 --- a/Sources/Stylophone.iOS/ViewControllers/FoldersViewController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/FoldersViewController.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; using CoreGraphics; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Strings = Stylophone.Localization.Strings.Resources; using Stylophone.Common.ViewModels; using Stylophone.iOS.Helpers; @@ -19,7 +19,7 @@ public class FilePathViewModelHolder : NSObject public FilePathViewModel ViewModel; } - public partial class FoldersViewController : UICollectionViewController, IViewController + public partial class FoldersViewController : UICollectionViewController, IUICollectionViewDelegateFlowLayout, IViewController { private FilePathViewModel _currentlyShownVm; private PropertyBinder _filePathBinder; @@ -41,6 +41,17 @@ public override async void ViewDidLoad() await InitTreeAsync(); } + + [Export("collectionView:layout:sizeForItemAtIndexPath:")] + public CGSize GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath) + { + if (TraitCollection.HorizontalSizeClass == UIUserInterfaceSizeClass.Compact && + TraitCollection.VerticalSizeClass == UIUserInterfaceSizeClass.Regular) + return new CGSize(96, 96); + else + return new CGSize(128, 128); + } + public override async void ItemSelected(UICollectionView collectionView, NSIndexPath indexPath) { collectionView.CellForItem(indexPath); diff --git a/Sources/Stylophone.iOS/ViewControllers/LibraryViewController.cs b/Sources/Stylophone.iOS/ViewControllers/LibraryViewController.cs index 5c33bc97..af83f99d 100644 --- a/Sources/Stylophone.iOS/ViewControllers/LibraryViewController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/LibraryViewController.cs @@ -3,7 +3,7 @@ using System; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Stylophone.iOS.Helpers; using Stylophone.iOS.ViewModels; using Strings = Stylophone.Localization.Strings.Resources; @@ -12,6 +12,7 @@ using System.Threading.Tasks; using System.Linq; using CoreGraphics; +using System.Drawing; namespace Stylophone.iOS.ViewControllers { @@ -20,7 +21,7 @@ public partial class AlbumViewModelHolder : NSObject public AlbumViewModel ViewModel; } - public partial class LibraryViewController : UICollectionViewController, IViewController, IUISearchBarDelegate + public partial class LibraryViewController : UICollectionViewController, IUICollectionViewDelegateFlowLayout, IViewController, IUISearchBarDelegate { public LibraryViewController(IntPtr handle) : base(handle) { @@ -37,6 +38,7 @@ public override async void AwakeFromNib() NavigationItem.LargeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Always; Title = LibraryViewModelBase.GetHeader(); + var searchController = new UISearchController(searchResultsController: null); NavigationItem.SearchController = searchController; NavigationItem.HidesSearchBarWhenScrolling = false; @@ -49,7 +51,7 @@ public override async void AwakeFromNib() await InitializeLibraryAsync(); } - + private async Task InitializeLibraryAsync() { if (ViewModel.IsSourceEmpty) @@ -125,6 +127,34 @@ private UIMenu GetMenuForViewModel(AlbumViewModel vm) return UIMenu.Create(new[] { playAction, addToQueueAction, addToPlaylistAction }); } + private CGSize? _incomingSize; + public override void ViewWillTransitionToSize(CGSize toSize, IUIViewControllerTransitionCoordinator coordinator) + { + base.ViewWillTransitionToSize(toSize, coordinator); + + // Keep track of the incoming size, since when we invalidate the collectionview's size won't have updated yet + _incomingSize = toSize; + var invalidation = new UICollectionViewFlowLayoutInvalidationContext(); + invalidation.InvalidateFlowLayoutDelegateMetrics = true; + + CollectionView.CollectionViewLayout.InvalidateLayout(invalidation); + } + + [Export("collectionView:layout:sizeForItemAtIndexPath:")] + public CGSize GetSizeForItem(UICollectionView collectionView, UICollectionViewLayout layout, NSIndexPath indexPath) + { + var referenceSize = _incomingSize ?? collectionView.Frame.Size; + var size = new CGSize(192, 192); + + if (referenceSize.Width < 600) + size = new CGSize(148, 148); + + if (referenceSize.Width < 350) + size = new CGSize(120, 120); + + return size; + } + [Export("searchBar:textDidChange:")] public void TextChanged(UISearchBar searchBar, string text) { diff --git a/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs b/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs index f10b86d7..2c8db93f 100644 --- a/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.cs @@ -4,7 +4,7 @@ using System.ComponentModel; using Strings = Stylophone.Localization.Strings.Resources; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using SkiaSharp; using Stylophone.Common.Helpers; using Stylophone.Common.ViewModels; @@ -12,6 +12,9 @@ using Stylophone.iOS.ViewModels; using UIKit; using Pop = ARSPopover.iOS; +using static Xamarin.Essentials.Permissions; +using CommunityToolkit.Mvvm.Input; +using CoreGraphics; namespace Stylophone.iOS.ViewControllers { @@ -77,13 +80,55 @@ public override void AwakeFromNib() public override void ViewWillAppear(bool animated) { base.ViewWillAppear(animated); - NavigationController.NavigationBar.TintColor = UIColor.LabelColor; + NavigationController.NavigationBar.TintColor = UIColor.Label; } public override void ViewWillDisappear(bool animated) { base.ViewWillDisappear(animated); NavigationController.NavigationBar.TintColor = null; + + (UIApplication.SharedApplication.Delegate as AppDelegate).Window.TintColor = (UIApplication.SharedApplication.Delegate as AppDelegate).AppColor; + } + + public override void ViewDidLayoutSubviews() + { + base.ViewDidLayoutSubviews(); + + if (!ViewModel.IsFullScreen) return; + + // Don't multi-line for small phones in vertical orientation + 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; + } + + // On compact widths, change the application tintcolor, as that's what is used instead of the navigation bar's + if (TraitCollection.HorizontalSizeClass == UIUserInterfaceSizeClass.Compact || + TraitCollection.VerticalSizeClass == UIUserInterfaceSizeClass.Compact) + { + (UIApplication.SharedApplication.Delegate as AppDelegate).Window.TintColor = UIColor.Label; + } + else + { + (UIApplication.SharedApplication.Delegate as AppDelegate).Window.TintColor = (UIApplication.SharedApplication.Delegate as AppDelegate).AppColor; + // Reset TitleView as it can disappear when moving from compact to normal size class + // TODO: this doesn't do anything.. + NavigationItem.TitleView = upNextView; + } } public override void ViewDidLoad() @@ -121,9 +166,18 @@ public override void ViewDidLoad() PlayPauseButton.PrimaryActionTriggered += (s, e) => ViewModel.ChangePlaybackState(); ShuffleButton.PrimaryActionTriggered += (s, e) => ViewModel.ToggleShuffle(); RepeatButton.PrimaryActionTriggered += (s, e) => ViewModel.ToggleRepeat(); - VolumeButton.PrimaryActionTriggered += (s, e) => ShowVolumePopover(VolumeButton); + VolumeButton.PrimaryActionTriggered += (s, e) => ShowVolumePopover(VolumeButton); + + // 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; + //View.BringSubviewToFront(AlbumArt); } private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e) @@ -154,6 +208,9 @@ private void OnVmPropertyChanged(object sender, PropertyChangedEventArgs e) UpdateButton(ShuffleButton, image); } + if (e.PropertyName == nameof(ViewModel.IsConsumeEnabled)) + _consumeAction.State = ViewModel.IsConsumeEnabled ? UIMenuElementState.On : UIMenuElementState.Off; + if (e.PropertyName == nameof(ViewModel.RepeatIcon)) { UpdateButton(RepeatButton, ViewModel.RepeatIcon); @@ -188,19 +245,25 @@ private void UpdateFullView(TrackViewModel currentTrack) _trackBinder.Bind(AlbumArt, "image", nameof(currentTrack.AlbumArt), valueTransformer: imageConverter); _trackBinder.Bind(AlbumBackground, "image", nameof(currentTrack.AlbumArt), valueTransformer: imageConverter); _trackBinder.Bind(BackgroundTint, "backgroundColor", nameof(currentTrack.DominantColor), valueTransformer: colorConverter); - //_trackBinder.Bind(PlayPauseButton, "tintColor", nameof(currentTrack.DominantColor), valueTransformer: colorConverter); + _trackBinder.Bind(TrackSlider, "maximumTrackTintColor", nameof(currentTrack.DominantColor), valueTransformer: colorConverter); } private void UpdateButton(UIButton button, string systemImg) => button?.SetImage(UIImage.GetSystemImage(systemImg), UIControlState.Normal); + private UIAction _consumeAction; private UIBarButtonItem CreateSettingsButton() { + _consumeAction = Binder.GetCommandAction(Strings.ActionToggleConsume, "fork.knife", new RelayCommand(ViewModel.ToggleConsume)); var addQueueAction = Binder.GetCommandAction(Strings.ContextMenuAddToPlaylist, "music.note.list", ViewModel.AddToPlaylistCommand); var viewAlbumAction = Binder.GetCommandAction(Strings.ContextMenuViewAlbum, "opticaldisc", ViewModel.ShowAlbumCommand); - var barButtonMenu = UIMenu.Create(new[] { addQueueAction, viewAlbumAction }); + // https://stackoverflow.com/questions/64738005/how-to-change-the-state-for-an-uiaction-inside-uimenu-at-runtime + var dynamicAction = UIDeferredMenuElement.CreateUncached(new UIDeferredMenuElementProviderHandler((completion) => + completion.Invoke(new[] { _consumeAction }))); + + var barButtonMenu = UIMenu.Create(new UIMenuElement[] { dynamicAction, addQueueAction, viewAlbumAction }); return new UIBarButtonItem(UIImage.GetSystemImage("ellipsis.circle"), barButtonMenu); } diff --git a/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.designer.cs b/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.designer.cs index fcbb6a4c..066aeb25 100644 --- a/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.designer.cs +++ b/Sources/Stylophone.iOS/ViewControllers/PlaybackViewController.designer.cs @@ -63,6 +63,9 @@ partial class PlaybackViewController [Outlet] UIKit.UISlider ServerVolumeSlider { get; set; } + [Outlet] + UIKit.UIView ShadowCaster { get; set; } + [Outlet] UIKit.UIButton ShuffleButton { get; set; } @@ -131,6 +134,11 @@ void ReleaseDesignerOutlets () LocalPlaybackView = null; } + if (LocalVolume != null) { + LocalVolume.Dispose (); + LocalVolume = null; + } + if (LocalVolumeSlider != null) { LocalVolumeSlider.Dispose (); LocalVolumeSlider = null; @@ -156,6 +164,11 @@ void ReleaseDesignerOutlets () ServerMuteButton = null; } + if (ServerVolume != null) { + ServerVolume.Dispose (); + ServerVolume = null; + } + if (ServerVolumeSlider != null) { ServerVolumeSlider.Dispose (); ServerVolumeSlider = null; @@ -196,14 +209,9 @@ void ReleaseDesignerOutlets () VolumePopover = null; } - if (LocalVolume != null) { - LocalVolume.Dispose (); - LocalVolume = null; - } - - if (ServerVolume != null) { - ServerVolume.Dispose (); - ServerVolume = null; + if (ShadowCaster != null) { + ShadowCaster.Dispose (); + ShadowCaster = null; } } } diff --git a/Sources/Stylophone.iOS/ViewControllers/PlaylistViewController.cs b/Sources/Stylophone.iOS/ViewControllers/PlaylistViewController.cs index 520eaab4..d05e7858 100644 --- a/Sources/Stylophone.iOS/ViewControllers/PlaylistViewController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/PlaylistViewController.cs @@ -6,7 +6,7 @@ using Stylophone.Common.ViewModels; using Stylophone.iOS.Helpers; using UIKit; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using CoreGraphics; using System.Linq; using Foundation; @@ -97,6 +97,12 @@ public override void AwakeFromNib() ArtContainer.Layer.ShadowRadius = 4; } + public override void ViewWillDisappear(bool animated) + { + base.ViewWillDisappear(animated); + ViewModel.Dispose(); + } + private void OnScroll(UIScrollView scrollView) { if (scrollView.ContentOffset.Y > 192) diff --git a/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs b/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs index a5a68c21..188d7a95 100644 --- a/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/QueueViewController.cs @@ -3,7 +3,7 @@ using System; using System.Linq; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Stylophone.Common.Services; using Stylophone.Common.ViewModels; using Stylophone.iOS.Helpers; @@ -88,7 +88,7 @@ private UIMenu GetRowContextMenu(NSIndexPath indexPath) var playAction = Binder.GetCommandAction(Strings.ContextMenuPlay, "play", ViewModel.PlayTrackCommand, trackList); var albumAction = Binder.GetCommandAction(Strings.ContextMenuViewAlbum, "opticaldisc", ViewModel.ViewAlbumCommand, trackList); - var playlistAction = Binder.GetCommandAction(Strings.ContextMenuAddToPlaylist, "music.note.list", ViewModel.AddToPlayListCommand, trackList); + var playlistAction = Binder.GetCommandAction(Strings.ContextMenuAddToPlaylist, "music.note.list", ViewModel.AddToPlaylistCommand, trackList); var removeAction = Binder.GetCommandAction(Strings.ContextMenuRemoveFromQueue, "trash", ViewModel.RemoveFromQueueCommand, trackList); removeAction.Attributes = UIMenuElementAttributes.Destructive; diff --git a/Sources/Stylophone.iOS/ViewControllers/SearchController.cs b/Sources/Stylophone.iOS/ViewControllers/SearchController.cs index d70be12d..bbf4b103 100644 --- a/Sources/Stylophone.iOS/ViewControllers/SearchController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/SearchController.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using MpcNET.Types; using Stylophone.Common.Interfaces; using Stylophone.iOS.ViewModels; diff --git a/Sources/Stylophone.iOS/ViewControllers/SearchResultsViewController.cs b/Sources/Stylophone.iOS/ViewControllers/SearchResultsViewController.cs index 393c7656..65e773bc 100644 --- a/Sources/Stylophone.iOS/ViewControllers/SearchResultsViewController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/SearchResultsViewController.cs @@ -4,12 +4,13 @@ using System.Collections.Generic; using System.Linq; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Stylophone.Common.ViewModels; using Stylophone.iOS.Helpers; using Stylophone.iOS.ViewModels; using Strings = Stylophone.Localization.Strings.Resources; using UIKit; +using Stylophone.Localization.Strings; namespace Stylophone.iOS.ViewControllers { @@ -25,7 +26,10 @@ public SearchResultsViewController (IntPtr handle) : base (handle) public override void ViewWillAppear(bool animated) { base.ViewWillAppear(animated); - Title = Ioc.Default.GetRequiredService().HeaderText; + Title = string.Format(Resources.SearchResultsFor, ViewModel.QueryText); + + // Don't display large title for search results as they're usually too long + NavigationItem.LargeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Never; } public override void ViewDidLoad() @@ -33,6 +37,8 @@ public override void ViewDidLoad() base.ViewDidLoad(); NavigationItem.LargeTitleDisplayMode = UINavigationItemLargeTitleDisplayMode.Always; + Title = SearchResultsViewModel.GetHeader(); + var negateBoolTransformer = NSValueTransformer.GetValueTransformer(nameof(ReverseBoolValueTransformer)); Binder.Bind(EmptyView, "hidden", nameof(ViewModel.IsSourceEmpty), valueTransformer: negateBoolTransformer); @@ -84,7 +90,7 @@ private UIMenu GetRowContextMenu(NSIndexPath indexPath) var queueAction = Binder.GetCommandAction(Strings.ContextMenuAddToQueue, "plus", ViewModel.AddToQueueCommand, trackList); var albumAction = Binder.GetCommandAction(Strings.ContextMenuViewAlbum, "opticaldisc", ViewModel.ViewAlbumCommand, trackList); - var playlistAction = Binder.GetCommandAction(Strings.ContextMenuAddToPlaylist, "music.note.list", ViewModel.AddToPlayListCommand, trackList); + var playlistAction = Binder.GetCommandAction(Strings.ContextMenuAddToPlaylist, "music.note.list", ViewModel.AddToPlaylistCommand, trackList); return UIMenu.Create(new[] { queueAction, albumAction, playlistAction }); } @@ -96,7 +102,7 @@ private UISwipeActionsConfiguration GetRowSwipeActions(NSIndexPath indexPath, bo trackList.Add(ViewModel?.Source[indexPath.Row]); var action = isLeadingSwipe ? Binder.GetContextualAction(UIContextualActionStyle.Normal, Strings.ContextMenuAddToQueue, ViewModel.AddToQueueCommand, trackList) - : Binder.GetContextualAction(UIContextualActionStyle.Normal, Strings.ContextMenuAddToPlaylist, ViewModel.AddToPlayListCommand, trackList); + : Binder.GetContextualAction(UIContextualActionStyle.Normal, Strings.ContextMenuAddToPlaylist, ViewModel.AddToPlaylistCommand, trackList); return UISwipeActionsConfiguration.FromActions(new[] { action }); } diff --git a/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs b/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs index 394c528a..557957db 100644 --- a/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.cs @@ -3,7 +3,7 @@ using System; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Stylophone.Common.ViewModels; using Stylophone.iOS.Helpers; using Stylophone.Localization.Strings; @@ -50,7 +50,7 @@ public override string TitleForFooter(UITableView tableView, nint section) return (int)section switch { 1 => Resources.SettingsLocalPlaybackText, - 2 => Resources.SettingsClearCacheDescription, + 2 => Resources.SettingsAlbumArtText, 3 => Resources.SettingsApplyOnRestart, _ => "", }; @@ -81,11 +81,12 @@ public override void ViewDidLoad() Binder.Bind(LocalPlaybackToggle, "enabled", nameof(ViewModel.IsStreamingAvailable)); Binder.Bind(LocalPlaybackToggle, "on", nameof(ViewModel.IsLocalPlaybackEnabled), true); Binder.Bind(AnalyticsToggle, "on", nameof(ViewModel.EnableAnalytics), true); + Binder.Bind(AlbumArtToggle, "on", nameof(ViewModel.IsAlbumArtFetchingEnabled), true); Binder.Bind(VersionLabel, "text", nameof(ViewModel.VersionDescription)); - Binder.BindButton(ClearCacheButton, Resources.SettingsClearCache, ViewModel.ClearCacheCommand); - Binder.BindButton(UpdateDatabaseButton, Resources.SettingsUpdateDatabase, ViewModel.RescanDbCommand); + Binder.BindButton(ClearCacheButton, Resources.SettingsClearCacheDescription, ViewModel.ClearCacheCommand); + Binder.BindButton(UpdateDatabaseButton, Resources.SettingsUpdateDbTitle, ViewModel.RescanDbCommand); Binder.BindButton(RateButton, Resources.RateAppPromptTitle, ViewModel.RateAppCommand); GithubButton.SetTitle(Resources.SettingsGithub, UIControlState.Normal); diff --git a/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.designer.cs b/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.designer.cs index e5e068b1..da6b3a74 100644 --- a/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.designer.cs +++ b/Sources/Stylophone.iOS/ViewControllers/SettingsViewController.designer.cs @@ -12,6 +12,9 @@ namespace Stylophone.iOS.ViewControllers [Register ("SettingsViewController")] partial class SettingsViewController { + [Outlet] + UIKit.UISwitch AlbumArtToggle { get; set; } + [Outlet] UIKit.UISwitch AnalyticsToggle { get; set; } @@ -74,6 +77,16 @@ void ReleaseDesignerOutlets () GithubButton = null; } + if (LocalPlaybackToggle != null) { + LocalPlaybackToggle.Dispose (); + LocalPlaybackToggle = null; + } + + if (AlbumArtToggle != null) { + AlbumArtToggle.Dispose (); + AlbumArtToggle = null; + } + if (RateButton != null) { RateButton.Dispose (); RateButton = null; @@ -128,11 +141,6 @@ void ReleaseDesignerOutlets () VersionLabel.Dispose (); VersionLabel = null; } - - if (LocalPlaybackToggle != null) { - LocalPlaybackToggle.Dispose (); - LocalPlaybackToggle = null; - } } } } diff --git a/Sources/Stylophone.iOS/ViewControllers/SidebarViewController.cs b/Sources/Stylophone.iOS/ViewControllers/SidebarViewController.cs index 76bc985a..7ddca3dc 100644 --- a/Sources/Stylophone.iOS/ViewControllers/SidebarViewController.cs +++ b/Sources/Stylophone.iOS/ViewControllers/SidebarViewController.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Linq; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Stylophone.Common.ViewModels; using Stylophone.iOS.Helpers; using Stylophone.iOS.ViewModels; diff --git a/Sources/Stylophone.iOS/ViewControllers/SubViews/AlbumCollectionViewCell.cs b/Sources/Stylophone.iOS/ViewControllers/SubViews/AlbumCollectionViewCell.cs index dadae72f..5c71f172 100644 --- a/Sources/Stylophone.iOS/ViewControllers/SubViews/AlbumCollectionViewCell.cs +++ b/Sources/Stylophone.iOS/ViewControllers/SubViews/AlbumCollectionViewCell.cs @@ -39,6 +39,16 @@ public override void AwakeFromNib() Layer.ShadowPath = UIBezierPath.FromRoundedRect(Bounds, 8).CGPath; } + public override void LayoutSubviews() + { + base.LayoutSubviews(); + + // Adjust content and shadow to current bounds of cell + var bounds = Bounds; + ContentView.Bounds = bounds; + Layer.ShadowPath = UIBezierPath.FromRoundedRect(bounds, 8).CGPath; + } + internal void Initialize(AlbumViewModel viewModel) { diff --git a/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.cs b/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.cs index 4a9f4d84..0d2b27f3 100644 --- a/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.cs +++ b/Sources/Stylophone.iOS/ViewControllers/SubViews/CompactPlaybackView.cs @@ -3,7 +3,7 @@ using System; using CoreGraphics; using Foundation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using SkiaSharp; using SkiaSharp.Views.iOS; using Stylophone.Common.ViewModels; @@ -39,6 +39,16 @@ public override void AwakeFromNib() AlbumArt.Layer.CornerRadius = 8; } + public override void LayoutSubviews() + { + base.LayoutSubviews(); + + if (TraitCollection.VerticalSizeClass == UIUserInterfaceSizeClass.Compact) + TrackTitle.Lines = 1; + else + TrackTitle.Lines = 2; + } + internal void Bind(TrackViewModel currentTrack) { if (currentTrack == null) diff --git a/Sources/Stylophone.iOS/ViewModels/PlaybackViewModel.cs b/Sources/Stylophone.iOS/ViewModels/PlaybackViewModel.cs index 494c0b50..0a8be8f5 100644 --- a/Sources/Stylophone.iOS/ViewModels/PlaybackViewModel.cs +++ b/Sources/Stylophone.iOS/ViewModels/PlaybackViewModel.cs @@ -1,7 +1,7 @@ using System; using System.ComponentModel; using System.Threading.Tasks; -using Microsoft.Toolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using SkiaSharp.Views.iOS; using Stylophone.Common.Helpers; using Stylophone.Common.Interfaces; diff --git a/Sources/Stylophone.iOS/ViewModels/ShellViewModel.cs b/Sources/Stylophone.iOS/ViewModels/ShellViewModel.cs index 45e85cfc..dd497c62 100644 --- a/Sources/Stylophone.iOS/ViewModels/ShellViewModel.cs +++ b/Sources/Stylophone.iOS/ViewModels/ShellViewModel.cs @@ -10,8 +10,9 @@ using Foundation; using Stylophone.iOS.ViewControllers; using Stylophone.iOS.Services; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using System.Threading.Tasks; +using System.ComponentModel; namespace Stylophone.iOS.ViewModels { @@ -33,6 +34,22 @@ internal void Initialize(UICollectionView collectionView, UICollectionViewDiffab var concreteNavService = _navigationService as NavigationService; concreteNavService.Navigated += UpdateNavigationViewSelection; + + PropertyChanged += UpdateDatabaseIndicator; + } + + private void UpdateDatabaseIndicator(object sender, PropertyChangedEventArgs e) + { + // Only run this code when IsServerUpdating changes + if (e.PropertyName != nameof(IsServerUpdating)) return; + + var snapshot = new NSDiffableDataSourceSectionSnapshot(); + var item = NavigationSidebarItem.GetRow(Strings.DatabaseUpdateHeader, null, null, UIImage.GetSystemImage("hourglass")); + + if (IsServerUpdating) + snapshot.AppendItems(new[] { item }); + + _sidebarDataSource.ApplySnapshot(snapshot, new NSString("database_update"), false); } private void UpdateNavigationViewSelection(object sender, CoreNavigationEventArgs e) @@ -71,12 +88,12 @@ protected override void UpdatePlaylistNavigation() _sidebarDataSource.ApplySnapshot(snapshot, new NSString("playlists"), false); } - protected override void OnLoaded() + protected override void Loaded() { } - protected override void OnItemInvoked(object itemInvoked) + protected override void Navigate(object itemInvoked) { if (itemInvoked is string s) { @@ -103,11 +120,5 @@ protected override void OnItemInvoked(object itemInvoked) } } } - - protected override void ShowInAppNotification(object sender, InAppNotificationRequestedEventArgs e) - { - // Noop on UIKit - // TODO make UWP only - } } } diff --git a/Sources/Stylophone.iOS/Views/AlbumDetail.storyboard b/Sources/Stylophone.iOS/Views/AlbumDetail.storyboard index a7d0acb4..2b5092c5 100644 --- a/Sources/Stylophone.iOS/Views/AlbumDetail.storyboard +++ b/Sources/Stylophone.iOS/Views/AlbumDetail.storyboard @@ -1,8 +1,8 @@ - - + + - + @@ -13,24 +13,24 @@ - + - + - + - + - + - + - + - + + + @@ -123,13 +125,13 @@ - - - - - - + + + - - + + + - + + - + - + + + - + + + + @@ -185,57 +200,98 @@ + - + + + - - + + + + + + + + - + - + - + + + + - + - + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + - + - @@ -279,6 +335,7 @@ + @@ -310,7 +367,7 @@ - + @@ -323,19 +380,19 @@ - - - - + - - - - + + - + - + @@ -692,16 +750,22 @@ + + + + + + - + - - + + @@ -709,6 +773,9 @@ + + + diff --git a/Sources/Stylophone.iOS/Views/Playlist.storyboard b/Sources/Stylophone.iOS/Views/Playlist.storyboard index acdfc616..4ead2beb 100644 --- a/Sources/Stylophone.iOS/Views/Playlist.storyboard +++ b/Sources/Stylophone.iOS/Views/Playlist.storyboard @@ -1,8 +1,8 @@ - - + + - + @@ -13,24 +13,24 @@ - + - + - + - + - + - + - + - - - + @@ -125,13 +123,13 @@ - - + + - + diff --git a/Sources/Stylophone.iOS/Views/Queue.storyboard b/Sources/Stylophone.iOS/Views/Queue.storyboard index c2f3afeb..400e84e4 100644 --- a/Sources/Stylophone.iOS/Views/Queue.storyboard +++ b/Sources/Stylophone.iOS/Views/Queue.storyboard @@ -1,8 +1,8 @@ - - + + - + @@ -12,21 +12,21 @@ - + - + - + - + - + - + - + - + - + - + diff --git a/Sources/Stylophone.iOS/Views/Settings.storyboard b/Sources/Stylophone.iOS/Views/Settings.storyboard index 031bb41a..f23c6199 100644 --- a/Sources/Stylophone.iOS/Views/Settings.storyboard +++ b/Sources/Stylophone.iOS/Views/Settings.storyboard @@ -1,8 +1,8 @@ - - + + - + @@ -13,24 +13,24 @@ - + - + - - + + - + - + @@ -38,7 +38,7 @@ - - + + - + - + @@ -94,15 +94,15 @@ - - + + - + - + @@ -131,16 +131,16 @@ - + - + - - - + + - + @@ -405,8 +441,9 @@ + - + @@ -424,11 +461,11 @@ - + - + diff --git a/Sources/Stylophone/App.xaml.cs b/Sources/Stylophone/App.xaml.cs index aebffdca..04fafed2 100644 --- a/Sources/Stylophone/App.xaml.cs +++ b/Sources/Stylophone/App.xaml.cs @@ -2,7 +2,7 @@ using Stylophone.Services; using Stylophone.ViewModels; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Stylophone.Common.Interfaces; using Stylophone.Common.Services; using Stylophone.Common.ViewModels; @@ -15,6 +15,11 @@ using Microsoft.AppCenter.Crashes; using Microsoft.Toolkit.Uwp.Helpers; using Windows.Foundation; +using Microsoft.Services.Store.Engagement; +#if DEBUG +#else +using System.Collections.Generic; +#endif namespace Stylophone { @@ -33,8 +38,8 @@ public App() Services = ConfigureServices(); Ioc.Default.ConfigureServices(Services); - InitializeComponent(); + UnhandledException += OnAppUnhandledException; // 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); @@ -61,6 +66,10 @@ protected override async void OnLaunched(LaunchActivatedEventArgs args) new ResourceDictionary { Source = new Uri(@"ms-appx:///Microsoft.UI.Xaml/DensityStyles/Compact.xaml", UriKind.Absolute) }); } + // Initialize MS Store Engagement notifications + StoreServicesEngagementManager engagementManager = StoreServicesEngagementManager.GetDefault(); + await engagementManager.RegisterNotificationChannelAsync(); + // Analytics SystemInformation.Instance.TrackAppUse(args); #if DEBUG @@ -91,6 +100,25 @@ protected override async void OnActivated(IActivatedEventArgs args) await ActivationService.ActivateAsync(args); } + private void OnAppUnhandledException(object sender, Windows.UI.Xaml.UnhandledExceptionEventArgs e) + { +#if DEBUG +#else + var enableAnalytics = Ioc.Default.GetRequiredService().GetValue(nameof(SettingsViewModel.EnableAnalytics), true); + if (enableAnalytics) + { + var dict = new Dictionary(); + dict.Add("exception", e.Exception.ToString()); + Analytics.TrackEvent("UnhandledCrash", dict); + } +#endif + var notificationService = Ioc.Default.GetRequiredService(); + notificationService.ShowErrorNotification(e.Exception); + + // Try to handle the exception in case it's not catastrophic + e.Handled = true; + } + private ActivationService CreateActivationService() { return new ActivationService(this, typeof(QueueViewModel), new Lazy(CreateShell)); diff --git a/Sources/Stylophone/Assets/AlbumPlaceholder.png b/Sources/Stylophone/Assets/AlbumPlaceholder.png index 51a9a9fd..574fb87e 100644 Binary files a/Sources/Stylophone/Assets/AlbumPlaceholder.png and b/Sources/Stylophone/Assets/AlbumPlaceholder.png differ diff --git a/Sources/Stylophone/Assets/LargeTile.scale-100.png b/Sources/Stylophone/Assets/LargeTile.scale-100.png index a2ef6361..33b5d425 100644 Binary files a/Sources/Stylophone/Assets/LargeTile.scale-100.png and b/Sources/Stylophone/Assets/LargeTile.scale-100.png differ diff --git a/Sources/Stylophone/Assets/LargeTile.scale-125.png b/Sources/Stylophone/Assets/LargeTile.scale-125.png index c809bef1..22eb7f5d 100644 Binary files a/Sources/Stylophone/Assets/LargeTile.scale-125.png and b/Sources/Stylophone/Assets/LargeTile.scale-125.png differ diff --git a/Sources/Stylophone/Assets/LargeTile.scale-150.png b/Sources/Stylophone/Assets/LargeTile.scale-150.png index 4cf202fd..c3309789 100644 Binary files a/Sources/Stylophone/Assets/LargeTile.scale-150.png and b/Sources/Stylophone/Assets/LargeTile.scale-150.png differ diff --git a/Sources/Stylophone/Assets/LargeTile.scale-200.png b/Sources/Stylophone/Assets/LargeTile.scale-200.png index 41a58af2..11aa196d 100644 Binary files a/Sources/Stylophone/Assets/LargeTile.scale-200.png and b/Sources/Stylophone/Assets/LargeTile.scale-200.png differ diff --git a/Sources/Stylophone/Assets/LargeTile.scale-400.png b/Sources/Stylophone/Assets/LargeTile.scale-400.png index 52e07d17..864cca8b 100644 Binary files a/Sources/Stylophone/Assets/LargeTile.scale-400.png and b/Sources/Stylophone/Assets/LargeTile.scale-400.png differ diff --git a/Sources/Stylophone/Assets/SmallTile.scale-100.png b/Sources/Stylophone/Assets/SmallTile.scale-100.png index 6469c283..b315b166 100644 Binary files a/Sources/Stylophone/Assets/SmallTile.scale-100.png and b/Sources/Stylophone/Assets/SmallTile.scale-100.png differ diff --git a/Sources/Stylophone/Assets/SmallTile.scale-125.png b/Sources/Stylophone/Assets/SmallTile.scale-125.png index c9bfa824..2ed2d396 100644 Binary files a/Sources/Stylophone/Assets/SmallTile.scale-125.png and b/Sources/Stylophone/Assets/SmallTile.scale-125.png differ diff --git a/Sources/Stylophone/Assets/SmallTile.scale-150.png b/Sources/Stylophone/Assets/SmallTile.scale-150.png index 877d0e2d..2602a5bf 100644 Binary files a/Sources/Stylophone/Assets/SmallTile.scale-150.png and b/Sources/Stylophone/Assets/SmallTile.scale-150.png differ diff --git a/Sources/Stylophone/Assets/SmallTile.scale-200.png b/Sources/Stylophone/Assets/SmallTile.scale-200.png index 595bdd9c..e5834f75 100644 Binary files a/Sources/Stylophone/Assets/SmallTile.scale-200.png and b/Sources/Stylophone/Assets/SmallTile.scale-200.png differ diff --git a/Sources/Stylophone/Assets/SmallTile.scale-400.png b/Sources/Stylophone/Assets/SmallTile.scale-400.png index c8aa0a3a..84006920 100644 Binary files a/Sources/Stylophone/Assets/SmallTile.scale-400.png and b/Sources/Stylophone/Assets/SmallTile.scale-400.png differ diff --git a/Sources/Stylophone/Assets/SplashScreen.scale-100.png b/Sources/Stylophone/Assets/SplashScreen.scale-100.png index b1bc16be..d170ce44 100644 Binary files a/Sources/Stylophone/Assets/SplashScreen.scale-100.png and b/Sources/Stylophone/Assets/SplashScreen.scale-100.png differ diff --git a/Sources/Stylophone/Assets/SplashScreen.scale-125.png b/Sources/Stylophone/Assets/SplashScreen.scale-125.png index fc97e455..23570fa4 100644 Binary files a/Sources/Stylophone/Assets/SplashScreen.scale-125.png and b/Sources/Stylophone/Assets/SplashScreen.scale-125.png differ diff --git a/Sources/Stylophone/Assets/SplashScreen.scale-150.png b/Sources/Stylophone/Assets/SplashScreen.scale-150.png index 853a185b..90b2ba3d 100644 Binary files a/Sources/Stylophone/Assets/SplashScreen.scale-150.png and b/Sources/Stylophone/Assets/SplashScreen.scale-150.png differ diff --git a/Sources/Stylophone/Assets/SplashScreen.scale-200.png b/Sources/Stylophone/Assets/SplashScreen.scale-200.png index a93e2339..8a197d3b 100644 Binary files a/Sources/Stylophone/Assets/SplashScreen.scale-200.png and b/Sources/Stylophone/Assets/SplashScreen.scale-200.png differ diff --git a/Sources/Stylophone/Assets/SplashScreen.scale-400.png b/Sources/Stylophone/Assets/SplashScreen.scale-400.png index db86f072..eed3b112 100644 Binary files a/Sources/Stylophone/Assets/SplashScreen.scale-400.png and b/Sources/Stylophone/Assets/SplashScreen.scale-400.png differ diff --git a/Sources/Stylophone/Assets/Square150x150Logo.scale-100.png b/Sources/Stylophone/Assets/Square150x150Logo.scale-100.png index 6faa5b5f..4a1f54e7 100644 Binary files a/Sources/Stylophone/Assets/Square150x150Logo.scale-100.png and b/Sources/Stylophone/Assets/Square150x150Logo.scale-100.png differ diff --git a/Sources/Stylophone/Assets/Square150x150Logo.scale-125.png b/Sources/Stylophone/Assets/Square150x150Logo.scale-125.png index cb9eb549..71709f59 100644 Binary files a/Sources/Stylophone/Assets/Square150x150Logo.scale-125.png and b/Sources/Stylophone/Assets/Square150x150Logo.scale-125.png differ diff --git a/Sources/Stylophone/Assets/Square150x150Logo.scale-150.png b/Sources/Stylophone/Assets/Square150x150Logo.scale-150.png index 2780e69a..29f90188 100644 Binary files a/Sources/Stylophone/Assets/Square150x150Logo.scale-150.png and b/Sources/Stylophone/Assets/Square150x150Logo.scale-150.png differ diff --git a/Sources/Stylophone/Assets/Square150x150Logo.scale-200.png b/Sources/Stylophone/Assets/Square150x150Logo.scale-200.png index 5db63d63..b9059323 100644 Binary files a/Sources/Stylophone/Assets/Square150x150Logo.scale-200.png and b/Sources/Stylophone/Assets/Square150x150Logo.scale-200.png differ diff --git a/Sources/Stylophone/Assets/Square150x150Logo.scale-400.png b/Sources/Stylophone/Assets/Square150x150Logo.scale-400.png index 29461182..60fac89c 100644 Binary files a/Sources/Stylophone/Assets/Square150x150Logo.scale-400.png and b/Sources/Stylophone/Assets/Square150x150Logo.scale-400.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png index 11868a44..a61a49d2 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png and b/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png index 370e16fc..d6d06b9f 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png and b/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png index 4e0c58aa..65c7c1fa 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png and b/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png index e6481501..aa91e82b 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png and b/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png index 848d07fc..8a8e36bb 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png and b/Sources/Stylophone/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-16.png index 11868a44..a61a49d2 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-16.png and b/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-24.png b/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-24.png index 44630a78..c5fb6724 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-24.png and b/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-24.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-256.png index 4e0c58aa..65c7c1fa 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-256.png and b/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-32.png index e6481501..aa91e82b 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-32.png and b/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-48.png index 848d07fc..8a8e36bb 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-48.png and b/Sources/Stylophone/Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.scale-100.png b/Sources/Stylophone/Assets/Square44x44Logo.scale-100.png index d0cff7c4..97f98335 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.scale-100.png and b/Sources/Stylophone/Assets/Square44x44Logo.scale-100.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.scale-125.png b/Sources/Stylophone/Assets/Square44x44Logo.scale-125.png index 15538f49..b644b3ed 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.scale-125.png and b/Sources/Stylophone/Assets/Square44x44Logo.scale-125.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.scale-150.png b/Sources/Stylophone/Assets/Square44x44Logo.scale-150.png index 5c11ecf2..8204509d 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.scale-150.png and b/Sources/Stylophone/Assets/Square44x44Logo.scale-150.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.scale-200.png b/Sources/Stylophone/Assets/Square44x44Logo.scale-200.png index 334c4a35..d96d2b85 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.scale-200.png and b/Sources/Stylophone/Assets/Square44x44Logo.scale-200.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.scale-400.png b/Sources/Stylophone/Assets/Square44x44Logo.scale-400.png index f44f6a7e..4707ea1a 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.scale-400.png and b/Sources/Stylophone/Assets/Square44x44Logo.scale-400.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-16.png b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-16.png index 11868a44..ee9bfff0 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-16.png and b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-16.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-24.png b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-24.png index 370e16fc..b23937a1 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-24.png and b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-24.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-24_altform-unplated.png index 370e16fc..d6d06b9f 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-256.png b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-256.png index 4e0c58aa..194a0cf4 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-256.png and b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-256.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-32.png b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-32.png index e6481501..368cff73 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-32.png and b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-32.png differ diff --git a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-48.png b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-48.png index 848d07fc..de4ea1bb 100644 Binary files a/Sources/Stylophone/Assets/Square44x44Logo.targetsize-48.png and b/Sources/Stylophone/Assets/Square44x44Logo.targetsize-48.png differ diff --git a/Sources/Stylophone/Assets/StoreLogo.backup.png b/Sources/Stylophone/Assets/StoreLogo.backup.png index 7385b56c..1c100570 100644 Binary files a/Sources/Stylophone/Assets/StoreLogo.backup.png and b/Sources/Stylophone/Assets/StoreLogo.backup.png differ diff --git a/Sources/Stylophone/Assets/StoreLogo.scale-100.png b/Sources/Stylophone/Assets/StoreLogo.scale-100.png index ce2df55c..67f8ebc6 100644 Binary files a/Sources/Stylophone/Assets/StoreLogo.scale-100.png and b/Sources/Stylophone/Assets/StoreLogo.scale-100.png differ diff --git a/Sources/Stylophone/Assets/StoreLogo.scale-125.png b/Sources/Stylophone/Assets/StoreLogo.scale-125.png index 58e1b867..7e84183f 100644 Binary files a/Sources/Stylophone/Assets/StoreLogo.scale-125.png and b/Sources/Stylophone/Assets/StoreLogo.scale-125.png differ diff --git a/Sources/Stylophone/Assets/StoreLogo.scale-150.png b/Sources/Stylophone/Assets/StoreLogo.scale-150.png index 89681e9f..4f862ade 100644 Binary files a/Sources/Stylophone/Assets/StoreLogo.scale-150.png and b/Sources/Stylophone/Assets/StoreLogo.scale-150.png differ diff --git a/Sources/Stylophone/Assets/StoreLogo.scale-200.png b/Sources/Stylophone/Assets/StoreLogo.scale-200.png index 6b417827..f2e830a2 100644 Binary files a/Sources/Stylophone/Assets/StoreLogo.scale-200.png and b/Sources/Stylophone/Assets/StoreLogo.scale-200.png differ diff --git a/Sources/Stylophone/Assets/StoreLogo.scale-400.png b/Sources/Stylophone/Assets/StoreLogo.scale-400.png index 93ee97d6..ead544af 100644 Binary files a/Sources/Stylophone/Assets/StoreLogo.scale-400.png and b/Sources/Stylophone/Assets/StoreLogo.scale-400.png differ diff --git a/Sources/Stylophone/Assets/Wide310x150Logo.scale-100.png b/Sources/Stylophone/Assets/Wide310x150Logo.scale-100.png index 35d2f001..b4065bfe 100644 Binary files a/Sources/Stylophone/Assets/Wide310x150Logo.scale-100.png and b/Sources/Stylophone/Assets/Wide310x150Logo.scale-100.png differ diff --git a/Sources/Stylophone/Assets/Wide310x150Logo.scale-125.png b/Sources/Stylophone/Assets/Wide310x150Logo.scale-125.png index 4b1a4a3c..2a1525b8 100644 Binary files a/Sources/Stylophone/Assets/Wide310x150Logo.scale-125.png and b/Sources/Stylophone/Assets/Wide310x150Logo.scale-125.png differ diff --git a/Sources/Stylophone/Assets/Wide310x150Logo.scale-150.png b/Sources/Stylophone/Assets/Wide310x150Logo.scale-150.png index 115ce017..8e83edcb 100644 Binary files a/Sources/Stylophone/Assets/Wide310x150Logo.scale-150.png and b/Sources/Stylophone/Assets/Wide310x150Logo.scale-150.png differ diff --git a/Sources/Stylophone/Assets/Wide310x150Logo.scale-200.png b/Sources/Stylophone/Assets/Wide310x150Logo.scale-200.png index b1bc16be..d170ce44 100644 Binary files a/Sources/Stylophone/Assets/Wide310x150Logo.scale-200.png and b/Sources/Stylophone/Assets/Wide310x150Logo.scale-200.png differ diff --git a/Sources/Stylophone/Assets/Wide310x150Logo.scale-400.png b/Sources/Stylophone/Assets/Wide310x150Logo.scale-400.png index a93e2339..8a197d3b 100644 Binary files a/Sources/Stylophone/Assets/Wide310x150Logo.scale-400.png and b/Sources/Stylophone/Assets/Wide310x150Logo.scale-400.png differ diff --git a/Sources/Stylophone/Behaviors/StackedNotificationsBehavior.cs b/Sources/Stylophone/Behaviors/StackedNotificationsBehavior.cs new file mode 100644 index 00000000..af7efe39 --- /dev/null +++ b/Sources/Stylophone/Behaviors/StackedNotificationsBehavior.cs @@ -0,0 +1,322 @@ +// 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/Behaviors/TreeViewCollapseBehavior.cs b/Sources/Stylophone/Behaviors/TreeViewCollapseBehavior.cs index 3946a7e9..791caaa6 100644 --- a/Sources/Stylophone/Behaviors/TreeViewCollapseBehavior.cs +++ b/Sources/Stylophone/Behaviors/TreeViewCollapseBehavior.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Windows.Input; -using Microsoft.Toolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Input; using Microsoft.Xaml.Interactivity; using WinUI = Microsoft.UI.Xaml.Controls; diff --git a/Sources/Stylophone/Converters/EnumToBooleanConverter.cs b/Sources/Stylophone/Converters/EnumToBooleanConverter.cs deleted file mode 100644 index cf0fad5c..00000000 --- a/Sources/Stylophone/Converters/EnumToBooleanConverter.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Stylophone.Localization.Strings; -using System; - -using Windows.UI.Xaml.Data; - -namespace Stylophone.Helpers -{ - public class EnumToBooleanConverter : IValueConverter - { - public Type EnumType { get; set; } - - public object Convert(object value, Type targetType, object parameter, string language) - { - if (parameter is string enumString) - { - if (!Enum.IsDefined(EnumType, value)) - { - throw new ArgumentException(Resources.ExceptionEnumToBooleanConverterValueMustBeAnEnum); - } - - var enumValue = Enum.Parse(EnumType, enumString); - - return enumValue.Equals(value); - } - - throw new ArgumentException(Resources.ExceptionEnumToBooleanConverterParameterMustBeAnEnumName); - } - - public object ConvertBack(object value, Type targetType, object parameter, string language) - { - if (parameter is string enumString) - { - return Enum.Parse(EnumType, enumString); - } - - throw new ArgumentException(Resources.ExceptionEnumToBooleanConverterParameterMustBeAnEnumName); - } - } -} diff --git a/Sources/Stylophone/Converters/ThemeToIntConverter.cs b/Sources/Stylophone/Converters/ThemeToIntConverter.cs new file mode 100644 index 00000000..2fef6770 --- /dev/null +++ b/Sources/Stylophone/Converters/ThemeToIntConverter.cs @@ -0,0 +1,32 @@ +using Stylophone.Common.Interfaces; +using Stylophone.Localization.Strings; +using System; + +using Windows.UI.Xaml.Data; + +namespace Stylophone.Helpers +{ + public class ThemeToIntConverter : IValueConverter + { + + public object Convert(object value, Type targetType, object parameter, string language) + { + + if (value is Theme t) + return (int)t; + + throw new ArgumentException("Not a Theme"); + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) + { + if (value is int enumInt) + { + // Cast enumInt to EnumType + return Enum.ToObject(typeof(Theme), enumInt); + } + + throw new ArgumentException("Not an int"); + } + } +} diff --git a/Sources/Stylophone/Helpers/AlternatingRowListView.cs b/Sources/Stylophone/Helpers/AlternatingRowListView.cs deleted file mode 100644 index 8c7df838..00000000 --- a/Sources/Stylophone/Helpers/AlternatingRowListView.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Windows.UI.Xaml.Controls; -using Windows.UI.Xaml; -using Windows.UI.Xaml.Media; - -namespace Stylophone.Helpers -{ - public class AlternatingRowListView : ListView - { - public static readonly DependencyProperty OddRowBackgroundProperty = - DependencyProperty.Register("OddRowBackground", typeof(Brush), typeof(AlternatingRowListView), null); - public Brush OddRowBackground - { - get { return (Brush)GetValue(OddRowBackgroundProperty); } - set { SetValue(OddRowBackgroundProperty, (Brush)value); } - } - - public static readonly DependencyProperty EvenRowBackgroundProperty = - DependencyProperty.Register("EvenRowBackground", typeof(Brush), typeof(AlternatingRowListView), null); - public Brush EvenRowBackground - { - get { return (Brush)GetValue(EvenRowBackgroundProperty); } - set { SetValue(EvenRowBackgroundProperty, (Brush)value); } - } - - protected override void PrepareContainerForItemOverride(DependencyObject element, object item) - { - base.PrepareContainerForItemOverride(element, item); - var listViewItem = element as ListViewItem; - - if (listViewItem != null) - { - var index = IndexFromContainer(element); - - if ((index + 1) % 2 == 1) - { - listViewItem.Background = OddRowBackground; - } - else - { - listViewItem.Background = EvenRowBackground; - } - - // Force the background to be repainted - listViewItem.UpdateLayout(); - } - - } - } -} diff --git a/Sources/Stylophone/Package.appxmanifest b/Sources/Stylophone/Package.appxmanifest index f43639de..4f4a9d4b 100644 --- a/Sources/Stylophone/Package.appxmanifest +++ b/Sources/Stylophone/Package.appxmanifest @@ -12,7 +12,7 @@ + Version="2.5.0.0" /> diff --git a/Sources/Stylophone/Package.tt b/Sources/Stylophone/Package.tt index 32499621..a2a9d648 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.2.0.0"; + string version = "2.5.0.0"; // Get configuration name at Build time string configName = Host.ResolveParameterValue("-", "-", "BuildConfiguration"); diff --git a/Sources/Stylophone/Services/ActivationService.cs b/Sources/Stylophone/Services/ActivationService.cs index 56935afc..9bac2ba9 100644 --- a/Sources/Stylophone/Services/ActivationService.cs +++ b/Sources/Stylophone/Services/ActivationService.cs @@ -2,7 +2,7 @@ using System.Threading.Tasks; using Stylophone.Activation; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Stylophone.Common.Interfaces; using Stylophone.Common.Services; using Stylophone.Common.ViewModels; diff --git a/Sources/Stylophone/Services/NavigationService.cs b/Sources/Stylophone/Services/NavigationService.cs index c6423561..43e49767 100644 --- a/Sources/Stylophone/Services/NavigationService.cs +++ b/Sources/Stylophone/Services/NavigationService.cs @@ -1,6 +1,6 @@ using Stylophone.ViewModels; using Stylophone.Views; -using Microsoft.Toolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.Toolkit.Uwp.UI.Animations; using Stylophone.Common.Interfaces; using Stylophone.Common.ViewModels; diff --git a/Sources/Stylophone/Services/NotificationService.cs b/Sources/Stylophone/Services/NotificationService.cs index 1b7c680b..ddec9317 100644 --- a/Sources/Stylophone/Services/NotificationService.cs +++ b/Sources/Stylophone/Services/NotificationService.cs @@ -1,28 +1,21 @@ using System; -using System.Threading.Tasks; - +using CommunityToolkit.Mvvm.Messaging.Messages; using Microsoft.Toolkit.Uwp.Notifications; -using Stylophone.Common.Interfaces; -using Windows.ApplicationModel.Activation; using Windows.UI.Notifications; +using Stylophone.Common.Interfaces; +using CommunityToolkit.Mvvm.Messaging; namespace Stylophone.Services { + public class NotificationService : NotificationServiceBase { - private IDispatcherService _dispatcherService; - - public NotificationService(IDispatcherService dispatcherService) + + public override void ShowInAppNotification(InAppNotification notificationObject) { - _dispatcherService = dispatcherService; + WeakReferenceMessenger.Default.Send(notificationObject); } - - public override void ShowInAppNotification(string notification, bool autoHide) - { - //TODO: check for compact mode - InvokeInAppNotificationRequested(new InAppNotificationRequestedEventArgs { NotificationText = notification, NotificationTime = autoHide ? 1500 : 0}); - } - + public override void ShowBasicToastNotification(string title, string description) { // Create the toast content diff --git a/Sources/Stylophone/Styles/StyloResources.xaml b/Sources/Stylophone/Styles/StyloResources.xaml index 4c24551e..aa34e0c3 100644 --- a/Sources/Stylophone/Styles/StyloResources.xaml +++ b/Sources/Stylophone/Styles/StyloResources.xaml @@ -13,10 +13,20 @@ - + + + + + + + + + + + diff --git a/Sources/Stylophone/Stylophone.csproj b/Sources/Stylophone/Stylophone.csproj index 890005a1..89186d51 100644 --- a/Sources/Stylophone/Stylophone.csproj +++ b/Sources/Stylophone/Stylophone.csproj @@ -2,7 +2,7 @@ - 8.0 + 9.0 Debug x86 {5A5CE693-A67D-4B28-8DCE-4B0EA3A47533} @@ -28,11 +28,11 @@ Always x86|x64|arm 0 - BA4DC65DC8E7D8306C0DA23E0768EB312CC64ED6 + AC6B4D78B6FC1C9719183572B36F08C049AFFB1E True DXFeatureLevel - Language=en;fr;pt + Language=en;fr;pt;zh @@ -140,16 +140,19 @@ - 4.5.0 + 4.5.3 - 4.5.0 + 4.5.3 6.0.0 - 6.2.13 + 6.2.14 + + + 10.1901.28001 7.1.2 @@ -167,19 +170,19 @@ 7.1.2 - 2.7.0 + 2.7.3 2.0.1 - 1.3.0 + 1.4.0 - 2.80.2 + 2.88.1 - 3.3.1 + 3.3.2 1.1.0 @@ -191,6 +194,7 @@ + SettingsBlockControl.xaml @@ -200,9 +204,8 @@ - - + @@ -437,6 +440,11 @@ false + + + Microsoft Engagement Framework + + - + - + diff --git a/Sources/Stylophone/Views/LibraryDetailPage.xaml.cs b/Sources/Stylophone/Views/LibraryDetailPage.xaml.cs index 75aa38cf..401ab77f 100644 --- a/Sources/Stylophone/Views/LibraryDetailPage.xaml.cs +++ b/Sources/Stylophone/Views/LibraryDetailPage.xaml.cs @@ -4,7 +4,7 @@ using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; using Stylophone.Common.ViewModels; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Stylophone.Common.Interfaces; namespace Stylophone.Views @@ -45,7 +45,7 @@ protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) private void Queue_Track(object sender, Windows.UI.Xaml.Input.DoubleTappedRoutedEventArgs e) { - var listView = sender as AlternatingRowListView; + var listView = sender as ListView; var trackVm = listView.SelectedItem as TrackViewModel; trackVm.AddToQueueCommand.Execute(trackVm.File); } diff --git a/Sources/Stylophone/Views/LibraryPage.xaml b/Sources/Stylophone/Views/LibraryPage.xaml index 6f7236cf..c101bd1e 100644 --- a/Sources/Stylophone/Views/LibraryPage.xaml +++ b/Sources/Stylophone/Views/LibraryPage.xaml @@ -125,7 +125,7 @@ IsEnabled="{Binding IsDetailLoading, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}" Text="{x:Bind strings:Resources.ContextMenuAddToQueue}" /> - - @@ -498,6 +497,7 @@ @@ -582,7 +582,7 @@ - + @@ -607,7 +607,7 @@ - + diff --git a/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml.cs b/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml.cs index e4d4f010..bbf7bd6b 100644 --- a/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml.cs +++ b/Sources/Stylophone/Views/Playback/NowPlayingBar.xaml.cs @@ -5,7 +5,7 @@ using Stylophone.Common.Helpers; using Stylophone.Common.Services; using Stylophone.Common.Interfaces; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; namespace Stylophone.Views { diff --git a/Sources/Stylophone/Views/Playback/OverlayView.xaml.cs b/Sources/Stylophone/Views/Playback/OverlayView.xaml.cs index 5a2bb016..738c6d10 100644 --- a/Sources/Stylophone/Views/Playback/OverlayView.xaml.cs +++ b/Sources/Stylophone/Views/Playback/OverlayView.xaml.cs @@ -1,5 +1,5 @@ using Stylophone.ViewModels; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Microsoft.Toolkit.Uwp; using Stylophone.Common.Helpers; using System; diff --git a/Sources/Stylophone/Views/PlaylistPage.xaml b/Sources/Stylophone/Views/PlaylistPage.xaml index 1577b2c6..167c5738 100644 --- a/Sources/Stylophone/Views/PlaylistPage.xaml +++ b/Sources/Stylophone/Views/PlaylistPage.xaml @@ -250,7 +250,7 @@ - - + - + - + diff --git a/Sources/Stylophone/Views/PlaylistPage.xaml.cs b/Sources/Stylophone/Views/PlaylistPage.xaml.cs index ab9c81e0..e3963d9f 100644 --- a/Sources/Stylophone/Views/PlaylistPage.xaml.cs +++ b/Sources/Stylophone/Views/PlaylistPage.xaml.cs @@ -1,5 +1,5 @@ using Stylophone.Helpers; -using Microsoft.Toolkit.Mvvm.DependencyInjection; +using CommunityToolkit.Mvvm.DependencyInjection; using Stylophone.Common.ViewModels; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Navigation; @@ -22,9 +22,15 @@ protected override async void OnNavigatedTo(NavigationEventArgs e) await ViewModel.LoadDataAsync(e.Parameter as string); } + protected override void OnNavigatedFrom(NavigationEventArgs e) + { + base.OnNavigatedFrom(e); + ViewModel.Dispose(); + } + private void Queue_Track(object sender, Windows.UI.Xaml.Input.DoubleTappedRoutedEventArgs e) { - var listView = sender as AlternatingRowListView; + var listView = sender as ListView; var trackVm = listView.SelectedItem as TrackViewModel; trackVm.AddToQueueCommand.Execute(trackVm.File); } diff --git a/Sources/Stylophone/Views/SearchResultsPage.xaml b/Sources/Stylophone/Views/SearchResultsPage.xaml index b0b5be8c..08eb1c45 100644 --- a/Sources/Stylophone/Views/SearchResultsPage.xaml +++ b/Sources/Stylophone/Views/SearchResultsPage.xaml @@ -43,19 +43,18 @@ BorderThickness="1" CornerRadius="8"> - - + @@ -83,9 +82,9 @@ - + - + - - + - - + + - + + @@ -25,16 +23,11 @@ - - - - - - - + + 1 + 0 - @@ -179,39 +172,13 @@ - - - - - - - Light - - - - - Dark - - - - - Default - - - - + + + + + + + @@ -220,24 +187,13 @@ - + + + + + + - - - - - - @@ -270,10 +226,17 @@ Text="{x:Bind strings:Resources.SettingsAbout}" /> + + + + + + + @@ -22,23 +22,9 @@ - - - - - - - - - + + + + + + diff --git a/Sources/Stylophone/Views/ShellPage.xaml.cs b/Sources/Stylophone/Views/ShellPage.xaml.cs index 3cc2a1d1..236829f2 100644 --- a/Sources/Stylophone/Views/ShellPage.xaml.cs +++ b/Sources/Stylophone/Views/ShellPage.xaml.cs @@ -36,7 +36,7 @@ public ShellPage() { InitializeComponent(); DataContext = ((App)Application.Current).Services.GetService(typeof(ShellViewModel)); - ViewModel.Initialize(shellFrame, navigationView, playlistContainer, notificationHolder, KeyboardAccelerators); + ViewModel.Initialize(shellFrame, navigationView, playlistContainer, stackedNotificationBehavior, KeyboardAccelerators); // Hide default title bar. var coreTitleBar = CoreApplication.GetCurrentView().TitleBar; diff --git a/icon.png b/icon.png index 4b5d9ebd..dae7f9b4 100644 Binary files a/icon.png and b/icon.png differ diff --git a/icon.svg b/icon.svg index 304cde1c..9714a4cb 100644 --- a/icon.svg +++ b/icon.svg @@ -1,2022 +1 @@ - -