diff --git a/Frameworks/Resources/InfoPlist.xcstrings b/Frameworks/Resources/InfoPlist.xcstrings index 10895c12..5c72a61e 100644 --- a/Frameworks/Resources/InfoPlist.xcstrings +++ b/Frameworks/Resources/InfoPlist.xcstrings @@ -136,6 +136,30 @@ } } }, + "QUICK_ACTION_CONNECT_TITLE" : { + "comment" : "Title of the Home Screen quick action that opens the connection picker.", + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Connect" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "连接" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "連線" + } + } + } + }, "NSCameraUsageDescription" : { "comment" : "Privacy - Camera Usage Description", "localizations" : { diff --git a/Memory/ARCHITECTURE.md b/Memory/ARCHITECTURE.md index c10897b8..036f1b0f 100644 --- a/Memory/ARCHITECTURE.md +++ b/Memory/ARCHITECTURE.md @@ -341,7 +341,11 @@ Each mobile scene owns one navigation/command model and one Assistant library mo including its selected conversation and New Chat draft. The app-wide Assistant runtime and conversation store remain shared so active runs are never duplicated. The scene model is published as a focused value, so hardware-keyboard commands never operate -another iPad window. Mobile +another iPad window. Home Screen quick actions enter through a per-window scene +delegate, are consumed once by that scene's navigation model, and never target another +iPad window. Connect is static; up to three dynamic history actions derive from the +shared event log. Workspace History and those actions keep the newest display row per +Host and connection kind without deleting duplicate persisted events. Mobile uses the macOS shortcuts for capabilities that exist on both platforms: navigation, connection/creation, terminal Find and Command Generator, Workspace close/switch, and Assistant send/stop. Workspace switching never reconnects the session or claims diff --git a/TFI/App/MobileRootView.swift b/TFI/App/MobileRootView.swift index a6e60d21..9aa3cac8 100644 --- a/TFI/App/MobileRootView.swift +++ b/TFI/App/MobileRootView.swift @@ -7,9 +7,18 @@ struct MobileRootView: View { @Environment(\.scenePhase) private var scenePhase @Environment(\.mobileInterfaceIdiom) private var interfaceIdiom @State private var scene: MobileSceneModel + private let pendingQuickAction: MobileHomeScreenQuickAction? + private let consumePendingQuickAction: () -> MobileHomeScreenQuickAction? - init(app: MobileAppModel, scene: MobileSceneModel? = nil) { + init( + app: MobileAppModel, + scene: MobileSceneModel? = nil, + pendingQuickAction: MobileHomeScreenQuickAction? = nil, + consumePendingQuickAction: @escaping () -> MobileHomeScreenQuickAction? = { nil } + ) { self.app = app + self.pendingQuickAction = pendingQuickAction + self.consumePendingQuickAction = consumePendingQuickAction _scene = State(initialValue: scene ?? MobileSceneModel(app: app)) } @@ -50,7 +59,23 @@ struct MobileRootView: View { app.renameWorkspace(workspace.id, to: title) } } - .onChange(of: scenePhase, initial: true) { _, phase in app.sceneChanged(scene.id, phase: phase) } + .onChange(of: scenePhase, initial: true) { _, phase in + app.sceneChanged(scene.id, phase: phase) + MobileHomeScreenQuickActions.refresh(app: app) + } + .onChange(of: app.connectionHistory.revision, initial: true) { _, _ in + MobileHomeScreenQuickActions.refresh(app: app) + } + .onChange(of: app.library.hosts) { _, _ in + MobileHomeScreenQuickActions.refresh(app: app) + } + .onChange(of: app.library.identities) { _, _ in + MobileHomeScreenQuickActions.refresh(app: app) + } + .onChange(of: pendingQuickAction, initial: true) { _, action in + guard action != nil, let action = consumePendingQuickAction() else { return } + _ = scene.perform(action) + } .onChange(of: app.pro.hasProAccess) { _, _ in app.reconcileSync() } .onChange(of: app.workspaces.map(\.id)) { _, workspaceIDs in scene.reconcileWorkspaceIDs(workspaceIDs) diff --git a/TFI/App/MobileSceneModel.swift b/TFI/App/MobileSceneModel.swift index 42b07f54..50af9fcf 100644 --- a/TFI/App/MobileSceneModel.swift +++ b/TFI/App/MobileSceneModel.swift @@ -91,6 +91,24 @@ final class MobileSceneModel { selectWorkspace(id) } + @discardableResult + func perform(_ quickAction: MobileHomeScreenQuickAction) -> Bool { + switch quickAction { + case .connect: + openConnectionPicker() + return true + case .history(let hostID, let kind): + guard app.library.host(hostID) != nil else { + selectedDestination = .workspaces + workspaceID = nil + isWorkspaceUnavailable = true + return false + } + open(hostID: hostID, kind: kind) + return true + } + } + /// Reuses this scene's most recently selected SSH workspace for the Host. /// Explicit connection actions continue to call `open(hostID:kind:)` so /// users can still create independent SSH and SFTP workspaces. diff --git a/TFI/App/TermindIOSApp.swift b/TFI/App/TermindIOSApp.swift index 22eb5257..aa3fcea1 100644 --- a/TFI/App/TermindIOSApp.swift +++ b/TFI/App/TermindIOSApp.swift @@ -4,7 +4,7 @@ import TermindApplication @main struct TermindIOSApp: App { - @UIApplicationDelegateAdaptor(MobileCloudNotifications.self) private var notifications + @UIApplicationDelegateAdaptor(MobileAppDelegate.self) private var appDelegate @AppStorage("mobile.appearance") private var appearance = "system" private let launchLanguage = AppLanguage.stored() private let startup: Result @@ -20,7 +20,7 @@ struct TermindIOSApp: App { WindowGroup { switch startup { case .success(let app): - MobileRootView(app: app) + MobileQuickActionSceneRoot(app: app) .modelContainer(app.container) .environment(\.mobileInterfaceIdiom, MobileInterfaceIdiom.current) .environment(\.locale, launchLanguage.locale) @@ -34,3 +34,16 @@ struct TermindIOSApp: App { .commands { MobileCommands() } } } + +private struct MobileQuickActionSceneRoot: View { + let app: MobileAppModel + @EnvironmentObject private var sceneDelegate: MobileSceneDelegate + + var body: some View { + MobileRootView( + app: app, + pendingQuickAction: sceneDelegate.pendingQuickAction, + consumePendingQuickAction: sceneDelegate.consumePendingQuickAction + ) + } +} diff --git a/TFI/Application/Workspaces/MobileConnectionHistory.swift b/TFI/Application/Workspaces/MobileConnectionHistory.swift new file mode 100644 index 00000000..3bba1c50 --- /dev/null +++ b/TFI/Application/Workspaces/MobileConnectionHistory.swift @@ -0,0 +1,41 @@ +import TermindApplication +import TermindDomain + +struct MobileConnectionHistoryRow: Identifiable { + let entry: ConnectionHistoryEntry + let host: Host + let loginAddress: String + + var id: ConnectionHistoryEntry.ID { entry.id } +} + +@MainActor +enum MobileConnectionHistory { + private struct Key: Hashable { + let hostID: Host.ID + let kind: ConnectionHistoryEntry.Kind + } + + static func rows( + entries: [ConnectionHistoryEntry], + library: HostLibraryModel, + limit: Int + ) -> [MobileConnectionHistoryRow] { + guard limit > 0 else { return [] } + + var seen: Set = [] + var rows: [MobileConnectionHistoryRow] = [] + for entry in entries { + guard let host = library.host(entry.hostID) else { continue } + guard seen.insert(Key(hostID: entry.hostID, kind: entry.kind)).inserted else { continue } + rows.append( + MobileConnectionHistoryRow( + entry: entry, + host: host, + loginAddress: library.loginAddress(for: host) + )) + if rows.count == limit { break } + } + return rows + } +} diff --git a/TFI/Features/Workspaces/MobileWorkspaceListView.swift b/TFI/Features/Workspaces/MobileWorkspaceListView.swift index 5836348e..7d6cdfc8 100644 --- a/TFI/Features/Workspaces/MobileWorkspaceListView.swift +++ b/TFI/Features/Workspaces/MobileWorkspaceListView.swift @@ -2,13 +2,6 @@ import SwiftUI import TermindDomain import UIKit -struct MobileConnectionHistoryRow: Identifiable { - let entry: ConnectionHistoryEntry - let host: Host - - var id: ConnectionHistoryEntry.ID { entry.id } -} - /// The registered Workspaces destination. struct MobileWorkspaceListView: View { let app: MobileAppModel @@ -143,10 +136,10 @@ struct MobileWorkspaceListView: View { var historyRows: [MobileConnectionHistoryRow] { _ = app.connectionHistory.revision - return Array( - app.connectionHistory.entries().lazy.compactMap { entry in - app.library.host(entry.hostID).map { MobileConnectionHistoryRow(entry: entry, host: $0) } - }.prefix(Self.historyLimit) + return MobileConnectionHistory.rows( + entries: app.connectionHistory.entries(), + library: app.library, + limit: Self.historyLimit ) } @@ -172,7 +165,7 @@ struct MobileWorkspaceListView: View { .accessibilityHidden(true) } HStack(alignment: .center, spacing: 12) { - Text(app.library.loginAddress(for: row.host)) + Text(row.loginAddress) .font(.caption.monospaced()) .foregroundStyle(.secondary) .lineLimit(1) diff --git a/TFI/Info.plist b/TFI/Info.plist index 9db24e6e..36ea3147 100644 --- a/TFI/Info.plist +++ b/TFI/Info.plist @@ -21,6 +21,17 @@ $(ICLOUD_CONTAINER_ENVIRONMENT) TermindKeychainAccessGroup $(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER) + UIApplicationShortcutItems + + + UIApplicationShortcutItemIconSymbolName + network + UIApplicationShortcutItemTitle + QUICK_ACTION_CONNECT_TITLE + UIApplicationShortcutItemType + com.akinokaede.termind.quickAction.connect + + UIBackgroundModes remote-notification diff --git a/TFI/Platform/MobileAppDelegate.swift b/TFI/Platform/MobileAppDelegate.swift new file mode 100644 index 00000000..efa95cb7 --- /dev/null +++ b/TFI/Platform/MobileAppDelegate.swift @@ -0,0 +1,29 @@ +import UIKit + +@MainActor +final class MobileAppDelegate: NSObject, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil + ) -> Bool { + if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { + application.registerForRemoteNotifications() + } + return true + } + + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + let configuration = UISceneConfiguration( + name: nil, + sessionRole: connectingSceneSession.role + ) + if connectingSceneSession.role == .windowApplication { + configuration.delegateClass = MobileSceneDelegate.self + } + return configuration + } +} diff --git a/TFI/Platform/MobileCloudNotifications.swift b/TFI/Platform/MobileCloudNotifications.swift deleted file mode 100644 index 4bb64151..00000000 --- a/TFI/Platform/MobileCloudNotifications.swift +++ /dev/null @@ -1,15 +0,0 @@ -import UIKit - -/// CKSyncEngine owns subscription delivery; registering with APNs enables silent cloud notifications. -@MainActor -final class MobileCloudNotifications: NSObject, UIApplicationDelegate { - func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil - ) -> Bool { - if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil { - application.registerForRemoteNotifications() - } - return true - } -} diff --git a/TFI/Platform/MobileHomeScreenQuickActions.swift b/TFI/Platform/MobileHomeScreenQuickActions.swift new file mode 100644 index 00000000..55190bc7 --- /dev/null +++ b/TFI/Platform/MobileHomeScreenQuickActions.swift @@ -0,0 +1,120 @@ +import Combine +import Foundation +import TermindDomain +import UIKit + +nonisolated enum MobileHomeScreenQuickAction: Equatable, Sendable { + case connect + case history(hostID: Host.ID, kind: ConnectionHistoryEntry.Kind) +} + +struct MobileHomeScreenQuickActionDescriptor: Equatable { + let action: MobileHomeScreenQuickAction + let title: String + let symbol: String + + var shortcutItem: UIApplicationShortcutItem { + let userInfo: [String: any NSSecureCoding]? + switch action { + case .connect: + userInfo = nil + case .history(let hostID, let kind): + userInfo = [ + MobileHomeScreenQuickActions.hostIDKey: hostID.uuidString as NSString, + MobileHomeScreenQuickActions.kindKey: kind.rawValue as NSString, + ] + } + return UIApplicationShortcutItem( + type: MobileHomeScreenQuickActions.type(for: action), + localizedTitle: title, + localizedSubtitle: nil, + icon: UIApplicationShortcutIcon(systemImageName: symbol), + userInfo: userInfo + ) + } +} + +@MainActor +enum MobileHomeScreenQuickActions { + static let historyLimit = 3 + static let connectType = "com.akinokaede.termind.quickAction.connect" + static let historyType = "com.akinokaede.termind.quickAction.history" + static let hostIDKey = "hostID" + static let kindKey = "kind" + + static func type(for action: MobileHomeScreenQuickAction) -> String { + switch action { + case .connect: connectType + case .history: historyType + } + } + + static func action(from shortcutItem: UIApplicationShortcutItem) -> MobileHomeScreenQuickAction? { + switch shortcutItem.type { + case connectType: + return .connect + case historyType: + guard + let hostIDValue = shortcutItem.userInfo?[hostIDKey] as? NSString, + let hostID = UUID(uuidString: hostIDValue as String), + let kindValue = shortcutItem.userInfo?[kindKey] as? NSString, + let kind = ConnectionHistoryEntry.Kind(rawValue: kindValue as String) + else { return nil } + return .history(hostID: hostID, kind: kind) + default: + return nil + } + } + + static func descriptors(rows: [MobileConnectionHistoryRow]) -> [MobileHomeScreenQuickActionDescriptor] { + rows.prefix(historyLimit).map { row in + return MobileHomeScreenQuickActionDescriptor( + action: .history(hostID: row.host.id, kind: row.entry.kind), + title: row.host.name, + symbol: row.entry.kind.symbol + ) + } + } + + static func refresh(app: MobileAppModel) { + let rows = MobileConnectionHistory.rows( + entries: app.connectionHistory.entries(), + library: app.library, + limit: historyLimit + ) + UIApplication.shared.shortcutItems = descriptors(rows: rows).map(\.shortcutItem) + } +} + +@MainActor +final class MobileSceneDelegate: NSObject, UIWindowSceneDelegate, ObservableObject { + @Published private(set) var pendingQuickAction: MobileHomeScreenQuickAction? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let shortcutItem = connectionOptions.shortcutItem else { return } + _ = receive(shortcutItem) + } + + func windowScene( + _ windowScene: UIWindowScene, + performActionFor shortcutItem: UIApplicationShortcutItem + ) async -> Bool { + receive(shortcutItem) + } + + @discardableResult + func receive(_ shortcutItem: UIApplicationShortcutItem) -> Bool { + guard let action = MobileHomeScreenQuickActions.action(from: shortcutItem) else { return false } + pendingQuickAction = action + return true + } + + func consumePendingQuickAction() -> MobileHomeScreenQuickAction? { + defer { pendingQuickAction = nil } + return pendingQuickAction + } +} diff --git a/TFITests/MobileConnectionPickerTests.swift b/TFITests/MobileConnectionPickerTests.swift index fa4c81a6..f7672f92 100644 --- a/TFITests/MobileConnectionPickerTests.swift +++ b/TFITests/MobileConnectionPickerTests.swift @@ -42,7 +42,7 @@ struct MobileConnectionPickerTests { #expect(selections.last?.1 == .sftp) } - @Test func emptyWorkspaceHistoryKeepsTheLatestFiveValidConnectionEventsWithoutDeduplication() throws { + @Test func emptyWorkspaceHistoryKeepsTheLatestFiveUniqueHostAndProtocolPairs() throws { let app = try MobileAppModel(inMemory: true) let scene = MobileSceneModel(app: app) let repeatedHost = Host(name: "Repeated", host: "repeated.example.invalid") @@ -52,11 +52,15 @@ struct MobileConnectionPickerTests { app.library.upsert(repeatedHost) for host in otherHosts { app.library.upsert(host) } + let newestSSH = try #require( + app.connectionHistory.record(kind: .ssh, for: repeatedHost.id, at: Date(timeIntervalSince1970: 70))) + _ = app.connectionHistory.record( + kind: .ssh, for: repeatedHost.id, at: Date(timeIntervalSince1970: 60)) + let newestSFTP = try #require( + app.connectionHistory.record(kind: .sftp, for: repeatedHost.id, at: Date(timeIntervalSince1970: 55))) let expectedEntries = try [ - #require( - app.connectionHistory.record(kind: .ssh, for: repeatedHost.id, at: Date(timeIntervalSince1970: 70))), - #require( - app.connectionHistory.record(kind: .ssh, for: repeatedHost.id, at: Date(timeIntervalSince1970: 60))), + newestSSH, + newestSFTP, #require( app.connectionHistory.record(kind: .sftp, for: otherHosts[0].id, at: Date(timeIntervalSince1970: 50))), #require( @@ -74,7 +78,8 @@ struct MobileConnectionPickerTests { #expect(rows.count == MobileWorkspaceListView.historyLimit) #expect(rows.map(\.entry.id) == expectedEntries.map(\.id)) #expect(rows[0].entry.hostID == rows[1].entry.hostID) - #expect(rows[0].entry.kind == rows[1].entry.kind) + #expect(rows[0].entry.kind != rows[1].entry.kind) + #expect(Set(rows.map { "\($0.entry.hostID.uuidString):\($0.entry.kind.rawValue)" }).count == rows.count) } @Test func sharedHostPickerUsesConfiguredTitleWithoutHostsHeaderOrProtocolControl() async throws { diff --git a/TFITests/MobileHomeScreenQuickActionTests.swift b/TFITests/MobileHomeScreenQuickActionTests.swift new file mode 100644 index 00000000..a7041dff --- /dev/null +++ b/TFITests/MobileHomeScreenQuickActionTests.swift @@ -0,0 +1,97 @@ +import Foundation +import TermindDomain +import Testing +import UIKit + +import struct TermindDomain.Host + +@testable import TermindIOS + +@Suite @MainActor +struct MobileHomeScreenQuickActionTests { + @Test func descriptorsUseTheNewestThreeUniqueValidHistoryRows() throws { + let app = try MobileAppModel(inMemory: true) + let repeated = Host(name: "Production", host: "production.example.invalid") + let others = (0..<3).map { Host(name: "Host \($0)", host: "host-\($0).example.invalid") } + app.library.upsert(repeated) + for host in others { app.library.upsert(host) } + + _ = app.connectionHistory.record( + kind: .ssh, for: repeated.id, at: Date(timeIntervalSince1970: 60)) + _ = app.connectionHistory.record( + kind: .ssh, for: repeated.id, at: Date(timeIntervalSince1970: 50)) + _ = app.connectionHistory.record( + kind: .sftp, for: repeated.id, at: Date(timeIntervalSince1970: 40)) + _ = app.connectionHistory.record( + kind: .ssh, for: others[0].id, at: Date(timeIntervalSince1970: 30)) + _ = app.connectionHistory.record( + kind: .sftp, for: others[1].id, at: Date(timeIntervalSince1970: 20)) + _ = app.connectionHistory.record( + kind: .ssh, for: others[2].id, at: Date(timeIntervalSince1970: 10)) + _ = app.connectionHistory.record( + kind: .ssh, for: UUID(), at: Date(timeIntervalSince1970: 70)) + + let rows = MobileConnectionHistory.rows( + entries: app.connectionHistory.entries(), + library: app.library, + limit: MobileHomeScreenQuickActions.historyLimit + ) + let descriptors = MobileHomeScreenQuickActions.descriptors(rows: rows) + + #expect(descriptors.count == 3) + #expect( + descriptors.map(\.action) + == [ + .history(hostID: repeated.id, kind: .ssh), + .history(hostID: repeated.id, kind: .sftp), + .history(hostID: others[0].id, kind: .ssh), + ]) + #expect(descriptors.map(\.title) == ["Production", "Production", "Host 0"]) + #expect(descriptors.map(\.symbol) == ["terminal", "folder", "terminal"]) + } + + @Test func historyShortcutPayloadRoundTripsAndMalformedItemsAreRejected() throws { + let hostID = UUID() + let row = MobileConnectionHistoryRow( + entry: ConnectionHistoryEntry(hostID: hostID, kind: .sftp), + host: Host(id: hostID, name: "Files", host: "files.example.invalid"), + loginAddress: "deploy@files.example.invalid" + ) + let descriptor = try #require( + MobileHomeScreenQuickActions.descriptors(rows: [row]).first) + let shortcutItem = descriptor.shortcutItem + + #expect(shortcutItem.type == MobileHomeScreenQuickActions.historyType) + #expect(shortcutItem.localizedTitle == "Files") + #expect(shortcutItem.localizedSubtitle == nil) + #expect(shortcutItem.icon != nil) + #expect( + MobileHomeScreenQuickActions.action(from: shortcutItem) + == .history(hostID: hostID, kind: .sftp)) + + let malformed = UIApplicationShortcutItem( + type: MobileHomeScreenQuickActions.historyType, + localizedTitle: "Malformed" + ) + let unknown = UIApplicationShortcutItem(type: "unknown", localizedTitle: "Unknown") + #expect(MobileHomeScreenQuickActions.action(from: malformed) == nil) + #expect(MobileHomeScreenQuickActions.action(from: unknown) == nil) + } + + @Test func sceneDelegateAcceptsKnownActionsAndConsumesEachOnce() { + let delegate = MobileSceneDelegate() + let connect = UIApplicationShortcutItem( + type: MobileHomeScreenQuickActions.connectType, + localizedTitle: "Connect" + ) + + #expect(delegate.receive(connect)) + #expect(delegate.pendingQuickAction == .connect) + #expect(delegate.consumePendingQuickAction() == .connect) + #expect(delegate.consumePendingQuickAction() == nil) + + let unknown = UIApplicationShortcutItem(type: "unknown", localizedTitle: "Unknown") + #expect(!delegate.receive(unknown)) + #expect(delegate.pendingQuickAction == nil) + } +} diff --git a/TFITests/MobileShortcutTests.swift b/TFITests/MobileShortcutTests.swift index 4445eafc..90a2e64d 100644 --- a/TFITests/MobileShortcutTests.swift +++ b/TFITests/MobileShortcutTests.swift @@ -72,6 +72,35 @@ struct MobileShortcutTests { #expect(scene.assistant.conversation.id != previousConversationID) } + @Test func homeScreenConnectActionUsesTheWorkspaceConnectionPicker() throws { + let app = try MobileAppModel(inMemory: true) + let scene = MobileSceneModel(app: app) + + #expect(scene.perform(.connect)) + #expect(scene.selectedDestination == .workspaces) + #expect(scene.workspaceID == nil) + #expect(scene.isConnectionPickerPresented) + } + + @Test func homeScreenHistoryActionOpensItsProtocolAndReportsAMissingHost() throws { + let app = try MobileAppModel(inMemory: true) + let host = Host(name: "Files", host: "files.example.invalid") + app.library.upsert(host) + let scene = MobileSceneModel(app: app) + defer { app.closeAll() } + + #expect(scene.perform(.history(hostID: host.id, kind: .sftp))) + scene.activatePendingWorkspace() + #expect(scene.activeWorkspace?.descriptor.hostID == host.id) + #expect(scene.activeWorkspace?.descriptor.kind == .sftp) + #expect(app.connectionHistory.entries(for: host.id).map(\.kind) == [.sftp]) + + let historyCount = app.connectionHistory.entries().count + #expect(!scene.perform(.history(hostID: UUID(), kind: .ssh))) + #expect(scene.isWorkspaceUnavailable) + #expect(app.connectionHistory.entries().count == historyCount) + } + @Test func hostCardActivationReusesTheScenesMostRecentSSHWorkspace() throws { let app = try MobileAppModel(inMemory: true) let host = Host(name: "Test", host: "example.invalid") diff --git a/Termind.xcodeproj/project.pbxproj b/Termind.xcodeproj/project.pbxproj index 8fa916f4..401b9fa2 100644 --- a/Termind.xcodeproj/project.pbxproj +++ b/Termind.xcodeproj/project.pbxproj @@ -1729,7 +1729,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = TFI/TFI.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = F6SUNDBBN9; GENERATE_INFOPLIST_FILE = YES; ICLOUD_CONTAINER_ENVIRONMENT = Development; @@ -1746,7 +1746,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.7.4; + MARKETING_VERSION = 1.7.5; PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind; PRODUCT_MODULE_NAME = TermindIOS; PRODUCT_NAME = Termind; @@ -1986,7 +1986,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = TFI/TFI.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 5; + CURRENT_PROJECT_VERSION = 6; DEVELOPMENT_TEAM = F6SUNDBBN9; GENERATE_INFOPLIST_FILE = YES; ICLOUD_CONTAINER_ENVIRONMENT = Production; @@ -2003,7 +2003,7 @@ "@executable_path/Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.7.4; + MARKETING_VERSION = 1.7.5; PRODUCT_BUNDLE_IDENTIFIER = com.akinokaede.termind; PRODUCT_MODULE_NAME = TermindIOS; PRODUCT_NAME = Termind;