Merge remote-tracking branch 'origin/main'
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
plugins {
|
||||
id("com.android.application") version "8.5.2" apply false
|
||||
id("com.android.application") version "8.6.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.24" apply false
|
||||
id("org.jetbrains.kotlin.plugin.serialization") version "1.9.24" apply false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct RootTabs: View {
|
||||
@EnvironmentObject private var appModel: NodeAppModel
|
||||
@@ -13,29 +14,13 @@ struct RootTabs: View {
|
||||
.tabItem { Label("Voice", systemImage: "mic") }
|
||||
|
||||
SettingsTab()
|
||||
.tabItem {
|
||||
VStack {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(systemName: "gearshape")
|
||||
Circle()
|
||||
.fill(self.settingsIndicatorColor)
|
||||
.frame(width: 9, height: 9)
|
||||
.overlay(
|
||||
Circle()
|
||||
.stroke(.black.opacity(0.2), lineWidth: 0.5))
|
||||
.shadow(
|
||||
color: self.settingsIndicatorGlowColor,
|
||||
radius: self.settingsIndicatorGlowRadius,
|
||||
x: 0,
|
||||
y: 0)
|
||||
.scaleEffect(self.settingsIndicatorScale)
|
||||
.opacity(self.settingsIndicatorOpacity)
|
||||
.offset(x: 7, y: -2)
|
||||
}
|
||||
Text("Settings")
|
||||
}
|
||||
}
|
||||
.tabItem { Label("Settings", systemImage: "gearshape") }
|
||||
}
|
||||
.background(TabBarControllerAccessor { tabBarController in
|
||||
guard let item = tabBarController.tabBar.items?[Self.settingsTabIndex] else { return }
|
||||
item.badgeValue = ""
|
||||
item.badgeColor = self.settingsBadgeColor
|
||||
})
|
||||
.onAppear { self.updateConnectingPulse(for: self.bridgeIndicatorState) }
|
||||
.onChange(of: self.bridgeIndicatorState) { _, newValue in
|
||||
self.updateConnectingPulse(for: newValue)
|
||||
@@ -48,55 +33,25 @@ struct RootTabs: View {
|
||||
case disconnected
|
||||
}
|
||||
|
||||
private static let settingsTabIndex = 2
|
||||
|
||||
private var bridgeIndicatorState: BridgeIndicatorState {
|
||||
if self.appModel.bridgeServerName != nil { return .connected }
|
||||
if self.appModel.bridgeStatusText.localizedCaseInsensitiveContains("connecting") { return .connecting }
|
||||
return .disconnected
|
||||
}
|
||||
|
||||
private var settingsIndicatorColor: Color {
|
||||
private var settingsBadgeColor: UIColor {
|
||||
switch self.bridgeIndicatorState {
|
||||
case .connected:
|
||||
Color.green
|
||||
UIColor.systemGreen
|
||||
case .connecting:
|
||||
Color.yellow
|
||||
UIColor.systemYellow.withAlphaComponent(self.isConnectingPulse ? 1.0 : 0.6)
|
||||
case .disconnected:
|
||||
Color.red
|
||||
UIColor.systemRed
|
||||
}
|
||||
}
|
||||
|
||||
private var settingsIndicatorGlowColor: Color {
|
||||
switch self.bridgeIndicatorState {
|
||||
case .connected:
|
||||
Color.green.opacity(0.75)
|
||||
case .connecting:
|
||||
Color.yellow.opacity(0.6)
|
||||
case .disconnected:
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
|
||||
private var settingsIndicatorGlowRadius: CGFloat {
|
||||
switch self.bridgeIndicatorState {
|
||||
case .connected:
|
||||
6
|
||||
case .connecting:
|
||||
self.isConnectingPulse ? 6 : 3
|
||||
case .disconnected:
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private var settingsIndicatorScale: CGFloat {
|
||||
guard self.bridgeIndicatorState == .connecting else { return 1 }
|
||||
return self.isConnectingPulse ? 1.12 : 0.96
|
||||
}
|
||||
|
||||
private var settingsIndicatorOpacity: Double {
|
||||
guard self.bridgeIndicatorState == .connecting else { return 1 }
|
||||
return self.isConnectingPulse ? 1.0 : 0.75
|
||||
}
|
||||
|
||||
private func updateConnectingPulse(for state: BridgeIndicatorState) {
|
||||
guard state == .connecting else {
|
||||
withAnimation(.easeOut(duration: 0.2)) { self.isConnectingPulse = false }
|
||||
@@ -109,3 +64,41 @@ struct RootTabs: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct TabBarControllerAccessor: UIViewControllerRepresentable {
|
||||
let onResolve: (UITabBarController) -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> ResolverViewController {
|
||||
ResolverViewController(onResolve: self.onResolve)
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: ResolverViewController, context: Context) {
|
||||
uiViewController.onResolve = self.onResolve
|
||||
uiViewController.resolveIfPossible()
|
||||
}
|
||||
}
|
||||
|
||||
private final class ResolverViewController: UIViewController {
|
||||
var onResolve: (UITabBarController) -> Void
|
||||
|
||||
init(onResolve: @escaping (UITabBarController) -> Void) {
|
||||
self.onResolve = onResolve
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
self.view.isHidden = true
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
self.resolveIfPossible()
|
||||
}
|
||||
|
||||
func resolveIfPossible() {
|
||||
guard let tabBarController = self.tabBarController else { return }
|
||||
self.onResolve(tabBarController)
|
||||
}
|
||||
}
|
||||
|
||||
43
apps/ios/Tests/DeepLinkParserTests.swift
Normal file
43
apps/ios/Tests/DeepLinkParserTests.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import ClawdisKit
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
@Suite struct DeepLinkParserTests {
|
||||
@Test func parseRejectsNonClawdisScheme() {
|
||||
let url = URL(string: "https://example.com/agent?message=hi")!
|
||||
#expect(DeepLinkParser.parse(url) == nil)
|
||||
}
|
||||
|
||||
@Test func parseRejectsEmptyMessage() {
|
||||
let url = URL(string: "clawdis://agent?message=%20%20%0A")!
|
||||
#expect(DeepLinkParser.parse(url) == nil)
|
||||
}
|
||||
|
||||
@Test func parseAgentLinkParsesCommonFields() {
|
||||
let url = URL(string: "clawdis://agent?message=Hello&deliver=1&sessionKey=node-iris&thinking=low&timeoutSeconds=30")!
|
||||
#expect(
|
||||
DeepLinkParser.parse(url) == .agent(
|
||||
.init(
|
||||
message: "Hello",
|
||||
sessionKey: "node-iris",
|
||||
thinking: "low",
|
||||
deliver: true,
|
||||
to: nil,
|
||||
channel: nil,
|
||||
timeoutSeconds: 30,
|
||||
key: nil)))
|
||||
}
|
||||
|
||||
@Test func parseRejectsNegativeTimeoutSeconds() {
|
||||
let url = URL(string: "clawdis://agent?message=Hello&timeoutSeconds=-1")!
|
||||
#expect(DeepLinkParser.parse(url) == .agent(.init(
|
||||
message: "Hello",
|
||||
sessionKey: nil,
|
||||
thinking: nil,
|
||||
deliver: false,
|
||||
to: nil,
|
||||
channel: nil,
|
||||
timeoutSeconds: nil,
|
||||
key: nil)))
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,16 @@ packages:
|
||||
ClawdisKit:
|
||||
path: ../shared/ClawdisKit
|
||||
|
||||
schemes:
|
||||
Clawdis:
|
||||
shared: true
|
||||
build:
|
||||
targets:
|
||||
Clawdis: all
|
||||
test:
|
||||
targets:
|
||||
- ClawdisTests
|
||||
|
||||
targets:
|
||||
Clawdis:
|
||||
type: application
|
||||
|
||||
Reference in New Issue
Block a user