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 @@
+
+
+
@@ -204,6 +207,9 @@
+
+
+
diff --git a/src/MijnBesluit.Uitroller/Instellingenvenster.xaml.cs b/src/MijnBesluit.Uitroller/Instellingenvenster.xaml.cs
index be5354f..b5e06bc 100644
--- a/src/MijnBesluit.Uitroller/Instellingenvenster.xaml.cs
+++ b/src/MijnBesluit.Uitroller/Instellingenvenster.xaml.cs
@@ -34,6 +34,8 @@ public partial class Instellingenvenster : Window
VeldGiteaUrl.Text = _instellingen.GiteaUrl ?? "";
VeldGiteaEigenaar.Text = _instellingen.GiteaEigenaar ?? "";
VeldGiteaRepo.Text = _instellingen.GiteaRepo ?? "";
+ VeldEmail.Text = _instellingen.Emailadres;
+ VeldGiteaVerversen.Text = _instellingen.GiteaVerversSeconden.ToString();
}
private static void Kies(System.Windows.Controls.TextBox veld)
@@ -65,6 +67,13 @@ public partial class Instellingenvenster : Window
return;
}
+ if (!int.TryParse(VeldGiteaVerversen.Text.Trim(), out var verversen) || verversen < 0)
+ {
+ MessageBox.Show(this, "Auto-verversen moet 0 (uit) of een positief aantal seconden zijn.", "Instellingen",
+ MessageBoxButton.OK, MessageBoxImage.Warning);
+ return;
+ }
+
_instellingen.Server = VeldServer.Text.Trim();
_instellingen.Poort = poort;
_instellingen.Geheim = VeldGeheim.Text.Trim();
@@ -75,6 +84,8 @@ public partial class Instellingenvenster : Window
_instellingen.GiteaUrl = string.IsNullOrWhiteSpace(VeldGiteaUrl.Text) ? null : VeldGiteaUrl.Text.Trim();
_instellingen.GiteaEigenaar = string.IsNullOrWhiteSpace(VeldGiteaEigenaar.Text) ? null : VeldGiteaEigenaar.Text.Trim();
_instellingen.GiteaRepo = string.IsNullOrWhiteSpace(VeldGiteaRepo.Text) ? null : VeldGiteaRepo.Text.Trim();
+ _instellingen.Emailadres = VeldEmail.Text.Trim();
+ _instellingen.GiteaVerversSeconden = verversen;
_instellingen.Bewaar();
DialogResult = true;
diff --git a/src/MijnBesluit.Uitroller/Lokaaltest.cs b/src/MijnBesluit.Uitroller/Lokaaltest.cs
index 7eae232..9a6e258 100644
--- a/src/MijnBesluit.Uitroller/Lokaaltest.cs
+++ b/src/MijnBesluit.Uitroller/Lokaaltest.cs
@@ -215,6 +215,11 @@ public sealed class Lokaaltest : IDisposable
{
var adres = _instellingen.Testadres.TrimEnd('/') + "/";
+ // Het e-mailadres alvast meegeven, zodat het op de inlogpagina ingevuld staat (de verse
+ // profielmap heeft geen onthoud-cookie). Alleen invullen, niet inloggen.
+ if (!string.IsNullOrWhiteSpace(_instellingen.Emailadres))
+ adres += "?email=" + Uri.EscapeDataString(_instellingen.Emailadres.Trim());
+
var chrome = new[]
{
_instellingen.Chrome,
diff --git a/src/MijnBesluit.Uitroller/Vensterstand.cs b/src/MijnBesluit.Uitroller/Vensterstand.cs
new file mode 100644
index 0000000..ab3f117
--- /dev/null
+++ b/src/MijnBesluit.Uitroller/Vensterstand.cs
@@ -0,0 +1,62 @@
+using System.IO;
+using System.Text.Json;
+
+namespace MijnBesluit.Uitroller;
+
+///
+/// De positie en grootte van een venster, onthouden tussen sessies. Los bestand
+/// (vensterstand.json, resp. instellingenvenster.json) in de map boven de exe, net als
+/// uitroller.json - zo botst het niet met de instellingen en overleeft het een build. Wordt bij
+/// het sluiten geschreven en bij het openen teruggezet, mits het venster nog (deels) op een
+/// beeldscherm valt. Geport van RenH.Uitroller.
+///
+public class Vensterstand
+{
+ public double Links { get; set; }
+ public double Boven { get; set; }
+ public double Breedte { get; set; }
+ public double Hoogte { get; set; }
+ public bool Gemaximaliseerd { get; set; }
+ public int Tabblad { get; set; }
+
+ /// Is er iets bruikbaars onthouden (een echte breedte/hoogte)?
+ public bool Bruikbaar => Breedte >= 200 && Hoogte >= 150;
+
+ public static Vensterstand Lees(string? pad = null, string? padNaastExe = null)
+ {
+ // Blijvende plek boven de exe; bestaat die nog niet maar wel de oude naast de exe, dan
+ // die eenmalig overnemen zodat de vensterstand niet verloren gaat bij de overstap. Met een
+ // eigen pad kan een tweede venster (bv. het instellingenscherm) zijn eigen stand bewaren.
+ pad ??= Instellingen.VensterstandPad;
+ padNaastExe ??= Instellingen.VensterstandPadNaastExe;
+ if (!File.Exists(pad) && File.Exists(padNaastExe))
+ {
+ try { File.Copy(padNaastExe, pad); } catch { }
+ }
+ if (!File.Exists(pad)) return new Vensterstand();
+
+ try
+ {
+ return JsonSerializer.Deserialize(File.ReadAllText(pad))
+ ?? new Vensterstand();
+ }
+ catch
+ {
+ return new Vensterstand();
+ }
+ }
+
+ public void Bewaar(string? pad = null)
+ {
+ pad ??= Instellingen.VensterstandPad;
+ try
+ {
+ File.WriteAllText(pad,
+ JsonSerializer.Serialize(this, new JsonSerializerOptions { WriteIndented = true }));
+ }
+ catch
+ {
+ // Vensterpositie niet kunnen onthouden mag het afsluiten nooit in de weg zitten.
+ }
+ }
+}
diff --git a/src/MijnBesluit.Web/Components/Pages/Inloggen.razor b/src/MijnBesluit.Web/Components/Pages/Inloggen.razor
index 307f056..c73a569 100644
--- a/src/MijnBesluit.Web/Components/Pages/Inloggen.razor
+++ b/src/MijnBesluit.Web/Components/Pages/Inloggen.razor
@@ -101,6 +101,11 @@
[SupplyParameterFromForm(FormName = "nieuwe-code")] private AanvraagGegevens Opnieuw { get; set; } = new();
[SupplyParameterFromQuery(Name = "terug")] private string? Terug { get; set; }
+ /// Alleen voor lokaal testen (zie MijnBesluit.Uitroller): het e-mailadres alvast
+ /// invullen op basis van ?email=, zodat je niet elke test opnieuw hoeft te typen. Vult alleen
+ /// in, logt niet in.
+ [SupplyParameterFromQuery(Name = "email")] private string? EmailQuery { get; set; }
+
private bool _codeVerstuurd;
private string? AfgeschermdAdres;
private string? Fout, Gelukt;
@@ -115,6 +120,9 @@
protected override void OnParametersSet()
{
+ if (!string.IsNullOrWhiteSpace(EmailQuery) && string.IsNullOrWhiteSpace(Aanvraag.Email))
+ Aanvraag.Email = EmailQuery;
+
if (AfgeschermdAdres is null && ToonCodeveld)
{
var adres = !string.IsNullOrWhiteSpace(Inlog.Email) ? Inlog.Email : Opnieuw.Email;