Add screenshot generator

This commit is contained in:
世界
2026-01-08 16:13:20 +08:00
parent 264c4eed81
commit 6b589cb771
57 changed files with 1931 additions and 109 deletions
+9
View File
@@ -91,6 +91,15 @@ public class CommandClient: ObservableObject {
self.init([connectionType], logMaxLines: logMaxLines)
}
public func setupMockData() {
isConnected = true
clashModeList = ["rule", "global", "direct"]
clashMode = "rule"
uplinkHistory = Array(repeating: CGFloat(1000), count: 30)
downlinkHistory = Array(repeating: CGFloat(5000), count: 30)
hasAnyConnection = true
}
public func connect() {
if isConnected {
return
+9 -1
View File
@@ -141,7 +141,13 @@ public class ExtensionEnvironments: ObservableObject {
public let selectedProfileUpdate = ObjectWillChangePublisher()
public let openSettings = ObjectWillChangePublisher()
public init() {}
public init() {
if Variant.screenshotMode {
extensionProfileLoading = false
extensionProfile = .mock
commandClient.setupMockData()
}
}
public func postReload() {
Task {
@@ -150,6 +156,7 @@ public class ExtensionEnvironments: ObservableObject {
}
public func reload() async {
if Variant.screenshotMode { return }
if let newProfile = try? await ExtensionProfile.load() {
if extensionProfile == nil || extensionProfile?.status == .invalid {
newProfile.register()
@@ -163,6 +170,7 @@ public class ExtensionEnvironments: ObservableObject {
}
public func connect() {
if Variant.screenshotMode { return }
guard let profile = extensionProfile else {
return
}
+42 -2
View File
@@ -12,9 +12,10 @@ private let logger = Logger(category: "ExtensionProfile")
public class ExtensionProfile: ObservableObject {
public static let controlKind = AppConfiguration.widgetControlKind
private let manager: NEVPNManager
private var connection: NEVPNConnection
private let manager: NEVPNManager?
private var connection: NEVPNConnection?
private var observer: Any?
private let isMock: Bool
@Published public var status: NEVPNStatus
@Published public var connectedDate: Date?
@@ -24,9 +25,28 @@ public class ExtensionProfile: ObservableObject {
connection = manager.connection
status = manager.connection.status
connectedDate = manager.connection.connectedDate
isMock = false
}
private init(mockStatus: NEVPNStatus, mockConnectedDate: Date?) {
manager = nil
connection = nil
status = mockStatus
connectedDate = mockConnectedDate
isMock = true
}
private static var _mock: ExtensionProfile?
public static var mock: ExtensionProfile {
if _mock == nil {
_mock = ExtensionProfile(mockStatus: .connected, mockConnectedDate: Date().addingTimeInterval(-3600))
}
return _mock!
}
public func register() {
guard !isMock, let manager else { return }
observer = NotificationCenter.default.addObserver(
forName: NSNotification.Name.NEVPNStatusDidChange,
object: manager.connection,
@@ -88,6 +108,7 @@ public class ExtensionProfile: ObservableObject {
}
private func setOnDemandRules(useDefaultRules: Bool) async {
guard let manager else { return }
if useDefaultRules {
manager.onDemandRules = Self.makeDefaultOnDemandRules()
} else {
@@ -97,6 +118,7 @@ public class ExtensionProfile: ObservableObject {
}
public func updateOnDemand(enabled: Bool, useDefaultRules: Bool) async throws {
guard let manager else { return }
manager.isOnDemandEnabled = enabled
await setOnDemandRules(useDefaultRules: useDefaultRules)
try await manager.saveToPreferences()
@@ -104,10 +126,19 @@ public class ExtensionProfile: ObservableObject {
@available(iOS 16.0, macOS 13.0, tvOS 17.0, *)
public func fetchLastDisconnectError() async throws {
guard let connection else { return }
try await connection.fetchLastDisconnectError()
}
public func start() async throws {
if isMock {
status = .connecting
try await Task.sleep(nanoseconds: 500_000_000)
status = .connected
connectedDate = Date()
return
}
guard let manager else { return }
try await fetchProfile()
manager.isEnabled = true
let alwaysOn = await SharedPreferences.alwaysOn.get()
@@ -131,6 +162,7 @@ public class ExtensionProfile: ObservableObject {
}
public func reloadService() async throws {
if isMock { return }
let options = try await prepareStartOptions()
let data = try ExtensionStartOptions.encode(options)
guard let session = connection as? NETunnelProviderSession else {
@@ -197,6 +229,14 @@ public class ExtensionProfile: ObservableObject {
}
public func stop() async throws {
if isMock {
status = .disconnecting
try await Task.sleep(nanoseconds: 300_000_000)
status = .disconnected
connectedDate = nil
return
}
guard let manager else { return }
if manager.isOnDemandEnabled {
manager.isOnDemandEnabled = false
try await manager.saveToPreferences()
@@ -0,0 +1,42 @@
import Foundation
import ObjectiveC
private var screenshotLocalizationBundleKey: UInt8 = 0
private final class ScreenshotBundle: Bundle, @unchecked Sendable {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
if let bundle = objc_getAssociatedObject(self, &screenshotLocalizationBundleKey) as? Bundle {
return bundle.localizedString(forKey: key, value: value, table: tableName)
}
return super.localizedString(forKey: key, value: value, table: tableName)
}
}
public enum ScreenshotLocalization {
public static func applyIfNeeded() {
let environment = ProcessInfo.processInfo.environment
guard let language = environment["SCREENSHOT_LANGUAGE"]?
.trimmingCharacters(in: .whitespacesAndNewlines),
!language.isEmpty
else {
return
}
UserDefaults.standard.set([language], forKey: "AppleLanguages")
if let locale = environment["SCREENSHOT_LOCALE"]?
.trimmingCharacters(in: .whitespacesAndNewlines),
!locale.isEmpty
{
UserDefaults.standard.set(locale, forKey: "AppleLocale")
}
if let path = Bundle.main.path(forResource: language, ofType: "lproj"),
let localizedBundle = Bundle(path: path)
{
objc_setAssociatedObject(Bundle.main, &screenshotLocalizationBundleKey, localizedBundle, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
object_setClass(Bundle.main, ScreenshotBundle.self)
}
}
}
private let _screenshotLocalizationApplied: Void = {
ScreenshotLocalization.applyIfNeeded()
}()
+6
View File
@@ -22,4 +22,10 @@ public enum Variant {
public static var debugNoIOS26 = false
public static var debugNoIOS18 = false
#endif
#if targetEnvironment(simulator)
public static let screenshotMode = true
#else
public static let screenshotMode = ProcessInfo.processInfo.arguments.contains("-FASTLANE_SNAPSHOT")
#endif
}