mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 23:48:45 -07:00
launcher: step-by-step progress UI replaces single-line status
The single HostStatusText/JoinStatusText line gave no sense of where the user was in the flow. Replaced with a step list: - StartupStep model (INotifyPropertyChanged) tracks Title, State (Pending/Active/Done/Failed), Detail, optional Progress 0..1 - ItemsControl + DataTemplate in App.xaml renders each step as a status bullet (empty ring -> filled brand -> green check -> red X), with conditional detail line + restyled mini progress bar - StartupFlow.NewHostFlow / NewJoinFlow build the canonical 5-step host and 2-step join lists; host flow swaps the tunnel step label for LocalNetwork mode - OnHostStart / OnJoinApply advance steps via StartStep/CompleteStep/ FailStep helpers and route progress callbacks to UpdateStepProgress so each download phase has its own bar - Errors mark the active step Failed with the message inline, leaving the rest of the list visible so the user sees where it broke
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
<Application x:Class="DorkNet.Launcher.App"
|
<Application x:Class="DorkNet.Launcher.App"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:backend="clr-namespace:DorkNet.Launcher.Backend"
|
||||||
StartupUri="MainWindow.xaml">
|
StartupUri="MainWindow.xaml">
|
||||||
|
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
@@ -245,5 +246,144 @@
|
|||||||
<Setter Property="CornerRadius" Value="999" />
|
<Setter Property="CornerRadius" Value="999" />
|
||||||
<Setter Property="Padding" Value="10,4" />
|
<Setter Property="Padding" Value="10,4" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<!-- ── Mini progress bar — restyled WPF ProgressBar to match the
|
||||||
|
launcher palette. Used inside the step-row template. ─────── -->
|
||||||
|
<Style x:Key="StepProgressBar" TargetType="ProgressBar">
|
||||||
|
<Setter Property="Minimum" Value="0" />
|
||||||
|
<Setter Property="Maximum" Value="100" />
|
||||||
|
<Setter Property="Height" Value="6" />
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Brand}" />
|
||||||
|
<Setter Property="Background" Value="{StaticResource BgDeeper}" />
|
||||||
|
<Setter Property="BorderThickness" Value="0" />
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ProgressBar">
|
||||||
|
<Border Background="{TemplateBinding Background}"
|
||||||
|
CornerRadius="3" ClipToBounds="True">
|
||||||
|
<Grid HorizontalAlignment="Stretch">
|
||||||
|
<Rectangle x:Name="PART_Track" />
|
||||||
|
<Border x:Name="PART_Indicator"
|
||||||
|
Background="{TemplateBinding Foreground}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
CornerRadius="3" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ── Bool→Visibility converter for HasDetail/HasProgress flags ─ -->
|
||||||
|
<BooleanToVisibilityConverter x:Key="BoolToVis" />
|
||||||
|
|
||||||
|
<!-- ── 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). -->
|
||||||
|
<DataTemplate x:Key="StepRowTemplate" DataType="{x:Type backend:StartupStep}">
|
||||||
|
<Grid Margin="0,0,0,14">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="28" />
|
||||||
|
<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="" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding State}" Value="Active">
|
||||||
|
<Setter Property="Text" Value="●" />
|
||||||
|
<Setter Property="FontSize" Value="9" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding State}" Value="Done">
|
||||||
|
<Setter Property="Text" Value="✓" />
|
||||||
|
<Setter Property="Foreground" Value="#0E0A1F" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding State}" Value="Failed">
|
||||||
|
<Setter Property="Text" Value="✕" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Title + (optional) detail + (optional) progress bar -->
|
||||||
|
<StackPanel Grid.Column="1" Margin="12,0,0,0">
|
||||||
|
<TextBlock Text="{Binding Title}" FontSize="14">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource InkSoft}" />
|
||||||
|
<Setter Property="FontWeight" Value="Normal" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding State}" Value="Active">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Ink}" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding State}" Value="Done">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Ink}" />
|
||||||
|
</DataTrigger>
|
||||||
|
<DataTrigger Binding="{Binding State}" Value="Failed">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Danger}" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding Detail}" TextWrapping="Wrap"
|
||||||
|
FontSize="12" Margin="0,4,0,0"
|
||||||
|
Visibility="{Binding HasDetail, Converter={StaticResource BoolToVis}}">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource InkSoft}" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding State}" Value="Failed">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource Danger}" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<ProgressBar Style="{StaticResource StepProgressBar}"
|
||||||
|
Value="{Binding ProgressPercent, Mode=OneWay}"
|
||||||
|
Margin="0,8,0,0"
|
||||||
|
Visibility="{Binding HasProgress, Converter={StaticResource BoolToVis}}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace DorkNet.Launcher.Backend;
|
||||||
|
|
||||||
|
/// <summary>A single visible step in the host/join startup flow. The
|
||||||
|
/// XAML <c>ItemsControl</c> binds to a collection of these and the
|
||||||
|
/// DataTemplate switches presentation off <see cref="State"/>.
|
||||||
|
///
|
||||||
|
/// <para>This is a tiny INotifyPropertyChanged DTO on purpose — no MVVM
|
||||||
|
/// framework, no commands, no view-model layer. Code-behind owns the
|
||||||
|
/// collection and mutates fields as the flow progresses.</para></summary>
|
||||||
|
public sealed class StartupStep : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public StartupStep(string title) { Title = title; }
|
||||||
|
|
||||||
|
public string Title { get; }
|
||||||
|
|
||||||
|
private StepState _state = StepState.Pending;
|
||||||
|
public StepState State
|
||||||
|
{
|
||||||
|
get => _state;
|
||||||
|
set { if (_state == value) return; _state = value; OnChanged(); OnChanged(nameof(IsActive)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _detail = "";
|
||||||
|
public string Detail
|
||||||
|
{
|
||||||
|
get => _detail;
|
||||||
|
set { if (_detail == value) return; _detail = value; OnChanged(); OnChanged(nameof(HasDetail)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>0..1 progress fraction, or null for "no progress bar"
|
||||||
|
/// (e.g. an indeterminate phase like "starting server"). The
|
||||||
|
/// DataTemplate hides the bar when null.</summary>
|
||||||
|
private double? _progress;
|
||||||
|
public double? Progress
|
||||||
|
{
|
||||||
|
get => _progress;
|
||||||
|
set { if (_progress == value) return; _progress = value; OnChanged(); OnChanged(nameof(HasProgress)); OnChanged(nameof(ProgressPercent)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasDetail => !string.IsNullOrEmpty(_detail);
|
||||||
|
public bool HasProgress => _progress.HasValue;
|
||||||
|
public bool IsActive => _state == StepState.Active;
|
||||||
|
|
||||||
|
/// <summary>0..100 for the ProgressBar Value binding.</summary>
|
||||||
|
public double ProgressPercent => (_progress ?? 0) * 100;
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
private void OnChanged([CallerMemberName] string? name = null)
|
||||||
|
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name!));
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum StepState
|
||||||
|
{
|
||||||
|
/// <summary>Hasn't started yet. Dim icon, no detail.</summary>
|
||||||
|
Pending,
|
||||||
|
/// <summary>In progress right now. Brand-coloured icon, detail +
|
||||||
|
/// optional progress bar visible.</summary>
|
||||||
|
Active,
|
||||||
|
/// <summary>Finished successfully. Green checkmark.</summary>
|
||||||
|
Done,
|
||||||
|
/// <summary>Failed — error in <see cref="StartupStep.Detail"/>. Red
|
||||||
|
/// X, the rest of the flow won't proceed.</summary>
|
||||||
|
Failed,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Helpers for the two known flows. Centralising the step
|
||||||
|
/// names here keeps the XAML free of hardcoded strings and makes it
|
||||||
|
/// obvious what stages a user will see.</summary>
|
||||||
|
public static class StartupFlow
|
||||||
|
{
|
||||||
|
public static ObservableCollection<StartupStep> NewHostFlow(bool isLocalNetwork) => new()
|
||||||
|
{
|
||||||
|
new StartupStep("Download server binary"),
|
||||||
|
new StartupStep(isLocalNetwork ? "Find your local network address" : "Set up Cloudflare tunnel"),
|
||||||
|
new StartupStep("Start the server"),
|
||||||
|
new StartupStep("Download client patcher"),
|
||||||
|
new StartupStep("Patch your Rec Room install"),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static ObservableCollection<StartupStep> NewJoinFlow() => new()
|
||||||
|
{
|
||||||
|
new StartupStep("Download client patcher"),
|
||||||
|
new StartupStep("Patch your Rec Room install"),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -219,13 +219,19 @@
|
|||||||
Margin="10,0,0,0" Visibility="Collapsed" />
|
Margin="10,0,0,0" Visibility="Collapsed" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Status -->
|
<!-- Progress steps — populated when the user hits
|
||||||
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
|
"Start hosting". Hidden until then so the
|
||||||
|
idle Host view stays clean. -->
|
||||||
|
<Border x:Name="HostStepsPanel" Style="{StaticResource Panel}"
|
||||||
|
Margin="0,0,0,16" Visibility="Collapsed">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Status" />
|
<TextBlock Style="{StaticResource SectionTitle}" Text="Progress" />
|
||||||
|
<ItemsControl x:Name="HostStepsList"
|
||||||
|
ItemTemplate="{StaticResource StepRowTemplate}" />
|
||||||
<TextBlock x:Name="HostStatusText"
|
<TextBlock x:Name="HostStatusText"
|
||||||
Foreground="{StaticResource InkSoft}"
|
Foreground="{StaticResource InkSoft}"
|
||||||
TextWrapping="Wrap" Text="Idle." FontSize="14" />
|
TextWrapping="Wrap" FontSize="13"
|
||||||
|
Margin="0,4,0,0" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -338,12 +344,16 @@
|
|||||||
Margin="10,0,0,0" Visibility="Collapsed" />
|
Margin="10,0,0,0" Visibility="Collapsed" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Border Style="{StaticResource Panel}">
|
<Border x:Name="JoinStepsPanel" Style="{StaticResource Panel}"
|
||||||
|
Visibility="Collapsed">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Status" />
|
<TextBlock Style="{StaticResource SectionTitle}" Text="Progress" />
|
||||||
|
<ItemsControl x:Name="JoinStepsList"
|
||||||
|
ItemTemplate="{StaticResource StepRowTemplate}" />
|
||||||
<TextBlock x:Name="JoinStatusText"
|
<TextBlock x:Name="JoinStatusText"
|
||||||
Foreground="{StaticResource InkSoft}"
|
Foreground="{StaticResource InkSoft}"
|
||||||
TextWrapping="Wrap" Text="Idle." FontSize="14" />
|
TextWrapping="Wrap" FontSize="13"
|
||||||
|
Margin="0,4,0,0" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
+102
-34
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Controls;
|
using System.Windows.Controls;
|
||||||
@@ -19,6 +20,8 @@ public partial class MainWindow : Window
|
|||||||
private Tunnel? _tunnel;
|
private Tunnel? _tunnel;
|
||||||
private VersionsManifest? _manifest;
|
private VersionsManifest? _manifest;
|
||||||
private string? _hostApex;
|
private string? _hostApex;
|
||||||
|
private ObservableCollection<StartupStep>? _hostSteps;
|
||||||
|
private ObservableCollection<StartupStep>? _joinSteps;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@@ -260,46 +263,63 @@ public partial class MainWindow : Window
|
|||||||
HostStopBtn.Visibility = Visibility.Visible;
|
HostStopBtn.Visibility = Visibility.Visible;
|
||||||
JoinCodePanel.Visibility = Visibility.Collapsed;
|
JoinCodePanel.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
|
// Build a fresh step list so progress is visible the moment the
|
||||||
|
// user clicks. Steps advance via StartStep/CompleteStep helpers
|
||||||
|
// below so flow stays readable.
|
||||||
|
_hostSteps = StartupFlow.NewHostFlow(_state.HostingMode == HostingMode.LocalNetwork);
|
||||||
|
HostStepsList.ItemsSource = _hostSteps;
|
||||||
|
HostStepsPanel.Visibility = Visibility.Visible;
|
||||||
|
HostStatusText.Text = "";
|
||||||
|
|
||||||
|
StartupStep? activeStep = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var progress = new Progress<DownloadProgress>(p =>
|
activeStep = StartStep(_hostSteps, 0);
|
||||||
HostStatusText.Text = FormatProgress("Downloading server", p));
|
var serverProgress = new Progress<DownloadProgress>(p => UpdateStepProgress(activeStep, p));
|
||||||
|
var serverDir = await _releases.EnsureServerAsync(version, serverProgress);
|
||||||
|
CompleteStep(activeStep);
|
||||||
|
|
||||||
HostStatusText.Text = "Downloading server binary…";
|
activeStep = StartStep(_hostSteps, 1);
|
||||||
var serverDir = await _releases.EnsureServerAsync(version, progress);
|
|
||||||
|
|
||||||
// Hosting mode branch — Internet (Cloudflare tunnel, joiners
|
|
||||||
// anywhere) vs LocalNetwork (LAN IP, joiners on same network,
|
|
||||||
// zero internet dependency).
|
|
||||||
if (_state.HostingMode == HostingMode.LocalNetwork)
|
if (_state.HostingMode == HostingMode.LocalNetwork)
|
||||||
{
|
{
|
||||||
_hostApex = LocalNetwork.GetLanIp();
|
_hostApex = LocalNetwork.GetLanIp();
|
||||||
HostStatusText.Text = $"Local-network mode: hosting on {_hostApex}";
|
activeStep.Detail = $"Hosting on {_hostApex}";
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
HostStatusText.Text = "Setting up Cloudflare tunnel (first run downloads ~17 MB)…";
|
|
||||||
_tunnel = new Tunnel();
|
_tunnel = new Tunnel();
|
||||||
|
activeStep.Detail = "Connecting to Cloudflare (first run downloads cloudflared, ~17 MB)";
|
||||||
var publicUrl = await _tunnel.StartAsync(
|
var publicUrl = await _tunnel.StartAsync(
|
||||||
localPort: 5005,
|
localPort: 5005,
|
||||||
downloadProgress: new Progress<DownloadProgress>(p =>
|
downloadProgress: new Progress<DownloadProgress>(p => UpdateStepProgress(activeStep!, p)));
|
||||||
HostStatusText.Text = FormatProgress("Downloading Cloudflare tunnel", p)));
|
|
||||||
_hostApex = new Uri(publicUrl).Host;
|
_hostApex = new Uri(publicUrl).Host;
|
||||||
|
activeStep.Detail = $"Tunnel ready: {_hostApex}";
|
||||||
|
activeStep.Progress = null;
|
||||||
}
|
}
|
||||||
|
CompleteStep(activeStep);
|
||||||
|
|
||||||
HostStatusText.Text = "Starting the server…";
|
activeStep = StartStep(_hostSteps, 2);
|
||||||
await _server.StartAsync(serverDir, _state, _hostApex);
|
await _server.StartAsync(serverDir, _state, _hostApex);
|
||||||
_state.SelectedVersion = version.VersionKey;
|
_state.SelectedVersion = version.VersionKey;
|
||||||
_state.Save();
|
_state.Save();
|
||||||
|
CompleteStep(activeStep);
|
||||||
|
|
||||||
HostStatusText.Text = "Downloading client patcher…";
|
activeStep = StartStep(_hostSteps, 3);
|
||||||
var patcherDir = await _releases.EnsurePatcherAsync(version, progress);
|
var patcherProgress = new Progress<DownloadProgress>(p => UpdateStepProgress(activeStep, p));
|
||||||
|
var patcherDir = await _releases.EnsurePatcherAsync(version, patcherProgress);
|
||||||
|
CompleteStep(activeStep);
|
||||||
|
|
||||||
HostStatusText.Text = "Patching your Rec Room client…";
|
activeStep = StartStep(_hostSteps, 4);
|
||||||
var patch = await _patcher.ApplyAsync(
|
var patch = await _patcher.ApplyAsync(
|
||||||
patcherDir, _state.RecRoomPath!,
|
patcherDir, _state.RecRoomPath!,
|
||||||
_state.PhotonAppId, _state.PhotonVoiceAppId, _state.PhotonRegion, _hostApex);
|
_state.PhotonAppId, _state.PhotonVoiceAppId, _state.PhotonRegion, _hostApex);
|
||||||
if (!patch.Ok) { ShowError("Patcher failed: " + Truncate(patch.Log, 500)); return; }
|
if (!patch.Ok)
|
||||||
|
{
|
||||||
|
FailStep(activeStep, "Patcher failed: " + Truncate(patch.Log, 300));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompleteStep(activeStep);
|
||||||
|
activeStep = null;
|
||||||
|
|
||||||
var code = JoinCode.Encode(new JoinPayload
|
var code = JoinCode.Encode(new JoinPayload
|
||||||
{
|
{
|
||||||
@@ -319,12 +339,50 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
ShowError(ex.Message);
|
if (activeStep is not null) FailStep(activeStep, ex.Message);
|
||||||
|
else HostStatusText.Text = ex.Message;
|
||||||
HostStartBtn.IsEnabled = true;
|
HostStartBtn.IsEnabled = true;
|
||||||
HostStopBtn.Visibility = Visibility.Collapsed;
|
HostStopBtn.Visibility = Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Step helpers ────────────────────────────────────────────────────
|
||||||
|
private static StartupStep StartStep(ObservableCollection<StartupStep> steps, int index)
|
||||||
|
{
|
||||||
|
var s = steps[index];
|
||||||
|
s.State = StepState.Active;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CompleteStep(StartupStep step)
|
||||||
|
{
|
||||||
|
step.State = StepState.Done;
|
||||||
|
step.Progress = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void FailStep(StartupStep step, string detail)
|
||||||
|
{
|
||||||
|
step.State = StepState.Failed;
|
||||||
|
step.Progress = null;
|
||||||
|
step.Detail = detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateStepProgress(StartupStep step, DownloadProgress p)
|
||||||
|
{
|
||||||
|
if (p.TotalBytes > 0)
|
||||||
|
{
|
||||||
|
var mb = p.BytesRead / (1024.0 * 1024.0);
|
||||||
|
var totalMb = p.TotalBytes / (1024.0 * 1024.0);
|
||||||
|
step.Detail = $"{mb:F1} / {totalMb:F1} MB ({(int)(p.Fraction * 100)}%)";
|
||||||
|
step.Progress = p.Fraction;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var mb = p.BytesRead / (1024.0 * 1024.0);
|
||||||
|
step.Detail = $"{mb:F1} MB downloaded";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnHostStop(object sender, RoutedEventArgs e)
|
private async void OnHostStop(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
await ShutdownServerAsync();
|
await ShutdownServerAsync();
|
||||||
@@ -332,7 +390,10 @@ public partial class MainWindow : Window
|
|||||||
HostStopBtn.Visibility = Visibility.Collapsed;
|
HostStopBtn.Visibility = Visibility.Collapsed;
|
||||||
HostLaunchBtn.Visibility = Visibility.Collapsed;
|
HostLaunchBtn.Visibility = Visibility.Collapsed;
|
||||||
JoinCodePanel.Visibility = Visibility.Collapsed;
|
JoinCodePanel.Visibility = Visibility.Collapsed;
|
||||||
HostStatusText.Text = "Stopped.";
|
HostStepsPanel.Visibility = Visibility.Collapsed;
|
||||||
|
HostStepsList.ItemsSource = null;
|
||||||
|
_hostSteps = null;
|
||||||
|
HostStatusText.Text = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ShutdownServerAsync()
|
private async Task ShutdownServerAsync()
|
||||||
@@ -389,25 +450,40 @@ public partial class MainWindow : Window
|
|||||||
{ ShowError($"Host is on a version this launcher doesn't know about: {payload.VersionKey}"); return; }
|
{ ShowError($"Host is on a version this launcher doesn't know about: {payload.VersionKey}"); return; }
|
||||||
|
|
||||||
JoinApplyBtn.IsEnabled = false;
|
JoinApplyBtn.IsEnabled = false;
|
||||||
|
_joinSteps = StartupFlow.NewJoinFlow();
|
||||||
|
JoinStepsList.ItemsSource = _joinSteps;
|
||||||
|
JoinStepsPanel.Visibility = Visibility.Visible;
|
||||||
|
JoinStatusText.Text = "";
|
||||||
|
|
||||||
|
StartupStep? activeStep = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var progress = new Progress<DownloadProgress>(p =>
|
activeStep = StartStep(_joinSteps, 0);
|
||||||
JoinStatusText.Text = FormatProgress("Downloading patcher", p));
|
var progress = new Progress<DownloadProgress>(p => UpdateStepProgress(activeStep, p));
|
||||||
|
|
||||||
JoinStatusText.Text = "Downloading client patcher…";
|
|
||||||
var patcherDir = await _releases.EnsurePatcherAsync(version, progress);
|
var patcherDir = await _releases.EnsurePatcherAsync(version, progress);
|
||||||
|
CompleteStep(activeStep);
|
||||||
|
|
||||||
JoinStatusText.Text = "Patching your Rec Room client…";
|
activeStep = StartStep(_joinSteps, 1);
|
||||||
var patch = await _patcher.ApplyAsync(
|
var patch = await _patcher.ApplyAsync(
|
||||||
patcherDir, _state.RecRoomPath!,
|
patcherDir, _state.RecRoomPath!,
|
||||||
payload.PhotonAppId, payload.PhotonVoiceAppId, payload.PhotonRegion, payload.Host);
|
payload.PhotonAppId, payload.PhotonVoiceAppId, payload.PhotonRegion, payload.Host);
|
||||||
if (!patch.Ok) { ShowError("Patcher failed: " + Truncate(patch.Log, 500)); return; }
|
if (!patch.Ok)
|
||||||
|
{
|
||||||
|
FailStep(activeStep, "Patcher failed: " + Truncate(patch.Log, 300));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
CompleteStep(activeStep);
|
||||||
|
activeStep = null;
|
||||||
|
|
||||||
JoinStatusText.Text = $"Patched and ready. Connecting to {payload.Host}. " +
|
JoinStatusText.Text = $"Patched and ready. Connecting to {payload.Host}. " +
|
||||||
"Click Launch Rec Room when you're ready to play.";
|
"Click Launch Rec Room when you're ready to play.";
|
||||||
JoinLaunchBtn.Visibility = Visibility.Visible;
|
JoinLaunchBtn.Visibility = Visibility.Visible;
|
||||||
}
|
}
|
||||||
catch (Exception ex) { ShowError(ex.Message); }
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (activeStep is not null) FailStep(activeStep, ex.Message);
|
||||||
|
else JoinStatusText.Text = ex.Message;
|
||||||
|
}
|
||||||
finally { RefreshJoinReadiness(); }
|
finally { RefreshJoinReadiness(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,14 +496,6 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ─────────────────────────────────────────────────────────
|
// ── Helpers ─────────────────────────────────────────────────────────
|
||||||
private static string FormatProgress(string label, DownloadProgress p)
|
|
||||||
{
|
|
||||||
if (p.TotalBytes <= 0) return $"{label}…";
|
|
||||||
var mb = p.BytesRead / (1024.0 * 1024.0);
|
|
||||||
var totalMb = p.TotalBytes / (1024.0 * 1024.0);
|
|
||||||
return $"{label}: {mb:F1} / {totalMb:F1} MB ({(int)(p.Fraction * 100)}%)";
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Truncate(string s, int max)
|
private static string Truncate(string s, int max)
|
||||||
=> s.Length <= max ? s : s[..max] + "...";
|
=> s.Length <= max ? s : s[..max] + "...";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user