diff --git a/Trdo/App.xaml.cs b/Trdo/App.xaml.cs
index 65d57c5..a085ee6 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,6 +156,7 @@ private void InitializeTrayIcon()
private void TrayIcon_ContextMenu(TrayIcon sender, TrayIconEventArgs args)
{
+ WindowPlacementService.CapturePointerAnchor();
args.Flyout = CreateFlyout();
}
@@ -166,6 +166,7 @@ private void TrayIcon_Selected(TrayIcon sender, TrayIconEventArgs args)
if (!_playerVm.CanPlay)
{
// No stations available, show the flyout to encourage user to add a station
+ WindowPlacementService.CapturePointerAnchor();
args.Flyout = CreateFlyout();
return;
}
@@ -190,6 +191,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 +274,7 @@ private void UpdatePlayPauseCommandText()
}
else if (_playerVm.IsPlaying)
{
-
+
// Include now playing info if available
if (_playerVm.HasNowPlaying)
{
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/Pages/PlayingPage.xaml.cs b/Trdo/Pages/PlayingPage.xaml.cs
index 4da53e9..e7998b4 100644
--- a/Trdo/Pages/PlayingPage.xaml.cs
+++ b/Trdo/Pages/PlayingPage.xaml.cs
@@ -9,7 +9,6 @@
using Trdo.Models;
using Trdo.Services;
using Trdo.ViewModels;
-using WinUIEx;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -23,9 +22,9 @@ public sealed partial class PlayingPage : Page
private const int MinIndexForScrolling = 3;
private const string FilledStar = "\uE735";
private const string OutlineStar = "\uE734";
-
+
private readonly FavoritesService _favoritesService = FavoritesService.Instance;
-
+
public PlayerViewModel ViewModel { get; }
private ShellViewModel? _shellViewModel;
@@ -117,7 +116,7 @@ private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.Pro
UpdatePlayButtonState();
UpdateStationSelection();
- if (e.PropertyName == nameof(PlayerViewModel.CurrentMetadata) ||
+ if (e.PropertyName == nameof(PlayerViewModel.CurrentMetadata) ||
e.PropertyName == nameof(PlayerViewModel.HasNowPlaying))
{
UpdateFavoriteButtonState();
@@ -321,7 +320,7 @@ private void NowPlayingInfo_Click(object sender, RoutedEventArgs e)
private void FavoriteButton_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("[PlayingPage] Favorite button clicked");
-
+
if (ViewModel.CurrentMetadata?.HasMetadata != true)
{
Debug.WriteLine("[PlayingPage] No metadata to favorite");
@@ -331,7 +330,7 @@ private void FavoriteButton_Click(object sender, RoutedEventArgs e)
string stationName = ViewModel.SelectedStation?.Name ?? "Unknown Station";
bool isFavorited = _favoritesService.ToggleFavorite(ViewModel.CurrentMetadata, stationName);
Debug.WriteLine($"[PlayingPage] Track favorite toggled. IsFavorited: {isFavorited}");
-
+
// UpdateFavoriteButtonState will be called via the FavoritesChanged event
}
@@ -356,8 +355,12 @@ private void EditStation_Click(object sender, RoutedEventArgs e)
if (sender is MenuFlyoutItem menuItem && menuItem.Tag is RadioStation station)
{
Debug.WriteLine($"[PlayingPage] Edit station clicked: {station.Name}");
- // Navigate to AddStation page in edit mode with the station data
- _shellViewModel?.NavigateToAddStationPage(station);
+ // Open a pop-out window for editing so the flyout closing doesn't clear the fields
+ WindowPlacementService.CapturePointerAnchor();
+ ManualStationWindow editWindow = new();
+ WindowHelper.Track(editWindow);
+ editWindow.LoadStationForEdit(station);
+ editWindow.Activate();
}
}
diff --git a/Trdo/Pages/SearchStation.xaml.cs b/Trdo/Pages/SearchStation.xaml.cs
index ecc3ad6..070ae68 100644
--- a/Trdo/Pages/SearchStation.xaml.cs
+++ b/Trdo/Pages/SearchStation.xaml.cs
@@ -1,7 +1,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
-using System;
+using Trdo.Controls;
using Trdo.Models;
using Trdo.Services;
using Trdo.ViewModels;
@@ -139,8 +139,11 @@ private void AddStationButton_Click(object sender, RoutedEventArgs e)
private void ManualEntryButton_Click(object sender, RoutedEventArgs e)
{
StopPreview();
- // Navigate to manual entry page
- _shellViewModel?.NavigateToAddStationPage();
+ // Open a pop-out window for manual station entry so the flyout closing doesn't clear the fields
+ WindowPlacementService.CapturePointerAnchor();
+ ManualStationWindow addWindow = new();
+ WindowHelper.Track(addWindow);
+ addWindow.Activate();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
diff --git a/Trdo/Services/WindowPlacementService.cs b/Trdo/Services/WindowPlacementService.cs
new file mode 100644
index 0000000..2c47938
--- /dev/null
+++ b/Trdo/Services/WindowPlacementService.cs
@@ -0,0 +1,114 @@
+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;
+
+ x = System.Math.Clamp(x, workArea.X, workArea.X + workArea.Width - width);
+ y = System.Math.Clamp(y, workArea.Y, workArea.Y + workArea.Height - height);
+
+ 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..cb3749a 100644
--- a/Trdo/Trdo.csproj
+++ b/Trdo/Trdo.csproj
@@ -20,6 +20,7 @@
+
@@ -119,6 +120,11 @@
MSBuild:Compile
+
+
+ MSBuild:Compile
+
+
MSBuild:Compile