Skip to content
Merged
26 changes: 22 additions & 4 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,9 @@
429BA2AF2C800CAB00A50996 /* SFSymbolEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429BA2AE2C800CAB00A50996 /* SFSymbolEntity.swift */; };
429BEA1A2D102F3A00F070F9 /* ConnectionErrorDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429BEA192D102F3A00F070F9 /* ConnectionErrorDetailsView.swift */; };
429BEA1D2D10315F00F070F9 /* SheetCloseButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429BEA1B2D1030EA00F070F9 /* SheetCloseButton.swift */; };
429C33C22F17A7010033EF5E /* HAUsagePredictionCommonControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429C33C12F17A7010033EF5E /* HAUsagePredictionCommonControl.swift */; };
429C33C32F17A7010033EF5E /* HAUsagePredictionCommonControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429C33C12F17A7010033EF5E /* HAUsagePredictionCommonControl.swift */; };
429C33C52F17CE4C0033EF5E /* AreaGridButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429C33C42F17CE4C0033EF5E /* AreaGridButton.swift */; };
429C33BF2F17989F0033EF5E /* EntityPickerViewModel.test.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429C33BC2F17986D0033EF5E /* EntityPickerViewModel.test.swift */; };
429C72202B28D0EC00BCD558 /* Haptics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429C721F2B28D0EC00BCD558 /* Haptics.swift */; };
429C82892DCCDB0F007B03AF /* HAProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42E4C9922DCC9A6600DF2813 /* HAProgressView.swift */; };
Expand Down Expand Up @@ -2562,6 +2565,8 @@
429BA2AE2C800CAB00A50996 /* SFSymbolEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SFSymbolEntity.swift; sourceTree = "<group>"; };
429BEA192D102F3A00F070F9 /* ConnectionErrorDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionErrorDetailsView.swift; sourceTree = "<group>"; };
429BEA1B2D1030EA00F070F9 /* SheetCloseButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SheetCloseButton.swift; sourceTree = "<group>"; };
429C33C12F17A7010033EF5E /* HAUsagePredictionCommonControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HAUsagePredictionCommonControl.swift; sourceTree = "<group>"; };
429C33C42F17CE4C0033EF5E /* AreaGridButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AreaGridButton.swift; sourceTree = "<group>"; };
429C33BC2F17986D0033EF5E /* EntityPickerViewModel.test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityPickerViewModel.test.swift; sourceTree = "<group>"; };
429C721F2B28D0EC00BCD558 /* Haptics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Haptics.swift; sourceTree = "<group>"; };
429D51C42EFA0E750014AD0D /* ModernAssistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernAssistView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -5384,6 +5389,16 @@
path = General;
sourceTree = "<group>";
};
429C33C02F17A6CF0033EF5E /* Models */ = {
isa = PBXGroup;
children = (
42333ADA2D0B1771001E8408 /* EntityRegistryListForDisplay.swift */,
42F1DA6F2B4EE2E8002729BC /* HAAreasRegistryResponse.swift */,
429C33C12F17A7010033EF5E /* HAUsagePredictionCommonControl.swift */,
);
path = Models;
sourceTree = "<group>";
};
429C33BE2F17988D0033EF5E /* EntityPicker */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -5773,11 +5788,12 @@
isa = PBXGroup;
children = (
42DFE8772EFB08C80058DADB /* HomeView.swift */,
428DC0132F0D921F003B08D5 /* Domain+AccentColor.swift */,
42DFE8782EFB08C80058DADB /* HomeViewModel.swift */,
428DC0132F0D921F003B08D5 /* Domain+AccentColor.swift */,
42F5C0822F02B6DB00C50310 /* HomeSectionsReorderView.swift */,
4289FCB82F0BFF50005189AA /* HomeViewConfiguration.swift */,
420C914F2F0C6CAC005D04A6 /* HomeViewCustomizationView.swift */,
429C33C42F17CE4C0033EF5E /* AreaGridButton.swift */,
);
path = Home;
sourceTree = "<group>";
Expand Down Expand Up @@ -7024,13 +7040,12 @@
D03D891820E0A85300D4F28D /* Shared */ = {
isa = PBXGroup;
children = (
42BB4C362CD26490003E47FD /* HATypedRequest+App.swift */,
429C33C02F17A6CF0033EF5E /* Models */,
4245DC022EBA42C3005E0E04 /* AreasService.swift */,
420CFC612D3F9C15009A94F3 /* Database */,
4278CB822D01F09400CFAAC9 /* AppGesture.swift */,
424D2D0F2C89DACE00C610F1 /* HAAppEntity.swift */,
42BB4C362CD26490003E47FD /* HATypedRequest+App.swift */,
42333ADA2D0B1771001E8408 /* EntityRegistryListForDisplay.swift */,
42F1DA6F2B4EE2E8002729BC /* HAAreasRegistryResponse.swift */,
426D9C722C9C582F00F278AF /* ControlEntityProvider.swift */,
42A47D4A2C9AEF10003C597D /* DataWidgetsUpdater.swift */,
427E92BB2D65E3FE0001566B /* WidgetInteractionType.swift */,
Expand Down Expand Up @@ -9307,6 +9322,7 @@
42F1DA6B2B4ED1BF002729BC /* CarPlayAreasZonesTemplate.swift in Sources */,
11A48D8124CA8ADB0021BDD9 /* NotificationCategory+Observation.swift in Sources */,
42C60F8D2F057DB70071A6F6 /* VerticalSlider.swift in Sources */,
429C33C52F17CE4C0033EF5E /* AreaGridButton.swift in Sources */,
4263A6692EFB2F670089C338 /* ModernAssistBackgroundView.swift in Sources */,
42462E692D9ED75900ECC8A7 /* LocationPermissionView.swift in Sources */,
42D0AE452D88259000D9715A /* WidgetDocumentationLink.swift in Sources */,
Expand Down Expand Up @@ -9593,6 +9609,7 @@
B672333F225DB68B0031D629 /* WebSocketMessage.swift in Sources */,
11AF4D1A249C8253006C74C0 /* PedometerSensor.swift in Sources */,
1117FB4D250C5F7C00895C13 /* DeviceBattery.swift in Sources */,
429C33C32F17A7010033EF5E /* HAUsagePredictionCommonControl.swift in Sources */,
42070EEC2BAC517A0031E96F /* AssistInAppIntentHandler.swift in Sources */,
11B38EF9275C54A300205C7B /* RenderTemplateIntentHandler.swift in Sources */,
427A7CDA2EBDFB1700D17841 /* AppArea.swift in Sources */,
Expand Down Expand Up @@ -10080,6 +10097,7 @@
B6A258482232539900ADD202 /* WebhookUpdateLocation.swift in Sources */,
B6B74CBA2283983800D58A68 /* CLKComplication+Strings.swift in Sources */,
119385A4249E8E360097F497 /* StorageSensor.swift in Sources */,
429C33C22F17A7010033EF5E /* HAUsagePredictionCommonControl.swift in Sources */,
D05A4D32216DD206009FD1EB /* MJPEGStreamer.swift in Sources */,
42B2637F2E16A1DC0042DF10 /* BaseColors.swift in Sources */,
426266452C11B02C0081A818 /* InteractiveImmediateMessages.swift in Sources */,
Expand Down
6 changes: 6 additions & 0 deletions Sources/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@
"home_sections_reorder_view.done" = "Done";
"home_sections_reorder_view.title" = "Reorder Rooms";
"home_view.context_menu.hide" = "Hide";
"home_view.areas.title" = "Areas";
"home_view.empty_state.no_entities" = "No entities found";
"home_view.menu.allow_multiple_selection" = "Allow multiple selection";
"home_view.menu.customize" = "Customize";
Expand All @@ -355,6 +356,11 @@
"home_view.menu.settings" = "Settings";
"home_view.menu.show_all" = "Show All";
"home_view.navigation.subtitle.experimental" = "Experimental feature";
"home_view.common_controls.title" = "Welcome %@";
"home_view.customization.common_controls.title" = "Controls prediction section";
"home_view.customization.areas_layout.title" = "Areas layout";
"home_view.customization.areas_layout.list.title" = "List";
"home_view.customization.areas_layout.grid.title" = "Grid";
"improv.button.continue" = "Continue";
"improv.connection_state.authorized" = "Setting up Wi-Fi";
"improv.connection_state.provisioning" = "Connecting to Wi-Fi";
Expand Down
52 changes: 52 additions & 0 deletions Sources/App/WebView/ExperimentalSpace/Home/AreaGridButton.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import SFSafeSymbols
import Shared
import SwiftUI

@available(iOS 26.0, *)
struct AreaGridButton: View {
enum Constants {
static let cornerRadius: CGFloat = DesignSystem.CornerRadius.two
static let borderLineWidth: CGFloat = DesignSystem.Border.Width.default
static let iconSize: CGFloat = 32
}

let section: HomeViewModel.RoomSection
let action: () -> Void

var body: some View {
Button(action: action) {
VStack(spacing: DesignSystem.Spaces.oneAndHalf) {
icon
Text(section.name)
.font(.footnote.weight(.semibold))
.foregroundColor(Color(uiColor: .label))
.lineLimit(2)
.truncationMode(.middle)
.multilineTextAlignment(.center)
.padding(.horizontal, DesignSystem.Spaces.one)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(1, contentMode: .fill)
.background(Color.tileBackground)
.clipShape(RoundedRectangle(cornerRadius: Constants.cornerRadius))
.overlay(
RoundedRectangle(cornerRadius: Constants.cornerRadius)
.stroke(Color.tileBorder, lineWidth: Constants.borderLineWidth)
)
}
}

private var icon: some View {
let materialDesignIcon = MaterialDesignIcons(
serversideValueNamed: section.icon.orEmpty,
fallback: .squareRoundedOutlineIcon
)
return Group {
Image(uiImage: materialDesignIcon.image(
ofSize: .init(width: Constants.iconSize, height: Constants.iconSize),
color: .haPrimary
))
}
.font(.system(size: Constants.iconSize))
}
}
102 changes: 87 additions & 15 deletions Sources/App/WebView/ExperimentalSpace/Home/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,29 +121,95 @@ struct HomeView: View {
sectionOrder: viewModel.configuration.sectionOrder,
visibleSectionIds: viewModel.configuration.visibleSectionIds
)
let layout = viewModel.configuration.areasLayout ?? .list

return ScrollView {
LazyVStack(
alignment: .leading,
spacing: DesignSystem.Spaces.three
) {
ForEach(filteredSections) { section in
let visibleEntities = visibleEntitiesForSection(section)
if !visibleEntities.isEmpty || viewModel.configuration.visibleSectionIds.contains(section.id) {
Section {
entityTilesGrid(
for: visibleEntities,
section: section
switch layout {
case .grid:
areasGridView(sections: filteredSections)
case .list:
areasListView(sections: filteredSections)
}
}
.transition(.opacity.combined(with: .move(edge: .bottom)))
}

private func areasListView(sections: [HomeViewModel.RoomSection]) -> some View {
LazyVStack(
alignment: .leading,
spacing: DesignSystem.Spaces.three
) {
predictionSection

// Display regular sections
ForEach(sections) { section in
let visibleEntities = visibleEntitiesForSection(section)
if !visibleEntities.isEmpty || viewModel.configuration.visibleSectionIds.contains(section.id) {
Section {
entityTilesGrid(
for: visibleEntities,
section: section
)
} header: {
sectionHeader(section.name, section: section)
}
}
}
}
.padding()
}

private func areasGridView(sections: [HomeViewModel.RoomSection]) -> some View {
VStack(alignment: .leading, spacing: .zero) {
predictionSection
.padding([.top, .horizontal])

VStack(alignment: .leading, spacing: DesignSystem.Spaces.two) {
EntityDisplayComponents.sectionHeader(
L10n.HomeView.Areas.title,
showChevron: false
)
LazyVGrid(
columns: [
GridItem(.adaptive(minimum: 100, maximum: 150), spacing: DesignSystem.Spaces.one),
],
spacing: DesignSystem.Spaces.one
) {
ForEach(sections) { section in
if !isReorderMode {
AreaGridButton(
section: section,
action: {
selectedRoom = (id: section.id, name: section.name)
}
)
} header: {
sectionHeader(section.name, section: section)
.matchedTransitionSource(id: section.id, in: roomNameSpace)
}
}
}
}
.padding()
}
.transition(.opacity.combined(with: .move(edge: .bottom)))
}

@ViewBuilder
private var predictionSection: some View {
if viewModel.configuration.showUsagePredictionSection {
// Display usage prediction common control section at the top
if let usagePredictionSection = viewModel.usagePredictionSection {
let visibleEntities = visibleEntitiesForSection(usagePredictionSection)
if !visibleEntities.isEmpty {
Section {
entityTilesGrid(
for: visibleEntities,
section: usagePredictionSection
)
} header: {
sectionHeader(usagePredictionSection.name, section: usagePredictionSection)
}
}
}
}
}

private func visibleEntitiesForSection(_ section: HomeViewModel.RoomSection) -> [HAEntity] {
Expand Down Expand Up @@ -352,7 +418,13 @@ struct HomeView: View {
@ViewBuilder
private func sectionHeader(_ title: String, section: HomeViewModel.RoomSection) -> some View {
Group {
if let section = viewModel.groupedEntities.first(where: { $0.name == title }) {
// Handle usage prediction section separately (not in groupedEntities)
if section.id == HomeViewModel.usagePredictionSectionId {
EntityDisplayComponents.sectionHeader(
title,
showChevron: false
)
} else if let section = viewModel.groupedEntities.first(where: { $0.name == title }) {
EntityDisplayComponents.sectionHeader(
title,
showChevron: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,60 @@
import Foundation
import GRDB
import SFSafeSymbols
import Shared

struct HomeViewConfiguration: Codable, FetchableRecord, PersistableRecord, Equatable {
enum AreasLayout: Codable, CaseIterable {
case list
case grid

var localizableName: String {
switch self {
case .list:
return L10n.HomeView.Customization.AreasLayout.List.title
case .grid:
return L10n.HomeView.Customization.AreasLayout.Grid.title
}
}

var icon: SFSymbol {
switch self {
case .list:
return .listBullet
case .grid:
return .squareGrid2x2Fill
}
}
}

/// Server identifier (primary key)
let id: String
var sectionOrder: [String]
var visibleSectionIds: Set<String>
var allowMultipleSelection: Bool
var entityOrderByRoom: [String: [String]]
var hiddenEntityIds: Set<String>
var showUsagePredictionSection: Bool
var areasLayout: AreasLayout?

init(
id: String,
sectionOrder: [String] = [],
visibleSectionIds: Set<String> = [],
allowMultipleSelection: Bool = false,
entityOrderByRoom: [String: [String]] = [:],
hiddenEntityIds: Set<String> = []
hiddenEntityIds: Set<String> = [],
showUsagePredictionSection: Bool = true,
areasLayout: AreasLayout? = .list
) {
self.id = id
self.sectionOrder = sectionOrder
self.visibleSectionIds = visibleSectionIds
self.allowMultipleSelection = allowMultipleSelection
self.entityOrderByRoom = entityOrderByRoom
self.hiddenEntityIds = hiddenEntityIds
self.showUsagePredictionSection = showUsagePredictionSection
self.areasLayout = areasLayout
}

/// Fetch configuration for a specific server
Expand Down
Loading
Loading