Add feedback for prompt change and power mode change
This commit is contained in:
parent
e52016c1e5
commit
45b9dcdc6e
@ -1,140 +0,0 @@
|
||||
# MiniRecorder Morphing Window Requirements
|
||||
|
||||
## 🎯 Core Requirements
|
||||
|
||||
### **Visual Behavior**
|
||||
1. **Visualizer Always Centered**: The audio visualizer must remain in the exact same position throughout all animations
|
||||
2. **Fixed Window Position**: The MiniRecorderView window position should never change during morphing
|
||||
3. **Horizontal-Only Expansion**: Only width should change, height remains constant at 34px
|
||||
4. **Hover-Triggered**: Expansion should occur on hover, collapse on hover exit
|
||||
|
||||
### **Layout States**
|
||||
|
||||
#### **Compact State (Default)**
|
||||
- **Width**: ~70px (just enough for visualizer + minimal padding)
|
||||
- **Content**: Audio visualizer/status display only
|
||||
- **Buttons**: Hidden/not rendered
|
||||
- **Centering**: Visualizer perfectly centered in compact window
|
||||
|
||||
#### **Expanded State (On Hover)**
|
||||
- **Width**: ~160px (current full width)
|
||||
- **Content**: RecorderPromptButton + Visualizer + RecorderPowerModeButton
|
||||
- **Buttons**: Fully visible and functional
|
||||
- **Centering**: Visualizer remains in same absolute screen position
|
||||
|
||||
## 🔧 Technical Constraints
|
||||
|
||||
### **Window Positioning**
|
||||
- Window's center point must remain constant
|
||||
- When expanding from 70px → 160px, window should grow equally left and right (45px each side)
|
||||
- `NSRect` calculations must account for center-anchored growth
|
||||
|
||||
### **Animation Requirements**
|
||||
- Smooth spring animation (~0.3-0.4s duration)
|
||||
- Buttons should appear/disappear gracefully (fade in/out or slide from edges)
|
||||
- No jarring movements or position jumps
|
||||
- Reversible animation (expand ↔ collapse)
|
||||
|
||||
### **SwiftUI Layout Considerations**
|
||||
- HStack with conditional button rendering
|
||||
- Visualizer maintains `frame(maxWidth: .infinity)` behavior in both states
|
||||
- Proper spacing and padding calculations for both states
|
||||
|
||||
## 💡 Recommended Implementation Strategy
|
||||
|
||||
### **Approach: Center-Anchored Window Growth with Sliding Buttons**
|
||||
|
||||
#### **Window Management (MiniRecorderPanel)**
|
||||
```
|
||||
Compact Window Rect:
|
||||
- Width: 70px
|
||||
- Height: 34px
|
||||
- X: screenCenter - 35px
|
||||
- Y: current Y position
|
||||
|
||||
Expanded Window Rect:
|
||||
- Width: 160px
|
||||
- Height: 34px
|
||||
- X: screenCenter - 80px // Grows left by 45px
|
||||
- Y: same Y position // Grows right by 45px
|
||||
```
|
||||
|
||||
#### **SwiftUI Layout (MiniRecorderView)**
|
||||
```
|
||||
HStack(spacing: 0) {
|
||||
// Left button - slides in from left edge
|
||||
if isExpanded {
|
||||
RecorderPromptButton()
|
||||
.transition(.move(edge: .leading).combined(with: .opacity))
|
||||
}
|
||||
|
||||
// Visualizer - always centered, never moves
|
||||
statusView
|
||||
.frame(width: visualizerWidth) // Fixed width
|
||||
|
||||
// Right button - slides in from right edge
|
||||
if isExpanded {
|
||||
RecorderPowerModeButton()
|
||||
.transition(.move(edge: .trailing).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### **State Management**
|
||||
- `@State private var isExpanded = false`
|
||||
- `@State private var isHovering = false`
|
||||
- Hover detection with debouncing for smooth UX
|
||||
- Window resize triggered by state changes
|
||||
|
||||
## 🎨 Animation Sequence
|
||||
|
||||
### **Expansion (Compact → Expanded)**
|
||||
1. **Trigger**: Mouse enters window bounds
|
||||
2. **Window**: Animate width 70px → 160px (center-anchored)
|
||||
3. **Buttons**: Slide in from edges with fade-in
|
||||
4. **Duration**: ~0.3s with spring easing
|
||||
5. **Result**: Visualizer appears unmoved, buttons visible
|
||||
|
||||
### **Collapse (Expanded → Compact)**
|
||||
1. **Trigger**: Mouse leaves window bounds (with delay)
|
||||
2. **Buttons**: Slide out to edges with fade-out
|
||||
3. **Window**: Animate width 160px → 70px (center-anchored)
|
||||
4. **Duration**: ~0.3s with spring easing
|
||||
5. **Result**: Back to visualizer-only, same position
|
||||
|
||||
## 🚫 Critical Don'ts
|
||||
|
||||
- **Never move the visualizer's absolute screen position**
|
||||
- **Never change the window's center point**
|
||||
- **Never animate height or vertical position**
|
||||
- **Never show jarring button pop-ins (use smooth transitions)**
|
||||
- **Never let buttons overlap the visualizer during animation**
|
||||
|
||||
## 📐 Calculations
|
||||
|
||||
### **Visualizer Dimensions**
|
||||
- AudioVisualizer: 12 bars × 3px + 11 × 2px spacing = 58px width
|
||||
- With padding: ~70px total compact width
|
||||
|
||||
### **Button Dimensions**
|
||||
- Each button: ~24px width + padding
|
||||
- Total button space: ~90px (45px per side)
|
||||
- Total expanded width: 70px + 90px = 160px
|
||||
|
||||
### **Center-Anchored Growth**
|
||||
```
|
||||
Compact X position: screenCenterX - 35px
|
||||
Expanded X position: screenCenterX - 80px
|
||||
Growth: 45px left + 45px right = 90px total
|
||||
```
|
||||
|
||||
## 🎯 Success Criteria
|
||||
|
||||
✅ **Visualizer never visually moves during any animation**
|
||||
✅ **Window position anchor point remains constant**
|
||||
✅ **Smooth hover-based expansion/collapse**
|
||||
✅ **Buttons appear/disappear gracefully**
|
||||
✅ **No layout jumps or glitches**
|
||||
✅ **Maintains current functionality in expanded state**
|
||||
|
||||
This approach ensures the visualizer appears completely stationary while the window "grows around it" to reveal the buttons, creating a seamless morphing effect.
|
||||
@ -9,4 +9,6 @@ extension Notification.Name {
|
||||
static let aiProviderKeyChanged = Notification.Name("aiProviderKeyChanged")
|
||||
static let licenseStatusChanged = Notification.Name("licenseStatusChanged")
|
||||
static let navigateToDestination = Notification.Name("navigateToDestination")
|
||||
static let promptSelectionChanged = Notification.Name("promptSelectionChanged")
|
||||
static let powerModeConfigurationApplied = Notification.Name("powerModeConfigurationApplied")
|
||||
}
|
||||
|
||||
@ -130,6 +130,10 @@ class PowerModeSessionManager {
|
||||
whisperState.currentTranscriptionModel?.name != modelName {
|
||||
await handleModelChange(to: selectedModel)
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
NotificationCenter.default.post(name: .powerModeConfigurationApplied, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreState(_ state: ApplicationState) async {
|
||||
|
||||
@ -46,6 +46,7 @@ class AIEnhancementService: ObservableObject {
|
||||
didSet {
|
||||
UserDefaults.standard.set(selectedPromptId?.uuidString, forKey: "selectedPromptId")
|
||||
NotificationCenter.default.post(name: .AppSettingsDidChange, object: nil)
|
||||
NotificationCenter.default.post(name: .promptSelectionChanged, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -50,28 +50,38 @@ struct MiniRecorderView: View {
|
||||
Group {
|
||||
if windowManager.isExpanded {
|
||||
RecorderPromptButton(showPopover: $showEnhancementPromptPopover)
|
||||
.padding(.leading, 3)
|
||||
.transition(.scale(scale: 0.5).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.frame(width: windowManager.isExpanded ? nil : 0)
|
||||
.frame(maxWidth: windowManager.isExpanded ? .infinity : 0)
|
||||
.clipped()
|
||||
.opacity(windowManager.isExpanded ? 1 : 0)
|
||||
.animation(.easeInOut(duration: 0.25), value: windowManager.isExpanded)
|
||||
|
||||
if windowManager.isExpanded {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// Fixed visualizer zone
|
||||
statusView
|
||||
.frame(maxWidth: .infinity)
|
||||
|
||||
if windowManager.isExpanded {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
// Right button zone
|
||||
Group {
|
||||
if windowManager.isExpanded {
|
||||
RecorderPowerModeButton(showPopover: $showPowerModePopover)
|
||||
.padding(.trailing, 3)
|
||||
.transition(.scale(scale: 0.5).combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.frame(width: windowManager.isExpanded ? nil : 0)
|
||||
.frame(maxWidth: windowManager.isExpanded ? .infinity : 0)
|
||||
.clipped()
|
||||
.opacity(windowManager.isExpanded ? 1 : 0)
|
||||
.animation(.easeInOut(duration: 0.25), value: windowManager.isExpanded)
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
|
||||
@ -26,11 +26,37 @@ class MiniWindowManager: ObservableObject {
|
||||
name: NSNotification.Name("HideMiniRecorder"),
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleFeedbackNotification),
|
||||
name: .promptSelectionChanged,
|
||||
object: nil
|
||||
)
|
||||
|
||||
NotificationCenter.default.addObserver(
|
||||
self,
|
||||
selector: #selector(handleFeedbackNotification),
|
||||
name: .powerModeConfigurationApplied,
|
||||
object: nil
|
||||
)
|
||||
}
|
||||
|
||||
@objc private func handleHideNotification() {
|
||||
hide()
|
||||
}
|
||||
|
||||
@objc private func handleFeedbackNotification() {
|
||||
guard isVisible, !isExpanded else { return }
|
||||
|
||||
expand()
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) {
|
||||
if self.isExpanded {
|
||||
self.collapse()
|
||||
}
|
||||
}
|
||||
}
|
||||
func show() {
|
||||
if isVisible { return }
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user