Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **Screen Recording HUD**: Added a recording live activity with optional native stop controls and configurable hover presentation.

### Changed

### Fixed
- Fixed Dynamic Island window pinning so it stays anchored while switching macOS Spaces.

### Removed

Expand Down
109 changes: 98 additions & 11 deletions DynamicIsland/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ struct ContentView: View {
@Default(.enableDoNotDisturbDetection) var enableDoNotDisturbDetection
@Default(.showDoNotDisturbIndicator) var showDoNotDisturbIndicator
@Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection
@Default(.showRecordingIndicator) var showRecordingIndicator
@Default(.recordingHoverStyle) var recordingHoverStyle
@Default(.recordingControlMode) var recordingControlMode
@Default(.enableCapsLockIndicator) var enableCapsLockIndicator
@Default(.enableExtensionLiveActivities) var enableExtensionLiveActivities
@Default(.showStandardMediaControls) var showStandardMediaControls
Expand Down Expand Up @@ -123,6 +126,13 @@ struct ContentView: View {
: 460
return CGSize(width: max(baseSize.width, inlineWidth), height: baseSize.height)
}

if recordingLiveActivityVisibleOnClosedNotch {
return CGSize(
width: vm.closedNotchSize.width + recordingHUDExtraWidth,
height: vm.effectiveClosedNotchHeight + recordingHUDExtraHeight
)
}

// Handle battery HUD expansion sizing
if vm.notchState == .closed &&
Expand Down Expand Up @@ -187,6 +197,10 @@ struct ContentView: View {
let preferredHeight = extensionMinimalisticPreferredHeight(baseSize: baseSize) {
return CGSize(width: baseSize.width, height: preferredHeight)
}

if coordinator.currentView == .llmUsage {
return CGSize(width: baseSize.width, height: max(baseSize.height, llmUsageOpenNotchHeight))
}

guard coordinator.currentView == .stats else {
return baseSize
Expand Down Expand Up @@ -330,13 +344,15 @@ struct ContentView: View {
}

private func closedMusicPairingEligible(hasActiveMusicSnapshot: Bool) -> Bool {
vm.notchState == .closed
&& hasActiveMusicSnapshot
&& coordinator.musicLiveActivityEnabled
&& closedMusicContentEnabled
&& !vm.hideOnClosed
&& !lockScreenManager.isLocked
&& !isMusicHUDDeferredAfterUnlock
isClosedMusicPairingEligible(
notchState: vm.notchState,
hasActiveMusicSnapshot: hasActiveMusicSnapshot,
musicLiveActivityEnabled: coordinator.musicLiveActivityEnabled,
closedMusicContentEnabled: closedMusicContentEnabled,
hideOnClosed: vm.hideOnClosed,
isLocked: lockScreenManager.isLocked,
isDeferredAfterUnlock: isMusicHUDDeferredAfterUnlock
)
}

private var closedLiveActivitySwapTransition: AnyTransition {
Expand Down Expand Up @@ -460,6 +476,52 @@ struct ContentView: View {
isCurrentScreenExpansionVisible ? coordinator.expandingView.type : nil
}

private var hasActiveMusicSnapshotForClosedPairing: Bool {
if musicManager.isPlaying { return true }

let hasMusicMetadata = !musicManager.songTitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|| !musicManager.artistName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
return !musicManager.isPlayerIdle && hasMusicMetadata
}

private var recordingLiveActivityVisibleOnClosedNotch: Bool {
recordingHUDLayout.isVisible
}

private var recordingHUDLayout: RecordingHUDLayout {
makeRecordingHUDLayout(
notchState: vm.notchState,
screenRecordingDetectionEnabled: enableScreenRecordingDetection,
showRecordingIndicator: showRecordingIndicator,
hideOnClosed: vm.hideOnClosed,
isRecording: recordingManager.isRecording,
closedMusicPairingEligible: closedMusicPairingEligible(
hasActiveMusicSnapshot: hasActiveMusicSnapshotForClosedPairing
),
recordingControlMode: recordingControlMode,
canStopFromHUD: recordingManager.canStopFromHUD,
enableMinimalisticUI: enableMinimalisticUI,
recordingHoverStyle: recordingHoverStyle,
expanded: isHovering
)
}

private var recordingHUDDefaultExpandedOnHover: Bool {
recordingHUDLayout.showsDefaultExpansion
}

private var recordingHUDInlineExpandedOnHover: Bool {
recordingHUDLayout.showsInlineExpansion
}

private var recordingHUDExtraWidth: CGFloat {
recordingHUDLayout.extraWidth
}

private var recordingHUDExtraHeight: CGFloat {
recordingHUDLayout.extraHeight
}

private var displayedBatteryHUDLevel: Int {
let resolvedLevel = batteryModel.activeTemporaryHUDLevelOverride
?? Int(batteryModel.levelBattery.rounded())
Expand Down Expand Up @@ -493,6 +555,21 @@ struct ContentView: View {
}
}

private var activeClosedRecordingSurfaceShape: AnyShape? {
guard recordingHUDDefaultExpandedOnHover else { return nil }

if isDynamicIslandMode {
return AnyShape(DynamicIslandPillShape(cornerRadius: dynamicIslandPillCornerRadiusInsets.opened))
}

return AnyShape(
NotchShape(
topCornerRadius: activeCornerRadiusInsets.closed.top,
bottomCornerRadius: 40
)
)
}

private func resolvedBatteryNotificationStyle(for kind: BatteryTemporaryHUDKind) -> BatteryNotificationStyle {
switch kind {
case .charging:
Expand All @@ -508,6 +585,9 @@ struct ContentView: View {
/// Resolves the clip/content shape per-screen: pill on non-notch screens
/// when dynamic island mode is active, standard notch shape otherwise.
private var resolvedClipShape: AnyShape {
if let activeClosedRecordingSurfaceShape {
return activeClosedRecordingSurfaceShape
}
if let activeClosedBatterySurfaceShape {
return activeClosedBatterySurfaceShape
}
Expand Down Expand Up @@ -571,6 +651,7 @@ struct ContentView: View {
handleHover(hovering)
}
.onTapGesture {
guard !recordingOpenGestureLocked else { return }
if handleClosedMusicWaveformTapIfNeeded() {
return
}
Expand Down Expand Up @@ -946,8 +1027,8 @@ struct ContentView: View {
TimerLiveActivity()
} else if (!isCurrentScreenExpansionVisible || currentScreenExpansionType == .reminder) && vm.notchState == .closed && reminderManager.isActive && enableReminderLiveActivity && !vm.hideOnClosed {
ReminderLiveActivity()
} else if (!isCurrentScreenExpansionVisible || currentScreenExpansionType == .recording) && vm.notchState == .closed && (recordingManager.isRecording || !recordingManager.isRecorderIdle) && Defaults[.enableScreenRecordingDetection] && !vm.hideOnClosed && !musicPairingEligible {
RecordingLiveActivity()
} else if (!isCurrentScreenExpansionVisible || currentScreenExpansionType == .recording) && vm.notchState == .closed && recordingManager.isRecording && Defaults[.enableScreenRecordingDetection] && Defaults[.showRecordingIndicator] && !vm.hideOnClosed && !musicPairingEligible {
RecordingLiveActivity(hoverAnimation: $isHovering, gestureProgress: $gestureProgress)
} else if (!isCurrentScreenExpansionVisible || currentScreenExpansionType == .download) && vm.notchState == .closed && downloadManager.isDownloading && Defaults[.enableDownloadListener] && !vm.hideOnClosed {
DownloadLiveActivity()
.transition(.blurReplace.animation(.interactiveSpring(dampingFraction: 1.2)))
Expand Down Expand Up @@ -1313,7 +1394,7 @@ struct ContentView: View {
return .reminder(reminder)
}

if enableScreenRecordingDetection && (recordingManager.isRecording || !recordingManager.isRecorderIdle) {
if enableScreenRecordingDetection && showRecordingIndicator && recordingManager.isRecording {
return .recording
}

Expand Down Expand Up @@ -2099,6 +2180,7 @@ struct ContentView: View {
await MainActor.run {
guard self.vm.notchState == .closed,
self.isHovering,
!self.recordingLiveActivityVisibleOnClosedNotch,
!self.isSneakPeekVisibleOnCurrentScreen,
!self.coordinator.isHoverOpenSuppressed else { return }

Expand Down Expand Up @@ -2278,6 +2360,7 @@ struct ContentView: View {

private func handleOpenScrollGesture(translation: CGFloat, phase: NSEvent.Phase) {
guard vm.notchState == .closed else { return }
guard !recordingOpenGestureLocked else { return }

withAnimation(.smooth) {
gestureProgress = (translation / Defaults[.gestureSensitivity]) * 20
Expand All @@ -2300,6 +2383,10 @@ struct ContentView: View {
}
}

private var recordingOpenGestureLocked: Bool {
recordingLiveActivityVisibleOnClosedNotch
}

private func handleCloseScrollGesture(translation: CGFloat, phase: NSEvent.Phase) {
guard vm.notchState == .open, !vm.isHoveringCalendar, !vm.isScrollGestureActive else { return }

Expand Down Expand Up @@ -2632,7 +2719,7 @@ struct ContentView: View {

private func hideMusicControlWindow() {}
#endif

private func shouldFixSizeForSneakPeek() -> Bool {
guard isSneakPeekVisibleOnCurrentScreen else { return false }
let style = resolvedSneakPeekStyle()
Expand Down
Loading
Loading