From 781f91545495f355e3a2139b5c8d2cfb12e0f3c4 Mon Sep 17 00:00:00 2001 From: Jonathan Flietstra Date: Sun, 27 Sep 2026 18:25:12 +0200 Subject: [PATCH] Uitrol 27-09-2026 18:25 --- src/MijnBesluit.AppUi/SchermBasis.cs | 3 + src/MijnBesluit.Uitroller/Cockpitmodel.cs | 13 ++ src/MijnBesluit.Uitroller/Hoofdvenster.xaml | 57 ++++++ .../Hoofdvenster.xaml.cs | 176 +++++++++++++++++- src/MijnBesluit.Uitroller/Instellingen.cs | 29 +++ .../Instellingenvenster.xaml | 8 +- .../Instellingenvenster.xaml.cs | 11 ++ src/MijnBesluit.Uitroller/Lokaaltest.cs | 5 + src/MijnBesluit.Uitroller/Vensterstand.cs | 62 ++++++ .../Components/Pages/Inloggen.razor | 8 + 10 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 src/MijnBesluit.Uitroller/Vensterstand.cs diff --git a/src/MijnBesluit.AppUi/SchermBasis.cs b/src/MijnBesluit.AppUi/SchermBasis.cs index fbc320f..abc0d67 100644 --- a/src/MijnBesluit.AppUi/SchermBasis.cs +++ b/src/MijnBesluit.AppUi/SchermBasis.cs @@ -151,6 +151,9 @@ public static class Tonen 6 => "Document", 7 => "Notitie", 8 => "Sociale media", + 9 => "Donorcodicil", + 10 => "Rijbewijs", + 11 => "Paspoort", _ => "Overig" }; diff --git a/src/MijnBesluit.Uitroller/Cockpitmodel.cs b/src/MijnBesluit.Uitroller/Cockpitmodel.cs index f9b4485..4741189 100644 --- a/src/MijnBesluit.Uitroller/Cockpitmodel.cs +++ b/src/MijnBesluit.Uitroller/Cockpitmodel.cs @@ -18,6 +18,19 @@ public sealed class Cockpitrepo public Brush StatusKleur { get; set; } = Brushes.Black; } +/// +/// Eén balk in een Cockpit-staafgrafiek (pagina's per project, wijzigingen per teamlid). De breedte +/// is al in pixels uitgerekend t.o.v. de grootste waarde, zodat de XAML alleen hoeft te tekenen. +/// Geport van RenH.Uitroller. +/// +public sealed class Cockpitbalk +{ + public string Label { get; set; } = ""; + public double BalkBreedte { get; set; } + public string Weergave { get; set; } = ""; + public Brush Kleur { get; set; } = Brushes.SteelBlue; +} + /// /// Eén regel in de tabel "Laatste wijzigingen": een commit uit de repo, met korte hash, auteur, /// wanneer en de eerste regel van het bericht. diff --git a/src/MijnBesluit.Uitroller/Hoofdvenster.xaml b/src/MijnBesluit.Uitroller/Hoofdvenster.xaml index cc6b25e..0d9f1a2 100644 --- a/src/MijnBesluit.Uitroller/Hoofdvenster.xaml +++ b/src/MijnBesluit.Uitroller/Hoofdvenster.xaml @@ -226,6 +226,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MijnBesluit.Uitroller/Hoofdvenster.xaml.cs b/src/MijnBesluit.Uitroller/Hoofdvenster.xaml.cs index 9c09ddd..1897c5e 100644 --- a/src/MijnBesluit.Uitroller/Hoofdvenster.xaml.cs +++ b/src/MijnBesluit.Uitroller/Hoofdvenster.xaml.cs @@ -1,3 +1,4 @@ +using System.IO; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Controls; @@ -20,6 +21,7 @@ public partial class Hoofdvenster : Window { private readonly Instellingen _instellingen = Instellingen.Lees(); private readonly Uitrolgeheugen _geheugen = Uitrolgeheugen.Lees(); + private readonly Vensterstand _vensterstand = Vensterstand.Lees(); private CancellationTokenSource? _stopper; /// Ververst de Gitea-knoppen vanzelf, zodat "Van Gitea (x)" oplicht als er @@ -34,6 +36,12 @@ public partial class Hoofdvenster : Window { InitializeComponent(); + // Vensterpositie/-grootte van de vorige keer terugzetten (bewaren gebeurt in + // Venster_Closing); het tabblad pas zodra Tabs bestaat (na InitializeComponent). + VensterstandTerugzetten(); + if (_vensterstand.Tabblad >= 0 && _vensterstand.Tabblad < Tabs.Items.Count) + Tabs.SelectedIndex = _vensterstand.Tabblad; + Doelregel.Text = $"Server: {_instellingen.Server}:{_instellingen.Poort} • " + $"Project: {_instellingen.Projectmap}"; @@ -292,6 +300,57 @@ public partial class Hoofdvenster : Window _teststopper?.Cancel(); _test?.Dispose(); _giteaTimer?.Stop(); + VensterstandBewaren(); + } + + /// Zet het venster terug op de onthouden plek en grootte, mits die nog (deels) op een + /// beeldscherm valt (anders opent hij gewoon gecentreerd, zoals in de XAML). Voorkomt dat het + /// venster buiten beeld opent als een collega minder of andere schermen heeft. Geport van + /// RenH.Uitroller. + private void VensterstandTerugzetten() + { + if (!_vensterstand.Bruikbaar) return; + + double schermL = SystemParameters.VirtualScreenLeft; + double schermB = SystemParameters.VirtualScreenTop; + double schermBr = SystemParameters.VirtualScreenWidth; + double schermH = SystemParameters.VirtualScreenHeight; + + // Minstens een stukje van de titelbalk moet grijpbaar blijven binnen het totale schermvlak. + const double kier = 60; + bool zichtbaar = + (_vensterstand.Links + _vensterstand.Breedte) > schermL + kier && + _vensterstand.Links < schermL + schermBr - kier && + (_vensterstand.Boven + _vensterstand.Hoogte) > schermB + kier && + _vensterstand.Boven < schermB + schermH - kier; + + if (!zichtbaar) return; + + WindowStartupLocation = WindowStartupLocation.Manual; + Left = _vensterstand.Links; + Top = _vensterstand.Boven; + Width = _vensterstand.Breedte; + Height = _vensterstand.Hoogte; + if (_vensterstand.Gemaximaliseerd) WindowState = WindowState.Maximized; + } + + /// Bewaart de huidige vensterplek, -grootte en het geopende tabblad. Bij een + /// gemaximaliseerd/geminimaliseerd venster de onderliggende normale maat (RestoreBounds), zodat + /// un-maximaliseren goed terugkomt. Geport van RenH.Uitroller. + private void VensterstandBewaren() + { + Rect b = WindowState == WindowState.Normal + ? new Rect(Left, Top, Width, Height) + : RestoreBounds; + if (b.IsEmpty || b.Width < 1 || b.Height < 1) return; + + _vensterstand.Links = b.Left; + _vensterstand.Boven = b.Top; + _vensterstand.Breedte = b.Width; + _vensterstand.Hoogte = b.Height; + _vensterstand.Gemaximaliseerd = WindowState == WindowState.Maximized; + _vensterstand.Tabblad = Tabs.SelectedIndex; + _vensterstand.Bewaar(); } private void KnopStop_Click(object sender, RoutedEventArgs e) @@ -590,6 +649,8 @@ public partial class Hoofdvenster : Window return new Giteaclient(h.Value.Basis, h.Value.Eigenaar, h.Value.Repo, token); } + /// (Her)start de automatische verversing van de Gitea-knoppen volgens de instelling + /// (Instellingen.GiteaVerversSeconden). 0 = uit; anders minimaal 10 seconden. private void GiteaTimerInstellen() { if (_giteaTimer == null) @@ -598,7 +659,12 @@ public partial class Hoofdvenster : Window _giteaTimer.Tick += GiteaTimer_Tick; } _giteaTimer.Stop(); - _giteaTimer.Interval = TimeSpan.FromSeconds(120); + + int sec = _instellingen.GiteaVerversSeconden; + if (sec <= 0) return; // 0 = automatische verversing uit + if (sec < 10) sec = 10; // ondergrens tegen te druk fetchen + + _giteaTimer.Interval = TimeSpan.FromSeconds(sec); _giteaTimer.Start(); } @@ -1150,6 +1216,17 @@ public partial class Hoofdvenster : Window CockpitStatus.Foreground = CockpitRood; } + // Grafieken: pagina's per project (razor-bestanden in Web/AppUi) en wijzigingen per + // teamlid (commits per auteur, hele geschiedenis). Zelfde eenvoudige staafjes als bij + // RenH.Uitroller (Rectangle met in code uitgerekende pixelbreedte), geen grafiekbibliotheek. + try + { + var (paginas, teamleden) = await Task.Run(() => CockpitStatistiek(map)); + CockpitPaginas.ItemsSource = paginas; + CockpitTeamleden.ItemsSource = teamleden; + } + catch { } + CockpitTijd.Text = $"Laatst ververst: {DateTime.Now:HH:mm:ss}"; // Claim-samenvatting: dezelfde lijst als op Samenwerken, hier alleen als tekst. @@ -1174,6 +1251,103 @@ public partial class Hoofdvenster : Window } } + // Kleuren voor de teamlid-balken (herhaalt zich als er meer teamleden zijn dan kleuren). + // Geport van RenH.Uitroller. + private static readonly Brush[] CockpitPalet = new Brush[] + { + new SolidColorBrush(Color.FromRgb(0x3B, 0x6C, 0xB3)), + new SolidColorBrush(Color.FromRgb(0x2E, 0x7D, 0x32)), + new SolidColorBrush(Color.FromRgb(0xB2, 0x5E, 0x09)), + new SolidColorBrush(Color.FromRgb(0x8E, 0x4E, 0xC8)), + new SolidColorBrush(Color.FromRgb(0x1C, 0x8A, 0x8A)), + new SolidColorBrush(Color.FromRgb(0xB3, 0x26, 0x1A)), + }; + + private const double CockpitBalkMax = 220; // pixels voor de grootste balk + + /// + /// Verzamelt de twee Cockpit-staafgrafieken. "Pagina's per project" telt .razor-bestanden per + /// UI-project (Web en AppUi delen dezelfde schermen, maar hebben elk hun eigen kopie - zie + /// eerder in dit project); "Wijzigingen per teamlid" telt commits per auteur over de hele + /// geschiedenis. Draait in de achtergrond, raakt geen UI aan. + /// + private (List Paginas, List Teamleden) CockpitStatistiek(string projectmap) + { + var projecten = new (string Label, string Map)[] + { + ("MijnBesluit.Web", Path.Combine(projectmap, "src", "MijnBesluit.Web")), + ("MijnBesluit.AppUi", Path.Combine(projectmap, "src", "MijnBesluit.AppUi")), + }; + + var paginaRuw = projecten.Select(p => (p.Label, Aantal: TelPaginas(p.Map))).ToList(); + int paginaMax = paginaRuw.Count == 0 ? 0 : paginaRuw.Max(x => x.Aantal); + var paginas = paginaRuw.Select(x => new Cockpitbalk + { + Label = x.Label, + Weergave = x.Aantal.ToString(), + BalkBreedte = BalkPx(x.Aantal, paginaMax), + Kleur = CockpitPalet[0] + }).ToList(); + + var perAuteur = new Dictionary(StringComparer.OrdinalIgnoreCase); + var log = Gitstand.Voer(projectmap, "log --no-merges --pretty=format:%an", 20); + if (log.Ok) + { + foreach (var regel in log.Uitvoer.Split('\n')) + { + var naam = regel.Trim('\r', ' '); + if (naam.Length == 0) continue; + perAuteur[naam] = perAuteur.TryGetValue(naam, out var n) ? n + 1 : 1; + } + } + + var top = perAuteur.OrderByDescending(kv => kv.Value).ToList(); + int auteurMax = top.Count == 0 ? 0 : top[0].Value; + var teamleden = new List(); + for (int i = 0; i < top.Count; i++) + { + teamleden.Add(new Cockpitbalk + { + Label = top[i].Key, + Weergave = top[i].Value.ToString(), + BalkBreedte = BalkPx(top[i].Value, auteurMax), + Kleur = CockpitPalet[i % CockpitPalet.Length] + }); + } + + return (paginas, teamleden); + } + + private static double BalkPx(int waarde, int max) + { + if (waarde <= 0 || max <= 0) return 0; + return Math.Max(4, waarde / (double)max * CockpitBalkMax); + } + + /// Telt de "pagina's" van een project: *.razor-bestanden, met uitzondering van + /// gegenereerde/externe mappen (bin, obj, .git, node_modules). Geport van RenH.Uitroller + /// (die *.cshtml/*.xaml telt - hier is dat *.razor, MijnBesluit's schermformaat). + private static int TelPaginas(string map) + { + try + { + if (string.IsNullOrWhiteSpace(map) || !Directory.Exists(map)) return 0; + var sep = Path.DirectorySeparatorChar; + int n = 0; + foreach (var f in Directory.EnumerateFiles(map, "*.razor", SearchOption.AllDirectories)) + { + var lp = f.ToLowerInvariant(); + if (lp.Contains(sep + "bin" + sep)) continue; + if (lp.Contains(sep + "obj" + sep)) continue; + if (lp.Contains(sep + "node_modules" + sep)) continue; + if (lp.Contains(sep + ".git" + sep)) continue; + n++; + } + return n; + } + catch { return 0; } + } + /// /// Een klein invoervenster voor één regel tekst (bijv. de commit-omschrijving). WPF heeft geen /// InputBox; dit is de eenvoudigste eigen variant. Null als de gebruiker annuleert. diff --git a/src/MijnBesluit.Uitroller/Instellingen.cs b/src/MijnBesluit.Uitroller/Instellingen.cs index e4bbde3..bba10cc 100644 --- a/src/MijnBesluit.Uitroller/Instellingen.cs +++ b/src/MijnBesluit.Uitroller/Instellingen.cs @@ -20,6 +20,20 @@ public class Instellingen /// Pad naar chrome.exe, als Chrome niet op de gewone plek staat. public string? Chrome { get; set; } + /// + /// Het e-mailadres dat bij "Lokaal testen" alvast op de inlogpagina ingevuld wordt (via + /// ?email=). Leeg laten = niets invullen. Alleen invullen, niet inloggen - de code moet nog + /// steeds met de hand. Geport van RenH.Uitroller. + /// + public string Emailadres { get; set; } = ""; + + /// + /// Elke zoveel seconden het venster vanzelf kijkt of er iets te pushen/op te halen is en de + /// knoppen "Naar Gitea"/"Van Gitea" bijwerkt. 0 = uit. Minimaal 10 seconden (kleinere waarden + /// worden opgehoogd). Geport van RenH.Uitroller. + /// + public int GiteaVerversSeconden { get; set; } = 120; + /// /// Gitea-gegevens (tabblad Ontwikkelen/Samenwerken: Naar/Van Gitea, release maken, claim-bord). /// Normaal hoeft hier niets in te staan: de basis-URL, eigenaar en repo worden afgeleid van de @@ -71,6 +85,21 @@ public class Instellingen public void Bewaar() => File.WriteAllText(Pad, JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true })); + // De vier paden hieronder volgen hetzelfde "map boven de exe, met terugval ernaast"-patroon als + // Pad/PadNaastExe hierboven - los bestand per venster, zodat vensterstand.json niet botst met + // uitroller.json en het instellingenscherm zijn eigen stand apart bewaart. Geport van RenH.Uitroller. + private static string BovenExe(string bestand) + { + var ouder = Directory.GetParent(AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar)); + return ouder is null ? Path.Combine(AppContext.BaseDirectory, bestand) : Path.Combine(ouder.FullName, bestand); + } + + public static string VensterstandPadNaastExe => Path.Combine(AppContext.BaseDirectory, "vensterstand.json"); + public static string VensterstandPad => BovenExe("vensterstand.json"); + + public static string InstellingenVensterPadNaastExe => Path.Combine(AppContext.BaseDirectory, "instellingenvenster.json"); + public static string InstellingenVensterPad => BovenExe("instellingenvenster.json"); + /// /// Wat er mankeert om te kunnen bouwen en inpakken. Null als dat kan. /// diff --git a/src/MijnBesluit.Uitroller/Instellingenvenster.xaml b/src/MijnBesluit.Uitroller/Instellingenvenster.xaml index 11ae0bc..0a06723 100644 --- a/src/MijnBesluit.Uitroller/Instellingenvenster.xaml +++ b/src/MijnBesluit.Uitroller/Instellingenvenster.xaml @@ -173,7 +173,7 @@ - + @@ -182,6 +182,9 @@