improve: enhance dashboard styling and metrics - Add Words/Minute and Words/Session metrics, improve card designs, update TimeEfficiencyView, ensure consistent styling
This commit is contained in:
parent
49aacc8cd6
commit
09b73a350c
@ -3,15 +3,31 @@ import SwiftUI
|
|||||||
struct MetricCard: View {
|
struct MetricCard: View {
|
||||||
let title: String
|
let title: String
|
||||||
let value: String
|
let value: String
|
||||||
|
let icon: String
|
||||||
|
let color: Color
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 16) {
|
||||||
Text(title)
|
HStack(spacing: 12) {
|
||||||
.font(.headline)
|
// Icon
|
||||||
.foregroundColor(.secondary)
|
Image(systemName: icon)
|
||||||
Text(value)
|
.font(.system(size: 24))
|
||||||
.font(.title)
|
.foregroundColor(color)
|
||||||
.fontWeight(.bold)
|
.frame(width: 32, height: 32)
|
||||||
|
.background(
|
||||||
|
Circle()
|
||||||
|
.fill(color.opacity(0.1))
|
||||||
|
)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Text(title)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
Text(value)
|
||||||
|
.font(.system(size: 24, weight: .bold, design: .rounded))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.padding()
|
.padding()
|
||||||
|
|||||||
@ -38,8 +38,30 @@ struct MetricsContent: View {
|
|||||||
|
|
||||||
private var metricsGrid: some View {
|
private var metricsGrid: some View {
|
||||||
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 20) {
|
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 20) {
|
||||||
MetricCard(title: "Words Captured", value: "\(totalWordsTranscribed)")
|
MetricCard(
|
||||||
MetricCard(title: "Voice-to-Text Sessions", value: "\(transcriptions.count)")
|
title: "Words Captured",
|
||||||
|
value: "\(totalWordsTranscribed)",
|
||||||
|
icon: "text.word.spacing",
|
||||||
|
color: .blue
|
||||||
|
)
|
||||||
|
MetricCard(
|
||||||
|
title: "Voice-to-Text Sessions",
|
||||||
|
value: "\(transcriptions.count)",
|
||||||
|
icon: "mic.circle.fill",
|
||||||
|
color: .green
|
||||||
|
)
|
||||||
|
MetricCard(
|
||||||
|
title: "Average Words/Minute",
|
||||||
|
value: String(format: "%.1f", averageWordsPerMinute),
|
||||||
|
icon: "speedometer",
|
||||||
|
color: .orange
|
||||||
|
)
|
||||||
|
MetricCard(
|
||||||
|
title: "Words/Session",
|
||||||
|
value: String(format: "%.1f", averageWordsPerSession),
|
||||||
|
icon: "chart.bar.fill",
|
||||||
|
color: .purple
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,4 +139,15 @@ struct MetricsContent: View {
|
|||||||
|
|
||||||
return dailyData.reversed()
|
return dailyData.reversed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add computed properties for new metrics
|
||||||
|
private var averageWordsPerMinute: Double {
|
||||||
|
guard totalRecordedTime > 0 else { return 0 }
|
||||||
|
return Double(totalWordsTranscribed) / (totalRecordedTime / 60.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var averageWordsPerSession: Double {
|
||||||
|
guard !transcriptions.isEmpty else { return 0 }
|
||||||
|
return Double(totalWordsTranscribed) / Double(transcriptions.count)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,8 +45,9 @@ struct TimeEfficiencyView: View {
|
|||||||
bottomSection
|
bottomSection
|
||||||
}
|
}
|
||||||
.padding(.vertical, 24)
|
.padding(.vertical, 24)
|
||||||
.background(backgroundDesign)
|
.background(Color(.controlBackgroundColor))
|
||||||
.overlay(borderOverlay)
|
.cornerRadius(10)
|
||||||
|
.shadow(radius: 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Subviews
|
// MARK: - Subviews
|
||||||
@ -154,34 +155,6 @@ struct TimeEfficiencyView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extension to allow hex color initialization
|
|
||||||
|
|
||||||
// MARK: - Styling Views
|
|
||||||
|
|
||||||
private var backgroundDesign: some View {
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.fill(Color(nsColor: .controlBackgroundColor))
|
|
||||||
}
|
|
||||||
|
|
||||||
private var borderOverlay: some View {
|
|
||||||
RoundedRectangle(cornerRadius: 12)
|
|
||||||
.stroke(
|
|
||||||
.linearGradient(
|
|
||||||
colors: [
|
|
||||||
Color(nsColor: .controlAccentColor).opacity(0.2),
|
|
||||||
Color.clear,
|
|
||||||
Color.clear,
|
|
||||||
Color(nsColor: .controlAccentColor).opacity(0.1)
|
|
||||||
],
|
|
||||||
startPoint: .topLeading,
|
|
||||||
endPoint: .bottomTrailing
|
|
||||||
),
|
|
||||||
lineWidth: 1
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private var efficiencyGradient: LinearGradient {
|
private var efficiencyGradient: LinearGradient {
|
||||||
LinearGradient(
|
LinearGradient(
|
||||||
|
|||||||
@ -43,7 +43,7 @@ struct MetricsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color(.windowBackgroundColor))
|
.background(Color(.controlBackgroundColor))
|
||||||
.task {
|
.task {
|
||||||
// Ensure the model context is ready
|
// Ensure the model context is ready
|
||||||
hasLoadedData = true
|
hasLoadedData = true
|
||||||
|
|||||||
@ -661,22 +661,31 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
|
|||||||
await toggleRecord()
|
await toggleRecord()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Start a parallel task for both UI and recording
|
// Serialize audio operations to prevent deadlocks
|
||||||
Task {
|
Task {
|
||||||
// Play start sound first
|
do {
|
||||||
SoundManager.shared.playStartSound()
|
// First start the audio engine
|
||||||
|
await MainActor.run {
|
||||||
// Start audio engine immediately - this can happen in parallel
|
audioEngine.startAudioEngine()
|
||||||
audioEngine.startAudioEngine()
|
}
|
||||||
|
|
||||||
// Show UI (this is quick now that we removed animations)
|
// Small delay to ensure audio system is ready
|
||||||
await MainActor.run {
|
try await Task.sleep(nanoseconds: 50_000_000) // 50ms
|
||||||
showRecorderPanel() // Modified version that doesn't start audio engine
|
|
||||||
isMiniRecorderVisible = true
|
// Now play the sound
|
||||||
|
SoundManager.shared.playStartSound()
|
||||||
|
|
||||||
|
// Show UI
|
||||||
|
await MainActor.run {
|
||||||
|
showRecorderPanel()
|
||||||
|
isMiniRecorderVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally start recording
|
||||||
|
await toggleRecord()
|
||||||
|
} catch {
|
||||||
|
logger.error("Error during recorder initialization: \(error)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start recording (this will happen in parallel with UI showing)
|
|
||||||
await toggleRecord()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user