initial
This commit is contained in:
125
.gitignore
vendored
Normal file
125
.gitignore
vendored
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
## Build results
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
[Ll]ogs/
|
||||||
|
TestResults/
|
||||||
|
artifacts/
|
||||||
|
*.VisualState.xml
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
## Rider / VS Code
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
## VS Working / Cache directories
|
||||||
|
.vs/
|
||||||
|
_ReSharper*/
|
||||||
|
_ReSharper.Caches/
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
## Build Artifacts
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.appx
|
||||||
|
*.appxbundle
|
||||||
|
*.msix
|
||||||
|
*.msixbundle
|
||||||
|
*.log
|
||||||
|
*.cache
|
||||||
|
*.pdb
|
||||||
|
*.mdb
|
||||||
|
|
||||||
|
## NuGet
|
||||||
|
*.nupkg
|
||||||
|
*.snupkg
|
||||||
|
*.nuspec
|
||||||
|
.nuget/
|
||||||
|
packages/
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
## Others
|
||||||
|
*.dbmdl
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*.tmp
|
||||||
|
*.temp
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
*.dif
|
||||||
|
*.patch
|
||||||
|
|
||||||
|
## ASP.NET and Blazor
|
||||||
|
node_modules/
|
||||||
|
wwwroot/dist/
|
||||||
|
wwwroot/lib/
|
||||||
|
wwwroot/**/*.map
|
||||||
|
|
||||||
|
## Generated code
|
||||||
|
Generated_Code/
|
||||||
|
ServiceFabricBackup/
|
||||||
|
ClientBin/
|
||||||
|
|
||||||
|
## Publish artifacts
|
||||||
|
publish/
|
||||||
|
dist/
|
||||||
|
out/
|
||||||
|
*.publishsettings
|
||||||
|
app.publish/
|
||||||
|
|
||||||
|
## Azure
|
||||||
|
*.azurePubxml
|
||||||
|
PublishProfiles/
|
||||||
|
App_Data/
|
||||||
|
*.Publish.xml
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.stackdump
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
*.log
|
||||||
|
*.tlog
|
||||||
|
|
||||||
|
## Coverage
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
*.opencover.xml
|
||||||
|
coverage*.json
|
||||||
|
coverage*.xml
|
||||||
|
coverage*.info
|
||||||
|
|
||||||
|
## Temporary files
|
||||||
|
~$*
|
||||||
|
*.tmp
|
||||||
|
*.bak
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*.DS_Store
|
||||||
|
|
||||||
|
## Git
|
||||||
|
*.orig
|
||||||
|
*.rej
|
||||||
|
*.diff
|
||||||
|
*.patch
|
||||||
|
|
||||||
|
## Environment files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
8
App.xaml
Normal file
8
App.xaml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<Application x:Class="Bedtimer.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
ShutdownMode="OnExplicitShutdown"
|
||||||
|
Startup="OnStartup"
|
||||||
|
Exit="OnExit">
|
||||||
|
<Application.Resources/>
|
||||||
|
</Application>
|
||||||
55
App.xaml.cs
Normal file
55
App.xaml.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace Bedtimer
|
||||||
|
{
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private Mutex? _single;
|
||||||
|
private TrayHost? _tray;
|
||||||
|
|
||||||
|
private void OnStartup(object sender, StartupEventArgs e)
|
||||||
|
{
|
||||||
|
var args = Environment.GetCommandLineArgs();
|
||||||
|
|
||||||
|
// 1) Elevated branches (exit immediately after doing the thing)
|
||||||
|
if (args.Length > 1 && args[1] == "--save-admin")
|
||||||
|
{
|
||||||
|
Config.SaveFromArgs(args);
|
||||||
|
Shutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle elevated autostart toggle: Bedtimer.exe --autostart-admin on|off
|
||||||
|
AutoStart.HandleAdminArgs(args); // returns normally when not matching
|
||||||
|
|
||||||
|
// 2) Single-instance guard
|
||||||
|
_single = new Mutex(true, @"Global\Bedtimer_Singleton", out bool isNew);
|
||||||
|
if (!isNew)
|
||||||
|
{
|
||||||
|
Shutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Optional bootstrap: enable autostart only if current process is admin
|
||||||
|
// (Non-admin users won't be prompted on first run; they can toggle from UI later.)
|
||||||
|
if (!AutoStart.IsEnabled() && Config.IsProcessElevated())
|
||||||
|
{
|
||||||
|
// Will set HKLM\...\Run directly since we’re already elevated
|
||||||
|
AutoStart.SetEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) Start tray + overlay
|
||||||
|
_tray = new TrayHost();
|
||||||
|
_tray.Init();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnExit(object sender, ExitEventArgs e)
|
||||||
|
{
|
||||||
|
_tray?.Dispose();
|
||||||
|
_single?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
AutoStart.cs
Normal file
85
AutoStart.cs
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
using Microsoft.Win32;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Bedtimer
|
||||||
|
{
|
||||||
|
public static class AutoStart
|
||||||
|
{
|
||||||
|
private const string RunKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
|
||||||
|
private const string ValueName = "Bedtimer";
|
||||||
|
|
||||||
|
// Public API used by UI
|
||||||
|
public static bool IsEnabled() => ExistsHKLM();
|
||||||
|
|
||||||
|
public static void SetEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (enabled) EnsureHKLM();
|
||||||
|
else DisableHKLM();
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
// Relaunch self elevated to write HKLM
|
||||||
|
var exe = Environment.ProcessPath!;
|
||||||
|
var args = enabled ? "--autostart-admin on" : "--autostart-admin off";
|
||||||
|
Process.Start(new ProcessStartInfo(exe, args)
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
Verb = "runas"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Branch the app handles on startup (see App.xaml.cs patch below)
|
||||||
|
public static void HandleAdminArgs(string[] args)
|
||||||
|
{
|
||||||
|
// args: ["--autostart-admin", "on"|"off"]
|
||||||
|
if (args.Length >= 3 && args[1] == "--autostart-admin")
|
||||||
|
{
|
||||||
|
if (string.Equals(args[2], "on", StringComparison.OrdinalIgnoreCase)) EnsureHKLM();
|
||||||
|
else DisableHKLM();
|
||||||
|
Environment.Exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------- Internal HKLM helpers (explicit registry view) --------
|
||||||
|
private static RegistryKey OpenHKLM()
|
||||||
|
{
|
||||||
|
var view = Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32;
|
||||||
|
return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ExistsHKLM()
|
||||||
|
{
|
||||||
|
using var root = OpenHKLM();
|
||||||
|
using var key = root.OpenSubKey(RunKey, false);
|
||||||
|
return key?.GetValue(ValueName) is string;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureHKLM()
|
||||||
|
{
|
||||||
|
using var root = OpenHKLM();
|
||||||
|
using var key = root.CreateSubKey(RunKey, true);
|
||||||
|
var exe = Environment.ProcessPath!;
|
||||||
|
var target = $"\"{exe}\""; // no args; single EXE handles all
|
||||||
|
key.SetValue(ValueName, target, RegistryValueKind.String);
|
||||||
|
|
||||||
|
// (Optional) Clean up any old per-user Run entries so we don't double-start
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var hkcu = Registry.CurrentUser.CreateSubKey(RunKey, true);
|
||||||
|
hkcu.DeleteValue(ValueName, false);
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DisableHKLM()
|
||||||
|
{
|
||||||
|
using var root = OpenHKLM();
|
||||||
|
using var key = root.CreateSubKey(RunKey, true);
|
||||||
|
key.DeleteValue(ValueName, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
Bedtimer.csproj
Normal file
12
Bedtimer.csproj
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
31
Bedtimer.sln
Normal file
31
Bedtimer.sln
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio Version 17
|
||||||
|
VisualStudioVersion = 17.14.36518.9
|
||||||
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bedtimer", "Bedtimer.csproj", "{B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Debug|x64.ActiveCfg = Debug|x64
|
||||||
|
{B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Debug|x64.Build.0 = Debug|x64
|
||||||
|
{B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Release|x64.ActiveCfg = Release|x64
|
||||||
|
{B2F2781D-A9DC-4225-BD3F-43CF485CC5BC}.Release|x64.Build.0 = Release|x64
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {F77F2FC4-235E-4350-9B69-EA23D2113489}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
121
Config.cs
Normal file
121
Config.cs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
using Microsoft.Win32;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Principal;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Bedtimer
|
||||||
|
{
|
||||||
|
public sealed class Config
|
||||||
|
{
|
||||||
|
public int BedMinutes { get; set; } = 22 * 60 + 30; // 10:30 PM
|
||||||
|
public int WarnLead { get; set; } = 60;
|
||||||
|
public int FlashLead { get; set; } = 20;
|
||||||
|
public int LockDelay { get; set; } = 5;
|
||||||
|
|
||||||
|
private const string RegPath = @"SOFTWARE\Whitlocktech\Bedtimer";
|
||||||
|
|
||||||
|
// --- Public API -------------------------------------------------------
|
||||||
|
|
||||||
|
public static Config LoadOrCreate()
|
||||||
|
{
|
||||||
|
// Read only; do NOT write defaults here.
|
||||||
|
using var k = OpenHKLM().OpenSubKey(RegPath, writable: false);
|
||||||
|
if (k == null) return new Config(); // defaults in memory only
|
||||||
|
|
||||||
|
int Get(string n, int d) => (k.GetValue(n) is int v) ? v : d;
|
||||||
|
|
||||||
|
return new Config
|
||||||
|
{
|
||||||
|
BedMinutes = Get(nameof(BedMinutes), 22 * 60 + 30),
|
||||||
|
WarnLead = Get(nameof(WarnLead), 60),
|
||||||
|
FlashLead = Get(nameof(FlashLead), 20),
|
||||||
|
LockDelay = Get(nameof(LockDelay), 5),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var root = OpenHKLM();
|
||||||
|
using var k = root.CreateSubKey(RegPath, writable: true);
|
||||||
|
k.SetValue(nameof(BedMinutes), BedMinutes, RegistryValueKind.DWord);
|
||||||
|
k.SetValue(nameof(WarnLead), WarnLead, RegistryValueKind.DWord);
|
||||||
|
k.SetValue(nameof(FlashLead), FlashLead, RegistryValueKind.DWord);
|
||||||
|
k.SetValue(nameof(LockDelay), LockDelay, RegistryValueKind.DWord);
|
||||||
|
Signals.RaiseConfigChanged();
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
// Relaunch self elevated to write HKLM in the SAME registry view
|
||||||
|
var exe = Environment.ProcessPath!;
|
||||||
|
var args = $"--save-admin {BedMinutes} {WarnLead} {FlashLead} {LockDelay}";
|
||||||
|
Process.Start(new ProcessStartInfo(exe, args)
|
||||||
|
{
|
||||||
|
UseShellExecute = true,
|
||||||
|
Verb = "runas"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void SaveFromArgs(string[] args)
|
||||||
|
{
|
||||||
|
// Accept: Bedtimer.exe --save-admin <bed> <warn> <flash> <delay>
|
||||||
|
// Find the flag and read the 4 numbers after it.
|
||||||
|
int i = Array.FindIndex(args, a => string.Equals(a, "--save-admin", StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (i < 0 || args.Length < i + 5) return;
|
||||||
|
|
||||||
|
var cfg = new Config
|
||||||
|
{
|
||||||
|
BedMinutes = int.Parse(args[i + 1]),
|
||||||
|
WarnLead = int.Parse(args[i + 2]),
|
||||||
|
FlashLead = int.Parse(args[i + 3]),
|
||||||
|
LockDelay = int.Parse(args[i + 4]),
|
||||||
|
};
|
||||||
|
|
||||||
|
using var root = OpenHKLM();
|
||||||
|
using var k = root.CreateSubKey(RegPath, writable: true);
|
||||||
|
k.SetValue(nameof(BedMinutes), cfg.BedMinutes, RegistryValueKind.DWord);
|
||||||
|
k.SetValue(nameof(WarnLead), cfg.WarnLead, RegistryValueKind.DWord);
|
||||||
|
k.SetValue(nameof(FlashLead), cfg.FlashLead, RegistryValueKind.DWord);
|
||||||
|
k.SetValue(nameof(LockDelay), cfg.LockDelay, RegistryValueKind.DWord);
|
||||||
|
Signals.RaiseConfigChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Keep %AppData% for state/logs
|
||||||
|
public static string Dir => System.IO.Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"Whitlocktech", "Bedtimer");
|
||||||
|
|
||||||
|
// --- Helpers ----------------------------------------------------------
|
||||||
|
|
||||||
|
private static RegistryKey OpenHKLM()
|
||||||
|
{
|
||||||
|
// Always open the correct view explicitly to avoid WOW6432 redirection issues
|
||||||
|
var view = Environment.Is64BitOperatingSystem ? RegistryView.Registry64 : RegistryView.Registry32;
|
||||||
|
return RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsProcessElevated()
|
||||||
|
{
|
||||||
|
using var id = WindowsIdentity.GetCurrent();
|
||||||
|
var p = new WindowsPrincipal(id);
|
||||||
|
return p.IsInRole(WindowsBuiltInRole.Administrator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Signals
|
||||||
|
{
|
||||||
|
public const string ConfigChangedName = @"Local\Bedtimer_Config_Changed";
|
||||||
|
public static void RaiseConfigChanged()
|
||||||
|
{
|
||||||
|
using var ev = new EventWaitHandle(false, EventResetMode.AutoReset, ConfigChangedName);
|
||||||
|
ev.Set();
|
||||||
|
}
|
||||||
|
public static EventWaitHandle SubscribeConfigChanged()
|
||||||
|
=> new EventWaitHandle(false, EventResetMode.AutoReset, ConfigChangedName);
|
||||||
|
}
|
||||||
|
}
|
||||||
43
MainWindow.xaml
Normal file
43
MainWindow.xaml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<Window x:Class="Bedtimer.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="Bedtimer Settings" Width="380" Height="260" ResizeMode="NoResize"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
<Grid Margin="16">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||||
|
<TextBlock Text="Bedtime:" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<ComboBox x:Name="BedHour" Width="60"/>
|
||||||
|
<TextBlock Text=":" VerticalAlignment="Center" Margin="4,0"/>
|
||||||
|
<ComboBox x:Name="BedMinute" Width="60"/>
|
||||||
|
<ComboBox x:Name="AmPm" Width="70" Margin="8,0,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Grid.Row="1" Margin="0,0,0,8">
|
||||||
|
<TextBlock Text="Warn lead (min):" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<TextBox x:Name="WarnLead" Width="80"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Grid.Row="2" Margin="0,0,0,8">
|
||||||
|
<TextBlock Text="Flash lead (min):" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<TextBox x:Name="FlashLead" Width="80"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Grid.Row="3" Margin="0,0,0,8">
|
||||||
|
<TextBlock Text="Lock delay (min after bedtime):" VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<TextBox x:Name="LockDelay" Width="80"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Grid.Row="4" HorizontalAlignment="Right">
|
||||||
|
<Button Content="Save" Width="100" Margin="0,0,8,0" Click="Save_Click"/>
|
||||||
|
<Button Content="Close" Width="100" Click="Close_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
64
MainWindow.xaml.cs
Normal file
64
MainWindow.xaml.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace Bedtimer
|
||||||
|
{
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private Config _cfg = null!;
|
||||||
|
|
||||||
|
public MainWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_cfg = Config.LoadOrCreate();
|
||||||
|
|
||||||
|
BedHour.ItemsSource = Enumerable.Range(1, 12);
|
||||||
|
BedMinute.ItemsSource = new[] { "00", "05", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55" };
|
||||||
|
AmPm.ItemsSource = new[] { "AM", "PM" };
|
||||||
|
|
||||||
|
// load values
|
||||||
|
var (h, m, am) = ToClock(_cfg.BedMinutes);
|
||||||
|
BedHour.SelectedItem = h;
|
||||||
|
BedMinute.SelectedItem = m.ToString("00");
|
||||||
|
AmPm.SelectedItem = am ? "AM" : "PM";
|
||||||
|
WarnLead.Text = _cfg.WarnLead.ToString();
|
||||||
|
FlashLead.Text = _cfg.FlashLead.ToString();
|
||||||
|
LockDelay.Text = _cfg.LockDelay.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (int hour, int minute, bool am) ToClock(int minutes)
|
||||||
|
{
|
||||||
|
int hh24 = minutes / 60;
|
||||||
|
int mm = minutes % 60;
|
||||||
|
bool am = hh24 < 12;
|
||||||
|
int hh12 = hh24 % 12; if (hh12 == 0) hh12 = 12;
|
||||||
|
return (hh12, mm, am);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ToMinutes(int hour12, int minute, bool am)
|
||||||
|
{
|
||||||
|
int hh24 = (hour12 % 12) + (am ? 0 : 12);
|
||||||
|
return hh24 * 60 + minute;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Save_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
int hour = (int)(BedHour.SelectedItem ?? 10);
|
||||||
|
int minute = int.Parse((string?)BedMinute.SelectedItem ?? "30");
|
||||||
|
bool am = ((string?)AmPm.SelectedItem ?? "PM") == "AM";
|
||||||
|
|
||||||
|
_cfg.BedMinutes = ToMinutes(hour, minute, am);
|
||||||
|
_cfg.WarnLead = int.TryParse(WarnLead.Text, out var w) ? Math.Max(1, w) : 60;
|
||||||
|
_cfg.FlashLead = int.TryParse(FlashLead.Text, out var f) ? Math.Max(1, f) : 20;
|
||||||
|
_cfg.LockDelay = int.TryParse(LockDelay.Text, out var d) ? Math.Max(0, d) : 5;
|
||||||
|
|
||||||
|
_cfg.Save(); // non-admin -> triggers UAC; admin -> writes directly
|
||||||
|
_cfg = Config.LoadOrCreate();
|
||||||
|
MessageBox.Show(this, "Saved.", "Bedtimer", MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
73
OverlayManager.cs
Normal file
73
OverlayManager.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using Microsoft.Win32;
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace Bedtimer
|
||||||
|
{
|
||||||
|
public sealed class OverlayManager : IDisposable
|
||||||
|
{
|
||||||
|
private Config _cfg = null!;
|
||||||
|
private OverlayWindow? _overlay;
|
||||||
|
private Thread? _cfgThread;
|
||||||
|
private string _cycleId = "";
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
_cfg = Config.LoadOrCreate();
|
||||||
|
BuildOverlay();
|
||||||
|
StartConfigListener();
|
||||||
|
|
||||||
|
// React to system time changes / DST
|
||||||
|
SystemEvents.TimeChanged += (_, __) => Application.Current.Dispatcher.Invoke(RecomputeSchedule);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildOverlay()
|
||||||
|
{
|
||||||
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
_overlay ??= new OverlayWindow();
|
||||||
|
_overlay.Show(); // safe: collapsed until T-60
|
||||||
|
RecomputeSchedule();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartConfigListener()
|
||||||
|
{
|
||||||
|
_cfgThread = new Thread(() =>
|
||||||
|
{
|
||||||
|
using var ev = Signals.SubscribeConfigChanged();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
ev.WaitOne();
|
||||||
|
Application.Current.Dispatcher.Invoke(RecomputeSchedule);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_cfgThread.IsBackground = true;
|
||||||
|
_cfgThread.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecomputeSchedule()
|
||||||
|
{
|
||||||
|
_cfg = Config.LoadOrCreate();
|
||||||
|
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var bedToday = now.Date.AddMinutes(_cfg.BedMinutes);
|
||||||
|
var bedT = (now <= bedToday) ? bedToday : bedToday.AddDays(1);
|
||||||
|
|
||||||
|
var warnT = bedT.AddMinutes(-_cfg.WarnLead);
|
||||||
|
var flashT = bedT.AddMinutes(-_cfg.FlashLead);
|
||||||
|
var lockT = bedT.AddMinutes(+_cfg.LockDelay);
|
||||||
|
|
||||||
|
// one lock per calendar day (next bedtime’s date)
|
||||||
|
_cycleId = bedT.ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
|
_overlay!.SetSchedule(_cycleId, warnT, flashT, bedT, lockT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// overlay lives/dies with app; nothing extra
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
OverlayWindow.xaml
Normal file
15
OverlayWindow.xaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<Window x:Class="Bedtimer.OverlayWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
AllowsTransparency="True" Background="Transparent"
|
||||||
|
WindowStyle="None" Topmost="True" ShowInTaskbar="False"
|
||||||
|
Width="360" Height="96">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="HudPanel" CornerRadius="10" Padding="12" Background="#66000000" Visibility="Collapsed">
|
||||||
|
<TextBlock x:Name="HudText" FontSize="24" FontWeight="SemiBold" Foreground="White"/>
|
||||||
|
</Border>
|
||||||
|
<Border x:Name="BedLabel" CornerRadius="10" Padding="12" Background="#99000000" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="It’s bedtime." FontSize="26" FontWeight="Bold" Foreground="White"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
155
OverlayWindow.xaml.cs
Normal file
155
OverlayWindow.xaml.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
State.cs
Normal file
49
State.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Bedtimer
|
||||||
|
{
|
||||||
|
public static class State
|
||||||
|
{
|
||||||
|
private sealed class Model
|
||||||
|
{
|
||||||
|
public string? LastLockCycleId { get; set; }
|
||||||
|
public DateTime LastLockUtc { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string PathJson => System.IO.Path.Combine(Config.Dir, "state.json");
|
||||||
|
private static readonly object _gate = new();
|
||||||
|
|
||||||
|
private static Model Load()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(PathJson)!);
|
||||||
|
if (!File.Exists(PathJson)) return new Model();
|
||||||
|
try { return JsonSerializer.Deserialize<Model>(File.ReadAllText(PathJson)) ?? new Model(); }
|
||||||
|
catch { return new Model(); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Save(Model m)
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(PathJson)!);
|
||||||
|
File.WriteAllText(PathJson, JsonSerializer.Serialize(m, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsLockedForCycle(string cycleId)
|
||||||
|
{
|
||||||
|
var m = Load();
|
||||||
|
return string.Equals(m.LastLockCycleId, cycleId, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void MarkLocked(string cycleId)
|
||||||
|
{
|
||||||
|
Save(new Model { LastLockCycleId = cycleId, LastLockUtc = DateTime.UtcNow });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
207
TrayHost.cs
Normal file
207
TrayHost.cs
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls; // ContextMenu, MenuItem
|
||||||
|
using System.Windows.Controls.Primitives; // PlacementMode
|
||||||
|
using System.Windows.Interop;
|
||||||
|
|
||||||
|
namespace Bedtimer
|
||||||
|
{
|
||||||
|
public sealed class TrayHost : IDisposable
|
||||||
|
{
|
||||||
|
// === Win32 interop ===
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
private struct NOTIFYICONDATA
|
||||||
|
{
|
||||||
|
public int cbSize;
|
||||||
|
public IntPtr hWnd;
|
||||||
|
public int uID;
|
||||||
|
public int uFlags;
|
||||||
|
public int uCallbackMessage;
|
||||||
|
public IntPtr hIcon;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||||
|
public string szTip;
|
||||||
|
public int dwState;
|
||||||
|
public int dwStateMask;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
|
||||||
|
public string szInfo;
|
||||||
|
public int uTimeoutOrVersion;
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
|
||||||
|
public string szInfoTitle;
|
||||||
|
public int dwInfoFlags;
|
||||||
|
public Guid guidItem;
|
||||||
|
public IntPtr hBalloonIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int NIM_ADD = 0x00000000;
|
||||||
|
private const int NIM_MODIFY = 0x00000001;
|
||||||
|
private const int NIM_DELETE = 0x00000002;
|
||||||
|
|
||||||
|
private const int NIF_MESSAGE = 0x00000001;
|
||||||
|
private const int NIF_ICON = 0x00000002;
|
||||||
|
private const int NIF_TIP = 0x00000004;
|
||||||
|
|
||||||
|
private const int WM_APP = 0x8000;
|
||||||
|
private const int WM_USER = 0x0400;
|
||||||
|
private const int WM_LBUTTONUP = 0x0202;
|
||||||
|
private const int WM_LBUTTONDBLCLK = 0x0203;
|
||||||
|
private const int WM_RBUTTONUP = 0x0205;
|
||||||
|
|
||||||
|
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern bool Shell_NotifyIcon(int dwMessage, ref NOTIFYICONDATA lpdata);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT lpPoint);
|
||||||
|
[DllImport("user32.dll")] private static extern IntPtr LoadIcon(IntPtr hInstance, IntPtr lpIconName);
|
||||||
|
private static readonly IntPtr IDI_INFORMATION = (IntPtr)0x7F04; // stock info icon
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct POINT { public int X; public int Y; }
|
||||||
|
|
||||||
|
// === Fields ===
|
||||||
|
private HwndSource? _src;
|
||||||
|
private IntPtr _hWnd = IntPtr.Zero;
|
||||||
|
private IntPtr _hIcon = IntPtr.Zero;
|
||||||
|
private int _iconId = 1;
|
||||||
|
private readonly int _callbackMsg = WM_APP + 1;
|
||||||
|
|
||||||
|
private ContextMenu? _menu;
|
||||||
|
private OverlayManager _overlay = null!;
|
||||||
|
|
||||||
|
public void Init()
|
||||||
|
{
|
||||||
|
// Hidden message window for tray callbacks
|
||||||
|
var param = new HwndSourceParameters("Bedtimer.TrayHostWindow")
|
||||||
|
{
|
||||||
|
Width = 0,
|
||||||
|
Height = 0,
|
||||||
|
WindowStyle = unchecked((int)0x80000000), // WS_POPUP
|
||||||
|
UsesPerPixelOpacity = false
|
||||||
|
};
|
||||||
|
_src = new HwndSource(param);
|
||||||
|
_src.AddHook(WndProc);
|
||||||
|
_hWnd = _src.Handle;
|
||||||
|
|
||||||
|
// Icon (stock system icon to avoid resources)
|
||||||
|
_hIcon = LoadIcon(IntPtr.Zero, IDI_INFORMATION);
|
||||||
|
|
||||||
|
// Add tray icon
|
||||||
|
var nid = new NOTIFYICONDATA
|
||||||
|
{
|
||||||
|
cbSize = Marshal.SizeOf<NOTIFYICONDATA>(),
|
||||||
|
hWnd = _hWnd,
|
||||||
|
uID = _iconId,
|
||||||
|
uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP,
|
||||||
|
uCallbackMessage = _callbackMsg,
|
||||||
|
hIcon = _hIcon,
|
||||||
|
szTip = "Bedtimer"
|
||||||
|
};
|
||||||
|
if (!Shell_NotifyIcon(NIM_ADD, ref nid))
|
||||||
|
throw new Win32Exception("Shell_NotifyIcon(NIM_ADD) failed.");
|
||||||
|
|
||||||
|
// Build WPF context menu
|
||||||
|
_menu = BuildMenu();
|
||||||
|
|
||||||
|
// Start overlay manager
|
||||||
|
_overlay = new OverlayManager();
|
||||||
|
_overlay.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private ContextMenu BuildMenu()
|
||||||
|
{
|
||||||
|
var menu = new ContextMenu();
|
||||||
|
|
||||||
|
var open = new MenuItem { Header = "Open Settings..." };
|
||||||
|
open.Click += (_, __) => ShowSettings();
|
||||||
|
menu.Items.Add(open);
|
||||||
|
|
||||||
|
var autostart = new MenuItem { Header = "Start with Windows", IsCheckable = true, IsChecked = AutoStart.IsEnabled() };
|
||||||
|
autostart.Click += (_, __) =>
|
||||||
|
{
|
||||||
|
var next = !AutoStart.IsEnabled();
|
||||||
|
AutoStart.SetEnabled(next); // may UAC prompt
|
||||||
|
autostart.IsChecked = AutoStart.IsEnabled(); // refresh state after hop
|
||||||
|
};
|
||||||
|
|
||||||
|
menu.Items.Add(autostart);
|
||||||
|
|
||||||
|
menu.Items.Add(new Separator());
|
||||||
|
|
||||||
|
var exit = new MenuItem { Header = "Exit" };
|
||||||
|
exit.Click += (_, __) => Application.Current.Shutdown();
|
||||||
|
menu.Items.Add(exit);
|
||||||
|
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowSettings()
|
||||||
|
{
|
||||||
|
// Bring up settings window (single instance)
|
||||||
|
foreach (Window w in Application.Current.Windows)
|
||||||
|
if (w is MainWindow mw) { mw.Activate(); return; }
|
||||||
|
|
||||||
|
var win = new MainWindow();
|
||||||
|
win.Show();
|
||||||
|
win.Activate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show WPF ContextMenu at the current cursor position
|
||||||
|
private void ShowMenuAtCursor()
|
||||||
|
{
|
||||||
|
if (_menu == null) return;
|
||||||
|
if (GetCursorPos(out var pt))
|
||||||
|
{
|
||||||
|
_menu.Placement = PlacementMode.AbsolutePoint;
|
||||||
|
_menu.HorizontalOffset = pt.X;
|
||||||
|
_menu.VerticalOffset = pt.Y;
|
||||||
|
_menu.IsOpen = true;
|
||||||
|
_menu.Focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||||
|
{
|
||||||
|
if (msg == _callbackMsg)
|
||||||
|
{
|
||||||
|
int code = lParam.ToInt32();
|
||||||
|
switch (code)
|
||||||
|
{
|
||||||
|
case WM_LBUTTONUP:
|
||||||
|
ShowSettings();
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
case WM_LBUTTONDBLCLK:
|
||||||
|
ShowSettings();
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
case WM_RBUTTONUP:
|
||||||
|
ShowMenuAtCursor();
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var nid = new NOTIFYICONDATA
|
||||||
|
{
|
||||||
|
cbSize = Marshal.SizeOf<NOTIFYICONDATA>(),
|
||||||
|
hWnd = _hWnd,
|
||||||
|
uID = _iconId
|
||||||
|
};
|
||||||
|
Shell_NotifyIcon(NIM_DELETE, ref nid);
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
|
||||||
|
_overlay?.Dispose();
|
||||||
|
_src?.RemoveHook(WndProc);
|
||||||
|
_src?.Dispose();
|
||||||
|
_src = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user