-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Update floating bar onboarding Mac lineup #5610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,14 +11,12 @@ struct OnboardingFloatingBarDemoView: View { | |
|
|
||
| @State private var barActivated = false | ||
| @State private var showContinue = false | ||
| @State private var pulseAnimation = false | ||
| @State private var keyMonitor: Any? | ||
|
|
||
| var body: some View { | ||
| VStack(spacing: 0) { | ||
| // Header | ||
| HStack { | ||
| Text("Ask omi anything") | ||
| Text("Ask omi which Mac fits you") | ||
| .font(.system(size: 18, weight: .semibold)) | ||
| .foregroundColor(OmiColors.textPrimary) | ||
|
|
||
|
|
@@ -40,38 +38,21 @@ struct OnboardingFloatingBarDemoView: View { | |
| Spacer() | ||
|
|
||
| // Content | ||
| VStack(spacing: 28) { | ||
| // Icon with glow | ||
| ZStack { | ||
| Circle() | ||
| .fill(OmiColors.purplePrimary.opacity(0.12)) | ||
| .frame(width: 96, height: 96) | ||
| .blur(radius: 18) | ||
| .scaleEffect(pulseAnimation ? 1.15 : 1.0) | ||
| .animation(.easeInOut(duration: 2).repeatForever(autoreverses: true), value: pulseAnimation) | ||
|
|
||
| Image(systemName: "rectangle.and.text.magnifyingglass") | ||
| .font(.system(size: 40)) | ||
| .foregroundStyle( | ||
| LinearGradient( | ||
| colors: [OmiColors.purplePrimary, OmiColors.purpleSecondary], | ||
| startPoint: .topLeading, | ||
| endPoint: .bottomTrailing | ||
| ) | ||
| ) | ||
| } | ||
| .onAppear { pulseAnimation = true } | ||
| VStack(spacing: 24) { | ||
| MacLineupPreview() | ||
| .frame(maxWidth: 980) | ||
|
|
||
| VStack(spacing: 10) { | ||
| VStack(spacing: 12) { | ||
| Text("The Floating Bar") | ||
| .font(.system(size: 24, weight: .bold)) | ||
| .foregroundColor(OmiColors.textPrimary) | ||
|
|
||
| Text("Ask anything and it responds using\neverything it knows about you.") | ||
| Text("Try asking: Which computer suits me best?") | ||
| .font(.system(size: 14)) | ||
| .foregroundColor(OmiColors.textSecondary) | ||
| .multilineTextAlignment(.center) | ||
| .lineSpacing(4) | ||
| .frame(maxWidth: 560) | ||
| } | ||
|
|
||
| if !barActivated { | ||
|
|
@@ -100,6 +81,7 @@ struct OnboardingFloatingBarDemoView: View { | |
| .transition(.opacity) | ||
| } | ||
| } | ||
| .padding(.top, 88) | ||
| .padding(.horizontal, 40) | ||
|
|
||
| Spacer() | ||
|
|
@@ -125,24 +107,28 @@ struct OnboardingFloatingBarDemoView: View { | |
| .onAppear { | ||
| // Set up the real floating bar (creates the window if needed) | ||
| FloatingControlBarManager.shared.setup(appState: appState, chatProvider: chatProvider) | ||
| // Unregister global shortcuts so we handle Cmd+Enter ourselves | ||
| GlobalShortcutManager.shared.unregisterShortcuts() | ||
| installKeyMonitor() | ||
| // Use the same global shortcut flow as the normal app so onboarding | ||
| // behaves like production when the user presses Cmd+Enter. | ||
| GlobalShortcutManager.shared.registerShortcuts() | ||
| } | ||
| .onDisappear { | ||
| removeKeyMonitor() | ||
| // Close the AI conversation panel on the floating bar so the next step starts clean | ||
| if FloatingControlBarManager.shared.barState?.showingAIConversation == true { | ||
| FloatingControlBarManager.shared.toggleAIInput() | ||
| } | ||
| // Re-register global shortcuts for subsequent steps and normal use | ||
| GlobalShortcutManager.shared.registerShortcuts() | ||
| } | ||
| .onChange(of: barActivated) { _, activated in | ||
| if activated { | ||
| Task { await waitForResponse() } | ||
| } | ||
| } | ||
| .onReceive(Timer.publish(every: 0.25, on: .main, in: .common).autoconnect()) { _ in | ||
| guard !barActivated, | ||
| FloatingControlBarManager.shared.barState?.showingAIConversation == true else { return } | ||
| withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { | ||
| barActivated = true | ||
| } | ||
| } | ||
|
Comment on lines
+125
to
+131
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The global shortcut now calls
The onboarding intent is "open the bar," not "toggle the bar." Consider using |
||
| } | ||
|
|
||
| // MARK: - Response Observer | ||
|
|
@@ -169,32 +155,6 @@ struct OnboardingFloatingBarDemoView: View { | |
| } | ||
| } | ||
|
|
||
| // MARK: - Key Monitor | ||
|
|
||
| private func installKeyMonitor() { | ||
| keyMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { event in | ||
| let mods = event.modifierFlags.intersection(.deviceIndependentFlagsMask) | ||
| if mods == .command && event.keyCode == 36 { // 36 = Return | ||
| if !barActivated { | ||
| // Activate the real floating bar's AI input | ||
| FloatingControlBarManager.shared.openAIInput() | ||
| withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { | ||
| barActivated = true | ||
| } | ||
| return nil | ||
| } | ||
| } | ||
| return event | ||
| } | ||
| } | ||
|
|
||
| private func removeKeyMonitor() { | ||
| if let monitor = keyMonitor { | ||
| NSEvent.removeMonitor(monitor) | ||
| keyMonitor = nil | ||
| } | ||
| } | ||
|
|
||
| // MARK: - Key Cap | ||
|
|
||
| private func keyCap(_ key: String) -> some View { | ||
|
|
@@ -214,3 +174,31 @@ struct OnboardingFloatingBarDemoView: View { | |
| ) | ||
| } | ||
| } | ||
|
|
||
| private struct MacLineupPreview: View { | ||
| private static let lineupImage: NSImage? = { | ||
| guard let url = Bundle.resourceBundle.url(forResource: "onboarding_mac_lineup", withExtension: "png") else { return nil } | ||
| return NSImage(contentsOf: url) | ||
| }() | ||
|
Comment on lines
+179
to
+182
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Synchronous image loading blocks the main thread
Consider loading the image off-thread and publishing it via a private struct MacLineupPreview: View {
@State private var nsImage: NSImage?
var body: some View {
Group {
if let nsImage {
Image(nsImage: nsImage)
.resizable()
.interpolation(.high)
.scaledToFit()
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
} else {
RoundedRectangle(cornerRadius: 24)
.fill(Color.white.opacity(0.06))
.frame(height: 280)
.overlay(
Text("Mac lineup image unavailable")
.font(.system(size: 14, weight: .medium))
.foregroundColor(OmiColors.textTertiary)
)
}
}
.task {
guard let url = Bundle.resourceBundle.url(
forResource: "onboarding_mac_lineup", withExtension: "png") else { return }
nsImage = await Task.detached(priority: .userInitiated) {
NSImage(contentsOf: url)
}.value
}
}
} |
||
|
|
||
| var body: some View { | ||
| Group { | ||
| if let nsImage = Self.lineupImage { | ||
| Image(nsImage: nsImage) | ||
| .resizable() | ||
| .interpolation(.high) | ||
| .scaledToFit() | ||
| .clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous)) | ||
| } else { | ||
| RoundedRectangle(cornerRadius: 24) | ||
| .fill(Color.white.opacity(0.06)) | ||
| .frame(height: 280) | ||
| .overlay( | ||
| Text("Mac lineup image unavailable") | ||
| .font(.system(size: 14, weight: .medium)) | ||
| .foregroundColor(OmiColors.textTertiary) | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Timer fires indefinitely after
barActivatedbecomestrueOnce
barActivatedis set totrue, the guard exits early on every subsequent tick, but the timer is never cancelled — it keeps firing at 4 Hz for the entire lifetime of the view (which includes the 60-secondwaitForResponseloop). Storing the timer'sCancellableand cancelling it when activation is detected would avoid this waste:Alternatively, using a dedicated
Cancellablestored in a@Statevar gives explicit control over when polling stops.