Skip to content
Closed
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
30 changes: 19 additions & 11 deletions openHAB/Models/SitemapPageViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ enum RowInteractionState: Equatable {

@MainActor
class SitemapPageViewModel: ObservableObject {
@Published var currentPage: OpenHABPage?
var currentPage: OpenHABPage?
@Published var searchText = "" {
didSet {
rebuildRowInputs()
Expand All @@ -69,6 +69,7 @@ class SitemapPageViewModel: ObservableObject {
@Published var error: (any LocalizedError)?
@Published var isLoading = true
@Published var isUpdating = false
@Published private(set) var pageTitle = ""
@Published var openHABRootUrl: String?
@Published var showSearchField = false
@Published private(set) var commandStates: [String: WidgetCommandLifecycleState] = [:]
Expand All @@ -87,8 +88,7 @@ class SitemapPageViewModel: ObservableObject {
private var defaultSitemap = ""
private var defaultSitemapLabel = ""
private var fallbackTitle = ""
@Published var pageId = ""
private var isLinkedPage = false
var pageId = ""
private var pageNetworkStatus: NetworkStatus?
private var pageNetworkStatusAvailable = false
private var activePageHandlingKey: String?
Expand All @@ -108,7 +108,7 @@ class SitemapPageViewModel: ObservableObject {
}
}

var pageTitle: String {
private func computePageTitle() -> String {
// Strip bracket content from title (e.g., "Living Room[2]" becomes "Living Room")
let title = currentPage?.title.components(separatedBy: "[")[0].trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if !title.isEmpty {
Expand All @@ -123,8 +123,11 @@ class SitemapPageViewModel: ObservableObject {
}
}

var isLinked: Bool {
isLinkedPage
private func refreshPageTitle() {
let newTitle = computePageTitle()
if pageTitle != newTitle {
pageTitle = newTitle
}
}

var commandLifecycleSummary: CommandLifecycleSummary {
Expand Down Expand Up @@ -183,7 +186,6 @@ class SitemapPageViewModel: ObservableObject {
init(pageUrl: String, title: String, pageId: String = "") {
loadSettings()
startObservers()
isLinkedPage = true
fallbackTitle = title
defaultSitemapLabel = title

Expand All @@ -201,11 +203,11 @@ class SitemapPageViewModel: ObservableObject {
} else {
self.pageId = pageId
}
refreshPageTitle()
}

/// Initializes the view model with a fixed set of widgets, without loading or polling
init(pageUrl: String = "", title: String = "Preview Page", pageId: String = "", widgets: [OpenHABWidget]) {
isLinkedPage = !pageUrl.isEmpty
fallbackTitle = title
self.pageId = pageId
currentPage = OpenHABPage(
Expand All @@ -216,6 +218,7 @@ class SitemapPageViewModel: ObservableObject {
widgets: widgets,
icon: ""
)
refreshPageTitle()
rebuildRowInputs()
}

Expand Down Expand Up @@ -299,7 +302,6 @@ class SitemapPageViewModel: ObservableObject {
guard let itemname, !itemname.isEmpty else { return }
sliderOverrideResetTasks[itemname]?.cancel()
sliderOverrideResetTasks[itemname] = nil
objectWillChange.send()
sliderValueOverrides[itemname] = value
}

Expand All @@ -325,6 +327,7 @@ extension SitemapPageViewModel {
func loadSettings() {
defaultSitemap = Preferences.shared.currentHomePreferences.defaultSitemap
showSearchField = Preferences.shared.applicationPreferences.showSearchField
refreshPageTitle()
}

func stopPageHandling() {
Expand Down Expand Up @@ -548,12 +551,13 @@ extension SitemapPageViewModel {

// Replace currentPage wholesale — no in-place widget reconciliation.
currentPage = page
refreshPageTitle()

_ = clearSyncedSliderOverrides(using: page.widgets)

if inputsChanged {
bumpWidgetVersions(from: rowInputs, to: previewInputs)
rowInputs = previewInputs
rowInputs = previewInputs.map { $0.applyingWidgetVersions(widgetUpdateVersions) }
}
}

Expand Down Expand Up @@ -628,6 +632,7 @@ extension SitemapPageViewModel {
}

currentPage = page
refreshPageTitle()
rebuildRowInputs()
}

Expand Down Expand Up @@ -658,6 +663,7 @@ extension SitemapPageViewModel {
defaultSitemap = name
defaultSitemapLabel = "" // Clear old label so it gets fetched for the new sitemap
pageId = path ?? ""
refreshPageTitle()
error = nil // Clear any previous errors when switching sitemaps
startPageHandling(forceRestart: true, reason: "push-sitemap")
}
Expand All @@ -674,6 +680,7 @@ extension SitemapPageViewModel {
// Find the sitemap matching our defaultSitemap name and get its label
if let sitemap = sitemaps.first(where: { $0.name == defaultSitemap }) {
defaultSitemapLabel = sitemap.label
refreshPageTitle()
// swiftformat:disable:next redundantSelf
logger.info("Found label '\(self.defaultSitemapLabel)' for sitemap '\(self.defaultSitemap)'")
} else {
Expand Down Expand Up @@ -704,6 +711,7 @@ extension SitemapPageViewModel {
// Auto-select the only available sitemap
defaultSitemap = filteredSitemaps[0].name
defaultSitemapLabel = filteredSitemaps[0].label
refreshPageTitle()
// swiftformat:disable:next redundantSelf
logger.info("Auto-selected single sitemap: \(self.defaultSitemap)")

Expand All @@ -715,6 +723,7 @@ extension SitemapPageViewModel {
// Multiple sitemaps available - select the first one
defaultSitemap = filteredSitemaps[0].name
defaultSitemapLabel = filteredSitemaps[0].label
refreshPageTitle()
// swiftformat:disable:next redundantSelf
logger.info("Auto-selected first sitemap from \(filteredSitemaps.count) available: \(self.defaultSitemap)")

Expand Down Expand Up @@ -1072,7 +1081,6 @@ private extension SitemapPageViewModel {
guard sliderValueOverrides[itemname] != nil else { return }
sliderOverrideResetTasks[itemname]?.cancel()
sliderOverrideResetTasks[itemname] = nil
objectWillChange.send()
sliderValueOverrides.removeValue(forKey: itemname)
}
}
Expand Down
8 changes: 2 additions & 6 deletions openHAB/UI/SwiftUI/EmbeddingRowInputView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,7 @@ private struct LinkedPageRowInputView: View {
let input: LinkedPageRowInput

var body: some View {
NavigationLink(
destination: SitemapPageView(
viewModel: SitemapPageViewModel(pageUrl: input.linkedPageLink, title: input.linkedPageTitle)
)
) {
NavigationLink(value: input.destination) {
LinkedPageRowContent(input: input)
}
.buttonStyle(.plain)
Expand All @@ -98,7 +94,7 @@ private struct LinkedPageRowContent: View {
var body: some View {
let displayState = input.displayState
HStack {
IconInputView(input: input.icon, rowIdentity: input.widgetId, size: CGSize(width: 32, height: 32))
IconInputView(input: input.icon, rowIdentity: input.destination.pageUrl, size: CGSize(width: 32, height: 32))

Text(displayState.labelText)
.ohTextToken(.rowLabel)
Expand Down
8 changes: 2 additions & 6 deletions openHAB/UI/SwiftUI/IconView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ struct IconInputView: View {
let fallbackSymbol: SFSymbol?
@ObservedObject private var networkTracker = MainActorNetworkTracker.shared
@Environment(\.colorScheme) private var colorScheme
@EnvironmentObject var viewModel: SitemapPageViewModel

let size: CGSize
let iconType: IconType = .svg

Expand Down Expand Up @@ -119,7 +117,7 @@ struct IconInputView: View {
.cancelOnDisappear(true)
.aspectRatio(contentMode: .fit)
.frame(width: size.width, height: size.height)
.id("\(viewModel.pageId)-\(rowIdentity)-\(colorScheme)")
.id("\(rowIdentity)-\(colorScheme)")
}
}
}
Expand All @@ -142,8 +140,6 @@ struct IconView: View {
@ObservedObject var widget: OpenHABWidget
@ObservedObject private var networkTracker = MainActorNetworkTracker.shared
@Environment(\.colorScheme) private var colorScheme
@EnvironmentObject var viewModel: SitemapPageViewModel

let size: CGSize
let iconType: IconType = .svg
/// Optional SF Symbol to show as fallback when network icon is unavailable (useful for previews)
Expand Down Expand Up @@ -220,7 +216,7 @@ struct IconView: View {
.cancelOnDisappear(true)
.aspectRatio(contentMode: .fit)
.frame(width: size.width, height: size.height)
.id("\(viewModel.pageId)-\(widget.id)-\(colorScheme)")
.id("\(widget.id)-\(colorScheme)")
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion openHAB/UI/SwiftUI/RowViewWithIcon.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import SwiftUI
/// caller-provided content inside an `HStack`.
struct RowViewWithIcon<Content: View>: View {
let input: any RowWithIconInput
let rowIdentity: String
let fallbackSymbol: SFSymbol?
let alignment: VerticalAlignment
let spacing: CGFloat?
Expand All @@ -24,18 +25,20 @@ struct RowViewWithIcon<Content: View>: View {
var body: some View {
HStack(alignment: alignment, spacing: spacing) {
if input.icon.showIcon {
IconInputView(input: input.icon, rowIdentity: input.widgetId, size: CGSize(width: 32, height: 32), fallbackSymbol: fallbackSymbol)
IconInputView(input: input.icon, rowIdentity: rowIdentity, size: CGSize(width: 32, height: 32), fallbackSymbol: fallbackSymbol)
}
content()
}
}

init(input: any RowWithIconInput,
rowIdentity: String? = nil,
fallbackSymbol: SFSymbol? = .questionmark,
alignment: VerticalAlignment = .center,
spacing: CGFloat? = nil,
@ViewBuilder content: @escaping () -> Content) {
self.input = input
self.rowIdentity = rowIdentity ?? input.widgetId
self.fallbackSymbol = fallbackSymbol
self.alignment = alignment
self.spacing = spacing
Expand Down
1 change: 0 additions & 1 deletion openHAB/UI/SwiftUI/Rows/FrameRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,5 @@ struct FrameRowView: View {
List([widget]) { widget in
FrameRowView(input: FrameRowInput.from(widget: widget))
}
.environmentObject(SitemapPageViewModel())
}
#endif
1 change: 0 additions & 1 deletion openHAB/UI/SwiftUI/Rows/GenericRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,5 @@ struct GenericRowView: View {
List([widget]) { widget in
GenericRowView(input: GenericRowInput.from(widget: widget))
}
.environmentObject(SitemapPageViewModel())
}
#endif
9 changes: 4 additions & 5 deletions openHAB/UI/SwiftUI/Rows/SegmentedRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private struct SegmentedRowContent: View {

var body: some View {
let selectedIndex = effectiveSelectedIndex(displayState: input.displayState, mappings: input.mappings)
RowViewWithIcon(input: input, fallbackSymbol: fallbackSymbol, spacing: 0) {
RowViewWithIcon(input: input, rowIdentity: input.rowID.rawValue, fallbackSymbol: fallbackSymbol, spacing: 0) {
if !input.displayState.labelText.isEmpty {
let labelText = input.displayState.labelText
Text(labelText)
Expand Down Expand Up @@ -338,17 +338,16 @@ private struct SegmentedRowContent: View {
struct SegmentedRowView: View {
let input: SegmentedRowInput
var fallbackSymbol: SFSymbol?

@EnvironmentObject var viewModel: SitemapPageViewModel
@Environment(\.sitemapRowActions) private var rowActions

var body: some View {
SegmentedRowContent(
input: input,
widgetVersion: viewModel.widgetUpdateVersion(for: input.rowID),
widgetVersion: input.widgetVersion,
fallbackSymbol: fallbackSymbol
) { command, policy, phase in
guard let itemName = input.itemName else { return }
viewModel.sendCommand(command, for: itemName, policy: policy, phase: phase)
rowActions.sendCommand(itemName, command, policy, phase)
}
}
}
Expand Down
9 changes: 4 additions & 5 deletions openHAB/UI/SwiftUI/Rows/SelectionRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ private struct SelectionRowContent: View {

@ViewBuilder
private func rowContent(displayedCommand: String) -> some View {
RowViewWithIcon(input: input) {
RowViewWithIcon(input: input, rowIdentity: input.rowID.rawValue) {
if !input.displayState.labelText.isEmpty {
let labelText = input.displayState.labelText
Text(labelText)
Expand Down Expand Up @@ -172,16 +172,15 @@ private struct SelectionRowContent: View {

struct SelectionRowView: View {
let input: SelectionRowInput

@EnvironmentObject var viewModel: SitemapPageViewModel
@Environment(\.sitemapRowActions) private var rowActions

var body: some View {
SelectionRowContent(
input: input,
widgetVersion: viewModel.widgetUpdateVersion(for: input.rowID)
widgetVersion: input.widgetVersion
) { command in
guard let itemName = input.itemName else { return }
viewModel.sendCommand(command, for: itemName)
rowActions.sendCommand(itemName, command, .immediate, .change)
}
}
}
Loading
Loading