@@ -5,120 +5,255 @@ import SwiftUI
55
66@available ( iOS 26 . 0 , * )
77struct RoomView : View {
8- @StateObject private var viewModel : RoomViewModel
9- @Environment ( \. dismiss) private var dismiss
8+ let server : Server
9+ let roomId : String
10+ let roomName : String
1011
11- init ( server: Server , roomId: String , roomName: String ) {
12- _viewModel = StateObject ( wrappedValue: RoomViewModel (
13- server: server,
14- roomId: roomId,
15- roomName: roomName
16- ) )
17- }
12+ @EnvironmentObject private var viewModel : HomeViewModel
13+ @Environment ( \. dismiss) private var dismiss
14+ @State private var showHidden = false
15+ @State private var showEditSheet = false
1816
1917 var body : some View {
2018 NavigationStack {
2119 contentView
22- . navigationTitle ( viewModel . roomName)
20+ . navigationTitle ( roomName)
2321 . navigationBarTitleDisplayMode ( . large)
2422 . toolbar {
23+ ToolbarItem ( placement: . topBarTrailing) {
24+ Button {
25+ showEditSheet = true
26+ } label: {
27+ Text ( " Edit " )
28+ }
29+ . buttonStyle ( . plain)
30+ }
31+
2532 ToolbarItem ( placement: . topBarTrailing) {
2633 Button {
2734 dismiss ( )
2835 } label: {
2936 Image ( systemSymbol: . xmark)
30- . fontWeight ( . semibold)
3137 }
3238 . buttonStyle ( . plain)
3339 }
3440 }
3541 . background ( ModernAssistBackgroundView ( theme: . homeAssistant) )
3642 }
37- . task {
38- await viewModel. loadEntities ( )
43+ . animation ( DesignSystem . Animation. default, value: showHidden)
44+ . sheet ( isPresented: $showEditSheet) {
45+ editEntitiesSheet
3946 }
4047 }
4148
42- // MARK: - Content Views
49+ // MARK: - Content View
4350
4451 private var contentView : some View {
45- ZStack {
46- if viewModel. isLoading {
47- EntityDisplayComponents . loadingView
48- } else if let errorMessage = viewModel. errorMessage {
49- EntityDisplayComponents . errorView ( errorMessage)
50- } else if viewModel. allEntities. isEmpty {
51- EntityDisplayComponents . emptyStateView ( message: L10n . RoomView. EmptyState. noEntities)
52- } else {
53- entitiesListView
54- }
55- }
56- . animation ( DesignSystem . Animation. default, value: viewModel. isLoading)
57- . animation ( DesignSystem . Animation. default, value: viewModel. errorMessage)
58- . animation ( DesignSystem . Animation. default, value: viewModel. allEntities. isEmpty)
59- . frame ( maxWidth: . infinity, maxHeight: . infinity)
60- }
61-
62- private var entitiesListView : some View {
6352 ScrollView {
6453 LazyVStack (
6554 alignment: . leading,
6655 spacing: DesignSystem . Spaces. three
6756 ) {
68- // Group entities by visibility status (checking against user's hidden entities)
69- let hiddenEntities = viewModel. allEntities. filter { viewModel. hiddenEntityIds. contains ( $0. entityId) }
70- let visibleEntities = viewModel. allEntities. filter { !viewModel. hiddenEntityIds. contains ( $0. entityId) }
71-
57+ // Visible Entities Section
7258 if !visibleEntities. isEmpty {
73- Section {
74- entityTilesGrid ( for: visibleEntities)
75- }
59+ entityTilesGrid ( for: visibleEntities, isHidden: false )
7660 }
7761
62+ // Show/Hide Hidden Entities Button
7863 if !hiddenEntities. isEmpty {
64+ Button {
65+ withAnimation ( DesignSystem . Animation. default) {
66+ showHidden. toggle ( )
67+ }
68+ } label: {
69+ HStack {
70+ Image ( systemSymbol: showHidden ? . eyeSlashFill : . eye)
71+ Text ( showHidden ? " Hide hidden entities " : " Show hidden entities " )
72+ }
73+ . font ( . body. weight ( . medium) )
74+ . foregroundStyle ( . secondary)
75+ . frame ( maxWidth: . infinity)
76+ . padding ( . vertical, DesignSystem . Spaces. two)
77+ . background (
78+ . regularMaterial,
79+ in: RoundedRectangle ( cornerRadius: DesignSystem . CornerRadius. oneAndHalf)
80+ )
81+ }
82+ . buttonStyle ( . plain)
83+ }
84+
85+ // Hidden Entities Section
86+ if showHidden, !hiddenEntities. isEmpty {
7987 Section {
8088 entityTilesGrid ( for: hiddenEntities, isHidden: true )
8189 } header: {
8290 sectionHeader ( L10n . RoomView. Section. hidden)
8391 }
8492 }
93+
94+ // Empty State
95+ if visibleEntities. isEmpty, hiddenEntities. isEmpty {
96+ EntityDisplayComponents . emptyStateView ( message: L10n . RoomView. EmptyState. noEntities)
97+ . frame ( maxWidth: . infinity, maxHeight: . infinity)
98+ . padding ( . top, DesignSystem . Spaces. six)
99+ }
85100 }
86101 . padding ( )
87102 }
88- . transition ( . opacity. combined ( with: . move( edge: . bottom) ) )
103+ }
104+
105+ // MARK: - Computed Properties
106+
107+ private var currentRoomSection : HomeViewModel . RoomSection ? {
108+ viewModel. groupedEntities. first ( where: { $0. id == roomId } )
109+ }
110+
111+ private var roomEntityIds : Set < String > {
112+ // Get area entity IDs directly from the database
113+ do {
114+ let areas = try AppArea . fetchAreas ( for: viewModel. server. identifier. rawValue)
115+ if let area = areas. first ( where: { $0. id == roomId } ) {
116+ return area. entities
117+ }
118+ } catch {
119+ Current . Log. error ( " Failed to fetch area entities: \( error. localizedDescription) " )
120+ }
121+ return [ ]
122+ }
123+
124+ private var allRoomEntities : [ HAEntity ] {
125+ // Get all entities from entity states that belong to this room
126+ viewModel. entityStates. values. filter { entity in
127+ roomEntityIds. contains ( entity. entityId)
128+ }
129+ }
130+
131+ private var visibleEntities : [ HAEntity ] {
132+ allRoomEntities. filter { entity in
133+ !viewModel. hiddenEntityIds. contains ( entity. entityId)
134+ }
135+ . sorted { $0. entityId < $1. entityId }
136+ }
137+
138+ private var hiddenEntities : [ HAEntity ] {
139+ allRoomEntities. filter { entity in
140+ viewModel. hiddenEntityIds. contains ( entity. entityId)
141+ }
142+ . sorted { $0. entityId < $1. entityId }
89143 }
90144
91145 // MARK: - Component Views
92146
147+ @ViewBuilder
93148 private func sectionHeader( _ title: String ) -> some View {
94- Text ( title)
95- . font ( . title3. bold ( ) )
96- . frame ( maxWidth: . infinity, alignment: . leading)
97- . padding ( . vertical, DesignSystem . Spaces. one)
149+ HStack {
150+ Text ( title)
151+ . font ( . title2. bold ( ) )
152+ . lineLimit ( 1 )
153+ . truncationMode ( . middle)
154+ Spacer ( )
155+ }
156+ . frame ( maxWidth: . infinity, alignment: . leading)
157+ . padding ( . top, DesignSystem . Spaces. one)
158+ . padding ( . horizontal, DesignSystem . Spaces. one)
98159 }
99160
100- private func entityTilesGrid( for entities: [ HAEntity ] , isHidden: Bool = false ) -> some View {
161+ @ViewBuilder
162+ private func entityTilesGrid( for entities: [ HAEntity ] , isHidden: Bool ) -> some View {
101163 EntityDisplayComponents . entityTilesGrid (
102164 entities: entities,
103- server: viewModel . server,
165+ server: server,
104166 isHidden: isHidden
105167 ) { entity in
106- Group {
107- if isHidden {
108- Button {
109- viewModel. unhideEntity ( entity. entityId)
110- } label: {
111- Label ( L10n . RoomView. ContextMenu. unhide, systemSymbol: . eye)
168+ if isHidden {
169+ Button {
170+ viewModel. unhideEntity ( entity. entityId)
171+ } label: {
172+ Label ( L10n . RoomView. ContextMenu. unhide, systemSymbol: . eye)
173+ }
174+ } else {
175+ Button ( role: . destructive) {
176+ viewModel. hideEntity ( entity. entityId)
177+ } label: {
178+ Label ( " Hide " , systemSymbol: . eyeSlash)
179+ }
180+ }
181+ }
182+ }
183+
184+ // MARK: - Edit Sheet
185+
186+ private var editEntitiesSheet : some View {
187+ NavigationStack {
188+ List {
189+ if !visibleEntities. isEmpty {
190+ Section {
191+ ForEach ( visibleEntities, id: \. entityId) { entity in
192+ entityRow ( entity: entity, isHidden: false )
193+ }
194+ } header: {
195+ Text ( " Visible Entities " )
196+ }
197+ }
198+
199+ if !hiddenEntities. isEmpty {
200+ Section {
201+ ForEach ( hiddenEntities, id: \. entityId) { entity in
202+ entityRow ( entity: entity, isHidden: true )
203+ }
204+ } header: {
205+ Text ( " Hidden Entities " )
206+ }
207+ }
208+ }
209+ . navigationTitle ( " Edit Entities " )
210+ . navigationBarTitleDisplayMode ( . inline)
211+ . toolbar {
212+ ToolbarItem ( placement: . topBarTrailing) {
213+ Button ( " Done " ) {
214+ showEditSheet = false
112215 }
216+ }
217+ }
218+ }
219+ }
220+
221+ @ViewBuilder
222+ private func entityRow( entity: HAEntity , isHidden: Bool ) -> some View {
223+ HStack {
224+ VStack ( alignment: . leading, spacing: DesignSystem . Spaces. half) {
225+ Text ( entity. attributes. friendlyName ?? entity. entityId)
226+ . font ( . body)
227+ Text ( entity. entityId)
228+ . font ( . caption)
229+ . foregroundStyle ( . secondary)
230+ }
231+
232+ Spacer ( )
233+
234+ Button {
235+ if isHidden {
236+ viewModel. unhideEntity ( entity. entityId)
113237 } else {
114- EmptyView ( )
238+ viewModel . hideEntity ( entity . entityId )
115239 }
240+ } label: {
241+ Image ( systemSymbol: isHidden ? . eye : . eyeSlash)
242+ . foregroundStyle ( isHidden ? . blue : . secondary)
243+ . font ( . title3)
116244 }
245+ . buttonStyle ( . plain)
117246 }
247+ . opacity ( isHidden ? 0.6 : 1.0 )
118248 }
119249}
120250
121251@available ( iOS 26 . 0 , * )
122252#Preview {
123- RoomView ( server: ServerFixture . standard, roomId: " living_room " , roomName: " Living Room " )
253+ RoomView (
254+ server: ServerFixture . standard,
255+ roomId: " room_1 " ,
256+ roomName: " Living Room "
257+ )
258+ . environmentObject ( HomeViewModel ( server: ServerFixture . standard) )
124259}
0 commit comments