Files
openclaw/apps/ios/Sources/Design/SettingsProTabSupport.swift
2026-06-04 22:52:52 -05:00

211 lines
6.8 KiB
Swift

import Darwin
import SwiftUI
import UIKit
enum SettingsRoute: Hashable {
case gateway
case permissions
case voice
case diagnostics
case privacy
case notifications
case about
}
enum SettingsLayout {
static let cardRadius: CGFloat = 12
static let rowHeight: CGFloat = 58
static let bottomContentPadding: CGFloat = 12
}
struct SettingsBottomOverlayInsetReader: UIViewRepresentable {
@Binding var inset: CGFloat
func makeCoordinator() -> Coordinator {
Coordinator(inset: self.$inset)
}
func makeUIView(context: Context) -> SettingsBottomOverlayInsetProbeView {
let view = SettingsBottomOverlayInsetProbeView()
view.onInsetChange = { value in
context.coordinator.updateInset(value)
}
return view
}
func updateUIView(_ uiView: SettingsBottomOverlayInsetProbeView, context: Context) {
context.coordinator.inset = self.$inset
uiView.onInsetChange = { value in
context.coordinator.updateInset(value)
}
uiView.updateInset()
}
final class Coordinator {
var inset: Binding<CGFloat>
init(inset: Binding<CGFloat>) {
self.inset = inset
}
func updateInset(_ value: CGFloat) {
let rounded = max(0, ceil(value))
guard abs(self.inset.wrappedValue - rounded) > 0.5 else { return }
self.inset.wrappedValue = rounded
}
}
}
final class SettingsBottomOverlayInsetProbeView: UIView {
var onInsetChange: ((CGFloat) -> Void)?
override func didMoveToWindow() {
super.didMoveToWindow()
self.updateInset()
}
override func layoutSubviews() {
super.layoutSubviews()
self.updateInset()
}
func updateInset() {
let value = self.visibleTabBarHeight()
DispatchQueue.main.async { [weak self] in
self?.onInsetChange?(value)
}
}
private func visibleTabBarHeight() -> CGFloat {
let tabBarController = self.nearestViewController()?.tabBarController
?? self.findTabBarController(in: self.window?.rootViewController)
guard let tabBar = tabBarController?.tabBar,
!tabBar.isHidden,
tabBar.alpha > 0.01,
tabBar.window != nil,
self.window != nil
else {
return 0
}
let tabFrame = tabBar.convert(tabBar.bounds, to: nil)
guard tabFrame.height.isFinite else { return 0 }
return max(0, tabFrame.height)
}
private func nearestViewController() -> UIViewController? {
var responder: UIResponder? = self
while let current = responder {
if let viewController = current as? UIViewController {
return viewController
}
responder = current.next
}
return nil
}
private func findTabBarController(in viewController: UIViewController?) -> UITabBarController? {
guard let viewController else { return nil }
if let tabBarController = viewController as? UITabBarController {
return tabBarController
}
if let tabBarController = self.findTabBarController(in: viewController.presentedViewController) {
return tabBarController
}
for child in viewController.children {
if let tabBarController = self.findTabBarController(in: child) {
return tabBarController
}
}
return nil
}
}
enum SettingsDiagnosticIssue: String, Equatable, CaseIterable {
case gatewayOffline
case discoveryUnavailable
case talkConfigMissing
case notificationsUnavailable
}
enum SettingsDiagnostics {
static func issues(
gatewayConnected: Bool,
discoveredGatewayCount: Int,
talkConfigLoaded: Bool,
notificationStatusText: String) -> [SettingsDiagnosticIssue]
{
var issues: [SettingsDiagnosticIssue] = []
if !gatewayConnected { issues.append(.gatewayOffline) }
if discoveredGatewayCount == 0 { issues.append(.discoveryUnavailable) }
if gatewayConnected, !talkConfigLoaded { issues.append(.talkConfigMissing) }
if notificationStatusText != "Allowed" { issues.append(.notificationsUnavailable) }
return issues
}
static func issueCount(
gatewayConnected: Bool,
discoveredGatewayCount: Int,
talkConfigLoaded: Bool,
notificationStatusText: String) -> Int
{
self.issues(
gatewayConnected: gatewayConnected,
discoveredGatewayCount: discoveredGatewayCount,
talkConfigLoaded: talkConfigLoaded,
notificationStatusText: notificationStatusText).count
}
static func timestamp(_ date: Date) -> String {
date.formatted(date: .omitted, time: .shortened)
}
}
extension SettingsProTab {
static func hasTailnetIPv4() -> Bool {
var addrList: UnsafeMutablePointer<ifaddrs>?
guard getifaddrs(&addrList) == 0, let first = addrList else { return false }
defer { freeifaddrs(addrList) }
for ptr in sequence(first: first, next: { $0.pointee.ifa_next }) {
let flags = Int32(ptr.pointee.ifa_flags)
let isUp = (flags & IFF_UP) != 0
let isLoopback = (flags & IFF_LOOPBACK) != 0
guard let addrPtr = ptr.pointee.ifa_addr else { continue }
let family = addrPtr.pointee.sa_family
if !isUp || isLoopback || family != UInt8(AF_INET) { continue }
var addr = addrPtr.pointee
var buffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let result = getnameinfo(
&addr,
socklen_t(addrPtr.pointee.sa_len),
&buffer,
socklen_t(buffer.count),
nil,
0,
NI_NUMERICHOST)
guard result == 0 else { continue }
let bytes = buffer.prefix { $0 != 0 }.map { UInt8(bitPattern: $0) }
guard let ip = String(bytes: bytes, encoding: .utf8) else { continue }
if self.isTailnetIPv4(ip) { return true }
}
return false
}
static func isTailnetHostOrIP(_ host: String) -> Bool {
let trimmed = host.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
if trimmed.hasSuffix(".ts.net") || trimmed.hasSuffix(".ts.net.") { return true }
return self.isTailnetIPv4(trimmed)
}
static func isTailnetIPv4(_ ip: String) -> Bool {
let parts = ip.split(separator: ".")
guard parts.count == 4 else { return false }
let octets = parts.compactMap { Int($0) }
guard octets.count == 4 else { return false }
let a = octets[0]
let b = octets[1]
guard (0...255).contains(a), (0...255).contains(b) else { return false }
return a == 100 && b >= 64 && b <= 127
}
}