Add screenshot generator
This commit is contained in:
+5
-1
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}()
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,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
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WindowAccessor: NSViewRepresentable {
|
||||||
|
final class WindowReportingView: NSView {
|
||||||
|
var onWindowChange: ((NSWindow?) -> Void)?
|
||||||
|
|
||||||
|
override func viewDidMoveToWindow() {
|
||||||
|
super.viewDidMoveToWindow()
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.onWindowChange?(self?.window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let callback: (NSWindow?) -> Void
|
||||||
|
|
||||||
|
func makeNSView(context _: Context) -> WindowReportingView {
|
||||||
|
let view = WindowReportingView()
|
||||||
|
view.onWindowChange = callback
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNSView(_ nsView: WindowReportingView, context _: Context) {
|
||||||
|
nsView.onWindowChange = callback
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
callback(nsView.window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -30,7 +30,7 @@
|
|||||||
<key>CFBundleLocalizations</key>
|
<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
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
//
|
||||||
|
// SnapshotHelper.swift
|
||||||
|
// Example
|
||||||
|
//
|
||||||
|
// Created by Felix Krause on 10/8/15.
|
||||||
|
//
|
||||||
|
|
||||||
|
// -----------------------------------------------------
|
||||||
|
// IMPORTANT: When modifying this file, make sure to
|
||||||
|
// increment the version number at the very
|
||||||
|
// bottom of the file to notify users about
|
||||||
|
// the new SnapshotHelper.swift
|
||||||
|
// -----------------------------------------------------
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
|
||||||
|
Snapshot.setupSnapshot(app, waitForAnimations: waitForAnimations)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func snapshot(_ name: String, waitForLoadingIndicator: Bool) {
|
||||||
|
if waitForLoadingIndicator {
|
||||||
|
Snapshot.snapshot(name)
|
||||||
|
} else {
|
||||||
|
Snapshot.snapshot(name, timeWaitingForIdle: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// - Parameters:
|
||||||
|
/// - name: The name of the snapshot
|
||||||
|
/// - timeout: Amount of seconds to wait until the network loading indicator disappears. Pass `0` if you don't want to wait.
|
||||||
|
@MainActor
|
||||||
|
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
|
||||||
|
Snapshot.snapshot(name, timeWaitingForIdle: timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum SnapshotError: Error, CustomDebugStringConvertible {
|
||||||
|
case cannotFindSimulatorHomeDirectory
|
||||||
|
case cannotRunOnPhysicalDevice
|
||||||
|
|
||||||
|
var debugDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .cannotFindSimulatorHomeDirectory:
|
||||||
|
return "Couldn't find simulator home location. Please, check SIMULATOR_HOST_HOME env variable."
|
||||||
|
case .cannotRunOnPhysicalDevice:
|
||||||
|
return "Can't use Snapshot on a physical device."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objcMembers
|
||||||
|
@MainActor
|
||||||
|
open class Snapshot: NSObject {
|
||||||
|
static var app: XCUIApplication?
|
||||||
|
static var waitForAnimations = true
|
||||||
|
static var cacheDirectory: URL?
|
||||||
|
static var screenshotsDirectory: URL? {
|
||||||
|
if let override = ProcessInfo.processInfo.environment["SCREENSHOTS_DIR"], !override.isEmpty {
|
||||||
|
return URL(fileURLWithPath: override, isDirectory: true)
|
||||||
|
}
|
||||||
|
return cacheDirectory?.appendingPathComponent("screenshots", isDirectory: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var deviceLanguage = ""
|
||||||
|
static var currentLocale = ""
|
||||||
|
|
||||||
|
open class func setupSnapshot(_ app: XCUIApplication, waitForAnimations: Bool = true) {
|
||||||
|
Snapshot.app = app
|
||||||
|
Snapshot.waitForAnimations = waitForAnimations
|
||||||
|
|
||||||
|
do {
|
||||||
|
let cacheDir = try getCacheDirectory()
|
||||||
|
Snapshot.cacheDirectory = cacheDir
|
||||||
|
if let screenshotsDirectory {
|
||||||
|
try FileManager.default.createDirectory(at: screenshotsDirectory, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
setLanguage(app)
|
||||||
|
setLocale(app)
|
||||||
|
setLaunchArguments(app)
|
||||||
|
} catch {
|
||||||
|
NSLog(error.localizedDescription)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class func setLanguage(_ app: XCUIApplication) {
|
||||||
|
guard let cacheDirectory else {
|
||||||
|
NSLog("CacheDirectory is not set - probably running on a physical device?")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = cacheDirectory.appendingPathComponent("language.txt")
|
||||||
|
|
||||||
|
do {
|
||||||
|
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
|
||||||
|
deviceLanguage = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
|
||||||
|
app.launchArguments += ["-AppleLanguages", "(\(deviceLanguage))"]
|
||||||
|
} catch {
|
||||||
|
NSLog("Couldn't detect/set language...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class func setLocale(_ app: XCUIApplication) {
|
||||||
|
guard let cacheDirectory else {
|
||||||
|
NSLog("CacheDirectory is not set - probably running on a physical device?")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = cacheDirectory.appendingPathComponent("locale.txt")
|
||||||
|
|
||||||
|
do {
|
||||||
|
let trimCharacterSet = CharacterSet.whitespacesAndNewlines
|
||||||
|
currentLocale = try String(contentsOf: path, encoding: .utf8).trimmingCharacters(in: trimCharacterSet)
|
||||||
|
} catch {
|
||||||
|
NSLog("Couldn't detect/set locale...")
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentLocale.isEmpty, !deviceLanguage.isEmpty {
|
||||||
|
currentLocale = Locale(identifier: deviceLanguage).identifier
|
||||||
|
}
|
||||||
|
|
||||||
|
if !currentLocale.isEmpty {
|
||||||
|
app.launchArguments += ["-AppleLocale", "\"\(currentLocale)\""]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class func setLaunchArguments(_ app: XCUIApplication) {
|
||||||
|
guard let cacheDirectory else {
|
||||||
|
NSLog("CacheDirectory is not set - probably running on a physical device?")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = cacheDirectory.appendingPathComponent("snapshot-launch_arguments.txt")
|
||||||
|
app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"]
|
||||||
|
|
||||||
|
do {
|
||||||
|
let launchArguments = try String(contentsOf: path, encoding: String.Encoding.utf8)
|
||||||
|
let regex = try NSRegularExpression(pattern: "(\\\".+?\\\"|\\S+)", options: [])
|
||||||
|
let matches = regex.matches(in: launchArguments, options: [], range: NSRange(location: 0, length: launchArguments.count))
|
||||||
|
let results = matches.map { result -> String in
|
||||||
|
(launchArguments as NSString).substring(with: result.range)
|
||||||
|
}
|
||||||
|
app.launchArguments += results
|
||||||
|
} catch {
|
||||||
|
NSLog("Couldn't detect/set launch_arguments...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval = 20) {
|
||||||
|
if timeout > 0 {
|
||||||
|
waitForLoadingIndicatorToDisappear(within: timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
NSLog("snapshot: \(name)") // more information about this, check out https://docs.fastlane.tools/actions/snapshot/#how-does-it-work
|
||||||
|
|
||||||
|
if Snapshot.waitForAnimations {
|
||||||
|
sleep(1) // Waiting for the animation to be finished (kind of)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(OSX)
|
||||||
|
guard let app else {
|
||||||
|
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.typeKey(XCUIKeyboardKeySecondaryFn, modifierFlags: [])
|
||||||
|
#else
|
||||||
|
|
||||||
|
guard self.app != nil else {
|
||||||
|
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let screenshot = XCUIScreen.main.screenshot()
|
||||||
|
#if os(iOS) && !targetEnvironment(macCatalyst)
|
||||||
|
let image = XCUIDevice.shared.orientation.isLandscape ? fixLandscapeOrientation(image: screenshot.image) : screenshot.image
|
||||||
|
#else
|
||||||
|
let image = screenshot.image
|
||||||
|
#endif
|
||||||
|
|
||||||
|
guard var simulator = ProcessInfo().environment["SIMULATOR_DEVICE_NAME"], let screenshotsDir = screenshotsDirectory else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
// The simulator name contains "Clone X of " inside the screenshot file when running parallelized UI Tests on concurrent devices
|
||||||
|
let regex = try NSRegularExpression(pattern: "Clone [0-9]+ of ")
|
||||||
|
let range = NSRange(location: 0, length: simulator.count)
|
||||||
|
simulator = regex.stringByReplacingMatches(in: simulator, range: range, withTemplate: "")
|
||||||
|
|
||||||
|
let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
|
||||||
|
#if swift(<5.0)
|
||||||
|
try UIImagePNGRepresentation(image)?.write(to: path, options: .atomic)
|
||||||
|
#else
|
||||||
|
try image.pngData()?.write(to: path, options: .atomic)
|
||||||
|
#endif
|
||||||
|
} catch {
|
||||||
|
NSLog("Problem writing screenshot: \(name) to \(screenshotsDir)/\(simulator)-\(name).png")
|
||||||
|
NSLog(error.localizedDescription)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
class func fixLandscapeOrientation(image: UIImage) -> UIImage {
|
||||||
|
#if os(watchOS)
|
||||||
|
return image
|
||||||
|
#else
|
||||||
|
if #available(iOS 10.0, *) {
|
||||||
|
let format = UIGraphicsImageRendererFormat()
|
||||||
|
format.scale = image.scale
|
||||||
|
let renderer = UIGraphicsImageRenderer(size: image.size, format: format)
|
||||||
|
return renderer.image { _ in
|
||||||
|
image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
class func waitForLoadingIndicatorToDisappear(within timeout: TimeInterval) {
|
||||||
|
#if os(tvOS)
|
||||||
|
return
|
||||||
|
#endif
|
||||||
|
|
||||||
|
guard let app else {
|
||||||
|
NSLog("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let networkLoadingIndicator = app.otherElements.deviceStatusBars.networkLoadingIndicators.element
|
||||||
|
let networkLoadingIndicatorDisappeared = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: networkLoadingIndicator)
|
||||||
|
_ = XCTWaiter.wait(for: [networkLoadingIndicatorDisappeared], timeout: timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
class func getCacheDirectory() throws -> URL {
|
||||||
|
let cachePath = "Library/Caches/tools.fastlane"
|
||||||
|
// on OSX config is stored in /Users/<username>/Library
|
||||||
|
// and on iOS/tvOS/WatchOS it's in simulator's home dir
|
||||||
|
#if os(OSX)
|
||||||
|
let homeDir = URL(fileURLWithPath: NSHomeDirectory())
|
||||||
|
return homeDir.appendingPathComponent(cachePath)
|
||||||
|
#elseif arch(i386) || arch(x86_64) || arch(arm64)
|
||||||
|
guard let simulatorHostHome = ProcessInfo().environment["SIMULATOR_HOST_HOME"] else {
|
||||||
|
throw SnapshotError.cannotFindSimulatorHomeDirectory
|
||||||
|
}
|
||||||
|
let homeDir = URL(fileURLWithPath: simulatorHostHome)
|
||||||
|
return homeDir.appendingPathComponent(cachePath)
|
||||||
|
#else
|
||||||
|
throw SnapshotError.cannotRunOnPhysicalDevice
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension XCUIElementAttributes {
|
||||||
|
var isNetworkLoadingIndicator: Bool {
|
||||||
|
if hasAllowListedIdentifier { return false }
|
||||||
|
|
||||||
|
let hasOldLoadingIndicatorSize = frame.size == CGSize(width: 10, height: 20)
|
||||||
|
let hasNewLoadingIndicatorSize = frame.size.width.isBetween(46, and: 47) && frame.size.height.isBetween(2, and: 3)
|
||||||
|
|
||||||
|
return hasOldLoadingIndicatorSize || hasNewLoadingIndicatorSize
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAllowListedIdentifier: Bool {
|
||||||
|
let allowListedIdentifiers = ["GeofenceLocationTrackingOn", "StandardLocationTrackingOn"]
|
||||||
|
|
||||||
|
return allowListedIdentifiers.contains(identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isStatusBar(_ deviceWidth: CGFloat) -> Bool {
|
||||||
|
if elementType == .statusBar { return true }
|
||||||
|
guard frame.origin == .zero else { return false }
|
||||||
|
|
||||||
|
let oldStatusBarSize = CGSize(width: deviceWidth, height: 20)
|
||||||
|
let newStatusBarSize = CGSize(width: deviceWidth, height: 44)
|
||||||
|
|
||||||
|
return [oldStatusBarSize, newStatusBarSize].contains(frame.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension XCUIElementQuery {
|
||||||
|
var networkLoadingIndicators: XCUIElementQuery {
|
||||||
|
let isNetworkLoadingIndicator = NSPredicate { evaluatedObject, _ in
|
||||||
|
guard let element = evaluatedObject as? XCUIElementAttributes else { return false }
|
||||||
|
|
||||||
|
return element.isNetworkLoadingIndicator
|
||||||
|
}
|
||||||
|
|
||||||
|
return containing(isNetworkLoadingIndicator)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
var deviceStatusBars: XCUIElementQuery {
|
||||||
|
guard let app = Snapshot.app else {
|
||||||
|
fatalError("XCUIApplication is not set. Please call setupSnapshot(app) before snapshot().")
|
||||||
|
}
|
||||||
|
|
||||||
|
let deviceWidth = app.windows.firstMatch.frame.width
|
||||||
|
|
||||||
|
let isStatusBar = NSPredicate { evaluatedObject, _ in
|
||||||
|
guard let element = evaluatedObject as? XCUIElementAttributes else { return false }
|
||||||
|
|
||||||
|
return element.isStatusBar(deviceWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
return containing(isStatusBar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension CGFloat {
|
||||||
|
func isBetween(_ numberA: CGFloat, and numberB: CGFloat) -> Bool {
|
||||||
|
numberA ... numberB ~= self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Please don't remove the lines below
|
||||||
|
// They are used to detect outdated configuration files
|
||||||
|
// SnapshotHelperVersion [1.31]
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import XCTest
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class SnapshotTests: XCTestCase {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
setupSnapshot(app)
|
||||||
|
app.launch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func test01Dashboard() throws {
|
||||||
|
snapshot("01_Dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
func test02Logs() throws {
|
||||||
|
if app.tabBars.buttons["Logs"].exists {
|
||||||
|
app.tabBars.buttons["Logs"].firstMatch.tap()
|
||||||
|
} else if app.buttons["Logs"].exists {
|
||||||
|
app.buttons["Logs"].firstMatch.tap()
|
||||||
|
}
|
||||||
|
sleep(1)
|
||||||
|
snapshot("02_Logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
func test03Settings() throws {
|
||||||
|
// iPad on iOS 18+ uses floating tab bar which creates nested elements
|
||||||
|
// Use firstMatch to handle multiple matching elements
|
||||||
|
if app.tabBars.buttons["Settings"].exists {
|
||||||
|
app.tabBars.buttons["Settings"].firstMatch.tap()
|
||||||
|
} else if app.buttons["Settings"].exists {
|
||||||
|
app.buttons["Settings"].firstMatch.tap()
|
||||||
|
} else {
|
||||||
|
let tabBar = app.tabBars.firstMatch
|
||||||
|
if tabBar.exists {
|
||||||
|
tabBar.buttons.element(boundBy: tabBar.buttons.count - 1).tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sleep(1)
|
||||||
|
snapshot("03_Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import Library
|
||||||
import MacLibrary
|
import 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
@@ -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>
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import AppKit
|
||||||
|
import CoreGraphics
|
||||||
|
import Darwin
|
||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
private var screenshotsDir: URL!
|
||||||
|
@MainActor
|
||||||
|
private var currentApp: XCUIApplication?
|
||||||
|
|
||||||
|
private func resolveScreenshotsDir() -> URL {
|
||||||
|
let env = ProcessInfo.processInfo.environment
|
||||||
|
if let override = env["SCREENSHOTS_DIR"], !override.isEmpty {
|
||||||
|
return URL(fileURLWithPath: override, isDirectory: true)
|
||||||
|
}
|
||||||
|
if let override = env["SNAPSHOT_SCREENSHOTS_PATH"], !override.isEmpty {
|
||||||
|
return URL(fileURLWithPath: override, isDirectory: true)
|
||||||
|
}
|
||||||
|
if let cachesDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first {
|
||||||
|
return cachesDir.appendingPathComponent("tools.fastlane/screenshots", isDirectory: true)
|
||||||
|
}
|
||||||
|
return URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
||||||
|
.appendingPathComponent("tools.fastlane/screenshots", isDirectory: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func setupSnapshot(_ app: XCUIApplication, waitForAnimations _: Bool = true) {
|
||||||
|
app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"]
|
||||||
|
currentApp = app
|
||||||
|
screenshotsDir = resolveScreenshotsDir()
|
||||||
|
try! FileManager.default.createDirectory(at: screenshotsDir, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func snapshot(_ name: String, waitForLoadingIndicator: Bool = true) {
|
||||||
|
snapshot(name, timeWaitingForIdle: waitForLoadingIndicator ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval) {
|
||||||
|
if timeout > 0 {
|
||||||
|
sleep(UInt32(timeout))
|
||||||
|
}
|
||||||
|
guard let app = currentApp else { return }
|
||||||
|
let path = screenshotsDir.appendingPathComponent("Mac-\(name).png")
|
||||||
|
var resolvedWindowID: CGWindowID?
|
||||||
|
let deadline = Date().addingTimeInterval(10)
|
||||||
|
while resolvedWindowID == nil, Date() < deadline {
|
||||||
|
resolvedWindowID = windowID(for: app)
|
||||||
|
if resolvedWindowID == nil {
|
||||||
|
usleep(200_000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let resolvedWindowID {
|
||||||
|
if let image = cgWindowListCreateImage(windowID: resolvedWindowID), let data = pngData(from: image) {
|
||||||
|
try! data.write(to: path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if captureWindowScreenshot(windowID: resolvedWindowID, to: path) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let screenshot = XCUIScreen.main.screenshot()
|
||||||
|
try! screenshot.pngRepresentation.write(to: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func captureWindowScreenshot(windowID: CGWindowID, to path: URL) -> Bool {
|
||||||
|
let task = Process()
|
||||||
|
task.executableURL = URL(fileURLWithPath: "/usr/sbin/screencapture")
|
||||||
|
task.arguments = ["-x", "-t", "png", "-o", "-l", "\(windowID)", path.path]
|
||||||
|
do {
|
||||||
|
try task.run()
|
||||||
|
task.waitUntilExit()
|
||||||
|
return task.terminationStatus == 0
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func windowID(for _: XCUIApplication) -> CGWindowID? {
|
||||||
|
guard let runningApp = NSRunningApplication.runningApplications(withBundleIdentifier: "io.nekohasekai.sfavt").first else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let pid = Int(runningApp.processIdentifier)
|
||||||
|
let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
|
||||||
|
guard let infoList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var candidate: (id: CGWindowID, area: CGFloat)?
|
||||||
|
for info in infoList {
|
||||||
|
guard let ownerPid = info[kCGWindowOwnerPID as String] as? Int, ownerPid == pid else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let layer = info[kCGWindowLayer as String] as? Int ?? 0
|
||||||
|
guard layer == 0 else { continue }
|
||||||
|
guard let boundsDict = info[kCGWindowBounds as String] as? [String: Any],
|
||||||
|
let bounds = CGRect(dictionaryRepresentation: boundsDict as CFDictionary)
|
||||||
|
else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let area = bounds.width * bounds.height
|
||||||
|
guard area > 0 else { continue }
|
||||||
|
guard let number = info[kCGWindowNumber as String] as? Int else { continue }
|
||||||
|
if candidate == nil || area > (candidate?.area ?? 0) {
|
||||||
|
candidate = (CGWindowID(number), area)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidate?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cgWindowListCreateImage(windowID: CGWindowID) -> CGImage? {
|
||||||
|
typealias CGWindowListCreateImageFunc = @convention(c) (CGRect, CGWindowListOption, CGWindowID, CGWindowImageOption) -> CGImage?
|
||||||
|
let rtldDefault = UnsafeMutableRawPointer(bitPattern: -2)
|
||||||
|
guard let symbol = dlsym(rtldDefault, "CGWindowListCreateImage") else { return nil }
|
||||||
|
let function = unsafeBitCast(symbol, to: CGWindowListCreateImageFunc.self)
|
||||||
|
let bounds = CGRect.null
|
||||||
|
let listOption = CGWindowListOption.optionIncludingWindow
|
||||||
|
let imageOption: CGWindowImageOption = [.boundsIgnoreFraming, .bestResolution]
|
||||||
|
return function(bounds, listOption, windowID, imageOption)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pngData(from image: CGImage) -> Data? {
|
||||||
|
let rep = NSBitmapImageRep(cgImage: image)
|
||||||
|
return rep.representation(using: .png, properties: [:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadScreenshotLanguage() -> (language: String?, locale: String?) {
|
||||||
|
let dir = resolveScreenshotsDir()
|
||||||
|
let language = readScreenshotText(dir.appendingPathComponent("language.txt"))
|
||||||
|
let locale = readScreenshotText(dir.appendingPathComponent("locale.txt"))
|
||||||
|
return (language, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func readScreenshotText(_ url: URL) -> String? {
|
||||||
|
guard let text = try? String(contentsOf: url, encoding: .utf8) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return trimmed.isEmpty ? nil : trimmed
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import XCTest
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class SnapshotTests: XCTestCase {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
app.terminate()
|
||||||
|
configureLanguage()
|
||||||
|
if let value = ProcessInfo.processInfo.environment["SCREENSHOT_WINDOW_PIXEL_HEIGHT"] {
|
||||||
|
app.launchEnvironment["SCREENSHOT_WINDOW_PIXEL_HEIGHT"] = value
|
||||||
|
}
|
||||||
|
if let value = ProcessInfo.processInfo.environment["SCREENSHOT_WINDOW_HEIGHT"] {
|
||||||
|
app.launchEnvironment["SCREENSHOT_WINDOW_HEIGHT"] = value
|
||||||
|
}
|
||||||
|
if let page = screenshotPage() {
|
||||||
|
app.launchEnvironment["SCREENSHOT_PAGE"] = page
|
||||||
|
} else {
|
||||||
|
app.launchEnvironment["SCREENSHOT_PAGE"] = ""
|
||||||
|
}
|
||||||
|
setupSnapshot(app)
|
||||||
|
app.launch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureLanguage() {
|
||||||
|
let environment = ProcessInfo.processInfo.environment
|
||||||
|
var language = environment["SCREENSHOT_LANGUAGE"]?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
var locale = environment["SCREENSHOT_LOCALE"]?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if language == nil || language?.isEmpty == true {
|
||||||
|
let cached = loadScreenshotLanguage()
|
||||||
|
language = cached.language
|
||||||
|
if locale == nil || locale?.isEmpty == true {
|
||||||
|
locale = cached.locale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let language, !language.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.launchArguments += ["-AppleLanguages", "(\(language))"]
|
||||||
|
app.launchEnvironment["SCREENSHOT_LANGUAGE"] = language
|
||||||
|
if let locale, !locale.isEmpty {
|
||||||
|
app.launchArguments += ["-AppleLocale", locale]
|
||||||
|
app.launchEnvironment["SCREENSHOT_LOCALE"] = locale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func screenshotPage() -> String? {
|
||||||
|
let testName = name
|
||||||
|
if testName.contains("test01Dashboard") {
|
||||||
|
return "dashboard"
|
||||||
|
}
|
||||||
|
if testName.contains("test02Logs") {
|
||||||
|
return "logs"
|
||||||
|
}
|
||||||
|
if testName.contains("test03Settings") {
|
||||||
|
return "settings"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func test01Dashboard() throws {
|
||||||
|
sleep(1)
|
||||||
|
snapshot("01_Dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
func test02Logs() throws {
|
||||||
|
sleep(1)
|
||||||
|
snapshot("02_Logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
func test03Settings() throws {
|
||||||
|
sleep(1)
|
||||||
|
snapshot("03_Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@ struct Application: App {
|
|||||||
@UIApplicationDelegateAdaptor private var appDelegate: ApplicationDelegate
|
@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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import Foundation
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
private var screenshotsDir: URL!
|
||||||
|
|
||||||
|
private func resolveScreenshotsDir() -> URL {
|
||||||
|
let env = ProcessInfo.processInfo.environment
|
||||||
|
if let override = env["SCREENSHOTS_DIR"], !override.isEmpty {
|
||||||
|
return URL(fileURLWithPath: override, isDirectory: true)
|
||||||
|
}
|
||||||
|
if let override = env["SNAPSHOT_SCREENSHOTS_PATH"], !override.isEmpty {
|
||||||
|
return URL(fileURLWithPath: override, isDirectory: true)
|
||||||
|
}
|
||||||
|
if let simulatorHostHome = env["SIMULATOR_HOST_HOME"] {
|
||||||
|
return URL(fileURLWithPath: simulatorHostHome)
|
||||||
|
.appendingPathComponent("Library/Caches/tools.fastlane/screenshots")
|
||||||
|
}
|
||||||
|
return URL(fileURLWithPath: NSHomeDirectory())
|
||||||
|
.appendingPathComponent("Library/Caches/tools.fastlane/screenshots")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func setupSnapshot(_ app: XCUIApplication, waitForAnimations _: Bool = true) {
|
||||||
|
app.launchArguments += ["-FASTLANE_SNAPSHOT", "YES", "-ui_testing"]
|
||||||
|
screenshotsDir = resolveScreenshotsDir()
|
||||||
|
try! FileManager.default.createDirectory(at: screenshotsDir, withIntermediateDirectories: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func snapshot(_ name: String, waitForLoadingIndicator: Bool = true) {
|
||||||
|
snapshot(name, timeWaitingForIdle: waitForLoadingIndicator ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func snapshot(_ name: String, timeWaitingForIdle timeout: TimeInterval) {
|
||||||
|
if timeout > 0 {
|
||||||
|
sleep(UInt32(timeout))
|
||||||
|
}
|
||||||
|
let simulator = ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] ?? "AppleTV"
|
||||||
|
let screenshot = XCUIScreen.main.screenshot()
|
||||||
|
let path = screenshotsDir.appendingPathComponent("\(simulator)-\(name).png")
|
||||||
|
try! screenshot.pngRepresentation.write(to: path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadScreenshotLanguage() -> (language: String?, locale: String?) {
|
||||||
|
let dir = resolveScreenshotsDir()
|
||||||
|
let language = readScreenshotText(dir.appendingPathComponent("language.txt"))
|
||||||
|
let locale = readScreenshotText(dir.appendingPathComponent("locale.txt"))
|
||||||
|
return (language, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func readScreenshotText(_ url: URL) -> String? {
|
||||||
|
guard let text = try? String(contentsOf: url, encoding: .utf8) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
return trimmed.isEmpty ? nil : trimmed
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import XCTest
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class SnapshotTests: XCTestCase {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
app.terminate()
|
||||||
|
configureLanguage()
|
||||||
|
if let page = screenshotPage() {
|
||||||
|
app.launchEnvironment["SCREENSHOT_PAGE"] = page
|
||||||
|
} else {
|
||||||
|
app.launchEnvironment["SCREENSHOT_PAGE"] = ""
|
||||||
|
}
|
||||||
|
setupSnapshot(app)
|
||||||
|
app.launch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func configureLanguage() {
|
||||||
|
let environment = ProcessInfo.processInfo.environment
|
||||||
|
var language = environment["SCREENSHOT_LANGUAGE"]?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
var locale = environment["SCREENSHOT_LOCALE"]?
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if language == nil || language?.isEmpty == true {
|
||||||
|
let cached = loadScreenshotLanguage()
|
||||||
|
language = cached.language
|
||||||
|
if locale == nil || locale?.isEmpty == true {
|
||||||
|
locale = cached.locale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
guard let language, !language.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.launchArguments += ["-AppleLanguages", "(\(language))"]
|
||||||
|
app.launchEnvironment["SCREENSHOT_LANGUAGE"] = language
|
||||||
|
if let locale, !locale.isEmpty {
|
||||||
|
app.launchArguments += ["-AppleLocale", locale]
|
||||||
|
app.launchEnvironment["SCREENSHOT_LOCALE"] = locale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func screenshotPage() -> String? {
|
||||||
|
let testName = name
|
||||||
|
if testName.contains("test01Dashboard") {
|
||||||
|
return "dashboard"
|
||||||
|
}
|
||||||
|
if testName.contains("test02Logs") {
|
||||||
|
return "logs"
|
||||||
|
}
|
||||||
|
if testName.contains("test03Settings") {
|
||||||
|
return "settings"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func test01Dashboard() throws {
|
||||||
|
sleep(1)
|
||||||
|
snapshot("01_Dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
func test02Logs() throws {
|
||||||
|
sleep(1)
|
||||||
|
snapshot("02_Logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
func test03Settings() throws {
|
||||||
|
sleep(1)
|
||||||
|
snapshot("03_Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import XCTest
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class SnapshotTests: XCTestCase {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
setupSnapshot(app)
|
||||||
|
app.launch()
|
||||||
|
}
|
||||||
|
|
||||||
|
func test01Dashboard() throws {
|
||||||
|
#if os(macOS)
|
||||||
|
if app.outlines.staticTexts["Dashboard"].exists {
|
||||||
|
app.outlines.staticTexts["Dashboard"].click()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
sleep(1)
|
||||||
|
snapshot("01_Dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
func test02Logs() throws {
|
||||||
|
#if os(iOS)
|
||||||
|
if app.tabBars.buttons["Logs"].exists {
|
||||||
|
app.tabBars.buttons["Logs"].firstMatch.tap()
|
||||||
|
} else if app.buttons["Logs"].exists {
|
||||||
|
app.buttons["Logs"].firstMatch.tap()
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
if app.outlines.staticTexts["Logs"].exists {
|
||||||
|
app.outlines.staticTexts["Logs"].click()
|
||||||
|
}
|
||||||
|
#elseif os(tvOS)
|
||||||
|
let remote = XCUIRemote.shared
|
||||||
|
for _ in 0 ..< 3 {
|
||||||
|
remote.press(.down)
|
||||||
|
usleep(300_000)
|
||||||
|
}
|
||||||
|
if app.cells["Logs"].exists {
|
||||||
|
app.cells["Logs"].tap()
|
||||||
|
} else {
|
||||||
|
remote.press(.select)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
sleep(1)
|
||||||
|
snapshot("02_Logs")
|
||||||
|
}
|
||||||
|
|
||||||
|
func test03Settings() throws {
|
||||||
|
#if os(iOS)
|
||||||
|
if app.tabBars.buttons["Settings"].exists {
|
||||||
|
app.tabBars.buttons["Settings"].firstMatch.tap()
|
||||||
|
} else if app.buttons["Settings"].exists {
|
||||||
|
app.buttons["Settings"].firstMatch.tap()
|
||||||
|
}
|
||||||
|
#elseif os(macOS)
|
||||||
|
if app.outlines.staticTexts["Settings"].exists {
|
||||||
|
app.outlines.staticTexts["Settings"].click()
|
||||||
|
}
|
||||||
|
#elseif os(tvOS)
|
||||||
|
let remote = XCUIRemote.shared
|
||||||
|
remote.press(.down)
|
||||||
|
usleep(300_000)
|
||||||
|
if app.cells["Settings"].exists {
|
||||||
|
app.cells["Settings"].tap()
|
||||||
|
} else {
|
||||||
|
remote.press(.select)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
sleep(1)
|
||||||
|
snapshot("03_Settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// UITests.swift
|
||||||
|
// UITests
|
||||||
|
//
|
||||||
|
// Created by sekai on 2026-01-06 15:19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class UITests: XCTestCase {
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
|
||||||
|
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||||
|
continueAfterFailure = false
|
||||||
|
|
||||||
|
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||||
|
}
|
||||||
|
|
||||||
|
override func tearDownWithError() throws {
|
||||||
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testExample() throws {
|
||||||
|
// UI tests must launch the application that they test.
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testLaunchPerformance() throws {
|
||||||
|
// This measures how long it takes to launch your application.
|
||||||
|
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
||||||
|
XCUIApplication().launch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
//
|
||||||
|
// UITestsLaunchTests.swift
|
||||||
|
// UITests
|
||||||
|
//
|
||||||
|
// Created by sekai on 2026-01-06 15:19.
|
||||||
|
//
|
||||||
|
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
final class UITestsLaunchTests: XCTestCase {
|
||||||
|
override class var runsForEachTargetApplicationUIConfiguration: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setUpWithError() throws {
|
||||||
|
continueAfterFailure = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testLaunch() throws {
|
||||||
|
let app = XCUIApplication()
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Insert steps here to perform after app launch but before taking a screenshot,
|
||||||
|
// such as logging into a test account or navigating somewhere in the app
|
||||||
|
|
||||||
|
let attachment = XCTAttachment(screenshot: app.screenshot())
|
||||||
|
attachment.name = "Launch Screen"
|
||||||
|
attachment.lifetime = .keepAlways
|
||||||
|
add(attachment)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,261 @@
|
|||||||
|
default_platform(:ios)
|
||||||
|
|
||||||
|
ENV["FASTLANE_XCODEBUILD_SETTINGS_RETRIES"] = "0"
|
||||||
|
ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "30"
|
||||||
|
|
||||||
|
SCREENSHOT_LANGUAGES = ["en-US", "zh-Hans"].freeze
|
||||||
|
SCREENSHOT_LOCALES = {
|
||||||
|
"zh-Hans" => "zh_CN"
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def reset_dir(path)
|
||||||
|
FileUtils.rm_rf(path)
|
||||||
|
FileUtils.mkdir_p(path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def screenshot_locale(language)
|
||||||
|
SCREENSHOT_LOCALES[language]
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_screenshot_language_files(dir, language, locale)
|
||||||
|
FileUtils.mkdir_p(dir)
|
||||||
|
File.write(File.join(dir, "language.txt"), language)
|
||||||
|
if locale
|
||||||
|
File.write(File.join(dir, "locale.txt"), locale)
|
||||||
|
else
|
||||||
|
FileUtils.rm_f(File.join(dir, "locale.txt"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def shutdown_simulator(name)
|
||||||
|
sh("xcrun simctl shutdown \"#{name}\" || true")
|
||||||
|
end
|
||||||
|
|
||||||
|
platform :ios do
|
||||||
|
desc "Generate iOS screenshots"
|
||||||
|
lane :screenshots do
|
||||||
|
ENV["SNAPSHOT_SIMULATOR_WAIT_FOR_BOOT_TIMEOUT"] = "10"
|
||||||
|
screenshots_cache = File.expand_path("~/Library/Caches/tools.fastlane/screenshots")
|
||||||
|
reset_dir(screenshots_cache)
|
||||||
|
capture_screenshots
|
||||||
|
shutdown_simulator("iPhone 11 Pro Max")
|
||||||
|
shutdown_simulator("iPad Pro 13-inch (M4)")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
platform :mac do
|
||||||
|
desc "Generate macOS screenshots"
|
||||||
|
lane :screenshots do
|
||||||
|
output_base_dir = File.expand_path("../screenshots/macos", __dir__)
|
||||||
|
screenshots_cache_candidates = [
|
||||||
|
File.expand_path("~/Library/Caches/tools.fastlane/screenshots"),
|
||||||
|
File.expand_path("~/Library/Containers/io.nekohasekai.sfavt.SFMUITests.xctrunner/Data/Library/Caches/tools.fastlane/screenshots")
|
||||||
|
].uniq
|
||||||
|
ENV["DISABLE_SWIFTLINT"] = "1"
|
||||||
|
ENV["SCREENSHOT_WINDOW_PIXEL_HEIGHT"] = "1000"
|
||||||
|
|
||||||
|
reset_dir(output_base_dir)
|
||||||
|
|
||||||
|
SCREENSHOT_LANGUAGES.each do |language|
|
||||||
|
output_dir = File.join(output_base_dir, language)
|
||||||
|
ENV["SCREENSHOT_LANGUAGE"] = language
|
||||||
|
locale = screenshot_locale(language)
|
||||||
|
if locale
|
||||||
|
ENV["SCREENSHOT_LOCALE"] = locale
|
||||||
|
else
|
||||||
|
ENV.delete("SCREENSHOT_LOCALE")
|
||||||
|
end
|
||||||
|
reset_dir(output_dir)
|
||||||
|
screenshots_cache_candidates.each do |dir|
|
||||||
|
reset_dir(dir)
|
||||||
|
write_screenshot_language_files(dir, language, locale)
|
||||||
|
end
|
||||||
|
|
||||||
|
run_tests(
|
||||||
|
project: "sing-box.xcodeproj",
|
||||||
|
scheme: "SFM",
|
||||||
|
destination: "platform=macOS",
|
||||||
|
result_bundle: false,
|
||||||
|
reinstall_app: false,
|
||||||
|
app_identifier: "io.nekohasekai.sfavt",
|
||||||
|
only_testing: ["SFMUITests/SnapshotTests"],
|
||||||
|
number_of_retries: 0
|
||||||
|
)
|
||||||
|
|
||||||
|
copied = {}
|
||||||
|
screenshots_cache_candidates.each do |dir|
|
||||||
|
Dir.glob("#{dir}/Mac-*.png").each do |file|
|
||||||
|
basename = File.basename(file)
|
||||||
|
next if copied[basename]
|
||||||
|
FileUtils.cp(file, output_dir)
|
||||||
|
copied[basename] = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
padded_files = Dir.glob("#{output_dir}/*.png")
|
||||||
|
magick_bin = `which magick 2>/dev/null`.strip
|
||||||
|
magick_bin = `which convert 2>/dev/null`.strip if magick_bin.empty?
|
||||||
|
if magick_bin.empty?
|
||||||
|
UI.user_error!("ImageMagick not found. Install it (magick/convert) to add shadow.")
|
||||||
|
end
|
||||||
|
|
||||||
|
canvas_width = 2880
|
||||||
|
canvas_height = 1800
|
||||||
|
target_width = 2620
|
||||||
|
target_height = 1620
|
||||||
|
shadow_opacity = 26
|
||||||
|
|
||||||
|
padded_files.each do |file|
|
||||||
|
size_output = sh(
|
||||||
|
magick_bin,
|
||||||
|
file,
|
||||||
|
"-format",
|
||||||
|
"%w %h",
|
||||||
|
"info:",
|
||||||
|
log: false
|
||||||
|
).strip
|
||||||
|
opaque_output = sh(
|
||||||
|
magick_bin,
|
||||||
|
file,
|
||||||
|
"-format",
|
||||||
|
"%[opaque]",
|
||||||
|
"info:",
|
||||||
|
log: false
|
||||||
|
).strip
|
||||||
|
width, height = size_output.split.map(&:to_i)
|
||||||
|
has_transparency = opaque_output != "True"
|
||||||
|
tmp_file = "#{file}.tmp.png"
|
||||||
|
|
||||||
|
if has_transparency
|
||||||
|
shadow_sigma = 24
|
||||||
|
shadow_offset = 8
|
||||||
|
shadow = "#{shadow_opacity}x#{shadow_sigma}+0+#{shadow_offset}"
|
||||||
|
sh(
|
||||||
|
magick_bin,
|
||||||
|
file,
|
||||||
|
"-alpha", "set",
|
||||||
|
"-background", "none",
|
||||||
|
"-trim",
|
||||||
|
"+repage",
|
||||||
|
"-resize",
|
||||||
|
"#{target_width}x#{target_height}",
|
||||||
|
"(",
|
||||||
|
"+clone",
|
||||||
|
"-background",
|
||||||
|
"black",
|
||||||
|
"-shadow",
|
||||||
|
shadow,
|
||||||
|
")",
|
||||||
|
"+swap",
|
||||||
|
"-background",
|
||||||
|
"white",
|
||||||
|
"-layers",
|
||||||
|
"merge",
|
||||||
|
"+repage",
|
||||||
|
"-gravity",
|
||||||
|
"center",
|
||||||
|
"-extent",
|
||||||
|
"#{canvas_width}x#{canvas_height}",
|
||||||
|
tmp_file
|
||||||
|
)
|
||||||
|
else
|
||||||
|
scale = [
|
||||||
|
target_width.to_f / width,
|
||||||
|
target_height.to_f / height
|
||||||
|
].min
|
||||||
|
scaled_width = (width * scale).round
|
||||||
|
scaled_height = (height * scale).round
|
||||||
|
mask_width = [scaled_width - 1, 1].max
|
||||||
|
mask_height = [scaled_height - 1, 1].max
|
||||||
|
corner_radius = [(28 * scale).round, 28].max
|
||||||
|
shadow_sigma = [(16 * scale).round, 1].max
|
||||||
|
shadow_offset = [(5 * scale).round, 1].max
|
||||||
|
shadow = "#{shadow_opacity}x#{shadow_sigma}+0+#{shadow_offset}"
|
||||||
|
sh(
|
||||||
|
magick_bin,
|
||||||
|
file,
|
||||||
|
"-resize",
|
||||||
|
"#{scaled_width}x#{scaled_height}",
|
||||||
|
"-alpha",
|
||||||
|
"set",
|
||||||
|
"(",
|
||||||
|
"-size",
|
||||||
|
"#{scaled_width}x#{scaled_height}",
|
||||||
|
"xc:none",
|
||||||
|
"-fill",
|
||||||
|
"white",
|
||||||
|
"-draw",
|
||||||
|
"roundrectangle 0,0 #{mask_width},#{mask_height},#{corner_radius},#{corner_radius}",
|
||||||
|
")",
|
||||||
|
"-compose",
|
||||||
|
"DstIn",
|
||||||
|
"-composite",
|
||||||
|
"-compose",
|
||||||
|
"over",
|
||||||
|
"(",
|
||||||
|
"+clone",
|
||||||
|
"-background",
|
||||||
|
"black",
|
||||||
|
"-shadow",
|
||||||
|
shadow,
|
||||||
|
")",
|
||||||
|
"+swap",
|
||||||
|
"-background",
|
||||||
|
"white",
|
||||||
|
"-layers",
|
||||||
|
"merge",
|
||||||
|
"+repage",
|
||||||
|
"-gravity",
|
||||||
|
"center",
|
||||||
|
"-extent",
|
||||||
|
"#{canvas_width}x#{canvas_height}",
|
||||||
|
tmp_file
|
||||||
|
)
|
||||||
|
end
|
||||||
|
FileUtils.mv(tmp_file, file, force: true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
platform :tvos do
|
||||||
|
desc "Generate tvOS screenshots"
|
||||||
|
lane :screenshots do
|
||||||
|
output_base_dir = File.expand_path("../screenshots/tvos", __dir__)
|
||||||
|
screenshots_cache = File.expand_path("~/Library/Caches/tools.fastlane/screenshots")
|
||||||
|
|
||||||
|
reset_dir(output_base_dir)
|
||||||
|
|
||||||
|
SCREENSHOT_LANGUAGES.each do |language|
|
||||||
|
output_dir = File.join(output_base_dir, language)
|
||||||
|
ENV["SCREENSHOT_LANGUAGE"] = language
|
||||||
|
locale = screenshot_locale(language)
|
||||||
|
if locale
|
||||||
|
ENV["SCREENSHOT_LOCALE"] = locale
|
||||||
|
else
|
||||||
|
ENV.delete("SCREENSHOT_LOCALE")
|
||||||
|
end
|
||||||
|
reset_dir(output_dir)
|
||||||
|
reset_dir(screenshots_cache)
|
||||||
|
write_screenshot_language_files(screenshots_cache, language, locale)
|
||||||
|
|
||||||
|
run_tests(
|
||||||
|
project: "sing-box.xcodeproj",
|
||||||
|
scheme: "SFT",
|
||||||
|
devices: ["Apple TV 4K (3rd generation)"],
|
||||||
|
result_bundle: false,
|
||||||
|
reinstall_app: true,
|
||||||
|
app_identifier: "io.nekohasekai.sfavt",
|
||||||
|
only_testing: ["SFTUITests/SnapshotTests"],
|
||||||
|
number_of_retries: 0,
|
||||||
|
cloned_source_packages_path: "/tmp/fastlane_source_packages",
|
||||||
|
derived_data_path: "/tmp/fastlane_derived_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
Dir.glob("#{screenshots_cache}/*.png").each do |file|
|
||||||
|
FileUtils.cp(file, output_dir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
shutdown_simulator("Apple TV 4K (3rd generation)")
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
fastlane documentation
|
||||||
|
----
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
Make sure you have the latest version of the Xcode command line tools installed:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xcode-select --install
|
||||||
|
```
|
||||||
|
|
||||||
|
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||||
|
|
||||||
|
# Available Actions
|
||||||
|
|
||||||
|
## iOS
|
||||||
|
|
||||||
|
### ios screenshots
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane ios screenshots
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate iOS screenshots
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
## Mac
|
||||||
|
|
||||||
|
### mac screenshots
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane mac screenshots
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate macOS screenshots
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
## tvos
|
||||||
|
|
||||||
|
### tvos screenshots
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane tvos screenshots
|
||||||
|
```
|
||||||
|
|
||||||
|
Generate tvOS screenshots
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||||
|
|
||||||
|
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||||
|
|
||||||
|
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# iOS Screenshots
|
||||||
|
# App Store requirements:
|
||||||
|
# - iPhone 6.5" Display
|
||||||
|
# - iPad 13" Display
|
||||||
|
devices([
|
||||||
|
"iPhone 11 Pro Max",
|
||||||
|
"iPad Pro 13-inch (M4)"
|
||||||
|
])
|
||||||
|
|
||||||
|
languages([
|
||||||
|
"en-US",
|
||||||
|
"zh-Hans"
|
||||||
|
])
|
||||||
|
|
||||||
|
project("./sing-box.xcodeproj")
|
||||||
|
scheme("SFI")
|
||||||
|
app_identifier("io.nekohasekai.sfavt")
|
||||||
|
output_directory("./screenshots/ios")
|
||||||
|
clear_previous_screenshots(true)
|
||||||
|
reinstall_app(true)
|
||||||
|
skip_helper_version_check(true)
|
||||||
|
override_status_bar(true)
|
||||||
|
dark_mode(true)
|
||||||
|
number_of_retries(0)
|
||||||
|
stop_after_first_error(true)
|
||||||
|
skip_package_dependencies_resolution(true)
|
||||||
|
disable_package_automatic_updates(true)
|
||||||
|
cloned_source_packages_path("/tmp/fastlane_source_packages")
|
||||||
|
derived_data_path("/tmp/fastlane_derived_data")
|
||||||
|
localize_simulator(true)
|
||||||
@@ -115,6 +115,27 @@
|
|||||||
remoteGlobalIDString = 3A4EAD0F2A4FEAE6005435B3;
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user