From a49d741e6e69fb5ee9769be0c8a8be6e0d293b66 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 5 Apr 2026 16:12:41 +0700 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20add=20Quick=20Connect=20widget=20fo?= =?UTF-8?q?r=20iOS=20=E2=80=94=20deep=20link,=20shared=20data,=20widget=20?= =?UTF-8?q?views?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TableProMobile/TableProMobile/AppState.swift | 24 ++++++++ .../TableProMobile/TableProMobileApp.swift | 7 +++ .../TableProMobileRelease.entitlements | 4 ++ .../Views/ConnectionListView.swift | 13 ++++ .../Helpers/DatabaseTypeStyle.swift | 34 +++++++++++ .../TableProWidget/QuickConnectEntry.swift | 23 ++++++++ .../TableProWidget/QuickConnectProvider.swift | 29 +++++++++ .../TableProWidget/QuickConnectWidget.swift | 22 +++++++ .../Shared/SharedConnectionStore.swift | 32 ++++++++++ .../Shared/WidgetConnectionItem.swift | 15 +++++ .../TableProWidget.entitlements | 10 ++++ .../Views/MediumWidgetView.swift | 59 +++++++++++++++++++ .../Views/QuickConnectEntryView.swift | 23 ++++++++ .../Views/SmallWidgetView.swift | 49 +++++++++++++++ 14 files changed, 344 insertions(+) create mode 100644 TableProMobile/TableProWidget/Helpers/DatabaseTypeStyle.swift create mode 100644 TableProMobile/TableProWidget/QuickConnectEntry.swift create mode 100644 TableProMobile/TableProWidget/QuickConnectProvider.swift create mode 100644 TableProMobile/TableProWidget/QuickConnectWidget.swift create mode 100644 TableProMobile/TableProWidget/Shared/SharedConnectionStore.swift create mode 100644 TableProMobile/TableProWidget/Shared/WidgetConnectionItem.swift create mode 100644 TableProMobile/TableProWidget/TableProWidget.entitlements create mode 100644 TableProMobile/TableProWidget/Views/MediumWidgetView.swift create mode 100644 TableProMobile/TableProWidget/Views/QuickConnectEntryView.swift create mode 100644 TableProMobile/TableProWidget/Views/SmallWidgetView.swift diff --git a/TableProMobile/TableProMobile/AppState.swift b/TableProMobile/TableProMobile/AppState.swift index b81587f2f..ade6a6ca2 100644 --- a/TableProMobile/TableProMobile/AppState.swift +++ b/TableProMobile/TableProMobile/AppState.swift @@ -7,12 +7,14 @@ import Foundation import Observation import TableProDatabase import TableProModels +import WidgetKit @MainActor @Observable final class AppState { var connections: [DatabaseConnection] = [] var groups: [ConnectionGroup] = [] var tags: [ConnectionTag] = [] + var pendingConnectionId: UUID? let connectionManager: ConnectionManager let syncCoordinator = IOSSyncCoordinator() let sshProvider: IOSSSHProvider @@ -37,11 +39,13 @@ final class AppState { groups = groupStorage.load() tags = tagStorage.load() secureStore.cleanOrphanedCredentials(validConnectionIds: Set(connections.map(\.id))) + updateWidgetData() syncCoordinator.onConnectionsChanged = { [weak self] merged in guard let self else { return } self.connections = merged self.storage.save(merged) + self.updateWidgetData() } syncCoordinator.onGroupsChanged = { [weak self] merged in @@ -67,6 +71,7 @@ final class AppState { func addConnection(_ connection: DatabaseConnection) { connections.append(connection) storage.save(connections) + updateWidgetData() syncCoordinator.markDirty(connection.id) syncCoordinator.scheduleSyncAfterChange() } @@ -75,6 +80,7 @@ final class AppState { if let index = connections.firstIndex(where: { $0.id == connection.id }) { connections[index] = connection storage.save(connections) + updateWidgetData() syncCoordinator.markDirty(connection.id) syncCoordinator.scheduleSyncAfterChange() } @@ -91,6 +97,7 @@ final class AppState { try? secureStore.delete(forKey: "com.TablePro.keypassphrase.\(connection.id.uuidString)") try? secureStore.delete(forKey: "com.TablePro.sshkeydata.\(connection.id.uuidString)") storage.save(connections) + updateWidgetData() syncCoordinator.markDeleted(connection.id) syncCoordinator.scheduleSyncAfterChange() } @@ -161,6 +168,23 @@ final class AppState { syncCoordinator.scheduleSyncAfterChange() } + // MARK: - Widget + + private func updateWidgetData() { + let items = connections.map { conn in + WidgetConnectionItem( + id: conn.id, + name: conn.name.isEmpty ? conn.host : conn.name, + type: conn.type.rawValue, + host: conn.host, + port: conn.port, + sortOrder: conn.sortOrder + ) + } + SharedConnectionStore.write(items) + WidgetCenter.shared.reloadAllTimelines() + } + // MARK: - Helpers func group(for id: UUID?) -> ConnectionGroup? { diff --git a/TableProMobile/TableProMobile/TableProMobileApp.swift b/TableProMobile/TableProMobile/TableProMobileApp.swift index 6b3ae016b..101607af0 100644 --- a/TableProMobile/TableProMobile/TableProMobileApp.swift +++ b/TableProMobile/TableProMobile/TableProMobileApp.swift @@ -23,6 +23,13 @@ struct TableProMobileApp: App { .environment(appState) } } + .onOpenURL { url in + guard url.scheme == "tablepro", + url.host == "connect", + let uuidString = url.pathComponents.dropFirst().first, + let uuid = UUID(uuidString: uuidString) else { return } + appState.pendingConnectionId = uuid + } .onChange(of: scenePhase) { _, phase in switch phase { case .active: diff --git a/TableProMobile/TableProMobile/TableProMobileRelease.entitlements b/TableProMobile/TableProMobile/TableProMobileRelease.entitlements index cacbeecd7..f8880a803 100644 --- a/TableProMobile/TableProMobile/TableProMobileRelease.entitlements +++ b/TableProMobile/TableProMobile/TableProMobileRelease.entitlements @@ -10,6 +10,10 @@ CloudKit + com.apple.security.application-groups + + group.com.TablePro.TableProMobile + keychain-access-groups $(AppIdentifierPrefix)com.TablePro.shared diff --git a/TableProMobile/TableProMobile/Views/ConnectionListView.swift b/TableProMobile/TableProMobile/Views/ConnectionListView.swift index 83fe6740f..50630e028 100644 --- a/TableProMobile/TableProMobile/Views/ConnectionListView.swift +++ b/TableProMobile/TableProMobile/Views/ConnectionListView.swift @@ -36,6 +36,12 @@ struct ConnectionListView: View { .navigationDestination(for: DatabaseConnection.self) { connection in ConnectedView(connection: connection) } + .onChange(of: appState.pendingConnectionId) { _, newId in + navigateToPendingConnection(newId) + } + .onAppear { + navigateToPendingConnection(appState.pendingConnectionId) + } .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { filterMenu @@ -236,6 +242,13 @@ struct ConnectionListView: View { } } + private func navigateToPendingConnection(_ id: UUID?) { + guard let id, + let connection = appState.connections.first(where: { $0.id == id }) else { return } + selectedConnection = connection + appState.pendingConnectionId = nil + } + private func connectionRow(_ connection: DatabaseConnection) -> some View { NavigationLink(value: connection) { ConnectionRow(connection: connection, tag: appState.tag(for: connection.tagId)) diff --git a/TableProMobile/TableProWidget/Helpers/DatabaseTypeStyle.swift b/TableProMobile/TableProWidget/Helpers/DatabaseTypeStyle.swift new file mode 100644 index 000000000..d083e2955 --- /dev/null +++ b/TableProMobile/TableProWidget/Helpers/DatabaseTypeStyle.swift @@ -0,0 +1,34 @@ +// +// DatabaseTypeStyle.swift +// TableProWidget +// + +import SwiftUI + +enum DatabaseTypeStyle { + static func iconName(for type: String) -> String { + switch type.lowercased() { + case "mysql", "mariadb": return "cylinder" + case "postgresql", "redshift": return "cylinder.split.1x2" + case "sqlite": return "doc" + case "redis": return "key" + case "mongodb": return "leaf" + case "clickhouse": return "bolt" + case "mssql": return "server.rack" + default: return "externaldrive" + } + } + + static func iconColor(for type: String) -> Color { + switch type.lowercased() { + case "mysql", "mariadb": return .orange + case "postgresql", "redshift": return .blue + case "sqlite": return .green + case "redis": return .red + case "mongodb": return .green + case "clickhouse": return .yellow + case "mssql": return .indigo + default: return .gray + } + } +} diff --git a/TableProMobile/TableProWidget/QuickConnectEntry.swift b/TableProMobile/TableProWidget/QuickConnectEntry.swift new file mode 100644 index 000000000..bc83bc221 --- /dev/null +++ b/TableProMobile/TableProWidget/QuickConnectEntry.swift @@ -0,0 +1,23 @@ +// +// QuickConnectEntry.swift +// TableProWidget +// + +import WidgetKit + +struct QuickConnectEntry: TimelineEntry { + let date: Date + let connections: [WidgetConnectionItem] + + static var placeholder: QuickConnectEntry { + QuickConnectEntry( + date: .now, + connections: [ + WidgetConnectionItem(id: UUID(), name: "Production", type: "postgresql", host: "db.example.com", port: 5432, sortOrder: 0), + WidgetConnectionItem(id: UUID(), name: "Local MySQL", type: "mysql", host: "localhost", port: 3306, sortOrder: 1), + WidgetConnectionItem(id: UUID(), name: "Redis Cache", type: "redis", host: "cache.local", port: 6379, sortOrder: 2), + WidgetConnectionItem(id: UUID(), name: "Analytics", type: "clickhouse", host: "ch.example.com", port: 8123, sortOrder: 3) + ] + ) + } +} diff --git a/TableProMobile/TableProWidget/QuickConnectProvider.swift b/TableProMobile/TableProWidget/QuickConnectProvider.swift new file mode 100644 index 000000000..9d900f900 --- /dev/null +++ b/TableProMobile/TableProWidget/QuickConnectProvider.swift @@ -0,0 +1,29 @@ +// +// QuickConnectProvider.swift +// TableProWidget +// + +import WidgetKit + +struct QuickConnectProvider: TimelineProvider { + func placeholder(in context: Context) -> QuickConnectEntry { + .placeholder + } + + func getSnapshot(in context: Context, completion: @escaping (QuickConnectEntry) -> Void) { + if context.isPreview { + completion(.placeholder) + return + } + let connections = SharedConnectionStore.read() + .sorted { $0.sortOrder < $1.sortOrder } + completion(QuickConnectEntry(date: .now, connections: connections)) + } + + func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { + let connections = SharedConnectionStore.read() + .sorted { $0.sortOrder < $1.sortOrder } + let entry = QuickConnectEntry(date: .now, connections: connections) + completion(Timeline(entries: [entry], policy: .never)) + } +} diff --git a/TableProMobile/TableProWidget/QuickConnectWidget.swift b/TableProMobile/TableProWidget/QuickConnectWidget.swift new file mode 100644 index 000000000..0189dec75 --- /dev/null +++ b/TableProMobile/TableProWidget/QuickConnectWidget.swift @@ -0,0 +1,22 @@ +// +// QuickConnectWidget.swift +// TableProWidget +// + +import SwiftUI +import WidgetKit + +@main +struct QuickConnectWidget: Widget { + let kind = "com.TablePro.QuickConnect" + + var body: some WidgetConfiguration { + StaticConfiguration(kind: kind, provider: QuickConnectProvider()) { entry in + QuickConnectEntryView(entry: entry) + .containerBackground(.fill.tertiary, for: .widget) + } + .configurationDisplayName("Quick Connect") + .description("Quickly connect to your databases.") + .supportedFamilies([.systemSmall, .systemMedium]) + } +} diff --git a/TableProMobile/TableProWidget/Shared/SharedConnectionStore.swift b/TableProMobile/TableProWidget/Shared/SharedConnectionStore.swift new file mode 100644 index 000000000..2b5e7837e --- /dev/null +++ b/TableProMobile/TableProWidget/Shared/SharedConnectionStore.swift @@ -0,0 +1,32 @@ +// +// SharedConnectionStore.swift +// TableProWidget +// + +import Foundation + +enum SharedConnectionStore { + private static let appGroupId = "group.com.TablePro.TableProMobile" + private static let fileName = "widget-connections.json" + + private static var fileURL: URL? { + FileManager.default + .containerURL(forSecurityApplicationGroupIdentifier: appGroupId)? + .appendingPathComponent(fileName) + } + + static func write(_ items: [WidgetConnectionItem]) { + guard let url = fileURL, + let data = try? JSONEncoder().encode(items) else { return } + try? data.write(to: url, options: .atomic) + } + + static func read() -> [WidgetConnectionItem] { + guard let url = fileURL, + let data = try? Data(contentsOf: url), + let items = try? JSONDecoder().decode([WidgetConnectionItem].self, from: data) else { + return [] + } + return items + } +} diff --git a/TableProMobile/TableProWidget/Shared/WidgetConnectionItem.swift b/TableProMobile/TableProWidget/Shared/WidgetConnectionItem.swift new file mode 100644 index 000000000..92ec23d02 --- /dev/null +++ b/TableProMobile/TableProWidget/Shared/WidgetConnectionItem.swift @@ -0,0 +1,15 @@ +// +// WidgetConnectionItem.swift +// TableProWidget +// + +import Foundation + +struct WidgetConnectionItem: Codable, Identifiable, Hashable { + let id: UUID + let name: String + let type: String + let host: String + let port: Int + let sortOrder: Int +} diff --git a/TableProMobile/TableProWidget/TableProWidget.entitlements b/TableProMobile/TableProWidget/TableProWidget.entitlements new file mode 100644 index 000000000..930e4ce9b --- /dev/null +++ b/TableProMobile/TableProWidget/TableProWidget.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.com.TablePro.TableProMobile + + + diff --git a/TableProMobile/TableProWidget/Views/MediumWidgetView.swift b/TableProMobile/TableProWidget/Views/MediumWidgetView.swift new file mode 100644 index 000000000..ad37538d1 --- /dev/null +++ b/TableProMobile/TableProWidget/Views/MediumWidgetView.swift @@ -0,0 +1,59 @@ +// +// MediumWidgetView.swift +// TableProWidget +// + +import SwiftUI + +struct MediumWidgetView: View { + let connections: [WidgetConnectionItem] + + private var displayedConnections: [WidgetConnectionItem] { + Array(connections.prefix(4)) + } + + private let columns = [ + GridItem(.flexible(), spacing: 8), + GridItem(.flexible(), spacing: 8) + ] + + var body: some View { + if connections.isEmpty { + VStack(spacing: 8) { + Image(systemName: "server.rack") + .font(.title2) + .foregroundStyle(.secondary) + Text("Add a connection in TablePro") + .font(.caption) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + LazyVGrid(columns: columns, spacing: 8) { + ForEach(displayedConnections) { connection in + Link(destination: URL(string: "tablepro://connect/\(connection.id.uuidString)")!) { + HStack(spacing: 8) { + Image(systemName: DatabaseTypeStyle.iconName(for: connection.type)) + .font(.callout) + .foregroundStyle(DatabaseTypeStyle.iconColor(for: connection.type)) + .frame(width: 28, height: 28) + .background(DatabaseTypeStyle.iconColor(for: connection.type).opacity(0.15)) + .clipShape(RoundedRectangle(cornerRadius: 6)) + + Text(connection.name) + .font(.caption) + .foregroundStyle(.primary) + .lineLimit(1) + + Spacer(minLength: 0) + } + .padding(.horizontal, 8) + .padding(.vertical, 6) + .background(.fill.quaternary) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + } + } + } + } +} diff --git a/TableProMobile/TableProWidget/Views/QuickConnectEntryView.swift b/TableProMobile/TableProWidget/Views/QuickConnectEntryView.swift new file mode 100644 index 000000000..3f70db162 --- /dev/null +++ b/TableProMobile/TableProWidget/Views/QuickConnectEntryView.swift @@ -0,0 +1,23 @@ +// +// QuickConnectEntryView.swift +// TableProWidget +// + +import SwiftUI +import WidgetKit + +struct QuickConnectEntryView: View { + @Environment(\.widgetFamily) private var family + let entry: QuickConnectEntry + + var body: some View { + switch family { + case .systemSmall: + SmallWidgetView(connections: entry.connections) + case .systemMedium: + MediumWidgetView(connections: entry.connections) + default: + SmallWidgetView(connections: entry.connections) + } + } +} diff --git a/TableProMobile/TableProWidget/Views/SmallWidgetView.swift b/TableProMobile/TableProWidget/Views/SmallWidgetView.swift new file mode 100644 index 000000000..69ed6da5f --- /dev/null +++ b/TableProMobile/TableProWidget/Views/SmallWidgetView.swift @@ -0,0 +1,49 @@ +// +// SmallWidgetView.swift +// TableProWidget +// + +import SwiftUI + +struct SmallWidgetView: View { + let connections: [WidgetConnectionItem] + + var body: some View { + if let connection = connections.first { + VStack(alignment: .leading, spacing: 8) { + Image(systemName: DatabaseTypeStyle.iconName(for: connection.type)) + .font(.title2) + .foregroundStyle(DatabaseTypeStyle.iconColor(for: connection.type)) + .frame(width: 36, height: 36) + .background(DatabaseTypeStyle.iconColor(for: connection.type).opacity(0.15)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + + Spacer() + + VStack(alignment: .leading, spacing: 2) { + Text(connection.name) + .font(.headline) + .lineLimit(1) + + Text(verbatim: "\(connection.host):\(connection.port)") + .font(.caption) + .foregroundStyle(.secondary) + .lineLimit(1) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .widgetURL(URL(string: "tablepro://connect/\(connection.id.uuidString)")) + } else { + VStack(spacing: 8) { + Image(systemName: "server.rack") + .font(.title2) + .foregroundStyle(.secondary) + Text("Add a connection in TablePro") + .font(.caption) + .foregroundStyle(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } +} From 2b9c121721c25817438d1773a3890206b077bb4d Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 5 Apr 2026 16:45:07 +0700 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20widget=20target=20setup=20=E2=80=94?= =?UTF-8?q?=20Xcode=20project,=20import=20WidgetKit,=20onOpenURL=20placeme?= =?UTF-8?q?nt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TableProMobile.xcodeproj/project.pbxproj | 193 +++++++++++++++++- .../TableProMobile/TableProMobileApp.swift | 28 +-- .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++++ .../Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 + TableProMobile/TableProWidget/Info.plist | 11 + .../Views/SmallWidgetView.swift | 1 + 8 files changed, 282 insertions(+), 14 deletions(-) create mode 100644 TableProMobile/TableProWidget/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 TableProMobile/TableProWidget/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 TableProMobile/TableProWidget/Assets.xcassets/Contents.json create mode 100644 TableProMobile/TableProWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 TableProMobile/TableProWidget/Info.plist diff --git a/TableProMobile/TableProMobile.xcodeproj/project.pbxproj b/TableProMobile/TableProMobile.xcodeproj/project.pbxproj index b5a9cb223..3193e2148 100644 --- a/TableProMobile/TableProMobile.xcodeproj/project.pbxproj +++ b/TableProMobile/TableProMobile.xcodeproj/project.pbxproj @@ -8,6 +8,9 @@ /* Begin PBXBuildFile section */ 5A87EEED2F7F893000D028D0 /* TableProSync in Frameworks */ = {isa = PBXBuildFile; productRef = 5A87EEEC2F7F893000D028D0 /* TableProSync */; }; + 5AA136062F82610F00ADCD58 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AA136052F82610F00ADCD58 /* WidgetKit.framework */; }; + 5AA136082F82610F00ADCD58 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AA136072F82610F00ADCD58 /* SwiftUI.framework */; }; + 5AA136132F82611000ADCD58 /* TableProWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 5AA136042F82610F00ADCD58 /* TableProWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 5AA3133A2F7EA5B4008EBA97 /* LibPQ.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AA313342F7EA5B4008EBA97 /* LibPQ.xcframework */; }; 5AA3133C2F7EA5B4008EBA97 /* Hiredis.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AA313352F7EA5B4008EBA97 /* Hiredis.xcframework */; }; 5AA3133E2F7EA5B4008EBA97 /* OpenSSL-Crypto.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5AA313362F7EA5B4008EBA97 /* OpenSSL-Crypto.xcframework */; }; @@ -21,6 +24,30 @@ 5AB9F3EF2F7C1D03001F3337 /* TableProQuery in Frameworks */ = {isa = PBXBuildFile; productRef = 5AB9F3EE2F7C1D03001F3337 /* TableProQuery */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 5AA136112F82611000ADCD58 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5AB9F3D12F7C1C12001F3337 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5AA136032F82610F00ADCD58; + remoteInfo = TableProWidgetExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 5AA136142F82611000ADCD58 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 5AA136132F82611000ADCD58 /* TableProWidgetExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + /* Begin PBXFileReference section */ 5A87ECDD2F7F88F200D028D0 /* AIPromptTemplates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIPromptTemplates.swift; sourceTree = ""; }; 5A87ECDE2F7F88F200D028D0 /* AIPromptTemplates+InlineSuggest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AIPromptTemplates+InlineSuggest.swift"; sourceTree = ""; }; @@ -480,6 +507,9 @@ 5A87EEE82F7F891F00D028D0 /* SyncMetadataStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncMetadataStorage.swift; sourceTree = ""; }; 5A87EEE92F7F891F00D028D0 /* SyncRecordMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncRecordMapper.swift; sourceTree = ""; }; 5A87EEEA2F7F891F00D028D0 /* SyncRecordType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncRecordType.swift; sourceTree = ""; }; + 5AA136042F82610F00ADCD58 /* TableProWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TableProWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 5AA136052F82610F00ADCD58 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; + 5AA136072F82610F00ADCD58 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 5AA313342F7EA5B4008EBA97 /* LibPQ.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = LibPQ.xcframework; path = ../Libs/ios/LibPQ.xcframework; sourceTree = ""; }; 5AA313352F7EA5B4008EBA97 /* Hiredis.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Hiredis.xcframework; path = ../Libs/ios/Hiredis.xcframework; sourceTree = ""; }; 5AA313362F7EA5B4008EBA97 /* OpenSSL-Crypto.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "OpenSSL-Crypto.xcframework"; path = "../Libs/ios/OpenSSL-Crypto.xcframework"; sourceTree = ""; }; @@ -490,7 +520,34 @@ 5AB9F3D92F7C1C12001F3337 /* TableProMobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TableProMobile.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 5AA136172F82611000ADCD58 /* Exceptions for "TableProWidget" folder in "TableProWidgetExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 5AA136032F82610F00ADCD58 /* TableProWidgetExtension */; + }; + 5AA136302F82660900ADCD58 /* Exceptions for "TableProWidget" folder in "TableProMobile" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Shared/SharedConnectionStore.swift, + Shared/WidgetConnectionItem.swift, + ); + target = 5AB9F3D82F7C1C12001F3337 /* TableProMobile */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ + 5AA136092F82610F00ADCD58 /* TableProWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 5AA136302F82660900ADCD58 /* Exceptions for "TableProWidget" folder in "TableProMobile" target */, + 5AA136172F82611000ADCD58 /* Exceptions for "TableProWidget" folder in "TableProWidgetExtension" target */, + ); + path = TableProWidget; + sourceTree = ""; + }; 5AB9F3DB2F7C1C12001F3337 /* TableProMobile */ = { isa = PBXFileSystemSynchronizedRootGroup; path = TableProMobile; @@ -499,6 +556,15 @@ /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ + 5AA136012F82610F00ADCD58 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5AA136082F82610F00ADCD58 /* SwiftUI.framework in Frameworks */, + 5AA136062F82610F00ADCD58 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5AB9F3D62F7C1C12001F3337 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -1543,6 +1609,8 @@ 5AA313372F7EA5B4008EBA97 /* MariaDB.xcframework */, 5AA313362F7EA5B4008EBA97 /* OpenSSL-Crypto.xcframework */, 5AA313392F7EA5B4008EBA97 /* OpenSSL-SSL.xcframework */, + 5AA136052F82610F00ADCD58 /* WidgetKit.framework */, + 5AA136072F82610F00ADCD58 /* SwiftUI.framework */, ); name = Frameworks; sourceTree = ""; @@ -1551,6 +1619,7 @@ isa = PBXGroup; children = ( 5AB9F3DB2F7C1C12001F3337 /* TableProMobile */, + 5AA136092F82610F00ADCD58 /* TableProWidget */, 5AA313332F7EA5B4008EBA97 /* Frameworks */, 5AB9F3DA2F7C1C12001F3337 /* Products */, ); @@ -1560,6 +1629,7 @@ isa = PBXGroup; children = ( 5AB9F3D92F7C1C12001F3337 /* TableProMobile.app */, + 5AA136042F82610F00ADCD58 /* TableProWidgetExtension.appex */, ); name = Products; sourceTree = ""; @@ -1567,6 +1637,28 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 5AA136032F82610F00ADCD58 /* TableProWidgetExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5AA136182F82611000ADCD58 /* Build configuration list for PBXNativeTarget "TableProWidgetExtension" */; + buildPhases = ( + 5AA136002F82610F00ADCD58 /* Sources */, + 5AA136012F82610F00ADCD58 /* Frameworks */, + 5AA136022F82610F00ADCD58 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 5AA136092F82610F00ADCD58 /* TableProWidget */, + ); + name = TableProWidgetExtension; + packageProductDependencies = ( + ); + productName = TableProWidgetExtension; + productReference = 5AA136042F82610F00ADCD58 /* TableProWidgetExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 5AB9F3D82F7C1C12001F3337 /* TableProMobile */ = { isa = PBXNativeTarget; buildConfigurationList = 5AB9F3E42F7C1C13001F3337 /* Build configuration list for PBXNativeTarget "TableProMobile" */; @@ -1574,10 +1666,12 @@ 5AB9F3D52F7C1C12001F3337 /* Sources */, 5AB9F3D62F7C1C12001F3337 /* Frameworks */, 5AB9F3D72F7C1C12001F3337 /* Resources */, + 5AA136142F82611000ADCD58 /* Embed Foundation Extensions */, ); buildRules = ( ); dependencies = ( + 5AA136122F82611000ADCD58 /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 5AB9F3DB2F7C1C12001F3337 /* TableProMobile */, @@ -1601,9 +1695,12 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 2640; + LastSwiftUpdateCheck = 2650; LastUpgradeCheck = 2650; TargetAttributes = { + 5AA136032F82610F00ADCD58 = { + CreatedOnToolsVersion = 26.5; + }; 5AB9F3D82F7C1C12001F3337 = { CreatedOnToolsVersion = 26.4; }; @@ -1627,11 +1724,19 @@ projectRoot = ""; targets = ( 5AB9F3D82F7C1C12001F3337 /* TableProMobile */, + 5AA136032F82610F00ADCD58 /* TableProWidgetExtension */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 5AA136022F82610F00ADCD58 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5AB9F3D72F7C1C12001F3337 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -1642,6 +1747,13 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 5AA136002F82610F00ADCD58 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 5AB9F3D52F7C1C12001F3337 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -1651,7 +1763,77 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 5AA136122F82611000ADCD58 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5AA136032F82610F00ADCD58 /* TableProWidgetExtension */; + targetProxy = 5AA136112F82611000ADCD58 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin XCBuildConfiguration section */ + 5AA136152F82611000ADCD58 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D7HJ5TFYCU; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TableProWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = TableProWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.TablePro.TableProMobile.Widget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5AA136162F82611000ADCD58 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = D7HJ5TFYCU; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = TableProWidget/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = TableProWidget; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.TablePro.TableProMobile.Widget; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 5AB9F3E22F7C1C13001F3337 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -1862,6 +2044,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 5AA136182F82611000ADCD58 /* Build configuration list for PBXNativeTarget "TableProWidgetExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5AA136152F82611000ADCD58 /* Debug */, + 5AA136162F82611000ADCD58 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 5AB9F3D42F7C1C12001F3337 /* Build configuration list for PBXProject "TableProMobile" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/TableProMobile/TableProMobile/TableProMobileApp.swift b/TableProMobile/TableProMobile/TableProMobileApp.swift index 101607af0..0416e039d 100644 --- a/TableProMobile/TableProMobile/TableProMobileApp.swift +++ b/TableProMobile/TableProMobile/TableProMobileApp.swift @@ -15,20 +15,22 @@ struct TableProMobileApp: App { var body: some Scene { WindowGroup { - if appState.hasCompletedOnboarding { - ConnectionListView() - .environment(appState) - } else { - OnboardingView() - .environment(appState) + Group { + if appState.hasCompletedOnboarding { + ConnectionListView() + .environment(appState) + } else { + OnboardingView() + .environment(appState) + } + } + .onOpenURL { url in + guard url.scheme == "tablepro", + url.host == "connect", + let uuidString = url.pathComponents.dropFirst().first, + let uuid = UUID(uuidString: uuidString) else { return } + appState.pendingConnectionId = uuid } - } - .onOpenURL { url in - guard url.scheme == "tablepro", - url.host == "connect", - let uuidString = url.pathComponents.dropFirst().first, - let uuid = UUID(uuidString: uuidString) else { return } - appState.pendingConnectionId = uuid } .onChange(of: scenePhase) { _, phase in switch phase { diff --git a/TableProMobile/TableProWidget/Assets.xcassets/AccentColor.colorset/Contents.json b/TableProMobile/TableProWidget/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/TableProMobile/TableProWidget/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TableProMobile/TableProWidget/Assets.xcassets/AppIcon.appiconset/Contents.json b/TableProMobile/TableProWidget/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..230588010 --- /dev/null +++ b/TableProMobile/TableProWidget/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TableProMobile/TableProWidget/Assets.xcassets/Contents.json b/TableProMobile/TableProWidget/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/TableProMobile/TableProWidget/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TableProMobile/TableProWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json b/TableProMobile/TableProWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/TableProMobile/TableProWidget/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/TableProMobile/TableProWidget/Info.plist b/TableProMobile/TableProWidget/Info.plist new file mode 100644 index 000000000..0f118fb75 --- /dev/null +++ b/TableProMobile/TableProWidget/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/TableProMobile/TableProWidget/Views/SmallWidgetView.swift b/TableProMobile/TableProWidget/Views/SmallWidgetView.swift index 69ed6da5f..a30d23d33 100644 --- a/TableProMobile/TableProWidget/Views/SmallWidgetView.swift +++ b/TableProMobile/TableProWidget/Views/SmallWidgetView.swift @@ -4,6 +4,7 @@ // import SwiftUI +import WidgetKit struct SmallWidgetView: View { let connections: [WidgetConnectionItem] From 678cb24369d30a30678b50161e39854ef71f4eea Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 5 Apr 2026 16:51:52 +0700 Subject: [PATCH 3/4] fix: use NavigationPath for widget deep link push navigation on iPhone --- .../Views/ConnectionListView.swift | 26 +++++++++++-------- .../TableProWidgetExtension.entitlements | 8 ++++++ 2 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 TableProMobile/TableProWidgetExtension.entitlements diff --git a/TableProMobile/TableProMobile/Views/ConnectionListView.swift b/TableProMobile/TableProMobile/Views/ConnectionListView.swift index 50630e028..a8d804512 100644 --- a/TableProMobile/TableProMobile/Views/ConnectionListView.swift +++ b/TableProMobile/TableProMobile/Views/ConnectionListView.swift @@ -12,6 +12,7 @@ struct ConnectionListView: View { @State private var showingAddConnection = false @State private var editingConnection: DatabaseConnection? @State private var selectedConnection: DatabaseConnection? + @State private var navigationPath = NavigationPath() @State private var showingGroupManagement = false @State private var showingTagManagement = false @State private var filterTagId: UUID? @@ -31,17 +32,19 @@ struct ConnectionListView: View { var body: some View { NavigationSplitView { - sidebar - .navigationTitle("Connections") - .navigationDestination(for: DatabaseConnection.self) { connection in - ConnectedView(connection: connection) - } - .onChange(of: appState.pendingConnectionId) { _, newId in - navigateToPendingConnection(newId) - } - .onAppear { - navigateToPendingConnection(appState.pendingConnectionId) - } + NavigationStack(path: $navigationPath) { + sidebar + .navigationTitle("Connections") + .navigationDestination(for: DatabaseConnection.self) { connection in + ConnectedView(connection: connection) + } + } + .onChange(of: appState.pendingConnectionId) { _, newId in + navigateToPendingConnection(newId) + } + .onAppear { + navigateToPendingConnection(appState.pendingConnectionId) + } .toolbar { ToolbarItemGroup(placement: .topBarTrailing) { filterMenu @@ -245,6 +248,7 @@ struct ConnectionListView: View { private func navigateToPendingConnection(_ id: UUID?) { guard let id, let connection = appState.connections.first(where: { $0.id == id }) else { return } + navigationPath.append(connection) selectedConnection = connection appState.pendingConnectionId = nil } diff --git a/TableProMobile/TableProWidgetExtension.entitlements b/TableProMobile/TableProWidgetExtension.entitlements new file mode 100644 index 000000000..2eb7e333a --- /dev/null +++ b/TableProMobile/TableProWidgetExtension.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.application-groups + + + From 3c9ce9566a600cd1db26e58f43fdabbc578351b7 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 5 Apr 2026 17:00:37 +0700 Subject: [PATCH 4/4] fix: widget deployment target 17.0, url.host deprecation, sort connections, CHANGELOG --- CHANGELOG.md | 1 + .../TableProMobile.xcodeproj/project.pbxproj | 8 +++++-- TableProMobile/TableProMobile/AppState.swift | 22 ++++++++++--------- .../TableProMobile/TableProMobileApp.swift | 2 +- 4 files changed, 20 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 238933a47..214b20c9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - iOS: connection groups and tags +- iOS: Quick Connect Home Screen widget ## [0.27.4] - 2026-04-05 diff --git a/TableProMobile/TableProMobile.xcodeproj/project.pbxproj b/TableProMobile/TableProMobile.xcodeproj/project.pbxproj index 3193e2148..abb4c75c9 100644 --- a/TableProMobile/TableProMobile.xcodeproj/project.pbxproj +++ b/TableProMobile/TableProMobile.xcodeproj/project.pbxproj @@ -510,6 +510,7 @@ 5AA136042F82610F00ADCD58 /* TableProWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TableProWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 5AA136052F82610F00ADCD58 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 5AA136072F82610F00ADCD58 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; + 5AA136322F82675600ADCD58 /* TableProWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TableProWidgetExtension.entitlements; sourceTree = ""; }; 5AA313342F7EA5B4008EBA97 /* LibPQ.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = LibPQ.xcframework; path = ../Libs/ios/LibPQ.xcframework; sourceTree = ""; }; 5AA313352F7EA5B4008EBA97 /* Hiredis.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Hiredis.xcframework; path = ../Libs/ios/Hiredis.xcframework; sourceTree = ""; }; 5AA313362F7EA5B4008EBA97 /* OpenSSL-Crypto.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "OpenSSL-Crypto.xcframework"; path = "../Libs/ios/OpenSSL-Crypto.xcframework"; sourceTree = ""; }; @@ -1618,6 +1619,7 @@ 5AB9F3D02F7C1C12001F3337 = { isa = PBXGroup; children = ( + 5AA136322F82675600ADCD58 /* TableProWidgetExtension.entitlements */, 5AB9F3DB2F7C1C12001F3337 /* TableProMobile */, 5AA136092F82610F00ADCD58 /* TableProWidget */, 5AA313332F7EA5B4008EBA97 /* Frameworks */, @@ -1777,6 +1779,7 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = TableProWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = D7HJ5TFYCU; @@ -1784,7 +1787,7 @@ INFOPLIST_FILE = TableProWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TableProWidget; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 26.5; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1808,6 +1811,7 @@ buildSettings = { ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CODE_SIGN_ENTITLEMENTS = TableProWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = D7HJ5TFYCU; @@ -1815,7 +1819,7 @@ INFOPLIST_FILE = TableProWidget/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = TableProWidget; INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 26.5; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/TableProMobile/TableProMobile/AppState.swift b/TableProMobile/TableProMobile/AppState.swift index ade6a6ca2..4ecadd311 100644 --- a/TableProMobile/TableProMobile/AppState.swift +++ b/TableProMobile/TableProMobile/AppState.swift @@ -171,16 +171,18 @@ final class AppState { // MARK: - Widget private func updateWidgetData() { - let items = connections.map { conn in - WidgetConnectionItem( - id: conn.id, - name: conn.name.isEmpty ? conn.host : conn.name, - type: conn.type.rawValue, - host: conn.host, - port: conn.port, - sortOrder: conn.sortOrder - ) - } + let items = connections + .sorted { ($0.sortOrder, $0.name) < ($1.sortOrder, $1.name) } + .map { conn in + WidgetConnectionItem( + id: conn.id, + name: conn.name.isEmpty ? conn.host : conn.name, + type: conn.type.rawValue, + host: conn.host, + port: conn.port, + sortOrder: conn.sortOrder + ) + } SharedConnectionStore.write(items) WidgetCenter.shared.reloadAllTimelines() } diff --git a/TableProMobile/TableProMobile/TableProMobileApp.swift b/TableProMobile/TableProMobile/TableProMobileApp.swift index 0416e039d..b6ba4070e 100644 --- a/TableProMobile/TableProMobile/TableProMobileApp.swift +++ b/TableProMobile/TableProMobile/TableProMobileApp.swift @@ -26,7 +26,7 @@ struct TableProMobileApp: App { } .onOpenURL { url in guard url.scheme == "tablepro", - url.host == "connect", + url.host(percentEncoded: false) == "connect", let uuidString = url.pathComponents.dropFirst().first, let uuid = UUID(uuidString: uuidString) else { return } appState.pendingConnectionId = uuid