初始化奇妙小屏幕控制器项目

This commit is contained in:
2026-09-08 22:56:52 +08:00
commit 8d368de3b5
491 changed files with 67678 additions and 0 deletions
@@ -0,0 +1,253 @@
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));
}
}