Files

254 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Microsoft.Win32;
namespace MatrixImageEditor;
public sealed class MainForm : Form
{
private readonly TextBox path = new() { ReadOnly = true, Dock = DockStyle.Fill };
private readonly Label version = new() { AutoSize = true, Text = "尚未打开镜像" };
private readonly TextBox username = new();
private readonly TextBox accountPassword = new() { UseSystemPasswordChar = true };
private readonly TextBox ssid = new();
private readonly TextBox wifiPassword = new() { UseSystemPasswordChar = true };
private readonly ComboBox mode = new() { DropDownStyle = ComboBoxStyle.DropDownList };
private readonly TextBox address = new();
private readonly NumericUpDown prefix = new() { Minimum = 1, Maximum = 32, Value = 24 };
private readonly TextBox gateway = new();
private readonly TextBox dns = new();
private readonly Button open = new() { Text = "打开镜像…", AutoSize = true, MinimumSize = new Size(130, 38) };
private readonly Button save = new() { Text = "修改原镜像", Enabled = false, AutoSize = true };
private readonly Button saveAs = new() { Text = "另存为…", Enabled = false, AutoSize = true };
private readonly ProgressBar progress = new() { Dock = DockStyle.Fill, Visible = false };
private string? imagePath;
private ImageConfig? loaded;
private bool isBusy;
private bool watchingDisplaySettings;
private static readonly Size PreferredLogicalSize = new(960, 760);
private static readonly Size MinimumLogicalSize = new(760, 600);
public MainForm()
{
var assemblyVersion = typeof(MainForm).Assembly.GetName().Version;
Text = $"核桃派镜像配置编辑器 {assemblyVersion?.Major}.{assemblyVersion?.Minor}.{assemblyVersion?.Build}";
AutoScaleMode = AutoScaleMode.Dpi;
StartPosition = FormStartPosition.Manual;
Font = new Font("Microsoft YaHei UI", 10F);
AllowDrop = true;
mode.Items.AddRange(["自动获取(DHCP)", "静态 IPv4"]);
mode.SelectedIndexChanged += (_, _) => UpdateNetworkFields();
save.Click += async (_, _) => await SaveOriginalAsync();
saveAs.Click += async (_, _) => await SaveAsAsync();
open.Click += (_, _) => OpenImageFromDialog();
DragEnter += (_, eventArgs) => { if (!isBusy && eventArgs.Data?.GetDataPresent(DataFormats.FileDrop) == true) eventArgs.Effect = DragDropEffects.Copy; };
DragDrop += (_, eventArgs) => { if (!isBusy && eventArgs.Data?.GetData(DataFormats.FileDrop) is string[] files && files.Length == 1) OpenImage(files[0]); };
Controls.Add(BuildLayout());
}
private Control BuildLayout()
{
var outer = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(24), ColumnCount = 1, RowCount = 6, AutoScroll = true };
for (var index = 0; index < 6; index++) outer.RowStyles.Add(new RowStyle(SizeType.AutoSize));
var header = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 2, Margin = new Padding(0, 0, 0, 12) };
header.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); header.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
path.Margin = new Padding(0, 4, 12, 4);
open.Margin = new Padding(0);
header.Controls.Add(path, 0, 0); header.Controls.Add(open, 1, 0);
outer.Controls.Add(header);
version.Margin = new Padding(0, 0, 0, 12);
outer.Controls.Add(version);
outer.Controls.Add(Group("登录账户", [("用户名", username), ("密码", PasswordPanel(accountPassword))]));
outer.Controls.Add(Group("Wi-Fi", [("SSID", ssid), ("密码", PasswordPanel(wifiPassword))]));
outer.Controls.Add(Group("IPv4", [("方式", mode), ("地址", address), ("前缀", prefix), ("网关", gateway), ("DNS(逗号分隔)", dns)]));
var footer = new TableLayoutPanel { Dock = DockStyle.Top, AutoSize = true, ColumnCount = 3, Margin = new Padding(0, 16, 0, 0) };
footer.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); footer.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); footer.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
progress.MinimumSize = new Size(200, 28);
save.MinimumSize = new Size(150, 40); save.Margin = new Padding(12, 0, 0, 0);
saveAs.MinimumSize = new Size(130, 40); saveAs.Margin = new Padding(12, 0, 0, 0);
footer.Controls.Add(progress, 0, 0); footer.Controls.Add(save, 1, 0); footer.Controls.Add(saveAs, 2, 0);
outer.Controls.Add(footer);
return outer;
}
private static GroupBox Group(string title, (string Label, Control Control)[] rows)
{
var box = new GroupBox { Text = title, Dock = DockStyle.Top, AutoSize = true, Padding = new Padding(16), Margin = new Padding(0, 0, 0, 12) };
var table = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 2, GrowStyle = TableLayoutPanelGrowStyle.AddRows };
table.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); table.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
foreach (var row in rows)
{
row.Control.Dock = DockStyle.Fill;
row.Control.MinimumSize = new Size(320, 0);
row.Control.Margin = new Padding(12, 5, 0, 5);
table.Controls.Add(new Label { Text = row.Label, AutoSize = true, Anchor = AnchorStyles.Left, Margin = new Padding(0, 7, 12, 7) }, 0, table.RowCount);
table.Controls.Add(row.Control, 1, table.RowCount++);
}
box.Controls.Add(table); return box;
}
private static Control PasswordPanel(TextBox password)
{
var panel = new TableLayoutPanel { Dock = DockStyle.Fill, AutoSize = true, ColumnCount = 2 };
panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); panel.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize));
var show = new CheckBox { Text = "显示", AutoSize = true };
show.CheckedChanged += (_, _) => password.UseSystemPasswordChar = !show.Checked;
password.Dock = DockStyle.Fill;
password.MinimumSize = new Size(260, 0);
password.Margin = new Padding(0, 0, 12, 0);
show.Anchor = AnchorStyles.Left;
show.Margin = new Padding(0);
panel.Controls.Add(password); panel.Controls.Add(show); return panel;
}
private void OpenImageFromDialog()
{
using var dialog = new OpenFileDialog
{
Filter = "核桃派镜像 (*.img)|*.img",
CheckFileExists = true,
CheckPathExists = true,
Multiselect = false,
ValidateNames = true,
};
if (dialog.ShowDialog(this) == DialogResult.OK) OpenImage(dialog.FileName);
}
private void OpenImage(string filename)
{
try
{
var config = ImageOperations.ReadConfig(filename);
ApplyLoadedConfig(Path.GetFullPath(filename), config);
}
catch (Exception exception) { AppDialog.Error(this, exception.Message, "无法打开镜像"); }
}
private void ApplyLoadedConfig(string filename, ImageConfig config)
{
imagePath = filename; loaded = config; path.Text = imagePath; version.Text = $"软件版本:{config.SoftwareVersion} 配置校验:正常";
username.Text = config.Account.Username; accountPassword.Text = config.Account.Password; ssid.Text = config.Wifi.Ssid; wifiPassword.Text = config.Wifi.Password;
mode.SelectedIndex = config.Ipv4.Mode == "static" ? 1 : 0; address.Text = config.Ipv4.Address; prefix.Value = config.Ipv4.Prefix == 0 ? 24 : config.Ipv4.Prefix; gateway.Text = config.Ipv4.Gateway; dns.Text = string.Join(",", config.Ipv4.Dns);
save.Enabled = saveAs.Enabled = true; UpdateNetworkFields();
}
private ImageConfig Collect()
{
if (loaded is null) throw new InvalidOperationException("尚未打开镜像");
var config = new ImageConfig
{
SchemaVersion = loaded.SchemaVersion, Product = loaded.Product, SoftwareVersion = loaded.SoftwareVersion,
Account = new AccountConfig { Username = username.Text.Trim(), Password = accountPassword.Text },
Wifi = new WifiConfig { Ssid = ssid.Text, Password = wifiPassword.Text },
Ipv4 = mode.SelectedIndex == 0
? new Ipv4Config { Mode = "dhcp" }
: new Ipv4Config { Mode = "static", Address = address.Text.Trim(), Prefix = (int)prefix.Value, Gateway = gateway.Text.Trim(), Dns = dns.Text.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).ToList() },
};
ImageConfigCodec.Validate(config); return config;
}
private async Task SaveOriginalAsync()
{
if (imagePath is null) return;
try
{
var config = Collect();
if (AppDialog.Confirm(this, "这会直接修改当前镜像。建议日常使用“另存为”保留原始包。\n\n确定继续吗?", "确认修改原镜像") != DialogResult.Yes) return;
var verified = await WriteAndVerifyAsync(imagePath, config);
ApplyLoadedConfig(imagePath, verified);
AppDialog.Information(this, "配置已保存,镜像与 SHA-256 摘要均已完成写后校验。", "保存成功");
}
catch (Exception exception)
{
AppDialog.Error(this, exception.Message, "保存失败");
}
}
private async Task SaveAsAsync()
{
if (imagePath is null) return;
using var dialog = new SaveFileDialog
{
Filter = "核桃派镜像 (*.img)|*.img",
DefaultExt = "img",
AddExtension = true,
ValidateNames = true,
CheckPathExists = true,
OverwritePrompt = false,
FileName = Path.GetFileNameWithoutExtension(imagePath) + "-自定义.img",
InitialDirectory = Path.GetDirectoryName(imagePath),
Title = "请重命名,以便区分设备或使用地点",
};
if (dialog.ShowDialog(this) != DialogResult.OK) return;
string? destination = null;
try
{
var config = Collect();
destination = ImagePathRules.ValidateNewDestination(imagePath, dialog.FileName);
SetBusy(true);
await Task.Run(() => File.Copy(imagePath, destination, overwrite: false));
var verified = await WriteAndVerifyAsync(destination, config, alreadyBusy: true);
ApplyLoadedConfig(destination, verified);
AppDialog.Information(this, "自定义镜像已保存并重新打开,镜像与 SHA-256 摘要均已完成写后校验。", "另存为成功");
}
catch (Exception exception)
{
var cleanupError = destination is null ? null : ImagePathRules.CleanupNewDestination(destination);
var message = cleanupError is null ? exception.Message : $"{exception.Message}\n\n清理未完成:{cleanupError}";
AppDialog.Error(this, message, "另存为失败");
}
finally { SetBusy(false); }
}
private async Task<ImageConfig> WriteAndVerifyAsync(string target, ImageConfig config, bool alreadyBusy = false)
{
if (!alreadyBusy) SetBusy(true);
try
{
var reporter = new Progress<int>(value => progress.Value = Math.Clamp(value, 0, 100));
return await Task.Run(() =>
{
ImageOperations.WriteConfig(target, config);
ImageOperations.WriteSha256(target, reporter);
return ImageOperations.ReadConfig(target);
});
}
finally { if (!alreadyBusy) SetBusy(false); }
}
private void SetBusy(bool value) { isBusy = value; open.Enabled = !value; save.Enabled = saveAs.Enabled = !value && loaded is not null; progress.Visible = value; progress.Value = 0; UseWaitCursor = value; }
private void UpdateNetworkFields() { var enabled = mode.SelectedIndex == 1; address.Enabled = prefix.Enabled = gateway.Enabled = dns.Enabled = enabled; }
protected override void OnShown(EventArgs eventArgs)
{
base.OnShown(eventArgs);
ResponsiveLayout.FitAndCenter(this, PreferredLogicalSize, MinimumLogicalSize, 0.9);
if (!watchingDisplaySettings)
{
SystemEvents.DisplaySettingsChanged += DisplaySettingsChanged;
watchingDisplaySettings = true;
}
}
protected override void OnDpiChanged(DpiChangedEventArgs eventArgs)
{
base.OnDpiChanged(eventArgs);
BeginInvoke(() => ResponsiveLayout.FitAndCenter(this, PreferredLogicalSize, MinimumLogicalSize, 0.9));
}
protected override void OnFormClosed(FormClosedEventArgs eventArgs)
{
if (watchingDisplaySettings)
{
SystemEvents.DisplaySettingsChanged -= DisplaySettingsChanged;
watchingDisplaySettings = false;
}
base.OnFormClosed(eventArgs);
}
private void DisplaySettingsChanged(object? sender, EventArgs eventArgs)
{
if (!IsDisposed && IsHandleCreated) BeginInvoke(() => ResponsiveLayout.FitAndCenter(this, PreferredLogicalSize, MinimumLogicalSize, 0.9));
}
}