feat: term session mgr (#846)
This commit is contained in:
@@ -4,6 +4,15 @@
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>IntentsSupportedIntents</key>
|
||||
<array>
|
||||
<string>ConfigurationIntent</string>
|
||||
</array>
|
||||
<key>NSSupportsLiveActivities</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
|
||||
@@ -12,5 +12,8 @@ import SwiftUI
|
||||
struct StatusWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
StatusWidget()
|
||||
if #available(iOSApplicationExtension 16.1, *) {
|
||||
TerminalLiveActivity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
185
ios/StatusWidget/TerminalLiveActivity.swift
Normal file
185
ios/StatusWidget/TerminalLiveActivity.swift
Normal file
@@ -0,0 +1,185 @@
|
||||
//
|
||||
// TerminalLiveActivity.swift
|
||||
// StatusWidget
|
||||
//
|
||||
// Renders the Live Activity UI for SSH/Terminal sessions.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
import ActivityKit
|
||||
|
||||
// Helper to map status strings to a color dot (case-insensitive).
|
||||
@inline(__always)
|
||||
private func getStatusDotColor(_ status: String) -> Color {
|
||||
switch status.lowercased() {
|
||||
case "connected":
|
||||
return .green
|
||||
case "connecting":
|
||||
return .yellow
|
||||
case "disconnected":
|
||||
return .red
|
||||
default:
|
||||
return .secondary
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize status for display: capitalize first letter only.
|
||||
@inline(__always)
|
||||
private func formatStatus(_ status: String) -> String {
|
||||
let trimmed = status.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard let first = trimmed.first else { return status }
|
||||
let head = String(first).uppercased()
|
||||
let tail = String(trimmed.dropFirst()).lowercased()
|
||||
return head + tail
|
||||
}
|
||||
|
||||
// Localize known statuses; fall back to formatted original.
|
||||
@inline(__always)
|
||||
private func localizedStatus(_ status: String) -> String {
|
||||
switch status.lowercased() {
|
||||
case "connected":
|
||||
return NSLocalizedString("Connected", comment: "Session connected status")
|
||||
case "connecting":
|
||||
return NSLocalizedString("Connecting", comment: "Session connecting status")
|
||||
case "disconnected":
|
||||
return NSLocalizedString("Disconnected", comment: "Session disconnected status")
|
||||
default:
|
||||
return formatStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
struct TerminalLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: TerminalAttributes.self) { context in
|
||||
let state = context.state
|
||||
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
HStack(spacing: 6) {
|
||||
Text(state.hasTerminal ? NSLocalizedString("Terminal", comment: "Terminal label") : "SSH")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
if state.connectionCount > 1 {
|
||||
Text("(\(state.connectionCount))")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Text(state.title)
|
||||
.font(.headline)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
Text(state.subtitle)
|
||||
.font(.subheadline)
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(.secondary)
|
||||
HStack(spacing: 8) {
|
||||
Circle()
|
||||
.fill(getStatusDotColor(state.status))
|
||||
.frame(width: 6, height: 6)
|
||||
Text(localizedStatus(state.status))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer(minLength: 8)
|
||||
Image(systemName: state.hasTerminal ? "terminal" : "bolt.horizontal.circle")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
.padding(.vertical, 10)
|
||||
} dynamicIsland: { context in
|
||||
DynamicIsland {
|
||||
DynamicIslandExpandedRegion(.leading) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack(spacing: 4) {
|
||||
Text(context.state.hasTerminal ? NSLocalizedString("Terminal", comment: "Terminal label") : "SSH")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
if context.state.connectionCount > 1 {
|
||||
Text("(\(context.state.connectionCount))")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
Text(context.state.title)
|
||||
.font(.subheadline)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.trailing) {
|
||||
VStack(alignment: .trailing, spacing: 6) {
|
||||
HStack(spacing: 6) {
|
||||
Circle()
|
||||
.fill(getStatusDotColor(context.state.status))
|
||||
.frame(width: 6, height: 6)
|
||||
Text(localizedStatus(context.state.status))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
.padding(.horizontal, 8)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
Text(context.state.subtitle)
|
||||
.font(.caption)
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
} compactLeading: {
|
||||
Image(systemName: context.state.hasTerminal ? "terminal" : "bolt.horizontal.circle")
|
||||
} compactTrailing: {
|
||||
EmptyView()
|
||||
} minimal: {
|
||||
Image(systemName: context.state.hasTerminal ? "terminal" : "bolt.horizontal.circle")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
@available(iOS 16.2, *)
|
||||
struct TerminalLiveActivity_Previews: PreviewProvider {
|
||||
static let attributes = TerminalAttributes(id: "preview")
|
||||
static let contentState = TerminalAttributes.ContentState(
|
||||
id: "preview",
|
||||
title: "root@server-01",
|
||||
subtitle: "CPU 37% • Mem 1.3G/2.0G",
|
||||
status: "Connected",
|
||||
startTime: Date().addingTimeInterval(-1234),
|
||||
hasTerminal: true,
|
||||
connectionCount: 2
|
||||
)
|
||||
|
||||
static var previews: some View {
|
||||
Group {
|
||||
// 锁屏 / 通知样式预览
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .content)
|
||||
.previewDisplayName("Lock Screen")
|
||||
|
||||
// 岛屿展开态预览
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.expanded))
|
||||
.previewDisplayName("Dynamic Island • Expanded")
|
||||
|
||||
// 岛屿紧凑态预览
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.compact))
|
||||
.previewDisplayName("Dynamic Island • Compact")
|
||||
|
||||
// 岛屿最小态预览
|
||||
attributes
|
||||
.previewContext(contentState, viewKind: .dynamicIsland(.minimal))
|
||||
.previewDisplayName("Dynamic Island • Minimal")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
39
ios/StatusWidget/TerminalLiveActivityAttributes.swift
Normal file
39
ios/StatusWidget/TerminalLiveActivityAttributes.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// TerminalLiveActivityAttributes.swift
|
||||
// StatusWidget
|
||||
//
|
||||
// Defines ActivityKit attributes and content state for SSH/Terminal Live Activities.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ActivityKit
|
||||
|
||||
@available(iOS 16.1, *)
|
||||
public struct TerminalAttributes: ActivityAttributes {
|
||||
public struct ContentState: Codable, Hashable {
|
||||
public var id: String
|
||||
public var title: String
|
||||
public var subtitle: String
|
||||
public var status: String
|
||||
public var startTime: Date
|
||||
public var hasTerminal: Bool
|
||||
public var connectionCount: Int
|
||||
|
||||
public init(id: String, title: String, subtitle: String, status: String, startTime: Date, hasTerminal: Bool, connectionCount: Int = 1) {
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.status = status
|
||||
self.startTime = startTime
|
||||
self.hasTerminal = hasTerminal
|
||||
self.connectionCount = connectionCount
|
||||
}
|
||||
}
|
||||
|
||||
public var id: String
|
||||
|
||||
public init(id: String) {
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
8
ios/StatusWidget/de.lproj/Localizable.strings
Normal file
8
ios/StatusWidget/de.lproj/Localizable.strings
Normal file
@@ -0,0 +1,8 @@
|
||||
"Terminal" = "Terminal";
|
||||
"Connected" = "Verbunden";
|
||||
"Connecting" = "Verbindung wird hergestellt";
|
||||
"Disconnected" = "Getrennt";
|
||||
"Multiple SSH sessions active" = "Mehrere aktive SSH-Sitzungen";
|
||||
"1 connection" = "1 Verbindung";
|
||||
"%d connections" = "%d Verbindungen";
|
||||
|
||||
8
ios/StatusWidget/en.lproj/Localizable.strings
Normal file
8
ios/StatusWidget/en.lproj/Localizable.strings
Normal file
@@ -0,0 +1,8 @@
|
||||
"Terminal" = "Terminal";
|
||||
"Connected" = "Connected";
|
||||
"Connecting" = "Connecting";
|
||||
"Disconnected" = "Disconnected";
|
||||
"Multiple SSH sessions active" = "Multiple SSH sessions active";
|
||||
"1 connection" = "1 connection";
|
||||
"%d connections" = "%d connections";
|
||||
|
||||
8
ios/StatusWidget/es.lproj/Localizable.strings
Normal file
8
ios/StatusWidget/es.lproj/Localizable.strings
Normal file
@@ -0,0 +1,8 @@
|
||||
"Terminal" = "Terminal";
|
||||
"Connected" = "Conectado";
|
||||
"Connecting" = "Conectando";
|
||||
"Disconnected" = "Desconectado";
|
||||
"Multiple SSH sessions active" = "Varias sesiones SSH activas";
|
||||
"1 connection" = "1 conexión";
|
||||
"%d connections" = "%d conexiones";
|
||||
|
||||
8
ios/StatusWidget/fr.lproj/Localizable.strings
Normal file
8
ios/StatusWidget/fr.lproj/Localizable.strings
Normal file
@@ -0,0 +1,8 @@
|
||||
"Terminal" = "Terminal";
|
||||
"Connected" = "Connecté";
|
||||
"Connecting" = "Connexion en cours";
|
||||
"Disconnected" = "Déconnecté";
|
||||
"Multiple SSH sessions active" = "Plusieurs sessions SSH actives";
|
||||
"1 connection" = "1 connexion";
|
||||
"%d connections" = "%d connexions";
|
||||
|
||||
8
ios/StatusWidget/id.lproj/Localizable.strings
Normal file
8
ios/StatusWidget/id.lproj/Localizable.strings
Normal file
@@ -0,0 +1,8 @@
|
||||
"Terminal" = "Terminal";
|
||||
"Connected" = "Terhubung";
|
||||
"Connecting" = "Menghubungkan";
|
||||
"Disconnected" = "Terputus";
|
||||
"Multiple SSH sessions active" = "Beberapa sesi SSH aktif";
|
||||
"1 connection" = "1 koneksi";
|
||||
"%d connections" = "%d koneksi";
|
||||
|
||||
8
ios/StatusWidget/ja.lproj/Localizable.strings
Normal file
8
ios/StatusWidget/ja.lproj/Localizable.strings
Normal file
@@ -0,0 +1,8 @@
|
||||
"Terminal" = "ターミナル";
|
||||
"Connected" = "接続済み";
|
||||
"Connecting" = "接続中";
|
||||
"Disconnected" = "切断";
|
||||
"Multiple SSH sessions active" = "複数の SSH セッションがアクティブ";
|
||||
"1 connection" = "1 件の接続";
|
||||
"%d connections" = "%d 件の接続";
|
||||
|
||||
8
ios/StatusWidget/pt-BR.lproj/Localizable.strings
Normal file
8
ios/StatusWidget/pt-BR.lproj/Localizable.strings
Normal file
@@ -0,0 +1,8 @@
|
||||
"Terminal" = "Terminal";
|
||||
"Connected" = "Conectado";
|
||||
"Connecting" = "Conectando";
|
||||
"Disconnected" = "Desconectado";
|
||||
"Multiple SSH sessions active" = "Várias sessões SSH ativas";
|
||||
"1 connection" = "1 conexão";
|
||||
"%d connections" = "%d conexões";
|
||||
|
||||
8
ios/StatusWidget/ru.lproj/Localizable.strings
Normal file
8
ios/StatusWidget/ru.lproj/Localizable.strings
Normal file
@@ -0,0 +1,8 @@
|
||||
"Terminal" = "Терминал";
|
||||
"Connected" = "Подключено";
|
||||
"Connecting" = "Подключение";
|
||||
"Disconnected" = "Отключено";
|
||||
"Multiple SSH sessions active" = "Несколько активных сеансов SSH";
|
||||
"1 connection" = "1 подключение";
|
||||
"%d connections" = "%d подключений";
|
||||
|
||||
8
ios/StatusWidget/zh-Hans.lproj/Localizable.strings
Normal file
8
ios/StatusWidget/zh-Hans.lproj/Localizable.strings
Normal file
@@ -0,0 +1,8 @@
|
||||
"Terminal" = "终端";
|
||||
"Connected" = "已连接";
|
||||
"Connecting" = "连接中";
|
||||
"Disconnected" = "已断开连接";
|
||||
"Multiple SSH sessions active" = "多个 SSH 会话正在活动";
|
||||
"1 connection" = "1 个连接";
|
||||
"%d connections" = "%d 个连接";
|
||||
|
||||
8
ios/StatusWidget/zh-Hant.lproj/Localizable.strings
Normal file
8
ios/StatusWidget/zh-Hant.lproj/Localizable.strings
Normal file
@@ -0,0 +1,8 @@
|
||||
"Terminal" = "終端機";
|
||||
"Connected" = "已連線";
|
||||
"Connecting" = "連線中";
|
||||
"Disconnected" = "已中斷連線";
|
||||
"Multiple SSH sessions active" = "多個 SSH 連線運行中";
|
||||
"1 connection" = "1 個連線";
|
||||
"%d connections" = "%d 個連線";
|
||||
|
||||
Reference in New Issue
Block a user