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); } } }