mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 11:14:51 +00:00
Add Mainland China proxy routing with independent rule workspaces. Preserve existing routing choices through equivalent preset mappings. BREAKING CHANGE: existing custom rules and enablement settings are not migrated. Recreate them in the appropriate preset.
211 lines
7.3 KiB
Swift
211 lines
7.3 KiB
Swift
import SwiftUI
|
|
|
|
/// 服务器导入面板:由窗口工具栏「导入服务器配置」按钮打开,窗口壳统一持有
|
|
/// 呈现;添加订阅表单由服务器分区自持(ServersView)。
|
|
private enum WorkspaceSheet: String, Identifiable {
|
|
case importServers
|
|
|
|
var id: String { rawValue }
|
|
}
|
|
|
|
/// 主窗口标签页外壳:WorkspaceRoute 持有导航位置;各分区持有页级动作。
|
|
/// 仅当前 destination 挂载内容,延续切页清理草稿与取消页面任务的生命周期;
|
|
/// 激活反馈仍由窗口壳持有,跨标签切换存续。
|
|
struct MainWindowView: View {
|
|
/// 纯转发的分区工作流与控制器:壳 body 不读它们(分区视图自行观察),
|
|
/// 用 let 转发,壳不重复订阅它们的发布。
|
|
let rulesWorkflow: RulesWorkspaces
|
|
@ObservedObject var route: WorkspaceRoute
|
|
@ObservedObject var workflow: CatalogWorkflow
|
|
@ObservedObject var control: ProxyControlWorkflow
|
|
/// 首页运行控制卡与菜单栏状态卡共享的状态显示 debounce 模型;壳 body
|
|
/// 不读它(分区视图自行观察),用 let 转发。
|
|
let statusDisplay: StatusDisplayModel
|
|
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()
|
|
@StateObject private var homeBrowser = HomeTargetBrowser()
|
|
/// 系统标签选择先落在 SwiftUI 自管状态,再同步到 route,避免在视图
|
|
/// 更新期间发布;外部导航反向同步,route 仍是导航位置的唯一来源。
|
|
@State private var tabSelection: WorkspaceDestination = WorkspaceRoute.initialDestination
|
|
@State private var selection: NodeID?
|
|
@State private var serverSection: ServerListSection = .manual
|
|
/// 全局导入面板由窗口壳持有。
|
|
@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 {
|
|
if let failure = workflow.libraryFailure,
|
|
destination == .home || destination == .servers
|
|
{
|
|
ServerLibraryRecoveryView(workflow: workflow, failure: failure)
|
|
} else {
|
|
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) {
|
|
homeBrowser.reconcile(in: workflow.tree, favorites: workflow.favorites)
|
|
}
|
|
.onChange(of: workflow.favoriteIDs) {
|
|
homeBrowser.reconcile(in: workflow.tree, favorites: workflow.favorites)
|
|
}
|
|
.onChange(of: selection) {
|
|
guard let selection, let node = workflow.tree.node(withID: selection) else { return }
|
|
guard serverSection != .favorites else { return }
|
|
serverSection = ServerListSection(source: 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: Binding(
|
|
get: { selection },
|
|
set: { id in
|
|
if id != nil { serverSection = .manual }
|
|
selection = id
|
|
}),
|
|
selectNewNodes: serverSection == .favorites)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 全局导入入口
|
|
|
|
private var importButton: some View {
|
|
Button {
|
|
presentWorkspaceSheet(.importServers)
|
|
} label: {
|
|
Label("导入", systemImage: "square.and.arrow.down")
|
|
}
|
|
.help("导入")
|
|
.disabled(workflow.libraryFailure != nil)
|
|
}
|
|
|
|
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,
|
|
statusDisplay: statusDisplay,
|
|
browser: homeBrowser,
|
|
activation: activationFeedback,
|
|
showServers: { route.navigate(to: .servers) },
|
|
clipboard: clipboard)
|
|
case .servers:
|
|
ServersView(
|
|
workflow: workflow,
|
|
activation: activationFeedback,
|
|
activeTargetID: control.snapshot.activeTarget?.id,
|
|
selection: $selection,
|
|
section: $serverSection,
|
|
expansion: expansion,
|
|
clipboard: clipboard,
|
|
imageClipboard: imageClipboard,
|
|
configurationGroupFileExporter: configurationGroupFileExporter,
|
|
qrImageSaver: qrImageSaver)
|
|
case .rules:
|
|
RulesPresetView(workspaces: rulesWorkflow)
|
|
case .settings:
|
|
SettingsView(
|
|
plugins: plugins, workflow: settingsWorkflow, loginController: loginController,
|
|
silentLaunch: silentLaunch
|
|
)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension WorkspaceDestination {
|
|
var label: String {
|
|
switch self {
|
|
case .home: String(localized: "首页")
|
|
case .servers: String(localized: "服务器")
|
|
case .rules: RulesCopy.text("代理规则")
|
|
case .settings: String(localized: "设置")
|
|
}
|
|
}
|
|
|
|
var systemImage: String {
|
|
switch self {
|
|
case .home: "house"
|
|
case .servers: "server.rack"
|
|
case .rules: "list.bullet.rectangle"
|
|
case .settings: "gearshape"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - 首页 destination 包装壳
|
|
|
|
/// 首页 destination 的包装视图:承载共享错误弹窗挂载,行为
|
|
/// 委托给 HomeView。旧版导入的主动弹窗已移除(docs/design/unified-server-
|
|
/// import.md),发现状态仅供统一导入面板的 Legacy 入口显隐。
|
|
private struct WorkspaceHomeView: View {
|
|
@ObservedObject var workflow: CatalogWorkflow
|
|
@ObservedObject var control: ProxyControlWorkflow
|
|
let statusDisplay: StatusDisplayModel
|
|
let browser: HomeTargetBrowser
|
|
let activation: ActivationFeedbackState
|
|
let showServers: () -> Void
|
|
let clipboard: any TextClipboard
|
|
@StateObject private var errors = ErrorAlertPresenter()
|
|
|
|
var body: some View {
|
|
HomeView(
|
|
workflow: workflow,
|
|
control: control,
|
|
statusDisplay: statusDisplay,
|
|
browser: browser,
|
|
activation: activation,
|
|
showServers: showServers,
|
|
clipboard: clipboard,
|
|
errors: errors
|
|
)
|
|
.presentingErrors(errors)
|
|
}
|
|
}
|