namespace DNFLoadout; internal sealed class TextPrompt : Form { private readonly TextBox _text = new() { Dock = DockStyle.Fill }; public string Value => _text.Text.Trim(); public TextPrompt(string title, string label, string initial = "") { Text = title; Width = 360; Height = 150; StartPosition = FormStartPosition.CenterParent; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; var panel = new TableLayoutPanel { Dock = DockStyle.Fill, Padding = new Padding(10), RowCount = 3 }; panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 28)); panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 36)); panel.RowStyles.Add(new RowStyle(SizeType.Absolute, 40)); panel.Controls.Add(new Label { Text = label + "(按回车确认)", Dock = DockStyle.Fill }, 0, 0); _text.Text = initial; panel.Controls.Add(_text, 0, 1); var ok = new Button { Text = "确定", DialogResult = DialogResult.OK, Dock = DockStyle.Right, Width = 90 }; var cancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel, Dock = DockStyle.Right, Width = 90 }; var buttons = new FlowLayoutPanel { Dock = DockStyle.Fill, FlowDirection = FlowDirection.RightToLeft }; buttons.Controls.Add(ok); buttons.Controls.Add(cancel); panel.Controls.Add(buttons, 0, 2); Controls.Add(panel); AcceptButton = ok; CancelButton = cancel; } } internal sealed class HotkeyDialog : Form { private readonly TextBox _display = new() { ReadOnly = true, Dock = DockStyle.Top, Height = 28 }; private int _modifiers; private int _keyCode; public int Modifiers => _modifiers; public int KeyCode => _keyCode; public HotkeyDialog(int modifiers, int keyCode) { Text = "设置快捷键"; Width = 360; Height = 190; StartPosition = FormStartPosition.CenterParent; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; MinimizeBox = false; _modifiers = modifiers; _keyCode = keyCode; _display.Text = Scheme.HotkeyTextOf(_modifiers, _keyCode); _display.KeyDown += (_, e) => { var (mods, code) = HotkeyManager.Capture(e.KeyData); if (code == 0) return; _modifiers = mods; _keyCode = code; _display.Text = Scheme.HotkeyTextOf(_modifiers, _keyCode); e.SuppressKeyPress = true; }; var hint = new Label { Text = "点击下面的输入框,然后按下想要的组合键。\n支持 F1~F12、字母、数字,可组合 Ctrl/Alt/Shift。", Dock = DockStyle.Fill, Padding = new Padding(8), }; var ok = new Button { Text = "确定", DialogResult = DialogResult.OK, Dock = DockStyle.Right, Width = 90 }; var cancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel, Dock = DockStyle.Right, Width = 90 }; var buttons = new FlowLayoutPanel { Dock = DockStyle.Bottom, Height = 40, FlowDirection = FlowDirection.RightToLeft }; buttons.Controls.Add(ok); buttons.Controls.Add(cancel); Controls.Add(hint); Controls.Add(_display); Controls.Add(buttons); AcceptButton = ok; CancelButton = cancel; } } internal sealed class MainForm : Form { private readonly TextBox _server = new() { Width = 260 }; private readonly TextBox _token = new() { Width = 260, UseSystemPasswordChar = true }; private readonly ListBox _schemes = new() { Dock = DockStyle.Fill, IntegralHeight = false }; private readonly Label _status = new() { Dock = DockStyle.Bottom, Height = 64, TextAlign = ContentAlignment.MiddleLeft, Padding = new Padding(6) }; private readonly Button _add = new() { Text = "新增方案", Width = 100 }; private readonly Button _hotkey = new() { Text = "设置快捷键", Width = 100 }; private readonly Button _saveCurrent = new() { Text = "保存当前配装", Width = 120 }; private readonly Button _apply = new() { Text = "应用方案", Width = 100 }; private readonly Button _delete = new() { Text = "删除方案", Width = 100 }; private readonly Button _saveConfig = new() { Text = "保存配置", Width = 100 }; private readonly Button _addPage = new() { Text = "新增换装页", Width = 100 }; private readonly Button _renamePage = new() { Text = "重命名换装页", Width = 100 }; private readonly Button _deletePage = new() { Text = "删除换装页", Width = 100 }; private readonly TabControl _pageTabs = new() { Dock = DockStyle.Top, Height = 34, Appearance = TabAppearance.Buttons, SizeMode = TabSizeMode.Fixed, ItemSize = new Size(120, 28), Multiline = false, }; private bool _syncingPageTabs; private readonly Config _config; private readonly HotkeyManager _hotkeys; private readonly Dictionary _cooldownUntil = new(StringComparer.OrdinalIgnoreCase); public MainForm() { Text = "DNF 一键换装"; Width = 720; Height = 520; StartPosition = FormStartPosition.CenterScreen; MinimumSize = new Size(680, 460); _config = ConfigStore.Load(); _server.Text = _config.ServerUrl; _token.Text = _config.AdminToken; var top = new TableLayoutPanel { Dock = DockStyle.Top, Height = 46, ColumnCount = 4, Padding = new Padding(8) }; for (var i = 0; i < 4; i++) top.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); top.Controls.Add(new Label { Text = "服务端", AutoSize = true, Anchor = AnchorStyles.Left }, 0, 0); top.Controls.Add(_server, 1, 0); top.Controls.Add(new Label { Text = "Admin Token", AutoSize = true, Anchor = AnchorStyles.Left }, 2, 0); top.Controls.Add(_token, 3, 0); var right = new FlowLayoutPanel { Dock = DockStyle.Right, Width = 130, FlowDirection = FlowDirection.TopDown, Padding = new Padding(6) }; foreach (var button in new[] { _addPage, _renamePage, _deletePage, _add, _hotkey, _saveCurrent, _apply, _delete, _saveConfig }) { button.Width = 112; right.Controls.Add(button); } Controls.Add(_status); Controls.Add(_schemes); Controls.Add(_pageTabs); Controls.Add(right); Controls.Add(top); _hotkeys = new HotkeyManager(_config); _hotkeys.Triggered += scheme => BeginInvoke(async () => await ApplySchemeAsync(scheme)); _pageTabs.SelectedIndexChanged += (_, _) => OnPageChanged(); _addPage.Click += (_, _) => AddPage(); _renamePage.Click += (_, _) => RenamePage(); _deletePage.Click += (_, _) => DeletePage(); _add.Click += (_, _) => AddScheme(); _hotkey.Click += (_, _) => SetHotkey(); _saveCurrent.Click += async (_, _) => await SaveCurrentAsync(); _apply.Click += async (_, _) => await ApplySelectedAsync(); _delete.Click += (_, _) => DeleteScheme(); _saveConfig.Click += (_, _) => SaveConfig(true); _schemes.DoubleClick += async (_, _) => await ApplySelectedAsync(); FormClosing += (_, _) => SaveConfig(false); RebuildPageTabs(); RefreshList(); } private Scheme? Selected => _schemes.SelectedIndex >= 0 && _schemes.SelectedIndex < _config.CurrentPage.Schemes.Count ? _config.CurrentPage.Schemes[_schemes.SelectedIndex] : null; private void RebuildPageTabs() { _syncingPageTabs = true; _pageTabs.TabPages.Clear(); foreach (var page in _config.Pages) { _pageTabs.TabPages.Add(new TabPage(page.Name)); } if (_pageTabs.TabPages.Count > 0) _pageTabs.SelectedIndex = Math.Clamp(_config.ActivePage, 0, _pageTabs.TabPages.Count - 1); _syncingPageTabs = false; } private void OnPageChanged() { if (_syncingPageTabs || _pageTabs.SelectedIndex < 0) return; _config.ActivePage = _pageTabs.SelectedIndex; _hotkeys.UpdateConfig(_config); RefreshList(); SaveConfig(false); } private void AddPage() { using var prompt = new TextPrompt("新增换装页", "换装页名称", $"换装页{_config.Pages.Count + 1}"); if (prompt.ShowDialog(this) != DialogResult.OK || prompt.Value.Length == 0) return; _config.Pages.Add(new LoadoutPage { Name = prompt.Value }); _config.ActivePage = _config.Pages.Count - 1; RebuildPageTabs(); RefreshList(); SaveConfig(false); } private void RenamePage() { var page = _config.CurrentPage; using var prompt = new TextPrompt("重命名换装页", "换装页名称", page.Name); if (prompt.ShowDialog(this) != DialogResult.OK || prompt.Value.Length == 0) return; page.Name = prompt.Value; RebuildPageTabs(); SaveConfig(false); } private void DeletePage() { if (_config.Pages.Count <= 1) { _status.Text = "至少保留一个换装页。"; return; } var page = _config.CurrentPage; if (MessageBox.Show(this, $"删除换装页“{page.Name}”及其中的全部方案?", "确认", MessageBoxButtons.YesNo) != DialogResult.Yes) return; _config.Pages.Remove(page); _config.ActivePage = Math.Clamp(_config.ActivePage, 0, _config.Pages.Count - 1); RebuildPageTabs(); RefreshList(); SaveConfig(false); } private void RefreshList() { var selected = _schemes.SelectedIndex; _schemes.Items.Clear(); foreach (var scheme in _config.CurrentPage.Schemes) _schemes.Items.Add(scheme.Display); if (_schemes.Items.Count > 0) _schemes.SelectedIndex = Math.Clamp(selected, 0, _schemes.Items.Count - 1); } private void AddScheme() { using var namePrompt = new TextPrompt("新增方案", "方案名称", $"方案{_config.CurrentPage.Schemes.Count + 1}"); if (namePrompt.ShowDialog(this) != DialogResult.OK || namePrompt.Value.Length == 0) return; using var characterPrompt = new TextPrompt("绑定角色", "角色游戏内名称", ""); if (characterPrompt.ShowDialog(this) != DialogResult.OK || characterPrompt.Value.Length == 0) return; var scheme = new Scheme { Name = namePrompt.Value, CharacterName = characterPrompt.Value, }; _config.CurrentPage.Schemes.Add(scheme); RefreshList(); _schemes.SelectedIndex = _config.CurrentPage.Schemes.Count - 1; SetHotkey(); } private void SetHotkey() { var scheme = Selected; if (scheme == null) return; using var dialog = new HotkeyDialog(scheme.Modifiers, scheme.KeyCode); if (dialog.ShowDialog(this) != DialogResult.OK || dialog.KeyCode == 0) return; scheme.Modifiers = dialog.Modifiers; scheme.KeyCode = dialog.KeyCode; RefreshList(); SaveConfig(false); } private async Task SaveCurrentAsync() { var scheme = Selected; if (scheme == null) { MessageBox.Show(this, "先选择一个方案"); return; } if (!ValidateConnection()) return; if (!EnsureCharacter(scheme)) return; try { SaveConfig(false); var client = new ServerClient(_config); var slots = await client.GetEquipmentAsync(scheme.CharacterName); scheme.Slots = slots; RefreshList(); SaveConfig(false); _status.Text = $"已保存方案“{scheme.Name}”:角色“{scheme.CharacterName}”,{slots.Count} 个部位。"; } catch (Exception ex) { _status.Text = "保存失败:" + ex.Message; } } private async Task ApplySelectedAsync() { var scheme = Selected; if (scheme == null) { MessageBox.Show(this, "先选择一个方案"); return; } await ApplySchemeAsync(scheme); } private async Task ApplySchemeAsync(Scheme scheme) { if (!ValidateConnection()) return; if (!EnsureCharacter(scheme)) return; if (!CheckCooldown(scheme)) return; if (scheme.Slots.Count == 0) { _status.Text = $"方案“{scheme.Name}”还没有保存配装。"; return; } try { SaveConfig(false); _status.Text = $"正在应用“{scheme.Name}”到角色“{scheme.CharacterName}”..."; var client = new ServerClient(_config); var result = await client.ApplyAsync(scheme.CharacterName, scheme.Slots); var applied = result.Applied ?? new List(); var skippedList = result.Skipped ?? new List(); var skipped = skippedList.Count == 0 ? "无跳过" : "跳过:" + string.Join("; ", skippedList.Select(s => $"槽{s.Slot}={s.ItemId} {s.Reason}")); if (result.CooldownSeconds > 0) { _cooldownUntil[scheme.CharacterName] = DateTime.UtcNow.AddSeconds(result.CooldownSeconds); _status.Text = $"应用“{scheme.Name}”完成:成功 {applied.Count} 件,{skipped};换装冷却 {result.CooldownSeconds} 秒" + (result.RefreshError is { Length: > 0 } ? " 刷新异常:" + result.RefreshError : ""); } else { _status.Text = $"应用“{scheme.Name}”完成:成功 {applied.Count} 件,{skipped}" + (result.RefreshError is { Length: > 0 } ? " 刷新异常:" + result.RefreshError : ""); } } catch (Exception ex) { _status.Text = "应用失败:" + ex.Message; } } private bool CheckCooldown(Scheme scheme) { if (_cooldownUntil.TryGetValue(scheme.CharacterName, out var until)) { var remaining = (int)Math.Ceiling((until - DateTime.UtcNow).TotalSeconds); if (remaining > 0) { _status.Text = $"角色“{scheme.CharacterName}”换装冷却中,还需 {remaining} 秒。"; return false; } _cooldownUntil.Remove(scheme.CharacterName); } return true; } private void DeleteScheme() { var scheme = Selected; if (scheme == null) return; if (MessageBox.Show(this, $"删除方案“{scheme.Name}”?", "确认", MessageBoxButtons.YesNo) != DialogResult.Yes) return; _config.CurrentPage.Schemes.Remove(scheme); RefreshList(); SaveConfig(false); } private bool EnsureCharacter(Scheme scheme) { if (scheme.CharacterName.Trim().Length > 0) return true; using var prompt = new TextPrompt("绑定角色", "角色游戏内名称", ""); if (prompt.ShowDialog(this) != DialogResult.OK || prompt.Value.Length == 0) return false; scheme.CharacterName = prompt.Value; RefreshList(); SaveConfig(false); return true; } private bool ValidateConnection() { _config.ServerUrl = _server.Text.Trim(); _config.AdminToken = _token.Text.Trim(); if (_config.ServerUrl.Length == 0 || _config.AdminToken.Length == 0) { _status.Text = "请填写服务端地址和 Admin Token。"; return false; } return true; } private void SaveConfig(bool show) { _config.ServerUrl = _server.Text.Trim(); _config.AdminToken = _token.Text.Trim(); if (!_syncingPageTabs && _pageTabs.SelectedIndex >= 0) _config.ActivePage = _pageTabs.SelectedIndex; ConfigStore.Save(_config); _hotkeys.UpdateConfig(_config); if (show) _status.Text = "配置已保存。"; } }