Add installer updater and server CLI

This commit is contained in:
Alexa
2026-05-27 20:54:53 +03:00
parent 4f44505759
commit 7852b410ee
25 changed files with 4153 additions and 703 deletions
+626 -176
View File
@@ -5,84 +5,103 @@
StartupUri="MainWindow.xaml">
<Application.Resources>
<!-- ── Palette ─────────────────────────────────────────────────
Rec Room-adjacent — warm coral + teal on a deep purple-navy.
Avoids the cold-grey "developer tool" look in favour of the
friendly game-launcher aesthetic. -->
<SolidColorBrush x:Key="Bg" Color="#16102A" />
<SolidColorBrush x:Key="BgDeeper" Color="#0E0A1F" />
<SolidColorBrush x:Key="BgElev" Color="#221833" />
<SolidColorBrush x:Key="BgElev2" Color="#2E2245" />
<SolidColorBrush x:Key="Border" Color="#3A2C56" />
<SolidColorBrush x:Key="BorderSoft" Color="#2A2042" />
<SolidColorBrush x:Key="Ink" Color="#F7F3FF" />
<SolidColorBrush x:Key="InkSoft" Color="#B8AED4" />
<SolidColorBrush x:Key="InkFaint" Color="#6E608E" />
<!-- Slate base, one decisive accent. The previous palette stacked
coral + teal + sunshine + pink which read as designer-mockup;
a single warm accent on a near-neutral background reads as a
real tool. -->
<SolidColorBrush x:Key="Bg" Color="#0D0F14" />
<SolidColorBrush x:Key="BgDeeper" Color="#07090C" />
<SolidColorBrush x:Key="BgElev" Color="#14171F" />
<SolidColorBrush x:Key="BgElev2" Color="#1C2029" />
<SolidColorBrush x:Key="BgInset" Color="#0A0C11" />
<SolidColorBrush x:Key="Border" Color="#262B38" />
<SolidColorBrush x:Key="BorderSoft" Color="#1A1E27" />
<SolidColorBrush x:Key="BorderStrong" Color="#343A4B" />
<SolidColorBrush x:Key="Ink" Color="#ECEEF3" />
<SolidColorBrush x:Key="InkSoft" Color="#8B95AB" />
<SolidColorBrush x:Key="InkFaint" Color="#4F596F" />
<!-- Brand: warm coral, with teal as the secondary accent. -->
<SolidColorBrush x:Key="Brand" Color="#FF7B5C" />
<SolidColorBrush x:Key="BrandHover" Color="#FF9277" />
<SolidColorBrush x:Key="BrandDeep" Color="#E55F40" />
<SolidColorBrush x:Key="Teal" Color="#44D6C4" />
<SolidColorBrush x:Key="TealDeep" Color="#2DB8A6" />
<SolidColorBrush x:Key="Sunshine" Color="#FFD555" />
<SolidColorBrush x:Key="Ok" Color="#5BE0A6" />
<SolidColorBrush x:Key="Danger" Color="#FF6B8A" />
<!-- Single brand accent — vermilion, used decisively on the
primary action only. Secondary/info uses cool steel; success
a single mint. Nothing else gets a colour. -->
<SolidColorBrush x:Key="Brand" Color="#FF5C3D" />
<SolidColorBrush x:Key="BrandHover" Color="#FF7659" />
<SolidColorBrush x:Key="BrandDeep" Color="#D9482B" />
<SolidColorBrush x:Key="BrandSoft" Color="#3A1F1A" />
<SolidColorBrush x:Key="Teal" Color="#5C9BE0" />
<SolidColorBrush x:Key="TealDeep" Color="#3C7CC0" />
<SolidColorBrush x:Key="Sunshine" Color="#E5B340" />
<SolidColorBrush x:Key="Ok" Color="#34D399" />
<SolidColorBrush x:Key="Danger" Color="#F87171" />
<!-- Wordmark gradient: coral → pink → teal across the "DorkNet" letters. -->
<LinearGradientBrush x:Key="WordmarkBrush" StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#FF7B5C" Offset="0" />
<GradientStop Color="#FF5C95" Offset="0.55" />
<GradientStop Color="#44D6C4" Offset="1" />
</LinearGradientBrush>
<!-- Wordmark: flat, decisive, one tone. The "·" between Dork and
Net is brand-coloured — a single small accent instead of a
rainbow gradient across the letters. -->
<SolidColorBrush x:Key="WordmarkBrush" Color="#ECEEF3" />
<!-- Subtle card-back gradient — radial illusion via linear top-left
tint into the base elev colour. -->
<LinearGradientBrush x:Key="CardBg" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#2A1F40" Offset="0" />
<GradientStop Color="#221833" Offset="1" />
</LinearGradientBrush>
<!-- Panel/card backgrounds — flat colours with subtle structure
instead of gradients that scream "designer mockup". The Host
and Join variants gain a faint accent tint so the eye lands
on the right area without the page turning into a rainbow. -->
<SolidColorBrush x:Key="CardBg" Color="#14171F" />
<SolidColorBrush x:Key="CardBgHost" Color="#181820" />
<SolidColorBrush x:Key="CardBgJoin" Color="#161A22" />
<LinearGradientBrush x:Key="CardBgHost" StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#3A1F35" Offset="0" />
<GradientStop Color="#221833" Offset="1" />
</LinearGradientBrush>
<!-- Subtle shadow on cards. The previous coral-tinted shadow on
every button was another designer-mockup tell — kept this
one neutral. -->
<DropShadowEffect x:Key="CardShadow" BlurRadius="20" ShadowDepth="0"
Color="#000000" Opacity="0.55" />
<DropShadowEffect x:Key="BtnShadow" BlurRadius="14" ShadowDepth="0"
Color="#FF5C3D" Opacity="0.32" />
<LinearGradientBrush x:Key="CardBgJoin" StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#1F3540" Offset="0" />
<GradientStop Color="#221833" Offset="1" />
</LinearGradientBrush>
<!-- Soft drop-shadow effect used on cards / primary buttons. -->
<DropShadowEffect x:Key="CardShadow" BlurRadius="24" ShadowDepth="0"
Color="#000000" Opacity="0.45" />
<DropShadowEffect x:Key="BtnShadow" BlurRadius="16" ShadowDepth="2"
Color="#FF5C40" Opacity="0.35" />
<!-- Shared vector glyphs. Path geometry instead of emoji
everywhere — emoji is one of the strongest "AI-generated UI"
tells, and these path strings ship at any zoom level. -->
<Geometry x:Key="IconServer">M2,4 L18,4 L18,8 L2,8 Z M2,10 L18,10 L18,14 L2,14 Z M5,6 L6,6 M5,12 L6,12 M15,6 L16,6 M15,12 L16,12</Geometry>
<Geometry x:Key="IconLink">M8,12 L12,8 M5,10 A4,4 0 0 1 9,6 L11,6 M11,14 L9,14 A4,4 0 0 1 5,10 M15,10 A4,4 0 0 1 11,14 M11,6 A4,4 0 0 1 15,10</Geometry>
<Geometry x:Key="IconGear">M10,1 L11,3.5 L13.5,2.5 L14,5 L16.5,5 L15.5,7.5 L18,9 L16,11 L17,13.5 L14.5,13.5 L14,16 L11.5,15 L10,17 L8.5,15 L6,16 L5.5,13.5 L3,13.5 L4,11 L2,9 L4.5,7.5 L3.5,5 L6,5 L6.5,2.5 L9,3.5 Z M10,7 A2,2 0 1 0 10,11 A2,2 0 1 0 10,7</Geometry>
<Geometry x:Key="IconFolder">M2,5 L7,5 L9,7 L18,7 L18,15 L2,15 Z</Geometry>
<Geometry x:Key="IconPlay">M5,3 L16,9 L5,15 Z</Geometry>
<Geometry x:Key="IconCheck">M3,9 L7,13 L15,5</Geometry>
<Geometry x:Key="IconX">M4,4 L14,14 M14,4 L4,14</Geometry>
<Geometry x:Key="IconArrowRight">M3,9 L15,9 M11,5 L15,9 L11,13</Geometry>
<Geometry x:Key="IconArrowLeft">M15,9 L3,9 M7,5 L3,9 L7,13</Geometry>
<Geometry x:Key="IconCopy">M5,4 L13,4 L13,12 L5,12 Z M3,6 L3,14 L11,14</Geometry>
<Geometry x:Key="IconMail">M2,5 L18,5 L18,15 L2,15 Z M2,5 L10,11 L18,5</Geometry>
<Geometry x:Key="IconExternal">M11,3 L17,3 L17,9 M17,3 L9,11 M15,11 L15,17 L3,17 L3,5 L9,5</Geometry>
<Geometry x:Key="IconDot">M10,8 A2,2 0 1 0 10,12 A2,2 0 1 0 10,8</Geometry>
<Geometry x:Key="IconQuestion">M7,7 A3,3 0 1 1 10,10 L10,12 M10,15 L10,15.5</Geometry>
<!-- ── TextBox ─────────────────────────────────────────────────── -->
<Style TargetType="TextBox">
<Setter Property="Background" Value="{StaticResource BgElev}" />
<Setter Property="Background" Value="{StaticResource BgInset}" />
<Setter Property="Foreground" Value="{StaticResource Ink}" />
<Setter Property="CaretBrush" Value="{StaticResource Ink}" />
<Setter Property="CaretBrush" Value="{StaticResource Brand}" />
<Setter Property="SelectionBrush" Value="{StaticResource Brand}" />
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="BorderThickness" Value="1.5" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="12,10" />
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="TextBox">
<Border Background="{TemplateBinding Background}"
<Border x:Name="root"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10">
CornerRadius="6">
<ScrollViewer x:Name="PART_ContentHost"
Margin="{TemplateBinding Padding}"
VerticalScrollBarVisibility="Auto" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="root" Property="BorderBrush" Value="{StaticResource BorderStrong}" />
</Trigger>
<Trigger Property="IsFocused" Value="True">
<Setter Property="BorderBrush" Value="{StaticResource Brand}" />
<Setter TargetName="root" Property="BorderBrush" Value="{StaticResource Brand}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
@@ -92,28 +111,227 @@
<!-- ── ComboBox ────────────────────────────────────────────────── -->
<Style TargetType="ComboBox">
<Setter Property="Background" Value="{StaticResource BgElev}" />
<Setter Property="Background" Value="{StaticResource BgInset}" />
<Setter Property="Foreground" Value="{StaticResource Ink}" />
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="BorderThickness" Value="1.5" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="12,10" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
<Setter Property="MinHeight" Value="40" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<ToggleButton x:Name="Toggle"
Background="{Binding Background, RelativeSource={RelativeSource TemplatedParent}}"
Foreground="{Binding Foreground, RelativeSource={RelativeSource TemplatedParent}}"
BorderBrush="{Binding BorderBrush, RelativeSource={RelativeSource TemplatedParent}}"
BorderThickness="{Binding BorderThickness, RelativeSource={RelativeSource TemplatedParent}}"
Padding="{Binding Padding, RelativeSource={RelativeSource TemplatedParent}}"
Focusable="False"
ClickMode="Press"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="Chrome"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6">
<Grid>
<ContentPresenter x:Name="ContentSite"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Content="{Binding SelectionBoxItem, RelativeSource={RelativeSource AncestorType=ComboBox}}"
ContentTemplate="{Binding SelectionBoxItemTemplate, RelativeSource={RelativeSource AncestorType=ComboBox}}"
ContentStringFormat="{Binding SelectionBoxItemStringFormat, RelativeSource={RelativeSource AncestorType=ComboBox}}"
RecognizesAccessKey="True" />
<Path Width="8" Height="5"
Margin="0,0,14,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Stroke="{StaticResource InkSoft}"
StrokeThickness="1.5"
StrokeLineJoin="Round"
Data="M 0 0 L 4 5 L 8 0" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Chrome" Property="BorderBrush" Value="{StaticResource BorderStrong}" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Chrome" Property="BorderBrush" Value="{StaticResource Brand}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<Popup x:Name="PART_Popup"
IsOpen="{TemplateBinding IsDropDownOpen}"
Placement="Bottom"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Fade">
<Grid MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
<Border Background="{StaticResource BgElev}"
BorderBrush="{StaticResource Border}"
BorderThickness="1"
CornerRadius="6"
Margin="0,4,0,0"
Effect="{StaticResource CardShadow}">
<ScrollViewer Margin="4"
CanContentScroll="True">
<ItemsPresenter />
</ScrollViewer>
</Border>
</Grid>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource InkFaint}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ComboBoxItem">
<Setter Property="Background" Value="{StaticResource BgElev}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource Ink}" />
<Setter Property="Padding" Value="10,8" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border x:Name="ItemChrome"
Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="ItemChrome" Property="Background" Value="{StaticResource BgElev2}" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="ItemChrome" Property="Background" Value="{StaticResource BrandSoft}" />
<Setter Property="Foreground" Value="{StaticResource Ink}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource InkFaint}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── RadioButton ─────────────────────────────────────────────── -->
<Style TargetType="RadioButton">
<Setter Property="Foreground" Value="{StaticResource Ink}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Padding" Value="10,8" />
<Setter Property="Margin" Value="0,0,0,2" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="row"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="1"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Grid Width="16" Height="16" Margin="0,0,10,0">
<Ellipse x:Name="ring"
Stroke="{StaticResource BorderStrong}"
StrokeThickness="1.5" />
<Ellipse x:Name="dot"
Width="6" Height="6"
Fill="{StaticResource Brand}"
Opacity="0" />
</Grid>
<ContentPresenter VerticalAlignment="Center" />
</StackPanel>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="row" Property="Background" Value="{StaticResource BgElev}" />
<Setter TargetName="ring" Property="Stroke" Value="{StaticResource Brand}" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="row" Property="Background" Value="{StaticResource BgElev}" />
<Setter TargetName="row" Property="BorderBrush" Value="{StaticResource BorderStrong}" />
<Setter TargetName="ring" Property="Stroke" Value="{StaticResource Brand}" />
<Setter TargetName="dot" Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── CheckBox ───────────────────────────────────────────────── -->
<Style TargetType="CheckBox">
<Setter Property="Foreground" Value="{StaticResource Ink}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="CheckBox">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<Border x:Name="box"
Width="16" Height="16" Margin="0,0,10,0"
Background="{StaticResource BgInset}"
BorderBrush="{StaticResource BorderStrong}"
BorderThickness="1.5"
CornerRadius="3">
<Path x:Name="tick"
Data="M 3 8 L 6.5 11 L 13 4"
Stroke="White"
StrokeThickness="1.8"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Opacity="0" />
</Border>
<ContentPresenter VerticalAlignment="Center" />
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource Brand}" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="box" Property="Background" Value="{StaticResource Brand}" />
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource Brand}" />
<Setter TargetName="tick" Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── Buttons ─────────────────────────────────────────────────── -->
<Style x:Key="Btn" TargetType="Button">
<Setter Property="Background" Value="{StaticResource BgElev2}" />
<Setter Property="Background" Value="{StaticResource BgElev}" />
<Setter Property="Foreground" Value="{StaticResource Ink}" />
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="BorderThickness" Value="1.5" />
<Setter Property="Padding" Value="16,10" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="16,9" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
@@ -121,92 +339,141 @@
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
<Border x:Name="root"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10">
CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="{TemplateBinding Padding}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="root" Property="Background" Value="{StaticResource BgElev2}" />
<Setter TargetName="root" Property="BorderBrush" Value="{StaticResource BorderStrong}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="root" Property="Background" Value="{StaticResource BgInset}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource InkFaint}" />
<Setter TargetName="root" Property="Background" Value="{StaticResource BgElev}" />
<Setter TargetName="root" Property="BorderBrush" Value="{StaticResource BorderSoft}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource Border}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderSoft}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource InkFaint}" />
<Setter Property="Background" Value="{StaticResource BgElev}" />
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="PrimaryBtn" TargetType="Button" BasedOn="{StaticResource Btn}">
<Style x:Key="PrimaryBtn" TargetType="Button">
<Setter Property="Background" Value="{StaticResource Brand}" />
<Setter Property="BorderBrush" Value="{StaticResource BrandDeep}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="22,12" />
<Setter Property="FontSize" Value="14" />
<Setter Property="BorderBrush" Value="{StaticResource Brand}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="22,11" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Effect" Value="{StaticResource BtnShadow}" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{StaticResource BrandHover}" />
<Setter Property="BorderBrush" Value="{StaticResource Brand}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{StaticResource BgElev2}" />
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="Foreground" Value="{StaticResource InkFaint}" />
<Setter Property="Effect" Value="{x:Null}" />
</Trigger>
</Style.Triggers>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="root"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="{TemplateBinding Padding}"
TextElement.Foreground="{TemplateBinding Foreground}"
TextElement.FontWeight="{TemplateBinding FontWeight}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="root" Property="Background" Value="{StaticResource BrandHover}" />
<Setter TargetName="root" Property="BorderBrush" Value="{StaticResource BrandHover}" />
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="root" Property="Background" Value="{StaticResource BrandDeep}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="root" Property="Background" Value="{StaticResource BgElev}" />
<Setter TargetName="root" Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="Foreground" Value="{StaticResource InkFaint}" />
<Setter Property="Effect" Value="{x:Null}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Big Play button — reserved for "Launch Rec Room". Bigger than
PrimaryBtn, with stronger presence — the visual anchor of the
ready-to-play view. -->
<Style x:Key="PlayBtn" TargetType="Button" BasedOn="{StaticResource PrimaryBtn}">
<Setter Property="Padding" Value="32,14" />
<Setter Property="FontSize" Value="15" />
<Setter Property="FontWeight" Value="Bold" />
</Style>
<!-- "TealBtn" key kept for PhotonWizard compatibility, but now a
secondary action style (steel blue, not teal). -->
<Style x:Key="TealBtn" TargetType="Button" BasedOn="{StaticResource Btn}">
<Setter Property="Background" Value="{StaticResource Teal}" />
<Setter Property="BorderBrush" Value="{StaticResource TealDeep}" />
<Setter Property="Foreground" Value="#0E0A1F" />
<Setter Property="Padding" Value="22,12" />
<Setter Property="FontSize" Value="14" />
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#5DE8D6" />
</Trigger>
</Style.Triggers>
<Setter Property="Foreground" Value="White" />
<Setter Property="Padding" Value="22,11" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style x:Key="GhostBtn" TargetType="Button" BasedOn="{StaticResource Btn}">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource InkSoft}" />
<Setter Property="Padding" Value="14,8" />
</Style>
<!-- ── Card buttons for first-run wizard ───────────────────────── -->
<!-- ── Choice cards (Host vs Join wizard step) ─────────────────── -->
<Style x:Key="ChoiceCardBtn" TargetType="Button">
<Setter Property="Background" Value="{StaticResource CardBg}" />
<Setter Property="Background" Value="{StaticResource BgElev}" />
<Setter Property="Foreground" Value="{StaticResource Ink}" />
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="BorderThickness" Value="1.5" />
<Setter Property="Padding" Value="28" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="28,26" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Stretch" />
<Setter Property="Effect" Value="{StaticResource CardShadow}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="cardBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="18">
<ContentPresenter Margin="{TemplateBinding Padding}" />
</Border>
<Grid>
<Border x:Name="cardBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10">
<ContentPresenter Margin="{TemplateBinding Padding}" />
</Border>
<!-- Accent stripe on hover — appears at the
top-left, decisive single mark instead of
a glow shadow surrounding the card. -->
<Border x:Name="stripe"
Width="3" HorizontalAlignment="Left"
Margin="0,16" Background="{StaticResource Brand}"
Opacity="0" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="cardBorder" Property="BorderBrush"
Value="{StaticResource Brand}" />
Value="{StaticResource BorderStrong}" />
<Setter TargetName="cardBorder" Property="Background"
Value="{StaticResource BgElev2}" />
<Setter TargetName="stripe" Property="Opacity" Value="1" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
@@ -219,41 +486,86 @@
<Setter Property="Background" Value="{StaticResource BgElev}" />
<Setter Property="BorderBrush" Value="{StaticResource BorderSoft}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="14" />
<Setter Property="Padding" Value="20" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="22" />
</Style>
<!-- ── Field label text ────────────────────────────────────────── -->
<!-- Section header used inside panels — small uppercase tag plus
title text. The eyebrow tag adds the "real product"
information density we want. -->
<Style x:Key="FieldLabel" TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource InkSoft}" />
<Setter Property="FontSize" Value="11" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="FontSize" Value="10" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Margin" Value="0,0,0,8" />
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
</Style>
<Style x:Key="SectionTitle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource Ink}" />
<Setter Property="FontSize" Value="16" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Margin" Value="0,0,0,12" />
<Setter Property="Margin" Value="0,0,0,14" />
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
</Style>
<!-- Numbered-step bullet used in walkthrough text. -->
<Style x:Key="StepBullet" TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource Ink}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="LineHeight" Value="22" />
<Setter Property="LineHeight" Value="20" />
<Setter Property="Margin" Value="0,0,0,6" />
<Setter Property="TextWrapping" Value="Wrap" />
</Style>
<!-- Pill-style "status chip" used in section headers. -->
<!-- Status chip — small uppercase pill with letter-spacing. -->
<Style x:Key="StatusChip" TargetType="Border">
<Setter Property="Background" Value="{StaticResource BgElev2}" />
<Setter Property="CornerRadius" Value="999" />
<Setter Property="Padding" Value="10,4" />
<Setter Property="Background" Value="{StaticResource BgElev}" />
<Setter Property="BorderBrush" Value="{StaticResource Border}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="4" />
<Setter Property="Padding" Value="8,3" />
</Style>
<!-- Sidebar nav button — a flat row with an icon + label. Active
state is a single brand-coloured edge stripe on the left, not
a filled pill. -->
<Style x:Key="NavBtn" TargetType="RadioButton">
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Foreground" Value="{StaticResource InkSoft}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
<Setter Property="Margin" Value="0,0,0,2" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Grid>
<Border x:Name="row"
Background="Transparent"
CornerRadius="6"
Padding="14,9">
<ContentPresenter VerticalAlignment="Center" />
</Border>
<Border x:Name="stripe"
Width="2" HorizontalAlignment="Left"
Margin="0,8" Background="{StaticResource Brand}"
Opacity="0" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="row" Property="Background" Value="{StaticResource BgElev}" />
<Setter Property="Foreground" Value="{StaticResource Ink}" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="row" Property="Background" Value="{StaticResource BgElev}" />
<Setter TargetName="stripe" Property="Opacity" Value="1" />
<Setter Property="Foreground" Value="{StaticResource Ink}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ── Mini progress bar — restyled WPF ProgressBar to match the
@@ -261,21 +573,21 @@
<Style x:Key="StepProgressBar" TargetType="ProgressBar">
<Setter Property="Minimum" Value="0" />
<Setter Property="Maximum" Value="100" />
<Setter Property="Height" Value="6" />
<Setter Property="Height" Value="4" />
<Setter Property="Foreground" Value="{StaticResource Brand}" />
<Setter Property="Background" Value="{StaticResource BgDeeper}" />
<Setter Property="Background" Value="{StaticResource BgInset}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ProgressBar">
<Border Background="{TemplateBinding Background}"
CornerRadius="3" ClipToBounds="True">
CornerRadius="2" ClipToBounds="True">
<Grid HorizontalAlignment="Stretch">
<Rectangle x:Name="PART_Track" />
<Border x:Name="PART_Indicator"
Background="{TemplateBinding Foreground}"
HorizontalAlignment="Left"
CornerRadius="3" />
CornerRadius="2" />
</Grid>
</Border>
</ControlTemplate>
@@ -283,74 +595,211 @@
</Setter>
</Style>
<!-- ── Bool→Visibility converter for HasDetail/HasProgress flags ─ -->
<BooleanToVisibilityConverter x:Key="BoolToVis" />
<!-- ── ScrollBar ───────────────────────────────────────────────
Native WPF scrollbars look like Windows 95 against a dark
slate theme. Replace with a thin, flat track + capsule
thumb that fades in on hover. Same styling for the V and H
orientations. -->
<Style x:Key="ScrollThumb" TargetType="Thumb">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Thumb">
<Border x:Name="thumb"
Background="#4A5165"
CornerRadius="4"
Margin="2,2" />
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="thumb" Property="Background" Value="{StaticResource InkSoft}" />
</Trigger>
<Trigger Property="IsDragging" Value="True">
<Setter TargetName="thumb" Property="Background" Value="{StaticResource Brand}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Invisible repeat buttons. Default ones come with arrow
glyphs and chunky chrome that we don't want. -->
<Style x:Key="ScrollRepeatHidden" TargetType="RepeatButton">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Focusable" Value="False" />
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RepeatButton">
<Rectangle Fill="Transparent" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ScrollBar">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Background" Value="#0A0C11" />
<Setter Property="Width" Value="12" />
<Setter Property="MinWidth" Value="12" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollBar">
<Border Background="{TemplateBinding Background}"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="1,0,0,0">
<Track Name="PART_Track"
IsDirectionReversed="True"
ViewportSize="{TemplateBinding ViewportSize}">
<Track.DecreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollRepeatHidden}"
Command="ScrollBar.PageUpCommand" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollThumb}" MinHeight="32" />
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollRepeatHidden}"
Command="ScrollBar.PageDownCommand" />
</Track.IncreaseRepeatButton>
</Track>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="Orientation" Value="Horizontal">
<Setter Property="Height" Value="12" />
<Setter Property="MinHeight" Value="12" />
<Setter Property="Width" Value="Auto" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollBar">
<Border Background="{TemplateBinding Background}"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="0,1,0,0">
<Track Name="PART_Track"
IsDirectionReversed="False"
ViewportSize="{TemplateBinding ViewportSize}">
<Track.DecreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollRepeatHidden}"
Command="ScrollBar.PageLeftCommand" />
</Track.DecreaseRepeatButton>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollThumb}" MinWidth="32" />
</Track.Thumb>
<Track.IncreaseRepeatButton>
<RepeatButton Style="{StaticResource ScrollRepeatHidden}"
Command="ScrollBar.PageRightCommand" />
</Track.IncreaseRepeatButton>
</Track>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
<!-- ScrollViewer wrapper picked up by every default ScrollViewer
(Window scrollers, the ScrollViewer in ComboBox popup,
ListBox/ItemsControl scrolling etc.). Same overlay-style
slim track on both axes. -->
<Style TargetType="ScrollViewer">
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
</Style>
<!-- ── StartupStep row template ─────────────────────────────────
A single row in the host/join step list. State drives the
status icon's colour + glyph; detail/progress are shown only
when populated (so pending steps render as a clean single
line). -->
status mark; detail/progress are shown only when populated. -->
<DataTemplate x:Key="StepRowTemplate" DataType="{x:Type backend:StartupStep}">
<Grid Margin="0,0,0,14">
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="28" />
<ColumnDefinition Width="24" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Status bullet — circle that recolours and re-glyphs
based on the bound State. Pending = empty ring,
Active = filled brand, Done = green checkmark,
Failed = red X. -->
<Border Grid.Column="0" Width="22" Height="22" CornerRadius="11"
VerticalAlignment="Top" Margin="0,1,0,0"
BorderThickness="2">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource InkFaint}" />
<Style.Triggers>
<DataTrigger Binding="{Binding State}" Value="Active">
<Setter Property="Background" Value="{StaticResource Brand}" />
<Setter Property="BorderBrush" Value="{StaticResource Brand}" />
</DataTrigger>
<DataTrigger Binding="{Binding State}" Value="Done">
<Setter Property="Background" Value="{StaticResource Ok}" />
<Setter Property="BorderBrush" Value="{StaticResource Ok}" />
</DataTrigger>
<DataTrigger Binding="{Binding State}" Value="Failed">
<Setter Property="Background" Value="{StaticResource Danger}" />
<Setter Property="BorderBrush" Value="{StaticResource Danger}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
FontSize="13" FontWeight="Bold" Foreground="White">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="" />
<!-- Status indicator — empty ring for pending, filled
accent for active, mint check for done, red X for
failed. Drawn as Path geometry, not emoji. -->
<Grid Grid.Column="0" Width="18" Height="18"
VerticalAlignment="Top" Margin="0,2,0,0">
<Ellipse x:Name="ring">
<Ellipse.Style>
<Style TargetType="Ellipse">
<Setter Property="Stroke" Value="{StaticResource BorderStrong}" />
<Setter Property="StrokeThickness" Value="1.5" />
<Setter Property="Fill" Value="Transparent" />
<Style.Triggers>
<DataTrigger Binding="{Binding State}" Value="Active">
<Setter Property="Text" Value="●" />
<Setter Property="FontSize" Value="9" />
<Setter Property="Stroke" Value="{StaticResource Brand}" />
</DataTrigger>
<DataTrigger Binding="{Binding State}" Value="Done">
<Setter Property="Text" Value="✓" />
<Setter Property="Foreground" Value="#0E0A1F" />
<Setter Property="Stroke" Value="{StaticResource Ok}" />
<Setter Property="Fill" Value="{StaticResource Ok}" />
</DataTrigger>
<DataTrigger Binding="{Binding State}" Value="Failed">
<Setter Property="Text" Value="✕" />
<Setter Property="Stroke" Value="{StaticResource Danger}" />
<Setter Property="Fill" Value="{StaticResource Danger}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Border>
</Ellipse.Style>
</Ellipse>
<Ellipse Width="6" Height="6" Fill="{StaticResource Brand}"
Visibility="Collapsed">
<Ellipse.Style>
<Style TargetType="Ellipse">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding State}" Value="Active">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
<Path Stroke="#0D0F14" StrokeThickness="2"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 4 9 L 7.5 12 L 14 5"
Width="18" Height="18">
<Path.Style>
<Style TargetType="Path">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding State}" Value="Done">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
<Path Stroke="White" StrokeThickness="2"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 5 5 L 13 13 M 13 5 L 5 13"
Width="18" Height="18">
<Path.Style>
<Style TargetType="Path">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding State}" Value="Failed">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Grid>
<!-- Title + (optional) detail + (optional) progress bar -->
<StackPanel Grid.Column="1" Margin="12,0,0,0">
<TextBlock Text="{Binding Title}" FontSize="14">
<StackPanel Grid.Column="1" Margin="14,0,0,0">
<TextBlock Text="{Binding Title}" FontSize="13">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource InkSoft}" />
@@ -374,6 +823,7 @@
<TextBlock Text="{Binding Detail}" TextWrapping="Wrap"
FontSize="12" Margin="0,4,0,0"
FontFamily="Cascadia Mono, Consolas"
Visibility="{Binding HasDetail, Converter={StaticResource BoolToVis}}">
<TextBlock.Style>
<Style TargetType="TextBlock">
+17 -1
View File
@@ -57,7 +57,7 @@ public sealed class ReleaseDownloader
var asset = await FindAssetAsync(
tagPrefix: version.ReleaseTagPrefix,
assetName: $"dorknet-server-{version.VersionKey}-win-x64.zip",
assetName: $"dorknet-server-{version.VersionKey}-{CurrentRid}.zip",
ct);
var zipPath = Path.Combine(AppPaths.LocalRoot, $"download-{Guid.NewGuid():N}.zip");
@@ -71,6 +71,22 @@ public sealed class ReleaseDownloader
return dir;
}
/// <summary>RID for asset naming — `win-x64`, `linux-x64`,
/// `linux-arm64`, `osx-x64`, `osx-arm64`. Matches the `-r` flag
/// the per-version release workflow passes to `dotnet publish`.</summary>
private static string CurrentRid =>
(OperatingSystem.IsWindows(), OperatingSystem.IsLinux(), OperatingSystem.IsMacOS(),
System.Runtime.InteropServices.RuntimeInformation.OSArchitecture) switch
{
(true, _, _, System.Runtime.InteropServices.Architecture.X64) => "win-x64",
(true, _, _, System.Runtime.InteropServices.Architecture.Arm64) => "win-arm64",
(_, true, _, System.Runtime.InteropServices.Architecture.X64) => "linux-x64",
(_, true, _, System.Runtime.InteropServices.Architecture.Arm64) => "linux-arm64",
(_, _, true, System.Runtime.InteropServices.Architecture.X64) => "osx-x64",
(_, _, true, System.Runtime.InteropServices.Architecture.Arm64) => "osx-arm64",
_ => "win-x64",
};
/// <summary>Returns the path to the unpacked client-patcher dir
/// for <paramref name="version"/>, downloading + caching if needed.</summary>
public async Task<string> EnsurePatcherAsync(
+23 -10
View File
@@ -102,17 +102,30 @@ public sealed class ServerProcess : IAsyncDisposable
private static string FindServerExecutable(string serverDir)
{
// Self-contained .NET publish produces a single .exe per
// RuntimeIdentifier. The server's csproj sets AssemblyName so
// we know the name; fall back to scanning if the rename
// changes.
var canonical = Path.Combine(serverDir, "DorkNet.Server.exe");
// Self-contained .NET publish drops a per-RID binary; on Windows
// it's DorkNet.Server.exe, on Linux/Mac it's DorkNet.Server
// (no extension). Try both, then scan as a last resort.
var ext = OperatingSystem.IsWindows() ? ".exe" : "";
var canonical = Path.Combine(serverDir, $"DorkNet.Server{ext}");
if (File.Exists(canonical)) return canonical;
var any = Directory.GetFiles(serverDir, "*.exe").FirstOrDefault();
if (any is null)
throw new FileNotFoundException(
$"No .exe found in {serverDir}. The release artifact may be corrupt.");
return any;
// Windows scan picks any .exe; on Unix, scan for files that are
// executable + match the expected name pattern.
if (OperatingSystem.IsWindows())
{
var any = Directory.GetFiles(serverDir, "*.exe").FirstOrDefault();
if (any is not null) return any;
}
else
{
var any = Directory.GetFiles(serverDir, "DorkNet.Server*")
.Where(p => !p.EndsWith(".dll") && !p.EndsWith(".pdb") && !p.EndsWith(".json"))
.FirstOrDefault();
if (any is not null) return any;
}
throw new FileNotFoundException(
$"No server binary found in {serverDir}. The release artifact may be corrupt.");
}
private static void ApplySqliteCompatibilityPatches(string serverDir)
+231
View File
@@ -0,0 +1,231 @@
using System.Formats.Tar;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
using System.Net.Http.Json;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
namespace DorkNet.Launcher.Backend;
/// <summary>Fetches <c>tunnelto</c> straight from the upstream
/// release feed (agrinman/tunnelto) the first time the launcher needs
/// it, so end users don't have to set up Scoop / Cargo / brew before
/// hosting. The downloaded binary lands in <see cref="AppPaths.LocalRoot"/>,
/// which is the first path <see cref="TunneltoTunnel"/> probes after
/// the side-by-side install dir.
///
/// <para>Upstream's release assets (as of 0.1.18) are uneven:
/// <list type="bullet">
/// <item>Windows ships a direct <c>tunnelto-windows.exe</c> — easy.</item>
/// <item>Linux ships a <c>tunnelto-linux.tar.gz</c> archive — easy.</item>
/// <item>macOS only publishes Homebrew bottles, which we can't crack
/// open cleanly. macOS falls through to a clear "brew install tunnelto"
/// error instead.</item>
/// </list></para></summary>
public sealed class TunneltoInstaller
{
private const string ReleasesApi =
"https://api.github.com/repos/agrinman/tunnelto/releases/latest";
private static readonly HttpClient Http = CreateHttp();
private static HttpClient CreateHttp()
{
var c = new HttpClient { Timeout = TimeSpan.FromMinutes(3) };
c.DefaultRequestHeaders.UserAgent.ParseAdd("DorkNet-Launcher-TunneltoInstaller/1.0");
c.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
return c;
}
/// <summary>Make sure tunnelto is available somewhere
/// <see cref="TunneltoTunnel"/> will find it. If it's already
/// installed, returns immediately. Otherwise downloads the
/// upstream release into <see cref="AppPaths.LocalRoot"/>.
/// Throws with platform-specific install hints when auto-install
/// isn't possible (currently macOS).</summary>
public async Task<string> EnsureInstalledAsync(
IProgress<DownloadProgress>? progress = null,
CancellationToken ct = default)
{
// Quickly probe the existing-install paths first so a repeat
// host start doesn't hammer the GitHub API.
var existing = TryFindExisting();
if (existing is not null) return existing;
if (OperatingSystem.IsMacOS())
{
// Upstream macOS releases are Homebrew bottles, not portable
// binaries — we'd have to fish the prefix out and that's a
// mess. Tell the user to use brew instead.
throw new PlatformNotSupportedException(
"Tunnelto auto-install isn't supported on macOS — install it manually: " +
"`brew install tunnelto` (or `cargo install tunnelto`).");
}
if (OperatingSystem.IsLinux() &&
RuntimeInformation.OSArchitecture != Architecture.X64)
{
// The only Linux asset is x64; ARM users have to build
// from source or pull via cargo.
throw new PlatformNotSupportedException(
"Tunnelto auto-install only covers Linux x64. On arm64, run: " +
"`cargo install tunnelto` (or grab a build from github.com/agrinman/tunnelto/releases).");
}
var release = await Http.GetFromJsonAsync<GitHubRelease>(ReleasesApi, ct)
?? throw new InvalidOperationException("Couldn't reach github.com/agrinman/tunnelto/releases.");
var (assetName, kind) = AssetForCurrentPlatform();
var asset = release.Assets?.FirstOrDefault(a =>
string.Equals(a.Name, assetName, StringComparison.OrdinalIgnoreCase));
if (asset?.BrowserDownloadUrl is null)
throw new InvalidOperationException(
$"The upstream release doesn't ship a '{assetName}' asset. " +
$"Got assets: {string.Join(", ", release.Assets?.Select(a => a.Name) ?? Array.Empty<string>())}");
var binaryName = OperatingSystem.IsWindows() ? "tunnelto.exe" : "tunnelto";
var dest = Path.Combine(AppPaths.LocalRoot, binaryName);
// Stream to a temp file first so a half-finished download
// doesn't leave a broken binary in place.
var stagingPath = Path.Combine(AppPaths.LocalRoot, $"tunnelto-staging-{Guid.NewGuid():N}");
try
{
await DownloadAsync(asset.BrowserDownloadUrl, stagingPath, progress, ct);
switch (kind)
{
case AssetKind.RawExe:
if (File.Exists(dest)) File.Delete(dest);
File.Move(stagingPath, dest);
break;
case AssetKind.TarGz:
ExtractTunneltoFromTarGz(stagingPath, dest);
File.Delete(stagingPath);
// Mark executable on Unix — extracted files often
// come out without the mode bits set.
if (!OperatingSystem.IsWindows())
TryChmodExec(dest);
break;
}
return dest;
}
catch
{
if (File.Exists(stagingPath)) try { File.Delete(stagingPath); } catch { }
throw;
}
}
/// <summary>Mirror of <see cref="TunneltoTunnel.ResolveExecutable"/>'s
/// search order — kept here so the installer can short-circuit when
/// the user already installed tunnelto via Scoop / brew / cargo.</summary>
private static string? TryFindExisting()
{
var isWindows = OperatingSystem.IsWindows();
var name = isWindows ? "tunnelto.exe" : "tunnelto";
var candidates = new List<string>
{
Path.Combine(AppContext.BaseDirectory, name),
Path.Combine(AppPaths.LocalRoot, name),
};
if (isWindows)
{
candidates.Add(Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"scoop", "shims", name));
}
foreach (var c in candidates)
if (File.Exists(c)) return c;
// PATH scan — last resort, in case the user has it somewhere
// unusual like /usr/local/bin or via cargo's bin dir.
var onPath = Environment.GetEnvironmentVariable("PATH")?
.Split(Path.PathSeparator)
.Select(p => Path.Combine(p.Trim(), name))
.FirstOrDefault(File.Exists);
return onPath;
}
private static (string assetName, AssetKind kind) AssetForCurrentPlatform()
{
if (OperatingSystem.IsWindows()) return ("tunnelto-windows.exe", AssetKind.RawExe);
if (OperatingSystem.IsLinux()) return ("tunnelto-linux.tar.gz", AssetKind.TarGz);
throw new PlatformNotSupportedException("No tunnelto auto-install for this platform.");
}
/// <summary>Walks the tarball looking for a regular file named
/// "tunnelto" (typically at the root). Writes it to
/// <paramref name="dest"/>. The archive itself is a single-binary
/// release so we don't need to faithfully recreate the directory
/// structure.</summary>
private static void ExtractTunneltoFromTarGz(string archivePath, string dest)
{
using var fs = File.OpenRead(archivePath);
using var gz = new GZipStream(fs, CompressionMode.Decompress);
using var tar = new TarReader(gz);
while (tar.GetNextEntry() is { } entry)
{
// Only interested in the binary itself. The archive may
// contain README / LICENSE which we skip.
var fname = Path.GetFileName(entry.Name);
if (entry.EntryType != TarEntryType.RegularFile) continue;
if (!fname.Equals("tunnelto", StringComparison.OrdinalIgnoreCase)) continue;
if (File.Exists(dest)) File.Delete(dest);
using var outFs = File.Create(dest);
entry.DataStream?.CopyTo(outFs);
return;
}
throw new InvalidDataException(
"tunnelto-linux.tar.gz didn't contain a 'tunnelto' binary. Asset may be malformed.");
}
/// <summary>Mark a file as executable on Unix systems. Uses
/// <see cref="File.SetUnixFileMode"/> (available since .NET 7).
/// No-op on Windows since the NTFS execute bit doesn't apply.</summary>
private static void TryChmodExec(string path)
{
try
{
var mode = File.GetUnixFileMode(path);
File.SetUnixFileMode(path,
mode | UnixFileMode.UserExecute | UnixFileMode.GroupExecute | UnixFileMode.OtherExecute);
}
catch { /* best-effort */ }
}
private static async Task DownloadAsync(
string url, string destPath,
IProgress<DownloadProgress>? progress,
CancellationToken ct)
{
using var resp = await Http.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
resp.EnsureSuccessStatusCode();
var total = resp.Content.Headers.ContentLength ?? -1L;
await using var src = await resp.Content.ReadAsStreamAsync(ct);
await using var dst = File.Create(destPath);
var buf = new byte[81920];
long read = 0;
int n;
while ((n = await src.ReadAsync(buf, ct)) > 0)
{
await dst.WriteAsync(buf.AsMemory(0, n), ct);
read += n;
progress?.Report(new DownloadProgress(read, total));
}
}
private enum AssetKind { RawExe, TarGz }
private sealed class GitHubRelease
{
[JsonPropertyName("tag_name")] public string? TagName { get; set; }
[JsonPropertyName("assets")] public List<GitHubAsset>? Assets { get; set; }
}
private sealed class GitHubAsset
{
[JsonPropertyName("name")] public string? Name { get; set; }
[JsonPropertyName("browser_download_url")] public string? BrowserDownloadUrl { get; set; }
}
}
+18 -10
View File
@@ -76,26 +76,34 @@ public sealed class TunneltoTunnel : IAsyncDisposable
private static string ResolveExecutable()
{
var sideBySide = Path.Combine(AppContext.BaseDirectory, "tunnelto.exe");
var isWindows = OperatingSystem.IsWindows();
var name = isWindows ? "tunnelto.exe" : "tunnelto";
var sideBySide = Path.Combine(AppContext.BaseDirectory, name);
if (File.Exists(sideBySide)) return sideBySide;
var local = Path.Combine(AppPaths.LocalRoot, "tunnelto.exe");
var local = Path.Combine(AppPaths.LocalRoot, name);
if (File.Exists(local)) return local;
var scoopShim = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"scoop", "shims", "tunnelto.exe");
if (File.Exists(scoopShim)) return scoopShim;
if (isWindows)
{
var scoopShim = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
"scoop", "shims", name);
if (File.Exists(scoopShim)) return scoopShim;
}
var onPath = Environment.GetEnvironmentVariable("PATH")?
.Split(';')
.Select(p => Path.Combine(p.Trim(), "tunnelto.exe"))
.Split(Path.PathSeparator)
.Select(p => Path.Combine(p.Trim(), name))
.FirstOrDefault(File.Exists);
if (onPath is not null) return onPath;
var hint = isWindows
? "scoop bucket add tunnelto https://github.com/asabi/scoop-tunnelto && scoop install tunnelto"
: "cargo install tunnelto # or grab a binary from github.com/agrinman/tunnelto/releases";
throw new FileNotFoundException(
"tunnelto.exe was not found. Install Tunnelto first: " +
"scoop bucket add tunnelto https://github.com/asabi/scoop-tunnelto && scoop install tunnelto");
$"{name} was not found. Install Tunnelto first: {hint}");
}
private static readonly Regex UrlRegex = new(
+166
View File
@@ -0,0 +1,166 @@
using System.IO;
using System.Net.Http;
using System.Net.Http.Json;
using System.Reflection;
using System.Text.Json.Serialization;
namespace DorkNet.Launcher.Backend;
/// <summary>Rolling-update check against GitHub releases. The launcher
/// asks for the latest release tag on startup, compares it to its own
/// assembly version, and (if newer) downloads the new self-contained
/// <c>dorknet.exe</c> next to the existing one + writes a tiny swap
/// script. The UI surfaces a banner with a button that runs the script
/// and exits — the script waits for the process to release the exe,
/// copies the new build in place, and relaunches.</summary>
public sealed class Updater
{
private const string ReleasesApi =
"https://api.github.com/repos/DorkSquadRR/DorkNet/releases/latest";
/// <summary>Asset name produced by the Inno installer's publish
/// step. Must match what's attached to GitHub releases.</summary>
private const string LauncherAssetName = "dorknet.exe";
private static readonly HttpClient Http = CreateHttp();
private static HttpClient CreateHttp()
{
var c = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
c.DefaultRequestHeaders.UserAgent.ParseAdd("DorkNet-Launcher-Updater/1.0");
c.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
return c;
}
/// <summary>Current launcher version, read from the assembly's
/// InformationalVersion / FileVersion attribute. Falls back to 0.0
/// in dev builds where neither is set.</summary>
public static Version CurrentVersion
{
get
{
var asm = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
var info = asm.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
if (!string.IsNullOrWhiteSpace(info) && TryParseLoose(info, out var v)) return v;
var fv = asm.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version;
if (!string.IsNullOrWhiteSpace(fv) && TryParseLoose(fv, out var fvv)) return fvv;
return new Version(0, 0);
}
}
/// <summary>Looks up GitHub's latest release. Returns null on
/// network failure or if the local version is already at-or-newer
/// than what's published. Never throws — update checks must never
/// block app launch.</summary>
public async Task<UpdateInfo?> CheckAsync(CancellationToken ct = default)
{
try
{
var release = await Http.GetFromJsonAsync<GitHubRelease>(ReleasesApi, ct);
if (release is null || string.IsNullOrEmpty(release.TagName)) return null;
if (!TryParseLoose(release.TagName, out var latest)) return null;
if (latest <= CurrentVersion) return null;
var asset = release.Assets?.FirstOrDefault(a =>
string.Equals(a.Name, LauncherAssetName, StringComparison.OrdinalIgnoreCase));
if (asset?.BrowserDownloadUrl is null) return null;
return new UpdateInfo(latest, asset.BrowserDownloadUrl, release.HtmlUrl);
}
catch { return null; }
}
/// <summary>Downloads the new exe to the user's local update
/// staging dir, writes a swap .cmd next to it, and returns the
/// path to that .cmd. Caller is expected to launch it and exit.</summary>
public async Task<string> StageUpdateAsync(
UpdateInfo info,
IProgress<DownloadProgress>? progress = null,
CancellationToken ct = default)
{
var stagingDir = Path.Combine(AppPaths.LocalRoot, "update");
Directory.CreateDirectory(stagingDir);
var newExePath = Path.Combine(stagingDir, LauncherAssetName);
using (var response = await Http.GetAsync(info.DownloadUrl,
HttpCompletionOption.ResponseHeadersRead, ct))
{
response.EnsureSuccessStatusCode();
var total = response.Content.Headers.ContentLength ?? -1;
await using var src = await response.Content.ReadAsStreamAsync(ct);
await using var dst = File.Create(newExePath);
var buf = new byte[81920];
long read = 0;
int n;
while ((n = await src.ReadAsync(buf, ct)) > 0)
{
await dst.WriteAsync(buf.AsMemory(0, n), ct);
read += n;
progress?.Report(new DownloadProgress(read, total));
}
}
var currentExe = GetCurrentExePath();
var swapScript = Path.Combine(stagingDir, "swap.cmd");
// The swap script:
// 1. Pings until the running dorknet exits (we exit immediately
// after spawning it, so this resolves fast).
// 2. Copies the new exe over the old one. /Y suppresses
// confirmation; failure exits with the original errorlevel.
// 3. Starts the new exe in its install directory.
// 4. Self-deletes via the start-then-del trick.
var script =
"@echo off\r\n" +
"setlocal\r\n" +
$"set OLD=\"{currentExe}\"\r\n" +
$"set NEW=\"{newExePath}\"\r\n" +
":wait\r\n" +
"tasklist /FI \"IMAGENAME eq dorknet.exe\" 2>NUL | find /I \"dorknet.exe\" >NUL\r\n" +
"if not errorlevel 1 (timeout /t 1 /nobreak >NUL & goto wait)\r\n" +
"copy /Y %NEW% %OLD% >NUL || (echo Copy failed & exit /b 1)\r\n" +
"start \"\" %OLD%\r\n" +
"del \"%~f0\"\r\n";
await File.WriteAllTextAsync(swapScript, script, ct);
return swapScript;
}
/// <summary>Best-effort path to the currently-running exe. Used by
/// the swap script as the destination to overwrite.</summary>
public static string GetCurrentExePath()
{
// Process.MainModule.FileName is the reliable source — handles
// single-file deployments (where AppContext.BaseDirectory points
// at an extracted temp dir, not the actual exe location).
var p = System.Diagnostics.Process.GetCurrentProcess();
return p.MainModule?.FileName
?? Path.Combine(AppContext.BaseDirectory, "dorknet.exe");
}
/// <summary>Parse "v0.2.0" / "0.2.0" / "v0.2.0-rc1" loosely into a
/// <see cref="Version"/>. Pre-release suffixes are dropped.</summary>
private static bool TryParseLoose(string s, out Version version)
{
version = new Version(0, 0);
if (string.IsNullOrWhiteSpace(s)) return false;
var t = s.Trim().TrimStart('v', 'V');
// Strip pre-release / build metadata suffix (semver "-..." or "+...").
var cut = t.IndexOfAny(new[] { '-', '+' });
if (cut >= 0) t = t.Substring(0, cut);
// Pad to at least two segments so "1" → "1.0" parses cleanly.
if (!t.Contains('.')) t += ".0";
return Version.TryParse(t, out version!);
}
private sealed class GitHubRelease
{
[JsonPropertyName("tag_name")] public string? TagName { get; set; }
[JsonPropertyName("html_url")] public string? HtmlUrl { get; set; }
[JsonPropertyName("assets")] public List<GitHubAsset>? Assets { get; set; }
}
private sealed class GitHubAsset
{
[JsonPropertyName("name")] public string? Name { get; set; }
[JsonPropertyName("browser_download_url")] public string? BrowserDownloadUrl { get; set; }
}
}
public sealed record UpdateInfo(Version Version, string DownloadUrl, string? ReleaseUrl);
+1215 -413
View File
@@ -2,8 +2,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="DorkNet"
Width="960" Height="700"
MinWidth="720" MinHeight="540"
Width="1080" Height="720"
MinWidth="900" MinHeight="600"
WindowStartupLocation="CenterScreen"
Background="{StaticResource Bg}"
TextElement.Foreground="{StaticResource Ink}"
@@ -11,115 +11,507 @@
TextOptions.TextFormattingMode="Ideal"
TextOptions.TextRenderingMode="ClearType">
<!-- Soft radial-ish glow in the top-left corner of the window for
atmosphere. WPF doesn't have a true radial gradient on Background
without nested Border tricks, so we stack a linear-gradient
overlay on top of the solid Bg. -->
<Grid>
<!-- Window background: a faint vignette in the top-left from the
brand colour into the slate base. No multi-stop rainbow — a
single warm corner anchored on near-neutral. -->
<Grid.Background>
<LinearGradientBrush StartPoint="0,0" EndPoint="1,1">
<GradientStop Color="#2A1E48" Offset="0" />
<GradientStop Color="#16102A" Offset="0.6" />
<GradientStop Color="#16102A" Offset="1" />
</LinearGradientBrush>
<RadialGradientBrush GradientOrigin="0,0" Center="0,0"
RadiusX="0.9" RadiusY="1.1">
<GradientStop Color="#1A1820" Offset="0" />
<GradientStop Color="#0D0F14" Offset="0.8" />
</RadialGradientBrush>
</Grid.Background>
<Grid Margin="36,28">
<!-- View slots — only one Visible at a time; code-behind toggles. -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- ── Welcome screen ─────────────────────────────────────
Only shown on truly first launch. After "Get started"
the user enters the setup wizard; subsequent launches
skip straight to Setup or HostView/JoinView. -->
<StackPanel x:Name="WelcomeView" Visibility="Collapsed"
VerticalAlignment="Center" HorizontalAlignment="Center"
MaxWidth="520">
<TextBlock Text="DorkNet"
FontSize="72" FontWeight="Bold"
Foreground="{StaticResource WordmarkBrush}"
HorizontalAlignment="Center"
FontFamily="Segoe UI Variable, Segoe UI" />
<TextBlock Text="Play together on your own Rec Room server."
Foreground="{StaticResource Ink}"
FontSize="18"
HorizontalAlignment="Center" Margin="0,8,0,32"
TextAlignment="Center" />
<Border Style="{StaticResource Panel}"
Background="{StaticResource CardBg}" Padding="28,24"
Margin="0,0,0,28">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
<TextBlock Text="✨" FontSize="20" Margin="0,0,12,0" />
<TextBlock Foreground="{StaticResource Ink}" FontSize="14"
VerticalAlignment="Center" TextWrapping="Wrap"
Text="Set up in about two minutes" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,14">
<TextBlock Text="💸" FontSize="20" Margin="0,0,12,0" />
<TextBlock Foreground="{StaticResource Ink}" FontSize="14"
VerticalAlignment="Center" TextWrapping="Wrap"
Text="Host for free — no credit card, no subscription" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="🎮" FontSize="20" Margin="0,0,12,0" />
<TextBlock Foreground="{StaticResource Ink}" FontSize="14"
VerticalAlignment="Center" TextWrapping="Wrap"
Text="Works with the regular Rec Room game — no rebuild needed" />
</StackPanel>
</StackPanel>
</Border>
<Button Style="{StaticResource PrimaryBtn}"
Content="Get started →"
Click="OnWelcomeContinue"
HorizontalAlignment="Center"
Padding="34,14" FontSize="15" />
<TextBlock Text="Not affiliated with Rec Room Inc. or Against Gravity."
HorizontalAlignment="Center"
Foreground="{StaticResource InkFaint}"
FontSize="11" Margin="0,28,0,0" />
</StackPanel>
<!-- ── Setup wizard ────────────────────────────────────────
Shown on first launch after Welcome. Walks through mode
pick, install location, server config + Photon. Once
the user finishes the last step, AppState.SetupComplete
flips and future launches go straight to HostView /
JoinView (which still hold the same fields for tweaks). -->
<Grid x:Name="SetupView" Visibility="Collapsed">
<!-- ── Sidebar ─────────────────────────────────────────────────
Persistent in Host/Join/Settings views. Hidden via code-behind
during Welcome/Setup so those screens can hero the brand. -->
<Border x:Name="MainNav" Grid.Column="0"
Width="232" Visibility="Collapsed"
Background="{StaticResource BgDeeper}"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="0,0,1,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,24">
<!-- Brand mark: brand-coloured square with a small notch
cut out, plus the wordmark in caps. Distinctive geometric
mark — no rainbow gradient on the text. -->
<Border Grid.Row="0" Padding="20,22,20,18"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal">
<TextBlock Text="DorkNet" FontSize="22" FontWeight="Bold"
Foreground="{StaticResource WordmarkBrush}" />
<Border Style="{StaticResource StatusChip}" Margin="14,0,0,0"
VerticalAlignment="Center">
<TextBlock x:Name="SetupStepIndicator"
Text="STEP 1" FontSize="10"
FontWeight="Bold" Foreground="{StaticResource Brand}" />
<Grid Width="28" Height="28" Margin="0,0,12,0">
<Border Background="{StaticResource Brand}"
CornerRadius="6" />
<Path Fill="White"
Data="M 8 8 L 14 8 L 14 14 L 20 14 L 20 20 L 8 20 Z"
Width="28" Height="28" />
</Grid>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="DORKNET" FontSize="13"
FontWeight="Bold"
Foreground="{StaticResource Ink}">
<TextBlock.Resources>
<Style TargetType="TextBlock">
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI" />
</Style>
</TextBlock.Resources>
</TextBlock>
<TextBlock Text="PRIVATE SERVER" FontSize="9"
FontWeight="SemiBold"
Foreground="{StaticResource InkFaint}"
Margin="0,2,0,0" />
</StackPanel>
</StackPanel>
</Border>
<!-- Nav rows — Host, Join, Settings. Each routes to the
same handlers the legacy mode-switcher uses. Visual
"active" state is set by the code-behind via IsChecked
to match whichever view is showing. -->
<StackPanel Grid.Row="1" Margin="12,16">
<TextBlock Text="MODE" Style="{StaticResource FieldLabel}"
Margin="6,0,0,8" />
<RadioButton x:Name="NavHostBtn" Style="{StaticResource NavBtn}"
AutomationProperties.AutomationId="NavHostBtn"
GroupName="MainNav" Click="OnPickHost">
<StackPanel Orientation="Horizontal">
<Path Width="18" Height="18"
Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=RadioButton}}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 2 4 L 16 4 L 16 8 L 2 8 Z M 2 10 L 16 10 L 16 14 L 2 14 Z M 4 6 L 5 6 M 4 12 L 5 12 M 13 6 L 14 6 M 13 12 L 14 12"
VerticalAlignment="Center" Margin="2,0,12,0" />
<TextBlock Text="Host a server" VerticalAlignment="Center" />
</StackPanel>
</RadioButton>
<RadioButton x:Name="NavJoinBtn" Style="{StaticResource NavBtn}"
AutomationProperties.AutomationId="NavJoinBtn"
GroupName="MainNav" Click="OnPickJoin">
<StackPanel Orientation="Horizontal">
<Path Width="18" Height="18"
Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=RadioButton}}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 7 11 L 11 7 M 5 9 A 3 3 0 0 1 9 5 L 11 5 M 11 13 L 9 13 A 3 3 0 0 1 5 9 M 15 9 A 3 3 0 0 1 11 13 M 11 5 A 3 3 0 0 1 15 9"
VerticalAlignment="Center" Margin="2,0,12,0" />
<TextBlock Text="Join a friend" VerticalAlignment="Center" />
</StackPanel>
</RadioButton>
<TextBlock Text="SYSTEM" Style="{StaticResource FieldLabel}"
Margin="6,20,0,8" />
<RadioButton x:Name="NavSettingsBtn" Style="{StaticResource NavBtn}"
AutomationProperties.AutomationId="NavSettingsBtn"
GroupName="MainNav" Click="OnOpenSettings">
<StackPanel Orientation="Horizontal">
<Path Width="18" Height="18"
Stroke="{Binding Foreground, RelativeSource={RelativeSource AncestorType=RadioButton}}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 9 2 L 10 4 L 12 3 L 12.5 5 L 14.5 5 L 14 7 L 16 8 L 14.5 9.5 L 15 11.5 L 13 11.5 L 12.5 13.5 L 10.5 13 L 9 14.5 L 7.5 13 L 5.5 13.5 L 5 11.5 L 3 11.5 L 3.5 9.5 L 2 8 L 4 7 L 3.5 5 L 5.5 5 L 6 3 L 8 4 Z M 9 6.5 A 1.5 1.5 0 1 0 9 9.5 A 1.5 1.5 0 1 0 9 6.5"
VerticalAlignment="Center" Margin="2,0,12,0" />
<TextBlock Text="Settings" VerticalAlignment="Center" />
</StackPanel>
</RadioButton>
<Button x:Name="NavRerunSetupBtn"
ToolTip="Re-run setup wizard"
Click="OnRerunSetup"
HorizontalContentAlignment="Stretch"
Background="Transparent" BorderBrush="Transparent"
BorderThickness="0" Padding="14,9"
Cursor="Hand"
Margin="0,0,0,2">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="root" Background="{TemplateBinding Background}"
CornerRadius="6" Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Stretch"
VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="root" Property="Background" Value="{StaticResource BgElev}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
<Path Width="18" Height="18"
Stroke="{StaticResource InkSoft}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 4 9 A 5 5 0 1 1 6 13 M 4 5 L 4 9 L 8 9"
VerticalAlignment="Center" Margin="2,0,12,0" />
<TextBlock Text="Re-run setup" VerticalAlignment="Center"
Foreground="{StaticResource InkSoft}"
FontSize="13" FontWeight="SemiBold" />
</StackPanel>
</Button>
</StackPanel>
<!-- Footer: status indicator + version. Information
density a real tool wears. -->
<Border Grid.Row="2" Padding="20,16"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="0,1,0,0">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
<Ellipse x:Name="SidebarStatusDot" Width="8" Height="8"
Fill="{StaticResource InkFaint}"
VerticalAlignment="Center" Margin="0,0,8,0" />
<TextBlock x:Name="SidebarStatusText"
Text="Idle" FontSize="11"
FontWeight="SemiBold"
Foreground="{StaticResource InkSoft}"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock x:Name="SidebarVersionText"
Text="DORKNET LAUNCHER · v0.1"
FontSize="9" FontWeight="SemiBold"
Foreground="{StaticResource InkFaint}"
FontFamily="Cascadia Mono, Consolas" />
<!-- Update banner — only visible after the
startup check finds a newer release. Clicking
stages the download + spawns the swap script. -->
<Border x:Name="UpdateBanner" Visibility="Collapsed"
Background="{StaticResource BrandSoft}"
BorderBrush="{StaticResource Brand}"
BorderThickness="1"
CornerRadius="6"
Padding="10,8"
Margin="0,12,0,0">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,4">
<Path Width="11" Height="11" Margin="0,0,6,0"
Stroke="{StaticResource Brand}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 5.5 1 L 5.5 8 M 2 5 L 5.5 8 L 9 5 M 1.5 10 L 9.5 10"
VerticalAlignment="Center" />
<TextBlock x:Name="UpdateBannerTitle"
Text="Update available"
FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brand}"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock x:Name="UpdateBannerVersionText"
Text=""
FontSize="10"
Foreground="{StaticResource InkSoft}"
FontFamily="Cascadia Mono, Consolas"
Margin="0,0,0,6" />
<Button x:Name="UpdateInstallBtn"
Click="OnInstallUpdate"
Content="Install &amp; restart"
Cursor="Hand"
HorizontalContentAlignment="Center"
Background="{StaticResource Brand}"
Foreground="White"
BorderBrush="{StaticResource Brand}"
BorderThickness="1"
Padding="0,6"
FontSize="10"
FontWeight="Bold">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border x:Name="root"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="{TemplateBinding Padding}"
TextElement.Foreground="{TemplateBinding Foreground}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="root" Property="Background" Value="{StaticResource BrandHover}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Button.Template>
</Button>
</StackPanel>
</Border>
</StackPanel>
</Grid>
</Border>
</Grid>
</Border>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"
Padding="0,0,8,0">
<!-- ── Content area ───────────────────────────────────────────── -->
<Grid Grid.Column="1">
<!-- ── Welcome screen ─────────────────────────────────────
Sidebar hidden during this view. Hero treatment with
bold geometry — no emoji marketing card. -->
<Grid x:Name="WelcomeView" Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0"
VerticalAlignment="Center" HorizontalAlignment="Center"
MaxWidth="640" Margin="48,0">
<!-- Geometric brand mark, oversized. -->
<Grid Width="84" Height="84" HorizontalAlignment="Center"
Margin="0,0,0,28">
<Border Background="{StaticResource Brand}"
CornerRadius="16"
Effect="{StaticResource BtnShadow}" />
<Path Fill="White"
Data="M 24 24 L 42 24 L 42 42 L 60 42 L 60 60 L 24 60 Z"
Width="84" Height="84" />
</Grid>
<TextBlock Text="DORKNET"
FontSize="64" FontWeight="Bold"
Foreground="{StaticResource Ink}"
HorizontalAlignment="Center"
FontFamily="Segoe UI Variable, Segoe UI" />
<TextBlock HorizontalAlignment="Center" Margin="0,4,0,0">
<Run Text="A private server for"
Foreground="{StaticResource InkSoft}" FontSize="16" />
<Run Text=" Rec Room. "
Foreground="{StaticResource Ink}" FontSize="16" FontWeight="SemiBold" />
<Run Text="Hosted on your machine, played with your friends."
Foreground="{StaticResource InkSoft}" FontSize="16" />
</TextBlock>
<!-- Feature row — no marketing card, just three flat
items with a small left rule. Reads like docs, not
a hero card. -->
<Grid Margin="0,44,0,40" HorizontalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="40" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" MaxWidth="180">
<Border Width="3" Height="20" Background="{StaticResource Brand}"
HorizontalAlignment="Left" Margin="0,0,0,10" />
<TextBlock Text="2-MIN SETUP" FontSize="10" FontWeight="Bold"
Foreground="{StaticResource Ink}"
Margin="0,0,0,6" />
<TextBlock Text="Auto-detects your install, downloads everything else."
FontSize="12" Foreground="{StaticResource InkSoft}"
TextWrapping="Wrap" LineHeight="18" />
</StackPanel>
<StackPanel Grid.Column="2" MaxWidth="180">
<Border Width="3" Height="20" Background="{StaticResource Brand}"
HorizontalAlignment="Left" Margin="0,0,0,10" />
<TextBlock Text="ZERO COST" FontSize="10" FontWeight="Bold"
Foreground="{StaticResource Ink}"
Margin="0,0,0,6" />
<TextBlock Text="No subscription, no card. Tunnelto handles the public address."
FontSize="12" Foreground="{StaticResource InkSoft}"
TextWrapping="Wrap" LineHeight="18" />
</StackPanel>
<StackPanel Grid.Column="4" MaxWidth="180">
<Border Width="3" Height="20" Background="{StaticResource Brand}"
HorizontalAlignment="Left" Margin="0,0,0,10" />
<TextBlock Text="STOCK CLIENT" FontSize="10" FontWeight="Bold"
Foreground="{StaticResource Ink}"
Margin="0,0,0,6" />
<TextBlock Text="Patches the official Rec Room install in place. No rebuild."
FontSize="12" Foreground="{StaticResource InkSoft}"
TextWrapping="Wrap" LineHeight="18" />
</StackPanel>
</Grid>
<Button Style="{StaticResource PrimaryBtn}"
Click="OnWelcomeContinue"
HorizontalAlignment="Center"
Padding="40,14" FontSize="14" FontWeight="Bold">
<StackPanel Orientation="Horizontal">
<TextBlock Text="GET STARTED" VerticalAlignment="Center"
Foreground="White" />
<Path Width="14" Height="14" Margin="14,0,0,0"
Stroke="White" StrokeThickness="1.8"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 2 7 L 12 7 M 8 3 L 12 7 L 8 11"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
<TextBlock Grid.Row="1"
Text="Not affiliated with Rec Room Inc. or Against Gravity. · AGPL-3.0"
HorizontalAlignment="Center"
Foreground="{StaticResource InkFaint}"
FontSize="10" FontWeight="SemiBold"
Margin="0,0,0,28" />
</Grid>
<!-- ── Setup wizard ─────────────────────────────────────── -->
<Grid x:Name="SetupView" Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Header bar: wordmark + step pill. Matches the
in-product header used by Host/Join. -->
<Border Grid.Row="0" Padding="40,26,40,18"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="0,0,0,1">
<Grid>
<StackPanel Orientation="Horizontal">
<Grid Width="22" Height="22" Margin="0,0,12,0">
<Border Background="{StaticResource Brand}" CornerRadius="5" />
<Path Fill="White"
Data="M 6 6 L 11 6 L 11 11 L 16 11 L 16 16 L 6 16 Z"
Width="22" Height="22" />
</Grid>
<TextBlock Text="DORKNET" FontSize="14"
FontWeight="Bold"
Foreground="{StaticResource Ink}"
VerticalAlignment="Center" />
<Border Background="{StaticResource BorderSoft}" Width="1"
Margin="14,4,14,4" />
<TextBlock Text="FIRST-RUN SETUP" FontSize="11"
FontWeight="SemiBold"
Foreground="{StaticResource InkSoft}"
VerticalAlignment="Center" />
</StackPanel>
<Border Style="{StaticResource StatusChip}" HorizontalAlignment="Right"
VerticalAlignment="Center">
<TextBlock x:Name="SetupStepIndicator"
Text="STEP 1 OF 5" FontSize="10"
FontWeight="Bold"
Foreground="{StaticResource Brand}" />
</Border>
</Grid>
</Border>
<!-- Stepper bar showing all five steps as a progress
rail. The active dot pulses on the brand colour;
completed segments fill. -->
<Border Grid.Row="1" Padding="40,18,40,18">
<Grid x:Name="SetupStepperGrid">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" x:Name="StepBar1"
Height="3" Margin="0,0,4,0"
Background="{StaticResource Brand}"
CornerRadius="2"
VerticalAlignment="Top" />
<Border Grid.Column="1" x:Name="StepBar2"
Height="3" Margin="4,0,4,0"
Background="{StaticResource Border}"
CornerRadius="2"
VerticalAlignment="Top" />
<Border Grid.Column="2" x:Name="StepBar3"
Height="3" Margin="4,0,4,0"
Background="{StaticResource Border}"
CornerRadius="2"
VerticalAlignment="Top" />
<Border Grid.Column="3" x:Name="StepBar4"
Height="3" Margin="4,0,4,0"
Background="{StaticResource Border}"
CornerRadius="2"
VerticalAlignment="Top" />
<Border Grid.Column="4" x:Name="StepBar5"
Height="3" Margin="4,0,0,0"
Background="{StaticResource Border}"
CornerRadius="2"
VerticalAlignment="Top" />
<StackPanel Grid.Column="0" Margin="0,12,0,0">
<TextBlock Text="01" FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brand}"
FontFamily="Cascadia Mono, Consolas" />
<TextBlock Text="Mode" FontSize="11"
Foreground="{StaticResource InkSoft}"
Margin="0,2,0,0" />
</StackPanel>
<StackPanel Grid.Column="1" Margin="0,12,0,0">
<TextBlock Text="02" FontSize="11" FontWeight="Bold"
Foreground="{StaticResource InkFaint}"
FontFamily="Cascadia Mono, Consolas" />
<TextBlock Text="Install" FontSize="11"
Foreground="{StaticResource InkFaint}"
Margin="0,2,0,0" />
</StackPanel>
<StackPanel Grid.Column="2" Margin="0,12,0,0">
<TextBlock Text="03" FontSize="11" FontWeight="Bold"
Foreground="{StaticResource InkFaint}"
FontFamily="Cascadia Mono, Consolas" />
<TextBlock Text="Server" FontSize="11"
Foreground="{StaticResource InkFaint}"
Margin="0,2,0,0" />
</StackPanel>
<StackPanel Grid.Column="3" Margin="0,12,0,0">
<TextBlock Text="04" FontSize="11" FontWeight="Bold"
Foreground="{StaticResource InkFaint}"
FontFamily="Cascadia Mono, Consolas" />
<TextBlock Text="Photon" FontSize="11"
Foreground="{StaticResource InkFaint}"
Margin="0,2,0,0" />
</StackPanel>
<StackPanel Grid.Column="4" Margin="0,12,0,0">
<TextBlock Text="05" FontSize="11" FontWeight="Bold"
Foreground="{StaticResource InkFaint}"
FontFamily="Cascadia Mono, Consolas" />
<TextBlock Text="Done" FontSize="11"
Foreground="{StaticResource InkFaint}"
Margin="0,2,0,0" />
</StackPanel>
</Grid>
</Border>
<ScrollViewer Grid.Row="2" VerticalScrollBarVisibility="Auto"
Padding="40,16,32,16">
<Grid MaxWidth="780" HorizontalAlignment="Stretch">
<!-- Setup step 1: Host or Join? -->
<StackPanel x:Name="SetupStep1" Visibility="Visible">
<TextBlock Text="What do you want to do?" FontSize="26"
FontWeight="Bold" Margin="0,0,0,8" />
<TextBlock Text="Pick a mode. You can change this later under Settings."
Foreground="{StaticResource InkSoft}" FontSize="14"
<TextBlock Text="What do you want to do?" FontSize="28"
FontWeight="Bold" Margin="0,12,0,8" />
<TextBlock Text="Pick a mode. You can switch later from the sidebar."
Foreground="{StaticResource InkSoft}" FontSize="13"
Margin="0,0,0,28" />
<Grid MaxWidth="780" HorizontalAlignment="Left">
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="20" />
@@ -130,18 +522,30 @@
Background="{StaticResource CardBgHost}"
Click="OnSetupPickHost">
<StackPanel>
<Border CornerRadius="14" Width="56" Height="56"
Background="#3D2B57" Margin="0,0,0,18"
HorizontalAlignment="Left">
<TextBlock Text="🛠" FontSize="32"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<TextBlock Text="Host a server" FontSize="22"
FontWeight="Bold" Margin="0,0,0,10" />
<Grid Width="44" Height="44" Margin="0,0,0,18"
HorizontalAlignment="Left">
<Border Background="{StaticResource BgInset}"
BorderBrush="{StaticResource Border}"
BorderThickness="1"
CornerRadius="10" />
<Path Width="44" Height="44"
Stroke="{StaticResource Brand}"
StrokeThickness="1.6"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 14 16 L 30 16 L 30 20 L 14 20 Z M 14 22 L 30 22 L 30 26 L 14 26 Z M 14 28 L 30 28 L 30 32 L 14 32 Z M 17 18 L 18 18 M 17 24 L 18 24 M 17 30 L 18 30" />
</Grid>
<TextBlock Text="HOST" FontSize="11"
FontWeight="Bold"
Foreground="{StaticResource Brand}"
Margin="0,0,0,6" />
<TextBlock Text="Run a server" FontSize="22"
FontWeight="Bold"
Foreground="{StaticResource Ink}"
Margin="0,0,0,10" />
<TextBlock Foreground="{StaticResource InkSoft}"
FontSize="13" TextWrapping="Wrap" LineHeight="20"
Text="Run a server for you and your friends. The launcher handles the tunnel, gives you a shareable join code." />
FontSize="12" TextWrapping="Wrap" LineHeight="20"
Text="The launcher runs the server, opens a tunnel, and hands you a join code your friends can paste." />
</StackPanel>
</Button>
@@ -149,18 +553,32 @@
Background="{StaticResource CardBgJoin}"
Click="OnSetupPickJoin">
<StackPanel>
<Border CornerRadius="14" Width="56" Height="56"
Background="#1C3C4A" Margin="0,0,0,18"
HorizontalAlignment="Left">
<TextBlock Text="🎮" FontSize="32"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<TextBlock Text="Join a friend" FontSize="22"
FontWeight="Bold" Margin="0,0,0,10" />
<Grid Width="44" Height="44" Margin="0,0,0,18"
HorizontalAlignment="Left">
<Border Background="{StaticResource BgInset}"
BorderBrush="{StaticResource Border}"
BorderThickness="1"
CornerRadius="10" />
<Path Width="44" Height="44"
Stroke="{StaticResource Teal}"
StrokeThickness="1.6"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 18 26 L 26 18 M 14 22 A 6 6 0 0 1 22 14 L 24 14 M 24 30 L 22 30 A 6 6 0 0 1 14 22 M 32 22 A 6 6 0 0 1 24 30 M 24 14 A 6 6 0 0 1 32 22" />
</Grid>
<TextBlock Text="JOIN" FontSize="11"
FontWeight="Bold"
Foreground="{StaticResource Teal}"
Margin="0,0,0,6" />
<TextBlock Text="Use a code" FontSize="22"
FontWeight="Bold"
Foreground="{StaticResource Ink}"
Margin="0,0,0,10" />
<TextBlock Foreground="{StaticResource InkSoft}"
FontSize="13" TextWrapping="Wrap" LineHeight="20"
Text="You have a join code from someone. Paste it and the launcher fetches the matching patcher." />
FontSize="12" TextWrapping="Wrap" LineHeight="20"
Text="Paste a join code from a friend. The launcher fetches the matching patcher and points your client at their server." />
</StackPanel>
</Button>
</Grid>
@@ -168,115 +586,142 @@
<!-- Setup step 2: Rec Room install -->
<StackPanel x:Name="SetupStep2" Visibility="Collapsed">
<TextBlock Text="Where's Rec Room installed?" FontSize="26"
FontWeight="Bold" Margin="0,0,0,8" />
<TextBlock Text="The launcher needs to find your Rec Room install folder. Pick the *_Data folder inside the install (e.g. Recroom_Release_Data)."
Foreground="{StaticResource InkSoft}" FontSize="14"
TextWrapping="Wrap" Margin="0,0,0,24" />
<TextBlock Text="Find your Rec Room install" FontSize="28"
FontWeight="Bold" Margin="0,12,0,8" />
<TextBlock TextWrapping="Wrap"
Foreground="{StaticResource InkSoft}" FontSize="13"
Margin="0,0,0,24"
Text="Point the launcher at the *_Data folder inside your Rec Room install (e.g. Recroom_Release_Data). Auto-detection runs first; pick manually if it misses." />
<Border x:Name="SetupDetectedBanner" Visibility="Collapsed"
Style="{StaticResource Panel}"
Background="{StaticResource CardBgJoin}"
Background="{StaticResource BgElev}"
BorderBrush="{StaticResource Ok}"
BorderThickness="1"
CornerRadius="8" Padding="18,14"
Margin="0,0,0,16">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<TextBlock Text="✓" FontSize="18"
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Width="22" Height="22"
Margin="0,0,14,0" VerticalAlignment="Top">
<Ellipse Fill="{StaticResource Ok}" />
<Path Stroke="#0D0F14" StrokeThickness="2.2"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 6 11 L 9.5 14.5 L 16 7"
Width="22" Height="22" />
</Grid>
<StackPanel Grid.Column="1">
<TextBlock Text="DETECTED INSTALL" FontSize="10"
FontWeight="Bold"
Foreground="{StaticResource Ok}"
Margin="0,0,10,0" />
<TextBlock Text="We found a Rec Room install"
FontSize="15" FontWeight="SemiBold"
VerticalAlignment="Center" />
Margin="0,0,0,4" />
<TextBlock x:Name="SetupDetectedPath"
Foreground="{StaticResource Ink}"
FontSize="12"
FontFamily="Cascadia Mono, Consolas"
TextWrapping="Wrap" />
</StackPanel>
<TextBlock x:Name="SetupDetectedPath"
Foreground="{StaticResource InkSoft}"
FontSize="12"
FontFamily="Cascadia Mono, Consolas"
TextWrapping="Wrap" Margin="28,0,0,0" />
</StackPanel>
</Grid>
</Border>
<TextBlock Style="{StaticResource FieldLabel}" Text="INSTALL FOLDER" />
<DockPanel Margin="0,0,0,12">
<Button DockPanel.Dock="Right" Style="{StaticResource Btn}"
Content="Pick…" Click="OnPickRecRoom" Margin="8,0,0,0" />
<Border Background="{StaticResource BgDeeper}"
BorderBrush="{StaticResource BorderSoft}" BorderThickness="1"
CornerRadius="10" Padding="12,10">
Content="Browse…" Click="OnPickRecRoom" Margin="8,0,0,0" />
<Border Background="{StaticResource BgInset}"
BorderBrush="{StaticResource Border}" BorderThickness="1"
CornerRadius="6" Padding="12,10">
<TextBlock x:Name="SetupRecRoomPath"
Text="(not picked)"
Foreground="{StaticResource InkSoft}"
TextTrimming="CharacterEllipsis" FontSize="13" />
FontFamily="Cascadia Mono, Consolas"
TextTrimming="CharacterEllipsis" FontSize="12" />
</Border>
</DockPanel>
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="11"
TextWrapping="Wrap"
Text="Don't have Rec Room installed yet? Grab it from the Rec Room website (or wherever you usually get it). DorkNet works with any official install." />
TextWrapping="Wrap" LineHeight="18"
Text="DorkNet works with any official Rec Room install. If you don't have one, grab the regular client first." />
</StackPanel>
<!-- Setup step 3 (HOST only): Server name + WHO CAN JOIN -->
<StackPanel x:Name="SetupStep3" Visibility="Collapsed">
<TextBlock Text="Set up your server" FontSize="26"
FontWeight="Bold" Margin="0,0,0,8" />
<TextBlock Text="Name your server so friends know what they're joining."
Foreground="{StaticResource InkSoft}" FontSize="14"
<TextBlock Text="Set up your server" FontSize="28"
FontWeight="Bold" Margin="0,12,0,8" />
<TextBlock Text="A name your friends will see, and how they reach you."
Foreground="{StaticResource InkSoft}" FontSize="13"
Margin="0,0,0,24" />
<TextBlock Style="{StaticResource FieldLabel}" Text="SERVER NAME" />
<TextBox x:Name="SetupServerName" Margin="0,0,0,20"
<TextBox x:Name="SetupServerName" Margin="0,0,0,24"
LostFocus="OnSetupServerNameChanged" />
<TextBlock Style="{StaticResource FieldLabel}" Text="WHO CAN JOIN?" />
<StackPanel Margin="0,0,0,16">
<RadioButton x:Name="SetupHostInternet" GroupName="SetupHostingMode"
Content="Friends anywhere (Tunnelto)"
IsChecked="True" Margin="0,0,0,6"
Checked="OnSetupHostingModeChanged"
Foreground="{StaticResource Ink}" />
Content="Friends anywhere · Tunnelto"
IsChecked="True"
Checked="OnSetupHostingModeChanged" />
<RadioButton x:Name="SetupHostRemote" GroupName="SetupHostingMode"
Content="Friends anywhere (Tunnelto wildcard base)"
Margin="0,0,0,6"
Checked="OnSetupHostingModeChanged"
Foreground="{StaticResource Ink}" />
Content="Friends anywhere · Tunnelto wildcard base"
Checked="OnSetupHostingModeChanged" />
<RadioButton x:Name="SetupHostLocal" GroupName="SetupHostingMode"
Content="Same WiFi only (sslip.io)"
Checked="OnSetupHostingModeChanged"
Foreground="{StaticResource Ink}" />
Content="Same WiFi only · sslip.io"
Checked="OnSetupHostingModeChanged" />
</StackPanel>
<StackPanel x:Name="SetupRemoteWildcardSection" Visibility="Collapsed">
<TextBlock Style="{StaticResource FieldLabel}" Text="TUNNELTO BASE HOST" />
<TextBox x:Name="SetupRemoteWildcardInput" Margin="0,0,0,8"
FontFamily="Cascadia Mono, Consolas" FontSize="12"
LostFocus="OnRemoteWildcardChanged"
ToolTip="Service URLs become api.base, auth.base, etc." />
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="11"
TextWrapping="Wrap" LineHeight="18"
Text="Pick a base host whose provider routes wildcard subdomains to the same tunnel. The launcher derives api.base, auth.base, photon.base from it." />
</StackPanel>
<TextBlock Style="{StaticResource FieldLabel}" Text="TUNNELTO BASE HOST" />
<TextBox x:Name="SetupRemoteWildcardInput" Margin="0,0,0,12"
LostFocus="OnRemoteWildcardChanged"
ToolTip="Auto-generated. Service URLs become api.base, auth.base, etc." />
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="11"
TextWrapping="Wrap"
Text="Tunnelto mode works for friends anywhere. For subdomain support, use a base host whose provider routes wildcard subdomains to the same tunnel." />
</StackPanel>
<!-- Setup step 4 (HOST only): Photon AppIds -->
<StackPanel x:Name="SetupStep4" Visibility="Collapsed">
<TextBlock Text="Connect your Photon account" FontSize="26"
FontWeight="Bold" Margin="0,0,0,8" />
<TextBlock Text="Connect your Photon account" FontSize="28"
FontWeight="Bold" Margin="0,12,0,8" />
<TextBlock TextWrapping="Wrap"
Foreground="{StaticResource InkSoft}" FontSize="14"
Margin="0,0,0,24"
Text="DorkNet needs your own Photon account (free, takes ~2 min) to handle in-room sync. Click the walkthrough below — it'll open the Photon website, tell you what to click, and bring you back here." />
Foreground="{StaticResource InkSoft}" FontSize="13"
Margin="0,0,0,20"
Text="Photon Cloud handles in-room realtime sync. Free to register; DorkNet never ships one of its own. The walkthrough does the registration in 2 minutes." />
<Button Style="{StaticResource PrimaryBtn}"
Content="Open setup walkthrough"
Click="OnOpenPhotonWizard"
HorizontalAlignment="Left" Margin="0,0,0,20" />
HorizontalAlignment="Left" Margin="0,0,0,24">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Margin="0,0,10,0"
Stroke="White" StrokeThickness="1.6"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 4 3 L 10 3 L 10 9 M 10 3 L 5 8 M 8 8 L 8 11 L 2 11 L 2 5 L 5 5"
VerticalAlignment="Center" />
<TextBlock Text="OPEN WALKTHROUGH" FontWeight="Bold"
Foreground="White" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Border Style="{StaticResource Panel}"
Background="{StaticResource BgDeeper}"
Margin="0,0,0,12">
<Border Background="{StaticResource BgInset}"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="1" CornerRadius="8"
Padding="18" Margin="0,0,0,12">
<StackPanel>
<TextBlock Foreground="{StaticResource InkSoft}" FontSize="12"
Margin="0,0,0,8"
Text="Already have AppIds? Paste them here." />
Margin="0,0,0,14"
Text="Already have AppIds? Paste them directly." />
<TextBlock Style="{StaticResource FieldLabel}" Text="REALTIME APPID" />
<TextBox x:Name="SetupPhotonAppId" Margin="0,0,0,12"
<TextBox x:Name="SetupPhotonAppId" Margin="0,0,0,14"
FontFamily="Cascadia Mono, Consolas" FontSize="12"
LostFocus="OnSetupPhotonChanged" />
<TextBlock Style="{StaticResource FieldLabel}" Text="VOICE APPID (OPTIONAL)" />
<TextBox x:Name="SetupPhotonVoiceAppId" Margin="0,0,0,0"
<TextBlock Style="{StaticResource FieldLabel}" Text="VOICE APPID · OPTIONAL" />
<TextBox x:Name="SetupPhotonVoiceAppId"
FontFamily="Cascadia Mono, Consolas" FontSize="12"
LostFocus="OnSetupPhotonChanged" />
</StackPanel>
@@ -286,27 +731,68 @@
Text="Realtime AppId required before you can host." />
</StackPanel>
<!-- Setup done: short confirmation card -->
<!-- Setup done -->
<StackPanel x:Name="SetupStepDone" Visibility="Collapsed">
<TextBlock Text="You're all set! 🎉" FontSize="26"
<Grid Width="64" Height="64" Margin="0,12,0,24" HorizontalAlignment="Left">
<Ellipse Fill="{StaticResource BrandSoft}" />
<Path Stroke="{StaticResource Brand}" StrokeThickness="2.5"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 22 32 L 30 40 L 44 26"
Width="64" Height="64" />
</Grid>
<TextBlock Text="You're ready to ship." FontSize="28"
FontWeight="Bold" Margin="0,0,0,12" />
<TextBlock TextWrapping="Wrap"
Foreground="{StaticResource Ink}" FontSize="14"
LineHeight="22"
Text="Setup's done. Hit Finish to open the main view — everything you just configured is saved, so you can hit Start hosting straight away." />
<TextBlock TextWrapping="Wrap" MaxWidth="520"
HorizontalAlignment="Left"
Foreground="{StaticResource InkSoft}" FontSize="13"
LineHeight="20"
Text="Setup is saved. Hit Finish to open the main view — your install, server name, and Photon AppIds are remembered for next launch." />
</StackPanel>
</Grid>
</ScrollViewer>
<!-- Footer nav -->
<Grid Grid.Row="2" Margin="0,20,0,0">
<Button x:Name="SetupBackBtn" HorizontalAlignment="Left"
Style="{StaticResource GhostBtn}"
Content="← Back" Click="OnSetupBack" Visibility="Collapsed" />
<Button x:Name="SetupNextBtn" HorizontalAlignment="Right"
Style="{StaticResource PrimaryBtn}"
Content="Next →" Click="OnSetupNext" Visibility="Collapsed" />
</Grid>
<Border Grid.Row="3" Padding="40,16,40,20"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="0,1,0,0"
Background="{StaticResource BgDeeper}">
<Grid>
<Button x:Name="SetupBackBtn" HorizontalAlignment="Left"
Style="{StaticResource Btn}"
Click="OnSetupBack" Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Margin="0,0,8,0"
Stroke="{StaticResource Ink}" StrokeThickness="1.8"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 12 7 L 2 7 M 6 3 L 2 7 L 6 11"
VerticalAlignment="Center" />
<TextBlock Text="Back" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button x:Name="SetupNextBtn" HorizontalAlignment="Right"
Style="{StaticResource PrimaryBtn}"
Click="OnSetupNext" Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<TextBlock x:Name="SetupNextLabel" Text="Next"
VerticalAlignment="Center" Foreground="White"
FontWeight="Bold" />
<Path Width="14" Height="14" Margin="10,0,0,0"
Stroke="White" StrokeThickness="1.8"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 2 7 L 12 7 M 8 3 L 12 7 L 8 11"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
</Border>
</Grid>
<!-- ── Host view ──────────────────────────────────────────── -->
@@ -316,98 +802,155 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header bar with wordmark + view label + settings button -->
<Grid Grid.Row="0" Margin="0,0,0,28">
<StackPanel Orientation="Horizontal">
<TextBlock Text="DorkNet" FontSize="22" FontWeight="Bold"
Foreground="{StaticResource WordmarkBrush}" />
<Border Style="{StaticResource StatusChip}" Margin="14,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="HOST MODE" FontSize="10"
FontWeight="Bold" Foreground="{StaticResource Brand}" />
</Border>
</StackPanel>
<Button HorizontalAlignment="Right" Style="{StaticResource GhostBtn}"
Content="Settings" Click="OnOpenSettings" />
</Grid>
<Border Grid.Row="0" Padding="32,22"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="0,0,0,1">
<Grid>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="HOST" FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brand}"
FontFamily="Cascadia Mono, Consolas"
VerticalAlignment="Center" />
<Border Background="{StaticResource Brand}" Width="14" Height="1"
Margin="10,0,10,0" VerticalAlignment="Center" />
<TextBlock Text="Run a private Rec Room server" FontSize="11"
Foreground="{StaticResource InkSoft}" FontWeight="SemiBold"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Host a server" FontSize="22" FontWeight="Bold"
Foreground="{StaticResource Ink}" Margin="0,4,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
VerticalAlignment="Center">
<CheckBox x:Name="HostScreenModeChk" Content="Screen mode"
VerticalAlignment="Center" Margin="0,0,8,0"
ToolTip="Launch in 2D desktop mode (+forcemode:screen) instead of VR."
Checked="OnScreenModeChanged" Unchecked="OnScreenModeChanged" />
</StackPanel>
</Grid>
</Border>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"
Padding="0,0,8,0">
Padding="32,24,24,24">
<StackPanel>
<!-- Setup panel -->
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
<!-- Server config panel -->
<Border Style="{StaticResource Panel}" Margin="0,0,0,18">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Setup" />
<Grid Margin="0,0,0,16">
<StackPanel Orientation="Horizontal">
<Path Width="16" Height="16"
Stroke="{StaticResource Brand}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 2 4 L 14 4 L 14 7 L 2 7 Z M 2 9 L 14 9 L 14 12 L 2 12 Z M 4 5.5 L 5 5.5 M 4 10.5 L 5 10.5 M 11 5.5 L 12 5.5 M 11 10.5 L 12 10.5"
VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBlock Text="Server" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource Ink}"
VerticalAlignment="Center" />
</StackPanel>
</Grid>
<TextBlock Style="{StaticResource FieldLabel}" Text="REC ROOM INSTALL" />
<DockPanel Margin="0,0,0,16">
<Button DockPanel.Dock="Right" Style="{StaticResource Btn}"
Content="Pick…" Click="OnPickRecRoom" Margin="8,0,0,0" />
<Border Background="{StaticResource BgDeeper}"
BorderBrush="{StaticResource BorderSoft}" BorderThickness="1"
CornerRadius="10" Padding="12,10">
Content="Browse…" Click="OnPickRecRoom" Margin="8,0,0,0" />
<Border Background="{StaticResource BgInset}"
BorderBrush="{StaticResource Border}" BorderThickness="1"
CornerRadius="6" Padding="12,10">
<TextBlock x:Name="HostRecRoomPath"
Text="(not picked)"
Foreground="{StaticResource InkSoft}"
TextTrimming="CharacterEllipsis" FontSize="13" />
FontFamily="Cascadia Mono, Consolas"
TextTrimming="CharacterEllipsis" FontSize="12" />
</Border>
</DockPanel>
<TextBlock Style="{StaticResource FieldLabel}" Text="SERVER NAME (SHOWN TO JOINERS)" />
<TextBlock Style="{StaticResource FieldLabel}" Text="SERVER NAME · SHOWN TO JOINERS" />
<TextBox x:Name="ServerNameInput" Margin="0,0,0,16"
LostFocus="OnServerNameChanged" />
<TextBlock Style="{StaticResource FieldLabel}" Text="REC ROOM VERSION TO HOST FOR" />
<ComboBox x:Name="VersionSelect" Margin="0,0,0,16" />
<ComboBox x:Name="VersionSelect" Margin="0,0,0,18" />
<Border Background="{StaticResource BorderSoft}" Height="1"
Margin="0,4,0,16" />
<TextBlock Style="{StaticResource FieldLabel}" Text="WHO CAN JOIN?" />
<StackPanel>
<RadioButton x:Name="HostModeInternet" GroupName="HostingMode"
Content="Friends anywhere (Tunnelto)"
Margin="0,0,0,6"
IsChecked="True" Checked="OnHostingModeChanged"
Foreground="{StaticResource Ink}" />
Content="Friends anywhere · Tunnelto"
IsChecked="True" Checked="OnHostingModeChanged" />
<RadioButton x:Name="HostModeRemote" GroupName="HostingMode"
Content="Tunnelto wildcard base"
Margin="0,0,0,6" Checked="OnHostingModeChanged"
Foreground="{StaticResource Ink}" />
Checked="OnHostingModeChanged" />
<RadioButton x:Name="HostModeLocal" GroupName="HostingMode"
Content="Same WiFi only (sslip.io)"
Checked="OnHostingModeChanged"
Foreground="{StaticResource Ink}" />
Content="Same WiFi only · sslip.io"
Checked="OnHostingModeChanged" />
</StackPanel>
<StackPanel x:Name="HostRemoteWildcardSection" Visibility="Collapsed"
Margin="0,16,0,0">
<TextBlock Style="{StaticResource FieldLabel}" Text="TUNNELTO BASE HOST" />
<TextBox x:Name="RemoteWildcardInput"
FontFamily="Cascadia Mono, Consolas" FontSize="12"
LostFocus="OnRemoteWildcardChanged"
ToolTip="Service URLs become api.base, auth.base, etc." />
</StackPanel>
<TextBlock Style="{StaticResource FieldLabel}" Text="TUNNELTO BASE HOST" Margin="0,14,0,6" />
<TextBox x:Name="RemoteWildcardInput" Margin="0,0,0,0"
LostFocus="OnRemoteWildcardChanged"
ToolTip="Auto-generated. Service URLs become api.base, auth.base, etc." />
</StackPanel>
</Border>
<!-- Photon panel -->
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
<!-- Photon config panel -->
<Border Style="{StaticResource Panel}" Margin="0,0,0,18">
<StackPanel>
<Grid Margin="0,0,0,12">
<TextBlock Style="{StaticResource SectionTitle}"
Text="Photon AppIds" Margin="0" />
<Grid Margin="0,0,0,16">
<StackPanel Orientation="Horizontal">
<Path Width="16" Height="16"
Stroke="{StaticResource Brand}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 8 1 L 12 7 L 8 7 L 8 15 L 4 9 L 8 9 Z"
VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBlock Text="Photon Cloud" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource Ink}"
VerticalAlignment="Center" />
</StackPanel>
<Button HorizontalAlignment="Right" Style="{StaticResource Btn}"
Content="Setup walkthrough" Click="OnOpenPhotonWizard"
Padding="14,6" FontSize="12" />
Click="OnOpenPhotonWizard"
Padding="12,5" FontSize="11">
<StackPanel Orientation="Horizontal">
<Path Width="11" Height="11" Margin="0,0,6,0"
Stroke="{StaticResource Ink}" StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 4 2 L 9 2 L 9 7 M 9 2 L 4 7"
VerticalAlignment="Center" />
<TextBlock Text="WALKTHROUGH" FontSize="11" FontWeight="SemiBold"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
<TextBlock Foreground="{StaticResource InkSoft}" FontSize="12"
Margin="0,0,0,12" TextWrapping="Wrap">
Photon Cloud powers in-room realtime sync. Free to register at
Margin="0,0,0,16" TextWrapping="Wrap" LineHeight="18">
Powers in-room realtime sync. Free at
<Hyperlink NavigateUri="https://dashboard.photonengine.com"
Foreground="{StaticResource Brand}"
TextDecorations="None"
RequestNavigate="OnNavigate">dashboard.photonengine.com</Hyperlink>;
DorkNet never bundles one. First time? Click "Setup walkthrough" above.
your AppIds stay local.
</TextBlock>
<TextBlock Style="{StaticResource FieldLabel}" Text="REALTIME APPID" />
<TextBox x:Name="PhotonAppIdInput" Margin="0,0,0,12"
<TextBox x:Name="PhotonAppIdInput" Margin="0,0,0,14"
FontFamily="Cascadia Mono, Consolas" FontSize="12"
LostFocus="OnPhotonChanged" />
<TextBlock Style="{StaticResource FieldLabel}" Text="VOICE APPID (OPTIONAL)" />
<TextBox x:Name="PhotonVoiceAppIdInput" Margin="0,0,0,12"
<TextBlock Style="{StaticResource FieldLabel}" Text="VOICE APPID · OPTIONAL" />
<TextBox x:Name="PhotonVoiceAppIdInput" Margin="0,0,0,14"
FontFamily="Cascadia Mono, Consolas" FontSize="12"
LostFocus="OnPhotonChanged" />
<TextBlock Style="{StaticResource FieldLabel}" Text="CLOUD REGION" />
@@ -425,38 +968,94 @@
</StackPanel>
</Border>
<!-- Action area -->
<StackPanel Orientation="Horizontal" Margin="0,4,0,16">
<Button x:Name="HostStartBtn" Style="{StaticResource PrimaryBtn}"
Content="Start hosting" Click="OnHostStart" />
<Button x:Name="HostStopBtn" Style="{StaticResource Btn}"
Content="Stop" Click="OnHostStop"
Margin="10,0,0,0" Visibility="Collapsed" />
<Button x:Name="HostLaunchBtn" Style="{StaticResource TealBtn}"
Content="Launch Rec Room" Click="OnLaunchRecRoom"
Margin="10,0,0,0" Visibility="Collapsed" />
<CheckBox x:Name="HostScreenModeChk" Content="Screen mode"
VerticalAlignment="Center" Margin="14,0,0,0"
Foreground="{StaticResource Ink}"
ToolTip="Launch in 2D desktop mode (passes +forcemode:screen) instead of VR."
Checked="OnScreenModeChanged" Unchecked="OnScreenModeChanged" />
</StackPanel>
<!-- Primary action row -->
<Border Background="{StaticResource BgElev}"
BorderBrush="{StaticResource Border}"
BorderThickness="1"
CornerRadius="8" Padding="22"
Margin="0,0,0,18">
<StackPanel Orientation="Horizontal">
<Button x:Name="HostStartBtn" Style="{StaticResource PrimaryBtn}"
Click="OnHostStart">
<StackPanel Orientation="Horizontal">
<Path Width="11" Height="11" Margin="0,0,10,0"
Fill="White"
Data="M 0 0 L 11 5.5 L 0 11 Z"
VerticalAlignment="Center" />
<TextBlock Text="START HOSTING" FontWeight="Bold"
Foreground="White" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button x:Name="HostStopBtn" Style="{StaticResource Btn}"
Click="OnHostStop"
Margin="10,0,0,0" Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<Rectangle Width="10" Height="10" Margin="0,0,8,0"
Fill="{StaticResource Danger}"
VerticalAlignment="Center" />
<TextBlock Text="STOP" FontWeight="Bold"
VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button x:Name="HostLaunchBtn" Style="{StaticResource PlayBtn}"
Click="OnLaunchRecRoom"
Margin="10,0,0,0" Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<Path Width="13" Height="13" Margin="0,0,12,0"
Fill="White"
Data="M 0 0 L 13 6.5 L 0 13 Z"
VerticalAlignment="Center" />
<TextBlock Text="LAUNCH REC ROOM" FontWeight="Bold"
Foreground="White" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button x:Name="HostAdminBtn" Style="{StaticResource Btn}"
Click="OnOpenAdminPanel"
ToolTip="Open the server's admin web panel in your browser. First account on a fresh server becomes the admin automatically."
Margin="10,0,0,0" Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<Path Width="13" Height="13" Margin="0,0,10,0"
Stroke="{StaticResource Ink}" StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 4 3 L 11 3 L 11 10 M 11 3 L 5 9 M 9 9 L 9 12 L 2 12 L 2 5 L 5 5"
VerticalAlignment="Center" />
<TextBlock Text="ADMIN PANEL" FontWeight="Bold"
VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</Border>
<!-- Progress steps — populated when the user hits
"Start hosting". Hidden until then so the
idle Host view stays clean. -->
<!-- Progress steps panel -->
<Border x:Name="HostStepsPanel" Style="{StaticResource Panel}"
Margin="0,0,0,16" Visibility="Collapsed">
Margin="0,0,0,18" Visibility="Collapsed">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Progress" />
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
<Path Width="16" Height="16"
Stroke="{StaticResource Brand}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 2 8 L 6 12 L 14 4"
VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBlock Text="Progress" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource Ink}"
VerticalAlignment="Center" />
</StackPanel>
<ItemsControl x:Name="HostStepsList"
ItemTemplate="{StaticResource StepRowTemplate}" />
<TextBlock x:Name="HostStatusText"
Foreground="{StaticResource InkSoft}"
TextWrapping="Wrap" FontSize="13"
TextWrapping="Wrap" FontSize="12"
LineHeight="18"
Margin="0,4,0,0" />
<StackPanel x:Name="HostRetryPanel" Visibility="Collapsed"
Orientation="Horizontal" Margin="0,12,0,0">
Orientation="Horizontal" Margin="0,16,0,0">
<Button x:Name="HostRetryBtn" Style="{StaticResource PrimaryBtn}"
Content="Retry" Click="OnHostRetry" />
<Button Style="{StaticResource GhostBtn}"
@@ -467,28 +1066,51 @@
</StackPanel>
</Border>
<!-- 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. -->
<!-- Join code "ticket" — visible once host is up.
Designed as a distinct artefact: monospace
join code, QR alongside, three share actions. -->
<Border x:Name="JoinCodePanel" Visibility="Collapsed"
Style="{StaticResource Panel}"
Background="{StaticResource CardBgHost}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}"
Text="🎉 Share this with friends" />
<Grid>
Background="{StaticResource BgElev}"
BorderBrush="{StaticResource Brand}"
BorderThickness="1"
CornerRadius="8" Padding="0"
Margin="0,0,0,18">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Grid.Row="0" Background="{StaticResource Brand}"
Padding="22,12">
<Grid>
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Margin="0,0,10,0"
Stroke="White" StrokeThickness="1.8"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 3 4 L 11 4 L 11 12 L 3 12 Z M 1 6 L 1 14 L 9 14"
VerticalAlignment="Center" />
<TextBlock Text="SERVER LIVE · SHARE THIS WITH FRIENDS"
FontSize="11" FontWeight="Bold"
Foreground="White"
VerticalAlignment="Center" />
</StackPanel>
<Ellipse HorizontalAlignment="Right" VerticalAlignment="Center"
Width="8" Height="8" Fill="White" />
</Grid>
</Border>
<Grid Grid.Row="1" Margin="22">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="White"
CornerRadius="10" Padding="8"
CornerRadius="6" Padding="8"
VerticalAlignment="Top"
Margin="0,0,16,0">
Margin="0,0,22,0">
<Image x:Name="JoinCodeQrImage"
Width="180" Height="180"
Width="172" Height="172"
RenderOptions.BitmapScalingMode="NearestNeighbor"
SnapsToDevicePixels="True"
UseLayoutRounding="True" />
@@ -497,34 +1119,55 @@
<StackPanel Grid.Column="1">
<TextBlock Style="{StaticResource FieldLabel}"
Text="JOIN CODE" />
<Border Background="{StaticResource BgDeeper}"
<Border Background="{StaticResource BgInset}"
BorderBrush="{StaticResource Border}" BorderThickness="1"
CornerRadius="10" Padding="12,10">
CornerRadius="6" Padding="12,10">
<TextBox x:Name="JoinCodeText" IsReadOnly="True"
FontFamily="Cascadia Mono, Consolas" FontSize="12"
FontFamily="Cascadia Mono, Consolas" FontSize="11"
Background="Transparent" BorderThickness="0"
TextWrapping="Wrap" />
</Border>
<WrapPanel Margin="0,12,0,0">
<WrapPanel Margin="0,14,0,0">
<Button Style="{StaticResource PrimaryBtn}"
Content="Copy code" Click="OnCopyJoinCode" />
Click="OnCopyJoinCode" Padding="16,8">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Margin="0,0,8,0"
Stroke="White" StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 3 2 L 9 2 L 9 8 L 3 8 Z M 1 4 L 1 10 L 7 10"
VerticalAlignment="Center" />
<TextBlock Text="COPY CODE" FontWeight="Bold"
Foreground="White" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Style="{StaticResource Btn}"
Content="Copy as message"
Click="OnCopyJoinCodeMessage"
Margin="10,0,0,0" />
Margin="8,0,0,0" Padding="14,7">
<TextBlock Text="Copy as message" />
</Button>
<Button Style="{StaticResource Btn}"
Content="Email to friend"
Click="OnEmailJoinCode"
Margin="10,0,0,0" />
Margin="8,0,0,0" Padding="14,7">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="9" Margin="0,0,8,0"
Stroke="{StaticResource Ink}" StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 1 1 L 11 1 L 11 8 L 1 8 Z M 1 1 L 6 5 L 11 1"
VerticalAlignment="Center" />
<TextBlock Text="Email" VerticalAlignment="Center" />
</StackPanel>
</Button>
</WrapPanel>
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="11"
TextWrapping="Wrap" Margin="0,12,0,0"
Text="Your friend opens DorkNet, picks &quot;Join a friend&quot;, and pastes the code. The QR encodes the same text — handy for getting it onto a phone for forwarding." />
TextWrapping="Wrap" Margin="0,14,0,0" LineHeight="18"
Text="Friend opens DorkNet → Join a friend → pastes. QR encodes the same string for phone forwarding." />
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</Border>
</StackPanel>
</ScrollViewer>
@@ -537,97 +1180,188 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,28">
<StackPanel Orientation="Horizontal">
<TextBlock Text="DorkNet" FontSize="22" FontWeight="Bold"
Foreground="{StaticResource WordmarkBrush}" />
<Border Style="{StaticResource StatusChip}" Margin="14,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="JOIN MODE" FontSize="10"
FontWeight="Bold" Foreground="{StaticResource Teal}" />
</Border>
</StackPanel>
<Button HorizontalAlignment="Right" Style="{StaticResource GhostBtn}"
Content="Settings" Click="OnOpenSettings" />
</Grid>
<Border Grid.Row="0" Padding="32,22"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="0,0,0,1">
<Grid>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="JOIN" FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Teal}"
FontFamily="Cascadia Mono, Consolas"
VerticalAlignment="Center" />
<Border Background="{StaticResource Teal}" Width="14" Height="1"
Margin="10,0,10,0" VerticalAlignment="Center" />
<TextBlock Text="Connect to a friend's private server" FontSize="11"
Foreground="{StaticResource InkSoft}" FontWeight="SemiBold"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Join a friend" FontSize="22" FontWeight="Bold"
Foreground="{StaticResource Ink}" Margin="0,4,0,0" />
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
VerticalAlignment="Center">
<CheckBox x:Name="JoinScreenModeChk" Content="Screen mode"
VerticalAlignment="Center"
ToolTip="Launch in 2D desktop mode (+forcemode:screen) instead of VR."
Checked="OnScreenModeChanged" Unchecked="OnScreenModeChanged" />
</StackPanel>
</Grid>
</Border>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"
Padding="0,0,8,0">
Padding="32,24,24,24">
<StackPanel>
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
<Border Style="{StaticResource Panel}" Margin="0,0,0,18">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Join code" />
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
<Path Width="16" Height="16"
Stroke="{StaticResource Teal}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 6 10 L 10 6 M 4 8 A 3 3 0 0 1 8 4 L 10 4 M 10 12 L 8 12 A 3 3 0 0 1 4 8 M 14 8 A 3 3 0 0 1 10 12 M 10 4 A 3 3 0 0 1 14 8"
VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBlock Text="Join code" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource Ink}"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock Style="{StaticResource FieldLabel}"
Text="PASTE THE JOIN CODE YOUR HOST SHARED" />
Text="PASTE THE CODE YOUR HOST SHARED" />
<TextBox x:Name="JoinCodeInput"
AcceptsReturn="True" TextWrapping="Wrap"
Height="80" VerticalContentAlignment="Top"
FontFamily="Cascadia Mono, Consolas" FontSize="12"
Height="92" VerticalContentAlignment="Top"
FontFamily="Cascadia Mono, Consolas" FontSize="11"
TextChanged="OnJoinCodeInputChanged" />
<Button Style="{StaticResource Btn}"
Content="Preview" Click="OnDecodeJoinCode"
HorizontalAlignment="Left" Margin="0,10,0,0" />
HorizontalAlignment="Left" Margin="0,12,0,0" />
</StackPanel>
</Border>
<Border x:Name="JoinPreviewBorder" Visibility="Collapsed"
Style="{StaticResource Panel}"
Background="{StaticResource CardBgJoin}"
Margin="0,0,0,16">
Background="{StaticResource BgElev}"
BorderBrush="{StaticResource Teal}"
BorderThickness="1"
CornerRadius="8" Padding="22"
Margin="0,0,0,18">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Server preview" />
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
<Path Width="16" Height="16"
Stroke="{StaticResource Teal}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 2 8 L 6 12 L 14 4"
VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBlock Text="Server preview" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource Ink}"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock x:Name="JoinPreviewText"
Foreground="{StaticResource Ink}"
TextWrapping="Wrap" FontSize="14" />
TextWrapping="Wrap" FontSize="12"
FontFamily="Cascadia Mono, Consolas"
LineHeight="20" />
</StackPanel>
</Border>
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
<Border Style="{StaticResource Panel}" Margin="0,0,0,18">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Your Rec Room install" />
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
<Path Width="16" Height="16"
Stroke="{StaticResource Teal}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
Data="M 2 5 L 6 5 L 8 7 L 14 7 L 14 13 L 2 13 Z"
VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBlock Text="Rec Room install" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource Ink}"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock Style="{StaticResource FieldLabel}" Text="LOCATION" />
<DockPanel>
<Button DockPanel.Dock="Right" Style="{StaticResource Btn}"
Content="Pick…" Click="OnPickRecRoom" Margin="8,0,0,0" />
<Border Background="{StaticResource BgDeeper}"
BorderBrush="{StaticResource BorderSoft}" BorderThickness="1"
CornerRadius="10" Padding="12,10">
Content="Browse…" Click="OnPickRecRoom" Margin="8,0,0,0" />
<Border Background="{StaticResource BgInset}"
BorderBrush="{StaticResource Border}" BorderThickness="1"
CornerRadius="6" Padding="12,10">
<TextBlock x:Name="JoinRecRoomPath"
Text="(not picked)"
Foreground="{StaticResource InkSoft}"
TextTrimming="CharacterEllipsis" FontSize="13" />
FontFamily="Cascadia Mono, Consolas"
TextTrimming="CharacterEllipsis" FontSize="12" />
</Border>
</DockPanel>
</StackPanel>
</Border>
<StackPanel Orientation="Horizontal" Margin="0,4,0,16">
<Button x:Name="JoinApplyBtn" Style="{StaticResource PrimaryBtn}"
Content="Patch" Click="OnJoinApply"
IsEnabled="False" />
<Button x:Name="JoinLaunchBtn" Style="{StaticResource TealBtn}"
Content="Launch Rec Room" Click="OnLaunchRecRoom"
Margin="10,0,0,0" Visibility="Collapsed" />
<CheckBox x:Name="JoinScreenModeChk" Content="Screen mode"
VerticalAlignment="Center" Margin="14,0,0,0"
Foreground="{StaticResource Ink}"
ToolTip="Launch in 2D desktop mode (passes +forcemode:screen) instead of VR."
Checked="OnScreenModeChanged" Unchecked="OnScreenModeChanged" />
</StackPanel>
<Border Background="{StaticResource BgElev}"
BorderBrush="{StaticResource Border}"
BorderThickness="1"
CornerRadius="8" Padding="22"
Margin="0,0,0,18">
<StackPanel Orientation="Horizontal">
<Button x:Name="JoinApplyBtn" Style="{StaticResource PrimaryBtn}"
Click="OnJoinApply" IsEnabled="False">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Margin="0,0,10,0"
Stroke="White" StrokeThickness="1.7"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 2 6 L 5 9 L 11 2"
VerticalAlignment="Center" />
<TextBlock Text="APPLY PATCH" FontWeight="Bold"
Foreground="White" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button x:Name="JoinLaunchBtn" Style="{StaticResource PlayBtn}"
Click="OnLaunchRecRoom"
Margin="10,0,0,0" Visibility="Collapsed">
<StackPanel Orientation="Horizontal">
<Path Width="13" Height="13" Margin="0,0,12,0"
Fill="White"
Data="M 0 0 L 13 6.5 L 0 13 Z"
VerticalAlignment="Center" />
<TextBlock Text="LAUNCH REC ROOM" FontWeight="Bold"
Foreground="White" VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</Border>
<Border x:Name="JoinStepsPanel" Style="{StaticResource Panel}"
Visibility="Collapsed">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Progress" />
<StackPanel Orientation="Horizontal" Margin="0,0,0,16">
<Path Width="16" Height="16"
Stroke="{StaticResource Teal}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 2 8 L 6 12 L 14 4"
VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBlock Text="Progress" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource Ink}"
VerticalAlignment="Center" />
</StackPanel>
<ItemsControl x:Name="JoinStepsList"
ItemTemplate="{StaticResource StepRowTemplate}" />
<TextBlock x:Name="JoinStatusText"
Foreground="{StaticResource InkSoft}"
TextWrapping="Wrap" FontSize="13"
TextWrapping="Wrap" FontSize="12"
LineHeight="18"
Margin="0,4,0,0" />
<StackPanel x:Name="JoinRetryPanel" Visibility="Collapsed"
Orientation="Horizontal" Margin="0,12,0,0">
Orientation="Horizontal" Margin="0,16,0,0">
<Button x:Name="JoinRetryBtn" Style="{StaticResource PrimaryBtn}"
Content="Retry" Click="OnJoinRetry" />
<Button Style="{StaticResource GhostBtn}"
@@ -648,82 +1382,150 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="0,0,0,28">
<StackPanel Orientation="Horizontal">
<TextBlock Text="DorkNet" FontSize="22" FontWeight="Bold"
Foreground="{StaticResource WordmarkBrush}" />
<Border Style="{StaticResource StatusChip}" Margin="14,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="SETTINGS" FontSize="10"
FontWeight="Bold" Foreground="{StaticResource Sunshine}" />
<Border Grid.Row="0" Padding="32,22"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="0,0,0,1">
<Grid>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="SYSTEM" FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Sunshine}"
FontFamily="Cascadia Mono, Consolas"
VerticalAlignment="Center" />
<Border Background="{StaticResource Sunshine}" Width="14" Height="1"
Margin="10,0,10,0" VerticalAlignment="Center" />
<TextBlock Text="Cache locations &amp; advanced options" FontSize="11"
Foreground="{StaticResource InkSoft}" FontWeight="SemiBold"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="Settings" FontSize="22" FontWeight="Bold"
Foreground="{StaticResource Ink}" Margin="0,4,0,0" />
</StackPanel>
<Button HorizontalAlignment="Right" Style="{StaticResource GhostBtn}"
Click="OnBackFromSettings" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<Path Width="12" Height="12" Margin="0,0,8,0"
Stroke="{StaticResource InkSoft}" StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 11 6 L 1 6 M 5 2 L 1 6 L 5 10"
VerticalAlignment="Center" />
<TextBlock Text="Back" VerticalAlignment="Center" />
</StackPanel>
</Button>
</Grid>
</Border>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto"
Padding="32,24,24,24">
<StackPanel>
<Border Style="{StaticResource Panel}" Margin="0,0,0,18">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Cache locations" />
<Border Background="{StaticResource BgInset}"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="1" CornerRadius="6"
Padding="14,12">
<TextBlock x:Name="CachePathsText"
Foreground="{StaticResource InkSoft}"
FontSize="11" TextWrapping="Wrap"
LineHeight="18"
FontFamily="Cascadia Mono, Consolas" />
</Border>
</StackPanel>
</Border>
<Border x:Name="LocalOverridesPanel" Visibility="Collapsed"
Background="{StaticResource BgElev}"
BorderBrush="{StaticResource Sunshine}"
BorderThickness="1"
CornerRadius="8" Padding="22"
Margin="0,0,0,18">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,12">
<Path Width="16" Height="16"
Stroke="{StaticResource Sunshine}"
StrokeThickness="1.5"
Fill="Transparent"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 8 1 L 15 14 L 1 14 Z M 8 6 L 8 10 M 8 12 L 8 12.5"
VerticalAlignment="Center" Margin="0,0,10,0" />
<TextBlock Text="Dev overrides active" FontSize="14" FontWeight="SemiBold"
Foreground="{StaticResource Ink}"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock Foreground="{StaticResource InkSoft}" FontSize="12"
Margin="0,0,0,10" TextWrapping="Wrap" LineHeight="18"
Text="The launcher is reading from local paths instead of GitHub. Unset DORKNET_LOCAL_MANIFEST / DORKNET_LOCAL_RELEASES to return to normal mode." />
<Border Background="{StaticResource BgInset}"
BorderBrush="{StaticResource BorderSoft}"
BorderThickness="1" CornerRadius="6"
Padding="14,12">
<TextBlock x:Name="LocalOverridesText"
Foreground="{StaticResource Ink}"
FontSize="11" TextWrapping="Wrap"
LineHeight="18"
FontFamily="Cascadia Mono, Consolas" />
</Border>
</StackPanel>
</Border>
<Border Style="{StaticResource Panel}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="About" />
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="VERSION"
FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource InkFaint}"
Width="100" />
<TextBlock Grid.Column="1" Text="v0.1"
FontSize="12" Foreground="{StaticResource Ink}"
FontFamily="Cascadia Mono, Consolas" />
</Grid>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="LICENSE"
FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource InkFaint}"
Width="100" />
<TextBlock Grid.Column="1" Text="AGPL-3.0"
FontSize="12" Foreground="{StaticResource Ink}"
FontFamily="Cascadia Mono, Consolas" />
</Grid>
<Grid Margin="0,0,0,8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="SOURCE"
FontSize="10" FontWeight="SemiBold"
Foreground="{StaticResource InkFaint}"
Width="100" />
<TextBlock Grid.Column="1" FontSize="12" FontFamily="Cascadia Mono, Consolas">
<Hyperlink NavigateUri="https://github.com/DorkSquadRR/DorkNet"
Foreground="{StaticResource Brand}"
TextDecorations="None"
RequestNavigate="OnNavigate">github.com/DorkSquadRR/DorkNet</Hyperlink>
</TextBlock>
</Grid>
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="10"
Margin="0,12,0,0"
Text="Not affiliated with Rec Room Inc. or Against Gravity." />
</StackPanel>
</Border>
</StackPanel>
<Button HorizontalAlignment="Right" Style="{StaticResource GhostBtn}"
Content="← Back" Click="OnBackFromSettings" />
</Grid>
<StackPanel Grid.Row="1">
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Switch mode" />
<StackPanel Orientation="Horizontal">
<Button Style="{StaticResource Btn}"
Content="Host" Click="OnPickHost" />
<Button Style="{StaticResource Btn}"
Content="Join" Click="OnPickJoin"
Margin="10,0,0,0" />
<Button Style="{StaticResource GhostBtn}"
Content="Re-run setup" Click="OnRerunSetup"
Margin="10,0,0,0" />
</StackPanel>
</StackPanel>
</Border>
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="Cache locations" />
<TextBlock x:Name="CachePathsText"
Foreground="{StaticResource InkSoft}"
FontSize="12" TextWrapping="Wrap"
FontFamily="Cascadia Mono, Consolas" />
</StackPanel>
</Border>
<!-- Dev-mode overrides — visible only when at least one
env var is set so end users never see it. -->
<Border x:Name="LocalOverridesPanel" Visibility="Collapsed"
Style="{StaticResource Panel}" Margin="0,0,0,16"
Background="{StaticResource CardBgHost}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}"
Text="🛠 Dev overrides active" />
<TextBlock Foreground="{StaticResource InkSoft}" FontSize="12"
Margin="0,-6,0,8" TextWrapping="Wrap"
Text="The launcher is reading from local paths instead of GitHub. Unset DORKNET_LOCAL_MANIFEST / DORKNET_LOCAL_RELEASES to return to normal mode." />
<TextBlock x:Name="LocalOverridesText"
Foreground="{StaticResource Ink}"
FontSize="12" TextWrapping="Wrap"
FontFamily="Cascadia Mono, Consolas" />
</StackPanel>
</Border>
<Border Style="{StaticResource Panel}">
<StackPanel>
<TextBlock Style="{StaticResource SectionTitle}" Text="About" />
<TextBlock Foreground="{StaticResource InkSoft}" FontSize="13">
DorkNet Launcher v0.1 ·
<Hyperlink NavigateUri="https://github.com/DorkSquadRR/DorkNet"
Foreground="{StaticResource Brand}"
RequestNavigate="OnNavigate">github.com/DorkSquadRR/DorkNet</Hyperlink>
· AGPL-3.0
</TextBlock>
<TextBlock Foreground="{StaticResource InkFaint}" FontSize="11"
Margin="0,8,0,0">
Not affiliated with Rec Room Inc. or Against Gravity.
</TextBlock>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</Grid>
+154 -1
View File
@@ -17,6 +17,7 @@ public partial class MainWindow : Window
private readonly ReleaseDownloader _releases = new();
private readonly ClientPatcher _patcher = new();
private readonly ServerProcess _server = new();
private readonly Updater _updater = new();
private IAsyncDisposable? _tunnel;
private VersionsManifest? _manifest;
private string? _hostApex;
@@ -53,6 +54,59 @@ public partial class MainWindow : Window
ReflectStateInUi();
await RefreshManifestAsync();
AutoSelectVersionFromInstall();
// Update check runs last, async fire-and-forget. Failures are
// swallowed inside Updater.CheckAsync — the launcher must never
// block on a slow GitHub roundtrip.
SidebarVersionText.Text = $"DORKNET LAUNCHER · v{Updater.CurrentVersion}";
_ = CheckForUpdatesAsync();
}
private UpdateInfo? _pendingUpdate;
private async Task CheckForUpdatesAsync()
{
var info = await _updater.CheckAsync();
if (info is null) return;
_pendingUpdate = info;
UpdateBannerVersionText.Text = $"v{Updater.CurrentVersion} → v{info.Version}";
UpdateBanner.Visibility = Visibility.Visible;
}
private async void OnInstallUpdate(object sender, RoutedEventArgs e)
{
if (_pendingUpdate is null) return;
UpdateInstallBtn.IsEnabled = false;
UpdateBannerTitle.Text = "Downloading…";
try
{
var progress = new Progress<DownloadProgress>(p =>
{
var mb = p.BytesRead / (1024.0 * 1024.0);
UpdateBannerVersionText.Text = p.TotalBytes > 0
? $"{mb:F1} / {p.TotalBytes / (1024.0 * 1024.0):F1} MB"
: $"{mb:F1} MB";
});
var script = await _updater.StageUpdateAsync(_pendingUpdate, progress);
UpdateBannerTitle.Text = "Restarting…";
// Launch the swap script detached, then bail out so it can
// overwrite the running exe.
await ShutdownServerAsync();
Process.Start(new ProcessStartInfo("cmd", $"/c \"{script}\"")
{
CreateNoWindow = true,
UseShellExecute = false,
});
Application.Current.Shutdown();
}
catch (Exception ex)
{
UpdateBannerTitle.Text = "Update failed";
UpdateBannerVersionText.Text = ex.Message.Length > 80
? ex.Message.Substring(0, 80) + "…" : ex.Message;
UpdateInstallBtn.IsEnabled = true;
}
}
/// <summary>If the user's install path resolves to a known build,
@@ -87,6 +141,17 @@ public partial class MainWindow : Window
HostView.Visibility = name == "host" ? Visibility.Visible : Visibility.Collapsed;
JoinView.Visibility = name == "join" ? Visibility.Visible : Visibility.Collapsed;
SettingsView.Visibility = name == "settings" ? Visibility.Visible : Visibility.Collapsed;
// Sidebar is part of the in-product chrome — hide it for the
// hero-style Welcome and the dedicated Setup wizard.
if (MainNav is not null)
MainNav.Visibility = (name == "host" || name == "join" || name == "settings")
? Visibility.Visible : Visibility.Collapsed;
if (NavHostBtn is not null) NavHostBtn.IsChecked = name == "host";
if (NavJoinBtn is not null) NavJoinBtn.IsChecked = name == "join";
if (NavSettingsBtn is not null) NavSettingsBtn.IsChecked = name == "settings";
if (name == "setup") RenderSetupStep();
}
@@ -164,6 +229,7 @@ public partial class MainWindow : Window
}
UpdateSetupPhotonStatus();
UpdateRemoteWildcardVisibility();
CachePathsText.Text =
$"State: {AppPaths.StateFile}\n" +
@@ -321,10 +387,14 @@ public partial class MainWindow : Window
var displayed = (_state.Mode == AppMode.Join && _setupStep == 5) ? 3 : _setupStep;
SetupStepIndicator.Text = $"STEP {displayed} OF {total}";
// Drive the horizontal stepper bars + numbered labels under each.
// A bar is "filled" when its step is the current step or behind it.
UpdateSetupStepperVisuals();
// Step 1 (mode pick) has no Back / Next — the cards self-advance.
SetupBackBtn.Visibility = _setupStep > 1 ? Visibility.Visible : Visibility.Collapsed;
SetupNextBtn.Visibility = _setupStep == 1 ? Visibility.Collapsed : Visibility.Visible;
SetupNextBtn.Content = _setupStep == 5 ? "Finish →" : "Next →";
SetupNextLabel.Text = _setupStep == 5 ? "Finish" : "Next";
// Refresh the install banner + Photon status each render — the
// user may have hit "Pick…" mid-wizard and we want the new path
@@ -335,6 +405,33 @@ public partial class MainWindow : Window
? Visibility.Collapsed : Visibility.Visible;
if (!string.IsNullOrEmpty(_state.RecRoomPath)) SetupDetectedPath.Text = _state.RecRoomPath;
UpdateSetupPhotonStatus();
// Re-sync the hosting-mode radios + base-host visibility every
// time we render. The Checked event during ReflectStateInUi
// doesn't always fire (XAML default IsChecked="True" on the
// first radio can race the initial sync), so re-asserting here
// guarantees step 3 shows the right state when the user lands
// on it via Next or Re-run setup.
if (SetupHostInternet is not null && SetupHostRemote is not null && SetupHostLocal is not null)
{
SetupHostInternet.IsChecked = _state.HostingMode == HostingMode.Internet;
SetupHostRemote.IsChecked = _state.HostingMode == HostingMode.RemoteWildcard;
SetupHostLocal.IsChecked = _state.HostingMode == HostingMode.LocalNetwork;
}
UpdateRemoteWildcardVisibility();
}
/// <summary>Repaint the 5-segment stepper rail to reflect the current
/// step. Filled bars trail behind the active step; the active bar
/// itself is brand-coloured. This is the visual replacement for the
/// old single chip.</summary>
private void UpdateSetupStepperVisuals()
{
var brand = (System.Windows.Media.Brush)FindResource("Brand");
var dim = (System.Windows.Media.Brush)FindResource("Border");
var bars = new[] { StepBar1, StepBar2, StepBar3, StepBar4, StepBar5 };
for (int i = 0; i < bars.Length; i++)
bars[i].Background = (i + 1) <= _setupStep ? brand : dim;
}
private void UpdateSetupPhotonStatus()
@@ -377,6 +474,20 @@ public partial class MainWindow : Window
HostModeLocal.IsChecked = _state.HostingMode == HostingMode.LocalNetwork;
HostModeRemote.IsChecked = _state.HostingMode == HostingMode.RemoteWildcard;
}
UpdateRemoteWildcardVisibility();
}
/// <summary>The Tunnelto base host field only matters when the user
/// picks the wildcard-base option. Plain Tunnelto auto-generates it;
/// LAN doesn't use it at all. Hide it everywhere else.</summary>
private void UpdateRemoteWildcardVisibility()
{
var show = _state.HostingMode == HostingMode.RemoteWildcard
? Visibility.Visible : Visibility.Collapsed;
if (SetupRemoteWildcardSection is not null)
SetupRemoteWildcardSection.Visibility = show;
if (HostRemoteWildcardSection is not null)
HostRemoteWildcardSection.Visibility = show;
}
private void OnSetupPhotonChanged(object sender, RoutedEventArgs e)
@@ -494,9 +605,21 @@ public partial class MainWindow : Window
? HostingMode.LocalNetwork
: HostingMode.Internet;
_state.Save();
UpdateRemoteWildcardVisibility();
}
// ── Host flow ───────────────────────────────────────────────────────
/// <summary>Reflect the running state on the sidebar status pill.
/// "Idle"/grey when nothing's running, "Starting"/amber while the
/// host steps are in flight, "Live"/mint once the server is up,
/// "Failed"/red on error.</summary>
private void SetSidebarStatus(string label, string colorKey)
{
if (SidebarStatusText is null || SidebarStatusDot is null) return;
SidebarStatusText.Text = label;
SidebarStatusDot.Fill = (System.Windows.Media.Brush)FindResource(colorKey);
}
private async void OnHostStart(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(_state.RecRoomPath))
@@ -525,6 +648,7 @@ public partial class MainWindow : Window
HostStartBtn.IsEnabled = false;
HostStopBtn.Visibility = Visibility.Visible;
JoinCodePanel.Visibility = Visibility.Collapsed;
SetSidebarStatus("Starting…", "Sunshine");
// Build a fresh step list so progress is visible the moment the
// user clicks. Steps advance via StartStep/CompleteStep helpers
@@ -631,6 +755,8 @@ public partial class MainWindow : Window
? $"Hosting on your local network at {_hostApex}. Share the join code with friends on the same WiFi."
: $"Hosting at https://{_hostApex}. Share the join code below or launch Rec Room to test.";
HostLaunchBtn.Visibility = Visibility.Visible;
HostAdminBtn.Visibility = Visibility.Visible;
SetSidebarStatus("Live", "Ok");
}
catch (Exception ex)
{
@@ -640,6 +766,7 @@ public partial class MainWindow : Window
ShowHostRetry();
HostStartBtn.IsEnabled = true;
HostStopBtn.Visibility = Visibility.Collapsed;
SetSidebarStatus("Failed", "Danger");
}
}
@@ -697,12 +824,38 @@ public partial class MainWindow : Window
HostStartBtn.IsEnabled = true;
HostStopBtn.Visibility = Visibility.Collapsed;
HostLaunchBtn.Visibility = Visibility.Collapsed;
HostAdminBtn.Visibility = Visibility.Collapsed;
JoinCodePanel.Visibility = Visibility.Collapsed;
HostStepsPanel.Visibility = Visibility.Collapsed;
HostStepsList.ItemsSource = null;
_hostSteps = null;
HostStatusText.Text = "";
HostRetryPanel.Visibility = Visibility.Collapsed;
SetSidebarStatus("Idle", "InkFaint");
}
/// <summary>Open the server's admin web panel in the user's default
/// browser. The admin SPA is published at <c>admin.&lt;apex&gt;</c>
/// over the same scheme the server uses (https for tunnel modes,
/// http for LAN). First account created on a fresh server is
/// auto-promoted; subsequent admins are added from inside the panel.</summary>
private void OnOpenAdminPanel(object sender, RoutedEventArgs e)
{
if (string.IsNullOrEmpty(_hostApex))
{
ShowError("Server isn't running. Start hosting first, then the admin panel becomes reachable.");
return;
}
var scheme = _state.HostingMode == HostingMode.LocalNetwork ? "http" : "https";
var url = $"{scheme}://admin.{_hostApex}";
try
{
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
catch (Exception ex)
{
ShowError($"Couldn't open {url}: {ex.Message}");
}
}
private async Task ShutdownServerAsync()