Desktop installer: release channel, saved choices, robust delete, logs
- Channel radio (Release / Master (Instabil)); master installs the rolling 'master' release from OmniNX/OmniNX-Master. Preselected from release_channel in the card's manifest.ini. - Variant, RAM (8gb) and Hekate-Pro choice are prefilled from the card (manifest.ini / config.ini) like the payload does, instead of silently switching users back to 4 GB / Standard. - Clear read-only attributes before deleting folders. - Remove leftover "OmniNX <Variant>" pack folders after installing. - Create empty folders from the pack. - Save every run's log to %LocalAppData%\OmniNX-Desktop-Installer\logs, plus a "Save log" button.
This commit is contained in:
1 parent
3e0d9c9950
commit
91133f7098
6 files changed
+207
-9
No files matched your search
@@ -9,12 +9,16 @@ public sealed class MainForm : Form
|
||||
private readonly GiteaClient _gitea = new();
|
||||
private readonly VersionDetectService _versionDetect = new();
|
||||
private readonly InstallEngine _installEngine = new();
|
||||
private readonly ConfigIniService _configIni = new();
|
||||
|
||||
private ComboBox _driveCombo = null!;
|
||||
private Button _refreshDrivesButton = null!;
|
||||
private Label _statusLabel = null!;
|
||||
private Label _releaseLabel = null!;
|
||||
private Button _checkReleaseButton = null!;
|
||||
private RadioButton _channelRelease = null!;
|
||||
private RadioButton _channelMaster = null!;
|
||||
private Button _saveLogButton = null!;
|
||||
private RadioButton _variantLight = null!;
|
||||
private RadioButton _variantStandard = null!;
|
||||
private RadioButton _variantOc = null!;
|
||||
@@ -81,10 +85,11 @@ public sealed class MainForm : Form
|
||||
AutoSize = true,
|
||||
Dock = DockStyle.Fill,
|
||||
ColumnCount = 1,
|
||||
RowCount = 2,
|
||||
RowCount = 3,
|
||||
};
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
panel.RowStyles.Add(new RowStyle(SizeType.AutoSize));
|
||||
|
||||
var title = new Label
|
||||
{
|
||||
@@ -115,11 +120,39 @@ public sealed class MainForm : Form
|
||||
releaseRow.Controls.Add(_releaseLabel);
|
||||
releaseRow.Controls.Add(_checkReleaseButton);
|
||||
|
||||
// Release channel: stable releases or the master build (internal testing)
|
||||
_channelRelease = new RadioButton { Text = "Release", AutoSize = true, Checked = true, Margin = new Padding(0, 0, 16, 0) };
|
||||
_channelMaster = new RadioButton { Text = "Master (Instabil)", AutoSize = true, ForeColor = Color.DarkOrange };
|
||||
_channelRelease.CheckedChanged += OnChannelChanged;
|
||||
_channelMaster.CheckedChanged += OnChannelChanged;
|
||||
|
||||
var channelRow = new FlowLayoutPanel
|
||||
{
|
||||
AutoSize = true,
|
||||
WrapContents = false,
|
||||
Dock = DockStyle.Fill,
|
||||
Margin = new Padding(0, 0, 0, 4),
|
||||
};
|
||||
channelRow.Controls.Add(new Label { Text = "Channel:", AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 3, 8, 0) });
|
||||
channelRow.Controls.Add(_channelRelease);
|
||||
channelRow.Controls.Add(_channelMaster);
|
||||
|
||||
panel.Controls.Add(title, 0, 0);
|
||||
panel.Controls.Add(releaseRow, 0, 1);
|
||||
panel.Controls.Add(channelRow, 0, 2);
|
||||
return panel;
|
||||
}
|
||||
|
||||
private ReleaseChannel SelectedChannel =>
|
||||
_channelMaster.Checked ? ReleaseChannel.Master : ReleaseChannel.Release;
|
||||
|
||||
private void OnChannelChanged(object? sender, EventArgs e)
|
||||
{
|
||||
// Fires for both radios; react once, on the one that became checked.
|
||||
if (sender is RadioButton { Checked: true })
|
||||
_ = LoadLatestReleaseAsync();
|
||||
}
|
||||
|
||||
private static Image? LoadEmbeddedImage(string resourceName)
|
||||
{
|
||||
var stream = typeof(MainForm).Assembly.GetManifestResourceStream(resourceName);
|
||||
@@ -157,7 +190,11 @@ public sealed class MainForm : Form
|
||||
layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
|
||||
|
||||
_driveCombo = new ComboBox { Dock = DockStyle.Fill, DropDownStyle = ComboBoxStyle.DropDownList };
|
||||
_driveCombo.SelectedIndexChanged += (_, _) => UpdateInstallationStatus();
|
||||
_driveCombo.SelectedIndexChanged += (_, _) =>
|
||||
{
|
||||
UpdateInstallationStatus();
|
||||
PrefillFromDrive();
|
||||
};
|
||||
|
||||
_refreshDrivesButton = new Button { Text = "Refresh", AutoSize = true };
|
||||
_refreshDrivesButton.Click += (_, _) => RefreshDrives();
|
||||
@@ -285,8 +322,12 @@ public sealed class MainForm : Form
|
||||
|
||||
_progressBar = new ProgressBar { Width = 500, Height = 24, Margin = new Padding(16, 6, 0, 0) };
|
||||
|
||||
_saveLogButton = new Button { Text = "Save log", AutoSize = true, Margin = new Padding(16, 4, 0, 0) };
|
||||
_saveLogButton.Click += (_, _) => SaveLogAs();
|
||||
|
||||
panel.Controls.Add(_installButton);
|
||||
panel.Controls.Add(_progressBar);
|
||||
panel.Controls.Add(_saveLogButton);
|
||||
return panel;
|
||||
}
|
||||
|
||||
@@ -380,18 +421,67 @@ public sealed class MainForm : Form
|
||||
}
|
||||
}
|
||||
|
||||
// Take over what the card already uses: saved RAM / Hekate-Pro choice (config.ini, same as
|
||||
// the payload) and the release channel from manifest.ini. Unknown values fall back to defaults.
|
||||
private void PrefillFromDrive()
|
||||
{
|
||||
var drive = SelectedDrive;
|
||||
if (drive is null)
|
||||
return;
|
||||
|
||||
var root = drive.RootDirectory.FullName;
|
||||
try
|
||||
{
|
||||
var use8Gb = _configIni.TryReadBool(root, "Ram", "8gb", out var ram) && ram;
|
||||
_ram8Gb.Checked = use8Gb;
|
||||
_ram4Gb.Checked = !use8Gb;
|
||||
|
||||
var usePro = _configIni.TryReadBool(root, "HekateAms", "pro", out var pro) && pro;
|
||||
_hekatePro.Checked = usePro;
|
||||
_hekateStandard.Checked = !usePro;
|
||||
|
||||
var status = _versionDetect.DetectInstallation(root);
|
||||
if (status.IsInstalled)
|
||||
{
|
||||
_variantLight.Checked = status.Variant == OmniNxVariant.Light;
|
||||
_variantStandard.Checked = status.Variant == OmniNxVariant.Standard;
|
||||
_variantOc.Checked = status.Variant == OmniNxVariant.Oc;
|
||||
|
||||
var master = status.Channel == ReleaseChannel.Master;
|
||||
if (_channelMaster.Checked != master)
|
||||
{
|
||||
_channelMaster.Checked = master;
|
||||
_channelRelease.Checked = !master;
|
||||
}
|
||||
}
|
||||
|
||||
if (use8Gb || usePro)
|
||||
Log($"Saved choice on card: RAM {(use8Gb ? "8 GB" : "4 GB")}, Hekate/AMS {(usePro ? "Pro" : "Standard")}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Could not read saved settings from {drive.Name}: {ex.Message}", error: true);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadLatestReleaseAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
SetUiEnabled(false);
|
||||
_releaseLabel.Text = "Release: fetching...";
|
||||
_latestRelease = await _gitea.GetLatestReleaseAsync();
|
||||
_releaseLabel.Text = $"Latest release: {_latestRelease.TagName} ({_latestRelease.Name})";
|
||||
Log($"Latest release: {_latestRelease.TagName}");
|
||||
var channel = SelectedChannel;
|
||||
_latestRelease = await _gitea.GetLatestReleaseAsync(channel);
|
||||
_releaseLabel.Text = channel == ReleaseChannel.Master
|
||||
? $"Master build: {_latestRelease.Name}"
|
||||
: $"Latest release: {_latestRelease.TagName} ({_latestRelease.Name})";
|
||||
Log(channel == ReleaseChannel.Master
|
||||
? $"Master build: {_latestRelease.Name}"
|
||||
: $"Latest release: {_latestRelease.TagName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_latestRelease = null;
|
||||
_releaseLabel.Text = "Release: fetch failed";
|
||||
Log($"Failed to fetch release: {ex.Message}", error: true);
|
||||
}
|
||||
@@ -437,7 +527,7 @@ public sealed class MainForm : Form
|
||||
$"Target drive: {drive.Name}\n" +
|
||||
$"Variant: {VariantHelper.GetDisplayName(variant)}\n" +
|
||||
$"Mode: {modeText}\n" +
|
||||
$"Release: {_latestRelease.TagName}\n" +
|
||||
$"Release: {(SelectedChannel == ReleaseChannel.Master ? "Master (Instabil) " + _latestRelease.Name : _latestRelease.TagName)}\n" +
|
||||
$"RAM: {(_ram8Gb.Checked ? "8 GB" : "4 GB")}\n" +
|
||||
$"Hekate/AMS: {(_hekatePro.Checked ? "Pro" : "Standard")}\n\n" +
|
||||
"All data on paths targeted by the installer may be modified or deleted.\n" +
|
||||
@@ -572,10 +662,54 @@ public sealed class MainForm : Form
|
||||
}
|
||||
finally
|
||||
{
|
||||
SaveLogToAppData();
|
||||
SetUiEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private static string LogDirectory => Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"OmniNX-Desktop-Installer",
|
||||
"logs");
|
||||
|
||||
// Every install run is kept as a log file for support requests (called on the UI thread).
|
||||
private void SaveLogToAppData()
|
||||
{
|
||||
try
|
||||
{
|
||||
PathUtils.EnsureDirectory(LogDirectory);
|
||||
var path = Path.Combine(LogDirectory, $"install-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
||||
File.WriteAllText(path, _logBox.Text);
|
||||
Log($"Log saved: {path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log($"Could not save log: {ex.Message}", error: true);
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveLogAs()
|
||||
{
|
||||
using var dialog = new SaveFileDialog
|
||||
{
|
||||
Title = "Save log",
|
||||
Filter = "Log files (*.log)|*.log|Text files (*.txt)|*.txt",
|
||||
FileName = $"omninx-installer-{DateTime.Now:yyyyMMdd-HHmmss}.log",
|
||||
};
|
||||
if (dialog.ShowDialog(this) != DialogResult.OK)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(dialog.FileName, _logBox.Text);
|
||||
Log($"Log saved: {dialog.FileName}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(this, ex.Message, "Save log", MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void Log(string message, bool error = false)
|
||||
{
|
||||
if (InvokeRequired)
|
||||
@@ -610,6 +744,9 @@ public sealed class MainForm : Form
|
||||
_hekateStandard.Enabled = enabled;
|
||||
_hekatePro.Enabled = enabled;
|
||||
_forceCleanCheck.Enabled = enabled;
|
||||
_channelRelease.Enabled = enabled;
|
||||
_channelMaster.Enabled = enabled;
|
||||
_saveLogButton.Enabled = enabled;
|
||||
}
|
||||
|
||||
protected override void OnLoad(EventArgs e)
|
||||
|
||||
@@ -8,6 +8,12 @@ public enum OmniNxVariant
|
||||
Oc,
|
||||
}
|
||||
|
||||
public enum ReleaseChannel
|
||||
{
|
||||
Release,
|
||||
Master,
|
||||
}
|
||||
|
||||
public enum InstallMode
|
||||
{
|
||||
Clean,
|
||||
@@ -30,6 +36,7 @@ public sealed class OmniNxStatus
|
||||
public bool IsInstalled { get; init; }
|
||||
public OmniNxVariant Variant { get; init; }
|
||||
public string Version { get; init; } = string.Empty;
|
||||
public ReleaseChannel Channel { get; init; } = ReleaseChannel.Release;
|
||||
}
|
||||
|
||||
public sealed class ReleaseInfo
|
||||
|
||||
@@ -17,9 +17,15 @@ public sealed class GiteaClient
|
||||
_baseUrl = (baseUrl ?? DefaultBaseUrl).TrimEnd('/');
|
||||
}
|
||||
|
||||
public async Task<ReleaseInfo> GetLatestReleaseAsync(CancellationToken cancellationToken = default)
|
||||
// Master builds: rolling release "master" in OmniNX/OmniNX-Master (built from main by CI).
|
||||
private const string MasterReleasePath = "/api/v1/repos/OmniNX/OmniNX-Master/releases/tags/master";
|
||||
private const string StableReleasePath = "/api/v1/repos/OmniNX/OmniNX/releases/latest";
|
||||
|
||||
public async Task<ReleaseInfo> GetLatestReleaseAsync(
|
||||
ReleaseChannel channel = ReleaseChannel.Release,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var url = $"{_baseUrl}/api/v1/repos/OmniNX/OmniNX/releases/latest";
|
||||
var url = _baseUrl + (channel == ReleaseChannel.Master ? MasterReleasePath : StableReleasePath);
|
||||
var response = await _http.GetFromJsonAsync<GiteaReleaseDto>(url, cancellationToken)
|
||||
?? throw new InvalidOperationException("Empty release response from Gitea.");
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ public sealed class InstallEngine
|
||||
await RunCleanAsync(driveRoot, staging, options, progress, cancellationToken);
|
||||
}
|
||||
|
||||
await Task.Run(() => RemovePackFoldersOnDrive(driveRoot, progress), cancellationToken);
|
||||
|
||||
Report(progress, options.Mode == InstallMode.Update
|
||||
? "Update completed successfully."
|
||||
: "Installation completed successfully.");
|
||||
@@ -177,6 +179,29 @@ public sealed class InstallEngine
|
||||
}
|
||||
}
|
||||
|
||||
// Same as the payload: pack folders copied to the card for the console installer
|
||||
// ("OmniNX Light/Standard/OC") are not needed after a desktop install.
|
||||
private static void RemovePackFoldersOnDrive(string driveRoot, IProgress<InstallProgressReport> progress)
|
||||
{
|
||||
foreach (var variant in new[] { OmniNxVariant.Light, OmniNxVariant.Standard, OmniNxVariant.Oc })
|
||||
{
|
||||
var folder = VariantHelper.GetStagingFolderName(variant);
|
||||
var path = Path.Combine(PathUtils.NormalizeDriveRoot(driveRoot), folder);
|
||||
if (!Directory.Exists(path))
|
||||
continue;
|
||||
|
||||
Report(progress, $"Removing leftover pack folder: {folder}");
|
||||
try
|
||||
{
|
||||
PathUtils.DeletePath(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Report(progress, $" Could not remove {folder}: {ex.Message}", isError: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void Report(IProgress<InstallProgressReport> progress, string message, bool isError = false)
|
||||
{
|
||||
progress.Report(new InstallProgressReport { Message = message, IsError = isError });
|
||||
|
||||
@@ -91,6 +91,11 @@ public static class PathUtils
|
||||
if (!Directory.Exists(src))
|
||||
return;
|
||||
|
||||
// Folders first, so empty folders from the pack exist on the SD as well.
|
||||
Directory.CreateDirectory(dst);
|
||||
foreach (var dir in Directory.GetDirectories(src, "*", SearchOption.AllDirectories))
|
||||
Directory.CreateDirectory(Path.Combine(dst, Path.GetRelativePath(src, dir)));
|
||||
|
||||
var files = Directory.GetFiles(src, "*", SearchOption.AllDirectories);
|
||||
var total = files.Length;
|
||||
var current = 0;
|
||||
@@ -115,7 +120,19 @@ public static class PathUtils
|
||||
}
|
||||
|
||||
if (Directory.Exists(path))
|
||||
Directory.Delete(path, recursive: true);
|
||||
{
|
||||
// Directory.Delete fails on read-only entries (common on cards from other packs or macOS).
|
||||
var root = new DirectoryInfo(path);
|
||||
foreach (var entry in root.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
|
||||
{
|
||||
if (entry.Attributes.HasFlag(FileAttributes.ReadOnly))
|
||||
entry.Attributes &= ~FileAttributes.ReadOnly;
|
||||
}
|
||||
if (root.Attributes.HasFlag(FileAttributes.ReadOnly))
|
||||
root.Attributes &= ~FileAttributes.ReadOnly;
|
||||
|
||||
root.Delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
public static int CountItemsRecursive(string path)
|
||||
|
||||
@@ -14,6 +14,7 @@ public sealed class VersionDetectService
|
||||
|
||||
var variant = OmniNxVariant.None;
|
||||
var version = string.Empty;
|
||||
var channel = ReleaseChannel.Release;
|
||||
var inSection = false;
|
||||
|
||||
foreach (var rawLine in File.ReadLines(manifest))
|
||||
@@ -36,6 +37,10 @@ public sealed class VersionDetectService
|
||||
variant = VariantHelper.ParsePackName(val);
|
||||
else if (key.Equals("version", StringComparison.Ordinal))
|
||||
version = val;
|
||||
else if (key.Equals("release_channel", StringComparison.Ordinal))
|
||||
channel = val.Equals("master", StringComparison.OrdinalIgnoreCase)
|
||||
? ReleaseChannel.Master
|
||||
: ReleaseChannel.Release;
|
||||
}
|
||||
|
||||
return new OmniNxStatus
|
||||
@@ -43,6 +48,7 @@ public sealed class VersionDetectService
|
||||
IsInstalled = variant != OmniNxVariant.None,
|
||||
Variant = variant,
|
||||
Version = version,
|
||||
Channel = channel,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user