mirror of
https://github.com/yggdrasil-network/yggdrasil-ios.git
synced 2025-04-28 22:25:10 +03:00
271 lines
11 KiB
Swift
271 lines
11 KiB
Swift
//
|
|
// ContentView.swift
|
|
// YggdrasilSwiftUI
|
|
//
|
|
// Created by Neil on 10/02/2023.
|
|
//
|
|
|
|
import SwiftUI
|
|
import CoreImage.CIFilterBuiltins
|
|
|
|
#if os(iOS)
|
|
typealias MyListStyle = DefaultListStyle
|
|
#else
|
|
typealias MyListStyle = SidebarListStyle
|
|
#endif
|
|
|
|
struct StatusView: View {
|
|
@ObservedObject private var appDelegate = Application.appDelegate
|
|
|
|
@State private var statusBadgeColor: SwiftUI.Color = .gray
|
|
@State private var statusBadgeText: String = "Not enabled"
|
|
@State private var showingPublicKeyPopover = false
|
|
|
|
let context = CIContext()
|
|
let filter = CIFilter.qrCodeGenerator()
|
|
|
|
private func getStatusBadgeColor() -> SwiftUI.Color {
|
|
if appDelegate.yggdrasilConnected {
|
|
return .green
|
|
} else if appDelegate.yggdrasilEnabled {
|
|
return .yellow
|
|
} else {
|
|
return .gray
|
|
}
|
|
}
|
|
|
|
private func getStatusBadgeText() -> String {
|
|
if !appDelegate.yggdrasilSupported {
|
|
return "Not supported on this device"
|
|
} else if !appDelegate.yggdrasilEnabled {
|
|
return "Not enabled"
|
|
} else if !appDelegate.yggdrasilConnected {
|
|
return "No peers connected"
|
|
} else {
|
|
return "Connected to \(appDelegate.yggdrasilPeers.filter { $0.up }.count) peer(s)"
|
|
}
|
|
}
|
|
|
|
func formatBytes(bytes: Double) -> String {
|
|
guard bytes > 0 else {
|
|
return "N/A"
|
|
}
|
|
|
|
// Adapted from http://stackoverflow.com/a/18650828
|
|
let suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
|
|
let k: Double = 1024
|
|
let i = floor(log(bytes) / log(k))
|
|
|
|
// Format number with thousands separator and everything below 1 GB with no decimal places.
|
|
let numberFormatter = NumberFormatter()
|
|
numberFormatter.maximumFractionDigits = i < 3 ? 0 : 1
|
|
numberFormatter.numberStyle = .decimal
|
|
|
|
let numberString = numberFormatter.string(from: NSNumber(value: bytes / pow(k, i))) ?? "Unknown"
|
|
let suffix = suffixes[Int(i)]
|
|
return "\(numberString) \(suffix)"
|
|
}
|
|
|
|
#if os(iOS)
|
|
func generateQRCode(from string: String) -> UIImage {
|
|
filter.message = Data(string.utf8)
|
|
|
|
if let outputImage = filter.outputImage {
|
|
if let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
|
|
return UIImage(cgImage: cgImage)
|
|
}
|
|
}
|
|
|
|
return UIImage(systemName: "xmark.circle") ?? UIImage()
|
|
}
|
|
#endif
|
|
|
|
var body: some View {
|
|
Form {
|
|
Section(content: {
|
|
VStack(alignment: .leading) {
|
|
Toggle("Enable Yggdrasil", isOn: $appDelegate.yggdrasilEnabled)
|
|
.disabled(!appDelegate.yggdrasilSupported)
|
|
.padding(.bottom, 2)
|
|
HStack {
|
|
Image(systemName: "circlebadge.fill")
|
|
.foregroundColor(statusBadgeColor)
|
|
.onAppear(perform: {
|
|
statusBadgeColor = getStatusBadgeColor()
|
|
})
|
|
.onChange(of: appDelegate.yggdrasilSupported) { newValue in
|
|
statusBadgeColor = getStatusBadgeColor()
|
|
}
|
|
.onChange(of: appDelegate.yggdrasilEnabled) { newValue in
|
|
statusBadgeColor = getStatusBadgeColor()
|
|
}
|
|
.onChange(of: appDelegate.yggdrasilConnected) { newValue in
|
|
statusBadgeColor = getStatusBadgeColor()
|
|
}
|
|
.onChange(of: appDelegate.yggdrasilPeers.count) { newValue in
|
|
statusBadgeColor = getStatusBadgeColor()
|
|
}
|
|
Text(statusBadgeText)
|
|
.foregroundColor(.gray)
|
|
.font(.system(size: 11))
|
|
.onAppear(perform: {
|
|
statusBadgeText = getStatusBadgeText()
|
|
})
|
|
.onChange(of: appDelegate.yggdrasilSupported) { newValue in
|
|
statusBadgeText = getStatusBadgeText()
|
|
}
|
|
.onChange(of: appDelegate.yggdrasilEnabled) { newValue in
|
|
statusBadgeText = getStatusBadgeText()
|
|
}
|
|
.onChange(of: appDelegate.yggdrasilConnected) { newValue in
|
|
statusBadgeText = getStatusBadgeText()
|
|
}
|
|
.onChange(of: appDelegate.yggdrasilPeers.count) { newValue in
|
|
statusBadgeText = getStatusBadgeText()
|
|
}
|
|
}
|
|
}
|
|
HStack {
|
|
Text("Version")
|
|
Spacer()
|
|
Text(appDelegate.yggdrasilVersion())
|
|
.foregroundColor(Color.gray)
|
|
}
|
|
}, header: {
|
|
Text("Status")
|
|
})
|
|
|
|
Section(content: {
|
|
HStack {
|
|
Text("IP")
|
|
Spacer()
|
|
Text(appDelegate.yggdrasilIP)
|
|
.foregroundColor(Color.gray)
|
|
.truncationMode(.head)
|
|
.lineLimit(1)
|
|
.textSelection(.enabled)
|
|
}
|
|
HStack {
|
|
Text("Subnet")
|
|
Spacer()
|
|
Text(appDelegate.yggdrasilSubnet)
|
|
.foregroundColor(Color.gray)
|
|
.truncationMode(.head)
|
|
.lineLimit(1)
|
|
.textSelection(.enabled)
|
|
}
|
|
HStack {
|
|
Text("Public Key")
|
|
Spacer()
|
|
Text(appDelegate.yggdrasilPublicKey)
|
|
.foregroundColor(Color.gray)
|
|
.font(.system(size: 13, design: .monospaced))
|
|
.truncationMode(.tail)
|
|
.lineLimit(1)
|
|
.textSelection(.enabled)
|
|
#if os(iOS)
|
|
if appDelegate.yggdrasilPublicKey != "N/A" {
|
|
Button("QR Code", systemImage: "qrcode", action: {
|
|
showingPublicKeyPopover = true
|
|
})
|
|
.labelStyle(.iconOnly)
|
|
.popover(isPresented: $showingPublicKeyPopover) {
|
|
Text("Public Key")
|
|
.font(.headline)
|
|
.padding()
|
|
Image(uiImage: generateQRCode(from: "\(appDelegate.yggdrasilPublicKey)"))
|
|
.interpolation(.none)
|
|
.resizable()
|
|
.scaledToFit()
|
|
.frame(width: 200, height: 200)
|
|
.padding()
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}, header: {
|
|
Text("Details")
|
|
})
|
|
|
|
Section(content: {
|
|
if self.appDelegate.yggdrasilPeers.count == 0 {
|
|
Text("No peers are connected")
|
|
.foregroundStyle(.tertiary)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
} else {
|
|
List(self.appDelegate.yggdrasilPeers.sorted(by: { a, b in
|
|
if a.up && !b.up {
|
|
return true
|
|
}
|
|
if !a.up && b.up {
|
|
return false
|
|
}
|
|
return a.remote < b.remote
|
|
}), id: \.remote) { peer in
|
|
VStack {
|
|
HStack {
|
|
Text(peer.remote)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.truncationMode(.tail)
|
|
.lineLimit(1)
|
|
.textSelection(.enabled)
|
|
.padding(.bottom, 2)
|
|
}
|
|
HStack {
|
|
Image(systemName: "circlebadge.fill")
|
|
.foregroundColor(peer.getStatusBadgeColor())
|
|
.onChange(of: peer.up) { newValue in
|
|
statusBadgeColor = peer.getStatusBadgeColor()
|
|
}
|
|
Text(peer.up ? "Connected" : "Not connected")
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.foregroundColor(Color.gray)
|
|
.font(.system(size: 11))
|
|
.truncationMode(.tail)
|
|
.lineLimit(1)
|
|
.textSelection(.enabled)
|
|
if peer.up {
|
|
Spacer()
|
|
if let uptime = peer.uptime {
|
|
Label(Duration(secondsComponent: uptime/1000000000, attosecondsComponent: 0).formatted(), systemImage: "clock")
|
|
.font(.system(size: 11))
|
|
.labelStyle(.titleAndIcon)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
if let rxBytes = peer.rxBytes {
|
|
Label(formatBytes(bytes: rxBytes), systemImage: "arrowshape.down")
|
|
.font(.system(size: 11))
|
|
.labelStyle(.titleAndIcon)
|
|
.foregroundStyle(.teal)
|
|
}
|
|
if let txBytes = peer.txBytes {
|
|
Label(formatBytes(bytes: txBytes), systemImage: "arrowshape.up")
|
|
.font(.system(size: 11))
|
|
.labelStyle(.titleAndIcon)
|
|
.foregroundStyle(.purple)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.all, 2)
|
|
.padding(.top, 4)
|
|
.padding(.bottom, 4)
|
|
}
|
|
}
|
|
}, header: {
|
|
Text("Peers")
|
|
})
|
|
}
|
|
.formStyle(.grouped)
|
|
.navigationTitle("Yggdrasil")
|
|
#if os(iOS)
|
|
.navigationBarTitleDisplayMode(.large)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
struct StatusView_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
StatusView()
|
|
}
|
|
}
|