mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -07:00
Add installer updater and server CLI
This commit is contained in:
+626
-176
@@ -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">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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 & 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 "Join a friend", 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 & 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
@@ -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.<apex></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()
|
||||
|
||||
Reference in New Issue
Block a user