diff --git a/CommonUI/.gitignore b/CommonUI/.gitignore
new file mode 100644
index 000000000..0023a5340
--- /dev/null
+++ b/CommonUI/.gitignore
@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc
diff --git a/CommonUI/Package.resolved b/CommonUI/Package.resolved
new file mode 100644
index 000000000..6ac18d527
--- /dev/null
+++ b/CommonUI/Package.resolved
@@ -0,0 +1,96 @@
+{
+ "originHash" : "822542af313faf8fe83f30c3acf880bf33b4214d7b137016615f52409ce94c8c",
+ "pins" : [
+ {
+ "identity" : "kingfisher",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/onevcat/Kingfisher.git",
+ "state" : {
+ "revision" : "d30a5fad881137e2267f96a8e3fc35c58999bb94",
+ "version" : "8.6.2"
+ }
+ },
+ {
+ "identity" : "sdwebimage",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/SDWebImage/SDWebImage.git",
+ "state" : {
+ "revision" : "36e79ba485e9bb4d3cd4e3318908866dac5e7b51",
+ "version" : "5.21.5"
+ }
+ },
+ {
+ "identity" : "sdwebimagesvgcoder",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/SDWebImage/SDWebImageSVGCoder.git",
+ "state" : {
+ "revision" : "85b5d58ad02c207c496fa34426dc6560d6ae32f0",
+ "version" : "1.8.0"
+ }
+ },
+ {
+ "identity" : "sfsafesymbols",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/SFSafeSymbols/SFSafeSymbols.git",
+ "state" : {
+ "revision" : "e01b3d4f861412f8dcee8d93c417d2c2b0cdfd77",
+ "version" : "7.0.0"
+ }
+ },
+ {
+ "identity" : "swift-collections",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-collections",
+ "state" : {
+ "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e",
+ "version" : "1.3.0"
+ }
+ },
+ {
+ "identity" : "swift-http-types",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-http-types.git",
+ "state" : {
+ "revision" : "45eb0224913ea070ec4fba17291b9e7ecf4749ca",
+ "version" : "1.5.1"
+ }
+ },
+ {
+ "identity" : "swift-numerics",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-numerics",
+ "state" : {
+ "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2",
+ "version" : "1.1.1"
+ }
+ },
+ {
+ "identity" : "swift-openapi-runtime",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-openapi-runtime",
+ "state" : {
+ "revision" : "7cdf33371bf89b23b9cf4fd3ce8d3c825c28fbe8",
+ "version" : "1.9.0"
+ }
+ },
+ {
+ "identity" : "swift-openapi-urlsession",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/apple/swift-openapi-urlsession",
+ "state" : {
+ "revision" : "279aa6b77be6aa842a4bf3c45fa79fa15edf3e07",
+ "version" : "1.2.0"
+ }
+ },
+ {
+ "identity" : "swift-timeout",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/swhitty/swift-timeout.git",
+ "state" : {
+ "revision" : "4efb73b593d5553b90766d531db701ecf2306237",
+ "version" : "0.4.1"
+ }
+ }
+ ],
+ "version" : 3
+}
diff --git a/CommonUI/Package.swift b/CommonUI/Package.swift
new file mode 100644
index 000000000..f97ec6c42
--- /dev/null
+++ b/CommonUI/Package.swift
@@ -0,0 +1,45 @@
+// swift-tools-version: 6.2
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+ name: "CommonUI",
+ platforms: [.iOS("26.0"), .watchOS("26.0"), .macOS("26.0")],
+ products: [
+ // Products define the executables and libraries a package produces, making them visible to other packages.
+ .library(
+ name: "CommonUI",
+ targets: ["CommonUI"]
+ )
+ ],
+ dependencies: [
+ .package(path: "../OpenHABCore"),
+ .package(url: "https://github.com/apple/swift-numerics", from: "1.0.0")
+ ],
+ targets: [
+ // Targets are the basic building blocks of a package, defining a module or a test suite.
+ // Targets can depend on other targets in this package and products from dependencies.
+ .target(
+ name: "CommonUI",
+ dependencies: ["OpenHABCore"],
+ swiftSettings: [
+ .enableUpcomingFeature("ExistentialAny"),
+ // .enableUpcomingFeature("InternalImportsByDefault"),
+ .enableUpcomingFeature("MemberImportVisibility"),
+ .enableUpcomingFeature("StrictConcurrency"),
+ .unsafeFlags([
+ "-Xfrontend", "-enable-actor-data-race-checks",
+ "-Xfrontend", "-strict-concurrency=complete"
+ ])
+ ]
+ ),
+ .testTarget(
+ name: "CommonUITests",
+ dependencies: [
+ .product(name: "Numerics", package: "swift-numerics"),
+ "CommonUI"
+ ]
+ )
+ ]
+)
diff --git a/CommonUI/Sources/CommonUI/ColorExtension.swift b/CommonUI/Sources/CommonUI/ColorExtension.swift
new file mode 100644
index 000000000..41d93a213
--- /dev/null
+++ b/CommonUI/Sources/CommonUI/ColorExtension.swift
@@ -0,0 +1,53 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import OpenHABCore
+import SwiftUI
+
+public extension Color {
+ init(fromString string: String) {
+ self.init(UIColor(fromString: string))
+ }
+
+ init(hex: String) {
+ self.init(UIColor(hex: hex))
+ }
+}
+
+public typealias Kelvin = Double
+
+public extension Color {
+ init(temperature: Kelvin) {
+ let components = componentsForColorTemperature(temperature: temperature)
+ self.init(red: components.r, green: components.g, blue: components.b)
+ }
+
+ func hexString() -> String {
+ let components = cgColor?.components
+ let r: CGFloat = components?[0] ?? 0.0
+ let g: CGFloat = components?[1] ?? 0.0
+ let b: CGFloat = components?[2] ?? 0.0
+
+ let hexString = String(format: "#%02lX%02lX%02lX", lroundf(Float(r * 255)), lroundf(Float(g * 255)), lroundf(Float(b * 255)))
+ return hexString
+ }
+}
+
+// For algorithm see https://web.archive.org/web/20151024031939/http://www.zombieprototypes.com/?p=210
+// Algorithm see http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
+// swiftlint:disable:next large_tuple
+func componentsForColorTemperature(temperature: Kelvin) -> (r: Double, g: Double, b: Double) {
+ let k = temperature / 100
+ let r = (k <= 66 ? 255 : (329.698727446 * pow(k - 60, -0.1332047592))).clamped(to: 0 ... 255.0) / 255
+ let g = (k <= 66 ? (99.4708025861 * log(k) - 161.1195681661) : 288.1221695283 * pow(k - 60, -0.0755148492)).clamped(to: 0 ... 255.0) / 255
+ let b = (k >= 66 ? 255 : (k <= 19 ? 0 : 138.5177312231 * log(k - 10) - 305.0447927307)).clamped(to: 0 ... 255.0) / 255
+ return (r: r, g: g, b: b)
+}
diff --git a/openHABWatch/Views/LogsViewer.swift b/CommonUI/Sources/CommonUI/LogsViewer.swift
similarity index 97%
rename from openHABWatch/Views/LogsViewer.swift
rename to CommonUI/Sources/CommonUI/LogsViewer.swift
index 9ba294479..4ec650f12 100644
--- a/openHABWatch/Views/LogsViewer.swift
+++ b/CommonUI/Sources/CommonUI/LogsViewer.swift
@@ -15,7 +15,7 @@ import SwiftUI
// Thanks to https://useyourloaf.com/blog/fetching-oslog-messages-in-swift/
-struct LogsViewer: View {
+public struct LogsViewer: View {
private static let template = NSPredicate(format:
"(subsystem BEGINSWITH $PREFIX)")
@@ -25,7 +25,7 @@ struct LogsViewer: View {
.system(size: 10)
.monospaced()
- var body: some View {
+ public var body: some View {
ScrollView {
Text(text)
.font(myFont)
@@ -36,6 +36,8 @@ struct LogsViewer: View {
}
}
+ public init() {}
+
private func fetchLogs() async -> String {
let calendar = Calendar.current
guard let dayAgo = calendar.date(
diff --git a/CommonUI/Sources/CommonUI/OHTextTokenStyle.swift b/CommonUI/Sources/CommonUI/OHTextTokenStyle.swift
new file mode 100644
index 000000000..a2e34e667
--- /dev/null
+++ b/CommonUI/Sources/CommonUI/OHTextTokenStyle.swift
@@ -0,0 +1,79 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import SwiftUI
+
+public enum OHTextToken {
+ case rowLabel
+ case rowValue
+ case rowValueCompact
+ case rowValueCallout
+ case section
+ case control
+ case secondary
+ case emphasis
+}
+
+public enum OHAccessibilityToken {
+ public static let minimumHitTarget: CGFloat = 44
+}
+
+private struct OHTextTokenModifier: ViewModifier {
+ let token: OHTextToken
+
+ func body(content: Content) -> some View {
+ let style = OHTextTokenStyle.from(token)
+ content
+ .font(style.font)
+ .lineLimit(style.lineLimit)
+ .minimumScaleFactor(style.minimumScaleFactor)
+ .truncationMode(.tail)
+ .multilineTextAlignment(.leading)
+ }
+}
+
+private struct OHTextTokenStyle {
+ let font: Font
+ let lineLimit: Int
+ let minimumScaleFactor: CGFloat
+
+ static func from(_ token: OHTextToken) -> OHTextTokenStyle {
+ switch token {
+ case .rowLabel:
+ OHTextTokenStyle(font: .body, lineLimit: 1, minimumScaleFactor: 0.9)
+ case .rowValue:
+ OHTextTokenStyle(font: .body, lineLimit: 1, minimumScaleFactor: 0.9)
+ case .rowValueCompact:
+ OHTextTokenStyle(font: .caption, lineLimit: 1, minimumScaleFactor: 0.9)
+ case .rowValueCallout:
+ OHTextTokenStyle(font: .callout, lineLimit: 1, minimumScaleFactor: 0.9)
+ case .section:
+ OHTextTokenStyle(font: .callout, lineLimit: 1, minimumScaleFactor: 0.85)
+ case .control:
+ OHTextTokenStyle(font: .footnote, lineLimit: 1, minimumScaleFactor: 0.85)
+ case .secondary:
+ OHTextTokenStyle(font: .caption, lineLimit: 1, minimumScaleFactor: 0.9)
+ case .emphasis:
+ OHTextTokenStyle(font: .headline, lineLimit: 1, minimumScaleFactor: 0.9)
+ }
+ }
+}
+
+public extension View {
+ func ohTextToken(_ token: OHTextToken) -> some View {
+ modifier(OHTextTokenModifier(token: token))
+ }
+
+ /// Applies the standard minimum tappable target used across row controls.
+ func ohMinimumHitTarget(_ minHeight: CGFloat = OHAccessibilityToken.minimumHitTarget) -> some View {
+ frame(minHeight: minHeight)
+ }
+}
diff --git a/CommonUI/Sources/CommonUI/PreviewConstants.swift b/CommonUI/Sources/CommonUI/PreviewConstants.swift
new file mode 100644
index 000000000..bda1d6366
--- /dev/null
+++ b/CommonUI/Sources/CommonUI/PreviewConstants.swift
@@ -0,0 +1,583 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import Foundation
+import OpenHABCore
+import os.log
+
+#if DEBUG
+// swiftlint:disable type_body_length
+public enum PreviewConstants {
+ public static let logger = Logger(subsystem: "org.openhab", category: "PreviewConstants")
+
+ public static let remoteURLString = "http://192.168.2.10:8080"
+
+ public static var openHABSitemapPage: OpenHABPage? {
+ let data = sitemapJson
+ do {
+ let sitemapPage = try data.decoded(as: Components.Schemas.PageDTO.self)
+ let openHABSitemapPage = OpenHABPage(sitemapPage)
+ return openHABSitemapPage
+ } catch {
+ logger.error("Should not throw \(error.localizedDescription)")
+ return nil
+ }
+ }
+
+ public static let sitemapJson = Data("""
+ {
+ "id": "watch",
+ "title": "watch",
+ "link": "http://192.168.2.10:8080/rest/sitemaps/watch/watch",
+ "leaf": true,
+ "timeout": false,
+ "widgets": [
+ {
+ "widgetId": "00",
+ "type": "Switch",
+ "visibility": true,
+ "label": "Licht Keller WC Decke",
+ "icon": "switch",
+ "mappings": [],
+ "item": {
+ "link": "http://192.168.2.15:8081/rest/items/lcnLightSwitch6_1",
+ "state": "OFF",
+ "type": "Switch",
+ "name": "lcnLightSwitch6_1",
+ "label": "Licht Keller WC Decke",
+ "tags": [
+ "Lighting"
+ ],
+ "groupNames": [
+ "gKellerLicht",
+ "gLcn"
+ ]
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "01",
+ "type": "Switch",
+ "visibility": true,
+ "label": "Licht Oberlicht",
+ "icon": "switch",
+ "mappings": [],
+ "item": {
+ "link": "http://192.168.2.15:8081/rest/items/lcnLightSwitch14_1",
+ "state": "OFF",
+ "type": "Switch",
+ "name": "lcnLightSwitch14_1",
+ "label": "Licht Oberlicht",
+ "tags": [
+ "Lighting"
+ ],
+ "groupNames": [
+ "gEGLicht",
+ "G_PresenceSimulation",
+ "gLcn"
+ ]
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "02",
+ "type": "Switch",
+ "visibility": true,
+ "label": "Licht Esstisch [Test]",
+ "icon": "switch",
+ "mappings": [],
+ "item": {
+ "link": "http://192.168.2.15:8081/rest/items/lcnLightSwitch20_1",
+ "state": "ON",
+ "type": "Switch",
+ "name": "lcnLightSwitch20_1",
+ "label": "Licht Esstisch",
+ "tags": [],
+ "groupNames": [
+ "gEGLicht",
+ "G_PresenceSimulation",
+ "gLcn",
+ "gStateON"
+ ]
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "03",
+ "type": "Slider",
+ "visibility": true,
+ "label": "Esstisch [100]",
+ "icon": "slider",
+ "mappings": [],
+ "switchSupport": false,
+ "sendFrequency": 0,
+ "item": {
+ "link": "http://192.168.2.10:8080/rest/items/lcnLightDimmer",
+ "state": "95",
+ "stateDescription": {
+ "pattern": "%s",
+ "readOnly": false,
+ "options": []
+ },
+ "type": "Dimmer",
+ "name": "lcnLightDimmer",
+ "label": "Esstisch",
+ "tags": [
+ "Lighting"
+ ],
+ "groupNames": [
+ "gEGLicht",
+ "gLcn"
+ ]
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "04",
+ "type": "Switch",
+ "visibility": true,
+ "label": "Fernsteuerung",
+ "icon": "switch",
+ "mappings": [
+ {
+ "command": "0",
+ "label": "Overwrite"
+ },
+ {
+ "command": "1",
+ "label": "Kalender"
+ },
+ {
+ "command": "2",
+ "label": "Automatik"
+ }
+ ],
+ "item": {
+ "link": "http://192.168.2.15:8081/rest/items/Automatik",
+ "state": "2",
+ "type": "String",
+ "name": "Automatik",
+ "tags": [],
+ "groupNames": []
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "05",
+ "type": "Switch",
+ "visibility": true,
+ "label": "Jalousie WZ Süd links",
+ "icon": "rollershutter",
+ "mappings": [],
+ "item": {
+ "link": "http://192.168.2.15:8081/rest/items/lcnJalousieWZSuedLinks",
+ "state": "0",
+ "type": "Rollershutter",
+ "name": "lcnJalousieWZSuedLinks",
+ "label": "Jalousie WZ Süd links",
+ "tags": [],
+ "groupNames": [
+ "gWZ",
+ "gEGJalousien",
+ "gHausJalousie",
+ "gJalousienSued",
+ "gEGJalousienSued",
+ "gLcn"
+ ]
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "06",
+ "type": "Setpoint",
+ "visibility": true,
+ "label": "Setpoint Temperature [21.0 °C]",
+ "icon": "temperature",
+ "mappings": [],
+ "minValue": 8,
+ "maxValue": 25,
+ "step": 0.5,
+ "item": {
+ "link": "http://192.168.2.15:8081/rest/items/ZimmerPaul_SetpointTemperature",
+ "state": "21.0 °C",
+ "stateDescription": {
+ "pattern": "%.1f %unit%",
+ "readOnly": false,
+ "options": []
+ },
+ "type": "Number:Temperature",
+ "name": "ZimmerPaul_SetpointTemperature",
+ "label": "Setpoint Temperature [%.1f °C]",
+ "category": "Temperature",
+ "tags": [],
+ "groupNames": []
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "07",
+ "type": "Text",
+ "visibility": true,
+ "label": "Aussentemperatur [11.5 °C]",
+ "icon": "temperature",
+ "mappings": [],
+ "item": {
+ "link": "http://192.168.2.15:8081/rest/items/TempAussen",
+ "state": "11.5",
+ "stateDescription": {
+ "pattern": "%.1f °C",
+ "readOnly": false,
+ "options": []
+ },
+ "type": "Number",
+ "name": "TempAussen",
+ "label": "Aussentemperatur",
+ "category": "temperature",
+ "tags": [],
+ "groupNames": [
+ "gOnewire"
+ ]
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "08",
+ "type": "Image",
+ "visibility": true,
+ "label": "",
+ "icon": "image",
+ "mappings": [],
+ "url": "http://192.168.2.15:8081/proxy?sitemap=watch.sitemap&widgetId=08",
+ "widgets": []
+ },
+ {
+ "widgetId": "09",
+ "type": "Mapview",
+ "visibility": true,
+ "label": "Location [-°N -°E -m]",
+ "icon": "mapview",
+ "mappings": [],
+ "height": 5,
+ "item": {
+ "link": "http://192.168.2.15:8081/rest/items/GPSTrackerTi_Location",
+ "state":"52.5200066,13.4049540",
+ "stateDescription": {
+ "pattern": "%2$s°N %3$s°E %1$sm",
+ "readOnly": true,
+ "options": []
+ },
+ "type": "Location",
+ "name": "GPSTrackerTi_Location",
+ "label": "Location",
+ "tags": [],
+ "groupNames": []
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "10",
+ "type": "Colorpicker",
+ "visibility": true,
+ "label": "Color",
+ "icon": "colorlight",
+ "mappings": [],
+ "item": {
+ "link": "http://192.168.2.160:8080/rest/items/LEDVANCECLA60RGBWZ3_Color",
+ "state": "UNDEF",
+ "type": "Color",
+ "name": "LEDVANCECLA60RGBWZ3_Color",
+ "label": "Color",
+ "category": "ColorLight",
+ "tags": [],
+ "groupNames": [
+ "dg_AllItems"
+ ]
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "11",
+ "type": "Setpoint",
+ "visibility": true,
+ "label": "item in seconds [2400.0 s]",
+ "labelSource": "SITEMAP_WIDGET",
+ "icon": "",
+ "staticIcon": false,
+ "pattern": "%.1f s",
+ "unit": "s",
+ "mappings": [],
+ "minValue": 300,
+ "maxValue": 3600,
+ "step": 60,
+ "item": {
+ "link": "http://192.168.2.10:8080/rest/items/testTime",
+ "state": "2400 s",
+ "stateDescription": {
+ "pattern": "%.0f %unit%",
+ "readOnly": false,
+ "options": []
+ },
+ "unitSymbol": "s",
+ "type": "Number:Time",
+ "name": "testTime",
+ "label": "",
+ "category": "",
+ "tags": [],
+ "groupNames": []
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "12",
+ "type": "Setpoint",
+ "visibility": true,
+ "label": "item in minutes [40.0 min]",
+ "labelSource": "SITEMAP_WIDGET",
+ "icon": "",
+ "staticIcon": false,
+ "pattern": "%.1f min",
+ "unit": "min",
+ "mappings": [],
+ "minValue": 5,
+ "maxValue": 60,
+ "step": 5,
+ "state": "40 min",
+ "item": {
+ "link": "http://192.168.2.10:8080/rest/items/testTime",
+ "state": "2400 s",
+ "stateDescription": {
+ "pattern": "%.0f %unit%",
+ "readOnly": false,
+ "options": []
+ },
+ "unitSymbol": "s",
+ "type": "Number:Time",
+ "name": "testTime",
+ "label": "",
+ "category": "",
+ "tags": [],
+ "groupNames": []
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "13",
+ "type": "Colortemperaturepicker",
+ "visibility": true,
+ "label": "Color Temperature [2700 K]",
+ "labelSource": "SITEMAP_WIDGET",
+ "icon": "colorwheel",
+ "staticIcon": true,
+ "pattern": "%.0f %unit%",
+ "unit": "K",
+ "mappings": [],
+ "item": {
+ "link": "http://192.168.2.10:8080/rest/items/test_LEDLight_ColorTemp",
+ "state": "2700.0 K",
+ "stateDescription": {
+ "pattern": "%.0f %unit%",
+ "readOnly": false,
+ "options": []
+ },
+ "unitSymbol": "K",
+ "type": "Number:Temperature",
+ "name": "test_LEDLight_ColorTemp",
+ "label": "",
+ "category": "",
+ "tags": [],
+ "groupNames": []
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "14",
+ "type": "Slider",
+ "visibility": true,
+ "label": "Brightness",
+ "labelSource": "SITEMAP_WIDGET",
+ "icon": "",
+ "staticIcon": false,
+ "unit": "",
+ "mappings": [],
+ "switchSupport": false,
+ "releaseOnly": false,
+ "item": {
+ "link": "http://192.168.2.10:8080/rest/items/test_LEDLight_Brightness",
+ "state": "NULL",
+ "type": "Dimmer",
+ "name": "test_LEDLight_Brightness",
+ "label": "",
+ "category": "",
+ "tags": [],
+ "groupNames": []
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "15",
+ "type": "Colorpicker",
+ "visibility": true,
+ "label": "Color",
+ "labelSource": "SITEMAP_WIDGET",
+ "icon": "colorwheel",
+ "staticIcon": false,
+ "unit": "",
+ "mappings": [],
+ "item": {
+ "link": "http://192.168.2.10:8080/rest/items/test_LEDLight_color",
+ "state": "0,0,73",
+ "type": "Color",
+ "name": "test_LEDLight_color",
+ "label": "test_LEDLight_color",
+ "category": "",
+ "tags": [],
+ "groupNames": []
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "16",
+ "type": "Buttongrid",
+ "visibility": true,
+ "label": "Remote Control [-]",
+ "labelSource": "SITEMAP_WIDGET",
+ "icon": "screen",
+ "staticIcon": true,
+ "pattern": "%s",
+ "unit": "",
+ "mappings": [
+ {
+ "row": 1,
+ "column": 1,
+ "command": "POWER",
+ "label": "Power",
+ "icon": "switch-off"
+ },
+ {
+ "row": 1,
+ "column": 2,
+ "command": "MENU",
+ "label": "Menu"
+ },
+ {
+ "row": 1,
+ "column": 3,
+ "command": "EXIT",
+ "label": "Exit"
+ },
+ {
+ "row": 2,
+ "column": 2,
+ "command": "UP",
+ "label": "Up",
+ "icon": "f7:arrowtriangle_up"
+ },
+ {
+ "row": 2,
+ "column": 4,
+ "command": "VOL_PLUS",
+ "label": "Volume +"
+ },
+ {
+ "row": 3,
+ "column": 1,
+ "command": "LEFT",
+ "label": "Left",
+ "icon": "f7:arrowtriangle_left"
+ },
+ {
+ "row": 3,
+ "column": 2,
+ "command": "OK",
+ "label": "Ok"
+ },
+ {
+ "row": 3,
+ "column": 3,
+ "command": "RIGHT",
+ "label": "Right",
+ "icon": "f7:arrowtriangle_right"
+ },
+ {
+ "row": 3,
+ "column": 4,
+ "command": "MUTE",
+ "label": "Mute",
+ "icon": "soundvolume_mute"
+ },
+ {
+ "row": 4,
+ "column": 2,
+ "command": "DOWN",
+ "label": "Down",
+ "icon": "f7:arrowtriangle_down"
+ },
+ {
+ "row": 4,
+ "column": 4,
+ "command": "VOL_MINUS",
+ "label": "Volume -"
+ }
+ ],
+ "item": {
+ "link": "http://192.168.2.10:8080/rest/items/test_RemoteControl",
+ "state": "NULL",
+ "stateDescription": {
+ "pattern": "%s",
+ "readOnly": false,
+ "options": []
+ },
+ "type": "String",
+ "name": "test_RemoteControl",
+ "label": "test_RemoteControl",
+ "category": "",
+ "tags": [],
+ "groupNames": []
+ },
+ "widgets": []
+ },
+ {
+ "widgetId": "17",
+ "type": "Input",
+ "visibility": true,
+ "label": "Meter [166000]",
+ "labelSource": "SITEMAP_WIDGET",
+ "icon": "energy",
+ "staticIcon": true,
+ "pattern": "%.0f %unit%",
+ "unit": "",
+ "mappings": [],
+ "inputHint": "number",
+ "item": {
+ "link": "http://192.168.2.10:8080/rest/items/Test_Meter_Reading",
+ "state": "166000.0",
+ "stateDescription": {
+ "pattern": "%.0f",
+ "readOnly": false,
+ "options": []
+ },
+ "type": "Number",
+ "name": "Test_Meter_Reading",
+ "label": "Test_Meter_Reading",
+ "category": "",
+ "tags": [],
+ "groupNames": []
+ },
+ "widgets": []
+ },
+
+ ]
+ }
+ """.utf8)
+}
+
+// swiftlint:enable type_body_length
+#endif
diff --git a/openHABWatch/Views/Utils/TextLabelView.swift b/CommonUI/Sources/CommonUI/TextLabelView.swift
similarity index 51%
rename from openHABWatch/Views/Utils/TextLabelView.swift
rename to CommonUI/Sources/CommonUI/TextLabelView.swift
index 7e15262a8..3fac62d18 100644
--- a/openHABWatch/Views/Utils/TextLabelView.swift
+++ b/CommonUI/Sources/CommonUI/TextLabelView.swift
@@ -12,19 +12,27 @@
import OpenHABCore
import SwiftUI
-struct TextLabelView: View {
+public struct TextLabelView: View {
@ObservedObject var widget: OpenHABWidget
- var lineLimit = 2
+ var font: Font?
+ var token: OHTextToken
+ var lineLimit: Int
- var body: some View {
+ public var body: some View {
Text(widget.labelText ?? "")
- .font(.caption)
+ .ohTextToken(token)
+ .font(font)
.lineLimit(lineLimit)
- .foregroundColor(!widget.labelcolor.isEmpty ? Color(fromString: widget.labelcolor) : .primary)
+ .foregroundStyle(!widget.labelcolor.isEmpty ? Color(fromString: widget.labelcolor) : .primary)
}
-}
-#Preview {
- let widget = UserData(preview: true).widgets[2]
- TextLabelView(widget: widget)
+ public init(widget: OpenHABWidget,
+ font: Font? = nil,
+ token: OHTextToken = .rowLabel,
+ lineLimit: Int = 1) {
+ self.widget = widget
+ self.font = font
+ self.token = token
+ self.lineLimit = lineLimit
+ }
}
diff --git a/CommonUI/Tests/CommonUITests/CommonUITests.swift b/CommonUI/Tests/CommonUITests/CommonUITests.swift
new file mode 100644
index 000000000..ed08dfbb7
--- /dev/null
+++ b/CommonUI/Tests/CommonUITests/CommonUITests.swift
@@ -0,0 +1,48 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+@testable import CommonUI
+import Numerics
+import Testing
+
+struct CommonUITests {
+ @Test func lowKelvinValue() {
+ let color = componentsForColorTemperature(temperature: 1000)
+ #expect(color.r.isApproximatelyEqual(to: 1.0, relativeTolerance: 0.01))
+ #expect(color.g < 0.5)
+ #expect(color.b.isApproximatelyEqual(to: 0.0, relativeTolerance: 0.01))
+ }
+
+ @Test func midKelvinValue() {
+ let color = componentsForColorTemperature(temperature: 5000)
+ #expect(color.r > 0.9)
+ #expect(color.g > 0.7)
+ #expect(color.b > 0.5)
+ }
+
+ @Test func highKelvinValue() {
+ let color = componentsForColorTemperature(temperature: 10000)
+ #expect(color.r > 0.7)
+ #expect(color.g > 0.8)
+ #expect(color.b.isApproximatelyEqual(to: 1.0, relativeTolerance: 0.01))
+ }
+
+ @Test func edgeKelvinValues() {
+ let veryLow = componentsForColorTemperature(temperature: 100)
+ #expect(veryLow.r.isApproximatelyEqual(to: 1.0, relativeTolerance: 0.01))
+ #expect(veryLow.b.isApproximatelyEqual(to: 0.0, relativeTolerance: 0.01))
+
+ let veryHigh = componentsForColorTemperature(temperature: 40000)
+ #expect(veryHigh.r <= 1.0)
+ #expect(veryHigh.g <= 1.0)
+ #expect(veryHigh.b.isApproximatelyEqual(to: 1.0, relativeTolerance: 0.01))
+ }
+}
diff --git a/OpenHABCore/Package.swift b/OpenHABCore/Package.swift
index e986e4632..e7893b109 100644
--- a/OpenHABCore/Package.swift
+++ b/OpenHABCore/Package.swift
@@ -1,11 +1,11 @@
-// swift-tools-version: 6.1
+// swift-tools-version: 6.2
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "OpenHABCore",
- platforms: [.iOS(.v16), .watchOS(.v10), .macOS(.v14)],
+ platforms: [.iOS("26.0"), .watchOS("26.0"), .macOS("26.0")],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
@@ -21,7 +21,8 @@ let package = Package(
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.5.1"),
.package(url: "https://github.com/SDWebImage/SDWebImageSVGCoder.git", from: "1.4.0"),
.package(url: "https://github.com/SFSafeSymbols/SFSafeSymbols.git", from: "7.0.0"),
- .package(url: "https://github.com/swhitty/swift-timeout.git", from: "0.4.0")
+ .package(url: "https://github.com/swhitty/swift-timeout.git", from: "0.4.0"),
+ .package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0")
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
@@ -35,7 +36,8 @@ let package = Package(
.product(name: "HTTPTypes", package: "swift-http-types"),
.product(name: "SDWebImageSVGCoder", package: "SDWebImageSVGCoder"),
.product(name: "SFSafeSymbols", package: "SFSafeSymbols"),
- .product(name: "Timeout", package: "swift-timeout")
+ .product(name: "Timeout", package: "swift-timeout"),
+ .product(name: "AsyncAlgorithms", package: "swift-async-algorithms")
],
swiftSettings: [
.enableUpcomingFeature("ExistentialAny"),
diff --git a/OpenHABCore/Sources/OpenHABCore/Model/NumberState.swift b/OpenHABCore/Sources/OpenHABCore/Model/NumberState.swift
index 917dc72fb..b26390cf4 100644
--- a/OpenHABCore/Sources/OpenHABCore/Model/NumberState.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Model/NumberState.swift
@@ -37,11 +37,18 @@ public struct NumberState: CustomStringConvertible, Equatable {
public func toString(locale: Locale?) -> String {
if let format, !format.isEmpty {
- let actualFormat = format
+ var actualFormat = format
.replacingOccurrences(of: "%unit%", with: unit ?? "")
// %s in Java is for Strings, but does not work in Swift, see
// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Strings/Articles/formatSpecifiers.html)
.replacingOccurrences(of: "%s", with: "%@")
+
+ // Escape trailing % that isn't already escaped (e.g., "%.0f %" should become "%.0f %%")
+ // This handles server-side format patterns that forgot to escape the percent sign
+ if actualFormat.hasSuffix(" %"), !actualFormat.hasSuffix(" %%") {
+ actualFormat = String(actualFormat.dropLast()) + "%%"
+ }
+
let formatValue: any CVarArg = if format.contains("%d") {
intValue
} else if format.contains("%s") {
diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABItem.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABItem.swift
index f75c5a9ca..dab02b398 100644
--- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABItem.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABItem.swift
@@ -122,6 +122,20 @@ public extension OpenHABItem {
}
return nil
}
+
+ func getImagePayload() -> ImagePayload {
+ switch type {
+ case .image:
+ guard let data = state?.components(separatedBy: ",")[safe: 1], let decodedData = Data(base64Encoded: data, options: .ignoreUnknownCharacters) else {
+ return .empty
+ }
+ return .embedded(data: decodedData)
+ case .stringItem:
+ return .link(url: URL(string: state ?? ""))
+ default:
+ return .empty
+ }
+ }
}
extension OpenHABItem {
diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift
index cb070e84b..1a91a0311 100644
--- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidget.swift
@@ -9,33 +9,11 @@
//
// SPDX-License-Identifier: EPL-2.0
-import Combine
+@_exported import Combine
import Foundation
-import MapKit
+@_exported import MapKit
import os.log
-public enum WidgetTypeEnum {
- case switcher(Bool)
- case slider //
- case segmented(Int)
- case unassigned
- case rollershutter
- case frame
- case setpoint
- case selection
- case colorpicker
- case chart
- case image
- case video
- case webview
- case mapview
-
- public var boolState: Bool {
- guard case let .switcher(value) = self else { return false }
- return value
- }
-}
-
public class OpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableObject {
public enum WidgetType: String, Decodable {
case chart = "Chart"
@@ -53,11 +31,38 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableObje
case text = "Text"
case video = "Video"
case webview = "Webview"
+ case colortemperaturepicker = "Colortemperaturepicker"
+ case buttongrid = "Buttongrid"
+ case button = "Button"
case unknown = "Unknown"
}
+ public enum LabelSource: String, Decodable {
+ case sitemapDefinition = "SITEMAP_WIDGET"
+ case itemLabel = "ITEM_LABEL"
+ case itemName = "ITEM_NAME"
+ case unknown = "UNKNOWN"
+ }
+
public enum InputHint: String, Decodable {
- case text, number, date, time, datetime, unknown
+ case text, number, date, time, dateTime, unknown
+
+ public init(rawValue: String) {
+ switch rawValue.lowercased() {
+ case "text":
+ self = .text
+ case "number":
+ self = .number
+ case "date":
+ self = .date
+ case "time":
+ self = .time
+ case "datetime", "dateTime":
+ self = .dateTime
+ default:
+ self = .unknown
+ }
+ }
}
public var id = ""
@@ -65,7 +70,8 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableObje
public var sendCommand: ((_ item: OpenHABItem, _ command: String?) -> Void)?
public var widgetId = ""
@Published public var label = ""
- public var icon = ""
+ @Published public var icon = ""
+
public var type: WidgetType = .unknown
public var url = ""
public var period = ""
@@ -90,14 +96,22 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableObje
public var mappings: [OpenHABWidgetMapping] = []
public var widgets: [OpenHABWidget] = []
public var visibility = true
- public var unit = ""
- public var pattern = ""
- public var staticIcon: Bool?
- public var labelSource = ""
+ public var unit: String?
+ public var pattern: String?
+ @Published public var staticIcon: Bool?
public var switchSupport = false
- public var yAxisDecimalPattern: String?
+ public var labelSource = LabelSource.unknown
+ public var releaseOnly: Bool?
+ public var row: Int?
+ public var column: Int?
+ public var releaseCommand: String?
+ public var command: String?
+ public var stateless: Bool?
+ public var readOnly: Bool? {
+ item?.stateDescription?.readOnly
+ }
- @Published public var stateEnumBinding: WidgetTypeEnum = .unassigned
+ public var yAxisDecimalPattern: String?
// Text prior to "["
public var labelText: String? {
@@ -126,6 +140,11 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableObje
}
}
+ /// Returns true if any mapping has press-and-release behavior
+ public var hasPressReleaseMappings: Bool {
+ mappingsOrItemOptions.contains { $0.hasPressReleaseBehavior }
+ }
+
public var stateValueAsBool: Bool? {
item?.state?.parseAsBool()
}
@@ -154,46 +173,6 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableObje
}
}
- public var stateEnum: WidgetTypeEnum {
- switch type {
- case .frame:
- .frame
- case .switchWidget:
- // Reflecting the discussion held in https://github.com/openhab/openhab-core/issues/952
- if !mappings.isEmpty {
- .segmented(Int(mappingIndex(byCommand: item?.state) ?? -1))
- } else if item?.isOfTypeOrGroupType(.switchItem) ?? false {
- .switcher(item?.state == "ON" ? true : false)
- } else if item?.isOfTypeOrGroupType(.rollershutter) ?? false {
- .rollershutter
- } else if !mappingsOrItemOptions.isEmpty {
- .segmented(Int(mappingIndex(byCommand: item?.state) ?? -1))
- } else {
- .switcher(item?.state == "ON" ? true : false)
- }
- case .setpoint:
- .setpoint
- case .slider:
- .slider
- case .selection:
- .selection
- case .colorpicker:
- .colorpicker
- case .chart:
- .chart
- case .image:
- .image
- case .video:
- .video
- case .webview:
- .webview
- case .mapview:
- .mapview
- default:
- .unassigned
- }
- }
-
public func sendItemUpdate(state: NumberState?) {
guard let item, let state else {
Logger.restAPI.info("ItemUpdate for Item or State = nil")
@@ -228,31 +207,39 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableObje
mappingsOrItemOptions.firstIndex { $0.command == command }
}
- public func iconState() -> String {
- var iconState = item?.state ?? ""
- if let item, let itemState = item.state {
- if item.isOfTypeOrGroupType(.color) {
- // For items that control a color item fetch the correct icon
- if type == .slider || (type == .switchWidget && mappings.isEmpty) {
- if let brightness = itemState.parseAsBrightness() {
- iconState = String(brightness)
- if type == .switchWidget {
- iconState = iconState == "0" ? "OFF" : "ON"
- }
+ public func mapCommandtoIndex(with command: String?) -> Int {
+ Int(mappingIndex(byCommand: command) ?? 0)
+ }
+
+ public func iconState() -> String? {
+ guard let item, let itemState = item.state else { return nil }
+ guard !itemState.isNoneIcon else { return nil }
+ if item.isOfTypeOrGroupType(.color) {
+ // For items that control a color item fetch the correct icon
+ if type == .slider || (type == .switchWidget && mappings.isEmpty) {
+ if let brightness = itemState.parseAsBrightness() {
+ let brightness = String(brightness)
+ if type == .switchWidget {
+ return brightness == "0" ? "OFF" : "ON"
} else {
- iconState = "OFF"
+ return brightness
}
- } else if let color = itemState.parseAsUIColor() {
- iconState = "#\(color.toHex() ?? "000000")"
+ } else {
+ return "OFF"
}
- } else if type == .switchWidget, mappings.isEmpty, !item.isOfTypeOrGroupType(.rollershutter) {
- // For switch items without mappings (just ON and OFF) that control a dimmer item
- // and which are not ON or OFF already, set the state to "OFF" instead of 0
- // or to "ON" to fetch the correct icon
- iconState = (itemState == "0" || itemState == "OFF") ? "OFF" : "ON"
+ } else if let color = itemState.parseAsUIColor() {
+ return "#\(color.hexString ?? "000000")"
}
+ } else if item.isOfTypeOrGroupType(.number) || item.isOfTypeOrGroupType(.numberWithDimension) {
+ let numberState = itemState.parseAsNumber(format: item.stateDescription?.numberPattern)
+ return numberState.toString(locale: Locale(identifier: "US"))
+ } else if type == .switchWidget, mappings.isEmpty, !item.isOfTypeOrGroupType(.rollershutter) {
+ // For switch items without mappings (just ON and OFF) that control a dimmer item
+ // and which are not ON or OFF already, set the state to "OFF" instead of 0
+ // or to "ON" to fetch the correct icon
+ return (itemState == "0" || itemState == "OFF") ? "OFF" : "ON"
}
- return iconState
+ return itemState
}
private func adj(_ raw: Double) -> Double {
@@ -260,6 +247,41 @@ public class OpenHABWidget: NSObject, MKAnnotation, Identifiable, ObservableObje
valueAdjustedToStep += minValue
return valueAdjustedToStep.clamped(to: minValue ... maxValue)
}
+
+ public func generateImageResult(rootUrl: String,
+ chartStyle: ChartStyle = .light) -> ImagePayload {
+ switch type {
+ case .chart:
+ guard let url = Endpoint.chart(
+ rootUrl: rootUrl,
+ period: period,
+ type: item?.type,
+ service: service,
+ name: item?.name,
+ legend: legend,
+ theme: chartStyle,
+ forceAsItem: forceAsItem,
+ yAxisDecimalPattern: yAxisDecimalPattern
+ ).url else {
+ Logger.restAPI.error("Failed to generate chart URL")
+ return .empty
+ }
+ return .link(url: url)
+
+ case .image:
+ if let item {
+ return item.getImagePayload()
+ }
+ guard let url = URL(string: url) else {
+ Logger.restAPI.error("Invalid image URL: \(self.url)")
+ return .empty
+ }
+ return .link(url: url)
+
+ default:
+ return .empty
+ }
+ }
}
public extension OpenHABWidget {
@@ -292,10 +314,16 @@ public extension OpenHABWidget {
visibility: Bool?,
switchSupport: Bool?,
forceAsItem: Bool?,
- unit: String?,
- pattern: String?,
- staticIcon: Bool?,
- labelSource: String?,
+ labelSource: LabelSource = .unknown,
+ releaseOnly: Bool? = nil,
+ row: Int? = nil,
+ column: Int? = nil,
+ releaseCommand: String? = nil,
+ command: String? = nil,
+ stateless: Bool? = nil,
+ staticIcon: Bool? = nil,
+ unit: String? = nil,
+ pattern: String? = nil,
yAxisDecimalPattern: String? = nil) {
self.init()
id = widgetId
@@ -305,8 +333,7 @@ public extension OpenHABWidget {
self.icon = icon
self.url = url ?? ""
self.period = period ?? ""
- self.minValue = minValue ?? 0.0
- self.maxValue = maxValue ?? 100.0
+
self.step = step ?? 1.0
// Consider a minimal refresh rate of 100 ms, but 0 is special and means 'no refresh'
if let refreshVal = refresh, refreshVal > 0 {
@@ -331,19 +358,36 @@ public extension OpenHABWidget {
self.widgets = widgets
// Sanitize minValue, maxValue and step: min <= max, step >= 0
- self.maxValue = max(self.minValue, self.maxValue)
+ if type != .colortemperaturepicker {
+ self.minValue = minValue ?? 0.0
+ self.maxValue = maxValue ?? 100.0
+ self.maxValue = max(self.minValue, self.maxValue)
+ } else {
+ self.minValue = minValue ?? 1000.0
+ self.maxValue = maxValue ?? 10000.0
+ self.maxValue = max(self.minValue, self.maxValue)
+ }
self.step = abs(self.step)
self.visibility = visibility ?? true
self.switchSupport = switchSupport ?? false
self.forceAsItem = forceAsItem
- self.unit = unit ?? ""
- self.pattern = pattern ?? ""
- self.staticIcon = staticIcon ?? false
- self.labelSource = labelSource ?? ""
- stateEnumBinding = stateEnum
+ self.unit = unit
+ self.pattern = pattern
+ self.staticIcon = staticIcon
+ self.labelSource = labelSource
+ self.releaseOnly = releaseOnly
+ self.row = row
+ self.column = column
+ self.releaseCommand = releaseCommand
+ self.command = command
+ self.stateless = stateless
self.yAxisDecimalPattern = yAxisDecimalPattern
}
+
+ convenience init(icon: String, iconColor: String? = nil) {
+ self.init(widgetId: "\(UUID())", label: "", icon: icon, type: .unknown, url: nil, period: nil, minValue: nil, maxValue: nil, step: nil, refresh: nil, height: nil, isLeaf: nil, iconColor: iconColor, labelColor: nil, valueColor: nil, service: nil, state: nil, text: nil, legend: nil, inputHint: nil, encoding: nil, item: nil, linkedPage: nil, mappings: [], widgets: [], visibility: nil, switchSupport: nil, forceAsItem: nil, labelSource: .unknown, releaseOnly: nil)
+ }
}
// Recursive parsing of nested widget structure
@@ -351,7 +395,24 @@ public extension [OpenHABWidget] {
mutating func flatten(_ widgets: [Element]) {
for widget in widgets {
append(widget)
- flatten(widget.widgets)
+ if widget.type != .buttongrid {
+ flatten(widget.widgets)
+ }
+ }
+ }
+}
+
+public extension OpenHABWidget {
+ var preferredRowHeight: CGFloat? {
+ switch type {
+ case .frame:
+ label.isEmpty ? 0 : 35.0
+ case .image, .chart, .video:
+ nil // Automatic sizing
+ case .webview, .mapview:
+ 44.0 * CGFloat(height ?? 8)
+ default:
+ 44.0
}
}
}
@@ -378,7 +439,7 @@ extension OpenHABWidget {
state: widget.state,
text: "",
legend: widget.legend,
- inputHint: InputHint(rawValue: widget.inputHint ?? "unknown") ?? .unknown,
+ inputHint: InputHint(rawValue: widget.inputHint ?? "unknown"),
encoding: widget.encoding,
item: OpenHABItem(widget.item),
linkedPage: OpenHABPage(widget.linkedPage),
@@ -387,11 +448,43 @@ extension OpenHABWidget {
visibility: widget.visibility,
switchSupport: widget.switchSupport,
forceAsItem: widget.forceAsItem,
+ labelSource: OpenHABWidget.LabelSource(rawValue: widget.labelSource ?? "") ?? .unknown,
+ releaseOnly: widget.releaseOnly,
+ row: widget.row.map { Int($0) },
+ column: widget.column.map { Int($0) },
+ releaseCommand: widget.releaseCommand,
+ command: widget.command,
+ stateless: widget.stateless,
+ staticIcon: widget.staticIcon,
unit: widget.unit,
pattern: widget.pattern,
- staticIcon: widget.staticIcon,
- labelSource: widget.labelSource,
yAxisDecimalPattern: widget.yAxisDecimalPattern
)
}
}
+
+// Required for behavior of Slider
+public extension OpenHABWidget {
+ func shouldUseSliderUpdatesDuringMove() -> Bool {
+ if let releaseOnly {
+ return !releaseOnly
+ }
+
+ guard let item else {
+ return false
+ }
+
+ if item.isOfTypeOrGroupType(.dimmer) ||
+ item.isOfTypeOrGroupType(.number) ||
+ item.isOfTypeOrGroupType(.color) {
+ return true
+ }
+
+ if item.isOfTypeOrGroupType(.numberWithDimension) {
+ // Allow live updates for percent values, but not for e.g. temperatures
+ return stateValueAsNumberState?.unit == "%"
+ }
+
+ return false
+ }
+}
diff --git a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidgetMapping.swift b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidgetMapping.swift
index f8cc78bab..e7531f55a 100644
--- a/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidgetMapping.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Model/OpenHABWidgetMapping.swift
@@ -14,15 +14,29 @@ import Foundation
public struct OpenHABWidgetMapping: Decodable, Sendable {
public var command = ""
public var label = ""
+ public var row: Int?
+ public var column: Int?
+ public var icon: String?
+ public var releaseCommand: String?
- public init(command: String?, label: String?) {
+ /// Returns true if this mapping has press-and-release behavior
+ public var hasPressReleaseBehavior: Bool {
+ guard let releaseCommand else { return false }
+ return !releaseCommand.isEmpty
+ }
+
+ public init(command: String?, label: String?, row: Int? = nil, column: Int? = nil, icon: String? = nil, releaseCommand: String? = nil) {
self.command = command.orEmpty
self.label = label.orEmpty
+ self.row = row
+ self.column = column
+ self.icon = icon
+ self.releaseCommand = releaseCommand
}
}
extension OpenHABWidgetMapping {
init(_ mapping: Components.Schemas.MappingDTO) {
- self.init(command: mapping.command, label: mapping.label)
+ self.init(command: mapping.command, label: mapping.label, row: mapping.row.map { Int($0) }, column: mapping.column.map { Int($0) }, icon: mapping.icon, releaseCommand: mapping.releaseCommand)
}
}
diff --git a/OpenHABCore/Sources/OpenHABCore/Model/WidgetDisplayState.swift b/OpenHABCore/Sources/OpenHABCore/Model/WidgetDisplayState.swift
new file mode 100644
index 000000000..8ca0baa89
--- /dev/null
+++ b/OpenHABCore/Sources/OpenHABCore/Model/WidgetDisplayState.swift
@@ -0,0 +1,62 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import Foundation
+
+/// Immutable snapshot of widget values required for row rendering.
+public struct WidgetDisplayState: Sendable {
+ public let widgetId: String
+ public let labelText: String
+ public let labelValue: String?
+ public let effectiveState: String
+ public let isOn: Bool
+ public let adjustedValue: Double
+ public let minValue: Double
+ public let maxValue: Double
+ public let step: Double
+ public let switchSupport: Bool
+ public let hasLinkedPage: Bool
+ public let readOnly: Bool
+ public let mappings: [OpenHABWidgetMapping]
+ public let hasPressReleaseMappings: Bool
+ public let selectedIndex: Int?
+ public let selectedLabel: String?
+}
+
+public extension OpenHABWidget {
+ var displayState: WidgetDisplayState {
+ let mappings = mappingsOrItemOptions
+ let effectiveState = state.isEmpty ? (item?.state ?? "") : state
+ let selectedIndex = mappingIndex(byCommand: item?.state).map { Int($0) }
+ let selectedLabel = selectedIndex.flatMap { index in
+ mappings.indices.contains(index) ? mappings[index].label : nil
+ }
+
+ return WidgetDisplayState(
+ widgetId: widgetId,
+ labelText: labelText ?? label,
+ labelValue: labelValue,
+ effectiveState: effectiveState,
+ isOn: effectiveState.parseAsBool(),
+ adjustedValue: adjustedValue,
+ minValue: minValue,
+ maxValue: maxValue,
+ step: step,
+ switchSupport: switchSupport,
+ hasLinkedPage: linkedPage != nil,
+ readOnly: readOnly ?? false,
+ mappings: mappings,
+ hasPressReleaseMappings: hasPressReleaseMappings,
+ selectedIndex: selectedIndex,
+ selectedLabel: selectedLabel
+ )
+ }
+}
diff --git a/OpenHABCore/Sources/OpenHABCore/Model/WidgetRenderingKind.swift b/OpenHABCore/Sources/OpenHABCore/Model/WidgetRenderingKind.swift
new file mode 100644
index 000000000..4228b7811
--- /dev/null
+++ b/OpenHABCore/Sources/OpenHABCore/Model/WidgetRenderingKind.swift
@@ -0,0 +1,88 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import Foundation
+
+public enum WidgetRenderingKind: Sendable {
+ case segmentedSwitch
+ case toggleSwitch
+ case rollershutterSwitch
+ case slider
+ case dateInput
+ case textInput
+ case text
+ case frame
+ case setpoint
+ case selection
+ case colorPicker
+ case image
+ case chart
+ case video
+ case webview
+ case mapview
+ case colorTemperaturePicker
+ case buttonGrid
+ case generic
+}
+
+public extension OpenHABWidget {
+ var renderingKind: WidgetRenderingKind {
+ switch type {
+ case .switchWidget:
+ if !mappings.isEmpty {
+ return .segmentedSwitch
+ }
+ if item?.isOfTypeOrGroupType(.switchItem) ?? false {
+ return .toggleSwitch
+ }
+ if item?.isOfTypeOrGroupType(.rollershutter) ?? false {
+ return .rollershutterSwitch
+ }
+ if !mappingsOrItemOptions.isEmpty {
+ return .segmentedSwitch
+ }
+ return .toggleSwitch
+ case .slider:
+ return .slider
+ case .input:
+ if [.date, .time, .dateTime].contains(inputHint) {
+ return .dateInput
+ }
+ return .textInput
+ case .text:
+ return .text
+ case .frame:
+ return .frame
+ case .setpoint:
+ return .setpoint
+ case .selection:
+ return .selection
+ case .colorpicker:
+ return .colorPicker
+ case .image:
+ return .image
+ case .chart:
+ return .chart
+ case .video:
+ return .video
+ case .webview:
+ return .webview
+ case .mapview:
+ return .mapview
+ case .colortemperaturepicker:
+ return .colorTemperaturePicker
+ case .buttongrid:
+ return .buttonGrid
+ case .group, .defaultWidget, .button, .unknown:
+ return .generic
+ }
+ }
+}
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/DoubleExtension.swift b/OpenHABCore/Sources/OpenHABCore/Util/DoubleExtension.swift
index 8d091bfe5..52598e432 100644
--- a/OpenHABCore/Sources/OpenHABCore/Util/DoubleExtension.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/DoubleExtension.swift
@@ -12,6 +12,10 @@
import Foundation
public extension Double {
+ var asColorTemperatureInKelvin: Double {
+ self < 1000 ? 1_000_000 / self : self
+ }
+
func valueText(step: Double) -> String {
let digits = max(-Decimal(step).exponent, 0)
let numberFormatter = NumberFormatter()
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/Endpoint.swift b/OpenHABCore/Sources/OpenHABCore/Util/Endpoint.swift
index 725293039..a4f454eaf 100644
--- a/OpenHABCore/Sources/OpenHABCore/Util/Endpoint.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/Endpoint.swift
@@ -187,8 +187,12 @@ public extension Endpoint {
if source == "if" || source == "iconify" {
queryItems = [URLQueryItem(name: "height", value: "64")]
- if !iconColor.isEmpty, let colorString = UIColor(fromString: iconColor).toHex() {
- queryItems.append(URLQueryItem(name: "color", value: "#\(colorString)"))
+ if !iconColor.isEmpty {
+ let uiColor = UIColor(fromString: iconColor)
+ let colorString = uiColor.hexString
+ if let colorString {
+ queryItems.append(URLQueryItem(name: "color", value: "#\(colorString)"))
+ }
}
if let widgetId {
queryItems.append(URLQueryItem(name: "widgetId", value: widgetId))
@@ -206,8 +210,8 @@ public extension Endpoint {
iconName = "none"
}
- if staticIcon != true {
- queryItems.append(URLQueryItem(name: "state", value: state ?? "null"))
+ if staticIcon != true, let state {
+ queryItems.append(URLQueryItem(name: "state", value: state))
}
queryItems.append(contentsOf: [
diff --git a/openHAB/UILabel+Localization.swift b/OpenHABCore/Sources/OpenHABCore/Util/ImageType.swift
similarity index 67%
rename from openHAB/UILabel+Localization.swift
rename to OpenHABCore/Sources/OpenHABCore/Util/ImageType.swift
index b5dd1fc5f..ba2582e35 100644
--- a/openHAB/UILabel+Localization.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/ImageType.swift
@@ -12,13 +12,14 @@
import Foundation
import UIKit
-extension UILabel {
- @IBInspectable var localizationKey: String {
- get {
- ""
- }
- set {
- text = NSLocalizedString(newValue, comment: "")
- }
- }
+public enum ImagePayload {
+ case link(url: URL?)
+ case embedded(data: Data)
+ case empty
+}
+
+public enum ImageType {
+ case link(url: URL?)
+ case embedded(image: UIImage?)
+ case empty
}
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/NWPathMonitoring.swift b/OpenHABCore/Sources/OpenHABCore/Util/NWPathMonitoring.swift
index fc802cb89..1e4caeb12 100644
--- a/OpenHABCore/Sources/OpenHABCore/Util/NWPathMonitoring.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/NWPathMonitoring.swift
@@ -20,20 +20,14 @@ final class RealPathMonitor: NWPathMonitoring, Sendable {
init() {
monitor = NWPathMonitor()
}
-
+
func startMonitoring(handler: @escaping (Bool) async -> Void) async {
- if #available(iOS 17, watchOS 10, *) {
- for await path in monitor {
- Logger.nwPathMonitoring.debug("Path monitor update: \(path.debugDescription)")
- await handler(path.status == .satisfied || path.status == .requiresConnection)
- }
- } else {
- for await path in monitor.paths() {
- await handler(path.status == .satisfied || path.status == .requiresConnection)
- }
+ for await path in monitor {
+ Logger.nwPathMonitoring.debug("Path monitor update: \(path.debugDescription)")
+ await handler(path.status == .satisfied || path.status == .requiresConnection)
}
}
-
+
func cancel() {
monitor.cancel()
}
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift b/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift
index 0eff3686a..5d4e170f0 100644
--- a/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/NetworkTracker.swift
@@ -523,16 +523,16 @@ public extension NetworkTracker {
return service
}
- func send(to item: OpenHABItem, command: String) async throws {
- try await send(to: item.name, command: command)
+ func send(to item: OpenHABItem, command: String, sourcePrefix: String? = nil, deviceId: String? = nil) async throws {
+ try await send(to: item.name, command: command, sourcePrefix: sourcePrefix, deviceId: deviceId)
}
- func send(to item: String, command: String) async throws {
- try await service().sendItemCommand(itemname: item, command: command, sourcePrefix: nil, deviceId: nil)
+ func send(to item: String, command: String, sourcePrefix: String? = nil, deviceId: String? = nil) async throws {
+ try await service().sendItemCommand(itemname: item, command: command, sourcePrefix: sourcePrefix, deviceId: deviceId)
}
- func updateState(item: OpenHABItem, state: String) async throws {
- try await service().updateItemState(itemname: item.name, with: state, sourcePrefix: nil, deviceId: nil)
+ func updateState(item: OpenHABItem, state: String, sourcePrefix: String? = nil, deviceId: String? = nil) async throws {
+ try await service().updateItemState(itemname: item.name, with: state, sourcePrefix: sourcePrefix, deviceId: deviceId)
}
func getStaticItems() async throws -> [OpenHABItem] {
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/OpenAPIService.swift b/OpenHABCore/Sources/OpenHABCore/Util/OpenAPIService.swift
index 99a8331c0..924b2d0b9 100644
--- a/OpenHABCore/Sources/OpenHABCore/Util/OpenAPIService.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/OpenAPIService.swift
@@ -95,18 +95,9 @@ public actor OpenAPIService {
}
private static func getServerURL(for url: URL) -> URL {
- if let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false),
- let host = urlComponents.host,
- host.contains("myopenhab.org"),
- host != "home.myopenhab.org" {
-// URL(string: "https://home.myopenhab.org")!
- var newComponents = urlComponents
- newComponents.host = "home.myopenhab.org"
-// newComponents.scheme = "https"
- return newComponents.url!
- } else {
- return url
- }
+ // Respect the configured connection URL. Forcing cloud hosts can fail
+ // DNS resolution on some networks/simulators and breaks sitemap loading.
+ url
}
private func prepareURLSessionConfiguration(longPolling: Bool) -> URLSessionConfiguration {
@@ -117,7 +108,11 @@ public actor OpenAPIService {
}
private func sourceComponent(deviceId: String?) -> String? {
+ #if os(watchOS)
+ let base = "org.openhab.watchos"
+ #else
let base = "org.openhab.ios"
+ #endif
guard let deviceId else { return base }
let trimmed = deviceId.trimmingCharacters(in: .whitespacesAndNewlines)
// Actor must not include the delegation separator per openHAB source spec.
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/OpenHABImageProcessor.swift b/OpenHABCore/Sources/OpenHABCore/Util/OpenHABImageProcessor.swift
index 25ec0f5f4..de223f357 100644
--- a/OpenHABCore/Sources/OpenHABCore/Util/OpenHABImageProcessor.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/OpenHABImageProcessor.swift
@@ -14,6 +14,9 @@ import Kingfisher
import os.log
import SDWebImageSVGCoder
import SFSafeSymbols
+#if canImport(AppKit)
+import WebKit
+#endif
/// An image processor for openHAB icons and images with SVG color preprocessing support.
///
@@ -224,6 +227,12 @@ public struct OpenHABImageProcessor: ImageProcessor {
guard !data.isEmpty else { return nil }
if isSVG(data: data) {
+ #if os(macOS)
+ if let image = renderSVGWithWebKit(data) {
+ return image
+ }
+ #endif
+
// Apply color preprocessing to SVG if iconColor is specified
let processedData = preprocessSVG(data)
@@ -239,6 +248,26 @@ public struct OpenHABImageProcessor: ImageProcessor {
}
}
+ #if os(macOS)
+ private func renderSVGWithWebKit(_ data: Data) -> NSImage? {
+ guard let svgString = String(data: data, encoding: .utf8) else { return nil }
+ let webView = WKWebView(frame: CGRect(origin: .zero, size: CGSize(width: 256, height: 256)))
+ webView.loadHTMLString("
\(svgString)", baseURL: nil)
+
+ let config = WKSnapshotConfiguration()
+ config.rect = CGRect(origin: .zero, size: webView.bounds.size)
+
+ var snapshotImage: NSImage?
+ let sema = DispatchSemaphore(value: 0)
+ webView.takeSnapshot(with: config) { image, _ in
+ snapshotImage = image
+ sema.signal()
+ }
+ _ = sema.wait(timeout: .now() + 2)
+ return snapshotImage
+ }
+ #endif
+
private func isSVG(data: Data?) -> Bool {
guard let data else { return false }
if let start = String(data: data.prefix(200), encoding: .utf8) {
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/Preferences.swift b/OpenHABCore/Sources/OpenHABCore/Util/Preferences.swift
index 5e5ecc9e0..652502cf7 100644
--- a/OpenHABCore/Sources/OpenHABCore/Util/Preferences.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/Preferences.swift
@@ -9,33 +9,37 @@
//
// SPDX-License-Identifier: EPL-2.0
-@preconcurrency import Combine
+import AsyncAlgorithms
import os.log
-import UIKit
+import SwiftUI
-@MainActor
-private let sharedDefaults = UserDefaults(suiteName: "group.org.openhab.app")!
+// Thread-safe access to UserDefaults - still needs main actor due to UserDefaults not being Sendable
+private nonisolated(unsafe) let sharedDefaults = UserDefaults(suiteName: "group.org.openhab.app")!
-@MainActor
@propertyWrapper
public struct UserDefault {
private let key: String
private let defaultValue: T
private let isHomeProperty: Bool
private let subject: CurrentValueSubject
+ private let channel: AsyncChannel
public var wrappedValue: T {
get {
PreferencesAccess.getPreference(key: key, defaultValue: defaultValue, encoder: { $0 }, decoder: { $0 as? T })
}
set {
- PreferencesAccess.preferenceChanged(newValue: newValue, key: key, isHomeProperty: isHomeProperty, subject: subject) { $0 }
+ PreferencesAccess.preferenceChanged(newValue: newValue, key: key, isHomeProperty: isHomeProperty, subject: subject, channel: channel) { $0 }
}
}
public var projectedValue: AnyPublisher {
subject.eraseToAnyPublisher()
}
+
+ public var asyncValues: AsyncChannel {
+ channel
+ }
public init(_ key: String, defaultValue: T, isHomeProperty: Bool = false) {
self.key = key
@@ -43,16 +47,17 @@ public struct UserDefault {
self.isHomeProperty = isHomeProperty
let currentValue = PreferencesAccess.getPreference(key: key, defaultValue: defaultValue, encoder: { $0 }, decoder: { $0 as? T })
subject = CurrentValueSubject(currentValue)
+ channel = AsyncChannel()
}
}
-@MainActor
@propertyWrapper
public struct UserDefaultObject {
private let key: String
private let defaultValue: T
private let isHomeProperty: Bool
private let subject: CurrentValueSubject
+ private let channel: AsyncChannel
private let objectDecoder: (Any) -> (T?) = {
guard let data = $0 as? Data else {
@@ -68,13 +73,17 @@ public struct UserDefaultObject {
PreferencesAccess.getPreference(key: key, defaultValue: defaultValue, encoder: objectEncoder, decoder: objectDecoder)
}
set {
- PreferencesAccess.preferenceChanged(newValue: newValue, key: key, isHomeProperty: isHomeProperty, subject: subject, converter: objectEncoder)
+ PreferencesAccess.preferenceChanged(newValue: newValue, key: key, isHomeProperty: isHomeProperty, subject: subject, channel: channel, converter: objectEncoder)
}
}
public var projectedValue: AnyPublisher {
subject.eraseToAnyPublisher()
}
+
+ public var asyncValues: AsyncChannel {
+ channel
+ }
init(_ key: String, defaultValue: T, isHomeProperty: Bool = false) {
self.key = key
@@ -84,11 +93,11 @@ public struct UserDefaultObject {
// Combine publication
let currentValue = PreferencesAccess.getPreference(key: key, defaultValue: defaultValue, encoder: objectEncoder, decoder: objectDecoder)
subject = CurrentValueSubject(currentValue)
+ channel = AsyncChannel()
}
}
-@MainActor
-public struct HomePreferences: Codable, Equatable {
+public struct HomePreferences: Codable, Equatable, Sendable {
public let id: UUID
public var defaultView = "web"
public var demomode = true
@@ -104,15 +113,54 @@ public struct HomePreferences: Codable, Equatable {
public var sitemapForWatchLabel = "watch"
public var homeName = "Home"
public var sseCommandItem = ""
+ public var lastSelectedTab = "main"
+ public var tabConfiguration = TabEntry.defaultConfiguration
fileprivate init(id: UUID) {
self.id = id
}
}
-@MainActor
-public struct ApplicationPreferences: Codable, Equatable {
+public struct TabEntry: Codable, Equatable, Hashable, Sendable {
+ public var id: String
+ public var enabled: Bool
+
+ public init(id: String, enabled: Bool) {
+ self.id = id
+ self.enabled = enabled
+ }
+
+ public static let defaultConfiguration: [TabEntry] = [
+ TabEntry(id: "main", enabled: true),
+ TabEntry(id: "sitemaps", enabled: true),
+ TabEntry(id: "tiles", enabled: true),
+ TabEntry(id: "system", enabled: true)
+ ]
+}
+
+public struct ApplicationPreferences: Codable, Equatable, Sendable {
public var showSearchField = true
+ public var sendCrashReports = false
+ public var idleOff = false
+ public var hideStatusBar = false
+}
+
+public struct ScreenSaverPreferences: Codable, Equatable, Sendable {
+ public var isEnabled = false
+ public var showsTime = true
+ public var showsDate = true
+ public var idleInterval = 120.0
+ public var movementInterval = 8.0
+ public var fontName = ""
+ public var timeFontRatio = 0.2
+ public var dateFontRatio = 0.4
+ public var enableDimming = true
+ public var dimLevel = 0.3
+ public var showsSeconds = false
+ public var use24Hour = false
+ public var fadeDuration = 2.0
+ public var restoreBrightness = true
+ public var wakeBrightness = 1.0
}
// MARK: Retrieving preference from user defaults, reacting to preference change
@@ -128,7 +176,7 @@ public struct ApplicationPreferences: Codable, Equatable {
// MARK: !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
private enum PreferencesAccess {
- @MainActor fileprivate static func getPreference(key: String, defaultValue: T, encoder: (T) -> (some Sendable)?, decoder: (Any?) -> T?) -> T {
+ fileprivate static func getPreference(key: String, defaultValue: T, encoder: (T) -> (some Sendable)?, decoder: (Any?) -> T?) -> T {
let preferenceValue = sharedDefaults.object(forKey: key)
if let preferenceConverted = decoder(preferenceValue) {
return preferenceConverted
@@ -144,7 +192,7 @@ private enum PreferencesAccess {
}
}
- @MainActor fileprivate static func preferenceChanged(newValue: T, key: String, isHomeProperty: Bool, subject: CurrentValueSubject, sanitize: (T) -> (T?) = { $0 }, converter: (T) -> (some Sendable)?) {
+ fileprivate static func preferenceChanged(newValue: T, key: String, isHomeProperty: Bool, subject: CurrentValueSubject, channel: AsyncChannel, sanitize: (T) -> (T?) = { $0 }, converter: (T) -> (some Sendable)?) {
guard let sanitized = sanitize(newValue) else {
Logger.preferences.debug("Preference \(key) new value \(String(describing: newValue), privacy: .private) could not be sanitized, will be ignored")
return
@@ -158,6 +206,11 @@ private enum PreferencesAccess {
sharedDefaults.set(convertedValue, forKey: key)
subject.send(sanitized)
+
+ // Send to AsyncChannel
+ Task {
+ await channel.send(sanitized)
+ }
}
}
@@ -170,12 +223,6 @@ public actor Preferences {
@UserDefaultObject("currentHomePreferences", defaultValue: HomePreferences(id: defaultHomeId))
public private(set) var currentHomePreferences: HomePreferences
- @UserDefault("sendCrashReports", defaultValue: false)
- public var sendCrashReports: Bool
-
- @UserDefault("idleOff", defaultValue: false)
- public var idleOff: Bool
-
@UserDefaultObject(
"applicationPreferences",
defaultValue:
@@ -183,53 +230,12 @@ public actor Preferences {
)
public private(set) var applicationPreferences: ApplicationPreferences
- @UserDefault("screensaverEnabled", defaultValue: false)
- public var screensaverEnabled: Bool
-
- @UserDefault("screensaverShowsTime", defaultValue: true)
- public var screensaverShowsTime: Bool
-
- @UserDefault("screensaverShowsDate", defaultValue: true)
- public var screensaverShowsDate: Bool
-
- @UserDefault("screensaverIdleInterval", defaultValue: 120.0)
- public var screensaverIdleInterval: Double
-
- @UserDefault("screensaverMovementInterval", defaultValue: 8.0)
- public var screensaverMovementInterval: Double
-
- @UserDefault("screensaverFontName", defaultValue: "")
- public var screensaverFontName: String
-
- @UserDefault("screensaverTimeFontRatio", defaultValue: 0.2)
- public var screensaverTimeFontRatio: Double
-
- @UserDefault("screensaverDateFontRatio", defaultValue: 0.4)
- public var screensaverDateFontRatio: Double
-
- @UserDefault("screensaverEnableDimming", defaultValue: true)
- public var screensaverEnableDimming: Bool
-
- @UserDefault("screensaverDimLevel", defaultValue: 0.3)
- public var screensaverDimLevel: Double
-
- @UserDefault("screensaverShowsSeconds", defaultValue: false)
- public var screensaverShowsSeconds: Bool
-
- @UserDefault("screensaverUse24Hour", defaultValue: false)
- public var screensaverUse24Hour: Bool
-
- @UserDefault("screensaverFadeDuration", defaultValue: 2.0)
- public var screensaverFadeDuration: Double
-
- @UserDefault("screensaverRestoreBrightness", defaultValue: true)
- public var screensaverRestoreBrightness: Bool
-
- @UserDefault("screensaverWakeBrightness", defaultValue: 1.0)
- public var screensaverWakeBrightness: Double
-
- @UserDefault("hideStatusBar", defaultValue: false)
- public var hideStatusBar: Bool
+ @UserDefaultObject(
+ "screensaverPreferences",
+ defaultValue:
+ ScreenSaverPreferences()
+ )
+ public private(set) var screensaverPreferences: ScreenSaverPreferences
@UserDefault("currentWebViewPath", defaultValue: "")
public var currentWebViewPath: String
@@ -248,20 +254,62 @@ public actor Preferences {
@UserDefault("didMigrateToMultipleHomes", defaultValue: false)
private var didMigrateToMultipleHomes: Bool
- @MainActor
+ @UserDefault("didMigrateScreenSaverPreferences", defaultValue: false)
+ private var didMigrateScreenSaverPreferences: Bool
+
+ @UserDefault("didMigrateApplicationPreferences", defaultValue: false)
+ private var didMigrateApplicationPreferences: Bool
+
private var internalPreferenceChangeOngoing = false
- @MainActor
private func internalPreferenceChange(_ change: () -> Void) {
internalPreferenceChangeOngoing = true
change()
internalPreferenceChangeOngoing = false
}
+
+ // MARK: - AsyncChannel Access
+
+ /// Access the AsyncChannel for currentHomePreferences
+ public var currentHomePreferencesChannel: AsyncChannel {
+ _currentHomePreferences.asyncValues
+ }
+
+ /// Access the AsyncChannel for applicationPreferences
+ public var applicationPreferencesChannel: AsyncChannel {
+ _applicationPreferences.asyncValues
+ }
+
+ /// Access the AsyncChannel for screensaverPreferences
+ public var screensaverPreferencesChannel: AsyncChannel {
+ _screensaverPreferences.asyncValues
+ }
+
+ /// Access the AsyncChannel for storedHomes
+ public var storedHomesChannel: AsyncChannel<[UUID: HomePreferences]> {
+ _storedHomes.asyncValues
+ }
+
+ // Setter methods for actor-isolated properties (used in migration)
+ func setDidMigrateToSharedDefaults(_ value: Bool) {
+ didMigrateToSharedDefaults = value
+ }
+
+ func setDidMigrateToMultipleHomes(_ value: Bool) {
+ didMigrateToMultipleHomes = value
+ }
+
+ func setDidMigrateScreenSaverPreferences(_ value: Bool) {
+ didMigrateScreenSaverPreferences = value
+ }
+
+ func setDidMigrateApplicationPreferences(_ value: Bool) {
+ didMigrateApplicationPreferences = value
+ }
}
// MARK: Multiple homes
-@MainActor
public extension Preferences {
func listStoredHomes() -> [UUID] {
let preferenceIds = storedHomes
@@ -343,27 +391,36 @@ public extension Preferences {
private func storeActiveHome() {
var all = storedHomes
- let homeId = Preferences.shared.activeHomeId
- all[homeId] = Preferences.shared.currentHomePreferences
+ let homeId = activeHomeId
+ all[homeId] = currentHomePreferences
storedHomes = all
Logger.preferences.debug("Stored preferences for current home \(homeId.uuidString)")
}
- func modifyActiveHome(modificationFunction: @MainActor (inout HomePreferences) -> Void) {
+ func modifyActiveHome(modificationFunction: @Sendable (inout HomePreferences) -> Void) {
var homePreferences = currentHomePreferences
modificationFunction(&homePreferences)
currentHomePreferences = homePreferences
storeActiveHome()
}
- func modifyApplicationPreferences(modificationFunction: @MainActor (inout ApplicationPreferences) -> Void) {
+ func modifyApplicationPreferences(modificationFunction: @Sendable (inout ApplicationPreferences) -> Void) {
var applicationPreferences = applicationPreferences
modificationFunction(&applicationPreferences)
self.applicationPreferences = applicationPreferences
}
+
+ func modifyScreenSaverPreferences(modificationFunction: @Sendable (inout ScreenSaverPreferences) -> Void) {
+ var screensaverPreferences = screensaverPreferences
+ modificationFunction(&screensaverPreferences)
+ self.screensaverPreferences = screensaverPreferences
+ }
+
+ func setCurrentWebViewPath(_ path: String) {
+ currentWebViewPath = path
+ }
}
-@MainActor
public extension Preferences {
func firstStoredHome(where predicate: (HomePreferences) -> Bool) -> (id: UUID, record: HomePreferences)? {
for (uuid, record) in storedHomes {
@@ -382,18 +439,19 @@ public extension Preferences {
// MARK: Migration
-@MainActor
public extension Preferences {
- static func migratePreferences() {
- Preferences.shared.initializeStoredHomes()
- migrateToSharedDefaultsIfRequired()
- migrateToMultipleHomesIfRequired()
+ static func migratePreferences() async {
+ await Preferences.shared.initializeStoredHomes()
+ await migrateToSharedDefaultsIfRequired()
+ await migrateToMultipleHomesIfRequired()
+ await migrateApplicationPreferencesIfRequired()
+ await migrateScreenSaverPreferencesIfRequired()
}
- private static func migrateToSharedDefaultsIfRequired() {
- guard !Preferences.shared.didMigrateToSharedDefaults else { return }
+ private static func migrateToSharedDefaultsIfRequired() async {
+ guard await !Preferences.shared.didMigrateToSharedDefaults else { return }
- Preferences.shared.modifyActiveHome { currentHomePreferences in
+ await Preferences.shared.modifyActiveHome { currentHomePreferences in
currentHomePreferences.localConnectionConfig.url = UserDefaults.standard.string(forKey: "localUrl") ?? currentHomePreferences.localConnectionConfig.url
currentHomePreferences.localConnectionConfig.alwaysSendBasicAuth = UserDefaults.standard.object(forKey: "alwaysSendCreds") as? Bool ?? currentHomePreferences.localConnectionConfig.alwaysSendBasicAuth
currentHomePreferences.localConnectionConfig.ignoreSSL = UserDefaults.standard.object(forKey: "ignoreSSL") as? Bool ?? currentHomePreferences.localConnectionConfig.ignoreSSL
@@ -408,18 +466,15 @@ public extension Preferences {
currentHomePreferences.defaultSitemap = UserDefaults.standard.string(forKey: "defaultSitemap") ?? currentHomePreferences.defaultSitemap
}
- Preferences.shared.idleOff = UserDefaults.standard.object(forKey: "idleOff") as? Bool ?? Preferences.shared.idleOff
- Preferences.shared.sendCrashReports = UserDefaults.standard.object(forKey: "sendCrashReports") as? Bool ?? Preferences.shared.sendCrashReports
-
- Preferences.shared.didMigrateToSharedDefaults = true
+ await Preferences.shared.setDidMigrateToSharedDefaults(true)
// this was done implicitly
- Preferences.shared.didMigrateToMultipleHomes = true
+ await Preferences.shared.setDidMigrateToMultipleHomes(true)
}
- private static func migrateToMultipleHomesIfRequired() {
- guard !Preferences.shared.didMigrateToMultipleHomes else { return }
+ private static func migrateToMultipleHomesIfRequired() async {
+ guard await !Preferences.shared.didMigrateToMultipleHomes else { return }
- migrateToSharedDefaultsIfRequired()
+ await migrateToSharedDefaultsIfRequired()
let oldLocalUrl = sharedDefaults.string(forKey: "localUrl")
let oldRemoteUrl = sharedDefaults.string(forKey: "remoteUrl")
@@ -428,21 +483,21 @@ public extension Preferences {
let oldAlwaysSendCreds = sharedDefaults.object(forKey: "alwaysSendCreds") as? Bool
let oldIgnoreSSL = sharedDefaults.object(forKey: "ignoreSSL") as? Bool
- // Create new configuration
- var newLocalConfiguration = Preferences.shared.currentHomePreferences.localConnectionConfig
- newLocalConfiguration.url = oldLocalUrl ?? newLocalConfiguration.url
- newLocalConfiguration.alwaysSendBasicAuth = oldAlwaysSendCreds ?? newLocalConfiguration.alwaysSendBasicAuth
- newLocalConfiguration.ignoreSSL = oldIgnoreSSL ?? newLocalConfiguration.ignoreSSL
-
- var newRemoteConfiguration = Preferences.shared.currentHomePreferences.remoteConnectionConfig
- newRemoteConfiguration.url = oldRemoteUrl ?? newRemoteConfiguration.url
- newRemoteConfiguration.username = oldUsername ?? newRemoteConfiguration.username
- newRemoteConfiguration.password = oldPassword ?? newRemoteConfiguration.password
- newRemoteConfiguration.alwaysSendBasicAuth = oldAlwaysSendCreds ?? newRemoteConfiguration.alwaysSendBasicAuth
- newRemoteConfiguration.ignoreSSL = oldIgnoreSSL ?? newRemoteConfiguration.ignoreSSL
-
// Save to Preferences
- Preferences.shared.modifyActiveHome { currentHomePreferences in
+ await Preferences.shared.modifyActiveHome { currentHomePreferences in
+ // Create new configuration inside the closure to avoid capture issues
+ var newLocalConfiguration = currentHomePreferences.localConnectionConfig
+ newLocalConfiguration.url = oldLocalUrl ?? newLocalConfiguration.url
+ newLocalConfiguration.alwaysSendBasicAuth = oldAlwaysSendCreds ?? newLocalConfiguration.alwaysSendBasicAuth
+ newLocalConfiguration.ignoreSSL = oldIgnoreSSL ?? newLocalConfiguration.ignoreSSL
+
+ var newRemoteConfiguration = currentHomePreferences.remoteConnectionConfig
+ newRemoteConfiguration.url = oldRemoteUrl ?? newRemoteConfiguration.url
+ newRemoteConfiguration.username = oldUsername ?? newRemoteConfiguration.username
+ newRemoteConfiguration.password = oldPassword ?? newRemoteConfiguration.password
+ newRemoteConfiguration.alwaysSendBasicAuth = oldAlwaysSendCreds ?? newRemoteConfiguration.alwaysSendBasicAuth
+ newRemoteConfiguration.ignoreSSL = oldIgnoreSSL ?? newRemoteConfiguration.ignoreSSL
+
currentHomePreferences.defaultView = sharedDefaults.string(forKey: "defaultView") ?? currentHomePreferences.defaultView
currentHomePreferences.demomode = sharedDefaults.object(forKey: "demomode") as? Bool ?? currentHomePreferences.demomode
currentHomePreferences.realTimeSliders = sharedDefaults.object(forKey: "realTimeSliders") as? Bool ?? currentHomePreferences.realTimeSliders
@@ -457,16 +512,105 @@ public extension Preferences {
currentHomePreferences.sitemapForWatchLabel = sharedDefaults.string(forKey: "sitemapForWatchLabel") ?? currentHomePreferences.sitemapForWatchLabel
}
- Preferences.shared.didMigrateToMultipleHomes = true
+ await Preferences.shared.setDidMigrateToMultipleHomes(true)
+ }
+
+ private static func migrateApplicationPreferencesIfRequired() async {
+ guard await !Preferences.shared.didMigrateApplicationPreferences else { return }
+
+ // Check if old preferences exist in UserDefaults
+ let oldSendCrashReports = sharedDefaults.object(forKey: "sendCrashReports") as? Bool
+ let oldIdleOff = sharedDefaults.object(forKey: "idleOff") as? Bool
+ let oldHideStatusBar = sharedDefaults.object(forKey: "hideStatusBar") as? Bool
+
+ // Only migrate if at least one old preference exists
+ if oldSendCrashReports != nil || oldIdleOff != nil || oldHideStatusBar != nil {
+ await Preferences.shared.modifyApplicationPreferences { prefs in
+ if let oldSendCrashReports { prefs.sendCrashReports = oldSendCrashReports }
+ if let oldIdleOff { prefs.idleOff = oldIdleOff }
+ if let oldHideStatusBar { prefs.hideStatusBar = oldHideStatusBar }
+ }
+
+ Logger.preferences.info("Migrated application preferences from individual keys to ApplicationPreferences struct")
+
+ // Clean up old keys
+ sharedDefaults.removeObject(forKey: "sendCrashReports")
+ sharedDefaults.removeObject(forKey: "idleOff")
+ sharedDefaults.removeObject(forKey: "hideStatusBar")
+ }
+
+ await Preferences.shared.setDidMigrateApplicationPreferences(true)
+ }
+
+ private static func migrateScreenSaverPreferencesIfRequired() async {
+ guard await !Preferences.shared.didMigrateScreenSaverPreferences else { return }
+
+ // Check if old preferences exist in UserDefaults
+ let oldEnabled = sharedDefaults.object(forKey: "screensaverEnabled") as? Bool
+ let oldShowsTime = sharedDefaults.object(forKey: "screensaverShowsTime") as? Bool
+ let oldShowsDate = sharedDefaults.object(forKey: "screensaverShowsDate") as? Bool
+ let oldIdleInterval = sharedDefaults.object(forKey: "screensaverIdleInterval") as? Double
+ let oldMovementInterval = sharedDefaults.object(forKey: "screensaverMovementInterval") as? Double
+ let oldFontName = sharedDefaults.string(forKey: "screensaverFontName")
+ let oldTimeFontRatio = sharedDefaults.object(forKey: "screensaverTimeFontRatio") as? Double
+ let oldDateFontRatio = sharedDefaults.object(forKey: "screensaverDateFontRatio") as? Double
+ let oldEnableDimming = sharedDefaults.object(forKey: "screensaverEnableDimming") as? Bool
+ let oldDimLevel = sharedDefaults.object(forKey: "screensaverDimLevel") as? Double
+ let oldShowsSeconds = sharedDefaults.object(forKey: "screensaverShowsSeconds") as? Bool
+ let oldUse24Hour = sharedDefaults.object(forKey: "screensaverUse24Hour") as? Bool
+ let oldFadeDuration = sharedDefaults.object(forKey: "screensaverFadeDuration") as? Double
+ let oldRestoreBrightness = sharedDefaults.object(forKey: "screensaverRestoreBrightness") as? Bool
+ let oldWakeBrightness = sharedDefaults.object(forKey: "screensaverWakeBrightness") as? Double
+
+ // Only migrate if at least one old preference exists
+ if oldEnabled != nil || oldShowsTime != nil || oldIdleInterval != nil {
+ await Preferences.shared.modifyScreenSaverPreferences { prefs in
+ if let oldEnabled { prefs.isEnabled = oldEnabled }
+ if let oldShowsTime { prefs.showsTime = oldShowsTime }
+ if let oldShowsDate { prefs.showsDate = oldShowsDate }
+ if let oldIdleInterval { prefs.idleInterval = oldIdleInterval }
+ if let oldMovementInterval { prefs.movementInterval = oldMovementInterval }
+ if let oldFontName { prefs.fontName = oldFontName }
+ if let oldTimeFontRatio { prefs.timeFontRatio = oldTimeFontRatio }
+ if let oldDateFontRatio { prefs.dateFontRatio = oldDateFontRatio }
+ if let oldEnableDimming { prefs.enableDimming = oldEnableDimming }
+ if let oldDimLevel { prefs.dimLevel = oldDimLevel }
+ if let oldShowsSeconds { prefs.showsSeconds = oldShowsSeconds }
+ if let oldUse24Hour { prefs.use24Hour = oldUse24Hour }
+ if let oldFadeDuration { prefs.fadeDuration = oldFadeDuration }
+ if let oldRestoreBrightness { prefs.restoreBrightness = oldRestoreBrightness }
+ if let oldWakeBrightness { prefs.wakeBrightness = oldWakeBrightness }
+ }
+
+ Logger.preferences.info("Migrated screen saver preferences from individual keys to ScreenSaverPreferences struct")
+
+ // Clean up old keys (optional, but keeps UserDefaults tidy)
+ sharedDefaults.removeObject(forKey: "screensaverEnabled")
+ sharedDefaults.removeObject(forKey: "screensaverShowsTime")
+ sharedDefaults.removeObject(forKey: "screensaverShowsDate")
+ sharedDefaults.removeObject(forKey: "screensaverIdleInterval")
+ sharedDefaults.removeObject(forKey: "screensaverMovementInterval")
+ sharedDefaults.removeObject(forKey: "screensaverFontName")
+ sharedDefaults.removeObject(forKey: "screensaverTimeFontRatio")
+ sharedDefaults.removeObject(forKey: "screensaverDateFontRatio")
+ sharedDefaults.removeObject(forKey: "screensaverEnableDimming")
+ sharedDefaults.removeObject(forKey: "screensaverDimLevel")
+ sharedDefaults.removeObject(forKey: "screensaverShowsSeconds")
+ sharedDefaults.removeObject(forKey: "screensaverUse24Hour")
+ sharedDefaults.removeObject(forKey: "screensaverFadeDuration")
+ sharedDefaults.removeObject(forKey: "screensaverRestoreBrightness")
+ sharedDefaults.removeObject(forKey: "screensaverWakeBrightness")
+ }
+
+ await Preferences.shared.setDidMigrateScreenSaverPreferences(true)
}
}
// MARK: All connections
-@MainActor
public extension Preferences {
func getNotificationConnection() -> ConnectionConfiguration? {
- getNotificationConnection(of: [Preferences.shared.currentHomePreferences.remoteConnectionConfig])
+ getNotificationConnection(of: [currentHomePreferences.remoteConnectionConfig])
}
func getNotificationConnection(of homeConfig: HomePreferences) -> ConnectionConfiguration? {
@@ -506,3 +650,79 @@ public extension ConnectionConfiguration {
priority: 1
)
}
+
+// MARK: - SwiftUI Observable Wrapper
+
+/// A @MainActor observable wrapper for Preferences that can be used in SwiftUI views
+@MainActor
+@Observable
+public final class PreferencesObserver {
+ public static let shared = PreferencesObserver()
+
+ public private(set) var currentHomePreferences: HomePreferences
+ public private(set) var applicationPreferences: ApplicationPreferences
+ public private(set) var screensaverPreferences: ScreenSaverPreferences
+
+ private var currentHomeTask: Task?
+ private var applicationTask: Task?
+ private var screensaverTask: Task?
+
+ private init() {
+ // Initialize with default values - will be updated immediately by channels
+ self.currentHomePreferences = HomePreferences(id: UUID())
+ self.applicationPreferences = ApplicationPreferences()
+ self.screensaverPreferences = ScreenSaverPreferences()
+
+ // Bootstrap initial values and start listeners
+ Task { [weak self] in
+ guard let self else { return }
+ // Fetch initial values from the actor
+ let initialHome = await Preferences.shared.currentHomePreferences
+ let initialApp = await Preferences.shared.applicationPreferences
+ let initialScreen = await Preferences.shared.screensaverPreferences
+
+ // Apply initial values on the main actor
+ self.currentHomePreferences = initialHome
+ self.applicationPreferences = initialApp
+ self.screensaverPreferences = initialScreen
+
+ // Start listening to async channels
+ self.currentHomeTask = Task { [weak self] in
+ guard let self else { return }
+ let channel = await Preferences.shared.currentHomePreferencesChannel
+ for await value in channel {
+ await MainActor.run { self.currentHomePreferences = value }
+ }
+ }
+
+ self.applicationTask = Task { [weak self] in
+ guard let self else { return }
+ let channel = await Preferences.shared.applicationPreferencesChannel
+ for await value in channel {
+ await MainActor.run { self.applicationPreferences = value }
+ }
+ }
+
+ self.screensaverTask = Task { [weak self] in
+ guard let self else { return }
+ let channel = await Preferences.shared.screensaverPreferencesChannel
+ for await value in channel {
+ await MainActor.run { self.screensaverPreferences = value }
+ }
+ }
+ }
+ }
+
+ @MainActor
+ deinit {
+ currentHomeTask?.cancel()
+ applicationTask?.cancel()
+ screensaverTask?.cancel()
+ }
+
+ /// Get notification connection for current home
+ public func getNotificationConnection() async -> ConnectionConfiguration? {
+ await Preferences.shared.getNotificationConnection()
+ }
+}
+
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/StringExtension.swift b/OpenHABCore/Sources/OpenHABCore/Util/StringExtension.swift
index f4024c11f..15f8528e8 100644
--- a/OpenHABCore/Sources/OpenHABCore/Util/StringExtension.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/StringExtension.swift
@@ -184,4 +184,15 @@ public extension String? {
""
}
}
+
+ var isNilOrEmpty: Bool {
+ self == nil || self == ""
+ }
+}
+
+public extension String {
+ var isNoneIcon: Bool {
+ let pattern = #"^(oh:([a-z]+:)?)?none$"#
+ return range(of: pattern, options: .regularExpression) != nil
+ }
}
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/UIColorExtension.swift b/OpenHABCore/Sources/OpenHABCore/Util/UIColorExtension.swift
index 69fe02c93..c6f493588 100644
--- a/OpenHABCore/Sources/OpenHABCore/Util/UIColorExtension.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/UIColorExtension.swift
@@ -162,6 +162,30 @@ public extension UIColor {
}
public extension UIColor {
+ var hexString: String? {
+ var red: CGFloat = 0
+ var green: CGFloat = 0
+ var blue: CGFloat = 0
+ var alpha: CGFloat = 0
+
+ guard getRed(&red, green: &green, blue: &blue, alpha: &alpha) else {
+ return nil
+ }
+
+ let r = UInt8(round(red * 255))
+ let g = UInt8(round(green * 255))
+ let b = UInt8(round(blue * 255))
+ let a = UInt8(round(alpha * 255))
+
+ func toHex(_ value: UInt8) -> String {
+ let hex = String(value, radix: 16, uppercase: true)
+ return hex.count == 1 ? "0" + hex : hex
+ }
+
+ let components = [r, g, b] + (a == 255 ? [] : [a])
+ return components.map(toHex).joined()
+ }
+
/// Initializes a UIColor from a string, supporting both named colors and hexadecimal color codes.
///
/// This convenience initializer attempts to parse the input string as either a named color
@@ -208,12 +232,7 @@ public extension UIColor {
}
// Try hex
let hexColor = UIColor(hex: string)
- // If hexColor is gray, input was invalid
- if hexColor.toHex() == UIColor.gray.toHex() {
- self.init(cgColor: UIColor.gray.cgColor)
- } else {
- self.init(cgColor: hexColor.cgColor)
- }
+ self.init(cgColor: hexColor.cgColor)
}
/// Initializes a UIColor from a hexadecimal color string.
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/WidgetCommandDispatcher.swift b/OpenHABCore/Sources/OpenHABCore/Util/WidgetCommandDispatcher.swift
new file mode 100644
index 000000000..6c673948b
--- /dev/null
+++ b/OpenHABCore/Sources/OpenHABCore/Util/WidgetCommandDispatcher.swift
@@ -0,0 +1,195 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import Foundation
+
+@MainActor
+public final class WidgetCommandDispatcher {
+ private var pendingTasks: [String: Task] = [:]
+
+ public init() {}
+
+ @MainActor
+ public func send(_ command: String?,
+ for widget: OpenHABWidget,
+ policy: WidgetCommandPolicy,
+ phase: WidgetCommandPhase = .change,
+ key: String? = nil,
+ fallbackItem: OpenHABItem? = nil) {
+ guard let command, !command.isEmpty else { return }
+
+ switch policy {
+ case .immediate:
+ dispatch(command: command, for: widget, fallbackItem: fallbackItem)
+ case let .debounce(duration):
+ guard phase != .release else {
+ dispatch(command: command, for: widget, fallbackItem: fallbackItem)
+ return
+ }
+ sendDebounced(
+ command,
+ for: widget,
+ duration: duration,
+ key: key,
+ fallbackItem: fallbackItem
+ )
+ case .finalOnly:
+ guard phase == .release else { return }
+ dispatch(command: command, for: widget, fallbackItem: fallbackItem)
+ case .pressRelease:
+ guard phase == .press || phase == .release else { return }
+ dispatch(command: command, for: widget, fallbackItem: fallbackItem)
+ }
+ }
+
+ @MainActor
+ public func send(_ command: String?,
+ for item: OpenHABItem?,
+ policy: WidgetCommandPolicy,
+ phase: WidgetCommandPhase = .change,
+ key: String? = nil,
+ execute: @escaping @MainActor (_ itemname: String, _ command: String) -> Void) {
+ guard let command, !command.isEmpty, let item else { return }
+
+ switch policy {
+ case .immediate:
+ execute(item.name, command)
+ case let .debounce(duration):
+ guard phase != .release else {
+ execute(item.name, command)
+ return
+ }
+ sendDebounced(
+ command,
+ for: item,
+ duration: duration,
+ key: key,
+ execute: execute
+ )
+ case .finalOnly:
+ guard phase == .release else { return }
+ execute(item.name, command)
+ case .pressRelease:
+ guard phase == .press || phase == .release else { return }
+ execute(item.name, command)
+ }
+ }
+
+ @MainActor
+ public func sendPress(_ command: String?,
+ for widget: OpenHABWidget,
+ fallbackItem: OpenHABItem? = nil) {
+ guard let command, !command.isEmpty else { return }
+ dispatch(command: command, for: widget, fallbackItem: fallbackItem)
+ }
+
+ @MainActor
+ public func sendRelease(_ command: String?,
+ for widget: OpenHABWidget,
+ fallbackItem: OpenHABItem? = nil) {
+ guard let command, !command.isEmpty else { return }
+ dispatch(command: command, for: widget, fallbackItem: fallbackItem)
+ }
+
+ @MainActor
+ public func sendItemUpdate(_ state: NumberState?,
+ for widget: OpenHABWidget,
+ fallbackItem: OpenHABItem? = nil) {
+ guard let state else { return }
+
+ if let item = widget.item ?? fallbackItem,
+ let sendCommand = widget.sendCommand {
+ if item.isOfTypeOrGroupType(.numberWithDimension) {
+ sendCommand(item, state.toString(locale: Locale(identifier: "US")))
+ } else {
+ sendCommand(item, state.stringValue)
+ }
+ return
+ }
+
+ widget.sendItemUpdate(state: state)
+ }
+
+ @MainActor
+ public func cancelPending(for widget: OpenHABWidget, key: String? = nil) {
+ let taskKey = commandKey(for: widget, key: key)
+ pendingTasks[taskKey]?.cancel()
+ pendingTasks.removeValue(forKey: taskKey)
+ }
+
+ @MainActor
+ public func cancelPending(for item: OpenHABItem, key: String? = nil) {
+ let taskKey = commandKey(for: item, key: key)
+ pendingTasks[taskKey]?.cancel()
+ pendingTasks.removeValue(forKey: taskKey)
+ }
+
+ @MainActor
+ private func sendDebounced(_ command: String,
+ for widget: OpenHABWidget,
+ duration: Duration,
+ key: String?,
+ fallbackItem: OpenHABItem?) {
+ let taskKey = commandKey(for: widget, key: key)
+ pendingTasks[taskKey]?.cancel()
+ pendingTasks[taskKey] = Task { @MainActor [weak self] in
+ try? await Task.sleep(for: duration)
+ guard !Task.isCancelled else { return }
+ self?.dispatch(command: command, for: widget, fallbackItem: fallbackItem)
+ self?.pendingTasks.removeValue(forKey: taskKey)
+ }
+ }
+
+ @MainActor
+ private func sendDebounced(_ command: String,
+ for item: OpenHABItem,
+ duration: Duration,
+ key: String?,
+ execute: @escaping @MainActor (_ itemname: String, _ command: String) -> Void) {
+ let taskKey = commandKey(for: item, key: key)
+ pendingTasks[taskKey]?.cancel()
+ pendingTasks[taskKey] = Task { @MainActor [weak self] in
+ try? await Task.sleep(for: duration)
+ guard !Task.isCancelled else { return }
+ execute(item.name, command)
+ self?.pendingTasks.removeValue(forKey: taskKey)
+ }
+ }
+
+ @MainActor
+ private func dispatch(command: String, for widget: OpenHABWidget, fallbackItem: OpenHABItem?) {
+ if let item = widget.item ?? fallbackItem,
+ let sendCommand = widget.sendCommand {
+ sendCommand(item, command)
+ return
+ }
+ widget.sendCommand(command)
+ }
+
+ private func commandKey(for widget: OpenHABWidget, key: String?) -> String {
+ if let key {
+ return "\(widget.widgetId)-\(key)"
+ }
+ return widget.widgetId
+ }
+
+ private func commandKey(for item: OpenHABItem, key: String?) -> String {
+ if let key {
+ return "\(item.name)-\(key)"
+ }
+ return item.name
+ }
+
+ deinit {
+ pendingTasks.values.forEach { $0.cancel() }
+ pendingTasks.removeAll()
+ }
+}
diff --git a/openHAB/ReusableView.swift b/OpenHABCore/Sources/OpenHABCore/Util/WidgetCommandLifecycleState.swift
similarity index 59%
rename from openHAB/ReusableView.swift
rename to OpenHABCore/Sources/OpenHABCore/Util/WidgetCommandLifecycleState.swift
index c50908411..8bd903570 100644
--- a/openHAB/ReusableView.swift
+++ b/OpenHABCore/Sources/OpenHABCore/Util/WidgetCommandLifecycleState.swift
@@ -9,17 +9,8 @@
//
// SPDX-License-Identifier: EPL-2.0
-import Foundation
-import UIKit
-
-protocol ReusableView {
- static var reuseIdentifier: String { get }
-}
-
-extension ReusableView {
- static var reuseIdentifier: String {
- String(describing: self)
- }
+public enum WidgetCommandLifecycleState: Sendable, Equatable {
+ case idle
+ case sending
+ case failed(message: String?)
}
-
-extension UITableViewCell: ReusableView {}
diff --git a/OpenHABCore/Sources/OpenHABCore/Util/WidgetCommandPolicy.swift b/OpenHABCore/Sources/OpenHABCore/Util/WidgetCommandPolicy.swift
new file mode 100644
index 000000000..57f077cdd
--- /dev/null
+++ b/OpenHABCore/Sources/OpenHABCore/Util/WidgetCommandPolicy.swift
@@ -0,0 +1,46 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import Foundation
+
+public enum WidgetCommandDefaults {
+ public static let slider: WidgetCommandPolicy = .debounce(.milliseconds(500))
+ public static let colorPicker: WidgetCommandPolicy = .debounce(.milliseconds(200))
+ public static let immediate: WidgetCommandPolicy = .immediate
+ public static let finalOnly: WidgetCommandPolicy = .finalOnly
+ public static let pressRelease: WidgetCommandPolicy = .pressRelease
+
+ public static func policy(for widget: OpenHABWidget) -> WidgetCommandPolicy {
+ switch widget.type {
+ case .slider:
+ slider
+ case .colorpicker:
+ colorPicker
+ default:
+ immediate
+ }
+ }
+}
+
+public enum WidgetCommandPhase: Sendable {
+ case press
+ case change
+ case release
+}
+
+public enum WidgetCommandPolicy: Sendable {
+ case immediate
+ case debounce(Duration)
+ /// Dispatches only when phase is `.release`.
+ case finalOnly
+ /// Dispatches only for `.press` and `.release` phases.
+ case pressRelease
+}
diff --git a/OpenHABCore/Tests/OpenHABCoreTests/ETagCheckerTests.swift b/OpenHABCore/Tests/OpenHABCoreTests/ETagCheckerTests.swift
index 797e537d5..4c0059f4e 100644
--- a/OpenHABCore/Tests/OpenHABCoreTests/ETagCheckerTests.swift
+++ b/OpenHABCore/Tests/OpenHABCoreTests/ETagCheckerTests.swift
@@ -13,6 +13,70 @@ import Foundation
@testable import OpenHABCore
import Testing
+// MARK: - Mock URLProtocol
+
+class MockURLProtocol: URLProtocol {
+ // Use nonisolated(unsafe) to suppress concurrency warnings
+ // Safe because tests run with .serialized, ensuring sequential execution
+ nonisolated(unsafe) static var mockResponses: [URL: (statusCode: Int, headers: [String: String])] = [:]
+ nonisolated(unsafe) static var shouldFail = false
+ nonisolated(unsafe) static var error: Error?
+
+ static func reset() {
+ mockResponses.removeAll()
+ shouldFail = false
+ error = nil
+ }
+
+ override class func canInit(with request: URLRequest) -> Bool {
+ true
+ }
+
+ override class func canonicalRequest(for request: URLRequest) -> URLRequest {
+ request
+ }
+
+ override func startLoading() {
+ if MockURLProtocol.shouldFail {
+ let error = MockURLProtocol.error ?? URLError(.badServerResponse)
+ client?.urlProtocol(self, didFailWithError: error)
+ return
+ }
+
+ guard let url = request.url else {
+ client?.urlProtocol(self, didFailWithError: URLError(.badURL))
+ return
+ }
+
+ guard let mock = MockURLProtocol.mockResponses[url] else {
+ // Return 404 if no mock configured
+ let response = HTTPURLResponse(
+ url: url,
+ statusCode: 404,
+ httpVersion: "HTTP/1.1",
+ headerFields: nil
+ )!
+ client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ client?.urlProtocol(self, didLoad: Data())
+ client?.urlProtocolDidFinishLoading(self)
+ return
+ }
+
+ let response = HTTPURLResponse(
+ url: url,
+ statusCode: mock.statusCode,
+ httpVersion: "HTTP/1.1",
+ headerFields: mock.headers
+ )!
+
+ client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
+ client?.urlProtocol(self, didLoad: Data())
+ client?.urlProtocolDidFinishLoading(self)
+ }
+
+ override func stopLoading() {}
+}
+
@Suite("ETagChecker Tests", .serialized)
struct ETagCheckerTests {
@Test("First run stores ETag and returns changed")
@@ -246,67 +310,3 @@ struct ETagCheckerTests {
)
}
}
-
-// MARK: - Mock URLProtocol
-
-class MockURLProtocol: URLProtocol {
- // Use nonisolated(unsafe) to suppress concurrency warnings
- // Safe because tests run with .serialized, ensuring sequential execution
- nonisolated(unsafe) static var mockResponses: [URL: (statusCode: Int, headers: [String: String])] = [:]
- nonisolated(unsafe) static var shouldFail: Bool = false
- nonisolated(unsafe) static var error: Error?
-
- override class func canInit(with request: URLRequest) -> Bool {
- true
- }
-
- override class func canonicalRequest(for request: URLRequest) -> URLRequest {
- request
- }
-
- override func startLoading() {
- if MockURLProtocol.shouldFail {
- let error = MockURLProtocol.error ?? URLError(.badServerResponse)
- client?.urlProtocol(self, didFailWithError: error)
- return
- }
-
- guard let url = request.url else {
- client?.urlProtocol(self, didFailWithError: URLError(.badURL))
- return
- }
-
- guard let mock = MockURLProtocol.mockResponses[url] else {
- // Return 404 if no mock configured
- let response = HTTPURLResponse(
- url: url,
- statusCode: 404,
- httpVersion: "HTTP/1.1",
- headerFields: nil
- )!
- client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
- client?.urlProtocol(self, didLoad: Data())
- client?.urlProtocolDidFinishLoading(self)
- return
- }
-
- let response = HTTPURLResponse(
- url: url,
- statusCode: mock.statusCode,
- httpVersion: "HTTP/1.1",
- headerFields: mock.headers
- )!
-
- client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
- client?.urlProtocol(self, didLoad: Data())
- client?.urlProtocolDidFinishLoading(self)
- }
-
- override func stopLoading() {}
-
- static func reset() {
- mockResponses.removeAll()
- shouldFail = false
- error = nil
- }
-}
diff --git a/OpenHABCore/Tests/OpenHABCoreTests/OpenHABWidgetIconStateTests.swift b/OpenHABCore/Tests/OpenHABCoreTests/OpenHABWidgetIconStateTests.swift
new file mode 100644
index 000000000..c2129b3b8
--- /dev/null
+++ b/OpenHABCore/Tests/OpenHABCoreTests/OpenHABWidgetIconStateTests.swift
@@ -0,0 +1,55 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import OpenHABCore
+import SwiftUI
+import Testing
+
+@Suite
+struct OpenHABWidgetIconStateTests {
+// @Test
+// func returnsLowercasedState() {
+// let widget = makeTestWidget(itemState: "ON")
+// #expect(widget.iconState() == "on")
+// }
+
+// @Test
+// func handlesMixedCase() {
+// let widget = makeTestWidget(itemState: "ClOsEd")
+// #expect(widget.iconState() == "closed")
+// }
+
+// @Test
+// func returnsNilWhenStateIsEmpty() {
+// let widget = makeTestWidget(itemState: "")
+// #expect(widget.iconState() == nil)
+// }
+
+ @Test
+ func returnsNilWhenStateIsNil() {
+ let widget = makeTestWidget(itemState: nil)
+ #expect(widget.iconState() == nil)
+ }
+
+ @Test
+ func acceptsNumericString() {
+ let widget = makeTestWidget(itemState: "123")
+ #expect(widget.iconState() == "123")
+ }
+
+ // MARK: - Helpers
+
+ private func makeTestWidget(itemState: String?) -> OpenHABWidget {
+ let dto = OpenHABWidget()
+ dto.item = OpenHABItem(name: "String", type: "String", state: itemState, link: "122", label: "labe", groupType: nil, stateDescription: nil, commandDescription: nil, members: [], category: nil, options: nil)
+ return dto
+ }
+}
diff --git a/OpenHABCore/Tests/OpenHABCoreTests/StringExtensionTests.swift b/OpenHABCore/Tests/OpenHABCoreTests/StringExtensionTests.swift
index 9e92d8d49..f54b04828 100644
--- a/OpenHABCore/Tests/OpenHABCoreTests/StringExtensionTests.swift
+++ b/OpenHABCore/Tests/OpenHABCoreTests/StringExtensionTests.swift
@@ -22,4 +22,20 @@ struct StringExtensionTests {
#expect("///".removeTrailingSlashes() == "")
#expect("".removeTrailingSlashes() == "")
}
+
+ @Test
+ func testIsNoneIcon() throws {
+ let testCases: [String: Bool] = [
+ "none": true,
+ "oh:none": true,
+ "oh:classic:none": true,
+ "oh:foo:none": true,
+ "f7:none": false,
+ "lights": false
+ ]
+
+ for (input, expected) in testCases {
+ #expect(input.isNoneIcon == expected, "\(input) failed")
+ }
+ }
}
diff --git a/OpenHABCore/Tests/OpenHABCoreTests/UIColorTests.swift b/OpenHABCore/Tests/OpenHABCoreTests/UIColorTests.swift
new file mode 100644
index 000000000..4312bdf4e
--- /dev/null
+++ b/OpenHABCore/Tests/OpenHABCoreTests/UIColorTests.swift
@@ -0,0 +1,66 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import OpenHABCore
+import Testing
+import UIKit
+
+@Suite
+struct UIColorTests {
+ @Test
+ func resolvesNamedColor_exactMatch() {
+ #expect(UIColor(fromString: "red") == .ohRed)
+ }
+
+ @Test
+ func resolvesNamedColor_caseInsensitive() {
+ #expect(UIColor(fromString: "NAvY") == .ohNavy)
+ }
+
+ @Test
+ func resolvesNamedColor_withWhitespace() {
+ #expect(UIColor(fromString: " green ") == .ohGreen)
+ }
+
+ @Test
+ func fallsBackToHexParsing() {
+ #expect(UIColor(fromString: "#FF0000") == UIColor(hex: "#FF0000"))
+ }
+
+ @Test
+ func returnsFallbackForInvalidColor() {
+ let color = UIColor(fromString: "notAColor")
+ let fallback = UIColor(hex: "notAColor")
+ #expect(color == fallback)
+ }
+
+ @Test
+ func hexStringForMultipleColors() {
+ // Array of (UIColor, expectedHexString)
+ let testCases: [(UIColor, String?)] = [
+ (UIColor.red, "FF0000"),
+ (UIColor.green, "00FF00"),
+ (UIColor.blue, "0000FF"),
+ (UIColor.white, "FFFFFF"),
+ (UIColor.black, "000000"),
+ (UIColor(red: 1, green: 0, blue: 0, alpha: 0.5), "FF000080"), // 50% alpha
+ (UIColor(patternImage: UIImage()), nil) // Non-RGB color should return nil
+ ]
+
+ for (index, testCase) in testCases.enumerated() {
+ let (color, expectedHex) = testCase
+ #expect(
+ color.hexString == expectedHex,
+ "Case \(index): Expected \(expectedHex ?? "nil"), got \(color.hexString ?? "nil")"
+ )
+ }
+ }
+}
diff --git a/OpenHABCore/Tests/OpenHABCoreTests/WidgetDisplayStateTests.swift b/OpenHABCore/Tests/OpenHABCoreTests/WidgetDisplayStateTests.swift
new file mode 100644
index 000000000..b2a44694e
--- /dev/null
+++ b/OpenHABCore/Tests/OpenHABCoreTests/WidgetDisplayStateTests.swift
@@ -0,0 +1,117 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import OpenHABCore
+import Testing
+
+@Suite
+struct WidgetDisplayStateTests {
+ @Test
+ func usesItemStateWhenWidgetStateIsEmpty() {
+ let widget = makeWidget(
+ widgetState: "",
+ itemState: "ON",
+ label: "Kitchen Light [On]"
+ )
+
+ let display = widget.displayState
+
+ #expect(display.effectiveState == "ON")
+ #expect(display.isOn)
+ #expect(display.labelText == "Kitchen Light")
+ #expect(display.labelValue == "On")
+ }
+
+ @Test
+ func widgetStateOverridesItemState() {
+ let widget = makeWidget(
+ widgetState: "OFF",
+ itemState: "ON",
+ label: "Switch"
+ )
+
+ let display = widget.displayState
+
+ #expect(display.effectiveState == "OFF")
+ #expect(!display.isOn)
+ }
+
+ @Test
+ func resolvesSelectedMappingFromItemState() {
+ let widget = makeWidget(
+ widgetState: "",
+ itemState: "AUTO",
+ label: "Mode",
+ mappings: [
+ OpenHABWidgetMapping(command: "MANUAL", label: "Manual"),
+ OpenHABWidgetMapping(command: "AUTO", label: "Auto")
+ ]
+ )
+
+ let display = widget.displayState
+
+ #expect(display.selectedIndex == 1)
+ #expect(display.selectedLabel == "Auto")
+ }
+
+ // MARK: - Helpers
+
+ private func makeWidget(widgetState: String,
+ itemState: String,
+ label: String,
+ mappings: [OpenHABWidgetMapping] = []) -> OpenHABWidget {
+ let item = OpenHABItem(
+ name: "Item",
+ type: "Switch",
+ state: itemState,
+ link: "",
+ label: nil,
+ groupType: nil,
+ stateDescription: nil,
+ commandDescription: nil,
+ members: [],
+ category: nil,
+ options: nil
+ )
+ let widget = OpenHABWidget(
+ widgetId: "widget-id",
+ label: label,
+ icon: "switch",
+ type: .switchWidget,
+ url: nil,
+ period: nil,
+ minValue: 0,
+ maxValue: 100,
+ step: 1,
+ refresh: nil,
+ height: nil,
+ isLeaf: nil,
+ iconColor: nil,
+ labelColor: nil,
+ valueColor: nil,
+ service: nil,
+ state: widgetState,
+ text: nil,
+ legend: nil,
+ inputHint: nil,
+ encoding: nil,
+ item: item,
+ linkedPage: nil,
+ mappings: mappings,
+ widgets: [],
+ visibility: true,
+ switchSupport: false,
+ forceAsItem: nil,
+ labelSource: .sitemapDefinition
+ )
+ return widget
+ }
+}
diff --git a/OpenHABCore/Tests/OpenHABCoreTests/WidgetRenderingKindTests.swift b/OpenHABCore/Tests/OpenHABCoreTests/WidgetRenderingKindTests.swift
new file mode 100644
index 000000000..40f96e4d7
--- /dev/null
+++ b/OpenHABCore/Tests/OpenHABCoreTests/WidgetRenderingKindTests.swift
@@ -0,0 +1,74 @@
+// Copyright (c) 2010-2026 Contributors to the openHAB project
+//
+// See the NOTICE file(s) distributed with this work for additional
+// information.
+//
+// This program and the accompanying materials are made available under the
+// terms of the Eclipse Public License 2.0 which is available at
+// http://www.eclipse.org/legal/epl-2.0
+//
+// SPDX-License-Identifier: EPL-2.0
+
+import OpenHABCore
+import Testing
+
+@Suite
+struct WidgetRenderingKindTests {
+ @Test
+ func switchWithMappingsUsesSegmentedKind() {
+ let widget = makeWidget(type: .switchWidget, itemType: "Switch")
+ widget.mappings = [OpenHABWidgetMapping(command: "ON", label: "On")]
+
+ #expect(widget.renderingKind == .segmentedSwitch)
+ }
+
+ @Test
+ func switchWithSwitchItemUsesToggleKind() {
+ let widget = makeWidget(type: .switchWidget, itemType: "Switch")
+
+ #expect(widget.renderingKind == .toggleSwitch)
+ }
+
+ @Test
+ func switchWithRollershutterItemUsesRollershutterKind() {
+ let widget = makeWidget(type: .switchWidget, itemType: "Rollershutter")
+
+ #expect(widget.renderingKind == .rollershutterSwitch)
+ }
+
+ @Test
+ func inputDateHintUsesDateInputKind() {
+ let widget = makeWidget(type: .input, itemType: "String")
+ widget.inputHint = .dateTime
+
+ #expect(widget.renderingKind == .dateInput)
+ }
+
+ @Test
+ func inputTextHintUsesTextInputKind() {
+ let widget = makeWidget(type: .input, itemType: "String")
+ widget.inputHint = .text
+
+ #expect(widget.renderingKind == .textInput)
+ }
+
+ private func makeWidget(type: OpenHABWidget.WidgetType, itemType: String) -> OpenHABWidget {
+ let item = OpenHABItem(
+ name: "Item",
+ type: itemType,
+ state: "OFF",
+ link: "",
+ label: nil,
+ groupType: nil,
+ stateDescription: nil,
+ commandDescription: nil,
+ members: [],
+ category: nil,
+ options: nil
+ )
+ let widget = OpenHABWidget()
+ widget.type = type
+ widget.item = item
+ return widget
+ }
+}
diff --git a/TestPlans/openHABTests.xctestplan b/TestPlans/openHABTests.xctestplan
index 3ed419e45..19f3494b5 100644
--- a/TestPlans/openHABTests.xctestplan
+++ b/TestPlans/openHABTests.xctestplan
@@ -15,6 +15,14 @@
"codeCoverage" : false
},
"testTargets" : [
+ {
+ "parallelizable" : true,
+ "target" : {
+ "containerPath" : "container:OpenHABCore",
+ "identifier" : "OpenHABCoreTests",
+ "name" : "OpenHABCoreTests"
+ }
+ },
{
"parallelizable" : true,
"target" : {
@@ -24,11 +32,10 @@
}
},
{
- "parallelizable" : true,
"target" : {
- "containerPath" : "container:OpenHABCore",
- "identifier" : "OpenHABCoreTests",
- "name" : "OpenHABCoreTests"
+ "containerPath" : "container:CommonUI",
+ "identifier" : "CommonUITests",
+ "name" : "CommonUITests"
}
}
],
diff --git a/openHAB.xcodeproj/project.pbxproj b/openHAB.xcodeproj/project.pbxproj
index e434e5906..c34fe7e5e 100644
--- a/openHAB.xcodeproj/project.pbxproj
+++ b/openHAB.xcodeproj/project.pbxproj
@@ -7,20 +7,31 @@
objects = {
/* Begin PBXBuildFile section */
+ AA0001022F5A000000000001 /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001012F5A000000000001 /* SplashView.swift */; };
+ AA0001042F5A000000000002 /* OpenHABApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0001032F5A000000000002 /* OpenHABApp.swift */; };
+ 0C93C5B057F863322A1B9820 /* SystemTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4229903F00160C965E22DE21 /* SystemTab.swift */; };
+ 10472F7AF99C4940A6144817 /* TextRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399449C421544C61AD83450C /* TextRow.swift */; };
1224F78F228A89FD00750965 /* WatchMessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1224F78D228A89FC00750965 /* WatchMessageService.swift */; };
+ 1CE7AC462008E29FA37F231D /* AppServicesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64BEA2793F07C36BB98D9B8 /* AppServicesViewModel.swift */; };
+ 2C9BBB28C068BCC10291A566 /* TilesTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE239F0348D19000408BA8AA /* TilesTab.swift */; };
2F08AFC72E5FADCF00E70611 /* NotificationCenterDelegateImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F08AFC62E5FADC500E70611 /* NotificationCenterDelegateImpl.swift */; };
+ 2F54B56D2F40AC1A00DA1F1A /* REFACTORING_SUMMARY.md in Resources */ = {isa = PBXBuildFile; fileRef = 2F54B56C2F40AC1A00DA1F1A /* REFACTORING_SUMMARY.md */; };
+ 2F54B5742F428ACA00DA1F1A /* AsyncAlgorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 2F54B5732F428ACA00DA1F1A /* AsyncAlgorithms */; };
+ 2F54B5772F428B0200DA1F1A /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = 2F54B5762F428B0200DA1F1A /* Algorithms */; };
2F55E7BB2DEE447700EC8350 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F55E7BA2DEE447700EC8350 /* SettingsView.swift */; };
2F55E7BD2DEE44A800EC8350 /* ClientCertificatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F55E7BC2DEE44A800EC8350 /* ClientCertificatesView.swift */; };
- 2F6412EE2CE494A80039FB28 /* DatePickerUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F6412ED2CE494A80039FB28 /* DatePickerUITableViewCell.swift */; };
+ 2F77DF3C2F43D3D700BE3744 /* OpenHABWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F77DF3B2F43D3D700BE3744 /* OpenHABWebView.swift */; };
+ 2F77DF3E2F43D42E00BE3744 /* WEBVIEW_MIGRATION.md in Resources */ = {isa = PBXBuildFile; fileRef = 2F77DF3D2F43D42E00BE3744 /* WEBVIEW_MIGRATION.md */; };
+ 2F77DF402F43D94C00BE3744 /* UIKIT_MIGRATION_ANALYSIS.md in Resources */ = {isa = PBXBuildFile; fileRef = 2F77DF3F2F43D94C00BE3744 /* UIKIT_MIGRATION_ANALYSIS.md */; };
+ 2F77DF422F43D96800BE3744 /* CertificateManagementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F77DF412F43D96800BE3744 /* CertificateManagementService.swift */; };
+ 2F77DF442F43D97400BE3744 /* IdleTimerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F77DF432F43D97400BE3744 /* IdleTimerService.swift */; };
+ 2F77DF462F43D9A400BE3744 /* UIKIT_REMOVAL_GUIDE.md in Resources */ = {isa = PBXBuildFile; fileRef = 2F77DF452F43D9A400BE3744 /* UIKIT_REMOVAL_GUIDE.md */; };
2FBCF58C2DEB0B7700CD5D83 /* HomeSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FBCF58B2DEB0B7700CD5D83 /* HomeSelectionView.swift */; };
- 2FEFD8F62BE7C5BE00E387B9 /* TextInputUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FEFD8F52BE7C5BE00E387B9 /* TextInputUITableViewCell.swift */; };
2FF459362E230C6A00C0B640 /* OpenHABIntentHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF459352E230C6A00C0B640 /* OpenHABIntentHelper.swift */; };
4D6470DA2561F935007B03FC /* openHABIntents.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4D6470D32561F935007B03FC /* openHABIntents.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
652B81042E2193B500648510 /* ScreenSaverSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 652B81032E2193B500648510 /* ScreenSaverSettingsView.swift */; };
652B81092E2193DA00648510 /* ScreenSaverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 652B81062E2193DA00648510 /* ScreenSaverManager.swift */; };
652B810A2E2193DA00648510 /* ScreenSaverConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 652B81052E2193DA00648510 /* ScreenSaverConfiguration.swift */; };
- 653B54C2285E714900298ECD /* OpenHABViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653B54C1285E714900298ECD /* OpenHABViewController.swift */; };
- 65570A7D2476D16A00D524EA /* OpenHABWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65570A7C2476D16A00D524EA /* OpenHABWebViewController.swift */; };
6557AF8F2C0241C10094D0C8 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 6557AF8E2C0241C10094D0C8 /* PrivacyInfo.xcprivacy */; };
6557AF902C0241C10094D0C8 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 6557AF8E2C0241C10094D0C8 /* PrivacyInfo.xcprivacy */; };
6557AF922C039D140094D0C8 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 6557AF912C039D140094D0C8 /* FirebaseMessaging */; };
@@ -28,13 +39,15 @@
657144512C1E438700C8A1F3 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657144502C1E438700C8A1F3 /* NotificationService.swift */; };
657144552C1E438700C8A1F3 /* NotificationService.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 6571444E2C1E438700C8A1F3 /* NotificationService.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
657144962C30A16700C8A1F3 /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = 657144952C30A16700C8A1F3 /* OpenHABCore */; };
- 65C2EF492E244C8500A0C19F /* OpenHABNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2EF482E244C8500A0C19F /* OpenHABNavigationController.swift */; };
65F055442E3D4E41004E98FE /* ItemSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F055432E3D4E41004E98FE /* ItemSelectionView.swift */; };
7BFFEA908B9E47FCB5C46E6E /* VideoStreamManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 905A534AB32C4104AC55A75C /* VideoStreamManager.swift */; };
+ 801159DCB99C03EFA71F4B9D /* MainWebTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5135184D9630899A34EFBA95 /* MainWebTab.swift */; };
+ 911CC92713E7DF11C3295A4C /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3C3E3BF4C930ADA95E6E997 /* SafariView.swift */; };
932602EE2382892B00EAD685 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DAC6608B236F6F4200F4501E /* Assets.xcassets */; };
933D7F0722E7015100621A03 /* OpenHABUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 933D7F0622E7015000621A03 /* OpenHABUITests.swift */; };
933D7F0F22E7030600621A03 /* SnapshotHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 933D7F0E22E7030600621A03 /* SnapshotHelper.swift */; };
934E592528F16EBA00162004 /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = 934E592428F16EBA00162004 /* OpenHABCore */; };
+ 934E592728F16EBA00162004 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 934E592628F16EBA00162004 /* Kingfisher */; };
934E592928F16EBA00162004 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 934E592828F16EBA00162004 /* DeviceKit */; };
935B484625342B8E00E44CF0 /* URL+Static.swift in Sources */ = {isa = PBXBuildFile; fileRef = 935B484525342B8E00E44CF0 /* URL+Static.swift */; };
93685A7A2ADE755C0077A9A6 /* openHABTests.xctestplan in Resources */ = {isa = PBXBuildFile; fileRef = 93685A792ADE755C0077A9A6 /* openHABTests.xctestplan */; };
@@ -42,18 +55,16 @@
937E4471270B36D000A98C26 /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = 937E4470270B36D000A98C26 /* OpenHABCore */; };
937E4473270B36DD00A98C26 /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = 937E4472270B36DD00A98C26 /* OpenHABCore */; };
937E4485270B379900A98C26 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 937E4484270B379900A98C26 /* DeviceKit */; };
+ 937E4488270B37A600A98C26 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 937E4487270B37A600A98C26 /* Kingfisher */; };
937E448C270B37CA00A98C26 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 937E448B270B37CA00A98C26 /* Kingfisher */; };
937E448E270B37D200A98C26 /* DeviceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 937E448D270B37D200A98C26 /* DeviceKit */; };
+ 937E4492270B37FE00A98C26 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 937E4491270B37FE00A98C26 /* Kingfisher */; };
937E44E2270B393C00A98C26 /* OpenHABCore in Frameworks */ = {isa = PBXBuildFile; productRef = 937E44E1270B393C00A98C26 /* OpenHABCore */; };
938620C0257E223C00A63200 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 938BF9C824EFCCC000E6B52F /* Localizable.strings */; };
938BF89624EFBC5400E6B52F /* LocalizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938BF89524EFBC5400E6B52F /* LocalizationTests.swift */; };
- 938BF9C424EFCB9F00E6B52F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 938BF9C324EFCB9F00E6B52F /* Main.storyboard */; };
- 938BF9C624EFCC0700E6B52F /* UILabel+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938BF9C524EFCC0700E6B52F /* UILabel+Localization.swift */; };
938BF9D024EFCCC000E6B52F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 938BF9C824EFCCC000E6B52F /* Localizable.strings */; };
938BF9D124EFCCC000E6B52F /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 938BF9CA24EFCCC000E6B52F /* InfoPlist.strings */; };
- 938BF9D324EFD0B700E6B52F /* UIViewController+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938BF9D224EFD0B700E6B52F /* UIViewController+Localization.swift */; };
938BF9D524EFD5B100E6B52F /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 938BF9C824EFCCC000E6B52F /* Localizable.strings */; };
- 938EDCE122C4FEB800661CA1 /* ScaleAspectFitImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 938EDCE022C4FEB800661CA1 /* ScaleAspectFitImageView.swift */; };
9397EDEC2587837000F266E1 /* openHABWatch.app in Embed Watch Content */ = {isa = PBXBuildFile; fileRef = DA0775152346705D0086C685 /* openHABWatch.app */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
93AEE42427D9D76B008EB207 /* GetItemStateIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6471112561FDA7007B03FC /* GetItemStateIntentHandler.swift */; };
93AEE42527D9D76E008EB207 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D6470D52561F935007B03FC /* IntentHandler.swift */; };
@@ -66,9 +77,11 @@
93F8063527AE6C620035A6B0 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 93F8063427AE6C620035A6B0 /* FirebaseCrashlytics */; };
93F8064727AE7A050035A6B0 /* SwiftMessages in Frameworks */ = {isa = PBXBuildFile; productRef = 93F8064627AE7A050035A6B0 /* SwiftMessages */; };
93F8064A27AE7A2E0035A6B0 /* FlexColorPicker in Frameworks */ = {isa = PBXBuildFile; productRef = 93F8064927AE7A2E0035A6B0 /* FlexColorPicker */; };
- 93F8065027AE7A830035A6B0 /* SideMenu in Frameworks */ = {isa = PBXBuildFile; productRef = 93F8064F27AE7A830035A6B0 /* SideMenu */; };
A3F4C3A51A49A5940019A09F /* MainLaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = A3F4C3A41A49A5940019A09F /* MainLaunchScreen.xib */; };
- B7D5ECE121499E55001B0EC6 /* MapViewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7D5ECE021499E55001B0EC6 /* MapViewTableViewCell.swift */; };
+ A75A53645D1542CBAC658099 /* TabCustomizationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3698C230427F48A782B1980B /* TabCustomizationSection.swift */; };
+ BE875E6CE1B6E05E492F29CE /* OpenHABTabRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52411B814C63C9F1CC8CED90 /* OpenHABTabRootView.swift */; };
+ C4377202F7D642B5A8349008 /* SelectionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86D8BF295C448039B2B85EB /* SelectionRow.swift */; };
+ D8C89708AE75DEFCD78566EC /* SitemapsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13181EE034B26E3A1FF7F4A8 /* SitemapsTab.swift */; };
DA0749DE23E0B5950057FA83 /* ColorPickerRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0749DD23E0B5950057FA83 /* ColorPickerRow.swift */; };
DA0749E023E0BF510057FA83 /* ColorSelection.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0749DF23E0BF510057FA83 /* ColorSelection.swift */; };
DA07751B2346705F0086C685 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DA07751A2346705F0086C685 /* Assets.xcassets */; };
@@ -81,19 +94,33 @@
DA0F37D023D4ACC7007EAB48 /* SliderRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0F37CF23D4ACC7007EAB48 /* SliderRow.swift */; };
DA10161B2DC7BAE500552D14 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA10161A2DC7BAE500552D14 /* SFSafeSymbols */; };
DA15BFBD23C6726400BD8ADA /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA15BFBC23C6726400BD8ADA /* AppSettings.swift */; };
- DA162BEC2CD3B53E0040DAE5 /* LogsViewer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA162BEB2CD3B53E0040DAE5 /* LogsViewer.swift */; };
DA19E25B22FD801D002F8F2F /* OpenHABGeneralTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA19E25A22FD801D002F8F2F /* OpenHABGeneralTests.swift */; };
DA21EAE22339621C001AB415 /* Throttler.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA21EAE12339621C001AB415 /* Throttler.swift */; };
+ DA2741002EA62F1F002FE576 /* SitemapPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2740FF2EA62F1F002FE576 /* SitemapPageView.swift */; };
+ DA2741022EA62FA3002FE576 /* SegmentSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2741012EA62FA3002FE576 /* SegmentSelectionView.swift */; };
DA28C362225241DE00AB409C /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DA28C361225241DE00AB409C /* WebKit.framework */; settings = {ATTRIBUTES = (Required, ); }; };
DA2AEB6E2D92BAD800897D80 /* PageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2AEB6D2D92BAD800897D80 /* PageLoader.swift */; };
- DA2AEB702D92CF3E00897D80 /* UITableViewCellExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2AEB6F2D92CF3E00897D80 /* UITableViewCellExtension.swift */; };
DA2AEBA02D92FB6500897D80 /* NoIconDisplayableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2AEB9F2D92FB6500897D80 /* NoIconDisplayableCell.swift */; };
DA2C4FD52B4F573300D1C533 /* SDWebImageSVGCoder in Frameworks */ = {isa = PBXBuildFile; productRef = DA2C4FD42B4F573300D1C533 /* SDWebImageSVGCoder */; };
+ DA2D2F8A2F3943A800EC605A /* WidgetRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2D2F892F3943A800EC605A /* WidgetRowViewModel.swift */; };
DA2E0AA423DC96E9009B0A99 /* ImageWithAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2E0AA323DC96E9009B0A99 /* ImageWithAction.swift */; };
DA2E0B0E23DCC153009B0A99 /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2E0B0D23DCC152009B0A99 /* MapView.swift */; };
DA2E0B1023DCC439009B0A99 /* MapViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2E0B0F23DCC439009B0A99 /* MapViewRow.swift */; };
DA32D1B42C8C98C40018D974 /* IconWithAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA32D1B32C8C98C40018D974 /* IconWithAction.swift */; };
DA3563D92E5096BE00BC0138 /* ScreenSaverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA3563D82E5096BE00BC0138 /* ScreenSaverView.swift */; };
+ DA35E2B02E1EDB86003987BB /* SetpointRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2AF2E1EDB86003987BB /* SetpointRowView.swift */; };
+ DA35E2BD2E1EEA9D003987BB /* MapRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2B52E1EEA9D003987BB /* MapRowView.swift */; };
+ DA35E2BE2E1EEA9D003987BB /* DatePickerInputRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2B32E1EEA9D003987BB /* DatePickerInputRowView.swift */; };
+ DA35E2BF2E1EEA9D003987BB /* WebRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2BC2E1EEA9D003987BB /* WebRowView.swift */; };
+ DA35E2C02E1EEA9D003987BB /* ColorPickerRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2B22E1EEA9D003987BB /* ColorPickerRowView.swift */; };
+ DA35E2C22E1EEA9D003987BB /* TextInputRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2BA2E1EEA9D003987BB /* TextInputRowView.swift */; };
+ DA35E2C32E1EEA9D003987BB /* ImageRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2B42E1EEA9D003987BB /* ImageRowView.swift */; };
+ DA35E2C42E1EEA9D003987BB /* SelectionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2B82E1EEA9D003987BB /* SelectionRowView.swift */; };
+ DA35E2C52E1EEA9D003987BB /* RollershutterRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2B62E1EEA9D003987BB /* RollershutterRowView.swift */; };
+ DA35E2C62E1EEA9D003987BB /* SegmentedRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2B72E1EEA9D003987BB /* SegmentedRowView.swift */; };
+ DA35E2C72E1EEA9D003987BB /* VideoRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2BB2E1EEA9D003987BB /* VideoRowView.swift */; };
+ DA35E2CB2E1F93AD003987BB /* ImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2CA2E1F93AD003987BB /* ImageView.swift */; };
+ DA35E2CD2E1F96CA003987BB /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA35E2CC2E1F96CA003987BB /* IconView.swift */; };
DA4800142D836892009CF127 /* ConnectionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4800132D836892009CF127 /* ConnectionSettingsView.swift */; };
DA4800162D836EF0009CF127 /* MainUISettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4800152D836EF0009CF127 /* MainUISettingsView.swift */; };
DA4800182D837221009CF127 /* AboutSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4800172D837221009CF127 /* AboutSettingsView.swift */; };
@@ -102,68 +129,63 @@
DA48001E2D837905009CF127 /* ApplicationSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA48001D2D837905009CF127 /* ApplicationSettingsView.swift */; };
DA4800212D839D3A009CF127 /* AnimatedSecureTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA4800202D839D39009CF127 /* AnimatedSecureTextField.swift */; };
DA4D4DB5233F9ACB00B37E37 /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = DA4D4DB4233F9ACB00B37E37 /* README.md */; };
- DA50C7BD2B0A51BD0009F716 /* SliderWithSwitchSupportRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA50C7BC2B0A51BD0009F716 /* SliderWithSwitchSupportRow.swift */; };
- DA50C7BF2B0A65300009F716 /* SliderWithSwitchSupportUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA50C7BE2B0A652F0009F716 /* SliderWithSwitchSupportUITableViewCell.swift */; };
DA5ED9BE2C850955004875E0 /* ClientCertificatesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA5ED9BD2C850955004875E0 /* ClientCertificatesViewModel.swift */; };
+ DA64ACA62DBEAD5600294F60 /* SitemapPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA64ACA52DBEAD5600294F60 /* SitemapPageViewModel.swift */; };
+ DA64ACA82DBEAD8300294F60 /* SitemapPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA64ACA72DBEAD8300294F60 /* SitemapPageView.swift */; };
+ DA64ACAA2DBEAD9000294F60 /* SitemapNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA64ACA92DBEAD9000294F60 /* SitemapNavigationView.swift */; };
DA65871F236F83CE007E2E7F /* UserDefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA65871E236F83CD007E2E7F /* UserDefaultsExtension.swift */; };
- DA6B2EEF2C861BC900DF77CF /* DrawerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6B2EEE2C861BC900DF77CF /* DrawerView.swift */; };
DA6B2EF12C87B59000DF77CF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6B2EF02C87B59000DF77CF /* NotificationsView.swift */; };
DA6B2EF52C89F8F200DF77CF /* ColorPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6B2EF42C89F8F200DF77CF /* ColorPickerView.swift */; };
DA6B2EF72C8B92E800DF77CF /* SelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA6B2EF62C8B92E800DF77CF /* SelectionView.swift */; };
- DA7125372EC892BB0067D7B2 /* LoggerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7125362EC892BB0067D7B2 /* LoggerView.swift */; };
- DA7224D223828D3400712D20 /* PreviewConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7224D123828D3300712D20 /* PreviewConstants.swift */; };
DA72E1B8236DEA0900B8EF3A /* AppMessageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA72E1B5236DEA0900B8EF3A /* AppMessageService.swift */; };
DA77E19B2D886D9B007CFF0F /* SingleConnectionSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA77E19A2D886D9B007CFF0F /* SingleConnectionSettingsView.swift */; };
DA7ACD5F2DC3DB130055CFC7 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA7ACD5E2DC3DB130055CFC7 /* SFSafeSymbols */; settings = {ATTRIBUTES = (Required, ); }; };
- DA7E1E4B2233986E002AEFD8 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7E1E47222EB00B002AEFD8 /* PlayerView.swift */; };
DA7F002D2EB376CF00DE943A /* ServerCertificatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA7F002C2EB376CF00DE943A /* ServerCertificatesView.swift */; };
DA817E7A234BF39B00C91824 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = DA817E79234BF39B00C91824 /* CHANGELOG.md */; };
DA88F8C622EC377200B408E5 /* ReleaseNotes.md in Resources */ = {isa = PBXBuildFile; fileRef = DA88F8C522EC377100B408E5 /* ReleaseNotes.md */; };
+ DA8B14B62F3A0DFF007753FD /* WidgetRowFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8B14B52F3A0DFF007753FD /* WidgetRowFactory.swift */; };
+ DA8B14B82F3A120E007753FD /* PreviewWidgetFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8B14B72F3A11F0007753FD /* PreviewWidgetFactory.swift */; };
+ DA8B14BA2F3A373A007753FD /* PreviewNavigationContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8B14B92F3A373A007753FD /* PreviewNavigationContainer.swift */; };
+ DA8B14BC2F3A3CB5007753FD /* WatchTypography.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA8B14BB2F3A3CB5007753FD /* WatchTypography.swift */; };
DA94AEB42EC896BE003BB3C8 /* SimpleMJPEGPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA94AEB32EC896BE003BB3C8 /* SimpleMJPEGPlayer.swift */; };
- DA95F3332E0F2B1700FE4474 /* OpenHABRootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA95F3322E0F2B1700FE4474 /* OpenHABRootViewController.swift */; };
- DA95F3352E0F2C1600FE4474 /* OpenHABSitemapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA95F3342E0F2C1600FE4474 /* OpenHABSitemapViewController.swift */; };
DA96415A2F292EE200CEC181 /* BonjourDiscoveryViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9641592F292EE200CEC181 /* BonjourDiscoveryViewModelTests.swift */; };
DA96415C2F292F0600CEC181 /* OpenHABEndPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA96415B2F292F0600CEC181 /* OpenHABEndPoint.swift */; };
DA9721C324E29A8F0092CCFD /* UserDefaultsBacked.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9721C224E29A8F0092CCFD /* UserDefaultsBacked.swift */; };
DA9A7EFD2D668D5900824156 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA9A7EFC2D668D5900824156 /* SFSafeSymbols */; };
DA9A7EFF2D66915900824156 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = DA9A7EFE2D66915900824156 /* SFSafeSymbols */; };
DA9F81872C85020F00B47B72 /* RTFTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9F81862C85020F00B47B72 /* RTFTextView.swift */; };
- DAA42BA821DC97E000244B2A /* NotificationTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA42BA721DC97DF00244B2A /* NotificationTableViewCell.swift */; };
- DAA42BAA21DC983B00244B2A /* VideoUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA42BA921DC983B00244B2A /* VideoUITableViewCell.swift */; };
- DAA42BAC21DC984A00244B2A /* WebUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAA42BAB21DC984A00244B2A /* WebUITableViewCell.swift */; };
DAA599B82EAC0FE7003A8726 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = DAA599B72EAC0FE7003A8726 /* AppIcon.icon */; };
DAA599B92EAC0FE7003A8726 /* AppIcon.icon in Resources */ = {isa = PBXBuildFile; fileRef = DAA599B72EAC0FE7003A8726 /* AppIcon.icon */; };
- DAAAB2812EA3843100F1B05D /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = DAAAB2802EA3843100F1B05D /* Kingfisher */; };
- DAAAB2832EA3874500F1B05D /* SegmentSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAAAB2822EA3874400F1B05D /* SegmentSelectionView.swift */; };
- DAAC30872CBBF0420041927F /* SitemapPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0775262346705F0086C685 /* SitemapPageView.swift */; };
DABB5E332D98972F009A4B8A /* SDWebImageSVGCoder in Frameworks */ = {isa = PBXBuildFile; productRef = DABB5E322D98972F009A4B8A /* SDWebImageSVGCoder */; };
DABED17B2E451694000B92EF /* BonjourDiscoverySheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABED17A2E451694000B92EF /* BonjourDiscoverySheet.swift */; };
DABED17D2E4516B4000B92EF /* BonjourDiscoveryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABED17C2E4516B4000B92EF /* BonjourDiscoveryViewModel.swift */; };
DAC131112DA3213100075AE2 /* SetSwitchStateIntentHandlerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC131102DA3213100075AE2 /* SetSwitchStateIntentHandlerTests.swift */; };
DAC131122DA32F5D00075AE2 /* Intents.intentdefinition in Resources */ = {isa = PBXBuildFile; fileRef = 935D340A257B7DC00020A404 /* Intents.intentdefinition */; };
- DAC65FC7236EDF3900F4501E /* SpinnerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC65FC6236EDF3900F4501E /* SpinnerViewController.swift */; };
DAC6608D236F771600F4501E /* PreferencesSwiftUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC6608C236F771600F4501E /* PreferencesSwiftUIView.swift */; };
DAC9395522B00E7600C5F423 /* XCTestCaseExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC9395422B00E7600C5F423 /* XCTestCaseExtension.swift */; };
+ DAC949FA2E219F0D007E67B7 /* CommonUI in Frameworks */ = {isa = PBXBuildFile; productRef = DAC949F92E219F0D007E67B7 /* CommonUI */; };
+ DAC949FC2E219F30007E67B7 /* CommonUI in Frameworks */ = {isa = PBXBuildFile; productRef = DAC949FB2E219F30007E67B7 /* CommonUI */; };
+ DAC949FE2E21A2D1007E67B7 /* FrameRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC949FD2E21A2D1007E67B7 /* FrameRowView.swift */; };
DAC9AF4924F966FA006DAE93 /* LazyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC9AF4824F966FA006DAE93 /* LazyView.swift */; };
DACA368E2D7440B9003CD237 /* OpenHABWidgetExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC9AF4624F9669F006DAE93 /* OpenHABWidgetExtension.swift */; };
- DAD085712AE4782D001D36BE /* OpenHABWatchAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAD085702AE4782D001D36BE /* OpenHABWatchAppTests.swift */; };
DAD0857B2AE4782F001D36BE /* OpenHABWatchUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAD0857A2AE4782F001D36BE /* OpenHABWatchUITests.swift */; };
DAD0857D2AE4782F001D36BE /* OpenHABWatchLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAD0857C2AE4782F001D36BE /* OpenHABWatchLaunchTests.swift */; };
DAD0858B2AE56F0E001D36BE /* OpenHABWatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAD0855F2AE47824001D36BE /* OpenHABWatch.swift */; };
- DADC420A2E7AB899004E866F /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = DADC42092E7AB899004E866F /* SDWebImage */; };
- DAEAA89D21E6B06400267EA3 /* ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAEAA89C21E6B06300267EA3 /* ReusableView.swift */; };
- DAEAA89F21E6B16600267EA3 /* UITableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAEAA89E21E6B16600267EA3 /* UITableView.swift */; };
- DAF0A28B2C56E3A300A14A6A /* RollershutterCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF0A28A2C56E3A300A14A6A /* RollershutterCell.swift */; };
- DAF0A28D2C56EF8900A14A6A /* SetpointCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF0A28C2C56EF8900A14A6A /* SetpointCell.swift */; };
- DAF0A28F2C56F1EE00A14A6A /* ColorPickerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF0A28E2C56F1EE00A14A6A /* ColorPickerCell.swift */; };
+ DADC420A2E7AB899004E866F /* (null) in Frameworks */ = {isa = PBXBuildFile; };
+ DAE2800A2E35F5590028EE24 /* IconURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE280092E35F5590028EE24 /* IconURLView.swift */; };
+ DAE7B4A72E26927C00B9FE99 /* ButtonGridRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAE7B4A62E26927C00B9FE99 /* ButtonGridRowView.swift */; };
+ DAEA21D82DBF472D00D54342 /* RowViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAEA21D72DBF472D00D54342 /* RowViewFactory.swift */; };
+ DAEA21DA2DBF477E00D54342 /* SwitchRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAEA21D92DBF477E00D54342 /* SwitchRowView.swift */; };
+ DAEA21DC2DBF47DA00D54342 /* SliderRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAEA21DB2DBF47DA00D54342 /* SliderRowView.swift */; };
+ DAEA21DE2DBF481300D54342 /* TextRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAEA21DD2DBF481300D54342 /* TextRowView.swift */; };
+ DAEA21E02DBF483E00D54342 /* GenericRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAEA21DF2DBF483E00D54342 /* GenericRowView.swift */; };
+ DAEE35072E224F6000B4A7F1 /* ColorTemperaturePickerRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAEE35062E224F6000B4A7F1 /* ColorTemperaturePickerRowView.swift */; };
DAF231D227BB6EEA00AB916C /* OpenHABSVGTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF231D127BB6EEA00AB916C /* OpenHABSVGTests.swift */; };
DAF231D827BB702500AB916C /* valid_xmlns.svg in Resources */ = {isa = PBXBuildFile; fileRef = DAF231D527BB702400AB916C /* valid_xmlns.svg */; };
DAF231D927BB702500AB916C /* invalid_xmlns.svg in Resources */ = {isa = PBXBuildFile; fileRef = DAF231D627BB702500AB916C /* invalid_xmlns.svg */; };
DAF231DB27BB828000AB916C /* pantryUseTagPoints2NonExistentElement.svg in Resources */ = {isa = PBXBuildFile; fileRef = DAF231DA27BB828000AB916C /* pantryUseTagPoints2NonExistentElement.svg */; };
DAF231E327BBD1A000AB916C /* embeddedpng_valid.svg in Resources */ = {isa = PBXBuildFile; fileRef = DAF231E227BBD1A000AB916C /* embeddedpng_valid.svg */; };
- DAF4578223D630C70018B495 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF4578123D630C70018B495 /* IconView.swift */; };
- DAF4578523D7807A0018B495 /* Color+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 934B610B2348D2F9009112D5 /* Color+Extension.swift */; };
- DAF4578723D798A50018B495 /* TextLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF4578623D798A50018B495 /* TextLabelView.swift */; };
+ DAF4578223D630C70018B495 /* WatchIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF4578123D630C70018B495 /* WatchIconView.swift */; };
DAF4578923D79AA50018B495 /* DetailTextLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF4578823D79AA50018B495 /* DetailTextLabelView.swift */; };
DAF457A023DA3E1C0018B495 /* SegmentRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF4579F23DA3E1C0018B495 /* SegmentRow.swift */; };
DAF457A223DB6C640018B495 /* RollershutterRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF457A123DB6C640018B495 /* RollershutterRow.swift */; };
@@ -172,14 +194,8 @@
DAF4581623DC48400018B495 /* GenericRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF4581523DC483F0018B495 /* GenericRow.swift */; };
DAF4581823DC4A050018B495 /* ImageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF4581723DC4A050018B495 /* ImageRow.swift */; };
DAF4581E23DC60020018B495 /* ImageRawRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF4581D23DC60020018B495 /* ImageRawRow.swift */; };
- DAF4F6C0222734D300C24876 /* NewImageUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF4F6BF222734D200C24876 /* NewImageUITableViewCell.swift */; };
- DF05FF231896BD2D00FF2F9B /* SelectionUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF05FF221896BD2D00FF2F9B /* SelectionUITableViewCell.swift */; };
- DF06F1FC18FEC2020011E7B9 /* ColorPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF06F1FB18FEC2020011E7B9 /* ColorPickerViewController.swift */; };
- DF4B84131886DAC400F34902 /* FrameUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF4B84121886DAC400F34902 /* FrameUITableViewCell.swift */; };
- DF4B84161886EACA00F34902 /* GenericUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF4B84151886EACA00F34902 /* GenericUITableViewCell.swift */; };
- DFA13CB418872EBD006355C3 /* SwitchUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA13CB318872EBD006355C3 /* SwitchUITableViewCell.swift */; };
- DFA16EBB18883DE500EDB0BB /* SliderUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA16EBA18883DE500EDB0BB /* SliderUITableViewCell.swift */; };
- DFA16EC118898A8400EDB0BB /* SegmentedUITableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFA16EC018898A8400EDB0BB /* SegmentedUITableViewCell.swift */; };
+ DAF5AA682E4F3A39004F18D7 /* EmbeddingRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAF5AA672E4F3A38004F18D7 /* EmbeddingRowView.swift */; };
+ DAFF80982E4F47830084513E /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = DAFF80972E4F47830084513E /* SDWebImage */; };
DFB2622B18830A3600D3244D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFB2622A18830A3600D3244D /* Foundation.framework */; };
DFB2622D18830A3600D3244D /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFB2622C18830A3600D3244D /* CoreGraphics.framework */; };
DFB2622F18830A3600D3244D /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFB2622E18830A3600D3244D /* UIKit.framework */; };
@@ -188,7 +204,6 @@
DFDA3CEA193CADB200888039 /* ping.wav in Resources */ = {isa = PBXBuildFile; fileRef = DFDA3CE9193CADB200888039 /* ping.wav */; };
DFDF45311932042B00A6E581 /* legal.rtf in Resources */ = {isa = PBXBuildFile; fileRef = DFDF45301932042B00A6E581 /* legal.rtf */; };
DFE10414197415F900D94943 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DFE10413197415F900D94943 /* Security.framework */; };
- DFFD8FD118EDBD4F003B502A /* UICircleButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFFD8FD018EDBD4F003B502A /* UICircleButton.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -227,6 +242,13 @@
remoteGlobalIDString = DFB2622618830A3600D3244D;
remoteInfo = openHAB;
};
+ DA8B15552F3BB74B007753FD /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = DFB2621F18830A3600D3244D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = DA0775142346705D0086C685;
+ remoteInfo = openHABWatch;
+ };
DAA0708C2B504B280060BB0E /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = DFB2621F18830A3600D3244D /* Project object */;
@@ -290,14 +312,25 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
+ AA0001012F5A000000000001 /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; };
+ AA0001032F5A000000000002 /* OpenHABApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABApp.swift; sourceTree = ""; };
1224F78D228A89FC00750965 /* WatchMessageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WatchMessageService.swift; sourceTree = ""; };
+ 13181EE034B26E3A1FF7F4A8 /* SitemapsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SitemapsTab.swift; sourceTree = ""; };
2F08AFC62E5FADC500E70611 /* NotificationCenterDelegateImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenterDelegateImpl.swift; sourceTree = ""; };
+ 2F54B56C2F40AC1A00DA1F1A /* REFACTORING_SUMMARY.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = REFACTORING_SUMMARY.md; sourceTree = ""; };
2F55E7BA2DEE447700EC8350 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; };
2F55E7BC2DEE44A800EC8350 /* ClientCertificatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientCertificatesView.swift; sourceTree = ""; };
- 2F6412ED2CE494A80039FB28 /* DatePickerUITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerUITableViewCell.swift; sourceTree = ""; };
+ 2F77DF3B2F43D3D700BE3744 /* OpenHABWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABWebView.swift; sourceTree = ""; };
+ 2F77DF3D2F43D42E00BE3744 /* WEBVIEW_MIGRATION.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = WEBVIEW_MIGRATION.md; sourceTree = ""; };
+ 2F77DF3F2F43D94C00BE3744 /* UIKIT_MIGRATION_ANALYSIS.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = UIKIT_MIGRATION_ANALYSIS.md; sourceTree = ""; };
+ 2F77DF412F43D96800BE3744 /* CertificateManagementService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateManagementService.swift; sourceTree = ""; };
+ 2F77DF432F43D97400BE3744 /* IdleTimerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdleTimerService.swift; sourceTree = ""; };
+ 2F77DF452F43D9A400BE3744 /* UIKIT_REMOVAL_GUIDE.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = UIKIT_REMOVAL_GUIDE.md; sourceTree = ""; };
2FBCF58B2DEB0B7700CD5D83 /* HomeSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeSelectionView.swift; sourceTree = ""; };
- 2FEFD8F52BE7C5BE00E387B9 /* TextInputUITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputUITableViewCell.swift; sourceTree = ""; };
2FF459352E230C6A00C0B640 /* OpenHABIntentHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABIntentHelper.swift; sourceTree = ""; };
+ 3698C230427F48A782B1980B /* TabCustomizationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabCustomizationSection.swift; sourceTree = ""; };
+ 399449C421544C61AD83450C /* TextRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRow.swift; sourceTree = ""; };
+ 4229903F00160C965E22DE21 /* SystemTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemTab.swift; sourceTree = ""; };
4D38D951256897490039DA6E /* SetNumberValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetNumberValueIntentHandler.swift; sourceTree = ""; };
4D38D959256897770039DA6E /* SetStringValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetStringValueIntentHandler.swift; sourceTree = ""; };
4D38D9612568978E0039DA6E /* SetColorValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetColorValueIntentHandler.swift; sourceTree = ""; };
@@ -309,19 +342,18 @@
4D64720D256315D9007B03FC /* openHABIntents.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = openHABIntents.entitlements; sourceTree = ""; };
4D647220256331B9007B03FC /* SetSwitchStateIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetSwitchStateIntentHandler.swift; sourceTree = ""; };
4D64724C256346BD007B03FC /* SetDimmerRollerValueIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDimmerRollerValueIntentHandler.swift; sourceTree = ""; };
+ 5135184D9630899A34EFBA95 /* MainWebTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWebTab.swift; sourceTree = ""; };
+ 52411B814C63C9F1CC8CED90 /* OpenHABTabRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABTabRootView.swift; sourceTree = ""; };
652B81032E2193B500648510 /* ScreenSaverSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverSettingsView.swift; sourceTree = ""; };
652B81052E2193DA00648510 /* ScreenSaverConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverConfiguration.swift; sourceTree = ""; };
652B81062E2193DA00648510 /* ScreenSaverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverManager.swift; sourceTree = ""; };
- 653B54C1285E714900298ECD /* OpenHABViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABViewController.swift; sourceTree = ""; };
653C09D41EAD691A00BA4C4A /* openHAB.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = openHAB.entitlements; sourceTree = ""; };
- 65570A7C2476D16A00D524EA /* OpenHABWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABWebViewController.swift; sourceTree = ""; };
6557AF8E2C0241C10094D0C8 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; };
656916D81FCB82BC00667B2A /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "openHAB/GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; };
6571444E2C1E438700C8A1F3 /* NotificationService.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NotificationService.appex; sourceTree = BUILT_PRODUCTS_DIR; };
657144502C1E438700C8A1F3 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; };
657144522C1E438700C8A1F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
657144972C30A3E300C8A1F3 /* NotificationService.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NotificationService.entitlements; sourceTree = ""; };
- 65C2EF482E244C8500A0C19F /* OpenHABNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABNavigationController.swift; sourceTree = ""; };
65F055432E3D4E41004E98FE /* ItemSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemSelectionView.swift; sourceTree = ""; };
905A534AB32C4104AC55A75C /* VideoStreamManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoStreamManager.swift; sourceTree = ""; };
931384B324F259BC00A73AB5 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; };
@@ -338,7 +370,6 @@
933D7F0622E7015000621A03 /* OpenHABUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABUITests.swift; sourceTree = ""; };
933D7F0822E7015100621A03 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
933D7F0E22E7030600621A03 /* SnapshotHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SnapshotHelper.swift; path = fastlane/SnapshotHelper.swift; sourceTree = SOURCE_ROOT; };
- 934B610B2348D2F9009112D5 /* Color+Extension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Color+Extension.swift"; sourceTree = ""; };
935B484525342B8E00E44CF0 /* URL+Static.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Static.swift"; sourceTree = ""; };
935D3412257B7E2F0020A404 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = Resources/nl.lproj/Intents.strings; sourceTree = ""; };
935D3419257B7E820020A404 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.intentdefinition; name = Base; path = Resources/Base.lproj/Intents.intentdefinition; sourceTree = ""; };
@@ -351,24 +382,21 @@
935D3451257B7EA60020A404 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = Resources/es.lproj/Intents.strings; sourceTree = ""; };
93685A792ADE755C0077A9A6 /* openHABTests.xctestplan */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = openHABTests.xctestplan; sourceTree = ""; };
938BF89524EFBC5400E6B52F /* LocalizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationTests.swift; sourceTree = ""; };
- 938BF9C324EFCB9F00E6B52F /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Main.storyboard; sourceTree = ""; };
- 938BF9C524EFCC0700E6B52F /* UILabel+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+Localization.swift"; sourceTree = ""; };
938BF9C924EFCCC000E6B52F /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; };
938BF9CB24EFCCC000E6B52F /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; };
938BF9CC24EFCCC000E6B52F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; };
938BF9CD24EFCCC000E6B52F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
938BF9CE24EFCCC000E6B52F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; };
938BF9CF24EFCCC000E6B52F /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; };
- 938BF9D224EFD0B700E6B52F /* UIViewController+Localization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Localization.swift"; sourceTree = ""; };
- 938EDCE022C4FEB800661CA1 /* ScaleAspectFitImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScaleAspectFitImageView.swift; sourceTree = ""; };
+ A3C3E3BF4C930ADA95E6E997 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; };
A3F4C3A41A49A5940019A09F /* MainLaunchScreen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; name = MainLaunchScreen.xib; path = ../MainLaunchScreen.xib; sourceTree = ""; };
- B7D5ECE021499E55001B0EC6 /* MapViewTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewTableViewCell.swift; sourceTree = ""; };
+ D64BEA2793F07C36BB98D9B8 /* AppServicesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppServicesViewModel.swift; sourceTree = ""; };
+ D86D8BF295C448039B2B85EB /* SelectionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionRow.swift; sourceTree = ""; };
DA0749DD23E0B5950057FA83 /* ColorPickerRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerRow.swift; sourceTree = ""; };
DA0749DF23E0BF510057FA83 /* ColorSelection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorSelection.swift; sourceTree = ""; };
DA0775152346705D0086C685 /* openHABWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = openHABWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
DA07751A2346705F0086C685 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
DA07751C2346705F0086C685 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
- DA0775262346705F0086C685 /* SitemapPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SitemapPageView.swift; sourceTree = ""; };
DA07752A2346705F0086C685 /* OpenHABWatchAppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABWatchAppDelegate.swift; sourceTree = ""; };
DA07752C2346705F0086C685 /* NotificationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationController.swift; sourceTree = ""; };
DA07752E2346705F0086C685 /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = ""; };
@@ -380,7 +408,6 @@
DA0DA9E12E0C9B74000C5D0A /* BuildTools */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = BuildTools; sourceTree = ""; };
DA0F37CF23D4ACC7007EAB48 /* SliderRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderRow.swift; sourceTree = ""; };
DA15BFBC23C6726400BD8ADA /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; };
- DA162BEB2CD3B53E0040DAE5 /* LogsViewer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogsViewer.swift; sourceTree = ""; };
DA19E25A22FD801D002F8F2F /* OpenHABGeneralTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABGeneralTests.swift; sourceTree = ""; };
DA1C2E4B230DC28F00FACFB0 /* Appfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Appfile; sourceTree = ""; };
DA1C2E4C230DC28F00FACFB0 /* SnapshotHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotHelper.swift; sourceTree = ""; };
@@ -416,10 +443,12 @@
DA1C2E6D230DC28F00FACFB0 /* primary_category.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = primary_category.txt; sourceTree = ""; };
DA1C2E6E230DC28F00FACFB0 /* Snapfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Snapfile; sourceTree = ""; };
DA21EAE12339621C001AB415 /* Throttler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Throttler.swift; sourceTree = ""; };
+ DA2740FF2EA62F1F002FE576 /* SitemapPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SitemapPageView.swift; sourceTree = ""; };
+ DA2741012EA62FA3002FE576 /* SegmentSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentSelectionView.swift; sourceTree = ""; };
DA28C361225241DE00AB409C /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; };
DA2AEB6D2D92BAD800897D80 /* PageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageLoader.swift; sourceTree = ""; };
- DA2AEB6F2D92CF3E00897D80 /* UITableViewCellExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableViewCellExtension.swift; sourceTree = ""; };
DA2AEB9F2D92FB6500897D80 /* NoIconDisplayableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoIconDisplayableCell.swift; sourceTree = ""; };
+ DA2D2F892F3943A800EC605A /* WidgetRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetRowViewModel.swift; sourceTree = ""; };
DA2DC22F21F2736C00830730 /* openHABTestsSwift.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = openHABTestsSwift.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DA2DC23321F2736C00830730 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
DA2E0AA323DC96E9009B0A99 /* ImageWithAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageWithAction.swift; sourceTree = ""; };
@@ -427,6 +456,19 @@
DA2E0B0F23DCC439009B0A99 /* MapViewRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewRow.swift; sourceTree = ""; };
DA32D1B32C8C98C40018D974 /* IconWithAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconWithAction.swift; sourceTree = ""; };
DA3563D82E5096BE00BC0138 /* ScreenSaverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverView.swift; sourceTree = ""; };
+ DA35E2AF2E1EDB86003987BB /* SetpointRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetpointRowView.swift; sourceTree = ""; };
+ DA35E2B22E1EEA9D003987BB /* ColorPickerRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerRowView.swift; sourceTree = ""; };
+ DA35E2B32E1EEA9D003987BB /* DatePickerInputRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickerInputRowView.swift; sourceTree = ""; };
+ DA35E2B42E1EEA9D003987BB /* ImageRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRowView.swift; sourceTree = ""; };
+ DA35E2B52E1EEA9D003987BB /* MapRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapRowView.swift; sourceTree = ""; };
+ DA35E2B62E1EEA9D003987BB /* RollershutterRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RollershutterRowView.swift; sourceTree = ""; };
+ DA35E2B72E1EEA9D003987BB /* SegmentedRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentedRowView.swift; sourceTree = ""; };
+ DA35E2B82E1EEA9D003987BB /* SelectionRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionRowView.swift; sourceTree = ""; };
+ DA35E2BA2E1EEA9D003987BB /* TextInputRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextInputRowView.swift; sourceTree = ""; };
+ DA35E2BB2E1EEA9D003987BB /* VideoRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoRowView.swift; sourceTree = ""; };
+ DA35E2BC2E1EEA9D003987BB /* WebRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebRowView.swift; sourceTree = ""; };
+ DA35E2CA2E1F93AD003987BB /* ImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageView.swift; sourceTree = ""; };
+ DA35E2CC2E1F96CA003987BB /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; };
DA4800132D836892009CF127 /* ConnectionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionSettingsView.swift; sourceTree = ""; };
DA4800152D836EF0009CF127 /* MainUISettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainUISettingsView.swift; sourceTree = ""; };
DA4800172D837221009CF127 /* AboutSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutSettingsView.swift; sourceTree = ""; };
@@ -436,43 +478,37 @@
DA4800202D839D39009CF127 /* AnimatedSecureTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedSecureTextField.swift; sourceTree = ""; };
DA4D4DB4233F9ACB00B37E37 /* README.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
DA4D4E0E2340A00200B37E37 /* Changes.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = Changes.md; sourceTree = ""; };
- DA50C7BC2B0A51BD0009F716 /* SliderWithSwitchSupportRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderWithSwitchSupportRow.swift; sourceTree = ""; };
- DA50C7BE2B0A652F0009F716 /* SliderWithSwitchSupportUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderWithSwitchSupportUITableViewCell.swift; sourceTree = ""; };
DA5ED9BD2C850955004875E0 /* ClientCertificatesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientCertificatesViewModel.swift; sourceTree = ""; };
+ DA64ACA52DBEAD5600294F60 /* SitemapPageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SitemapPageViewModel.swift; sourceTree = ""; };
+ DA64ACA72DBEAD8300294F60 /* SitemapPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SitemapPageView.swift; sourceTree = ""; };
+ DA64ACA92DBEAD9000294F60 /* SitemapNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SitemapNavigationView.swift; sourceTree = ""; };
DA65871E236F83CD007E2E7F /* UserDefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsExtension.swift; sourceTree = ""; };
- DA6B2EEE2C861BC900DF77CF /* DrawerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DrawerView.swift; sourceTree = ""; };
DA6B2EF02C87B59000DF77CF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; };
DA6B2EF42C89F8F200DF77CF /* ColorPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerView.swift; sourceTree = ""; };
DA6B2EF62C8B92E800DF77CF /* SelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionView.swift; sourceTree = ""; };
- DA7125362EC892BB0067D7B2 /* LoggerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerView.swift; sourceTree = ""; };
- DA7224D123828D3300712D20 /* PreviewConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewConstants.swift; sourceTree = ""; };
DA72E1B5236DEA0900B8EF3A /* AppMessageService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppMessageService.swift; sourceTree = ""; };
DA77E19A2D886D9B007CFF0F /* SingleConnectionSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleConnectionSettingsView.swift; sourceTree = ""; };
- DA7E1E47222EB00B002AEFD8 /* PlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = ""; };
DA7F002C2EB376CF00DE943A /* ServerCertificatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerCertificatesView.swift; sourceTree = ""; };
DA817E79234BF39B00C91824 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; };
DA88F8C522EC377100B408E5 /* ReleaseNotes.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = ReleaseNotes.md; sourceTree = ""; };
+ DA8B14B52F3A0DFF007753FD /* WidgetRowFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetRowFactory.swift; sourceTree = ""; };
+ DA8B14B72F3A11F0007753FD /* PreviewWidgetFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewWidgetFactory.swift; sourceTree = ""; };
+ DA8B14B92F3A373A007753FD /* PreviewNavigationContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewNavigationContainer.swift; sourceTree = ""; };
+ DA8B14BB2F3A3CB5007753FD /* WatchTypography.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchTypography.swift; sourceTree = ""; };
+ DA8B15512F3BB74B007753FD /* openHABWatchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = openHABWatchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
DA94AEB32EC896BE003BB3C8 /* SimpleMJPEGPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleMJPEGPlayer.swift; sourceTree = ""; };
- DA94AF432EC8DE41003BB3C8 /* VideoStreamManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoStreamManager.swift; sourceTree = ""; };
- DA95F3322E0F2B1700FE4474 /* OpenHABRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABRootViewController.swift; sourceTree = ""; };
- DA95F3342E0F2C1600FE4474 /* OpenHABSitemapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABSitemapViewController.swift; sourceTree = ""; };
DA9641592F292EE200CEC181 /* BonjourDiscoveryViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BonjourDiscoveryViewModelTests.swift; sourceTree = ""; };
DA96415B2F292F0600CEC181 /* OpenHABEndPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABEndPoint.swift; sourceTree = ""; };
DA9721C224E29A8F0092CCFD /* UserDefaultsBacked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsBacked.swift; sourceTree = ""; };
DA9F81862C85020F00B47B72 /* RTFTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RTFTextView.swift; sourceTree = ""; };
- DAA42BA721DC97DF00244B2A /* NotificationTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationTableViewCell.swift; sourceTree = ""; };
- DAA42BA921DC983B00244B2A /* VideoUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoUITableViewCell.swift; sourceTree = ""; };
- DAA42BAB21DC984A00244B2A /* WebUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebUITableViewCell.swift; sourceTree = ""; };
DAA599B72EAC0FE7003A8726 /* AppIcon.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = AppIcon.icon; sourceTree = ""; };
- DAAAB2822EA3874400F1B05D /* SegmentSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SegmentSelectionView.swift; sourceTree = ""; };
DABED17A2E451694000B92EF /* BonjourDiscoverySheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BonjourDiscoverySheet.swift; sourceTree = ""; };
DABED17C2E4516B4000B92EF /* BonjourDiscoveryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BonjourDiscoveryViewModel.swift; sourceTree = ""; };
DAC131102DA3213100075AE2 /* SetSwitchStateIntentHandlerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetSwitchStateIntentHandlerTests.swift; sourceTree = ""; };
- DAC65FC6236EDF3900F4501E /* SpinnerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerViewController.swift; sourceTree = ""; };
DAC6608B236F6F4200F4501E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
DAC6608C236F771600F4501E /* PreferencesSwiftUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesSwiftUIView.swift; sourceTree = ""; };
- DAC9394322AD4A7A00C5F423 /* OpenHABWatchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABWatchTests.swift; sourceTree = ""; };
DAC9395422B00E7600C5F423 /* XCTestCaseExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestCaseExtension.swift; sourceTree = ""; };
+ DAC949FD2E21A2D1007E67B7 /* FrameRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameRowView.swift; sourceTree = ""; };
DAC9AF4624F9669F006DAE93 /* OpenHABWidgetExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABWidgetExtension.swift; sourceTree = ""; };
DAC9AF4824F966FA006DAE93 /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; };
DACB636127D3FC6500041931 /* error.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = error.png; sourceTree = ""; };
@@ -480,25 +516,26 @@
DAD0855F2AE47824001D36BE /* OpenHABWatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABWatch.swift; sourceTree = ""; };
DAD085662AE4782A001D36BE /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
DAD0856C2AE4782B001D36BE /* openHABWatchSwiftUI Watch AppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "openHABWatchSwiftUI Watch AppTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
- DAD085702AE4782D001D36BE /* OpenHABWatchAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABWatchAppTests.swift; sourceTree = ""; };
DAD085762AE4782E001D36BE /* openHABWatchSwiftUI Watch AppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "openHABWatchSwiftUI Watch AppUITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
DAD0857A2AE4782F001D36BE /* OpenHABWatchUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABWatchUITests.swift; sourceTree = ""; };
DAD0857C2AE4782F001D36BE /* OpenHABWatchLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABWatchLaunchTests.swift; sourceTree = ""; };
DAD488B3287DDDFE00414693 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = Resources/nb.lproj/Intents.strings; sourceTree = ""; };
DAD488B4287DDDFF00414693 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Localizable.strings; sourceTree = ""; };
DAD488B5287DDDFF00414693 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/InfoPlist.strings; sourceTree = ""; };
- DAEAA89C21E6B06300267EA3 /* ReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReusableView.swift; sourceTree = ""; };
- DAEAA89E21E6B16600267EA3 /* UITableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UITableView.swift; sourceTree = ""; };
- DAF0A28A2C56E3A300A14A6A /* RollershutterCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RollershutterCell.swift; sourceTree = ""; };
- DAF0A28C2C56EF8900A14A6A /* SetpointCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetpointCell.swift; sourceTree = ""; };
- DAF0A28E2C56F1EE00A14A6A /* ColorPickerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPickerCell.swift; sourceTree = ""; };
+ DAE280092E35F5590028EE24 /* IconURLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconURLView.swift; sourceTree = ""; };
+ DAE7B4A62E26927C00B9FE99 /* ButtonGridRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonGridRowView.swift; sourceTree = ""; };
+ DAEA21D72DBF472D00D54342 /* RowViewFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowViewFactory.swift; sourceTree = ""; };
+ DAEA21D92DBF477E00D54342 /* SwitchRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchRowView.swift; sourceTree = ""; };
+ DAEA21DB2DBF47DA00D54342 /* SliderRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SliderRowView.swift; sourceTree = ""; };
+ DAEA21DD2DBF481300D54342 /* TextRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextRowView.swift; sourceTree = ""; };
+ DAEA21DF2DBF483E00D54342 /* GenericRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericRowView.swift; sourceTree = ""; };
+ DAEE35062E224F6000B4A7F1 /* ColorTemperaturePickerRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorTemperaturePickerRowView.swift; sourceTree = ""; };
DAF231D127BB6EEA00AB916C /* OpenHABSVGTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenHABSVGTests.swift; sourceTree = ""; };
DAF231D527BB702400AB916C /* valid_xmlns.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = valid_xmlns.svg; sourceTree = ""; };
DAF231D627BB702500AB916C /* invalid_xmlns.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = invalid_xmlns.svg; sourceTree = ""; };
DAF231DA27BB828000AB916C /* pantryUseTagPoints2NonExistentElement.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = pantryUseTagPoints2NonExistentElement.svg; sourceTree = ""; };
DAF231E227BBD1A000AB916C /* embeddedpng_valid.svg */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = embeddedpng_valid.svg; sourceTree = ""; };
- DAF4578123D630C70018B495 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; };
- DAF4578623D798A50018B495 /* TextLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextLabelView.swift; sourceTree = ""; };
+ DAF4578123D630C70018B495 /* WatchIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchIconView.swift; sourceTree = ""; };
DAF4578823D79AA50018B495 /* DetailTextLabelView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailTextLabelView.swift; sourceTree = ""; };
DAF4579F23DA3E1C0018B495 /* SegmentRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentRow.swift; sourceTree = ""; };
DAF457A123DB6C640018B495 /* RollershutterRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RollershutterRow.swift; sourceTree = ""; };
@@ -507,16 +544,9 @@
DAF4581523DC483F0018B495 /* GenericRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericRow.swift; sourceTree = ""; };
DAF4581723DC4A050018B495 /* ImageRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRow.swift; sourceTree = ""; };
DAF4581D23DC60020018B495 /* ImageRawRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageRawRow.swift; sourceTree = ""; };
- DAF4F6BF222734D200C24876 /* NewImageUITableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewImageUITableViewCell.swift; sourceTree = ""; };
+ DAF5AA672E4F3A38004F18D7 /* EmbeddingRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmbeddingRowView.swift; sourceTree = ""; };
DAF6F4112C67E83B0083883E /* openapiCorrected.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = openapiCorrected.json; sourceTree = ""; };
DAFD2FE62E0D96700059A1EB /* OsLogRewriter */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = OsLogRewriter; sourceTree = ""; };
- DF05FF221896BD2D00FF2F9B /* SelectionUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionUITableViewCell.swift; sourceTree = ""; };
- DF06F1FB18FEC2020011E7B9 /* ColorPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorPickerViewController.swift; sourceTree = ""; };
- DF4B84121886DAC400F34902 /* FrameUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FrameUITableViewCell.swift; sourceTree = ""; };
- DF4B84151886EACA00F34902 /* GenericUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenericUITableViewCell.swift; sourceTree = ""; };
- DFA13CB318872EBD006355C3 /* SwitchUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwitchUITableViewCell.swift; sourceTree = ""; };
- DFA16EBA18883DE500EDB0BB /* SliderUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SliderUITableViewCell.swift; sourceTree = ""; };
- DFA16EC018898A8400EDB0BB /* SegmentedUITableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SegmentedUITableViewCell.swift; sourceTree = ""; };
DFB2622718830A3600D3244D /* openHAB.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = openHAB.app; sourceTree = BUILT_PRODUCTS_DIR; };
DFB2622A18830A3600D3244D /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; };
DFB2622C18830A3600D3244D /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; };
@@ -528,21 +558,11 @@
DFDA3CE9193CADB200888039 /* ping.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ping.wav; sourceTree = ""; };
DFDF45301932042B00A6E581 /* legal.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = legal.rtf; sourceTree = ""; };
DFE10413197415F900D94943 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
- DFFD8FD018EDBD4F003B502A /* UICircleButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UICircleButton.swift; sourceTree = ""; };
+ EE239F0348D19000408BA8AA /* TilesTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TilesTab.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
- DA2AEB752D92D32000897D80 /* Cells */ = {
- isa = PBXFileSystemSynchronizedRootGroup;
- exceptions = (
- );
- explicitFileTypes = {
- };
- explicitFolders = (
- );
- path = Cells;
- sourceTree = "";
- };
+ DA8B15522F3BB74B007753FD /* openHABWatchTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = openHABWatchTests; sourceTree = ""; };
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
@@ -550,6 +570,8 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
+ DAC949FC2E219F30007E67B7 /* CommonUI in Frameworks */,
+ 934E592728F16EBA00162004 /* Kingfisher in Frameworks */,
937E4473270B36DD00A98C26 /* OpenHABCore in Frameworks */,
DA2C4FD52B4F573300D1C533 /* SDWebImageSVGCoder in Frameworks */,
934E592528F16EBA00162004 /* OpenHABCore in Frameworks */,
@@ -565,6 +587,7 @@
buildActionMask = 2147483647;
files = (
DA7ACD5F2DC3DB130055CFC7 /* SFSafeSymbols in Frameworks */,
+ 937E4492270B37FE00A98C26 /* Kingfisher in Frameworks */,
937E44E2270B393C00A98C26 /* OpenHABCore in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -592,6 +615,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ DA8B154E2F3BB74B007753FD /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
DAD085692AE4782A001D36BE /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -614,17 +644,20 @@
DFB2622B18830A3600D3244D /* Foundation.framework in Frameworks */,
937E4485270B379900A98C26 /* DeviceKit in Frameworks */,
DABB5E332D98972F009A4B8A /* SDWebImageSVGCoder in Frameworks */,
- DAAAB2812EA3843100F1B05D /* Kingfisher in Frameworks */,
DFB2622F18830A3600D3244D /* UIKit.framework in Frameworks */,
- DADC420A2E7AB899004E866F /* SDWebImage in Frameworks */,
+ DAFF80982E4F47830084513E /* SDWebImage in Frameworks */,
+ DADC420A2E7AB899004E866F /* (null) in Frameworks */,
93F8064A27AE7A2E0035A6B0 /* FlexColorPicker in Frameworks */,
DA28C362225241DE00AB409C /* WebKit.framework in Frameworks */,
- 93F8065027AE7A830035A6B0 /* SideMenu in Frameworks */,
+ DAC949FA2E219F0D007E67B7 /* CommonUI in Frameworks */,
DFE10414197415F900D94943 /* Security.framework in Frameworks */,
+ 2F54B5742F428ACA00DA1F1A /* AsyncAlgorithms in Frameworks */,
+ 2F54B5772F428B0200DA1F1A /* Algorithms in Frameworks */,
93F8064727AE7A050035A6B0 /* SwiftMessages in Frameworks */,
DFB2622D18830A3600D3244D /* CoreGraphics.framework in Frameworks */,
93F8063527AE6C620035A6B0 /* FirebaseCrashlytics in Frameworks */,
DA9A7EFF2D66915900824156 /* SFSafeSymbols in Frameworks */,
+ 937E4488270B37A600A98C26 /* Kingfisher in Frameworks */,
937E4471270B36D000A98C26 /* OpenHABCore in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -657,6 +690,39 @@
path = openHABWatch/External;
sourceTree = SOURCE_ROOT;
};
+ 2F7D1D592EFEC12A004A786D /* RootView */ = {
+ isa = PBXGroup;
+ children = (
+ D64BEA2793F07C36BB98D9B8 /* AppServicesViewModel.swift */,
+ 52411B814C63C9F1CC8CED90 /* OpenHABTabRootView.swift */,
+ 5135184D9630899A34EFBA95 /* MainWebTab.swift */,
+ 13181EE034B26E3A1FF7F4A8 /* SitemapsTab.swift */,
+ EE239F0348D19000408BA8AA /* TilesTab.swift */,
+ 4229903F00160C965E22DE21 /* SystemTab.swift */,
+ A3C3E3BF4C930ADA95E6E997 /* SafariView.swift */,
+ 2F54B56C2F40AC1A00DA1F1A /* REFACTORING_SUMMARY.md */,
+ );
+ path = RootView;
+ sourceTree = "";
+ };
+ 2F7D1D5A2EFEC137004A786D /* SitemapView */ = {
+ isa = PBXGroup;
+ children = (
+ DA9F81862C85020F00B47B72 /* RTFTextView.swift */,
+ DA6B2EF62C8B92E800DF77CF /* SelectionView.swift */,
+ DAF5AA672E4F3A38004F18D7 /* EmbeddingRowView.swift */,
+ DAE280092E35F5590028EE24 /* IconURLView.swift */,
+ DA35E2CC2E1F96CA003987BB /* IconView.swift */,
+ DA35E2CA2E1F93AD003987BB /* ImageView.swift */,
+ DAEA21D72DBF472D00D54342 /* RowViewFactory.swift */,
+ DA64ACA72DBEAD8300294F60 /* SitemapPageView.swift */,
+ DA64ACA92DBEAD9000294F60 /* SitemapNavigationView.swift */,
+ 2F77DF3B2F43D3D700BE3744 /* OpenHABWebView.swift */,
+ 2F77DF3D2F43D42E00BE3744 /* WEBVIEW_MIGRATION.md */,
+ );
+ path = SitemapView;
+ sourceTree = "";
+ };
4D6470D42561F935007B03FC /* openHABIntents */ = {
isa = PBXGroup;
children = (
@@ -729,6 +795,8 @@
DA9721C224E29A8F0092CCFD /* UserDefaultsBacked.swift */,
DAC9AF4624F9669F006DAE93 /* OpenHABWidgetExtension.swift */,
DAC9AF4824F966FA006DAE93 /* LazyView.swift */,
+ DA8B14B52F3A0DFF007753FD /* WidgetRowFactory.swift */,
+ DA2D2F892F3943A800EC605A /* WidgetRowViewModel.swift */,
);
path = Model;
sourceTree = "";
@@ -836,7 +904,6 @@
938BF89524EFBC5400E6B52F /* LocalizationTests.swift */,
DA9641592F292EE200CEC181 /* BonjourDiscoveryViewModelTests.swift */,
DAC9395422B00E7600C5F423 /* XCTestCaseExtension.swift */,
- DAC9394322AD4A7A00C5F423 /* OpenHABWatchTests.swift */,
DAF231D127BB6EEA00AB916C /* OpenHABSVGTests.swift */,
DA2DC23321F2736C00830730 /* Info.plist */,
DA96415B2F292F0600CEC181 /* OpenHABEndPoint.swift */,
@@ -844,6 +911,22 @@
path = openHABTestsSwift;
sourceTree = "";
};
+ DA35E2B12E1EEA58003987BB /* SwiftUI */ = {
+ isa = PBXGroup;
+ children = (
+ 2FBCF58B2DEB0B7700CD5D83 /* HomeSelectionView.swift */,
+ DA6B2EF02C87B59000DF77CF /* NotificationsView.swift */,
+ AA0001012F5A000000000001 /* SplashView.swift */,
+ 2F7D1D592EFEC12A004A786D /* RootView */,
+ 2F7D1D5A2EFEC137004A786D /* SitemapView */,
+ DAC949FF2E21A473007E67B7 /* Rows */,
+ DA48001F2D837CD8009CF127 /* SettingsView */,
+ 652B81082E2193DA00648510 /* ScreenSaver */,
+ DF4B84101886DA9900F34902 /* Widgets */,
+ );
+ path = SwiftUI;
+ sourceTree = "";
+ };
DA48001F2D837CD8009CF127 /* SettingsView */ = {
isa = PBXGroup;
children = (
@@ -852,6 +935,7 @@
652B81032E2193B500648510 /* ScreenSaverSettingsView.swift */,
DA4800172D837221009CF127 /* AboutSettingsView.swift */,
DA48001D2D837905009CF127 /* ApplicationSettingsView.swift */,
+ 3698C230427F48A782B1980B /* TabCustomizationSection.swift */,
2F55E7BC2DEE44A800EC8350 /* ClientCertificatesView.swift */,
DA4800132D836892009CF127 /* ConnectionSettingsView.swift */,
DA77E19A2D886D9B007CFF0F /* SingleConnectionSettingsView.swift */,
@@ -868,8 +952,7 @@
DA658720236F841F007E2E7F /* Views */ = {
isa = PBXGroup;
children = (
- DA0775262346705F0086C685 /* SitemapPageView.swift */,
- DA162BEB2CD3B53E0040DAE5 /* LogsViewer.swift */,
+ DA2740FF2EA62F1F002FE576 /* SitemapPageView.swift */,
DAC6608C236F771600F4501E /* PreferencesSwiftUIView.swift */,
DAF457A323DB7A820018B495 /* Rows */,
DAF457A723DBA2C40018B495 /* Utils */,
@@ -885,21 +968,38 @@
path = openHABIntentsTests;
sourceTree = "";
};
- DACE66522C63B2070069E514 /* openapitest */ = {
+ DAC949FF2E21A473007E67B7 /* Rows */ = {
isa = PBXGroup;
children = (
- DAF6F4112C67E83B0083883E /* openapiCorrected.json */,
+ DAE7B4A62E26927C00B9FE99 /* ButtonGridRowView.swift */,
+ DA35E2B22E1EEA9D003987BB /* ColorPickerRowView.swift */,
+ DAEE35062E224F6000B4A7F1 /* ColorTemperaturePickerRowView.swift */,
+ DA35E2B32E1EEA9D003987BB /* DatePickerInputRowView.swift */,
+ DAC949FD2E21A2D1007E67B7 /* FrameRowView.swift */,
+ DAEA21DF2DBF483E00D54342 /* GenericRowView.swift */,
+ DA35E2B42E1EEA9D003987BB /* ImageRowView.swift */,
+ DA35E2B52E1EEA9D003987BB /* MapRowView.swift */,
+ DA35E2B62E1EEA9D003987BB /* RollershutterRowView.swift */,
+ DA35E2B72E1EEA9D003987BB /* SegmentedRowView.swift */,
+ DA35E2B82E1EEA9D003987BB /* SelectionRowView.swift */,
+ DA35E2AF2E1EDB86003987BB /* SetpointRowView.swift */,
+ DAEA21DB2DBF47DA00D54342 /* SliderRowView.swift */,
+ DAEA21D92DBF477E00D54342 /* SwitchRowView.swift */,
+ DA35E2BA2E1EEA9D003987BB /* TextInputRowView.swift */,
+ DAEA21DD2DBF481300D54342 /* TextRowView.swift */,
+ DA35E2BB2E1EEA9D003987BB /* VideoRowView.swift */,
+ DA35E2BC2E1EEA9D003987BB /* WebRowView.swift */,
);
- name = openapitest;
- path = openapi/openapitest;
+ path = Rows;
sourceTree = "";
};
- DAD0856F2AE4782D001D36BE /* openHABWatchSwiftUI Watch AppTests */ = {
+ DACE66522C63B2070069E514 /* openapitest */ = {
isa = PBXGroup;
children = (
- DAD085702AE4782D001D36BE /* OpenHABWatchAppTests.swift */,
+ DAF6F4112C67E83B0083883E /* openapiCorrected.json */,
);
- path = "openHABWatchSwiftUI Watch AppTests";
+ name = openapitest;
+ path = openapi/openapitest;
sourceTree = "";
};
DAD085792AE4782F001D36BE /* openHABWatchSwiftUI Watch AppUITests */ = {
@@ -915,6 +1015,8 @@
isa = PBXGroup;
children = (
DAD085662AE4782A001D36BE /* Preview Assets.xcassets */,
+ DA8B14B92F3A373A007753FD /* PreviewNavigationContainer.swift */,
+ DA8B14B72F3A11F0007753FD /* PreviewWidgetFactory.swift */,
);
path = "Preview Content";
sourceTree = "