Skip to content

Commit 8cdfb4f

Browse files
Copilotbgoncal
andauthored
Experiment: Native HomeView/dashboard (#4142)
Basic implementation of a native dashboard - WIP - [ ] Support more domains toggle action when tapping icon - [x] Light - [x] Cover - [x] Switch - [ ] Create the equivalent of "more info dialog" for each domain - [x] Light - [x] Cover - [x] Switch - [x] Add haptics - [ ] Add more cards such as a camera card - [ ] Find a way to display sensors information such as humidity and temperature - [ ] Allow customizing background - [x] Allow opening the native dashboard from iOS controls - [ ] Allow opening the native dashboard from Shortcuts - [ ] Add an advanced option somewhere in settings that allow user to see the native dashboard first instead of web UI - [ ] Add error state - [x] Add empty state - [x] Add loading state - [x] Allow reordering rooms - [x] Allow reordering cards <img width="1232" height="1158" alt="CleanShot 2026-01-05 at 16 19 59@2x" src="https://github.com/user-attachments/assets/84ae8748-ec40-430c-a820-fcb6a88a6270" /> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bgoncal <5808343+bgoncal@users.noreply.github.com>
1 parent 345f57b commit 8cdfb4f

File tree

63 files changed

+5968
-254
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+5968
-254
lines changed

HomeAssistant.xcodeproj/project.pbxproj

Lines changed: 254 additions & 8 deletions
Large diffs are not rendered by default.

Podfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ pod 'XCGLogger'
3737

3838
# Keep Starscream reference even though HAKit already install it, because it defines our fork with the necessary fix
3939
pod 'Starscream', git: 'https://github.com/bgoncal/starscream', branch: 'ha-URLSession-fix'
40-
pod 'HAKit', git: 'https://github.com/home-assistant/HAKit.git', tag: '0.4.8'
41-
pod 'HAKit/PromiseKit', git: 'https://github.com/home-assistant/HAKit.git', tag: '0.4.8'
42-
pod 'HAKit/Mocks', git: 'https://github.com/home-assistant/HAKit.git', tag: '0.4.8'
40+
pod 'HAKit', git: 'https://github.com/home-assistant/HAKit.git', tag: '0.4.9'
41+
pod 'HAKit/PromiseKit', git: 'https://github.com/home-assistant/HAKit.git', tag: '0.4.9'
42+
pod 'HAKit/Mocks', git: 'https://github.com/home-assistant/HAKit.git', tag: '0.4.9'
4343

4444
def test_pods
4545
pod 'OHHTTPStubs/Swift'

Podfile.lock

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,13 @@ PODS:
5454
- GRDB.swift (7.8.0):
5555
- GRDB.swift/standard (= 7.8.0)
5656
- GRDB.swift/standard (7.8.0)
57-
- HAKit (0.4.8):
58-
- HAKit/Core (= 0.4.8)
59-
- HAKit/Core (0.4.8):
57+
- HAKit (0.4.9):
58+
- HAKit/Core (= 0.4.9)
59+
- HAKit/Core (0.4.9):
6060
- Starscream (~> 4.0.4)
61-
- HAKit/Mocks (0.4.8):
61+
- HAKit/Mocks (0.4.9):
6262
- HAKit/Core
63-
- HAKit/PromiseKit (0.4.8):
63+
- HAKit/PromiseKit (0.4.9):
6464
- HAKit/Core
6565
- PromiseKit (~> 8.1.1)
6666
- Improv-iOS (0.0.6)
@@ -132,9 +132,9 @@ DEPENDENCIES:
132132
- Eureka (from `https://github.com/xmartlabs/Eureka`, branch `master`)
133133
- FirebaseMessaging
134134
- GRDB.swift (from `https://github.com/groue/GRDB.swift.git`, tag `v7.8.0`)
135-
- HAKit (from `https://github.com/home-assistant/HAKit.git`, tag `0.4.8`)
136-
- HAKit/Mocks (from `https://github.com/home-assistant/HAKit.git`, tag `0.4.8`)
137-
- HAKit/PromiseKit (from `https://github.com/home-assistant/HAKit.git`, tag `0.4.8`)
135+
- HAKit (from `https://github.com/home-assistant/HAKit.git`, tag `0.4.9`)
136+
- HAKit/Mocks (from `https://github.com/home-assistant/HAKit.git`, tag `0.4.9`)
137+
- HAKit/PromiseKit (from `https://github.com/home-assistant/HAKit.git`, tag `0.4.9`)
138138
- Improv-iOS (~> 0.0.6)
139139
- KeychainAccess
140140
- MBProgressHUD (~> 1.2.0)
@@ -206,7 +206,7 @@ EXTERNAL SOURCES:
206206
:tag: v7.8.0
207207
HAKit:
208208
:git: https://github.com/home-assistant/HAKit.git
209-
:tag: 0.4.8
209+
:tag: 0.4.9
210210
ObjectMapper:
211211
:branch: master
212212
:git: https://github.com/tristanhimmelman/ObjectMapper.git
@@ -241,7 +241,7 @@ CHECKOUT OPTIONS:
241241
:tag: v7.8.0
242242
HAKit:
243243
:git: https://github.com/home-assistant/HAKit.git
244-
:tag: 0.4.8
244+
:tag: 0.4.9
245245
ObjectMapper:
246246
:commit: a593b4d647a970b3d184d046f8f52b945083ccf9
247247
:git: https://github.com/tristanhimmelman/ObjectMapper.git
@@ -270,7 +270,7 @@ SPEC CHECKSUMS:
270270
GoogleDataTransport: ea169759df570f4e37bdee1623ec32a7e64e67c4
271271
GoogleUtilities: c2bdc4cf2ce786c4d2e6b3bcfd599a25ca78f06f
272272
GRDB.swift: 682e07f771a9100f0bdf40fd0bed57b83ca08e29
273-
HAKit: 64da9eb0e3990420ab34ce029b29675f478ce311
273+
HAKit: 2e0570970efe11fa54ad5cceb5d4c4c3fca4c603
274274
Improv-iOS: 8973990c1b1f3e3aed7fc600c8efce95359cadd0
275275
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
276276
MBProgressHUD: 3ee5efcc380f6a79a7cc9b363dd669c5e1ae7406
@@ -295,6 +295,6 @@ SPEC CHECKSUMS:
295295
ViewRow: 8da541e9c019f3be63f4e28f311a62cac2045ecf
296296
XCGLogger: 1943831ef907df55108b0b18657953f868de973b
297297

298-
PODFILE CHECKSUM: 72f5b6d55792b4a04c356b46ad4d6b0106161842
298+
PODFILE CHECKSUM: 3a6770107ce893659bd5725d7ce07dd1c9a9c065
299299

300300
COCOAPODS: 1.15.2
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import SwiftUI
2+
3+
/// A reusable animated background view with gradient and floating orbs
4+
/// Designed for use across multiple views in the app
5+
struct ModernAssistBackgroundView: View {
6+
@Environment(\.colorScheme) private var colorScheme
7+
8+
// MARK: - Constants
9+
10+
private enum Constants {
11+
static let orbSize: CGFloat = 300
12+
static let orbRadius: CGFloat = 150
13+
static let backgroundBlurRadius: CGFloat = 40
14+
static let orbYOffsetMin: CGFloat = -50
15+
static let orbYOffsetMax: CGFloat = 50
16+
static let orbYOffset2Min: CGFloat = 0
17+
static let orbYOffset2Max: CGFloat = 100
18+
static let orbXOffsetLeft: CGFloat = -100
19+
static let orbOpacity: Double = 0.3
20+
static let ambientAnimationDuration: Double = 4
21+
}
22+
23+
// MARK: - Properties
24+
25+
let theme: ModernAssistTheme
26+
@State private var pulseAnimation = false
27+
28+
// MARK: - Body
29+
30+
var body: some View {
31+
LinearGradient(
32+
colors: theme.gradientColors(for: colorScheme),
33+
startPoint: .topLeading,
34+
endPoint: .bottomTrailing
35+
)
36+
.ignoresSafeArea()
37+
.overlay {
38+
// Animated orbs in background
39+
GeometryReader { geometry in
40+
ZStack {
41+
// First orb - left side
42+
Circle()
43+
.fill(
44+
RadialGradient(
45+
colors: [
46+
theme.orbColors(for: colorScheme).0.opacity(theme.orbOpacity(
47+
for: colorScheme,
48+
defaultOpacity: Constants.orbOpacity
49+
)),
50+
Color.clear,
51+
],
52+
center: .center,
53+
startRadius: 0,
54+
endRadius: Constants.orbRadius
55+
)
56+
)
57+
.frame(width: Constants.orbSize, height: Constants.orbSize)
58+
.offset(
59+
x: Constants.orbXOffsetLeft,
60+
y: pulseAnimation ? Constants.orbYOffsetMin : Constants.orbYOffsetMax
61+
)
62+
.blur(radius: Constants.backgroundBlurRadius)
63+
64+
// Second orb - right side
65+
Circle()
66+
.fill(
67+
RadialGradient(
68+
colors: [
69+
theme.orbColors(for: colorScheme).1.opacity(theme.orbOpacity(
70+
for: colorScheme,
71+
defaultOpacity: Constants.orbOpacity
72+
)),
73+
Color.clear,
74+
],
75+
center: .center,
76+
startRadius: 0,
77+
endRadius: Constants.orbRadius
78+
)
79+
)
80+
.frame(width: Constants.orbSize, height: Constants.orbSize)
81+
.offset(
82+
x: geometry.size.width - Constants.orbXOffsetLeft * -1,
83+
y: pulseAnimation ? Constants.orbYOffset2Max : Constants.orbYOffset2Min
84+
)
85+
.blur(radius: Constants.backgroundBlurRadius)
86+
}
87+
}
88+
}
89+
.onAppear {
90+
startAmbientAnimation()
91+
}
92+
}
93+
94+
// MARK: - Animations
95+
96+
private func startAmbientAnimation() {
97+
withAnimation(.easeInOut(duration: Constants.ambientAnimationDuration).repeatForever(autoreverses: true)) {
98+
pulseAnimation = true
99+
}
100+
}
101+
}
102+
103+
// MARK: - Preview
104+
105+
#Preview("Home Assistant Theme - Light") {
106+
ModernAssistBackgroundView(theme: .homeAssistant)
107+
.environment(\.colorScheme, .light)
108+
}
109+
110+
#Preview("Home Assistant Theme - Dark") {
111+
ModernAssistBackgroundView(theme: .homeAssistant)
112+
.environment(\.colorScheme, .dark)
113+
}
114+
115+
#Preview("Aurora Theme") {
116+
ModernAssistBackgroundView(theme: .aurora)
117+
}
118+
119+
#Preview("Sunset Theme") {
120+
ModernAssistBackgroundView(theme: .sunset)
121+
}
122+
123+
#Preview("Ocean Theme") {
124+
ModernAssistBackgroundView(theme: .ocean)
125+
}

