mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 10:54:50 +00:00
175 lines
6.3 KiB
Swift
175 lines
6.3 KiB
Swift
import SwiftUI
|
|
|
|
/// 装配当前节点的详情会话;查询、参数装载与命令均留在共用视图之外。
|
|
struct CatalogNodeDetailView: View {
|
|
@Environment(\.locale) private var locale
|
|
@Environment(\.timeZone) private var timeZone
|
|
@ObservedObject var workflow: CatalogWorkflow
|
|
@ObservedObject var activation: ActivationFeedbackState
|
|
let nodeID: NodeID
|
|
let isActiveTarget: Bool
|
|
/// 子树含活动目标(严格后代),与行级空心闪电同源判定。
|
|
let containsActiveTarget: Bool
|
|
let errors: ErrorAlertPresenter
|
|
@StateObject private var parameters = ServerDetailParameters()
|
|
|
|
private var node: CatalogTreeNode? { workflow.tree.node(withID: nodeID) }
|
|
private var eligibility: ActivationEligibility? { workflow.activationEligibility(for: nodeID) }
|
|
|
|
var body: some View {
|
|
if let node {
|
|
Group {
|
|
if let module = workflow.connectivity {
|
|
ObservedConnectivityProperties(
|
|
module: module, node: node, serverName: workflow.displayName(for:)
|
|
) { properties in
|
|
detailView(for: node, connectivity: properties)
|
|
}
|
|
} else {
|
|
detailView(for: node)
|
|
}
|
|
}
|
|
.onAppear {
|
|
if !node.isGroup {
|
|
parameters.showServer(nodeID, load: workflow.serverDetailPluginOptions)
|
|
}
|
|
}
|
|
.onReceive(workflow.serverDetailChanges) { affected in
|
|
parameters.didRefresh(affectedServers: affected, load: workflow.serverDetailPluginOptions)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func detailView(
|
|
for node: CatalogTreeNode, connectivity: NodeDetailPresentation.Properties? = nil
|
|
) -> some View {
|
|
var detail = presentation(for: node)
|
|
if let connectivity { detail.properties.append(connectivity) }
|
|
return NodeDetailView(detail: detail) {
|
|
primaryActions(for: node)
|
|
} secondaryActions: {
|
|
if let module = workflow.connectivity {
|
|
ConnectivityTestButton(module: module, node: node)
|
|
}
|
|
Button(
|
|
workflow.isFavorite(nodeID)
|
|
? String(localized: "取消收藏", table: "ServerDetails")
|
|
: String(localized: "收藏", table: "ServerDetails")
|
|
) {
|
|
do {
|
|
try workflow.setFavorite(nodeID, isFavorite: !workflow.isFavorite(nodeID))
|
|
} catch {
|
|
errors.present(error)
|
|
}
|
|
}
|
|
if !node.isGroup && parameters.failure != nil {
|
|
Button(String(localized: "重新加载", table: "ServerDetails")) {
|
|
parameters.reload(load: workflow.serverDetailPluginOptions)
|
|
}
|
|
}
|
|
} feedback: {
|
|
ActivationDetailFeedbackView(
|
|
messages: detail.activationMessages,
|
|
completion: activation.completion?.targetID == nodeID ? activation.completion : nil)
|
|
if let subscription = subscriptionSummary(for: node) {
|
|
SubscriptionRefreshFeedbackView(
|
|
isRefreshing: workflow.refreshingSubscriptionIDs.contains(subscription.id),
|
|
completion: workflow.subscriptionRefreshCompletions[subscription.id]
|
|
)
|
|
.id(subscription.id)
|
|
}
|
|
} propertiesFooter: { group in
|
|
if group.id == .subscription,
|
|
let fraction = subscriptionSummary(for: node)?.information?.usedFraction
|
|
{
|
|
ProgressView(value: fraction) {
|
|
Text("流量消耗")
|
|
} currentValueLabel: {
|
|
Text(fraction, format: .percent.precision(.fractionLength(0)))
|
|
}
|
|
.progressViewStyle(.linear)
|
|
.tint(.accentColor)
|
|
} else {
|
|
nil
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func primaryActions(for node: CatalogTreeNode) -> some View {
|
|
Button(
|
|
activation.pendingTargetID == nodeID
|
|
? String(localized: "激活中…", table: "ServerDetails")
|
|
: String(localized: "激活", table: "ServerDetails"),
|
|
systemImage: "bolt.fill"
|
|
) {
|
|
Task { @MainActor in
|
|
do {
|
|
_ = try await activation.activate(nodeID, via: workflow)
|
|
} catch {
|
|
// 激活状态记录失败,详情在 feedback 区域显示结果。
|
|
}
|
|
}
|
|
}
|
|
.disabled(!(eligibility?.canActivate ?? false) || activation.isPending)
|
|
if let subscription = subscriptionSummary(for: node) {
|
|
Button(
|
|
workflow.refreshingSubscriptionIDs.contains(subscription.id)
|
|
? String(localized: "更新中…", table: "ServerDetails")
|
|
: String(localized: "更新", table: "ServerDetails"),
|
|
systemImage: "arrow.clockwise"
|
|
) {
|
|
Task { await workflow.refreshSubscription(subscription.id) }
|
|
}
|
|
.disabled(workflow.refreshingSubscriptionIDs.contains(subscription.id))
|
|
}
|
|
}
|
|
|
|
private func presentation(for node: CatalogTreeNode) -> NodeDetailPresentation {
|
|
let server = node.isGroup ? nil : workflow.serverDetailPresentation(for: nodeID)
|
|
let subscription = subscriptionSummary(for: node)
|
|
var detail = NodeDetailPresentation(
|
|
node: node, server: server, eligibility: eligibility, isActiveTarget: isActiveTarget,
|
|
containsActiveTarget: containsActiveTarget,
|
|
subscription: subscription, locale: locale, timeZone: timeZone)
|
|
if let server, server.plugin.selection != .none {
|
|
if parameters.failure != nil {
|
|
detail.appendPluginParameterFailure()
|
|
} else {
|
|
detail.appendPluginParameters(parameters.options.rawText)
|
|
}
|
|
}
|
|
return detail
|
|
}
|
|
|
|
private func subscriptionSummary(for node: CatalogTreeNode) -> SubscriptionSummary? {
|
|
guard node.isRootSubscriptionGroup else { return nil }
|
|
return workflow.subscriptions.first { $0.groupID == node.id }
|
|
}
|
|
}
|
|
|
|
/// 只观察测试资料并生成属性;布局由通用节点详情负责。
|
|
private struct ObservedConnectivityProperties<Content: View>: View {
|
|
@Environment(\.locale) private var locale
|
|
@Environment(\.timeZone) private var timeZone
|
|
@ObservedObject var module: ServerConnectivity
|
|
let node: CatalogTreeNode
|
|
let serverName: (NodeID) -> String
|
|
@ViewBuilder let content: (NodeDetailPresentation.Properties) -> Content
|
|
|
|
var body: some View {
|
|
if node.isGroup {
|
|
TimelineView(.periodic(from: .now, by: 1)) { context in
|
|
content(
|
|
NodeDetailPresentation.connectivitySummary(
|
|
module.summary(for: node.connectivityServerIDs, at: context.date),
|
|
serverName: serverName, locale: locale, timeZone: timeZone))
|
|
}
|
|
} else {
|
|
content(
|
|
NodeDetailPresentation.connectivityHistory(
|
|
module.history(for: node.id), locale: locale, timeZone: timeZone))
|
|
}
|
|
}
|
|
}
|