feat(KAN-54): add local iOS and iPadOS Xcode builds
@@ -31,4 +31,12 @@ yarn-error.log
|
||||
|
||||
/native/node_modules
|
||||
/native/src-tauri/target
|
||||
/native/src-tauri/gen
|
||||
/native/src-tauri/gen/*
|
||||
!/native/src-tauri/gen/apple/
|
||||
/native/src-tauri/gen/apple/build/
|
||||
/native/src-tauri/gen/apple/Externals/
|
||||
/native/src-tauri/gen/apple/assets/
|
||||
/native/src-tauri/gen/apple/Pods/
|
||||
/native/src-tauri/gen/apple/ExportOptions.plist
|
||||
/native/src-tauri/gen/apple/**/*.xcuserstate
|
||||
/native/src-tauri/gen/apple/**/xcuserdata/
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
"build:macos": "tauri build --debug --bundles app",
|
||||
"test": "node --test tests/*.test.js && cargo test --manifest-path src-tauri/Cargo.toml --locked",
|
||||
"check": "cargo fmt --manifest-path src-tauri/Cargo.toml --check && cargo clippy --manifest-path src-tauri/Cargo.toml --locked --all-targets --all-features -- -D warnings",
|
||||
"smoke:macos": "cargo test --manifest-path src-tauri/Cargo.toml --locked --features smoke-test,custom-protocol --test macos_smoke"
|
||||
"smoke:macos": "cargo test --manifest-path src-tauri/Cargo.toml --locked --features smoke-test,custom-protocol --test macos_smoke",
|
||||
"dev:ios": "tauri ios dev",
|
||||
"build:ios:simulator": "rm -rf src-tauri/gen/apple/build/arm64-sim/KanbanGit.app && tauri ios build --debug --target aarch64-sim --no-sign --ci",
|
||||
"build:ios:device": "tauri ios build --debug --target aarch64 --export-method debugging --archive-only",
|
||||
"open:ios": "tauri ios dev --open --no-dev-server",
|
||||
"prepare:ios": "xcodegen generate --spec src-tauri/gen/apple/project.yml",
|
||||
"test:ios": "xcodebuild -project src-tauri/gen/apple/kanbangit-native.xcodeproj -scheme kanbangit-native_iOS -configuration debug -destination \"platform=iOS Simulator,id=${IOS_SIMULATOR_ID:?Set IOS_SIMULATOR_ID to an installed simulator UUID}\" -parallel-testing-enabled NO CODE_SIGNING_ALLOWED=NO test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "2.11.4"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
xcuserdata/
|
||||
build/
|
||||
Externals/
|
||||
|
After Width: | Height: | Size: 422 B |
|
After Width: | Height: | Size: 548 B |
|
After Width: | Height: | Size: 548 B |
|
After Width: | Height: | Size: 800 B |
|
After Width: | Height: | Size: 415 B |
|
After Width: | Height: | Size: 845 B |
|
After Width: | Height: | Size: 845 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 548 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 955 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-20x20@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-20x20@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29x29@2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-29x29@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-40x40@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-40x40@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60x60@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "AppIcon-60x60@3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-20x20@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "20x20",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-20x20@2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-29x29@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "29x29",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-29x29@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-40x40@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "40x40",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-40x40@2x-1.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-76x76@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"size" : "76x76",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-76x76@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "83.5x83.5",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "AppIcon-83.5x83.5@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "AppIcon-512@2x.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17150" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="Y6W-OH-hqX">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17122"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="s0d-6b-0kx">
|
||||
<objects>
|
||||
<viewController id="Y6W-OH-hqX" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="5EZ-qb-Rvc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<viewLayoutGuide key="safeArea" id="vDu-zF-Fre"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Ief-a0-LHa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,14 @@
|
||||
platform :ios, '17.0'
|
||||
|
||||
target 'kanbangit-native_iOS' do
|
||||
# Pods for kanbangit-native_iOS
|
||||
end
|
||||
|
||||
# Inherit the deployment target from the Podfile.
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings.delete 'IPHONEOS_DEPLOYMENT_TARGET'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,58 @@
|
||||
import Foundation
|
||||
|
||||
struct WorkspaceServer: Codable, Equatable {
|
||||
let url: URL
|
||||
let profile: UUID
|
||||
|
||||
static func normalize(_ input: String) -> URL? {
|
||||
let input = input.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard input.lowercased().hasPrefix("https://"),
|
||||
!input.contains("\\"),
|
||||
input.rangeOfCharacter(from: .controlCharacters) == nil,
|
||||
var components = URLComponents(string: input),
|
||||
components.scheme?.lowercased() == "https",
|
||||
let host = components.host, !host.isEmpty,
|
||||
components.user == nil, components.password == nil,
|
||||
components.query == nil, components.fragment == nil,
|
||||
components.port.map({ (1...65535).contains($0) }) ?? true else { return nil }
|
||||
components.scheme = "https"
|
||||
components.host = host.lowercased()
|
||||
if components.port == 443 { components.port = nil }
|
||||
if !components.path.hasSuffix("/") { components.path += "/" }
|
||||
return components.url
|
||||
}
|
||||
|
||||
func allows(_ destination: URL) -> Bool {
|
||||
destination.scheme?.lowercased() == "https"
|
||||
&& destination.host?.lowercased() == url.host?.lowercased()
|
||||
&& (destination.port ?? 443) == (url.port ?? 443)
|
||||
&& destination.user == nil && destination.password == nil
|
||||
}
|
||||
|
||||
static func selecting(_ url: URL, current: WorkspaceServer?) -> WorkspaceServer {
|
||||
current?.url == url ? current! : WorkspaceServer(url: url, profile: UUID())
|
||||
}
|
||||
}
|
||||
|
||||
struct WorkspaceConnection {
|
||||
private(set) var revision = 0
|
||||
private(set) var ready = false
|
||||
private(set) var connecting = false
|
||||
|
||||
mutating func begin() -> Int {
|
||||
revision += 1
|
||||
connecting = true
|
||||
return revision
|
||||
}
|
||||
|
||||
mutating func finish() {
|
||||
connecting = false
|
||||
ready = true
|
||||
}
|
||||
|
||||
mutating func fail(_ expectedRevision: Int) -> Bool {
|
||||
guard expectedRevision == revision, connecting else { return false }
|
||||
connecting = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
@_cdecl("install_ios_workspace")
|
||||
func installIOSWorkspace() {
|
||||
DispatchQueue.main.async {
|
||||
guard let root = UIApplication.shared.connectedScenes
|
||||
.compactMap({ $0 as? UIWindowScene }).flatMap(\.windows)
|
||||
.first(where: { $0.isKeyWindow })?.rootViewController
|
||||
?? UIApplication.shared.windows.first?.rootViewController else { return }
|
||||
guard !root.children.contains(where: { $0 is WorkspaceViewController }) else { return }
|
||||
let controller = WorkspaceViewController()
|
||||
root.view.subviews.forEach { $0.isHidden = true }
|
||||
root.addChild(controller)
|
||||
root.view.addSubview(controller.view)
|
||||
controller.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
controller.view.topAnchor.constraint(equalTo: root.view.topAnchor),
|
||||
controller.view.bottomAnchor.constraint(equalTo: root.view.bottomAnchor),
|
||||
controller.view.leadingAnchor.constraint(equalTo: root.view.leadingAnchor),
|
||||
controller.view.trailingAnchor.constraint(equalTo: root.view.trailingAnchor),
|
||||
])
|
||||
controller.didMove(toParent: root)
|
||||
}
|
||||
}
|
||||
|
||||
final class WorkspaceViewController: UIViewController, WKNavigationDelegate, WKUIDelegate, UITextFieldDelegate {
|
||||
private let defaults: UserDefaults
|
||||
private let serverKey = "ios.activeServer"
|
||||
private(set) var server: WorkspaceServer?
|
||||
private(set) var workspace: WKWebView?
|
||||
private var connection = WorkspaceConnection()
|
||||
private var pendingNavigation: WKNavigation?
|
||||
private var timeout: DispatchWorkItem?
|
||||
private let launcher = UIScrollView()
|
||||
private let stack = UIStackView()
|
||||
private let address = UITextField()
|
||||
private let status = UILabel()
|
||||
private let connectButton = UIButton(type: .system)
|
||||
private let resumeButton = UIButton(type: .system)
|
||||
private let toolbar = UIToolbar()
|
||||
private var backButton: UIBarButtonItem!
|
||||
private var appearance: UISegmentedControl!
|
||||
|
||||
init(defaults: UserDefaults = .standard) {
|
||||
self.defaults = defaults
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
||||
|
||||
private func text(_ key: String) -> String {
|
||||
NSLocalizedString(key, comment: "KanbanGit iOS workspace")
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
view.tintColor = UIColor(red: 0, green: 0.43, blue: 0.18, alpha: 1)
|
||||
configureLauncher()
|
||||
configureToolbar()
|
||||
if let data = defaults.data(forKey: serverKey) {
|
||||
if let saved = try? JSONDecoder().decode(WorkspaceServer.self, from: data),
|
||||
WorkspaceServer.normalize(saved.url.absoluteString) == saved.url {
|
||||
server = saved
|
||||
} else {
|
||||
status.text = text("storageError")
|
||||
}
|
||||
}
|
||||
address.text = server?.url.absoluteString ?? "https://kanbangit.de/"
|
||||
showSettings()
|
||||
if let server { open(server.url) }
|
||||
}
|
||||
|
||||
private func label(_ key: String, style: UIFont.TextStyle = .body) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text(key)
|
||||
label.font = .preferredFont(forTextStyle: style)
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}
|
||||
|
||||
private func configureLauncher() {
|
||||
launcher.accessibilityIdentifier = "serverSettings"
|
||||
launcher.keyboardDismissMode = .interactive
|
||||
launcher.alwaysBounceVertical = true
|
||||
view.addSubview(launcher)
|
||||
launcher.translatesAutoresizingMaskIntoConstraints = false
|
||||
NSLayoutConstraint.activate([
|
||||
launcher.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
launcher.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
|
||||
launcher.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
launcher.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
])
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 18
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
launcher.addSubview(stack)
|
||||
let width = stack.widthAnchor.constraint(equalTo: launcher.frameLayoutGuide.widthAnchor, constant: -40)
|
||||
width.priority = .defaultHigh
|
||||
NSLayoutConstraint.activate([
|
||||
stack.topAnchor.constraint(equalTo: launcher.contentLayoutGuide.topAnchor, constant: 32),
|
||||
stack.bottomAnchor.constraint(equalTo: launcher.contentLayoutGuide.bottomAnchor, constant: -32),
|
||||
stack.centerXAnchor.constraint(equalTo: launcher.frameLayoutGuide.centerXAnchor),
|
||||
stack.leadingAnchor.constraint(greaterThanOrEqualTo: launcher.contentLayoutGuide.leadingAnchor, constant: 20),
|
||||
stack.trailingAnchor.constraint(lessThanOrEqualTo: launcher.contentLayoutGuide.trailingAnchor, constant: -20),
|
||||
stack.widthAnchor.constraint(lessThanOrEqualToConstant: 480), width,
|
||||
])
|
||||
stack.addArrangedSubview(label("appName", style: .headline))
|
||||
stack.addArrangedSubview(label("title", style: .largeTitle))
|
||||
stack.addArrangedSubview(label("description"))
|
||||
stack.addArrangedSubview(label("serverLabel", style: .headline))
|
||||
address.borderStyle = .roundedRect
|
||||
address.keyboardType = .URL
|
||||
address.textContentType = .URL
|
||||
address.autocorrectionType = .no
|
||||
address.autocapitalizationType = .none
|
||||
address.clearButtonMode = .whileEditing
|
||||
address.returnKeyType = .go
|
||||
address.delegate = self
|
||||
address.accessibilityLabel = text("serverLabel")
|
||||
address.accessibilityIdentifier = "serverAddress"
|
||||
address.heightAnchor.constraint(greaterThanOrEqualToConstant: 48).isActive = true
|
||||
stack.addArrangedSubview(address)
|
||||
stack.addArrangedSubview(label("serverHelp", style: .footnote))
|
||||
connectButton.configuration = .filled()
|
||||
connectButton.setTitle(text("openWorkspace"), for: .normal)
|
||||
connectButton.accessibilityIdentifier = "openWorkspace"
|
||||
connectButton.addTarget(self, action: #selector(connect), for: .touchUpInside)
|
||||
stack.addArrangedSubview(connectButton)
|
||||
status.numberOfLines = 0
|
||||
status.accessibilityIdentifier = "connectionStatus"
|
||||
status.font = .preferredFont(forTextStyle: .body)
|
||||
status.adjustsFontForContentSizeCategory = true
|
||||
stack.addArrangedSubview(status)
|
||||
resumeButton.setTitle(text("resume"), for: .normal)
|
||||
resumeButton.accessibilityIdentifier = "resumeWorkspace"
|
||||
resumeButton.addTarget(self, action: #selector(showWorkspace), for: .touchUpInside)
|
||||
stack.addArrangedSubview(resumeButton)
|
||||
stack.addArrangedSubview(label("appearance", style: .headline))
|
||||
appearance = UISegmentedControl(items: [text("system"), text("light"), text("dark")])
|
||||
appearance.selectedSegmentIndex = min(2, max(0, defaults.integer(forKey: "ios.appearance")))
|
||||
appearance.accessibilityIdentifier = "appearance"
|
||||
appearance.addTarget(self, action: #selector(changeAppearance), for: .valueChanged)
|
||||
stack.addArrangedSubview(appearance)
|
||||
[connectButton, resumeButton, appearance].forEach {
|
||||
$0.heightAnchor.constraint(greaterThanOrEqualToConstant: 44).isActive = true
|
||||
}
|
||||
changeAppearance()
|
||||
}
|
||||
|
||||
private func configureToolbar() {
|
||||
toolbar.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(toolbar)
|
||||
backButton = UIBarButtonItem(title: text("back"), style: .plain, target: self, action: #selector(goBack))
|
||||
backButton.accessibilityIdentifier = "workspaceBack"
|
||||
let settings = UIBarButtonItem(title: text("serverSettings"), style: .plain, target: self, action: #selector(showSettings))
|
||||
settings.accessibilityIdentifier = "showServerSettings"
|
||||
toolbar.items = [backButton, UIBarButtonItem(systemItem: .flexibleSpace), settings]
|
||||
NSLayoutConstraint.activate([
|
||||
toolbar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
toolbar.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
toolbar.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@objc private func changeAppearance() {
|
||||
defaults.set(appearance.selectedSegmentIndex, forKey: "ios.appearance")
|
||||
overrideUserInterfaceStyle = [.unspecified, .light, .dark][appearance.selectedSegmentIndex]
|
||||
}
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool { connect(); return true }
|
||||
|
||||
@objc private func connect() {
|
||||
guard let url = WorkspaceServer.normalize(address.text ?? "") else {
|
||||
status.text = text("invalidServer")
|
||||
UIAccessibility.post(notification: .announcement, argument: status.text)
|
||||
return
|
||||
}
|
||||
if let server, server.url != url {
|
||||
let alert = UIAlertController(title: text("confirmSwitch"), message: text("switchWarning"), preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: text("cancel"), style: .cancel))
|
||||
alert.addAction(UIAlertAction(title: text("confirmSwitch"), style: .destructive) { [weak self] _ in self?.open(url) })
|
||||
present(alert, animated: true)
|
||||
} else { open(url) }
|
||||
}
|
||||
|
||||
func open(_ url: URL) {
|
||||
if server?.url == url, connection.ready { showWorkspace(); return }
|
||||
let selected = WorkspaceServer.selecting(url, current: server)
|
||||
guard let data = try? JSONEncoder().encode(selected) else { status.text = text("storageError"); return }
|
||||
defaults.set(data, forKey: serverKey)
|
||||
timeout?.cancel()
|
||||
workspace?.stopLoading()
|
||||
workspace?.navigationDelegate = nil
|
||||
workspace?.removeFromSuperview()
|
||||
connection = WorkspaceConnection()
|
||||
server = selected
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.websiteDataStore = WKWebsiteDataStore(forIdentifier: selected.profile)
|
||||
// Remote content receives a plain WKWebView, with no native message handlers or Tauri bridge.
|
||||
let webview = WKWebView(frame: .zero, configuration: configuration)
|
||||
webview.navigationDelegate = self
|
||||
webview.uiDelegate = self
|
||||
webview.allowsBackForwardNavigationGestures = true
|
||||
webview.accessibilityIdentifier = "workspace"
|
||||
webview.translatesAutoresizingMaskIntoConstraints = false
|
||||
webview.isHidden = true
|
||||
view.insertSubview(webview, belowSubview: launcher)
|
||||
NSLayoutConstraint.activate([
|
||||
webview.topAnchor.constraint(equalTo: toolbar.bottomAnchor),
|
||||
webview.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor),
|
||||
webview.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
||||
webview.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
||||
])
|
||||
workspace = webview
|
||||
view.endEditing(true)
|
||||
address.text = url.absoluteString
|
||||
beginNavigation()
|
||||
pendingNavigation = webview.load(URLRequest(url: url))
|
||||
}
|
||||
|
||||
private func beginNavigation() {
|
||||
timeout?.cancel()
|
||||
let revision = connection.begin()
|
||||
status.text = text("connecting")
|
||||
connectButton.isEnabled = false
|
||||
resumeButton.isHidden = !connection.ready
|
||||
let work = DispatchWorkItem { [weak self] in self?.fail(revision) }
|
||||
timeout = work
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 15, execute: work)
|
||||
}
|
||||
|
||||
private func fail(_ revision: Int) {
|
||||
guard connection.fail(revision) else { return }
|
||||
timeout?.cancel()
|
||||
status.text = text("connectionError")
|
||||
connectButton.isEnabled = true
|
||||
connectButton.setTitle(text("retry"), for: .normal)
|
||||
showSettings()
|
||||
}
|
||||
|
||||
@objc private func showSettings() {
|
||||
view.endEditing(true)
|
||||
launcher.isHidden = false
|
||||
toolbar.isHidden = true
|
||||
workspace?.isHidden = true
|
||||
resumeButton.isHidden = !connection.ready
|
||||
}
|
||||
|
||||
@objc private func showWorkspace() {
|
||||
guard connection.ready else { return }
|
||||
view.endEditing(true)
|
||||
launcher.isHidden = true
|
||||
toolbar.isHidden = false
|
||||
workspace?.isHidden = false
|
||||
backButton.isEnabled = workspace?.canGoBack == true
|
||||
}
|
||||
|
||||
@objc private func goBack() { if workspace?.canGoBack == true { workspace?.goBack() } }
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor action: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
guard webView === workspace, let url = action.request.url,
|
||||
server?.allows(url) == true else { decisionHandler(.cancel); return }
|
||||
if action.targetFrame == nil {
|
||||
decisionHandler(.cancel)
|
||||
webView.load(action.request)
|
||||
} else { decisionHandler(.allow) }
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor response: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
|
||||
guard webView === workspace, let url = response.response.url,
|
||||
server?.allows(url) == true else { decisionHandler(.cancel); return }
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
||||
guard webView === workspace else { return }
|
||||
pendingNavigation = navigation
|
||||
beginNavigation()
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
guard webView === workspace, navigation === pendingNavigation else { return }
|
||||
let firstLoad = !connection.ready
|
||||
timeout?.cancel()
|
||||
connection.finish()
|
||||
connectButton.isEnabled = true
|
||||
connectButton.setTitle(text("openWorkspace"), for: .normal)
|
||||
status.text = text("connected")
|
||||
resumeButton.isHidden = false
|
||||
backButton.isEnabled = webView.canGoBack
|
||||
if firstLoad { showWorkspace() }
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
navigationFailed(webView, navigation, error)
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
navigationFailed(webView, navigation, error)
|
||||
}
|
||||
|
||||
private func navigationFailed(_ webView: WKWebView, _ navigation: WKNavigation?, _ error: Error) {
|
||||
guard webView === workspace, navigation === pendingNavigation,
|
||||
(error as NSError).code != NSURLErrorCancelled else { return }
|
||||
fail(connection.revision)
|
||||
}
|
||||
|
||||
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
||||
guard webView === workspace else { return }
|
||||
connection = WorkspaceConnection()
|
||||
status.text = text("processTerminated")
|
||||
connectButton.isEnabled = true
|
||||
showSettings()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace ffi {
|
||||
extern "C" {
|
||||
void start_app();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
"appName" = "KanbanGit";
|
||||
"title" = "Choose your workspace.";
|
||||
"description" = "Connect to your KanbanGit installation over HTTPS.";
|
||||
"serverLabel" = "Server address";
|
||||
"serverHelp" = "Use the installation address without login details, a query or a fragment.";
|
||||
"openWorkspace" = "Open workspace";
|
||||
"retry" = "Try again";
|
||||
"resume" = "Return to workspace";
|
||||
"switchWarning" = "Switching servers closes the current workspace and discards unsaved inputs. You will sign in separately on the new server.";
|
||||
"confirmSwitch" = "Switch server";
|
||||
"cancel" = "Cancel";
|
||||
"connecting" = "Connecting to your server…";
|
||||
"connected" = "Your workspace is open. Use Server settings to change servers.";
|
||||
"connectionError" = "The server did not finish loading. Check your connection, server address and HTTPS certificate, then try again. An already open workspace and its inputs are kept.";
|
||||
"invalidServer" = "Enter a valid HTTPS installation address without login details, a query or a fragment.";
|
||||
"storageError" = "The saved server setting could not be read. Enter your server address again.";
|
||||
"processTerminated" = "iOS closed the workspace to free memory. Open it again to continue. Unsaved inputs may have been lost.";
|
||||
"appearance" = "Appearance";
|
||||
"system" = "System";
|
||||
"light" = "Light";
|
||||
"dark" = "Dark";
|
||||
"back" = "Back";
|
||||
"serverSettings" = "Server settings";
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "bindings/bindings.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
#include <cstdlib>
|
||||
#include <unistd.h>
|
||||
|
||||
extern "C" void install_ios_workspace(void);
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
@autoreleasepool {
|
||||
// Xcode mirrors os_log to stderr. Tauri 2.11 redirects stderr back to os_log,
|
||||
// which can fill its pipe and block UIKit/XCTest. Keep Xcode's original streams.
|
||||
const bool debuggerLogging = getenv("OS_ACTIVITY_DT_MODE") != nullptr
|
||||
|| getenv("XCTestConfigurationFilePath") != nullptr;
|
||||
__block int originalOut = debuggerLogging ? dup(STDOUT_FILENO) : -1;
|
||||
__block int originalError = debuggerLogging ? dup(STDERR_FILENO) : -1;
|
||||
[[NSNotificationCenter defaultCenter]
|
||||
addObserverForName:UIApplicationDidBecomeActiveNotification
|
||||
object:nil
|
||||
queue:NSOperationQueue.mainQueue
|
||||
usingBlock:^(NSNotification *notification) {
|
||||
if (originalOut >= 0) {
|
||||
dup2(originalOut, STDOUT_FILENO);
|
||||
close(originalOut);
|
||||
originalOut = -1;
|
||||
}
|
||||
if (originalError >= 0) {
|
||||
dup2(originalError, STDERR_FILENO);
|
||||
close(originalError);
|
||||
originalError = -1;
|
||||
}
|
||||
install_ios_workspace();
|
||||
}];
|
||||
ffi::start_app();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import XCTest
|
||||
import WebKit
|
||||
|
||||
final class WorkspaceTests: XCTestCase {
|
||||
func testServerValidationAndOriginBoundary() throws {
|
||||
let url = try XCTUnwrap(WorkspaceServer.normalize(" HTTPS://Example.com:443/kanban "))
|
||||
XCTAssertEqual(url.absoluteString, "https://example.com/kanban/")
|
||||
let server = WorkspaceServer(url: url, profile: UUID())
|
||||
for value in ["http://example.com/", "https://user:secret@example.com/", "https://example.com/?token=1", "https://example.com/#x", "https://exa\nmple.com", "https://example.com\\evil", "file:///etc/passwd", "javascript:alert(1)", "https://example.com:99999/"] {
|
||||
XCTAssertNil(WorkspaceServer.normalize(value), value)
|
||||
}
|
||||
for value in ["https://example.com/login", "https://example.com/two-factor-challenge", "https://example.com/projects/1?ticket=54#details"] {
|
||||
XCTAssertTrue(server.allows(try XCTUnwrap(URL(string: value))), value)
|
||||
}
|
||||
for value in ["http://example.com/", "https://example.com:8443/", "https://evil.example.com/", "https://user@example.com/", "file:///tmp/index.html", "tauri://localhost/action/open"] {
|
||||
XCTAssertFalse(server.allows(try XCTUnwrap(URL(string: value))), value)
|
||||
}
|
||||
}
|
||||
|
||||
func testServerChangesNeverReusePreviousProfile() throws {
|
||||
let firstURL = try XCTUnwrap(WorkspaceServer.normalize("https://example.com/"))
|
||||
let otherURL = try XCTUnwrap(WorkspaceServer.normalize("https://example.com/other/"))
|
||||
let first = WorkspaceServer.selecting(firstURL, current: nil)
|
||||
XCTAssertEqual(WorkspaceServer.selecting(firstURL, current: first), first)
|
||||
let second = WorkspaceServer.selecting(otherURL, current: first)
|
||||
XCTAssertNotEqual(second.profile, first.profile)
|
||||
XCTAssertNotEqual(WorkspaceServer.selecting(firstURL, current: second).profile, first.profile)
|
||||
XCTAssertEqual(try JSONDecoder().decode(WorkspaceServer.self, from: JSONEncoder().encode(first)), first)
|
||||
}
|
||||
|
||||
func testTimeoutDoesNotDiscardLoadedWorkspaceOrAffectLaterNavigation() {
|
||||
var state = WorkspaceConnection()
|
||||
let first = state.begin()
|
||||
let second = state.begin()
|
||||
XCTAssertFalse(state.fail(first))
|
||||
state.finish()
|
||||
XCTAssertFalse(state.fail(second))
|
||||
let third = state.begin()
|
||||
XCTAssertTrue(state.fail(third))
|
||||
XCTAssertTrue(state.ready)
|
||||
XCTAssertFalse(state.fail(third))
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testWKWebViewPersistsCookiesAcrossViewsAndIsolatesServerChanges() async throws {
|
||||
let profile = UUID()
|
||||
let first = WKWebViewConfiguration()
|
||||
first.websiteDataStore = WKWebsiteDataStore(forIdentifier: profile)
|
||||
let cookie = try XCTUnwrap(HTTPCookie(properties: [
|
||||
.domain: "example.com", .path: "/", .name: "laravel_session", .value: "test-session",
|
||||
.secure: "TRUE", .expires: Date().addingTimeInterval(3600),
|
||||
]))
|
||||
await first.websiteDataStore.httpCookieStore.setCookie(cookie)
|
||||
let restored = WKWebViewConfiguration()
|
||||
restored.websiteDataStore = WKWebsiteDataStore(forIdentifier: profile)
|
||||
let restoredCookies = await restored.websiteDataStore.httpCookieStore.allCookies()
|
||||
XCTAssertTrue(restoredCookies.contains { $0.name == "laravel_session" && $0.value == "test-session" })
|
||||
let other = WKWebsiteDataStore(forIdentifier: UUID())
|
||||
let otherCookies = await other.httpCookieStore.allCookies()
|
||||
XCTAssertTrue(otherCookies.isEmpty)
|
||||
await restored.websiteDataStore.httpCookieStore.deleteCookie(cookie)
|
||||
let loggedOut = await first.websiteDataStore.httpCookieStore.allCookies()
|
||||
XCTAssertFalse(loggedOut.contains { $0.name == "laravel_session" })
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testControllerRestoresServerAndCreatesIsolatedUnprivilegedWebView() throws {
|
||||
let name = "KAN54-tests-\(UUID())"
|
||||
let defaults = try XCTUnwrap(UserDefaults(suiteName: name))
|
||||
defer { defaults.removePersistentDomain(forName: name) }
|
||||
let controller = WorkspaceViewController(defaults: defaults)
|
||||
controller.loadViewIfNeeded()
|
||||
let url = try XCTUnwrap(WorkspaceServer.normalize("https://example.invalid/"))
|
||||
controller.open(url)
|
||||
let server = try XCTUnwrap(controller.server)
|
||||
XCTAssertEqual(controller.workspace?.configuration.websiteDataStore.identifier, server.profile)
|
||||
XCTAssertTrue(controller.workspace?.configuration.userContentController.userScripts.isEmpty == true)
|
||||
let restored = WorkspaceViewController(defaults: defaults)
|
||||
restored.loadViewIfNeeded()
|
||||
XCTAssertEqual(restored.server, server)
|
||||
let other = try XCTUnwrap(WorkspaceServer.normalize("https://other.invalid/"))
|
||||
restored.open(other)
|
||||
XCTAssertNotEqual(restored.server?.profile, server.profile)
|
||||
}
|
||||
@MainActor
|
||||
func testConnectionFailureKeepsLoadedFormAndAllowsResume() async throws {
|
||||
let name = "KAN54-form-\(UUID())"
|
||||
let defaults = try XCTUnwrap(UserDefaults(suiteName: name))
|
||||
defer { defaults.removePersistentDomain(forName: name) }
|
||||
let controller = WorkspaceViewController(defaults: defaults)
|
||||
controller.loadViewIfNeeded()
|
||||
let url = try XCTUnwrap(WorkspaceServer.normalize("https://example.invalid/"))
|
||||
controller.open(url)
|
||||
let webview = try XCTUnwrap(controller.workspace)
|
||||
webview.stopLoading()
|
||||
let navigation = try XCTUnwrap(webview.loadHTMLString("<html><body><input id='draft' value='unsaved ticket'></body></html>", baseURL: url))
|
||||
var loaded = false
|
||||
for _ in 0..<50 {
|
||||
if let value = try? await webview.evaluateJavaScript("document.getElementById('draft')?.value"),
|
||||
value as? String == "unsaved ticket" { loaded = true; break }
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
}
|
||||
XCTAssertTrue(loaded)
|
||||
controller.webView(webview, didFinish: navigation)
|
||||
controller.webView(webview, didStartProvisionalNavigation: navigation)
|
||||
controller.webView(webview, didFailProvisionalNavigation: navigation,
|
||||
withError: NSError(domain: NSURLErrorDomain, code: NSURLErrorNotConnectedToInternet))
|
||||
XCTAssertTrue(webview.isHidden)
|
||||
controller.open(url)
|
||||
XCTAssertTrue(controller.workspace === webview)
|
||||
XCTAssertFalse(webview.isHidden)
|
||||
let value = try await webview.evaluateJavaScript("document.getElementById('draft').value")
|
||||
XCTAssertEqual(value as? String, "unsaved ticket")
|
||||
let bridge = try await webview.evaluateJavaScript("typeof window.__TAURI_INTERNALS__")
|
||||
XCTAssertEqual(bridge as? String, "undefined")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import XCTest
|
||||
|
||||
final class WorkspaceUITests: XCTestCase {
|
||||
@MainActor
|
||||
private func reveal(_ element: XCUIElement, in scrollView: XCUIElement) {
|
||||
for _ in 0..<8 {
|
||||
if scrollView.frame.contains(CGPoint(x: element.frame.midX, y: element.frame.midY)), element.isHittable { return }
|
||||
let start = scrollView.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5))
|
||||
let direction: CGFloat = element.frame.midY > scrollView.frame.midY ? 0.2 : 0.8
|
||||
let end = scrollView.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: direction))
|
||||
start.press(forDuration: 0.05, thenDragTo: end)
|
||||
}
|
||||
XCTAssertTrue(element.isHittable)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testLauncherKeyboardValidationRotationAndAppearance() {
|
||||
continueAfterFailure = false
|
||||
XCUIDevice.shared.orientation = .portrait
|
||||
let app = XCUIApplication()
|
||||
app.launch()
|
||||
let settings = app.buttons["showServerSettings"]
|
||||
if settings.waitForExistence(timeout: 3) { settings.tap() }
|
||||
let address = app.textFields["serverAddress"]
|
||||
XCTAssertTrue(address.waitForExistence(timeout: 15))
|
||||
address.tap()
|
||||
let clear = address.buttons.firstMatch
|
||||
if clear.exists { clear.tap() }
|
||||
address.typeText("http://invalid.example")
|
||||
app.buttons["openWorkspace"].tap()
|
||||
XCTAssertTrue(app.staticTexts["connectionStatus"].label.contains("valid HTTPS"))
|
||||
XCUIDevice.shared.orientation = .landscapeLeft
|
||||
let launcher = app.scrollViews["serverSettings"]
|
||||
reveal(address, in: launcher)
|
||||
let open = app.buttons["openWorkspace"]
|
||||
reveal(open, in: launcher)
|
||||
let dark = app.segmentedControls["appearance"].buttons["Dark"]
|
||||
reveal(dark, in: launcher)
|
||||
dark.tap()
|
||||
XCTAssertTrue(dark.isSelected)
|
||||
app.segmentedControls["appearance"].buttons["System"].tap()
|
||||
XCUIDevice.shared.orientation = .portrait
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 77;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
009F8FEC2765B2F19DC12CA8 /* WorkspaceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08DF9B2F499E1EB89EB39BC5 /* WorkspaceTests.swift */; };
|
||||
0FAB3CC7BB466BFF871C0C30 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 31630170C358EA8657E34854 /* Assets.xcassets */; };
|
||||
205D0CBA33F7D3C59AAD5300 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63685B1189795CAEA85F7AB7 /* QuartzCore.framework */; };
|
||||
333E7761EA87AE29CE04F1AC /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = F46963153D20DB21D97843E9 /* main.mm */; };
|
||||
34C189B72A1B9EBE33D700D1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F63BC1BDE65F5AEBE5B13519 /* LaunchScreen.storyboard */; };
|
||||
5E764E27CA85A3746845F417 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8243D0C3821DAC731FECF105 /* UIKit.framework */; };
|
||||
6514B0DB3A5CCCDF7385AC18 /* WorkspaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE63F98563975EF4FF2A10FB /* WorkspaceViewController.swift */; };
|
||||
70EBA05ACBA5402617A3371C /* WorkspacePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554AB059B4228085924F4959 /* WorkspacePolicy.swift */; };
|
||||
7245AD23F39C4620BAA3AA92 /* WorkspaceUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED199829CC1B1598F58EDC9B /* WorkspaceUITests.swift */; };
|
||||
73954B7AADE8C37663BEF288 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CA65E67046326EA008175045 /* Metal.framework */; };
|
||||
98D5B23C9BE2F62C23DF397A /* WorkspacePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554AB059B4228085924F4959 /* WorkspacePolicy.swift */; };
|
||||
A46344B6DF1B3A0F2798B751 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = C8C02BC0E757897278EB954A /* Localizable.strings */; };
|
||||
B939982700E800053A9C7663 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE3B71153677F722CAC6DCF7 /* Security.framework */; };
|
||||
C362C102D4950A81D89BB8C8 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92EBA86D28BBFD1C822CC6F6 /* WebKit.framework */; };
|
||||
C44BDCDEA454DBD106D51C9C /* WorkspaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE63F98563975EF4FF2A10FB /* WorkspaceViewController.swift */; };
|
||||
CD64D99EBD87E6FFC0C9001C /* MetalKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D581BB90601D7A89F8F188E2 /* MetalKit.framework */; };
|
||||
D8772A5E508092D6F831058A /* libapp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 91991DFDB0C064B06DF854ED /* libapp.a */; };
|
||||
EB37EAF992C1A156795CAEC0 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CF713E7BFE3F6B5BAA3550E /* CoreGraphics.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
8665A25D07C5F1A295164797 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = AA6AD2A91A9E86D319D86D59 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 721CF221CEDDC28FF2C5EBD5;
|
||||
remoteInfo = "kanbangit-native_iOS";
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
01E10FB645809173AF1D4C2D /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = "<group>"; };
|
||||
05A753935B6CB0316DA56D96 /* desktop.rs */ = {isa = PBXFileReference; path = desktop.rs; sourceTree = "<group>"; };
|
||||
08DF9B2F499E1EB89EB39BC5 /* WorkspaceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceTests.swift; sourceTree = "<group>"; };
|
||||
0CF713E7BFE3F6B5BAA3550E /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
|
||||
23960010246A2A72E9940D3F /* kanbangit-native_iOS.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = "kanbangit-native_iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2B96972F58CC158AB3DFF94A /* lib.rs */ = {isa = PBXFileReference; path = lib.rs; sourceTree = "<group>"; };
|
||||
31630170C358EA8657E34854 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
4C76059D31C50B198779A5F7 /* en.lproj */ = {isa = PBXFileReference; path = en.lproj; sourceTree = "<group>"; };
|
||||
554AB059B4228085924F4959 /* WorkspacePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspacePolicy.swift; sourceTree = "<group>"; };
|
||||
63685B1189795CAEA85F7AB7 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
|
||||
7440C7F0426E6B3977DD1D94 /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = "<group>"; };
|
||||
8243D0C3821DAC731FECF105 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
|
||||
91991DFDB0C064B06DF854ED /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = "<group>"; };
|
||||
92EBA86D28BBFD1C822CC6F6 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
|
||||
946BA5CC3FD90AD90D101A36 /* kanbangit-native_iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "kanbangit-native_iOS.entitlements"; sourceTree = "<group>"; };
|
||||
9CAD0AE17A7966154CA29001 /* WorkspaceUITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = WorkspaceUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
A372EBDD353022BDB3E78972 /* WorkspaceTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = WorkspaceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AEBAD9ABE758897C50BC1F2E /* locale.rs */ = {isa = PBXFileReference; path = locale.rs; sourceTree = "<group>"; };
|
||||
CA65E67046326EA008175045 /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
|
||||
CAF08AD60D2F29A879CDAEBB /* ios.rs */ = {isa = PBXFileReference; path = ios.rs; sourceTree = "<group>"; };
|
||||
D13B8D854BEB7757AB95231B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
D581BB90601D7A89F8F188E2 /* MetalKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalKit.framework; path = System/Library/Frameworks/MetalKit.framework; sourceTree = SDKROOT; };
|
||||
DE3B71153677F722CAC6DCF7 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
||||
E8BA104C81071623E1FF367D /* server.rs */ = {isa = PBXFileReference; path = server.rs; sourceTree = "<group>"; };
|
||||
ED199829CC1B1598F58EDC9B /* WorkspaceUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceUITests.swift; sourceTree = "<group>"; };
|
||||
ED4469441C3E0F37B1123572 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
EE63F98563975EF4FF2A10FB /* WorkspaceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceViewController.swift; sourceTree = "<group>"; };
|
||||
F46963153D20DB21D97843E9 /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = "<group>"; };
|
||||
F63BC1BDE65F5AEBE5B13519 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
32E70C778E91CB2E8CD86715 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D8772A5E508092D6F831058A /* libapp.a in Frameworks */,
|
||||
EB37EAF992C1A156795CAEC0 /* CoreGraphics.framework in Frameworks */,
|
||||
73954B7AADE8C37663BEF288 /* Metal.framework in Frameworks */,
|
||||
CD64D99EBD87E6FFC0C9001C /* MetalKit.framework in Frameworks */,
|
||||
205D0CBA33F7D3C59AAD5300 /* QuartzCore.framework in Frameworks */,
|
||||
B939982700E800053A9C7663 /* Security.framework in Frameworks */,
|
||||
5E764E27CA85A3746845F417 /* UIKit.framework in Frameworks */,
|
||||
C362C102D4950A81D89BB8C8 /* WebKit.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
140A7DB7E2461E64B1FBFA70 /* Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
08DF9B2F499E1EB89EB39BC5 /* WorkspaceTests.swift */,
|
||||
);
|
||||
path = Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
157AF88201BDE4D99AA2E14C = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
31630170C358EA8657E34854 /* Assets.xcassets */,
|
||||
F63BC1BDE65F5AEBE5B13519 /* LaunchScreen.storyboard */,
|
||||
4058655E6F37D054E25D1F37 /* kanbangit-native_iOS */,
|
||||
8606F3EE5A736385061692EF /* Sources */,
|
||||
335B1262A9019C70C7F887A5 /* src */,
|
||||
140A7DB7E2461E64B1FBFA70 /* Tests */,
|
||||
3C9F31BB065F210267ECA390 /* UITests */,
|
||||
3028C3786B437115B6486F1C /* Frameworks */,
|
||||
59D2DE9FD94E6D2D64CB0117 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
2A11A26F0082A5075CD710C6 /* bindings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
01E10FB645809173AF1D4C2D /* bindings.h */,
|
||||
);
|
||||
path = bindings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3028C3786B437115B6486F1C /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CF713E7BFE3F6B5BAA3550E /* CoreGraphics.framework */,
|
||||
91991DFDB0C064B06DF854ED /* libapp.a */,
|
||||
CA65E67046326EA008175045 /* Metal.framework */,
|
||||
D581BB90601D7A89F8F188E2 /* MetalKit.framework */,
|
||||
63685B1189795CAEA85F7AB7 /* QuartzCore.framework */,
|
||||
DE3B71153677F722CAC6DCF7 /* Security.framework */,
|
||||
8243D0C3821DAC731FECF105 /* UIKit.framework */,
|
||||
92EBA86D28BBFD1C822CC6F6 /* WebKit.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
335B1262A9019C70C7F887A5 /* src */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
05A753935B6CB0316DA56D96 /* desktop.rs */,
|
||||
CAF08AD60D2F29A879CDAEBB /* ios.rs */,
|
||||
2B96972F58CC158AB3DFF94A /* lib.rs */,
|
||||
AEBAD9ABE758897C50BC1F2E /* locale.rs */,
|
||||
7440C7F0426E6B3977DD1D94 /* main.rs */,
|
||||
E8BA104C81071623E1FF367D /* server.rs */,
|
||||
);
|
||||
name = src;
|
||||
path = ../../src;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
3C9F31BB065F210267ECA390 /* UITests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
ED199829CC1B1598F58EDC9B /* WorkspaceUITests.swift */,
|
||||
);
|
||||
path = UITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
4058655E6F37D054E25D1F37 /* kanbangit-native_iOS */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D13B8D854BEB7757AB95231B /* Info.plist */,
|
||||
946BA5CC3FD90AD90D101A36 /* kanbangit-native_iOS.entitlements */,
|
||||
);
|
||||
path = "kanbangit-native_iOS";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
59D2DE9FD94E6D2D64CB0117 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
23960010246A2A72E9940D3F /* kanbangit-native_iOS.app */,
|
||||
A372EBDD353022BDB3E78972 /* WorkspaceTests.xctest */,
|
||||
9CAD0AE17A7966154CA29001 /* WorkspaceUITests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8606F3EE5A736385061692EF /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
89AA023B8C28CB860513C685 /* kanbangit-native */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
89AA023B8C28CB860513C685 /* kanbangit-native */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4C76059D31C50B198779A5F7 /* en.lproj */,
|
||||
F46963153D20DB21D97843E9 /* main.mm */,
|
||||
554AB059B4228085924F4959 /* WorkspacePolicy.swift */,
|
||||
EE63F98563975EF4FF2A10FB /* WorkspaceViewController.swift */,
|
||||
2A11A26F0082A5075CD710C6 /* bindings */,
|
||||
C8C02BC0E757897278EB954A /* Localizable.strings */,
|
||||
);
|
||||
path = "kanbangit-native";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
721CF221CEDDC28FF2C5EBD5 /* kanbangit-native_iOS */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = F0C7EA9610FB69A5EA27B950 /* Build configuration list for PBXNativeTarget "kanbangit-native_iOS" */;
|
||||
buildPhases = (
|
||||
C74F06A7ECCC2C6C79C74A2D /* Build Rust Code */,
|
||||
F39C45598E4345705F3820FA /* Sources */,
|
||||
4A4C7FC1B57785DDD6B301FB /* Resources */,
|
||||
32E70C778E91CB2E8CD86715 /* Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = "kanbangit-native_iOS";
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = "kanbangit-native_iOS";
|
||||
productReference = 23960010246A2A72E9940D3F /* kanbangit-native_iOS.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
988CFF304CB81C8F3A4D6612 /* WorkspaceUITests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = E6F418AEF58E5F3D739C68F1 /* Build configuration list for PBXNativeTarget "WorkspaceUITests" */;
|
||||
buildPhases = (
|
||||
0258B26407CD2D8C61079F84 /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
5658F3B2915DC3A46531EDCE /* PBXTargetDependency */,
|
||||
);
|
||||
name = WorkspaceUITests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = WorkspaceUITests;
|
||||
productReference = 9CAD0AE17A7966154CA29001 /* WorkspaceUITests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.ui-testing";
|
||||
};
|
||||
CBC4561F8018D37DD49A0598 /* WorkspaceTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 26F675D59EF454AE0D263F35 /* Build configuration list for PBXNativeTarget "WorkspaceTests" */;
|
||||
buildPhases = (
|
||||
2A0467365B852D44D1A9FD5B /* Sources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = WorkspaceTests;
|
||||
packageProductDependencies = (
|
||||
);
|
||||
productName = WorkspaceTests;
|
||||
productReference = A372EBDD353022BDB3E78972 /* WorkspaceTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
AA6AD2A91A9E86D319D86D59 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1430;
|
||||
TargetAttributes = {
|
||||
721CF221CEDDC28FF2C5EBD5 = {
|
||||
DevelopmentTeam = "";
|
||||
ProvisioningStyle = Automatic;
|
||||
};
|
||||
988CFF304CB81C8F3A4D6612 = {
|
||||
TestTargetID = 721CF221CEDDC28FF2C5EBD5;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = A9CA551DD3F3EBCCF8C41CC1 /* Build configuration list for PBXProject "kanbangit-native" */;
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = 157AF88201BDE4D99AA2E14C;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 59D2DE9FD94E6D2D64CB0117 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
721CF221CEDDC28FF2C5EBD5 /* kanbangit-native_iOS */,
|
||||
CBC4561F8018D37DD49A0598 /* WorkspaceTests */,
|
||||
988CFF304CB81C8F3A4D6612 /* WorkspaceUITests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
4A4C7FC1B57785DDD6B301FB /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0FAB3CC7BB466BFF871C0C30 /* Assets.xcassets in Resources */,
|
||||
34C189B72A1B9EBE33D700D1 /* LaunchScreen.storyboard in Resources */,
|
||||
A46344B6DF1B3A0F2798B751 /* Localizable.strings in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
C74F06A7ECCC2C6C79C74A2D /* Build Rust Code */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "Build Rust Code";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a",
|
||||
"$(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "export PATH=\"$(dirname \"$(rustup which cargo)\"):$PATH\"\nexport RUSTC=\"$(rustup which rustc)\"\ncd \"$SRCROOT/../../..\"\n./node_modules/.bin/tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
0258B26407CD2D8C61079F84 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
7245AD23F39C4620BAA3AA92 /* WorkspaceUITests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
2A0467365B852D44D1A9FD5B /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
98D5B23C9BE2F62C23DF397A /* WorkspacePolicy.swift in Sources */,
|
||||
009F8FEC2765B2F19DC12CA8 /* WorkspaceTests.swift in Sources */,
|
||||
6514B0DB3A5CCCDF7385AC18 /* WorkspaceViewController.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
F39C45598E4345705F3820FA /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
70EBA05ACBA5402617A3371C /* WorkspacePolicy.swift in Sources */,
|
||||
C44BDCDEA454DBD106D51C9C /* WorkspaceViewController.swift in Sources */,
|
||||
333E7761EA87AE29CE04F1AC /* main.mm in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
5658F3B2915DC3A46531EDCE /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 721CF221CEDDC28FF2C5EBD5 /* kanbangit-native_iOS */;
|
||||
targetProxy = 8665A25D07C5F1A295164797 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
C8C02BC0E757897278EB954A /* Localizable.strings */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
ED4469441C3E0F37B1123572 /* en */,
|
||||
);
|
||||
name = Localizable.strings;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
0C46162024E99BEFF872CABF /* release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ARCHS = (
|
||||
arm64,
|
||||
);
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "kanbangit-native_iOS/kanbangit-native_iOS.entitlements";
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\".\"",
|
||||
);
|
||||
INFOPLIST_FILE = "kanbangit-native_iOS/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.desktop;
|
||||
PRODUCT_MODULE_NAME = KanbanGitNative;
|
||||
PRODUCT_NAME = "KanbanGit";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALID_ARCHS = arm64;
|
||||
};
|
||||
name = release;
|
||||
};
|
||||
390A27695F28BF3AA9A049C9 /* debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"$(inherited)",
|
||||
"DEBUG=1",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = debug;
|
||||
};
|
||||
5110A47C3B60906E18EE09D2 /* debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.uitests;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = "kanbangit-native_iOS";
|
||||
};
|
||||
name = debug;
|
||||
};
|
||||
C81211E40BBB158A79AEBE3D /* release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
};
|
||||
name = release;
|
||||
};
|
||||
D41150B2CAE4512D04C26759 /* debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ARCHS = (
|
||||
arm64,
|
||||
);
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = "kanbangit-native_iOS/kanbangit-native_iOS.entitlements";
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"\".\"",
|
||||
);
|
||||
INFOPLIST_FILE = "kanbangit-native_iOS/Info.plist";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.desktop;
|
||||
PRODUCT_MODULE_NAME = KanbanGitNative;
|
||||
PRODUCT_NAME = "KanbanGit";
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
VALID_ARCHS = arm64;
|
||||
};
|
||||
name = debug;
|
||||
};
|
||||
EF104E75BD04AFEF735BAA52 /* release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.uitests;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_TARGET_NAME = "kanbangit-native_iOS";
|
||||
};
|
||||
name = release;
|
||||
};
|
||||
FB6E824120F9ED3CFE30E713 /* debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.tests;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = debug;
|
||||
};
|
||||
FF1553DD6765EEA42FF50CDD /* release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.tests;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
26F675D59EF454AE0D263F35 /* Build configuration list for PBXNativeTarget "WorkspaceTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
FB6E824120F9ED3CFE30E713 /* debug */,
|
||||
FF1553DD6765EEA42FF50CDD /* release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = debug;
|
||||
};
|
||||
A9CA551DD3F3EBCCF8C41CC1 /* Build configuration list for PBXProject "kanbangit-native" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
390A27695F28BF3AA9A049C9 /* debug */,
|
||||
C81211E40BBB158A79AEBE3D /* release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = debug;
|
||||
};
|
||||
E6F418AEF58E5F3D739C68F1 /* Build configuration list for PBXNativeTarget "WorkspaceUITests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
5110A47C3B60906E18EE09D2 /* debug */,
|
||||
EF104E75BD04AFEF735BAA52 /* release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = debug;
|
||||
};
|
||||
F0C7EA9610FB69A5EA27B950 /* Build configuration list for PBXNativeTarget "kanbangit-native_iOS" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
D41150B2CAE4512D04C26759 /* debug */,
|
||||
0C46162024E99BEFF872CABF /* release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = debug;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = AA6AD2A91A9E86D319D86D59 /* Project object */;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?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>BuildSystemType</key>
|
||||
<string>Original</string>
|
||||
<key>DisableBuildSystemDeprecationDiagnostic</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "721CF221CEDDC28FF2C5EBD5"
|
||||
BuildableName = "KanbanGit.app"
|
||||
BlueprintName = "kanbangit-native_iOS"
|
||||
ReferencedContainer = "container:kanbangit-native.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "721CF221CEDDC28FF2C5EBD5"
|
||||
BuildableName = "KanbanGit.app"
|
||||
BlueprintName = "kanbangit-native_iOS"
|
||||
ReferencedContainer = "container:kanbangit-native.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_BACKTRACE"
|
||||
value = "full"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_LOG"
|
||||
value = "info"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "CBC4561F8018D37DD49A0598"
|
||||
BuildableName = "WorkspaceTests.xctest"
|
||||
BlueprintName = "WorkspaceTests"
|
||||
ReferencedContainer = "container:kanbangit-native.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "988CFF304CB81C8F3A4D6612"
|
||||
BuildableName = "WorkspaceUITests.xctest"
|
||||
BlueprintName = "WorkspaceUITests"
|
||||
ReferencedContainer = "container:kanbangit-native.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "721CF221CEDDC28FF2C5EBD5"
|
||||
BuildableName = "KanbanGit.app"
|
||||
BlueprintName = "kanbangit-native_iOS"
|
||||
ReferencedContainer = "container:kanbangit-native.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_BACKTRACE"
|
||||
value = "full"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_LOG"
|
||||
value = "info"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "release"
|
||||
shouldUseLaunchSchemeArgsEnv = "NO"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "721CF221CEDDC28FF2C5EBD5"
|
||||
BuildableName = "KanbanGit.app"
|
||||
BlueprintName = "kanbangit-native_iOS"
|
||||
ReferencedContainer = "container:kanbangit-native.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<EnvironmentVariables>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_BACKTRACE"
|
||||
value = "full"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "RUST_LOG"
|
||||
value = "info"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?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>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
<string>metal</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?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/>
|
||||
</plist>
|
||||
@@ -0,0 +1,122 @@
|
||||
name: kanbangit-native
|
||||
options:
|
||||
bundleIdPrefix: de.kanbangit.desktop
|
||||
deploymentTarget:
|
||||
iOS: 17.0
|
||||
fileGroups: [../../src]
|
||||
configs:
|
||||
debug: debug
|
||||
release: release
|
||||
settingGroups:
|
||||
app:
|
||||
base:
|
||||
PRODUCT_NAME: KanbanGit
|
||||
SWIFT_VERSION: "5.0"
|
||||
PRODUCT_MODULE_NAME: KanbanGitNative
|
||||
TARGETED_DEVICE_FAMILY: "1,2"
|
||||
CODE_SIGN_STYLE: Automatic
|
||||
DEVELOPMENT_TEAM: ""
|
||||
ENABLE_USER_SCRIPT_SANDBOXING: false
|
||||
PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.desktop
|
||||
targetTemplates:
|
||||
app:
|
||||
type: application
|
||||
sources:
|
||||
- path: Sources
|
||||
scheme:
|
||||
environmentVariables:
|
||||
RUST_BACKTRACE: full
|
||||
RUST_LOG: info
|
||||
settings:
|
||||
groups: [app]
|
||||
targets:
|
||||
kanbangit-native_iOS:
|
||||
type: application
|
||||
platform: iOS
|
||||
sources:
|
||||
- path: Sources
|
||||
- path: Assets.xcassets
|
||||
- path: kanbangit-native_iOS
|
||||
- path: LaunchScreen.storyboard
|
||||
info:
|
||||
path: kanbangit-native_iOS/Info.plist
|
||||
properties:
|
||||
LSRequiresIPhoneOS: true
|
||||
UILaunchStoryboardName: LaunchScreen
|
||||
UIRequiredDeviceCapabilities: [arm64, metal]
|
||||
UISupportedInterfaceOrientations:
|
||||
- UIInterfaceOrientationPortrait
|
||||
- UIInterfaceOrientationLandscapeLeft
|
||||
- UIInterfaceOrientationLandscapeRight
|
||||
UISupportedInterfaceOrientations~ipad:
|
||||
- UIInterfaceOrientationPortrait
|
||||
- UIInterfaceOrientationPortraitUpsideDown
|
||||
- UIInterfaceOrientationLandscapeLeft
|
||||
- UIInterfaceOrientationLandscapeRight
|
||||
CFBundleShortVersionString: 0.1.0
|
||||
CFBundleVersion: "0.1.0"
|
||||
entitlements:
|
||||
path: kanbangit-native_iOS/kanbangit-native_iOS.entitlements
|
||||
scheme:
|
||||
environmentVariables:
|
||||
RUST_BACKTRACE: full
|
||||
RUST_LOG: info
|
||||
testTargets:
|
||||
- WorkspaceTests
|
||||
- WorkspaceUITests
|
||||
settings:
|
||||
base:
|
||||
ENABLE_BITCODE: false
|
||||
ARCHS: [arm64]
|
||||
VALID_ARCHS: arm64
|
||||
LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||
LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true
|
||||
EXCLUDED_ARCHS[sdk=iphoneos*]: x86_64
|
||||
groups: [app]
|
||||
dependencies:
|
||||
- framework: libapp.a
|
||||
embed: false
|
||||
- sdk: CoreGraphics.framework
|
||||
- sdk: Metal.framework
|
||||
- sdk: MetalKit.framework
|
||||
- sdk: QuartzCore.framework
|
||||
- sdk: Security.framework
|
||||
- sdk: UIKit.framework
|
||||
- sdk: WebKit.framework
|
||||
preBuildScripts:
|
||||
- script: |
|
||||
export PATH="$(dirname "$(rustup which cargo)"):$PATH"
|
||||
export RUSTC="$(rustup which rustc)"
|
||||
cd "$SRCROOT/../../.."
|
||||
./node_modules/.bin/tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}
|
||||
name: Build Rust Code
|
||||
basedOnDependencyAnalysis: false
|
||||
outputFiles:
|
||||
- $(SRCROOT)/Externals/x86_64/${CONFIGURATION}/libapp.a
|
||||
- $(SRCROOT)/Externals/arm64/${CONFIGURATION}/libapp.a
|
||||
WorkspaceTests:
|
||||
type: bundle.unit-test
|
||||
platform: iOS
|
||||
sources:
|
||||
- path: Tests
|
||||
- path: Sources/kanbangit-native/WorkspacePolicy.swift
|
||||
- path: Sources/kanbangit-native/WorkspaceViewController.swift
|
||||
- path: Sources/kanbangit-native/en.lproj
|
||||
settings:
|
||||
base:
|
||||
GENERATE_INFOPLIST_FILE: YES
|
||||
PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.tests
|
||||
SWIFT_VERSION: "5.0"
|
||||
WorkspaceUITests:
|
||||
type: bundle.ui-testing
|
||||
platform: iOS
|
||||
sources: [UITests]
|
||||
dependencies:
|
||||
- target: kanbangit-native_iOS
|
||||
settings:
|
||||
base:
|
||||
GENERATE_INFOPLIST_FILE: YES
|
||||
PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.uitests
|
||||
SWIFT_VERSION: "5.0"
|
||||
TEST_TARGET_NAME: kanbangit-native_iOS
|
||||
@@ -0,0 +1,456 @@
|
||||
use crate::locale::en;
|
||||
use crate::server::{validate_url, Server, DEFAULT_SERVER};
|
||||
use std::{path::PathBuf, sync::Mutex, time::Duration};
|
||||
use tauri::{
|
||||
menu::{MenuBuilder, SubmenuBuilder},
|
||||
webview::{NewWindowResponse, PageLoadEvent},
|
||||
AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowEvent,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Connection {
|
||||
server: Option<Server>,
|
||||
label: Option<String>,
|
||||
revision: u64,
|
||||
ready: bool,
|
||||
phase: &'static str,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
fn begin_navigation(&mut self) -> u64 {
|
||||
self.revision += 1;
|
||||
self.phase = "connecting";
|
||||
self.revision
|
||||
}
|
||||
|
||||
fn timeout(&mut self, revision: u64) -> bool {
|
||||
if self.revision != revision || self.phase != "connecting" {
|
||||
return false;
|
||||
}
|
||||
self.phase = "connectionError";
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn allows_server_navigation(server: &Url, destination: &Url) -> bool {
|
||||
destination.scheme() == "https"
|
||||
&& destination.origin() == server.origin()
|
||||
&& destination.username().is_empty()
|
||||
&& destination.password().is_none()
|
||||
}
|
||||
|
||||
fn allows_launcher_navigation(destination: &Url) -> bool {
|
||||
destination.scheme() == "tauri"
|
||||
&& destination.host_str() == Some("localhost")
|
||||
&& destination.port().is_none()
|
||||
&& destination.username().is_empty()
|
||||
&& destination.password().is_none()
|
||||
}
|
||||
|
||||
fn configuration_path(app: &AppHandle) -> tauri::Result<PathBuf> {
|
||||
Ok(app.path().app_config_dir()?.join("active-server"))
|
||||
}
|
||||
|
||||
fn update_launcher(app: &AppHandle) {
|
||||
let state = app.state::<Mutex<Connection>>();
|
||||
let state = state.lock().unwrap();
|
||||
let mut message = Url::parse("tauri://localhost/").unwrap();
|
||||
message
|
||||
.query_pairs_mut()
|
||||
.append_pair(
|
||||
"server",
|
||||
state
|
||||
.server
|
||||
.as_ref()
|
||||
.map_or(DEFAULT_SERVER, |server| server.url.as_str()),
|
||||
)
|
||||
.append_pair("phase", state.phase)
|
||||
.append_pair("ready", if state.ready { "true" } else { "false" });
|
||||
if let Some(window) = app.get_webview_window("launcher") {
|
||||
let _ = window.eval(format!(
|
||||
"window.updateLauncher?.('{}')",
|
||||
message.query().unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn show_launcher(app: &AppHandle) {
|
||||
update_launcher(app);
|
||||
if let Some(window) = app.get_webview_window("launcher") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_navigation(app: &AppHandle, label: &str) {
|
||||
let state = app.state::<Mutex<Connection>>();
|
||||
let revision = {
|
||||
let mut state = state.lock().unwrap();
|
||||
if state.label.as_deref() != Some(label) {
|
||||
return;
|
||||
}
|
||||
state.begin_navigation()
|
||||
};
|
||||
update_launcher(app);
|
||||
let handle = app.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(Duration::from_secs(15));
|
||||
let state = handle.state::<Mutex<Connection>>();
|
||||
let timed_out = state.lock().unwrap().timeout(revision);
|
||||
if timed_out {
|
||||
show_launcher(&handle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn open_workspace(app: &AppHandle, input: &str) -> Result<(), &'static str> {
|
||||
let url = validate_url(input)?;
|
||||
let state = app.state::<Mutex<Connection>>();
|
||||
let (server, old_label, label) = {
|
||||
let mut state = state.lock().unwrap();
|
||||
let same_server = state
|
||||
.server
|
||||
.as_ref()
|
||||
.is_some_and(|server| server.url == url);
|
||||
if same_server && state.ready {
|
||||
if let Some(window) = state
|
||||
.label
|
||||
.as_ref()
|
||||
.and_then(|label| app.get_webview_window(label))
|
||||
{
|
||||
window.show().map_err(|_| "connectionError")?;
|
||||
window.set_focus().map_err(|_| "connectionError")?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let server = if same_server {
|
||||
state.server.clone().unwrap()
|
||||
} else {
|
||||
Server::new(url).map_err(|_| "storageError")?
|
||||
};
|
||||
server
|
||||
.save(&configuration_path(app).map_err(|_| "storageError")?)
|
||||
.map_err(|_| "storageError")?;
|
||||
let old_label = state.label.take();
|
||||
state.server = Some(server.clone());
|
||||
state.ready = false;
|
||||
state.revision += 1;
|
||||
let label = format!("workspace-{}", state.revision);
|
||||
state.label = Some(label.clone());
|
||||
(server, old_label, label)
|
||||
};
|
||||
if let Some(window) = old_label.and_then(|label| app.get_webview_window(&label)) {
|
||||
window.close().map_err(|_| "connectionError")?;
|
||||
}
|
||||
begin_navigation(app, &label);
|
||||
let handle = app.clone();
|
||||
let navigation_label = label.clone();
|
||||
let server_url = server.url.clone();
|
||||
let window = WebviewWindowBuilder::new(app, &label, WebviewUrl::External(server.url))
|
||||
.title(en::APP_NAME)
|
||||
.inner_size(1280.0, 850.0)
|
||||
.min_inner_size(375.0, 600.0)
|
||||
.center()
|
||||
.visible(false)
|
||||
.data_store_identifier(server.profile)
|
||||
.on_navigation(move |destination| {
|
||||
if !allows_server_navigation(&server_url, destination) {
|
||||
return false;
|
||||
}
|
||||
begin_navigation(&handle, &navigation_label);
|
||||
true
|
||||
})
|
||||
.on_new_window(|_, _| NewWindowResponse::Deny)
|
||||
.on_page_load(|window, payload| {
|
||||
if payload.event() != PageLoadEvent::Finished {
|
||||
return;
|
||||
}
|
||||
let app = window.app_handle();
|
||||
let state = app.state::<Mutex<Connection>>();
|
||||
let should_show = {
|
||||
let mut state = state.lock().unwrap();
|
||||
if state.label.as_deref() != Some(window.label()) {
|
||||
return;
|
||||
}
|
||||
let first_load = !state.ready;
|
||||
state.ready = true;
|
||||
state.phase = "connected";
|
||||
first_load
|
||||
};
|
||||
update_launcher(app);
|
||||
if should_show {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
if let Some(launcher) = app.get_webview_window("launcher") {
|
||||
let _ = launcher.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
.map_err(|_| "connectionError")?;
|
||||
let handle = app.clone();
|
||||
window.on_window_event(move |event| {
|
||||
if matches!(event, WindowEvent::Destroyed) {
|
||||
let state = handle.state::<Mutex<Connection>>();
|
||||
let current = {
|
||||
let mut state = state.lock().unwrap();
|
||||
if state.label.as_deref() == Some(&label) {
|
||||
state.label = None;
|
||||
state.ready = false;
|
||||
state.revision += 1;
|
||||
state.phase = "idle";
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
if current {
|
||||
show_launcher(&handle);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_launcher_action(app: &AppHandle, destination: &Url) {
|
||||
let result = match destination.path() {
|
||||
"/action/open" => {
|
||||
let input = destination
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "server")
|
||||
.map(|(_, value)| value.into_owned())
|
||||
.unwrap_or_default();
|
||||
open_workspace(app, &input)
|
||||
}
|
||||
"/action/resume" => {
|
||||
let label = app
|
||||
.state::<Mutex<Connection>>()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.label
|
||||
.clone();
|
||||
if let Some(window) = label.and_then(|label| app.get_webview_window(&label)) {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
};
|
||||
if let Err(error) = result {
|
||||
app.state::<Mutex<Connection>>().lock().unwrap().phase = error;
|
||||
show_launcher(app);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context<R: tauri::Runtime>() -> tauri::Context<R> {
|
||||
tauri::generate_context!()
|
||||
}
|
||||
|
||||
pub fn builder() -> tauri::Builder<tauri::Wry> {
|
||||
tauri::Builder::default().setup(|app| {
|
||||
let (server, phase) = match Server::load(&configuration_path(app.handle())?) {
|
||||
Ok(server) => (server, "idle"),
|
||||
Err(_) => (None, "storageError"),
|
||||
};
|
||||
app.manage(Mutex::new(Connection {
|
||||
server,
|
||||
phase,
|
||||
..Default::default()
|
||||
}));
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(
|
||||
&SubmenuBuilder::new(app, en::APP_NAME)
|
||||
.text("server-settings", en::SERVER_SETTINGS)
|
||||
.separator()
|
||||
.quit()
|
||||
.build()?,
|
||||
)
|
||||
.item(
|
||||
&SubmenuBuilder::new(app, en::EDIT)
|
||||
.undo()
|
||||
.redo()
|
||||
.separator()
|
||||
.cut()
|
||||
.copy()
|
||||
.paste()
|
||||
.select_all()
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
app.set_menu(menu)?;
|
||||
app.on_menu_event(|app, event| {
|
||||
if event.id().as_ref() == "server-settings" {
|
||||
show_launcher(app);
|
||||
}
|
||||
});
|
||||
let handle = app.handle().clone();
|
||||
let launcher =
|
||||
WebviewWindowBuilder::new(app, "launcher", WebviewUrl::App("index.html".into()))
|
||||
.title(en::APP_NAME)
|
||||
.inner_size(560.0, 720.0)
|
||||
.min_inner_size(375.0, 640.0)
|
||||
.center()
|
||||
.on_navigation(move |destination| {
|
||||
if !allows_launcher_navigation(destination) {
|
||||
return false;
|
||||
}
|
||||
if destination.path().starts_with("/action/") {
|
||||
let handle = handle.clone();
|
||||
let destination = destination.clone();
|
||||
let _ = handle.clone().run_on_main_thread(move || {
|
||||
handle_launcher_action(&handle, &destination)
|
||||
});
|
||||
return false;
|
||||
}
|
||||
matches!(destination.path(), "" | "/" | "/index.html")
|
||||
})
|
||||
.on_new_window(|_, _| NewWindowResponse::Deny)
|
||||
.on_page_load(|window, payload| {
|
||||
if payload.event() == PageLoadEvent::Finished {
|
||||
let app = window.app_handle().clone();
|
||||
update_launcher(&app);
|
||||
std::thread::spawn(move || {
|
||||
let handle = app.clone();
|
||||
let _ = app.run_on_main_thread(move || {
|
||||
let server = handle
|
||||
.state::<Mutex<Connection>>()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.server
|
||||
.clone();
|
||||
if let Some(server) = server {
|
||||
if let Err(error) = open_workspace(&handle, server.url.as_str())
|
||||
{
|
||||
handle.state::<Mutex<Connection>>().lock().unwrap().phase =
|
||||
error;
|
||||
show_launcher(&handle);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.build()?;
|
||||
let window = launcher.clone();
|
||||
launcher.on_window_event(move |event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let _ = window.hide();
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn navigation_is_limited_to_the_selected_https_origin() {
|
||||
let server = validate_url("https://example.com:8443/app/").unwrap();
|
||||
for path in [
|
||||
"/login",
|
||||
"/two-factor-challenge",
|
||||
"/projects/1?ticket=52#details",
|
||||
] {
|
||||
assert!(allows_server_navigation(
|
||||
&server,
|
||||
&server.join(path).unwrap()
|
||||
));
|
||||
}
|
||||
for destination in [
|
||||
"http://example.com:8443/",
|
||||
"https://example.com/",
|
||||
"https://other.example.com:8443/",
|
||||
"https://user:secret@example.com:8443/",
|
||||
"tauri://localhost/action/open?server=https://evil.test",
|
||||
"file:///etc/passwd",
|
||||
"data:text/html,hello",
|
||||
"javascript:alert(1)",
|
||||
] {
|
||||
assert!(!allows_server_navigation(
|
||||
&server,
|
||||
&Url::parse(destination).unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launcher_actions_are_restricted_to_the_bundled_origin() {
|
||||
assert!(allows_launcher_navigation(
|
||||
&Url::parse("tauri://localhost/action/open").unwrap()
|
||||
));
|
||||
for input in [
|
||||
DEFAULT_SERVER,
|
||||
"https://localhost/",
|
||||
"tauri://other/index.html",
|
||||
"tauri://user@localhost/index.html",
|
||||
] {
|
||||
assert!(!allows_launcher_navigation(&Url::parse(input).unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_timeouts_do_not_replace_newer_connections_or_loaded_forms() {
|
||||
let mut state = Connection::default();
|
||||
let first = state.begin_navigation();
|
||||
let second = state.begin_navigation();
|
||||
assert!(!state.timeout(first));
|
||||
state.ready = true;
|
||||
assert!(state.timeout(second));
|
||||
assert!(state.ready);
|
||||
assert!(!state.timeout(second));
|
||||
let third = state.begin_navigation();
|
||||
state.phase = "connected";
|
||||
assert!(!state.timeout(third));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_pages_cannot_invoke_native_commands() {
|
||||
let app = tauri::test::mock_builder().build(context()).unwrap();
|
||||
let window = WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"workspace-1",
|
||||
WebviewUrl::External(Url::parse(DEFAULT_SERVER).unwrap()),
|
||||
)
|
||||
.build()
|
||||
.unwrap();
|
||||
for command in [
|
||||
"plugin:window|close",
|
||||
"plugin:webview|create_webview_window",
|
||||
"plugin:path|resolve_directory",
|
||||
] {
|
||||
let response = tauri::test::get_ipc_response(
|
||||
&window,
|
||||
tauri::webview::InvokeRequest {
|
||||
cmd: command.into(),
|
||||
callback: tauri::ipc::CallbackFn(0),
|
||||
error: tauri::ipc::CallbackFn(1),
|
||||
url: Url::parse(DEFAULT_SERVER).unwrap(),
|
||||
body: tauri::ipc::InvokeBody::default(),
|
||||
headers: Default::default(),
|
||||
invoke_key: tauri::test::INVOKE_KEY.into(),
|
||||
},
|
||||
);
|
||||
assert!(response
|
||||
.unwrap_err()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("not allowed"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packaged_app_exposes_no_native_capabilities_or_global_api() {
|
||||
let config: serde_json::Value =
|
||||
serde_json::from_str(include_str!("../tauri.conf.json")).unwrap();
|
||||
assert_eq!(config["bundle"]["macOS"]["minimumSystemVersion"], "14.0");
|
||||
assert_eq!(config["app"]["withGlobalTauri"], false);
|
||||
assert_eq!(
|
||||
config["app"]["security"]["capabilities"],
|
||||
serde_json::json!([])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
use tauri::{WebviewUrl, WebviewWindowBuilder};
|
||||
|
||||
pub fn context<R: tauri::Runtime>() -> tauri::Context<R> {
|
||||
tauri::generate_context!()
|
||||
}
|
||||
|
||||
pub fn builder() -> tauri::Builder<tauri::Wry> {
|
||||
tauri::Builder::default().setup(|app| {
|
||||
WebviewWindowBuilder::new(app, "host", WebviewUrl::App("index.html".into()))
|
||||
.on_navigation(|_| false)
|
||||
.build()?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -1,464 +1,18 @@
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
mod desktop;
|
||||
#[cfg(target_os = "ios")]
|
||||
mod ios;
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
mod locale;
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
mod server;
|
||||
|
||||
use locale::en;
|
||||
use server::{validate_url, Server, DEFAULT_SERVER};
|
||||
use std::{path::PathBuf, sync::Mutex, time::Duration};
|
||||
use tauri::{
|
||||
menu::{MenuBuilder, SubmenuBuilder},
|
||||
webview::{NewWindowResponse, PageLoadEvent},
|
||||
AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowEvent,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Connection {
|
||||
server: Option<Server>,
|
||||
label: Option<String>,
|
||||
revision: u64,
|
||||
ready: bool,
|
||||
phase: &'static str,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
fn begin_navigation(&mut self) -> u64 {
|
||||
self.revision += 1;
|
||||
self.phase = "connecting";
|
||||
self.revision
|
||||
}
|
||||
|
||||
fn timeout(&mut self, revision: u64) -> bool {
|
||||
if self.revision != revision || self.phase != "connecting" {
|
||||
return false;
|
||||
}
|
||||
self.phase = "connectionError";
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn allows_server_navigation(server: &Url, destination: &Url) -> bool {
|
||||
destination.scheme() == "https"
|
||||
&& destination.origin() == server.origin()
|
||||
&& destination.username().is_empty()
|
||||
&& destination.password().is_none()
|
||||
}
|
||||
|
||||
fn allows_launcher_navigation(destination: &Url) -> bool {
|
||||
destination.scheme() == "tauri"
|
||||
&& destination.host_str() == Some("localhost")
|
||||
&& destination.port().is_none()
|
||||
&& destination.username().is_empty()
|
||||
&& destination.password().is_none()
|
||||
}
|
||||
|
||||
fn configuration_path(app: &AppHandle) -> tauri::Result<PathBuf> {
|
||||
Ok(app.path().app_config_dir()?.join("active-server"))
|
||||
}
|
||||
|
||||
fn update_launcher(app: &AppHandle) {
|
||||
let state = app.state::<Mutex<Connection>>();
|
||||
let state = state.lock().unwrap();
|
||||
let mut message = Url::parse("tauri://localhost/").unwrap();
|
||||
message
|
||||
.query_pairs_mut()
|
||||
.append_pair(
|
||||
"server",
|
||||
state
|
||||
.server
|
||||
.as_ref()
|
||||
.map_or(DEFAULT_SERVER, |server| server.url.as_str()),
|
||||
)
|
||||
.append_pair("phase", state.phase)
|
||||
.append_pair("ready", if state.ready { "true" } else { "false" });
|
||||
if let Some(window) = app.get_webview_window("launcher") {
|
||||
let _ = window.eval(format!(
|
||||
"window.updateLauncher?.('{}')",
|
||||
message.query().unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn show_launcher(app: &AppHandle) {
|
||||
update_launcher(app);
|
||||
if let Some(window) = app.get_webview_window("launcher") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
|
||||
fn begin_navigation(app: &AppHandle, label: &str) {
|
||||
let state = app.state::<Mutex<Connection>>();
|
||||
let revision = {
|
||||
let mut state = state.lock().unwrap();
|
||||
if state.label.as_deref() != Some(label) {
|
||||
return;
|
||||
}
|
||||
state.begin_navigation()
|
||||
};
|
||||
update_launcher(app);
|
||||
let handle = app.clone();
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(Duration::from_secs(15));
|
||||
let state = handle.state::<Mutex<Connection>>();
|
||||
let timed_out = state.lock().unwrap().timeout(revision);
|
||||
if timed_out {
|
||||
show_launcher(&handle);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn open_workspace(app: &AppHandle, input: &str) -> Result<(), &'static str> {
|
||||
let url = validate_url(input)?;
|
||||
let state = app.state::<Mutex<Connection>>();
|
||||
let (server, old_label, label) = {
|
||||
let mut state = state.lock().unwrap();
|
||||
let same_server = state
|
||||
.server
|
||||
.as_ref()
|
||||
.is_some_and(|server| server.url == url);
|
||||
if same_server && state.ready {
|
||||
if let Some(window) = state
|
||||
.label
|
||||
.as_ref()
|
||||
.and_then(|label| app.get_webview_window(label))
|
||||
{
|
||||
window.show().map_err(|_| "connectionError")?;
|
||||
window.set_focus().map_err(|_| "connectionError")?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let server = if same_server {
|
||||
state.server.clone().unwrap()
|
||||
} else {
|
||||
Server::new(url).map_err(|_| "storageError")?
|
||||
};
|
||||
server
|
||||
.save(&configuration_path(app).map_err(|_| "storageError")?)
|
||||
.map_err(|_| "storageError")?;
|
||||
let old_label = state.label.take();
|
||||
state.server = Some(server.clone());
|
||||
state.ready = false;
|
||||
state.revision += 1;
|
||||
let label = format!("workspace-{}", state.revision);
|
||||
state.label = Some(label.clone());
|
||||
(server, old_label, label)
|
||||
};
|
||||
if let Some(window) = old_label.and_then(|label| app.get_webview_window(&label)) {
|
||||
window.close().map_err(|_| "connectionError")?;
|
||||
}
|
||||
begin_navigation(app, &label);
|
||||
let handle = app.clone();
|
||||
let navigation_label = label.clone();
|
||||
let server_url = server.url.clone();
|
||||
let window = WebviewWindowBuilder::new(app, &label, WebviewUrl::External(server.url))
|
||||
.title(en::APP_NAME)
|
||||
.inner_size(1280.0, 850.0)
|
||||
.min_inner_size(375.0, 600.0)
|
||||
.center()
|
||||
.visible(false)
|
||||
.data_store_identifier(server.profile)
|
||||
.on_navigation(move |destination| {
|
||||
if !allows_server_navigation(&server_url, destination) {
|
||||
return false;
|
||||
}
|
||||
begin_navigation(&handle, &navigation_label);
|
||||
true
|
||||
})
|
||||
.on_new_window(|_, _| NewWindowResponse::Deny)
|
||||
.on_page_load(|window, payload| {
|
||||
if payload.event() != PageLoadEvent::Finished {
|
||||
return;
|
||||
}
|
||||
let app = window.app_handle();
|
||||
let state = app.state::<Mutex<Connection>>();
|
||||
let should_show = {
|
||||
let mut state = state.lock().unwrap();
|
||||
if state.label.as_deref() != Some(window.label()) {
|
||||
return;
|
||||
}
|
||||
let first_load = !state.ready;
|
||||
state.ready = true;
|
||||
state.phase = "connected";
|
||||
first_load
|
||||
};
|
||||
update_launcher(app);
|
||||
if should_show {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
if let Some(launcher) = app.get_webview_window("launcher") {
|
||||
let _ = launcher.hide();
|
||||
}
|
||||
}
|
||||
})
|
||||
.build()
|
||||
.map_err(|_| "connectionError")?;
|
||||
let handle = app.clone();
|
||||
window.on_window_event(move |event| {
|
||||
if matches!(event, WindowEvent::Destroyed) {
|
||||
let state = handle.state::<Mutex<Connection>>();
|
||||
let current = {
|
||||
let mut state = state.lock().unwrap();
|
||||
if state.label.as_deref() == Some(&label) {
|
||||
state.label = None;
|
||||
state.ready = false;
|
||||
state.revision += 1;
|
||||
state.phase = "idle";
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
if current {
|
||||
show_launcher(&handle);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_launcher_action(app: &AppHandle, destination: &Url) {
|
||||
let result = match destination.path() {
|
||||
"/action/open" => {
|
||||
let input = destination
|
||||
.query_pairs()
|
||||
.find(|(key, _)| key == "server")
|
||||
.map(|(_, value)| value.into_owned())
|
||||
.unwrap_or_default();
|
||||
open_workspace(app, &input)
|
||||
}
|
||||
"/action/resume" => {
|
||||
let label = app
|
||||
.state::<Mutex<Connection>>()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.label
|
||||
.clone();
|
||||
if let Some(window) = label.and_then(|label| app.get_webview_window(&label)) {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
_ => Ok(()),
|
||||
};
|
||||
if let Err(error) = result {
|
||||
app.state::<Mutex<Connection>>().lock().unwrap().phase = error;
|
||||
show_launcher(app);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context<R: tauri::Runtime>() -> tauri::Context<R> {
|
||||
tauri::generate_context!()
|
||||
}
|
||||
|
||||
pub fn builder() -> tauri::Builder<tauri::Wry> {
|
||||
tauri::Builder::default().setup(|app| {
|
||||
let (server, phase) = match Server::load(&configuration_path(app.handle())?) {
|
||||
Ok(server) => (server, "idle"),
|
||||
Err(_) => (None, "storageError"),
|
||||
};
|
||||
app.manage(Mutex::new(Connection {
|
||||
server,
|
||||
phase,
|
||||
..Default::default()
|
||||
}));
|
||||
let menu = MenuBuilder::new(app)
|
||||
.item(
|
||||
&SubmenuBuilder::new(app, en::APP_NAME)
|
||||
.text("server-settings", en::SERVER_SETTINGS)
|
||||
.separator()
|
||||
.quit()
|
||||
.build()?,
|
||||
)
|
||||
.item(
|
||||
&SubmenuBuilder::new(app, en::EDIT)
|
||||
.undo()
|
||||
.redo()
|
||||
.separator()
|
||||
.cut()
|
||||
.copy()
|
||||
.paste()
|
||||
.select_all()
|
||||
.build()?,
|
||||
)
|
||||
.build()?;
|
||||
app.set_menu(menu)?;
|
||||
app.on_menu_event(|app, event| {
|
||||
if event.id().as_ref() == "server-settings" {
|
||||
show_launcher(app);
|
||||
}
|
||||
});
|
||||
let handle = app.handle().clone();
|
||||
let launcher =
|
||||
WebviewWindowBuilder::new(app, "launcher", WebviewUrl::App("index.html".into()))
|
||||
.title(en::APP_NAME)
|
||||
.inner_size(560.0, 720.0)
|
||||
.min_inner_size(375.0, 640.0)
|
||||
.center()
|
||||
.on_navigation(move |destination| {
|
||||
if !allows_launcher_navigation(destination) {
|
||||
return false;
|
||||
}
|
||||
if destination.path().starts_with("/action/") {
|
||||
let handle = handle.clone();
|
||||
let destination = destination.clone();
|
||||
let _ = handle.clone().run_on_main_thread(move || {
|
||||
handle_launcher_action(&handle, &destination)
|
||||
});
|
||||
return false;
|
||||
}
|
||||
matches!(destination.path(), "" | "/" | "/index.html")
|
||||
})
|
||||
.on_new_window(|_, _| NewWindowResponse::Deny)
|
||||
.on_page_load(|window, payload| {
|
||||
if payload.event() == PageLoadEvent::Finished {
|
||||
let app = window.app_handle().clone();
|
||||
update_launcher(&app);
|
||||
std::thread::spawn(move || {
|
||||
let handle = app.clone();
|
||||
let _ = app.run_on_main_thread(move || {
|
||||
let server = handle
|
||||
.state::<Mutex<Connection>>()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.server
|
||||
.clone();
|
||||
if let Some(server) = server {
|
||||
if let Err(error) = open_workspace(&handle, server.url.as_str())
|
||||
{
|
||||
handle.state::<Mutex<Connection>>().lock().unwrap().phase =
|
||||
error;
|
||||
show_launcher(&handle);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.build()?;
|
||||
let window = launcher.clone();
|
||||
launcher.on_window_event(move |event| {
|
||||
if let WindowEvent::CloseRequested { api, .. } = event {
|
||||
api.prevent_close();
|
||||
let _ = window.hide();
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
pub use desktop::{builder, context};
|
||||
#[cfg(target_os = "ios")]
|
||||
pub use ios::{builder, context};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
builder().run(context()).expect("Could not start KanbanGit");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn navigation_is_limited_to_the_selected_https_origin() {
|
||||
let server = validate_url("https://example.com:8443/app/").unwrap();
|
||||
for path in [
|
||||
"/login",
|
||||
"/two-factor-challenge",
|
||||
"/projects/1?ticket=52#details",
|
||||
] {
|
||||
assert!(allows_server_navigation(
|
||||
&server,
|
||||
&server.join(path).unwrap()
|
||||
));
|
||||
}
|
||||
for destination in [
|
||||
"http://example.com:8443/",
|
||||
"https://example.com/",
|
||||
"https://other.example.com:8443/",
|
||||
"https://user:secret@example.com:8443/",
|
||||
"tauri://localhost/action/open?server=https://evil.test",
|
||||
"file:///etc/passwd",
|
||||
"data:text/html,hello",
|
||||
"javascript:alert(1)",
|
||||
] {
|
||||
assert!(!allows_server_navigation(
|
||||
&server,
|
||||
&Url::parse(destination).unwrap()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launcher_actions_are_restricted_to_the_bundled_origin() {
|
||||
assert!(allows_launcher_navigation(
|
||||
&Url::parse("tauri://localhost/action/open").unwrap()
|
||||
));
|
||||
for input in [
|
||||
DEFAULT_SERVER,
|
||||
"https://localhost/",
|
||||
"tauri://other/index.html",
|
||||
"tauri://user@localhost/index.html",
|
||||
] {
|
||||
assert!(!allows_launcher_navigation(&Url::parse(input).unwrap()));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_timeouts_do_not_replace_newer_connections_or_loaded_forms() {
|
||||
let mut state = Connection::default();
|
||||
let first = state.begin_navigation();
|
||||
let second = state.begin_navigation();
|
||||
assert!(!state.timeout(first));
|
||||
state.ready = true;
|
||||
assert!(state.timeout(second));
|
||||
assert!(state.ready);
|
||||
assert!(!state.timeout(second));
|
||||
let third = state.begin_navigation();
|
||||
state.phase = "connected";
|
||||
assert!(!state.timeout(third));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_pages_cannot_invoke_native_commands() {
|
||||
let app = tauri::test::mock_builder().build(context()).unwrap();
|
||||
let window = WebviewWindowBuilder::new(
|
||||
&app,
|
||||
"workspace-1",
|
||||
WebviewUrl::External(Url::parse(DEFAULT_SERVER).unwrap()),
|
||||
)
|
||||
.build()
|
||||
.unwrap();
|
||||
for command in [
|
||||
"plugin:window|close",
|
||||
"plugin:webview|create_webview_window",
|
||||
"plugin:path|resolve_directory",
|
||||
] {
|
||||
let response = tauri::test::get_ipc_response(
|
||||
&window,
|
||||
tauri::webview::InvokeRequest {
|
||||
cmd: command.into(),
|
||||
callback: tauri::ipc::CallbackFn(0),
|
||||
error: tauri::ipc::CallbackFn(1),
|
||||
url: Url::parse(DEFAULT_SERVER).unwrap(),
|
||||
body: tauri::ipc::InvokeBody::default(),
|
||||
headers: Default::default(),
|
||||
invoke_key: tauri::test::INVOKE_KEY.into(),
|
||||
},
|
||||
);
|
||||
assert!(response
|
||||
.unwrap_err()
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("not allowed"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packaged_app_exposes_no_native_capabilities_or_global_api() {
|
||||
let config: serde_json::Value =
|
||||
serde_json::from_str(include_str!("../tauri.conf.json")).unwrap();
|
||||
assert_eq!(config["bundle"]["macOS"]["minimumSystemVersion"], "14.0");
|
||||
assert_eq!(config["app"]["withGlobalTauri"], false);
|
||||
assert_eq!(
|
||||
config["app"]["security"]["capabilities"],
|
||||
serde_json::json!([])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"bundle": {
|
||||
"iOS": { "minimumSystemVersion": "17.0" }
|
||||
}
|
||||
}
|
||||