diff --git a/Frameworks/Resources/Localizable.xcstrings b/Frameworks/Resources/Localizable.xcstrings index d17f99b6..281b8009 100644 --- a/Frameworks/Resources/Localizable.xcstrings +++ b/Frameworks/Resources/Localizable.xcstrings @@ -18345,47 +18345,6 @@ } } }, - "Connections Preferred (Pro)" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Verbindungen bevorzugen (Pro)" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Privilégier les connexions (Pro)" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "接続を優先(Pro)" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "연결 우선 (Pro)" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "优先连接(Pro)" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "優先連線(Pro)" - } - } - } - }, "Contacting the App Store…" : { "extractionState" : "manual", "localizations" : { @@ -31604,43 +31563,43 @@ } } }, - "Free includes up to 3 monitoring cards. Upgrade to Pro for more." : { + "Free includes up to 3 hosts. Upgrade to Pro to unlock this host." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", - "value" : "Die kostenlose Version enthält bis zu 3 Überwachungskarten. Mit Pro können Sie weitere hinzufügen." + "value" : "Die kostenlose Version unterstützt bis zu 3 Hosts. Führe ein Upgrade auf Pro durch, um diesen Host freizuschalten." } }, "fr" : { "stringUnit" : { "state" : "translated", - "value" : "La version gratuite inclut jusqu’à 3 cartes de surveillance. Passez à Pro pour en ajouter davantage." + "value" : "La version gratuite prend en charge jusqu’à 3 hôtes. Passez à Pro pour déverrouiller cet hôte." } }, "ja" : { "stringUnit" : { "state" : "translated", - "value" : "無料版では監視カードを3枚まで追加できます。さらに追加するにはProにアップグレードしてください。" + "value" : "無料版ではホストを3台まで利用できます。Proにアップグレードして、このホストのロックを解除してください。" } }, "ko" : { "stringUnit" : { "state" : "translated", - "value" : "무료 버전에는 모니터링 카드가 최대 3개까지 포함됩니다. 더 추가하려면 Pro로 업그레이드하세요." + "value" : "무료 버전은 호스트를 최대 3대까지 지원합니다. Pro로 업그레이드하여 이 호스트의 잠금을 해제하세요." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", - "value" : "免费版最多可添加3张监控卡片,升级Pro可添加更多。" + "value" : "免费版最多支持3台主机。升级Pro以解锁此主机。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", - "value" : "免費版最多可新增3張監控卡片,升級Pro可新增更多。" + "value" : "免費版最多支援3台主機。升級Pro以解鎖此主機。" } } } @@ -40160,47 +40119,6 @@ } } }, - "Max Connections (Pro)" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Max. Verbindungen (Pro)" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Connexions max. (Pro)" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "最大接続数(Pro)" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "최대 연결 수 (Pro)" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "最大连接数(Pro)" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "連線數上限(Pro)" - } - } - } - }, "Max Output" : { "extractionState" : "manual", "localizations" : { @@ -65853,47 +65771,6 @@ } } }, - "Smart (Pro)" : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intelligent (Pro)" - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Intelligent (Pro)" - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "スマート(Pro)" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "스마트 (Pro)" - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "智能(Pro)" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "智慧(Pro)" - } - } - } - }, "Smart uses channels for files up to 64 MB and prefers additional connections for larger files. Transfers with an unknown size use channels." : { "extractionState" : "manual", "localizations" : { @@ -82359,47 +82236,6 @@ } } }, - "Upgrade to Pro to unlock this host." : { - "extractionState" : "manual", - "localizations" : { - "de" : { - "stringUnit" : { - "state" : "translated", - "value" : "Führe ein Upgrade auf Pro durch, um diesen Host freizuschalten." - } - }, - "fr" : { - "stringUnit" : { - "state" : "translated", - "value" : "Passez à Pro pour déverrouiller cet hôte." - } - }, - "ja" : { - "stringUnit" : { - "state" : "translated", - "value" : "Proにアップグレードしてこのホストをロック解除。" - } - }, - "ko" : { - "stringUnit" : { - "state" : "translated", - "value" : "Pro로 업그레이드하여 이 호스트의 잠금을 해제하세요." - } - }, - "zh-Hans" : { - "stringUnit" : { - "state" : "translated", - "value" : "升级到Pro以解锁此主机。" - } - }, - "zh-Hant" : { - "stringUnit" : { - "state" : "translated", - "value" : "升級至Pro以解鎖此主機。" - } - } - } - }, "Upload" : { "extractionState" : "manual", "localizations" : { diff --git a/Frameworks/TermindPresentation/SFTP/SFTPSettingsFields.swift b/Frameworks/TermindPresentation/SFTP/SFTPSettingsFields.swift index c879499a..dc76985f 100644 --- a/Frameworks/TermindPresentation/SFTP/SFTPSettingsFields.swift +++ b/Frameworks/TermindPresentation/SFTP/SFTPSettingsFields.swift @@ -15,10 +15,10 @@ public struct SFTPSettingsFields: View { } public var body: some View { Picker(AppStrings.SFTP.concurrentStrategy, selection: access.strategy(for: model)) { - Text(access.isAllowed ? AppStrings.SFTP.smart : AppStrings.Pro.smartStrategy) + Text(AppStrings.SFTP.smart) .tag(SFTPConcurrencyStrategy.smart) Text(AppStrings.SFTP.channelsOnly).tag(SFTPConcurrencyStrategy.channelsOnly) - Text(access.isAllowed ? AppStrings.SFTP.connectionsPreferred : AppStrings.Pro.connectionsPreferredStrategy) + Text(AppStrings.SFTP.connectionsPreferred) .tag(SFTPConcurrencyStrategy.connectionsPreferred) } if access.strategy(for: model).wrappedValue == .smart { diff --git a/Frameworks/TermindPresentation/Strings/Pro.swift b/Frameworks/TermindPresentation/Strings/Pro.swift index f92b8bda..90edca11 100644 --- a/Frameworks/TermindPresentation/Strings/Pro.swift +++ b/Frameworks/TermindPresentation/Strings/Pro.swift @@ -2,10 +2,6 @@ import Foundation extension AppStrings { public nonisolated enum Pro { - public static var smartStrategy: LocalizedStringResource { "Smart (Pro)" } - public static var connectionsPreferredStrategy: LocalizedStringResource { "Connections Preferred (Pro)" } - public static var maxConnections: LocalizedStringResource { "Max Connections (Pro)" } - public static var contactingTheAppStoreProgress: LocalizedStringResource { "Contacting the App Store…" } @@ -14,7 +10,9 @@ extension AppStrings { "Termind Pro is not available from the App Store right now." } - public static var upgradeToUnlockHost: LocalizedStringResource { "Upgrade to Pro to unlock this host." } + public static var upgradeToUnlockHost: LocalizedStringResource { + "Free includes up to 3 hosts. Upgrade to Pro to unlock this host." + } public static var verifyingAccess: LocalizedStringResource { "Checking Pro access…" } public static var accessVerificationFailed: LocalizedStringResource { "Unable to verify Pro access. Please try again." diff --git a/TFI/App/MobileAppModel.swift b/TFI/App/MobileAppModel.swift index 9400d35a..40b5512c 100644 --- a/TFI/App/MobileAppModel.swift +++ b/TFI/App/MobileAppModel.swift @@ -507,7 +507,7 @@ final class MobileAppModel { updateWorkspaceActivity() } - private func updateWorkspaceActivity() { + func updateWorkspaceActivity() { // A failed read is not an authoritative empty list. guard hasLoadedWorkspaceSnapshot else { return } liveActivities.update(workspaceActivitySessions) diff --git a/TFI/App/MobileSceneModel.swift b/TFI/App/MobileSceneModel.swift index 4f0f56ea..6fbc8078 100644 --- a/TFI/App/MobileSceneModel.swift +++ b/TFI/App/MobileSceneModel.swift @@ -448,6 +448,7 @@ final class MobileSceneModel { return } if WorkspaceActivityAttributes.accepts(url) { + app.updateWorkspaceActivity() isWorkspaceUnavailable = false pendingWorkspaceID = nil selectedDestination = .workspaces @@ -463,6 +464,8 @@ final class MobileSceneModel { } return } + // Even an expired link must reconcile system cards against the authoritative local list. + app.updateWorkspaceActivity() isWorkspaceUnavailable = false let id = sourceID guard let owner = app.workspaceOwner(id) else { diff --git a/TFI/Platform/MobileLiveActivityController.swift b/TFI/Platform/MobileLiveActivityController.swift index 4909e02f..6f0e6420 100644 --- a/TFI/Platform/MobileLiveActivityController.swift +++ b/TFI/Platform/MobileLiveActivityController.swift @@ -12,20 +12,14 @@ final class MobileLiveActivityController { private let services: MobileLiveActivityServices private var focusedWorkspaceID: UUID? private var pendingStart = false + private var pendingState: WorkspaceActivityAttributes.ContentState? init(enabled: Bool = true, services: MobileLiveActivityServices = .live) { self.enabled = enabled self.services = services if enabled { - let restored = services.activities() - activity = restored.first { $0.state == .active || $0.state == .stale } + activity = services.activities().first { $0.state == .active || $0.state == .stale } focusedWorkspaceID = activity?.focusedWorkspaceID - let retainedID = activity?.id - updates = Task { - for duplicate in restored where duplicate.id != retainedID { - await duplicate.end() - } - } } } @@ -35,38 +29,61 @@ final class MobileLiveActivityController { update(sessions) } - /// All starts, updates and ends share a serial queue, including close-last/open-new races. + /// Serialize system calls, but coalesce queued changes so closed workspaces are never recreated. func start(_ sessions: [WorkspaceActivityAttributes.Session]) { schedule(sessions, mayStart: true) } func update(_ sessions: [WorkspaceActivityAttributes.Session]) { schedule(sessions, mayStart: false) } private func schedule(_ sessions: [WorkspaceActivityAttributes.Session], mayStart: Bool) { guard enabled else { return } - let state = WorkspaceActivityAttributes.ContentState.snapshot( + pendingState = WorkspaceActivityAttributes.ContentState.snapshot( sessions, focusedID: focusedWorkspaceID) - let previous = updates + if sessions.isEmpty { + pendingStart = false + } else if mayStart { + pendingStart = true + } + guard updates == nil else { return } updates = Task { [weak self] in - await previous?.value guard let self else { return } - if mayStart { pendingStart = true } - guard state.workspaceCount > 0 else { - pendingStart = false - if let activity { await activity.end() } - activity = nil - return + while let state = pendingState { + pendingState = nil + await reconcile(state) } - if let activity, activity.state == .active || activity.state == .stale { - pendingStart = false + updates = nil + } + } + + private func reconcile(_ state: WorkspaceActivityAttributes.ContentState) async { + // ActivityKit can expose old cards after startup. The cached handle is not the full inventory. + var activities = services.activities() + if let activity, !activities.contains(where: { $0.id == activity.id }) { activities.append(activity) } + let usable = activities.filter { $0.state == .active || $0.state == .stale } + let retained = + state.workspaceCount > 0 + ? usable.first(where: { $0.id == activity?.id }) ?? usable.first : nil + activity = retained + if retained != nil { pendingStart = false } + for other in activities where other.id != retained?.id { await other.end() } + + // An open/close may have arrived while dismissal was suspended. Publish only the newest list. + guard pendingState == nil, state.workspaceCount > 0 else { return } + if let activity { + if activity.state == .active || activity.state == .stale { await activity.update(state) return } - // An explicit open may arrive before activation. Retain its intent until a foreground update. - // Once consumed, ordinary updates cannot resurrect a user-dismissed activity. - guard pendingStart, services.canStart() else { return } - pendingStart = false - do { - activity = try services.request(state) - error = nil - } catch { self.error = error.localizedDescription } + await activity.end() + self.activity = nil + } + // An explicit open may arrive before activation. Retain its intent until a foreground update. + // Once consumed, ordinary updates cannot resurrect a user-dismissed activity. + guard pendingState == nil, pendingStart, services.canStart() else { return } + pendingStart = false + do { + activity = try services.request(state) + error = nil + } catch { + self.error = error.localizedDescription } } } diff --git a/TFITests/MobileLiveActivityRestorationTests.swift b/TFITests/MobileLiveActivityRestorationTests.swift index 085f0cb5..3785e517 100644 --- a/TFITests/MobileLiveActivityRestorationTests.swift +++ b/TFITests/MobileLiveActivityRestorationTests.swift @@ -64,6 +64,57 @@ struct MobileLiveActivityRestorationTests { #expect(await eventually { existing.ended }) } + @Test func refreshAdoptsLateSystemActivityAndDismissesDuplicates() async { + let backend = Backend() + let controller = MobileLiveActivityController(services: backend.services) + let current = session() + controller.update([current]) + for _ in 0..<20 { await Task.yield() } + let late = TestActivity(state: .stale) + let duplicate = TestActivity() + let ended = TestActivity(state: .ended) + backend.existing = [late, duplicate, ended] + + controller.update([current]) + + #expect(await eventually { late.updates.last?.sessions.map(\.id) == [current.id] }) + #expect(duplicate.ended && ended.ended) + #expect(!late.ended) + #expect(backend.requests.isEmpty) + } + + @Test func emptySnapshotDismissesEveryLateSystemActivity() async { + let backend = Backend() + let controller = MobileLiveActivityController(services: backend.services) + controller.update([]) + for _ in 0..<20 { await Task.yield() } + let late = TestActivity() + let ended = TestActivity(state: .ended) + backend.existing = [late, ended] + + controller.update([]) + + #expect(await eventually { late.ended && ended.ended }) + #expect(backend.requests.isEmpty) + } + + @Test func explicitOpenDismissesEndedCardBeforeReplacingIt() async { + let backend = Backend() + let existing = TestActivity() + backend.existing = [existing] + let controller = MobileLiveActivityController(services: backend.services) + controller.update([session()]) + #expect(await eventually { existing.updates.count == 1 }) + existing.state = .ended + let next = session() + + controller.start([next]) + + #expect(await eventually { backend.requests.count == 1 }) + #expect(existing.ended) + #expect(backend.requests.first?.sessions.map(\.id) == [next.id]) + } + @Test func closeLastThenOpenWaitsForOldActivityToFinishEnding() async { let backend = Backend() let existing = TestActivity() @@ -71,21 +122,143 @@ struct MobileLiveActivityRestorationTests { backend.existing = [existing] let controller = MobileLiveActivityController(services: backend.services) controller.update([]) + #expect(await eventually { existing.endContinuation != nil }) let next = session() controller.start([next]) controller.focus(next.id, in: [next]) - #expect(await eventually { existing.endContinuation != nil }) #expect(backend.requests.isEmpty) existing.endContinuation?.resume() existing.endContinuation = nil - #expect(await eventually { backend.created.first?.updates.count == 1 }) + #expect(await eventually { backend.requests.count == 1 }) #expect(existing.ended) #expect(backend.requests.count == 1) - #expect(backend.created.first?.updates.first?.focusedWorkspaceID == next.id) + #expect(backend.requests.first?.focusedWorkspaceID == next.id) #expect(backend.created.first?.ended == false) } + @Test func rapidOpenCloseWhileEndingOnlyPublishesTheLatestWorkspace() async { + let backend = Backend() + let existing = TestActivity() + existing.suspendsEnd = true + backend.existing = [existing] + let controller = MobileLiveActivityController(services: backend.services) + controller.update([]) + #expect(await eventually { existing.endContinuation != nil }) + for _ in 0..<5 { + controller.start([session()]) + controller.update([]) + } + let latest = session() + controller.start([latest]) + existing.endContinuation?.resume() + existing.endContinuation = nil + + #expect(await eventually { backend.requests.last?.sessions.map(\.id) == [latest.id] }) + #expect(backend.requests.count == 1) + #expect(backend.created.filter { !$0.ended }.count == 1) + } + + @Test func closeDuringDuplicateCleanupDoesNotPublishTheObsoleteOpen() async { + let backend = Backend() + let existing = TestActivity() + let duplicate = TestActivity() + duplicate.suspendsEnd = true + backend.existing = [existing, duplicate] + let controller = MobileLiveActivityController(services: backend.services) + controller.start([session()]) + #expect(await eventually { duplicate.endContinuation != nil }) + + controller.update([]) + duplicate.endContinuation?.resume() + duplicate.endContinuation = nil + + #expect(await eventually { existing.ended && duplicate.ended }) + #expect(existing.updates.isEmpty) + #expect(backend.requests.isEmpty) + } + + @Test func dismissingRetainedCardDuringCleanupDoesNotReplayItsStartIntent() async { + let backend = Backend() + let existing = TestActivity() + let duplicate = TestActivity() + duplicate.suspendsEnd = true + backend.existing = [existing, duplicate] + let controller = MobileLiveActivityController(services: backend.services) + let current = session() + controller.start([current]) + #expect(await eventually { duplicate.endContinuation != nil }) + + existing.state = .dismissed + controller.update([current]) + duplicate.endContinuation?.resume() + duplicate.endContinuation = nil + + #expect(await eventually { existing.ended && duplicate.ended }) + #expect(existing.updates.isEmpty) + #expect(backend.requests.isEmpty) + } + + @Test func staleLinkAfterRepeatedOpenCloseCleansLateCardWithoutRebuilding() async throws { + let backend = Backend() + var dependencies = MobileAppDependencies.testing() + dependencies.liveActivities = MobileLiveActivityController(services: backend.services) + let app = try MobileAppModel(dependencies: dependencies) + let host = TermindDomain.Host(name: "Server", host: "example.invalid") + app.library.upsert(host) + let scene = MobileSceneModel(app: app, sessionID: "window") + var closedID: UUID? + for index in 1...3 { + let id = try #require(scene.workspaceSession.open(hostID: host.id, kind: .ssh)) + closedID = id + #expect(await eventually { backend.requests.count == index }) + scene.closeAllWorkspaces() + #expect(await eventually { backend.created.last?.ended == true }) + } + let id = try #require(closedID) + let late = TestActivity(focusedWorkspaceID: id) + late.contentState = .snapshot( + [.init(id: id, title: "Closed", kind: "ssh", connected: false)], focusedID: id) + backend.existing.append(late) + + scene.open(WorkspaceActivityAttributes.destinationURL(for: id)) + + #expect(scene.isWorkspaceUnavailable) + #expect(await eventually { late.ended }) + #expect(!app.containsWorkspace(id)) + #expect(app.workspaces.isEmpty) + #expect(backend.requests.count == 3) + } + + @Test func staleLinkKeepsUnrelatedSavedWorkspaceAndDoesNotReconnectIt() async throws { + let name = "ActivityStaleLink.\(UUID())" + let defaults = try #require(UserDefaults(suiteName: name)) + defer { defaults.removePersistentDomain(forName: name) } + let descriptor = MobileWorkspaceDescriptor(hostID: UUID(), title: "Unavailable Host", kind: .ssh) + try MobileWorkspaceStore(defaults: defaults).save(.init(sessions: ["window": [descriptor]])) + let backend = Backend() + var dependencies = MobileAppDependencies.testing(defaults: defaults) + dependencies.liveActivities = MobileLiveActivityController(services: backend.services) + let app = try MobileAppModel(dependencies: dependencies) + let scene = MobileSceneModel(app: app, sessionID: "window") + for _ in 0..<20 { await Task.yield() } + let stale = session() + let late = TestActivity(focusedWorkspaceID: stale.id) + late.contentState = .snapshot([stale], focusedID: stale.id) + backend.existing.append(late) + + scene.open(stale.destinationURL) + + #expect(scene.isWorkspaceUnavailable) + #expect(await eventually { late.updates.last?.sessions.map(\.id) == [descriptor.id] }) + #expect(!late.ended) + #expect(scene.workspaceSession.savedWorkspaceDescriptors == [descriptor]) + #expect(app.workspaces.isEmpty) + #expect(backend.requests.isEmpty) + scene.closeAllWorkspaces() + #expect(await eventually { late.ended }) + } + @Test(arguments: [false, true]) func coldLaunchPublishesSavedRecordsAndCanCloseWithoutConnecting(openBeforeClose: Bool) async throws { let name = "ActivityRestoration.\(UUID())" @@ -153,16 +326,19 @@ struct MobileLiveActivityRestorationTests { let backend = Backend() let existing = TestActivity() existing.contentState = .snapshot([session()], focusedID: nil) - backend.existing = [existing] + let duplicate = TestActivity() + backend.existing = [existing, duplicate] var dependencies = MobileAppDependencies.testing(defaults: defaults) dependencies.liveActivities = MobileLiveActivityController(services: backend.services) let app = try MobileAppModel(dependencies: dependencies) app.sceneChanged(UUID(), phase: .active) app.persistWorkspaceDescriptors() + let scene = MobileSceneModel(app: app, sessionID: "window") + scene.open(WorkspaceActivityAttributes.destinationURL(for: UUID())) for _ in 0..<20 { await Task.yield() } #expect(app.error != nil) #expect(existing.updates.isEmpty) - #expect(!existing.ended) + #expect(!existing.ended && !duplicate.ended) #expect(defaults.data(forKey: MobileWorkspaceStore.restorationKey) == damaged) } @@ -263,7 +439,7 @@ struct MobileLiveActivityRestorationTests { var created: [TestActivity] = [] var services: MobileLiveActivityServices { MobileLiveActivityServices( - activities: { self.existing }, + activities: { (self.existing + self.created).filter { !$0.ended } }, canStart: { self.canStart }, request: { self.requests.append($0) @@ -296,6 +472,7 @@ struct MobileLiveActivityRestorationTests { func end() async { if suspendsEnd { + suspendsEnd = false await withCheckedContinuation { endContinuation = $0 } } ended = true diff --git a/TFM/Features/HostEditor/HostEditorView.swift b/TFM/Features/HostEditor/HostEditorView.swift index 6749970e..114e8b30 100644 --- a/TFM/Features/HostEditor/HostEditorView.swift +++ b/TFM/Features/HostEditor/HostEditorView.swift @@ -330,9 +330,7 @@ struct HostEditorView: View { in: Host.portForwardingMaxConnectionsRange ) { HStack { - Text( - app.proAccess.canUse(.portForwardingMultiConnection) - ? AppStrings.Vault.maxConnections : AppStrings.Pro.maxConnections) + Text(AppStrings.Vault.maxConnections) Spacer() Text(forwardingConnections.wrappedValue, format: .number).monospacedDigit() } diff --git a/Termind.xcodeproj/project.pbxproj b/Termind.xcodeproj/project.pbxproj index 7d388af0..1848eb57 100644 --- a/Termind.xcodeproj/project.pbxproj +++ b/Termind.xcodeproj/project.pbxproj @@ -2033,7 +2033,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = TFI/TFI.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 86; + CURRENT_PROJECT_VERSION = 87; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GENERATE_INFOPLIST_FILE = YES; ICLOUD_CONTAINER_ENVIRONMENT = Development; @@ -2162,7 +2162,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 86; + CURRENT_PROJECT_VERSION = 87; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TFIWidgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Termind Workspaces"; @@ -2345,7 +2345,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = TFI/TFI.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 86; + CURRENT_PROJECT_VERSION = 87; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GENERATE_INFOPLIST_FILE = YES; ICLOUD_CONTAINER_ENVIRONMENT = Production; @@ -2384,7 +2384,7 @@ buildSettings = { APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 86; + CURRENT_PROJECT_VERSION = 87; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = TFIWidgets/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = "Termind Workspaces"; @@ -2481,7 +2481,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 61; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = F6SUNDBBN9; @@ -2551,7 +2551,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 61; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = F6SUNDBBN9; @@ -2597,7 +2597,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; @@ -2635,7 +2635,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 97; + CURRENT_PROJECT_VERSION = 98; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "";