Redesign dashboard
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
public struct ClashModeCard: View {
|
||||
@EnvironmentObject private var commandClient: CommandClient
|
||||
@State private var clashMode: String = ""
|
||||
@State private var alert: Alert?
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
if shouldShowPicker {
|
||||
DashboardCardView(title: "Mode", isHalfWidth: false) {
|
||||
Picker("", selection: Binding(get: {
|
||||
clashMode
|
||||
}, set: { newMode in
|
||||
clashMode = newMode
|
||||
Task {
|
||||
await setClashMode(newMode)
|
||||
}
|
||||
})) {
|
||||
ForEach(commandClient.clashModeList, id: \.self) { mode in
|
||||
Text(mode)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
.onAppear {
|
||||
clashMode = commandClient.clashMode
|
||||
}
|
||||
.onChangeCompat(of: commandClient.clashMode) { newValue in
|
||||
clashMode = newValue
|
||||
}
|
||||
.alertBinding($alert)
|
||||
}
|
||||
}
|
||||
|
||||
private var shouldShowPicker: Bool {
|
||||
commandClient.clashModeList.count > 1
|
||||
}
|
||||
|
||||
private nonisolated func setClashMode(_ newMode: String) async {
|
||||
do {
|
||||
try LibboxNewStandaloneCommandClient()!.setClashMode(newMode)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
public struct ConnectionsCard: View {
|
||||
@EnvironmentObject private var commandClient: CommandClient
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
DashboardCardView(title: "Connections", isHalfWidth: true) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if ApplicationLibrary.inPreview {
|
||||
CardLine(String(localized: "Inbound"), "34")
|
||||
CardLine(String(localized: "Outbound"), "28")
|
||||
} else if let message = commandClient.status {
|
||||
CardLine(String(localized: "Inbound"), "\(message.connectionsIn)")
|
||||
CardLine(String(localized: "Outbound"), "\(message.connectionsOut)")
|
||||
} else {
|
||||
CardLine(String(localized: "Inbound"), "...")
|
||||
CardLine(String(localized: "Outbound"), "...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CardLine: View {
|
||||
private let name: String
|
||||
private let value: String
|
||||
|
||||
init(_ name: String, _ value: String) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(name)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public enum DashboardCard: String, CaseIterable, Identifiable, Codable, Hashable {
|
||||
case status
|
||||
case connections
|
||||
case traffic
|
||||
case trafficTotal
|
||||
case httpProxy
|
||||
case clashMode
|
||||
case profile
|
||||
|
||||
public var id: String { rawValue }
|
||||
|
||||
public var title: LocalizedStringKey {
|
||||
switch self {
|
||||
case .status:
|
||||
return "Status"
|
||||
case .connections:
|
||||
return "Connections"
|
||||
case .traffic:
|
||||
return "Traffic"
|
||||
case .trafficTotal:
|
||||
return "Traffic Total"
|
||||
case .httpProxy:
|
||||
return "HTTP Proxy"
|
||||
case .clashMode:
|
||||
return "Clash Mode"
|
||||
case .profile:
|
||||
return "Profile"
|
||||
}
|
||||
}
|
||||
|
||||
public var systemImage: String {
|
||||
switch self {
|
||||
case .status:
|
||||
return "info.circle.fill"
|
||||
case .connections:
|
||||
return "link.circle.fill"
|
||||
case .traffic:
|
||||
return "arrow.up.arrow.down.circle.fill"
|
||||
case .trafficTotal:
|
||||
return "chart.bar.fill"
|
||||
case .httpProxy:
|
||||
return "network"
|
||||
case .clashMode:
|
||||
return "circle.grid.2x2.fill"
|
||||
case .profile:
|
||||
return "person.crop.circle.fill"
|
||||
}
|
||||
}
|
||||
|
||||
public var isHalfWidth: Bool {
|
||||
switch self {
|
||||
case .status, .connections, .traffic, .trafficTotal:
|
||||
return true
|
||||
case .httpProxy, .clashMode, .profile:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public static var defaultCards: [DashboardCard] {
|
||||
allCases
|
||||
}
|
||||
|
||||
public static var defaultOrder: [DashboardCard] {
|
||||
[.status, .connections, .traffic, .trafficTotal, .httpProxy, .clashMode, .profile]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct DashboardCardView<Content: View>: View {
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
private let title: String
|
||||
private let isHalfWidth: Bool
|
||||
@ViewBuilder private let content: () -> Content
|
||||
|
||||
public init(title: String, isHalfWidth: Bool = false, @ViewBuilder content: @escaping () -> Content) {
|
||||
self.title = title
|
||||
self.isHalfWidth = isHalfWidth
|
||||
self.content = content
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading, spacing: title.isEmpty ? 0 : 12) {
|
||||
if !title.isEmpty {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
}
|
||||
content()
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
#if os(tvOS)
|
||||
.padding(EdgeInsets(top: 20, leading: 26, bottom: 20, trailing: 26))
|
||||
#else
|
||||
.padding(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16))
|
||||
#endif
|
||||
.modifier(CardStyleModifier(colorScheme: colorScheme))
|
||||
}
|
||||
}
|
||||
|
||||
private struct CardStyleModifier: ViewModifier {
|
||||
let colorScheme: ColorScheme
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 26.0, macOS 26.0, tvOS 26.0, *) {
|
||||
content
|
||||
.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 16))
|
||||
} else {
|
||||
content
|
||||
.background(backgroundColor)
|
||||
.cornerRadius(16)
|
||||
}
|
||||
}
|
||||
|
||||
private var backgroundColor: Color {
|
||||
#if os(iOS)
|
||||
return Color(uiColor: .secondarySystemGroupedBackground)
|
||||
#elseif os(macOS)
|
||||
return Color(nsColor: .textBackgroundColor)
|
||||
#elseif os(tvOS)
|
||||
switch colorScheme {
|
||||
case .dark:
|
||||
return Color(uiColor: .black)
|
||||
default:
|
||||
return Color(uiColor: .white)
|
||||
}
|
||||
#else
|
||||
return Color.clear
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
public struct HTTPProxyCard: View {
|
||||
@EnvironmentObject private var profile: ExtensionProfile
|
||||
@Binding private var systemProxyAvailable: Bool
|
||||
@Binding private var systemProxyEnabled: Bool
|
||||
private let onToggle: (Bool) async -> Void
|
||||
|
||||
public init(
|
||||
systemProxyAvailable: Binding<Bool>,
|
||||
systemProxyEnabled: Binding<Bool>,
|
||||
onToggle: @escaping (Bool) async -> Void
|
||||
) {
|
||||
_systemProxyAvailable = systemProxyAvailable
|
||||
_systemProxyEnabled = systemProxyEnabled
|
||||
self.onToggle = onToggle
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
DashboardCardView(title: "", isHalfWidth: false) {
|
||||
Toggle("System HTTP Proxy", isOn: $systemProxyEnabled)
|
||||
.onChangeCompat(of: systemProxyEnabled) { newValue in
|
||||
Task {
|
||||
await onToggle(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
public struct ProfileCard: View {
|
||||
@Binding private var profileList: [ProfilePreview]
|
||||
@Binding private var selectedProfileID: Int64
|
||||
|
||||
public init(
|
||||
profileList: Binding<[ProfilePreview]>,
|
||||
selectedProfileID: Binding<Int64>
|
||||
) {
|
||||
_profileList = profileList
|
||||
_selectedProfileID = selectedProfileID
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
DashboardCardView(title: "Profile", isHalfWidth: false) {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
#if os(iOS) || os(tvOS)
|
||||
Picker("", selection: $selectedProfileID) {
|
||||
ForEach(profileList, id: \.id) { profile in
|
||||
Text(profile.name).tag(profile.id)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.labelsHidden()
|
||||
#elseif os(macOS)
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ForEach(profileList, id: \.id) { profile in
|
||||
HStack {
|
||||
Button {
|
||||
selectedProfileID = profile.id
|
||||
} label: {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: selectedProfileID == profile.id ? "circle.fill" : "circle")
|
||||
.font(.system(size: 12))
|
||||
Text(profile.name)
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(selectedProfileID == profile.id ? .accentColor : .primary)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
public struct StatusCard: View {
|
||||
@EnvironmentObject private var commandClient: CommandClient
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
DashboardCardView(title: "Status", isHalfWidth: true) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if ApplicationLibrary.inPreview {
|
||||
CardLine(String(localized: "Memory"), "6.4 MB")
|
||||
CardLine(String(localized: "Goroutines"), "89")
|
||||
} else if let message = commandClient.status {
|
||||
CardLine(String(localized: "Memory"), LibboxFormatMemoryBytes(message.memory))
|
||||
CardLine(String(localized: "Goroutines"), "\(message.goroutines)")
|
||||
} else {
|
||||
CardLine(String(localized: "Memory"), "...")
|
||||
CardLine(String(localized: "Goroutines"), "...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CardLine: View {
|
||||
private let name: String
|
||||
private let value: String
|
||||
|
||||
init(_ name: String, _ value: String) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(name)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
public struct TrafficCard: View {
|
||||
@EnvironmentObject private var commandClient: CommandClient
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
DashboardCardView(title: "Traffic", isHalfWidth: true) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if ApplicationLibrary.inPreview {
|
||||
CardLine(String(localized: "Uplink"), "38 B/s")
|
||||
CardLine(String(localized: "Downlink"), "249 MB/s")
|
||||
} else if let message = commandClient.status, message.trafficAvailable {
|
||||
CardLine(String(localized: "Uplink"), "\(LibboxFormatBytes(message.uplink))/s")
|
||||
CardLine(String(localized: "Downlink"), "\(LibboxFormatBytes(message.downlink))/s")
|
||||
} else {
|
||||
CardLine(String(localized: "Uplink"), "...")
|
||||
CardLine(String(localized: "Downlink"), "...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CardLine: View {
|
||||
private let name: String
|
||||
private let value: String
|
||||
|
||||
init(_ name: String, _ value: String) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(name)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
public struct TrafficTotalCard: View {
|
||||
@EnvironmentObject private var commandClient: CommandClient
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
DashboardCardView(title: "Traffic Total", isHalfWidth: true) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
if ApplicationLibrary.inPreview {
|
||||
CardLine(String(localized: "Uplink"), "52 MB")
|
||||
CardLine(String(localized: "Downlink"), "5.6 GB")
|
||||
} else if let message = commandClient.status, message.trafficAvailable {
|
||||
CardLine(String(localized: "Uplink"), LibboxFormatBytes(message.uplinkTotal))
|
||||
CardLine(String(localized: "Downlink"), LibboxFormatBytes(message.downlinkTotal))
|
||||
} else {
|
||||
CardLine(String(localized: "Uplink"), "...")
|
||||
CardLine(String(localized: "Downlink"), "...")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct CardLine: View {
|
||||
private let name: String
|
||||
private let value: String
|
||||
|
||||
init(_ name: String, _ value: String) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(name)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user