diff --git a/launcher/Backend/QrCodeRenderer.cs b/launcher/Backend/QrCodeRenderer.cs
new file mode 100644
index 0000000..9cb7f59
--- /dev/null
+++ b/launcher/Backend/QrCodeRenderer.cs
@@ -0,0 +1,60 @@
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using QRCoder;
+
+namespace DorkNet.Launcher.Backend;
+
+/// Turns a join-code string into a WPF
+/// using QRCoder's raw module matrix. We render the matrix directly to a
+/// rather than going through PNG bytes so we
+/// avoid pulling in System.Drawing.Common (Windows-only, slated for
+/// trimming in cross-platform .NET).
+///
+/// Colour choice matches the launcher palette: white (Ink) modules
+/// on the deep purple-navy (BgDeeper) background so the QR is legible
+/// against the dark panel.
+public static class QrCodeRenderer
+{
+ public static BitmapSource Render(string text, int pixelSize = 220)
+ {
+ // QRCodeGenerator -> QRCodeData (matrix of bools) -> WriteableBitmap.
+ using var gen = new QRCodeGenerator();
+ using var data = gen.CreateQrCode(text, QRCodeGenerator.ECCLevel.M);
+ var modules = data.ModuleMatrix;
+ var moduleCount = modules.Count;
+ if (moduleCount == 0)
+ throw new InvalidOperationException("QR generation produced an empty matrix.");
+
+ // Scale up to the requested pixel size — int division so each
+ // module is an exact pixel count (avoids subpixel blur). Quiet
+ // zone (4-module margin) is part of QRCodeData already.
+ var moduleSize = Math.Max(1, pixelSize / moduleCount);
+ var dim = moduleCount * moduleSize;
+
+ var bmp = new WriteableBitmap(dim, dim, 96, 96, PixelFormats.Bgra32, null);
+ var stride = dim * 4;
+ var pixels = new byte[dim * stride];
+
+ // Light pixel = Ink (#F7F3FF), dark pixel = BgDeeper (#0E0A1F).
+ // Order is BGRA in the buffer.
+ var light = new byte[] { 0xFF, 0xF3, 0xF7, 0xFF };
+ var dark = new byte[] { 0x1F, 0x0A, 0x0E, 0xFF };
+
+ for (int y = 0; y < moduleCount; y++)
+ for (int x = 0; x < moduleCount; x++)
+ {
+ var on = modules[y][x];
+ var src = on ? dark : light;
+ for (int py = 0; py < moduleSize; py++)
+ for (int px = 0; px < moduleSize; px++)
+ {
+ var offset = ((y * moduleSize + py) * stride) + ((x * moduleSize + px) * 4);
+ Buffer.BlockCopy(src, 0, pixels, offset, 4);
+ }
+ }
+
+ bmp.WritePixels(new System.Windows.Int32Rect(0, 0, dim, dim), pixels, stride, 0);
+ bmp.Freeze();
+ return bmp;
+ }
+}
diff --git a/launcher/DorkNet.Launcher.csproj b/launcher/DorkNet.Launcher.csproj
index 54563f7..2eaf223 100644
--- a/launcher/DorkNet.Launcher.csproj
+++ b/launcher/DorkNet.Launcher.csproj
@@ -20,4 +20,11 @@
The Backend/* classes (release downloader, server process, etc.) are
UI-framework-agnostic and stay unchanged from the PhotinoNET draft. -->
+
+
+
+
+
diff --git a/launcher/MainWindow.xaml b/launcher/MainWindow.xaml
index e958a6a..cae9de4 100644
--- a/launcher/MainWindow.xaml
+++ b/launcher/MainWindow.xaml
@@ -235,28 +235,63 @@
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/launcher/MainWindow.xaml.cs b/launcher/MainWindow.xaml.cs
index fce1c62..a97a0d9 100644
--- a/launcher/MainWindow.xaml.cs
+++ b/launcher/MainWindow.xaml.cs
@@ -331,6 +331,7 @@ public partial class MainWindow : Window
Name = _state.ServerName,
});
JoinCodeText.Text = code;
+ JoinCodeQrImage.Source = QrCodeRenderer.Render(code);
JoinCodePanel.Visibility = Visibility.Visible;
HostStatusText.Text = _state.HostingMode == HostingMode.LocalNetwork
? $"Hosting on your local network at {_hostApex}. Share the join code with friends on the same WiFi."
@@ -408,6 +409,38 @@ public partial class MainWindow : Window
catch (Exception ex) { ShowError("Copy failed: " + ex.Message); }
}
+ private void OnCopyJoinCodeMessage(object sender, RoutedEventArgs e)
+ {
+ var name = string.IsNullOrWhiteSpace(_state.ServerName)
+ ? "my DorkNet server"
+ : _state.ServerName;
+ var msg =
+ $"Join {name} on DorkNet! Grab the launcher at " +
+ "https://github.com/DorkSquadRR/DorkNet and paste this code:\n\n" +
+ JoinCodeText.Text;
+ try { Clipboard.SetText(msg); }
+ catch (Exception ex) { ShowError("Copy failed: " + ex.Message); }
+ }
+
+ private void OnEmailJoinCode(object sender, RoutedEventArgs e)
+ {
+ var name = string.IsNullOrWhiteSpace(_state.ServerName)
+ ? "my DorkNet server"
+ : _state.ServerName;
+ // mailto: body is URL-encoded; Uri.EscapeDataString handles
+ // the newlines + code text without trashing slashes or =.
+ var subject = Uri.EscapeDataString($"Join {name} on DorkNet");
+ var body = Uri.EscapeDataString(
+ $"Hey! Want to play on {name}?\n\n" +
+ "1. Download the DorkNet launcher: https://github.com/DorkSquadRR/DorkNet\n" +
+ "2. Open it and pick \"Join a friend\"\n" +
+ "3. Paste this code:\n\n" +
+ JoinCodeText.Text);
+ try { Process.Start(new ProcessStartInfo($"mailto:?subject={subject}&body={body}")
+ { UseShellExecute = true }); }
+ catch (Exception ex) { ShowError("Email failed: " + ex.Message); }
+ }
+
// ── Join flow ───────────────────────────────────────────────────────
private void OnJoinCodeInputChanged(object sender, TextChangedEventArgs e)
=> RefreshJoinReadiness();