mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-10-26 19:53:44 +00:00
feat(servers): show subscription details in server view
This commit is contained in:
@@ -15,13 +15,21 @@ struct NodeDetailPresentation {
|
||||
|
||||
let title: String
|
||||
var subtitle: String
|
||||
let secondarySubtitle: String?
|
||||
var properties: [Properties]
|
||||
|
||||
init(
|
||||
node: CatalogTreeNode, server: ServerDetailPresentation?,
|
||||
eligibility: ActivationEligibility?, isActiveTarget: Bool
|
||||
eligibility: ActivationEligibility?, isActiveTarget: Bool,
|
||||
subscription: SubscriptionSummary? = nil,
|
||||
locale: Locale = .current, timeZone: TimeZone = .current
|
||||
) {
|
||||
title = node.name
|
||||
let isRootSubscriptionGroup = Self.isRootSubscriptionGroup(node)
|
||||
secondarySubtitle =
|
||||
isRootSubscriptionGroup
|
||||
? String(localized: "配置由远端管理,本地不能修改。", table: "ServerDetails")
|
||||
: nil
|
||||
var descriptions = [
|
||||
node.isGroup
|
||||
? (node.isManual
|
||||
@@ -57,10 +65,67 @@ struct NodeDetailPresentation {
|
||||
}
|
||||
}
|
||||
subtitle = descriptions.joined(separator: " · ")
|
||||
properties = [Self.information(for: node, eligibility: eligibility)]
|
||||
properties = []
|
||||
if isRootSubscriptionGroup,
|
||||
let subscriptionProperties = Self.subscriptionInformation(
|
||||
summary: subscription, locale: locale, timeZone: timeZone)
|
||||
{
|
||||
properties.append(subscriptionProperties)
|
||||
}
|
||||
properties.append(Self.information(for: node, eligibility: eligibility))
|
||||
if let server { properties.append(Self.connection(for: server)) }
|
||||
}
|
||||
|
||||
private static func isRootSubscriptionGroup(_ node: CatalogTreeNode) -> Bool {
|
||||
node.isGroup && node.source == .subscription && node.parentID == nil
|
||||
}
|
||||
|
||||
private static func subscriptionInformation(
|
||||
summary: SubscriptionSummary?, locale: Locale, timeZone: TimeZone
|
||||
) -> Properties? {
|
||||
guard let summary else { return nil }
|
||||
var items: [Property] = []
|
||||
if let date = summary.lastSucceededAt {
|
||||
items.append(
|
||||
Property(
|
||||
label: String(localized: "最后更新时间", table: "ServerDetails"),
|
||||
value: subscriptionTimestamp(date, locale: locale, timeZone: timeZone)))
|
||||
}
|
||||
if let used = summary.information?.bytesUsed {
|
||||
items.append(
|
||||
Property(
|
||||
label: String(localized: "已用流量", table: "ServerDetails"),
|
||||
value: SubscriptionInformation.byteCountText(used, locale: locale)))
|
||||
}
|
||||
if let remaining = summary.information?.bytesRemaining {
|
||||
items.append(
|
||||
Property(
|
||||
label: String(localized: "剩余流量", table: "ServerDetails"),
|
||||
value: SubscriptionInformation.byteCountText(remaining, locale: locale)))
|
||||
}
|
||||
if let date = summary.information?.expiresAt {
|
||||
items.append(
|
||||
Property(
|
||||
label: String(localized: "到期时间", table: "ServerDetails"),
|
||||
value: subscriptionTimestamp(date, locale: locale, timeZone: timeZone)))
|
||||
}
|
||||
if let date = summary.information?.trafficResetAt {
|
||||
items.append(
|
||||
Property(
|
||||
label: String(localized: "重置时间", table: "ServerDetails"),
|
||||
value: subscriptionTimestamp(date, locale: locale, timeZone: timeZone)))
|
||||
}
|
||||
guard !items.isEmpty else { return nil }
|
||||
return Properties(title: String(localized: "订阅", table: "ServerDetails"), items: items)
|
||||
}
|
||||
|
||||
private static func subscriptionTimestamp(
|
||||
_ date: Date, locale: Locale, timeZone: TimeZone
|
||||
) -> String {
|
||||
date.formatted(
|
||||
Date.FormatStyle(date: .abbreviated, time: .shortened, locale: locale, timeZone: timeZone))
|
||||
}
|
||||
|
||||
private static func connection(for server: ServerDetailPresentation) -> Properties {
|
||||
let program: String
|
||||
switch server.plugin.selection {
|
||||
|
||||
@@ -341,6 +341,73 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"配置由远端管理,本地不能修改。": {
|
||||
"extractionState": "manual",
|
||||
"comment": "Second subtitle line for the root group of a subscription.",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Managed remotely; cannot be modified locally."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"最后更新时间": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Last Update"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"已用流量": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Used Data"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"剩余流量": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Remaining Data"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"到期时间": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Expiration"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"重置时间": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Traffic Reset"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"version": "1.0"
|
||||
|
||||
@@ -2,6 +2,8 @@ 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
|
||||
@@ -67,8 +69,10 @@ struct CatalogNodeDetailView: View {
|
||||
|
||||
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)
|
||||
node: node, server: server, eligibility: eligibility, isActiveTarget: isActiveTarget,
|
||||
subscription: subscription, locale: locale, timeZone: timeZone)
|
||||
if let server, server.plugin.selection != .none {
|
||||
if parameters.failure != nil {
|
||||
detail.appendPluginParameterFailure()
|
||||
@@ -89,4 +93,9 @@ struct CatalogNodeDetailView: View {
|
||||
}
|
||||
return detail
|
||||
}
|
||||
|
||||
private func subscriptionSummary(for node: CatalogTreeNode) -> SubscriptionSummary? {
|
||||
guard node.isGroup, node.source == .subscription, node.parentID == nil else { return nil }
|
||||
return workflow.subscriptions.first { $0.groupID == node.id }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,18 @@ struct NodeDetailView<Actions: View, Supplement: View>: View {
|
||||
Text(detail.title)
|
||||
.font(.title2.weight(.semibold))
|
||||
.textSelection(.enabled)
|
||||
Text(detail.subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(detail.subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
if let secondarySubtitle = detail.secondarySubtitle {
|
||||
Text(secondarySubtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
}
|
||||
HStack(spacing: 16) { actions }
|
||||
.buttonStyle(.link)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user