feat(app): add flow diagnostics and refine interface
CI / Validate manifests (push) Has been cancelled
CI / SwiftLint (push) Has been cancelled
CI / iOS / Extension (push) Has been cancelled
CI / macOS (push) Has been cancelled

Improve failure recovery visibility, strengthen account-session handling, and make the cross-platform interface more consistent.
This commit is contained in:
Rocky
2026-08-26 14:40:06 +08:00
parent 1ee032bdb9
commit 39002336c0
78 changed files with 2737 additions and 735 deletions
@@ -1,7 +1,7 @@
// MonthlyUsageCalendar.swift
// OSGKeyboard · HostSupport
//
// Current-month dictation heat map. Every date remains visible; the accent
// Current-month dictation heat map. Every date remains visible; the monochrome
// circle's opacity communicates that day's character count relative to the
// busiest day in the same month.
@@ -158,21 +158,21 @@ public struct MonthlyUsageCalendar: View {
let isToday = calendar.isDateInToday(point.date)
let isFuture = calendar.startOfDay(for: point.date) > calendar.startOfDay(for: Date())
let opacity = fillOpacity(for: point.value, isFuture: isFuture)
let usesLightText = opacity >= 0.48
let usesContrastingText = opacity >= 0.48
return Text(day.formatted())
.font(.system(size: compact ? 12 : 13, weight: isToday ? .semibold : .medium, design: .rounded))
.foregroundStyle(
isFuture
? palette.textTertiary.opacity(0.45)
: (usesLightText ? palette.textOnAccent : palette.textPrimary)
: (usesContrastingText ? palette.background : palette.textPrimary)
)
.frame(width: cellDiameter, height: cellDiameter)
.background(palette.accent.opacity(opacity), in: Circle())
.background(palette.textPrimary.opacity(opacity), in: Circle())
.overlay {
if isToday {
Circle()
.stroke(palette.accent, lineWidth: 1.5)
.stroke(palette.textPrimary, lineWidth: 1.5)
}
}
.frame(maxWidth: .infinity)
@@ -188,7 +188,7 @@ public struct MonthlyUsageCalendar: View {
private func fillOpacity(for value: Int, isFuture: Bool) -> Double {
guard value > 0, !isFuture else { return 0 }
let ratio = min(max(Double(value) / Double(maximumValue), 0), 1)
return 0.14 + sqrt(ratio) * 0.62
return 0.05 + pow(ratio, 1.15) * 0.73
}
}
@@ -112,7 +112,7 @@ public struct SupportDeveloperSection: View {
}
.frame(maxWidth: .infinity)
.padding(.vertical, Spacing.sm)
.foregroundStyle(.white)
.foregroundStyle(OSGColor.fixedLightContent)
.background(palette.accent, in: RoundedRectangle(cornerRadius: Radius.large, style: .continuous))
}
.buttonStyle(.plain)
@@ -223,12 +223,12 @@ public struct UsageStatsCluster<Header: View>: View {
VStack(spacing: CardLayoutMetrics.compactItemSpacing) {
HStack(spacing: CardLayoutMetrics.compactItemSpacing) {
compactCell(
systemImage: "waveform.badge.microphone",
systemImage: "timer",
value: UsageStatisticsStore.formatDuration(dictationDurationSeconds, language: language),
label: SharedL10n.string("stat.dictationTime", language: language)
)
compactCell(
systemImage: "text.quote",
systemImage: "quote.bubble",
value: UsageStatisticsStore.formatCount(dictationCharacterCount, language: language),
label: SharedL10n.string("stat.words", language: language),
action: onOpenHistory
@@ -242,7 +242,7 @@ public struct UsageStatsCluster<Header: View>: View {
label: SharedL10n.string("stat.translation", language: language)
)
compactCell(
systemImage: "books.vertical",
systemImage: "book.pages",
value: UsageStatisticsStore.formatCount(dictionaryTermCount, language: language),
label: SharedL10n.string("stat.dictionary", language: language),
action: onOpenDictionary
@@ -292,9 +292,9 @@ public struct UsageStatsCluster<Header: View>: View {
return ZStack(alignment: .bottomTrailing) {
// Subtle watermark stays fully visible within the card edges.
Image(systemName: systemImage)
.font(.system(size: 26, weight: .semibold))
.foregroundStyle(palette.textPrimary.opacity(0.06))
.frame(width: 32, height: 32, alignment: .center)
.font(.system(size: 38, weight: .ultraLight))
.foregroundStyle(palette.textPrimary.opacity(0.10))
.frame(width: 44, height: 44, alignment: .center)
.offset(x: Spacing.xs, y: Spacing.xs)
VStack(alignment: .leading, spacing: Spacing.xxs) {
@@ -323,6 +323,7 @@ public struct UsageStatsCluster<Header: View>: View {
}
}
.clipShape(shape)
.cardElevation()
}
private var disclosureIndicator: some View {
@@ -539,22 +539,18 @@ public actor AccountAPIClient {
}
private func clearSession() async throws {
cachedSession = nil
didLoadSession = true
var storageFailed = false
do {
try await sessionVault.clearSession()
} catch {
storageFailed = true
}
do {
try await sessionVault.clearRefreshTransaction()
} catch {
storageFailed = true
}
if storageFailed {
// Keep the cached session aligned with Keychain so a transient
// deletion failure can be retried instead of resurrecting on launch.
throw AccountAPIError.secureStorage
}
cachedSession = nil
didLoadSession = true
// A stale operation identifier is harmless once its refresh token is
// gone; do not turn a successful credential purge into a failed logout.
try? await sessionVault.clearRefreshTransaction()
}
private func invalidateSession(ifAccessTokenMatches expectedAccessToken: String) async throws {
@@ -562,12 +558,7 @@ public actor AccountAPIClient {
current.accessToken == expectedAccessToken else {
return
}
do {
try await clearSession()
} catch {
publishSessionInvalidation()
throw error
}
try await clearSession()
publishSessionInvalidation()
}