feat(ios,android): P2 wave, git panel, token UX, per-host WS token, docs
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.
This commit is contained in:
122
ios/App/WebTerm/Screens/SettingsScreen.swift
Normal file
122
ios/App/WebTerm/Screens/SettingsScreen.swift
Normal file
@@ -0,0 +1,122 @@
|
||||
import SwiftUI
|
||||
|
||||
/// T-iOS-34 · 设置页 —— 目前只有一件事:**主题**(跟随系统 / 深色 / 浅色)。
|
||||
///
|
||||
/// 刻意不做成「一堆开关」:终端字号跟随系统 Dynamic Type(`TerminalTheme` 取
|
||||
/// `.footnote` 度量),主机/令牌在配对页,快捷键栏在终端工具栏 —— 那些都有更近
|
||||
/// 的入口,搬到这里只会多一条重复路径。
|
||||
///
|
||||
/// 选择即生效:`ThemeStore.select` 落盘 + `@Observable` 触发根视图重算
|
||||
/// `preferredColorScheme`,不需要「保存」按钮。
|
||||
struct SettingsScreen: View {
|
||||
/// 主题真值(`RootView` 持有并注入;这里只读写它,不自己存一份)。
|
||||
let themeStore: ThemeStore
|
||||
|
||||
/// 当前**有效**外观 —— 根视图的 `preferredColorScheme` 已经把它压进了环境,
|
||||
/// 所以终端预览直接读环境即可,不必再算一遍「跟随系统时到底是哪档」。
|
||||
@Environment(\.colorScheme) private var effectiveScheme
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
themeSection
|
||||
terminalPreviewSection
|
||||
}
|
||||
.navigationTitle(SettingsCopy.title)
|
||||
}
|
||||
|
||||
// MARK: - 主题选择
|
||||
|
||||
private var themeSection: some View {
|
||||
Section {
|
||||
ForEach(AppTheme.allCases, id: \.self) { theme in
|
||||
themeRow(theme)
|
||||
}
|
||||
} header: {
|
||||
Text(SettingsCopy.themeHeader)
|
||||
} footer: {
|
||||
Text(SettingsCopy.themeFooter)
|
||||
}
|
||||
}
|
||||
|
||||
private func themeRow(_ theme: AppTheme) -> some View {
|
||||
Button {
|
||||
themeStore.select(theme)
|
||||
DS.Haptics.selection()
|
||||
} label: {
|
||||
HStack(spacing: DS.Space.md12) {
|
||||
Image(systemName: theme.symbolName)
|
||||
.foregroundStyle(DS.Palette.accent)
|
||||
.frame(width: DS.Space.xxl24)
|
||||
Text(theme.label)
|
||||
.foregroundStyle(DS.Palette.textPrimary)
|
||||
Spacer(minLength: DS.Space.sm8)
|
||||
if themeStore.theme == theme {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundStyle(DS.Palette.accent)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
}
|
||||
.frame(minHeight: DS.Layout.minHitTarget)
|
||||
}
|
||||
.accessibilityIdentifier("settings.theme.\(theme.rawValue)")
|
||||
// 选中态对 VoiceOver 可见(勾号是视觉信号,不是语义信号)。
|
||||
.accessibilityAddTraits(themeStore.theme == theme ? [.isSelected] : [])
|
||||
}
|
||||
|
||||
// MARK: - 终端画布预览
|
||||
|
||||
/// 主题的最大可见差异就是终端那一整块画布,所以给一个真实取色的预览条:
|
||||
/// 背景/前景/光标全部来自 `TerminalPalette`(与真终端同一出处)。
|
||||
private var terminalPreviewSection: some View {
|
||||
Section {
|
||||
let colors = TerminalPalette.colors(for: effectiveScheme)
|
||||
HStack(spacing: DS.Space.xs2) {
|
||||
Text(verbatim: SettingsCopy.terminalSample)
|
||||
.font(DS.Typography.mono(.footnote))
|
||||
.foregroundStyle(Color(uiColor: colors.foreground))
|
||||
Rectangle()
|
||||
.fill(Color(uiColor: colors.caret))
|
||||
.frame(width: DS.Space.sm8, height: DS.Space.lg16)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(DS.Space.md12)
|
||||
.background(
|
||||
Color(uiColor: colors.background),
|
||||
in: RoundedRectangle(cornerRadius: DS.Radius.sm8)
|
||||
)
|
||||
.accessibilityElement(children: .ignore)
|
||||
.accessibilityLabel(SettingsCopy.terminalPreviewA11y)
|
||||
} header: {
|
||||
Text(SettingsCopy.terminalHeader)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置页用户可见文案。
|
||||
enum SettingsCopy {
|
||||
static let title = "设置"
|
||||
static let themeHeader = "外观"
|
||||
static let themeFooter = "默认深色 —— 与网页端一致。选「跟随系统」时随 iOS 的浅色/深色切换。"
|
||||
static let terminalHeader = "终端预览"
|
||||
static let terminalSample = "$ claude --resume"
|
||||
static let terminalPreviewA11y = "终端配色预览"
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("SettingsScreen") {
|
||||
NavigationStack {
|
||||
SettingsScreen(themeStore: ThemeStore(defaults: PreviewThemeDefaults()))
|
||||
}
|
||||
}
|
||||
|
||||
/// 预览用的内存 defaults —— 预览绝不写用户的真实 `UserDefaults`。
|
||||
private final class PreviewThemeDefaults: ThemeDefaults {
|
||||
private var values: [String: String] = [:]
|
||||
|
||||
func themeRaw(forKey key: String) -> String? { values[key] }
|
||||
|
||||
func setThemeRaw(_ value: String, forKey key: String) {
|
||||
values = values.merging([key: value]) { _, new in new }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user