mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
Update Third Party stuff
This commit is contained in:
parent
94a612f327
commit
27b16a3d15
8 changed files with 636 additions and 99 deletions
|
|
@ -7,7 +7,7 @@
|
||||||
"location" : "https://github.com/Ebullioscopic/AtollExtensionKit",
|
"location" : "https://github.com/Ebullioscopic/AtollExtensionKit",
|
||||||
"state" : {
|
"state" : {
|
||||||
"branch" : "main",
|
"branch" : "main",
|
||||||
"revision" : "61d9a7be60416f107eb45b470eb3c8a5296b180f"
|
"revision" : "464423a5ffd29c1836f93662a5cb0529e7c1c355"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -495,6 +495,7 @@ struct ContentView: View {
|
||||||
let musicSecondary = resolveMusicSecondaryLiveActivity(isMusicPairingEligible: musicPairingEligible)
|
let musicSecondary = resolveMusicSecondaryLiveActivity(isMusicPairingEligible: musicPairingEligible)
|
||||||
let extensionSecondaryPayloadID = extensionSecondaryPayloadID(for: musicSecondary)
|
let extensionSecondaryPayloadID = extensionSecondaryPayloadID(for: musicSecondary)
|
||||||
let extensionStandalonePayload = resolvedExtensionStandalonePayload(excluding: extensionSecondaryPayloadID)
|
let extensionStandalonePayload = resolvedExtensionStandalonePayload(excluding: extensionSecondaryPayloadID)
|
||||||
|
let activeSneakPeekStyle = resolvedSneakPeekStyle()
|
||||||
let expansionMatchesSecondary: Bool = {
|
let expansionMatchesSecondary: Bool = {
|
||||||
guard let musicSecondary else { return false }
|
guard let musicSecondary else { return false }
|
||||||
switch musicSecondary {
|
switch musicSecondary {
|
||||||
|
|
@ -540,6 +541,23 @@ struct ContentView: View {
|
||||||
.frame(width: 76, alignment: .trailing)
|
.frame(width: 76, alignment: .trailing)
|
||||||
}
|
}
|
||||||
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
|
.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center)
|
||||||
|
} else if coordinator.sneakPeek.show,
|
||||||
|
case let .extensionLiveActivity(bundleID, activityID) = coordinator.sneakPeek.type,
|
||||||
|
activeSneakPeekStyle == .inline,
|
||||||
|
vm.notchState == .closed,
|
||||||
|
!vm.hideOnClosed {
|
||||||
|
let inlinePayload = extensionLiveActivityManager.payload(bundleIdentifier: bundleID, activityID: activityID)
|
||||||
|
ExtensionInlineSneakPeekView(
|
||||||
|
payload: inlinePayload,
|
||||||
|
title: coordinator.sneakPeek.title,
|
||||||
|
subtitle: coordinator.sneakPeek.subtitle,
|
||||||
|
accentColor: coordinator.sneakPeek.accentColor ?? .gray,
|
||||||
|
notchHeight: vm.effectiveClosedNotchHeight,
|
||||||
|
closedNotchWidth: vm.closedNotchSize.width,
|
||||||
|
isHovering: isHovering,
|
||||||
|
gestureProgress: gestureProgress
|
||||||
|
)
|
||||||
|
.transition(.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) {
|
||||||
InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress)
|
InlineHUD(type: $coordinator.sneakPeek.type, value: $coordinator.sneakPeek.value, icon: $coordinator.sneakPeek.icon, hoverAnimation: $isHovering, gestureProgress: $gestureProgress)
|
||||||
.transition(
|
.transition(
|
||||||
|
|
@ -568,17 +586,19 @@ 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,
|
||||||
layout: layout,
|
layout: layout,
|
||||||
isHovering: isHovering
|
isHovering: isHovering,
|
||||||
|
notchState: vm.notchState
|
||||||
)
|
)
|
||||||
.transition(.opacity.combined(with: .scale))
|
|
||||||
} else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed {
|
} else if !coordinator.expandingView.show && vm.notchState == .closed && (!musicManager.isPlaying && musicManager.isPlayerIdle) && Defaults[.showNotHumanFace] && !vm.hideOnClosed {
|
||||||
DynamicIslandFaceAnimation().animation(.interactiveSpring, value: musicManager.isPlayerIdle)
|
DynamicIslandFaceAnimation().animation(.interactiveSpring, value: musicManager.isPlayerIdle)
|
||||||
} else if vm.notchState == .open {
|
} else if vm.notchState == .open {
|
||||||
|
|
@ -599,7 +619,7 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
// Old sneak peek music
|
// Old sneak peek music
|
||||||
else if coordinator.sneakPeek.type == .music {
|
else if coordinator.sneakPeek.type == .music {
|
||||||
if vm.notchState == .closed && !vm.hideOnClosed && Defaults[.sneakPeekStyles] == .standard {
|
if vm.notchState == .closed && !vm.hideOnClosed && activeSneakPeekStyle == .standard {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
Image(systemName: "music.note")
|
Image(systemName: "music.note")
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
|
|
@ -612,7 +632,7 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
// Timer sneak peek
|
// Timer sneak peek
|
||||||
else if coordinator.sneakPeek.type == .timer {
|
else if coordinator.sneakPeek.type == .timer {
|
||||||
if !vm.hideOnClosed && Defaults[.sneakPeekStyles] == .standard {
|
if !vm.hideOnClosed && activeSneakPeekStyle == .standard {
|
||||||
HStack(alignment: .center) {
|
HStack(alignment: .center) {
|
||||||
Image(systemName: "timer")
|
Image(systemName: "timer")
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
|
|
@ -624,7 +644,7 @@ struct ContentView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if coordinator.sneakPeek.type == .reminder {
|
else if coordinator.sneakPeek.type == .reminder {
|
||||||
if !vm.hideOnClosed && Defaults[.sneakPeekStyles] == .standard, let reminder = reminderManager.activeReminder {
|
if !vm.hideOnClosed && activeSneakPeekStyle == .standard, let reminder = reminderManager.activeReminder {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
let chipColor = Color(nsColor: reminder.event.calendar.color).ensureMinimumBrightness(factor: 0.7)
|
let chipColor = Color(nsColor: reminder.event.calendar.color).ensureMinimumBrightness(factor: 0.7)
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
|
|
@ -642,10 +662,33 @@ struct ContentView: View {
|
||||||
.padding(.bottom, 10)
|
.padding(.bottom, 10)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Extension live activity sneak peek
|
||||||
|
else if case .extensionLiveActivity = coordinator.sneakPeek.type {
|
||||||
|
if !vm.hideOnClosed && activeSneakPeekStyle == .standard {
|
||||||
|
HStack(alignment: .center, spacing: 6) {
|
||||||
|
if !coordinator.sneakPeek.icon.isEmpty {
|
||||||
|
Image(systemName: coordinator.sneakPeek.icon)
|
||||||
|
}
|
||||||
|
GeometryReader { geo in
|
||||||
|
let combinedText = coordinator.sneakPeek.subtitle.isEmpty ?
|
||||||
|
coordinator.sneakPeek.title :
|
||||||
|
"\(coordinator.sneakPeek.title) - \(coordinator.sneakPeek.subtitle)"
|
||||||
|
MarqueeText(
|
||||||
|
.constant(combinedText),
|
||||||
|
textColor: coordinator.sneakPeek.accentColor ?? .gray,
|
||||||
|
minDuration: 1,
|
||||||
|
frameWidth: geo.size.width
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundStyle(coordinator.sneakPeek.accentColor ?? .gray)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.conditionalModifier((coordinator.sneakPeek.show && coordinator.sneakPeek.type == .music && vm.notchState == .closed && !vm.hideOnClosed && Defaults[.sneakPeekStyles] == .standard) || (coordinator.sneakPeek.show && coordinator.sneakPeek.type == .timer && !vm.hideOnClosed && Defaults[.sneakPeekStyles] == .standard) || (coordinator.sneakPeek.show && (coordinator.sneakPeek.type != .music && coordinator.sneakPeek.type != .timer) && (vm.notchState == .closed))) { view in
|
.conditionalModifier(shouldFixSizeForSneakPeek()) { view in
|
||||||
view
|
view
|
||||||
.fixedSize()
|
.fixedSize()
|
||||||
}
|
}
|
||||||
|
|
@ -1224,11 +1267,17 @@ struct ContentView: View {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
private func extensionStandaloneLayout(for payload: ExtensionLiveActivityPayload, notchHeight: CGFloat, isHovering: Bool) -> ExtensionStandaloneLayout {
|
private func extensionStandaloneLayout(for payload: ExtensionLiveActivityPayload, notchHeight: CGFloat, isHovering: Bool, suppressingCenterText: 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)
|
||||||
let centerWidth = max(vm.closedNotchSize.width + (isHovering ? 8 : 0), 96)
|
let centerWidth: CGFloat = {
|
||||||
|
if suppressingCenterText {
|
||||||
|
let base = vm.closedNotchSize.width * 0.35
|
||||||
|
return max(64, min(base, 96))
|
||||||
|
}
|
||||||
|
return max(vm.closedNotchSize.width + (isHovering ? 8 : 0), 96)
|
||||||
|
}()
|
||||||
let trailingWidth = ExtensionLayoutMetrics.trailingWidth(
|
let trailingWidth = ExtensionLayoutMetrics.trailingWidth(
|
||||||
for: payload,
|
for: payload,
|
||||||
baseWidth: leadingWidth,
|
baseWidth: leadingWidth,
|
||||||
|
|
@ -1241,10 +1290,22 @@ 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 {
|
||||||
|
guard vm.notchState == .closed else { return false }
|
||||||
|
let descriptor = payload.descriptor
|
||||||
|
guard descriptor.sneakPeekConfig?.enabled ?? true else { return false }
|
||||||
|
guard coordinator.sneakPeek.show else { return false }
|
||||||
|
guard case let .extensionLiveActivity(bundleID, activityID) = coordinator.sneakPeek.type else { return false }
|
||||||
|
guard bundleID == payload.bundleIdentifier && activityID == descriptor.id else { return false }
|
||||||
|
let style = coordinator.sneakPeek.styleOverride ?? Defaults[.sneakPeekStyles]
|
||||||
|
return style == .inline
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
private final class ExtensionRoutingDiagnostics {
|
private final class ExtensionRoutingDiagnostics {
|
||||||
static let shared = ExtensionRoutingDiagnostics()
|
static let shared = ExtensionRoutingDiagnostics()
|
||||||
|
|
@ -1771,6 +1832,28 @@ struct ContentView: View {
|
||||||
|
|
||||||
private func hideMusicControlWindow() {}
|
private func hideMusicControlWindow() {}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
private func shouldFixSizeForSneakPeek() -> Bool {
|
||||||
|
guard coordinator.sneakPeek.show else { return false }
|
||||||
|
let style = resolvedSneakPeekStyle()
|
||||||
|
|
||||||
|
// Check for extension sneak peek
|
||||||
|
if case .extensionLiveActivity = coordinator.sneakPeek.type {
|
||||||
|
return vm.notchState == .closed && style == .standard
|
||||||
|
}
|
||||||
|
|
||||||
|
// Original logic for other types
|
||||||
|
let isMusicSneak = coordinator.sneakPeek.type == .music && vm.notchState == .closed && !vm.hideOnClosed && style == .standard
|
||||||
|
let isTimerSneak = coordinator.sneakPeek.type == .timer && !vm.hideOnClosed && style == .standard
|
||||||
|
let isReminderSneak = coordinator.sneakPeek.type == .reminder && !vm.hideOnClosed && style == .standard
|
||||||
|
let isOtherSneak = coordinator.sneakPeek.type != .music && coordinator.sneakPeek.type != .timer && coordinator.sneakPeek.type != .reminder && vm.notchState == .closed
|
||||||
|
|
||||||
|
return isMusicSneak || isTimerSneak || isReminderSneak || isOtherSneak
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvedSneakPeekStyle() -> SneakPeekStyle {
|
||||||
|
coordinator.sneakPeek.styleOverride ?? Defaults[.sneakPeekStyles]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum MusicSecondaryLiveActivity: Equatable {
|
private enum MusicSecondaryLiveActivity: Equatable {
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import Combine
|
||||||
import Defaults
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum SneakContentType {
|
enum SneakContentType: Equatable {
|
||||||
case brightness
|
case brightness
|
||||||
case volume
|
case volume
|
||||||
case backlight
|
case backlight
|
||||||
|
|
@ -25,6 +25,34 @@ enum SneakContentType {
|
||||||
case privacy
|
case privacy
|
||||||
case lockScreen
|
case lockScreen
|
||||||
case capsLock
|
case capsLock
|
||||||
|
case extensionLiveActivity(bundleID: String, activityID: String)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension SneakContentType {
|
||||||
|
static func == (lhs: SneakContentType, rhs: SneakContentType) -> Bool {
|
||||||
|
switch (lhs, rhs) {
|
||||||
|
case (.brightness, .brightness),
|
||||||
|
(.volume, .volume),
|
||||||
|
(.backlight, .backlight),
|
||||||
|
(.music, .music),
|
||||||
|
(.mic, .mic),
|
||||||
|
(.battery, .battery),
|
||||||
|
(.download, .download),
|
||||||
|
(.timer, .timer),
|
||||||
|
(.reminder, .reminder),
|
||||||
|
(.recording, .recording),
|
||||||
|
(.doNotDisturb, .doNotDisturb),
|
||||||
|
(.bluetoothAudio, .bluetoothAudio),
|
||||||
|
(.privacy, .privacy),
|
||||||
|
(.lockScreen, .lockScreen),
|
||||||
|
(.capsLock, .capsLock):
|
||||||
|
return true
|
||||||
|
case let (.extensionLiveActivity(lb, la), .extensionLiveActivity(rb, ra)):
|
||||||
|
return lb == rb && la == ra
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sneakPeek {
|
struct sneakPeek {
|
||||||
|
|
@ -32,6 +60,10 @@ struct sneakPeek {
|
||||||
var type: SneakContentType = .music
|
var type: SneakContentType = .music
|
||||||
var value: CGFloat = 0
|
var value: CGFloat = 0
|
||||||
var icon: String = ""
|
var icon: String = ""
|
||||||
|
var title: String = ""
|
||||||
|
var subtitle: String = ""
|
||||||
|
var accentColor: Color?
|
||||||
|
var styleOverride: SneakPeekStyle? = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BrowserType {
|
enum BrowserType {
|
||||||
|
|
@ -175,19 +207,40 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func toggleSneakPeek(status: Bool, type: SneakContentType, duration: TimeInterval = 1.5, value: CGFloat = 0, icon: String = "") {
|
func toggleSneakPeek(
|
||||||
|
status: Bool,
|
||||||
|
type: SneakContentType,
|
||||||
|
duration: TimeInterval = 1.5,
|
||||||
|
value: CGFloat = 0,
|
||||||
|
icon: String = "",
|
||||||
|
title: String = "",
|
||||||
|
subtitle: String = "",
|
||||||
|
accentColor: Color? = nil,
|
||||||
|
styleOverride: SneakPeekStyle? = nil
|
||||||
|
) {
|
||||||
let resolvedDuration: TimeInterval
|
let resolvedDuration: TimeInterval
|
||||||
switch type {
|
switch type {
|
||||||
case .timer:
|
case .timer:
|
||||||
resolvedDuration = 10
|
resolvedDuration = 10
|
||||||
case .reminder:
|
case .reminder:
|
||||||
resolvedDuration = Defaults[.reminderSneakPeekDuration]
|
resolvedDuration = Defaults[.reminderSneakPeekDuration]
|
||||||
|
case .extensionLiveActivity:
|
||||||
|
resolvedDuration = duration
|
||||||
default:
|
default:
|
||||||
resolvedDuration = duration
|
resolvedDuration = duration
|
||||||
}
|
}
|
||||||
sneakPeekDuration = resolvedDuration
|
sneakPeekDuration = resolvedDuration
|
||||||
let bypassedTypes: [SneakContentType] = [.music, .timer, .reminder, .bluetoothAudio]
|
let bypassedTypes: [SneakContentType] = [.music, .timer, .reminder, .bluetoothAudio]
|
||||||
if !bypassedTypes.contains(type) && !Defaults[.enableSystemHUD] {
|
|
||||||
|
// Check if it's an extension type
|
||||||
|
let isExtensionType: Bool
|
||||||
|
if case .extensionLiveActivity = type {
|
||||||
|
isExtensionType = true
|
||||||
|
} else {
|
||||||
|
isExtensionType = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isExtensionType && !bypassedTypes.contains(type) && !Defaults[.enableSystemHUD] {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
@ -196,6 +249,10 @@ class DynamicIslandViewCoordinator: ObservableObject {
|
||||||
self.sneakPeek.type = type
|
self.sneakPeek.type = type
|
||||||
self.sneakPeek.value = value
|
self.sneakPeek.value = value
|
||||||
self.sneakPeek.icon = icon
|
self.sneakPeek.icon = icon
|
||||||
|
self.sneakPeek.title = title
|
||||||
|
self.sneakPeek.subtitle = subtitle
|
||||||
|
self.sneakPeek.accentColor = accentColor
|
||||||
|
self.sneakPeek.styleOverride = styleOverride
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16013,6 +16013,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Reset Live Activities" : {
|
||||||
|
"comment" : "A menu item that resets live activities for a particular app.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"Reset Lock Screen Widgets" : {
|
||||||
|
"comment" : "A menu item that resets all lock screen widgets for a particular app.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Reset Rate Limits" : {
|
"Reset Rate Limits" : {
|
||||||
"comment" : "A button that resets the rate limits for a particular extension.",
|
"comment" : "A button that resets the rate limits for a particular extension.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,20 @@ 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 {
|
||||||
let payload: ExtensionLiveActivityPayload
|
let payload: ExtensionLiveActivityPayload
|
||||||
let layout: ExtensionStandaloneLayout
|
let layout: ExtensionStandaloneLayout
|
||||||
let isHovering: Bool
|
let isHovering: Bool
|
||||||
|
let notchState: NotchState
|
||||||
|
|
||||||
private var descriptor: AtollLiveActivityDescriptor { payload.descriptor }
|
private var descriptor: AtollLiveActivityDescriptor { payload.descriptor }
|
||||||
private var contentHeight: CGFloat { layout.contentHeight }
|
private var contentHeight: CGFloat { layout.contentHeight }
|
||||||
private var accentColor: Color { descriptor.accentColor.swiftUIColor }
|
private var accentColor: Color { descriptor.accentColor.swiftUIColor }
|
||||||
private var resolvedLeadingContent: AtollTrailingContent {
|
private var resolvedLeadingContent: AtollTrailingContent {
|
||||||
descriptor.leadingContent ?? .icon(descriptor.leadingIcon)
|
resolvedExtensionLeadingContent(for: descriptor)
|
||||||
}
|
}
|
||||||
private var resolvedCenterStyle: ExtensionCenterContentView.Style {
|
private var resolvedCenterStyle: ExtensionCenterContentView.Style {
|
||||||
switch descriptor.centerTextStyle {
|
switch descriptor.centerTextStyle {
|
||||||
|
|
@ -33,6 +35,10 @@ struct ExtensionLiveActivityStandaloneView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var shouldSuppressCenterText: Bool {
|
||||||
|
layout.suppressingCenterText
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
ExtensionLeadingContentView(
|
ExtensionLeadingContentView(
|
||||||
|
|
@ -40,7 +46,8 @@ struct ExtensionLiveActivityStandaloneView: View {
|
||||||
badge: descriptor.badgeIcon,
|
badge: descriptor.badgeIcon,
|
||||||
accent: accentColor,
|
accent: accentColor,
|
||||||
frameWidth: layout.leadingWidth,
|
frameWidth: layout.leadingWidth,
|
||||||
frameHeight: contentHeight
|
frameHeight: contentHeight,
|
||||||
|
defaultIcon: descriptor.leadingIcon
|
||||||
)
|
)
|
||||||
.frame(width: layout.leadingWidth, height: contentHeight)
|
.frame(width: layout.leadingWidth, height: contentHeight)
|
||||||
|
|
||||||
|
|
@ -48,12 +55,16 @@ struct ExtensionLiveActivityStandaloneView: View {
|
||||||
.fill(Color.black)
|
.fill(Color.black)
|
||||||
.frame(width: layout.centerWidth, height: contentHeight)
|
.frame(width: layout.centerWidth, height: contentHeight)
|
||||||
.overlay(
|
.overlay(
|
||||||
ExtensionCenterContentView(
|
Group {
|
||||||
descriptor: descriptor,
|
if !shouldSuppressCenterText {
|
||||||
accent: accentColor,
|
ExtensionCenterContentView(
|
||||||
width: layout.centerWidth,
|
descriptor: descriptor,
|
||||||
style: resolvedCenterStyle
|
accent: accentColor,
|
||||||
)
|
width: layout.centerWidth,
|
||||||
|
style: resolvedCenterStyle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
ExtensionMusicWingView(
|
ExtensionMusicWingView(
|
||||||
|
|
@ -64,6 +75,10 @@ struct ExtensionLiveActivityStandaloneView: View {
|
||||||
.frame(width: layout.trailingWidth, height: contentHeight)
|
.frame(width: layout.trailingWidth, height: contentHeight)
|
||||||
}
|
}
|
||||||
.frame(width: layout.totalWidth, height: layout.outerHeight + (isHovering ? 8 : 0))
|
.frame(width: layout.totalWidth, height: layout.outerHeight + (isHovering ? 8 : 0))
|
||||||
|
.transition(.asymmetric(
|
||||||
|
insertion: .scale(scale: 0.95).combined(with: .opacity).animation(.spring(response: 0.4, dampingFraction: 0.8)),
|
||||||
|
removal: .scale(scale: 0.95).combined(with: .opacity).animation(.spring(response: 0.3, dampingFraction: 0.9))
|
||||||
|
))
|
||||||
.animation(.smooth(duration: 0.25), value: payload.id)
|
.animation(.smooth(duration: 0.25), value: payload.id)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
logExtensionDiagnostics("Displaying extension live activity \(payload.descriptor.id) for \(payload.bundleIdentifier) as standalone view")
|
logExtensionDiagnostics("Displaying extension live activity \(payload.descriptor.id) for \(payload.bundleIdentifier) as standalone view")
|
||||||
|
|
@ -82,32 +97,37 @@ struct ExtensionMusicWingView: View {
|
||||||
|
|
||||||
private var descriptor: AtollLiveActivityDescriptor { payload.descriptor }
|
private var descriptor: AtollLiveActivityDescriptor { payload.descriptor }
|
||||||
private var accentColor: Color { descriptor.accentColor.swiftUIColor }
|
private var accentColor: Color { descriptor.accentColor.swiftUIColor }
|
||||||
|
private var trailingRenderable: ExtensionTrailingRenderable {
|
||||||
|
resolvedExtensionTrailingRenderable(for: descriptor)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .trailing, spacing: 6) {
|
VStack(alignment: .trailing, spacing: 6) {
|
||||||
if case .none = descriptor.trailingContent {
|
switch trailingRenderable {
|
||||||
EmptyView()
|
case let .content(content):
|
||||||
} else {
|
if case .none = content {
|
||||||
ExtensionEdgeContentView(
|
EmptyView()
|
||||||
content: descriptor.trailingContent,
|
} else {
|
||||||
accent: accentColor,
|
ExtensionEdgeContentView(
|
||||||
availableWidth: trailingWidth,
|
content: content,
|
||||||
alignment: .trailing
|
accent: accentColor,
|
||||||
)
|
availableWidth: trailingWidth,
|
||||||
|
alignment: .trailing
|
||||||
|
)
|
||||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
}
|
}
|
||||||
|
case let .indicator(indicator):
|
||||||
if let indicator = descriptor.progressIndicator, indicator != .none {
|
|
||||||
ExtensionProgressIndicatorView(
|
ExtensionProgressIndicatorView(
|
||||||
indicator: indicator,
|
indicator: indicator,
|
||||||
progress: descriptor.progress,
|
progress: descriptor.progress,
|
||||||
accent: accentColor,
|
accent: accentColor,
|
||||||
estimatedDuration: descriptor.estimatedDuration
|
estimatedDuration: descriptor.estimatedDuration,
|
||||||
|
maxVisualHeight: notchHeight
|
||||||
)
|
)
|
||||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||||
} else {
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(minLength: 0)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||||
.padding(.trailing, 8)
|
.padding(.trailing, 8)
|
||||||
|
|
@ -127,23 +147,39 @@ struct ExtensionLeadingContentView: View {
|
||||||
let accent: Color
|
let accent: Color
|
||||||
let frameWidth: CGFloat
|
let frameWidth: CGFloat
|
||||||
let frameHeight: CGFloat
|
let frameHeight: CGFloat
|
||||||
|
let defaultIcon: AtollIconDescriptor
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if case let .icon(iconDescriptor) = content {
|
Group {
|
||||||
ExtensionCompositeIconView(
|
switch content {
|
||||||
leading: iconDescriptor,
|
case let .icon(iconDescriptor):
|
||||||
badge: badge,
|
ExtensionCompositeIconView(
|
||||||
accent: accent,
|
leading: iconDescriptor,
|
||||||
size: frameHeight
|
badge: badge,
|
||||||
)
|
accent: accent,
|
||||||
} else {
|
size: frameHeight
|
||||||
ExtensionEdgeContentView(
|
)
|
||||||
content: content,
|
case let .animation(data, size):
|
||||||
accent: accent,
|
let resolvedSize = CGSize(
|
||||||
availableWidth: frameWidth,
|
width: min(size.width, frameHeight),
|
||||||
alignment: .leading
|
height: min(size.height, frameHeight)
|
||||||
)
|
)
|
||||||
|
ExtensionLottieView(data: data, size: resolvedSize)
|
||||||
|
.frame(width: frameHeight, height: frameHeight)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: frameHeight * 0.18, style: .continuous)
|
||||||
|
.fill(Color.white.opacity(0.08))
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
ExtensionCompositeIconView(
|
||||||
|
leading: defaultIcon,
|
||||||
|
badge: badge,
|
||||||
|
accent: accent,
|
||||||
|
size: frameHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.frame(width: frameWidth, height: frameHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,3 +238,125 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ExtensionInlineSneakPeekView: View {
|
||||||
|
let payload: ExtensionLiveActivityPayload?
|
||||||
|
let title: String
|
||||||
|
let subtitle: String
|
||||||
|
let accentColor: Color
|
||||||
|
let notchHeight: CGFloat
|
||||||
|
let closedNotchWidth: CGFloat
|
||||||
|
let isHovering: Bool
|
||||||
|
let gestureProgress: CGFloat
|
||||||
|
|
||||||
|
private var descriptor: AtollLiveActivityDescriptor? { payload?.descriptor }
|
||||||
|
private var resolvedAccent: Color { descriptor?.accentColor.swiftUIColor ?? accentColor }
|
||||||
|
|
||||||
|
private var contentHeight: CGFloat {
|
||||||
|
max(0, notchHeight - (isHovering ? 0 : 12))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var leadingWidth: CGFloat {
|
||||||
|
let base = max(contentHeight, 44)
|
||||||
|
return max(base * 0.85, base + gestureProgress / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var centerWidth: CGFloat {
|
||||||
|
max(closedNotchWidth + (isHovering ? 8 : 0), 120)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var trailingWidth: CGFloat {
|
||||||
|
guard let payload else { return max(leadingWidth, 120) }
|
||||||
|
return ExtensionLayoutMetrics.trailingWidth(
|
||||||
|
for: payload,
|
||||||
|
baseWidth: leadingWidth,
|
||||||
|
maxWidth: leadingWidth + centerWidth * 0.6
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var marqueeFrameWidth: CGFloat {
|
||||||
|
max(48, centerWidth - 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var resolvedTitle: String {
|
||||||
|
title.isEmpty ? "Extension" : title
|
||||||
|
}
|
||||||
|
|
||||||
|
private var resolvedSubtitle: String {
|
||||||
|
subtitle.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
leadingWing
|
||||||
|
.frame(width: leadingWidth, height: contentHeight)
|
||||||
|
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.black)
|
||||||
|
.frame(width: centerWidth, height: contentHeight)
|
||||||
|
.overlay(alignment: .bottomLeading) {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
if !resolvedTitle.isEmpty {
|
||||||
|
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
|
||||||
|
.frame(width: trailingWidth, height: contentHeight)
|
||||||
|
}
|
||||||
|
.frame(height: notchHeight + (isHovering ? 8 : 0), alignment: .center)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var leadingWing: some View {
|
||||||
|
if let descriptor {
|
||||||
|
ExtensionLeadingContentView(
|
||||||
|
content: resolvedExtensionLeadingContent(for: descriptor),
|
||||||
|
badge: descriptor.badgeIcon,
|
||||||
|
accent: resolvedAccent,
|
||||||
|
frameWidth: leadingWidth,
|
||||||
|
frameHeight: contentHeight,
|
||||||
|
defaultIcon: descriptor.leadingIcon
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
RoundedRectangle(cornerRadius: contentHeight * 0.25, style: .continuous)
|
||||||
|
.fill(resolvedAccent.gradient)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var trailingWing: some View {
|
||||||
|
if let payload {
|
||||||
|
ExtensionMusicWingView(
|
||||||
|
payload: payload,
|
||||||
|
notchHeight: contentHeight,
|
||||||
|
trailingWidth: trailingWidth
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Rectangle()
|
||||||
|
.fill(resolvedAccent.gradient)
|
||||||
|
.mask {
|
||||||
|
AudioSpectrumView(isPlaying: .constant(true))
|
||||||
|
.frame(width: 18, height: 12)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import AppKit
|
import AppKit
|
||||||
import AtollExtensionKit
|
import AtollExtensionKit
|
||||||
|
import Lottie
|
||||||
|
import LottieUI
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
// MARK: - Color Conversion
|
// MARK: - Color Conversion
|
||||||
|
|
||||||
|
|
@ -130,7 +133,15 @@ struct ExtensionIconView: View {
|
||||||
} else {
|
} else {
|
||||||
fallbackSymbol
|
fallbackSymbol
|
||||||
}
|
}
|
||||||
case .lottie, .none:
|
case let .lottie(animationData, targetSize):
|
||||||
|
let resolvedSize = CGSize(width: min(targetSize.width, size.width), height: min(targetSize.height, size.height))
|
||||||
|
ExtensionLottieView(data: animationData, size: resolvedSize)
|
||||||
|
.frame(width: size.width, height: size.height)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: cornerRadius(for: descriptor), style: .continuous)
|
||||||
|
.fill(Color.white.opacity(0.08))
|
||||||
|
)
|
||||||
|
case .none:
|
||||||
fallbackSymbol
|
fallbackSymbol
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -187,6 +198,69 @@ struct ExtensionCompositeIconView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Lottie Rendering
|
||||||
|
|
||||||
|
struct ExtensionLottieView: View {
|
||||||
|
let data: Data
|
||||||
|
let size: CGSize
|
||||||
|
var loopMode: LottieLoopMode = .loop
|
||||||
|
@State private var cachedURL: URL?
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if let cachedURL {
|
||||||
|
LottieView(
|
||||||
|
state: LUStateData(
|
||||||
|
type: .loadedFrom(cachedURL),
|
||||||
|
speed: 1.0,
|
||||||
|
loopMode: loopMode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ProgressView()
|
||||||
|
.controlSize(.small)
|
||||||
|
.tint(.white.opacity(0.7))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: size.width, height: size.height)
|
||||||
|
.task {
|
||||||
|
if cachedURL == nil {
|
||||||
|
cachedURL = ExtensionLottieCache.shared.url(for: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private final class ExtensionLottieCache {
|
||||||
|
static let shared = ExtensionLottieCache()
|
||||||
|
private let queue = DispatchQueue(label: "com.atoll.extension-lottie-cache")
|
||||||
|
private var cache: [String: URL] = [:]
|
||||||
|
|
||||||
|
func url(for data: Data) -> URL? {
|
||||||
|
queue.sync {
|
||||||
|
let key = Self.hash(data: data)
|
||||||
|
if let existing = cache[key], FileManager.default.fileExists(atPath: existing.path) {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = FileManager.default.temporaryDirectory.appendingPathComponent("extension-lottie-\(key).json", conformingTo: .json)
|
||||||
|
do {
|
||||||
|
try data.write(to: url, options: .atomic)
|
||||||
|
cache[key] = url
|
||||||
|
return url
|
||||||
|
} catch {
|
||||||
|
cache.removeValue(forKey: key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func hash(data: Data) -> String {
|
||||||
|
let digest = SHA256.hash(data: data)
|
||||||
|
return digest.map { String(format: "%02x", $0) }.joined()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Progress Rendering
|
// MARK: - Progress Rendering
|
||||||
|
|
||||||
struct ExtensionProgressIndicatorView: View {
|
struct ExtensionProgressIndicatorView: View {
|
||||||
|
|
@ -194,39 +268,59 @@ struct ExtensionProgressIndicatorView: View {
|
||||||
let progress: Double
|
let progress: Double
|
||||||
let accent: Color
|
let accent: Color
|
||||||
let estimatedDuration: TimeInterval?
|
let estimatedDuration: TimeInterval?
|
||||||
|
let maxVisualHeight: CGFloat?
|
||||||
|
|
||||||
|
init(
|
||||||
|
indicator: AtollProgressIndicator,
|
||||||
|
progress: Double,
|
||||||
|
accent: Color,
|
||||||
|
estimatedDuration: TimeInterval?,
|
||||||
|
maxVisualHeight: CGFloat? = nil
|
||||||
|
) {
|
||||||
|
self.indicator = indicator
|
||||||
|
self.progress = progress
|
||||||
|
self.accent = accent
|
||||||
|
self.estimatedDuration = estimatedDuration
|
||||||
|
self.maxVisualHeight = maxVisualHeight
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
switch indicator {
|
switch indicator {
|
||||||
case let .ring(diameter, strokeWidth):
|
case let .ring(diameter, strokeWidth, color):
|
||||||
|
let ringColor = color?.swiftUIColor ?? accent
|
||||||
|
let resolvedDiameter = resolvedRingDiameter(requested: diameter)
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
.stroke(Color.white.opacity(0.15), lineWidth: strokeWidth)
|
.stroke(Color.white.opacity(0.15), lineWidth: strokeWidth)
|
||||||
Circle()
|
Circle()
|
||||||
.trim(from: 0, to: CGFloat(max(0, min(progress, 1))))
|
.trim(from: 0, to: CGFloat(max(0, min(progress, 1))))
|
||||||
.stroke(accent, style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round))
|
.stroke(ringColor, style: StrokeStyle(lineWidth: strokeWidth, lineCap: .round))
|
||||||
.rotationEffect(.degrees(-90))
|
.rotationEffect(.degrees(-90))
|
||||||
.animation(.smooth(duration: 0.25), value: progress)
|
.animation(.smooth(duration: 0.25), value: progress)
|
||||||
}
|
}
|
||||||
.frame(width: diameter, height: diameter)
|
.frame(width: resolvedDiameter, height: resolvedDiameter)
|
||||||
case let .bar(width, height, cornerRadius):
|
case let .bar(width, height, cornerRadius, color):
|
||||||
|
let barColor = color?.swiftUIColor ?? accent
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(Color.white.opacity(0.18))
|
.fill(Color.white.opacity(0.18))
|
||||||
.frame(width: width ?? 80, height: height)
|
.frame(width: width ?? 80, height: height)
|
||||||
.overlay(alignment: .leading) {
|
.overlay(alignment: .leading) {
|
||||||
Capsule()
|
Capsule()
|
||||||
.fill(accent)
|
.fill(barColor)
|
||||||
.frame(width: (width ?? 80) * CGFloat(max(0, min(progress, 1))), height: height)
|
.frame(width: (width ?? 80) * CGFloat(max(0, min(progress, 1))), height: height)
|
||||||
.animation(.smooth(duration: 0.25), value: progress)
|
.animation(.smooth(duration: 0.25), value: progress)
|
||||||
}
|
}
|
||||||
case let .percentage(font):
|
case let .percentage(font, color):
|
||||||
|
let textColor = color?.swiftUIColor ?? accent
|
||||||
Text("\(Int(progress * 100))%")
|
Text("\(Int(progress * 100))%")
|
||||||
.font(font.swiftUIFont())
|
.font(font.swiftUIFont())
|
||||||
.foregroundStyle(accent)
|
.foregroundStyle(textColor)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
case let .countdown(font):
|
case let .countdown(font, color):
|
||||||
|
let textColor = color?.swiftUIColor ?? accent
|
||||||
Text(countdownText)
|
Text(countdownText)
|
||||||
.font(font.swiftUIFont())
|
.font(font.swiftUIFont())
|
||||||
.foregroundStyle(accent)
|
.foregroundStyle(textColor)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
case .lottie:
|
case .lottie:
|
||||||
Image(systemName: "sparkles")
|
Image(systemName: "sparkles")
|
||||||
|
|
@ -252,6 +346,12 @@ struct ExtensionProgressIndicatorView: View {
|
||||||
private var formatPercent: String {
|
private var formatPercent: String {
|
||||||
"\(Int(progress * 100))%"
|
"\(Int(progress * 100))%"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func resolvedRingDiameter(requested: CGFloat) -> CGFloat {
|
||||||
|
guard let maxVisualHeight else { return requested }
|
||||||
|
let clamped = min(requested, max(maxVisualHeight - 6, 18))
|
||||||
|
return max(18, clamped)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Edge Content Rendering
|
// MARK: - Edge Content Rendering
|
||||||
|
|
@ -264,23 +364,23 @@ struct ExtensionEdgeContentView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
switch content {
|
switch content {
|
||||||
case let .text(value, font: font):
|
case let .text(value, font: font, color: color):
|
||||||
Text(value)
|
Text(value)
|
||||||
.font(font.swiftUIFont())
|
.font(font.swiftUIFont())
|
||||||
.foregroundStyle(accent)
|
.foregroundStyle(resolvedColor(color))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
.frame(maxWidth: .infinity, alignment: alignment)
|
.frame(maxWidth: .infinity, alignment: alignment)
|
||||||
case let .marquee(value, font: font, minDuration: duration):
|
case let .marquee(value, font: font, minDuration: duration, color: color):
|
||||||
MarqueeText(
|
MarqueeText(
|
||||||
.constant(value),
|
.constant(value),
|
||||||
font: font.swiftUIFont(),
|
font: font.swiftUIFont(),
|
||||||
nsFont: .body,
|
nsFont: .body,
|
||||||
textColor: accent,
|
textColor: resolvedColor(color),
|
||||||
minDuration: duration,
|
minDuration: duration,
|
||||||
frameWidth: max(24, availableWidth - 12)
|
frameWidth: max(24, availableWidth - 12)
|
||||||
)
|
)
|
||||||
case let .countdownText(targetDate, font: font):
|
case let .countdownText(targetDate, font: font, color: color):
|
||||||
ExtensionCountdownTextView(targetDate: targetDate, font: font, accent: accent)
|
ExtensionCountdownTextView(targetDate: targetDate, font: font, accent: accent, customColor: color?.swiftUIColor)
|
||||||
.frame(maxWidth: .infinity, alignment: alignment)
|
.frame(maxWidth: .infinity, alignment: alignment)
|
||||||
case let .icon(descriptor):
|
case let .icon(descriptor):
|
||||||
ExtensionIconView(
|
ExtensionIconView(
|
||||||
|
|
@ -297,26 +397,30 @@ struct ExtensionEdgeContentView: View {
|
||||||
AudioSpectrumView(isPlaying: .constant(true))
|
AudioSpectrumView(isPlaying: .constant(true))
|
||||||
.frame(width: 16, height: 12)
|
.frame(width: 16, height: 12)
|
||||||
}
|
}
|
||||||
case .animation:
|
case let .animation(data, size):
|
||||||
Image(systemName: "sparkles")
|
let resolvedWidth = min(size.width, availableWidth)
|
||||||
.font(.system(size: 14, weight: .medium))
|
ExtensionLottieView(data: data, size: CGSize(width: resolvedWidth, height: size.height))
|
||||||
.foregroundStyle(accent)
|
|
||||||
case .none:
|
case .none:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func resolvedColor(_ descriptorColor: AtollColorDescriptor?) -> Color {
|
||||||
|
descriptorColor?.swiftUIColor ?? accent
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExtensionCountdownTextView: View {
|
struct ExtensionCountdownTextView: View {
|
||||||
let targetDate: Date
|
let targetDate: Date
|
||||||
let font: AtollFontDescriptor
|
let font: AtollFontDescriptor
|
||||||
let accent: Color
|
let accent: Color
|
||||||
|
let customColor: Color?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineView(.periodic(from: .now, by: 1)) { context in
|
TimelineView(.periodic(from: .now, by: 1)) { context in
|
||||||
Text(formattedRemaining(since: context.date))
|
Text(formattedRemaining(since: context.date))
|
||||||
.font(font.swiftUIFont())
|
.font(font.swiftUIFont())
|
||||||
.foregroundStyle(accent)
|
.foregroundStyle(customColor ?? accent)
|
||||||
.monospacedDigit()
|
.monospacedDigit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -337,8 +441,13 @@ struct ExtensionCountdownTextView: View {
|
||||||
|
|
||||||
enum ExtensionLayoutMetrics {
|
enum ExtensionLayoutMetrics {
|
||||||
static func trailingWidth(for payload: ExtensionLiveActivityPayload, baseWidth: CGFloat, maxWidth: CGFloat? = nil) -> CGFloat {
|
static func trailingWidth(for payload: ExtensionLiveActivityPayload, baseWidth: CGFloat, maxWidth: CGFloat? = nil) -> CGFloat {
|
||||||
var width = edgeWidth(for: payload.descriptor.trailingContent, baseWidth: baseWidth, maxWidth: maxWidth)
|
let renderable = resolvedExtensionTrailingRenderable(for: payload.descriptor)
|
||||||
if let indicator = payload.descriptor.progressIndicator {
|
var width: CGFloat
|
||||||
|
switch renderable {
|
||||||
|
case let .content(content):
|
||||||
|
width = edgeWidth(for: content, baseWidth: baseWidth, maxWidth: maxWidth)
|
||||||
|
case let .indicator(indicator):
|
||||||
|
width = edgeWidth(for: .none, baseWidth: baseWidth, maxWidth: maxWidth)
|
||||||
width = max(width, widthForProgress(indicator))
|
width = max(width, widthForProgress(indicator))
|
||||||
}
|
}
|
||||||
if let maxWidth {
|
if let maxWidth {
|
||||||
|
|
@ -357,13 +466,13 @@ enum ExtensionLayoutMetrics {
|
||||||
|
|
||||||
private static func widthForContent(_ content: AtollTrailingContent, baseWidth: CGFloat) -> CGFloat {
|
private static func widthForContent(_ content: AtollTrailingContent, baseWidth: CGFloat) -> CGFloat {
|
||||||
switch content {
|
switch content {
|
||||||
case let .text(text, font: font):
|
case let .text(text, font: font, color: _):
|
||||||
let measured = ExtensionTextMeasurer.width(for: text, font: font.nsFont())
|
let measured = ExtensionTextMeasurer.width(for: text, font: font.nsFont())
|
||||||
return max(baseWidth, measured + 32)
|
return max(baseWidth, measured + 32)
|
||||||
case let .marquee(text, font: font, _):
|
case let .marquee(text, font: font, minDuration: _, color: _):
|
||||||
let measured = ExtensionTextMeasurer.width(for: text, font: font.nsFont())
|
let measured = ExtensionTextMeasurer.width(for: text, font: font.nsFont())
|
||||||
return max(baseWidth, min(measured + 40, baseWidth * 2))
|
return max(baseWidth, min(measured + 40, baseWidth * 2))
|
||||||
case let .countdownText(targetDate, font: font):
|
case let .countdownText(targetDate, font: font, color: _):
|
||||||
let sample = targetDate.timeIntervalSinceNow >= 3600 ? "00:00:00" : "00:00"
|
let sample = targetDate.timeIntervalSinceNow >= 3600 ? "00:00:00" : "00:00"
|
||||||
let measured = ExtensionTextMeasurer.width(for: sample, font: font.nsFont())
|
let measured = ExtensionTextMeasurer.width(for: sample, font: font.nsFont())
|
||||||
return max(baseWidth, measured + 24)
|
return max(baseWidth, measured + 24)
|
||||||
|
|
@ -380,13 +489,13 @@ enum ExtensionLayoutMetrics {
|
||||||
|
|
||||||
private static func widthForProgress(_ indicator: AtollProgressIndicator) -> CGFloat {
|
private static func widthForProgress(_ indicator: AtollProgressIndicator) -> CGFloat {
|
||||||
switch indicator {
|
switch indicator {
|
||||||
case let .ring(diameter, _):
|
case let .ring(diameter, _, _):
|
||||||
return diameter + 20
|
return diameter + 20
|
||||||
case let .bar(width, _, _):
|
case let .bar(width, _, _, _):
|
||||||
return (width ?? 72) + 12
|
return (width ?? 72) + 12
|
||||||
case .percentage:
|
case .percentage(_, _):
|
||||||
return 60
|
return 60
|
||||||
case .countdown:
|
case .countdown(_, _):
|
||||||
return 74
|
return 74
|
||||||
case let .lottie(_, size):
|
case let .lottie(_, size):
|
||||||
return size.width + 16
|
return size.width + 16
|
||||||
|
|
@ -402,3 +511,41 @@ enum ExtensionTextMeasurer {
|
||||||
return max(1, text.size(withAttributes: attributes).width)
|
return max(1, text.size(withAttributes: attributes).width)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ExtensionTrailingRenderable {
|
||||||
|
case content(AtollTrailingContent)
|
||||||
|
case indicator(AtollProgressIndicator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvedExtensionLeadingContent(for descriptor: AtollLiveActivityDescriptor) -> AtollTrailingContent {
|
||||||
|
guard let override = descriptor.leadingContent else {
|
||||||
|
return .icon(descriptor.leadingIcon)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch override {
|
||||||
|
case .icon, .animation:
|
||||||
|
return override
|
||||||
|
default:
|
||||||
|
return .icon(descriptor.leadingIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolvedExtensionTrailingRenderable(for descriptor: AtollLiveActivityDescriptor) -> ExtensionTrailingRenderable {
|
||||||
|
if let indicator = descriptor.progressIndicator,
|
||||||
|
indicator.isRenderable,
|
||||||
|
descriptor.trailingContent == .none {
|
||||||
|
return .indicator(indicator)
|
||||||
|
}
|
||||||
|
return .content(descriptor.trailingContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AtollProgressIndicator {
|
||||||
|
var isRenderable: Bool {
|
||||||
|
switch self {
|
||||||
|
case .none:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -143,8 +143,11 @@ struct ExtensionsSettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
private struct ExtensionEntryRow: View {
|
private struct ExtensionEntryRow: View {
|
||||||
@ObservedObject private var authManager = ExtensionAuthorizationManager.shared
|
@ObservedObject private var authManager = ExtensionAuthorizationManager.shared
|
||||||
|
@ObservedObject private var liveActivityManager = ExtensionLiveActivityManager.shared
|
||||||
|
@ObservedObject private var widgetManager = ExtensionLockScreenWidgetManager.shared
|
||||||
let entry: ExtensionAuthorizationEntry
|
let entry: ExtensionAuthorizationEntry
|
||||||
let onRemove: () -> Void
|
let onRemove: () -> Void
|
||||||
|
|
||||||
|
|
@ -395,6 +398,8 @@ private struct ExtensionEntryRow: View {
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
resetMenu
|
||||||
|
|
||||||
Button("Remove") {
|
Button("Remove") {
|
||||||
onRemove()
|
onRemove()
|
||||||
}
|
}
|
||||||
|
|
@ -403,6 +408,32 @@ private struct ExtensionEntryRow: View {
|
||||||
.tint(.red)
|
.tint(.red)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var resetMenu: some View {
|
||||||
|
Menu {
|
||||||
|
Button("Reset Live Activities") {
|
||||||
|
liveActivityManager.dismissAll(for: entry.bundleIdentifier)
|
||||||
|
}
|
||||||
|
.disabled(!hasLiveActivities)
|
||||||
|
|
||||||
|
Button("Reset Lock Screen Widgets") {
|
||||||
|
widgetManager.dismissAll(for: entry.bundleIdentifier)
|
||||||
|
}
|
||||||
|
.disabled(!hasWidgets)
|
||||||
|
} label: {
|
||||||
|
Label("Reset", systemImage: "arrow.counterclockwise.circle")
|
||||||
|
}
|
||||||
|
.menuStyle(.borderlessButton)
|
||||||
|
.controlSize(.small)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hasLiveActivities: Bool {
|
||||||
|
liveActivityManager.activeActivities.contains { $0.bundleIdentifier == entry.bundleIdentifier }
|
||||||
|
}
|
||||||
|
|
||||||
|
private var hasWidgets: Bool {
|
||||||
|
widgetManager.activeWidgets.contains { $0.bundleIdentifier == entry.bundleIdentifier }
|
||||||
|
}
|
||||||
|
|
||||||
private func infoRow(label: String, value: String) -> some View {
|
private func infoRow(label: String, value: String) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import Defaults
|
import Defaults
|
||||||
import AtollExtensionKit
|
import AtollExtensionKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
final class ExtensionLiveActivityManager: ObservableObject {
|
final class ExtensionLiveActivityManager: ObservableObject {
|
||||||
|
|
@ -39,6 +40,7 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
||||||
throw ExtensionValidationError.invalidDescriptor("Structure validation failed")
|
throw ExtensionValidationError.invalidDescriptor("Structure validation failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let isUpdate: Bool
|
||||||
if let index = activeActivities.firstIndex(where: { $0.descriptor.id == descriptor.id && $0.bundleIdentifier == bundleIdentifier }) {
|
if let index = activeActivities.firstIndex(where: { $0.descriptor.id == descriptor.id && $0.bundleIdentifier == bundleIdentifier }) {
|
||||||
let payload = ExtensionLiveActivityPayload(
|
let payload = ExtensionLiveActivityPayload(
|
||||||
bundleIdentifier: bundleIdentifier,
|
bundleIdentifier: bundleIdentifier,
|
||||||
|
|
@ -49,25 +51,35 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
||||||
sortActivities()
|
sortActivities()
|
||||||
authorizationManager.recordActivity(for: bundleIdentifier, scope: .liveActivities)
|
authorizationManager.recordActivity(for: bundleIdentifier, scope: .liveActivities)
|
||||||
Logger.log("Replaced extension live activity \(descriptor.id) for \(bundleIdentifier)", category: .extensions)
|
Logger.log("Replaced extension live activity \(descriptor.id) for \(bundleIdentifier)", category: .extensions)
|
||||||
broadcastSnapshot()
|
isUpdate = true
|
||||||
return
|
} else {
|
||||||
}
|
guard activeActivities.count < Defaults[maxCapacityKey] else {
|
||||||
|
logDiagnostics("Rejected live activity \(descriptor.id) from \(bundleIdentifier): capacity limit \(Defaults[maxCapacityKey]) reached")
|
||||||
|
throw ExtensionValidationError.exceedsCapacity
|
||||||
|
}
|
||||||
|
|
||||||
guard activeActivities.count < Defaults[maxCapacityKey] else {
|
let payload = ExtensionLiveActivityPayload(
|
||||||
logDiagnostics("Rejected live activity \(descriptor.id) from \(bundleIdentifier): capacity limit \(Defaults[maxCapacityKey]) reached")
|
bundleIdentifier: bundleIdentifier,
|
||||||
throw ExtensionValidationError.exceedsCapacity
|
descriptor: descriptor,
|
||||||
|
receivedAt: .now
|
||||||
|
)
|
||||||
|
activeActivities.append(payload)
|
||||||
|
sortActivities()
|
||||||
|
authorizationManager.recordActivity(for: bundleIdentifier, scope: .liveActivities)
|
||||||
|
logDiagnostics("Queued live activity \(descriptor.id) for \(bundleIdentifier); total activities: \(activeActivities.count)")
|
||||||
|
isUpdate = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload = ExtensionLiveActivityPayload(
|
|
||||||
bundleIdentifier: bundleIdentifier,
|
|
||||||
descriptor: descriptor,
|
|
||||||
receivedAt: .now
|
|
||||||
)
|
|
||||||
activeActivities.append(payload)
|
|
||||||
sortActivities()
|
|
||||||
authorizationManager.recordActivity(for: bundleIdentifier, scope: .liveActivities)
|
|
||||||
logDiagnostics("Queued live activity \(descriptor.id) for \(bundleIdentifier); total activities: \(activeActivities.count)")
|
|
||||||
broadcastSnapshot()
|
broadcastSnapshot()
|
||||||
|
|
||||||
|
// Trigger sneak peek (defaulting to enabled for legacy descriptors)
|
||||||
|
let resolvedConfig = descriptor.sneakPeekConfig ?? .default
|
||||||
|
if resolvedConfig.enabled {
|
||||||
|
let shouldShow = !isUpdate || resolvedConfig.showOnUpdate
|
||||||
|
if shouldShow {
|
||||||
|
triggerSneakPeek(for: descriptor, bundleIdentifier: bundleIdentifier, config: resolvedConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(descriptor: AtollLiveActivityDescriptor, bundleIdentifier: String) throws {
|
func update(descriptor: AtollLiveActivityDescriptor, bundleIdentifier: String) throws {
|
||||||
|
|
@ -92,7 +104,9 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
||||||
|
|
||||||
func dismiss(activityID: String, bundleIdentifier: String) {
|
func dismiss(activityID: String, bundleIdentifier: String) {
|
||||||
let previousCount = activeActivities.count
|
let previousCount = activeActivities.count
|
||||||
activeActivities.removeAll { $0.descriptor.id == activityID && $0.bundleIdentifier == bundleIdentifier }
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
|
||||||
|
activeActivities.removeAll { $0.descriptor.id == activityID && $0.bundleIdentifier == bundleIdentifier }
|
||||||
|
}
|
||||||
if previousCount != activeActivities.count {
|
if previousCount != activeActivities.count {
|
||||||
Logger.log("Dismissed extension live activity \(activityID) from \(bundleIdentifier)", category: .extensions)
|
Logger.log("Dismissed extension live activity \(activityID) from \(bundleIdentifier)", category: .extensions)
|
||||||
ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: activityID)
|
ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: activityID)
|
||||||
|
|
@ -105,7 +119,9 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
||||||
let ids = activeActivities
|
let ids = activeActivities
|
||||||
.filter { $0.bundleIdentifier == bundleIdentifier }
|
.filter { $0.bundleIdentifier == bundleIdentifier }
|
||||||
.map { $0.descriptor.id }
|
.map { $0.descriptor.id }
|
||||||
activeActivities.removeAll { $0.bundleIdentifier == bundleIdentifier }
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.9)) {
|
||||||
|
activeActivities.removeAll { $0.bundleIdentifier == bundleIdentifier }
|
||||||
|
}
|
||||||
ids.forEach { ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: $0) }
|
ids.forEach { ExtensionXPCServiceHost.shared.notifyActivityDismiss(bundleIdentifier: bundleIdentifier, activityID: $0) }
|
||||||
if !ids.isEmpty {
|
if !ids.isEmpty {
|
||||||
logDiagnostics("Removed all live activities for \(bundleIdentifier); ids: \(ids.joined(separator: ", "))")
|
logDiagnostics("Removed all live activities for \(bundleIdentifier); ids: \(ids.joined(separator: ", "))")
|
||||||
|
|
@ -119,6 +135,10 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
||||||
.sorted(by: descriptorComparator)
|
.sorted(by: descriptorComparator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func payload(bundleIdentifier: String, activityID: String) -> ExtensionLiveActivityPayload? {
|
||||||
|
activeActivities.first { $0.bundleIdentifier == bundleIdentifier && $0.descriptor.id == activityID }
|
||||||
|
}
|
||||||
|
|
||||||
private func descriptorComparator(lhs: ExtensionLiveActivityPayload, rhs: ExtensionLiveActivityPayload) -> Bool {
|
private func descriptorComparator(lhs: ExtensionLiveActivityPayload, rhs: ExtensionLiveActivityPayload) -> Bool {
|
||||||
if lhs.descriptor.priority == rhs.descriptor.priority {
|
if lhs.descriptor.priority == rhs.descriptor.priority {
|
||||||
return lhs.receivedAt < rhs.receivedAt
|
return lhs.receivedAt < rhs.receivedAt
|
||||||
|
|
@ -144,6 +164,39 @@ final class ExtensionLiveActivityManager: ObservableObject {
|
||||||
logDiagnostics("Applied external live activity snapshot from PID \(sourcePID) (count: \(payloads.count))")
|
logDiagnostics("Applied external live activity snapshot from PID \(sourcePID) (count: \(payloads.count))")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func triggerSneakPeek(for descriptor: AtollLiveActivityDescriptor, bundleIdentifier: String, config: AtollSneakPeekConfig) {
|
||||||
|
let coordinator = DynamicIslandViewCoordinator.shared
|
||||||
|
let duration = config.duration ?? 2.5
|
||||||
|
let accentColor = descriptor.accentColor.swiftUIColor
|
||||||
|
let styleOverride: SneakPeekStyle? = {
|
||||||
|
guard let requestedStyle = config.style else { return nil }
|
||||||
|
switch requestedStyle {
|
||||||
|
case .inline:
|
||||||
|
return .inline
|
||||||
|
case .standard:
|
||||||
|
return .standard
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
let resolvedTitle = descriptor.sneakPeekTitle?.isEmpty == false ? descriptor.sneakPeekTitle! : descriptor.title
|
||||||
|
let resolvedSubtitle = descriptor.sneakPeekSubtitle ?? descriptor.subtitle ?? ""
|
||||||
|
|
||||||
|
// Pass the activity's id and bundleID to the sneak peek so the specialized view can look up details if needed
|
||||||
|
coordinator.toggleSneakPeek(
|
||||||
|
status: true,
|
||||||
|
type: .extensionLiveActivity(bundleID: bundleIdentifier, activityID: descriptor.id),
|
||||||
|
duration: duration,
|
||||||
|
value: 0,
|
||||||
|
icon: "",
|
||||||
|
title: resolvedTitle,
|
||||||
|
subtitle: resolvedSubtitle,
|
||||||
|
accentColor: accentColor,
|
||||||
|
styleOverride: styleOverride
|
||||||
|
)
|
||||||
|
|
||||||
|
logDiagnostics("Triggered sneak peek for \(descriptor.id) from \(bundleIdentifier) with duration \(duration)s")
|
||||||
|
}
|
||||||
|
|
||||||
private func logDiagnostics(_ message: String) {
|
private func logDiagnostics(_ message: String) {
|
||||||
guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return }
|
guard Defaults[.extensionDiagnosticsLoggingEnabled] else { return }
|
||||||
Logger.log(message, category: .extensions)
|
Logger.log(message, category: .extensions)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue