- 换行错乱根因:tmux 客户端格子改以 ghostty 实测为权威(像素除法 偏大 1 行/列 → 远端按更宽排版、本地自动换行掉字符),单 pane 实测 格子直发 refresh-client;kickoff 用 raw surface 真实格子;顶栏 格子数 tmux 模式读 clientGrid(模拟器连本机 sshd 端到端验证) - 顶带落地为真实 chrome 行:✕/⌄ 独立按钮组 + 弱化标题 + TmuxWindowTabGroup window tab 组,pane 不再被悬浮胶囊遮内容 - 放弃移动端分屏:删 SplitMenu/分屏入口,tmux window 切换上移顶部 tab,侧边栏改列活动会话;底部动作条合并状态行 - Info.plist 补 NSLocalNetworkUsageDescription(LAN 直连必需, 缺失时 socket 静默超时) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
140 lines
5.5 KiB
Swift
140 lines
5.5 KiB
Swift
import SwiftUI
|
||
|
||
// 设计稿 `3c 关闭确认`。(`3b 新建分屏菜单` 已随「放弃移动端分屏」移除。)
|
||
//
|
||
// 语义分工(务必别混):
|
||
// - **黄点 / ⌘M / 下滑 = 最小化**:不确认,直接回首页,会话继续跑。
|
||
// - **红点 / ⌘W = 关闭**:弹一次确认,按主机能力两套文案;`esc` 取消、`⏎` 选推荐项。
|
||
|
||
/// 浮层容器:遮罩 + 居中卡片(对话框 / 菜单共用)。
|
||
///
|
||
/// 遮罩淡入、卡片从 96% 长出来(`TX.Motion.pop`)—— 两段分开写才有层次;整块套一个
|
||
/// `.transition(.opacity)` 的话卡片只是跟着变淡,弹出感全无。
|
||
/// 动画事务由 `AppRouter.present/dismissModal` 提供。
|
||
struct TXModalScrim<Content: View>: View {
|
||
let onDismiss: () -> Void
|
||
@ViewBuilder let content: Content
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
TX.Scrim.color.opacity(TX.Scrim.dialog)
|
||
.ignoresSafeArea()
|
||
.contentShape(Rectangle())
|
||
.onTapGesture(perform: onDismiss)
|
||
.transition(.opacity)
|
||
content
|
||
.transition(TX.Motion.pop)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 关闭确认(3c)
|
||
|
||
/// 关闭确认。两套文案由会话能力决定:
|
||
/// - **tmux 会话**:推荐「仅断开」(`detach-client`,服务端继续跑);左侧次要文字按钮「结束会话」
|
||
/// (`kill-session`,**需再确认一次**)。
|
||
/// - **原生窗口**:琥珀警告「没有 tmux 兜底,前台进程会随之结束」,推荐「取消」,危险按钮「关闭窗口」。
|
||
struct CloseConfirmDialog: View {
|
||
@ObservedObject var session: TerminalSession
|
||
/// 仅断开(tmux)/ 直接关闭(原生窗口)
|
||
let onDetach: () -> Void
|
||
/// 结束 tmux 会话(kill-session)
|
||
let onKill: () -> Void
|
||
let onCancel: () -> Void
|
||
@EnvironmentObject var theme: TXThemeManager
|
||
@State private var confirmingKill = false
|
||
private var p: TXPalette { theme.palette }
|
||
|
||
private var isTmux: Bool { session.tmuxSummary != nil }
|
||
|
||
var body: some View {
|
||
TXModalScrim(onDismiss: onCancel) {
|
||
VStack(alignment: .leading, spacing: 0) {
|
||
Text(title)
|
||
.font(TX.Font.mono(15, .medium))
|
||
.foregroundStyle(p.text)
|
||
.padding(.bottom, 8)
|
||
message
|
||
.padding(.bottom, TX.Space.l)
|
||
buttons
|
||
}
|
||
.padding(TX.Space.l)
|
||
.frame(width: isTmux ? 436 : 404)
|
||
.background(p.surfaceDark, in: RoundedRectangle(cornerRadius: TX.Radius.dialog))
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: TX.Radius.dialog)
|
||
.strokeBorder(p.borderStrong, lineWidth: 1)
|
||
)
|
||
.txShadow(TX.Elevation.dialog)
|
||
}
|
||
}
|
||
|
||
private var title: String {
|
||
if let t = session.tmuxSummary, !t.name.isEmpty {
|
||
return "关闭 \(session.displayName) · \(t.name)?"
|
||
}
|
||
// 原生窗口带窗口编号(首版每主机单窗口,恒为 1;多窗口是二期能力)。
|
||
return "关闭 \(session.displayName) · 窗口 1?"
|
||
}
|
||
|
||
@ViewBuilder private var message: some View {
|
||
if let t = session.tmuxSummary {
|
||
Text("tmux 会话 (\(t.windows) windows / \(t.panes) panes)。仅断开后服务端继续跑,下次原样接回。")
|
||
.font(TX.Font.mono(12))
|
||
.foregroundStyle(p.text3)
|
||
.lineSpacing(4)
|
||
} else {
|
||
Text("没有 tmux 兜底,")
|
||
.font(TX.Font.mono(12))
|
||
.foregroundStyle(p.text3)
|
||
+ Text("前台进程会随之结束")
|
||
.font(TX.Font.mono(12, .medium))
|
||
.foregroundStyle(p.warning)
|
||
+ Text("。想留着就点黄点最小化。")
|
||
.font(TX.Font.mono(12))
|
||
.foregroundStyle(p.text3)
|
||
}
|
||
}
|
||
|
||
@ViewBuilder private var buttons: some View {
|
||
HStack(spacing: TX.Space.s) {
|
||
if isTmux {
|
||
// 次要文字按钮(危险):kill-session 需再点一次确认。
|
||
Button {
|
||
if confirmingKill { onKill() } else { confirmingKill = true }
|
||
} label: {
|
||
Text(confirmingKill ? "确认结束会话?" : "结束会话")
|
||
.font(TX.Font.mono(12, confirmingKill ? .semibold : .regular))
|
||
.foregroundStyle(p.danger)
|
||
}
|
||
.buttonStyle(.txPress)
|
||
}
|
||
Spacer()
|
||
TXButton(title: "取消", action: onCancel)
|
||
if isTmux {
|
||
TXButton(title: "仅断开 ⏎", emphasized: true, action: onDetach)
|
||
} else {
|
||
dangerButton
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 危险按钮(设计给的精确色:描边 #8d5560 / 底 rgba(92,58,63,.35) / 字 #f5a0ac)。
|
||
private var dangerButton: some View {
|
||
Button(action: onDetach) {
|
||
Text("关闭窗口")
|
||
.font(TX.Font.mono(12, .medium))
|
||
.foregroundStyle(Color(hex: 0xF5A0AC))
|
||
.padding(.horizontal, 14)
|
||
.frame(height: 32)
|
||
.background(Color(hex: 0x5C3A3F).opacity(0.35),
|
||
in: RoundedRectangle(cornerRadius: TX.Radius.control))
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: TX.Radius.control)
|
||
.strokeBorder(Color(hex: 0x8D5560), lineWidth: 1)
|
||
)
|
||
}
|
||
.buttonStyle(.txPress)
|
||
}
|
||
}
|