mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 16:14:50 +00:00
153 lines
5.0 KiB
Swift
153 lines
5.0 KiB
Swift
import SwiftUI
|
|
|
|
/// 服务器表单通过目录事实解释名称引用;参数草稿独立于插件来源。
|
|
struct ServerPluginSection: View {
|
|
@Binding var selection: PluginSelection
|
|
@ObservedObject var options: PluginOptionsDraft
|
|
let plugin: PluginSectionState?
|
|
let isEditable: Bool
|
|
/// 参数字段的行内校验错误(字节超限或未完成行)。
|
|
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.programs.map(\.program), id: \.self) { program in
|
|
Text(program).tag(
|
|
selection == .unknown(program: program)
|
|
? selection : PluginSelection.named(program: program))
|
|
}
|
|
if let program = plugin.unresolvedProgram(for: selection) {
|
|
Text(program).tag(selection)
|
|
}
|
|
if plugin.selection != selection,
|
|
let program = plugin.unresolvedProgram(for: plugin.selection)
|
|
{
|
|
Text(program).tag(plugin.selection)
|
|
}
|
|
}
|
|
.disabled(!isEditable)
|
|
} else {
|
|
LabeledContent("插件") {
|
|
Text("不可用").foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var detail: some View {
|
|
if let plugin {
|
|
switch selection {
|
|
case .none:
|
|
EmptyView()
|
|
case .named(let program), .unknown(let program):
|
|
if plugin.unresolvedProgram(for: selection) != nil {
|
|
unknownNotice(program: program, plugin: plugin)
|
|
} else {
|
|
namedPluginDetails(program: program, plugin: plugin)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 目录中的名称引用:参数编辑区与可用性警告;订阅只读走独立只读视图。
|
|
@ViewBuilder
|
|
private func namedPluginDetails(program: String, plugin: PluginSectionState) -> some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
availabilityNotice(program: program, plugin: plugin)
|
|
Text("插件参数")
|
|
.font(.callout.weight(.medium))
|
|
.foregroundStyle(.secondary)
|
|
if isEditable {
|
|
PluginOptionsEditor(
|
|
draft: options,
|
|
fieldError: optionsError,
|
|
fieldFocus: optionsFocus)
|
|
} else {
|
|
PluginOptionsReadOnlyEditor(draft: options)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 集外引用:原样保留并明确指出当前 app 无法提供该插件。
|
|
@ViewBuilder
|
|
private func unknownNotice(program: String, plugin: PluginSectionState) -> some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
if plugin.mappingsUnreadable {
|
|
availabilityNotice(program: program, plugin: plugin)
|
|
} else {
|
|
warning(Text("未找到插件“\(program)”。引用和已保存参数将保留;可添加同名用户插件,或改选其他插件或“无”。"))
|
|
}
|
|
if plugin.optionsPresent {
|
|
Text("插件参数已配置(存于钥匙串,原样保留)。")
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func availabilityNotice(program: String, plugin: PluginSectionState) -> some View {
|
|
if let message = plugin.availabilityWarning(for: program) {
|
|
warning(Text(message))
|
|
}
|
|
}
|
|
|
|
private func warning(_ text: Text) -> some View {
|
|
Label {
|
|
text
|
|
} icon: {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
}
|
|
.font(.footnote)
|
|
.foregroundStyle(.orange)
|
|
}
|
|
}
|
|
|
|
/// 查看模式切换(参数列表 ⇄ 原始文本);被未完成行拒绝时回弹到当前模式。
|
|
struct PluginOptionsModePicker: View {
|
|
@ObservedObject var draft: PluginOptionsDraft
|
|
@State private var displayedMode = PluginOptionsDraft.Mode.table
|
|
|
|
var body: some View {
|
|
Picker("查看模式", selection: modeBinding) {
|
|
Text("参数列表").tag(PluginOptionsDraft.Mode.table)
|
|
Text("原始文本").tag(PluginOptionsDraft.Mode.rawText)
|
|
}
|
|
.pickerStyle(.segmented)
|
|
.labelsHidden()
|
|
.onChange(of: draft.mode) { _, mode in
|
|
displayedMode = mode
|
|
}
|
|
}
|
|
|
|
private var modeBinding: Binding<PluginOptionsDraft.Mode> {
|
|
Binding(
|
|
get: { displayedMode },
|
|
set: { requested in
|
|
displayedMode = requested
|
|
// macOS 分段 Picker 在 SwiftUI 更新事务内调用绑定 setter;草稿的
|
|
// @Published 更新须离开该事务,避免向观察视图同步发布变更。
|
|
Task { @MainActor in
|
|
let succeeded =
|
|
requested == .rawText ? draft.switchToRawText() : draft.switchToTable()
|
|
displayedMode = succeeded ? requested : draft.mode
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/// 插件参数编辑区:Table 布局的无边框输入行——行首「值/开关」
|
|
/// 分段选择器决定行的值形态,底部空行即快速添加入口,长值经「编辑值…」
|
|
/// 独立编辑;切换查看模式不触发表单变更检测。
|