feat: shimmering generate button (#49)

This commit is contained in:
Owen Gretzinger
2025-07-29 14:36:24 -04:00
committed by GitHub
parent 2b7940fc30
commit fb827dec2c
2 changed files with 130 additions and 82 deletions
@@ -26,6 +26,13 @@ class MeetingViewModel: ObservableObject {
@Published var isValidatingKey = false // Indicates API key validation in progress @Published var isValidatingKey = false // Indicates API key validation in progress
@Published var isStartingRecording = false // Indicates recording start in progress @Published var isStartingRecording = false // Indicates recording start in progress
// Computed property to determine if Generate button should animate
var shouldAnimateGenerateButton: Bool {
let generateButtonEnabled = !meeting.transcript.isEmpty && !isGeneratingNotes && !isRecording && !isStartingRecording
let noEnhancedNotesYet = meeting.generatedNotes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
return generateButtonEnabled && noEnhancedNotesYet
}
// Computed property that always uses the direct RecordingSessionManager check // Computed property that always uses the direct RecordingSessionManager check
var isRecording: Bool { var isRecording: Bool {
return recordingSessionManager.isRecordingMeeting(meeting.id) return recordingSessionManager.isRecordingMeeting(meeting.id)
+43 -2
View File
@@ -241,6 +241,8 @@ struct MeetingDetailContentView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 20) { VStack(alignment: .leading, spacing: 20) {
VStack(alignment: .leading, spacing: 8) {
// Meeting Title with Menu // Meeting Title with Menu
HStack { HStack {
TextField("Meeting Title", text: $viewModel.meeting.title) TextField("Meeting Title", text: $viewModel.meeting.title)
@@ -283,7 +285,7 @@ struct MeetingDetailContentView: View {
Spacer() Spacer()
// Right: Generate and Recording Buttons // Right: Generate and Recording Buttons
HStack(spacing: 12) { HStack(spacing: 8) {
// Generate Button (Dropdown) // Generate Button (Dropdown)
Menu { Menu {
ForEach(viewModel.templates) { template in ForEach(viewModel.templates) { template in
@@ -308,6 +310,15 @@ struct MeetingDetailContentView: View {
.frame(minWidth: 110, minHeight: 36) .frame(minWidth: 110, minHeight: 36)
.background(Color.green.opacity(0.1)) .background(Color.green.opacity(0.1))
.cornerRadius(8) .cornerRadius(8)
.overlay(
// Shimmer overlay when ready
Group {
if viewModel.shouldAnimateGenerateButton {
ShimmerOverlay()
.clipShape(RoundedRectangle(cornerRadius: 8))
}
}
)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
.disabled(viewModel.meeting.transcript.isEmpty || viewModel.isGeneratingNotes || viewModel.isRecording || viewModel.isStartingRecording) .disabled(viewModel.meeting.transcript.isEmpty || viewModel.isGeneratingNotes || viewModel.isRecording || viewModel.isStartingRecording)
@@ -331,11 +342,12 @@ struct MeetingDetailContentView: View {
.help(cannotStartRecording ? "Another meeting is currently being recorded" : "Start or stop recording for this meeting") .help(cannotStartRecording ? "Another meeting is currently being recorded" : "Start or stop recording for this meeting")
} }
} }
}
// Content Area with Tab-specific Headers // Content Area with Tab-specific Headers
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
// Tab Header with Copy and Edit buttons // Tab Header with Copy and Edit buttons
HStack { HStack(spacing: 8) {
Text(viewModel.selectedTab.rawValue) Text(viewModel.selectedTab.rawValue)
.font(.headline) .font(.headline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@@ -489,6 +501,35 @@ struct MeetingDetailContentView: View {
} }
} }
// MARK: - Shimmer Overlay
struct ShimmerOverlay: View {
@State private var animate: Bool = false
var body: some View {
GeometryReader { geo in
let width = geo.size.width
let height = geo.size.height
RoundedRectangle(cornerRadius: 8)
.fill(
LinearGradient(
gradient: Gradient(colors: [Color.clear, Color.green.opacity(0.1), Color.clear]),
startPoint: UnitPoint(x: animate ? 2.5 : -1, y: 0.5),
endPoint: UnitPoint(x: animate ? 3.5 : 0, y: 0.5)
)
)
.frame(width: width, height: height)
.onAppear {
animate = true
}
.animation(
Animation.linear(duration: 1.5).repeatForever(autoreverses: false),
value: animate
)
}
.allowsHitTesting(false)
}
}
#Preview { #Preview {
MeetingListView(settingsViewModel: SettingsViewModel()) MeetingListView(settingsViewModel: SettingsViewModel())
} }