diff --git a/Trdo/App.xaml.cs b/Trdo/App.xaml.cs
index 65d57c5..0ca9768 100644
--- a/Trdo/App.xaml.cs
+++ b/Trdo/App.xaml.cs
@@ -11,7 +11,6 @@
using Trdo.Pages;
using Trdo.Services;
using Trdo.ViewModels;
-using Windows.UI;
using Windows.UI.ViewManagement;
using WinUIEx;
@@ -157,16 +156,36 @@ private void InitializeTrayIcon()
private void TrayIcon_ContextMenu(TrayIcon sender, TrayIconEventArgs args)
{
- args.Flyout = CreateFlyout();
+ if (SettingsService.TrayClickBehavior == 1)
+ {
+ // Swapped: right click plays/pauses (fall back to flyout if no station selected)
+ if (_playerVm.CanPlay)
+ {
+ _playerVm.Toggle();
+ _ = UpdateTrayIconAsync();
+ return;
+ }
+ }
+
+ // Default: right click opens flyout; also fallback when no station is available
+ ShowFlyout(args);
}
private void TrayIcon_Selected(TrayIcon sender, TrayIconEventArgs args)
{
+ if (SettingsService.TrayClickBehavior == 1)
+ {
+ // Swapped: left click opens flyout
+ ShowFlyout(args);
+ return;
+ }
+
+ // Default: left click plays/pauses
// Check if we can play (have stations available and one selected)
if (!_playerVm.CanPlay)
{
// No stations available, show the flyout to encourage user to add a station
- args.Flyout = CreateFlyout();
+ ShowFlyout(args);
return;
}
@@ -175,6 +194,12 @@ private void TrayIcon_Selected(TrayIcon sender, TrayIconEventArgs args)
_ = UpdateTrayIconAsync();
}
+ private void ShowFlyout(TrayIconEventArgs args)
+ {
+ WindowPlacementService.CapturePointerAnchor();
+ args.Flyout = CreateFlyout();
+ }
+
private Flyout CreateFlyout()
{
Flyout flyout = new()
@@ -190,6 +215,7 @@ private Flyout CreateFlyout()
flyout.Opened += (s, e) =>
{
+ WindowPlacementService.CapturePointerAnchor();
// Clear the back stack when flyout opens to prevent accumulation
Services.NavigationService.Instance.ClearBackStack();
};
@@ -272,7 +298,7 @@ private void UpdatePlayPauseCommandText()
}
else if (_playerVm.IsPlaying)
{
-
+
// Include now playing info if available
if (_playerVm.HasNowPlaying)
{
diff --git a/Trdo/Assets/apple_music.svg b/Trdo/Assets/apple_music.svg
new file mode 100644
index 0000000..6b5affd
--- /dev/null
+++ b/Trdo/Assets/apple_music.svg
@@ -0,0 +1,5 @@
+
diff --git a/Trdo/Assets/discogs.svg b/Trdo/Assets/discogs.svg
new file mode 100644
index 0000000..957eeec
--- /dev/null
+++ b/Trdo/Assets/discogs.svg
@@ -0,0 +1,42 @@
+
+
diff --git a/Trdo/Assets/ytmusic.svg b/Trdo/Assets/ytmusic.svg
new file mode 100644
index 0000000..f9df22a
--- /dev/null
+++ b/Trdo/Assets/ytmusic.svg
@@ -0,0 +1,12 @@
+
diff --git a/Trdo/Controls/ManualStationWindow.xaml b/Trdo/Controls/ManualStationWindow.xaml
new file mode 100644
index 0000000..b222264
--- /dev/null
+++ b/Trdo/Controls/ManualStationWindow.xaml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Trdo/Controls/ManualStationWindow.xaml.cs b/Trdo/Controls/ManualStationWindow.xaml.cs
new file mode 100644
index 0000000..2c7cc49
--- /dev/null
+++ b/Trdo/Controls/ManualStationWindow.xaml.cs
@@ -0,0 +1,60 @@
+using Trdo.Models;
+using Trdo.Services;
+using Trdo.ViewModels;
+using WinUIEx;
+
+namespace Trdo.Controls;
+
+///
+/// A small standalone window for manually adding or editing a radio station.
+/// Opens as a pop-out window so that closing the tray flyout does not clear the form fields.
+///
+public sealed partial class ManualStationWindow : WindowEx
+{
+ public AddStationViewModel ViewModel { get; }
+
+ public ManualStationWindow()
+ {
+ InitializeComponent();
+
+ ViewModel = new AddStationViewModel();
+ ViewModel.SetPlayerViewModel(PlayerViewModel.Shared);
+
+ ExtendsContentIntoTitleBar = true;
+ SetTitleBar(ModernTitlebar);
+
+ Activated += ManualStationWindow_Activated;
+ }
+
+ ///
+ /// Opens the window pre-filled with the given station's data for editing.
+ ///
+ public void LoadStationForEdit(RadioStation station)
+ {
+ ViewModel.LoadStationForEdit(station);
+ }
+
+ private void ManualStationWindow_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
+ {
+ // Focus the station name field once the window is ready
+ if (args.WindowActivationState != Microsoft.UI.Xaml.WindowActivationState.Deactivated)
+ {
+ WindowPlacementService.PositionWindowNearAnchor(this, 400, 500);
+ StationNameTextBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
+ Activated -= ManualStationWindow_Activated;
+ }
+ }
+
+ private void SaveButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ if (ViewModel.Save())
+ {
+ Close();
+ }
+ }
+
+ private void CancelButton_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
+ {
+ Close();
+ }
+}
diff --git a/Trdo/Controls/TutorialWindow.xaml.cs b/Trdo/Controls/TutorialWindow.xaml.cs
index 0f8327f..758c27c 100644
--- a/Trdo/Controls/TutorialWindow.xaml.cs
+++ b/Trdo/Controls/TutorialWindow.xaml.cs
@@ -13,6 +13,16 @@ public TutorialWindow()
ExtendsContentIntoTitleBar = true;
SetTitleBar(ModernTitlebar);
+ Activated += TutorialWindow_Activated;
+ }
+
+ private void TutorialWindow_Activated(object sender, Microsoft.UI.Xaml.WindowActivatedEventArgs args)
+ {
+ if (args.WindowActivationState == Microsoft.UI.Xaml.WindowActivationState.Deactivated)
+ return;
+
+ WindowPlacementService.PositionWindowNearAnchor(this, 400, 600);
+ Activated -= TutorialWindow_Activated;
}
private void Button_Click(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
diff --git a/Trdo/Package.appxmanifest b/Trdo/Package.appxmanifest
index 09ba118..acd1508 100644
--- a/Trdo/Package.appxmanifest
+++ b/Trdo/Package.appxmanifest
@@ -11,7 +11,7 @@
+ Version="1.9.1.0" />
diff --git a/Trdo/Pages/FavoritesPage.xaml b/Trdo/Pages/FavoritesPage.xaml
index 591b8c1..d8cc599 100644
--- a/Trdo/Pages/FavoritesPage.xaml
+++ b/Trdo/Pages/FavoritesPage.xaml
@@ -31,7 +31,9 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="12"
- Visibility="{x:Bind ViewModel.Favorites.Count, Mode=OneWay, Converter={StaticResource CountToEmptyStateVisibilityConverter}}">
+ Visibility="{x:Bind ViewModel.Favorites.Count,
+ Mode=OneWay,
+ Converter={StaticResource CountToEmptyStateVisibilityConverter}}">
+ Visibility="{x:Bind ViewModel.Favorites.Count,
+ Mode=OneWay,
+ Converter={StaticResource CountToHasItemsVisibilityConverter}}">
+
+
+
+
@@ -22,198 +51,360 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
diff --git a/Trdo/Pages/SettingsPage.xaml.cs b/Trdo/Pages/SettingsPage.xaml.cs
index f159cb9..4048468 100644
--- a/Trdo/Pages/SettingsPage.xaml.cs
+++ b/Trdo/Pages/SettingsPage.xaml.cs
@@ -13,6 +13,7 @@ public sealed partial class SettingsPage : Page
private static extern nint GetActiveWindow();
private float _displayLevel;
+ private bool _isUpdatingAutoPlayToggle;
public SettingsViewModel ViewModel { get; }
@@ -133,4 +134,49 @@ private async void ExportButton_Click(object sender, RoutedEventArgs e)
ImportExportInfoBar.IsOpen = true;
}
}
+
+ private void AutoPlayOnStartupToggle_Toggled(object sender, RoutedEventArgs e)
+ {
+ if (_isUpdatingAutoPlayToggle)
+ {
+ return;
+ }
+
+ bool isEnabled = ViewModel.IsAutoPlayOnStartupEnabled;
+ bool requestedState = AutoPlayOnStartupToggle.IsOn;
+
+ if (requestedState == isEnabled)
+ {
+ if (!requestedState)
+ {
+ AutoPlayWarningInfoBar.IsOpen = false;
+ }
+
+ return;
+ }
+
+ if (!requestedState)
+ {
+ ViewModel.IsAutoPlayOnStartupEnabled = false;
+ AutoPlayWarningInfoBar.IsOpen = false;
+ return;
+ }
+
+ SetAutoPlayToggle(false);
+ AutoPlayWarningInfoBar.IsOpen = true;
+ }
+
+ private void ConfirmAutoPlayButton_Click(object sender, RoutedEventArgs e)
+ {
+ ViewModel.IsAutoPlayOnStartupEnabled = true;
+ SetAutoPlayToggle(true);
+ AutoPlayWarningInfoBar.IsOpen = false;
+ }
+
+ private void SetAutoPlayToggle(bool value)
+ {
+ _isUpdatingAutoPlayToggle = true;
+ AutoPlayOnStartupToggle.IsOn = value;
+ _isUpdatingAutoPlayToggle = false;
+ }
}
diff --git a/Trdo/Services/MusicSearchLinkService.cs b/Trdo/Services/MusicSearchLinkService.cs
new file mode 100644
index 0000000..3b04137
--- /dev/null
+++ b/Trdo/Services/MusicSearchLinkService.cs
@@ -0,0 +1,29 @@
+using System;
+using System.Threading.Tasks;
+using Windows.System;
+using Windows.System.UserProfile;
+
+namespace Trdo.Services;
+
+internal static class MusicSearchLinkService
+{
+ private const string DefaultAppleMusicStorefront = "us";
+
+ public static Uri CreateAppleMusicSearchUri(string searchText)
+ {
+ string storefront = GlobalizationPreferences.HomeGeographicRegion;
+ if (string.IsNullOrWhiteSpace(storefront) || storefront.Length != 2)
+ {
+ storefront = DefaultAppleMusicStorefront;
+ }
+
+ string encodedSearchText = Uri.EscapeDataString(searchText);
+ return new Uri($"https://music.apple.com/{storefront.ToLowerInvariant()}/search?term={encodedSearchText}");
+ }
+
+ public static async Task LaunchAppleMusicWebSearchAsync(string searchText)
+ {
+ Uri webSearchUri = CreateAppleMusicSearchUri(searchText);
+ await Launcher.LaunchUriAsync(webSearchUri);
+ }
+}
diff --git a/Trdo/Services/SettingsService.cs b/Trdo/Services/SettingsService.cs
index a16f265..d8e4724 100644
--- a/Trdo/Services/SettingsService.cs
+++ b/Trdo/Services/SettingsService.cs
@@ -1,3 +1,4 @@
+using System;
using Windows.Storage;
namespace Trdo.Services;
@@ -9,6 +10,53 @@ public static class SettingsService
{
private const string IsFirstRunKey = "IsFirstRun";
private const string IsVolumeSliderVisibleKey = "IsVolumeSliderVisible";
+ private const string AutoPlayOnStartupKey = "AutoPlayOnStartup";
+ private const string IsSpotifyEnabledKey = "IsSpotifyEnabled";
+ private const string IsDiscogsEnabledKey = "IsDiscogsEnabled";
+ private const string IsAppleMusicEnabledKey = "IsAppleMusicEnabled";
+ private const string IsYouTubeMusicEnabledKey = "IsYouTubeMusicEnabled";
+ private const string TrayClickBehaviorKey = "TrayClickBehavior";
+
+ public static event EventHandler? MusicSearchServicesChanged;
+
+ ///
+ /// Gets or sets whether the app should automatically start playing the last selected station on startup.
+ /// Defaults to false when no saved value exists.
+ ///
+ public static bool AutoPlayOnStartup
+ {
+ get
+ {
+ try
+ {
+ if (ApplicationData.Current.LocalSettings.Values.TryGetValue(AutoPlayOnStartupKey, out object? value))
+ {
+ return value switch
+ {
+ bool b => b,
+ string s when bool.TryParse(s, out bool b2) => b2,
+ _ => false
+ };
+ }
+ return false;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ set
+ {
+ try
+ {
+ ApplicationData.Current.LocalSettings.Values[AutoPlayOnStartupKey] = value;
+ }
+ catch
+ {
+ // Silently fail if unable to save
+ }
+ }
+ }
///
/// Gets or sets whether the volume slider is visible on the playing page.
@@ -49,6 +97,126 @@ string s when bool.TryParse(s, out bool b2) => b2,
}
}
+ ///
+ /// Gets or sets whether Spotify search links are shown.
+ /// Defaults to true when no saved value exists.
+ ///
+ public static bool IsSpotifyEnabled
+ {
+ get => GetBoolSetting(IsSpotifyEnabledKey, defaultValue: true);
+ set => SetBoolSetting(IsSpotifyEnabledKey, value);
+ }
+
+ ///
+ /// Gets or sets whether Discogs search links are shown.
+ /// Defaults to true when no saved value exists.
+ ///
+ public static bool IsDiscogsEnabled
+ {
+ get => GetBoolSetting(IsDiscogsEnabledKey, defaultValue: true);
+ set => SetBoolSetting(IsDiscogsEnabledKey, value);
+ }
+
+ ///
+ /// Gets or sets whether Apple Music search links are shown.
+ /// Defaults to true when no saved value exists.
+ ///
+ public static bool IsAppleMusicEnabled
+ {
+ get => GetBoolSetting(IsAppleMusicEnabledKey, defaultValue: false);
+ set => SetBoolSetting(IsAppleMusicEnabledKey, value);
+ }
+
+ ///
+ /// Gets or sets whether YouTube Music search links are shown.
+ /// Defaults to true when no saved value exists.
+ ///
+ public static bool IsYouTubeMusicEnabled
+ {
+ get => GetBoolSetting(IsYouTubeMusicEnabledKey, defaultValue: false);
+ set => SetBoolSetting(IsYouTubeMusicEnabledKey, value);
+ }
+
+ ///
+ /// Gets or sets the tray icon click behavior.
+ /// 0 = left click plays/pauses, right click opens flyout (default).
+ /// 1 = left click opens flyout, right click plays/pauses.
+ ///
+ public static int TrayClickBehavior
+ {
+ get
+ {
+ try
+ {
+ if (ApplicationData.Current.LocalSettings.Values.TryGetValue(TrayClickBehaviorKey, out object? value))
+ {
+ return value switch
+ {
+ int i => i,
+ string s when int.TryParse(s, out int i2) => i2,
+ _ => 0
+ };
+ }
+ return 0;
+ }
+ catch
+ {
+ return 0;
+ }
+ }
+ set
+ {
+ try
+ {
+ // Only valid values are 0 (default) and 1 (swapped)
+ if (value < 0 || value > 1)
+ value = 0;
+ ApplicationData.Current.LocalSettings.Values[TrayClickBehaviorKey] = value;
+ }
+ catch
+ {
+ // Silently fail if unable to save
+ }
+ }
+ }
+
+ private static bool GetBoolSetting(string key, bool defaultValue)
+ {
+ try
+ {
+ if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out object? value))
+ {
+ return value switch
+ {
+ bool b => b,
+ string s when bool.TryParse(s, out bool b2) => b2,
+ _ => defaultValue
+ };
+ }
+ return defaultValue;
+ }
+ catch
+ {
+ return defaultValue;
+ }
+ }
+
+ private static void SetBoolSetting(string key, bool value)
+ {
+ try
+ {
+ ApplicationData.Current.LocalSettings.Values[key] = value;
+ if (key is IsSpotifyEnabledKey or IsDiscogsEnabledKey or IsAppleMusicEnabledKey or IsYouTubeMusicEnabledKey)
+ {
+ MusicSearchServicesChanged?.Invoke(null, EventArgs.Empty);
+ }
+ }
+ catch
+ {
+ // Silently fail if unable to save
+ }
+ }
+
///
/// Gets whether this is the first run of the application
///
diff --git a/Trdo/Services/WindowPlacementService.cs b/Trdo/Services/WindowPlacementService.cs
new file mode 100644
index 0000000..10e7576
--- /dev/null
+++ b/Trdo/Services/WindowPlacementService.cs
@@ -0,0 +1,117 @@
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+using System.Runtime.InteropServices;
+using Windows.Graphics;
+using WinUIEx;
+
+namespace Trdo.Services;
+
+internal static partial class WindowPlacementService
+{
+ private const int WindowMargin = 12;
+ private static PointInt32? _lastAnchorPoint;
+
+ public static void CapturePointerAnchor()
+ {
+ if (GetCursorPos(out POINT point))
+ _lastAnchorPoint = new PointInt32(point.X, point.Y);
+ }
+
+ public static void PositionWindowNearAnchor(Window window, int width, int height)
+ {
+ PointInt32 anchor = GetAnchorPoint();
+ DisplayArea? displayArea = DisplayArea.GetFromPoint(anchor, DisplayAreaFallback.Nearest);
+ RectInt32 workArea = displayArea?.WorkArea ?? DisplayArea.Primary.WorkArea;
+
+ bool placeLeft = anchor.X >= workArea.X + (workArea.Width / 2);
+ bool placeAbove = anchor.Y >= workArea.Y + (workArea.Height / 2);
+
+ int x = placeLeft ? anchor.X - width - WindowMargin : anchor.X + WindowMargin;
+ int y = placeAbove ? anchor.Y - height - WindowMargin : anchor.Y + WindowMargin;
+
+ int maxX = System.Math.Max(workArea.X, workArea.X + workArea.Width - width);
+ int maxY = System.Math.Max(workArea.Y, workArea.Y + workArea.Height - height);
+
+ x = System.Math.Clamp(x, workArea.X, maxX);
+ y = System.Math.Clamp(y, workArea.Y, maxY);
+
+ window.MoveAndResize(x, y, width, height);
+ }
+
+ private static PointInt32 GetAnchorPoint()
+ {
+ if (_lastAnchorPoint is PointInt32 anchor)
+ return anchor;
+
+ if (TryGetTaskbarAnchorPoint(out anchor))
+ return anchor;
+
+ RectInt32 workArea = DisplayArea.Primary.WorkArea;
+ return new PointInt32(workArea.X + workArea.Width - WindowMargin, workArea.Y + workArea.Height - WindowMargin);
+ }
+
+ private static bool TryGetTaskbarAnchorPoint(out PointInt32 anchor)
+ {
+ APPBARDATA appBarData = new()
+ {
+ cbSize = Marshal.SizeOf()
+ };
+
+ if (SHAppBarMessage(ABM_GETTASKBARPOS, ref appBarData) == 0)
+ {
+ anchor = default;
+ return false;
+ }
+
+ anchor = appBarData.uEdge switch
+ {
+ ABE_BOTTOM => new PointInt32(appBarData.rc.Right - WindowMargin, appBarData.rc.Top + ((appBarData.rc.Bottom - appBarData.rc.Top) / 2)),
+ ABE_TOP => new PointInt32(appBarData.rc.Right - WindowMargin, appBarData.rc.Bottom - WindowMargin),
+ ABE_LEFT => new PointInt32(appBarData.rc.Right - WindowMargin, appBarData.rc.Bottom - WindowMargin),
+ ABE_RIGHT => new PointInt32(appBarData.rc.Left + WindowMargin, appBarData.rc.Bottom - WindowMargin),
+ _ => new PointInt32(appBarData.rc.Right - WindowMargin, appBarData.rc.Bottom - WindowMargin)
+ };
+
+ return true;
+ }
+
+ private const uint ABM_GETTASKBARPOS = 0x00000005;
+ private const uint ABE_LEFT = 0;
+ private const uint ABE_TOP = 1;
+ private const uint ABE_RIGHT = 2;
+ private const uint ABE_BOTTOM = 3;
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct POINT
+ {
+ public int X;
+ public int Y;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct RECT
+ {
+ public int Left;
+ public int Top;
+ public int Right;
+ public int Bottom;
+ }
+
+ [StructLayout(LayoutKind.Sequential)]
+ private struct APPBARDATA
+ {
+ public int cbSize;
+ public nint hWnd;
+ public uint uCallbackMessage;
+ public uint uEdge;
+ public RECT rc;
+ public nint lParam;
+ }
+
+ [LibraryImport("user32.dll")]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ private static partial bool GetCursorPos(out POINT point);
+
+ [LibraryImport("shell32.dll")]
+ private static partial uint SHAppBarMessage(uint dwMessage, ref APPBARDATA pData);
+}
diff --git a/Trdo/Trdo.csproj b/Trdo/Trdo.csproj
index a5f24ad..f74031a 100644
--- a/Trdo/Trdo.csproj
+++ b/Trdo/Trdo.csproj
@@ -18,8 +18,12 @@
+
+
+
+
@@ -55,10 +59,10 @@
-
-
-
-
+
+
+
+
@@ -77,9 +81,18 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
PreserveNewest
+
+ PreserveNewest
+
PreserveNewest
@@ -119,6 +132,11 @@
MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
MSBuild:Compile
diff --git a/Trdo/ViewModels/NowPlayingViewModel.cs b/Trdo/ViewModels/NowPlayingViewModel.cs
index a4e3465..fab3047 100644
--- a/Trdo/ViewModels/NowPlayingViewModel.cs
+++ b/Trdo/ViewModels/NowPlayingViewModel.cs
@@ -40,6 +40,7 @@ public NowPlayingViewModel()
{
// Subscribe to metadata changes for UI updates
_player.StreamMetadataChanged += OnStreamMetadataChanged;
+ SettingsService.MusicSearchServicesChanged += OnMusicSearchServicesChanged;
// Subscribe to favorites changes to update UI
_favoritesService.FavoritesChanged += (_, _) =>
@@ -53,6 +54,15 @@ public NowPlayingViewModel()
Debug.WriteLine($"[NowPlayingViewModel] Initialized with {PlaylistHistory.Count} history items from service");
}
+ private void OnMusicSearchServicesChanged(object? sender, EventArgs e)
+ {
+ OnPropertyChanged(nameof(IsSpotifyEnabled));
+ OnPropertyChanged(nameof(IsDiscogsEnabled));
+ OnPropertyChanged(nameof(IsAppleMusicEnabled));
+ OnPropertyChanged(nameof(IsYouTubeMusicEnabled));
+ OnPropertyChanged(nameof(HasEnabledMusicServices));
+ }
+
private void OnStreamMetadataChanged(object? sender, StreamMetadata metadata)
{
OnPropertyChanged(nameof(CurrentMetadata));
@@ -67,6 +77,11 @@ private void OnStreamMetadataChanged(object? sender, StreamMetadata metadata)
OnPropertyChanged(nameof(ShowRawStreamTitle));
OnPropertyChanged(nameof(DiscogsSearchQuery));
OnPropertyChanged(nameof(SpotifySearchQuery));
+ OnPropertyChanged(nameof(IsSpotifyEnabled));
+ OnPropertyChanged(nameof(IsDiscogsEnabled));
+ OnPropertyChanged(nameof(IsAppleMusicEnabled));
+ OnPropertyChanged(nameof(IsYouTubeMusicEnabled));
+ OnPropertyChanged(nameof(HasEnabledMusicServices));
// History is now managed by PlaylistHistoryService singleton
UpdateCurrentTrackFavoriteStatus();
@@ -234,6 +249,60 @@ private async Task OpenSpotifyWeb()
await Launcher.LaunchUriAsync(new Uri(webUrl));
}
+ ///
+ /// Opens Apple Music search with the current track information.
+ ///
+ public async Task SearchOnAppleMusic()
+ {
+ if (!HasMetadata)
+ return;
+
+ string searchText = DisplayText.Length > 0 ? DisplayText : StreamTitle;
+ await MusicSearchLinkService.LaunchAppleMusicWebSearchAsync(searchText);
+ }
+
+ ///
+ /// Opens YouTube Music search with the current track information.
+ ///
+ public async Task SearchOnYouTubeMusic()
+ {
+ if (!HasMetadata)
+ return;
+
+ string query = Uri.EscapeDataString(DisplayText.Length > 0 ? DisplayText : StreamTitle);
+ string url = $"https://music.youtube.com/search?q={query}";
+ await Launcher.LaunchUriAsync(new Uri(url));
+ }
+
+ ///
+ /// Gets whether Spotify search links should be shown.
+ ///
+ public bool IsSpotifyEnabled => SettingsService.IsSpotifyEnabled;
+
+ ///
+ /// Gets whether Discogs search links should be shown.
+ ///
+ public bool IsDiscogsEnabled => SettingsService.IsDiscogsEnabled;
+
+ ///
+ /// Gets whether Apple Music search links should be shown.
+ ///
+ public bool IsAppleMusicEnabled => SettingsService.IsAppleMusicEnabled;
+
+ ///
+ /// Gets whether YouTube Music search links should be shown.
+ ///
+ public bool IsYouTubeMusicEnabled => SettingsService.IsYouTubeMusicEnabled;
+
+ ///
+ /// Gets whether at least one music service search link should be shown.
+ ///
+ public bool HasEnabledMusicServices =>
+ IsSpotifyEnabled ||
+ IsDiscogsEnabled ||
+ IsAppleMusicEnabled ||
+ IsYouTubeMusicEnabled;
+
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
diff --git a/Trdo/ViewModels/PlayerViewModel.cs b/Trdo/ViewModels/PlayerViewModel.cs
index 53f21ff..614d335 100644
--- a/Trdo/ViewModels/PlayerViewModel.cs
+++ b/Trdo/ViewModels/PlayerViewModel.cs
@@ -108,6 +108,13 @@ public PlayerViewModel()
{
Debug.WriteLine($"[PlayerViewModel] Initializing stream with URL: {_selectedStation.StreamUrl}");
InitializeStream(_selectedStation.StreamUrl);
+
+ // Auto-play on startup if the setting is enabled
+ if (SettingsService.AutoPlayOnStartup)
+ {
+ Debug.WriteLine("[PlayerViewModel] AutoPlayOnStartup is enabled, starting playback...");
+ _player.Play();
+ }
}
else
{
diff --git a/Trdo/ViewModels/SettingsViewModel.cs b/Trdo/ViewModels/SettingsViewModel.cs
index 213852c..4eecf9e 100644
--- a/Trdo/ViewModels/SettingsViewModel.cs
+++ b/Trdo/ViewModels/SettingsViewModel.cs
@@ -20,6 +20,11 @@ public partial class SettingsViewModel : INotifyPropertyChanged
private string _startupToggleText = "Off";
private string _watchdogToggleText = "Off";
private string _autoBufferToggleText = "Off";
+ private string _autoPlayOnStartupToggleText = "Off";
+ private string _spotifyToggleText = "On";
+ private string _discogsToggleText = "On";
+ private string _appleMusicToggleText = "On";
+ private string _youtubeMusicToggleText = "On";
private StartupTask? _startupTask;
private bool _initDone;
@@ -27,7 +32,7 @@ public partial class SettingsViewModel : INotifyPropertyChanged
public SettingsViewModel()
{
- _playerViewModel = new PlayerViewModel();
+ _playerViewModel = PlayerViewModel.Shared;
// Subscribe to PlayerViewModel property changes
_playerViewModel.PropertyChanged += (_, args) =>
@@ -57,6 +62,11 @@ public SettingsViewModel()
// Initialize toggle text
WatchdogToggleText = GetToggleText(_playerViewModel.WatchdogEnabled);
AutoBufferToggleText = GetToggleText(_playerViewModel.AutoBufferIncreaseEnabled);
+ AutoPlayOnStartupToggleText = GetToggleText(SettingsService.AutoPlayOnStartup);
+ SpotifyToggleText = GetToggleText(SettingsService.IsSpotifyEnabled);
+ DiscogsToggleText = GetToggleText(SettingsService.IsDiscogsEnabled);
+ AppleMusicToggleText = GetToggleText(SettingsService.IsAppleMusicEnabled);
+ YouTubeMusicToggleText = GetToggleText(SettingsService.IsYouTubeMusicEnabled);
// Initialize startup task
_ = InitializeStartupTaskAsync();
@@ -101,6 +111,152 @@ public string StartupToggleText
}
}
+ ///
+ /// Gets or sets whether the app should automatically start playing the last selected station on startup.
+ ///
+ public bool IsAutoPlayOnStartupEnabled
+ {
+ get => SettingsService.AutoPlayOnStartup;
+ set
+ {
+ if (value == SettingsService.AutoPlayOnStartup) return;
+ SettingsService.AutoPlayOnStartup = value;
+ OnPropertyChanged();
+ AutoPlayOnStartupToggleText = GetToggleText(value);
+ }
+ }
+
+ public string AutoPlayOnStartupToggleText
+ {
+ get => _autoPlayOnStartupToggleText;
+ set
+ {
+ if (value == _autoPlayOnStartupToggleText) return;
+ _autoPlayOnStartupToggleText = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets whether Spotify search links are shown in Now Playing and Favorites.
+ ///
+ public bool IsSpotifyEnabled
+ {
+ get => SettingsService.IsSpotifyEnabled;
+ set
+ {
+ if (value == SettingsService.IsSpotifyEnabled) return;
+ SettingsService.IsSpotifyEnabled = value;
+ OnPropertyChanged();
+ SpotifyToggleText = GetToggleText(value);
+ }
+ }
+
+ public string SpotifyToggleText
+ {
+ get => _spotifyToggleText;
+ set
+ {
+ if (value == _spotifyToggleText) return;
+ _spotifyToggleText = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets whether Discogs search links are shown in Now Playing and Favorites.
+ ///
+ public bool IsDiscogsEnabled
+ {
+ get => SettingsService.IsDiscogsEnabled;
+ set
+ {
+ if (value == SettingsService.IsDiscogsEnabled) return;
+ SettingsService.IsDiscogsEnabled = value;
+ OnPropertyChanged();
+ DiscogsToggleText = GetToggleText(value);
+ }
+ }
+
+ public string DiscogsToggleText
+ {
+ get => _discogsToggleText;
+ set
+ {
+ if (value == _discogsToggleText) return;
+ _discogsToggleText = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets whether Apple Music search links are shown in Now Playing and Favorites.
+ ///
+ public bool IsAppleMusicEnabled
+ {
+ get => SettingsService.IsAppleMusicEnabled;
+ set
+ {
+ if (value == SettingsService.IsAppleMusicEnabled) return;
+ SettingsService.IsAppleMusicEnabled = value;
+ OnPropertyChanged();
+ AppleMusicToggleText = GetToggleText(value);
+ }
+ }
+
+ public string AppleMusicToggleText
+ {
+ get => _appleMusicToggleText;
+ set
+ {
+ if (value == _appleMusicToggleText) return;
+ _appleMusicToggleText = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets whether YouTube Music search links are shown in Now Playing and Favorites.
+ ///
+ public bool IsYouTubeMusicEnabled
+ {
+ get => SettingsService.IsYouTubeMusicEnabled;
+ set
+ {
+ if (value == SettingsService.IsYouTubeMusicEnabled) return;
+ SettingsService.IsYouTubeMusicEnabled = value;
+ OnPropertyChanged();
+ YouTubeMusicToggleText = GetToggleText(value);
+ }
+ }
+
+ public string YouTubeMusicToggleText
+ {
+ get => _youtubeMusicToggleText;
+ set
+ {
+ if (value == _youtubeMusicToggleText) return;
+ _youtubeMusicToggleText = value;
+ OnPropertyChanged();
+ }
+ }
+
+ ///
+ /// Gets or sets the tray icon click behavior.
+ /// 0 = left click plays/pauses, right click opens flyout (default).
+ /// 1 = left click opens flyout, right click plays/pauses.
+ ///
+ public int TrayClickBehavior
+ {
+ get => SettingsService.TrayClickBehavior;
+ set
+ {
+ if (value == SettingsService.TrayClickBehavior) return;
+ SettingsService.TrayClickBehavior = value;
+ OnPropertyChanged();
+ }
+ }
+
public bool IsWatchdogEnabled
{
get => _playerViewModel.WatchdogEnabled;