Add screenshot generator

This commit is contained in:
世界
2026-01-08 16:13:20 +08:00
parent 264c4eed81
commit 6b589cb771
57 changed files with 1931 additions and 109 deletions
+5 -1
View File
@@ -7,4 +7,8 @@ xcuserdata/
CLAUDE.md CLAUDE.md
.claude .claude
.mcp.json .mcp.json
.serena /screenshots/
/fastlane/report.xml
/fastlane/test_output/
/fastlane/.bundle/
/fastlane/.cache/
@@ -2,5 +2,4 @@ import Foundation
public class ApplicationLibrary { public class ApplicationLibrary {
public static let bundle = Bundle(for: ApplicationLibrary.self) 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]) return UIMenu(children: [stateMenu, sortMenu, closeAction])
} }
} }
#elseif os(macOS) #elseif os(macOS)
private struct ConnectionMenuView: View { private struct ConnectionMenuView: View {
@Binding var connectionStateFilter: ConnectionStateFilter @Binding var connectionStateFilter: ConnectionStateFilter
@@ -56,7 +56,7 @@ public class ConnectionListViewModel: BaseViewModel {
} }
public func connect() { public func connect() {
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
isLoading = false isLoading = false
return return
} }
@@ -39,14 +39,14 @@ import SwiftUI
#endif #endif
} else { } else {
content.onAppear { content.onAppear {
guard !ApplicationLibrary.inPreview, profile.status.isConnected else { guard !Variant.screenshotMode, profile.status.isConnected else {
return return
} }
Task { Task {
await coordinator.reloadSystemProxy() await coordinator.reloadSystemProxy()
} }
}.onChangeCompat(of: profile.status) { status in }.onChangeCompat(of: profile.status) { status in
guard !ApplicationLibrary.inPreview, status == .connected else { guard !Variant.screenshotMode, status == .connected else {
return return
} }
Task { Task {
@@ -60,7 +60,7 @@ import SwiftUI
Group { Group {
#if os(iOS) #if os(iOS)
if useLegacyTabView { if useLegacyTabView {
if ApplicationLibrary.inPreview || profile.status.isConnectedStrict { if Variant.screenshotMode || profile.status.isConnectedStrict {
VStack { VStack {
pageSelector pageSelector
pageContent pageContent
@@ -126,11 +126,7 @@ import SwiftUI
#endif #endif
#endif #endif
.onAppear { .onAppear {
if ApplicationLibrary.inPreview {
environments.commandClient.connect()
} else {
environments.connect() environments.connect()
}
}.onChangeCompat(of: scenePhase) { phase in }.onChangeCompat(of: scenePhase) { phase in
guard phase == .active else { guard phase == .active else {
return return
@@ -19,13 +19,19 @@ public struct ButtonVisibilityState {
return 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 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 showConnectionsButton = isConnected
showGroupsButton = isConnected && (commandClient.groups?.isEmpty == false) showGroupsButton = isConnected && hasGroups
} }
private mutating func reset() { private mutating func reset() {
@@ -11,7 +11,7 @@ public struct ConnectionsCard: View {
DashboardCardView(title: "", isHalfWidth: true) { DashboardCardView(title: "", isHalfWidth: true) {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
DashboardCardHeader(icon: "link.circle.fill", title: "Connections") DashboardCardHeader(icon: "link.circle.fill", title: "Connections")
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
DashboardCardLine(String(localized: "Inbound"), "34") DashboardCardLine(String(localized: "Inbound"), "34")
DashboardCardLine(String(localized: "Outbound"), "28") DashboardCardLine(String(localized: "Outbound"), "28")
} else if let message = commandClient.status { } else if let message = commandClient.status {
@@ -12,7 +12,7 @@ public struct DownloadTrafficCard: View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
DashboardCardHeader(icon: "arrow.down.circle.fill", title: "Download") DashboardCardHeader(icon: "arrow.down.circle.fill", title: "Download")
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
Text("249 MB/s") Text("249 MB/s")
.font(.title2) .font(.title2)
.fontWeight(.medium) .fontWeight(.medium)
@@ -11,7 +11,7 @@ public struct StatusCard: View {
DashboardCardView(title: "", isHalfWidth: true) { DashboardCardView(title: "", isHalfWidth: true) {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
DashboardCardHeader(icon: "info.circle.fill", title: "Status") DashboardCardHeader(icon: "info.circle.fill", title: "Status")
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
DashboardCardLine(String(localized: "Memory"), "6.4 MB") DashboardCardLine(String(localized: "Memory"), "6.4 MB")
DashboardCardLine(String(localized: "Goroutines"), "89") DashboardCardLine(String(localized: "Goroutines"), "89")
} else if let message = commandClient.status { } else if let message = commandClient.status {
@@ -12,7 +12,7 @@ public struct UploadTrafficCard: View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
DashboardCardHeader(icon: "arrow.up.circle.fill", title: "Upload") DashboardCardHeader(icon: "arrow.up.circle.fill", title: "Upload")
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
Text("38 B/s") Text("38 B/s")
.font(.title2) .font(.title2)
.fontWeight(.medium) .fontWeight(.medium)
@@ -28,7 +28,7 @@ public struct ExtensionStatusView: View {
Group { Group {
VStack { VStack {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columnCount), alignment: .leading) { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: columnCount), alignment: .leading) {
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
StatusItem(String(localized: "Status")) { StatusItem(String(localized: "Status")) {
StatusLine(String(localized: "Memory"), "6.4 MB") StatusLine(String(localized: "Memory"), "6.4 MB")
StatusLine(String(localized: "Goroutines"), "89") StatusLine(String(localized: "Goroutines"), "89")
@@ -10,16 +10,7 @@ public struct StartStopButton: View {
public var body: some View { public var body: some View {
Group { Group {
if ApplicationLibrary.inPreview { if let profile = environments.extensionProfile {
Button {} label: {
#if os(tvOS)
Image(systemName: "stop.fill")
#else
Label("Stop", systemImage: "stop.fill")
#endif
}
.labelStyle(.iconOnly)
} else if let profile = environments.extensionProfile {
ToggleConnectionButton().environmentObject(profile) ToggleConnectionButton().environmentObject(profile)
} else { } else {
Button {} label: { Button {} label: {
@@ -108,6 +99,7 @@ public struct StartStopButton: View {
.disabled(!profile.status.isEnabled) .disabled(!profile.status.isEnabled)
.alert($alert) .alert($alert)
.onReceive(timer) { _ in .onReceive(timer) { _ in
guard !Variant.screenshotMode else { return }
Task { @MainActor in Task { @MainActor in
currentTime = Date() currentTime = Date()
} }
@@ -140,7 +132,12 @@ public struct StartStopButton: View {
private var runtimeDuration: String? { private var runtimeDuration: String? {
guard let connectedDate = profile.connectedDate else { return nil } 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 } guard interval >= 0 else { return nil }
let hours = Int(interval) / 3600 let hours = Int(interval) / 3600
@@ -77,9 +77,7 @@ public struct DashboardView: View {
@ViewBuilder @ViewBuilder
private var mainContent: some View { private var mainContent: some View {
if ApplicationLibrary.inPreview { if environments.extensionProfileLoading {
activeDashboardView
} else if environments.extensionProfileLoading {
ProgressView() ProgressView()
} else if let profile = environments.extensionProfile { } else if let profile = environments.extensionProfile {
activeDashboardView activeDashboardView
@@ -48,7 +48,7 @@ public final class DashboardViewModel: BaseViewModel {
defer { isLoading = false } defer { isLoading = false }
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
profileList = [ profileList = [
ProfilePreview(Profile(id: 0, name: "profile local", type: .local, path: "")), ProfilePreview(Profile(id: 0, name: "profile local", type: .local, path: "")),
ProfilePreview(Profile(id: 1, name: "profile remote", type: .remote, path: "", lastUpdated: Date(timeIntervalSince1970: 0))), 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) .alert($coordinator.alert)
.disabled(!ApplicationLibrary.inPreview && (!profile.status.isSwitchable || coordinator.reasserting)) .disabled(!Variant.screenshotMode && (!profile.status.isSwitchable || coordinator.reasserting))
} }
@ViewBuilder @ViewBuilder
@@ -85,9 +85,9 @@ public struct OverviewView: View {
private func shouldShowCard(_ card: DashboardCard) -> Bool { private func shouldShowCard(_ card: DashboardCard) -> Bool {
switch card { switch card {
case .status, .connections, .uploadTraffic, .downloadTraffic, .clashMode: case .status, .connections, .uploadTraffic, .downloadTraffic, .clashMode:
return ApplicationLibrary.inPreview || profile.status.isConnected return Variant.screenshotMode || profile.status.isConnected
case .httpProxy: case .httpProxy:
return (ApplicationLibrary.inPreview || profile.status.isConnectedStrict) && systemProxyAvailable return (Variant.screenshotMode || profile.status.isConnectedStrict) && systemProxyAvailable
case .profile: case .profile:
return true return true
} }
@@ -14,17 +14,18 @@ public class GroupListViewModel: BaseViewModel {
} }
public func connect() { public func connect() {
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
groups = [ groups = [
OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [ OutboundGroup(tag: "my_group", type: "selector", selected: "server", selectable: true, isExpand: true, items: [
OutboundGroupItem(tag: "server", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 12), OutboundGroupItem(tag: "server", type: "Shadowsocks", urlTestTime: .now, urlTestDelay: 10),
OutboundGroupItem(tag: "server2", type: "WireGuard", urlTestTime: .now, urlTestDelay: 34), OutboundGroupItem(tag: "server2", type: "WireGuard", urlTestTime: .now, urlTestDelay: 20),
OutboundGroupItem(tag: "auto", type: "URLTest", urlTestTime: .now, urlTestDelay: 100), 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 isLoading = false
} }
+2 -4
View File
@@ -98,7 +98,7 @@ private struct LogViewContent: View {
func updateUIView(_ uiView: UIButton, context _: Context) { func updateUIView(_ uiView: UIButton, context _: Context) {
uiView.menu = createMenu() uiView.menu = createMenu()
if #available(iOS 26.0, *) { if #available(iOS 17.0, *) {
uiView.tintColor = colorScheme == .dark ? .white : .black uiView.tintColor = colorScheme == .dark ? .white : .black
} }
} }
@@ -225,7 +225,7 @@ private struct LogContentInnerView: View {
var body: some View { var body: some View {
Group { Group {
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
previewContent previewContent
} else if dataModel.isEmpty { } else if dataModel.isEmpty {
emptyContent emptyContent
@@ -240,8 +240,6 @@ private struct LogContentInnerView: View {
private var previewContent: some View { private var previewContent: some View {
let logList = [ let logList = [
"(packet-tunnel) log server started", "(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", "INFO[0000] router: updated default interface en0, index 11",
"inbound/tun[0]: started at utun3", "inbound/tun[0]: started at utun3",
"sing-box started (1.666s)", "sing-box started (1.666s)",
@@ -17,6 +17,25 @@ public enum NavigationPage: Int, CaseIterable, Identifiable {
} }
public extension NavigationPage { 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) #if os(macOS)
static var macosDefaultPages: [NavigationPage] { static var macosDefaultPages: [NavigationPage] {
[.logs, .settings] [.logs, .settings]
@@ -98,7 +98,7 @@ public struct CoreView: View {
} }
private nonisolated func loadSettings() async { private nonisolated func loadSettings() async {
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
await MainActor.run { await MainActor.run {
version = "<redacted>" version = "<redacted>"
dataSize = LibboxFormatBytes(1000 * 1000 * 10) dataSize = LibboxFormatBytes(1000 * 1000 * 10)
@@ -12,7 +12,7 @@ final class SettingViewModel: BaseViewModel {
nonisolated func checkTaiwanFlagAvailability() async { nonisolated func checkTaiwanFlagAvailability() async {
let available: Bool let available: Bool
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
available = true available = true
} else { } else {
available = !DeviceCensorship.isChinaDevice() available = !DeviceCensorship.isChinaDevice()
+9
View File
@@ -91,6 +91,15 @@ public class CommandClient: ObservableObject {
self.init([connectionType], logMaxLines: logMaxLines) 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() { public func connect() {
if isConnected { if isConnected {
return return
+9 -1
View File
@@ -141,7 +141,13 @@ public class ExtensionEnvironments: ObservableObject {
public let selectedProfileUpdate = ObjectWillChangePublisher() public let selectedProfileUpdate = ObjectWillChangePublisher()
public let openSettings = ObjectWillChangePublisher() public let openSettings = ObjectWillChangePublisher()
public init() {} public init() {
if Variant.screenshotMode {
extensionProfileLoading = false
extensionProfile = .mock
commandClient.setupMockData()
}
}
public func postReload() { public func postReload() {
Task { Task {
@@ -150,6 +156,7 @@ public class ExtensionEnvironments: ObservableObject {
} }
public func reload() async { public func reload() async {
if Variant.screenshotMode { return }
if let newProfile = try? await ExtensionProfile.load() { if let newProfile = try? await ExtensionProfile.load() {
if extensionProfile == nil || extensionProfile?.status == .invalid { if extensionProfile == nil || extensionProfile?.status == .invalid {
newProfile.register() newProfile.register()
@@ -163,6 +170,7 @@ public class ExtensionEnvironments: ObservableObject {
} }
public func connect() { public func connect() {
if Variant.screenshotMode { return }
guard let profile = extensionProfile else { guard let profile = extensionProfile else {
return return
} }
+42 -2
View File
@@ -12,9 +12,10 @@ private let logger = Logger(category: "ExtensionProfile")
public class ExtensionProfile: ObservableObject { public class ExtensionProfile: ObservableObject {
public static let controlKind = AppConfiguration.widgetControlKind public static let controlKind = AppConfiguration.widgetControlKind
private let manager: NEVPNManager private let manager: NEVPNManager?
private var connection: NEVPNConnection private var connection: NEVPNConnection?
private var observer: Any? private var observer: Any?
private let isMock: Bool
@Published public var status: NEVPNStatus @Published public var status: NEVPNStatus
@Published public var connectedDate: Date? @Published public var connectedDate: Date?
@@ -24,9 +25,28 @@ public class ExtensionProfile: ObservableObject {
connection = manager.connection connection = manager.connection
status = manager.connection.status status = manager.connection.status
connectedDate = manager.connection.connectedDate 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() { public func register() {
guard !isMock, let manager else { return }
observer = NotificationCenter.default.addObserver( observer = NotificationCenter.default.addObserver(
forName: NSNotification.Name.NEVPNStatusDidChange, forName: NSNotification.Name.NEVPNStatusDidChange,
object: manager.connection, object: manager.connection,
@@ -88,6 +108,7 @@ public class ExtensionProfile: ObservableObject {
} }
private func setOnDemandRules(useDefaultRules: Bool) async { private func setOnDemandRules(useDefaultRules: Bool) async {
guard let manager else { return }
if useDefaultRules { if useDefaultRules {
manager.onDemandRules = Self.makeDefaultOnDemandRules() manager.onDemandRules = Self.makeDefaultOnDemandRules()
} else { } else {
@@ -97,6 +118,7 @@ public class ExtensionProfile: ObservableObject {
} }
public func updateOnDemand(enabled: Bool, useDefaultRules: Bool) async throws { public func updateOnDemand(enabled: Bool, useDefaultRules: Bool) async throws {
guard let manager else { return }
manager.isOnDemandEnabled = enabled manager.isOnDemandEnabled = enabled
await setOnDemandRules(useDefaultRules: useDefaultRules) await setOnDemandRules(useDefaultRules: useDefaultRules)
try await manager.saveToPreferences() try await manager.saveToPreferences()
@@ -104,10 +126,19 @@ public class ExtensionProfile: ObservableObject {
@available(iOS 16.0, macOS 13.0, tvOS 17.0, *) @available(iOS 16.0, macOS 13.0, tvOS 17.0, *)
public func fetchLastDisconnectError() async throws { public func fetchLastDisconnectError() async throws {
guard let connection else { return }
try await connection.fetchLastDisconnectError() try await connection.fetchLastDisconnectError()
} }
public func start() async throws { 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() try await fetchProfile()
manager.isEnabled = true manager.isEnabled = true
let alwaysOn = await SharedPreferences.alwaysOn.get() let alwaysOn = await SharedPreferences.alwaysOn.get()
@@ -131,6 +162,7 @@ public class ExtensionProfile: ObservableObject {
} }
public func reloadService() async throws { public func reloadService() async throws {
if isMock { return }
let options = try await prepareStartOptions() let options = try await prepareStartOptions()
let data = try ExtensionStartOptions.encode(options) let data = try ExtensionStartOptions.encode(options)
guard let session = connection as? NETunnelProviderSession else { guard let session = connection as? NETunnelProviderSession else {
@@ -197,6 +229,14 @@ public class ExtensionProfile: ObservableObject {
} }
public func stop() async throws { 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 { if manager.isOnDemandEnabled {
manager.isOnDemandEnabled = false manager.isOnDemandEnabled = false
try await manager.saveToPreferences() try await manager.saveToPreferences()
@@ -0,0 +1,42 @@
import Foundation
import ObjectiveC
private var screenshotLocalizationBundleKey: UInt8 = 0
private final class ScreenshotBundle: Bundle, @unchecked Sendable {
override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
if let bundle = objc_getAssociatedObject(self, &screenshotLocalizationBundleKey) as? Bundle {
return bundle.localizedString(forKey: key, value: value, table: tableName)
}
return super.localizedString(forKey: key, value: value, table: tableName)
}
}
public enum ScreenshotLocalization {
public static func applyIfNeeded() {
let environment = ProcessInfo.processInfo.environment
guard let language = environment["SCREENSHOT_LANGUAGE"]?
.trimmingCharacters(in: .whitespacesAndNewlines),
!language.isEmpty
else {
return
}
UserDefaults.standard.set([language], forKey: "AppleLanguages")
if let locale = environment["SCREENSHOT_LOCALE"]?
.trimmingCharacters(in: .whitespacesAndNewlines),
!locale.isEmpty
{
UserDefaults.standard.set(locale, forKey: "AppleLocale")
}
if let path = Bundle.main.path(forResource: language, ofType: "lproj"),
let localizedBundle = Bundle(path: path)
{
objc_setAssociatedObject(Bundle.main, &screenshotLocalizationBundleKey, localizedBundle, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
object_setClass(Bundle.main, ScreenshotBundle.self)
}
}
}
private let _screenshotLocalizationApplied: Void = {
ScreenshotLocalization.applyIfNeeded()
}()
+6
View File
@@ -22,4 +22,10 @@ public enum Variant {
public static var debugNoIOS26 = false public static var debugNoIOS26 = false
public static var debugNoIOS18 = false public static var debugNoIOS18 = false
#endif #endif
#if targetEnvironment(simulator)
public static let screenshotMode = true
#else
public static let screenshotMode = ProcessInfo.processInfo.arguments.contains("-FASTLANE_SNAPSHOT")
#endif
} }
+6 -1
View File
@@ -32,7 +32,12 @@ open class ApplicationDelegate: NSObject, NSApplicationDelegate, UNUserNotificat
let launchedAsLogInItem = let launchedAsLogInItem =
event?.eventID == kAEOpenApplication && event?.eventID == kAEOpenApplication &&
event?.paramDescriptor(forKeyword: keyAEPropData)?.enumCodeValue == keyAELaunchedAsLogInItem 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.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
} else { } else {
-14
View File
@@ -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 { private class WindowCloseDelegate: NSObject, NSWindowDelegate {
var allowClose = false var allowClose = false
private let windowState: WindowState private let windowState: WindowState
+57 -3
View File
@@ -1,4 +1,6 @@
import AppKit
import ApplicationLibrary import ApplicationLibrary
import Foundation
import Library import Library
import SwiftUI import SwiftUI
@@ -6,17 +8,49 @@ import SwiftUI
public struct MainView: View { public struct MainView: View {
@Environment(\.controlActiveState) private var controlActiveState @Environment(\.controlActiveState) private var controlActiveState
@EnvironmentObject private var environments: ExtensionEnvironments @EnvironmentObject private var environments: ExtensionEnvironments
@StateObject private var viewModel = MainViewModel() @StateObject private var viewModel: MainViewModel
@State private var showCardManagement = false @State private var showCardManagement = false
@State private var cardConfigurationVersion = 0 @State private var cardConfigurationVersion = 0
@State private var settingsNavigationPath = NavigationPath() @State private var settingsNavigationPath = NavigationPath()
@State private var pendingSettingsPage: SettingsPage? @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 private let profileEditor: (Binding<String>, Bool) -> AnyView = { text, isEditable in
AnyView(ProfileEditorWrapperView(text: text, isEditable: isEditable)) 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 { public var body: some View {
NavigationSplitView { NavigationSplitView {
@@ -31,7 +65,27 @@ public struct MainView: View {
.environment(\.settingsNavigationPath, $settingsNavigationPath) .environment(\.settingsNavigationPath, $settingsNavigationPath)
.navigationSplitViewColumnWidth(650) .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 { .onAppear {
viewModel.onAppear(environments: environments) viewModel.onAppear(environments: environments)
} }
+6 -1
View File
@@ -6,10 +6,15 @@ import SwiftUI
@MainActor @MainActor
public class MainViewModel: BaseViewModel { public class MainViewModel: BaseViewModel {
@Published public var selection = NavigationPage.dashboard @Published public var selection: NavigationPage
@Published public var importProfile: LibboxProfileContent? @Published public var importProfile: LibboxProfileContent?
@Published public var importRemoteProfile: LibboxImportRemoteProfile? @Published public var importRemoteProfile: LibboxImportRemoteProfile?
public init(selection: NavigationPage = .dashboard) {
self.selection = selection
super.init()
}
public func onAppear(environments: ExtensionEnvironments) { public func onAppear(environments: ExtensionEnvironments) {
environments.postReload() environments.postReload()
#if !DEBUG #if !DEBUG
+2 -22
View File
@@ -9,7 +9,7 @@ private struct SidebarContentView: View {
var environments: ExtensionEnvironments var environments: ExtensionEnvironments
private var hasGroups: Bool { private var hasGroups: Bool {
environments.commandClient.groups?.isEmpty == false Variant.screenshotMode || environments.commandClient.groups?.isEmpty == false
} }
var body: some View { var body: some View {
@@ -77,9 +77,7 @@ public struct SidebarView: View {
} }
public var body: some View { public var body: some View {
if ApplicationLibrary.inPreview { if environments.extensionProfileLoading {
previewContent
} else if environments.extensionProfileLoading {
ProgressView() ProgressView()
} else if let profile = environments.extensionProfile { } else if let profile = environments.extensionProfile {
SidebarContentView( 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 @ViewBuilder
private var disconnectedContent: some View { private var disconnectedContent: some View {
List(selection: $localSelection) { List(selection: $localSelection) {
+1 -1
View File
@@ -203,7 +203,7 @@ public class StatusBarController: NSObject, NSMenuDelegate {
guard let groups else { return } guard let groups else { return }
let selectableGroups = groups.filter { $0.selectable } let selectableGroups = groups.filter(\.selectable)
if selectableGroups.isEmpty { if selectableGroups.isEmpty {
groupsItem?.isHidden = true groupsItem?.isHidden = true
return return
+29
View File
@@ -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
View File
@@ -30,7 +30,7 @@
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>en</string> <string>en</string>
<string>zh_CN</string> <string>zh-Hans</string>
</array> </array>
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
+1 -1
View File
@@ -63,7 +63,7 @@ struct MainView: View {
} }
var body: some View { var body: some View {
if ApplicationLibrary.inPreview { if Variant.screenshotMode {
mainBody.preferredColorScheme(.dark) mainBody.preferredColorScheme(.dark)
} else { } else {
mainBody mainBody
+319
View File
@@ -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]
+43
View File
@@ -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")
}
}
+5
View File
@@ -1,3 +1,4 @@
import Library
import MacLibrary import MacLibrary
import SwiftUI import SwiftUI
@@ -5,6 +6,10 @@ import SwiftUI
struct Application: App { struct Application: App {
@NSApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate @NSApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
init() {
ScreenshotLocalization.applyIfNeeded()
}
var body: some Scene { var body: some Scene {
MacApplication() MacApplication()
} }
+1 -1
View File
@@ -105,7 +105,7 @@
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>en</string> <string>en</string>
<string>zh_CN</string> <string>zh-Hans</string>
</array> </array>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
+142
View File
@@ -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
}
+78
View File
@@ -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")
}
}
+4
View File
@@ -7,6 +7,10 @@ struct Application: App {
@UIApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate @UIApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
@StateObject private var environments = ExtensionEnvironments() @StateObject private var environments = ExtensionEnvironments()
init() {
ScreenshotLocalization.applyIfNeeded()
}
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
MainView() MainView()
+1 -1
View File
@@ -51,7 +51,7 @@
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>en</string> <string>en</string>
<string>zh_CN</string> <string>zh-Hans</string>
</array> </array>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
+17 -1
View File
@@ -1,4 +1,5 @@
import ApplicationLibrary import ApplicationLibrary
import Foundation
import Libbox import Libbox
import Library import Library
import SwiftUI import SwiftUI
@@ -6,7 +7,16 @@ import SwiftUI
struct MainView: View { struct MainView: View {
@Environment(\.scenePhase) private var scenePhase @Environment(\.scenePhase) private var scenePhase
@EnvironmentObject private var environments: ExtensionEnvironments @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 importProfile: LibboxProfileContent?
@State private var importRemoteProfile: LibboxImportRemoteProfile? @State private var importRemoteProfile: LibboxImportRemoteProfile?
@@ -22,6 +32,12 @@ struct MainView: View {
} }
} }
.onAppear { .onAppear {
if Variant.screenshotMode,
let pageValue = ProcessInfo.processInfo.environment["SCREENSHOT_PAGE"],
let page = NavigationPage(snapshotValue: pageValue)
{
selection = page
}
environments.postReload() environments.postReload()
} }
.onChangeCompat(of: scenePhase) { newValue in .onChangeCompat(of: scenePhase) { newValue in
+58
View File
@@ -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
}
+72
View File
@@ -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")
}
}
+74
View File
@@ -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")
}
}
+40
View File
@@ -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 its 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()
}
}
}
+32
View File
@@ -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)
}
}
+261
View File
@@ -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
+58
View File
@@ -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).
+30
View File
@@ -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)
+356
View File
@@ -115,6 +115,27 @@
remoteGlobalIDString = 3A4EAD0F2A4FEAE6005435B3; remoteGlobalIDString = 3A4EAD0F2A4FEAE6005435B3;
remoteInfo = ApplicationLibrary; 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 */ = { 3A76504A2A4F07F6003945C5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 3AEC20BD2A45991900A63465 /* Project object */; 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; }; 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; }; 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; }; 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; }; 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; }; 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; }; 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 */ /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup 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>"; }; 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>"; }; 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>"; }; 3ADDCEC02E8B7240009ACE1D /* SFM */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (3ADDCEC22E8B7240009ACE1D /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = SFM; sourceTree = "<group>"; };
@@ -619,6 +647,27 @@
); );
runOnlyForDeploymentPostprocessing = 0; 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 */ = { 3A77016A2A4E6B34008F031F /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -764,6 +813,10 @@
3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */, 3ADDCFD52E8B72BC009ACE1D /* WidgetExtension */,
3AAAFB232EF5218F004C69AD /* FileProviderExtension */, 3AAAFB232EF5218F004C69AD /* FileProviderExtension */,
3AE595722F08C34000C13426 /* HelperService */, 3AE595722F08C34000C13426 /* HelperService */,
3A63135E2F0CEF190060A550 /* UITests */,
3A6313732F0CEF3D0060A550 /* SFIUITests */,
3A6313952F0CEF740060A550 /* SFTUITests */,
3A6313A82F0CEFDD0060A550 /* SFMUITests */,
3AEC20C72A45991900A63465 /* Products */, 3AEC20C72A45991900A63465 /* Products */,
3AEC21012A459AE300A63465 /* Frameworks */, 3AEC21012A459AE300A63465 /* Frameworks */,
3AE5C9042F08E53400C13426 /* Recovered References */, 3AE5C9042F08E53400C13426 /* Recovered References */,
@@ -787,6 +840,9 @@
3AE395F22C21A5CA00647718 /* WidgetExtension.appex */, 3AE395F22C21A5CA00647718 /* WidgetExtension.appex */,
3AAAFB202EF5218F004C69AD /* FileProviderExtension.appex */, 3AAAFB202EF5218F004C69AD /* FileProviderExtension.appex */,
3AE595712F08C34000C13426 /* RootHelper */, 3AE595712F08C34000C13426 /* RootHelper */,
3A6313722F0CEF3D0060A550 /* SFIUITests.xctest */,
3A6313942F0CEF740060A550 /* SFTUITests.xctest */,
3A6313A72F0CEFDD0060A550 /* SFMUITests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -880,6 +936,75 @@
productReference = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; productReference = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */;
productType = "com.apple.product-type.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 */ = { 3A77016C2A4E6B34008F031F /* IntentsExtension */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 3A7701772A4E6B34008F031F /* Build configuration list for PBXNativeTarget "IntentsExtension" */; buildConfigurationList = 3A7701772A4E6B34008F031F /* Build configuration list for PBXNativeTarget "IntentsExtension" */;
@@ -1181,6 +1306,18 @@
CreatedOnToolsVersion = 15.0; CreatedOnToolsVersion = 15.0;
LastSwiftMigration = 1500; LastSwiftMigration = 1500;
}; };
3A6313712F0CEF3D0060A550 = {
CreatedOnToolsVersion = 26.2;
TestTargetID = 3AEC20F22A459AB400A63465;
};
3A6313932F0CEF740060A550 = {
CreatedOnToolsVersion = 26.2;
TestTargetID = 3AC03B952A72BF3300B7946F;
};
3A6313A62F0CEFDD0060A550 = {
CreatedOnToolsVersion = 26.2;
TestTargetID = 3AEC21082A459B1900A63465;
};
3A77016C2A4E6B34008F031F = { 3A77016C2A4E6B34008F031F = {
CreatedOnToolsVersion = 15.0; CreatedOnToolsVersion = 15.0;
}; };
@@ -1257,6 +1394,9 @@
3AE395F12C21A5CA00647718 /* WidgetExtension */, 3AE395F12C21A5CA00647718 /* WidgetExtension */,
3AAAFB1F2EF5218F004C69AD /* FileProviderExtension */, 3AAAFB1F2EF5218F004C69AD /* FileProviderExtension */,
3AE595702F08C34000C13426 /* RootHelper */, 3AE595702F08C34000C13426 /* RootHelper */,
3A6313712F0CEF3D0060A550 /* SFIUITests */,
3A6313932F0CEF740060A550 /* SFTUITests */,
3A6313A62F0CEFDD0060A550 /* SFMUITests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@@ -1270,6 +1410,27 @@
); );
runOnlyForDeploymentPostprocessing = 0; 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 */ = { 3AAAFB1E2EF5218F004C69AD /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -1363,6 +1524,27 @@
); );
runOnlyForDeploymentPostprocessing = 0; 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 */ = { 3A7701692A4E6B34008F031F /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -1485,6 +1667,21 @@
target = 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */; target = 3A4EAD0F2A4FEAE6005435B3 /* ApplicationLibrary */;
targetProxy = 3A4FB15E2A73468C007012B9 /* PBXContainerItemProxy */; 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 */ = { 3A76504B2A4F07F6003945C5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency; isa = PBXTargetDependency;
target = 3AEC211C2A459B4700A63465 /* Library */; target = 3AEC211C2A459B4700A63465 /* Library */;
@@ -1750,6 +1947,138 @@
}; };
name = Release; 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 */ = { 3A7701782A4E6B34008F031F /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@@ -2851,6 +3180,33 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release; 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" */ = { 3A7701772A4E6B34008F031F /* Build configuration list for PBXNativeTarget "IntentsExtension" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( 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"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1520" LastUpgradeVersion = "2620"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -28,6 +29,19 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "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> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1520" LastUpgradeVersion = "2620"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1520" LastUpgradeVersion = "2620"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -28,6 +29,19 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "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> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"
@@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1520" LastUpgradeVersion = "2620"
version = "1.7"> version = "1.7">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
@@ -28,6 +29,19 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES" shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "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> </TestAction>
<LaunchAction <LaunchAction
buildConfiguration = "Debug" buildConfiguration = "Debug"