mirror of
https://github.com/netbirdio/ios-client.git
synced 2026-09-23 09:28:33 -07:00
201 lines
6.7 KiB
Swift
201 lines
6.7 KiB
Swift
//
|
|
// RouteCard.swift
|
|
// NetBirdiOS
|
|
//
|
|
// Created by Pascal Fischer on 29.06.24.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct RouteCard: View {
|
|
@ObservedObject var route: RoutesSelectionInfo
|
|
@Binding var selectedRouteId: UUID?
|
|
@State var orientationTop: Bool
|
|
@ObservedObject var peerViewModel: PeerViewModel
|
|
@ObservedObject var routeViewModel: RoutesViewModel
|
|
|
|
@State private var tooltipSize: CGSize = .zero
|
|
@GestureState private var isPressing: Bool = false
|
|
|
|
var body: some View {
|
|
HStack {
|
|
HStack {
|
|
RoundedRectangle(cornerRadius: 8)
|
|
.fill(statusIndicatorColor)
|
|
.frame(width: 8, height: 40)
|
|
|
|
VStack(alignment: .leading) {
|
|
Text(route.name)
|
|
.foregroundColor(Color("TextPeerCard"))
|
|
if let network = route.network, network.contains("0.0.0.0/0") {
|
|
Image("direction-sign")
|
|
}
|
|
}
|
|
.padding(.leading, 5)
|
|
|
|
Spacer()
|
|
|
|
Text(routeDisplayText)
|
|
.foregroundColor(Color("TextPeerCard"))
|
|
.padding(.leading, 3)
|
|
}
|
|
.padding()
|
|
.background(Color("BgPeerCard"))
|
|
.cornerRadius(8)
|
|
.simultaneousGesture(
|
|
TapGesture()
|
|
.updating($isPressing) { _, gestureState, _ in
|
|
gestureState = true
|
|
}
|
|
.onEnded {
|
|
if !isPressing {
|
|
withAnimation {
|
|
toggleRouteSelection()
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
Toggle("", isOn: Binding(
|
|
get: { route.selected },
|
|
set: { newValue in
|
|
routeViewModel.toggleSelected(for: route.id)
|
|
}
|
|
))
|
|
.labelsHidden()
|
|
.toggleStyle(SwitchToggleStyle(tint: .orange))
|
|
.onChange(of: route.selected) { newValue in
|
|
newValue ? routeViewModel.selectRoute(route: route) : routeViewModel.deselectRoute(route: route)
|
|
}
|
|
.padding(.trailing, 15)
|
|
}
|
|
.background(Color("BgPeerCard"))
|
|
.cornerRadius(8)
|
|
.overlay(
|
|
GeometryReader { parentGeometry in
|
|
ZStack {
|
|
if selectedRouteId == route.id {
|
|
RouteTooltipView(route: route, orientationTop: orientationTop, selectedRouteId: $selectedRouteId)
|
|
.background(GeometryReader { tooltipGeometry in
|
|
Color.clear
|
|
.onAppear {
|
|
tooltipSize = tooltipGeometry.size
|
|
}
|
|
})
|
|
.position(
|
|
x: parentGeometry.size.width / 2,
|
|
y: orientationTop
|
|
? parentGeometry.size.height - (tooltipSize.height / 2) - 50
|
|
: (tooltipSize.height / 2) + 50
|
|
)
|
|
.opacity(1)
|
|
}
|
|
}
|
|
.frame(width: parentGeometry.size.width, height: parentGeometry.size.height, alignment: .center)
|
|
},
|
|
alignment: .center
|
|
)
|
|
}
|
|
|
|
private var statusIndicatorColor: Color {
|
|
if peerViewModel.peerInfo.contains(where: { info in
|
|
info.connStatus == "Connected" && (info.routes.contains(route.network ?? "") || route.domains?.contains(where: { $0.domain.contains(route.network ?? "") }) == true)
|
|
}) {
|
|
return Color.green
|
|
}
|
|
return route.selected ? Color.yellow : Color.gray.opacity(0.5)
|
|
}
|
|
|
|
private var routeDisplayText: String {
|
|
if route.network == "invalid Prefix" {
|
|
if let domains = route.domains, domains.count > 2 {
|
|
return "\(domains.count) Domains"
|
|
}
|
|
return route.domains?.map { $0.domain }.joined(separator: ", ") ?? ""
|
|
}
|
|
return route.network ?? "Unknown"
|
|
}
|
|
|
|
private func toggleRouteSelection() {
|
|
routeViewModel.selectedRouteId = routeViewModel.selectedRouteId == route.id ? nil : route.id
|
|
}
|
|
}
|
|
|
|
struct SizePreferenceKey: PreferenceKey {
|
|
static var defaultValue: CGSize = .zero
|
|
|
|
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
|
|
value = nextValue()
|
|
}
|
|
}
|
|
|
|
struct RouteTooltipView: View {
|
|
@ObservedObject var route: RoutesSelectionInfo
|
|
@State var orientationTop: Bool
|
|
@Binding var selectedRouteId: UUID?
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 10) {
|
|
Text(route.name)
|
|
.font(.headline)
|
|
Divider()
|
|
detailInfo()
|
|
}
|
|
.padding()
|
|
.background(Color(UIColor.systemGray6))
|
|
.cornerRadius(5)
|
|
.shadow(radius: 5)
|
|
.frame(width: UIScreen.main.bounds.width * 0.8)
|
|
.overlay(
|
|
Triangle()
|
|
.fill(Color(UIColor.systemGray6))
|
|
.frame(width: 20, height: 10)
|
|
.rotationEffect(.degrees(orientationTop ? 180 : 0 ))
|
|
.offset(x: 0, y: orientationTop ? 10 : -10), alignment: orientationTop ? .bottom : .top
|
|
)
|
|
.transition(.identity)
|
|
}
|
|
|
|
@ViewBuilder
|
|
func detailInfo() -> some View {
|
|
Group {
|
|
if route.network == "invalid Prefix" {
|
|
if let domains = route.domains {
|
|
ForEach(domains, id: \.self) { domain in
|
|
detailRow(label: domain.domain, value: domain.resolvedips ?? "")
|
|
}
|
|
}
|
|
} else {
|
|
detailRow(label: "Network", value: route.network ?? "")
|
|
}
|
|
}
|
|
.font(.footnote)
|
|
}
|
|
|
|
@ViewBuilder
|
|
func detailRow(label: String, value: String) -> some View {
|
|
HStack {
|
|
Text("\(label):").bold()
|
|
Spacer()
|
|
Text(value)
|
|
.multilineTextAlignment(.leading)
|
|
.foregroundColor(.gray)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
|
|
struct Triangle: Shape {
|
|
func path(in rect: CGRect) -> Path {
|
|
var path = Path()
|
|
path.move(to: CGPoint(x: rect.midX, y: rect.minY))
|
|
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
|
|
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
|
|
path.closeSubpath()
|
|
return path
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|