Update fix for Sneak Peek

This commit is contained in:
Hariharan Mudaliar 2026-01-18 19:40:05 +05:30
commit a4e58b9e70
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
3 changed files with 137 additions and 183 deletions

View file

@ -13,6 +13,7 @@ import Foundation
import KeyboardShortcuts import KeyboardShortcuts
import SwiftUI import SwiftUI
import SwiftUIIntrospect import SwiftUIIntrospect
import AtollExtensionKit
#if canImport(AppKit) #if canImport(AppKit)
import AppKit import AppKit
#elseif canImport(UIKit) #elseif canImport(UIKit)
@ -563,13 +564,10 @@ struct ContentView: View {
let inlinePayload = extensionLiveActivityManager.payload(bundleIdentifier: bundleID, activityID: activityID) let inlinePayload = extensionLiveActivityManager.payload(bundleIdentifier: bundleID, activityID: activityID)
ExtensionInlineSneakPeekView( ExtensionInlineSneakPeekView(
payload: inlinePayload, payload: inlinePayload,
title: coordinator.sneakPeek.title,
subtitle: coordinator.sneakPeek.subtitle,
accentColor: coordinator.sneakPeek.accentColor ?? .gray, accentColor: coordinator.sneakPeek.accentColor ?? .gray,
notchHeight: vm.effectiveClosedNotchHeight, notchHeight: vm.effectiveClosedNotchHeight,
closedNotchWidth: vm.closedNotchSize.width, closedNotchWidth: vm.closedNotchSize.width,
isHovering: isHovering, isHovering: isHovering
gestureProgress: gestureProgress
) )
.transition(AnyTransition.move(edge: .trailing).combined(with: .opacity)) .transition(AnyTransition.move(edge: .trailing).combined(with: .opacity))
} else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && (coordinator.sneakPeek.type != .volume || vm.notchState == .closed) { } else if coordinator.sneakPeek.show && Defaults[.inlineHUD] && (coordinator.sneakPeek.type != .music) && (coordinator.sneakPeek.type != .battery) && (coordinator.sneakPeek.type != .timer) && (coordinator.sneakPeek.type != .reminder) && (coordinator.sneakPeek.type != .volume || vm.notchState == .closed) {
@ -600,12 +598,10 @@ struct ContentView: View {
} else if (!coordinator.expandingView.show || coordinator.expandingView.type == .privacy) && vm.notchState == .closed && privacyManager.hasAnyIndicator && (Defaults[.enableCameraDetection] || Defaults[.enableMicrophoneDetection]) && !vm.hideOnClosed { } else if (!coordinator.expandingView.show || coordinator.expandingView.type == .privacy) && vm.notchState == .closed && privacyManager.hasAnyIndicator && (Defaults[.enableCameraDetection] || Defaults[.enableMicrophoneDetection]) && !vm.hideOnClosed {
PrivacyLiveActivity() PrivacyLiveActivity()
} else if let extensionPayload = extensionStandalonePayload { } else if let extensionPayload = extensionStandalonePayload {
let suppressingCenterText = shouldSuppressExtensionCenter(for: extensionPayload)
let layout = extensionStandaloneLayout( let layout = extensionStandaloneLayout(
for: extensionPayload, for: extensionPayload,
notchHeight: vm.effectiveClosedNotchHeight, notchHeight: vm.effectiveClosedNotchHeight,
isHovering: isHovering, isHovering: isHovering
suppressingCenterText: suppressingCenterText
) )
ExtensionLiveActivityStandaloneView( ExtensionLiveActivityStandaloneView(
payload: extensionPayload, payload: extensionPayload,
@ -676,25 +672,31 @@ struct ContentView: View {
} }
} }
// Extension live activity sneak peek // Extension live activity sneak peek
else if case .extensionLiveActivity = coordinator.sneakPeek.type { else if case let .extensionLiveActivity(bundleID, activityID) = coordinator.sneakPeek.type {
if !vm.hideOnClosed && activeSneakPeekStyle == .standard { if !vm.hideOnClosed && activeSneakPeekStyle == .standard {
HStack(alignment: .center, spacing: 6) { let payload = extensionLiveActivityManager.payload(bundleIdentifier: bundleID, activityID: activityID)
if !coordinator.sneakPeek.icon.isEmpty { let descriptor = payload?.descriptor
Image(systemName: coordinator.sneakPeek.icon) let accent = (descriptor?.accentColor.swiftUIColor ?? coordinator.sneakPeek.accentColor ?? .gray)
} .ensureMinimumBrightness(factor: 0.7)
GeometryReader { geo in GeometryReader { geo in
let combinedText = coordinator.sneakPeek.subtitle.isEmpty ? HStack(spacing: 6) {
coordinator.sneakPeek.title : RoundedRectangle(cornerRadius: 2)
"\(coordinator.sneakPeek.title) - \(coordinator.sneakPeek.subtitle)" .fill(accent)
.frame(width: 8, height: 12)
MarqueeText( MarqueeText(
.constant(combinedText), .constant(
textColor: coordinator.sneakPeek.accentColor ?? .gray, extensionSneakPeekText(
preferredTitle: coordinator.sneakPeek.title,
preferredSubtitle: coordinator.sneakPeek.subtitle,
descriptor: descriptor
)
),
textColor: accent,
minDuration: 1, minDuration: 1,
frameWidth: geo.size.width frameWidth: max(0, geo.size.width - 14)
) )
} }
} }
.foregroundStyle(coordinator.sneakPeek.accentColor ?? .gray)
.padding(.bottom, 10) .padding(.bottom, 10)
} }
} }
@ -776,6 +778,19 @@ struct ContentView: View {
} }
} }
private func extensionSneakPeekText(preferredTitle: String, preferredSubtitle: String?, descriptor: AtollLiveActivityDescriptor?) -> String {
let trimmedPreferredTitle = preferredTitle.trimmingCharacters(in: .whitespacesAndNewlines)
let descriptorTitle = descriptor?.title.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Extension"
let title = trimmedPreferredTitle.isEmpty ? descriptorTitle : trimmedPreferredTitle
let trimmedPreferredSubtitle = preferredSubtitle?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let descriptorSubtitle = descriptor?.subtitle?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
let subtitle = !trimmedPreferredSubtitle.isEmpty ? trimmedPreferredSubtitle : descriptorSubtitle
guard !subtitle.isEmpty else { return title }
return "\(title) • \(subtitle)"
}
private let reminderTimeFormatter: DateFormatter = { private let reminderTimeFormatter: DateFormatter = {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.dateStyle = .none formatter.dateStyle = .none
@ -1286,7 +1301,7 @@ struct ContentView: View {
return payload return payload
} }
private func extensionStandaloneLayout(for payload: ExtensionLiveActivityPayload, notchHeight: CGFloat, isHovering: Bool, suppressingCenterText: Bool) -> ExtensionStandaloneLayout { private func extensionStandaloneLayout(for payload: ExtensionLiveActivityPayload, notchHeight: CGFloat, isHovering: Bool) -> ExtensionStandaloneLayout {
let outerHeight = notchHeight let outerHeight = notchHeight
let contentHeight = max(0, notchHeight - (isHovering ? 0 : 12)) let contentHeight = max(0, notchHeight - (isHovering ? 0 : 12))
let leadingWidth = max(contentHeight, 44) let leadingWidth = max(contentHeight, 44)
@ -1303,15 +1318,10 @@ struct ContentView: View {
contentHeight: contentHeight, contentHeight: contentHeight,
leadingWidth: leadingWidth, leadingWidth: leadingWidth,
centerWidth: centerWidth, centerWidth: centerWidth,
trailingWidth: trailingWidth, trailingWidth: trailingWidth
suppressingCenterText: suppressingCenterText
) )
} }
private func shouldSuppressExtensionCenter(for payload: ExtensionLiveActivityPayload) -> Bool {
vm.notchState == .closed
}
@MainActor @MainActor
private final class ExtensionRoutingDiagnostics { private final class ExtensionRoutingDiagnostics {
static let shared = ExtensionRoutingDiagnostics() static let shared = ExtensionRoutingDiagnostics()

View file

@ -9,7 +9,6 @@ struct ExtensionStandaloneLayout {
let leadingWidth: CGFloat let leadingWidth: CGFloat
let centerWidth: CGFloat let centerWidth: CGFloat
let trailingWidth: CGFloat let trailingWidth: CGFloat
let suppressingCenterText: Bool
} }
struct ExtensionLiveActivityStandaloneView: View { struct ExtensionLiveActivityStandaloneView: View {
@ -23,17 +22,6 @@ struct ExtensionLiveActivityStandaloneView: View {
private var resolvedLeadingContent: AtollTrailingContent { private var resolvedLeadingContent: AtollTrailingContent {
resolvedExtensionLeadingContent(for: descriptor) resolvedExtensionLeadingContent(for: descriptor)
} }
private var resolvedCenterStyle: ExtensionCenterContentView.Style {
switch descriptor.centerTextStyle {
case .inline:
return .inline
case .standard:
return .stacked
case .inheritUser:
return Defaults[.sneakPeekStyles] == .inline ? .inline : .stacked
}
}
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
ExtensionLeadingContentView( ExtensionLeadingContentView(
@ -49,20 +37,7 @@ struct ExtensionLiveActivityStandaloneView: View {
Rectangle() Rectangle()
.fill(Color.black) .fill(Color.black)
.frame(width: layout.centerWidth, height: contentHeight) .frame(width: layout.centerWidth, height: contentHeight)
.overlay( .overlay(EmptyView())
Group {
if layout.suppressingCenterText {
EmptyView()
} else {
ExtensionCenterContentView(
descriptor: descriptor,
accent: accentColor,
width: layout.centerWidth,
style: resolvedCenterStyle
)
}
}
)
ExtensionMusicWingView( ExtensionMusicWingView(
payload: payload, payload: payload,
@ -182,57 +157,6 @@ struct ExtensionLeadingContentView: View {
} }
} }
struct ExtensionCenterContentView: View {
enum Style {
case stacked
case inline
}
let descriptor: AtollLiveActivityDescriptor
let accent: Color
let width: CGFloat
let style: Style
var body: some View {
switch style {
case .stacked:
VStack(alignment: .leading, spacing: 2) {
Text(descriptor.title)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(.white)
.lineLimit(1)
if let subtitle = descriptor.subtitle, !subtitle.isEmpty {
Text(subtitle)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Color.white.opacity(0.7))
.lineLimit(1)
}
}
.padding(.horizontal, 12)
.frame(maxWidth: .infinity, alignment: .leading)
case .inline:
HStack(alignment: .center, spacing: 8) {
MarqueeText(
.constant(descriptor.title),
font: .system(size: 13, weight: .semibold),
nsFont: .body,
textColor: .white,
minDuration: 0.4,
frameWidth: max(40, width * 0.55)
)
Spacer(minLength: 4)
if let subtitle = descriptor.subtitle, !subtitle.isEmpty {
Text(subtitle)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(Color.white.opacity(0.75))
.lineLimit(1)
}
}
.padding(.horizontal, 12)
}
}
}
private func logExtensionDiagnostics(_ message: String) { private func logExtensionDiagnostics(_ message: String) {
guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return } guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return }
Logger.log(message, category: .extensions) Logger.log(message, category: .extensions)
@ -382,120 +306,140 @@ struct ExtensionNotchSectionView: View {
struct ExtensionInlineSneakPeekView: View { struct ExtensionInlineSneakPeekView: View {
let payload: ExtensionLiveActivityPayload? let payload: ExtensionLiveActivityPayload?
let title: String
let subtitle: String?
let accentColor: Color let accentColor: Color
let notchHeight: CGFloat let notchHeight: CGFloat
let closedNotchWidth: CGFloat let closedNotchWidth: CGFloat
let isHovering: Bool let isHovering: Bool
let gestureProgress: CGFloat
private let wingPadding: CGFloat = 16
private var descriptor: AtollLiveActivityDescriptor? { payload?.descriptor } private var descriptor: AtollLiveActivityDescriptor? { payload?.descriptor }
private var resolvedAccent: Color { descriptor?.accentColor.swiftUIColor ?? accentColor } private var resolvedAccent: Color { descriptor?.accentColor.swiftUIColor ?? accentColor }
private var contentHeight: CGFloat { private var trailingRenderable: ExtensionTrailingRenderable? {
max(0, notchHeight - (isHovering ? 0 : 12)) descriptor.map { resolvedExtensionTrailingRenderable(for: $0) }
} }
private var leadingWidth: CGFloat { private var contentHeight: CGFloat {
let base = max(contentHeight, 44) max(0, notchHeight - (isHovering ? 0 : 12))
return max(base * 0.85, base + gestureProgress / 2)
} }
private var centerWidth: CGFloat { private var centerWidth: CGFloat {
max(closedNotchWidth + (isHovering ? 8 : 0), 120) max(closedNotchWidth + (isHovering ? 8 : 0), 120)
} }
private var trailingWidth: CGFloat { private var iconDiameter: CGFloat {
max(leadingWidth * 0.9, 120) max(contentHeight - 8, 28)
} }
private var marqueeFrameWidth: CGFloat { private var leftWingWidth: CGFloat {
max(48, centerWidth - 24) wingPadding + iconDiameter
} }
private var resolvedTitle: String { private var baseTrailingWidth: CGFloat {
let descriptorTitle = descriptor?.title ?? "Extension" max(iconDiameter, 56)
let preferred = title.isEmpty ? descriptorTitle : title
return preferred.isEmpty ? "Extension" : preferred
} }
private var resolvedSubtitle: String { private var trailingContentWidth: CGFloat {
if let subtitle, !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { guard let payload else { return baseTrailingWidth }
return subtitle.trimmingCharacters(in: .whitespacesAndNewlines) return ExtensionLayoutMetrics.trailingWidth(
} for: payload,
if let descriptorSubtitle = descriptor?.subtitle, !descriptorSubtitle.isEmpty { baseWidth: baseTrailingWidth,
return descriptorSubtitle maxWidth: baseTrailingWidth + centerWidth * 0.6
} )
return "" }
private var rightWingWidth: CGFloat {
wingPadding + trailingContentWidth
}
private var trailingContentAvailableWidth: CGFloat {
max(0, trailingContentWidth - wingPadding)
} }
var body: some View { var body: some View {
HStack(spacing: 0) { HStack(spacing: 0) {
leadingWing leadingWing
.frame(width: leadingWidth, height: contentHeight) centerSection
Rectangle()
.fill(Color.black)
.frame(width: centerWidth, height: contentHeight)
.overlay(alignment: .bottomLeading) {
VStack(alignment: .leading, spacing: 2) {
MarqueeText(
.constant(resolvedTitle),
font: .system(size: 12, weight: .semibold),
nsFont: .subheadline,
textColor: .white,
minDuration: 0.4,
frameWidth: marqueeFrameWidth
)
if !resolvedSubtitle.isEmpty {
Text(resolvedSubtitle)
.font(.system(size: 11, weight: .regular))
.foregroundStyle(Color.white.opacity(0.75))
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(.horizontal, 12)
.padding(.bottom, 6)
}
trailingWing trailingWing
.frame(width: trailingWidth, height: contentHeight)
} }
.frame(height: notchHeight + (isHovering ? 8 : 0), alignment: .center) .frame(height: notchHeight + (isHovering ? 8 : 0), alignment: .center)
} }
@ViewBuilder
private var leadingWing: some View { private var leadingWing: some View {
if let descriptor { Color.clear
ExtensionLeadingContentView( .frame(width: leftWingWidth, height: contentHeight)
content: resolvedExtensionLeadingContent(for: descriptor), .background(alignment: .leading) {
badge: descriptor.badgeIcon, Group {
accent: resolvedAccent, if let descriptor {
frameWidth: leadingWidth, ExtensionLeadingContentView(
frameHeight: contentHeight, content: resolvedExtensionLeadingContent(for: descriptor),
defaultIcon: descriptor.leadingIcon badge: descriptor.badgeIcon,
) accent: resolvedAccent,
} else { frameWidth: iconDiameter,
RoundedRectangle(cornerRadius: contentHeight * 0.25, style: .continuous) frameHeight: iconDiameter,
.fill(resolvedAccent.gradient) defaultIcon: descriptor.leadingIcon
} )
} else {
RoundedRectangle(cornerRadius: iconDiameter * 0.25, style: .continuous)
.fill(resolvedAccent.gradient)
.frame(width: iconDiameter, height: iconDiameter)
}
}
.frame(width: iconDiameter, height: iconDiameter)
.frame(maxHeight: .infinity, alignment: .center)
.padding(.leading, wingPadding / 2)
}
}
private var centerSection: some View {
Rectangle()
.fill(Color.black)
.frame(width: centerWidth, height: contentHeight)
} }
@ViewBuilder @ViewBuilder
private var trailingWing: some View { private var trailingWing: some View {
VStack { Color.clear
Rectangle() .frame(width: rightWingWidth, height: contentHeight)
.fill(resolvedAccent.gradient) .background(alignment: .trailing) {
.mask { if let descriptor, let trailingRenderable {
AudioSpectrumView(isPlaying: .constant(true)) VStack(alignment: .trailing, spacing: 6) {
.frame(width: 18, height: 12) switch trailingRenderable {
.frame(maxWidth: .infinity, maxHeight: .infinity) case let .content(content):
if case .none = content {
Spacer(minLength: 0)
} else {
ExtensionEdgeContentView(
content: content,
accent: resolvedAccent,
availableWidth: trailingContentAvailableWidth,
alignment: .trailing
)
.frame(maxWidth: trailingContentAvailableWidth, alignment: .trailing)
}
case let .indicator(indicator):
ExtensionProgressIndicatorView(
indicator: indicator,
progress: descriptor.progress,
accent: resolvedAccent,
estimatedDuration: descriptor.estimatedDuration,
maxVisualHeight: contentHeight
)
}
Spacer(minLength: 0)
}
.padding(.trailing, wingPadding / 2)
.padding(.vertical, 6)
} else {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(resolvedAccent.opacity(0.25))
.frame(width: max(32, trailingContentAvailableWidth * 0.6), height: max(16, contentHeight * 0.4))
.padding(.trailing, wingPadding / 2)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .trailing)
.padding(.vertical, 6)
} }
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
.padding(.trailing, 8)
.padding(.vertical, 6)
} }
} }

View file

@ -352,8 +352,8 @@ struct ExtensionProgressIndicatorView: View {
private func resolvedRingDiameter(requested: CGFloat) -> CGFloat { private func resolvedRingDiameter(requested: CGFloat) -> CGFloat {
guard let maxVisualHeight else { return requested } guard let maxVisualHeight else { return requested }
let clamped = min(requested, max(maxVisualHeight - 6, 18)) let target = max(min(maxVisualHeight - 12, 24), 16)
return max(18, clamped) return min(requested, target)
} }
} }