mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-31 23:03:43 +00:00
feat: show subscription usage in detail properties footer
This commit is contained in:
@@ -14,7 +14,15 @@ struct NodeDetailPresentation {
|
||||
let value: String
|
||||
}
|
||||
|
||||
struct Properties: Equatable {
|
||||
enum PropertiesID: Hashable {
|
||||
case subscription
|
||||
case information
|
||||
case connection
|
||||
case pluginParameters
|
||||
}
|
||||
|
||||
struct Properties: Equatable, Identifiable {
|
||||
let id: PropertiesID
|
||||
let title: String
|
||||
let items: [Property]
|
||||
}
|
||||
@@ -121,7 +129,8 @@ struct NodeDetailPresentation {
|
||||
value: subscriptionTimestamp(date, locale: locale, timeZone: timeZone)))
|
||||
}
|
||||
guard !items.isEmpty else { return nil }
|
||||
return Properties(title: String(localized: "订阅", table: "ServerDetails"), items: items)
|
||||
return Properties(
|
||||
id: .subscription, title: String(localized: "订阅", table: "ServerDetails"), items: items)
|
||||
}
|
||||
|
||||
private static func subscriptionTimestamp(
|
||||
@@ -138,7 +147,7 @@ struct NodeDetailPresentation {
|
||||
case .named(let name), .unknown(let name): program = name
|
||||
}
|
||||
return Properties(
|
||||
title: String(localized: "连接信息", table: "ServerDetails"),
|
||||
id: .connection, title: String(localized: "连接信息", table: "ServerDetails"),
|
||||
items: [
|
||||
Property(label: String(localized: "地址", table: "ServerDetails"), value: server.address),
|
||||
Property(
|
||||
@@ -171,7 +180,8 @@ struct NodeDetailPresentation {
|
||||
label: String(localized: "修改时间", table: "ServerDetails"),
|
||||
value: Self.timestamp(node.updatedAt)),
|
||||
]
|
||||
return Properties(title: String(localized: "信息", table: "ServerDetails"), items: information)
|
||||
return Properties(
|
||||
id: .information, title: String(localized: "信息", table: "ServerDetails"), items: information)
|
||||
}
|
||||
|
||||
mutating func appendPluginParameters(_ raw: String) {
|
||||
@@ -189,13 +199,15 @@ struct NodeDetailPresentation {
|
||||
items = [Property(label: String(localized: "原始文本", table: "ServerDetails"), value: raw)]
|
||||
}
|
||||
properties.append(
|
||||
Properties(title: String(localized: "插件参数", table: "ServerDetails"), items: items))
|
||||
Properties(
|
||||
id: .pluginParameters, title: String(localized: "插件参数", table: "ServerDetails"),
|
||||
items: items))
|
||||
}
|
||||
|
||||
mutating func appendPluginParameterFailure() {
|
||||
properties.append(
|
||||
Properties(
|
||||
title: String(localized: "插件参数", table: "ServerDetails"),
|
||||
id: .pluginParameters, title: String(localized: "插件参数", table: "ServerDetails"),
|
||||
items: [
|
||||
Property(
|
||||
label: String(localized: "插件参数", table: "ServerDetails"),
|
||||
|
||||
@@ -75,6 +75,20 @@ struct CatalogNodeDetailView: View {
|
||||
)
|
||||
.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
|
||||
}
|
||||
} supplement: {
|
||||
if let module = workflow.connectivity {
|
||||
ConnectivityDetailView(module: module, node: node, workflow: workflow)
|
||||
|
||||
@@ -2,12 +2,14 @@ import SwiftUI
|
||||
|
||||
/// 只接受展示值与外部动作;不查询节点类型、工作流或凭据。
|
||||
struct NodeDetailView<
|
||||
PrimaryActions: View, SecondaryActions: View, Feedback: View, Supplement: View
|
||||
PrimaryActions: View, SecondaryActions: View, Feedback: View, PropertiesFooter: View,
|
||||
Supplement: View
|
||||
>: View {
|
||||
let detail: NodeDetailPresentation
|
||||
@ViewBuilder let primaryActions: PrimaryActions
|
||||
@ViewBuilder let secondaryActions: SecondaryActions
|
||||
@ViewBuilder let feedback: Feedback
|
||||
let propertiesFooter: (NodeDetailPresentation.Properties) -> PropertiesFooter?
|
||||
@ViewBuilder let supplement: Supplement
|
||||
|
||||
var body: some View {
|
||||
@@ -45,8 +47,9 @@ struct NodeDetailView<
|
||||
feedback
|
||||
}
|
||||
Divider()
|
||||
ForEach(Array(detail.properties.enumerated()), id: \.offset) { _, group in
|
||||
if !group.items.isEmpty {
|
||||
ForEach(detail.properties) { group in
|
||||
let footer = propertiesFooter(group)
|
||||
if !group.items.isEmpty || footer != nil {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(group.title).font(.headline)
|
||||
Grid(alignment: .leading, horizontalSpacing: 16, verticalSpacing: 8) {
|
||||
@@ -62,6 +65,7 @@ struct NodeDetailView<
|
||||
}
|
||||
}
|
||||
}
|
||||
footer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import AppKit
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Testing
|
||||
|
||||
@testable import ShadowsocksX_NG2
|
||||
@@ -75,6 +77,47 @@ struct NodeDetailPresentationTests {
|
||||
#expect(detail.activationMessages == ["插件文件不可执行,请检查文件类型和执行权限。"])
|
||||
}
|
||||
|
||||
@Test(arguments: [false, true])
|
||||
func propertiesFooterRendersWithAndWithoutRows(hasRows: Bool) throws {
|
||||
var detail = NodeDetailPresentation(
|
||||
node: node(isGroup: true), server: nil, eligibility: nil, isActiveTarget: false)
|
||||
detail.properties = [
|
||||
.init(
|
||||
id: .information, title: "Properties", items: hasRows ? [.init(label: "A", value: "B")] : []
|
||||
)
|
||||
]
|
||||
let host = NSHostingView(
|
||||
rootView: NodeDetailView(detail: detail) {
|
||||
EmptyView()
|
||||
} secondaryActions: {
|
||||
EmptyView()
|
||||
} feedback: {
|
||||
EmptyView()
|
||||
} propertiesFooter: { group in
|
||||
group.id == .information ? FooterMarker() : nil
|
||||
} supplement: {
|
||||
EmptyView()
|
||||
})
|
||||
host.frame = NSRect(x: 0, y: 0, width: 480, height: 600)
|
||||
host.layoutSubtreeIfNeeded()
|
||||
#expect(containsFooter(in: host))
|
||||
}
|
||||
|
||||
private func containsFooter(in view: NSView) -> Bool {
|
||||
view.identifier?.rawValue == "properties-footer"
|
||||
|| view.subviews.contains { containsFooter(in: $0) }
|
||||
}
|
||||
|
||||
private struct FooterMarker: NSViewRepresentable {
|
||||
func makeNSView(context: Context) -> NSTextField {
|
||||
let field = NSTextField(labelWithString: "Custom footer")
|
||||
field.identifier = NSUserInterfaceItemIdentifier("properties-footer")
|
||||
return field
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSTextField, context: Context) {}
|
||||
}
|
||||
|
||||
private func node(isGroup: Bool, invalidReasons: [LeafInvalidationReason] = []) -> CatalogTreeNode
|
||||
{
|
||||
CatalogTreeNode(
|
||||
|
||||
Reference in New Issue
Block a user