@@ -96,7 +96,6 @@ class SitemapPageViewModel: ObservableObject {
9696 private var commandStateResetTasks : [ String : Task < Void , Never > ] = [ : ]
9797 private var commandStateVersions : [ String : Int ] = [ : ]
9898 private var queuedCommands : [ String : QueuedCommand ] = [ : ]
99- private var rowWidgetIndex : [ RowID : OpenHABWidget ] = [ : ]
10099 private var sliderValueOverrides : [ String : Double ] = [ : ]
101100 private var sliderOverrideResetTasks : [ String : Task < Void , Never > ] = [ : ]
102101 private var lastForegroundRefreshAt : Date = . distantPast
@@ -267,28 +266,12 @@ class SitemapPageViewModel: ObservableObject {
267266
268267 func rebuildRowInputs( ) {
269268 let pageKey = " \( defaultSitemap) | \( pageId) "
270- let widgets = relevantWidgets
271- let inputs = SitemapRowInputMapper . map ( pageKey: pageKey, widgets: widgets)
272- rowWidgetIndex = buildWidgetIndex ( pageKey: pageKey, widgets: widgets)
269+ let inputs = SitemapRowInputMapper . map ( pageKey: pageKey, widgets: relevantWidgets)
273270 if inputs != rowInputs {
274271 rowInputs = inputs
275272 }
276273 }
277274
278- private func buildWidgetIndex( pageKey: String , widgets: [ OpenHABWidget ] ) -> [ RowID : OpenHABWidget ] {
279- var occurrenceByWidgetID : [ String : Int ] = [ : ]
280- var index : [ RowID : OpenHABWidget ] = [ : ]
281- index. reserveCapacity ( widgets. count)
282- for widget in widgets {
283- let identityWidgetID = SitemapRowInputMapper . rowIdentityWidgetID ( for: widget)
284- occurrenceByWidgetID [ identityWidgetID, default: 0 ] += 1
285- let occurrence = occurrenceByWidgetID [ identityWidgetID] !
286- let rowID = RowID ( pageKey: pageKey, widgetId: identityWidgetID, occurrence: occurrence)
287- index [ rowID] = widget
288- }
289- return index
290- }
291-
292275 /// Increments `widgetUpdateVersions` for each row whose content differs between `oldInputs`
293276 /// and `newInputs`, keyed by full row identity.
294277 func bumpWidgetVersions( from oldInputs: [ SitemapRowInput ] , to newInputs: [ SitemapRowInput ] ) {
@@ -303,10 +286,6 @@ class SitemapPageViewModel: ObservableObject {
303286 }
304287 }
305288
306- func widget( for rowID: RowID ) -> OpenHABWidget ? {
307- rowWidgetIndex [ rowID]
308- }
309-
310289 func widgetUpdateVersion( for rowID: RowID ) -> Int {
311290 widgetUpdateVersions [ rowID. rawValue] ?? 0
312291 }
@@ -547,7 +526,7 @@ extension SitemapPageViewModel {
547526
548527 let pageKey = " \( defaultSitemap) | \( pageId) "
549528
550- // Snapshot what the list would render from the new data — before any widget mutation.
529+ // Snapshot new inputs from fresh page data — no widget object mutation.
551530 let incomingFiltered : [ OpenHABWidget ]
552531 if searchText. isEmpty {
553532 incomingFiltered = page. widgets
@@ -559,36 +538,23 @@ extension SitemapPageViewModel {
559538 let previewInputs = SitemapRowInputMapper . map ( pageKey: pageKey, widgets: incomingFiltered)
560539
561540 let titleChanged = currentPage == nil || currentPage? . title != page. title
562- let canSkipReconciliation = searchText. isEmpty && previewInputs == rowInputs && !titleChanged
563- guard !canSkipReconciliation else {
541+ let inputsChanged = previewInputs != rowInputs
542+
543+ // When search is empty and nothing the UI renders has changed, skip the update entirely.
544+ if searchText. isEmpty, !inputsChanged, !titleChanged {
564545 _ = clearSyncedSliderOverrides ( using: page. widgets)
565546 return
566547 }
567548
568- // Something changed — reconcile widget objects and update stored state.
569- let currentWidgets = currentPage? . widgets ?? [ ]
570- let structureChanged = currentWidgets. count != page. widgets. count
571- || !zip( currentWidgets, page. widgets) . allSatisfy { $0. widgetId == $1. widgetId }
572- let reconciledWidgets = reconcileWidgets ( page. widgets, with: currentWidgets)
573- injectSendCommand ( for: reconciledWidgets)
574-
575- if structureChanged || titleChanged {
576- page. widgets = reconciledWidgets
577- currentPage = page
578- } else {
579- currentPage? . widgets = reconciledWidgets
580- }
581-
582- _ = clearSyncedSliderOverrides ( using: reconciledWidgets)
583-
584- // Rebuild command-dispatch index from the now-current reconciled widgets.
585- rowWidgetIndex = buildWidgetIndex ( pageKey: pageKey, widgets: relevantWidgets)
549+ // Replace currentPage wholesale — no in-place widget reconciliation.
550+ currentPage = page
586551
587- // Bump widget versions only for rows whose content actually changed.
588- bumpWidgetVersions ( from: rowInputs, to: previewInputs)
552+ _ = clearSyncedSliderOverrides ( using: page. widgets)
589553
590- // Publish new row inputs — guaranteed to differ from current (checked above).
591- rowInputs = previewInputs
554+ if inputsChanged {
555+ bumpWidgetVersions ( from: rowInputs, to: previewInputs)
556+ rowInputs = previewInputs
557+ }
592558 }
593559
594560 private func clearSyncedSliderOverrides( using widgets: [ OpenHABWidget ] ) -> Int {
@@ -661,80 +627,10 @@ extension SitemapPageViewModel {
661627 throw SitemapPageError . noData
662628 }
663629
664- injectSendCommand ( for: page. widgets)
665630 currentPage = page
666631 rebuildRowInputs ( )
667632 }
668633
669- private func reconcileWidgets( _ newWidgets: [ OpenHABWidget ] , with currentWidgets: [ OpenHABWidget ] ) -> [ OpenHABWidget ] {
670- var buckets : [ String : [ OpenHABWidget ] ] = [ : ]
671- for widget in currentWidgets {
672- buckets [ widget. widgetId, default: [ ] ] . append ( widget)
673- }
674-
675- var reconciled : [ OpenHABWidget ] = [ ]
676- reconciled. reserveCapacity ( newWidgets. count)
677-
678- for newWidget in newWidgets {
679- if var candidates = buckets [ newWidget. widgetId] , !candidates. isEmpty {
680- let existing = candidates. removeFirst ( )
681- buckets [ newWidget. widgetId] = candidates
682-
683- // Always copy server properties to avoid missing updates when
684- // non-keyed fields change (for example group summary/state rows).
685- let previousChildren = existing. widgets
686- copyWidgetProperties ( from: newWidget, to: existing)
687- existing. widgets = reconcileWidgets ( newWidget. widgets, with: previousChildren)
688-
689- reconciled. append ( existing)
690- } else {
691- reconciled. append ( newWidget)
692- }
693- }
694-
695- return reconciled
696- }
697-
698- private func copyWidgetProperties( from source: OpenHABWidget , to target: OpenHABWidget ) {
699- target. label = source. label
700- target. icon = source. icon
701- target. state = source. state
702- target. type = source. type
703- target. isLeaf = source. isLeaf
704- target. item = source. item
705- target. iconColor = source. iconColor
706- target. labelcolor = source. labelcolor
707- target. valuecolor = source. valuecolor
708- target. url = source. url
709- target. period = source. period
710- target. service = source. service
711- target. legend = source. legend
712- target. refresh = source. refresh
713- target. height = source. height
714- target. forceAsItem = source. forceAsItem
715- target. minValue = source. minValue
716- target. maxValue = source. maxValue
717- target. step = source. step
718- target. pattern = source. pattern
719- target. unit = source. unit
720- target. switchSupport = source. switchSupport
721- target. mappings = source. mappings
722- target. linkedPage = source. linkedPage
723- target. visibility = source. visibility
724- target. staticIcon = source. staticIcon
725- target. text = source. text
726- target. inputHint = source. inputHint
727- target. encoding = source. encoding
728- target. labelSource = source. labelSource
729- target. releaseOnly = source. releaseOnly
730- target. row = source. row
731- target. column = source. column
732- target. releaseCommand = source. releaseCommand
733- target. command = source. command
734- target. stateless = source. stateless
735- target. yAxisDecimalPattern = source. yAxisDecimalPattern
736- }
737-
738634 private func shouldRetryLongPolling( after error: any Error ) -> Bool {
739635 if let urlError = OpenAPIErrorInspector . underlyingURLError ( from: error) {
740636 switch urlError. code {
@@ -757,17 +653,6 @@ extension SitemapPageViewModel {
757653 return false
758654 }
759655
760- private func injectSendCommand( for widgets: [ OpenHABWidget ] ) {
761- for widget in widgets {
762- widget. sendCommand = { [ weak self] item, command in
763- self ? . sendCommand ( item, commandToSend: command)
764- }
765-
766- // If widget has nested children (e.g., frames/groups), inject recursively
767- injectSendCommand ( for: widget. widgets)
768- }
769- }
770-
771656 @MainActor
772657 func pushSitemap( name: String , path: String ? ) async {
773658 defaultSitemap = name
0 commit comments