Refactor Dashboard
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user