Refactor Dashboard

This commit is contained in:
世界
2025-11-26 22:25:51 +08:00
parent 2e2fc223bf
commit f6d14dbb8a
27 changed files with 773 additions and 742 deletions
@@ -0,0 +1,117 @@
import Library
import SwiftUI
@MainActor public struct CardManagementSheet: View {
@StateObject private var configuration = DashboardCardConfiguration()
@Binding private var configurationVersion: Int
public init(configurationVersion: Binding<Int>) {
_configurationVersion = configurationVersion
}
public var body: some View {
NavigationStackCompat {
Group {
if configuration.isLoading {
ProgressView()
} else {
listContent
}
}
.navigationTitle("Dashboard Items")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
.toolbar {
#if os(iOS) || os(tvOS)
ToolbarItem(placement: .topBarTrailing) {
Button("Reset", role: .destructive) {
Task {
await configuration.resetToDefault()
configurationVersion += 1
}
}
}
#else
ToolbarItem(placement: .automatic) {
Button("Reset", role: .destructive) {
Task {
await configuration.resetToDefault()
configurationVersion += 1
}
}
}
#endif
}
}
}
private var listContent: some View {
List {
ForEach(configuration.cardOrder) { card in
CardRow(
card: card,
isEnabled: configuration.isEnabled(card),
onToggle: {
configuration.toggleCard(card)
configurationVersion += 1
}
)
}
.onMove { source, destination in
configuration.moveCard(from: source, to: destination)
configurationVersion += 1
}
}
.applyContentMargins()
}
}
private struct CardRow: View {
let card: DashboardCard
let isEnabled: Bool
let onToggle: () -> Void
var body: some View {
HStack(spacing: 12) {
Image(systemName: "line.3.horizontal")
.foregroundColor(.secondary)
.imageScale(.small)
if isProfileCard {
Label(card.title, systemImage: card.systemImage)
Spacer()
Text("Required")
.font(.footnote)
.foregroundColor(.secondary)
} else {
Toggle(isOn: isToggleEnabled) {
Label(card.title, systemImage: card.systemImage)
}
.opacity(isEnabled ? 1.0 : 0.5)
}
}
}
private var isToggleEnabled: Binding<Bool> {
Binding(
get: { isProfileCard || isEnabled },
set: { _ in if !isProfileCard { onToggle() } }
)
}
private var isProfileCard: Bool {
card == .profile
}
}
private extension View {
@ViewBuilder
func applyContentMargins() -> some View {
if #available(iOS 17.0, macOS 14.0, tvOS 17.0, *) {
contentMargins(.top, 0, for: .scrollContent)
} else {
self
}
}
}
@@ -11,37 +11,16 @@ public struct ConnectionsCard: View {
DashboardCardView(title: "Connections", isHalfWidth: true) {
VStack(alignment: .leading, spacing: 8) {
if ApplicationLibrary.inPreview {
CardLine(String(localized: "Inbound"), "34")
CardLine(String(localized: "Outbound"), "28")
DashboardCardLine(String(localized: "Inbound"), "34")
DashboardCardLine(String(localized: "Outbound"), "28")
} else if let message = commandClient.status {
CardLine(String(localized: "Inbound"), "\(message.connectionsIn)")
CardLine(String(localized: "Outbound"), "\(message.connectionsOut)")
DashboardCardLine(String(localized: "Inbound"), "\(message.connectionsIn)")
DashboardCardLine(String(localized: "Outbound"), "\(message.connectionsOut)")
} else {
CardLine(String(localized: "Inbound"), "...")
CardLine(String(localized: "Outbound"), "...")
DashboardCardLine(String(localized: "Inbound"), "...")
DashboardCardLine(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,107 @@
import Foundation
import Library
@MainActor
public final class DashboardCardConfiguration: ObservableObject {
@Published public private(set) var enabledCards: [DashboardCard] = []
@Published public private(set) var cardOrder: [DashboardCard] = []
@Published public private(set) var isLoading = true
public init() {
Task {
await reload()
}
}
public func reload() async {
isLoading = true
enabledCards = await loadEnabledCards()
cardOrder = await loadCardOrder()
isLoading = false
}
public func isEnabled(_ card: DashboardCard) -> Bool {
enabledCards.contains(card)
}
public func toggleCard(_ card: DashboardCard) {
guard card != .profile else { return }
// Update state synchronously so UI reflects change immediately
if enabledCards.contains(card) {
enabledCards.removeAll { $0 == card }
} else {
enabledCards = insertInOrder(card, into: enabledCards)
}
// Save asynchronously in background
Task {
await saveEnabledCards()
}
}
public func moveCard(from source: IndexSet, to destination: Int) {
cardOrder.move(fromOffsets: source, toOffset: destination)
// Save asynchronously in background
Task {
await saveCardOrder()
}
}
public func resetToDefault() async {
await SharedPreferences.enabledDashboardCards.set([])
await SharedPreferences.dashboardCardOrder.set([])
await reload()
}
public var orderedEnabledCards: [DashboardCard] {
cardOrder.filter { enabledCards.contains($0) }
}
private func loadEnabledCards() async -> [DashboardCard] {
let saved = await SharedPreferences.enabledDashboardCards.get()
guard !saved.isEmpty else { return DashboardCard.defaultCards }
var cards = saved.compactMap { DashboardCard(rawValue: $0) }
if !cards.contains(.profile) {
cards.append(.profile)
await SharedPreferences.enabledDashboardCards.set(cards.map(\.rawValue))
}
return cards
}
private func loadCardOrder() async -> [DashboardCard] {
let saved = await SharedPreferences.dashboardCardOrder.get()
guard !saved.isEmpty else { return DashboardCard.defaultOrder }
var order = saved.compactMap { DashboardCard(rawValue: $0) }
let existingSet = Set(order)
let newCards = DashboardCard.allCases.filter { !existingSet.contains($0) }
order.append(contentsOf: newCards)
return order
}
private func saveEnabledCards() async {
await SharedPreferences.enabledDashboardCards.set(enabledCards.map(\.rawValue))
}
private func saveCardOrder() async {
await SharedPreferences.dashboardCardOrder.set(cardOrder.map(\.rawValue))
}
private func insertInOrder(_ card: DashboardCard, into cards: [DashboardCard]) -> [DashboardCard] {
guard let cardIndex = cardOrder.firstIndex(of: card) else {
return cards + [card]
}
let insertIndex = cards.filter { enabledCard in
guard let enabledIndex = cardOrder.firstIndex(of: enabledCard) else { return false }
return enabledIndex < cardIndex
}.count
var result = cards
result.insert(card, at: insertIndex)
return result
}
}
@@ -0,0 +1,22 @@
import SwiftUI
public struct DashboardCardLine: View {
private let label: String
private let value: String
public init(_ label: String, _ value: String) {
self.label = label
self.value = value
}
public var body: some View {
HStack {
Text(label)
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
Text(value)
.font(.subheadline)
}
}
}
@@ -11,37 +11,16 @@ public struct StatusCard: 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")
DashboardCardLine(String(localized: "Memory"), "6.4 MB")
DashboardCardLine(String(localized: "Goroutines"), "89")
} else if let message = commandClient.status {
CardLine(String(localized: "Memory"), LibboxFormatMemoryBytes(message.memory))
CardLine(String(localized: "Goroutines"), "\(message.goroutines)")
DashboardCardLine(String(localized: "Memory"), LibboxFormatMemoryBytes(message.memory))
DashboardCardLine(String(localized: "Goroutines"), "\(message.goroutines)")
} else {
CardLine(String(localized: "Memory"), "...")
CardLine(String(localized: "Goroutines"), "...")
DashboardCardLine(String(localized: "Memory"), "...")
DashboardCardLine(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)
}
}
}
@@ -11,37 +11,16 @@ public struct TrafficCard: 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")
DashboardCardLine(String(localized: "Uplink"), "38 B/s")
DashboardCardLine(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")
DashboardCardLine(String(localized: "Uplink"), "\(LibboxFormatBytes(message.uplink))/s")
DashboardCardLine(String(localized: "Downlink"), "\(LibboxFormatBytes(message.downlink))/s")
} else {
CardLine(String(localized: "Uplink"), "...")
CardLine(String(localized: "Downlink"), "...")
DashboardCardLine(String(localized: "Uplink"), "...")
DashboardCardLine(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)
}
}
}
@@ -11,37 +11,16 @@ public struct TrafficTotalCard: 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")
DashboardCardLine(String(localized: "Uplink"), "52 MB")
DashboardCardLine(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))
DashboardCardLine(String(localized: "Uplink"), LibboxFormatBytes(message.uplinkTotal))
DashboardCardLine(String(localized: "Downlink"), LibboxFormatBytes(message.downlinkTotal))
} else {
CardLine(String(localized: "Uplink"), "...")
CardLine(String(localized: "Downlink"), "...")
DashboardCardLine(String(localized: "Uplink"), "...")
DashboardCardLine(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)
}
}
}