mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-29 13:43:49 +00:00
feat(ui): consolidate workspace add actions
This commit is contained in:
@@ -1,5 +1,23 @@
|
||||
import SwiftUI
|
||||
|
||||
private enum WorkspaceSheet: String, Identifiable {
|
||||
case importURL
|
||||
case qrImport
|
||||
case legacyImport
|
||||
case addSubscription
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var destination: WorkspaceDestination {
|
||||
switch self {
|
||||
case .importURL, .qrImport, .legacyImport:
|
||||
.servers
|
||||
case .addSubscription:
|
||||
.subscriptions
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 主窗口外壳(地图 #52,票 #53):NavigationSplitView 侧栏承载五项导航与
|
||||
/// 底部常驻代理状态卡;详情区按 route destination 承载各分区视图。分区大
|
||||
/// 标题由窗口标题承担(组合根把 destination label 推给开窗器,见
|
||||
@@ -20,8 +38,8 @@ struct MainWindowView: View {
|
||||
let diagnosticReportExporter: any DiagnosticReportExporter
|
||||
|
||||
@State private var selection: NodeID?
|
||||
/// 分区动作的呈现状态(票 #56/#58):添加订阅 sheet 与诊断导出由壳持有。
|
||||
@State private var showAddSubscription = false
|
||||
/// 全局添加菜单打开的表单,以及诊断导出由窗口壳持有。
|
||||
@State private var presentedWorkspaceSheet: WorkspaceSheet?
|
||||
@State private var exportedDiagnosticsPath: String?
|
||||
@StateObject private var shellActionErrors = ErrorAlertPresenter()
|
||||
|
||||
@@ -34,12 +52,27 @@ struct MainWindowView: View {
|
||||
}
|
||||
.frame(minWidth: 920, minHeight: 580)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigation) {
|
||||
addMenu
|
||||
}
|
||||
ToolbarItemGroup(placement: .primaryAction) {
|
||||
destinationActions
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showAddSubscription) {
|
||||
AddSubscriptionSheet(workflow: workflow, errors: shellActionErrors)
|
||||
.toolbar(removing: .sidebarToggle)
|
||||
.sheet(item: $presentedWorkspaceSheet) { sheet in
|
||||
switch sheet {
|
||||
case .importURL:
|
||||
ImportURLSheet(
|
||||
workflow: workflow, errors: shellActionErrors, clipboard: clipboard,
|
||||
selection: $selection)
|
||||
case .qrImport:
|
||||
QRImportSheet(workflow: workflow, errors: shellActionErrors, selection: $selection)
|
||||
case .legacyImport:
|
||||
LegacyImportSheet(workflow: workflow)
|
||||
case .addSubscription:
|
||||
AddSubscriptionSheet(workflow: workflow, errors: shellActionErrors)
|
||||
}
|
||||
}
|
||||
.alert(
|
||||
"操作失败",
|
||||
@@ -65,6 +98,31 @@ struct MainWindowView: View {
|
||||
|
||||
// MARK: - 分区动作槽位
|
||||
|
||||
private var addMenu: some View {
|
||||
Menu {
|
||||
Button("通过 URL 导入…") { presentWorkspaceSheet(.importURL) }
|
||||
Button("从二维码图片导入…") { presentWorkspaceSheet(.qrImport) }
|
||||
if workflow.legacyImportState.snapshotFound {
|
||||
Button(legacyImportActionTitle) { presentWorkspaceSheet(.legacyImport) }
|
||||
}
|
||||
Divider()
|
||||
Button("添加订阅…") { presentWorkspaceSheet(.addSubscription) }
|
||||
} label: {
|
||||
Label("添加", systemImage: "plus")
|
||||
}
|
||||
.labelStyle(.iconOnly)
|
||||
.help("添加")
|
||||
}
|
||||
|
||||
private var legacyImportActionTitle: String {
|
||||
workflow.legacyImportState.completed ? "再次导入旧版本服务器…" : "导入旧版本服务器…"
|
||||
}
|
||||
|
||||
private func presentWorkspaceSheet(_ sheet: WorkspaceSheet) {
|
||||
route.navigate(to: sheet.destination)
|
||||
presentedWorkspaceSheet = sheet
|
||||
}
|
||||
|
||||
/// 分区动作(票 #53/#56/#57):页级动作随 destination 切换,经工具栏桥接
|
||||
/// 出现在窗口工具栏右端;首页/服务器无页级动作。例外:设置的保存/恢复
|
||||
/// 默认是表单级提交动作,由 SettingsView 内容顶部行承载(动作与表单同置,
|
||||
@@ -79,9 +137,6 @@ struct MainWindowView: View {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
}
|
||||
Button("添加订阅", systemImage: "plus") {
|
||||
showAddSubscription = true
|
||||
}
|
||||
Button("更新全部", systemImage: "arrow.triangle.2.circlepath") {
|
||||
Task { await workflow.refreshAllSubscriptions() }
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import SwiftUI
|
||||
|
||||
/// 订阅分区(spec #21 D11,issue #35/#41,地图 #52 票 #56):每订阅一张卡片
|
||||
/// (图标 + 名称 + 「HTTPS 订阅地址已隐藏」+ 状态徽标 + 更新/重试 + 「…」菜单 +
|
||||
/// 三列元数据 + 脱敏说明行),失败卡警告色描边。添加订阅与更新全部在窗口
|
||||
/// 工具栏(由主窗口壳提供)。卡片数据来自订阅 projection(非敏感);结构化
|
||||
/// 刷新状态在呈现层本地化(story 42)。
|
||||
/// 三列元数据 + 脱敏说明行),失败卡警告色描边。添加订阅通过窗口工具栏全局
|
||||
/// 「添加」菜单进入,更新全部仍在本页工具栏。卡片数据来自订阅 projection
|
||||
/// (非敏感);结构化刷新状态在呈现层本地化(story 42)。
|
||||
struct SubscriptionsView: View {
|
||||
@ObservedObject var workflow: CatalogWorkflow
|
||||
let errors: ErrorAlertPresenter
|
||||
@@ -38,7 +38,7 @@ struct SubscriptionsView: View {
|
||||
if workflow.subscriptions.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"暂无订阅", systemImage: "arrow.triangle.2.circlepath",
|
||||
description: Text("用工具栏「添加订阅」粘贴 HTTPS 订阅地址(SIP-008 JSON)")
|
||||
description: Text("用窗口左上方的「添加」菜单添加 HTTPS 订阅地址(SIP-008 JSON)")
|
||||
)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import SwiftUI
|
||||
|
||||
/// 服务器 destination(spec #21 D11,issue #32/#34/#35/#41):保留服务器目录树、
|
||||
/// 详情编辑与导入行为。workspace destination 由外层主窗口壳持有(新壳在
|
||||
/// MainWindowView.swift,票 #53);本视图只持有服务器功能的 sheet、alert、
|
||||
/// 确认弹窗与编辑草稿边界;selection 由外层绑定,以便订阅删除仍能清除失效
|
||||
/// 的服务器选择。
|
||||
/// 服务器 destination(spec #21 D11,issue #32/#34/#35/#41):承载服务器目录树、
|
||||
/// 详情编辑与分组命令。workspace destination 由外层主窗口壳持有(新壳在
|
||||
/// MainWindowView.swift,票 #53);全局导入表单由窗口壳呈现,本视图持有服务器
|
||||
/// 功能的 sheet、alert、确认弹窗与编辑草稿边界;selection 由外层绑定,以便订阅
|
||||
/// 删除仍能清除失效的服务器选择。
|
||||
struct ServersView: View {
|
||||
|
||||
@ObservedObject var workflow: CatalogWorkflow
|
||||
@@ -21,11 +21,9 @@ struct ServersView: View {
|
||||
@State private var renameText = ""
|
||||
@State private var newGroupParent: NodeID?
|
||||
@State private var newGroupName = ""
|
||||
@State private var isPresentingNewGroup = false
|
||||
@State private var deleteTarget: NodeID?
|
||||
@State private var moveTarget: NodeID?
|
||||
@State private var showImportURLSheet = false
|
||||
@State private var showQRImportSheet = false
|
||||
@State private var showLegacyImportSheet = false
|
||||
@State private var rootDropHovering = false
|
||||
|
||||
var body: some View {
|
||||
@@ -51,13 +49,11 @@ struct ServersView: View {
|
||||
}
|
||||
.alert(
|
||||
"新建分组",
|
||||
isPresented: Binding(
|
||||
get: { newGroupParent != nil },
|
||||
set: { if !$0 { newGroupParent = nil } })
|
||||
isPresented: $isPresentingNewGroup
|
||||
) {
|
||||
TextField("名称", text: $newGroupName)
|
||||
Button("创建") { commitNewGroup() }
|
||||
Button("取消", role: .cancel) { newGroupParent = nil }
|
||||
Button("取消", role: .cancel) { cancelNewGroup() }
|
||||
}
|
||||
.alert(
|
||||
"操作失败",
|
||||
@@ -81,16 +77,6 @@ struct ServersView: View {
|
||||
} message: {
|
||||
Text(deleteMessage)
|
||||
}
|
||||
.sheet(isPresented: $showImportURLSheet) {
|
||||
ImportURLSheet(
|
||||
workflow: workflow, errors: errors, clipboard: clipboard, selection: $selection)
|
||||
}
|
||||
.sheet(isPresented: $showQRImportSheet) {
|
||||
QRImportSheet(workflow: workflow, errors: errors, selection: $selection)
|
||||
}
|
||||
.sheet(isPresented: $showLegacyImportSheet) {
|
||||
LegacyImportSheet(workflow: workflow)
|
||||
}
|
||||
.sheet(
|
||||
item: Binding(
|
||||
get: { moveTarget.map(MoveContext.init) },
|
||||
@@ -113,8 +99,7 @@ struct ServersView: View {
|
||||
renameText = workflow.displayName(for: id)
|
||||
},
|
||||
onNewGroup: { parent in
|
||||
newGroupParent = parent
|
||||
newGroupName = ""
|
||||
presentNewGroup(in: parent)
|
||||
},
|
||||
onMove: { moveTarget = $0 },
|
||||
onDelete: { deleteTarget = $0 }
|
||||
@@ -136,7 +121,7 @@ struct ServersView: View {
|
||||
if workflow.tree.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"暂无服务器", systemImage: "server.rack",
|
||||
description: Text("用工具栏「添加」导入 ss:// 链接或新建分组")
|
||||
description: Text("用工具栏的「添加」菜单导入 ss:// 链接,或用「新建分组」按钮创建分组")
|
||||
)
|
||||
.allowsHitTesting(false)
|
||||
}
|
||||
@@ -196,35 +181,34 @@ extension ServersView {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 工具栏(添加与新建分组)
|
||||
// MARK: - 工具栏(新建分组)
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbarContent: some ToolbarContent {
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Menu {
|
||||
Button("通过 URL 导入…") { showImportURLSheet = true }
|
||||
Button("从二维码图片导入…") { showQRImportSheet = true }
|
||||
if workflow.legacyImportState.snapshotFound {
|
||||
Divider()
|
||||
Button(
|
||||
workflow.legacyImportState.completed ? "再次导入 Legacy 配置…" : "导入 Legacy 配置…"
|
||||
) {
|
||||
showLegacyImportSheet = true
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
Button("新建分组…") {
|
||||
newGroupParent = workflow.importTargetParent(for: selection)
|
||||
newGroupName = ""
|
||||
}
|
||||
Button {
|
||||
presentNewGroup(in: workflow.importTargetParent(for: selection))
|
||||
} label: {
|
||||
Label("添加", systemImage: "plus")
|
||||
Label("新建分组", systemImage: "folder.badge.plus")
|
||||
}
|
||||
.labelStyle(.iconOnly)
|
||||
.help("新建分组")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 告警动作
|
||||
|
||||
private func presentNewGroup(in parent: NodeID?) {
|
||||
newGroupParent = parent
|
||||
newGroupName = ""
|
||||
isPresentingNewGroup = true
|
||||
}
|
||||
|
||||
private func cancelNewGroup() {
|
||||
isPresentingNewGroup = false
|
||||
newGroupParent = nil
|
||||
}
|
||||
|
||||
private func commitRename() {
|
||||
guard let id = renameTarget else { return }
|
||||
renameTarget = nil
|
||||
@@ -238,11 +222,12 @@ extension ServersView {
|
||||
}
|
||||
|
||||
private func commitNewGroup() {
|
||||
guard let parent = newGroupParent else { return }
|
||||
newGroupParent = nil
|
||||
let parent = newGroupParent
|
||||
let name = newGroupName
|
||||
cancelNewGroup()
|
||||
Task {
|
||||
do {
|
||||
_ = try await workflow.createGroup(named: newGroupName, into: parent)
|
||||
_ = try await workflow.createGroup(named: name, into: parent)
|
||||
} catch {
|
||||
errors.present(error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user