fix: track onboarding completion separately, extend popover to 30s
Replace EXOHasLaunchedBefore (set immediately on launch) with EXOOnboardingCompleted (set only when user interacts with the popout). This ensures onboarding re-triggers on reinstall if never completed. Extend auto-open timer from 5s to 30s and remove auto-dismiss so the popover persists until the user clicks Open Dashboard or X. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5db9030535
commit
5e4531bc8b
@@ -68,6 +68,9 @@ struct EXOApp: App {
|
||||
.environmentObject(settingsWindowController)
|
||||
.onReceive(controller.$isFirstLaunchReady) { ready in
|
||||
if ready {
|
||||
firstLaunchPopout.onComplete = { [weak controller] in
|
||||
controller?.markOnboardingCompleted()
|
||||
}
|
||||
firstLaunchPopout.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import Foundation
|
||||
private let customNamespaceKey = "EXOCustomNamespace"
|
||||
private let hfTokenKey = "EXOHFToken"
|
||||
private let enableImageModelsKey = "EXOEnableImageModels"
|
||||
private let hasLaunchedBeforeKey = "EXOHasLaunchedBefore"
|
||||
private let onboardingCompletedKey = "EXOOnboardingCompleted"
|
||||
|
||||
@MainActor
|
||||
final class ExoProcessController: ObservableObject {
|
||||
@@ -118,9 +118,8 @@ final class ExoProcessController: ObservableObject {
|
||||
process = child
|
||||
status = .running
|
||||
|
||||
// Detect first-ever launch to trigger welcome popout
|
||||
if !UserDefaults.standard.bool(forKey: hasLaunchedBeforeKey) {
|
||||
UserDefaults.standard.set(true, forKey: hasLaunchedBeforeKey)
|
||||
// Show welcome popout if onboarding was never completed
|
||||
if !UserDefaults.standard.bool(forKey: onboardingCompletedKey) {
|
||||
isFirstLaunchReady = true
|
||||
}
|
||||
} catch {
|
||||
@@ -174,6 +173,17 @@ final class ExoProcessController: ObservableObject {
|
||||
launch()
|
||||
}
|
||||
|
||||
/// Mark onboarding as completed (user interacted with the welcome popout).
|
||||
func markOnboardingCompleted() {
|
||||
UserDefaults.standard.set(true, forKey: onboardingCompletedKey)
|
||||
}
|
||||
|
||||
/// Reset onboarding so the welcome popout appears on next launch.
|
||||
func resetOnboarding() {
|
||||
UserDefaults.standard.removeObject(forKey: onboardingCompletedKey)
|
||||
isFirstLaunchReady = false
|
||||
}
|
||||
|
||||
func scheduleLaunch(after seconds: TimeInterval) {
|
||||
cancelPendingLaunch()
|
||||
let start = max(1, Int(ceil(seconds)))
|
||||
|
||||
@@ -9,16 +9,22 @@ final class FirstLaunchPopout {
|
||||
private var countdownTask: Task<Void, Never>?
|
||||
private static let dashboardURL = "http://localhost:52415/"
|
||||
|
||||
/// Called when the user completes onboarding (clicks Open Dashboard or dismisses).
|
||||
var onComplete: (() -> Void)?
|
||||
|
||||
func show() {
|
||||
guard panel == nil else { return }
|
||||
|
||||
let hostingView = NSHostingView(
|
||||
rootView: WelcomeCalloutView(
|
||||
countdownDuration: 30,
|
||||
onDismiss: { [weak self] in
|
||||
self?.onComplete?()
|
||||
self?.dismiss()
|
||||
},
|
||||
onOpen: { [weak self] in
|
||||
self?.openDashboard()
|
||||
self?.onComplete?()
|
||||
self?.dismiss()
|
||||
}))
|
||||
hostingView.frame = NSRect(x: 0, y: 0, width: 280, height: 100)
|
||||
@@ -60,15 +66,12 @@ final class FirstLaunchPopout {
|
||||
window.animator().alphaValue = 1
|
||||
}
|
||||
|
||||
// Auto-open dashboard after 5s then dismiss
|
||||
// Auto-open dashboard after 30s but keep popout visible until user interacts
|
||||
countdownTask = Task {
|
||||
try? await Task.sleep(nanoseconds: 5_000_000_000)
|
||||
try? await Task.sleep(nanoseconds: 30_000_000_000)
|
||||
if !Task.isCancelled {
|
||||
openDashboard()
|
||||
try? await Task.sleep(nanoseconds: 800_000_000)
|
||||
if !Task.isCancelled {
|
||||
dismiss()
|
||||
}
|
||||
onComplete?()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -99,12 +102,20 @@ final class FirstLaunchPopout {
|
||||
|
||||
/// Minimal welcome callout — friendly pointer, not a wall of text.
|
||||
private struct WelcomeCalloutView: View {
|
||||
let countdownDuration: Int
|
||||
let onDismiss: () -> Void
|
||||
let onOpen: () -> Void
|
||||
@State private var countdown = 5
|
||||
@State private var countdown: Int
|
||||
@State private var timerTask: Task<Void, Never>?
|
||||
@State private var appeared = false
|
||||
|
||||
init(countdownDuration: Int, onDismiss: @escaping () -> Void, onOpen: @escaping () -> Void) {
|
||||
self.countdownDuration = countdownDuration
|
||||
self.onDismiss = onDismiss
|
||||
self.onOpen = onOpen
|
||||
self._countdown = State(initialValue: countdownDuration)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack(alignment: .top) {
|
||||
@@ -142,7 +153,7 @@ private struct WelcomeCalloutView: View {
|
||||
Spacer()
|
||||
|
||||
if countdown > 0 {
|
||||
Text("Opening in \(countdown)s")
|
||||
Text("Auto-opens in \(countdown)s")
|
||||
.font(.system(.caption2, design: .default))
|
||||
.foregroundColor(.secondary.opacity(0.6))
|
||||
.monospacedDigit()
|
||||
|
||||
Reference in New Issue
Block a user