From ea8121700ab5ec3a20de14e2e487b21ee20bab62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Pantale=C3=A3o?= <5808343+bgoncal@users.noreply.github.com> Date: Mon, 16 Dec 2024 14:54:57 +0100 Subject: [PATCH] Fetch panels instead of subscribe --- HomeAssistant.xcodeproj/project.pbxproj | 24 +++++- Sources/App/AppDelegate.swift | 1 - Sources/App/Scenes/WebViewSceneDelegate.swift | 2 +- .../WidgetOpenPageIntent+Observation.swift | 61 -------------- .../Widgets/OpenPage/PageAppEntity.swift | 50 +++++------- .../Extensions/GRDB+Initialization.swift | 35 ++++++++ Sources/Shared/Environment/Environment.swift | 4 + .../Intents/OpenPageIntentHandler.swift | 40 +++++----- Sources/Shared/Panels/AppPanel.swift | 80 +++++++++++++++++++ Sources/Shared/Panels/PanelsUpdater.swift | 47 +++++++++++ 10 files changed, 227 insertions(+), 117 deletions(-) delete mode 100644 Sources/App/Settings/Observation/WidgetOpenPageIntent+Observation.swift create mode 100644 Sources/Shared/Panels/AppPanel.swift create mode 100644 Sources/Shared/Panels/PanelsUpdater.swift diff --git a/HomeAssistant.xcodeproj/project.pbxproj b/HomeAssistant.xcodeproj/project.pbxproj index a4745356e..e0252e7a5 100644 --- a/HomeAssistant.xcodeproj/project.pbxproj +++ b/HomeAssistant.xcodeproj/project.pbxproj @@ -416,7 +416,6 @@ 11E1639B250B1B760076D612 /* OnboardingStateObservation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E16399250B1B760076D612 /* OnboardingStateObservation.swift */; }; 11E5CF8124BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E5CF8024BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift */; }; 11E5CF8224BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E5CF8024BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift */; }; - 11E7C4B02702E03000667342 /* WidgetOpenPageIntent+Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E7C4AF2702E03000667342 /* WidgetOpenPageIntent+Observation.swift */; }; 11E99A5027156854003C8A65 /* OnboardingTerminalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11E99A4F27156854003C8A65 /* OnboardingTerminalViewController.swift */; }; 11ED43962726599D00B5FD45 /* OnboardingAuthStepModels.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11ED43952726599D00B5FD45 /* OnboardingAuthStepModels.test.swift */; }; 11ED439827265B9C00B5FD45 /* OnboardingAuthStepNotify.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 11ED439727265B9C00B5FD45 /* OnboardingAuthStepNotify.test.swift */; }; @@ -761,6 +760,8 @@ 42CE8FB62B46D14C00C707F9 /* FrontendStrings+Values.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42CE8FB42B46CAFD00C707F9 /* FrontendStrings+Values.swift */; }; 42CE8FB72B46D14C00C707F9 /* FrontendStrings+Values.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42CE8FB42B46CAFD00C707F9 /* FrontendStrings+Values.swift */; }; 42CE8FBB2B46DB6200C707F9 /* Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = B65B15042273188300635D5C /* Assets.swift */; }; + 42D334272D105990008D8E78 /* AppPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D334262D105990008D8E78 /* AppPanel.swift */; }; + 42D334282D105990008D8E78 /* AppPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D334262D105990008D8E78 /* AppPanel.swift */; }; 42D3E49C2C5BB88F00444BE6 /* WatchBatterySensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D3E49B2C5BB88F00444BE6 /* WatchBatterySensor.swift */; }; 42D3E49D2C5BB88F00444BE6 /* WatchBatterySensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D3E49B2C5BB88F00444BE6 /* WatchBatterySensor.swift */; }; 42D3E4A12C5BCD1100444BE6 /* WatchContext.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42D3E4A02C5BCD1100444BE6 /* WatchContext.test.swift */; }; @@ -783,6 +784,8 @@ 42DD84132B14ACAB00936F16 /* Color+ColorAsset.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DD84122B14ACAB00936F16 /* Color+ColorAsset.swift */; }; 42DD84162B14D7AC00936F16 /* WebViewExternalBusMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DD84142B14D68C00936F16 /* WebViewExternalBusMessage.swift */; }; 42DD84192B14D83B00936F16 /* WebViewExternalBusMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DD84182B14D83B00936F16 /* WebViewExternalBusMessageTests.swift */; }; + 42DE75D32D1061A600FF379F /* PanelsUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DE75D12D105F3000FF379F /* PanelsUpdater.swift */; }; + 42DE75D42D1061A600FF379F /* PanelsUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DE75D12D105F3000FF379F /* PanelsUpdater.swift */; }; 42DEDA9A2C5B926400E9D29D /* AppVersionSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DEDA992C5B926400E9D29D /* AppVersionSensor.swift */; }; 42DEDA9B2C5B926400E9D29D /* AppVersionSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DEDA992C5B926400E9D29D /* AppVersionSensor.swift */; }; 42DF6B2D2CCF8A2200D7EC14 /* PermissionRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42DF6B2C2CCF8A2200D7EC14 /* PermissionRequestView.swift */; }; @@ -1744,7 +1747,6 @@ 11DE9F3925B614EB0081C0ED /* Application.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = Application.xib; sourceTree = ""; }; 11E16399250B1B760076D612 /* OnboardingStateObservation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingStateObservation.swift; sourceTree = ""; }; 11E5CF8024BBCE1B009AC30F /* ProcessInfo+BackgroundTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ProcessInfo+BackgroundTask.swift"; sourceTree = ""; }; - 11E7C4AF2702E03000667342 /* WidgetOpenPageIntent+Observation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WidgetOpenPageIntent+Observation.swift"; sourceTree = ""; }; 11E99A4F27156854003C8A65 /* OnboardingTerminalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingTerminalViewController.swift; sourceTree = ""; }; 11ED43952726599D00B5FD45 /* OnboardingAuthStepModels.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepModels.test.swift; sourceTree = ""; }; 11ED439727265B9C00B5FD45 /* OnboardingAuthStepNotify.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingAuthStepNotify.test.swift; sourceTree = ""; }; @@ -2063,6 +2065,7 @@ 42CE8FAC2B46C12C00C707F9 /* Domain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Domain.swift; sourceTree = ""; }; 42CE8FAE2B46C3D600C707F9 /* CoreStrings+Values.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CoreStrings+Values.swift"; sourceTree = ""; }; 42CE8FB42B46CAFD00C707F9 /* FrontendStrings+Values.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FrontendStrings+Values.swift"; sourceTree = ""; }; + 42D334262D105990008D8E78 /* AppPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPanel.swift; sourceTree = ""; }; 42D3E49B2C5BB88F00444BE6 /* WatchBatterySensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchBatterySensor.swift; sourceTree = ""; }; 42D3E4A02C5BCD1100444BE6 /* WatchContext.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchContext.test.swift; sourceTree = ""; }; 42D3E4A72C5D00AD00444BE6 /* ScriptAppIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScriptAppIntent.swift; sourceTree = ""; }; @@ -2085,6 +2088,7 @@ 42DD84372B15DC3F00936F16 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Frontend.strings; sourceTree = ""; }; 42DD84382B15DC3F00936F16 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/InfoPlist.strings; sourceTree = ""; }; 42DD84392B15DC3F00936F16 /* et */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = et; path = et.lproj/Localizable.strings; sourceTree = ""; }; + 42DE75D12D105F3000FF379F /* PanelsUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanelsUpdater.swift; sourceTree = ""; }; 42DEDA992C5B926400E9D29D /* AppVersionSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionSensor.swift; sourceTree = ""; }; 42DF6B2C2CCF8A2200D7EC14 /* PermissionRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionRequestView.swift; sourceTree = ""; }; 42DF6B2E2CCF918D00D7EC14 /* BluetoothPermissionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BluetoothPermissionView.swift; sourceTree = ""; }; @@ -3129,7 +3133,6 @@ children = ( 11EE9B5624C68F5700404AF8 /* Action+Observation.swift */, 11A48D8024CA8ADB0021BDD9 /* NotificationCategory+Observation.swift */, - 11E7C4AF2702E03000667342 /* WidgetOpenPageIntent+Observation.swift */, ); path = Observation; sourceTree = ""; @@ -4150,6 +4153,15 @@ path = Domain; sourceTree = ""; }; + 42D334252D105981008D8E78 /* Panels */ = { + isa = PBXGroup; + children = ( + 42DE75D12D105F3000FF379F /* PanelsUpdater.swift */, + 42D334262D105990008D8E78 /* AppPanel.swift */, + ); + path = Panels; + sourceTree = ""; + }; 42D3E49F2C5BCCF600444BE6 /* Watch */ = { isa = PBXGroup; children = ( @@ -4949,6 +4961,7 @@ 426740A42B17348700C1DD73 /* Assets */, 42CE8FAB2B46C11E00C707F9 /* Domain */, 42CA28AC2B101D320093B31A /* DesignSystem */, + 42D334252D105981008D8E78 /* Panels */, 11B38EE0275C545C00205C7B /* Intents */, D014EEAA212928EC008EA6F5 /* API */, D0FF79C920D7787F0034574D /* ClientEvents */, @@ -6896,7 +6909,6 @@ 11E99A5027156854003C8A65 /* OnboardingTerminalViewController.swift in Sources */, 422F951F2CFDF7C5003B7514 /* HAApplicationShortcutItem.swift in Sources */, 1101568424D770B2009424C9 /* NFCWriter.swift in Sources */, - 11E7C4B02702E03000667342 /* WidgetOpenPageIntent+Observation.swift in Sources */, 1187DE4624D7E1BD00F0A6A6 /* SimulatorNFCManager.swift in Sources */, 1185DF96271FBB9800ED7D9A /* OnboardingAuthLogin.swift in Sources */, 425573E62B5838B600145217 /* MaterialDesignIcons+CarPlay.swift in Sources */, @@ -7220,6 +7232,7 @@ 11CFD78227364F450082D557 /* Identifier.swift in Sources */, 11AF4D17249C8083006C74C0 /* With.swift in Sources */, 42333ADB2D0B1771001E8408 /* EntityRegistryListForDisplay.swift in Sources */, + 42DE75D42D1061A600FF379F /* PanelsUpdater.swift in Sources */, 11B38EF7275C54A300205C7B /* UpdateSensorsIntentHandler.swift in Sources */, 1141182B24AFA10900E6525C /* WebhookResponseHandler.swift in Sources */, 426266462C11B02C0081A818 /* InteractiveImmediateMessages.swift in Sources */, @@ -7289,6 +7302,7 @@ 11F2F27F258725D300F61F7C /* NotificationAttachmentErrorImage.swift in Sources */, B67CE8A622200F220034C1D0 /* HAAPI.swift in Sources */, 1105CE1D272B9CB300F33BD8 /* ServerManager.swift in Sources */, + 42D334272D105990008D8E78 /* AppPanel.swift in Sources */, 1141182724AF9A0500E6525C /* WebhookManager.swift in Sources */, 1104FC9225322C1800B8BE34 /* Dictionary+Additions.swift in Sources */, 118261F824F8D6B0000795C6 /* SensorProviderDependencies.swift in Sources */, @@ -7378,6 +7392,7 @@ 11C4629124B14E6B00031902 /* XCGLogger+UNNotification.swift in Sources */, 110ED56325A563D600489AF7 /* DisplaySensor.swift in Sources */, 424151FB2CD8EF2200D7A6F9 /* MagicItem+Migration.swift in Sources */, + 42DE75D32D1061A600FF379F /* PanelsUpdater.swift in Sources */, 11AF4D16249C8083006C74C0 /* With.swift in Sources */, 11B38EEC275C54A200205C7B /* IntentHandlerFactory.swift in Sources */, 1182620124F9C3F7000795C6 /* HACoreBlahObject.swift in Sources */, @@ -7426,6 +7441,7 @@ 116C0C2F267EB90F00A992E4 /* UserDefaultsValueSync.swift in Sources */, 11A3BD2D26192210005237E6 /* LocalPushManager.swift in Sources */, 42FCD00C2B9B25D60057783F /* ThreadClientProtocol.swift in Sources */, + 42D334282D105990008D8E78 /* AppPanel.swift in Sources */, 11C4628824B109C100031902 /* WebhookResponseLocation.swift in Sources */, 11AF4D19249C8253006C74C0 /* PedometerSensor.swift in Sources */, B6872E632226841400C475D1 /* MobileAppRegistrationRequest.swift in Sources */, diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index b4f3b89fa..f5701e4d4 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -401,7 +401,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { _ = Realm.live() Action.setupObserver() NotificationCategory.setupObserver() - WidgetOpenPageIntent.setupObserver() } private func setupMenus() { diff --git a/Sources/App/Scenes/WebViewSceneDelegate.swift b/Sources/App/Scenes/WebViewSceneDelegate.swift index 20e062115..e811e2e8c 100644 --- a/Sources/App/Scenes/WebViewSceneDelegate.swift +++ b/Sources/App/Scenes/WebViewSceneDelegate.swift @@ -120,7 +120,7 @@ final class WebViewSceneDelegate: NSObject, UIWindowSceneDelegate { UIApplication.shared.applicationState == .active }) Current.periodicAppEntitiesUpdater().setup() - Current.periodicAppEntitiesUpdater().updateAppEntities() + Current.panelsUpdater().update() } func windowScene( diff --git a/Sources/App/Settings/Observation/WidgetOpenPageIntent+Observation.swift b/Sources/App/Settings/Observation/WidgetOpenPageIntent+Observation.swift deleted file mode 100644 index 0d4516da3..000000000 --- a/Sources/App/Settings/Observation/WidgetOpenPageIntent+Observation.swift +++ /dev/null @@ -1,61 +0,0 @@ -import HAKit -import PromiseKit -import Shared -import WidgetKit - -extension WidgetOpenPageIntent { - private class Observer { - var container: PerServerContainer? - - func start() { - container = .init { server in - guard let connection = Current.api(for: server)?.connection else { - Current.Log.error("No API available to ibserver open page intent") - return .init(HAMockCancellable({})) - } - let token = connection.caches.panels.subscribe { _, panels in - Self.handle(panels: panels, server: server) - } - - return .init(token) { $1.cancel() } - } - } - - enum HandlePanelsError: Error { - case unchanged - } - - private static func handle(panels: HAPanels, server: Server) { - let key = OpenPageIntentHandler.cacheKey(serverIdentifier: server.identifier.rawValue) - - firstly { - Current.diskCache.value(for: key) as Promise - }.recover { _ in - .value(HAPanels(panelsByPath: [:])) - }.then { current -> Promise in - guard panels != current else { - return .init(error: HandlePanelsError.unchanged) - } - - WidgetCenter.shared.reloadTimelines(ofKind: WidgetsKind.openPage.rawValue) - return .value(()) - }.then { - Current.diskCache.set(panels, for: key) - }.done { - Current.Log.info("updated timeline and cache due to server \(server.identifier)") - }.catch { error in - if !(error is HandlePanelsError) { - Current.Log.verbose("didn't reload panels widget from server \(server.identifier): \(error)") - } - } - } - } - - private static var observer: Observer? - - static func setupObserver() { - observer = with(Observer()) { - $0.start() - } - } -} diff --git a/Sources/Extensions/Widgets/OpenPage/PageAppEntity.swift b/Sources/Extensions/Widgets/OpenPage/PageAppEntity.swift index c393b0d20..f577ccf7f 100644 --- a/Sources/Extensions/Widgets/OpenPage/PageAppEntity.swift +++ b/Sources/Extensions/Widgets/OpenPage/PageAppEntity.swift @@ -30,7 +30,11 @@ struct PageAppEntityQuery: EntityQuery, EntityStringQuery { panels.filter({ panel in identifiers.contains(id(for: panel, server: server)) }).compactMap { panel in - PageAppEntity(id: id(for: panel, server: server), panel: panel, serverId: server.identifier.rawValue) + PageAppEntity( + id: id(for: panel, server: server), + panel: toHAPanel(appPanel: panel), + serverId: server.identifier.rawValue + ) } } } @@ -42,7 +46,7 @@ struct PageAppEntityQuery: EntityQuery, EntityStringQuery { }).map({ panel in PageAppEntity( id: id(for: panel, server: server), - panel: panel, + panel: toHAPanel(appPanel: panel), serverId: server.identifier.rawValue ) })) @@ -54,42 +58,30 @@ struct PageAppEntityQuery: EntityQuery, EntityStringQuery { .init(.init(stringLiteral: server.info.name), items: panels.map({ panel in PageAppEntity( id: id(for: panel, server: server), - panel: panel, + panel: toHAPanel(appPanel: panel), serverId: server.identifier.rawValue ) })) })) } - func id(for panel: HAPanel, server: Server) -> String { + func id(for panel: AppPanel, server: Server) -> String { "\(server.identifier.rawValue)-\(panel.path)" } - private func panels() async throws -> [Server: [HAPanel]] { - await withCheckedContinuation { continuation in - var panelsPerServer: [Server: [HAPanel]] = [:] - var finishedPipesCount = 0 - for server in Current.servers.all { - ( - Current.diskCache - .value( - for: OpenPageIntentHandler - .cacheKey(serverIdentifier: server.identifier.rawValue) - ) as Promise - ).pipe { result in - switch result { - case let .fulfilled(panels): - panelsPerServer[server] = panels.allPanels - case let .rejected(error): - Current.Log.error("Failed to retrieve HAPanels, error: \(error.localizedDescription)") - } - finishedPipesCount += 1 + // Since AppPanels came afterwards we need to keep the same + // object as before to not break previously created widgets + private func toHAPanel(appPanel: AppPanel) -> HAPanel { + .init( + icon: appPanel.icon, + title: appPanel.title, + path: appPanel.path, + component: appPanel.component, + showInSidebar: appPanel.showInSidebar + ) + } - if finishedPipesCount == Current.servers.all.count { - continuation.resume(returning: panelsPerServer) - } - } - } - } + private func panels() async throws -> [Server: [AppPanel]] { + try AppPanel.panelsPerServer() } } diff --git a/Sources/Shared/Common/Extensions/GRDB+Initialization.swift b/Sources/Shared/Common/Extensions/GRDB+Initialization.swift index aa040c460..c773f1700 100644 --- a/Sources/Shared/Common/Extensions/GRDB+Initialization.swift +++ b/Sources/Shared/Common/Extensions/GRDB+Initialization.swift @@ -8,6 +8,7 @@ public enum GRDBDatabaseTable: String { case carPlayConfig case clientEvent case appEntityRegistryListForDisplay + case appPanel } public enum DatabaseTables { @@ -55,6 +56,16 @@ public enum DatabaseTables { case entityId case registry } + + public enum AppPanel: String { + case id + case serverId + case icon + case title + case path + case component + case showInSidebar + } } public extension DatabaseQueue { @@ -94,6 +105,7 @@ public extension DatabaseQueue { var shouldCreateCarPlayConfig = false var shouldCreateClientEvent = false var shouldCreateAppEntityRegistryListForDisplay = false + var shouldCreateAppPanel = false do { try database.read { db in @@ -104,6 +116,8 @@ public extension DatabaseQueue { shouldCreateClientEvent = try !db.tableExists(GRDBDatabaseTable.clientEvent.rawValue) shouldCreateAppEntityRegistryListForDisplay = try !db .tableExists(GRDBDatabaseTable.appEntityRegistryListForDisplay.rawValue) + shouldCreateAppPanel = try !db + .tableExists(GRDBDatabaseTable.appPanel.rawValue) } } catch { let errorMessage = "Failed to check if GRDB tables exist, error: \(error.localizedDescription)" @@ -210,5 +224,26 @@ public extension DatabaseQueue { Current.Log.error(errorMessage) } } + + // AppPanel + if shouldCreateAppPanel { + do { + try database.write { db in + try db.create(table: GRDBDatabaseTable.appPanel.rawValue) { t in + t.primaryKey(DatabaseTables.AppPanel.id.rawValue, .text).notNull() + t.column(DatabaseTables.AppPanel.serverId.rawValue, .text).notNull() + t.column(DatabaseTables.AppPanel.icon.rawValue, .text) + t.column(DatabaseTables.AppPanel.title.rawValue, .text).notNull() + t.column(DatabaseTables.AppPanel.path.rawValue, .text).notNull() + t.column(DatabaseTables.AppPanel.component.rawValue, .text).notNull() + t.column(DatabaseTables.AppPanel.showInSidebar.rawValue, .boolean).notNull() + } + } + } catch { + let errorMessage = + "Failed to create AppPanel GRDB table, error: \(error.localizedDescription)" + Current.Log.error(errorMessage) + } + } } } diff --git a/Sources/Shared/Environment/Environment.swift b/Sources/Shared/Environment/Environment.swift index 32f1b463d..15f3c3293 100644 --- a/Sources/Shared/Environment/Environment.swift +++ b/Sources/Shared/Environment/Environment.swift @@ -122,6 +122,10 @@ public class AppEnvironment { PeriodicAppEntitiesModelUpdater.shared } + public var panelsUpdater: () -> PanelsUpdaterProtocol = { + PanelsUpdater.shared + } + #if os(iOS) public var realmFatalPresentation: ((UIViewController) -> Void)? #endif diff --git a/Sources/Shared/Intents/OpenPageIntentHandler.swift b/Sources/Shared/Intents/OpenPageIntentHandler.swift index da2b95a01..ff1fa96a2 100644 --- a/Sources/Shared/Intents/OpenPageIntentHandler.swift +++ b/Sources/Shared/Intents/OpenPageIntentHandler.swift @@ -8,29 +8,27 @@ public class OpenPageIntentHandler: NSObject, OpenPageIntentHandling, WidgetOpen public static func panels(completion: @escaping ([IntentPanel]) -> Void) { var intentPanels: [IntentPanel] = [] - var finishedPipesCount = 0 - for server in Current.servers.all { - ( - Current.diskCache - .value( - for: OpenPageIntentHandler - .cacheKey(serverIdentifier: server.identifier.rawValue) - ) as Promise - ).pipe { result in - switch result { - case let .fulfilled(panels): - intentPanels.append(contentsOf: panels.allPanels.map { haPanel in - IntentPanel(panel: haPanel, server: server) - }) - case let .rejected(error): - Current.Log.error("Failed to retrieve HAPanels, error: \(error.localizedDescription)") - } - finishedPipesCount += 1 + do { + let panelsPerServer = try AppPanel.panelsPerServer() - if finishedPipesCount == Current.servers.all.count { - completion(intentPanels) - } + for (server, panels) in panelsPerServer { + intentPanels.append(contentsOf: panels.map { appPanel in + IntentPanel( + panel: .init( + icon: appPanel.icon, + title: appPanel.title, + path: appPanel.path, + component: appPanel.component, + showInSidebar: appPanel.showInSidebar + ), + server: server + ) + }) } + completion(intentPanels) + } catch { + Current.Log.error("Widget error fetching panels: \(error)") + completion([]) } } diff --git a/Sources/Shared/Panels/AppPanel.swift b/Sources/Shared/Panels/AppPanel.swift new file mode 100644 index 000000000..193daa996 --- /dev/null +++ b/Sources/Shared/Panels/AppPanel.swift @@ -0,0 +1,80 @@ +import Foundation +import GRDB +import HAKit + +public struct AppPanel: Codable, FetchableRecord, PersistableRecord { + public var id: String = UUID().uuidString + public var serverId: String = "" + public var icon: String? = nil + public var title: String = "" + public var path: String = "" + public var component: String = "" + public var showInSidebar: Bool = true + + public init( + id: String = UUID().uuidString, + serverId: String, + icon: String? = nil, + title: String, + path: String, + component: String, + showInSidebar: Bool + ) { + self.id = id + self.serverId = serverId + self.icon = icon + self.title = title + self.path = path + self.component = component + self.showInSidebar = showInSidebar + } + + public static func panels(serverId: String) throws -> [AppPanel]? { + try Current.database.read({ db in + try AppPanel + .filter( + Column(DatabaseTables.AppPanel.serverId.rawValue) == serverId + ) + .fetchAll(db) + }) + } + + public static func panelsPerServer() throws -> [Server: [AppPanel]] { + var panelsPerServer: [Server: [AppPanel]] = [:] + var finishedPipesCount = 0 + for server in Current.servers.all { + do { + if let panels = try AppPanel.panels(serverId: server.identifier.rawValue), !panels.isEmpty { + panelsPerServer[server] = panels + } + } catch { + Current.Log.error("Widget error fetching panels for server \(server.identifier.rawValue): \(error)") + } + } + return panelsPerServer + } +} + +struct HAPanelResponse: HADataDecodable { + let componentName: String? + let icon: String? + let title: String? + let config: String? + let urlPath: String? + + enum CodingKeys: String, CodingKey { + case componentName = "component_name" + case icon + case title + case config + case urlPath = "url_path" + } + + init(data: HAData) throws { + self.componentName = try data.decode("component_name") + self.icon = try data.decode("icon") + self.title = try data.decode("title") + self.config = try data.decode("config") + self.urlPath = try data.decode("url_path") + } +} diff --git a/Sources/Shared/Panels/PanelsUpdater.swift b/Sources/Shared/Panels/PanelsUpdater.swift new file mode 100644 index 000000000..7718ee6b0 --- /dev/null +++ b/Sources/Shared/Panels/PanelsUpdater.swift @@ -0,0 +1,47 @@ +import Foundation +import PromiseKit + +public protocol PanelsUpdaterProtocol { + func update() +} + +final class PanelsUpdater: PanelsUpdaterProtocol { + static var shared = PanelsUpdater() + + private var tokens: [(promise: Promise, cancel: () -> Void)?] = [] + + public func update() { + tokens.forEach({ $0?.cancel() }) + tokens = [] + for server in Current.servers.all { + let request = Current.api(for: server)?.connection.send(.panels()) + tokens.append(request) + request?.promise.done({ [weak self] panels in + self?.saveInDatabase(panels, server: server) + }).cauterize() + } + } + + private func saveInDatabase(_ panels: HAPanels, server: Server) { + let appPanels = panels.allPanels.map { panel in + AppPanel( + serverId: server.identifier.rawValue, + icon: panel.icon, + title: panel.title, + path: panel.path, + component: panel.component, + showInSidebar: panel.showInSidebar + ) + } + + do { + try Current.database.write { db in + for panel in appPanels { + try panel.save(db) + } + } + } catch { + Current.Log.error("Error saving panels in database: \(error)") + } + } +}