Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions App/BlurtiOS/BlurtDesign/DesignTokens.swift
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,18 @@ package enum DesignTokens {
package nonisolated static let termPad: CGFloat = 14
/// The key-term field: the brand's input corners (radius/input).
package nonisolated static let termRadius: CGFloat = 8
/// The typing field's border, orange once a word goes wrong.
package nonisolated static let typingFieldBorder: CGFloat = 2
/// Inside the typing field.
package nonisolated static let typingFieldPad: CGFloat = 12
/// Between the lines of words to type.
package nonisolated static let typingLineSpacing: CGFloat = 8
/// The countdown's thump in the last five seconds.
package nonisolated static let typingPulse: CGFloat = 1.06
/// Between the result's blocks.
package nonisolated static let typingResultGap: CGFloat = 20
/// The least room the words take, so the field below stays put.
package nonisolated static let typingWordsHeight: CGFloat = 180
package nonisolated static let voiceBarHeight: CGFloat = 32
/// The voice element's box in the voice bar — the most it takes (the ribs awake, the streak); the + sits beside what shows.
package nonisolated static let voiceBarWidth: CGFloat = 160
Expand Down Expand Up @@ -485,6 +497,20 @@ package enum DesignTokens {
package nonisolated static let sizeTerm: CGFloat = 17
/// The app's serif headline.
package nonisolated static let sizeTitle: CGFloat = 34
/// The typing test's countdown.
package nonisolated static let sizeTypingClock: CGFloat = 64
/// What you've typed.
package nonisolated static let sizeTypingField: CGFloat = 22
/// Its live words per minute.
package nonisolated static let sizeTypingLive: CGFloat = 28
/// The result's rank, in the serif.
package nonisolated static let sizeTypingRank: CGFloat = 28
/// The result's words per minute.
package nonisolated static let sizeTypingResult: CGFloat = 96
/// The result's WPM.
package nonisolated static let sizeTypingUnit: CGFloat = 16
/// The words to type.
package nonisolated static let sizeTypingWords: CGFloat = 24
/// Uppercase mono at 14: the brand's E2 tracking.
package nonisolated static let trackingCta: CGFloat = 1.4
/// Uppercase mono at 12: the brand's E1 tracking.
Expand Down Expand Up @@ -530,6 +556,10 @@ package enum DesignTokens {
package nonisolated static let stateFade: Double = 0.5
/// The voice bar becoming the field.
package nonisolated static let termSwap: Double = 0.4
/// The typing field's border turning, and the countdown's thump.
package nonisolated static let typingFlash: Double = 0.15
/// How often the typing test's clock and live speed redraw.
package nonisolated static let typingTick: Double = 0.1
/// The orb and the wave crossing, either way.
package nonisolated static let waveFade: Double = 0.7
}
Expand Down Expand Up @@ -745,6 +775,12 @@ package enum DesignTokens {
"metrics.term/inset": "2",
"metrics.term/pad": "14",
"metrics.term/radius": "8",
"metrics.typing/field-border": "2",
"metrics.typing/field-pad": "12",
"metrics.typing/line-spacing": "8",
"metrics.typing/pulse": "1.06",
"metrics.typing/result-gap": "20",
"metrics.typing/words-height": "180",
"metrics.voice/bar-height": "32",
"metrics.voice/bar-width": "160",
"metrics.voice/home-height": "112",
Expand Down Expand Up @@ -781,6 +817,13 @@ package enum DesignTokens {
"type.size/tab-label": "11",
"type.size/term": "17",
"type.size/title": "34",
"type.size/typing-clock": "64",
"type.size/typing-field": "22",
"type.size/typing-live": "28",
"type.size/typing-rank": "28",
"type.size/typing-result": "96",
"type.size/typing-unit": "16",
"type.size/typing-words": "24",
"type.tracking/cta": "1.4",
"type.tracking/eyebrow": "1.2",
"type.weight/glyph": "medium",
Expand All @@ -805,6 +848,8 @@ package enum DesignTokens {
"motion.sheen-working": "1.6",
"motion.state-fade": "0.5",
"motion.term-swap": "0.4",
"motion.typing-flash": "0.15",
"motion.typing-tick": "0.1",
"motion.wave-fade": "0.7",
]
}
Expand Down
3 changes: 3 additions & 0 deletions App/BlurtiOS/BlurtiOS/Sources/BlurtiOSApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ struct BlurtiOSApp: App {
probe.task { await start() }
} else if let voice = KeyboardGalleryView.VoiceStillView.parse(CommandLine.arguments) {
voice
} else if CommandLine.arguments.contains("-BlurtTypingTest") {
// The easter egg as the root, for a screenshot without a tap.
TypingTestView()
} else if CommandLine.arguments.contains("-BlurtSettings") {
// The settings sheet as the root, for a screenshot without a tap.
SettingsView(coordinator: coordinator)
Expand Down
28 changes: 23 additions & 5 deletions App/BlurtiOS/BlurtiOS/Sources/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ struct SettingsView: View {
VoiceElementKind.shipped.rawValue
@State private var showsKeyEntry = false
@State private var confirmsStatsReset = false
@State private var showsTypingTest = false
@AppStorage(TypingSpeed.defaultsKey) private var typingSpeed: Double = 0
@AppStorage(EnhancedTranscriptsStore.defaultsKey) private var enhancedTranscripts =
EnhancedTranscriptsStore.defaultValue

Expand All @@ -40,6 +42,14 @@ struct SettingsView: View {
ToolbarItem(placement: .confirmationAction) { Button("Done") { dismiss() } }
}
.sheet(isPresented: $showsKeyEntry) { KeyEntryView(apiKey: coordinator.apiKey) }
// On the form, not on a section: a section's modifiers apply to each of
// its rows, so a cover there was presented once per row at the same time.
.fullScreenCover(isPresented: $showsTypingTest) { TypingTestView() }
.confirmationDialog("Reset statistics?", isPresented: $confirmsStatsReset, titleVisibility: .visible) {
Button("Reset", role: .destructive) { DictationStats.reset() }
} message: {
Text("Your dictation, word and time-saved totals go back to zero.")
}
}
.tint(BlurtBrand.accent)
}
Expand Down Expand Up @@ -130,11 +140,6 @@ struct SettingsView: View {
} footer: {
Text("Dictations, words and time saved are counted on this phone and never sent.")
}
.confirmationDialog("Reset statistics?", isPresented: $confirmsStatsReset, titleVisibility: .visible) {
Button("Reset", role: .destructive) { DictationStats.reset() }
} message: {
Text("Your dictation, word and time-saved totals go back to zero.")
}
}

private var aboutSection: some View {
Expand All @@ -143,6 +148,19 @@ struct SettingsView: View {
Link("Blurt on GitHub", destination: URL(string: "https://github.com/AssemblyAI/blurt") ?? URL(filePath: "/"))
Link("AssemblyAI's privacy policy", destination: AIConsent.privacyPolicyURL)
Text("Powered by AssemblyAI").foregroundStyle(BlurtBrand.muted)
// The easter egg: a link, not a button, so it stays a find rather than
// another call to action. Its result becomes time saved's typing speed.
Button {
showsTypingTest = true
} label: {
Label("How fast can you type?", systemImage: "keyboard")
}
.foregroundStyle(BlurtBrand.accent)
if typingSpeed > 0 {
Button("Using your \(Int(typingSpeed)) wpm · Reset to average") { TypingSpeed.resetToAverage() }
.font(BlurtType.body(DesignTokens.Typography.sizeCaption))
.foregroundStyle(BlurtBrand.muted)
}
} header: {
Eyebrow("About")
}
Expand Down
5 changes: 4 additions & 1 deletion App/BlurtiOS/BlurtiOS/Sources/StatsCard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import SwiftUI
/// totals stay on the phone (`DictationStats`).
struct StatsCard: View {
@AppStorage(DictationStats.defaultsKey) private var raw = Data()
/// The speed time saved is measured against: the phone average, or the
/// easter egg's latest result (`TypingSpeed`).
@AppStorage(TypingSpeed.defaultsKey) private var typingSpeed: Double = 0

var body: some View {
let stats = DictationStats.decode(raw)
let saved = stats.timeSaved()
let saved = stats.timeSaved(typingWordsPerMinute: TypingSpeed.resolved(typingSpeed))
let dictations = [(StatFormat.count(stats.dictations), String?.none)]
let words = [(StatFormat.count(stats.words), String?.none)]
let timeSaved = StatFormat.durationParts(saved).map { ($0.value, Optional($0.unit)) }
Expand Down
Loading