From 1ad22b8697e725eed42eba50a0ac3acb3d0a5d9d Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Thu, 20 Aug 2026 20:26:09 +0800 Subject: [PATCH] fix(home): simplify transient Flow status card Show only connection state while Flow is preparing or unavailable, and remove the card entirely once the session is healthy. --- OSGKeyboard/Views/HomeView.swift | 178 ++++++------------ .../FlowHomePiPStatusPolicyTests.swift | 50 +++++ 2 files changed, 110 insertions(+), 118 deletions(-) diff --git a/OSGKeyboard/Views/HomeView.swift b/OSGKeyboard/Views/HomeView.swift index 1b69bdd..2bfb5eb 100644 --- a/OSGKeyboard/Views/HomeView.swift +++ b/OSGKeyboard/Views/HomeView.swift @@ -1,8 +1,8 @@ // HomeView.swift // OSGKeyboard · Main App // -// Home: logo, flow hints, usage stats, history + dictionary entry card, -// then engine / session status at the scroll bottom. History/dictionary +// Home: logo, transient Flow connection status, usage stats, history + +// dictionary entry card. History/dictionary // open via push (system back) rather than bottom-tab destinations. import OSGKeyboardShared @@ -74,6 +74,27 @@ enum FlowHomePiPStatusPolicy { guard case .failed = lifecycle else { return false } return !needsPermissionSetup } + + static func shouldShowConnectionCard( + lifecycle: FlowPiPLifecycleState, + needsPermissionSetup: Bool, + needsAPIKeySetup: Bool, + hasSessionWarning: Bool, + isRecording: Bool, + isProcessing: Bool, + isHostReady: Bool + ) -> Bool { + if needsPermissionSetup || needsAPIKeySetup || hasSessionWarning { + return true + } + if isRecording || isProcessing { + return false + } + if case .active = lifecycle { + return !isHostReady + } + return true + } } struct HomeView: View { @@ -84,7 +105,6 @@ struct HomeView: View { @ObservedObject private var config = ProviderConfig.shared @ObservedObject private var speechHistory = SpeechHistoryStore.shared @EnvironmentObject private var flowManager: FlowSessionManager - @State private var keyboardHintDismissed = HomeGuideState.isKeyboardHintDismissed @State private var micStatus = AppPermissions.micStatus @State private var speechStatus = AppPermissions.speechStatus @State private var path = NavigationPath() @@ -134,12 +154,18 @@ struct HomeView: View { } } - private var shouldShowKeyboardHint: Bool { - !keyboardHintDismissed - && !KeyboardSetupBridge.isReadyForOnboardingSkip - && !needsPermissionSetup - && flowManager.sessionWarning == nil - && !needsAPIKeySetup + /// Healthy sessions need no persistent chrome. Keep the connection card + /// only for setup, startup, recovery, or a genuinely unavailable session. + private var showsFlowConnectionCard: Bool { + FlowHomePiPStatusPolicy.shouldShowConnectionCard( + lifecycle: flowManager.pipLifecycleState, + needsPermissionSetup: needsPermissionSetup, + needsAPIKeySetup: needsAPIKeySetup, + hasSessionWarning: flowManager.sessionWarning != nil, + isRecording: flowManager.isUtteranceRecording, + isProcessing: flowManager.isUtteranceProcessing, + isHostReady: FlowSessionBridge.isHostReady() + ) } var body: some View { @@ -204,11 +230,13 @@ struct HomeView: View { .padding(.top, logoTopPadding) .padding(.bottom, logoBottomPadding) - // Engine + Flow status now stays directly below the - // logo so startup and recovery are always visible. - scrollStatusFooter - .padding(.horizontal, Spacing.lg) - .padding(.bottom, extrasBottomPadding) + // Connection status is transient: once Flow is ready, + // content moves up and the card disappears completely. + if showsFlowConnectionCard { + scrollStatusFooter + .padding(.horizontal, Spacing.lg) + .padding(.bottom, extrasBottomPadding) + } HomeUsageStatsSection(layout: .stacked, compact: isCompact) .padding(.horizontal, Spacing.lg) @@ -226,13 +254,10 @@ struct HomeView: View { } } - /// Engine + Flow status — visible near the logo instead of at scroll bottom. + /// Compact Flow connection status — no engine/model details. private var scrollStatusFooter: some View { setupGuidanceCard { - engineStatusLine flowStatusFooter - Divider() - flowSessionExtras } } @@ -243,7 +268,9 @@ struct HomeView: View { VStack(alignment: .leading, spacing: Spacing.lg) { wideHeroHeader - scrollStatusFooter + if showsFlowConnectionCard { + scrollStatusFooter + } HomeUsageStatsSection(layout: .split) @@ -494,8 +521,7 @@ struct HomeView: View { .fill(flowStatusColor) .frame(width: 6, height: 6) - if needsAPIKeySetup { - // 无按钮:引导卡片已提示去设置填 API Key。 + if needsPermissionSetup || needsAPIKeySetup { Text("home.flow.notReady") .font(TypeStyle.caption2) .foregroundStyle(palette.warning) @@ -532,8 +558,19 @@ struct HomeView: View { .minimumScaleFactor(0.85) } - if needsAPIKeySetup { - // 无按钮:引导卡片已提示去设置填 API Key。 + if needsPermissionSetup { + Button(action: handlePermissionGuidanceAction) { + Text( + AppPermissions.canRequestPermissionsInApp + ? "home.setup.permission.request" + : "home.flow.openSettings" + ) + .font(TypeStyle.caption2) + .foregroundStyle(palette.accent) + } + .buttonStyle(.plain) + .padding(.leading, Spacing.xs) + } else if needsAPIKeySetup { EmptyView() } else if canRetryPiP { Button { @@ -584,69 +621,6 @@ struct HomeView: View { .animation(Motion.soft, value: flowManager.isActive) } - // MARK: - Flow extras (warnings / hints) - - @ViewBuilder - private var flowSessionExtras: some View { - if needsPermissionSetup { - VStack(alignment: .leading, spacing: Spacing.sm) { - Text(AppPermissions.homePermissionGuidanceMessage) - .font(TypeStyle.caption2) - .foregroundStyle(palette.warning) - .fixedSize(horizontal: false, vertical: true) - Button(action: handlePermissionGuidanceAction) { - Text( - AppPermissions.canRequestPermissionsInApp - ? "home.setup.permission.request" - : "home.flow.openSettings" - ) - .font(TypeStyle.caption) - .foregroundStyle(palette.accent) - } - .buttonStyle(.plain) - } - } else if let warning = flowManager.sessionWarning { - VStack(alignment: .leading, spacing: Spacing.sm) { - Text(warning) - .font(TypeStyle.caption2) - .foregroundStyle(palette.warning) - .fixedSize(horizontal: false, vertical: true) - } - } else if needsAPIKeySetup { - VStack(alignment: .leading, spacing: Spacing.sm) { - Text(config.isLocalEngine - ? "home.setup.polishKeyMissing" - : "home.setup.cloudIncomplete") - .font(TypeStyle.caption2) - .foregroundStyle(palette.warning) - .fixedSize(horizontal: false, vertical: true) - } - } else if shouldShowKeyboardHint { - VStack(alignment: .leading, spacing: Spacing.sm) { - Text("home.setup.keyboardHint") - .font(TypeStyle.caption2) - .foregroundStyle(palette.textSecondary) - .fixedSize(horizontal: false, vertical: true) - Button { - keyboardHintDismissed = true - HomeGuideState.dismissKeyboardHint() - } label: { - Text("home.setup.keyboardHint.dismiss") - .font(TypeStyle.caption) - .foregroundStyle(palette.accent) - } - .buttonStyle(.plain) - } - } else { - Text("home.flow.hint") - .font(TypeStyle.caption2) - .foregroundStyle(palette.textTertiary) - .multilineTextAlignment(.center) - .frame(maxWidth: .infinity) - .padding(.horizontal, Spacing.sm) - } - } - private func setupGuidanceCard(@ViewBuilder content: () -> Content) -> some View { VStack(alignment: .leading, spacing: Spacing.sm) { content() @@ -696,22 +670,6 @@ struct HomeView: View { } } - private var engineStatusLine: some View { - Text( - EngineServiceLabel.summary( - engineMode: config.engineMode, - providerId: config.providerId, - model: config.model, - asrProviderId: config.asrProviderId, - asrModel: config.asrModel - ) - ) - .font(TypeStyle.caption2) - .foregroundStyle(palette.textSecondary) - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - } - /// Rows shown inside the history / dictionary preview cards. private static let libraryPreviewLimit = 3 @@ -722,19 +680,3 @@ struct HomeView: View { return formatter }() } - -// MARK: - Home guidance persistence - -private enum HomeGuideState { - private static let keyboardHintDismissedKey = "home.keyboardHintDismissed" - - static var isKeyboardHintDismissed: Bool { - guard AppGroup.isAvailable else { return false } - return AppGroup.defaults.bool(forKey: keyboardHintDismissedKey) - } - - static func dismissKeyboardHint() { - guard AppGroup.isAvailable else { return } - AppGroup.defaults.set(true, forKey: keyboardHintDismissedKey) - } -} diff --git a/OSGKeyboardTests/FlowHomePiPStatusPolicyTests.swift b/OSGKeyboardTests/FlowHomePiPStatusPolicyTests.swift index 783e49b..221b524 100644 --- a/OSGKeyboardTests/FlowHomePiPStatusPolicyTests.swift +++ b/OSGKeyboardTests/FlowHomePiPStatusPolicyTests.swift @@ -82,6 +82,40 @@ final class FlowHomePiPStatusPolicyTests: XCTestCase { ) } + func testConnectionCardDisappearsWhenSessionIsHealthy() { + XCTAssertFalse( + shouldShowConnectionCard( + lifecycle: .active, + isHostReady: true + ) + ) + } + + func testConnectionCardRemainsVisibleWhilePreparingOrFailed() { + XCTAssertTrue( + shouldShowConnectionCard( + lifecycle: .preparing(attempt: 1, total: 3), + isHostReady: false + ) + ) + XCTAssertTrue( + shouldShowConnectionCard( + lifecycle: .failed(.timedOut), + isHostReady: false + ) + ) + } + + func testNormalRecordingDoesNotBringConnectionCardBack() { + XCTAssertFalse( + shouldShowConnectionCard( + lifecycle: .active, + isRecording: true, + isHostReady: false + ) + ) + } + private func descriptor( for lifecycle: FlowPiPLifecycleState ) -> FlowHomePiPStatusDescriptor { @@ -94,4 +128,20 @@ final class FlowHomePiPStatusPolicyTests: XCTestCase { isHostReady: false ) } + + private func shouldShowConnectionCard( + lifecycle: FlowPiPLifecycleState, + isRecording: Bool = false, + isHostReady: Bool + ) -> Bool { + FlowHomePiPStatusPolicy.shouldShowConnectionCard( + lifecycle: lifecycle, + needsPermissionSetup: false, + needsAPIKeySetup: false, + hasSessionWarning: false, + isRecording: isRecording, + isProcessing: false, + isHostReady: isHostReady + ) + } }