This commit is contained in:
2025-10-26 10:40:21 -05:00
commit 45b9d70c90
14 changed files with 1043 additions and 0 deletions

155
OverlayWindow.xaml.cs Normal file
View File

@@ -0,0 +1,155 @@
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
namespace Bedtimer
{
public partial class OverlayWindow : Window
{
private readonly DispatcherTimer _ui;
private readonly DateTime _procStart = DateTime.Now;
private string _cycleId = "";
private DateTime WarnT, FlashT, BedT, LockT;
public OverlayWindow()
{
InitializeComponent();
Width = 360; Height = 96;
Background = System.Windows.Media.Brushes.Transparent;
WindowStyle = WindowStyle.None;
AllowsTransparency = true;
ShowInTaskbar = false;
Topmost = true;
Loaded += (_, __) =>
{
PositionTopRight(12);
MakeClickThrough(true);
};
_ui = new DispatcherTimer(TimeSpan.FromSeconds(1), DispatcherPriority.Render, (_, __) => Tick(), Dispatcher);
_ui.Start();
}
// unified signature: includes cycleId
public void SetSchedule(string cycleId, DateTime warnT, DateTime flashT, DateTime bedT, DateTime lockT)
{
_cycleId = cycleId;
WarnT = warnT; FlashT = flashT; BedT = bedT; LockT = lockT;
}
private void Tick()
{
var now = DateTime.Now;
// Prevent instant re-lock right after login/app start
bool startupGrace = (now - _procStart) < TimeSpan.FromSeconds(90);
// If we already locked for this cycle, stay hidden until next schedule is computed
if (!string.IsNullOrEmpty(_cycleId) && State.IsLockedForCycle(_cycleId))
{
Visibility = Visibility.Collapsed;
return;
}
if (now < WarnT)
{
Visibility = Visibility.Collapsed;
return;
}
if (now >= WarnT && now < FlashT)
{
Visibility = Visibility.Visible;
MakeClickThrough(true);
ShowClock(now, withCountdown: false);
return;
}
if (now >= FlashT && now < BedT)
{
Visibility = Visibility.Visible;
MakeClickThrough(true);
FlashBriefly(now);
ShowClock(now, withCountdown: true);
return;
}
if (now >= BedT && now < LockT)
{
Visibility = Visibility.Visible;
MakeClickThrough(true);
ShowBedtime();
return;
}
if (now >= LockT)
{
if (!startupGrace && !string.IsNullOrEmpty(_cycleId) && !State.IsLockedForCycle(_cycleId))
{
TryLock();
State.MarkLocked(_cycleId);
}
Visibility = Visibility.Collapsed;
}
}
private void ShowClock(DateTime now, bool withCountdown)
{
BedLabel.Visibility = Visibility.Collapsed;
HudPanel.Visibility = Visibility.Visible;
var t = now.ToString("h:mm tt");
HudText.Text = withCountdown ? $"{t} • Bed in {Pretty(BedT - now)}" : t;
}
private void ShowBedtime()
{
HudPanel.Visibility = Visibility.Collapsed;
BedLabel.Visibility = Visibility.Visible;
}
private static string Pretty(TimeSpan ts)
{
if (ts.TotalMinutes < 1) return $"{ts.Seconds}s";
if (ts.TotalHours < 1) return $"{(int)ts.TotalMinutes}m {ts.Seconds}s";
return $"{(int)ts.TotalHours}h {ts.Minutes}m";
}
private void FlashBriefly(DateTime now)
{
if (now.Second == 0)
{
var orig = Opacity; Opacity = 0.2;
_ = Task.Delay(1000).ContinueWith(_ => Dispatcher.Invoke(() => Opacity = orig));
}
}
[DllImport("user32.dll")] private static extern bool LockWorkStation();
private static void TryLock() { try { LockWorkStation(); } catch { } }
private void PositionTopRight(int margin)
{
var r = System.Windows.SystemParameters.WorkArea;
Left = r.Right - Width - margin;
Top = r.Top + margin;
}
private const int GWL_EXSTYLE = -20, WS_EX_TRANSPARENT = 0x20, WS_EX_TOOLWINDOW = 0x80, WS_EX_NOACTIVATE = 0x08000000;
[DllImport("user32.dll")] private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")] private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
private void MakeClickThrough(bool enable)
{
var hwnd = new WindowInteropHelper(this).Handle;
int ex = GetWindowLong(hwnd, GWL_EXSTYLE);
ex |= WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE;
ex = enable ? (ex | WS_EX_TRANSPARENT) : (ex & ~WS_EX_TRANSPARENT);
SetWindowLong(hwnd, GWL_EXSTYLE, ex);
}
}
}