mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-12 11:35:07 +00:00
201 lines
6.8 KiB
Swift
201 lines
6.8 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 diagnostics: DiagnosticsWorkflow
|
|
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 diagnosticReportExporter: any DiagnosticReportExporter
|
|
let configurationGroupFileExporter: any ConfigurationGroupFileExporter
|
|
let qrImageSaver: any QrImageSaver
|
|
|
|
@StateObject private var homeServerList = HomeServerListState()
|
|
/// 激活反馈共享状态:首页目标树、侧栏右键与分组详情三个激活入口共用。
|
|
@StateObject private var activationFeedback = ActivationFeedbackState()
|
|
/// 系统标签选择先落在 SwiftUI 自管状态,再同步到 route,避免在视图
|
|
/// 更新期间发布;外部导航反向同步,route 仍是导航位置的唯一来源。
|
|
@State private var tabSelection: WorkspaceDestination = WorkspaceRoute.initialDestination
|
|
@State private var selection: NodeID?
|
|
/// 全局导入面板由窗口壳持有。
|
|
@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: workflow.tree, initial: true) {
|
|
homeServerList.update(tree: workflow.tree, activeTargetID: control.snapshot.activeTarget?.id)
|
|
}
|
|
.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,
|
|
serverList: homeServerList,
|
|
activation: activationFeedback,
|
|
clipboard: clipboard,
|
|
onManageServers: { route.navigate(to: .servers) })
|
|
case .servers:
|
|
ServersView(
|
|
workflow: workflow,
|
|
activation: activationFeedback,
|
|
activeTargetID: control.snapshot.activeTarget?.id,
|
|
selection: $selection,
|
|
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)
|
|
case .diagnostics:
|
|
DiagnosticsView(
|
|
diagnostics: diagnostics,
|
|
clipboard: clipboard,
|
|
exporter: diagnosticReportExporter)
|
|
}
|
|
}
|
|
|
|
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: "设置"
|
|
case .diagnostics: "诊断"
|
|
}
|
|
}
|
|
|
|
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"
|
|
case .diagnostics: "chart.bar"
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 serverList: HomeServerListState
|
|
let activation: ActivationFeedbackState
|
|
let clipboard: any TextClipboard
|
|
let onManageServers: () -> Void
|
|
@StateObject private var errors = ErrorAlertPresenter()
|
|
|
|
var body: some View {
|
|
HomeView(
|
|
workflow: workflow,
|
|
control: control,
|
|
serverList: serverList,
|
|
activation: activation,
|
|
clipboard: clipboard,
|
|
onManageServers: onManageServers,
|
|
errors: errors
|
|
)
|
|
.presentingErrors(errors)
|
|
}
|
|
}
|