mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 23:18:44 -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"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:backend="clr-namespace:DorkNet.Launcher.Backend"
|
||||
StartupUri="MainWindow.xaml">
|
||||
|
||||
<Application.Resources>
|
||||
@@ -245,5 +246,144 @@
|
||||
<Setter Property="CornerRadius" Value="999" />
|
||||
<Setter Property="Padding" Value="10,4" />
|
||||
</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>
|
||||
|
||||
@@ -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" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Status -->
|
||||
<Border Style="{StaticResource Panel}" Margin="0,0,0,16">
|
||||
<!-- Progress steps — populated when the user hits
|
||||
"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>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Status" />
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Progress" />
|
||||
<ItemsControl x:Name="HostStepsList"
|
||||
ItemTemplate="{StaticResource StepRowTemplate}" />
|
||||
<TextBlock x:Name="HostStatusText"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextWrapping="Wrap" Text="Idle." FontSize="14" />
|
||||
TextWrapping="Wrap" FontSize="13"
|
||||
Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
@@ -338,12 +344,16 @@
|
||||
Margin="10,0,0,0" Visibility="Collapsed" />
|
||||
</StackPanel>
|
||||
|
||||
<Border Style="{StaticResource Panel}">
|
||||
<Border x:Name="JoinStepsPanel" Style="{StaticResource Panel}"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Status" />
|
||||
<TextBlock Style="{StaticResource SectionTitle}" Text="Progress" />
|
||||
<ItemsControl x:Name="JoinStepsList"
|
||||
ItemTemplate="{StaticResource StepRowTemplate}" />
|
||||
<TextBlock x:Name="JoinStatusText"
|
||||
Foreground="{StaticResource InkSoft}"
|
||||
TextWrapping="Wrap" Text="Idle." FontSize="14" />
|
||||
TextWrapping="Wrap" FontSize="13"
|
||||
Margin="0,4,0,0" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
+102
-34
@@ -1,3 +1,4 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
@@ -19,6 +20,8 @@ public partial class MainWindow : Window
|
||||
private Tunnel? _tunnel;
|
||||
private VersionsManifest? _manifest;
|
||||
private string? _hostApex;
|
||||
private ObservableCollection<StartupStep>? _hostSteps;
|
||||
private ObservableCollection<StartupStep>? _joinSteps;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
@@ -260,46 +263,63 @@ public partial class MainWindow : Window
|
||||
HostStopBtn.Visibility = Visibility.Visible;
|
||||
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
|
||||
{
|
||||
var progress = new Progress<DownloadProgress>(p =>
|
||||
HostStatusText.Text = FormatProgress("Downloading server", p));
|
||||
activeStep = StartStep(_hostSteps, 0);
|
||||
var serverProgress = new Progress<DownloadProgress>(p => UpdateStepProgress(activeStep, p));
|
||||
var serverDir = await _releases.EnsureServerAsync(version, serverProgress);
|
||||
CompleteStep(activeStep);
|
||||
|
||||
HostStatusText.Text = "Downloading server binary…";
|
||||
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).
|
||||
activeStep = StartStep(_hostSteps, 1);
|
||||
if (_state.HostingMode == HostingMode.LocalNetwork)
|
||||
{
|
||||
_hostApex = LocalNetwork.GetLanIp();
|
||||
HostStatusText.Text = $"Local-network mode: hosting on {_hostApex}";
|
||||
activeStep.Detail = $"Hosting on {_hostApex}";
|
||||
}
|
||||
else
|
||||
{
|
||||
HostStatusText.Text = "Setting up Cloudflare tunnel (first run downloads ~17 MB)…";
|
||||
_tunnel = new Tunnel();
|
||||
activeStep.Detail = "Connecting to Cloudflare (first run downloads cloudflared, ~17 MB)";
|
||||
var publicUrl = await _tunnel.StartAsync(
|
||||
localPort: 5005,
|
||||
downloadProgress: new Progress<DownloadProgress>(p =>
|
||||
HostStatusText.Text = FormatProgress("Downloading Cloudflare tunnel", p)));
|
||||
downloadProgress: new Progress<DownloadProgress>(p => UpdateStepProgress(activeStep!, p)));
|
||||
_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);
|
||||
_state.SelectedVersion = version.VersionKey;
|
||||
_state.Save();
|
||||
CompleteStep(activeStep);
|
||||
|
||||
HostStatusText.Text = "Downloading client patcher…";
|
||||
var patcherDir = await _releases.EnsurePatcherAsync(version, progress);
|
||||
activeStep = StartStep(_hostSteps, 3);
|
||||
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(
|
||||
patcherDir, _state.RecRoomPath!,
|
||||
_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
|
||||
{
|
||||
@@ -319,12 +339,50 @@ public partial class MainWindow : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ShowError(ex.Message);
|
||||
if (activeStep is not null) FailStep(activeStep, ex.Message);
|
||||
else HostStatusText.Text = ex.Message;
|
||||
HostStartBtn.IsEnabled = true;
|
||||
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)
|
||||
{
|
||||
await ShutdownServerAsync();
|
||||
@@ -332,7 +390,10 @@ public partial class MainWindow : Window
|
||||
HostStopBtn.Visibility = Visibility.Collapsed;
|
||||
HostLaunchBtn.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()
|
||||
@@ -389,25 +450,40 @@ public partial class MainWindow : Window
|
||||
{ ShowError($"Host is on a version this launcher doesn't know about: {payload.VersionKey}"); return; }
|
||||
|
||||
JoinApplyBtn.IsEnabled = false;
|
||||
_joinSteps = StartupFlow.NewJoinFlow();
|
||||
JoinStepsList.ItemsSource = _joinSteps;
|
||||
JoinStepsPanel.Visibility = Visibility.Visible;
|
||||
JoinStatusText.Text = "";
|
||||
|
||||
StartupStep? activeStep = null;
|
||||
try
|
||||
{
|
||||
var progress = new Progress<DownloadProgress>(p =>
|
||||
JoinStatusText.Text = FormatProgress("Downloading patcher", p));
|
||||
|
||||
JoinStatusText.Text = "Downloading client patcher…";
|
||||
activeStep = StartStep(_joinSteps, 0);
|
||||
var progress = new Progress<DownloadProgress>(p => UpdateStepProgress(activeStep, p));
|
||||
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(
|
||||
patcherDir, _state.RecRoomPath!,
|
||||
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}. " +
|
||||
"Click Launch Rec Room when you're ready to play.";
|
||||
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(); }
|
||||
}
|
||||
|
||||
@@ -420,14 +496,6 @@ public partial class MainWindow : Window
|
||||
}
|
||||
|
||||
// ── 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)
|
||||
=> s.Length <= max ? s : s[..max] + "...";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user