diff --git a/.gitignore b/.gitignore index 0cac521..4e233e7 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,11 @@ yarn-error.log /native/src-tauri/target /native/src-tauri/gen/* !/native/src-tauri/gen/android/ +!/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 7231269..7f59812 100644 --- a/native/package.json +++ b/native/package.json @@ -9,9 +9,15 @@ "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", "dev:android": "tauri android dev", - "build:android": "tauri android build --debug --apk --target aarch64" + "build:android": "tauri android build --debug --apk --target aarch64", + "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/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 752ca78..1d8d55b 100644 --- a/native/src-tauri/src/lib.rs +++ b/native/src-tauri/src/lib.rs @@ -1,16 +1,20 @@ #[cfg(target_os = "android")] mod android; -#[cfg(not(target_os = "android"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] mod desktop; -#[cfg(not(target_os = "android"))] +#[cfg(target_os = "ios")] +mod ios; +#[cfg(not(any(target_os = "android", target_os = "ios")))] mod locale; -#[cfg(not(target_os = "android"))] +#[cfg(not(any(target_os = "android", target_os = "ios")))] mod server; #[cfg(target_os = "android")] pub use android::{builder, context}; -#[cfg(not(target_os = "android"))] +#[cfg(not(any(target_os = "android", 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() { 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" } + } +}