This commit is contained in:
世界
2023-10-02 22:12:33 +08:00
parent c1c4f084b9
commit c5a29e0956
16 changed files with 256 additions and 247 deletions
@@ -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)
}
}
}
@@ -7,10 +7,11 @@ import SwiftUI
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: [Profile] = []
@State private var selectedProfileID: Int64!
@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
@@ -73,22 +74,19 @@ public struct ActiveDashboardView: View {
OverviewView($profileList, $selectedProfileID, $systemProxyAvailable, $systemProxyEnabled)
#endif
}
#if os(iOS) || os(tvOS)
.onChangeCompat(of: scenePhase) { newValue in
if newValue == .active {
Task {
await doReload()
.onReceive(environments.profileUpdate) { _ in
Task {
await doReload()
}
}
.onReceive(environments.selectedProfileUpdate) { _ in
Task {
selectedProfileID = await SharedPreferences.selectedProfileID.get()
if profile.status.isConnected {
await doReloadSystemProxy()
}
}
}
.onChangeCompat(of: parentSelection.wrappedValue) { newValue in
if newValue == .dashboard {
Task {
await doReload()
}
}
}
#endif
.alertBinding($alert)
}
@@ -98,8 +96,8 @@ public struct ActiveDashboardView: View {
}
if ApplicationLibrary.inPreview {
profileList = [
Profile(id: 0, name: "profile local", type: .local, path: ""),
Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0)),
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
@@ -107,7 +105,7 @@ public struct ActiveDashboardView: View {
} else {
do {
profileList = try await ProfileManager.list()
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
if profileList.isEmpty {
return
}
@@ -116,7 +114,7 @@ public struct ActiveDashboardView: View {
profile.id == selectedProfileID
})
.isEmpty {
selectedProfileID = profileList[0].id!
selectedProfileID = profileList[0].id
await SharedPreferences.selectedProfileID.set(selectedProfileID)
}
@@ -31,7 +31,7 @@ public extension DashboardPage {
}
@MainActor
func contentView(_ profileList: Binding<[Profile]>, _ selectedProfileID: Binding<Int64?>, _ systemProxyAvailable: Binding<Bool>, _ systemProxyEnabled: Binding<Bool>) -> some View {
func contentView(_ profileList: Binding<[ProfilePreview]>, _ selectedProfileID: Binding<Int64>, _ systemProxyAvailable: Binding<Bool>, _ systemProxyEnabled: Binding<Bool>) -> some View {
viewBuilder {
switch self {
case .overview:
@@ -5,19 +5,26 @@ import SwiftUI
@MainActor
public struct OverviewView: View {
public static let NotificationUpdateSelectedProfile = Notification.Name("update-selected-profile")
@Environment(\.selection) private var selection
@EnvironmentObject private var environments: ExtensionEnvironments
@EnvironmentObject private var profile: ExtensionProfile
@Binding private var profileList: [Profile]
@Binding private var selectedProfileID: Int64!
@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
@State private var observer: Any?
public init(_ profileList: Binding<[Profile]>, _ selectedProfileID: Binding<Int64?>, _ systemProxyAvailable: Binding<Bool>, _ systemProxyEnabled: Binding<Bool>) {
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
@@ -45,7 +52,7 @@ public struct OverviewView: View {
}
}
Section("Profile") {
Picker(selection: $selectedProfileID) {
Picker(selection: selectedProfileIDLocal) {
ForEach(profileList, id: \.id) { profile in
Text(profile.name).tag(profile.id)
}
@@ -63,7 +70,7 @@ public struct OverviewView: View {
}
Section("Profile") {
ForEach(profileList, id: \.id) { profile in
Picker(profile.name, selection: $selectedProfileID) {
Picker(profile.name, selection: selectedProfileIDLocal) {
Text("").tag(profile.id)
}
}
@@ -74,44 +81,24 @@ public struct OverviewView: View {
}
}
.alertBinding($alert)
.onChangeCompat(of: selectedProfileID) {
reasserting = true
Task {
await switchProfile(selectedProfileID!)
}
}
.disabled(!ApplicationLibrary.inPreview && (!profile.status.isSwitchable || reasserting))
#if os(macOS)
.onAppear {
if observer == nil {
observer = NotificationCenter.default.addObserver(forName: OverviewView.NotificationUpdateSelectedProfile, object: nil, queue: nil, using: { newProfileID in
selectedProfileID = newProfileID.object as! Int64
})
}
}
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
}
#endif
}
private nonisolated func switchProfile(_ newProfileID: Int64) async {
private func switchProfile(_ newProfileID: Int64) async {
await SharedPreferences.selectedProfileID.set(newProfileID)
NotificationCenter.default.post(name: OverviewView.NotificationUpdateSelectedProfile, object: newProfileID)
if await profile.status.isConnected {
environments.selectedProfileUpdate.send()
if profile.status.isConnected {
do {
try LibboxNewStandaloneCommandClient()!.serviceReload()
try await serviceReload()
} catch {
await MainActor.run {
alert = Alert(error)
}
alert = Alert(error)
}
}
await MainActor.run {
reasserting = false
}
reasserting = false
}
private nonisolated func serviceReload() async throws {
try LibboxNewStandaloneCommandClient()?.serviceReload()
}
private nonisolated func setSystemProxyEnabled(_ isEnabled: Bool) async {
+58 -50
View File
@@ -5,53 +5,30 @@ public struct LogView: View {
@Environment(\.selection) private var selection
@EnvironmentObject private var environments: ExtensionEnvironments
private let logFont = Font.system(.caption2, design: .monospaced)
public init() {}
public 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 environments.logClient.logList.isEmpty {
VStack {
if environments.logClient.isConnected {
Text("Empty logs")
} else {
Text("Service not started").onAppear {
environments.connectLog()
}
}
}
} else {
ScrollViewReader { reader in
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(environments.logClient.logList.enumerated()), id: \.offset) { it in
ForEach(Array(logList.enumerated()), id: \.offset) { it in
Text(it.element)
.font(logFont)
#if os(tvOS)
@@ -59,12 +36,6 @@ public struct LogView: View {
#endif
Spacer(minLength: 8)
}
.onChangeCompat(of: environments.logClient.logList.count) { newCount in
withAnimation {
reader.scrollTo(newCount - 1)
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
@@ -73,8 +44,45 @@ public struct LogView: View {
.focusEffectDisabled()
.focusSection()
#endif
.onAppear {
reader.scrollTo(environments.logClient.logList.count - 1)
} 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)
}
}
}
}
@@ -7,17 +7,15 @@ public struct EditProfileView: View {
@Environment(\.openWindow) private var openWindow
#endif
@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?
private let updateCallback: (() -> Void)?
public init(_ updateCallback: (() -> Void)? = nil) {
self.updateCallback = updateCallback
}
public init() {}
public var body: some View {
FormView {
FormItem("Name") {
@@ -170,7 +168,7 @@ public struct EditProfileView: View {
do {
try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC)))
try await profile.updateRemoteProfile()
await performCallback()
environments.profileUpdate.send()
} catch {
alert = Alert(error)
}
@@ -183,7 +181,7 @@ public struct EditProfileView: View {
alert = Alert(error)
return
}
await performCallback()
environments.profileUpdate.send()
dismiss()
}
@@ -201,14 +199,6 @@ public struct EditProfileView: View {
}
isChanged = false
isLoading = false
await performCallback()
}
private func performCallback() async {
if let updateCallback {
updateCallback()
} else {
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
}
environments.profileUpdate.send()
}
}
@@ -9,6 +9,7 @@ public struct NewProfileView: View {
public static let windowID = "new-profile"
#endif
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.dismiss) private var dismiss
@State private var isSaving = false
@@ -27,9 +28,7 @@ public struct NewProfileView: View {
public let url: String
}
private let callback: (() async -> Void)?
public init(_ importRequest: ImportRequest? = nil, _ callback: (() async -> Void)? = nil) {
self.callback = callback
public init(_ importRequest: ImportRequest? = nil) {
if let importRequest {
_profileName = .init(initialValue: importRequest.name)
_profileType = .init(initialValue: .remote)
@@ -156,12 +155,9 @@ public struct NewProfileView: View {
alert = Alert(error)
return
}
if let callback {
await callback()
}
environments.profileUpdate.send()
dismiss()
#if os(macOS)
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
resetFields()
#endif
}
@@ -6,8 +6,7 @@ import SwiftUI
@MainActor
public struct ProfileView: View {
public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile")
@EnvironmentObject private var environments: ExtensionEnvironments
@Environment(\.importProfile) private var importProfile
@Environment(\.importRemoteProfile) private var importRemoteProfile
@State private var importRemoteProfileRequest: NewProfileView.ImportRequest?
@@ -17,7 +16,7 @@ public struct ProfileView: View {
@State private var isUpdating = false
@State private var alert: Alert?
@State private var profileList: [Profile] = []
@State private var profileList: [ProfilePreview] = []
#if os(iOS) || os(tvOS)
@State private var editMode = EditMode.inactive
@@ -29,10 +28,7 @@ public struct ProfileView: View {
@Environment(\.devicePickerSupports) private var devicePickerSupports
#endif
@State private var observer: Any?
public init() {}
public var body: some View {
VStack {
if isLoading {
@@ -46,17 +42,13 @@ public struct ProfileView: View {
ZStack {
if let importRemoteProfileRequest {
NavigationDestinationCompat(isPresented: $importRemoteProfilePresented) {
NewProfileView(importRemoteProfileRequest) {
await doReload()
}
NewProfileView(importRemoteProfileRequest)
}
}
FormView {
#if os(iOS)
NavigationLink {
NewProfileView {
await doReload()
}
NewProfileView()
} label: {
Text("New Profile").foregroundColor(.accentColor)
}
@@ -64,9 +56,7 @@ public struct ProfileView: View {
#elseif os(tvOS)
Section {
NavigationLink {
NewProfileView {
await doReload()
}
NewProfileView()
} label: {
Text("New Profile").foregroundColor(.accentColor)
}
@@ -85,7 +75,7 @@ public struct ProfileView: View {
Text("Empty profiles")
} else {
List {
ForEach(profileList, id: \.mustID) { profile in
ForEach(profileList, id: \.id) { profile in
viewBuilder {
if editMode.isEditing == true {
Text(profile.name)
@@ -106,7 +96,7 @@ public struct ProfileView: View {
} else {
FormView {
List {
ForEach(profileList, id: \.mustID) { profile in
ForEach(profileList, id: \.id) { profile in
ProfileItem(self, profile)
}
.onMove(perform: moveProfile)
@@ -128,15 +118,6 @@ public struct ProfileView: View {
importRemoteProfile.wrappedValue = nil
createImportRemoteProfileDialog(remoteProfile)
}
#if os(macOS)
if observer == nil {
observer = NotificationCenter.default.addObserver(forName: ProfileView.notificationName, object: nil, queue: .main) { _ in
Task {
await doReload()
}
}
}
#endif
}
.onChangeCompat(of: importProfile.wrappedValue) { newValue in
if let newValue {
@@ -150,23 +131,14 @@ public struct ProfileView: View {
createImportRemoteProfileDialog(newValue)
}
}
#if os(macOS)
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
observer = nil
.onReceive(environments.profileUpdate) { _ in
profileList = []
isLoading = true
// not updated, but why?
// Task {
// await doReload()
// }
}
.toolbar {
ToolbarItem {
Button {
openWindow(id: NewProfileView.windowID)
} label: {
Label("New Profile", systemImage: "plus.square.fill")
}
}
}
#endif
#if os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
@@ -232,18 +204,15 @@ public struct ProfileView: View {
private func doReload() async {
if ApplicationLibrary.inPreview {
profileList = [
Profile(id: 0, name: "profile local", type: .local, path: ""),
Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0)),
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 {
defer {
isLoading = false
}
do {
if !profileList.isEmpty {
profileList.removeAll()
}
profileList = try await ProfileManager.list()
profileList = try await ProfileManager.list().map { ProfilePreview($0) }
} catch {
alert = Alert(error)
return
@@ -279,11 +248,12 @@ public struct ProfileView: View {
private func moveProfile(from source: IndexSet, to destination: Int) {
profileList.move(fromOffsets: source, toOffset: destination)
for (index, profile) in profileList.enumerated() {
profile.order = UInt32(index)
profileList[index].order = UInt32(index)
profile.origin.order = UInt32(index)
}
Task {
do {
try await ProfileManager.update(profileList)
try await ProfileManager.update(profileList.map(\.origin))
} catch {
alert = Alert(error)
}
@@ -292,7 +262,7 @@ public struct ProfileView: View {
private func deleteProfile(where profileIndex: IndexSet) {
let profileToDelete = profileIndex.map { index in
profileList[index]
profileList[index].origin
}
profileList.remove(atOffsets: profileIndex)
Task {
@@ -306,16 +276,16 @@ public struct ProfileView: View {
public struct ProfileItem: View {
private let parent: ProfileView
private let profile: Profile
public init(_ parent: ProfileView, _ profile: Profile) {
@State private var profile: ProfilePreview
public init(_ parent: ProfileView, _ profile: ProfilePreview) {
self.parent = parent
self.profile = profile
_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)
body0.draggable(profile.origin)
} else {
body0
}
@@ -329,23 +299,20 @@ public struct ProfileView: View {
viewBuilder {
#if !os(macOS)
NavigationLink {
EditProfileView {
Task {
await parent.doReload()
}
}.environmentObject(profile)
EditProfileView().environmentObject(profile.origin)
} label: {
Text(profile.name)
}
.contextMenu {
ProfileShareButton(parent.$alert, profile) {
ProfileShareButton(parent.$alert, profile.origin) {
Label("Share", systemImage: "square.and.arrow.up.fill")
}
if profile.type == .remote {
Button {
parent.isUpdating = true
Task {
await parent.updateProfile(profile)
await parent.updateProfile(profile.origin)
profile = ProfilePreview(profile.origin)
}
} label: {
Label("Update", systemImage: "arrow.clockwise")
@@ -353,7 +320,7 @@ public struct ProfileView: View {
}
Button(role: .destructive) {
Task {
await parent.deleteProfile(profile)
await parent.deleteProfile(profile.origin)
}
} label: {
Label("Delete", systemImage: "trash.fill")
@@ -365,7 +332,7 @@ public struct ProfileView: View {
Text(profile.name)
if profile.type == .remote {
Spacer(minLength: 4)
Text("Last Updated: \(profile.lastUpdatedString)").font(.caption)
Text("Last Updated: \(profile.origin.lastUpdatedString)").font(.caption)
}
}
HStack {
@@ -373,23 +340,24 @@ public struct ProfileView: View {
Button {
parent.isUpdating = true
Task {
await parent.updateProfile(profile)
await parent.updateProfile(profile.origin)
profile = ProfilePreview(profile.origin)
}
} label: {
Image(systemName: "arrow.clockwise")
}
}
ProfileShareButton(parent.$alert, profile) {
ProfileShareButton(parent.$alert, profile.origin) {
Image(systemName: "square.and.arrow.up.fill")
}
Button {
parent.openWindow(id: EditProfileWindowView.windowID, value: profile.mustID)
parent.openWindow(id: EditProfileWindowView.windowID, value: profile.id)
} label: {
Image(systemName: "pencil")
}
Button {
Task {
await parent.deleteProfile(profile)
await parent.deleteProfile(profile.origin)
}
} label: {
Image(systemName: "trash.fill")