Update stuff

This commit is contained in:
Hariharan Mudaliar 2026-01-18 01:46:35 +05:30
commit 9214c61544
No known key found for this signature in database
GPG key ID: 025583EB0BE5640E
18 changed files with 985 additions and 118 deletions

View file

@ -16,7 +16,6 @@ struct ExtensionLiveActivityStandaloneView: View {
let payload: ExtensionLiveActivityPayload
let layout: ExtensionStandaloneLayout
let isHovering: Bool
let notchState: NotchState
private var descriptor: AtollLiveActivityDescriptor { payload.descriptor }
private var contentHeight: CGFloat { layout.contentHeight }
@ -35,10 +34,6 @@ struct ExtensionLiveActivityStandaloneView: View {
}
}
private var shouldSuppressCenterText: Bool {
layout.suppressingCenterText
}
var body: some View {
HStack(spacing: 0) {
ExtensionLeadingContentView(
@ -56,7 +51,9 @@ struct ExtensionLiveActivityStandaloneView: View {
.frame(width: layout.centerWidth, height: contentHeight)
.overlay(
Group {
if !shouldSuppressCenterText {
if layout.suppressingCenterText {
EmptyView()
} else {
ExtensionCenterContentView(
descriptor: descriptor,
accent: accentColor,
@ -75,10 +72,12 @@ struct ExtensionLiveActivityStandaloneView: View {
.frame(width: layout.trailingWidth, height: contentHeight)
}
.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))
))
.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)
.onAppear {
logExtensionDiagnostics("Displaying extension live activity \(payload.descriptor.id) for \(payload.bundleIdentifier) as standalone view")
@ -106,7 +105,7 @@ struct ExtensionMusicWingView: View {
switch trailingRenderable {
case let .content(content):
if case .none = content {
EmptyView()
Spacer(minLength: 0)
} else {
ExtensionEdgeContentView(
content: content,
@ -126,7 +125,7 @@ struct ExtensionMusicWingView: View {
)
.frame(maxWidth: .infinity, alignment: .trailing)
}
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
@ -239,10 +238,152 @@ private func logExtensionDiagnostics(_ message: String) {
Logger.log(message, category: .extensions)
}
struct ExtensionNotchExperienceTabView: View {
let payload: ExtensionNotchExperiencePayload
@Default(.enableExtensionNotchInteractiveWebViews) private var interactiveWebViewsEnabled
private var descriptor: AtollNotchExperienceDescriptor { payload.descriptor }
private var tabConfiguration: AtollNotchExperienceDescriptor.TabConfiguration? { descriptor.tab }
private var accentColor: Color { descriptor.accentColor.swiftUIColor }
private var allowInteractiveWebViews: Bool {
interactiveWebViewsEnabled && (tabConfiguration?.allowWebInteraction ?? false)
}
var body: some View {
Group {
if let tabConfiguration {
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 18) {
header(for: tabConfiguration)
ForEach(Array(tabConfiguration.sections.enumerated()), id: \.offset) { index, section in
ExtensionNotchSectionView(
section: section,
accent: accentColor,
allowWebInteraction: allowInteractiveWebViews
)
.accessibilityIdentifier("extension-notch-section-\(payload.descriptor.id)-\(index)")
}
if let webDescriptor = tabConfiguration.webContent {
ExtensionWebContentView(descriptor: webDescriptor, allowInteraction: allowInteractiveWebViews)
.frame(height: webDescriptor.preferredHeight)
.frame(maxWidth: webDescriptor.maximumContentWidth ?? .infinity)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
if let footnote = tabConfiguration.footnote {
Text(footnote)
.font(.system(size: 11, weight: .regular))
.foregroundStyle(Color.white.opacity(0.65))
.lineLimit(2)
}
}
.padding(.vertical, 14)
.padding(.horizontal, 16)
}
} else {
Text("Extension tab unavailable")
.font(.system(size: 13, weight: .medium))
.foregroundStyle(.white.opacity(0.6))
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(tabBackground)
.clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous))
}
@ViewBuilder
private func header(for configuration: AtollNotchExperienceDescriptor.TabConfiguration) -> some View {
HStack(spacing: 10) {
Group {
if let badgeIcon = configuration.badgeIcon {
ExtensionIconView(
descriptor: badgeIcon,
tint: accentColor,
size: CGSize(width: 32, height: 32),
cornerRadius: 10
)
} else {
Image(systemName: configuration.iconSymbolName ?? "puzzlepiece.extension")
.font(.system(size: 18, weight: .semibold))
.foregroundStyle(.white)
.frame(width: 36, height: 36)
.background(accentColor.opacity(0.2))
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
}
}
VStack(alignment: .leading, spacing: 2) {
Text(configuration.title)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(.white)
.lineLimit(1)
}
Spacer(minLength: 0)
}
}
private var tabBackground: some View {
AnyView(
LinearGradient(
colors: [Color.white.opacity(0.04), accentColor.opacity(0.08)],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
)
}
}
struct ExtensionNotchSectionView: View {
let section: AtollNotchContentSection
let accent: Color
let allowWebInteraction: Bool
var body: some View {
VStack(alignment: .leading, spacing: 12) {
ExtensionNotchSectionHeader(section: section)
layoutContent
}
.padding(14)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.white.opacity(0.04))
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
}
@ViewBuilder
private var layoutContent: some View {
switch section.layout {
case .stack:
VStack(alignment: .leading, spacing: 10) {
elementViews
}
case .columns:
LazyVGrid(columns: [GridItem(.flexible(), spacing: 10), GridItem(.flexible(), spacing: 10)], spacing: 12) {
elementViews
}
case .metrics:
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: 10) {
elementViews
}
}
}
@ViewBuilder
private var elementViews: some View {
ForEach(Array(section.elements.enumerated()), id: \.offset) { index, element in
ExtensionWidgetElementView(
element: element,
accent: accent,
allowWebInteraction: allowWebInteraction
)
.accessibilityIdentifier("extension-notch-element-\(index)")
}
}
}
struct ExtensionInlineSneakPeekView: View {
let payload: ExtensionLiveActivityPayload?
let title: String
let subtitle: String
let subtitle: String?
let accentColor: Color
let notchHeight: CGFloat
let closedNotchWidth: CGFloat
@ -251,7 +392,6 @@ struct ExtensionInlineSneakPeekView: View {
private var descriptor: AtollLiveActivityDescriptor? { payload?.descriptor }
private var resolvedAccent: Color { descriptor?.accentColor.swiftUIColor ?? accentColor }
private var contentHeight: CGFloat {
max(0, notchHeight - (isHovering ? 0 : 12))
}
@ -274,11 +414,19 @@ struct ExtensionInlineSneakPeekView: View {
}
private var resolvedTitle: String {
title.isEmpty ? "Extension" : title
let descriptorTitle = descriptor?.title ?? "Extension"
let preferred = title.isEmpty ? descriptorTitle : title
return preferred.isEmpty ? "Extension" : preferred
}
private var resolvedSubtitle: String {
subtitle.trimmingCharacters(in: .whitespacesAndNewlines)
if let subtitle, !subtitle.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return subtitle.trimmingCharacters(in: .whitespacesAndNewlines)
}
if let descriptorSubtitle = descriptor?.subtitle, !descriptorSubtitle.isEmpty {
return descriptorSubtitle
}
return ""
}
var body: some View {
@ -291,16 +439,14 @@ struct ExtensionInlineSneakPeekView: View {
.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
)
}
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))
@ -338,11 +484,6 @@ struct ExtensionInlineSneakPeekView: View {
@ViewBuilder
private var trailingWing: some View {
spectrumPlaceholder
}
@ViewBuilder
private var spectrumPlaceholder: some View {
VStack {
Rectangle()
.fill(resolvedAccent.gradient)
@ -357,3 +498,77 @@ struct ExtensionInlineSneakPeekView: View {
.padding(.vertical, 6)
}
}
struct ExtensionNotchSectionHeader: View {
let section: AtollNotchContentSection
var body: some View {
VStack(alignment: .leading, spacing: 2) {
if let title = section.title {
Text(title)
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.white)
}
if let subtitle = section.subtitle {
Text(subtitle)
.font(.system(size: 11, weight: .regular))
.foregroundStyle(Color.white.opacity(0.7))
}
}
}
}
struct ExtensionMinimalisticExperienceView: View {
let payload: ExtensionNotchExperiencePayload
let albumArtNamespace: Namespace.ID
@Default(.enableExtensionNotchInteractiveWebViews) private var interactiveWebViewsEnabled
private var descriptor: AtollNotchExperienceDescriptor { payload.descriptor }
private var configuration: AtollNotchExperienceDescriptor.MinimalisticConfiguration? { descriptor.minimalistic }
private var accent: Color { descriptor.accentColor.swiftUIColor }
var body: some View {
Group {
if let configuration {
let hasWebContent = configuration.webContent != nil
ScrollView(.vertical, showsIndicators: false) {
VStack(alignment: .leading, spacing: 16) {
if let headline = configuration.headline {
Text(headline)
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(.white)
}
if let subtitle = configuration.subtitle {
Text(subtitle)
.font(.system(size: 12, weight: .regular))
.foregroundStyle(Color.white.opacity(0.75))
}
ForEach(Array(configuration.sections.enumerated()), id: \.offset) { index, section in
ExtensionNotchSectionView(
section: section,
accent: accent,
allowWebInteraction: interactiveWebViewsEnabled
)
.accessibilityIdentifier("extension-minimalistic-section-\(payload.descriptor.id)-\(index)")
}
if let webDescriptor = configuration.webContent {
ExtensionWebContentView(
descriptor: webDescriptor,
allowInteraction: interactiveWebViewsEnabled
)
.frame(height: webDescriptor.preferredHeight)
.frame(maxWidth: webDescriptor.maximumContentWidth ?? .infinity)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
}
}
.padding(.horizontal, 14)
.padding(.top, 10)
.padding(.bottom, hasWebContent ? 0 : 10)
}
} else {
MinimalisticMusicPlayerView(albumArtNamespace: albumArtNamespace)
}
}
}
}

View file

@ -52,53 +52,12 @@ struct ExtensionLockScreenWidgetView: View {
}
}
@ViewBuilder
private func view(for element: AtollWidgetContentElement) -> some View {
switch element {
case let .text(text, font: font, color: color, alignment: _):
Text(text)
.font(font.swiftUIFont())
.foregroundStyle((color?.swiftUIColor) ?? Color.white.opacity(0.9))
.lineLimit(2)
case let .icon(iconDescriptor, tint):
ExtensionIconView(
descriptor: iconDescriptor,
tint: (tint?.swiftUIColor) ?? accentColor,
size: CGSize(width: 28, height: 28),
cornerRadius: 8
)
case let .progress(indicator, value, color):
ExtensionProgressIndicatorView(
indicator: indicator,
progress: value,
accent: (color?.swiftUIColor) ?? accentColor,
estimatedDuration: nil
)
case let .graph(data, color, size):
ExtensionGraphView(data: data, color: color.swiftUIColor, size: size)
.frame(width: size.width, height: size.height)
case let .gauge(value, minValue, maxValue, style, color):
ExtensionGaugeView(
value: value,
minValue: minValue,
maxValue: maxValue,
style: style,
accent: (color?.swiftUIColor) ?? accentColor
)
.frame(maxWidth: .infinity)
case let .spacer(height):
Color.clear
.frame(height: height)
case let .divider(color, thickness):
Rectangle()
.fill(color.swiftUIColor.opacity(0.4))
.frame(height: thickness)
case let .webView(webDescriptor):
ExtensionWebContentView(descriptor: webDescriptor)
.frame(height: webDescriptor.preferredHeight)
.frame(maxWidth: webDescriptor.maximumContentWidth ?? .infinity)
.allowsHitTesting(false)
}
ExtensionWidgetElementView(
element: element,
accent: accentColor,
allowWebInteraction: false
)
}
private func alignment(for element: AtollWidgetContentElement) -> Alignment {
@ -235,8 +194,62 @@ private func logWidgetDiagnostics(_ message: String) {
Logger.log(message, category: .extensions)
}
private struct ExtensionWebContentView: NSViewRepresentable {
struct ExtensionWidgetElementView: View {
let element: AtollWidgetContentElement
let accent: Color
let allowWebInteraction: Bool
var body: some View {
switch element {
case let .text(text, font: font, color: color, alignment: _):
Text(text)
.font(font.swiftUIFont())
.foregroundStyle((color?.swiftUIColor) ?? Color.white.opacity(0.9))
.lineLimit(2)
case let .icon(iconDescriptor, tint):
ExtensionIconView(
descriptor: iconDescriptor,
tint: (tint?.swiftUIColor) ?? accent,
size: CGSize(width: 28, height: 28),
cornerRadius: 8
)
case let .progress(indicator, value, color):
ExtensionProgressIndicatorView(
indicator: indicator,
progress: value,
accent: (color?.swiftUIColor) ?? accent,
estimatedDuration: nil
)
case let .graph(data, color, size):
ExtensionGraphView(data: data, color: color.swiftUIColor, size: size)
.frame(width: size.width, height: size.height)
case let .gauge(value, minValue, maxValue, style, color):
ExtensionGaugeView(
value: value,
minValue: minValue,
maxValue: maxValue,
style: style,
accent: (color?.swiftUIColor) ?? accent
)
.frame(maxWidth: .infinity)
case let .spacer(height):
Color.clear
.frame(height: height)
case let .divider(color, thickness):
Rectangle()
.fill(color.swiftUIColor.opacity(0.4))
.frame(height: thickness)
case let .webView(webDescriptor):
ExtensionWebContentView(descriptor: webDescriptor, allowInteraction: allowWebInteraction)
.frame(height: webDescriptor.preferredHeight)
.frame(maxWidth: webDescriptor.maximumContentWidth ?? .infinity)
}
}
}
struct ExtensionWebContentView: NSViewRepresentable {
let descriptor: AtollWidgetWebContentDescriptor
let allowInteraction: Bool
func makeCoordinator() -> Coordinator {
Coordinator(descriptor: descriptor)
@ -246,7 +259,8 @@ private struct ExtensionWebContentView: NSViewRepresentable {
let configuration = WKWebViewConfiguration()
configuration.suppressesIncrementalRendering = false
configuration.preferences.javaScriptCanOpenWindowsAutomatically = false
let webView = NonInteractiveWKWebView(frame: .zero, configuration: configuration)
let webView = ConfigurableWKWebView(frame: .zero, configuration: configuration)
webView.allowInteraction = allowInteraction
webView.navigationDelegate = context.coordinator
webView.wantsLayer = true
applyConfiguration(descriptor, to: webView)
@ -259,6 +273,9 @@ private struct ExtensionWebContentView: NSViewRepresentable {
context.coordinator.descriptor = descriptor
webView.navigationDelegate = context.coordinator
applyConfiguration(descriptor, to: webView)
if let configurableView = webView as? ConfigurableWKWebView {
configurableView.allowInteraction = allowInteraction
}
if context.coordinator.lastHTML != descriptor.html {
webView.loadHTMLString(descriptor.html, baseURL: nil)
context.coordinator.lastHTML = descriptor.html
@ -277,13 +294,16 @@ private struct ExtensionWebContentView: NSViewRepresentable {
}
}
private final class NonInteractiveWKWebView: WKWebView {
private final class ConfigurableWKWebView: WKWebView {
var allowInteraction = false
override func hitTest(_ point: NSPoint) -> NSView? {
nil
guard allowInteraction else { return nil }
return super.hitTest(point)
}
override var acceptsFirstResponder: Bool {
false
allowInteraction
}
}