feat(ios): P1-B — W7 UI wave: deep links, timeline, quick-reply, diff, projects, session switcher, thumbnails, lock-screen push
T-iOS-22: DeepLinkRouter (full-field whitelist, cold-start stash, route(from:) for push-tap reuse), 21 tests T-iOS-24: TimelineSheet mirroring web render() order; disabled→empty-state; reuses AwayDigest onExpand T-iOS-25: QuickReply chips (built-ins mirror quick-reply.ts via KeyByteMap; visible iff live gate && !readOnly) T-iOS-27: read-only DiffScreen + App-layer DiffFetcher (RO no-Origin; APIClient fold-in noted for T-iOS-38 owner) T-iOS-26: Projects list/detail — grouping byte-identical to web group keys (prefs-shared collapse state), prefs-clobber defenses (no blind PUT on empty base; adopt server echo), claude\r bootstrap T-iOS-23: UnreadLedger + TitleSanitizer (SessionCore, +15 tests), lastOutputAt decode, list-boundary re-sanitize T-iOS-29: new-in-cwd (untrusted cwd, no bootstrap re-injection) + exited-session reopen; fixes stale-controller SwiftTerm view bug via .id(controller.id) T-iOS-28: offscreen SwiftTerm thumbnail pipeline (LRU 32, concurrency gate 2, 256KiB cap, grid clamp) T-iOS-21: PushRegistrar (WEBTERM_GATE category: Allow=.authenticationRequired, no .foreground) + NotificationActionHandler (whitelisted payload, token never persisted, bg-task-wrapped POST, 403 fallback) CRITICAL fix (verify-found boot crash): @Sendable literals on UN completion closures — MainActor-inherited closures trapped Swift 6 executor check on UN's background queue; boot re-verified (no new crash reports, permission prompt reached, privacy shade correctly covering during system alert) Verified: 261 pkg + 247 app + 10 integration tests green; 7/7 semantics checks; Owns audit clean
This commit is contained in:
76
ios/App/WebTerm/ViewModels/ProjectDetailViewModel.swift
Normal file
76
ios/App/WebTerm/ViewModels/ProjectDetailViewModel.swift
Normal file
@@ -0,0 +1,76 @@
|
||||
import APIClient
|
||||
import Foundation
|
||||
import Observation
|
||||
import WireProtocol
|
||||
|
||||
/// T-iOS-26 · 项目详情状态(`GET /projects/detail?path=` → phase 状态机,
|
||||
/// 与 DiffViewModel/TimelineViewModel 同一纪律)。
|
||||
///
|
||||
/// fetch 闭包注入 —— 生产由 `forHost` 包 `APIClient.projectDetail(path:)`
|
||||
/// (builder 百分号编码、400/404/500 `{error}` → 类型化错误均在 T-iOS-38
|
||||
/// 完成并已测);测试注入 fake。本 VM 只归约用户可见的三种结局:
|
||||
/// - 成功 → `.loaded(ProjectDetail)`(sessions/worktrees/hasClaudeMd 透传);
|
||||
/// - 400 → `.failed(.pathInvalid)`、404 → `.failed(.notFound)`、
|
||||
/// 500/解码/传输 → `.failed(.unavailable)` —— 全部可经 `load()` 重试。
|
||||
@MainActor
|
||||
@Observable
|
||||
final class ProjectDetailViewModel {
|
||||
/// 用户可见的失败三分类(文案映射在 ProjectDetailScreen)。
|
||||
enum Failure: Equatable {
|
||||
case pathInvalid
|
||||
case notFound
|
||||
case unavailable
|
||||
}
|
||||
|
||||
enum Phase: Equatable {
|
||||
case loading
|
||||
case loaded(ProjectDetail)
|
||||
case failed(Failure)
|
||||
}
|
||||
|
||||
private(set) var phase: Phase = .loading
|
||||
/// 详情/diff 的目标项目路径(来自列表行 —— 服务器数据;只透传给
|
||||
/// builder,绝不本地拼 URL)。
|
||||
let path: String
|
||||
|
||||
@ObservationIgnored
|
||||
private let fetch: @Sendable () async throws -> ProjectDetail
|
||||
|
||||
init(path: String, fetch: @escaping @Sendable () async throws -> ProjectDetail) {
|
||||
self.path = path
|
||||
self.fetch = fetch
|
||||
}
|
||||
|
||||
/// 生产装配缝(ProjectsViewModel.makeDetailViewModel 经此构造)。
|
||||
static func forHost(
|
||||
endpoint: HostEndpoint, http: any HTTPTransport, path: String
|
||||
) -> ProjectDetailViewModel {
|
||||
let client = APIClient(endpoint: endpoint, http: http)
|
||||
return ProjectDetailViewModel(path: path, fetch: {
|
||||
try await client.projectDetail(path: path)
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch 并呈现。也是「重试」路径:`.failed` 后可再次调用。
|
||||
func load() async {
|
||||
phase = .loading
|
||||
do {
|
||||
phase = .loaded(try await fetch())
|
||||
} catch let error as APIClientError {
|
||||
phase = .failed(Self.failure(for: error))
|
||||
} catch {
|
||||
phase = .failed(.unavailable) // 传输层等其余错误:可重试兜底
|
||||
}
|
||||
}
|
||||
|
||||
private static func failure(for error: APIClientError) -> Failure {
|
||||
switch error {
|
||||
case .projectPathInvalid, .invalidRequest:
|
||||
return .pathInvalid
|
||||
case .projectNotFound:
|
||||
return .notFound
|
||||
default:
|
||||
return .unavailable
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user