Add screenshot generator
This commit is contained in:
+5
-1
@@ -7,4 +7,8 @@ xcuserdata/
|
||||
CLAUDE.md
|
||||
.claude
|
||||
.mcp.json
|
||||
.serena
|
||||
/screenshots/
|
||||
/fastlane/report.xml
|
||||
/fastlane/test_output/
|
||||
/fastlane/.bundle/
|
||||
/fastlane/.cache/
|
||||
|
||||
@@ -2,5 +2,4 @@ import Foundation
|
||||
|
||||
public class ApplicationLibrary {
|
||||
public static let bundle = Bundle(for: ApplicationLibrary.self)
|
||||
public static let inPreview = false
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@ public struct ConnectionListView: View {
|
||||
return UIMenu(children: [stateMenu, sortMenu, closeAction])
|
||||
}
|
||||
}
|
||||
|
||||
#elseif os(macOS)
|
||||
private struct ConnectionMenuView: View {
|
||||
@Binding var connectionStateFilter: ConnectionStateFilter
|
||||
|
||||
@@ -56,7 +56,7 @@ public class ConnectionListViewModel: BaseViewModel {
|
||||
}
|
||||
|
||||
public func connect() {
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
@@ -39,14 +39,14 @@ import SwiftUI
|
||||
#endif
|
||||
} else {
|
||||
content.onAppear {
|
||||
guard !ApplicationLibrary.inPreview, profile.status.isConnected else {
|
||||
guard !Variant.screenshotMode, profile.status.isConnected else {
|
||||
return
|
||||
}
|
||||
Task {
|
||||
await coordinator.reloadSystemProxy()
|
||||
}
|
||||
}.onChangeCompat(of: profile.status) { status in
|
||||
guard !ApplicationLibrary.inPreview, status == .connected else {
|
||||
guard !Variant.screenshotMode, status == .connected else {
|
||||
return
|
||||
}
|
||||
Task {
|
||||
@@ -60,7 +60,7 @@ import SwiftUI
|
||||
Group {
|
||||
#if os(iOS)
|
||||
if useLegacyTabView {
|
||||
if ApplicationLibrary.inPreview || profile.status.isConnectedStrict {
|
||||
if Variant.screenshotMode || profile.status.isConnectedStrict {
|
||||
VStack {
|
||||
pageSelector
|
||||
pageContent
|
||||
@@ -126,11 +126,7 @@ import SwiftUI
|
||||
#endif
|
||||
#endif
|
||||
.onAppear {
|
||||
if ApplicationLibrary.inPreview {
|
||||
environments.commandClient.connect()
|
||||
} else {
|
||||
environments.connect()
|
||||
}
|
||||
environments.connect()
|
||||
}.onChangeCompat(of: scenePhase) { phase in
|
||||
guard phase == .active else {
|
||||
return
|
||||
|
||||
@@ -19,13 +19,19 @@ public struct ButtonVisibilityState {
|
||||
return
|
||||
}
|
||||
|
||||
groupsCount = commandClient.groups?.count ?? 0
|
||||
let actualGroupsCount = commandClient.groups?.count ?? 0
|
||||
let screenshotFallbackGroupsCount = 2
|
||||
groupsCount = Variant.screenshotMode && actualGroupsCount == 0
|
||||
? screenshotFallbackGroupsCount
|
||||
: actualGroupsCount
|
||||
connectionsCount = commandClient.connections?.count ?? 0
|
||||
|
||||
let isConnected = ApplicationLibrary.inPreview || profile.status.isConnectedStrict
|
||||
let isConnected = Variant.screenshotMode || profile.status.isConnectedStrict
|
||||
|
||||
let hasGroups = Variant.screenshotMode || (commandClient.groups?.isEmpty == false)
|
||||
|
||||
showConnectionsButton = isConnected
|
||||
showGroupsButton = isConnected && (commandClient.groups?.isEmpty == false)
|
||||
showGroupsButton = isConnected && hasGroups
|
||||
}
|
||||
|
||||
private mutating func reset() {
|
||||
|
||||
@@ -11,7 +11,7 @@ public struct ConnectionsCard: View {
|
||||
DashboardCardView(title: "", isHalfWidth: true) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
DashboardCardHeader(icon: "link.circle.fill", title: "Connections")
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
DashboardCardLine(String(localized: "Inbound"), "34")
|
||||
DashboardCardLine(String(localized: "Outbound"), "28")
|
||||
} else if let message = commandClient.status {
|
||||
|
||||
@@ -12,7 +12,7 @@ public struct DownloadTrafficCard: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
DashboardCardHeader(icon: "arrow.down.circle.fill", title: "Download")
|
||||
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
Text("249 MB/s")
|
||||
.font(.title2)
|
||||
.fontWeight(.medium)
|
||||
|
||||
@@ -11,7 +11,7 @@ public struct StatusCard: View {
|
||||
DashboardCardView(title: "", isHalfWidth: true) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
DashboardCardHeader(icon: "info.circle.fill", title: "Status")
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
DashboardCardLine(String(localized: "Memory"), "6.4 MB")
|
||||
DashboardCardLine(String(localized: "Goroutines"), "89")
|
||||
} else if let message = commandClient.status {
|
||||
|
||||
@@ -12,7 +12,7 @@ public struct UploadTrafficCard: View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
DashboardCardHeader(icon: "arrow.up.circle.fill", title: "Upload")
|
||||
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
Text("38 B/s")
|
||||
.font(.title2)
|
||||
.fontWeight(.medium)
|
||||
|
||||
@@ -28,7 +28,7 @@ public struct ExtensionStatusView: View {
|
||||
Group {
|
||||
VStack {
|
||||
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columnCount), alignment: .leading) {
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
StatusItem(String(localized: "Status")) {
|
||||
StatusLine(String(localized: "Memory"), "6.4 MB")
|
||||
StatusLine(String(localized: "Goroutines"), "89")
|
||||
|
||||
@@ -10,16 +10,7 @@ public struct StartStopButton: View {
|
||||
|
||||
public var body: some View {
|
||||
Group {
|
||||
if ApplicationLibrary.inPreview {
|
||||
Button {} label: {
|
||||
#if os(tvOS)
|
||||
Image(systemName: "stop.fill")
|
||||
#else
|
||||
Label("Stop", systemImage: "stop.fill")
|
||||
#endif
|
||||
}
|
||||
.labelStyle(.iconOnly)
|
||||
} else if let profile = environments.extensionProfile {
|
||||
if let profile = environments.extensionProfile {
|
||||
ToggleConnectionButton().environmentObject(profile)
|
||||
} else {
|
||||
Button {} label: {
|
||||
@@ -108,6 +99,7 @@ public struct StartStopButton: View {
|
||||
.disabled(!profile.status.isEnabled)
|
||||
.alert($alert)
|
||||
.onReceive(timer) { _ in
|
||||
guard !Variant.screenshotMode else { return }
|
||||
Task { @MainActor in
|
||||
currentTime = Date()
|
||||
}
|
||||
@@ -140,7 +132,12 @@ public struct StartStopButton: View {
|
||||
|
||||
private var runtimeDuration: String? {
|
||||
guard let connectedDate = profile.connectedDate else { return nil }
|
||||
let interval = currentTime.timeIntervalSince(connectedDate)
|
||||
let interval: TimeInterval
|
||||
if Variant.screenshotMode {
|
||||
interval = 3600
|
||||
} else {
|
||||
interval = currentTime.timeIntervalSince(connectedDate)
|
||||
}
|
||||
guard interval >= 0 else { return nil }
|
||||
|
||||
let hours = Int(interval) / 3600
|
||||
|
||||
@@ -77,9 +77,7 @@ public struct DashboardView: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
if ApplicationLibrary.inPreview {
|
||||
activeDashboardView
|
||||
} else if environments.extensionProfileLoading {
|
||||
if environments.extensionProfileLoading {
|
||||
ProgressView()
|
||||
} else if let profile = environments.extensionProfile {
|
||||
activeDashboardView
|
||||
|
||||
@@ -48,7 +48,7 @@ public final class DashboardViewModel: BaseViewModel {
|
||||
|
||||
defer { isLoading = false }
|
||||
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
profileList = [
|
||||
ProfilePreview(Profile(id: 0, name: "profile local", type: .local, path: "")),
|
||||
ProfilePreview(Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0))),
|
||||
|
||||
@@ -41,7 +41,7 @@ public struct OverviewView: View {
|
||||
}
|
||||
}
|
||||
.alert($coordinator.alert)
|
||||
.disabled(!ApplicationLibrary.inPreview && (!profile.status.isSwitchable || coordinator.reasserting))
|
||||
.disabled(!Variant.screenshotMode && (!profile.status.isSwitchable || coordinator.reasserting))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
@@ -85,9 +85,9 @@ public struct OverviewView: View {
|
||||
private func shouldShowCard(_ card: DashboardCard) -> Bool {
|
||||
switch card {
|
||||
case .status, .connections, .uploadTraffic, .downloadTraffic, .clashMode:
|
||||
return ApplicationLibrary.inPreview || profile.status.isConnected
|
||||
return Variant.screenshotMode || profile.status.isConnected
|
||||
case .httpProxy:
|
||||
return (ApplicationLibrary.inPreview || profile.status.isConnectedStrict) && systemProxyAvailable
|
||||
return (Variant.screenshotMode || profile.status.isConnectedStrict) && systemProxyAvailable
|
||||
case .profile:
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -14,17 +14,18 @@ public class GroupListViewModel: BaseViewModel {
|
||||
}
|
||||
|
||||
public func connect() {
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
groups = [
|
||||
OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [
|
||||
OutboundGroupItem(tag: "server", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12),
|
||||
OutboundGroupItem(tag: "server2", type: "WireGuard", urlTestTime: .now, urlTestDelay: 34),
|
||||
OutboundGroupItem(tag: "auto", type: "URLTest", urlTestTime: .now, urlTestDelay: 100),
|
||||
OutboundGroupItem(tag: "server", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 10),
|
||||
OutboundGroupItem(tag: "server2", type: "WireGuard", urlTestTime: .now, urlTestDelay: 20),
|
||||
OutboundGroupItem(tag: "auto", type: "URLTest", urlTestTime: .now, urlTestDelay: 30),
|
||||
]),
|
||||
OutboundGroup(tag: "Auto", type: "urltest", selected: "Tokyo", selectable: true, isExpand: false, items: [
|
||||
OutboundGroupItem(tag: "Tokyo", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 10),
|
||||
OutboundGroupItem(tag: "Singapore", type: "VMess", urlTestTime: .now, urlTestDelay: 20),
|
||||
OutboundGroupItem(tag: "Hong Kong", type: "Trojan", urlTestTime: .now, urlTestDelay: 15),
|
||||
]),
|
||||
OutboundGroup(tag: "group2", type: "urltest", selected: "client", selectable: true, isExpand: false, items:
|
||||
(0 ..< 234).map { index in
|
||||
OutboundGroupItem(tag: "client\(index)", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: UInt16(100 + index * 10))
|
||||
}),
|
||||
]
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ private struct LogViewContent: View {
|
||||
|
||||
func updateUIView(_ uiView: UIButton, context _: Context) {
|
||||
uiView.menu = createMenu()
|
||||
if #available(iOS 26.0, *) {
|
||||
if #available(iOS 17.0, *) {
|
||||
uiView.tintColor = colorScheme == .dark ? .white : .black
|
||||
}
|
||||
}
|
||||
@@ -225,7 +225,7 @@ private struct LogContentInnerView: View {
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
previewContent
|
||||
} else if dataModel.isEmpty {
|
||||
emptyContent
|
||||
@@ -240,8 +240,6 @@ private struct LogContentInnerView: View {
|
||||
private var previewContent: some View {
|
||||
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)",
|
||||
|
||||
@@ -17,6 +17,25 @@ public enum NavigationPage: Int, CaseIterable, Identifiable {
|
||||
}
|
||||
|
||||
public extension NavigationPage {
|
||||
init?(snapshotValue: String) {
|
||||
switch snapshotValue.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() {
|
||||
case "dashboard":
|
||||
self = .dashboard
|
||||
case "logs":
|
||||
self = .logs
|
||||
case "settings":
|
||||
self = .settings
|
||||
#if os(macOS)
|
||||
case "groups":
|
||||
self = .groups
|
||||
case "connections":
|
||||
self = .connections
|
||||
#endif
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
static var macosDefaultPages: [NavigationPage] {
|
||||
[.logs, .settings]
|
||||
|
||||
@@ -98,7 +98,7 @@ public struct CoreView: View {
|
||||
}
|
||||
|
||||
private nonisolated func loadSettings() async {
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
await MainActor.run {
|
||||
version = "<redacted>"
|
||||
dataSize = LibboxFormatBytes(1000 * 1000 * 10)
|
||||
|
||||
@@ -12,7 +12,7 @@ final class SettingViewModel: BaseViewModel {
|
||||
|
||||
nonisolated func checkTaiwanFlagAvailability() async {
|
||||
let available: Bool
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
available = true
|
||||
} else {
|
||||
available = !DeviceCensorship.isChinaDevice()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}()
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -32,7 +32,12 @@ open class ApplicationDelegate: NSObject, NSApplicationDelegate, UNUserNotificat
|
||||
let launchedAsLogInItem =
|
||||
event?.eventID == kAEOpenApplication &&
|
||||
event?.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem
|
||||
if SharedPreferences.inDebug || !launchedAsLogInItem || !SharedPreferences.showMenuBarExtra.getBlocking() || !SharedPreferences.menuBarExtraInBackground.getBlocking() {
|
||||
let shouldShowWindow = Variant.screenshotMode ||
|
||||
SharedPreferences.inDebug ||
|
||||
!launchedAsLogInItem ||
|
||||
!SharedPreferences.showMenuBarExtra.getBlocking() ||
|
||||
!SharedPreferences.menuBarExtraInBackground.getBlocking()
|
||||
if shouldShowWindow {
|
||||
NSApp.setActivationPolicy(.regular)
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
} else {
|
||||
|
||||
@@ -157,20 +157,6 @@ private class WindowState {
|
||||
}
|
||||
}
|
||||
|
||||
private struct WindowAccessor: NSViewRepresentable {
|
||||
let callback: (NSWindow?) -> Void
|
||||
|
||||
func makeNSView(context _: Context) -> NSView {
|
||||
let view = NSView()
|
||||
DispatchQueue.main.async {
|
||||
callback(view.window)
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_: NSView, context _: Context) {}
|
||||
}
|
||||
|
||||
private class WindowCloseDelegate: NSObject, NSWindowDelegate {
|
||||
var allowClose = false
|
||||
private let windowState: WindowState
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import AppKit
|
||||
import ApplicationLibrary
|
||||
import Foundation
|
||||
import Library
|
||||
import SwiftUI
|
||||
|
||||
@@ -6,17 +8,49 @@ import SwiftUI
|
||||
public struct MainView: View {
|
||||
@Environment(\.controlActiveState) private var controlActiveState
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@StateObject private var viewModel = MainViewModel()
|
||||
@StateObject private var viewModel: MainViewModel
|
||||
@State private var showCardManagement = false
|
||||
@State private var cardConfigurationVersion = 0
|
||||
@State private var settingsNavigationPath = NavigationPath()
|
||||
@State private var pendingSettingsPage: SettingsPage?
|
||||
@State private var didConfigureScreenshotWindow = false
|
||||
@State private var pendingScreenshotSelection: NavigationPage?
|
||||
|
||||
private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
|
||||
AnyView(ProfileEditorWrapperView(text: text, isEditable: isEditable))
|
||||
}
|
||||
|
||||
public init() {}
|
||||
private let screenshotDefaultPixelHeight: CGFloat = 1000
|
||||
|
||||
public init() {
|
||||
let initialSelection: NavigationPage = .dashboard
|
||||
if Variant.screenshotMode,
|
||||
let pageValue = ProcessInfo.processInfo.environment["SCREENSHOT_PAGE"],
|
||||
let page = NavigationPage(snapshotValue: pageValue)
|
||||
{
|
||||
_pendingScreenshotSelection = State(initialValue: page)
|
||||
} else {
|
||||
_pendingScreenshotSelection = State(initialValue: nil)
|
||||
}
|
||||
_viewModel = StateObject(wrappedValue: MainViewModel(selection: initialSelection))
|
||||
}
|
||||
|
||||
private func screenshotTargetHeight(baseHeight _: CGFloat, window: NSWindow) -> CGFloat {
|
||||
let scale = max(window.backingScaleFactor, 1)
|
||||
if let pixelOverride = ProcessInfo.processInfo.environment["SCREENSHOT_WINDOW_PIXEL_HEIGHT"] {
|
||||
let trimmed = pixelOverride.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let height = Double(trimmed), height > 0 {
|
||||
return CGFloat(height) / scale
|
||||
}
|
||||
}
|
||||
if let override = ProcessInfo.processInfo.environment["SCREENSHOT_WINDOW_HEIGHT"] {
|
||||
let trimmed = override.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let height = Double(trimmed), height > 0 {
|
||||
return CGFloat(height)
|
||||
}
|
||||
}
|
||||
return screenshotDefaultPixelHeight / scale
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
NavigationSplitView {
|
||||
@@ -31,7 +65,27 @@ public struct MainView: View {
|
||||
.environment(\.settingsNavigationPath, $settingsNavigationPath)
|
||||
.navigationSplitViewColumnWidth(650)
|
||||
}
|
||||
.frame(minHeight: 500)
|
||||
.frame(minHeight: Variant.screenshotMode ? 0 : 500)
|
||||
.background(WindowAccessor { window in
|
||||
guard Variant.screenshotMode, !didConfigureScreenshotWindow, let window else { return }
|
||||
didConfigureScreenshotWindow = true
|
||||
DispatchQueue.main.async {
|
||||
window.hasShadow = true
|
||||
let baseSize = window.contentLayoutRect.size
|
||||
let targetHeight = screenshotTargetHeight(baseHeight: baseSize.height, window: window)
|
||||
let targetSize = NSSize(width: baseSize.width, height: targetHeight)
|
||||
guard targetSize.width > 0, targetSize.height > 0 else { return }
|
||||
window.contentMinSize = targetSize
|
||||
window.contentMaxSize = targetSize
|
||||
window.minSize = targetSize
|
||||
window.maxSize = targetSize
|
||||
window.setContentSize(targetSize)
|
||||
if let pending = pendingScreenshotSelection, pending != viewModel.selection {
|
||||
viewModel.selection = pending
|
||||
pendingScreenshotSelection = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
.onAppear {
|
||||
viewModel.onAppear(environments: environments)
|
||||
}
|
||||
|
||||
@@ -6,10 +6,15 @@ import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public class MainViewModel: BaseViewModel {
|
||||
@Published public var selection = NavigationPage.dashboard
|
||||
@Published public var selection: NavigationPage
|
||||
@Published public var importProfile: LibboxProfileContent?
|
||||
@Published public var importRemoteProfile: LibboxImportRemoteProfile?
|
||||
|
||||
public init(selection: NavigationPage = .dashboard) {
|
||||
self.selection = selection
|
||||
super.init()
|
||||
}
|
||||
|
||||
public func onAppear(environments: ExtensionEnvironments) {
|
||||
environments.postReload()
|
||||
#if !DEBUG
|
||||
|
||||
@@ -9,7 +9,7 @@ private struct SidebarContentView: View {
|
||||
var environments: ExtensionEnvironments
|
||||
|
||||
private var hasGroups: Bool {
|
||||
environments.commandClient.groups?.isEmpty == false
|
||||
Variant.screenshotMode || environments.commandClient.groups?.isEmpty == false
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -77,9 +77,7 @@ public struct SidebarView: View {
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
if ApplicationLibrary.inPreview {
|
||||
previewContent
|
||||
} else if environments.extensionProfileLoading {
|
||||
if environments.extensionProfileLoading {
|
||||
ProgressView()
|
||||
} else if let profile = environments.extensionProfile {
|
||||
SidebarContentView(
|
||||
@@ -93,24 +91,6 @@ public struct SidebarView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var previewContent: some View {
|
||||
List(selection: $localSelection) {
|
||||
Section(NavigationPage.dashboard.title) {
|
||||
Label("Overview", systemImage: "text.and.command.macwindow")
|
||||
.tint(.textColor)
|
||||
.tag(NavigationPage.dashboard)
|
||||
NavigationPage.groups.label.tag(NavigationPage.groups)
|
||||
NavigationPage.connections.label.tag(NavigationPage.connections)
|
||||
}
|
||||
ForEach(NavigationPage.macosDefaultPages, id: \.self) { it in
|
||||
it.label
|
||||
}
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.scrollDisabled(true)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var disconnectedContent: some View {
|
||||
List(selection: $localSelection) {
|
||||
|
||||
@@ -203,7 +203,7 @@ public class StatusBarController: NSObject, NSMenuDelegate {
|
||||
|
||||
guard let groups else { return }
|
||||
|
||||
let selectableGroups = groups.filter { $0.selectable }
|
||||
let selectableGroups = groups.filter(\.selectable)
|
||||
if selectableGroups.isEmpty {
|
||||
groupsItem?.isHidden = true
|
||||
return
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import SwiftUI
|
||||
|
||||
struct WindowAccessor: NSViewRepresentable {
|
||||
final class WindowReportingView: NSView {
|
||||
var onWindowChange: ((NSWindow?) -> Void)?
|
||||
|
||||
override func viewDidMoveToWindow() {
|
||||
super.viewDidMoveToWindow()
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.onWindowChange?(self?.window)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let callback: (NSWindow?) -> Void
|
||||
|
||||
func makeNSView(context _: Context) -> WindowReportingView {
|
||||
let view = WindowReportingView()
|
||||
view.onWindowChange = callback
|
||||
return view
|
||||
}
|
||||
|
||||
func updateNSView(_ nsView: WindowReportingView, context _: Context) {
|
||||
nsView.onWindowChange = callback
|
||||
DispatchQueue.main.async {
|
||||
callback(nsView.window)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>zh_CN</string>
|
||||
<string>zh-Hans</string>
|
||||
</array>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ struct MainView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if ApplicationLibrary.inPreview {
|
||||
if Variant.screenshotMode {
|
||||
mainBody.preferredColorScheme(.dark)
|
||||
} else {
|
||||
mainBody
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
//
|
||||
// SnapshotHelper.swift
|
||||
// Example
|
||||
//
|
||||
// Created by Felix Krause on 10/8/15.
|
||||
//
|
||||
|
||||
// -----------------------------------------------------
|
||||
// IMPORTANT: When modifying this file, make sure to
|
||||
// increment the version number at the very
|
||||
// bottom of the file to notify users about
|
||||
// the new SnapshotHelper.swift
|
||||
// -----------------------------------------------------
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
|
||||
Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
|
||||
if waitForLoadingIndicator {
|
||||
Snapshot.snapshot(name)
|
||||
} else {
|
||||
Snapshot.snapshot(name, timeWaitingForIdle: 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// - Parameters:
|
||||
/// - name: The name of the snapshot
|
||||
/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait.
|
||||
@MainActor
|
||||
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
|
||||
Snapshot.snapshot(name, timeWaitingForIdle: timeout)
|
||||
}
|
||||
|
||||
enum SnapshotError: Error, CustomDebugStringConvertible {
|
||||
case cannotFindSimulatorHomeDirectory
|
||||
case cannotRunOnPhysicalDevice
|
||||
|
||||
var debugDescription: String {
|
||||
switch self {
|
||||
case .cannotFindSimulatorHomeDirectory:
|
||||
return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable."
|
||||
case .cannotRunOnPhysicalDevice:
|
||||
return "Can't use Snapshot on a physical device."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
@MainActor
|
||||
open class Snapshot: NSObject {
|
||||
static var app: XCUIApplication?
|
||||
static var waitForAnimations = true
|
||||
static var cacheDirectory: URL?
|
||||
static var screenshotsDirectory: URL? {
|
||||
if let override = ProcessInfo.processInfo.environment["SCREENSHOTS_DIR"], !override.isEmpty {
|
||||
return URL(fileURLWithPath: override, isDirectory: true)
|
||||
}
|
||||
return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true)
|
||||
}
|
||||
|
||||
static var deviceLanguage = ""
|
||||
static var currentLocale = ""
|
||||
|
||||
open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
|
||||
Snapshot.app = app
|
||||
Snapshot.waitForAnimations = waitForAnimations
|
||||
|
||||
do {
|
||||
let cacheDir = try getCacheDirectory()
|
||||
Snapshot.cacheDirectory = cacheDir
|
||||
if let screenshotsDirectory {
|
||||
try FileManager.default.createDirectory(at: screenshotsDirectory, withIntermediateDirectories: true)
|
||||
}
|
||||
setLanguage(app)
|
||||
setLocale(app)
|
||||
setLaunchArguments(app)
|
||||
} catch {
|
||||
NSLog(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
class func setLanguage(_ app: XCUIApplication) {
|
||||
guard let cacheDirectory else {
|
||||
NSLog("CacheDirectory is not set - probably running on a physical device?")
|
||||
return
|
||||
}
|
||||
|
||||
let path = cacheDirectory.appendingPathComponent("language.txt")
|
||||
|
||||
do {
|
||||
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
|
||||
deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
|
||||
app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"]
|
||||
} catch {
|
||||
NSLog("Couldn't detect/set language...")
|
||||
}
|
||||
}
|
||||
|
||||
class func setLocale(_ app: XCUIApplication) {
|
||||
guard let cacheDirectory else {
|
||||
NSLog("CacheDirectory is not set - probably running on a physical device?")
|
||||
return
|
||||
}
|
||||
|
||||
let path = cacheDirectory.appendingPathComponent("locale.txt")
|
||||
|
||||
do {
|
||||
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
|
||||
currentLocale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
|
||||
} catch {
|
||||
NSLog("Couldn't detect/set locale...")
|
||||
}
|
||||
|
||||
if currentLocale.isEmpty, !deviceLanguage.isEmpty {
|
||||
currentLocale = Locale(identifier: deviceLanguage).identifier
|
||||
}
|
||||
|
||||
if !currentLocale.isEmpty {
|
||||
app.launchArguments += ["-AppleLocale", "\"\(currentLocale)\""]
|
||||
}
|
||||
}
|
||||
|
||||
class func setLaunchArguments(_ app: XCUIApplication) {
|
||||
guard let cacheDirectory else {
|
||||
NSLog("CacheDirectory is not set - probably running on a physical device?")
|
||||
return
|
||||
}
|
||||
|
||||
let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt")
|
||||
app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"]
|
||||
|
||||
do {
|
||||
let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8)
|
||||
let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: [])
|
||||
let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count))
|
||||
let results = matches.map { result -> String in
|
||||
(launchArguments as NSString).substring(with: result.range)
|
||||
}
|
||||
app.launchArguments += results
|
||||
} catch {
|
||||
NSLog("Couldn't detect/set launch_arguments...")
|
||||
}
|
||||
}
|
||||
|
||||
open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
|
||||
if timeout > 0 {
|
||||
waitForLoadingIndicatorToDisappear(within: timeout)
|
||||
}
|
||||
|
||||
NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work
|
||||
|
||||
if Snapshot.waitForAnimations {
|
||||
sleep(1) // Waiting for the animation to be finished (kind of)
|
||||
}
|
||||
|
||||
#if os(OSX)
|
||||
guard let app else {
|
||||
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||
return
|
||||
}
|
||||
|
||||
app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: [])
|
||||
#else
|
||||
|
||||
guard self.app != nil else {
|
||||
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||
return
|
||||
}
|
||||
|
||||
let screenshot = XCUIScreen.main.screenshot()
|
||||
#if os(iOS) && !targetEnvironment(macCatalyst)
|
||||
let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image
|
||||
#else
|
||||
let image = screenshot.image
|
||||
#endif
|
||||
|
||||
guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return }
|
||||
|
||||
do {
|
||||
// The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices
|
||||
let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ")
|
||||
let range = NSRange(location: 0, length: simulator.count)
|
||||
simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "")
|
||||
|
||||
let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
|
||||
#if swift(<5.0)
|
||||
try UIImagePNGRepresentation(image)?.write(to: path, options: .atomic)
|
||||
#else
|
||||
try image.pngData()?.write(to: path, options: .atomic)
|
||||
#endif
|
||||
} catch {
|
||||
NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png")
|
||||
NSLog(error.localizedDescription)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
class func fixLandscapeOrientation(image: UIImage) -> UIImage {
|
||||
#if os(watchOS)
|
||||
return image
|
||||
#else
|
||||
if #available(iOS 10.0, *) {
|
||||
let format = UIGraphicsImageRendererFormat()
|
||||
format.scale = image.scale
|
||||
let renderer = UIGraphicsImageRenderer(size: image.size, format: format)
|
||||
return renderer.image { _ in
|
||||
image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
|
||||
}
|
||||
} else {
|
||||
return image
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) {
|
||||
#if os(tvOS)
|
||||
return
|
||||
#endif
|
||||
|
||||
guard let app else {
|
||||
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||
return
|
||||
}
|
||||
|
||||
let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element
|
||||
let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator)
|
||||
_ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout)
|
||||
}
|
||||
|
||||
class func getCacheDirectory() throws -> URL {
|
||||
let cachePath = "Library/Caches/tools.fastlane"
|
||||
// on OSX config is stored in /Users/<username>/Library
|
||||
// and on iOS/tvOS/WatchOS it's in simulator's home dir
|
||||
#if os(OSX)
|
||||
let homeDir = URL(fileURLWithPath: NSHomeDirectory())
|
||||
return homeDir.appendingPathComponent(cachePath)
|
||||
#elseif arch(i386) || arch(x86_64) || arch(arm64)
|
||||
guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else {
|
||||
throw SnapshotError.cannotFindSimulatorHomeDirectory
|
||||
}
|
||||
let homeDir = URL(fileURLWithPath: simulatorHostHome)
|
||||
return homeDir.appendingPathComponent(cachePath)
|
||||
#else
|
||||
throw SnapshotError.cannotRunOnPhysicalDevice
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private extension XCUIElementAttributes {
|
||||
var isNetworkLoadingIndicator: Bool {
|
||||
if hasAllowListedIdentifier { return false }
|
||||
|
||||
let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20)
|
||||
let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3)
|
||||
|
||||
return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize
|
||||
}
|
||||
|
||||
var hasAllowListedIdentifier: Bool {
|
||||
let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"]
|
||||
|
||||
return allowListedIdentifiers.contains(identifier)
|
||||
}
|
||||
|
||||
func isStatusBar(_ deviceWidth: CGFloat) -> Bool {
|
||||
if elementType == .statusBar { return true }
|
||||
guard frame.origin == .zero else { return false }
|
||||
|
||||
let oldStatusBarSize = CGSize(width: deviceWidth, height: 20)
|
||||
let newStatusBarSize = CGSize(width: deviceWidth, height: 44)
|
||||
|
||||
return [oldStatusBarSize, newStatusBarSize].contains(frame.size)
|
||||
}
|
||||
}
|
||||
|
||||
private extension XCUIElementQuery {
|
||||
var networkLoadingIndicators: XCUIElementQuery {
|
||||
let isNetworkLoadingIndicator = NSPredicate { evaluatedObject, _ in
|
||||
guard let element = evaluatedObject as? XCUIElementAttributes else { return false }
|
||||
|
||||
return element.isNetworkLoadingIndicator
|
||||
}
|
||||
|
||||
return containing(isNetworkLoadingIndicator)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
var deviceStatusBars: XCUIElementQuery {
|
||||
guard let app = Snapshot.app else {
|
||||
fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||
}
|
||||
|
||||
let deviceWidth = app.windows.firstMatch.frame.width
|
||||
|
||||
let isStatusBar = NSPredicate { evaluatedObject, _ in
|
||||
guard let element = evaluatedObject as? XCUIElementAttributes else { return false }
|
||||
|
||||
return element.isStatusBar(deviceWidth)
|
||||
}
|
||||
|
||||
return containing(isStatusBar)
|
||||
}
|
||||
}
|
||||
|
||||
private extension CGFloat {
|
||||
func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool {
|
||||
numberA ... numberB ~= self
|
||||
}
|
||||
}
|
||||
|
||||
// Please don't remove the lines below
|
||||
// They are used to detect outdated configuration files
|
||||
// SnapshotHelperVersion [1.31]
|
||||
@@ -0,0 +1,43 @@
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
final class SnapshotTests: XCTestCase {
|
||||
let app = XCUIApplication()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
setupSnapshot(app)
|
||||
app.launch()
|
||||
}
|
||||
|
||||
func test01Dashboard() throws {
|
||||
snapshot("01_Dashboard")
|
||||
}
|
||||
|
||||
func test02Logs() throws {
|
||||
if app.tabBars.buttons["Logs"].exists {
|
||||
app.tabBars.buttons["Logs"].firstMatch.tap()
|
||||
} else if app.buttons["Logs"].exists {
|
||||
app.buttons["Logs"].firstMatch.tap()
|
||||
}
|
||||
sleep(1)
|
||||
snapshot("02_Logs")
|
||||
}
|
||||
|
||||
func test03Settings() throws {
|
||||
// iPad on iOS 18+ uses floating tab bar which creates nested elements
|
||||
// Use firstMatch to handle multiple matching elements
|
||||
if app.tabBars.buttons["Settings"].exists {
|
||||
app.tabBars.buttons["Settings"].firstMatch.tap()
|
||||
} else if app.buttons["Settings"].exists {
|
||||
app.buttons["Settings"].firstMatch.tap()
|
||||
} else {
|
||||
let tabBar = app.tabBars.firstMatch
|
||||
if tabBar.exists {
|
||||
tabBar.buttons.element(boundBy: tabBar.buttons.count - 1).tap()
|
||||
}
|
||||
}
|
||||
sleep(1)
|
||||
snapshot("03_Settings")
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import Library
|
||||
import MacLibrary
|
||||
import SwiftUI
|
||||
|
||||
@@ -5,6 +6,10 @@ import SwiftUI
|
||||
struct Application: App {
|
||||
@NSApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
|
||||
|
||||
init() {
|
||||
ScreenshotLocalization.applyIfNeeded()
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
MacApplication()
|
||||
}
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>zh_CN</string>
|
||||
<string>zh-Hans</string>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import AppKit
|
||||
import CoreGraphics
|
||||
import Darwin
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
private var screenshotsDir: URL!
|
||||
@MainActor
|
||||
private var currentApp: XCUIApplication?
|
||||
|
||||
private func resolveScreenshotsDir() -> URL {
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
if let override = env["SCREENSHOTS_DIR"], !override.isEmpty {
|
||||
return URL(fileURLWithPath: override, isDirectory: true)
|
||||
}
|
||||
if let override = env["SNAPSHOT_SCREENSHOTS_PATH"], !override.isEmpty {
|
||||
return URL(fileURLWithPath: override, isDirectory: true)
|
||||
}
|
||||
if let cachesDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
|
||||
return cachesDir.appendingPathComponent("tools.fastlane/screenshots", isDirectory: true)
|
||||
}
|
||||
return URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||
.appendingPathComponent("tools.fastlane/screenshots", isDirectory: true)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func setupSnapshot(_ app: XCUIApplication, waitForAnimations _: Bool = true) {
|
||||
app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"]
|
||||
currentApp = app
|
||||
screenshotsDir = resolveScreenshotsDir()
|
||||
try! FileManager.default.createDirectory(at: screenshotsDir, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func snapshot(_ name: String, waitForLoadingIndicator: Bool = true) {
|
||||
snapshot(name, timeWaitingForIdle: waitForLoadingIndicator ? 1 : 0)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval) {
|
||||
if timeout > 0 {
|
||||
sleep(UInt32(timeout))
|
||||
}
|
||||
guard let app = currentApp else { return }
|
||||
let path = screenshotsDir.appendingPathComponent("Mac-\(name).png")
|
||||
var resolvedWindowID: CGWindowID?
|
||||
let deadline = Date().addingTimeInterval(10)
|
||||
while resolvedWindowID == nil, Date() < deadline {
|
||||
resolvedWindowID = windowID(for: app)
|
||||
if resolvedWindowID == nil {
|
||||
usleep(200_000)
|
||||
}
|
||||
}
|
||||
if let resolvedWindowID {
|
||||
if let image = cgWindowListCreateImage(windowID: resolvedWindowID), let data = pngData(from: image) {
|
||||
try! data.write(to: path)
|
||||
return
|
||||
}
|
||||
if captureWindowScreenshot(windowID: resolvedWindowID, to: path) {
|
||||
return
|
||||
}
|
||||
}
|
||||
let screenshot = XCUIScreen.main.screenshot()
|
||||
try! screenshot.pngRepresentation.write(to: path)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func captureWindowScreenshot(windowID: CGWindowID, to path: URL) -> Bool {
|
||||
let task = Process()
|
||||
task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture")
|
||||
task.arguments = ["-x", "-t", "png", "-o", "-l", "\(windowID)", path.path]
|
||||
do {
|
||||
try task.run()
|
||||
task.waitUntilExit()
|
||||
return task.terminationStatus == 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func windowID(for _: XCUIApplication) -> CGWindowID? {
|
||||
guard let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: "io.nekohasekai.sfavt").first else {
|
||||
return nil
|
||||
}
|
||||
let pid = Int(runningApp.processIdentifier)
|
||||
let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
|
||||
guard let infoList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else {
|
||||
return nil
|
||||
}
|
||||
var candidate: (id: CGWindowID, area: CGFloat)?
|
||||
for info in infoList {
|
||||
guard let ownerPid = info[kCGWindowOwnerPID as String] as? Int, ownerPid == pid else {
|
||||
continue
|
||||
}
|
||||
let layer = info[kCGWindowLayer as String] as? Int ?? 0
|
||||
guard layer == 0 else { continue }
|
||||
guard let boundsDict = info[kCGWindowBounds as String] as? [String: Any],
|
||||
let bounds = CGRect(dictionaryRepresentation: boundsDict as CFDictionary)
|
||||
else {
|
||||
continue
|
||||
}
|
||||
let area = bounds.width * bounds.height
|
||||
guard area > 0 else { continue }
|
||||
guard let number = info[kCGWindowNumber as String] as? Int else { continue }
|
||||
if candidate == nil || area > (candidate?.area ?? 0) {
|
||||
candidate = (CGWindowID(number), area)
|
||||
}
|
||||
}
|
||||
return candidate?.id
|
||||
}
|
||||
|
||||
private func cgWindowListCreateImage(windowID: CGWindowID) -> CGImage? {
|
||||
typealias CGWindowListCreateImageFunc = @convention(c) (CGRect, CGWindowListOption, CGWindowID, CGWindowImageOption) -> CGImage?
|
||||
let rtldDefault = UnsafeMutableRawPointer(bitPattern: -2)
|
||||
guard let symbol = dlsym(rtldDefault, "CGWindowListCreateImage") else { return nil }
|
||||
let function = unsafeBitCast(symbol, to: CGWindowListCreateImageFunc.self)
|
||||
let bounds = CGRect.null
|
||||
let listOption = CGWindowListOption.optionIncludingWindow
|
||||
let imageOption: CGWindowImageOption = [.boundsIgnoreFraming, .bestResolution]
|
||||
return function(bounds, listOption, windowID, imageOption)
|
||||
}
|
||||
|
||||
private func pngData(from image: CGImage) -> Data? {
|
||||
let rep = NSBitmapImageRep(cgImage: image)
|
||||
return rep.representation(using: .png, properties: [:])
|
||||
}
|
||||
|
||||
func loadScreenshotLanguage() -> (language: String?, locale: String?) {
|
||||
let dir = resolveScreenshotsDir()
|
||||
let language = readScreenshotText(dir.appendingPathComponent("language.txt"))
|
||||
let locale = readScreenshotText(dir.appendingPathComponent("locale.txt"))
|
||||
return (language, locale)
|
||||
}
|
||||
|
||||
private func readScreenshotText(_ url: URL) -> String? {
|
||||
guard let text = try? String(contentsOf: url, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
final class SnapshotTests: XCTestCase {
|
||||
let app = XCUIApplication()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
app.terminate()
|
||||
configureLanguage()
|
||||
if let value = ProcessInfo.processInfo.environment["SCREENSHOT_WINDOW_PIXEL_HEIGHT"] {
|
||||
app.launchEnvironment["SCREENSHOT_WINDOW_PIXEL_HEIGHT"] = value
|
||||
}
|
||||
if let value = ProcessInfo.processInfo.environment["SCREENSHOT_WINDOW_HEIGHT"] {
|
||||
app.launchEnvironment["SCREENSHOT_WINDOW_HEIGHT"] = value
|
||||
}
|
||||
if let page = screenshotPage() {
|
||||
app.launchEnvironment["SCREENSHOT_PAGE"] = page
|
||||
} else {
|
||||
app.launchEnvironment["SCREENSHOT_PAGE"] = ""
|
||||
}
|
||||
setupSnapshot(app)
|
||||
app.launch()
|
||||
}
|
||||
|
||||
private func configureLanguage() {
|
||||
let environment = ProcessInfo.processInfo.environment
|
||||
var language = environment["SCREENSHOT_LANGUAGE"]?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var locale = environment["SCREENSHOT_LOCALE"]?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if language == nil || language?.isEmpty == true {
|
||||
let cached = loadScreenshotLanguage()
|
||||
language = cached.language
|
||||
if locale == nil || locale?.isEmpty == true {
|
||||
locale = cached.locale
|
||||
}
|
||||
}
|
||||
guard let language, !language.isEmpty else {
|
||||
return
|
||||
}
|
||||
app.launchArguments += ["-AppleLanguages", "(\(language))"]
|
||||
app.launchEnvironment["SCREENSHOT_LANGUAGE"] = language
|
||||
if let locale, !locale.isEmpty {
|
||||
app.launchArguments += ["-AppleLocale", locale]
|
||||
app.launchEnvironment["SCREENSHOT_LOCALE"] = locale
|
||||
}
|
||||
}
|
||||
|
||||
private func screenshotPage() -> String? {
|
||||
let testName = name
|
||||
if testName.contains("test01Dashboard") {
|
||||
return "dashboard"
|
||||
}
|
||||
if testName.contains("test02Logs") {
|
||||
return "logs"
|
||||
}
|
||||
if testName.contains("test03Settings") {
|
||||
return "settings"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func test01Dashboard() throws {
|
||||
sleep(1)
|
||||
snapshot("01_Dashboard")
|
||||
}
|
||||
|
||||
func test02Logs() throws {
|
||||
sleep(1)
|
||||
snapshot("02_Logs")
|
||||
}
|
||||
|
||||
func test03Settings() throws {
|
||||
sleep(1)
|
||||
snapshot("03_Settings")
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,10 @@ struct Application: App {
|
||||
@UIApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
|
||||
@StateObject private var environments = ExtensionEnvironments()
|
||||
|
||||
init() {
|
||||
ScreenshotLocalization.applyIfNeeded()
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
MainView()
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@
|
||||
<key>CFBundleLocalizations</key>
|
||||
<array>
|
||||
<string>en</string>
|
||||
<string>zh_CN</string>
|
||||
<string>zh-Hans</string>
|
||||
</array>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
|
||||
+17
-1
@@ -1,4 +1,5 @@
|
||||
import ApplicationLibrary
|
||||
import Foundation
|
||||
import Libbox
|
||||
import Library
|
||||
import SwiftUI
|
||||
@@ -6,7 +7,16 @@ import SwiftUI
|
||||
struct MainView: View {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||||
@State private var selection = NavigationPage.dashboard
|
||||
@State private var selection: NavigationPage = {
|
||||
if Variant.screenshotMode,
|
||||
let pageValue = ProcessInfo.processInfo.environment["SCREENSHOT_PAGE"],
|
||||
let page = NavigationPage(snapshotValue: pageValue)
|
||||
{
|
||||
return page
|
||||
}
|
||||
return .dashboard
|
||||
}()
|
||||
|
||||
@State private var importProfile: LibboxProfileContent?
|
||||
@State private var importRemoteProfile: LibboxImportRemoteProfile?
|
||||
|
||||
@@ -22,6 +32,12 @@ struct MainView: View {
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if Variant.screenshotMode,
|
||||
let pageValue = ProcessInfo.processInfo.environment["SCREENSHOT_PAGE"],
|
||||
let page = NavigationPage(snapshotValue: pageValue)
|
||||
{
|
||||
selection = page
|
||||
}
|
||||
environments.postReload()
|
||||
}
|
||||
.onChangeCompat(of: scenePhase) { newValue in
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
private var screenshotsDir: URL!
|
||||
|
||||
private func resolveScreenshotsDir() -> URL {
|
||||
let env = ProcessInfo.processInfo.environment
|
||||
if let override = env["SCREENSHOTS_DIR"], !override.isEmpty {
|
||||
return URL(fileURLWithPath: override, isDirectory: true)
|
||||
}
|
||||
if let override = env["SNAPSHOT_SCREENSHOTS_PATH"], !override.isEmpty {
|
||||
return URL(fileURLWithPath: override, isDirectory: true)
|
||||
}
|
||||
if let simulatorHostHome = env["SIMULATOR_HOST_HOME"] {
|
||||
return URL(fileURLWithPath: simulatorHostHome)
|
||||
.appendingPathComponent("Library/Caches/tools.fastlane/screenshots")
|
||||
}
|
||||
return URL(fileURLWithPath: NSHomeDirectory())
|
||||
.appendingPathComponent("Library/Caches/tools.fastlane/screenshots")
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func setupSnapshot(_ app: XCUIApplication, waitForAnimations _: Bool = true) {
|
||||
app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"]
|
||||
screenshotsDir = resolveScreenshotsDir()
|
||||
try! FileManager.default.createDirectory(at: screenshotsDir, withIntermediateDirectories: true)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func snapshot(_ name: String, waitForLoadingIndicator: Bool = true) {
|
||||
snapshot(name, timeWaitingForIdle: waitForLoadingIndicator ? 1 : 0)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval) {
|
||||
if timeout > 0 {
|
||||
sleep(UInt32(timeout))
|
||||
}
|
||||
let simulator = ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] ?? "AppleTV"
|
||||
let screenshot = XCUIScreen.main.screenshot()
|
||||
let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
|
||||
try! screenshot.pngRepresentation.write(to: path)
|
||||
}
|
||||
|
||||
func loadScreenshotLanguage() -> (language: String?, locale: String?) {
|
||||
let dir = resolveScreenshotsDir()
|
||||
let language = readScreenshotText(dir.appendingPathComponent("language.txt"))
|
||||
let locale = readScreenshotText(dir.appendingPathComponent("locale.txt"))
|
||||
return (language, locale)
|
||||
}
|
||||
|
||||
private func readScreenshotText(_ url: URL) -> String? {
|
||||
guard let text = try? String(contentsOf: url, encoding: .utf8) else {
|
||||
return nil
|
||||
}
|
||||
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return trimmed.isEmpty ? nil : trimmed
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
final class SnapshotTests: XCTestCase {
|
||||
let app = XCUIApplication()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
app.terminate()
|
||||
configureLanguage()
|
||||
if let page = screenshotPage() {
|
||||
app.launchEnvironment["SCREENSHOT_PAGE"] = page
|
||||
} else {
|
||||
app.launchEnvironment["SCREENSHOT_PAGE"] = ""
|
||||
}
|
||||
setupSnapshot(app)
|
||||
app.launch()
|
||||
}
|
||||
|
||||
private func configureLanguage() {
|
||||
let environment = ProcessInfo.processInfo.environment
|
||||
var language = environment["SCREENSHOT_LANGUAGE"]?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
var locale = environment["SCREENSHOT_LOCALE"]?
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if language == nil || language?.isEmpty == true {
|
||||
let cached = loadScreenshotLanguage()
|
||||
language = cached.language
|
||||
if locale == nil || locale?.isEmpty == true {
|
||||
locale = cached.locale
|
||||
}
|
||||
}
|
||||
guard let language, !language.isEmpty else {
|
||||
return
|
||||
}
|
||||
app.launchArguments += ["-AppleLanguages", "(\(language))"]
|
||||
app.launchEnvironment["SCREENSHOT_LANGUAGE"] = language
|
||||
if let locale, !locale.isEmpty {
|
||||
app.launchArguments += ["-AppleLocale", locale]
|
||||
app.launchEnvironment["SCREENSHOT_LOCALE"] = locale
|
||||
}
|
||||
}
|
||||
|
||||
private func screenshotPage() -> String? {
|
||||
let testName = name
|
||||
if testName.contains("test01Dashboard") {
|
||||
return "dashboard"
|
||||
}
|
||||
if testName.contains("test02Logs") {
|
||||
return "logs"
|
||||
}
|
||||
if testName.contains("test03Settings") {
|
||||
return "settings"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func test01Dashboard() throws {
|
||||
sleep(1)
|
||||
snapshot("01_Dashboard")
|
||||
}
|
||||
|
||||
func test02Logs() throws {
|
||||
sleep(1)
|
||||
snapshot("02_Logs")
|
||||
}
|
||||
|
||||
func test03Settings() throws {
|
||||
sleep(1)
|
||||
snapshot("03_Settings")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import XCTest
|
||||
|
||||
@MainActor
|
||||
final class SnapshotTests: XCTestCase {
|
||||
let app = XCUIApplication()
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
setupSnapshot(app)
|
||||
app.launch()
|
||||
}
|
||||
|
||||
func test01Dashboard() throws {
|
||||
#if os(macOS)
|
||||
if app.outlines.staticTexts["Dashboard"].exists {
|
||||
app.outlines.staticTexts["Dashboard"].click()
|
||||
}
|
||||
#endif
|
||||
sleep(1)
|
||||
snapshot("01_Dashboard")
|
||||
}
|
||||
|
||||
func test02Logs() throws {
|
||||
#if os(iOS)
|
||||
if app.tabBars.buttons["Logs"].exists {
|
||||
app.tabBars.buttons["Logs"].firstMatch.tap()
|
||||
} else if app.buttons["Logs"].exists {
|
||||
app.buttons["Logs"].firstMatch.tap()
|
||||
}
|
||||
#elseif os(macOS)
|
||||
if app.outlines.staticTexts["Logs"].exists {
|
||||
app.outlines.staticTexts["Logs"].click()
|
||||
}
|
||||
#elseif os(tvOS)
|
||||
let remote = XCUIRemote.shared
|
||||
for _ in 0 ..< 3 {
|
||||
remote.press(.down)
|
||||
usleep(300_000)
|
||||
}
|
||||
if app.cells["Logs"].exists {
|
||||
app.cells["Logs"].tap()
|
||||
} else {
|
||||
remote.press(.select)
|
||||
}
|
||||
#endif
|
||||
sleep(1)
|
||||
snapshot("02_Logs")
|
||||
}
|
||||
|
||||
func test03Settings() throws {
|
||||
#if os(iOS)
|
||||
if app.tabBars.buttons["Settings"].exists {
|
||||
app.tabBars.buttons["Settings"].firstMatch.tap()
|
||||
} else if app.buttons["Settings"].exists {
|
||||
app.buttons["Settings"].firstMatch.tap()
|
||||
}
|
||||
#elseif os(macOS)
|
||||
if app.outlines.staticTexts["Settings"].exists {
|
||||
app.outlines.staticTexts["Settings"].click()
|
||||
}
|
||||
#elseif os(tvOS)
|
||||
let remote = XCUIRemote.shared
|
||||
remote.press(.down)
|
||||
usleep(300_000)
|
||||
if app.cells["Settings"].exists {
|
||||
app.cells["Settings"].tap()
|
||||
} else {
|
||||
remote.press(.select)
|
||||
}
|
||||
#endif
|
||||
sleep(1)
|
||||
snapshot("03_Settings")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// UITests.swift
|
||||
// UITests
|
||||
//
|
||||
// Created by sekai on 2026-01-06 15:19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class UITests: XCTestCase {
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
|
||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||
continueAfterFailure = false
|
||||
|
||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testExample() throws {
|
||||
// UI tests must launch the application that they test.
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testLaunchPerformance() throws {
|
||||
// This measures how long it takes to launch your application.
|
||||
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||
XCUIApplication().launch()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// UITestsLaunchTests.swift
|
||||
// UITests
|
||||
//
|
||||
// Created by sekai on 2026-01-06 15:19.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
final class UITestsLaunchTests: XCTestCase {
|
||||
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||
true
|
||||
}
|
||||
|
||||
override func setUpWithError() throws {
|
||||
continueAfterFailure = false
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testLaunch() throws {
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
|
||||
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||
// such as logging into a test account or navigating somewhere in the app
|
||||
|
||||
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||
attachment.name = "Launch Screen"
|
||||
attachment.lifetime = .keepAlways
|
||||
add(attachment)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,261 @@
|
||||
default_platform(:ios)
|
||||
|
||||
ENV["FASTLANE_XCODEBUILD_SETTINGS_RETRIES"] = "0"
|
||||
ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "30"
|
||||
|
||||
SCREENSHOT_LANGUAGES = ["en-US", "zh-Hans"].freeze
|
||||
SCREENSHOT_LOCALES = {
|
||||
"zh-Hans" => "zh_CN"
|
||||
}.freeze
|
||||
|
||||
def reset_dir(path)
|
||||
FileUtils.rm_rf(path)
|
||||
FileUtils.mkdir_p(path)
|
||||
end
|
||||
|
||||
def screenshot_locale(language)
|
||||
SCREENSHOT_LOCALES[language]
|
||||
end
|
||||
|
||||
def write_screenshot_language_files(dir, language, locale)
|
||||
FileUtils.mkdir_p(dir)
|
||||
File.write(File.join(dir, "language.txt"), language)
|
||||
if locale
|
||||
File.write(File.join(dir, "locale.txt"), locale)
|
||||
else
|
||||
FileUtils.rm_f(File.join(dir, "locale.txt"))
|
||||
end
|
||||
end
|
||||
|
||||
def shutdown_simulator(name)
|
||||
sh("xcrun simctl shutdown \"#{name}\" || true")
|
||||
end
|
||||
|
||||
platform :ios do
|
||||
desc "Generate iOS screenshots"
|
||||
lane :screenshots do
|
||||
ENV["SNAPSHOT_SIMULATOR_WAIT_FOR_BOOT_TIMEOUT"] = "10"
|
||||
screenshots_cache = File.expand_path("~/Library/Caches/tools.fastlane/screenshots")
|
||||
reset_dir(screenshots_cache)
|
||||
capture_screenshots
|
||||
shutdown_simulator("iPhone 11 Pro Max")
|
||||
shutdown_simulator("iPad Pro 13-inch (M4)")
|
||||
end
|
||||
end
|
||||
|
||||
platform :mac do
|
||||
desc "Generate macOS screenshots"
|
||||
lane :screenshots do
|
||||
output_base_dir = File.expand_path("../screenshots/macos", __dir__)
|
||||
screenshots_cache_candidates = [
|
||||
File.expand_path("~/Library/Caches/tools.fastlane/screenshots"),
|
||||
File.expand_path("~/Library/Containers/io.nekohasekai.sfavt.SFMUITests.xctrunner/Data/Library/Caches/tools.fastlane/screenshots")
|
||||
].uniq
|
||||
ENV["DISABLE_SWIFTLINT"] = "1"
|
||||
ENV["SCREENSHOT_WINDOW_PIXEL_HEIGHT"] = "1000"
|
||||
|
||||
reset_dir(output_base_dir)
|
||||
|
||||
SCREENSHOT_LANGUAGES.each do |language|
|
||||
output_dir = File.join(output_base_dir, language)
|
||||
ENV["SCREENSHOT_LANGUAGE"] = language
|
||||
locale = screenshot_locale(language)
|
||||
if locale
|
||||
ENV["SCREENSHOT_LOCALE"] = locale
|
||||
else
|
||||
ENV.delete("SCREENSHOT_LOCALE")
|
||||
end
|
||||
reset_dir(output_dir)
|
||||
screenshots_cache_candidates.each do |dir|
|
||||
reset_dir(dir)
|
||||
write_screenshot_language_files(dir, language, locale)
|
||||
end
|
||||
|
||||
run_tests(
|
||||
project: "sing-box.xcodeproj",
|
||||
scheme: "SFM",
|
||||
destination: "platform=macOS",
|
||||
result_bundle: false,
|
||||
reinstall_app: false,
|
||||
app_identifier: "io.nekohasekai.sfavt",
|
||||
only_testing: ["SFMUITests/SnapshotTests"],
|
||||
number_of_retries: 0
|
||||
)
|
||||
|
||||
copied = {}
|
||||
screenshots_cache_candidates.each do |dir|
|
||||
Dir.glob("#{dir}/Mac-*.png").each do |file|
|
||||
basename = File.basename(file)
|
||||
next if copied[basename]
|
||||
FileUtils.cp(file, output_dir)
|
||||
copied[basename] = true
|
||||
end
|
||||
end
|
||||
|
||||
padded_files = Dir.glob("#{output_dir}/*.png")
|
||||
magick_bin = `which magick 2>/dev/null`.strip
|
||||
magick_bin = `which convert 2>/dev/null`.strip if magick_bin.empty?
|
||||
if magick_bin.empty?
|
||||
UI.user_error!("ImageMagick not found. Install it (magick/convert) to add shadow.")
|
||||
end
|
||||
|
||||
canvas_width = 2880
|
||||
canvas_height = 1800
|
||||
target_width = 2620
|
||||
target_height = 1620
|
||||
shadow_opacity = 26
|
||||
|
||||
padded_files.each do |file|
|
||||
size_output = sh(
|
||||
magick_bin,
|
||||
file,
|
||||
"-format",
|
||||
"%w %h",
|
||||
"info:",
|
||||
log: false
|
||||
).strip
|
||||
opaque_output = sh(
|
||||
magick_bin,
|
||||
file,
|
||||
"-format",
|
||||
"%[opaque]",
|
||||
"info:",
|
||||
log: false
|
||||
).strip
|
||||
width, height = size_output.split.map(&:to_i)
|
||||
has_transparency = opaque_output != "True"
|
||||
tmp_file = "#{file}.tmp.png"
|
||||
|
||||
if has_transparency
|
||||
shadow_sigma = 24
|
||||
shadow_offset = 8
|
||||
shadow = "#{shadow_opacity}x#{shadow_sigma}+0+#{shadow_offset}"
|
||||
sh(
|
||||
magick_bin,
|
||||
file,
|
||||
"-alpha", "set",
|
||||
"-background", "none",
|
||||
"-trim",
|
||||
"+repage",
|
||||
"-resize",
|
||||
"#{target_width}x#{target_height}",
|
||||
"(",
|
||||
"+clone",
|
||||
"-background",
|
||||
"black",
|
||||
"-shadow",
|
||||
shadow,
|
||||
")",
|
||||
"+swap",
|
||||
"-background",
|
||||
"white",
|
||||
"-layers",
|
||||
"merge",
|
||||
"+repage",
|
||||
"-gravity",
|
||||
"center",
|
||||
"-extent",
|
||||
"#{canvas_width}x#{canvas_height}",
|
||||
tmp_file
|
||||
)
|
||||
else
|
||||
scale = [
|
||||
target_width.to_f / width,
|
||||
target_height.to_f / height
|
||||
].min
|
||||
scaled_width = (width * scale).round
|
||||
scaled_height = (height * scale).round
|
||||
mask_width = [scaled_width - 1, 1].max
|
||||
mask_height = [scaled_height - 1, 1].max
|
||||
corner_radius = [(28 * scale).round, 28].max
|
||||
shadow_sigma = [(16 * scale).round, 1].max
|
||||
shadow_offset = [(5 * scale).round, 1].max
|
||||
shadow = "#{shadow_opacity}x#{shadow_sigma}+0+#{shadow_offset}"
|
||||
sh(
|
||||
magick_bin,
|
||||
file,
|
||||
"-resize",
|
||||
"#{scaled_width}x#{scaled_height}",
|
||||
"-alpha",
|
||||
"set",
|
||||
"(",
|
||||
"-size",
|
||||
"#{scaled_width}x#{scaled_height}",
|
||||
"xc:none",
|
||||
"-fill",
|
||||
"white",
|
||||
"-draw",
|
||||
"roundrectangle 0,0 #{mask_width},#{mask_height},#{corner_radius},#{corner_radius}",
|
||||
")",
|
||||
"-compose",
|
||||
"DstIn",
|
||||
"-composite",
|
||||
"-compose",
|
||||
"over",
|
||||
"(",
|
||||
"+clone",
|
||||
"-background",
|
||||
"black",
|
||||
"-shadow",
|
||||
shadow,
|
||||
")",
|
||||
"+swap",
|
||||
"-background",
|
||||
"white",
|
||||
"-layers",
|
||||
"merge",
|
||||
"+repage",
|
||||
"-gravity",
|
||||
"center",
|
||||
"-extent",
|
||||
"#{canvas_width}x#{canvas_height}",
|
||||
tmp_file
|
||||
)
|
||||
end
|
||||
FileUtils.mv(tmp_file, file, force: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
platform :tvos do
|
||||
desc "Generate tvOS screenshots"
|
||||
lane :screenshots do
|
||||
output_base_dir = File.expand_path("../screenshots/tvos", __dir__)
|
||||
screenshots_cache = File.expand_path("~/Library/Caches/tools.fastlane/screenshots")
|
||||
|
||||
reset_dir(output_base_dir)
|
||||
|
||||
SCREENSHOT_LANGUAGES.each do |language|
|
||||
output_dir = File.join(output_base_dir, language)
|
||||
ENV["SCREENSHOT_LANGUAGE"] = language
|
||||
locale = screenshot_locale(language)
|
||||
if locale
|
||||
ENV["SCREENSHOT_LOCALE"] = locale
|
||||
else
|
||||
ENV.delete("SCREENSHOT_LOCALE")
|
||||
end
|
||||
reset_dir(output_dir)
|
||||
reset_dir(screenshots_cache)
|
||||
write_screenshot_language_files(screenshots_cache, language, locale)
|
||||
|
||||
run_tests(
|
||||
project: "sing-box.xcodeproj",
|
||||
scheme: "SFT",
|
||||
devices: ["Apple TV 4K (3rd generation)"],
|
||||
result_bundle: false,
|
||||
reinstall_app: true,
|
||||
app_identifier: "io.nekohasekai.sfavt",
|
||||
only_testing: ["SFTUITests/SnapshotTests"],
|
||||
number_of_retries: 0,
|
||||
cloned_source_packages_path: "/tmp/fastlane_source_packages",
|
||||
derived_data_path: "/tmp/fastlane_derived_data"
|
||||
)
|
||||
|
||||
Dir.glob("#{screenshots_cache}/*.png").each do |file|
|
||||
FileUtils.cp(file, output_dir)
|
||||
end
|
||||
end
|
||||
shutdown_simulator("Apple TV 4K (3rd generation)")
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
fastlane documentation
|
||||
----
|
||||
|
||||
# Installation
|
||||
|
||||
Make sure you have the latest version of the Xcode command line tools installed:
|
||||
|
||||
```sh
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||
|
||||
# Available Actions
|
||||
|
||||
## iOS
|
||||
|
||||
### ios screenshots
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios screenshots
|
||||
```
|
||||
|
||||
Generate iOS screenshots
|
||||
|
||||
----
|
||||
|
||||
|
||||
## Mac
|
||||
|
||||
### mac screenshots
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane mac screenshots
|
||||
```
|
||||
|
||||
Generate macOS screenshots
|
||||
|
||||
----
|
||||
|
||||
|
||||
## tvos
|
||||
|
||||
### tvos screenshots
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane tvos screenshots
|
||||
```
|
||||
|
||||
Generate tvOS screenshots
|
||||
|
||||
----
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||
|
||||
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||
|
||||
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||
@@ -0,0 +1,30 @@
|
||||
# iOS Screenshots
|
||||
# App Store requirements:
|
||||
# - iPhone 6.5" Display
|
||||
# - iPad 13" Display
|
||||
devices([
|
||||
"iPhone 11 Pro Max",
|
||||
"iPad Pro 13-inch (M4)"
|
||||
])
|
||||
|
||||
languages([
|
||||
"en-US",
|
||||
"zh-Hans"
|
||||
])
|
||||
|
||||
project("./sing-box.xcodeproj")
|
||||
scheme("SFI")
|
||||
app_identifier("io.nekohasekai.sfavt")
|
||||
output_directory("./screenshots/ios")
|
||||
clear_previous_screenshots(true)
|
||||
reinstall_app(true)
|
||||
skip_helper_version_check(true)
|
||||
override_status_bar(true)
|
||||
dark_mode(true)
|
||||
number_of_retries(0)
|
||||
stop_after_first_error(true)
|
||||
skip_package_dependencies_resolution(true)
|
||||
disable_package_automatic_updates(true)
|
||||
cloned_source_packages_path("/tmp/fastlane_source_packages")
|
||||
derived_data_path("/tmp/fastlane_derived_data")
|
||||
localize_simulator(true)
|
||||
@@ -115,6 +115,27 @@
|
||||
remoteGlobalIDString = 3A4EAD0F2A4FEAE6005435B3;
|
||||
remoteInfo = ApplicationLibrary;
|
||||
};
|
||||
3A6313782F0CEF3D0060A550 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 3AEC20F22A459AB400A63465;
|
||||
remoteInfo = SFI;
|
||||
};
|
||||
3A63139A2F0CEF740060A550 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 3AC03B952A72BF3300B7946F;
|
||||
remoteInfo = SFT;
|
||||
};
|
||||
3A6313AD2F0CEFDD0060A550 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 3AEC21082A459B1900A63465;
|
||||
remoteInfo = SFM;
|
||||
};
|
||||
3A76504A2A4F07F6003945C5 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */;
|
||||
@@ -419,6 +440,9 @@
|
||||
3A3DEBE12A4FFA1A00373BF4 /* ExtensionFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ExtensionFoundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.0.sdk/System/Library/Frameworks/ExtensionFoundation.framework; sourceTree = DEVELOPER_DIR; };
|
||||
3A3DEBE62A4FFA6000373BF4 /* AppIntents.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppIntents.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.0.sdk/System/Library/Frameworks/AppIntents.framework; sourceTree = DEVELOPER_DIR; };
|
||||
3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApplicationLibrary.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3A6313722F0CEF3D0060A550 /* SFIUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SFIUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3A6313942F0CEF740060A550 /* SFTUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SFTUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3A6313A72F0CEFDD0060A550 /* SFMUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SFMUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3A77016D2A4E6B34008F031F /* IntentsExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.extensionkit-extension"; includeInIndex = 0; path = IntentsExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3AAAFB202EF5218F004C69AD /* FileProviderExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FileProviderExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3AAAFB212EF5218F004C69AD /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
|
||||
@@ -584,6 +608,10 @@
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
3A63135E2F0CEF190060A550 /* UITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = UITests; sourceTree = "<group>"; };
|
||||
3A6313732F0CEF3D0060A550 /* SFIUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SFIUITests; sourceTree = "<group>"; };
|
||||
3A6313952F0CEF740060A550 /* SFTUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SFTUITests; sourceTree = "<group>"; };
|
||||
3A6313A82F0CEFDD0060A550 /* SFMUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = SFMUITests; sourceTree = "<group>"; };
|
||||
3AAAFB232EF5218F004C69AD /* FileProviderExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3AAAFB322EF5218F004C69AD /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = FileProviderExtension; sourceTree = "<group>"; };
|
||||
3ADDCEB42E8B723B009ACE1D /* SFI */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCEBC2E8B723B009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = SFI; sourceTree = "<group>"; };
|
||||
3ADDCEC02E8B7240009ACE1D /* SFM */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCEC22E8B7240009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = SFM; sourceTree = "<group>"; };
|
||||
@@ -619,6 +647,27 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A63136F2F0CEF3D0060A550 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A6313912F0CEF740060A550 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A6313A42F0CEFDD0060A550 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A77016A2A4E6B34008F031F /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -764,6 +813,10 @@
|
||||
3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */,
|
||||
3AAAFB232EF5218F004C69AD /* FileProviderExtension */,
|
||||
3AE595722F08C34000C13426 /* HelperService */,
|
||||
3A63135E2F0CEF190060A550 /* UITests */,
|
||||
3A6313732F0CEF3D0060A550 /* SFIUITests */,
|
||||
3A6313952F0CEF740060A550 /* SFTUITests */,
|
||||
3A6313A82F0CEFDD0060A550 /* SFMUITests */,
|
||||
3AEC20C72A45991900A63465 /* Products */,
|
||||
3AEC21012A459AE300A63465 /* Frameworks */,
|
||||
3AE5C9042F08E53400C13426 /* Recovered References */,
|
||||
@@ -787,6 +840,9 @@
|
||||
3AE395F22C21A5CA00647718 /* WidgetExtension.appex */,
|
||||
3AAAFB202EF5218F004C69AD /* FileProviderExtension.appex */,
|
||||
3AE595712F08C34000C13426 /* RootHelper */,
|
||||
3A6313722F0CEF3D0060A550 /* SFIUITests.xctest */,
|
||||
3A6313942F0CEF740060A550 /* SFTUITests.xctest */,
|
||||
3A6313A72F0CEFDD0060A550 /* SFMUITests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -880,6 +936,75 @@
|
||||
productReference = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */;
|
||||
productType = "com.apple.product-type.framework";
|
||||
};
|
||||
3A6313712F0CEF3D0060A550 /* SFIUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 3A63137A2F0CEF3D0060A550 /* Build configuration list for PBXNativeTarget "SFIUITests" */;
|
||||
buildPhases = (
|
||||
3A63136E2F0CEF3D0060A550 /* Sources */,
|
||||
3A63136F2F0CEF3D0060A550 /* Frameworks */,
|
||||
3A6313702F0CEF3D0060A550 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
3A6313792F0CEF3D0060A550 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
3A6313732F0CEF3D0060A550 /* SFIUITests */,
|
||||
);
|
||||
name = SFIUITests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = SFIUITests;
|
||||
productReference = 3A6313722F0CEF3D0060A550 /* SFIUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
3A6313932F0CEF740060A550 /* SFTUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 3A63139C2F0CEF740060A550 /* Build configuration list for PBXNativeTarget "SFTUITests" */;
|
||||
buildPhases = (
|
||||
3A6313902F0CEF740060A550 /* Sources */,
|
||||
3A6313912F0CEF740060A550 /* Frameworks */,
|
||||
3A6313922F0CEF740060A550 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
3A63139B2F0CEF740060A550 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
3A6313952F0CEF740060A550 /* SFTUITests */,
|
||||
);
|
||||
name = SFTUITests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = SFTUITests;
|
||||
productReference = 3A6313942F0CEF740060A550 /* SFTUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
3A6313A62F0CEFDD0060A550 /* SFMUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 3A6313AF2F0CEFDD0060A550 /* Build configuration list for PBXNativeTarget "SFMUITests" */;
|
||||
buildPhases = (
|
||||
3A6313A32F0CEFDD0060A550 /* Sources */,
|
||||
3A6313A42F0CEFDD0060A550 /* Frameworks */,
|
||||
3A6313A52F0CEFDD0060A550 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
3A6313AE2F0CEFDD0060A550 /* PBXTargetDependency */,
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
3A6313A82F0CEFDD0060A550 /* SFMUITests */,
|
||||
);
|
||||
name = SFMUITests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = SFMUITests;
|
||||
productReference = 3A6313A72F0CEFDD0060A550 /* SFMUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
3A77016C2A4E6B34008F031F /* IntentsExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 3A7701772A4E6B34008F031F /* Build configuration list for PBXNativeTarget "IntentsExtension" */;
|
||||
@@ -1181,6 +1306,18 @@
|
||||
CreatedOnToolsVersion = 15.0;
|
||||
LastSwiftMigration = 1500;
|
||||
};
|
||||
3A6313712F0CEF3D0060A550 = {
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
TestTargetID = 3AEC20F22A459AB400A63465;
|
||||
};
|
||||
3A6313932F0CEF740060A550 = {
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
TestTargetID = 3AC03B952A72BF3300B7946F;
|
||||
};
|
||||
3A6313A62F0CEFDD0060A550 = {
|
||||
CreatedOnToolsVersion = 26.2;
|
||||
TestTargetID = 3AEC21082A459B1900A63465;
|
||||
};
|
||||
3A77016C2A4E6B34008F031F = {
|
||||
CreatedOnToolsVersion = 15.0;
|
||||
};
|
||||
@@ -1257,6 +1394,9 @@
|
||||
3AE395F12C21A5CA00647718 /* WidgetExtension */,
|
||||
3AAAFB1F2EF5218F004C69AD /* FileProviderExtension */,
|
||||
3AE595702F08C34000C13426 /* RootHelper */,
|
||||
3A6313712F0CEF3D0060A550 /* SFIUITests */,
|
||||
3A6313932F0CEF740060A550 /* SFTUITests */,
|
||||
3A6313A62F0CEFDD0060A550 /* SFMUITests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -1270,6 +1410,27 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A6313702F0CEF3D0060A550 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A6313922F0CEF740060A550 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A6313A52F0CEFDD0060A550 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3AAAFB1E2EF5218F004C69AD /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -1363,6 +1524,27 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A63136E2F0CEF3D0060A550 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A6313902F0CEF740060A550 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A6313A32F0CEFDD0060A550 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
3A7701692A4E6B34008F031F /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -1485,6 +1667,21 @@
|
||||
target = 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */;
|
||||
targetProxy = 3A4FB15E2A73468C007012B9 /* PBXContainerItemProxy */;
|
||||
};
|
||||
3A6313792F0CEF3D0060A550 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 3AEC20F22A459AB400A63465 /* SFI */;
|
||||
targetProxy = 3A6313782F0CEF3D0060A550 /* PBXContainerItemProxy */;
|
||||
};
|
||||
3A63139B2F0CEF740060A550 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 3AC03B952A72BF3300B7946F /* SFT */;
|
||||
targetProxy = 3A63139A2F0CEF740060A550 /* PBXContainerItemProxy */;
|
||||
};
|
||||
3A6313AE2F0CEFDD0060A550 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 3AEC21082A459B1900A63465 /* SFM */;
|
||||
targetProxy = 3A6313AD2F0CEFDD0060A550 /* PBXContainerItemProxy */;
|
||||
};
|
||||
3A76504B2A4F07F6003945C5 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 3AEC211C2A459B4700A63465 /* Library */;
|
||||
@@ -1750,6 +1947,138 @@
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
3A63137B2F0CEF3D0060A550 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 287TTNZF8L;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt.SFIUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = SFI;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3A63137C2F0CEF3D0060A550 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 287TTNZF8L;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt.SFIUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = SFI;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
3A63139D2F0CEF740060A550 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 287TTNZF8L;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt.SFTUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = appletvos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TEST_TARGET_NAME = SFT;
|
||||
TVOS_DEPLOYMENT_TARGET = 26.2;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3A63139E2F0CEF740060A550 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 287TTNZF8L;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt.SFTUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = appletvos;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = 3;
|
||||
TEST_TARGET_NAME = SFT;
|
||||
TVOS_DEPLOYMENT_TARGET = 26.2;
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
3A6313B02F0CEFDD0060A550 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 287TTNZF8L;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt.SFMUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = SFM;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
3A6313B12F0CEFDD0060A550 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 287TTNZF8L;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 26.2;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.nekohasekai.sfavt.SFMUITests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = macosx;
|
||||
STRING_CATALOG_GENERATE_SYMBOLS = NO;
|
||||
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_TARGET_NAME = SFM;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
3A7701782A4E6B34008F031F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -2851,6 +3180,33 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
3A63137A2F0CEF3D0060A550 /* Build configuration list for PBXNativeTarget "SFIUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3A63137B2F0CEF3D0060A550 /* Debug */,
|
||||
3A63137C2F0CEF3D0060A550 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
3A63139C2F0CEF740060A550 /* Build configuration list for PBXNativeTarget "SFTUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3A63139D2F0CEF740060A550 /* Debug */,
|
||||
3A63139E2F0CEF740060A550 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
3A6313AF2F0CEFDD0060A550 /* Build configuration list for PBXNativeTarget "SFMUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
3A6313B02F0CEFDD0060A550 /* Debug */,
|
||||
3A6313B12F0CEFDD0060A550 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
3A7701772A4E6B34008F031F /* Build configuration list for PBXNativeTarget "IntentsExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1520"
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -28,6 +29,19 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3A6313712F0CEF3D0060A550"
|
||||
BuildableName = "SFIUITests.xctest"
|
||||
BlueprintName = "SFIUITests"
|
||||
ReferencedContainer = "container:sing-box.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1520"
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1520"
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -28,6 +29,19 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3A6313A62F0CEFDD0060A550"
|
||||
BuildableName = "SFMUITests.xctest"
|
||||
BlueprintName = "SFMUITests"
|
||||
ReferencedContainer = "container:sing-box.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1520"
|
||||
LastUpgradeVersion = "2620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
@@ -28,6 +29,19 @@
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "3A6313932F0CEF740060A550"
|
||||
BuildableName = "SFTUITests.xctest"
|
||||
BlueprintName = "SFTUITests"
|
||||
ReferencedContainer = "container:sing-box.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
|
||||
Reference in New Issue
Block a user