Improve dashboard & Request network on China iPhone model

This commit is contained in:
世界
2023-07-17 12:25:16 +08:00
parent f441b89efb
commit 2d3c0aa15d
9 changed files with 256 additions and 66 deletions
@@ -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
}
}
@@ -34,30 +34,38 @@ public struct ActiveDashboardView: View {
if profileList.isEmpty {
Text("Empty profiles")
} else {
#if os(iOS)
StartStopButton()
#endif
if profile.status.isConnected {
Section("Status") {
ExtensionStatusView()
}
}
Section("Profile") {
VStack {
#if os(iOS)
Picker(selection: $selectedProfileID) {
ForEach(profileList, id: \.id) { profile in
Text(profile.name).tag(profile.id)
if profile.status.isConnected {
ExtensionStatusView()
.listStyle(.automatic)
.navigationBarTitleDisplayMode(.inline)
}
FormView {
StartStopButton()
Section("Profile") {
Picker(selection: $selectedProfileID) {
ForEach(profileList, id: \.id) { profile in
Text(profile.name).tag(profile.id)
}
} label: {}
.pickerStyle(.inline)
}
} label: {}
.pickerStyle(.inline)
#elseif os(macOS)
ForEach(profileList, id: \.id) { profile in
Picker(profile.name, selection: $selectedProfileID) {
Text("").tag(profile.id)
}
#else
if profile.status.isConnected {
ExtensionStatusView()
}
FormView {
Section("Profile") {
ForEach(profileList, id: \.id) { profile in
Picker(profile.name, selection: $selectedProfileID) {
Text("").tag(profile.id)
}
}
.pickerStyle(.radioGroup)
}
}
.pickerStyle(.radioGroup)
#endif
}
.onChange(of: selectedProfileID) { _ in
@@ -6,11 +6,13 @@ public struct DashboardView: View {
public init() {}
public var body: some View {
FormView {
viewBuilder {
if let profile = extensionProfile.wrappedValue {
ActiveDashboardView().environmentObject(profile)
} else {
InstallProfileButton()
FormView {
InstallProfileButton()
}
}
}.navigationTitle("Dashboard")
}
@@ -6,6 +6,7 @@ public struct ExtensionStatusView: View {
@State private var commandClient: LibboxCommandClient?
@State private var message: LibboxStatusMessage?
@State private var connectTask: Task<Void, Error>?
@State private var columnCount: Int = 4
@State private var errorPresented = false
@State private var errorMessage = ""
@@ -15,23 +16,43 @@ public struct ExtensionStatusView: View {
public var body: some View {
viewBuilder {
if let message {
FormTextItem("Memory", LibboxFormatBytes(message.memory))
FormTextItem("Goroutines", "\(message.goroutines)")
FormTextItem("Connections", "\(message.connections)").contextMenu {
Button("Close", role: .destructive) {
Task.detached {
closeConnections()
VStack {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columnCount), alignment: .leading) {
if let message {
StatusItem("Memory", LibboxFormatBytes(message.memory))
StatusItem("Goroutines", "\(message.goroutines)")
if message.trafficAvailable {
StatusItem("Inbound Connections", "\(message.connectionsIn)")
StatusItem("Outbound Connections", "\(message.connectionsOut)")
StatusItem("Uplink", LibboxFormatBytes(message.uplink) + "/s")
StatusItem("Downlink", LibboxFormatBytes(message.downlink) + "/s")
StatusItem("Uplink Total", LibboxFormatBytes(message.uplinkTotal))
StatusItem("Downlink Total", LibboxFormatBytes(message.downlinkTotal))
} else {
StatusItem("Connections", "\(message.connectionsOut)")
}
} else {
StatusItem("Memory", "Loading...")
StatusItem("Goroutines", "Loading...")
StatusItem("Connections", "Loading...")
}
}.background {
GeometryReader { geometry in
Rectangle()
.fill(.clear)
.frame(height: 1)
.onChange(of: geometry.size.width) { newValue in
updateColumnCount(newValue)
}
.onAppear {
updateColumnCount(geometry.size.width)
}
}.padding()
}
} else {
FormTextItem("Memory", "Loading...")
FormTextItem("Goroutines", "Loading...")
FormTextItem("Connections", "Loading...")
}
.frame(alignment: .topLeading)
.padding([.top, .leading, .trailing])
}
.onAppear(perform: doReload)
.onDisappear {
connectTask?.cancel()
@@ -85,6 +106,15 @@ public struct ExtensionStatusView: View {
}
}
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
}
}
private func closeConnections() {
do {
try LibboxNewStandaloneCommandClient(FilePath.sharedDirectory.relativePath)?.closeConnections()
@@ -113,4 +143,38 @@ public struct ExtensionStatusView: View {
func writeGroups(_: LibboxOutboundGroupIteratorProtocol?) {}
}
private struct StatusItem: View {
private let name: String
private let value: String
init(_ name: String, _ value: String) {
self.name = name
self.value = value
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack {
Text(name)
.font(.subheadline)
.foregroundColor(.secondary)
Spacer()
}
Text(value)
.font(.system(size: 16))
}
.frame(minWidth: 125)
.padding(EdgeInsets(top: 10, leading: 13, bottom: 10, trailing: 13))
.background(backgroundColor)
.cornerRadius(10)
}
private var backgroundColor: Color {
#if os(iOS)
return Color(uiColor: .secondarySystemGroupedBackground)
#elseif os(macOS)
return Color(nsColor: .textBackgroundColor)
#endif
}
}
}
@@ -21,6 +21,7 @@ public struct SettingView: View {
@State private var disableMemoryLimit = false
@State private var version = ""
@State private var dataSize = ""
@State private var taiwanFlagAvailable = false
@State private var errorPresented = false
@State private var errorMessage = ""
@@ -80,6 +81,9 @@ public struct SettingView: View {
}
.foregroundColor(.red)
}
Section("Debug") {
FormTextItem("Taiwan Flag Available", taiwanFlagAvailable.description)
}
}
}
}
@@ -121,6 +125,7 @@ public struct SettingView: View {
dataSize = "Loading..."
isLoading = false
dataSize = (try? FilePath.workingDirectory.formattedSize()) ?? "Unknown"
taiwanFlagAvailable = !DeviceCensorship.isChinaDevice()
}
private func clearWorkingDirectory() {
+4
View File
@@ -16,4 +16,8 @@ public enum SharedPreferences {
@Preference<Bool>("show_menu_bar_extra", defaultValue: true) public static var showMenuBarExtra
@Preference<Bool>("started_by_user", defaultValue: false) public static var startedByUser
#endif
#if os(iOS)
@Preference<Bool>("network_permission_requested", defaultValue: false) public static var networkPermissionRequested
#endif
}
+23
View File
@@ -15,6 +15,29 @@ class ApplicationDelegate: NSObject, UIApplicationDelegate {
NSLog("setup background task error: \(error.localizedDescription)")
}
}
Task.detached {
await self.requestNetworkPermission()
}
return true
}
private func requestNetworkPermission() {
if UIDevice.current.userInterfaceIdiom != .phone {
return
}
if SharedPreferences.networkPermissionRequested {
return
}
if !DeviceCensorship.isChinaDevice() {
SharedPreferences.networkPermissionRequested = true
return
}
URLSession.shared.dataTask(with: URL(string: "http://captive.apple.com")!) { _, response, _ in
if let response = response as? HTTPURLResponse {
if response.statusCode == 200 {
SharedPreferences.networkPermissionRequested = true
}
}
}.resume()
}
}
+9 -5
View File
@@ -72,6 +72,7 @@
3AC1944F2A50247300BD8CB9 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */; };
3AC194502A502DFE00BD8CB9 /* ServiceNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC1944C2A50206C00BD8CB9 /* ServiceNotification.swift */; };
3AC194522A50303300BD8CB9 /* ApplicationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC194512A50303300BD8CB9 /* ApplicationDelegate.swift */; };
3AC5EC082A6417470077AF34 /* DeviceCensorship.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AC5EC072A6417470077AF34 /* DeviceCensorship.swift */; };
3ADF8DFB2A4AFDF500900CC8 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC210D2A459B1900A63465 /* MainView.swift */; };
3AEAEE992A4F16430059612D /* Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3A096F862A4ED3DE00D4A2ED /* Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
3AEC20F62A459AB400A63465 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AEC20F52A459AB400A63465 /* Application.swift */; };
@@ -298,6 +299,7 @@
3AC1944C2A50206C00BD8CB9 /* ServiceNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceNotification.swift; sourceTree = "<group>"; };
3AC1944E2A50247300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = "<group>"; };
3AC194512A50303300BD8CB9 /* ApplicationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDelegate.swift; sourceTree = "<group>"; };
3AC5EC072A6417470077AF34 /* DeviceCensorship.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceCensorship.swift; sourceTree = "<group>"; };
3ADF8DF12A4AF59900900CC8 /* ActiveDashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveDashboardView.swift; sourceTree = "<group>"; };
3ADF8DF32A4AF9B500900CC8 /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = "<group>"; };
3ADF8DF62A4AFB2C00900CC8 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
@@ -615,9 +617,9 @@
isa = PBXGroup;
children = (
3AF342A22A4A9B9B002B34AC /* InstallProfileButton.swift */,
3AF342CC2A4AA88C002B34AC /* StartStopButton.swift */,
3AF342D02A4AACC4002B34AC /* ExtensionStatusView.swift */,
3ADF8DF12A4AF59900900CC8 /* ActiveDashboardView.swift */,
3AF342CC2A4AA88C002B34AC /* StartStopButton.swift */,
3ADF8DF32A4AF9B500900CC8 /* DashboardView.swift */,
);
path = Dashboard;
@@ -629,6 +631,7 @@
3A7E90302A46745A00D53052 /* ViewBuilder.swift */,
3AF342D32A4AADB2002B34AC /* Formtem.swift */,
3ADF8E022A4B118700900CC8 /* Binding+Unwrap.swift */,
3AC5EC072A6417470077AF34 /* DeviceCensorship.swift */,
);
path = Abstract;
sourceTree = "<group>";
@@ -921,6 +924,7 @@
3A4EAD262A4FEB65005435B3 /* ExtensionStatusView.swift in Sources */,
3A4EAD252A4FEB65005435B3 /* StartStopButton.swift in Sources */,
3A4EAD2B2A4FEB6D005435B3 /* ViewBuilder.swift in Sources */,
3AC5EC082A6417470077AF34 /* DeviceCensorship.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1431,7 +1435,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.3.1;
OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
@@ -1473,7 +1477,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.3.1;
OTHER_CODE_SIGN_FLAGS = "--deep";
OTHER_LDFLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
@@ -1513,7 +1517,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.3.1;
OTHER_CODE_SIGN_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box β";
@@ -1551,7 +1555,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 13.0;
MARKETING_VERSION = 1.0;
MARKETING_VERSION = 1.3.1;
OTHER_CODE_SIGN_FLAGS = "";
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfa;
PRODUCT_NAME = "sing-box β";
@@ -12,42 +12,42 @@
<key>Associations (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>8</integer>
</dict>
<key>Associations (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>9</integer>
</dict>
<key>Associations (Playground) 3.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>27</integer>
</dict>
<key>Associations (Playground) 4.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>28</integer>
</dict>
<key>Associations (Playground) 5.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>29</integer>
</dict>
<key>Associations (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>7</integer>
</dict>
@@ -69,42 +69,42 @@
<key>MyPlayground (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>4</integer>
</dict>
<key>MyPlayground (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>6</integer>
</dict>
<key>MyPlayground (Playground) 3.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>23</integer>
</dict>
<key>MyPlayground (Playground) 4.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>24</integer>
</dict>
<key>MyPlayground (Playground) 5.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>26</integer>
</dict>
<key>MyPlayground (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>3</integer>
</dict>
@@ -136,84 +136,84 @@
<key>Tour (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>15</integer>
</dict>
<key>Tour (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>17</integer>
</dict>
<key>Tour (Playground) 3.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>33</integer>
</dict>
<key>Tour (Playground) 4.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>34</integer>
</dict>
<key>Tour (Playground) 5.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>35</integer>
</dict>
<key>Tour (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>14</integer>
</dict>
<key>TransactionObserver (Playground) 1.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>12</integer>
</dict>
<key>TransactionObserver (Playground) 2.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>13</integer>
</dict>
<key>TransactionObserver (Playground) 3.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>30</integer>
</dict>
<key>TransactionObserver (Playground) 4.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>31</integer>
</dict>
<key>TransactionObserver (Playground) 5.xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>32</integer>
</dict>
<key>TransactionObserver (Playground).xcscheme</key>
<dict>
<key>isShown</key>
<false />
<false/>
<key>orderHint</key>
<integer>11</integer>
</dict>
@@ -238,17 +238,17 @@
<key>3A096F852A4ED3DE00D4A2ED</key>
<dict>
<key>primary</key>
<true />
<true/>
</dict>
<key>3A77016C2A4E6B34008F031F</key>
<dict>
<key>primary</key>
<true />
<true/>
</dict>
<key>3AEC211C2A459B4700A63465</key>
<dict>
<key>primary</key>
<true />
<true/>
</dict>
</dict>
</dict>