App layer, four sequential slices (a shared .xcodeproj means adding files regenerates it, so these could not run in parallel): - token UX end to end: pairing prompts for a token when a host 401s, POST /auth validates it, and 204-without-Set-Cookie is correctly read as "this server has auth disabled" rather than "authenticated". A host paired before the token was turned on recovers by re-pairing in place. Remove-host now exists and finally gives PushRegistrar.handleHostRemoved a caller. - project git panel + worktree lifecycle (T-iOS-32) + claude --resume history — the parity gap with Android and the web front end. - terminal search (T-iOS-33) and voice PTT (T-iOS-31) with an epoch guard so a session switch between dictation and confirm cannot inject into the wrong session. - theme + Dynamic Type (T-iOS-34) and web ?join= interop (T-iOS-35). RootView no longer hard-locks .preferredColorScheme(.dark). Also unpins SwiftTerm to 1.15.0 by dropping the local hasActiveSelection that collided with the upstream one, verified green from a fresh derivedDataPath. Includes the two HIGH fixes the security review found: - iOS resolved the WS token host-independently, so a token-gated host sitting next to an open one could never open a terminal and no on-screen remedy could fix it. Now one transport per host; cross-host leakage is structurally impossible since both read paths return only that host's own value. - Android reported the host's own git-credential 401 (git-ops.ts:108, "Push authentication required on the host.") as "your access token is wrong", because a blanket 401 mapping ran ahead of the per-route one. Git-write routes are now ROUTE_DEFINED and keep the server's message. And the doc sync: README/ios README no longer claim the client is unmerged on feat/ios-client, the Clients section finally lists Android, and the plan checkboxes reflect what is actually built. iOS 534 app tests + 452 package tests; Android 687 tests.
99 lines
3.8 KiB
Swift
99 lines
3.8 KiB
Swift
import SwiftUI
|
||
import UIKit
|
||
|
||
/// T-iOS-34 · 终端画布配色 —— **跟随主题**的那一半。
|
||
///
|
||
/// 此前终端是"固定暖深色,不随外观变"(对齐桌面)。浅色主题落地后这条不成立:
|
||
/// 用户选了浅色,却有一整块纯黑画布占满屏,是最刺眼的破绽。所以画布按有效外观
|
||
/// 取两套值:
|
||
/// - 深色 = `#100F0D` / `#ECE9E3`(桌面 web `--bg` / `--text`,**逐字节不变**);
|
||
/// - 浅色 = `#F6F7F9` / `#1A1D24`(镜像 web `public/settings.ts` 的
|
||
/// `THEMES.light`,iOS 与桌面浅色档观感一致)。
|
||
///
|
||
/// caret / selection 用**该档**的 accent 解析值(深色金 `#E3A64A` / 浅色深金
|
||
/// `#C9892F`),不写死深色档的金。
|
||
///
|
||
/// 说明(重要):`SwiftTerm.TerminalView` 在 `nativeBackgroundColor` 的 setter 里
|
||
/// 就把 UIColor 压成了内部 `terminal.backgroundColor`(`iOSTerminalView.swift:1207`),
|
||
/// 且没有 `traitCollectionDidChange` 重取 —— 因此**动态 UIColor 只能保证"构建时
|
||
/// 取对档"**,主题在会话进行中切换必须由视图侧重新 `apply` 一次。本文件提供两种
|
||
/// 形态供调用点选择:`colors(for:)`(已解析,重套用)与 `dynamic*`(动态 UIColor,
|
||
/// 构建时取当前 trait)。
|
||
enum TerminalPalette {
|
||
|
||
/// 一档终端画布的四个颜色。全部是**已解析**的具体色(不是动态色),
|
||
/// 便于直接交给 SwiftTerm,也便于测试逐字节断言。
|
||
struct Colors: Equatable {
|
||
let background: UIColor
|
||
let foreground: UIColor
|
||
let caret: UIColor
|
||
let selection: UIColor
|
||
}
|
||
|
||
/// 深色档(桌面对齐,历史值)。
|
||
private enum Dark {
|
||
static let background: UInt32 = 0x100F_0D
|
||
static let foreground: UInt32 = 0xECE9_E3
|
||
}
|
||
|
||
/// 浅色档(镜像 web `THEMES.light`)。
|
||
private enum Light {
|
||
static let background: UInt32 = 0xF6F7_F9
|
||
static let foreground: UInt32 = 0x1A1D_24
|
||
}
|
||
|
||
static func colors(for scheme: ColorScheme) -> Colors {
|
||
let style = scheme.userInterfaceStyle
|
||
let accent = DS.Palette.accentUIColor()
|
||
.resolvedColor(with: UITraitCollection(userInterfaceStyle: style))
|
||
let isDark = style == .dark
|
||
return Colors(
|
||
background: UIColor(hex: isDark ? Dark.background : Light.background),
|
||
foreground: UIColor(hex: isDark ? Dark.foreground : Light.foreground),
|
||
caret: accent,
|
||
selection: accent
|
||
)
|
||
}
|
||
|
||
/// 动态背景色 —— 取值时机决定档位(SwiftUI/UIKit 的 trait 解析)。
|
||
static func dynamicBackground() -> UIColor {
|
||
UIColor { trait in colors(for: trait.colorScheme).background }
|
||
}
|
||
|
||
/// 动态前景色。
|
||
static func dynamicForeground() -> UIColor {
|
||
UIColor { trait in colors(for: trait.colorScheme).foreground }
|
||
}
|
||
}
|
||
|
||
// MARK: - 外观桥(ColorScheme ↔ UIUserInterfaceStyle)
|
||
|
||
extension ColorScheme {
|
||
/// SwiftUI → UIKit。`ColorScheme` 只有 light/dark 两个 case,故是全函数。
|
||
var userInterfaceStyle: UIUserInterfaceStyle {
|
||
self == .dark ? .dark : .light
|
||
}
|
||
}
|
||
|
||
extension UITraitCollection {
|
||
/// UIKit → SwiftUI。`.unspecified` 按 iOS 的默认外观算作浅色。
|
||
var colorScheme: ColorScheme {
|
||
userInterfaceStyle == .dark ? .dark : .light
|
||
}
|
||
}
|
||
|
||
// MARK: - Hex
|
||
|
||
extension UIColor {
|
||
/// 从 `0xRRGGBB` 构建不透明 sRGB 色。设计 token 的十六进制值在文档/CSS 里
|
||
/// 就是这个写法,直接用它可以逐字节对照,不必手算 0–1 分量。
|
||
convenience init(hex: UInt32, alpha: CGFloat = 1) {
|
||
self.init(
|
||
red: CGFloat((hex >> 16) & 0xFF) / 255,
|
||
green: CGFloat((hex >> 8) & 0xFF) / 255,
|
||
blue: CGFloat(hex & 0xFF) / 255,
|
||
alpha: alpha
|
||
)
|
||
}
|
||
}
|