diff --git a/.gitignore b/.gitignore
index 2219081..d5149e8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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/
diff --git a/native/package.json b/native/package.json
index e755dfb..2bbd856 100644
--- a/native/package.json
+++ b/native/package.json
@@ -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"
diff --git a/native/src-tauri/gen/apple/.gitignore b/native/src-tauri/gen/apple/.gitignore
new file mode 100644
index 0000000..6726e2f
--- /dev/null
+++ b/native/src-tauri/gen/apple/.gitignore
@@ -0,0 +1,3 @@
+xcuserdata/
+build/
+Externals/
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png
new file mode 100644
index 0000000..3ba6c85
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png
new file mode 100644
index 0000000..e3f34d5
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png
new file mode 100644
index 0000000..e3f34d5
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png
new file mode 100644
index 0000000..8bcff76
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png
new file mode 100644
index 0000000..1ed85c0
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png
new file mode 100644
index 0000000..b9cd314
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png
new file mode 100644
index 0000000..b9cd314
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png
new file mode 100644
index 0000000..6b56941
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png
new file mode 100644
index 0000000..e3f34d5
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png
new file mode 100644
index 0000000..51b1c2a
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png
new file mode 100644
index 0000000..51b1c2a
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png
new file mode 100644
index 0000000..30decc9
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png
new file mode 100644
index 0000000..beb2f50
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png
new file mode 100644
index 0000000..30decc9
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png
new file mode 100644
index 0000000..cb0c0b2
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png
new file mode 100644
index 0000000..cdd488d
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png
new file mode 100644
index 0000000..56e188e
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png
new file mode 100644
index 0000000..283715a
Binary files /dev/null and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/Contents.json b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..90eea7e
--- /dev/null
+++ b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/Contents.json b/native/src-tauri/gen/apple/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..da4a164
--- /dev/null
+++ b/native/src-tauri/gen/apple/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
\ No newline at end of file
diff --git a/native/src-tauri/gen/apple/LaunchScreen.storyboard b/native/src-tauri/gen/apple/LaunchScreen.storyboard
new file mode 100644
index 0000000..81b5f90
--- /dev/null
+++ b/native/src-tauri/gen/apple/LaunchScreen.storyboard
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/native/src-tauri/gen/apple/Podfile b/native/src-tauri/gen/apple/Podfile
new file mode 100644
index 0000000..a517b3c
--- /dev/null
+++ b/native/src-tauri/gen/apple/Podfile
@@ -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
diff --git a/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspacePolicy.swift b/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspacePolicy.swift
new file mode 100644
index 0000000..730d682
--- /dev/null
+++ b/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspacePolicy.swift
@@ -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
+ }
+}
diff --git a/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspaceViewController.swift b/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspaceViewController.swift
new file mode 100644
index 0000000..e8d90a1
--- /dev/null
+++ b/native/src-tauri/gen/apple/Sources/kanbangit-native/WorkspaceViewController.swift
@@ -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()
+ }
+}
diff --git a/native/src-tauri/gen/apple/Sources/kanbangit-native/bindings/bindings.h b/native/src-tauri/gen/apple/Sources/kanbangit-native/bindings/bindings.h
new file mode 100644
index 0000000..6c2bd87
--- /dev/null
+++ b/native/src-tauri/gen/apple/Sources/kanbangit-native/bindings/bindings.h
@@ -0,0 +1,7 @@
+#pragma once
+
+namespace ffi {
+ extern "C" {
+ void start_app();
+ }
+}
diff --git a/native/src-tauri/gen/apple/Sources/kanbangit-native/en.lproj/Localizable.strings b/native/src-tauri/gen/apple/Sources/kanbangit-native/en.lproj/Localizable.strings
new file mode 100644
index 0000000..ddac212
--- /dev/null
+++ b/native/src-tauri/gen/apple/Sources/kanbangit-native/en.lproj/Localizable.strings
@@ -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";
diff --git a/native/src-tauri/gen/apple/Sources/kanbangit-native/main.mm b/native/src-tauri/gen/apple/Sources/kanbangit-native/main.mm
new file mode 100644
index 0000000..e0f0e74
--- /dev/null
+++ b/native/src-tauri/gen/apple/Sources/kanbangit-native/main.mm
@@ -0,0 +1,36 @@
+#include "bindings/bindings.h"
+#import
+#include
+#include
+
+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;
+ }
+}
diff --git a/native/src-tauri/gen/apple/Tests/WorkspaceTests.swift b/native/src-tauri/gen/apple/Tests/WorkspaceTests.swift
new file mode 100644
index 0000000..95d8602
--- /dev/null
+++ b/native/src-tauri/gen/apple/Tests/WorkspaceTests.swift
@@ -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("", 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")
+ }
+
+}
diff --git a/native/src-tauri/gen/apple/UITests/WorkspaceUITests.swift b/native/src-tauri/gen/apple/UITests/WorkspaceUITests.swift
new file mode 100644
index 0000000..ace0c39
--- /dev/null
+++ b/native/src-tauri/gen/apple/UITests/WorkspaceUITests.swift
@@ -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
+ }
+}
diff --git a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..03f3633
--- /dev/null
+++ b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj
@@ -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 = ""; };
+ 05A753935B6CB0316DA56D96 /* desktop.rs */ = {isa = PBXFileReference; path = desktop.rs; sourceTree = ""; };
+ 08DF9B2F499E1EB89EB39BC5 /* WorkspaceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceTests.swift; sourceTree = ""; };
+ 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 = ""; };
+ 31630170C358EA8657E34854 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 4C76059D31C50B198779A5F7 /* en.lproj */ = {isa = PBXFileReference; path = en.lproj; sourceTree = ""; };
+ 554AB059B4228085924F4959 /* WorkspacePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspacePolicy.swift; sourceTree = ""; };
+ 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 = ""; };
+ 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 = ""; };
+ 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 = ""; };
+ 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 = ""; };
+ 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 = ""; };
+ D13B8D854BEB7757AB95231B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
+ 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 = ""; };
+ ED199829CC1B1598F58EDC9B /* WorkspaceUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceUITests.swift; sourceTree = ""; };
+ ED4469441C3E0F37B1123572 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
+ EE63F98563975EF4FF2A10FB /* WorkspaceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceViewController.swift; sourceTree = ""; };
+ F46963153D20DB21D97843E9 /* main.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = main.mm; sourceTree = ""; };
+ F63BC1BDE65F5AEBE5B13519 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; };
+/* 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 = "";
+ };
+ 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 = "";
+ };
+ 2A11A26F0082A5075CD710C6 /* bindings */ = {
+ isa = PBXGroup;
+ children = (
+ 01E10FB645809173AF1D4C2D /* bindings.h */,
+ );
+ path = bindings;
+ sourceTree = "";
+ };
+ 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 = "";
+ };
+ 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 = "";
+ };
+ 3C9F31BB065F210267ECA390 /* UITests */ = {
+ isa = PBXGroup;
+ children = (
+ ED199829CC1B1598F58EDC9B /* WorkspaceUITests.swift */,
+ );
+ path = UITests;
+ sourceTree = "";
+ };
+ 4058655E6F37D054E25D1F37 /* kanbangit-native_iOS */ = {
+ isa = PBXGroup;
+ children = (
+ D13B8D854BEB7757AB95231B /* Info.plist */,
+ 946BA5CC3FD90AD90D101A36 /* kanbangit-native_iOS.entitlements */,
+ );
+ path = "kanbangit-native_iOS";
+ sourceTree = "";
+ };
+ 59D2DE9FD94E6D2D64CB0117 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 23960010246A2A72E9940D3F /* kanbangit-native_iOS.app */,
+ A372EBDD353022BDB3E78972 /* WorkspaceTests.xctest */,
+ 9CAD0AE17A7966154CA29001 /* WorkspaceUITests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 8606F3EE5A736385061692EF /* Sources */ = {
+ isa = PBXGroup;
+ children = (
+ 89AA023B8C28CB860513C685 /* kanbangit-native */,
+ );
+ path = Sources;
+ sourceTree = "";
+ };
+ 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 = "";
+ };
+/* 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 = "";
+ };
+/* 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 */;
+}
diff --git a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..ac90d5a
--- /dev/null
+++ b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,10 @@
+
+
+
+
+ BuildSystemType
+ Original
+ DisableBuildSystemDeprecationDiagnostic
+
+
+
diff --git a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/xcshareddata/xcschemes/kanbangit-native_iOS.xcscheme b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/xcshareddata/xcschemes/kanbangit-native_iOS.xcscheme
new file mode 100644
index 0000000..470d0ab
--- /dev/null
+++ b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/xcshareddata/xcschemes/kanbangit-native_iOS.xcscheme
@@ -0,0 +1,144 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist b/native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist
new file mode 100644
index 0000000..e0b1335
--- /dev/null
+++ b/native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist
@@ -0,0 +1,44 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ $(PRODUCT_NAME)
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 0.1.0
+ CFBundleVersion
+ 0.1.0
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIRequiredDeviceCapabilities
+
+ arm64
+ metal
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
\ No newline at end of file
diff --git a/native/src-tauri/gen/apple/kanbangit-native_iOS/kanbangit-native_iOS.entitlements b/native/src-tauri/gen/apple/kanbangit-native_iOS/kanbangit-native_iOS.entitlements
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/native/src-tauri/gen/apple/kanbangit-native_iOS/kanbangit-native_iOS.entitlements
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/native/src-tauri/gen/apple/project.yml b/native/src-tauri/gen/apple/project.yml
new file mode 100644
index 0000000..155ef24
--- /dev/null
+++ b/native/src-tauri/gen/apple/project.yml
@@ -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
diff --git a/native/src-tauri/src/desktop.rs b/native/src-tauri/src/desktop.rs
new file mode 100644
index 0000000..ac50ba1
--- /dev/null
+++ b/native/src-tauri/src/desktop.rs
@@ -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,
+ label: Option,
+ 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 {
+ Ok(app.path().app_config_dir()?.join("active-server"))
+}
+
+fn update_launcher(app: &AppHandle) {
+ let state = app.state::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ 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::>();
+ 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::>()
+ .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::>().lock().unwrap().phase = error;
+ show_launcher(app);
+ }
+}
+
+pub fn context() -> tauri::Context {
+ tauri::generate_context!()
+}
+
+pub fn builder() -> tauri::Builder {
+ 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::>()
+ .lock()
+ .unwrap()
+ .server
+ .clone();
+ if let Some(server) = server {
+ if let Err(error) = open_workspace(&handle, server.url.as_str())
+ {
+ handle.state::>().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!([])
+ );
+ }
+}
diff --git a/native/src-tauri/src/ios.rs b/native/src-tauri/src/ios.rs
new file mode 100644
index 0000000..591731b
--- /dev/null
+++ b/native/src-tauri/src/ios.rs
@@ -0,0 +1,14 @@
+use tauri::{WebviewUrl, WebviewWindowBuilder};
+
+pub fn context() -> tauri::Context {
+ tauri::generate_context!()
+}
+
+pub fn builder() -> tauri::Builder {
+ tauri::Builder::default().setup(|app| {
+ WebviewWindowBuilder::new(app, "host", WebviewUrl::App("index.html".into()))
+ .on_navigation(|_| false)
+ .build()?;
+ Ok(())
+ })
+}
diff --git a/native/src-tauri/src/lib.rs b/native/src-tauri/src/lib.rs
index ec24cda..34cd168 100644
--- a/native/src-tauri/src/lib.rs
+++ b/native/src-tauri/src/lib.rs
@@ -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,
- label: Option,
- 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 {
- Ok(app.path().app_config_dir()?.join("active-server"))
-}
-
-fn update_launcher(app: &AppHandle) {
- let state = app.state::>();
- 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::>();
- 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::>();
- 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::>();
- 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::>();
- 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::>();
- 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::>()
- .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::>().lock().unwrap().phase = error;
- show_launcher(app);
- }
-}
-
-pub fn context() -> tauri::Context {
- tauri::generate_context!()
-}
-
-pub fn builder() -> tauri::Builder {
- 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::>()
- .lock()
- .unwrap()
- .server
- .clone();
- if let Some(server) = server {
- if let Err(error) = open_workspace(&handle, server.url.as_str())
- {
- handle.state::>().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!([])
- );
- }
-}
diff --git a/native/src-tauri/tauri.ios.conf.json b/native/src-tauri/tauri.ios.conf.json
new file mode 100644
index 0000000..6722463
--- /dev/null
+++ b/native/src-tauri/tauri.ios.conf.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://schema.tauri.app/config/2",
+ "bundle": {
+ "iOS": { "minimumSystemVersion": "17.0" }
+ }
+}