Improve UI
This commit is contained in:
@@ -1,286 +1,137 @@
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
|
||||
import StoreKit
|
||||
import SwiftUI
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
import ServiceManagement
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
public struct SettingView: View {
|
||||
#if os(macOS)
|
||||
@Environment(\.openWindow) private var openWindow
|
||||
#endif
|
||||
private enum Tabs: Int, CaseIterable, Identifiable {
|
||||
public var id: Self {
|
||||
self
|
||||
}
|
||||
|
||||
@State private var isLoading = true
|
||||
#if os(macOS)
|
||||
case app
|
||||
#endif
|
||||
|
||||
#if os(macOS)
|
||||
@State private var startAtLogin = false
|
||||
@Environment(\.showMenuBarExtra) private var showMenuBarExtra
|
||||
@State private var keepMenuBarInBackground = false
|
||||
#endif
|
||||
case core, packetTunnel, profileOverride, sponsor
|
||||
|
||||
@State private var alwaysOn = false
|
||||
@State private var disableMemoryLimit = false
|
||||
var label: some View {
|
||||
Label(title, systemImage: iconImage)
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
@State private var includeAllNetworks = false
|
||||
#endif
|
||||
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 .profileOverride:
|
||||
return NSLocalizedString("Profile Override", comment: "")
|
||||
case .sponsor:
|
||||
return NSLocalizedString("Sponsor", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
@State private var ignoreDeviceSleep = false
|
||||
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 .profileOverride:
|
||||
return "square.dashed.inset.filled"
|
||||
case .sponsor:
|
||||
return "heart.fill"
|
||||
}
|
||||
}
|
||||
|
||||
@State private var version = ""
|
||||
@State private var dataSize = ""
|
||||
@MainActor
|
||||
var contentView: some View {
|
||||
viewBuilder {
|
||||
switch self {
|
||||
#if os(macOS)
|
||||
case .app:
|
||||
MacAppView()
|
||||
#endif
|
||||
case .core:
|
||||
CoreView()
|
||||
case .packetTunnel:
|
||||
PacketTunnelView()
|
||||
case .profileOverride:
|
||||
ProfileOverrideView()
|
||||
case .sponsor:
|
||||
SponsorView()
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
#if os(iOS)
|
||||
.background(Color(uiColor: .systemGroupedBackground))
|
||||
#endif
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var navigationLink: some View {
|
||||
NavigationLink {
|
||||
contentView
|
||||
} label: {
|
||||
label
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@State private var isLoading = false
|
||||
@State private var taiwanFlagAvailable = false
|
||||
@State private var alert: Alert?
|
||||
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
viewBuilder {
|
||||
if isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task {
|
||||
await loadSettings()
|
||||
}
|
||||
FormView {
|
||||
#if os(macOS)
|
||||
Tabs.app.navigationLink
|
||||
#endif
|
||||
ForEach([Tabs.core, Tabs.packetTunnel, Tabs.profileOverride]) { it in
|
||||
it.navigationLink
|
||||
}
|
||||
Section("About") {
|
||||
Link(destination: URL(string: "https://sing-box.sagernet.org/")!) {
|
||||
Label("Documentation", systemImage: "doc.on.doc.fill")
|
||||
}
|
||||
} else {
|
||||
FormView {
|
||||
#if os(macOS)
|
||||
Section("MacOS") {
|
||||
Toggle("Start At Login", isOn: $startAtLogin)
|
||||
.onChangeCompat(of: startAtLogin) { newValue in
|
||||
Task {
|
||||
updateLoginItems(newValue)
|
||||
}
|
||||
}
|
||||
Toggle("Show in Menu Bar", isOn: showMenuBarExtra)
|
||||
.onChange(of: showMenuBarExtra.wrappedValue) { newValue in
|
||||
Task {
|
||||
await SharedPreferences.showMenuBarExtra.set(newValue)
|
||||
if !newValue {
|
||||
keepMenuBarInBackground = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if showMenuBarExtra.wrappedValue {
|
||||
Toggle("Keep Menu Bar in Background", isOn: $keepMenuBarInBackground)
|
||||
.onChangeCompat(of: keepMenuBarInBackground) { newValue in
|
||||
Task {
|
||||
await SharedPreferences.menuBarExtraInBackground.set(newValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Section("Packet Tunnel") {
|
||||
Toggle("Always On", isOn: $alwaysOn)
|
||||
.onChangeCompat(of: alwaysOn) { newValue in
|
||||
Task {
|
||||
await SharedPreferences.alwaysOn.set(newValue)
|
||||
await updateAlwaysOn(newValue)
|
||||
}
|
||||
}
|
||||
Toggle("Disable Memory Limit", isOn: $disableMemoryLimit)
|
||||
.onChangeCompat(of: disableMemoryLimit) { newValue in
|
||||
Task {
|
||||
await SharedPreferences.disableMemoryLimit.set(newValue)
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
Toggle("Include All Networks", isOn: $includeAllNetworks)
|
||||
.onChangeCompat(of: includeAllNetworks) { newValue in
|
||||
Task {
|
||||
await SharedPreferences.includeAllNetworks.set(newValue)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Toggle("Ignore Device Sleep", isOn: $ignoreDeviceSleep)
|
||||
.onChangeCompat(of: ignoreDeviceSleep) { newValue in
|
||||
Task {
|
||||
await SharedPreferences.ignoreDeviceSleep.set(newValue)
|
||||
}
|
||||
}
|
||||
#if os(macOS)
|
||||
if Variant.useSystemExtension {
|
||||
HStack {
|
||||
Button("Update System Extension") {
|
||||
Task {
|
||||
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")) {}
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
Button {
|
||||
Task {
|
||||
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")) {}
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text("Uninstall System Extension").foregroundColor(.red)
|
||||
}
|
||||
}.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
#endif
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.accentColor)
|
||||
#if !os(tvOS)
|
||||
RequestReviewButton {
|
||||
Label("Rate on the App Store", systemImage: "text.bubble.fill")
|
||||
}
|
||||
Section("Core") {
|
||||
FormTextItem("Version", version)
|
||||
FormTextItem("Data Size", dataSize)
|
||||
#if os(iOS) || os(tvOS)
|
||||
NavigationLink(destination: ServiceLogView()) {
|
||||
Text("View Service Log")
|
||||
}
|
||||
Button("Clear Working Directory") {
|
||||
Task {
|
||||
await clearWorkingDirectory()
|
||||
#endif
|
||||
Tabs.sponsor.navigationLink
|
||||
}
|
||||
Section("Debug") {
|
||||
NavigationLink {
|
||||
ServiceLogView()
|
||||
} label: {
|
||||
Label("Service Log", systemImage: "doc.on.clipboard")
|
||||
}
|
||||
FormTextItem("Taiwan Flag Available", "touchid") {
|
||||
if isLoading {
|
||||
Text("Loading...")
|
||||
.onAppear {
|
||||
Task.detached {
|
||||
taiwanFlagAvailable = !DeviceCensorship.isChinaDevice()
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
#elseif os(macOS)
|
||||
HStack {
|
||||
Button("View Service Log") {
|
||||
openWindow(id: ServiceLogView.windowID)
|
||||
}
|
||||
Button("Open Working Directory") {
|
||||
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: FilePath.workingDirectory.relativePath)
|
||||
}
|
||||
Button {
|
||||
Task {
|
||||
await clearWorkingDirectory()
|
||||
}
|
||||
} label: {
|
||||
Text("Clear Working Directory").foregroundColor(.red)
|
||||
}
|
||||
}.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
#endif
|
||||
}
|
||||
Section("Debug") {
|
||||
FormTextItem("Taiwan Flag Available", taiwanFlagAvailable.description)
|
||||
} else {
|
||||
Text(taiwanFlagAvailable.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.alertBinding($alert)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
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)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private func loadSettings() async {
|
||||
#if os(macOS)
|
||||
startAtLogin = SMAppService.mainApp.status == .enabled
|
||||
keepMenuBarInBackground = await SharedPreferences.menuBarExtraInBackground.get()
|
||||
#endif
|
||||
alwaysOn = await SharedPreferences.alwaysOn.get()
|
||||
disableMemoryLimit = await SharedPreferences.disableMemoryLimit.get()
|
||||
#if !os(tvOS)
|
||||
includeAllNetworks = await SharedPreferences.includeAllNetworks.get()
|
||||
#endif
|
||||
ignoreDeviceSleep = await SharedPreferences.ignoreDeviceSleep.get()
|
||||
if ApplicationLibrary.inPreview {
|
||||
version = "<redacted>"
|
||||
dataSize = LibboxFormatBytes(1000 * 1000 * 10)
|
||||
taiwanFlagAvailable = true
|
||||
isLoading = false
|
||||
} else {
|
||||
version = LibboxVersion()
|
||||
dataSize = "Loading..."
|
||||
taiwanFlagAvailable = !DeviceCensorship.isChinaDevice()
|
||||
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 clearWorkingDirectory() async {
|
||||
try? FileManager.default.removeItem(at: FilePath.workingDirectory)
|
||||
await MainActor.run {
|
||||
isLoading = true
|
||||
}
|
||||
}
|
||||
|
||||
private func updateAlwaysOn(_ newState: Bool) async {
|
||||
guard let profile = try? await ExtensionProfile.load() else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
try await profile.updateAlwaysOn(newState)
|
||||
} catch {
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.navigationTitle("Settings")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user