feat: tab shell, home/onboarding UI, settings and keyboard polish
Introduce MainTabView with history and Liquid Glass dock; refresh home and onboarding layouts; unify accent green and provider localization; refine settings/API rows; redesign keyboard mic and flanking controls; set Utility iPhone-only targets and Flow session reliability fixes.
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
// HistoryView.swift
|
||||
// OSGKeyboard · Main App
|
||||
|
||||
import SwiftUI
|
||||
import OSGKeyboardShared
|
||||
|
||||
struct HistoryView: View {
|
||||
@Environment(\.themePalette) private var palette: ThemePalette
|
||||
@ObservedObject private var store = SpeechHistoryStore.shared
|
||||
@State private var showClearConfirm = false
|
||||
|
||||
private static let dayFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateStyle = .medium
|
||||
f.timeStyle = .none
|
||||
return f
|
||||
}()
|
||||
|
||||
private static let timeFormatter: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateStyle = .none
|
||||
f.timeStyle = .short
|
||||
return f
|
||||
}()
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
palette.background.ignoresSafeArea()
|
||||
|
||||
if store.entries.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
VStack(spacing: 0) {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: Spacing.xl) {
|
||||
ForEach(store.groupedByDay, id: \.day) { group in
|
||||
daySection(day: group.day, items: group.items)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, Spacing.md)
|
||||
.padding(.vertical, Spacing.md)
|
||||
.padding(.bottom, 100)
|
||||
}
|
||||
|
||||
clearFooter
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(LocalizedStringKey("history.title"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.confirmationDialog(
|
||||
LocalizedStringKey("history.clear.title"),
|
||||
isPresented: $showClearConfirm,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
Button(LocalizedStringKey("history.clear.confirm"), role: .destructive) {
|
||||
store.clearAll()
|
||||
}
|
||||
Button(LocalizedStringKey("common.cancel"), role: .cancel) {}
|
||||
} message: {
|
||||
Text("history.clear.message")
|
||||
}
|
||||
}
|
||||
|
||||
private var emptyState: some View {
|
||||
VStack(spacing: Spacing.sm) {
|
||||
Spacer()
|
||||
MaterialIcon(name: .menuBook, size: 36)
|
||||
.foregroundStyle(palette.textTertiary.opacity(0.5))
|
||||
Text("history.empty")
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, Spacing.xl)
|
||||
}
|
||||
|
||||
private func daySection(day: Date, items: [SpeechHistoryEntry]) -> some View {
|
||||
VStack(alignment: .leading, spacing: Spacing.sm) {
|
||||
Text(Self.dayFormatter.string(from: day))
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(items.enumerated()), id: \.element.id) { index, entry in
|
||||
historyRow(entry)
|
||||
if index < items.count - 1 {
|
||||
Divider().background(palette.divider)
|
||||
}
|
||||
}
|
||||
}
|
||||
.background(palette.surface, in: RoundedRectangle(cornerRadius: Radius.xl, style: .continuous))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
|
||||
.stroke(palette.divider, lineWidth: 0.5)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func historyRow(_ entry: SpeechHistoryEntry) -> some View {
|
||||
VStack(alignment: .leading, spacing: Spacing.xxs) {
|
||||
Text(Self.timeFormatter.string(from: entry.createdAt))
|
||||
.font(TypeStyle.caption2)
|
||||
.foregroundStyle(palette.textTertiary)
|
||||
.monospacedDigit()
|
||||
Text(entry.text)
|
||||
.font(TypeStyle.body)
|
||||
.foregroundStyle(palette.textPrimary)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(Spacing.md)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private var clearFooter: some View {
|
||||
Button(role: .destructive) {
|
||||
showClearConfirm = true
|
||||
} label: {
|
||||
Text("history.clear.button")
|
||||
.font(TypeStyle.caption)
|
||||
.foregroundStyle(palette.danger)
|
||||
.frame(maxWidth: .infinity, minHeight: 44)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.padding(.horizontal, Spacing.md)
|
||||
.padding(.bottom, Spacing.sm)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user