add swiftUI code
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct ActiveDashboardView: View {
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@Environment(\.selection) private var parentSelection
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@EnvironmentObject private var profile: ExtensionProfile
|
||||
@State private var isLoading = true
|
||||
@State private var profileList: [ProfilePreview] = []
|
||||
@State private var selectedProfileID: Int64 = 0
|
||||
@State private var alert: Alert?
|
||||
@State private var selection = DashboardPage.overview
|
||||
@State private var systemProxyAvailable = false
|
||||
@State private var systemProxyEnabled = false
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
if isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task {
|
||||
await doReload()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ApplicationLibrary.inPreview {
|
||||
body1
|
||||
} else {
|
||||
body1
|
||||
.onAppear {
|
||||
Task {
|
||||
await doReloadSystemProxy()
|
||||
}
|
||||
}
|
||||
.onChangeCompat(of: profile.status) { newStatus in
|
||||
if newStatus == .connected {
|
||||
Task {
|
||||
await doReloadSystemProxy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var body1: some View {
|
||||
VStack {
|
||||
#if os(iOS) || os(tvOS)
|
||||
if ApplicationLibrary.inPreview || profile.status.isConnectedStrict {
|
||||
Picker("Page", selection: $selection) {
|
||||
ForEach(DashboardPage.allCases) { page in
|
||||
page.label
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
#if os(iOS)
|
||||
.padding([.leading, .trailing])
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
TabView(selection: $selection) {
|
||||
ForEach(DashboardPage.allCases) { page in
|
||||
page.contentView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
.tag(page)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .always))
|
||||
} else {
|
||||
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
}
|
||||
#elseif os(macOS)
|
||||
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
#endif
|
||||
}
|
||||
.onReceive(environments.profileUpdate) { _ in
|
||||
Task {
|
||||
await doReload()
|
||||
}
|
||||
}
|
||||
.onReceive(environments.selectedProfileUpdate) { _ in
|
||||
Task {
|
||||
selectedProfileID = await SharedPreferences.selectedProfileID.get()
|
||||
if profile.status.isConnected {
|
||||
await doReloadSystemProxy()
|
||||
}
|
||||
}
|
||||
}
|
||||
.alertBinding($alert)
|
||||
}
|
||||
|
||||
private func doReload() async {
|
||||
defer {
|
||||
isLoading = false
|
||||
}
|
||||
if ApplicationLibrary.inPreview {
|
||||
profileList = [
|
||||
ProfilePreview(Profile(id: 0, name: "profile local", type: .local, path: "")),
|
||||
ProfilePreview(Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0))),
|
||||
]
|
||||
systemProxyAvailable = true
|
||||
systemProxyEnabled = true
|
||||
selectedProfileID = 0
|
||||
|
||||
} else {
|
||||
do {
|
||||
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
|
||||
if profileList.isEmpty {
|
||||
return
|
||||
}
|
||||
selectedProfileID = await SharedPreferences.selectedProfileID.get()
|
||||
if profileList.filter({ profile in
|
||||
profile.id == selectedProfileID
|
||||
})
|
||||
.isEmpty {
|
||||
selectedProfileID = profileList[0].id
|
||||
await SharedPreferences.selectedProfileID.set(selectedProfileID)
|
||||
}
|
||||
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
environments.emptyProfiles = profileList.isEmpty
|
||||
}
|
||||
|
||||
private nonisolated func doReloadSystemProxy() async {
|
||||
do {
|
||||
let status = try LibboxNewStandaloneCommandClient()!.getSystemProxyStatus()
|
||||
await MainActor.run {
|
||||
systemProxyAvailable = status.available
|
||||
systemProxyEnabled = status.enabled
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
//
|
||||
// ActiveDashboardViewMACOS.swift
|
||||
// ApplicationLibrary
|
||||
//
|
||||
// Created by Mac on 2024/10/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct ActiveDashboardViewMACOS: View {
|
||||
@Environment(\.scenePhase) var scenePhase
|
||||
@Environment(\.selection) private var parentSelection
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@EnvironmentObject private var profile: ExtensionProfile
|
||||
@State private var isLoading = true
|
||||
@State private var profileList: [ProfilePreview] = []
|
||||
@State private var selectedProfileID: Int64 = 0
|
||||
@State private var alert: Alert?
|
||||
@State private var selection = DashboardPage.overview
|
||||
@State private var systemProxyAvailable = false
|
||||
@State private var systemProxyEnabled = false
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
if isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task {
|
||||
await doReload()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ApplicationLibrary.inPreview {
|
||||
body1
|
||||
} else {
|
||||
body1
|
||||
.onAppear {
|
||||
Task {
|
||||
await doReloadSystemProxy()
|
||||
}
|
||||
}
|
||||
.onChangeCompat(of: profile.status) { newStatus in
|
||||
if newStatus == .connected {
|
||||
Task {
|
||||
await doReloadSystemProxy()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var body1: some View {
|
||||
VStack {
|
||||
#if os(iOS) || os(tvOS)
|
||||
if ApplicationLibrary.inPreview || profile.status.isConnectedStrict {
|
||||
/* Picker("Page", selection: $selection) {
|
||||
ForEach(DashboardPage.allCases) { page in
|
||||
page.label
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
#if os(iOS)
|
||||
.padding([.leading, .trailing])
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
TabView(selection: $selection) {
|
||||
ForEach(DashboardPage.allCases) { page in
|
||||
page.contentView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
.tag(page)
|
||||
}
|
||||
}
|
||||
.tabViewStyle(.page(indexDisplayMode: .always))
|
||||
*/
|
||||
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
} else {
|
||||
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
}
|
||||
#elseif os(macOS)
|
||||
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
|
||||
#endif
|
||||
}
|
||||
.onReceive(environments.profileUpdate) { _ in
|
||||
Task {
|
||||
await doReload()
|
||||
}
|
||||
}
|
||||
.onReceive(environments.selectedProfileUpdate) { _ in
|
||||
Task {
|
||||
selectedProfileID = await SharedPreferences.selectedProfileID.get()
|
||||
if profile.status.isConnected {
|
||||
await doReloadSystemProxy()
|
||||
}
|
||||
}
|
||||
}
|
||||
.alertBinding($alert)
|
||||
}
|
||||
|
||||
private func doReload() async {
|
||||
defer {
|
||||
isLoading = false
|
||||
}
|
||||
if ApplicationLibrary.inPreview {
|
||||
profileList = [
|
||||
ProfilePreview(Profile(id: 0, name: "profile local", type: .local, path: "")),
|
||||
ProfilePreview(Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0))),
|
||||
]
|
||||
systemProxyAvailable = true
|
||||
systemProxyEnabled = true
|
||||
selectedProfileID = 0
|
||||
|
||||
} else {
|
||||
do {
|
||||
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
|
||||
if profileList.isEmpty {
|
||||
//add profile remote
|
||||
|
||||
Task {
|
||||
if StoreManager.shared.getSuburlData().count > 5 {
|
||||
await createProfile()
|
||||
}
|
||||
|
||||
}
|
||||
await environments.reload()
|
||||
|
||||
return
|
||||
}
|
||||
selectedProfileID = await SharedPreferences.selectedProfileID.get()
|
||||
if profileList.filter({ profile in
|
||||
profile.id == selectedProfileID
|
||||
})
|
||||
.isEmpty {
|
||||
selectedProfileID = profileList[0].id
|
||||
await SharedPreferences.selectedProfileID.set(selectedProfileID)
|
||||
}
|
||||
|
||||
|
||||
// try await ProfileManager.list().forEach { p in
|
||||
// await ProfileManager.update(p)
|
||||
// }
|
||||
|
||||
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
return
|
||||
}
|
||||
}
|
||||
environments.emptyProfiles = profileList.isEmpty
|
||||
}
|
||||
|
||||
private nonisolated func doReloadSystemProxy() async {
|
||||
do {
|
||||
let status = try LibboxNewStandaloneCommandClient()!.getSystemProxyStatus()
|
||||
await MainActor.run {
|
||||
systemProxyAvailable = status.available
|
||||
systemProxyEnabled = status.enabled
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func createProfile() async {
|
||||
do {
|
||||
try await createProfileBackground()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
environments.profileUpdate.send()
|
||||
}
|
||||
|
||||
private nonisolated func createProfileBackground() async throws {
|
||||
let nextProfileID = try await ProfileManager.nextID()
|
||||
|
||||
var savePath = ""
|
||||
let remoteURL: String = StoreManager.shared.getSuburlData()
|
||||
var lastUpdated: Date? = nil
|
||||
let remoteContent = try HTTPClient().getString(remoteURL)
|
||||
var error: NSError?
|
||||
LibboxCheckConfig(remoteContent, &error)
|
||||
if let error {
|
||||
throw error
|
||||
}
|
||||
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
|
||||
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
|
||||
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
|
||||
try remoteContent.write(to: profileConfig, atomically: true, encoding: .utf8)
|
||||
savePath = profileConfig.relativePath
|
||||
|
||||
lastUpdated = .now
|
||||
|
||||
try await ProfileManager.create(Profile(
|
||||
name: "VPN",
|
||||
type: ProfileType.remote,
|
||||
path: savePath,
|
||||
remoteURL: remoteURL,
|
||||
autoUpdate: true,
|
||||
autoUpdateInterval: 60,
|
||||
lastUpdated: lastUpdated
|
||||
))
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
try UIProfileUpdateTask.configure()
|
||||
let list = try await ProfileManager.list()
|
||||
list.forEach { profileOne in
|
||||
print("\(profileOne.path) \n \(profileOne.remoteURL ?? "") \n \(profileOne.id ?? 0 )")
|
||||
}
|
||||
#else
|
||||
try await ProfileUpdateTask.configure()
|
||||
#endif
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct ClashModeView: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@StateObject private var commandClient = CommandClient(.clashMode)
|
||||
@State private var clashMode = ""
|
||||
@State private var alert: Alert?
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
VStack {
|
||||
|
||||
if commandClient.clashModeList.count > 1 {
|
||||
Picker("", selection: Binding(get: {
|
||||
clashMode
|
||||
}, set: { newMode in
|
||||
clashMode = newMode
|
||||
Task {
|
||||
await setClashMode(newMode)
|
||||
await restartVPNServer()
|
||||
}
|
||||
}), content: {
|
||||
ForEach(commandClient.clashModeList, id: \.self) { it in
|
||||
/*if it.lowercased() == "rule" {
|
||||
Text("规则模式")
|
||||
}else if it.lowercased() == "direct" {
|
||||
Text("直连模式")
|
||||
}else if it.lowercased() == "global" {
|
||||
Text("全局模式")
|
||||
}else{
|
||||
|
||||
}*/
|
||||
Text(it)
|
||||
|
||||
}
|
||||
})
|
||||
.pickerStyle(.segmented)
|
||||
.padding([.top], 8)
|
||||
}else{
|
||||
//DEBUG
|
||||
/*
|
||||
let newclashModeList = ["规则模式","直连模式","全局模式"]
|
||||
Picker("", selection: Binding(get: {
|
||||
clashMode
|
||||
}, set: { newMode in
|
||||
|
||||
}), content: {
|
||||
ForEach(newclashModeList, id: \.self) { it in
|
||||
/*if it.lowercased() == "rule" {
|
||||
Text("规则模式")
|
||||
}else if it.lowercased() == "direct" {
|
||||
Text("直连模式")
|
||||
}else if it.lowercased() == "global" {
|
||||
Text("全局模式")
|
||||
}else{
|
||||
Text(it)
|
||||
}*/
|
||||
|
||||
Text(it)
|
||||
|
||||
}
|
||||
})
|
||||
.pickerStyle(.segmented)
|
||||
.padding([.top], 8)
|
||||
*/
|
||||
}
|
||||
|
||||
}
|
||||
.onReceive(commandClient.$clashMode) { newMode in
|
||||
clashMode = newMode
|
||||
}
|
||||
.padding([.leading, .trailing])
|
||||
.onAppear {
|
||||
commandClient.connect()
|
||||
}
|
||||
.onDisappear {
|
||||
commandClient.disconnect()
|
||||
}
|
||||
.onChangeCompat(of: scenePhase) { newValue in
|
||||
print("\(String(describing: commandClient.status))")
|
||||
if newValue == .active {
|
||||
commandClient.connect()
|
||||
} else {
|
||||
commandClient.disconnect()
|
||||
}
|
||||
}
|
||||
.alertBinding($alert)
|
||||
}
|
||||
|
||||
private nonisolated func setClashMode(_ newMode: String) async {
|
||||
do {
|
||||
try LibboxNewStandaloneCommandClient()!.setClashMode(newMode)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func restartVPNServer() async {
|
||||
do {
|
||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
||||
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import Foundation
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
public enum DashboardPage: Int, CaseIterable, Identifiable {
|
||||
public var id: Self {
|
||||
self
|
||||
}
|
||||
|
||||
case overview
|
||||
case groups
|
||||
}
|
||||
|
||||
public extension DashboardPage {
|
||||
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .overview:
|
||||
return NSLocalizedString("Overview", comment: "")
|
||||
case .groups:
|
||||
return NSLocalizedString("Groups", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var label: some View {
|
||||
switch self {
|
||||
case .overview:
|
||||
return Label("Overview", systemImage: "text.and.command.macwindow")
|
||||
case .groups:
|
||||
return Label("Groups", systemImage: "rectangle.3.group.fill")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func contentView(_ profileList: Binding<[ProfilePreview]>, _ selectedProfileID: Binding<Int64>, _ systemProxyAvailable: Binding<Bool>, _ systemProxyEnabled: Binding<Bool>) -> some View {
|
||||
viewBuilder {
|
||||
switch self {
|
||||
case .overview:
|
||||
OverviewView(profileList, selectedProfileID, systemProxyAvailable, systemProxyEnabled)
|
||||
case .groups:
|
||||
GroupListView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct DashboardView: View {
|
||||
#if os(macOS)
|
||||
@Environment(\.controlActiveState) private var controlActiveState
|
||||
@State private var isLoading = true
|
||||
@State private var systemExtensionInstalled = true
|
||||
#endif
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
viewBuilder {
|
||||
#if os(macOS)
|
||||
HStack{
|
||||
|
||||
|
||||
|
||||
Button {
|
||||
withAnimation{
|
||||
// isUserViewActive.toggle()
|
||||
}
|
||||
} label: {
|
||||
|
||||
Image(systemName: "circle.grid.cross")
|
||||
.font(.title2)
|
||||
.padding(5)
|
||||
.foregroundColor(.white)
|
||||
.background(
|
||||
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(.white.opacity(0.25),lineWidth: 1)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
/* Button {
|
||||
withAnimation{
|
||||
isSubscriptionActive.toggle()
|
||||
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "slider.horizontal.3")
|
||||
.font(.title2)
|
||||
.padding(12)
|
||||
.background(
|
||||
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(.white.opacity(0.25),lineWidth: 1)
|
||||
)
|
||||
} */
|
||||
|
||||
|
||||
// Text Bubble
|
||||
Button {
|
||||
withAnimation{
|
||||
// isSubscriptionActive.toggle()
|
||||
|
||||
}
|
||||
} label: {
|
||||
|
||||
Text("超值折扣")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 6)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.orange)
|
||||
)
|
||||
.overlay(
|
||||
TriangleShape()
|
||||
.fill(Color.orange)
|
||||
.frame(width: 10, height: 10)
|
||||
.offset(x: 10)
|
||||
, alignment: .trailing
|
||||
)
|
||||
|
||||
// Panda Image with VIP Tag
|
||||
HStack(spacing: 10){
|
||||
Text(verbatim: "")
|
||||
Image("applogo") // Replace with actual image asset
|
||||
.resizable()
|
||||
.frame(width: 30, height: 30)
|
||||
.cornerRadius(15)
|
||||
|
||||
// Text("VIP")
|
||||
// .font(.caption)
|
||||
// .fontWeight(.bold)
|
||||
// .foregroundColor(.yellow)
|
||||
// .padding(4)
|
||||
// .background(Color.orange)
|
||||
// .cornerRadius(4)
|
||||
}
|
||||
|
||||
}
|
||||
.padding()
|
||||
|
||||
Button {
|
||||
withAnimation{
|
||||
// isKefuActive.toggle()
|
||||
}
|
||||
} label: {
|
||||
|
||||
Image(systemName: "person.fill.questionmark")
|
||||
.font(.title2)
|
||||
.padding(5)
|
||||
.foregroundColor(.white)
|
||||
.background(
|
||||
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(.white.opacity(0.25),lineWidth: 1)
|
||||
)
|
||||
Text("客服").foregroundColor(.white)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if Variant.useSystemExtension {
|
||||
viewBuilder {
|
||||
if !systemExtensionInstalled {
|
||||
FormView {
|
||||
InstallSystemExtensionButton {
|
||||
await reload()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DashboardView0()
|
||||
}
|
||||
}.onAppear {
|
||||
Task {
|
||||
await reload()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DashboardView0()
|
||||
}
|
||||
#else
|
||||
DashboardView0()
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
.onChangeCompat(of: controlActiveState) { newValue in
|
||||
if newValue != .inactive {
|
||||
if Variant.useSystemExtension {
|
||||
if !isLoading {
|
||||
Task {
|
||||
await reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
|
||||
struct TriangleShape: Shape {
|
||||
func path(in rect: CGRect) -> Path {
|
||||
var path = Path()
|
||||
path.move(to: CGPoint(x: rect.minX, y: rect.minY))
|
||||
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
|
||||
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
|
||||
path.closeSubpath()
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#if os(macOS)
|
||||
private nonisolated func reload() async {
|
||||
let systemExtensionInstalled = await SystemExtension.isInstalled()
|
||||
await MainActor.run {
|
||||
self.systemExtensionInstalled = systemExtensionInstalled
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
struct DashboardView0: View {
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
|
||||
var body: some View {
|
||||
if ApplicationLibrary.inPreview {
|
||||
// ActiveDashboardView()/
|
||||
ActiveDashboardViewMACOS()
|
||||
} else if environments.extensionProfileLoading {
|
||||
ProgressView()
|
||||
} else if let profile = environments.extensionProfile {
|
||||
DashboardView1().environmentObject(profile)
|
||||
} else {
|
||||
FormView {
|
||||
|
||||
InstallProfileButton { error in
|
||||
//add profile remote
|
||||
|
||||
await environments.reload()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct DashboardView1: View {
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@EnvironmentObject private var profile: ExtensionProfile
|
||||
@State private var alert: Alert?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
// ActiveDashboardView()
|
||||
ActiveDashboardViewMACOS()
|
||||
}
|
||||
.alertBinding($alert)
|
||||
.onChangeCompat(of: profile.status) { newValue in
|
||||
if newValue == .disconnecting || newValue == .connected {
|
||||
Task {
|
||||
await checkServiceError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func checkServiceError() async {
|
||||
var error: NSError?
|
||||
let message = LibboxReadServiceError(&error)
|
||||
if error != nil {
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
alert = Alert(title: Text("Service Error"), message: Text(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// DashboardViewMacOS.swift
|
||||
// ApplicationLibrary
|
||||
//
|
||||
// Created by Mac on 2024/10/22.
|
||||
//
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct DashboardViewMacOS: View {
|
||||
#if os(macOS)
|
||||
@Environment(\.controlActiveState) private var controlActiveState
|
||||
@State private var isLoading = true
|
||||
@State private var systemExtensionInstalled = true
|
||||
#endif
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
viewBuilder {
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
viewBuilder {
|
||||
if !systemExtensionInstalled {
|
||||
FormView {
|
||||
InstallSystemExtensionButton {
|
||||
await reload()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DashboardView0()
|
||||
|
||||
}
|
||||
}.onAppear {
|
||||
Task {
|
||||
await reload()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DashboardView0()
|
||||
}
|
||||
#else
|
||||
DashboardView0()
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
.onChangeCompat(of: controlActiveState) { newValue in
|
||||
if newValue != .inactive {
|
||||
if Variant.useSystemExtension {
|
||||
if !isLoading {
|
||||
Task {
|
||||
await reload()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
private nonisolated func reload() async {
|
||||
let systemExtensionInstalled = await SystemExtension.isInstalled()
|
||||
await MainActor.run {
|
||||
self.systemExtensionInstalled = systemExtensionInstalled
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
struct DashboardView0: View {
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
|
||||
var body: some View {
|
||||
if ApplicationLibrary.inPreview {
|
||||
ActiveDashboardView()
|
||||
} else if environments.extensionProfileLoading {
|
||||
ProgressView()
|
||||
} else if let profile = environments.extensionProfile {
|
||||
DashboardView1().environmentObject(profile)
|
||||
} else {
|
||||
FormView {
|
||||
|
||||
InstallProfileButton {
|
||||
//add profile remote
|
||||
|
||||
await environments.reload()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
struct DashboardView1: View {
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@EnvironmentObject private var profile: ExtensionProfile
|
||||
@State private var alert: Alert?
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
ActiveDashboardView()
|
||||
|
||||
}
|
||||
.alertBinding($alert)
|
||||
.onChangeCompat(of: profile.status) { newValue in
|
||||
if newValue == .disconnecting || newValue == .connected {
|
||||
Task {
|
||||
await checkServiceError()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nonisolated func checkServiceError() async {
|
||||
var error: NSError?
|
||||
let message = LibboxReadServiceError(&error)
|
||||
if error != nil {
|
||||
return
|
||||
}
|
||||
await MainActor.run {
|
||||
alert = Alert(title: Text("Service Error"), message: Text(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
public struct ExtensionStatusView: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@StateObject private var commandClient = CommandClient(.status)
|
||||
@State private var columnCount: Int = 4
|
||||
@State private var alert: Alert?
|
||||
|
||||
private let infoFont = Font.system(.caption, design: .monospaced)
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
if columnCount == 1 {
|
||||
ScrollView {
|
||||
body0
|
||||
}
|
||||
} else {
|
||||
body0
|
||||
}
|
||||
}
|
||||
|
||||
public var body0: some View {
|
||||
viewBuilder {
|
||||
VStack {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columnCount), alignment: .leading) {
|
||||
if ApplicationLibrary.inPreview {
|
||||
StatusItem("Status") {
|
||||
StatusLine("Memory", "6.4 MB")
|
||||
StatusLine("Goroutines", "89")
|
||||
}
|
||||
StatusItem("Connections") {
|
||||
StatusLine("Inbound", "34")
|
||||
StatusLine("Outbound", "28")
|
||||
}
|
||||
StatusItem("Traffic") {
|
||||
StatusLine("Uplink", "38 B/s")
|
||||
StatusLine("Downlink", "249 MB/s")
|
||||
}
|
||||
StatusItem("TrafficTotal") {
|
||||
StatusLine("Uplink", "52 MB")
|
||||
StatusLine("Downlink", "5.6 GB")
|
||||
}
|
||||
} else if let message = commandClient.status {
|
||||
// StatusItem("Status") {
|
||||
// StatusLine("Memory", LibboxFormatMemoryBytes(message.memory))
|
||||
// StatusLine("Goroutines", "\(message.goroutines)")
|
||||
// }
|
||||
// StatusItem("Connections") {
|
||||
// StatusLine("Inbound", "\(message.connectionsIn)")
|
||||
// StatusLine("Outbound", "\(message.connectionsOut)")
|
||||
// }
|
||||
if message.trafficAvailable {
|
||||
// StatusItem("网速") {
|
||||
// StatusLine("上传", "\(LibboxFormatBytes(message.uplink))/s")
|
||||
// StatusLine("下载", "\(LibboxFormatBytes(message.downlink))/s")
|
||||
// }
|
||||
StatusItem("总流量") {
|
||||
HStack(spacing: 10, content: {
|
||||
StatusLine("上传", LibboxFormatBytes(message.uplinkTotal))
|
||||
Spacer()
|
||||
StatusLine("下载", LibboxFormatBytes(message.downlinkTotal))
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StatusItem("总流量") {
|
||||
HStack(spacing: 10, content: {
|
||||
StatusLine("上传", ".../s")
|
||||
Spacer()
|
||||
StatusLine("下载", ".../s")
|
||||
})
|
||||
}
|
||||
// StatusItem("Connections") {
|
||||
// StatusLine("Inbound", "...")
|
||||
// StatusLine("Outbound", "...")
|
||||
// }
|
||||
}
|
||||
}.background {
|
||||
GeometryReader { geometry in
|
||||
Rectangle()
|
||||
.fill(.clear)
|
||||
.frame(height: 1)
|
||||
.onChangeCompat(of: geometry.size.width) { newValue in
|
||||
updateColumnCount(newValue)
|
||||
}
|
||||
.onAppear {
|
||||
updateColumnCount(geometry.size.width)
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
.frame(alignment: .topLeading)
|
||||
.padding([.top, .leading, .trailing])
|
||||
}
|
||||
.onAppear {
|
||||
commandClient.connect()
|
||||
}
|
||||
.onDisappear {
|
||||
commandClient.disconnect()
|
||||
}
|
||||
.onChangeCompat(of: scenePhase) { newValue in
|
||||
if newValue == .active {
|
||||
commandClient.connect()
|
||||
} else {
|
||||
commandClient.disconnect()
|
||||
}
|
||||
}
|
||||
.alertBinding($alert)
|
||||
}
|
||||
|
||||
private func updateColumnCount(_ width: Double) {
|
||||
|
||||
let v = Int(Int(width) / 155)
|
||||
let new = v <= 1 ? 1 : (v > 4 ? 4 : (v % 2 == 0 ? v : v - 1))
|
||||
|
||||
if new != columnCount {
|
||||
columnCount = new
|
||||
}
|
||||
|
||||
//固定只有一行显示。
|
||||
print(columnCount)
|
||||
columnCount = 1
|
||||
}
|
||||
|
||||
private struct StatusItem<T>: View where T: View {
|
||||
private let title: String
|
||||
@ViewBuilder private let content: () -> T
|
||||
|
||||
init(_ title: String, @ViewBuilder content: @escaping () -> T) {
|
||||
self.title = title
|
||||
self.content = content
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Text(title)
|
||||
.font(.subheadline)
|
||||
Spacer()
|
||||
}.padding(.bottom, 8)
|
||||
content()
|
||||
}
|
||||
.frame(minWidth: 125, alignment: .topLeading)
|
||||
#if os(tvOS)
|
||||
.padding(EdgeInsets(top: 20, leading: 26, bottom: 20, trailing: 26))
|
||||
#else
|
||||
.padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13))
|
||||
#endif
|
||||
.background(backgroundColor.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
private var backgroundColor: Color {
|
||||
#if os(iOS)
|
||||
return Color(uiColor: .secondarySystemGroupedBackground)
|
||||
#elseif os(macOS)
|
||||
return Color(nsColor: .textBackgroundColor)
|
||||
#elseif os(tvOS)
|
||||
return Color(uiColor: .black)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private struct StatusLine: View {
|
||||
private let name: String
|
||||
private let value: String
|
||||
|
||||
init(_ name: String, _ value: String) {
|
||||
self.name = name
|
||||
self.value = value
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(name)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
Text(value)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct InstallProfileButton: View {
|
||||
|
||||
private let callback: (Error?) async -> Void
|
||||
public init(_ callback: @escaping ((Error?) async -> Void)) {
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
parsePowerButton()
|
||||
|
||||
/* FormButton {
|
||||
Task {
|
||||
await installProfile()
|
||||
}
|
||||
} label: {
|
||||
Label("安装VPN网络扩展", systemImage: "lock.doc.fill")
|
||||
.font(.title3)
|
||||
.padding(15)
|
||||
.foregroundColor(.red)
|
||||
.background(
|
||||
|
||||
RoundedRectangle(cornerRadius: 10)
|
||||
.strokeBorder(.white.opacity(0.25),lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.alertBinding($alert)*/
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
func parsePowerButton()->some View{
|
||||
|
||||
Button {
|
||||
|
||||
Task {
|
||||
await installProfile()
|
||||
}
|
||||
} label: {
|
||||
|
||||
ZStack{
|
||||
|
||||
ZStack{
|
||||
//
|
||||
// LottieView(animationFileName: "1d2a0fe5" , loopMode: .loop)
|
||||
LottieView(animationFileName: "8dfa14a6" , loopMode: .loop).scaleEffect(0.2)
|
||||
Text("开启服务").bold().font(.title2).foregroundStyle(Color.white)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
// Max Frame...
|
||||
.frame(width: 190,height: 190)
|
||||
|
||||
}.padding()
|
||||
|
||||
|
||||
}
|
||||
|
||||
private func installProfile() async {
|
||||
|
||||
print("installProfile...")
|
||||
do {
|
||||
try await ExtensionProfile.install()
|
||||
await callback(nil)
|
||||
|
||||
|
||||
} catch {
|
||||
print("installProfile: \(error.localizedDescription)")
|
||||
await callback(error)
|
||||
// alert = Alert(error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
#if os(macOS)
|
||||
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct InstallSystemExtensionButton: View {
|
||||
@State private var alert: Alert?
|
||||
private let callback: () async -> Void
|
||||
public init(_ callback: @escaping () async -> Void) {
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
FormButton {
|
||||
Task {
|
||||
await installSystemExtension()
|
||||
}
|
||||
} label: {
|
||||
Label("Install System Extension", systemImage: "lock.doc.fill")
|
||||
}
|
||||
.alertBinding($alert)
|
||||
}
|
||||
|
||||
private func installSystemExtension() async {
|
||||
do {
|
||||
if let result = try await SystemExtension.install() {
|
||||
if result == .willCompleteAfterReboot {
|
||||
alert = Alert(errorMessage: "Need Reboot")
|
||||
}
|
||||
}
|
||||
await callback()
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,296 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct OverviewView: View {
|
||||
@Environment(\.selection) private var selection
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@EnvironmentObject private var profile: ExtensionProfile
|
||||
@Binding private var profileList: [ProfilePreview]
|
||||
@Binding private var selectedProfileID: Int64
|
||||
@Binding private var systemProxyAvailable: Bool
|
||||
@Binding private var systemProxyEnabled: Bool
|
||||
@State private var alert: Alert?
|
||||
@State private var reasserting = false
|
||||
|
||||
private var selectedProfileIDLocal: Binding<Int64> {
|
||||
$selectedProfileID.withSetter { newValue in
|
||||
reasserting = true
|
||||
Task { [self] in
|
||||
await switchProfile(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public init(_ profileList: Binding<[ProfilePreview]>, _ selectedProfileID: Binding<Int64>, _ systemProxyAvailable: Binding<Bool>, _ systemProxyEnabled: Binding<Bool>) {
|
||||
_profileList = profileList
|
||||
_selectedProfileID = selectedProfileID
|
||||
_systemProxyAvailable = systemProxyAvailable
|
||||
_systemProxyEnabled = systemProxyEnabled
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
if profileList.isEmpty
|
||||
{
|
||||
VStack {
|
||||
|
||||
parsePowerButton()
|
||||
|
||||
}.alertBinding($alert)
|
||||
|
||||
}else{
|
||||
VStack {
|
||||
#if os(iOS) || os(tvOS)
|
||||
StartStopButton()
|
||||
#elseif os(macOS)
|
||||
StartStopButton().buttonStyle(PlainButtonStyle()) // 移除按钮的默认样式
|
||||
#endif
|
||||
|
||||
if ApplicationLibrary.inPreview || profile.status.isConnected {
|
||||
ClashModeView()
|
||||
ExtensionStatusView()
|
||||
Spacer()
|
||||
}
|
||||
}.alertBinding($alert)
|
||||
.disabled(!ApplicationLibrary.inPreview && (!profile.status.isSwitchable || reasserting))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func parsePowerButton()->some View{
|
||||
VStack{
|
||||
Button {
|
||||
|
||||
|
||||
} label: {
|
||||
|
||||
ZStack{
|
||||
|
||||
ZStack{
|
||||
|
||||
LottieView(animationFileName: "1d2a0fe5" , loopMode: .playOnce).scaleEffect(0.2)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
// Max Frame...
|
||||
.frame(width: 190,height: 190)
|
||||
.background(
|
||||
|
||||
ZStack{
|
||||
|
||||
// Rings....
|
||||
Circle()
|
||||
.trim(from: 0.3, to: 0.5)
|
||||
.stroke(
|
||||
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color("gray"),
|
||||
Color("gray")
|
||||
.opacity(0.5),
|
||||
Color("gray")
|
||||
.opacity(0.3),
|
||||
Color( "gray")
|
||||
.opacity(0.1),
|
||||
|
||||
], startPoint: .leading, endPoint: .trailing),
|
||||
|
||||
style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round)
|
||||
)
|
||||
// Shadows...
|
||||
.shadow(color: Color( "gray"), radius: 5, x: 1, y: -4)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0.3, to: 0.55)
|
||||
.stroke(
|
||||
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color( "gray2"),
|
||||
Color( "gray2")
|
||||
.opacity(0.5),
|
||||
Color("gray2")
|
||||
.opacity(0.3),
|
||||
Color( "gray2")
|
||||
.opacity(0.1),
|
||||
|
||||
], startPoint: .leading, endPoint: .trailing),
|
||||
|
||||
style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round)
|
||||
)
|
||||
// Shadows...
|
||||
.shadow(color: Color( "gray2"), radius: 5, x: 1, y: -4)
|
||||
.rotationEffect(.init(degrees: 160))
|
||||
|
||||
// Main Little Ring...
|
||||
Circle()
|
||||
.stroke(
|
||||
|
||||
Color("Ring1")
|
||||
.opacity(0.01),
|
||||
lineWidth: 11
|
||||
)
|
||||
// Toggling Shadow when button is Clicked...
|
||||
.shadow(color: Color("Ring2").opacity( 0), radius: 5, x: 1, y: -4)
|
||||
}
|
||||
|
||||
|
||||
)
|
||||
|
||||
}.padding().disabled(true)
|
||||
// .padding(.top,UIScreen.main.bounds.height < 750 ? 30 : 100)
|
||||
Text("正在初始化...").font(.system(size: 18, weight: .semibold)).foregroundStyle(.white)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public var bodyOld: some View {
|
||||
VStack {
|
||||
|
||||
if profileList.isEmpty {
|
||||
VStack{
|
||||
LottieView(animationFileName: "8c31d47d" , loopMode: .loop)
|
||||
} .aspectRatio(contentMode: .fill)
|
||||
.scaleEffect(0.1).frame(width: 190,height: 190).padding()
|
||||
// VStack{
|
||||
// LottieView(animationFileName: "51a05581" , loopMode: .loop)
|
||||
// }.padding()
|
||||
// Spacer()
|
||||
|
||||
//Text("Empty profiles")
|
||||
//retry download profiles
|
||||
|
||||
} else {
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
StartStopButton()
|
||||
/**
|
||||
Picker("Profile", selection: selectedProfileIDLocal, content: {
|
||||
ForEach(profileList, id: \.self) { it in
|
||||
Text(it.name).tag(it.id)
|
||||
|
||||
}
|
||||
})
|
||||
.pickerStyle(.segmented)
|
||||
.padding([.top], 8)
|
||||
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// Section("Profile") {
|
||||
// Picker(selection: selectedProfileIDLocal) {
|
||||
// ForEach(profileList, id: \.id) { profile in
|
||||
// Text(profile.name).tag(profile.id)
|
||||
// }
|
||||
// } label: {}
|
||||
// .pickerStyle(.inline)
|
||||
// }
|
||||
|
||||
|
||||
#elseif os(macOS)
|
||||
StartStopButton().buttonStyle(PlainButtonStyle()) // 移除按钮的默认样式
|
||||
|
||||
/*if ApplicationLibrary.inPreview || profile.status.isConnectedStrict, systemProxyAvailable {
|
||||
Toggle("HTTP Proxy", isOn: $systemProxyEnabled)
|
||||
.onChangeCompat(of: systemProxyEnabled) { newValue in
|
||||
Task {
|
||||
await setSystemProxyEnabled(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section("Profile") {
|
||||
ForEach(profileList, id: \.id) { profile in
|
||||
Picker(profile.name, selection: selectedProfileIDLocal) {
|
||||
Text("").tag(profile.id)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.radioGroup)
|
||||
}*/
|
||||
#endif
|
||||
/*
|
||||
FormView {
|
||||
#if os(iOS) || os(tvOS)
|
||||
StartStopButton()
|
||||
if ApplicationLibrary.inPreview || profile.status.isConnectedStrict, systemProxyAvailable {
|
||||
Toggle("HTTP Proxy", isOn: $systemProxyEnabled)
|
||||
.onChangeCompat(of: systemProxyEnabled) { newValue in
|
||||
Task {
|
||||
await setSystemProxyEnabled(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// Profile订阅列表
|
||||
Section("Profile") {
|
||||
Picker(selection: selectedProfileIDLocal) {
|
||||
ForEach(profileList, id: \.id) { profile in
|
||||
Text(profile.name).tag(profile.id)
|
||||
}
|
||||
} label: {}
|
||||
.pickerStyle(.inline)
|
||||
}*/
|
||||
#elseif os(macOS)
|
||||
if ApplicationLibrary.inPreview || profile.status.isConnectedStrict, systemProxyAvailable {
|
||||
Toggle("HTTP Proxy", isOn: $systemProxyEnabled)
|
||||
.onChangeCompat(of: systemProxyEnabled) { newValue in
|
||||
Task {
|
||||
await setSystemProxyEnabled(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section("Profile") {
|
||||
ForEach(profileList, id: \.id) { profile in
|
||||
Picker(profile.name, selection: selectedProfileIDLocal) {
|
||||
Text("").tag(profile.id)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.radioGroup)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
*/
|
||||
|
||||
if ApplicationLibrary.inPreview || profile.status.isConnected {
|
||||
ClashModeView()
|
||||
ExtensionStatusView()
|
||||
}
|
||||
}
|
||||
}
|
||||
.alertBinding($alert)
|
||||
.disabled(!ApplicationLibrary.inPreview && (!profile.status.isSwitchable || reasserting))
|
||||
}
|
||||
|
||||
private func switchProfile(_ newProfileID: Int64) async {
|
||||
await SharedPreferences.selectedProfileID.set(newProfileID)
|
||||
environments.selectedProfileUpdate.send()
|
||||
if profile.status.isConnected {
|
||||
do {
|
||||
try await serviceReload()
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
reasserting = false
|
||||
}
|
||||
|
||||
private nonisolated func serviceReload() async throws {
|
||||
try LibboxNewStandaloneCommandClient()!.serviceReload()
|
||||
}
|
||||
|
||||
private nonisolated func setSystemProxyEnabled(_ isEnabled: Bool) async {
|
||||
do {
|
||||
try LibboxNewStandaloneCommandClient()!.setSystemProxyEnabled(isEnabled)
|
||||
await SharedPreferences.systemProxyEnabled.set(isEnabled)
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import Library
|
||||
import Lottie
|
||||
import NetworkExtension
|
||||
import SwiftUI
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct StartStopButton: View {
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
viewBuilder {
|
||||
if ApplicationLibrary.inPreview {
|
||||
#if os(iOS) || os(tvOS)
|
||||
Toggle(isOn: .constant(true)) {
|
||||
Text("Enabled")
|
||||
}
|
||||
#elseif os(macOS)
|
||||
Button {} label: {
|
||||
Label("Stop", systemImage: "stop.fill")
|
||||
}
|
||||
#endif
|
||||
|
||||
} else if let profile = environments.extensionProfile {
|
||||
Button0().environmentObject(profile)
|
||||
} else {
|
||||
#if os(iOS) || os(tvOS)
|
||||
EmptyView()
|
||||
#elseif os(macOS)
|
||||
Button {} label: {
|
||||
Label("Start", systemImage: "play.fill")
|
||||
}
|
||||
.disabled(true)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.disabled(environments.emptyProfiles)
|
||||
}
|
||||
|
||||
private struct Button0: View {
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@EnvironmentObject private var profile: ExtensionProfile
|
||||
@State private var alert: Alert?
|
||||
@State var vpnStatus : NEVPNStatus = .invalid
|
||||
var body: some View {
|
||||
viewBuilder {
|
||||
#if os(iOS) || os(tvOS)
|
||||
/* Toggle(isOn: Binding(get: {
|
||||
profile.status.isConnected
|
||||
}, set: { newValue, _ in
|
||||
Task {
|
||||
await switchProfile(newValue)
|
||||
}
|
||||
})) {
|
||||
Text("Enabled")
|
||||
}*/
|
||||
VStack{
|
||||
PowerButton()
|
||||
|
||||
Label {
|
||||
if vpnStatus == .connected {
|
||||
Text("已连接")
|
||||
}else if vpnStatus == .disconnected {
|
||||
Text("断开连接")
|
||||
}else if vpnStatus == .connecting {
|
||||
Text("连接中...")
|
||||
}else if vpnStatus == .disconnecting {
|
||||
Text("断开中...")
|
||||
}else {
|
||||
Text("切换中...")
|
||||
}
|
||||
|
||||
} icon: {
|
||||
Image(systemName: vpnStatus.isConnectedStrict ? "checkmark.shield" : "shield.slash")
|
||||
}
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
// Spacer()
|
||||
|
||||
}
|
||||
|
||||
#elseif os(macOS)
|
||||
|
||||
VStack{
|
||||
PowerButton().background(Color.clear)
|
||||
|
||||
Label {
|
||||
if vpnStatus == .connected {
|
||||
Text("已连接")
|
||||
}else if vpnStatus == .disconnected {
|
||||
Text("断开连接")
|
||||
}else if vpnStatus == .connecting {
|
||||
Text("连接中...")
|
||||
}else if vpnStatus == .disconnecting {
|
||||
Text("断开中...")
|
||||
}else {
|
||||
Text("切换中...")
|
||||
}
|
||||
} icon: {
|
||||
Image(systemName: vpnStatus.isConnectedStrict ? "checkmark.shield" : "shield.slash")
|
||||
}
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
//Spacer()
|
||||
|
||||
}
|
||||
|
||||
/*Button {
|
||||
Task {
|
||||
await switchProfile(!profile.status.isConnectedStrict)
|
||||
}
|
||||
} label: {
|
||||
if !profile.status.isConnected {
|
||||
Label("Start", systemImage: "play.fill")
|
||||
} else {
|
||||
Label("Stop", systemImage: "stop.fill")
|
||||
}
|
||||
}
|
||||
*/
|
||||
#endif
|
||||
}
|
||||
.disabled(!profile.status.isEnabled)
|
||||
.alertBinding($alert)
|
||||
.onAppear {
|
||||
Task{
|
||||
vpnStatus = profile.status //.isConnected
|
||||
}
|
||||
}.onChangeCompat(of: profile.status) { newStatus in
|
||||
|
||||
vpnStatus = newStatus
|
||||
// if newStatus == .connected {
|
||||
// Task {
|
||||
// await doReloadSystemProxy()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func PowerButton()->some View{
|
||||
|
||||
Button {
|
||||
|
||||
Task {
|
||||
if self.vpnStatus.isConnected {
|
||||
await switchProfile(false)
|
||||
}else{
|
||||
await switchProfile(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
} label: {
|
||||
|
||||
ZStack{
|
||||
|
||||
ZStack{
|
||||
#if os(iOS) || os(tvOS)
|
||||
#endif
|
||||
if vpnStatus == .connected {
|
||||
LottieView(animationFileName: "51a05581" , loopMode: .playOnce).scaleEffect(0.5)
|
||||
}else if (vpnStatus == .connecting || vpnStatus == .disconnecting) {
|
||||
LottieView(animationFileName: "65ea130a" , loopMode: .loop)
|
||||
} else{
|
||||
LottieView(animationFileName: "1d2a0fe5" , loopMode: .playOnce).scaleEffect(0.2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
// Max Frame...
|
||||
.frame(width: 190,height: 190)
|
||||
.background(
|
||||
|
||||
ZStack{
|
||||
|
||||
// Rings....
|
||||
Circle()
|
||||
.trim(from: vpnStatus.isConnected ? 0 : 0.3, to: vpnStatus.isConnected ? 1 : 0.5)
|
||||
.stroke(
|
||||
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color(vpnStatus.isConnected ? "Ring1" : "gray"),
|
||||
Color(vpnStatus.isConnected ? "Ring1" : "gray")
|
||||
.opacity(0.5),
|
||||
Color(vpnStatus.isConnected ? "Ring1" : "gray")
|
||||
.opacity(0.3),
|
||||
Color(vpnStatus.isConnected ? "Ring1" : "gray")
|
||||
.opacity(0.1),
|
||||
|
||||
], startPoint: .leading, endPoint: .trailing),
|
||||
|
||||
style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round)
|
||||
)
|
||||
// Shadows...
|
||||
.shadow(color: Color(vpnStatus.isConnected ? "Ring1" : "gray"), radius: 5, x: 1, y: -4)
|
||||
|
||||
Circle()
|
||||
.trim(from: vpnStatus.isConnected ? 0 : 0.3, to: vpnStatus.isConnected ? 1 : 0.55)
|
||||
.stroke(
|
||||
|
||||
LinearGradient(colors: [
|
||||
|
||||
Color(vpnStatus.isConnected ? "Ring2" : "gray2"),
|
||||
Color(vpnStatus.isConnected ? "Ring2" : "gray2")
|
||||
.opacity(0.5),
|
||||
Color(vpnStatus.isConnected ? "Ring2" : "gray2")
|
||||
.opacity(0.3),
|
||||
Color(vpnStatus.isConnected ? "Ring2" : "gray2")
|
||||
.opacity(0.1),
|
||||
|
||||
], startPoint: .leading, endPoint: .trailing),
|
||||
|
||||
style: StrokeStyle(lineWidth: 4, lineCap: .round, lineJoin: .round)
|
||||
)
|
||||
// Shadows...
|
||||
.shadow(color: Color(vpnStatus.isConnected ? "Ring2" : "gray2"), radius: 5, x: 1, y: -4)
|
||||
.rotationEffect(.init(degrees: 160))
|
||||
|
||||
// Main Little Ring...
|
||||
Circle()
|
||||
.stroke(
|
||||
|
||||
Color("Ring1")
|
||||
.opacity(0.01),
|
||||
lineWidth: 11
|
||||
)
|
||||
// Toggling Shadow when button is Clicked...
|
||||
.shadow(color: Color("Ring2").opacity(vpnStatus.isConnected ? 0.04 : 0), radius: 5, x: 1, y: -4)
|
||||
}
|
||||
|
||||
|
||||
)
|
||||
|
||||
}.padding()
|
||||
// .padding(.top,UIScreen.main.bounds.height < 750 ? 30 : 100)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private nonisolated func switchProfile(_ isEnabled: Bool) async {
|
||||
do {
|
||||
if isEnabled {
|
||||
try await profile.start()
|
||||
await environments.logClient.connect()
|
||||
|
||||
} else {
|
||||
try await profile.stop()
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*private nonisolated func doReloadSystemProxy() async {
|
||||
do {
|
||||
|
||||
let status = try LibboxNewStandaloneCommandClient()!.getSystemProxyStatus()
|
||||
await MainActor.run {
|
||||
|
||||
}
|
||||
} catch {
|
||||
await MainActor.run {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#if os(iOS) || os(tvOS)
|
||||
struct LottieView: UIViewRepresentable {
|
||||
|
||||
var animationFileName: String
|
||||
let loopMode: LottieLoopMode
|
||||
|
||||
func updateUIView(_ uiView: UIViewType, context: Context) {
|
||||
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> Lottie.LottieAnimationView {
|
||||
let animationView = LottieAnimationView(name: animationFileName)
|
||||
animationView.loopMode = loopMode
|
||||
animationView.play()
|
||||
animationView.contentMode = .scaleAspectFit
|
||||
|
||||
return animationView
|
||||
}
|
||||
}
|
||||
#elseif os(macOS)
|
||||
struct LottieView: NSViewRepresentable {
|
||||
|
||||
var animationFileName: String
|
||||
let loopMode: LottieLoopMode
|
||||
|
||||
func makeNSView(context: Context) -> NSView {
|
||||
let view = NSView()
|
||||
let animationView = LottieAnimationView(name: animationFileName)
|
||||
animationView.loopMode = loopMode
|
||||
animationView.play()
|
||||
animationView.contentMode = .scaleAspectFit
|
||||
|
||||
// Add the Lottie animation view as a subview of the NSView
|
||||
animationView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(animationView)
|
||||
|
||||
// Set up constraints to fill the parent view
|
||||
NSLayoutConstraint.activate([
|
||||
animationView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
animationView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
animationView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
animationView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: NSView, context: Context) {
|
||||
// Optionally implement updates to the NSView
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// XufeiTixingButton.swift
|
||||
// ApplicationLibrary
|
||||
//
|
||||
// Created by Mac on 2024/10/26.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
//
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public struct XufeiTixingButton: View {
|
||||
|
||||
private let callback: () async -> Void
|
||||
public init(_ callback: @escaping (() async -> Void)) {
|
||||
self.callback = callback
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
parsePowerButton()
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
func parsePowerButton()->some View{
|
||||
|
||||
Button {
|
||||
|
||||
Task {
|
||||
await callback()
|
||||
}
|
||||
} label: {
|
||||
|
||||
ZStack{
|
||||
|
||||
ZStack{
|
||||
//
|
||||
// LottieView(animationFileName: "1d2a0fe5" , loopMode: .loop)
|
||||
LottieView(animationFileName: "8dfa14a6" , loopMode: .loop).scaleEffect(0.2)
|
||||
Text("开启服务").bold().font(.title2).foregroundStyle(Color.white)
|
||||
//LottieView(animationFileName: "8dfa14a6" , loopMode: .loop).scaleEffect(0.5)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
// Max Frame...
|
||||
.frame(width: 190,height: 190)
|
||||
|
||||
}.padding()
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user