mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-14 10:13:54 +00:00
issue #81 第一部分:手动表单产品上限(名称 128、地址 255、密码 1024 用户可见字符;插件参数 65,536 UTF-8 字节;端口最多五位十进制数字)。 校验收进 ServerFormFields 唯一入口 validateForSubmit(),字段错误 typed 呈现、修正即清除、首错按栅格顺序定位;validateName/hasNameError 退役。端口改十进制字符串草稿,draft 在草稿不可解析时为 nil、仅在 校验通过后消费,数值等价改写不算变更——无效输入不再悄悄提交旧绑定 值。与装载基线逐字相同的超长字段放行(历史数据兼容);导入与订阅 路径不受限。新错误文案入 Localizable.xcstrings(中英)。
115 lines
3.9 KiB
Swift
115 lines
3.9 KiB
Swift
import SwiftUI
|
|
|
|
/// 服务器详情的插件区(issue #38/D10,地图 #52 票 #55):受管选择器——
|
|
/// 「无」+ 受管列表;集外引用(Legacy 导入/订阅带入)追加显式「本版本未
|
|
/// 提供」项使当前状态可见并原样保留。选中受管项才显示参数输入与可用性警告。
|
|
struct ServerPluginSection: View {
|
|
@Binding var selection: PluginSelection
|
|
@Binding var optionsText: String
|
|
let plugin: PluginSectionState?
|
|
let isEditable: Bool
|
|
/// 参数字段的行内校验错误(issue #81,如字节超限)。
|
|
let optionsError: ServerFormFieldError?
|
|
let optionsFocus: FocusState<ServerFormField?>.Binding
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
picker
|
|
detail
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var picker: some View {
|
|
if let plugin {
|
|
Picker("插件", selection: $selection) {
|
|
Text("无").tag(PluginSelection.none)
|
|
ForEach(plugin.managed, id: \.program) { info in
|
|
Text(info.program).tag(PluginSelection.managed(program: info.program))
|
|
}
|
|
if case .unknown(let program) = plugin.selection {
|
|
Text("\(program)(本版本未提供)").tag(PluginSelection.unknown(program: program))
|
|
}
|
|
}
|
|
.labelsHidden()
|
|
.accessibilityLabel("插件")
|
|
.disabled(!isEditable)
|
|
} else {
|
|
Text("不可用").foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var detail: some View {
|
|
if let plugin {
|
|
switch selection {
|
|
case .none:
|
|
EmptyView()
|
|
case .managed(let program):
|
|
managedDetails(program: program, plugin: plugin)
|
|
case .unknown(let program):
|
|
unknownNotice(program: program, plugin: plugin)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 选中受管项:参数输入与可用性警告。
|
|
@ViewBuilder
|
|
private func managedDetails(program: String, plugin: PluginSectionState) -> some View {
|
|
if plugin.managed.contains(where: { $0.program == program }) {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
if !plugin.provided {
|
|
Label(
|
|
"受管插件可执行文件缺失:激活会被点名拒绝;请重新安装本 app。",
|
|
systemImage: "exclamationmark.triangle.fill"
|
|
)
|
|
.font(.footnote)
|
|
.foregroundStyle(.orange)
|
|
}
|
|
Text("插件参数")
|
|
.font(.callout.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
TextField(
|
|
"插件参数",
|
|
text: $optionsText,
|
|
prompt: Text("选择插件后填写参数(如 mode=websocket;host=…)")
|
|
)
|
|
.textFieldStyle(.roundedBorder)
|
|
.focused(optionsFocus, equals: .pluginOptions)
|
|
.disabled(!isEditable)
|
|
if let optionsError {
|
|
Text(optionsErrorText(optionsError))
|
|
.font(.footnote)
|
|
.foregroundStyle(.red)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 集外引用:原样保留并明确指出当前 app 无法提供该插件。
|
|
@ViewBuilder
|
|
private func unknownNotice(program: String, plugin: PluginSectionState) -> some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
Label(
|
|
"本版本未提供「\(program)」:引用原样保留,激活包含该服务器会被点名拒绝;可改选「无」或受管插件。",
|
|
systemImage: "exclamationmark.triangle.fill"
|
|
)
|
|
.font(.footnote)
|
|
.foregroundStyle(.orange)
|
|
if plugin.optionsPresent {
|
|
Text("插件参数已配置(存于钥匙串,原样保留)。")
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 参数错误文案:只呈现原因与上限,不回显参数内容。
|
|
private func optionsErrorText(_ error: ServerFormFieldError) -> LocalizedStringKey {
|
|
switch error {
|
|
case .tooManyBytes(let limit): return "插件参数最多 \(limit) 个字节"
|
|
case .missingName, .tooManyCharacters, .invalidPort: return ""
|
|
}
|
|
}
|
|
}
|