Files
ShadowsocksX-NG/ShadowsocksX-NG2/App/UI/Workspace/MainWindowView.swift
T

188 lines
6.2 KiB
Swift

import SwiftUI
/// 服务器导入面板:由窗口工具栏「导入服务器配置」按钮打开,窗口壳统一持有
/// 呈现;添加订阅表单由订阅分区自持(SubscriptionsView)。
private enum WorkspaceSheet: String, Identifiable {
case importServers
var id: String { rawValue }
}
/// 主窗口标签页外壳:WorkspaceRoute 持有导航位置;各分区持有页级动作。
/// 仅当前 destination 挂载内容,延续切页清理草稿与取消页面任务的生命周期;
/// 激活反馈仍由窗口壳持有,跨标签切换存续。
struct MainWindowView: View {
/// 纯转发的分区工作流与控制器:壳 body 不读它们(分区视图自行观察),
/// 用 let 转发,壳不重复订阅它们的发布。
let rulesWorkflow: RulesWorkflow
@ObservedObject var route: WorkspaceRoute
@ObservedObject var workflow: CatalogWorkflow
@ObservedObject var control: ProxyControlWorkflow
let plugins: PluginManagementModel
let settingsWorkflow: SettingsWorkflow
let loginController: LaunchAtLoginController
let silentLaunch: SilentLaunchController
/// 目录树折叠状态:组合根持有的长寿命对象,跨 destination 切换存续;服务器
/// 侧栏使用(见 CatalogExpansionState)。
let expansion: CatalogExpansionState
let clipboard: any TextClipboard
let imageClipboard: any ImageClipboard
let configurationGroupFileExporter: any ConfigurationGroupFileExporter
let qrImageSaver: any QrImageSaver
/// 激活反馈共享状态:侧栏右键与分组详情两个激活入口共用。
@StateObject private var activationFeedback = ActivationFeedbackState()
/// 系统标签选择先落在 SwiftUI 自管状态,再同步到 route,避免在视图
/// 更新期间发布;外部导航反向同步,route 仍是导航位置的唯一来源。
@State private var tabSelection: WorkspaceDestination = WorkspaceRoute.initialDestination
@State private var selection: NodeID?
@State private var serverSource: NodeSource = .manual
@State private var serverSortOrder: [ServerTableSort] = []
/// 全局导入面板由窗口壳持有。
@State private var presentedWorkspaceSheet: WorkspaceSheet?
var body: some View {
TabView(selection: $tabSelection) {
ForEach(WorkspaceDestination.allCases) { destination in
Tab(destination.label, systemImage: destination.systemImage, value: destination) {
if route.destination == destination {
destinationView(destination)
}
}
}
}
.tabViewStyle(.automatic)
.toolbar {
ToolbarItem(placement: .navigation) {
importButton
}
}
.onChange(of: tabSelection) { _, destination in
route.navigate(to: destination)
}
.onChange(of: route.destination, initial: true) { _, destination in
if tabSelection != destination {
tabSelection = destination
}
}
.onChange(of: selection) {
guard let selection, let node = workflow.tree.node(withID: selection) else { return }
serverSource = node.source
expansion.reveal(selection, in: workflow.tree)
}
.navigationTitle(route.destination.label)
.frame(minWidth: 920, minHeight: 580)
.sheet(item: $presentedWorkspaceSheet) { sheet in
switch sheet {
case .importServers:
ImportServersSheet(
workflow: workflow, clipboard: clipboard, selection: $selection)
}
}
}
// MARK: - 全局导入入口
private var importButton: some View {
Button {
presentWorkspaceSheet(.importServers)
} label: {
Label("导入", systemImage: "square.and.arrow.down")
}
.help("导入")
}
private func presentWorkspaceSheet(_ sheet: WorkspaceSheet) {
route.navigate(to: .servers)
presentedWorkspaceSheet = sheet
}
// MARK: - 分区承载
@ViewBuilder
private func destinationView(_ destination: WorkspaceDestination) -> some View {
switch destination {
case .home:
WorkspaceHomeView(
workflow: workflow,
control: control,
clipboard: clipboard)
case .servers:
ServersView(
workflow: workflow,
activation: activationFeedback,
activeTargetID: control.snapshot.activeTarget?.id,
selection: $selection,
source: $serverSource,
sortOrder: $serverSortOrder,
expansion: expansion,
clipboard: clipboard,
imageClipboard: imageClipboard,
configurationGroupFileExporter: configurationGroupFileExporter,
qrImageSaver: qrImageSaver)
case .subscriptions:
SubscriptionsView(
workflow: workflow,
onNodesRemoved: clearSelectionIfInvalidated)
case .rules:
RulesView(workflow: rulesWorkflow)
case .settings:
SettingsView(
plugins: plugins, workflow: settingsWorkflow, loginController: loginController,
silentLaunch: silentLaunch
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func clearSelectionIfInvalidated(_ removed: Set<NodeID>) {
if let selection, removed.contains(selection) {
self.selection = nil
}
}
}
extension WorkspaceDestination {
var label: String {
switch self {
case .home: "首页"
case .servers: "服务器"
case .subscriptions: "订阅"
case .rules: RulesCopy.text("代理规则")
case .settings: "设置"
}
}
var systemImage: String {
switch self {
case .home: "house"
case .servers: "server.rack"
case .subscriptions: "arrow.triangle.2.circlepath"
case .rules: "list.bullet.rectangle"
case .settings: "gearshape"
}
}
}
// MARK: - 首页 destination 包装壳
/// 首页 destination 的包装视图:承载共享错误弹窗挂载(票 #53/#54),行为
/// 委托给 HomeView。旧版导入的主动弹窗已移除(docs/design/unified-server-
/// import.md),发现状态仅供统一导入面板的 Legacy 入口显隐。
private struct WorkspaceHomeView: View {
@ObservedObject var workflow: CatalogWorkflow
@ObservedObject var control: ProxyControlWorkflow
let clipboard: any TextClipboard
@StateObject private var errors = ErrorAlertPresenter()
var body: some View {
HomeView(
workflow: workflow,
control: control,
clipboard: clipboard,
errors: errors
)
.presentingErrors(errors)
}
}