@@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public extension Bool {
|
||||
func toString() -> String {
|
||||
if self {
|
||||
return String(localized: "true")
|
||||
} else {
|
||||
return String(localized: "false")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,8 +20,8 @@ public struct ConnectionDetailsView: View {
|
||||
if let closedAt = connection.closedAt {
|
||||
FormTextItem("Closed At", closedAt.myFormat)
|
||||
}
|
||||
FormTextItem("Upload", LibboxFormatBytes(connection.uploadTotal))
|
||||
FormTextItem("Download", LibboxFormatBytes(connection.downloadTotal))
|
||||
FormTextItem("Uplink", LibboxFormatBytes(connection.uploadTotal))
|
||||
FormTextItem("Downlink", LibboxFormatBytes(connection.downloadTotal))
|
||||
Section("Metadata") {
|
||||
FormTextItem("Inbound", connection.inbound)
|
||||
FormTextItem("Inbound Type", connection.inboundType)
|
||||
|
||||
@@ -31,11 +31,11 @@ public extension DashboardPage {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .overview:
|
||||
return NSLocalizedString("Overview", comment: "")
|
||||
return String(localized: "Overview")
|
||||
case .groups:
|
||||
return NSLocalizedString("Groups", comment: "")
|
||||
return String(localized: "Groups")
|
||||
case .connections:
|
||||
return NSLocalizedString("Connections", comment: "")
|
||||
return String(localized: "Connections")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ public struct DashboardView: View {
|
||||
await MainActor.run {
|
||||
alert = Alert(
|
||||
title: Text("Full Disk Access permission is required"),
|
||||
message: Text("Please grant the permission for SFMExtension, then we can continue."),
|
||||
message: Text("Please grant the permission for **SFMExtension**, then we can continue."),
|
||||
primaryButton: .default(Text("Authorize"), action: openFDASettings),
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
|
||||
@@ -27,49 +27,49 @@ public struct ExtensionStatusView: View {
|
||||
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(String(localized: "Status")) {
|
||||
StatusLine(String(localized: "Memory"), "6.4 MB")
|
||||
StatusLine(String(localized: "Goroutines"), "89")
|
||||
}
|
||||
StatusItem("Connections") {
|
||||
StatusLine("Inbound", "34")
|
||||
StatusLine("Outbound", "28")
|
||||
StatusItem(String(localized: "Connections")) {
|
||||
StatusLine(String(localized: "Inbound"), "34")
|
||||
StatusLine(String(localized: "Outbound"), "28")
|
||||
}
|
||||
StatusItem("Traffic") {
|
||||
StatusLine("Uplink", "38 B/s")
|
||||
StatusLine("Downlink", "249 MB/s")
|
||||
StatusItem(String(localized: "Traffic")) {
|
||||
StatusLine(String(localized: "Uplink"), "38 B/s")
|
||||
StatusLine(String(localized: "Downlink"), "249 MB/s")
|
||||
}
|
||||
StatusItem("TrafficTotal") {
|
||||
StatusLine("Uplink", "52 MB")
|
||||
StatusLine("Downlink", "5.6 GB")
|
||||
StatusItem(String(localized: "Traffic Total")) {
|
||||
StatusLine(String(localized: "Uplink"), "52 MB")
|
||||
StatusLine(String(localized: "Downlink"), "5.6 GB")
|
||||
}
|
||||
} else if let message = commandClient.status {
|
||||
StatusItem("Status") {
|
||||
StatusLine("Memory", LibboxFormatMemoryBytes(message.memory))
|
||||
StatusLine("Goroutines", "\(message.goroutines)")
|
||||
StatusItem(String(localized: "Status")) {
|
||||
StatusLine(String(localized: "Memory"), LibboxFormatMemoryBytes(message.memory))
|
||||
StatusLine(String(localized: "Goroutines"), "\(message.goroutines)")
|
||||
}
|
||||
StatusItem("Connections") {
|
||||
StatusLine("Inbound", "\(message.connectionsIn)")
|
||||
StatusLine("Outbound", "\(message.connectionsOut)")
|
||||
StatusItem(String(localized: "Connections")) {
|
||||
StatusLine(String(localized: "Inbound"), "\(message.connectionsIn)")
|
||||
StatusLine(String(localized: "Outbound"), "\(message.connectionsOut)")
|
||||
}
|
||||
if message.trafficAvailable {
|
||||
StatusItem("Traffic") {
|
||||
StatusLine("Uplink", "\(LibboxFormatBytes(message.uplink))/s")
|
||||
StatusLine("Downlink", "\(LibboxFormatBytes(message.downlink))/s")
|
||||
StatusItem(String(localized: "Traffic")) {
|
||||
StatusLine(String(localized: "Uplink"), "\(LibboxFormatBytes(message.uplink))/s")
|
||||
StatusLine(String(localized: "Downlink"), "\(LibboxFormatBytes(message.downlink))/s")
|
||||
}
|
||||
StatusItem("Traffic Total") {
|
||||
StatusLine("Uplink", LibboxFormatBytes(message.uplinkTotal))
|
||||
StatusLine("Downlink", LibboxFormatBytes(message.downlinkTotal))
|
||||
StatusItem(String(localized: "Traffic Total")) {
|
||||
StatusLine(String(localized: "Uplink"), LibboxFormatBytes(message.uplinkTotal))
|
||||
StatusLine(String(localized: "Downlink"), LibboxFormatBytes(message.downlinkTotal))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StatusItem("Status") {
|
||||
StatusLine("Memory", "...")
|
||||
StatusLine("Goroutines", "...")
|
||||
StatusItem(String(localized: "Status")) {
|
||||
StatusLine(String(localized: "Memory"), "...")
|
||||
StatusLine(String(localized: "Goroutines"), "...")
|
||||
}
|
||||
StatusItem("Connections") {
|
||||
StatusLine("Inbound", "...")
|
||||
StatusLine("Outbound", "...")
|
||||
StatusItem(String(localized: "Connections")) {
|
||||
StatusLine(String(localized: "Inbound"), "...")
|
||||
StatusLine(String(localized: "Outbound"), "...")
|
||||
}
|
||||
}
|
||||
}.background {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
do {
|
||||
if let result = try await SystemExtension.install() {
|
||||
if result == .willCompleteAfterReboot {
|
||||
alert = Alert(errorMessage: "Need Reboot")
|
||||
alert = Alert(errorMessage: String(localized: "Need Reboot"))
|
||||
}
|
||||
}
|
||||
await callback()
|
||||
|
||||
@@ -32,19 +32,19 @@ public extension NavigationPage {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .dashboard:
|
||||
return NSLocalizedString("Dashboard", comment: "")
|
||||
return String(localized: "Dashboard")
|
||||
#if os(macOS)
|
||||
case .groups:
|
||||
return NSLocalizedString("Groups", comment: "")
|
||||
return String(localized: "Groups")
|
||||
case .connections:
|
||||
return NSLocalizedString("Connections", comment: "")
|
||||
#endif
|
||||
case .logs:
|
||||
return NSLocalizedString("Logs", comment: "")
|
||||
return String(localized: "Logs")
|
||||
case .profiles:
|
||||
return NSLocalizedString("Profiles", comment: "")
|
||||
return String(localized: "Profiles")
|
||||
case .settings:
|
||||
return NSLocalizedString("Settings", comment: "")
|
||||
return String(localized: "Settings")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,9 +104,9 @@
|
||||
|
||||
private var navigationTitle: String {
|
||||
if readOnly {
|
||||
return "View Content"
|
||||
return String(localized: "View Content")
|
||||
} else {
|
||||
return "Edit Content"
|
||||
return String(localized: "Edit Content")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public struct EditProfileView: View {
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
FormView {
|
||||
FormItem("Name") {
|
||||
FormItem(String(localized: "Name")) {
|
||||
TextField("Name", text: $profile.name, prompt: Text("Required"))
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
@@ -31,17 +31,17 @@ public struct EditProfileView: View {
|
||||
}
|
||||
.disabled(true)
|
||||
if profile.type == .icloud {
|
||||
FormItem("Path") {
|
||||
FormItem(String(localized: "Path")) {
|
||||
TextField("Path", text: $profile.path, prompt: Text("Required"))
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
} else if profile.type == .remote {
|
||||
FormItem("URL") {
|
||||
FormItem(String(localized: "URL")) {
|
||||
TextField("URL", text: $profile.remoteURL.unwrapped(""), prompt: Text("Required"))
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
Toggle("Auto Update", isOn: $profile.autoUpdate)
|
||||
FormItem("Auto Update Interval") {
|
||||
FormItem(String(localized: "Auto Update Interval")) {
|
||||
TextField("Auto Update Interval", text: $profile.autoUpdateInterval.stringBinding(defaultValue: 60), prompt: Text("In Minutes"))
|
||||
.multilineTextAlignment(.trailing)
|
||||
#if os(iOS)
|
||||
|
||||
@@ -34,7 +34,7 @@ public struct NewProfileView: View {
|
||||
|
||||
public var body: some View {
|
||||
FormView {
|
||||
FormItem("Name") {
|
||||
FormItem(String(localized: "Name")) {
|
||||
TextField("Name", text: $profileName, prompt: Text("Required"))
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
@@ -76,17 +76,17 @@ public struct NewProfileView: View {
|
||||
}
|
||||
}
|
||||
} else if profileType == .icloud {
|
||||
FormItem("Path") {
|
||||
FormItem(String(localized: "Path")) {
|
||||
TextField("Path", text: $remotePath, prompt: Text("Required"))
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
} else if profileType == .remote {
|
||||
FormItem("URL") {
|
||||
FormItem(String(localized: "URL")) {
|
||||
TextField("URL", text: $remotePath, prompt: Text("Required"))
|
||||
.multilineTextAlignment(.trailing)
|
||||
}
|
||||
Toggle("Auto Update", isOn: $autoUpdate)
|
||||
FormItem("Auto Update Interval") {
|
||||
FormItem(String(localized: "Auto Update Interval")) {
|
||||
TextField("Auto Update Interval", text: $autoUpdateInterval.stringBinding(defaultValue: 60), prompt: Text("In Minutes"))
|
||||
.multilineTextAlignment(.trailing)
|
||||
#if os(iOS)
|
||||
@@ -135,15 +135,15 @@ public struct NewProfileView: View {
|
||||
isSaving = false
|
||||
}
|
||||
if profileName.isEmpty {
|
||||
alert = Alert(errorMessage: "Missing profile name")
|
||||
alert = Alert(errorMessage: String(localized: "Missing profile name"))
|
||||
return
|
||||
}
|
||||
if remotePath.isEmpty {
|
||||
if profileType == .icloud {
|
||||
alert = Alert(errorMessage: "Missing path")
|
||||
alert = Alert(errorMessage: String(localized: "Missing path"))
|
||||
return
|
||||
} else if profileType == .remote {
|
||||
alert = Alert(errorMessage: "Missing URL")
|
||||
alert = Alert(errorMessage: String(localized: "Missing URL"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
import AppKit
|
||||
import Library
|
||||
import ServiceManagement
|
||||
import SwiftUI
|
||||
#endif
|
||||
|
||||
public struct MacAppView: View {
|
||||
@State private var isLoading = true
|
||||
public struct AppView: View {
|
||||
@State private var isLoading = true
|
||||
|
||||
@State private var startAtLogin = false
|
||||
@Environment(\.showMenuBarExtra) private var showMenuBarExtra
|
||||
@State private var menuBarExtraInBackground = false
|
||||
@State private var startAtLogin = false
|
||||
@Environment(\.showMenuBarExtra) private var showMenuBarExtra
|
||||
@State private var menuBarExtraInBackground = false
|
||||
|
||||
@State private var alert: Alert?
|
||||
@State private var alert: Alert?
|
||||
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
viewBuilder {
|
||||
if isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task {
|
||||
await loadSettings()
|
||||
}
|
||||
public init() {}
|
||||
public var body: some View {
|
||||
viewBuilder {
|
||||
if isLoading {
|
||||
ProgressView().onAppear {
|
||||
Task {
|
||||
await loadSettings()
|
||||
}
|
||||
} else {
|
||||
FormView {
|
||||
}
|
||||
} else {
|
||||
FormView {
|
||||
#if os(macOS)
|
||||
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)
|
||||
}
|
||||
@@ -66,21 +69,26 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
.alertBinding($alert)
|
||||
.navigationTitle("App")
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
.alertBinding($alert)
|
||||
.navigationTitle("App")
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
|
||||
private func loadSettings() async {
|
||||
private func loadSettings() async {
|
||||
#if os(macOS)
|
||||
startAtLogin = SMAppService.mainApp.status == .enabled
|
||||
menuBarExtraInBackground = await SharedPreferences.menuBarExtraInBackground.get()
|
||||
isLoading = false
|
||||
}
|
||||
#endif
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
|
||||
private func updateLoginItems(_ startAtLogin: Bool) {
|
||||
do {
|
||||
@@ -143,6 +151,6 @@
|
||||
alert = Alert(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -87,11 +87,11 @@ public struct ServiceLogView: View {
|
||||
guard let value = element.value as? Int8, value != 0 else { return identifier }
|
||||
return identifier + String(UnicodeScalar(UInt8(value)))
|
||||
}
|
||||
var deviceInfo = "Machine: " + machineName + "\n"
|
||||
var deviceInfo = String("Machine: ") + machineName + "\n"
|
||||
#if os(iOS)
|
||||
await deviceInfo += "System: " + (UIDevice.current.systemName) + " " + (UIDevice.current.systemVersion) + "\n"
|
||||
await deviceInfo += String("System: ") + (UIDevice.current.systemName) + " " + (UIDevice.current.systemVersion) + "\n"
|
||||
#elseif os(macOS)
|
||||
deviceInfo += "System: macOS " + ProcessInfo().operatingSystemVersionString + "\n"
|
||||
deviceInfo += String("System: ") + "macOS " + ProcessInfo().operatingSystemVersionString + "\n"
|
||||
#endif
|
||||
content = deviceInfo + "\n" + content
|
||||
}
|
||||
|
||||
@@ -22,18 +22,18 @@ public struct SettingView: View {
|
||||
switch self {
|
||||
#if os(macOS)
|
||||
case .app:
|
||||
return NSLocalizedString("App", comment: "")
|
||||
return String(localized: "App")
|
||||
#endif
|
||||
case .core:
|
||||
return NSLocalizedString("Core", comment: "")
|
||||
return String(localized: "Core")
|
||||
case .packetTunnel:
|
||||
return NSLocalizedString("Packet Tunnel", comment: "")
|
||||
return String(localized: "Packet Tunnel")
|
||||
case .onDemandRules:
|
||||
return NSLocalizedString("On Demand Rules", comment: "")
|
||||
return String(localized: "On Demand Rules")
|
||||
case .profileOverride:
|
||||
return NSLocalizedString("Profile Override", comment: "")
|
||||
return String(localized: "Profile Override")
|
||||
case .sponsors:
|
||||
return NSLocalizedString("Sponsors", comment: "")
|
||||
return String(localized: "Sponsors")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ public struct SettingView: View {
|
||||
switch self {
|
||||
#if os(macOS)
|
||||
case .app:
|
||||
MacAppView()
|
||||
AppView()
|
||||
#endif
|
||||
case .core:
|
||||
CoreView()
|
||||
@@ -106,11 +106,29 @@ public struct SettingView: View {
|
||||
}
|
||||
#if !os(tvOS)
|
||||
Section("About") {
|
||||
Link(destination: URL(string: "https://sing-box.sagernet.org/")!) {
|
||||
Link(destination: URL(string: String(localized: "https://sing-box.sagernet.org/"))!) {
|
||||
Label("Documentation", systemImage: "doc.on.doc.fill")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.accentColor)
|
||||
.contextMenu {
|
||||
Link(destination: URL(string: String(localized: "https://sing-box.sagernet.org/changelog/"))!) {
|
||||
Text("Changelog")
|
||||
}
|
||||
Link(destination: URL(string: String(localized: "https://sing-box.sagernet.org/configuration/"))!) {
|
||||
Text("Configuration")
|
||||
}
|
||||
}
|
||||
Link(destination: URL(string: String("https://github.com/SagerNet/sing-box"))!) {
|
||||
Label("Source Code", systemImage: "pills.fill")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.foregroundColor(.accentColor)
|
||||
.contextMenu {
|
||||
Link(destination: URL(string: String("https://github.com/SagerNet/sing-box/releases"))!) {
|
||||
Text("Releases")
|
||||
}
|
||||
}
|
||||
RequestReviewButton {
|
||||
Label("Rate on the App Store", systemImage: "text.bubble.fill")
|
||||
}
|
||||
@@ -145,7 +163,7 @@ public struct SettingView: View {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text(taiwanFlagAvailable.description)
|
||||
Text(taiwanFlagAvailable.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ public struct SponsorsView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
FormButton("GitHub Sponsor (recommended)") {
|
||||
FormButton(String(localized: "GitHub Sponsors (recommended)")) {
|
||||
openURL(URL(string: "https://github.com/sponsors/nekohasekai")!)
|
||||
}
|
||||
FormButton("Other methods") {
|
||||
FormButton(String(localized: "Other methods")) {
|
||||
openURL(URL(string: "https://sekai.icu/sponsors/")!)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user