add swiftUI code

This commit is contained in:
zeus
2025-01-22 14:09:10 +08:00
parent 68e7b7347c
commit 8a99853829
2531 changed files with 486215 additions and 0 deletions
@@ -0,0 +1,74 @@
import Foundation
import SwiftUI
public extension Alert {
init(_ error: Error, _ dismissAction: (() -> Void)? = nil) {
self.init(
errorMessage: error.localizedDescription,
dismissAction
)
}
init(errorMessage: String, _ dismissAction: (() -> Void)? = nil) {
self.init(
title: Text("错误提示"),
message: Text(errorMessage),
dismissButton: .default(Text("确定")) {
dismissAction?()
}
)
}
init(title: String = "提示", okMessage: String, _ dismissAction: (() -> Void)? = nil) {
self.init(
title: Text(title),
message: Text(okMessage),
dismissButton: .default(Text("确定")) {
dismissAction?()
}
)
}
init(OKORNOtitle: String = "提示", YesOrNOMessage: String, _ primaryAction: (() -> Void)? = nil) {
self.init(
title: Text(OKORNOtitle),
message: Text(YesOrNOMessage),
primaryButton: .default(Text("确定"), action: {
primaryAction?()
}),
secondaryButton: .cancel(Text("取消"), action: {
})
)
}
}
public extension View {
func alertBinding(_ binding: Binding<Alert?>) -> some View {
alert(isPresented: Binding(get: {
binding.wrappedValue != nil
}, set: { newValue, _ in
if !newValue {
binding.wrappedValue = nil
}
})) {
binding.wrappedValue!
}
}
func alertBinding(_ binding: Binding<Alert?>, _ isLoading: Binding<Bool>) -> some View {
alert(isPresented: Binding(get: {
binding.wrappedValue != nil
}, set: { newValue, _ in
if !newValue, !isLoading.wrappedValue {
binding.wrappedValue = nil
}
})) {
binding.wrappedValue!
}
}
}
@@ -0,0 +1,14 @@
import Foundation
import SwiftUI
public struct BackButton: View {
@Environment(\.dismiss) private var dismiss
public var body: some View {
Button {
dismiss()
} label: {
Image(systemName: "chevron.backward")
}
}
}
@@ -0,0 +1,12 @@
import Foundation
import SwiftUI
public extension Binding {
func withSetter(_ setter: @escaping (Value) -> Void) -> Binding<Value> {
Binding {
wrappedValue
} set: { [setter] newValue, _ in
setter(newValue)
}
}
}
@@ -0,0 +1,29 @@
import SwiftUI
public extension Binding {
func unwrapped<T>(_ defaultValue: T) -> Binding<T> where Value == T? {
Binding<T>(get: {
wrappedValue ?? defaultValue
}, set: { newValue in
wrappedValue = newValue
})
}
}
public extension Binding where Value == Int32 {
func stringBinding(defaultValue: Int32) -> Binding<String> {
Binding<String> {
var intValue = wrappedValue
if intValue == 0 {
intValue = defaultValue
}
return String(intValue)
} set: { newValue in
var newIntValue = Int32(newValue) ?? defaultValue
if newIntValue == 0 {
newIntValue = defaultValue
}
wrappedValue = newIntValue
}
}
}
@@ -0,0 +1,46 @@
import Foundation
import SwiftUI
public struct DeleteButton<Label>: View where Label: View {
private let action: () async -> Void
private let label: Label
@State private var performDelete = false
@State private var timer: Timer?
@State private var isLoading = false
public init(action: @escaping () async -> Void, @ViewBuilder label: () -> Label) {
self.action = action
self.label = label()
}
public var body: some View {
Button(role: .destructive) {
isLoading = true
if let timer {
timer.invalidate()
}
if !performDelete {
performDelete = true
timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false) { _ in
timer = nil
performDelete = false
}
isLoading = true
} else {
Task {
await action()
isLoading = false
performDelete = false
}
}
} label: {
if !performDelete {
label
} else {
label.foregroundStyle(.red)
}
}
.disabled(isLoading)
}
}
@@ -0,0 +1,80 @@
import Foundation
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
public enum DeviceCensorship {
public static func isChinaDevice() -> Bool {
let bannedCharacter = "\u{1F1F9}\u{1F1FC}" as NSString
var imageData: Data
#if canImport(UIKit)
let attributes = [NSAttributedString.Key.font:
UIFont.systemFont(ofSize: 8)]
UIGraphicsBeginImageContext(bannedCharacter.size(withAttributes: attributes))
bannedCharacter.draw(at: CGPoint(x: 0, y: 0), withAttributes: attributes)
var imagePNG: Data?
if let charImage = UIGraphicsGetImageFromCurrentImageContext() {
imagePNG = charImage.pngData()
}
UIGraphicsEndImageContext()
guard let imagePNG else {
return false
}
guard let uiImage = UIImage(data: imagePNG) else {
return false
}
guard let cgImage = uiImage.cgImage else { return false }
guard let cgImageData = cgImage.dataProvider?.data as Data? else { return false }
imageData = cgImageData
#elseif canImport(AppKit)
let attributes = [NSAttributedString.Key.font:
NSFont.systemFont(ofSize: 8)]
let characterSize = bannedCharacter.size(withAttributes: attributes)
let characterRect = NSRect(origin: .zero, size: characterSize)
let characterBitmap = NSBitmapImageRep(bitmapDataPlanes: nil,
pixelsWide: Int(characterSize.width),
pixelsHigh: Int(characterSize.height),
bitsPerSample: 8,
samplesPerPixel: 4,
hasAlpha: true,
isPlanar: false,
colorSpaceName: NSColorSpaceName.calibratedRGB,
bytesPerRow: 0,
bitsPerPixel: 0)
NSGraphicsContext.saveGraphicsState()
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: characterBitmap!)
NSGraphicsContext.current?.imageInterpolation = .high
bannedCharacter.draw(in: characterRect, withAttributes: attributes)
NSGraphicsContext.restoreGraphicsState()
guard let imagePNG = characterBitmap?.representation(using: .png, properties: [:]) else {
return false
}
guard let nsImage = NSImage(data: imagePNG) else {
return false
}
guard let cgImage = nsImage.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
return false
}
guard let cgImageData = cgImage.dataProvider?.data as Data? else { return false }
imageData = cgImageData
#endif
let rawData: UnsafePointer<UInt8> = CFDataGetBytePtr(imageData as CFData)
for index in stride(from: 0, to: imageData.count, by: 4) {
let r = rawData[index]
let g = rawData[index + 1]
let b = rawData[index + 2]
if !(r == g && g == b) {
return false
}
}
return true
}
}
@@ -0,0 +1,122 @@
import Foundation
import SwiftUI
public func FormView(@ViewBuilder content: () -> some View) -> some View {
Form {
content()
}
#if os(macOS)
.formStyle(.grouped)
#endif
}
public func FormTextItem(_ name: LocalizedStringKey, _ value: String) -> some View {
HStack {
Text(name)
Spacer()
Text(value)
.multilineTextAlignment(.trailing)
.font(Font.system(.caption, design: .monospaced))
#if os(iOS) || os(macOS)
.textSelection(.enabled)
#endif
}
}
public func FormTextItem(_ name: LocalizedStringKey, _ systemImage: String, @ViewBuilder _ value: () -> some View) -> some View {
HStack {
Label(name, systemImage: systemImage)
Spacer()
value()
.multilineTextAlignment(.trailing)
.font(Font.system(.caption, design: .monospaced))
#if os(iOS) || os(macOS)
.textSelection(.enabled)
#endif
}
}
public func FormItem(_ title: String, @ViewBuilder content: () -> some View) -> some View {
#if os(iOS) || os(tvOS)
HStack {
Text(title)
.lineLimit(1)
.layoutPriority(1)
Spacer()
Spacer()
content()
}
#elseif os(macOS)
content()
#endif
}
public func FormToggle(_ titleKey: LocalizedStringKey, _ subtitleKey: LocalizedStringKey, _ isOn: Binding<Bool>, _ action: @escaping (_ newValue: Bool) async -> Void) -> some View {
#if os(macOS)
Toggle(isOn: isOn) {
VStack(alignment: .leading) {
Text(titleKey)
Spacer()
Text(subtitleKey)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
.onChangeCompat(of: isOn.wrappedValue) { newValue in
Task {
await action(newValue)
}
}
#else
Section {
Toggle(titleKey, isOn: isOn)
.onChangeCompat(of: isOn.wrappedValue) { newValue in
Task {
await action(newValue)
}
}
} footer: {
Text(subtitleKey)
.frame(maxWidth: .infinity, alignment: .leading)
}
#endif
}
public func FormButton(action: @escaping () -> Void, @ViewBuilder label: () -> some View) -> some View {
Button(action: action, label: label)
#if os(macOS)
.buttonStyle(.plain)
.foregroundColor(.accentColor)
#endif
}
public func FormButton(_ titleKey: some StringProtocol, action: @escaping () -> Void) -> some View {
Button(titleKey, action: action)
#if os(macOS)
.buttonStyle(.plain)
.foregroundColor(.accentColor)
#endif
}
public func FormButton(role: ButtonRole?, action: @escaping () -> Void, @ViewBuilder label: () -> some View) -> some View {
Button(role: role, action: action, label: label)
#if os(macOS)
.buttonStyle(.plain)
.foregroundColor(.accentColor)
#endif
}
public func FormNavigationLink(@ViewBuilder destination: () -> some View, @ViewBuilder label: () -> some View) -> some View {
#if !os(tvOS)
return NavigationLink(destination: destination, label: label)
#else
return NavigationLink(destination: {
destination()
.toolbar {
ToolbarItemGroup(placement: .topBarLeading) {
BackButton()
}
}
}, label: label)
#endif
}
@@ -0,0 +1,11 @@
import SwiftUI
func NavigationDestinationCompat(isPresented: Binding<Bool>, @ViewBuilder destination: () -> some View) -> some View {
NavigationLink(
destination: destination(),
isActive: isPresented,
label: {
EmptyView()
}
)
}
@@ -0,0 +1,17 @@
import SwiftUI
public func NavigationStackCompat(@ViewBuilder content: () -> some View) -> some View {
viewBuilder {
if #available(iOS 17.0, *) {
// view not updating in iOS 16, but why?
NavigationStack {
content()
}
} else {
NavigationView(content: content)
#if !os(macOS)
.navigationViewStyle(.stack)
#endif
}
}
}
@@ -0,0 +1,49 @@
#if !os(tvOS)
import StoreKit
import SwiftUI
public func RequestReviewButton(label: @escaping () -> some View) -> some View {
viewBuilder {
if #available(iOS 16.0, macOS 13.0, visionOS 1.0, *) {
RequestReviewButton0(label: label)
} else {
#if os(iOS)
RequestReviewButton1(label: label)
#else
EmptyView()
#endif
}
}
}
@available(iOS 16.0, macOS 13.0, visionOS 1.0, *)
struct RequestReviewButton0<Label: View>: View {
@Environment(\.requestReview) private var requestReview
private let label: () -> Label
init(label: @escaping () -> Label) {
self.label = label
}
var body: some View {
FormButton(action: {
requestReview()
}, label: label)
}
}
struct RequestReviewButton1<Label: View>: View {
private let label: () -> Label
init(label: @escaping () -> Label) {
self.label = label
}
var body: some View {
Button(action: {
SKStoreReviewController.requestReview()
}, label: label)
}
}
#endif
@@ -0,0 +1,147 @@
import Foundation
import Library
import SwiftUI
#if canImport(UIKit)
import UIKit
#elseif canImport(AppKit)
import AppKit
#endif
@MainActor
public struct ProfileShareButton<Label>: View where Label: View {
private let alert: Binding<Alert?>
private let profile: Profile
private let label: () -> Label
public init(_ alert: Binding<Alert?>, _ profile: Profile, label: @escaping () -> Label) {
self.alert = alert
self.profile = profile
self.label = label
}
public var body: some View {
#if os(iOS)
if #available(iOS 17.4, *) {
bodyCompat
} else if #available(iOS 16.0, *) {
ShareLink(item: profile, subject: Text(profile.name), preview: SharePreview("Share profile"), label: label)
} else if UIDevice.current.userInterfaceIdiom != .pad {
bodyCompat
}
#else
bodyCompat
#endif
}
private var bodyCompat: some View {
ShareButtonCompat(alert, label: label) {
try profile.toContent().generateShareFile()
}
}
}
public struct ShareButtonCompat<Label>: View where Label: View {
private let label: () -> Label
private let itemURL: () throws -> URL
@Binding private var alert: Alert?
#if os(macOS)
@State private var sharePresented = false
#endif
public init(_ alert: Binding<Alert?>, @ViewBuilder label: @escaping () -> Label, itemURL: @escaping () throws -> URL) {
_alert = alert
self.label = label
self.itemURL = itemURL
}
public var body: some View {
Button(action: shareItem, label: label)
#if os(macOS)
.background(SharingServicePicker($sharePresented, $alert, itemURL))
#endif
}
private func shareItem() {
#if os(iOS)
Task {
await shareItem0()
}
#elseif os(macOS)
sharePresented = true
#endif
}
#if os(iOS)
private nonisolated func shareItem0() async {
do {
let shareItem = try itemURL()
await MainActor.run {
shareItem1(shareItem)
}
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private func shareItem1(_ item: URL) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
windowScene.keyWindow?.rootViewController?.present(UIActivityViewController(activityItems: [item], applicationActivities: nil), animated: true, completion: nil)
}
}
#endif
}
#if os(macOS)
private struct SharingServicePicker: NSViewRepresentable {
@Binding private var isPresented: Bool
@Binding private var alert: Alert?
private let item: () throws -> URL
init(_ isPresented: Binding<Bool>, _ alert: Binding<Alert?>, _ item: @escaping () throws -> URL) {
_isPresented = isPresented
_alert = alert
self.item = item
}
func makeNSView(context _: Context) -> NSView {
let view = NSView()
return view
}
func updateNSView(_ nsView: NSView, context: Context) {
if isPresented {
do {
let picker = try NSSharingServicePicker(items: [item()])
picker.delegate = context.coordinator
DispatchQueue.main.async {
picker.show(relativeTo: .zero, of: nsView, preferredEdge: .minY)
}
} catch {
alert = Alert(error)
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, NSSharingServicePickerDelegate {
private let parent: SharingServicePicker
init(_ parent: SharingServicePicker) {
self.parent = parent
}
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose _: NSSharingService?) {
sharingServicePicker.delegate = nil
parent.isPresented = false
}
}
}
#endif
@@ -0,0 +1,6 @@
import Foundation
import SwiftUI
public func viewBuilder(@ViewBuilder _ builder: () -> some View) -> some View {
builder()
}
@@ -0,0 +1,23 @@
import SwiftUI
public extension View {
func onChangeCompat(of value: some Equatable, _ action: @escaping () -> Void) -> some View {
// if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) {
// return onChange(of: value, action)
// } else {
onChange(of: value) { _ in
action()
}
// }
}
func onChangeCompat<V>(of value: V, _ action: @escaping (_ newValue: V) -> Void) -> some View where V: Equatable {
// if #available(iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0, *) {
// return onChange(of: value) { _, newValue in
// action(newValue)
// }
// } else {
onChange(of: value, perform: action)
// }
}
}
@@ -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)
}
}
}
@@ -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()
}
}
@@ -0,0 +1,58 @@
import Foundation
import Libbox
import Library
import SwiftUI
public extension EnvironmentValues {
private struct showMenuBarExtraKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(true)
}
var showMenuBarExtra: Binding<Bool> {
get {
self[showMenuBarExtraKey.self]
}
set {
self[showMenuBarExtraKey.self] = newValue
}
}
private struct selectionKey: EnvironmentKey {
static let defaultValue: Binding<NavigationPage> = .constant(.dashboard)
}
var selection: Binding<NavigationPage> {
get {
self[selectionKey.self]
}
set {
self[selectionKey.self] = newValue
}
}
private struct importRemoteProfileKey: EnvironmentKey {
static var defaultValue: Binding<LibboxImportRemoteProfile?> = .constant(nil)
}
var importRemoteProfile: Binding<LibboxImportRemoteProfile?> {
get {
self[importRemoteProfileKey.self]
}
set {
self[importRemoteProfileKey.self] = newValue
}
}
private struct importProfileKey: EnvironmentKey {
static var defaultValue: Binding<LibboxProfileContent?> = .constant(nil)
}
var importProfile: Binding<LibboxProfileContent?> {
get {
self[importProfileKey.self]
}
set {
self[importProfileKey.self] = newValue
}
}
}
@@ -0,0 +1,280 @@
import Libbox
import Network
import Library
import SwiftUI
@MainActor
public struct GroupItemView: View {
private let _group: Binding<OutboundGroup>
private var group: OutboundGroup {
_group.wrappedValue
}
private let item: OutboundGroupItem
public init(_ group: Binding<OutboundGroup>, _ item: OutboundGroupItem) {
_group = group
self.item = item
}
@State private var alert: Alert?
/* old style
public var body: some View {
Button {
if group.selectable, group.selected != item.tag {
Task {
await selectOutbound()
UserDefaults.standard.setValue("\(item.tag)", forKey: "selectedNode")
}
}
} label: {
HStack {
VStack {
HStack {
Text(item.tag)
.font(.subheadline)
.foregroundStyle(.foreground)
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: 0)
}
Spacer(minLength: 8)
HStack {
Text(item.displayType)
.font(.caption)
.foregroundColor(.secondary)
Spacer(minLength: 0)
}
}
Spacer(minLength: 0)
VStack {
if group.selected == item.tag {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
}
Spacer(minLength: 0)
if item.urlTestDelay > 0 {
Text(item.delayString)
.font(.caption)
.foregroundColor(item.delayColor)
}
}
}
}
#if !os(tvOS)
.buttonStyle(.borderless)
.padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13))
.background(backgroundColor)
.cornerRadius(10)
#endif
.alertBinding($alert)
}
*/
func testPing(to server: String, port: Int, completion: @escaping (TimeInterval?) -> Void) {
let startTime = Date()
let connection = NWConnection(host: NWEndpoint.Host(server), port: NWEndpoint.Port(rawValue: UInt16(port))!, using: .tcp)
connection.stateUpdateHandler = { state in
switch state {
case .ready:
//
let pingTime = Date().timeIntervalSince(startTime)
completion(pingTime)
connection.cancel() //
case .failed(_):
completion(nil)
connection.cancel()
default:
break
}
}
//
connection.start(queue: .global())
}
public var body: some View {
Button {
print("\(group.selectable) group.selected - item.tag: >>> "+group.selected + " " + item.tag)
// print(item.toString)
if group.selectable, group.selected != item.tag {
// var item = _group.wrappedValue.items[index] // Make a mutable copy of the item
Task {
if let server = item.server ,let port = item.server_port{
//
testPing(to: server, port: port) { pingTime in
if let ping = pingTime {
print("Ping 时间: \(ping)")
Task{
await pingOutbound(tag: item.tag,pingtime: UInt16((pingTime ?? 0)*1000))
}
} else {
print("连接超时")
}
}
UserDefaults.standard.setValue("\(item.tag)", forKey: "selectedNode")
await selectOutbound()
}else{
await selectOutbound()
UserDefaults.standard.setValue("\(item.tag)", forKey: "selectedNode")
//
await restartVPNServer()
}
}
}
} label: {
VStack(spacing: 2) {
HStack {
VStack(alignment: .leading, spacing: 8) {
HStack {
// Image(server.name)
// .resizable()
// .aspectRatio(contentMode: .fit)
// .frame(width: 20, height: 20)
Text(item.tag)
.font(.subheadline)
.fontWeight(.semibold)
.foregroundColor(.white)
}
Label {
Text(item.type)
// Text(item.urlTestDelay <= 3000 ? "线" : "")
} icon: {
// Image(systemName: item.urlTestDelay <= 3000 ? "checkmark" : "xmark")
} .foregroundColor(.gray)
// .foregroundColor(item.urlTestDelay <= 3000 ?.green : .red)
.font(.caption2)
}
Spacer(minLength: 10)
// Change Server Button...
VStack {
if group.selected == item.tag {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(Color.accentColor)
}
Spacer(minLength: 0)
if item.urlTestDelay > 0 {
Text(item.delayString)
.font(.caption)
.foregroundColor(item.delayColor)
}else{
//
Text("...")
.font(.caption)
.foregroundColor(.gray)
}
}
}
.frame(height: 50)
.padding(.horizontal)
Divider()
}
}
#if !os(tvOS)
.buttonStyle(.borderless)
// .padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13))
// .background(backgroundColor)
// .cornerRadius(10)
#endif
.alertBinding($alert)
}
private nonisolated func pingOutbound(tag: String,pingtime: UInt16) async {
let newGroup = await group
let newitems = newGroup.items.map { item in
var mutableItem = item
if (item.tag == tag) {
mutableItem.urlTestDelay = pingtime
}
return mutableItem
}
let newOut = OutboundGroup(tag: newGroup.tag, type: newGroup.type, selected:tag, selectable: newGroup.selectable, isExpand: newGroup.isExpand, items: newitems)
await MainActor.run { [newOut] in
_group.wrappedValue = newOut
}
}
private nonisolated func selectOutbound() async {
do {
try await LibboxNewStandaloneCommandClient()!.selectOutbound(group.tag, outboundTag: item.tag)
var newGroup = await group
newGroup.selected = item.tag
await MainActor.run { [newGroup] in
_group.wrappedValue = newGroup
}
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private nonisolated func setSystemProxyEnabled(_ isEnabled: Bool) async {
do {
try LibboxNewStandaloneCommandClient()!.setSystemProxyEnabled(isEnabled)
await SharedPreferences.systemProxyEnabled.set(isEnabled)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private nonisolated func restartVPNServer() async {
do {
try LibboxNewStandaloneCommandClient()!.serviceReload()
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private var backgroundColor: Color {
#if os(iOS)
return Color(uiColor: .secondarySystemGroupedBackground)
#elseif os(macOS)
return Color(nsColor: .textBackgroundColor)
#elseif os(tvOS)
return Color.black
#endif
}
}
@@ -0,0 +1,124 @@
import Libbox
import Library
import SwiftUI
public struct GroupListView: View {
@Environment(\.scenePhase) private var scenePhase
@State private var isLoading = true
@StateObject private var commandClient = CommandClient(.groups)
@State private var groups: [OutboundGroup] = []
public init() {
}
public var body: some View {
VStack {
if isLoading {
Spacer().frame(height: 20)
Text("请开启VPN后查看节点数据").foregroundColor(.red)
} else if !groups.isEmpty {
ScrollView {
VStack {
ForEach(groups, id: \.hashValue) { it in
GroupView(it)
}
}.padding() //.background(Color.gray.opacity(0.1))
}
} else {
Spacer().frame(height: 20)
ProgressView()
}
}
.onAppear {
connect()
}
.onDisappear {
commandClient.disconnect()
}
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active {
commandClient.connect()
} else {
commandClient.disconnect()
}
}
.onReceive(commandClient.$groups, perform: { groups in
if let groups {
setGroups(groups)
}
})
}
private func connect() {
if ApplicationLibrary.inPreview {
groups = [
OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [
OutboundGroupItem(tag: "server", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12),
OutboundGroupItem(tag: "server2", type: "WireGuard", urlTestTime: .now, urlTestDelay: 34),
OutboundGroupItem(tag: "auto", type: "URLTest", urlTestTime: .now, urlTestDelay: 100),
]),
OutboundGroup(tag: "group2", type: "urltest", selected: "client", selectable: true, isExpand: false, items:
(0 ..< 234).map { index in
OutboundGroupItem(tag: "client\(index)", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: UInt16(100 + index * 10))
}),
]
isLoading = false
} else {
#if targetEnvironment(simulator)
groups = [
OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [
OutboundGroupItem(tag: "🇭🇰🇭🇰Hong Kong 01", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12),
OutboundGroupItem(tag: "🇭🇰🇭🇰Hong Kong 03", type: "WireGuard", urlTestTime: .now, urlTestDelay: 34),
OutboundGroupItem(tag: "🇭🇰🇭🇰Hong Kong 04", type: "URLTest", urlTestTime: .now, urlTestDelay: 100),
OutboundGroupItem(tag: "🇯🇵🇯🇵🇯🇵Japan 01", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12),
OutboundGroupItem(tag: "🇯🇵🇯🇵Japan Kong 01", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 112),
OutboundGroupItem(tag: "🇯🇵🇯🇵Japan Kong 02", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 1222),
OutboundGroupItem(tag: "🇯🇵🇯🇵Japan Kong 03", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 4412),
OutboundGroupItem(tag: "🇯🇵🇯🇵Japan Kong 04", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 512),
]),
]
isLoading = false
#endif
commandClient.connect()
}
}
private func setGroups(_ goGroups: [LibboxOutboundGroup]) {
var groups = [OutboundGroup]()
if let goGroup = goGroups.first {
goGroup.isExpand = true
var items = [OutboundGroupItem]()
let itemIterator = goGroup.getItems()!
while itemIterator.hasNext() {
let goItem = itemIterator.next()!
items.append(OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: Double(goItem.urlTestTime)), urlTestDelay: UInt16(goItem.urlTestDelay)))
}
groups.append(OutboundGroup(tag: goGroup.tag, type: goGroup.type, selected: goGroup.selected, selectable: goGroup.selectable, isExpand: goGroup.isExpand, items: items))
UserDefaults.standard.setValue("\(goGroup.tag)", forKey: "goGrouptag")
}
/*
var groups = [OutboundGroup]()
for goGroup in goGroups {
print(goGroup.tag)
//
goGroup.isExpand = true
var items = [OutboundGroupItem]()
let itemIterator = goGroup.getItems()!
while itemIterator.hasNext() {
let goItem = itemIterator.next()!
items.append(OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: Double(goItem.urlTestTime)), urlTestDelay: UInt16(goItem.urlTestDelay)))
}
groups.append(OutboundGroup(tag: goGroup.tag, type: goGroup.type, selected: goGroup.selected, selectable: goGroup.selectable, isExpand: goGroup.isExpand, items: items))
}*/
self.groups = groups
isLoading = false
}
}
@@ -0,0 +1,176 @@
import Libbox
import Library
import SwiftUI
@MainActor
public struct GroupView: View {
@State private var group: OutboundGroup
@State private var geometryWidth: CGFloat = 300
@State private var alert: Alert?
public init(_ group: OutboundGroup) {
_group = State(initialValue: group)
}
private var title: some View {
HStack {
// Text(group.tag)
// .font(.headline)
//Text()
Text(group.tag)
.font(.subheadline)
.foregroundColor(.secondary)
Text("\(group.items.count)")
.font(.subheadline)
.padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4))
.background(Color.gray.opacity(0.5))
.cornerRadius(4)
Button {
group.isExpand = !group.isExpand
Task {
await setGroupExpand()
}
} label: {
HStack{
if group.isExpand {
Image(systemName: "arrow.down.to.line")
Text("收起").font(.subheadline)
} else {
Image(systemName: "arrow.up.to.line")
Text("展开").font(.subheadline)
}
}
}
#if os(macOS) || os(tvOS)
.buttonStyle(.plain)
#endif
Button {
Task {
await doURLTest()
}
} label: {
Image(systemName: "bolt.fill")
Text("测速").font(.subheadline)
}
#if os(macOS) || os(tvOS)
.buttonStyle(.plain)
#endif
}
.alertBinding($alert)
.padding([.top, .bottom], 10)
}
public var body: some View {
Section {
if group.isExpand {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()),
count: explandColumnCount()))
{
ForEach(group.items, id: \.tag) { it in
GroupItemView($group, it)
}
}
} else {
VStack(spacing: 5) {
ForEach(Array(itemGroups.enumerated()), id: \.offset) { items in
HStack(spacing: 5) {
ForEach(items.element, id: \.tag) { it in
ZStack {
Rectangle()
.fill(it.delayColor)
if it.tag == group.selected {
Rectangle()
.fill(Color.white)
#if !os(tvOS)
.frame(width: 5, height: 5)
#else
.frame(width: 15, height: 15)
#endif
}
}
#if !os(tvOS)
.frame(width: 10, height: 10)
#else
.frame(width: 30, height: 30)
#endif
}
}.frame(maxWidth: .infinity, alignment: .topLeading)
}
}
}
} header: {
//title .frame(maxWidth: .infinity, alignment: .topLeading)
}
.background {
/* GeometryReader { geometry in
Rectangle()
.fill(.clear)
.frame(height: 1)
.onChangeCompat(of: geometry.size.width) { newValue in
geometryWidth = newValue
}
.onAppear {
geometryWidth = geometry.size.width
}
}.padding() */
}.padding(.leading,-18.0)
}
private var itemGroups: [[OutboundGroupItem]] {
let count: Int
#if os(tvOS)
count = Int(Int(geometryWidth) / 40)
#else
count = Int(Int(geometryWidth) / 20)
#endif
if count == 0 {
return [group.items]
} else {
return group.items.chunked(
into: count
)
}
}
private func explandColumnCount() -> Int {
let standardCount = Int(Int(geometryWidth) / 180)
#if os(iOS)
return standardCount < 2 ? 1 : standardCount
#elseif os(tvOS)
return 4
#else
return standardCount < 1 ? 1 : standardCount
#endif
}
private nonisolated func doURLTest() async {
do {
try await LibboxNewStandaloneCommandClient()!.urlTest(group.tag)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private nonisolated func setGroupExpand() async {
do {
try await LibboxNewStandaloneCommandClient()!.setGroupExpand(group.tag, isExpand: group.isExpand)
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
}
private extension Array {
func chunked(into size: Int) -> [[Element]] {
stride(from: 0, to: count, by: size).map {
Array(self[$0 ..< Swift.min($0 + size, count)])
}
}
}
@@ -0,0 +1,12 @@
//
// NodesListView.swift
// ApplicationLibrary
//
// Created by Mac on 2024/10/26.
//
import Foundation
import SwiftUI
//NodesListView
@@ -0,0 +1,55 @@
import Foundation
import Libbox
import SwiftUI
public struct OutboundGroup: Codable {
let tag: String
let type: String
var selected: String
let selectable: Bool
var isExpand: Bool
let items: [OutboundGroupItem]
var hashValue: Int {
var value = tag.hashValue
(value, _) = value.addingReportingOverflow(selected.hashValue)
for item in items {
(value, _) = value.addingReportingOverflow(item.urlTestTime.hashValue)
}
return value
}
public init(tag: String, type: String, selected: String, selectable: Bool,isExpand: Bool,items: [OutboundGroupItem] ) {
self.tag = tag
self.type = type
self.selected = selected
self.selectable = selectable
self.isExpand = isExpand
self.items = items
}
}
public extension OutboundGroup {
var newhashValue: Int {
var value = tag.hashValue
(value, _) = value.addingReportingOverflow(selected.hashValue)
for item in items {
(value, _) = value.addingReportingOverflow(item.urlTestTime.hashValue)
}
return value
}
}
public extension OutboundGroup {
var displayType: String {
LibboxProxyDisplayType(type)
}
}
@@ -0,0 +1,63 @@
import Foundation
import Libbox
import SwiftUI
public struct OutboundGroupItem: Codable {
public let tag: String
public let type: String
public let urlTestTime: Date
public var urlTestDelay: UInt16
public var server: String? = ""
public var server_port: Int? = 0
public var method: String? = ""
public var password: String? = ""
//server: String?,server_port: String?,method: String?,serverpassword String?
//
public init(tag: String, type: String, urlTestTime: Date, urlTestDelay: UInt16) {
self.tag = tag
self.type = type
self.urlTestTime = urlTestTime
self.urlTestDelay = urlTestDelay
}
}
public extension OutboundGroupItem {
mutating func setpingms(pingDelay: UInt16){
urlTestDelay = pingDelay
}
var toString: String {
"\(tag) - \(type) - \(server ?? "") - \(server_port ?? 0) - \(method ?? "") - \(password ?? "") "
}
var displayType: String {
LibboxProxyDisplayType(type)
}
var delayString: String {
"\(urlTestDelay)ms"
}
var delayColor: Color {
switch urlTestDelay {
case 0:
return .gray
case ..<400:
return .green
case ..<800:
return .yellow
case 800 ..< 1500:
return .orange
default:
return .red
}
}
}
@@ -0,0 +1,90 @@
import Library
import SwiftUI
public struct LogView: View {
@Environment(\.selection) private var selection
@EnvironmentObject private var environments: ExtensionEnvironments
public init() {}
public var body: some View {
LogView0().environmentObject(environments.logClient)
}
private struct LogView0: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var logClient: CommandClient
private let logFont = Font.system(.caption2, design: .monospaced)
var body: some View {
if ApplicationLibrary.inPreview {
let logList = [
"(packet-tunnel) log server started",
"INFO[0000] router: loaded geoip database: 250 codes",
"INFO[0000] router: loaded geosite database: 1400 codes",
"INFO[0000] router: updated default interface en0, index 11",
"inbound/tun[0]: started at utun3",
"sing-box started (1.666s)",
]
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(logList.enumerated()), id: \.offset) { it in
Text(it.element)
.font(logFont)
#if os(tvOS)
.focusable()
#endif
Spacer(minLength: 8)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
#if os(tvOS)
.focusEffectDisabled()
.focusSection()
#endif
} else if logClient.logList.isEmpty {
VStack {
if logClient.isConnected {
Text("Empty logs")
} else {
Text("Service not started").onAppear {
environments.connectLog()
}
}
}
} else {
ScrollViewReader { reader in
ScrollView {
VStack(alignment: .leading, spacing: 0) {
ForEach(Array(logClient.logList.enumerated()), id: \.offset) { it in
Text(it.element)
.font(logFont)
#if os(tvOS)
.focusable()
#endif
Spacer(minLength: 8)
}
.onChangeCompat(of: logClient.logList.count) { newCount in
withAnimation {
reader.scrollTo(newCount - 1)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
#if os(tvOS)
.focusEffectDisabled()
.focusSection()
#endif
.onAppear {
reader.scrollTo(logClient.logList.count - 1)
}
}
}
}
}
}
@@ -0,0 +1,109 @@
import Foundation
import Library
import SwiftUI
public enum NavigationPage: Int, CaseIterable, Identifiable {
public var id: Self {
self
}
case dashboard
#if os(macOS)
case groups
#endif
case logs
case profiles
case settings
}
public extension NavigationPage {
#if os(macOS)
static var macosDefaultPages: [NavigationPage] {
[.logs, .profiles, .settings]
}
#endif
var label: some View {
Label(title, systemImage: iconImage)
.tint(.textColor)
}
var title: String {
switch self {
case .dashboard:
return NSLocalizedString("Dashboard", comment: "")
#if os(macOS)
case .groups:
return NSLocalizedString("Groups", comment: "")
#endif
case .logs:
return NSLocalizedString("Logs", comment: "")
case .profiles:
return NSLocalizedString("Profiles", comment: "")
case .settings:
return NSLocalizedString("Settings", comment: "")
}
}
private var iconImage: String {
switch self {
case .dashboard:
return "text.and.command.macwindow"
#if os(macOS)
case .groups:
return "rectangle.3.group.fill"
#endif
case .logs:
return "doc.text.fill"
case .profiles:
return "list.bullet.rectangle.fill"
case .settings:
return "gear.circle.fill"
}
}
@MainActor
var contentView: some View {
viewBuilder {
switch self {
case .dashboard:
DashboardView()
#if os(macOS)
case .groups:
GroupListView()
#endif
case .logs:
LogView()
case .profiles:
ProfileView()
case .settings:
SettingView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
#if os(iOS)
.background(Color(uiColor: .systemGroupedBackground))
#endif
}
@MainActor
var contentViewMac: some View{
viewBuilder {
DashboardView().frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
//DashboardViewMac().frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
#if os(macOS)
func visible(_ profile: ExtensionProfile?) -> Bool {
switch self {
case .groups:
return profile?.status.isConnectedStrict == true
default:
return true
}
}
#endif
}
@@ -0,0 +1,154 @@
#if os(iOS) || os(macOS)
import Foundation
import Library
import SwiftUI
@MainActor
public struct EditProfileContentView: View {
public struct Context: Codable, Hashable {
public let profileID: Int64
public let readOnly: Bool
}
private let profileID: Int64?
private let readOnly: Bool
public init(_ context: Context?) {
profileID = context?.profileID
readOnly = context?.readOnly == true
}
@Environment(\.dismiss) private var dismiss
@State private var isLoading = true
@State private var profile: Profile!
@State private var profileContent = ""
@State private var isChanged = false
@State private var alert: Alert?
public var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task {
await loadContent()
}
}
} else {
viewBuilder {
if readOnly {
TextEditor(text: .constant(profileContent))
} else {
TextEditor(text: $profileContent)
}
}
.font(Font.system(.caption2, design: .monospaced))
.autocorrectionDisabled(true)
// https://stackoverflow.com/questions/66721935/swiftui-how-to-disable-the-smart-quotes-in-texteditor
// https://stackoverflow.com/questions/74034171/textfield-with-autocorrectiondisabled-still-shows-predictive-text-bar
.textContentType(.init(rawValue: ""))
#if os(iOS)
.keyboardType(.asciiCapable)
.textInputAutocapitalization(.none)
.background(Color(UIColor.secondarySystemGroupedBackground))
#elseif os(macOS)
.padding()
#endif
.onChangeCompat(of: profileContent) {
isChanged = true
}
}
}
.alertBinding($alert)
.navigationTitle(navigationTitle)
#if os(macOS)
.toolbar {
ToolbarItemGroup(placement: .navigation) {
if !readOnly {
Button {
Task {
await saveContent()
}
} label: {
Label("Save", image: "save")
}
.disabled(!isChanged)
} else {
Button {
NSPasteboard.general.setString(profileContent, forType: .fileContents)
} label: {
Label("Copy", systemImage: "clipboard.fill")
}
}
}
}
#elseif os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if !readOnly {
Button("Save") {
Task {
await saveContent()
}
}.disabled(!isChanged)
} else {
Button("Copy") {
UIPasteboard.general.string = profileContent
}
}
}
}
.navigationBarTitleDisplayMode(.inline)
#endif
}
private var navigationTitle: String {
if readOnly {
return "View Content"
} else {
return "Edit Content"
}
}
private func loadContent() async {
do {
try await loadContentBackground()
} catch {
alert = Alert(error)
}
isLoading = false
}
private nonisolated func loadContentBackground() async throws {
guard let profileID else {
throw NSError(domain: "Context destroyed", code: 0)
}
guard let profile = try await ProfileManager.get(profileID) else {
throw NSError(domain: "Profile missing", code: 0)
}
let profileContent = try profile.read()
await MainActor.run {
self.profile = profile
self.profileContent = profileContent
}
}
private func saveContent() async {
guard let profile else {
return
}
do {
try await saveContentBackground(profile)
} catch {
alert = Alert(error)
return
}
isChanged = false
}
private nonisolated func saveContentBackground(_ profile: Profile) async throws {
try await profile.write(profileContent)
}
}
#endif
@@ -0,0 +1,177 @@
import Libbox
import Library
import SwiftUI
@MainActor
public struct EditProfileView: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var profile: Profile
@State private var isLoading = false
@State private var isChanged = false
@State private var alert: Alert?
@State private var shareLinkPresented = false
@State private var shareLinkText: String?
public init() {}
public var body: some View {
FormView {
FormItem("Name") {
TextField("Name", text: $profile.name, prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
Picker(selection: $profile.type) {
Text("Local").tag(ProfileType.local)
Text("iCloud").tag(ProfileType.icloud)
Text("Remote").tag(ProfileType.remote)
} label: {
Text("Type")
}
.disabled(true)
if profile.type == .icloud {
FormItem("Path") {
TextField("Path", text: $profile.path, prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
} else if profile.type == .remote {
FormItem("URL") {
TextField("URL", text: $profile.remoteURL.unwrapped(""), prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
Toggle("Auto Update", isOn: $profile.autoUpdate)
FormItem("Auto Update Interval") {
TextField("Auto Update Interval", text: $profile.autoUpdateInterval.stringBinding(defaultValue: 60), prompt: Text("In Minutes"))
.multilineTextAlignment(.trailing)
#if os(iOS)
.keyboardType(.numberPad)
#endif
}
}
if profile.type == .remote {
Section("Status") {
FormTextItem("Last Updated", profile.lastUpdatedString)
}
}
Section("Action") {
if profile.type != .remote {
#if os(iOS) || os(macOS)
FormNavigationLink {
EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: false))
} label: {
Label("Edit Content", systemImage: "pencil")
.foregroundColor(.accentColor)
}
#endif
} else {
#if os(iOS) || os(macOS)
FormNavigationLink {
EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true))
} label: {
Label("View Content", systemImage: "doc.fill")
.foregroundColor(.accentColor)
}
#endif
FormButton {
isLoading = true
Task {
await updateProfile()
}
} label: {
Label("Update", systemImage: "arrow.clockwise")
}
.foregroundColor(.accentColor)
.disabled(isLoading)
}
FormButton(role: .destructive) {
Task {
await deleteProfile()
}
} label: {
Label("Delete", systemImage: "trash.fill")
}
.foregroundColor(.red)
}
}
.onChangeCompat(of: profile.name) {
isChanged = true
}
.onChangeCompat(of: profile.remoteURL) {
isChanged = true
}
.onChangeCompat(of: profile.autoUpdate) {
isChanged = true
}
.disabled(isLoading)
#if os(macOS)
.toolbar {
ToolbarItemGroup(placement: .navigation) {
Button {
isLoading = true
Task {
await saveProfile()
}
} label: {
Image("save", bundle: ApplicationLibrary.bundle, label: Text("Save"))
}
.disabled(isLoading || !isChanged)
}
}
#elseif os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
isLoading = true
Task {
await saveProfile()
}
}.disabled(!isChanged)
}
}
#endif
.alertBinding($alert)
.navigationTitle("Edit Profile")
}
private func updateProfile() async {
defer {
isLoading = false
}
do {
try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC)))
try await profile.updateRemoteProfile()
environments.profileUpdate.send()
} catch {
alert = Alert(error)
}
}
private func deleteProfile() async {
do {
try await ProfileManager.delete(profile)
} catch {
alert = Alert(error)
return
}
environments.profileUpdate.send()
dismiss()
}
private func saveProfile() async {
do {
_ = try await ProfileManager.update(profile)
#if os(iOS) || os(tvOS)
try await UIProfileUpdateTask.configure()
#else
try await ProfileUpdateTask.configure()
#endif
} catch {
alert = Alert(error)
return
}
isChanged = false
isLoading = false
environments.profileUpdate.send()
}
}
@@ -0,0 +1,210 @@
#if os(tvOS)
import DeviceDiscoveryUI
import Libbox
import Library
import SwiftUI
@MainActor
public struct ImportProfileView: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.dismiss) private var dismiss
@State private var isLoading = false
@State private var selected = false
@State private var alert: Alert?
@State private var connection: NWConnection?
@State private var socket: NWSocket?
@State private var profiles: [LibboxProfilePreview]?
@State private var isImporting = false
public init() {}
public var body: some View {
VStack(alignment: .center) {
if !selected {
Form {
Section {
EmptyView()
} footer: {
Text("To import configurations from your iPhone or iPad, make sure sing-box is the **same version** on both devices and **VPN is disabled**.")
}
DevicePicker(
.applicationService(name: "sing-box:profile"))
{ endpoint in
selected = true
Task {
await handleEndpoint(endpoint)
}
} label: {
Text("Select Device")
} fallback: {
EmptyView()
} parameters: {
.applicationService
}
}
} else if let profiles {
Form {
Section {
EmptyView()
} footer: {
Text("\(profiles.count) Profiles")
}
ForEach(profiles, id: \.profileID) { profile in
Button(profile.name) {
isLoading = true
Task {
selectProfile(profileID: profile.profileID)
isLoading = false
}
}.disabled(isLoading || isImporting)
}
}
} else {
Text("Connecting...")
}
}
.focusSection()
.alertBinding($alert)
.navigationTitle("Import Profile")
}
private func reset() {
if let connection {
connection.stateUpdateHandler = nil
connection.cancel()
self.connection = nil
}
if let socket {
socket.cancel()
self.socket = nil
}
selected = false
profiles = nil
}
private func handleEndpoint(_ endpoint: NWEndpoint) async {
let connection = NWConnection(to: endpoint, using: NWParameters.applicationService)
self.connection = connection
socket = NWSocket(connection)
connection.stateUpdateHandler = { state in
switch state {
case let .failed(error):
DispatchQueue.main.async { [self] in
reset()
alert = Alert(error)
}
default: break
}
}
connection.start(queue: .global())
do {
try await loopMessages()
} catch {
alert = Alert(error)
reset()
}
}
private nonisolated func loopMessages() async throws {
guard let socket = await socket else {
return
}
var message: Data
while true {
do {
message = try socket.read()
} catch {
throw NSError(domain: "read from connection: \(error.localizedDescription)", code: 0)
}
var error: NSError?
switch Int64(message[0]) {
case LibboxMessageTypeError:
let message = LibboxDecodeErrorMessage(message, &error)
if let error {
throw error
}
if let message {
throw NSError(domain: "remote error: \(message.message)", code: 0)
}
case LibboxMessageTypeProfileList:
let decoder = LibboxProfileDecoder()
try decoder.decode(message)
let iterator = decoder.iterator()!
var profiles = [LibboxProfilePreview]()
while iterator.hasNext() {
let profile = iterator.next()!
if profile.type == LibboxProfileTypeiCloud {
// not supported on tvOS
continue
}
profiles.append(profile)
}
await MainActor.run { [self, profiles] in
self.profiles = profiles
isImporting = false
}
case LibboxMessageTypeProfileContent:
let content = LibboxDecodeProfileContent(message, &error)
if let error {
throw error
}
try await importProfile(content!)
return
default:
throw NSError(domain: "unknown message type \(message[0])", code: 0)
}
}
}
private func selectProfile(profileID: Int64) {
guard let connection else {
return
}
guard let socket else {
return
}
connection.stateUpdateHandler = nil
let request = LibboxProfileContentRequest()
request.profileID = profileID
do {
try socket.write(request.encode())
isImporting = true
} catch {
alert = Alert(error)
reset()
}
}
private nonisolated func importProfile(_ content: LibboxProfileContent) async throws {
var type: ProfileType = .local
switch content.type {
case LibboxProfileTypeLocal:
type = .local
case LibboxProfileTypeiCloud:
type = .icloud
case LibboxProfileTypeRemote:
type = .remote
default:
break
}
let nextProfileID = try await ProfileManager.nextID()
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
try content.config.write(to: profileConfig, atomically: true, encoding: .utf8)
var lastUpdated: Date?
if content.lastUpdated > 0 {
lastUpdated = Date(timeIntervalSince1970: Double(content.lastUpdated))
}
try await ProfileManager.create(Profile(name: content.name, type: type, path: profileConfig.relativePath, remoteURL: content.remotePath, autoUpdate: content.autoUpdate, lastUpdated: lastUpdated))
await reset()
await MainActor.run {
environments.profileUpdate.send()
dismiss()
}
}
}
#endif
@@ -0,0 +1,253 @@
import Foundation
import Libbox
import Library
import SwiftUI
@MainActor
public struct NewProfileView: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.dismiss) private var dismiss
@State private var isSaving = false
@State private var profileName = "Sub"
@State private var profileType = ProfileType.remote
@State private var fileImport = false
@State private var fileURL: URL!
@State private var remotePath = ""
@State private var autoUpdate = true
@State private var autoUpdateInterval: Int32 = 60
@State private var pickerPresented = false
@State private var alert: Alert?
public struct ImportRequest: Codable, Hashable {
public let name: String
public let url: String
}
public init(_ importRequest: ImportRequest? = nil) {
if let importRequest {
_profileName = .init(initialValue: importRequest.name)
_profileType = .init(initialValue: .remote)
_remotePath = .init(initialValue: importRequest.url)
}
}
public var body: some View {
FormView {
FormItem("Name") {
TextField("Name", text: $profileName, prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
Picker(selection: $profileType) {
#if !os(tvOS)
Text("Local").tag(ProfileType.local)
Text("iCloud").tag(ProfileType.icloud)
#endif
Text("Remote").tag(ProfileType.remote)
} label: {
Text("Type")
}
if profileType == .local {
Picker(selection: $fileImport) {
Text("Create New").tag(false)
Text("Import").tag(true)
} label: {
Text("File")
}
#if os(tvOS)
.disabled(true)
#endif
viewBuilder {
if fileImport {
HStack {
Text("File Path")
Spacer()
Spacer()
if let fileURL {
Button(fileURL.fileName) {
pickerPresented = true
}
} else {
Button("Choose") {
pickerPresented = true
}
}
}
}
}
} else if profileType == .icloud {
FormItem("Path") {
TextField("Path", text: $remotePath, prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
} else if profileType == .remote {
FormItem("URL") {
TextField("URL", text: $remotePath, prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
Toggle("Auto Update", isOn: $autoUpdate)
FormItem("Auto Update Interval") {
TextField("Auto Update Interval", text: $autoUpdateInterval.stringBinding(defaultValue: 60), prompt: Text("In Minutes"))
.multilineTextAlignment(.trailing)
#if os(iOS)
.keyboardType(.numberPad)
#endif
}
}
Section {
if !isSaving {
FormButton {
isSaving = true
Task {
await createProfile()
}
} label: {
Label("Create", systemImage: "doc.fill.badge.plus")
}
} else {
ProgressView()
}
}
}
.navigationTitle("New Profile")
.alertBinding($alert)
#if os(iOS) || os(macOS)
.fileImporter(
isPresented: $pickerPresented,
allowedContentTypes: [.json],
allowsMultipleSelection: false
) { result in
do {
let urls = try result.get()
if !urls.isEmpty {
fileURL = urls[0]
}
} catch {
alert = Alert(error)
return
}
}
#endif
}
private func createProfile() async {
defer {
isSaving = false
}
if profileName.isEmpty {
alert = Alert(errorMessage: "Missing profile name")
return
}
if remotePath.isEmpty {
if profileType == .icloud {
alert = Alert(errorMessage: "Missing path")
return
} else if profileType == .remote {
alert = Alert(errorMessage: "Missing URL")
return
}
}
do {
try await createProfileBackground()
} catch {
alert = Alert(error)
return
}
environments.profileUpdate.send()
dismiss()
#if os(macOS)
resetFields()
#endif
}
private func resetFields() {
profileName = ""
profileType = .local
fileImport = false
fileURL = nil
remotePath = ""
}
private nonisolated func createProfileBackground() async throws {
let nextProfileID = try await ProfileManager.nextID()
var savePath = ""
var remoteURL: String? = nil
var lastUpdated: Date? = nil
let profileName = await profileName
let profileType = await profileType
let fileImport = await fileImport
let fileURL = await fileURL
let remotePath = await remotePath
let autoUpdate = await autoUpdate
let autoUpdateInterval = await autoUpdateInterval
if profileType == .local {
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
if fileImport {
guard let fileURL else {
throw NSError(domain: "Missing file", code: 0)
}
if !fileURL.startAccessingSecurityScopedResource() {
throw NSError(domain: "Missing access to selected file", code: 0)
}
defer {
fileURL.stopAccessingSecurityScopedResource()
}
try String(contentsOf: fileURL).write(to: profileConfig, atomically: true, encoding: .utf8)
} else {
try "{}".write(to: profileConfig, atomically: true, encoding: .utf8)
}
savePath = profileConfig.relativePath
} else if profileType == .icloud {
if !FileManager.default.fileExists(atPath: FilePath.iCloudDirectory.path) {
try FileManager.default.createDirectory(at: FilePath.iCloudDirectory, withIntermediateDirectories: true)
}
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(remotePath, isDirectory: false)
_ = saveURL.startAccessingSecurityScopedResource()
defer {
saveURL.stopAccessingSecurityScopedResource()
}
do {
_ = try String(contentsOf: saveURL)
} catch {
try "{}".write(to: saveURL, atomically: true, encoding: .utf8)
}
savePath = remotePath
} else if profileType == .remote {
let remoteContent = try HTTPClient().getString(remotePath)
print(remoteContent)
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
remoteURL = remotePath
lastUpdated = .now
}
try await ProfileManager.create(Profile(
name: profileName,
type: profileType,
path: savePath,
remoteURL: remoteURL,
autoUpdate: autoUpdate,
autoUpdateInterval: autoUpdateInterval,
lastUpdated: lastUpdated
))
if profileType == .remote {
#if os(iOS) || os(tvOS)
try await UIProfileUpdateTask.configure()
#else
try await ProfileUpdateTask.configure()
#endif
}
}
}
@@ -0,0 +1,423 @@
import Foundation
import Libbox
import Library
import Network
import QRCode
import SwiftUI
@MainActor
public struct ProfileView: View {
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.importProfile) private var importProfile
@Environment(\.importRemoteProfile) private var importRemoteProfile
@State private var importRemoteProfileRequest: NewProfileView.ImportRequest?
@State private var importRemoteProfilePresented = false
@State private var isLoading = true
@State private var isUpdating = false
@State private var alert: Alert?
@State private var profileList: [ProfilePreview] = []
#if os(iOS) || os(tvOS)
@State private var editMode = EditMode.inactive
#endif
#if os(tvOS)
@Environment(\.devicePickerSupports) private var devicePickerSupports
#endif
public init() {}
public var body: some View {
VStack {
if isLoading {
ProgressView().onAppear {
Task {
await doReload()
}
}
} else {
ZStack {
if let importRemoteProfileRequest {
NavigationDestinationCompat(isPresented: $importRemoteProfilePresented) {
NewProfileView(importRemoteProfileRequest)
}
}
FormView {
#if os(iOS)
FormNavigationLink {
NewProfileView()
} label: {
Text("New Profile").foregroundColor(.accentColor)
}
.disabled(editMode.isEditing)
#elseif os(macOS)
FormNavigationLink {
NewProfileView()
} label: {
Text("New Profile")
}
#elseif os(tvOS)
Section {
FormNavigationLink {
NewProfileView()
} label: {
Text("New Profile").foregroundColor(.accentColor)
}
if ApplicationLibrary.inPreview || devicePickerSupports(.applicationService(name: "sing-box"), parameters: { .applicationService }) {
FormNavigationLink {
ImportProfileView()
} label: {
Text("Import Profile").foregroundColor(.accentColor)
}
}
}
#endif
if profileList.isEmpty {
Text("Empty profiles")
} else {
List {
ForEach(profileList, id: \.id) { profile in
viewBuilder {
#if os(iOS) || os(tvOS)
if editMode.isEditing == true {
Text(profile.name)
} else {
ProfileItem(self, profile)
}
#else
ProfileItem(self, profile)
#endif
}
}
.onMove(perform: moveProfile)
.onDelete(perform: deleteProfile)
}
}
}
}
}
}
.disabled(isUpdating)
.alertBinding($alert, $isLoading)
.onAppear {
if let profile = importProfile.wrappedValue {
importProfile.wrappedValue = nil
createImportProfileDialog(profile)
}
if let remoteProfile = importRemoteProfile.wrappedValue {
importRemoteProfile.wrappedValue = nil
createImportRemoteProfileDialog(remoteProfile)
}
}
.onChangeCompat(of: importProfile.wrappedValue) { newValue in
if let newValue {
importProfile.wrappedValue = nil
createImportProfileDialog(newValue)
}
}
.onChangeCompat(of: importRemoteProfile.wrappedValue) { newValue in
if let newValue {
importRemoteProfile.wrappedValue = nil
createImportRemoteProfileDialog(newValue)
}
}
.onReceive(environments.profileUpdate) { _ in
Task {
await doReload()
}
}
#if os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton().disabled(profileList.isEmpty)
}
}
#elseif os(tvOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if editMode == .inactive {
Button("Edit") {
editMode = .active
}
.disabled(profileList.isEmpty)
} else {
Button("Done") {
editMode = .inactive
}
}
}
}
#endif
#if os(iOS) || os(tvOS)
.environment(\.editMode, $editMode)
#endif
}
private func createImportProfileDialog(_ profile: LibboxProfileContent) {
alert = Alert(
title: Text("Import Profile"),
message: Text("Are you sure to import profile \(profile.name)?"),
primaryButton: .default(Text("Import")) {
Task {
do {
try await profile.importProfile()
} catch {
alert = Alert(error)
return
}
await doReload()
}
},
secondaryButton: .cancel()
)
}
private func createImportRemoteProfileDialog(_ newValue: LibboxImportRemoteProfile) {
importRemoteProfileRequest = .init(name: newValue.name, url: newValue.url)
alert = Alert(
title: Text("Import Remote Profile"),
message: Text("Are you sure to import remote profile \(newValue.name)? You will connect to \(newValue.host) to download the configuration."),
primaryButton: .default(Text("Import")) {
importRemoteProfilePresented = true
},
secondaryButton: .cancel()
)
}
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))),
]
} else {
do {
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
} catch {
alert = Alert(error)
return
}
}
environments.emptyProfiles = profileList.isEmpty
}
private func updateProfile(_ profile: Profile) async {
await updateProfileBackground(profile)
isUpdating = false
}
private nonisolated func updateProfileBackground(_ profile: Profile) async {
do {
_ = try await profile.updateRemoteProfile()
} catch {
await MainActor.run {
alert = Alert(error)
}
}
}
private func deleteProfile(_ profile: Profile) async {
do {
_ = try await ProfileManager.delete(profile)
} catch {
alert = Alert(error)
return
}
environments.profileUpdate.send()
}
private func moveProfile(from source: IndexSet, to destination: Int) {
profileList.move(fromOffsets: source, toOffset: destination)
for (index, profile) in profileList.enumerated() {
profileList[index].order = UInt32(index)
profile.origin.order = UInt32(index)
}
Task {
do {
try await ProfileManager.update(profileList.map(\.origin))
} catch {
alert = Alert(error)
}
environments.profileUpdate.send()
}
}
private func deleteProfile(where profileIndex: IndexSet) {
let profileToDelete = profileIndex.map { index in
profileList[index].origin
}
profileList.remove(atOffsets: profileIndex)
environments.emptyProfiles = profileList.isEmpty
Task {
do {
_ = try await ProfileManager.delete(profileToDelete)
} catch {
alert = Alert(error)
}
environments.profileUpdate.send()
}
}
@MainActor
public struct ProfileItem: View {
private let parent: ProfileView
@State private var profile: ProfilePreview
@State private var shareLinkPresented = false
public init(_ parent: ProfileView, _ profile: ProfilePreview) {
self.parent = parent
_profile = State(initialValue: profile)
}
public var body: some View {
#if os(iOS) || os(macOS)
if #available(iOS 16.0, macOS 13.0,*) {
body0.draggable(profile.origin)
} else {
body0
}
#else
body0
#endif
}
private var body0: some View {
viewBuilder {
#if !os(macOS)
FormNavigationLink {
EditProfileView().environmentObject(profile.origin)
} label: {
Text(profile.name)
}
.sheet(isPresented: $shareLinkPresented) {
shareLinkView.padding()
}
.contextMenu {
ProfileShareButton(parent.$alert, profile.origin) {
Label("Share", systemImage: "square.and.arrow.up.fill")
}
if profile.type == .remote {
Button {
shareLinkPresented = true
} label: {
Label("Share URL as QR Code", systemImage: "qrcode")
}
Button {
parent.isUpdating = true
Task {
await parent.updateProfile(profile.origin)
profile = ProfilePreview(profile.origin)
}
} label: {
Label("Update", systemImage: "arrow.clockwise")
}
}
Button(role: .destructive) {
Task {
await parent.deleteProfile(profile.origin)
}
} label: {
Label("Delete", systemImage: "trash.fill")
}
}
#else
FormNavigationLink {
EditProfileView().environmentObject(profile.origin)
} label: {
HStack {
VStack(alignment: .leading) {
Text(profile.name)
if profile.type == .remote {
Spacer(minLength: 4)
Text("Last Updated: \(profile.origin.lastUpdatedString)").font(.caption)
}
}
HStack {
if profile.type == .remote {
Button {
parent.isUpdating = true
Task {
await parent.updateProfile(profile.origin)
profile = ProfilePreview(profile.origin)
}
} label: {
Image(systemName: "arrow.clockwise")
}
.padding(.leading, 4)
Button {
shareLinkPresented = true
} label: {
Image(systemName: "qrcode")
}
.padding(.leading, 4)
.popover(isPresented: $shareLinkPresented, arrowEdge: .bottom) {
shareLinkView
}
}
ProfileShareButton(parent.$alert, profile.origin) {
Image(systemName: "square.and.arrow.up.fill")
}
.padding(.leading, 4)
Button {
Task {
await parent.deleteProfile(profile.origin)
}
} label: {
Image(systemName: "trash.fill")
}
.padding([.leading, .trailing], 4)
}
.buttonStyle(.plain)
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
}
#endif
}
}
private var shareLinkView: some View {
#if os(iOS)
viewBuilder {
if #available(iOS 16.0, *) {
shareLinkView0
.presentationDetents([.medium])
.presentationDragIndicator(.visible)
} else {
shareLinkView0
}
}
#elseif os(macOS)
shareLinkView0
.frame(minWidth: 300, minHeight: 300)
#else
shareLinkView0
#endif
}
private var foregroundColor: CGColor {
#if canImport(UIKit)
return UIColor.label.cgColor
#elseif canImport(AppKit)
return NSColor.labelColor.cgColor
#endif
}
private var shareLinkView0: some View {
QRCodeViewUI(
content: LibboxGenerateRemoteProfileImportLink(profile.name, profile.remoteURL!),
errorCorrection: .low,
foregroundColor: foregroundColor,
backgroundColor: CGColor(gray: 1.0, alpha: 0.0)
)
}
}
}
@@ -0,0 +1,95 @@
import Libbox
import Library
import SwiftUI
public struct CoreView: View {
@State private var isLoading = true
@State private var version = ""
@State private var dataSize = ""
public init() {}
public var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task {
await loadSettings()
}
}
} else {
FormView {
FormTextItem("Version", version)
FormTextItem("Data Size", dataSize)
Section("Working Directory") {
#if os(macOS)
FormButton {
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: FilePath.workingDirectory.relativePath)
} label: {
Label("Open", systemImage: "macwindow.and.cursorarrow")
}
#endif
FormButton(role: .destructive) {
Task {
await destroyWorkingDirectory()
}
} label: {
Label("Destroy", systemImage: "trash.fill")
}
.foregroundColor(.red)
}
}
}
}
.navigationTitle("Core")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
private nonisolated func loadSettings() async {
if ApplicationLibrary.inPreview {
version = "<redacted>"
dataSize = LibboxFormatBytes(1000 * 1000 * 10)
isLoading = false
} else {
version = LibboxVersion()
dataSize = "Loading..."
isLoading = false
await loadSettingsBackground()
}
}
private nonisolated func loadSettingsBackground() async {
let dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown"
await MainActor.run {
self.dataSize = dataSize
}
}
private nonisolated func destroyWorkingDirectory() async {
try? FileManager.default.removeItem(at: FilePath.workingDirectory)
await MainActor.run {
isLoading = true
}
}
}
private extension URL {
func formattedSize() throws -> String? {
guard let urls = FileManager.default.enumerator(at: self, includingPropertiesForKeys: nil)?.allObjects as? [URL] else {
return nil
}
let size = try urls.lazy.reduce(0) {
try ($1.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0) + $0
}
let formatter = ByteCountFormatter()
formatter.countStyle = .file
guard let byteCount = formatter.string(for: size) else {
return nil
}
return byteCount
}
}
@@ -0,0 +1,152 @@
#if os(macOS)
import AppKit
import Library
import ServiceManagement
import SwiftUI
public struct MacAppView: View {
@State private var isLoading = true
@State private var startAtLogin = false
@Environment(\.showMenuBarExtra) private var showMenuBarExtra
@State private var menuBarExtraInBackground = false
@State private var alert: Alert?
public init() {}
public var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task {
await loadSettings()
}
}
} else {
FormView {
FormToggle("Start At Login", "Launch the application when the system is logged in. If enabled at the same time as `Show in Menu Bar` and `Keep Menu Bar in Background`, the application interface will not be opened automatically.", $startAtLogin) { newValue in
updateLoginItems(newValue)
}
Toggle("Show in Menu Bar", isOn: showMenuBarExtra)
.onChangeCompat(of: showMenuBarExtra.wrappedValue) { newValue in
Task {
await SharedPreferences.showMenuBarExtra.set(newValue)
if !newValue {
menuBarExtraInBackground = false
}
}
}
if showMenuBarExtra.wrappedValue {
Toggle("Keep Menu Bar in Background", isOn: $menuBarExtraInBackground)
.onChangeCompat(of: menuBarExtraInBackground) { newValue in
Task {
await SharedPreferences.menuBarExtraInBackground.set(newValue)
}
}
}
if Variant.useSystemExtension {
Section("System Extension") {
FormButton {
Task {
await updateSystemExtension()
}
} label: {
Label("Update", systemImage: "arrow.down.doc.fill")
}
FormButton(role: .destructive) {
Task {
await uninstallSystemExtension()
}
} label: {
Label("Uninstall", systemImage: "trash.fill").foregroundColor(.red)
}
}
}
}
}
}
.alertBinding($alert)
.navigationTitle("App")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
private func loadSettings() async {
startAtLogin = SMAppService.mainApp.status == .enabled
menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get()
isLoading = false
}
private func updateLoginItems(_ startAtLogin: Bool) {
do {
if startAtLogin {
if SMAppService.mainApp.status == .enabled {
try? SMAppService.mainApp.unregister()
}
try SMAppService.mainApp.register()
} else {
try SMAppService.mainApp.unregister()
}
} catch {
alert = Alert(error)
}
}
private func updateSystemExtension() async {
do {
if let result = try await SystemExtension.install(forceUpdate: true) {
switch result {
case .completed:
alert = Alert(
title: Text("Update"),
message: Text("System Extension updated."),
dismissButton: .default(Text("Ok")) {}
)
case .willCompleteAfterReboot:
alert = Alert(
title: Text("Update"),
message: Text("Reboot required."),
dismissButton: .default(Text("Ok")) {}
)
@unknown default:
fatalError()
}
}
} catch {
alert = Alert(error)
}
}
private func uninstallSystemExtension() async {
do {
if let result = try await SystemExtension.uninstall() {
switch result {
case .completed:
alert = Alert(
title: Text("Uninstall"),
message: Text("System Extension removed."),
dismissButton: .default(Text("Ok")) {}
)
case .willCompleteAfterReboot:
alert = Alert(
title: Text("Uninstall"),
message: Text("Reboot required."),
dismissButton: .default(Text("Ok")) {}
)
@unknown default:
fatalError()
}
}
} catch {
alert = Alert(error)
}
}
}
#endif
@@ -0,0 +1,50 @@
import Foundation
import Library
import SwiftUI
public struct OnDemandRulesView: View {
@State private var isLoading = true
@State private var alwaysOn = false
public init() {}
public var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task.detached {
await loadSettings()
}
}
} else {
FormView {
FormToggle("Always On", """
Implement always-on via on-demand rules.
This should not be an intended use of the API, so you cannot disable VPN in system settings. To stop the service manually, use the in-app interface or simply delete the VPN profile.
""", $alwaysOn) { newValue in
await SharedPreferences.alwaysOn.set(newValue)
}
FormButton {
Task {
await SharedPreferences.resetOnDemandRules()
isLoading = true
}
} label: {
Label("Reset", systemImage: "eraser.fill")
}
.foregroundColor(.red)
}
}
}
.navigationTitle("On Demand Rules")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
private func loadSettings() async {
alwaysOn = await SharedPreferences.alwaysOn.get()
isLoading = false
}
}
@@ -0,0 +1,108 @@
import Library
import SwiftUI
public struct PacketTunnelView: View {
@State private var isLoading = true
@State private var ignoreMemoryLimit = false
@State private var includeAllNetworks = false
@State private var excludeAPNs = false
@State private var excludeCellularServices = false
@State private var excludeLocalNetworks = false
@State private var enforceRoutes = false
public init() {}
public var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task.detached {
await loadSettings()
}
}
} else {
FormView {
FormToggle("Ignore Memory Limit", """
Do not enforce memory limits on sing-box. Will cause OOM on non-jailbroken iOS and tvOS devices.
""", $ignoreMemoryLimit) { newValue in
await SharedPreferences.ignoreMemoryLimit.set(newValue)
}
#if !os(tvOS)
FormToggle("includeAllNetworks", """
If this property is true, the system routes network traffic through the tunnel except traffic for designated system services necessary for maintaining expected device functionality. You can exclude some types of traffic using the **excludeAPNs**, **excludeLocalNetworks**, and **excludeCellularServices** properties in combination with this property.
when enabled, the default TUN stack is changed to `gvisor`, and the `system` and `mixed` stacks are not available.
[Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/3131931-includeallnetworks)
""", $includeAllNetworks) { newValue in
await SharedPreferences.includeAllNetworks.set(newValue)
}
FormToggle("excludeAPNs", """
If this property is true, the system excludes Apple Push Notification services (APNs) traffic, but only when the **includeAllNetworks** property is also true.
[Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/4140516-excludeapns)
""", $excludeAPNs) { newValue in
await SharedPreferences.excludeAPNs.set(newValue)
}
FormToggle("excludeCellularServices", """
If this property is true, the system excludes cellular services — such as Wi-Fi Calling, MMS, SMS, and Visual Voicemail — but only when the **includeAllNetworks** property is also true. This property doesnt impact services that use the cellular network only — such as VoLTE — which the system automatically excludes.
[Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/4140517-excludecellularservices)
""", $excludeCellularServices) { newValue in
await SharedPreferences.excludeCellularServices.set(newValue)
}
FormToggle("excludeLocalNetworks", """
If this property is true, the system excludes network connections to hosts on the local network — such as AirPlay, AirDrop, and CarPlay — but only when the **includeAllNetworks** or **enforceRoutes** property is also true.
[Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/3143658-excludelocalnetworks)
""", $excludeLocalNetworks) { newValue in
await SharedPreferences.excludeLocalNetworks.set(newValue)
}
FormToggle("enforceRoutes", """
If this property is true when the **includeAllNetworks** property is false, the system scopes the included routes to the VPN and the excluded routes to the current primary network interface. This property supersedes the system routing table and scoping operations by apps.
If you set both the **enforceRoutes** and **excludeLocalNetworks** properties to true, the system excludes network connections to hosts on the local network.
[Apple Documentation](https://developer.apple.com/documentation/networkextension/nevpnprotocol/3689459-enforceroutes)
""", $enforceRoutes) { newValue in
await SharedPreferences.enforceRoutes.set(newValue)
}
#endif
FormButton {
Task {
await SharedPreferences.resetPacketTunnel()
isLoading = true
}
} label: {
Label("Reset", systemImage: "eraser.fill")
}
.foregroundColor(.red)
}
}
}
.navigationTitle("Packet Tunnel")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
private func loadSettings() async {
ignoreMemoryLimit = await SharedPreferences.ignoreMemoryLimit.get()
#if !os(tvOS)
includeAllNetworks = await SharedPreferences.includeAllNetworks.get()
excludeAPNs = await SharedPreferences.excludeAPNs.get()
excludeCellularServices = await SharedPreferences.excludeCellularServices.get()
excludeLocalNetworks = await SharedPreferences.excludeLocalNetworks.get()
enforceRoutes = await SharedPreferences.enforceRoutes.get()
#endif
isLoading = false
}
}
@@ -0,0 +1,59 @@
import Library
import SwiftUI
public struct ProfileOverrideView: View {
@State private var isLoading = true
@State private var excludeDefaultRoute = false
@State private var autoRouteUseSubRangesByDefault = false
@State private var excludeAPNsRoute = false
public init() {}
public var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task.detached {
await loadSettings()
}
}
} else {
FormView {
FormToggle("Hide VPN Icon", "Append `0.0.0.0/31` to `inet4_route_exclude_address` if not exists.", $excludeDefaultRoute) { newValue in
await SharedPreferences.excludeDefaultRoute.set(newValue)
}
FormToggle("No Default Route", """
By default, segment routing is used in `auto_route` instead of global routing. If `*_<route_address/route_exclude_address>` exists in the configuration, this item will not take effect on the corresponding network (commonly used to resolve HomeKit compatibility issues).
""", $autoRouteUseSubRangesByDefault) { newValue in
await SharedPreferences.autoRouteUseSubRangesByDefault.set(newValue)
}
FormToggle("Exclude APNs Route", "Append `push.apple.com` to `bypass_domain`, and `17.0.0.0/8` to `inet4_route_exclude_address`.", $excludeAPNsRoute) { newValue in
await SharedPreferences.excludeAPNsRoute.set(newValue)
}
FormButton {
Task {
await SharedPreferences.resetProfileOverride()
isLoading = true
}
} label: {
Label("Reset", systemImage: "eraser.fill")
}
.foregroundColor(.red)
}
}
}
.navigationTitle("Profile Override")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
private func loadSettings() async {
excludeDefaultRoute = await SharedPreferences.excludeDefaultRoute.get()
autoRouteUseSubRangesByDefault = await SharedPreferences.autoRouteUseSubRangesByDefault.get()
excludeAPNsRoute = await SharedPreferences.excludeAPNsRoute.get()
isLoading = false
}
}
@@ -0,0 +1,112 @@
import Foundation
import Library
import SwiftUI
@MainActor
public struct ServiceLogView: View {
@Environment(\.dismiss) private var dismiss
@State private var isLoading = true
@State private var content = ""
@State private var alert: Alert?
private let logFont = Font.system(.caption, design: .monospaced)
public init() {}
public var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task {
await loadContent()
}
}
} else {
if content.isEmpty {
Text("Empty content")
} else {
ScrollView {
Text(content)
.font(logFont)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
.padding()
}
}
}
.toolbar {
if !content.isEmpty {
#if !os(tvOS)
ShareButtonCompat($alert) {
Label("Export", systemImage: "square.and.arrow.up.fill")
} itemURL: {
try content.generateShareFile(name: "service.log")
}
#endif
Button(role: .destructive) {
Task {
await deleteContent()
}
} label: {
#if !os(tvOS)
Label("Delete", systemImage: "trash.fill")
#else
Image(systemName: "trash.fill")
#endif
}
}
}
.alertBinding($alert)
.navigationTitle("Service Log")
#if os(tvOS)
.focusable()
#endif
}
private nonisolated func loadContent() async {
var content = ""
do {
content = try String(contentsOf: FilePath.cacheDirectory.appendingPathComponent("stderr.log"))
} catch {}
if content.isEmpty {
do {
content = try String(contentsOf: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old"))
} catch {}
}
#if DEBUG
if content.isEmpty {
content = "Empty content"
}
#endif
if !content.isEmpty {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let machineName = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
var deviceInfo = "Machine: " + machineName + "\n"
#if os(iOS)
await deviceInfo += "System: " + (UIDevice.current.systemName) + " " + (UIDevice.current.systemVersion) + "\n"
#elseif os(macOS)
deviceInfo += "System: macOS " + ProcessInfo().operatingSystemVersionString + "\n"
#endif
content = deviceInfo + "\n" + content
}
await MainActor.run { [content] in
self.content = content
isLoading = false
}
}
private nonisolated func deleteContent() async {
try? FileManager.default.removeItem(at: FilePath.cacheDirectory.appendingPathComponent("stderr.log"))
try? FileManager.default.removeItem(at: FilePath.cacheDirectory.appendingPathComponent("stderr.log.old"))
await MainActor.run {
dismiss()
isLoading = true
}
}
}
@@ -0,0 +1,154 @@
import Library
import SwiftUI
public struct SettingView: View {
private enum Tabs: Int, CaseIterable, Identifiable {
public var id: Self {
self
}
#if os(macOS)
case app
#endif
case core, packetTunnel, onDemandRules, profileOverride, sponsors
var label: some View {
Label(title, systemImage: iconImage)
}
var title: String {
switch self {
#if os(macOS)
case .app:
return NSLocalizedString("App", comment: "")
#endif
case .core:
return NSLocalizedString("Core", comment: "")
case .packetTunnel:
return NSLocalizedString("Packet Tunnel", comment: "")
case .onDemandRules:
return NSLocalizedString("On Demand Rules", comment: "")
case .profileOverride:
return NSLocalizedString("Profile Override", comment: "")
case .sponsors:
return NSLocalizedString("Sponsors", comment: "")
}
}
private var iconImage: String {
switch self {
#if os(macOS)
case .app:
return "app.badge.fill"
#endif
case .core:
return "shippingbox.fill"
case .packetTunnel:
return "aspectratio.fill"
case .onDemandRules:
return "filemenu.and.selection"
case .profileOverride:
return "square.dashed.inset.filled"
case .sponsors:
return "heart.fill"
}
}
@MainActor
var contentView: some View {
viewBuilder {
switch self {
#if os(macOS)
case .app:
MacAppView()
#endif
case .core:
CoreView()
case .packetTunnel:
PacketTunnelView()
case .onDemandRules:
OnDemandRulesView()
case .profileOverride:
ProfileOverrideView()
case .sponsors:
SponsorsView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
#if os(iOS)
.background(Color(uiColor: .systemGroupedBackground))
#endif
}
@MainActor
var navigationLink: some View {
FormNavigationLink {
contentView
} label: {
label
}
}
}
@State private var isLoading = true
@State private var taiwanFlagAvailable = false
public init() {}
public var body: some View {
FormView {
#if os(macOS)
Tabs.app.navigationLink
#endif
ForEach([Tabs.core, Tabs.packetTunnel, Tabs.onDemandRules, Tabs.profileOverride]) { it in
it.navigationLink
}
#if !os(tvOS)
Section("About") {
Link(destination: URL(string: "https://sing-box.sagernet.org/")!) {
Label("Documentation", systemImage: "doc.on.doc.fill")
}
.buttonStyle(.plain)
.foregroundColor(.accentColor)
RequestReviewButton {
Label("Rate on the App Store", systemImage: "text.bubble.fill")
}
#if os(macOS)
if Variant.useSystemExtension {
Tabs.sponsors.navigationLink
}
#endif
}
#endif
Section("Debug") {
FormNavigationLink {
ServiceLogView()
} label: {
Label("Service Log", systemImage: "doc.on.clipboard")
}
FormTextItem("Taiwan Flag Available", "touchid") {
if isLoading {
Text("Loading...")
.onAppear {
Task.detached {
let available: Bool
if ApplicationLibrary.inPreview {
available = true
} else {
available = !DeviceCensorship.isChinaDevice()
}
await MainActor.run {
taiwanFlagAvailable = available
isLoading = false
}
}
}
} else {
Text(taiwanFlagAvailable.description)
}
}
}
}
}
}
@@ -0,0 +1,29 @@
import Foundation
import SwiftUI
public struct SponsorsView: View {
@Environment(\.openURL) private var openURL
public init() {}
public var body: some View {
FormView {
Section {
EmptyView()
} footer: {
Text("**If Ive defended your modern life, please consider sponsoring me.**")
.frame(maxWidth: .infinity, alignment: .leading)
}
FormButton("GitHub Sponsor (recommended)") {
openURL(URL(string: "https://github.com/sponsors/nekohasekai")!)
}
FormButton("Other methods") {
openURL(URL(string: "https://sekai.icu/sponsors/")!)
}
}
.navigationTitle("Sponsors")
#if os(iOS)
.navigationBarTitleDisplayMode(.inline)
#endif
}
}
@@ -0,0 +1,63 @@
//
// ActivityIndicatorModifier.swift
// ApplicationLibrary
//
// Created by Mac on 2024/10/23.
//
import Foundation
import SwiftUI
struct ActivityIndicator: UIViewRepresentable {
@Binding var isAnimating: Bool
let style: UIActivityIndicatorView.Style
func makeUIView(context: UIViewRepresentableContext<ActivityIndicator>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicator>) {
isAnimating ? uiView.startAnimating() : uiView.stopAnimating()
}
}
public struct ActivityIndicatorModifier: AnimatableModifier {
var isLoading: Bool
public init(isLoading: Bool, color: Color = .primary, lineWidth: CGFloat = 3) {
self.isLoading = isLoading
}
var animatableData: Bool {
get { isLoading }
set { isLoading = newValue }
}
public func body(content: Content) -> some View {
ZStack {
if isLoading {
GeometryReader { geometry in
ZStack(alignment: .center) {
content
.disabled(self.isLoading)
.blur(radius: self.isLoading ? 3 : 0)
VStack {
// Text("...").font(.subheadline)
ActivityIndicator(isAnimating: .constant(true), style: .large)
}
.frame(width: geometry.size.width / 2,
height: geometry.size.height / 5)
.background(Color.secondary.colorInvert())
.foregroundColor(Color.primary)
.cornerRadius(20)
.opacity(self.isLoading ? 1 : 0)
.position(x: geometry.frame(in: .local).midX, y: geometry.frame(in: .local).midY)
}
}
} else {
content
}
}
}
}