Files
sing-box-for-apple/ApplicationLibrary/Views/Dashboard/Components/StartStopButton.swift
T
2025-11-27 16:10:55 +08:00

155 lines
5.6 KiB
Swift

import Library
import NetworkExtension
import SwiftUI
@MainActor
public struct StartStopButton: View {
@EnvironmentObject private var environments: ExtensionEnvironments
public init() {}
public var body: some View {
viewBuilder {
if ApplicationLibrary.inPreview {
Button {} label: {
Label("Stop", systemImage: "stop.fill")
}
.labelStyle(.iconOnly)
} else if let profile = environments.extensionProfile {
ToggleConnectionButton().environmentObject(profile)
} else {
Button {} label: {
Label("Start", systemImage: "play.fill")
}
.labelStyle(.iconOnly)
.disabled(true)
}
}
.disabled(environments.emptyProfiles)
}
private struct ToggleConnectionButton: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile
@State private var alert: Alert?
@State private var currentTime = Date()
private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
var body: some View {
Button {
Task {
await switchProfile(!profile.status.isConnected)
}
} label: {
#if os(iOS)
HStack(spacing: 8) {
if showRuntimeDuration, profile.status.isConnectedStrict, let duration = runtimeDuration {
Text(duration)
.font(.caption)
.foregroundStyle(.secondary)
.monospacedDigit()
.fixedSize()
.transition(.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .trailing).combined(with: .opacity)
))
}
if !profile.status.isConnected {
Label("Start", systemImage: "play.fill")
} else {
Label("Stop", systemImage: "stop.fill")
}
}
.animation(.spring(response: 0.35, dampingFraction: 0.75), value: profile.status.isConnectedStrict)
#else
HStack(spacing: 8) {
if profile.status.isConnectedStrict, let duration = runtimeDuration {
Text(duration)
.font(.caption)
.foregroundStyle(.secondary)
.monospacedDigit()
.fixedSize()
.transition(.asymmetric(
insertion: .move(edge: .trailing).combined(with: .opacity),
removal: .move(edge: .trailing).combined(with: .opacity)
))
}
if !profile.status.isConnected {
Label("Start", systemImage: "play.fill")
} else {
Label("Stop", systemImage: "stop.fill")
}
}
.animation(.spring(response: 0.35, dampingFraction: 0.75), value: profile.status.isConnectedStrict)
#endif
}
.labelStyle(.iconOnly)
#if os(iOS)
.modifier(PrimaryTintModifier())
#else
.tint(.primary)
#endif
.disabled(!profile.status.isEnabled)
.alertBinding($alert)
.onReceive(timer) { _ in
currentTime = Date()
}
}
#if os(iOS)
private var showRuntimeDuration: Bool {
if #available(iOS 26.0, *), !Variant.debugNoIOS26 {
return true
}
return false
}
#endif
private var runtimeDuration: String? {
guard let connectedDate = profile.connectedDate else { return nil }
let interval = currentTime.timeIntervalSince(connectedDate)
guard interval >= 0 else { return nil }
let hours = Int(interval) / 3600
let minutes = Int(interval) / 60 % 60
let seconds = Int(interval) % 60
if hours > 0 {
return String(format: "%d:%02d:%02d", hours, minutes, seconds)
} else {
return String(format: "%d:%02d", minutes, seconds)
}
}
private nonisolated func switchProfile(_ isEnabled: Bool) async {
do {
if isEnabled {
try await profile.start()
await environments.commandClient.connect()
} else {
try await profile.stop()
}
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
}
}
#if os(iOS)
private struct PrimaryTintModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(iOS 26.0, *), !Variant.debugNoIOS26 {
content.tint(.primary)
} else {
content
}
}
}
#endif