From bbb8457a650102b54c408f289f2a6fe810bbb032 Mon Sep 17 00:00:00 2001 From: Fernando Bunn Date: Fri, 2 Oct 2020 21:10:17 +0100 Subject: [PATCH] Version 2.0.0 (#5) New Pi Monitor Widget iPad OS support --- PiStatsMobile/IntentHandler/Info.plist | 42 +++ .../IntentHandler/IntentHandler.entitlements | 10 + .../IntentHandler/IntentHandler.swift | 40 +++ .../PiStatsMobile.xcodeproj/project.pbxproj | 248 +++++++++++++++++- .../xcshareddata/swiftpm/Package.resolved | 4 +- .../Base.lproj/PiholeIntents.intentdefinition | 167 ++++++++++++ .../PiStatsMobile/PiStatsMobileApp.swift | 11 - .../PiStatsMobile/Supporting Files/Info.plist | 4 + .../PiStatsMobile/Utils/Constants.swift | 12 + .../Utils/DisableDurationManager.swift | 2 +- .../Utils/UserDefaultExtensions.swift | 3 +- .../PiStatsMobile/Utils/ViewUtils.swift | 24 ++ .../Views/Piholes/MetricsView.swift | 4 +- .../Views/Piholes/PiholeStatsList.swift | 80 ++++-- .../en.lproj/PiholeIntents.strings | 7 + .../pt-BR.lproj/PiholeIntents.strings | 7 + .../Contents.json | 38 +++ .../Core/PiMonitorTimelineProvider.swift | 65 +++++ .../PiStatsWidget/PiMonitorWidget.swift | 49 ++++ .../PiStatsWidget/PiStatsWidgets.swift | 2 +- .../PiStatsWidget/ViewStatsWidget.swift | 2 - .../Views/CircleBadgeStatus.swift | 15 +- .../PiMonitor/PiMonitorStatusHeader.swift | 32 +++ .../Views/PiMonitor/PiMonitorView.swift | 104 ++++++++ .../Views/PiMonitor/PiMonitorWidgetView.swift | 49 ++++ PiStatsMobile/Shared/Core/Pihole.swift | 18 +- .../Shared/Core/PiholeDataProvider.swift | 9 +- .../Core/UserData/UserPreferences.swift | 73 +++--- PiStatsMobile/Shared/StatsItemType.swift | 8 +- PiStatsMobile/Shared/UIConstants.swift | 10 +- 30 files changed, 1030 insertions(+), 109 deletions(-) create mode 100644 PiStatsMobile/IntentHandler/Info.plist create mode 100644 PiStatsMobile/IntentHandler/IntentHandler.entitlements create mode 100644 PiStatsMobile/IntentHandler/IntentHandler.swift create mode 100644 PiStatsMobile/PiStatsMobile/Base.lproj/PiholeIntents.intentdefinition create mode 100644 PiStatsMobile/PiStatsMobile/Utils/Constants.swift create mode 100644 PiStatsMobile/PiStatsMobile/Utils/ViewUtils.swift create mode 100644 PiStatsMobile/PiStatsMobile/en.lproj/PiholeIntents.strings create mode 100644 PiStatsMobile/PiStatsMobile/pt-BR.lproj/PiholeIntents.strings create mode 100644 PiStatsMobile/PiStatsWidget/Assets.xcassets/PiMonitorWidgetBackground.colorset/Contents.json create mode 100644 PiStatsMobile/PiStatsWidget/Core/PiMonitorTimelineProvider.swift create mode 100644 PiStatsMobile/PiStatsWidget/PiMonitorWidget.swift create mode 100644 PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorStatusHeader.swift create mode 100644 PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorView.swift create mode 100644 PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorWidgetView.swift diff --git a/PiStatsMobile/IntentHandler/Info.plist b/PiStatsMobile/IntentHandler/Info.plist new file mode 100644 index 0000000..8a89a7a --- /dev/null +++ b/PiStatsMobile/IntentHandler/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + IntentHandler + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSExtension + + NSExtensionAttributes + + IntentsRestrictedWhileLocked + + IntentsRestrictedWhileProtectedDataUnavailable + + IntentsSupported + + SelectPiholeIntent + + + NSExtensionPointIdentifier + com.apple.intents-service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).IntentHandler + + + diff --git a/PiStatsMobile/IntentHandler/IntentHandler.entitlements b/PiStatsMobile/IntentHandler/IntentHandler.entitlements new file mode 100644 index 0000000..832a3a7 --- /dev/null +++ b/PiStatsMobile/IntentHandler/IntentHandler.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.dev.bunn.PiStatsMobile + + + diff --git a/PiStatsMobile/IntentHandler/IntentHandler.swift b/PiStatsMobile/IntentHandler/IntentHandler.swift new file mode 100644 index 0000000..3abf026 --- /dev/null +++ b/PiStatsMobile/IntentHandler/IntentHandler.swift @@ -0,0 +1,40 @@ +// +// IntentHandler.swift +// IntentHandler +// +// Created by Fernando Bunn on 30/09/2020. +// + +import Intents + +class IntentHandler: INExtension, SelectPiholeIntentHandling { + + func providePiholeOptionsCollection(for intent: SelectPiholeIntent, with completion: @escaping (INObjectCollection?, Error?) -> Void) { + let piholes = validPiholes().map { + PiholeIntent( + identifier: $0.id.uuidString, + display: $0.title + ) + } + let collection = INObjectCollection(items: piholes) + completion(collection, nil) + } + + func defaultPihole(for intent: SelectPiholeIntent) -> PiholeIntent? { + if let pihole = validPiholes().first { + return PiholeIntent( + identifier: pihole.id.uuidString, + display: pihole.title + ) + } + return nil + } + + override func handler(for intent: INIntent) -> Any { + return self + } + + private func validPiholes() -> [Pihole] { + Pihole.restoreAll().filter{ $0.hasPiMonitor } + } +} diff --git a/PiStatsMobile/PiStatsMobile.xcodeproj/project.pbxproj b/PiStatsMobile/PiStatsMobile.xcodeproj/project.pbxproj index 12ab58c..a9ba195 100644 --- a/PiStatsMobile/PiStatsMobile.xcodeproj/project.pbxproj +++ b/PiStatsMobile/PiStatsMobile.xcodeproj/project.pbxproj @@ -13,12 +13,32 @@ 310647D024BD120600E2DA90 /* PiStatsDisplayWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310647CF24BD120600E2DA90 /* PiStatsDisplayWidgetView.swift */; }; 310647D224BD13D700E2DA90 /* MediumStatsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 310647D124BD13D700E2DA90 /* MediumStatsItem.swift */; }; 31115BC824B29A1700C91212 /* PiholeSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31115BC724B29A1700C91212 /* PiholeSetupView.swift */; }; + 311A3E1C25252504005464D2 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319B082A24B53246008B3C2F /* Logger.swift */; }; + 311A3E2225252509005464D2 /* UserPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319B082F24B53269008B3C2F /* UserPreferences.swift */; }; + 311A3E282525250C005464D2 /* APIToken.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319B082E24B53268008B3C2F /* APIToken.swift */; }; + 311A3E2E2525250E005464D2 /* KeyChainPasswordItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319B082C24B53255008B3C2F /* KeyChainPasswordItem.swift */; }; + 311A3E3425252544005464D2 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3132CDBE252522DA00542F6F /* IntentHandler.swift */; }; + 311A3E4425252766005464D2 /* UserDefaultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3182E28F24BB6DA100BF53D9 /* UserDefaultExtensions.swift */; }; + 311A3E4A2525276A005464D2 /* Pihole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319B082624B5321C008B3C2F /* Pihole.swift */; }; + 311A3E5625252775005464D2 /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311D0A7824B0F405003F18DB /* UIConstants.swift */; }; + 311A3E5C25252778005464D2 /* StatsItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311D0A7624B0EFE3003F18DB /* StatsItemType.swift */; }; + 311A3E68252527D9005464D2 /* SwiftHole in Frameworks */ = {isa = PBXBuildFile; productRef = 311A3E67252527D9005464D2 /* SwiftHole */; }; + 311A3E6A252527DC005464D2 /* PiMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = 311A3E69252527DC005464D2 /* PiMonitor */; }; 311D0A7524B0ED05003F18DB /* StatsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311D0A7424B0ED05003F18DB /* StatsItemView.swift */; }; 311D0A7724B0EFE3003F18DB /* StatsItemType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311D0A7624B0EFE3003F18DB /* StatsItemType.swift */; }; 311D0A7924B0F405003F18DB /* UIConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311D0A7824B0F405003F18DB /* UIConstants.swift */; }; 311D0A7B24B0F613003F18DB /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 311D0A7A24B0F613003F18DB /* StatsView.swift */; }; + 3132CDBF252522DA00542F6F /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3132CDBE252522DA00542F6F /* IntentHandler.swift */; }; + 3132CDC3252522DA00542F6F /* IntentHandler.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 3132CDBC252522DA00542F6F /* IntentHandler.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 3132CDCC252522F400542F6F /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3132CDBE252522DA00542F6F /* IntentHandler.swift */; }; + 313599D32527224300A12AC9 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 313599D22527224300A12AC9 /* Constants.swift */; }; + 313599D42527224300A12AC9 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 313599D22527224300A12AC9 /* Constants.swift */; }; + 313599D52527224300A12AC9 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 313599D22527224300A12AC9 /* Constants.swift */; }; 3137D20D24D217CD0021ACD3 /* PiMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = 3137D20C24D217CD0021ACD3 /* PiMonitor */; }; 3142B27A24CDE2EE00484AB2 /* MetricsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3142B27924CDE2EE00484AB2 /* MetricsView.swift */; }; + 315825AD25275812001160EA /* PiholeIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 315825B125275812001160EA /* PiholeIntents.intentdefinition */; }; + 315825AE25275812001160EA /* PiholeIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 315825B125275812001160EA /* PiholeIntents.intentdefinition */; }; + 315825AF25275812001160EA /* PiholeIntents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 315825B125275812001160EA /* PiholeIntents.intentdefinition */; }; 315F5EC024BA162600FED38F /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 315F5EBF24BA162600FED38F /* WidgetKit.framework */; }; 315F5EC224BA162600FED38F /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 315F5EC124BA162600FED38F /* SwiftUI.framework */; }; 315F5EC524BA162600FED38F /* ViewStatsWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 315F5EC424BA162600FED38F /* ViewStatsWidget.swift */; }; @@ -40,6 +60,9 @@ 3175EB6324AE597A00E7B5FF /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3175EB6224AE597A00E7B5FF /* Preview Assets.xcassets */; }; 3175EB6E24AE597A00E7B5FF /* PiStatsMobileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3175EB6D24AE597A00E7B5FF /* PiStatsMobileTests.swift */; }; 3175EB7924AE597A00E7B5FF /* PiStatsMobileUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3175EB7824AE597A00E7B5FF /* PiStatsMobileUITests.swift */; }; + 317636A2252658F5005AED55 /* ViewUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317636A1252658F5005AED55 /* ViewUtils.swift */; }; + 317636A3252658F5005AED55 /* ViewUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317636A1252658F5005AED55 /* ViewUtils.swift */; }; + 317636AB25265AB7005AED55 /* PiMonitorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317636A925265AB7005AED55 /* PiMonitorView.swift */; }; 3176C733251FB547002DF0A3 /* DisableDurationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3176C732251FB547002DF0A3 /* DisableDurationManager.swift */; }; 317D481D24B3ADE200897EA9 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 317D481F24B3ADE200897EA9 /* Localizable.strings */; }; 317D482224B3D2F000897EA9 /* CodeScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 317D482124B3D2F000897EA9 /* CodeScannerView.swift */; }; @@ -47,6 +70,7 @@ 3182E29024BB6DA100BF53D9 /* UserDefaultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3182E28F24BB6DA100BF53D9 /* UserDefaultExtensions.swift */; }; 3182E29124BB6DA100BF53D9 /* UserDefaultExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3182E28F24BB6DA100BF53D9 /* UserDefaultExtensions.swift */; }; 319771AC24DB24BA0005E6BF /* ScannedPihole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319771AB24DB24BA0005E6BF /* ScannedPihole.swift */; }; + 31986A9425252AC500261BF5 /* PiMonitorTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31986A9325252AC500261BF5 /* PiMonitorTimelineProvider.swift */; }; 319B082824B5321C008B3C2F /* Pihole.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319B082624B5321C008B3C2F /* Pihole.swift */; }; 319B082924B5321C008B3C2F /* PiholeDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319B082724B5321C008B3C2F /* PiholeDataProvider.swift */; }; 319B082B24B53246008B3C2F /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 319B082A24B53246008B3C2F /* Logger.swift */; }; @@ -61,11 +85,21 @@ 31AECCFC24C7A55A00E381E0 /* PiholeEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AECCFB24C7A55A00E381E0 /* PiholeEntry.swift */; }; 31AECCFE24C7A57300E381E0 /* PiholeTimelineProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31AECCFD24C7A57300E381E0 /* PiholeTimelineProvider.swift */; }; 31BCBE6324D2197B0077B6AE /* PiMonitor in Frameworks */ = {isa = PBXBuildFile; productRef = 31BCBE6224D2197B0077B6AE /* PiMonitor */; }; + 31C1A585252691A900431C87 /* PiMonitorStatusHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31C1A584252691A900431C87 /* PiMonitorStatusHeader.swift */; }; 31DB2A2A2517B1C200D67F9E /* CustomDurationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31DB2A292517B1C200D67F9E /* CustomDurationsView.swift */; }; + 31E153072523AD61008DD6CD /* PiMonitorWidgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E153012523AD44008DD6CD /* PiMonitorWidgetView.swift */; }; + 31E153102523AD69008DD6CD /* PiMonitorWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31E152FB2523AC62008DD6CD /* PiMonitorWidget.swift */; }; 31FC1DAA24B64BA900319E6F /* PiholeDataProviderListManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31FC1DA924B64BA900319E6F /* PiholeDataProviderListManager.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 3132CDC1252522DA00542F6F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 3175EB5024AE597900E7B5FF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 3132CDBB252522DA00542F6F; + remoteInfo = IntentHandler; + }; 315F5EC924BA162800FED38F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 3175EB5024AE597900E7B5FF /* Project object */; @@ -97,6 +131,7 @@ dstSubfolderSpec = 13; files = ( 315F5ECB24BA162800FED38F /* PiStatsWidgetExtension.appex in Embed App Extensions */, + 3132CDC3252522DA00542F6F /* IntentHandler.appex in Embed App Extensions */, ); name = "Embed App Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -114,13 +149,20 @@ 311D0A7624B0EFE3003F18DB /* StatsItemType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsItemType.swift; sourceTree = ""; }; 311D0A7824B0F405003F18DB /* UIConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIConstants.swift; sourceTree = ""; }; 311D0A7A24B0F613003F18DB /* StatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsView.swift; sourceTree = ""; }; + 3132CDBC252522DA00542F6F /* IntentHandler.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = IntentHandler.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 3132CDBE252522DA00542F6F /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = ""; }; + 3132CDC0252522DA00542F6F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 313599D22527224300A12AC9 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 3142B27924CDE2EE00484AB2 /* MetricsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MetricsView.swift; sourceTree = ""; }; + 315825B025275812001160EA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Base.lproj/PiholeIntents.intentdefinition; sourceTree = ""; }; + 315825B825275832001160EA /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/PiholeIntents.strings; sourceTree = ""; }; 315F5EBD24BA162600FED38F /* PiStatsWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PiStatsWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 315F5EBF24BA162600FED38F /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 315F5EC124BA162600FED38F /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 315F5EC424BA162600FED38F /* ViewStatsWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewStatsWidget.swift; sourceTree = ""; }; 315F5EC624BA162800FED38F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 315F5EC824BA162800FED38F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3171772B25277BBA001B1CAB /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/PiholeIntents.strings"; sourceTree = ""; }; 3175EB5824AE597900E7B5FF /* PiStatsMobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PiStatsMobile.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3175EB5B24AE597900E7B5FF /* PiStatsMobileApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiStatsMobileApp.swift; sourceTree = ""; }; 3175EB5D24AE597900E7B5FF /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -133,6 +175,8 @@ 3175EB7424AE597A00E7B5FF /* PiStatsMobileUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PiStatsMobileUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3175EB7824AE597A00E7B5FF /* PiStatsMobileUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiStatsMobileUITests.swift; sourceTree = ""; }; 3175EB7A24AE597A00E7B5FF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 317636A1252658F5005AED55 /* ViewUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewUtils.swift; sourceTree = ""; }; + 317636A925265AB7005AED55 /* PiMonitorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiMonitorView.swift; sourceTree = ""; }; 3176C732251FB547002DF0A3 /* DisableDurationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableDurationManager.swift; sourceTree = ""; }; 317D481E24B3ADE200897EA9 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; 317D482024B3ADEA00897EA9 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; @@ -141,6 +185,8 @@ 3182E28B24BB6A2300BF53D9 /* PiStatsMobile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = PiStatsMobile.entitlements; sourceTree = ""; }; 3182E28F24BB6DA100BF53D9 /* UserDefaultExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultExtensions.swift; sourceTree = ""; }; 319771AB24DB24BA0005E6BF /* ScannedPihole.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScannedPihole.swift; sourceTree = ""; }; + 31986A9325252AC500261BF5 /* PiMonitorTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiMonitorTimelineProvider.swift; sourceTree = ""; }; + 31986AA025252D9600261BF5 /* IntentHandler.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = IntentHandler.entitlements; sourceTree = ""; }; 319B082624B5321C008B3C2F /* Pihole.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pihole.swift; sourceTree = ""; }; 319B082724B5321C008B3C2F /* PiholeDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PiholeDataProvider.swift; sourceTree = ""; }; 319B082A24B53246008B3C2F /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; @@ -154,12 +200,24 @@ 31AECCC424C7A17400E381E0 /* PiStatsWidgets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiStatsWidgets.swift; sourceTree = ""; }; 31AECCFB24C7A55A00E381E0 /* PiholeEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiholeEntry.swift; sourceTree = ""; }; 31AECCFD24C7A57300E381E0 /* PiholeTimelineProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiholeTimelineProvider.swift; sourceTree = ""; }; + 31C1A584252691A900431C87 /* PiMonitorStatusHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiMonitorStatusHeader.swift; sourceTree = ""; }; 31D1F58A24BB7F8500D50FDB /* StatusHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusHeaderView.swift; sourceTree = ""; }; 31DB2A292517B1C200D67F9E /* CustomDurationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomDurationsView.swift; sourceTree = ""; }; + 31E152FB2523AC62008DD6CD /* PiMonitorWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiMonitorWidget.swift; sourceTree = ""; }; + 31E153012523AD44008DD6CD /* PiMonitorWidgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiMonitorWidgetView.swift; sourceTree = ""; }; 31FC1DA924B64BA900319E6F /* PiholeDataProviderListManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PiholeDataProviderListManager.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 3132CDB9252522DA00542F6F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 311A3E6A252527DC005464D2 /* PiMonitor in Frameworks */, + 311A3E68252527D9005464D2 /* SwiftHole in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 315F5EBA24BA162600FED38F /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -210,6 +268,7 @@ 310647CA24BD10D600E2DA90 /* Views */ = { isa = PBXGroup; children = ( + 31C1A5832526919300431C87 /* PiMonitor */, 310647CB24BD10E900E2DA90 /* CircleBadgeStatus.swift */, 310647CD24BD115F00E2DA90 /* SmallStatsItem.swift */, 310647D124BD13D700E2DA90 /* MediumStatsItem.swift */, @@ -218,6 +277,16 @@ path = Views; sourceTree = ""; }; + 3132CDBD252522DA00542F6F /* IntentHandler */ = { + isa = PBXGroup; + children = ( + 31986AA025252D9600261BF5 /* IntentHandler.entitlements */, + 3132CDBE252522DA00542F6F /* IntentHandler.swift */, + 3132CDC0252522DA00542F6F /* Info.plist */, + ); + path = IntentHandler; + sourceTree = ""; + }; 315F5EBE24BA162600FED38F /* Frameworks */ = { isa = PBXGroup; children = ( @@ -233,6 +302,7 @@ 31AECCFA24C7A54900E381E0 /* Core */, 310647CA24BD10D600E2DA90 /* Views */, 315F5EC424BA162600FED38F /* ViewStatsWidget.swift */, + 31E152FB2523AC62008DD6CD /* PiMonitorWidget.swift */, 31AECCC424C7A17400E381E0 /* PiStatsWidgets.swift */, 315F5EC624BA162800FED38F /* Assets.xcassets */, 315F5EC824BA162800FED38F /* Info.plist */, @@ -259,6 +329,7 @@ 3175EB6C24AE597A00E7B5FF /* PiStatsMobileTests */, 3175EB7724AE597A00E7B5FF /* PiStatsMobileUITests */, 315F5EC324BA162600FED38F /* PiStatsWidget */, + 3132CDBD252522DA00542F6F /* IntentHandler */, 315F5EBE24BA162600FED38F /* Frameworks */, 3175EB5924AE597900E7B5FF /* Products */, ); @@ -271,6 +342,7 @@ 3175EB6924AE597A00E7B5FF /* PiStatsMobileTests.xctest */, 3175EB7424AE597A00E7B5FF /* PiStatsMobileUITests.xctest */, 315F5EBD24BA162600FED38F /* PiStatsWidgetExtension.appex */, + 3132CDBC252522DA00542F6F /* IntentHandler.appex */, ); name = Products; sourceTree = ""; @@ -285,6 +357,7 @@ 319F965B24B2658300B680D3 /* Views */, 317D481724B3ACCE00897EA9 /* Utils */, 317D481824B3ACDD00897EA9 /* Supporting Files */, + 315825B125275812001160EA /* PiholeIntents.intentdefinition */, ); path = PiStatsMobile; sourceTree = ""; @@ -322,6 +395,8 @@ 317D482124B3D2F000897EA9 /* CodeScannerView.swift */, 3182E28F24BB6DA100BF53D9 /* UserDefaultExtensions.swift */, 319771AB24DB24BA0005E6BF /* ScannedPihole.swift */, + 317636A1252658F5005AED55 /* ViewUtils.swift */, + 313599D22527224300A12AC9 /* Constants.swift */, ); path = Utils; sourceTree = ""; @@ -369,11 +444,22 @@ isa = PBXGroup; children = ( 31AECCFB24C7A55A00E381E0 /* PiholeEntry.swift */, + 31986A9325252AC500261BF5 /* PiMonitorTimelineProvider.swift */, 31AECCFD24C7A57300E381E0 /* PiholeTimelineProvider.swift */, ); path = Core; sourceTree = ""; }; + 31C1A5832526919300431C87 /* PiMonitor */ = { + isa = PBXGroup; + children = ( + 31E153012523AD44008DD6CD /* PiMonitorWidgetView.swift */, + 317636A925265AB7005AED55 /* PiMonitorView.swift */, + 31C1A584252691A900431C87 /* PiMonitorStatusHeader.swift */, + ); + path = PiMonitor; + sourceTree = ""; + }; 31F3DFE224B50E84001D4679 /* Piholes */ = { isa = PBXGroup; children = ( @@ -410,6 +496,27 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 3132CDBB252522DA00542F6F /* IntentHandler */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3132CDC4252522DA00542F6F /* Build configuration list for PBXNativeTarget "IntentHandler" */; + buildPhases = ( + 3132CDB8252522DA00542F6F /* Sources */, + 3132CDB9252522DA00542F6F /* Frameworks */, + 3132CDBA252522DA00542F6F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IntentHandler; + packageProductDependencies = ( + 311A3E67252527D9005464D2 /* SwiftHole */, + 311A3E69252527DC005464D2 /* PiMonitor */, + ); + productName = IntentHandler; + productReference = 3132CDBC252522DA00542F6F /* IntentHandler.appex */; + productType = "com.apple.product-type.app-extension"; + }; 315F5EBC24BA162600FED38F /* PiStatsWidgetExtension */ = { isa = PBXNativeTarget; buildConfigurationList = 315F5ECF24BA162800FED38F /* Build configuration list for PBXNativeTarget "PiStatsWidgetExtension" */; @@ -444,6 +551,7 @@ ); dependencies = ( 315F5ECA24BA162800FED38F /* PBXTargetDependency */, + 3132CDC2252522DA00542F6F /* PBXTargetDependency */, ); name = PiStatsMobile; packageProductDependencies = ( @@ -499,6 +607,9 @@ LastSwiftUpdateCheck = 1200; LastUpgradeCheck = 1200; TargetAttributes = { + 3132CDBB252522DA00542F6F = { + CreatedOnToolsVersion = 12.0.1; + }; 315F5EBC24BA162600FED38F = { CreatedOnToolsVersion = 12.0; }; @@ -537,11 +648,19 @@ 3175EB6824AE597A00E7B5FF /* PiStatsMobileTests */, 3175EB7324AE597A00E7B5FF /* PiStatsMobileUITests */, 315F5EBC24BA162600FED38F /* PiStatsWidgetExtension */, + 3132CDBB252522DA00542F6F /* IntentHandler */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 3132CDBA252522DA00542F6F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 315F5EBB24BA162600FED38F /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -577,25 +696,52 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 3132CDB8252522DA00542F6F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 311A3E4A2525276A005464D2 /* Pihole.swift in Sources */, + 313599D52527224300A12AC9 /* Constants.swift in Sources */, + 311A3E1C25252504005464D2 /* Logger.swift in Sources */, + 311A3E2225252509005464D2 /* UserPreferences.swift in Sources */, + 311A3E4425252766005464D2 /* UserDefaultExtensions.swift in Sources */, + 311A3E282525250C005464D2 /* APIToken.swift in Sources */, + 311A3E2E2525250E005464D2 /* KeyChainPasswordItem.swift in Sources */, + 311A3E5625252775005464D2 /* UIConstants.swift in Sources */, + 3132CDBF252522DA00542F6F /* IntentHandler.swift in Sources */, + 315825AF25275812001160EA /* PiholeIntents.intentdefinition in Sources */, + 311A3E5C25252778005464D2 /* StatsItemType.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 315F5EB924BA162600FED38F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 315F5EDC24BA2D3200FED38F /* Logger.swift in Sources */, + 31C1A585252691A900431C87 /* PiMonitorStatusHeader.swift in Sources */, 315F5ED724BA2D0800FED38F /* Pihole.swift in Sources */, 315F5ED824BA2D0800FED38F /* PiholeDataProvider.swift in Sources */, + 317636A3252658F5005AED55 /* ViewUtils.swift in Sources */, 315F5EDA24BA2D0C00FED38F /* UserPreferences.swift in Sources */, + 3132CDCC252522F400542F6F /* IntentHandler.swift in Sources */, + 313599D42527224300A12AC9 /* Constants.swift in Sources */, 315F5EDB24BA2D2700FED38F /* KeyChainPasswordItem.swift in Sources */, 310647CC24BD10E900E2DA90 /* CircleBadgeStatus.swift in Sources */, 31AECCFE24C7A57300E381E0 /* PiholeTimelineProvider.swift in Sources */, 31AECCC524C7A17400E381E0 /* PiStatsWidgets.swift in Sources */, + 31986A9425252AC500261BF5 /* PiMonitorTimelineProvider.swift in Sources */, + 31E153102523AD69008DD6CD /* PiMonitorWidget.swift in Sources */, 3182E29124BB6DA100BF53D9 /* UserDefaultExtensions.swift in Sources */, 315F5ED924BA2D0C00FED38F /* APIToken.swift in Sources */, 310647D024BD120600E2DA90 /* PiStatsDisplayWidgetView.swift in Sources */, 315F5ED124BA1DF800FED38F /* StatsItemType.swift in Sources */, + 31E153072523AD61008DD6CD /* PiMonitorWidgetView.swift in Sources */, 315F5EC524BA162600FED38F /* ViewStatsWidget.swift in Sources */, + 317636AB25265AB7005AED55 /* PiMonitorView.swift in Sources */, 310647CE24BD115F00E2DA90 /* SmallStatsItem.swift in Sources */, 31AECCFC24C7A55A00E381E0 /* PiholeEntry.swift in Sources */, + 315825AE25275812001160EA /* PiholeIntents.intentdefinition in Sources */, 315F5ED424BA1E1500FED38F /* UIConstants.swift in Sources */, 310647D224BD13D700E2DA90 /* MediumStatsItem.swift in Sources */, ); @@ -610,8 +756,10 @@ 317D482224B3D2F000897EA9 /* CodeScannerView.swift in Sources */, 319B083024B53269008B3C2F /* APIToken.swift in Sources */, 31FC1DAA24B64BA900319E6F /* PiholeDataProviderListManager.swift in Sources */, + 313599D32527224300A12AC9 /* Constants.swift in Sources */, 3175EB5E24AE597900E7B5FF /* ContentView.swift in Sources */, 3102D30B24B7BC7900EC8120 /* SettingsView.swift in Sources */, + 315825AD25275812001160EA /* PiholeIntents.intentdefinition in Sources */, 311D0A7B24B0F613003F18DB /* StatsView.swift in Sources */, 311D0A7924B0F405003F18DB /* UIConstants.swift in Sources */, 319F965D24B265D200B680D3 /* PiholeStatsList.swift in Sources */, @@ -622,6 +770,7 @@ 31A4D54224B92D220033EF53 /* AppDelegate.swift in Sources */, 318087FE24BDB11F00EF5159 /* StatusHeaderView.swift in Sources */, 319B083124B53269008B3C2F /* UserPreferences.swift in Sources */, + 311A3E3425252544005464D2 /* IntentHandler.swift in Sources */, 31DB2A2A2517B1C200D67F9E /* CustomDurationsView.swift in Sources */, 31115BC824B29A1700C91212 /* PiholeSetupView.swift in Sources */, 319B082924B5321C008B3C2F /* PiholeDataProvider.swift in Sources */, @@ -630,6 +779,7 @@ 31A9298A2517CF4B00FA9F50 /* CountdownPickerViewRepresentable.swift in Sources */, 3182E29024BB6DA100BF53D9 /* UserDefaultExtensions.swift in Sources */, 31A9297F2517CF0000FA9F50 /* CountdownPickerView.swift in Sources */, + 317636A2252658F5005AED55 /* ViewUtils.swift in Sources */, 3175EB5C24AE597900E7B5FF /* PiStatsMobileApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -653,6 +803,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 3132CDC2252522DA00542F6F /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 3132CDBB252522DA00542F6F /* IntentHandler */; + targetProxy = 3132CDC1252522DA00542F6F /* PBXContainerItemProxy */; + }; 315F5ECA24BA162800FED38F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 315F5EBC24BA162600FED38F /* PiStatsWidgetExtension */; @@ -671,6 +826,16 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 315825B125275812001160EA /* PiholeIntents.intentdefinition */ = { + isa = PBXVariantGroup; + children = ( + 315825B025275812001160EA /* Base */, + 315825B825275832001160EA /* en */, + 3171772B25277BBA001B1CAB /* pt-BR */, + ); + name = PiholeIntents.intentdefinition; + sourceTree = ""; + }; 317D481F24B3ADE200897EA9 /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( @@ -683,6 +848,50 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 3132CDC5252522DA00542F6F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = IntentHandler/IntentHandler.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 13; + DEVELOPMENT_TEAM = B2RUA6XMHC; + INFOPLIST_FILE = IntentHandler/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.0.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.bunn.PiStatsMobile.IntentHandler; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 3132CDC6252522DA00542F6F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = IntentHandler/IntentHandler.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 13; + DEVELOPMENT_TEAM = B2RUA6XMHC; + INFOPLIST_FILE = IntentHandler/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 2.0.0; + PRODUCT_BUNDLE_IDENTIFIER = dev.bunn.PiStatsMobile.IntentHandler; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; 315F5ECD24BA162800FED38F /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -690,7 +899,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = PiStatsWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = B2RUA6XMHC; INFOPLIST_FILE = PiStatsWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -698,7 +907,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = dev.bunn.PiStatsMobile.PiStatsWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -714,7 +923,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = PiStatsWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_TEAM = B2RUA6XMHC; INFOPLIST_FILE = PiStatsWidget/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -722,7 +931,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = dev.bunn.PiStatsMobile.PiStatsWidget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -855,7 +1064,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = PiStatsMobile/PiStatsMobile.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_ASSET_PATHS = "\"PiStatsMobile/Supporting Files/Preview Content\""; DEVELOPMENT_TEAM = B2RUA6XMHC; ENABLE_PREVIEWS = YES; @@ -865,11 +1074,11 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = dev.bunn.PiStatsMobile; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -881,7 +1090,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = PiStatsMobile/PiStatsMobile.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 10; + CURRENT_PROJECT_VERSION = 13; DEVELOPMENT_ASSET_PATHS = "\"PiStatsMobile/Supporting Files/Preview Content\""; DEVELOPMENT_TEAM = B2RUA6XMHC; ENABLE_PREVIEWS = YES; @@ -891,11 +1100,11 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 2.0.0; PRODUCT_BUNDLE_IDENTIFIER = dev.bunn.PiStatsMobile; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; @@ -986,6 +1195,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 3132CDC4252522DA00542F6F /* Build configuration list for PBXNativeTarget "IntentHandler" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3132CDC5252522DA00542F6F /* Debug */, + 3132CDC6252522DA00542F6F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 315F5ECF24BA162800FED38F /* Build configuration list for PBXNativeTarget "PiStatsWidgetExtension" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -1053,6 +1271,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 311A3E67252527D9005464D2 /* SwiftHole */ = { + isa = XCSwiftPackageProductDependency; + package = 315F5EDD24BA2E3900FED38F /* XCRemoteSwiftPackageReference "SwiftHole" */; + productName = SwiftHole; + }; + 311A3E69252527DC005464D2 /* PiMonitor */ = { + isa = XCSwiftPackageProductDependency; + package = 3137D20B24D217CD0021ACD3 /* XCRemoteSwiftPackageReference "PiMonitor" */; + productName = PiMonitor; + }; 3137D20C24D217CD0021ACD3 /* PiMonitor */ = { isa = XCSwiftPackageProductDependency; package = 3137D20B24D217CD0021ACD3 /* XCRemoteSwiftPackageReference "PiMonitor" */; diff --git a/PiStatsMobile/PiStatsMobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/PiStatsMobile/PiStatsMobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5ab032c..40d34dc 100644 --- a/PiStatsMobile/PiStatsMobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/PiStatsMobile/PiStatsMobile.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/Bunn/PiMonitor", "state": { "branch": null, - "revision": "a89879d3fb601871361dfc1f6b9d6cd7c4c42a11", - "version": "0.0.4" + "revision": "c0dbc72b466e437de94041948c576dc318e9af8a", + "version": "0.0.5" } }, { diff --git a/PiStatsMobile/PiStatsMobile/Base.lproj/PiholeIntents.intentdefinition b/PiStatsMobile/PiStatsMobile/Base.lproj/PiholeIntents.intentdefinition new file mode 100644 index 0000000..c3c1d86 --- /dev/null +++ b/PiStatsMobile/PiStatsMobile/Base.lproj/PiholeIntents.intentdefinition @@ -0,0 +1,167 @@ + + + + + INEnums + + INIntentDefinitionModelVersion + 1.2 + INIntentDefinitionNamespace + pwIO52 + INIntentDefinitionSystemVersion + 19H2 + INIntentDefinitionToolsBuildVersion + 12A7300 + INIntentDefinitionToolsVersion + 12.0.1 + INIntents + + + INIntentCategory + information + INIntentDescription + Select Pihole to display stats + INIntentDescriptionID + 9VVPSk + INIntentEligibleForWidgets + + INIntentIneligibleForSuggestions + + INIntentLastParameterTag + 2 + INIntentName + SelectPihole + INIntentParameters + + + INIntentParameterConfigurable + + INIntentParameterDisplayName + Pihole + INIntentParameterDisplayNameID + PiJeMO + INIntentParameterDisplayPriority + 1 + INIntentParameterName + pihole + INIntentParameterObjectType + PiholeIntent + INIntentParameterObjectTypeNamespace + pwIO52 + INIntentParameterPromptDialogs + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Configuration + + + INIntentParameterPromptDialogCustom + + INIntentParameterPromptDialogType + Primary + + + INIntentParameterSupportsDynamicEnumeration + + INIntentParameterTag + 2 + INIntentParameterType + Object + + + INIntentResponse + + INIntentResponseCodes + + + INIntentResponseCodeName + success + INIntentResponseCodeSuccess + + + + INIntentResponseCodeName + failure + + + + INIntentTitle + Select Pihole + INIntentTitleID + XxlkUM + INIntentType + Custom + INIntentVerb + View + + + INTypes + + + INTypeDisplayName + Pihole Intent + INTypeDisplayNameID + YlcCql + INTypeLastPropertyTag + 100 + INTypeName + PiholeIntent + INTypeProperties + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 1 + INTypePropertyName + identifier + INTypePropertyTag + 1 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 2 + INTypePropertyName + displayString + INTypePropertyTag + 2 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 3 + INTypePropertyName + pronunciationHint + INTypePropertyTag + 3 + INTypePropertyType + String + + + INTypePropertyDefault + + INTypePropertyDisplayPriority + 4 + INTypePropertyName + alternativeSpeakableMatches + INTypePropertySupportsMultipleValues + + INTypePropertyTag + 4 + INTypePropertyType + SpeakableString + + + + + + diff --git a/PiStatsMobile/PiStatsMobile/PiStatsMobileApp.swift b/PiStatsMobile/PiStatsMobile/PiStatsMobileApp.swift index 6856f6d..390581c 100644 --- a/PiStatsMobile/PiStatsMobile/PiStatsMobileApp.swift +++ b/PiStatsMobile/PiStatsMobile/PiStatsMobileApp.swift @@ -12,16 +12,6 @@ final class DataModel: ObservableObject { let piholeProviderListManager = PiholeDataProviderListManager() let userPreferences = UserPreferences.shared private var offlineBadgeCancellable: AnyCancellable? - - init() { - setupCancellables() - } - - private func setupCancellables() { - offlineBadgeCancellable = userPreferences.$displayIconBadgeForOfflinePiholes.receive(on: DispatchQueue.main).sink { [weak self] value in - self?.piholeProviderListManager.shouldUpdateIconBadgeWithOfflinePiholes = value - } - } } @main @@ -35,6 +25,5 @@ struct PiStatsMobileApp: App { .environmentObject(dataModel.piholeProviderListManager) .environmentObject(dataModel.userPreferences) } - } } diff --git a/PiStatsMobile/PiStatsMobile/Supporting Files/Info.plist b/PiStatsMobile/PiStatsMobile/Supporting Files/Info.plist index 7896edc..fe3d2a3 100644 --- a/PiStatsMobile/PiStatsMobile/Supporting Files/Info.plist +++ b/PiStatsMobile/PiStatsMobile/Supporting Files/Info.plist @@ -35,6 +35,10 @@ NSCameraUsageDescription Camera permission is necessary to read the API Token QRCode + NSUserActivityTypes + + SelectPiholeIntent + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/PiStatsMobile/PiStatsMobile/Utils/Constants.swift b/PiStatsMobile/PiStatsMobile/Utils/Constants.swift new file mode 100644 index 0000000..97286c9 --- /dev/null +++ b/PiStatsMobile/PiStatsMobile/Utils/Constants.swift @@ -0,0 +1,12 @@ +// +// Constants.swift +// PiStatsMobile +// +// Created by Fernando Bunn on 02/10/2020. +// + +import Foundation + +struct Constants { + static let appGroup = "group.dev.bunn.PiStatsMobile" +} diff --git a/PiStatsMobile/PiStatsMobile/Utils/DisableDurationManager.swift b/PiStatsMobile/PiStatsMobile/Utils/DisableDurationManager.swift index 28f6b94..788403b 100644 --- a/PiStatsMobile/PiStatsMobile/Utils/DisableDurationManager.swift +++ b/PiStatsMobile/PiStatsMobile/Utils/DisableDurationManager.swift @@ -72,7 +72,7 @@ class DisableDurationManager: ObservableObject { } func addNewItem() { - items.append(DisableTimeItem(timeInterval: 30)) + items.append(DisableTimeItem(timeInterval: 0)) setupCancellables() } diff --git a/PiStatsMobile/PiStatsMobile/Utils/UserDefaultExtensions.swift b/PiStatsMobile/PiStatsMobile/Utils/UserDefaultExtensions.swift index 2ba22cb..e891387 100644 --- a/PiStatsMobile/PiStatsMobile/Utils/UserDefaultExtensions.swift +++ b/PiStatsMobile/PiStatsMobile/Utils/UserDefaultExtensions.swift @@ -10,7 +10,6 @@ import Foundation extension UserDefaults { static func shared() -> UserDefaults { - let appGroup = "group.dev.bunn.PiStatsMobile" - return UserDefaults(suiteName: appGroup)! + return UserDefaults(suiteName: Constants.appGroup)! } } diff --git a/PiStatsMobile/PiStatsMobile/Utils/ViewUtils.swift b/PiStatsMobile/PiStatsMobile/Utils/ViewUtils.swift new file mode 100644 index 0000000..0edfc65 --- /dev/null +++ b/PiStatsMobile/PiStatsMobile/Utils/ViewUtils.swift @@ -0,0 +1,24 @@ +// +// ViewUtils.swift +// PiStatsMobile +// +// Created by Fernando Bunn on 01/10/2020. +// + +import SwiftUI + +struct ViewUtils { + + static func shieldStatusImageForDataProvider(_ dataProvider: PiholeDataProvider) -> some View { + if dataProvider.hasErrorMessages || dataProvider.status == .enabledAndDisabled { + return Image(systemName: UIConstants.SystemImages.piholeStatusWarning) + .foregroundColor(UIConstants.Colors.statusWarning) + } else if dataProvider.status == .allEnabled { + return Image(systemName: UIConstants.SystemImages.piholeStatusOnline) + .foregroundColor(UIConstants.Colors.statusOnline) + } else { + return Image(systemName: UIConstants.SystemImages.piholeStatusOffline) + .foregroundColor(UIConstants.Colors.statusOffline) + } + } +} diff --git a/PiStatsMobile/PiStatsMobile/Views/Piholes/MetricsView.swift b/PiStatsMobile/PiStatsMobile/Views/Piholes/MetricsView.swift index f57706d..8372414 100644 --- a/PiStatsMobile/PiStatsMobile/Views/Piholes/MetricsView.swift +++ b/PiStatsMobile/PiStatsMobile/Views/Piholes/MetricsView.swift @@ -8,7 +8,7 @@ import SwiftUI import PiMonitor -struct MetricItem: Identifiable { +fileprivate struct MetricItem: Identifiable { let value: String let systemName: String let helpText: String @@ -19,7 +19,7 @@ struct MetricsView: View { @ObservedObject var dataProvider: PiholeDataProvider private let imageSize: CGFloat = 15 - func getMetricItems() -> [MetricItem] { + private func getMetricItems() -> [MetricItem] { return [ MetricItem(value: dataProvider.temperature, systemName: UIConstants.SystemImages.metricTemperature, helpText: "Raspberry Pi temperature"), MetricItem(value: dataProvider.uptime, systemName: UIConstants.SystemImages.metricUptime, helpText: "Raspberry Pi uptime"), diff --git a/PiStatsMobile/PiStatsMobile/Views/Piholes/PiholeStatsList.swift b/PiStatsMobile/PiStatsMobile/Views/Piholes/PiholeStatsList.swift index 951597e..822f899 100644 --- a/PiStatsMobile/PiStatsMobile/Views/Piholes/PiholeStatsList.swift +++ b/PiStatsMobile/PiStatsMobile/Views/Piholes/PiholeStatsList.swift @@ -20,6 +20,8 @@ final class StatsListConfig: ObservableObject { struct PiholeStatsList: View { @StateObject private var viewModel = StatsListConfig() + @Environment(\.horizontalSizeClass) var horizontalSizeClass: UserInterfaceSizeClass? + @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? @EnvironmentObject private var userPreferences: UserPreferences @EnvironmentObject private var piholeProviderListManager: PiholeDataProviderListManager @Environment(\.scenePhase) private var phase @@ -27,37 +29,76 @@ struct PiholeStatsList: View { I want to make this logic on the @App but it seems there's a bug on Beta2 More info here: https://twitter.com/fcbunn/status/1281905574695886848?s=21 */ - + + private let columns = [ + GridItem(.flexible()), + GridItem(.flexible()) + ] + + private func regularSetup() -> some View { + Group { + LazyVGrid(columns: columns, alignment: .center, spacing: 10) { + ForEach(piholeProviderListManager.providerList, id: \.id) { provider in + StatsView(dataProvider: provider) + .onTapGesture() { + viewModel.openPiholeSetup(provider.piholes.first) + } + } + if piholeProviderListManager.isEmpty == false { + addPiholeButton() + } + } + if piholeProviderListManager.isEmpty == true { + addPiholeButton() + } + } + } + + private func addPiholeButton() -> some View { + Button(action: { + viewModel.openPiholeSetup() + }, label: { + ZStack { + Circle() + .frame(width: UIConstants.Geometry.addPiholeButtonHeight, height: UIConstants.Geometry.addPiholeButtonHeight, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) + Image(systemName: UIConstants.SystemImages.addPiholeButton) + .foregroundColor(.white) + .font(.largeTitle) + } + }) + .shadow(radius: UIConstants.Geometry.shadowRadius) + .padding() + } + + private func compactSetup() -> some View { + Group { + ForEach(piholeProviderListManager.providerList, id: \.id) { provider in + StatsView(dataProvider: provider) + .onTapGesture() { + viewModel.openPiholeSetup(provider.piholes.first) + } + } + addPiholeButton() + } + } + var body: some View { ZStack { Color(.systemGroupedBackground) .edgesIgnoringSafeArea(.all) ScrollView { + if userPreferences.displayAllPiholes { StatsView(dataProvider: piholeProviderListManager.allPiholesProvider) Divider() } - ForEach(piholeProviderListManager.providerList, id: \.id) { provider in - StatsView(dataProvider: provider) - .onTapGesture() { - viewModel.openPiholeSetup(provider.piholes.first) - } + if horizontalSizeClass == .regular && verticalSizeClass == .regular { + regularSetup() + } else { + compactSetup() } - Button(action: { - viewModel.openPiholeSetup() - }, label: { - ZStack { - Circle() - .frame(width: UIConstants.Geometry.addPiholeButtonHeight, height: UIConstants.Geometry.addPiholeButtonHeight, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) - Image(systemName: UIConstants.SystemImages.addPiholeButton) - .foregroundColor(.white) - .font(.largeTitle) - } - }) - .shadow(radius: UIConstants.Geometry.shadowRadius) - .padding() if piholeProviderListManager.isEmpty { Text(UIConstants.Strings.addFirstPiholeCaption) } @@ -76,7 +117,6 @@ struct PiholeStatsList: View { case .background: WidgetCenter.shared.reloadAllTimelines() @unknown default: break - // Fallback for future cases } } } diff --git a/PiStatsMobile/PiStatsMobile/en.lproj/PiholeIntents.strings b/PiStatsMobile/PiStatsMobile/en.lproj/PiholeIntents.strings new file mode 100644 index 0000000..b9e8e72 --- /dev/null +++ b/PiStatsMobile/PiStatsMobile/en.lproj/PiholeIntents.strings @@ -0,0 +1,7 @@ +"PiJeMO" = "Pihole"; + +"XxlkUM" = "Select Pihole"; + +"YlcCql" = "Pihole Intent"; + +"9VVPSk" = "Select Pihole to display stats"; diff --git a/PiStatsMobile/PiStatsMobile/pt-BR.lproj/PiholeIntents.strings b/PiStatsMobile/PiStatsMobile/pt-BR.lproj/PiholeIntents.strings new file mode 100644 index 0000000..b9e8e72 --- /dev/null +++ b/PiStatsMobile/PiStatsMobile/pt-BR.lproj/PiholeIntents.strings @@ -0,0 +1,7 @@ +"PiJeMO" = "Pihole"; + +"XxlkUM" = "Select Pihole"; + +"YlcCql" = "Pihole Intent"; + +"9VVPSk" = "Select Pihole to display stats"; diff --git a/PiStatsMobile/PiStatsWidget/Assets.xcassets/PiMonitorWidgetBackground.colorset/Contents.json b/PiStatsMobile/PiStatsWidget/Assets.xcassets/PiMonitorWidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..92ce3e6 --- /dev/null +++ b/PiStatsMobile/PiStatsWidget/Assets.xcassets/PiMonitorWidgetBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.110", + "green" : "0.102", + "red" : "0.098" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/PiStatsMobile/PiStatsWidget/Core/PiMonitorTimelineProvider.swift b/PiStatsMobile/PiStatsWidget/Core/PiMonitorTimelineProvider.swift new file mode 100644 index 0000000..5930185 --- /dev/null +++ b/PiStatsMobile/PiStatsWidget/Core/PiMonitorTimelineProvider.swift @@ -0,0 +1,65 @@ +// +// PiMonitorTimelineProvider.swift +// PiStatsMobile +// +// Created by Fernando Bunn on 30/09/2020. +// + + +import WidgetKit +import Foundation +import os.log + +struct PiMonitorTimelineProvider: IntentTimelineProvider { + typealias Intent = SelectPiholeIntent + + typealias Entry = PiholeEntry + private static let fakePihole = PiholeDataProvider.previewData() + private let log = Logger().osLog(describing: PiMonitorTimelineProvider.self) + + func placeholder(in context: Context) -> PiholeEntry { + PiholeEntry(piholeDataProvider: PiholeDataProvider.previewData(), date: Date(), widgetFamily: .systemSmall) + } + + func getSnapshot(for configuration: Intent, in context: Context, completion: @escaping (PiholeEntry) -> Void) { + let entry = PiholeEntry(piholeDataProvider: PiMonitorTimelineProvider.fakePihole, date: Date(), widgetFamily: context.family) + completion(entry) + } + + + func getTimeline(for configuration: SelectPiholeIntent, in context: Context, completion: @escaping (Timeline) -> Void) { + let currentDate = Date() + let refreshDate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)! + + if let identifier = configuration.pihole?.identifier, + let piholeUUID = UUID(uuidString: identifier), + let pihole = Pihole.restore(piholeUUID) { + let provider = PiholeDataProvider(piholes: [pihole]) + os_log("get timeline called") + let dispatchGroup = DispatchGroup() + + dispatchGroup.enter() + provider.fetchSummaryData { + dispatchGroup.leave() + } + + dispatchGroup.enter() + provider.fetchMetricsData { + dispatchGroup.leave() + } + + dispatchGroup.notify(queue: DispatchQueue.main) { + let entry = PiholeEntry(piholeDataProvider: provider, date: Date(), widgetFamily: context.family) + let timeline = Timeline(entries: [entry], policy: .after(refreshDate)) + completion(timeline) + } + + } else { + os_log("No pihole found/selected") + let provider = PiholeDataProvider(piholes: []) + let entry = PiholeEntry(piholeDataProvider: provider, date: Date(), widgetFamily: context.family) + let timeline = Timeline(entries: [entry], policy: .after(refreshDate)) + completion(timeline) + } + } +} diff --git a/PiStatsMobile/PiStatsWidget/PiMonitorWidget.swift b/PiStatsMobile/PiStatsWidget/PiMonitorWidget.swift new file mode 100644 index 0000000..6388d8f --- /dev/null +++ b/PiStatsMobile/PiStatsWidget/PiMonitorWidget.swift @@ -0,0 +1,49 @@ +// +// PiMonitorWidget.swift +// PiStatsMobile +// +// Created by Fernando Bunn on 29/09/2020. +// + +import WidgetKit +import SwiftUI + +private struct PlaceholderView : View { + var body: some View { + + PiMonitorView(provider: PiholeDataProvider.previewData(), shouldDisplayStats: false ).redacted(reason: .placeholder) + } +} + +struct PiMonitorWidget: Widget { + private let kind: String = "PiMonitorWidget" + public var body: some WidgetConfiguration { + + IntentConfiguration( + kind: "dev.bunn.PiStatsMobile.SelectPiholeIntent", + intent: SelectPiholeIntent.self, + provider: PiMonitorTimelineProvider() + ) { entry in + PiMonitorWidgetView(entry: entry) + } + .configurationDisplayName("Pi Monitor") + .description("Display metrics for your Raspberry Pi") + .supportedFamilies([.systemSmall, .systemMedium]) + } +} + + +struct PiMonitorWidget_Previews: PreviewProvider { + static var previews: some View { + + PiMonitorWidgetView(entry: PiholeEntry(piholeDataProvider: PiholeDataProvider.previewData(), date: Date(), widgetFamily: .systemSmall)) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + + PiMonitorWidgetView(entry: PiholeEntry(piholeDataProvider: PiholeDataProvider.previewData(), date: Date(), widgetFamily: .systemMedium)) + .previewContext(WidgetPreviewContext(family: .systemMedium)) + + PlaceholderView() + .previewContext(WidgetPreviewContext(family: .systemSmall)) + } +} + diff --git a/PiStatsMobile/PiStatsWidget/PiStatsWidgets.swift b/PiStatsMobile/PiStatsWidget/PiStatsWidgets.swift index c55cb53..6a3d1b4 100644 --- a/PiStatsMobile/PiStatsWidget/PiStatsWidgets.swift +++ b/PiStatsMobile/PiStatsWidget/PiStatsWidgets.swift @@ -13,6 +13,6 @@ struct PiStatsWidgets: WidgetBundle { @WidgetBundleBuilder var body: some Widget { ViewStatsWidget() - //ToggleStatusWidget() + PiMonitorWidget() } } diff --git a/PiStatsMobile/PiStatsWidget/ViewStatsWidget.swift b/PiStatsMobile/PiStatsWidget/ViewStatsWidget.swift index 726280c..3a920a1 100644 --- a/PiStatsMobile/PiStatsWidget/ViewStatsWidget.swift +++ b/PiStatsMobile/PiStatsWidget/ViewStatsWidget.swift @@ -29,9 +29,7 @@ struct ViewStatsWidget: Widget { public var body: some WidgetConfiguration { StaticConfiguration(kind: kind, provider: PiholeTimelineProvider()) { entry in PiStatsDisplayWidgetView(entry: entry) - } - .configurationDisplayName("Pi Stats") .description("Display the status of your pi-holes") .supportedFamilies([.systemSmall, .systemMedium]) diff --git a/PiStatsMobile/PiStatsWidget/Views/CircleBadgeStatus.swift b/PiStatsMobile/PiStatsWidget/Views/CircleBadgeStatus.swift index e2a1e63..6819e7e 100644 --- a/PiStatsMobile/PiStatsWidget/Views/CircleBadgeStatus.swift +++ b/PiStatsMobile/PiStatsWidget/Views/CircleBadgeStatus.swift @@ -16,22 +16,9 @@ struct CircleBadgeStatus: View { .foregroundColor(.white) .frame(width: circleSize, height: circleSize) - imageForDataProvider(dataProvider) + ViewUtils.shieldStatusImageForDataProvider(dataProvider) .font(.title2) } - - func imageForDataProvider(_ dataProvider: PiholeDataProvider) -> some View { - if dataProvider.hasErrorMessages { - return Image(systemName: UIConstants.SystemImages.piholeStatusWarning) - .foregroundColor(UIConstants.Colors.statusWarning) - } else if dataProvider.status == .allEnabled { - return Image(systemName: UIConstants.SystemImages.piholeStatusOnline) - .foregroundColor(UIConstants.Colors.statusOnline) - } else { - return Image(systemName: UIConstants.SystemImages.piholeStatusOffline) - .foregroundColor(UIConstants.Colors.statusOffline) - } - } } struct CircleBadgeStatus_Previews: PreviewProvider { diff --git a/PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorStatusHeader.swift b/PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorStatusHeader.swift new file mode 100644 index 0000000..98e5a05 --- /dev/null +++ b/PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorStatusHeader.swift @@ -0,0 +1,32 @@ +// +// PiMonitorStatusHeader.swift +// PiStatsWidgetExtension +// +// Created by Fernando Bunn on 01/10/2020. +// + +import SwiftUI + +struct PiMonitorStatusHeader: View { + var provider: PiholeDataProvider + + var body: some View { + VStack (alignment:.leading) { + Label(title: { + Text(provider.name) + }, icon: { + ViewUtils.shieldStatusImageForDataProvider(provider) + }) + .minimumScaleFactor(0.75) + .font(Font.headline.weight(.bold)) + Divider() + Spacer() + } + } +} + +struct PiMonitorStatusHeader_Previews: PreviewProvider { + static var previews: some View { + PiMonitorStatusHeader(provider: PiholeDataProvider.previewData()) + } +} diff --git a/PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorView.swift b/PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorView.swift new file mode 100644 index 0000000..371eaaa --- /dev/null +++ b/PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorView.swift @@ -0,0 +1,104 @@ +// +// PiMonitorView.swift +// PiStatsMobile +// +// Created by Fernando Bunn on 01/10/2020. +// + +import SwiftUI +import WidgetKit + +fileprivate struct ListItem : Identifiable{ + let id = UUID() + let text: String + let systemImage: String + let color: Color +} +struct PiMonitorStatsView: View { + let imageSize: CGFloat = 15 + fileprivate let listItems: [ListItem] + + var body: some View { + VStack (alignment:.leading, spacing: 6.0) { + + ForEach(listItems) { item in + Label(title: { + Text(item.text) + }, icon: { + Image(systemName: item.systemImage) + .frame(width: imageSize, height: imageSize) + }) + .font(Font.body.weight(.medium)) + .minimumScaleFactor(0.80) + .foregroundColor(item.color) + } + } + } +} + +struct PiMonitorView: View { + var provider: PiholeDataProvider + let imageSize: CGFloat = 15 + let shouldDisplayStats: Bool + + var body: some View { + VStack (alignment:.leading) { + PiMonitorStatusHeader(provider: provider) + + HStack { + PiMonitorStatsView(listItems: getMetricListItems(provider)) + .font(Font.body.weight(.semibold)) + .minimumScaleFactor(0.89) + + if shouldDisplayStats { + Spacer() + + PiMonitorStatsView(listItems: getStatsListItems(provider)) + } + } + } + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .topLeading + ).padding() + .font(.headline) + } + + private func getMetricListItems(_ provider: PiholeDataProvider) -> [ListItem] { + return [ + + ListItem(text: provider.memoryUsage, systemImage: UIConstants.SystemImages.metricMemoryUsage, color: UIConstants.Colors.totalQueries), + + ListItem(text: provider.uptime, systemImage: UIConstants.SystemImages.metricUptime, color: UIConstants.Colors.queriesBlocked), + + ListItem(text: provider.temperature, systemImage: UIConstants.SystemImages.metricTemperature, color: UIConstants.Colors.domainsOnBlocklist), + + ListItem(text: provider.loadAverage, systemImage: UIConstants.SystemImages.metricLoadAverage, color: UIConstants.Colors.percentBlocked), + ] + } + + private func getStatsListItems(_ provider: PiholeDataProvider) -> [ListItem] { + return [ + ListItem(text: provider.totalQueries, systemImage: UIConstants.SystemImages.totalQueries, color: UIConstants.Colors.totalQueries), + + ListItem(text: provider.queriesBlocked, systemImage: UIConstants.SystemImages.queriesBlocked, color: UIConstants.Colors.queriesBlocked), + + ListItem(text: provider.domainsOnBlocklist, systemImage: UIConstants.SystemImages.domainsOnBlockList, color: UIConstants.Colors.domainsOnBlocklist), + + ListItem(text: provider.percentBlocked, systemImage: UIConstants.SystemImages.percentBlocked, color: UIConstants.Colors.percentBlocked), + ] + } +} + +struct PiMonitorView_Previews: PreviewProvider { + static var previews: some View { + PiMonitorView(provider: PiholeDataProvider.previewData(), shouldDisplayStats: true) + .previewContext(WidgetPreviewContext(family: .systemMedium)) + + PiMonitorView(provider: PiholeDataProvider.previewData(), shouldDisplayStats: false) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + + + } +} diff --git a/PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorWidgetView.swift b/PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorWidgetView.swift new file mode 100644 index 0000000..36d87ad --- /dev/null +++ b/PiStatsMobile/PiStatsWidget/Views/PiMonitor/PiMonitorWidgetView.swift @@ -0,0 +1,49 @@ +// +// PiMonitorWidgetView.swift +// PiStatsMobile +// +// Created by Fernando Bunn on 29/09/2020. +// + +import SwiftUI +import WidgetKit + +struct PiMonitorWidgetView: View { + var entry: PiholeEntry + var shouldDisplayStats: Bool { + entry.widgetFamily == .systemMedium + } + + var body: some View { + ZStack { + UIConstants.Colors.piMonitorWidgetBackground + + if entry.piholeDataProvider.piholes.count == 0 { + PiMonitorView(provider: PiholeDataProvider.previewData() , shouldDisplayStats: shouldDisplayStats).redacted(reason: .placeholder) + } else if entry.piholeDataProvider.canDisplayMetrics == false { + VStack (spacing: 10) { + Image(systemName: UIConstants.SystemImages.piholeSetupMonitor) + .foregroundColor(UIConstants.Colors.domainsOnBlocklist) + + Text("\(UIConstants.Strings.Widget.piholeNotEnabledOn) \(entry.piholeDataProvider.name)") + .multilineTextAlignment(.center) + } + .font(Font.headline.weight(.semibold)) + .padding() + } + else { + PiMonitorView(provider: entry.piholeDataProvider, shouldDisplayStats: shouldDisplayStats) + } + } + } +} + +struct PiMonitorWidgetView_Previews: PreviewProvider { + static var previews: some View { + PiMonitorWidgetView(entry: PiholeEntry.init(piholeDataProvider: PiholeDataProvider.previewData(), date: Date(), widgetFamily: .systemMedium)) + .previewContext(WidgetPreviewContext(family: .systemMedium)) + + PiMonitorWidgetView(entry: PiholeEntry.init(piholeDataProvider: PiholeDataProvider.previewData(), date: Date(), widgetFamily: .systemMedium)) + .previewContext(WidgetPreviewContext(family: .systemSmall)) + } +} diff --git a/PiStatsMobile/Shared/Core/Pihole.swift b/PiStatsMobile/Shared/Core/Pihole.swift index c80951f..610103b 100644 --- a/PiStatsMobile/Shared/Core/Pihole.swift +++ b/PiStatsMobile/Shared/Core/Pihole.swift @@ -17,7 +17,8 @@ class Pihole: Identifiable, ObservableObject { private(set) var metrics: PiMetrics? private(set) var active = false private lazy var keychainToken = APIToken(accountName: self.id.uuidString) - + private let servicesTimeout: TimeInterval = 10 + var displayName: String? var address: String var piMonitorPort: Int? @@ -56,12 +57,19 @@ class Pihole: Identifiable, ObservableObject { address.components(separatedBy: ":").first ?? "" } + var title: String { + if let name = displayName { + return name + } + return host + } + private var service: SwiftHole { - SwiftHole(host: host, port: port, apiToken: apiToken, timeoutInterval: 10) + SwiftHole(host: host, port: port, apiToken: apiToken, timeoutInterval: servicesTimeout) } private var piMonitorService: PiMonitor { - PiMonitor(host: host, port: piMonitorPort ?? 8088) + PiMonitor(host: host, port: piMonitorPort ?? 8088, timeoutInterval: servicesTimeout) } required init(from decoder: Decoder) throws { @@ -92,7 +100,9 @@ class Pihole: Identifiable, ObservableObject { } static func previewData() -> Pihole { - Pihole(address: "127.0.0.1") + let pihole = Pihole(address: "127.0.0.1") + pihole.hasPiMonitor = true + return pihole } private func getPort(_ address: String) -> Int? { diff --git a/PiStatsMobile/Shared/Core/PiholeDataProvider.swift b/PiStatsMobile/Shared/Core/PiholeDataProvider.swift index 63a6e94..43d2009 100644 --- a/PiStatsMobile/Shared/Core/PiholeDataProvider.swift +++ b/PiStatsMobile/Shared/Core/PiholeDataProvider.swift @@ -21,6 +21,11 @@ class PiholeDataProvider: ObservableObject, Identifiable { provider.percentBlocked = "12,3%" provider.domainsOnBlocklist = "12,345" provider.status = .allEnabled + + provider.temperature = "23 ºC" + provider.memoryUsage = "50%" + provider.loadAverage = "0.1, 0.3, 0.6" + provider.uptime = "23h 2m" return provider } @@ -92,7 +97,7 @@ class PiholeDataProvider: ObservableObject, Identifiable { if piholes.count > 1 { self.name = UIConstants.Strings.allPiholesTitle } else if let firstPihole = piholes.first { - self.name = firstPihole.displayName ?? firstPihole.host + self.name = firstPihole.title } setupCancellables() } @@ -253,7 +258,6 @@ class PiholeDataProvider: ObservableObject, Identifiable { completion?() return } - let dispatchGroup = DispatchGroup() piholes.forEach { pihole in @@ -262,6 +266,7 @@ class PiholeDataProvider: ObservableObject, Identifiable { DispatchQueue.main.async { self.updateMetrics(pihole.metrics) } + dispatchGroup.leave() } } dispatchGroup.notify(queue: DispatchQueue.main) { diff --git a/PiStatsMobile/Shared/Core/UserData/UserPreferences.swift b/PiStatsMobile/Shared/Core/UserData/UserPreferences.swift index a5db347..865fb32 100644 --- a/PiStatsMobile/Shared/Core/UserData/UserPreferences.swift +++ b/PiStatsMobile/Shared/Core/UserData/UserPreferences.swift @@ -14,7 +14,6 @@ private enum Keys: String { case displayStatsAsList case displayStatsIcons case displayAllPiholes - case displayIconBadgeForOfflinePiholes case disableTimes case temperatureScale } @@ -34,65 +33,73 @@ class UserPreferences: ObservableObject { return .celsius } } + + init() { + migrateStandardUserDefaultToGroupIfNecessary() + } + + private func migrateStandardUserDefaultToGroupIfNecessary() { + let keys = [ + Keys.displayAllPiholes.rawValue, + Keys.disablePermanently.rawValue, + Keys.displayStatsAsList.rawValue, + Keys.displayStatsIcons.rawValue, + Keys.temperatureScale.rawValue, + Keys.disableTimes.rawValue + ] + + keys.forEach { key in + if let value = UserDefaults.standard.object(forKey: key) { + UserDefaults.standard.removeObject(forKey: key) + /* + If I only set the disableTimes using the shared().setValue, it's getter returns nil and then returns the default set of intervals. + */ + if key == Keys.disableTimes.rawValue { + if let times = value as? [TimeInterval] { + disableTimes = times + } + } else { + UserDefaults.shared().setValue(value, forKey: key) + } + } + } + + UserDefaults.shared().synchronize() + } - @AppStorage(Keys.displayAllPiholes.rawValue) var displayAllPiholes: Bool = false { + @AppStorage(Keys.displayAllPiholes.rawValue, store: UserDefaults(suiteName: Constants.appGroup)) var displayAllPiholes: Bool = false { willSet { objectWillChange.send() } } - @AppStorage(Keys.disablePermanently.rawValue) var disablePermanently: Bool = false { + @AppStorage(Keys.disablePermanently.rawValue, store: UserDefaults(suiteName: Constants.appGroup)) var disablePermanently: Bool = false { willSet { objectWillChange.send() } } - @AppStorage(Keys.displayStatsAsList.rawValue) var displayStatsAsList: Bool = false { + @AppStorage(Keys.displayStatsAsList.rawValue, store: UserDefaults(suiteName: Constants.appGroup)) var displayStatsAsList: Bool = false { willSet { objectWillChange.send() } } - @AppStorage(Keys.displayStatsIcons.rawValue) var displayStatsIcons: Bool = true { + @AppStorage(Keys.displayStatsIcons.rawValue, store: UserDefaults(suiteName: Constants.appGroup)) var displayStatsIcons: Bool = true { willSet { objectWillChange.send() } } - @Published var disableTimes: [TimeInterval] = UserDefaults.standard.object(forKey: Keys.disableTimes.rawValue) as? [TimeInterval] ?? [30, 60, 300] { + @Published var disableTimes: [TimeInterval] = UserDefaults.shared().object(forKey: Keys.disableTimes.rawValue) as? [TimeInterval] ?? [30, 60, 300] { didSet { - UserDefaults.standard.set(disableTimes, forKey: Keys.disableTimes.rawValue) + UserDefaults.shared().set(disableTimes, forKey: Keys.disableTimes.rawValue) } } - - @AppStorage(Keys.temperatureScale.rawValue) var temperatureScale: Int = 0 { + @AppStorage(Keys.temperatureScale.rawValue, store: UserDefaults(suiteName: Constants.appGroup)) var temperatureScale: Int = 0 { willSet { objectWillChange.send() } } - - /* - TODO: Improve this: - I'm not sure how to have an AppStorage + Published, so I used standard UserDefaults - Also, the handling of the requestAuthorization and badge reset should be done by the caller since this - class should only be responsible for data storage and not business logic - */ - @Published var displayIconBadgeForOfflinePiholes: Bool = UserDefaults.standard.object(forKey: Keys.displayIconBadgeForOfflinePiholes.rawValue) as? Bool ?? false { - willSet { - if newValue { - UNUserNotificationCenter.current().requestAuthorization(options: .badge) { (granted, error) in - if granted == false { - DispatchQueue.main.async { - self.displayIconBadgeForOfflinePiholes = false - } - } - } - } else { - // UIApplication.shared.applicationIconBadgeNumber = 0 - } - } didSet { - UserDefaults.standard.set(displayIconBadgeForOfflinePiholes, forKey: Keys.displayIconBadgeForOfflinePiholes.rawValue) - } - } } diff --git a/PiStatsMobile/Shared/StatsItemType.swift b/PiStatsMobile/Shared/StatsItemType.swift index 58eb623..f6ee0af 100644 --- a/PiStatsMobile/Shared/StatsItemType.swift +++ b/PiStatsMobile/Shared/StatsItemType.swift @@ -16,13 +16,13 @@ enum StatsItemType { var imageName: String { switch self { case .domainsOnBlockList: - return "list.bullet" + return UIConstants.SystemImages.domainsOnBlockList case .totalQueries: - return "globe" + return UIConstants.SystemImages.totalQueries case .queriesBlocked: - return "hand.raised" + return UIConstants.SystemImages.queriesBlocked case .percentBlocked: - return "chart.pie" + return UIConstants.SystemImages.percentBlocked } } diff --git a/PiStatsMobile/Shared/UIConstants.swift b/PiStatsMobile/Shared/UIConstants.swift index de46573..aa166e6 100644 --- a/PiStatsMobile/Shared/UIConstants.swift +++ b/PiStatsMobile/Shared/UIConstants.swift @@ -29,6 +29,7 @@ struct UIConstants { static let statusOnline = Color("StatusOnline") static let statusWarning = Color("StatusWarning") static let errorMessage = Color("StatusOffline") + static let piMonitorWidgetBackground = Color("PiMonitorWidgetBackground") } struct Strings { @@ -58,7 +59,6 @@ struct UIConstants { static let piMonitorExplanation = "Pi Monitor is a service that helps you to monitor your Raspberry Pi by showing you information like temperature, memory usage and more!\n\nIn order to use it you'll need to install it in your Raspberry Pi." static let addFirstPiholeCaption = "Tap here to add your first pi-hole" - static let displayIconBadgeForOfflinePiholes = "Display icon badge for offline pi-holes" static let piholesNavigationTitle = "Pi-holes" static let settingsNavigationTitle = "Settings" static let disablePiholeOptionsTitle = "Disable Pi-hole" @@ -70,6 +70,10 @@ struct UIConstants { static let allPiholesTitle = "All Pi-holes" static let temperatureScaleCelsius = "°C" static let temperatureScaleFahrenheit = "°F" + + struct Widget { + static let piholeNotEnabledOn = "Pi Monitor is not enabled on" + } struct Preferences { static let sectionInterface = "Interface" @@ -135,5 +139,9 @@ struct UIConstants { static let addNewCustomDisableTime = "plus" static let piMonitorTemperature = "thermometer" + static let domainsOnBlockList = "list.bullet" + static let totalQueries = "globe" + static let queriesBlocked = "hand.raised" + static let percentBlocked = "chart.pie" } }