using System.Reflection; using System.Text.Json; using Microsoft.Win32; using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.WinForms; namespace MyStart; static class Program { [STAThread] static void Main(string[] args) { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.SetHighDpiMode(HighDpiMode.PerMonitorV2); bool minimized = args.Any(a => a.Equals("--minimized", StringComparison.OrdinalIgnoreCase) || a.Equals("--min", StringComparison.OrdinalIgnoreCase) || a.Equals("--autostart", StringComparison.OrdinalIgnoreCase)); Application.Run(new MainForm(minimized)); } } class MainForm : Form { private readonly WebView2 _web = new() { Dock = DockStyle.Fill }; private const string Host = "mystart.local"; private bool _pendingMaximize; private System.Windows.Forms.Timer? _syncTimer; private string? _syncDir; // OneDrive\MyStart, or null if no OneDrive found private string? _syncOwnFilePath; // OneDrive\MyStart\sync-.json -- WE are the only writer of this file private string? _syncMarkerPath; // local record of what we last pushed/pulled, survives restarts private string? _lastSyncedDataRaw; private bool _syncInProgress; public MainForm(bool startMinimized = false) { Text = "MyStart"; StartPosition = FormStartPosition.CenterScreen; Width = 1280; Height = 800; MinimumSize = new System.Drawing.Size(480, 360); ShowInTaskbar = true; BackColor = System.Drawing.Color.Black; // No title bar / border. Close with Alt+F4, minimize via the taskbar button. FormBorderStyle = FormBorderStyle.None; if (startMinimized) { // Autostart: sit minimized on the taskbar; restore to a normal window on first open. WindowState = FormWindowState.Minimized; _pendingMaximize = true; Resize += (_, _) => { if (_pendingMaximize && WindowState != FormWindowState.Minimized) { _pendingMaximize = false; WindowState = FormWindowState.Normal; } }; } // else: leave WindowState at its default (Normal) -> starts as a normal // 1280x800 window, not maximized/fullscreen. try { var asm = Assembly.GetExecutingAssembly(); var icoName = asm.GetManifestResourceNames() .FirstOrDefault(n => n.EndsWith("app.ico", StringComparison.OrdinalIgnoreCase)); if (icoName != null) { using var s = asm.GetManifestResourceStream(icoName); if (s != null) Icon = new Icon(s); } } catch { /* icon optional */ } Controls.Add(_web); Shown += async (_, _) => await InitAsync(); } // Keep a borderless maximized window from covering the taskbar. protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); MaximizedBounds = Screen.FromHandle(Handle).WorkingArea; } private async Task InitAsync() { // Persistent data dir -> localStorage (settings/bookmarks) survive across runs // and independent of where the .exe lives. string dataDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "MyStart"); Directory.CreateDirectory(dataDir); // Optional OneDrive sync: if OneDrive is set up on this PC, drop a JSON // snapshot of the whole app state (all Reiter/profiles) in a MyStart // subfolder there. OneDrive itself does the actual transfer between // PCs; we just poll files there. No OneDrive -> sync silently stays off. // // Each PC writes ONLY its own file (sync-.json) and reads // everyone else's. Two devices never write the same path, so there is // nothing for OneDrive to see as a conflict -- a shared single file that // both PCs wrote to used to make OneDrive spawn "conflicted copy" files // whenever both saved around the same moment. string? oneDriveRoot = Environment.GetEnvironmentVariable("OneDrive") ?? Environment.GetEnvironmentVariable("OneDriveConsumer") ?? Environment.GetEnvironmentVariable("OneDriveCommercial"); if (!string.IsNullOrEmpty(oneDriveRoot)) { try { string syncDir = Path.Combine(oneDriveRoot, "MyStart"); Directory.CreateDirectory(syncDir); _syncDir = syncDir; string device = Environment.MachineName; foreach (char c in Path.GetInvalidFileNameChars()) device = device.Replace(c, '_'); _syncOwnFilePath = Path.Combine(syncDir, "sync-" + device + ".json"); _syncMarkerPath = Path.Combine(dataDir, "sync-marker.json"); _lastSyncedDataRaw = File.Exists(_syncMarkerPath) ? File.ReadAllText(_syncMarkerPath) : null; } catch { _syncDir = null; _syncOwnFilePath = null; } } var env = await CoreWebView2Environment.CreateAsync(null, dataDir); await _web.EnsureCoreWebView2Async(env); // Extract the embedded app to a fixed folder, served via a stable origin. string appDir = Path.Combine(dataDir, "app"); Directory.CreateDirectory(appDir); string htmlPath = Path.Combine(appDir, "index.html"); var asm = Assembly.GetExecutingAssembly(); string resName = asm.GetManifestResourceNames() .First(n => n.EndsWith("app-index.html", StringComparison.OrdinalIgnoreCase)); await using (var res = asm.GetManifestResourceStream(resName)!) await using (var file = File.Create(htmlPath)) await res.CopyToAsync(file); var core = _web.CoreWebView2; core.SetVirtualHostNameToFolderMapping(Host, appDir, CoreWebView2HostResourceAccessKind.Allow); core.Settings.IsStatusBarEnabled = false; core.Settings.AreDefaultContextMenusEnabled = true; core.Settings.IsWebMessageEnabled = true; // Autostart toggle from the in-app "MyStart" info page. core.WebMessageReceived += (_, e) => { try { using var doc = JsonDocument.Parse(e.WebMessageAsJson); var root = doc.RootElement; if (!root.TryGetProperty("type", out var t)) return; switch (t.GetString()) { case "autostart-get": PostAutostartState(); break; case "autostart-set": bool on = root.TryGetProperty("value", out var v) && v.ValueKind == JsonValueKind.True; SetAutostart(on); PostAutostartState(); break; case "window-drag": StartWindowDrag(); break; case "window-resize": if (root.TryGetProperty("edge", out var edgeProp)) StartWindowResize(edgeProp.GetString()); break; case "window-min": WindowState = FormWindowState.Minimized; break; case "window-max": WindowState = WindowState == FormWindowState.Maximized ? FormWindowState.Normal : FormWindowState.Maximized; break; case "window-close": Close(); break; } } catch { /* ignore malformed messages */ } }; // Bookmark links that open a new window -> user's default browser. core.NewWindowRequested += (_, e) => { e.Handled = true; OpenExternal(e.Uri); }; // Same-tab clicks to external sites -> default browser, keep the app loaded. core.NavigationStarting += (_, e) => { var uri = e.Uri ?? string.Empty; if (uri.StartsWith("https://" + Host, StringComparison.OrdinalIgnoreCase)) return; // internal if (uri.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || uri.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) { e.Cancel = true; OpenExternal(uri); } }; // Kick a sync check after every completed load (first start, and any // reload the sync pull/push itself triggers), then keep polling. core.NavigationCompleted += async (_, e) => { if (!e.IsSuccess) return; await SyncTickAsync(isClosing: false); if (_syncTimer == null && _syncDir != null) { _syncTimer = new System.Windows.Forms.Timer { Interval = 30_000 }; _syncTimer.Tick += async (_, _) => await SyncTickAsync(isClosing: false); _syncTimer.Start(); } }; core.Navigate("https://" + Host + "/index.html"); } // --- OneDrive sync tick: export current state from the page (opaque JSON // blob, the page understands its own structure -- we don't). If it's new // since our last known-synced state, push it to OUR OWN file (never a // shared one -- see the comment in InitAsync on why). Otherwise look at // every other PC's file and pull in whichever is newest, if it actually // differs -- skipped while the window is actively focused so we never // yank a reload out from under someone mid-edit; it'll catch up on the // next tick after they click away. --- private async Task SyncTickAsync(bool isClosing) { if (_syncDir == null || _syncOwnFilePath == null || _syncInProgress) return; _syncInProgress = true; try { string raw = await _web.CoreWebView2.ExecuteScriptAsync( "window.__mystartSyncExport && window.__mystartSyncExport()"); string? exportJson = JsonSerializer.Deserialize(raw); if (string.IsNullOrEmpty(exportJson)) return; string? localDataRaw = ExtractDataRaw(exportJson); if (localDataRaw == null) return; if (localDataRaw != _lastSyncedDataRaw) { // Something changed here since we last synced -> push to our own file. await File.WriteAllTextAsync(_syncOwnFilePath, exportJson); _lastSyncedDataRaw = localDataRaw; TryWriteMarker(localDataRaw); PostSyncStatus(true, "gesendet"); } else if (!isClosing) { var peer = FindLatestPeer(); if (peer == null || peer.Value.DataRaw == localDataRaw) { PostSyncStatus(true, "aktuell"); } else if (!ContainsFocus || WindowState == FormWindowState.Minimized) { string js = "window.__mystartSyncImport && window.__mystartSyncImport(" + JsonSerializer.Serialize(peer.Value.FullJson) + ")"; await _web.CoreWebView2.ExecuteScriptAsync(js); _lastSyncedDataRaw = peer.Value.DataRaw; TryWriteMarker(peer.Value.DataRaw); PostSyncStatus(true, "aktualisiert"); } else { PostSyncStatus(true, "wartet (Fenster gerade aktiv)"); } } } catch (Exception ex) { PostSyncStatus(false, ex.Message); } finally { _syncInProgress = false; } } // Every other PC's sync-*.json (plus a one-time look at the old shared // sync.json from before each PC had its own file, so nothing already // written there gets silently stranded), newest "savedAt" wins. private (string FullJson, string DataRaw)? FindLatestPeer() { if (_syncDir == null) return null; string? bestSavedAt = null; string? bestFullJson = null; string? bestDataRaw = null; var candidates = Directory.EnumerateFiles(_syncDir, "sync-*.json") .Where(p => !string.Equals(p, _syncOwnFilePath, StringComparison.OrdinalIgnoreCase)); string legacyPath = Path.Combine(_syncDir, "sync.json"); if (File.Exists(legacyPath)) candidates = candidates.Append(legacyPath); foreach (var path in candidates) { string json; try { json = File.ReadAllText(path); } catch { continue; } try { using var doc = JsonDocument.Parse(json); if (!doc.RootElement.TryGetProperty("savedAt", out var saProp)) continue; if (!doc.RootElement.TryGetProperty("data", out var dataProp)) continue; string? savedAt = saProp.GetString(); if (savedAt == null) continue; if (bestSavedAt == null || string.CompareOrdinal(savedAt, bestSavedAt) > 0) { bestSavedAt = savedAt; bestFullJson = json; bestDataRaw = dataProp.GetRawText(); } } catch { /* skip unreadable/mid-write file, try again next tick */ } } return bestFullJson != null && bestDataRaw != null ? (bestFullJson, bestDataRaw) : null; } private static string? ExtractDataRaw(string json) { try { using var doc = JsonDocument.Parse(json); return doc.RootElement.TryGetProperty("data", out var data) ? data.GetRawText() : null; } catch { return null; } } private void TryWriteMarker(string dataRaw) { try { if (_syncMarkerPath != null) File.WriteAllText(_syncMarkerPath, dataRaw); } catch { /* best effort */ } } private void PostSyncStatus(bool ok, string message) { try { var json = JsonSerializer.Serialize(new { type = "sync-status", ok, message }); _web.CoreWebView2.PostWebMessageAsJson(json); } catch { /* page may be mid-reload */ } } protected override void OnFormClosing(FormClosingEventArgs e) { base.OnFormClosing(e); // Best-effort final push; not awaited/blocking (would risk deadlocking // the UI thread against WebView2's own message pump on shutdown). The // 30s poll already covers normal use -- this just shaves the tail end. if (_syncDir != null) _ = SyncTickAsync(isClosing: true); } // --- Autostart (per-user HKCU Run key, points at THIS exe with --minimized) --- private const string RunKeyPath = @"Software\Microsoft\Windows\CurrentVersion\Run"; private const string RunValueName = "MyStart"; private static string ExePath => Environment.ProcessPath ?? Application.ExecutablePath; private static bool IsAutostartEnabled() { try { using var k = Registry.CurrentUser.OpenSubKey(RunKeyPath, false); return k?.GetValue(RunValueName) is string v && v.Length > 0; } catch { return false; } } private static void SetAutostart(bool on) { try { using var k = Registry.CurrentUser.OpenSubKey(RunKeyPath, true) ?? Registry.CurrentUser.CreateSubKey(RunKeyPath, true); if (k == null) return; if (on) k.SetValue(RunValueName, "\"" + ExePath + "\" --minimized"); else if (k.GetValue(RunValueName) != null) k.DeleteValue(RunValueName, false); } catch { /* ignore */ } } private void PostAutostartState() { var json = "{\"type\":\"autostart\",\"enabled\":" + (IsAutostartEnabled() ? "true" : "false") + "}"; try { _web.CoreWebView2.PostWebMessageAsJson(json); } catch { } } // --- Free window drag for the borderless window (page sends "window-drag" // on grip mousedown / Alt+drag; we hand off to the native move loop). --- [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern bool ReleaseCapture(); [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); private const int WM_NCLBUTTONDOWN = 0xA1; private const int HTCAPTION = 0x2; private void StartWindowDrag() { // A maximized window can't be moved -> restore first, centred under cursor-ish. if (WindowState == FormWindowState.Maximized) WindowState = FormWindowState.Normal; ReleaseCapture(); SendMessage(Handle, WM_NCLBUTTONDOWN, (IntPtr)HTCAPTION, IntPtr.Zero); } // --- Free window resize for the borderless window. The WebView2 control // fills the entire client area, so the OS never gets a chance to hit-test // the window's own edges for a resize cursor/drag -- every mouse message // lands in the browser first. The page (bundle.js) instead watches the // mouse position itself, and on mousedown within a few px of an edge/ // corner sends "window-resize" with which edge; we hand off to the same // native resize loop Windows uses for a normal sizable window. --- private static readonly Dictionary ResizeEdges = new() { ["n"] = 12, ["s"] = 15, ["e"] = 11, ["w"] = 10, ["ne"] = 14, ["nw"] = 13, ["se"] = 17, ["sw"] = 16, }; private void StartWindowResize(string? edge) { // Only a normal (non-maximized, non-minimized) window can be resized this way. if (WindowState != FormWindowState.Normal) return; if (edge == null || !ResizeEdges.TryGetValue(edge, out var htCode)) return; ReleaseCapture(); SendMessage(Handle, WM_NCLBUTTONDOWN, (IntPtr)htCode, IntPtr.Zero); } private static void OpenExternal(string uri) { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = uri, UseShellExecute = true }); } catch { /* ignore */ } } }