Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 61 additions & 28 deletions openHAB/Models/SitemapPageViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -267,32 +267,48 @@ class SitemapPageViewModel: ObservableObject {

func rebuildRowInputs() {
let pageKey = "\(defaultSitemap)|\(pageId)"
let widgets = relevantWidgets
let inputs = SitemapRowInputMapper.map(pageKey: pageKey, widgets: widgets)
rowWidgetIndex = buildWidgetIndex(pageKey: pageKey, widgets: widgets)
if inputs != rowInputs {
rowInputs = inputs
}
}

private func buildWidgetIndex(pageKey: String, widgets: [OpenHABWidget]) -> [RowID: OpenHABWidget] {
var occurrenceByWidgetID: [String: Int] = [:]
var inputs: [SitemapRowInput] = []
var index: [RowID: OpenHABWidget] = [:]
inputs.reserveCapacity(relevantWidgets.count)
index.reserveCapacity(relevantWidgets.count)

for widget in relevantWidgets {
index.reserveCapacity(widgets.count)
for widget in widgets {
let identityWidgetID = SitemapRowInputMapper.rowIdentityWidgetID(for: widget)
occurrenceByWidgetID[identityWidgetID, default: 0] += 1
let occurrence = occurrenceByWidgetID[identityWidgetID]!
let rowID = RowID(pageKey: pageKey, widgetId: identityWidgetID, occurrence: occurrence)
let input = SitemapRowInputMapper.map(widget: widget, rowID: rowID)
inputs.append(input)
index[rowID] = widget
}
return index
}

rowWidgetIndex = index
rowInputs = inputs
/// Increments `widgetUpdateVersions` for each row whose content differs between `oldInputs`
/// and `newInputs`, keyed by full row identity.
func bumpWidgetVersions(from oldInputs: [SitemapRowInput], to newInputs: [SitemapRowInput]) {
if newInputs.count == oldInputs.count {
for (new, old) in zip(newInputs, oldInputs) where new != old {
widgetUpdateVersions[new.rowID.rawValue, default: 0] += 1
}
} else {
for input in newInputs {
widgetUpdateVersions[input.rowID.rawValue, default: 0] += 1
}
}
}

func widget(for rowID: RowID) -> OpenHABWidget? {
rowWidgetIndex[rowID]
}

func widgetUpdateVersion(for widgetId: String) -> Int {
widgetUpdateVersions[widgetId] ?? 0
func widgetUpdateVersion(for rowID: RowID) -> Int {
widgetUpdateVersions[rowID.rawValue] ?? 0
}

func sliderOverrideValue(for itemname: String?) -> Double? {
Expand Down Expand Up @@ -528,34 +544,51 @@ extension SitemapPageViewModel {
@MainActor
private func updateUI(with page: OpenHABPage, origin: PageUpdateOrigin) {
logger.debug("Incoming sitemap update origin=\(origin.rawValue, privacy: .public), widgets=\(page.widgets.count)")
let newWidgets = page.widgets

// Check if list structure changed (count, order, or IDs)
let pageKey = "\(defaultSitemap)|\(pageId)"

// Snapshot what the list would render from the new data — before any widget mutation.
let incomingFiltered: [OpenHABWidget]
if searchText.isEmpty {
incomingFiltered = page.widgets
} else {
incomingFiltered = page.widgets.filter {
$0.label.lowercased().contains(searchText.lowercased()) && $0.type != .frame
}
}
let previewInputs = SitemapRowInputMapper.map(pageKey: pageKey, widgets: incomingFiltered)

let titleChanged = currentPage == nil || currentPage?.title != page.title
let canSkipReconciliation = searchText.isEmpty && previewInputs == rowInputs && !titleChanged
guard !canSkipReconciliation else {
_ = clearSyncedSliderOverrides(using: page.widgets)
return
}

// Something changed — reconcile widget objects and update stored state.
let currentWidgets = currentPage?.widgets ?? []
let structureChanged = currentWidgets.count != newWidgets.count
|| !zip(currentWidgets, newWidgets).allSatisfy { $0.widgetId == $1.widgetId }
let reconciledWidgets = reconcileWidgets(newWidgets, with: currentWidgets)
let structureChanged = currentWidgets.count != page.widgets.count
|| !zip(currentWidgets, page.widgets).allSatisfy { $0.widgetId == $1.widgetId }
let reconciledWidgets = reconcileWidgets(page.widgets, with: currentWidgets)
injectSendCommand(for: reconciledWidgets)

// Only replace currentPage when structure or title changed
if structureChanged || currentPage?.title != page.title || currentPage == nil {
if structureChanged || titleChanged {
page.widgets = reconciledWidgets
injectSendCommand(for: reconciledWidgets)
currentPage = page
} else {
currentPage?.widgets = reconciledWidgets
// Inject sendCommand into existing widgets without replacing the page
injectSendCommand(for: reconciledWidgets)
}

trackWidgetUpdates(in: reconciledWidgets)
_ = clearSyncedSliderOverrides(using: reconciledWidgets)
rebuildRowInputs()
}

private func trackWidgetUpdates(in widgets: [OpenHABWidget]) {
for widget in widgets {
widgetUpdateVersions[widget.widgetId, default: 0] += 1
}
// Rebuild command-dispatch index from the now-current reconciled widgets.
rowWidgetIndex = buildWidgetIndex(pageKey: pageKey, widgets: relevantWidgets)

// Bump widget versions only for rows whose content actually changed.
bumpWidgetVersions(from: rowInputs, to: previewInputs)

// Publish new row inputs — guaranteed to differ from current (checked above).
rowInputs = previewInputs
}

private func clearSyncedSliderOverrides(using widgets: [OpenHABWidget]) -> Int {
Expand Down
2 changes: 1 addition & 1 deletion openHAB/UI/SwiftUI/Rows/ImageRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ private struct ImageRowContent: View {
}

private var chartWidgetVersion: Int {
viewModel.widgetUpdateVersion(for: input.widgetId)
viewModel.widgetUpdateVersion(for: input.rowID)
}

private var chartSyncToken: String {
Expand Down
24 changes: 15 additions & 9 deletions openHAB/UI/SwiftUI/Rows/SegmentedRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ struct SegmentedRowView: View {
var body: some View {
SegmentedRowContent(
input: input,
widgetVersion: viewModel.widgetUpdateVersion(for: input.widgetId),
widgetVersion: viewModel.widgetUpdateVersion(for: input.rowID),
fallbackSymbol: fallbackSymbol
) { command, policy, phase in
guard let itemName = input.itemName else { return }
Expand Down Expand Up @@ -377,13 +377,16 @@ private extension SegmentedRowView {
icon: String = "switch",
mappings: [OpenHABWidgetMapping],
selectedState: String? = nil) -> SegmentedRowInput {
SegmentedRowInput.from(widget: PreviewWidgetFactory.segmented(
label: label,
mappings: mappings,
selectedState: selectedState,
icon: icon,
valueText: detailLabel
))
SegmentedRowInput.from(
widget: PreviewWidgetFactory.segmented(
label: label,
mappings: mappings,
selectedState: selectedState,
icon: icon,
valueText: detailLabel
),
rowID: RowID(pageKey: "preview", widgetId: UUID().uuidString, occurrence: 1)
)
}
}

Expand Down Expand Up @@ -577,7 +580,10 @@ private extension SegmentedRowView {
#Preview("From PreviewConstants") {
PreviewList {
SegmentedRowView(
input: SegmentedRowInput.from(widget: PreviewConstants.openHABSitemapPage!.widgets[4])
input: SegmentedRowInput.from(
widget: PreviewConstants.openHABSitemapPage!.widgets[4],
rowID: RowID(pageKey: "preview", widgetId: UUID().uuidString, occurrence: 1)
)
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion openHAB/UI/SwiftUI/Rows/SelectionRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ struct SelectionRowView: View {
var body: some View {
SelectionRowContent(
input: input,
widgetVersion: viewModel.widgetUpdateVersion(for: input.widgetId)
widgetVersion: viewModel.widgetUpdateVersion(for: input.rowID)
) { command in
guard let itemName = input.itemName else { return }
viewModel.sendCommand(command, for: itemName)
Expand Down
9 changes: 6 additions & 3 deletions openHAB/UI/SwiftUI/Rows/SliderRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ struct SliderRowView: View {
makeSliderRowContent(
SliderRowConfig(
input: input,
widgetVersion: viewModel.widgetUpdateVersion(for: input.widgetId),
widgetVersion: viewModel.widgetUpdateVersion(for: input.rowID),
overrideValue: viewModel.sliderOverrideValue(for: input.itemName),
fallbackSymbol: fallbackSymbol,
viewModel: viewModel
Expand All @@ -245,7 +245,7 @@ private extension SliderRowView {
step: step,
icon: icon,
switchSupport: switchSupport
))
), rowID: RowID(pageKey: "preview", widgetId: UUID().uuidString, occurrence: 1))
}
}

Expand Down Expand Up @@ -335,7 +335,10 @@ private extension SliderRowView {
#Preview("From PreviewConstants") {
PreviewList {
SliderRowView(
input: SliderRowInput.from(widget: PreviewConstants.openHABSitemapPage!.widgets[3]),
input: SliderRowInput.from(
widget: PreviewConstants.openHABSitemapPage!.widgets[3],
rowID: RowID(pageKey: "preview", widgetId: UUID().uuidString, occurrence: 1)
),
fallbackSymbol: .sliderHorizontal3
)
}
Expand Down
8 changes: 4 additions & 4 deletions openHAB/UI/SwiftUI/SitemapRowInputMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,11 @@ enum SitemapRowInputMapper {

switch widget.renderingKind {
case .slider:
return SitemapRowInput.slider(rowID, SliderRowInput.from(widget: widget))
return SitemapRowInput.slider(rowID, SliderRowInput.from(widget: widget, rowID: rowID))
case .selection:
return SitemapRowInput.selection(rowID, SelectionRowInput.from(widget: widget))
return SitemapRowInput.selection(rowID, SelectionRowInput.from(widget: widget, rowID: rowID))
case .segmentedSwitch:
return SitemapRowInput.segmented(rowID, SegmentedRowInput.from(widget: widget))
return SitemapRowInput.segmented(rowID, SegmentedRowInput.from(widget: widget, rowID: rowID))
case .frame:
return SitemapRowInput.frame(rowID, FrameRowInput.from(widget: widget))
case .text:
Expand All @@ -57,7 +57,7 @@ enum SitemapRowInputMapper {
case .colorPicker:
return SitemapRowInput.colorPicker(rowID, ColorPickerRowInput.from(widget: widget))
case .image, .chart, .video, .webview, .mapview:
return SitemapRowInput.media(rowID, MediaRowInput.from(widget: widget))
return SitemapRowInput.media(rowID, MediaRowInput.from(widget: widget, rowID: rowID))
case .colorTemperaturePicker:
return SitemapRowInput.colorTemperature(rowID, ColorTemperatureRowInput.from(widget: widget))
case .buttonGrid:
Expand Down
16 changes: 12 additions & 4 deletions openHAB/UI/SwiftUI/WidgetRowInputs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ struct RowIconInput: Equatable, Sendable {
}

struct SelectionRowInput: Equatable, RowWithIconInput {
let rowID: RowID
let displayState: WidgetDisplayState
let mappings: [OpenHABWidgetMapping]
let labelColor: String
Expand All @@ -53,9 +54,10 @@ struct SelectionRowInput: Equatable, RowWithIconInput {
let icon: RowIconInput
let itemName: String?

static func from(widget: OpenHABWidget) -> SelectionRowInput {
static func from(widget: OpenHABWidget, rowID: RowID) -> SelectionRowInput {
let displayState = widget.displayState
return SelectionRowInput(
rowID: rowID,
displayState: displayState,
mappings: displayState.mappings,
labelColor: widget.labelcolor,
Expand All @@ -69,6 +71,7 @@ struct SelectionRowInput: Equatable, RowWithIconInput {
}

struct SegmentedRowInput: Equatable, RowWithIconInput {
let rowID: RowID
let displayState: WidgetDisplayState
let mappings: [OpenHABWidgetMapping]
let labelColor: String
Expand All @@ -77,9 +80,10 @@ struct SegmentedRowInput: Equatable, RowWithIconInput {
let icon: RowIconInput
let itemName: String?

static func from(widget: OpenHABWidget) -> SegmentedRowInput {
static func from(widget: OpenHABWidget, rowID: RowID) -> SegmentedRowInput {
let displayState = widget.displayState
return SegmentedRowInput(
rowID: rowID,
displayState: displayState,
mappings: displayState.mappings,
labelColor: widget.labelcolor,
Expand Down Expand Up @@ -408,6 +412,7 @@ struct ColorTemperatureRowInput: Equatable, RowWithIconInput {
}

struct MediaRowInput: Equatable {
let rowID: RowID
let widgetId: String
let renderingKind: WidgetRenderingKind
let displayState: WidgetDisplayState
Expand All @@ -423,12 +428,13 @@ struct MediaRowInput: Equatable {
let coordinateLatitude: Double?
let coordinateLongitude: Double?

static func from(widget: OpenHABWidget) -> MediaRowInput {
static func from(widget: OpenHABWidget, rowID: RowID) -> MediaRowInput {
let coordinate = widget.coordinate
let hasValidCoordinate = (-90.0 ... 90.0).contains(coordinate.latitude)
&& (-180.0 ... 180.0).contains(coordinate.longitude)

return MediaRowInput(
rowID: rowID,
widgetId: widget.widgetId,
renderingKind: widget.renderingKind,
displayState: widget.displayState,
Expand Down Expand Up @@ -478,6 +484,7 @@ struct TextRowInput: Equatable, RowWithIconInput {
}

struct SliderRowInput: Equatable, RowWithIconInput {
let rowID: RowID
let widgetId: String
let displayState: WidgetDisplayState
let numberPattern: String?
Expand All @@ -500,7 +507,7 @@ struct SliderRowInput: Equatable, RowWithIconInput {
displayState.minValue ... displayState.maxValue
}

static func from(widget: OpenHABWidget) -> SliderRowInput {
static func from(widget: OpenHABWidget, rowID: RowID) -> SliderRowInput {
let displayState = widget.displayState
let numberPattern = if let pattern = widget.pattern, !pattern.isEmpty {
pattern
Expand All @@ -515,6 +522,7 @@ struct SliderRowInput: Equatable, RowWithIconInput {
}

return SliderRowInput(
rowID: rowID,
widgetId: widget.widgetId,
displayState: displayState,
numberPattern: numberPattern,
Expand Down
Loading
Loading