Sources/App/Assist/ModernAssistView.swift

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ struct ModernAssistView: View, KeyboardReadable {
1414
static let inputHeight: CGFloat = 50
1515
static let textFieldHeight: CGFloat = 50
1616
static let buttonWidth: CGFloat = 60
17-
static let orbSize: CGFloat = 300
18-
static let orbRadius: CGFloat = 150
1917

2018
// Spacing
2119
static let horizontalPadding: CGFloat = DesignSystem.Spaces.two
@@ -40,17 +38,11 @@ struct ModernAssistView: View, KeyboardReadable {
4038
// Offsets
4139
static let topMaterialOffset: CGFloat = 0
4240
static let bottomMaterialOffset: CGFloat = DesignSystem.Spaces.two
43-
static let orbYOffsetMin: CGFloat = -50
44-
static let orbYOffsetMax: CGFloat = 50
45-
static let orbYOffset2Min: CGFloat = 0
46-
static let orbYOffset2Max: CGFloat = 100
47-
static let orbXOffsetLeft: CGFloat = -100
4841
static let pickerXOffset: CGFloat = -5
4942

5043
// Opacity
5144
static let topMaterialOpacity: Double = 0.5
5245
static let bottomMaterialOpacity: Double = 0.9
53-
static let orbOpacity: Double = 0.3
5446
static let whiteTextOpacity: Double = 0.95
5547
static let buttonTextOpacity: Double = 0.7
5648
static let assistantBubbleOpacity: Double = 0.1
@@ -66,7 +58,6 @@ struct ModernAssistView: View, KeyboardReadable {
6658
static let titleFontSize: CGFloat = 34
6759

6860
// Animation Durations
69-
static let ambientAnimationDuration: Double = 4
7061
static let recordingAnimationDuration: Double = 1.5
7162
static let sendSpringResponse: Double = 0.3
7263
static let recordingSpringResponse: Double = 0.4
@@ -78,8 +69,6 @@ struct ModernAssistView: View, KeyboardReadable {
7869

7970
@Binding var isRecording: Bool
8071
@Binding var inputText: String
81-
@State private var pulseAnimation = false
82-
@State private var glowIntensity: CGFloat = 0
8372
@Binding var selectedTheme: ModernAssistTheme
8473
@Binding var selectedPipeline: String
8574
@FocusState private var isTextFieldFocused: Bool
@@ -124,8 +113,7 @@ struct ModernAssistView: View, KeyboardReadable {
124113
var body: some View {
125114
NavigationStack {
126115
ZStack {
127-
backgroundGradient
128-
.ignoresSafeArea()
116+
ModernAssistBackgroundView(theme: selectedTheme)
129117
chatArea
130118
}
131119
.toolbar(content: {
@@ -148,76 +136,12 @@ struct ModernAssistView: View, KeyboardReadable {
148136
modernInputArea
149137
})
150138
.scrollEdgeEffectStyle(.soft, for: .all)
151-
.onAppear {
152-
startAmbientAnimation()
153-
}
154139
.onReceive(keyboardPublisher) { newIsKeyboardVisible in
155140
keyboardVisible = newIsKeyboardVisible
156141
}
157142
}
158143
}
159144

160-
// MARK: - Background
161-
162-
private var backgroundGradient: some View {
163-
LinearGradient(
164-
colors: selectedTheme.gradientColors(for: colorScheme),
165-
startPoint: .topLeading,
166-
endPoint: .bottomTrailing
167-
)
168-
.ignoresSafeArea()
169-
.overlay {
170-
// Animated orbs in background
171-
GeometryReader { geometry in
172-
ZStack {
173-
Circle()
174-
.fill(
175-
RadialGradient(
176-
colors: [
177-
selectedTheme.orbColors(for: colorScheme).0.opacity(selectedTheme.orbOpacity(
178-
for: colorScheme,
179-
defaultOpacity: Constants.orbOpacity
180-
)),
181-
Color.clear,
182-
],
183-
center: .center,
184-
startRadius: 0,
185-
endRadius: Constants.orbRadius
186-
)
187-
)
188-
.frame(width: Constants.orbSize, height: Constants.orbSize)
189-
.offset(
190-
x: Constants.orbXOffsetLeft,
191-
y: pulseAnimation ? Constants.orbYOffsetMin : Constants.orbYOffsetMax
192-
)
193-
.blur(radius: Constants.backgroundBlurRadius)
194-
195-
Circle()
196-
.fill(
197-
RadialGradient(
198-
colors: [
199-
selectedTheme.orbColors(for: colorScheme).1.opacity(selectedTheme.orbOpacity(
200-
for: colorScheme,
201-
defaultOpacity: Constants.orbOpacity
202-
)),
203-
Color.clear,
204-
],
205-
center: .center,
206-
startRadius: 0,
207-
endRadius: Constants.orbRadius
208-
)
209-
)
210-
.frame(width: Constants.orbSize, height: Constants.orbSize)
211-
.offset(
212-
x: geometry.size.width - Constants.orbXOffsetLeft * -1,
213-
y: pulseAnimation ? Constants.orbYOffset2Max : Constants.orbYOffset2Min
214-
)
215-
.blur(radius: Constants.backgroundBlurRadius)
216-
}
217-
}
218-
}
219-
}
220-
221145
// MARK: - Header
222146

223147
private var modernHeader: some View {
@@ -450,19 +374,6 @@ struct ModernAssistView: View, KeyboardReadable {
450374
}
451375

452376
// MARK: - Animations
453-
454-
private func startAmbientAnimation() {
455-
withAnimation(.easeInOut(duration: Constants.ambientAnimationDuration).repeatForever(autoreverses: true)) {
456-
pulseAnimation = true
457-
}
458-
459-
// Start recording animation when recording
460-
withAnimation(.easeInOut(duration: Constants.recordingAnimationDuration).repeatForever(autoreverses: true)) {
461-
if isRecording {
462-
glowIntensity = 1
463-
}
464-
}
465-
}
466377
}
467378

468379
// MARK: - Background Theme

0 commit comments

Comments
 (0)