Refactor Dashboard traffic cards with line charts
This commit is contained in:
@@ -11,20 +11,23 @@ public struct ClashModeCard: View {
|
|||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
if shouldShowPicker {
|
if shouldShowPicker {
|
||||||
DashboardCardView(title: String(localized: "Mode"), isHalfWidth: false) {
|
DashboardCardView(title: "", isHalfWidth: false) {
|
||||||
Picker("", selection: Binding(get: {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
clashMode
|
DashboardCardHeader(icon: "circle.grid.2x2.fill", title: "Mode")
|
||||||
}, set: { newMode in
|
Picker("", selection: Binding(get: {
|
||||||
clashMode = newMode
|
clashMode
|
||||||
Task {
|
}, set: { newMode in
|
||||||
await setClashMode(newMode)
|
clashMode = newMode
|
||||||
}
|
Task {
|
||||||
})) {
|
await setClashMode(newMode)
|
||||||
ForEach(commandClient.clashModeList, id: \.self) { mode in
|
}
|
||||||
Text(mode)
|
})) {
|
||||||
|
ForEach(commandClient.clashModeList, id: \.self) { mode in
|
||||||
|
Text(mode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
}
|
}
|
||||||
.pickerStyle(.segmented)
|
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
clashMode = commandClient.clashMode
|
clashMode = commandClient.clashMode
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ public struct ConnectionsCard: View {
|
|||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
DashboardCardView(title: String(localized: "Connections"), isHalfWidth: true) {
|
DashboardCardView(title: "", isHalfWidth: true) {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
DashboardCardHeader(icon: "link.circle.fill", title: "Connections")
|
||||||
if ApplicationLibrary.inPreview {
|
if ApplicationLibrary.inPreview {
|
||||||
DashboardCardLine(String(localized: "Inbound"), "34")
|
DashboardCardLine(String(localized: "Inbound"), "34")
|
||||||
DashboardCardLine(String(localized: "Outbound"), "28")
|
DashboardCardLine(String(localized: "Outbound"), "28")
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import SwiftUI
|
|||||||
public enum DashboardCard: String, CaseIterable, Identifiable, Codable, Hashable {
|
public enum DashboardCard: String, CaseIterable, Identifiable, Codable, Hashable {
|
||||||
case status
|
case status
|
||||||
case connections
|
case connections
|
||||||
case traffic
|
case uploadTraffic
|
||||||
case trafficTotal
|
case downloadTraffic
|
||||||
case httpProxy
|
case httpProxy
|
||||||
case clashMode
|
case clashMode
|
||||||
case profile
|
case profile
|
||||||
@@ -18,12 +18,12 @@ public enum DashboardCard: String, CaseIterable, Identifiable, Codable, Hashable
|
|||||||
return "Status"
|
return "Status"
|
||||||
case .connections:
|
case .connections:
|
||||||
return "Connections"
|
return "Connections"
|
||||||
case .traffic:
|
case .uploadTraffic:
|
||||||
return "Traffic"
|
return "Upload"
|
||||||
case .trafficTotal:
|
case .downloadTraffic:
|
||||||
return "Traffic Total"
|
return "Download"
|
||||||
case .httpProxy:
|
case .httpProxy:
|
||||||
return "HTTP Proxy"
|
return "System HTTP Proxy"
|
||||||
case .clashMode:
|
case .clashMode:
|
||||||
return "Clash Mode"
|
return "Clash Mode"
|
||||||
case .profile:
|
case .profile:
|
||||||
@@ -37,22 +37,22 @@ public enum DashboardCard: String, CaseIterable, Identifiable, Codable, Hashable
|
|||||||
return "info.circle.fill"
|
return "info.circle.fill"
|
||||||
case .connections:
|
case .connections:
|
||||||
return "link.circle.fill"
|
return "link.circle.fill"
|
||||||
case .traffic:
|
case .uploadTraffic:
|
||||||
return "arrow.up.arrow.down.circle.fill"
|
return "arrow.up.circle.fill"
|
||||||
case .trafficTotal:
|
case .downloadTraffic:
|
||||||
return "chart.bar.fill"
|
return "arrow.down.circle.fill"
|
||||||
case .httpProxy:
|
case .httpProxy:
|
||||||
return "network"
|
return "network"
|
||||||
case .clashMode:
|
case .clashMode:
|
||||||
return "circle.grid.2x2.fill"
|
return "circle.grid.2x2.fill"
|
||||||
case .profile:
|
case .profile:
|
||||||
return "person.crop.circle.fill"
|
return "doc.text.fill"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var isHalfWidth: Bool {
|
public var isHalfWidth: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .status, .connections, .traffic, .trafficTotal:
|
case .status, .connections, .uploadTraffic, .downloadTraffic:
|
||||||
return true
|
return true
|
||||||
case .httpProxy, .clashMode, .profile:
|
case .httpProxy, .clashMode, .profile:
|
||||||
return false
|
return false
|
||||||
@@ -64,6 +64,6 @@ public enum DashboardCard: String, CaseIterable, Identifiable, Codable, Hashable
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static var defaultOrder: [DashboardCard] {
|
public static var defaultOrder: [DashboardCard] {
|
||||||
[.status, .connections, .traffic, .trafficTotal, .httpProxy, .clashMode, .profile]
|
[.uploadTraffic, .downloadTraffic, .status, .connections, .httpProxy, .clashMode, .profile]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ public final class DashboardCardConfiguration: ObservableObject {
|
|||||||
let saved = await SharedPreferences.enabledDashboardCards.get()
|
let saved = await SharedPreferences.enabledDashboardCards.get()
|
||||||
guard !saved.isEmpty else { return DashboardCard.defaultCards }
|
guard !saved.isEmpty else { return DashboardCard.defaultCards }
|
||||||
|
|
||||||
var cards = saved.compactMap { DashboardCard(rawValue: $0) }
|
var cards = saved.compactMap { migrateCardName($0) }.compactMap { DashboardCard(rawValue: $0) }
|
||||||
if !cards.contains(.profile) {
|
if !cards.contains(.profile) {
|
||||||
cards.append(.profile)
|
cards.append(.profile)
|
||||||
await SharedPreferences.enabledDashboardCards.set(cards.map(\.rawValue))
|
|
||||||
}
|
}
|
||||||
|
await SharedPreferences.enabledDashboardCards.set(cards.map(\.rawValue))
|
||||||
return cards
|
return cards
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,13 +71,25 @@ public final class DashboardCardConfiguration: ObservableObject {
|
|||||||
let saved = await SharedPreferences.dashboardCardOrder.get()
|
let saved = await SharedPreferences.dashboardCardOrder.get()
|
||||||
guard !saved.isEmpty else { return DashboardCard.defaultOrder }
|
guard !saved.isEmpty else { return DashboardCard.defaultOrder }
|
||||||
|
|
||||||
var order = saved.compactMap { DashboardCard(rawValue: $0) }
|
var order = saved.compactMap { migrateCardName($0) }.compactMap { DashboardCard(rawValue: $0) }
|
||||||
let existingSet = Set(order)
|
let existingSet = Set(order)
|
||||||
let newCards = DashboardCard.allCases.filter { !existingSet.contains($0) }
|
let newCards = DashboardCard.allCases.filter { !existingSet.contains($0) }
|
||||||
order.append(contentsOf: newCards)
|
order.append(contentsOf: newCards)
|
||||||
|
await SharedPreferences.dashboardCardOrder.set(order.map(\.rawValue))
|
||||||
return order
|
return order
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func migrateCardName(_ name: String) -> String {
|
||||||
|
switch name {
|
||||||
|
case "traffic":
|
||||||
|
return "uploadTraffic"
|
||||||
|
case "trafficTotal":
|
||||||
|
return "downloadTraffic"
|
||||||
|
default:
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func saveEnabledCards() async {
|
private func saveEnabledCards() async {
|
||||||
await SharedPreferences.enabledDashboardCards.set(enabledCards.map(\.rawValue))
|
await SharedPreferences.enabledDashboardCards.set(enabledCards.map(\.rawValue))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct DashboardCardHeader: View {
|
||||||
|
private let icon: String
|
||||||
|
private let title: LocalizedStringKey
|
||||||
|
|
||||||
|
public init(icon: String, title: LocalizedStringKey) {
|
||||||
|
self.icon = icon
|
||||||
|
self.title = title
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.foregroundStyle(.primary)
|
||||||
|
Text(title)
|
||||||
|
.font(.headline)
|
||||||
|
.fontWeight(.bold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import Libbox
|
||||||
|
import Library
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct DownloadTrafficCard: View {
|
||||||
|
@EnvironmentObject private var commandClient: CommandClient
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
DashboardCardView(title: "", isHalfWidth: true) {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
DashboardCardHeader(icon: "arrow.down.circle.fill", title: "Download")
|
||||||
|
|
||||||
|
if ApplicationLibrary.inPreview {
|
||||||
|
Text("249 MB/s")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Text("5.6 GB")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else if let message = commandClient.status, message.trafficAvailable {
|
||||||
|
Text("\(LibboxFormatBytes(message.downlink))/s")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Text(LibboxFormatBytes(message.downlinkTotal))
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
Text("...")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Text("...")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
TrafficLineChart(
|
||||||
|
data: commandClient.downlinkHistory,
|
||||||
|
lineColor: .primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,7 @@ public struct HTTPProxyCard: View {
|
|||||||
public var body: some View {
|
public var body: some View {
|
||||||
DashboardCardView(title: "", isHalfWidth: false) {
|
DashboardCardView(title: "", isHalfWidth: false) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("System HTTP Proxy")
|
DashboardCardHeader(icon: "network", title: "System HTTP Proxy")
|
||||||
Spacer()
|
Spacer()
|
||||||
Toggle("", isOn: $systemProxyEnabled)
|
Toggle("", isOn: $systemProxyEnabled)
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
|
|||||||
@@ -104,9 +104,7 @@ public struct ProfileCard: View {
|
|||||||
|
|
||||||
private var headerView: some View {
|
private var headerView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Profile")
|
DashboardCardHeader(icon: "doc.text.fill", title: "Profile")
|
||||||
.font(.headline)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ public struct StatusCard: View {
|
|||||||
public init() {}
|
public init() {}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
DashboardCardView(title: String(localized: "Status"), isHalfWidth: true) {
|
DashboardCardView(title: "", isHalfWidth: true) {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
DashboardCardHeader(icon: "info.circle.fill", title: "Status")
|
||||||
if ApplicationLibrary.inPreview {
|
if ApplicationLibrary.inPreview {
|
||||||
DashboardCardLine(String(localized: "Memory"), "6.4 MB")
|
DashboardCardLine(String(localized: "Memory"), "6.4 MB")
|
||||||
DashboardCardLine(String(localized: "Goroutines"), "89")
|
DashboardCardLine(String(localized: "Goroutines"), "89")
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import Libbox
|
|
||||||
import Library
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
public struct TrafficCard: View {
|
|
||||||
@EnvironmentObject private var commandClient: CommandClient
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
DashboardCardView(title: String(localized: "Traffic"), isHalfWidth: true) {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
if ApplicationLibrary.inPreview {
|
|
||||||
DashboardCardLine(String(localized: "Uplink"), "38 B/s")
|
|
||||||
DashboardCardLine(String(localized: "Downlink"), "249 MB/s")
|
|
||||||
} else if let message = commandClient.status, message.trafficAvailable {
|
|
||||||
DashboardCardLine(String(localized: "Uplink"), "\(LibboxFormatBytes(message.uplink))/s")
|
|
||||||
DashboardCardLine(String(localized: "Downlink"), "\(LibboxFormatBytes(message.downlink))/s")
|
|
||||||
} else {
|
|
||||||
DashboardCardLine(String(localized: "Uplink"), "...")
|
|
||||||
DashboardCardLine(String(localized: "Downlink"), "...")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct TrafficLineChart: View {
|
||||||
|
private let data: [CGFloat]
|
||||||
|
private let lineColor: Color
|
||||||
|
private let gridColor: Color
|
||||||
|
private let chartHeight: CGFloat
|
||||||
|
|
||||||
|
public init(
|
||||||
|
data: [CGFloat],
|
||||||
|
lineColor: Color = .primary,
|
||||||
|
gridColor: Color = Color.secondary.opacity(0.3),
|
||||||
|
chartHeight: CGFloat = 60
|
||||||
|
) {
|
||||||
|
self.data = data
|
||||||
|
self.lineColor = lineColor
|
||||||
|
self.gridColor = gridColor
|
||||||
|
self.chartHeight = chartHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Canvas { context, size in
|
||||||
|
let width = size.width
|
||||||
|
let height = size.height
|
||||||
|
let maxValue = max((data.max() ?? 1) * 1.2, 1)
|
||||||
|
let pointCount = data.count
|
||||||
|
|
||||||
|
drawGrid(context: context, width: width, height: height)
|
||||||
|
|
||||||
|
guard pointCount > 1 else { return }
|
||||||
|
|
||||||
|
let spacing = width / CGFloat(pointCount - 1)
|
||||||
|
let points = data.enumerated().map { index, value in
|
||||||
|
let x = CGFloat(index) * spacing
|
||||||
|
let normalizedValue = min(max(value / maxValue, 0), 1)
|
||||||
|
let y = height * (1 - normalizedValue)
|
||||||
|
return CGPoint(x: x, y: y)
|
||||||
|
}
|
||||||
|
|
||||||
|
drawLine(context: context, points: points, height: height)
|
||||||
|
}
|
||||||
|
.frame(height: chartHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func drawGrid(context: GraphicsContext, width: CGFloat, height: CGFloat) {
|
||||||
|
let gridLineCount = 3
|
||||||
|
for i in 0 ... gridLineCount {
|
||||||
|
let y = height * CGFloat(i) / CGFloat(gridLineCount)
|
||||||
|
var path = Path()
|
||||||
|
path.move(to: CGPoint(x: 0, y: y))
|
||||||
|
path.addLine(to: CGPoint(x: width, y: y))
|
||||||
|
context.stroke(
|
||||||
|
path,
|
||||||
|
with: .color(gridColor),
|
||||||
|
style: StrokeStyle(lineWidth: 1, dash: [5, 5])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func drawLine(context: GraphicsContext, points: [CGPoint], height: CGFloat) {
|
||||||
|
guard !points.isEmpty else { return }
|
||||||
|
|
||||||
|
var linePath = Path()
|
||||||
|
linePath.move(to: points[0])
|
||||||
|
for point in points.dropFirst() {
|
||||||
|
linePath.addLine(to: point)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.stroke(
|
||||||
|
linePath,
|
||||||
|
with: .color(lineColor),
|
||||||
|
style: StrokeStyle(lineWidth: 2, lineCap: .round, lineJoin: .round)
|
||||||
|
)
|
||||||
|
|
||||||
|
var fillPath = linePath
|
||||||
|
if let lastPoint = points.last {
|
||||||
|
fillPath.addLine(to: CGPoint(x: lastPoint.x, y: height))
|
||||||
|
fillPath.addLine(to: CGPoint(x: 0, y: height))
|
||||||
|
fillPath.addLine(to: points[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fill(fillPath, with: .color(lineColor.opacity(0.1)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import Libbox
|
|
||||||
import Library
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
public struct TrafficTotalCard: View {
|
|
||||||
@EnvironmentObject private var commandClient: CommandClient
|
|
||||||
|
|
||||||
public init() {}
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
DashboardCardView(title: String(localized: "Traffic Total"), isHalfWidth: true) {
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
if ApplicationLibrary.inPreview {
|
|
||||||
DashboardCardLine(String(localized: "Uplink"), "52 MB")
|
|
||||||
DashboardCardLine(String(localized: "Downlink"), "5.6 GB")
|
|
||||||
} else if let message = commandClient.status, message.trafficAvailable {
|
|
||||||
DashboardCardLine(String(localized: "Uplink"), LibboxFormatBytes(message.uplinkTotal))
|
|
||||||
DashboardCardLine(String(localized: "Downlink"), LibboxFormatBytes(message.downlinkTotal))
|
|
||||||
} else {
|
|
||||||
DashboardCardLine(String(localized: "Uplink"), "...")
|
|
||||||
DashboardCardLine(String(localized: "Downlink"), "...")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import Libbox
|
||||||
|
import Library
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct UploadTrafficCard: View {
|
||||||
|
@EnvironmentObject private var commandClient: CommandClient
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
DashboardCardView(title: "", isHalfWidth: true) {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
DashboardCardHeader(icon: "arrow.up.circle.fill", title: "Upload")
|
||||||
|
|
||||||
|
if ApplicationLibrary.inPreview {
|
||||||
|
Text("38 B/s")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Text("52 MB")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else if let message = commandClient.status, message.trafficAvailable {
|
||||||
|
Text("\(LibboxFormatBytes(message.uplink))/s")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Text(LibboxFormatBytes(message.uplinkTotal))
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
Text("...")
|
||||||
|
.font(.title2)
|
||||||
|
.fontWeight(.medium)
|
||||||
|
Text("...")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
|
||||||
|
TrafficLineChart(
|
||||||
|
data: commandClient.uplinkHistory,
|
||||||
|
lineColor: .primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,7 +84,7 @@ public struct OverviewView: View {
|
|||||||
|
|
||||||
private func shouldShowCard(_ card: DashboardCard) -> Bool {
|
private func shouldShowCard(_ card: DashboardCard) -> Bool {
|
||||||
switch card {
|
switch card {
|
||||||
case .status, .connections, .traffic, .trafficTotal, .clashMode:
|
case .status, .connections, .uploadTraffic, .downloadTraffic, .clashMode:
|
||||||
return ApplicationLibrary.inPreview || profile.status.isConnected
|
return ApplicationLibrary.inPreview || profile.status.isConnected
|
||||||
case .httpProxy:
|
case .httpProxy:
|
||||||
return (ApplicationLibrary.inPreview || profile.status.isConnectedStrict) && systemProxyAvailable
|
return (ApplicationLibrary.inPreview || profile.status.isConnectedStrict) && systemProxyAvailable
|
||||||
@@ -102,11 +102,11 @@ public struct OverviewView: View {
|
|||||||
case .connections:
|
case .connections:
|
||||||
ConnectionsCard()
|
ConnectionsCard()
|
||||||
.environmentObject(environments.commandClient)
|
.environmentObject(environments.commandClient)
|
||||||
case .traffic:
|
case .uploadTraffic:
|
||||||
TrafficCard()
|
UploadTrafficCard()
|
||||||
.environmentObject(environments.commandClient)
|
.environmentObject(environments.commandClient)
|
||||||
case .trafficTotal:
|
case .downloadTraffic:
|
||||||
TrafficTotalCard()
|
DownloadTrafficCard()
|
||||||
.environmentObject(environments.commandClient)
|
.environmentObject(environments.commandClient)
|
||||||
case .httpProxy:
|
case .httpProxy:
|
||||||
HTTPProxyCard(
|
HTTPProxyCard(
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ public extension NavigationPage {
|
|||||||
return "list.bullet.rectangle.portrait.fill"
|
return "list.bullet.rectangle.portrait.fill"
|
||||||
#endif
|
#endif
|
||||||
case .logs:
|
case .logs:
|
||||||
return "doc.text.fill"
|
return "list.bullet.rectangle"
|
||||||
case .settings:
|
case .settings:
|
||||||
return "gear.circle.fill"
|
return "gear.circle.fill"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ public class CommandClient: ObservableObject {
|
|||||||
@Published public var hasAnyConnection: Bool = false
|
@Published public var hasAnyConnection: Bool = false
|
||||||
public var rawConnections: LibboxConnections?
|
public var rawConnections: LibboxConnections?
|
||||||
|
|
||||||
|
@Published public var uplinkHistory: [CGFloat] = Array(repeating: 0, count: 30)
|
||||||
|
@Published public var downlinkHistory: [CGFloat] = Array(repeating: 0, count: 30)
|
||||||
|
|
||||||
// Batch processing for logs
|
// Batch processing for logs
|
||||||
private var pendingLogs: [LogEntry] = []
|
private var pendingLogs: [LogEntry] = []
|
||||||
private var logBatchTimer: DispatchWorkItem?
|
private var logBatchTimer: DispatchWorkItem?
|
||||||
@@ -270,6 +273,12 @@ public class CommandClient: ObservableObject {
|
|||||||
func writeStatus(_ message: LibboxStatusMessage?) {
|
func writeStatus(_ message: LibboxStatusMessage?) {
|
||||||
DispatchQueue.main.async { [self] in
|
DispatchQueue.main.async { [self] in
|
||||||
commandClient.status = message
|
commandClient.status = message
|
||||||
|
if let message = message, message.trafficAvailable {
|
||||||
|
commandClient.uplinkHistory.removeFirst()
|
||||||
|
commandClient.uplinkHistory.append(CGFloat(message.uplink))
|
||||||
|
commandClient.downlinkHistory.removeFirst()
|
||||||
|
commandClient.downlinkHistory.append(CGFloat(message.downlink))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@
|
|||||||
"" : {
|
"" : {
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
|
"..." : {
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
"**If I’ve defended your modern life, please consider sponsoring me.**" : {
|
"**If I’ve defended your modern life, please consider sponsoring me.**" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
@@ -25,6 +28,9 @@
|
|||||||
},
|
},
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
|
"%@/s" : {
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
"%lld" : {
|
"%lld" : {
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
@@ -50,6 +56,18 @@
|
|||||||
"↓ %@/s" : {
|
"↓ %@/s" : {
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
|
"5.6 GB" : {
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"38 B/s" : {
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"52 MB" : {
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
|
"249 MB/s" : {
|
||||||
|
"shouldTranslate" : false
|
||||||
|
},
|
||||||
"About" : {
|
"About" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
@@ -609,6 +627,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Download" : {
|
||||||
|
"localizations" : {
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "下载"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"Edit" : {
|
"Edit" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
@@ -846,6 +874,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"HTTP Proxy" : {
|
"HTTP Proxy" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"zh-Hans" : {
|
"zh-Hans" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1976,6 +2005,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Upload" : {
|
||||||
|
"localizations" : {
|
||||||
|
"zh-Hans" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "上传"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"URL" : {
|
"URL" : {
|
||||||
"shouldTranslate" : false
|
"shouldTranslate" : false
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user