mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
launcher: QR code + share buttons on host ready panel
The single Copy button left it on the user to format a message and
get the code into Discord/email. Added a layout that makes sharing
obvious:
- QR encodes the join code text so a friend with a phone can scan
the host's screen and forward the code without typing it
- "Copy as message" formats a one-line "Join {server} on DorkNet!
Download at github.com/... and paste this code:" pitch
- "Email to friend" opens the default mail client with subject +
body prefilled, code in the body
- QR built via QRCoder -> WriteableBitmap directly (no
System.Drawing dependency); palette matches the launcher
This commit is contained in:
@@ -0,0 +1,60 @@
|
|||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
using QRCoder;
|
||||||
|
|
||||||
|
namespace DorkNet.Launcher.Backend;
|
||||||
|
|
||||||
|
/// <summary>Turns a join-code string into a WPF <see cref="BitmapSource"/>
|
||||||
|
/// using QRCoder's raw module matrix. We render the matrix directly to a
|
||||||
|
/// <see cref="WriteableBitmap"/> rather than going through PNG bytes so we
|
||||||
|
/// avoid pulling in System.Drawing.Common (Windows-only, slated for
|
||||||
|
/// trimming in cross-platform .NET).
|
||||||
|
///
|
||||||
|
/// <para>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.</para></summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,4 +20,11 @@
|
|||||||
The Backend/* classes (release downloader, server process, etc.) are
|
The Backend/* classes (release downloader, server process, etc.) are
|
||||||
UI-framework-agnostic and stay unchanged from the PhotinoNET draft. -->
|
UI-framework-agnostic and stay unchanged from the PhotinoNET draft. -->
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- QR rendering for the share panel — generates the code as raw
|
||||||
|
module bytes, MainWindow draws it onto a WPF Image. No System.Drawing
|
||||||
|
dependency that way, so it stays portable. -->
|
||||||
|
<PackageReference Include="QRCoder" Version="1.6.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -235,16 +235,36 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Join code -->
|
<!-- Join code panel — appears once the host is up.
|
||||||
|
QR encodes the same code text; friends can
|
||||||
|
scan with a phone to grab it without typing. -->
|
||||||
<Border x:Name="JoinCodePanel" Visibility="Collapsed"
|
<Border x:Name="JoinCodePanel" Visibility="Collapsed"
|
||||||
Style="{StaticResource Panel}"
|
Style="{StaticResource Panel}"
|
||||||
Background="{StaticResource CardBgHost}">
|
Background="{StaticResource CardBgHost}">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Style="{StaticResource SectionTitle}"
|
<TextBlock Style="{StaticResource SectionTitle}"
|
||||||
Text="🎉 Share this with friends" />
|
Text="🎉 Share this with friends" />
|
||||||
<DockPanel>
|
|
||||||
<Button DockPanel.Dock="Right" Style="{StaticResource PrimaryBtn}"
|
<Grid>
|
||||||
Content="Copy" Click="OnCopyJoinCode" Margin="10,0,0,0" />
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Column="0" Background="White"
|
||||||
|
CornerRadius="10" Padding="8"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
Margin="0,0,16,0">
|
||||||
|
<Image x:Name="JoinCodeQrImage"
|
||||||
|
Width="180" Height="180"
|
||||||
|
RenderOptions.BitmapScalingMode="NearestNeighbor"
|
||||||
|
SnapsToDevicePixels="True"
|
||||||
|
UseLayoutRounding="True" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1">
|
||||||
|
<TextBlock Style="{StaticResource FieldLabel}"
|
||||||
|
Text="JOIN CODE" />
|
||||||
<Border Background="{StaticResource BgDeeper}"
|
<Border Background="{StaticResource BgDeeper}"
|
||||||
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
BorderBrush="{StaticResource Border}" BorderThickness="1"
|
||||||
CornerRadius="10" Padding="12,10">
|
CornerRadius="10" Padding="12,10">
|
||||||
@@ -253,10 +273,25 @@
|
|||||||
Background="Transparent" BorderThickness="0"
|
Background="Transparent" BorderThickness="0"
|
||||||
TextWrapping="Wrap" />
|
TextWrapping="Wrap" />
|
||||||
</Border>
|
</Border>
|
||||||
</DockPanel>
|
|
||||||
|
<WrapPanel Margin="0,12,0,0">
|
||||||
|
<Button Style="{StaticResource PrimaryBtn}"
|
||||||
|
Content="Copy code" Click="OnCopyJoinCode" />
|
||||||
|
<Button Style="{StaticResource Btn}"
|
||||||
|
Content="Copy as message"
|
||||||
|
Click="OnCopyJoinCodeMessage"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
<Button Style="{StaticResource Btn}"
|
||||||
|
Content="Email to friend"
|
||||||
|
Click="OnEmailJoinCode"
|
||||||
|
Margin="10,0,0,0" />
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="11"
|
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="11"
|
||||||
Text="Paste this in their DorkNet launcher's Join screen."
|
TextWrapping="Wrap" Margin="0,12,0,0"
|
||||||
Margin="0,8,0,0" />
|
Text="Your friend opens DorkNet, picks "Join a friend", and pastes the code. The QR encodes the same text — handy for getting it onto a phone for forwarding." />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ public partial class MainWindow : Window
|
|||||||
Name = _state.ServerName,
|
Name = _state.ServerName,
|
||||||
});
|
});
|
||||||
JoinCodeText.Text = code;
|
JoinCodeText.Text = code;
|
||||||
|
JoinCodeQrImage.Source = QrCodeRenderer.Render(code);
|
||||||
JoinCodePanel.Visibility = Visibility.Visible;
|
JoinCodePanel.Visibility = Visibility.Visible;
|
||||||
HostStatusText.Text = _state.HostingMode == HostingMode.LocalNetwork
|
HostStatusText.Text = _state.HostingMode == HostingMode.LocalNetwork
|
||||||
? $"Hosting on your local network at {_hostApex}. Share the join code with friends on the same WiFi."
|
? $"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); }
|
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 ───────────────────────────────────────────────────────
|
// ── Join flow ───────────────────────────────────────────────────────
|
||||||
private void OnJoinCodeInputChanged(object sender, TextChangedEventArgs e)
|
private void OnJoinCodeInputChanged(object sender, TextChangedEventArgs e)
|
||||||
=> RefreshJoinReadiness();
|
=> RefreshJoinReadiness();
|
||||||
|
|||||||
Reference in New Issue
Block a user