import ActivityKit import SwiftUI import WidgetKit /// Lock-screen / Dynamic Island rendering of the aggregate session Activity (UX_IOS §5.3): /// glance = running count + needs-you count + the most urgent session; tap opens the app /// (straight to the approval, via the notification-style route). Self-contained colors — the /// extension doesn't link the app's Theme, so the two palette constants it needs live here /// (the same teal/amber/blue as `Palette`). struct SessionLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: NucleicSessionAttributes.self) { context in // Lock screen banner. LockScreenView(state: context.state, hostName: context.attributes.hostName) .activityBackgroundTint(Color.black.opacity(0.55)) .activitySystemActionForegroundColor(.white) } dynamicIsland: { context in DynamicIsland { DynamicIslandExpandedRegion(.leading) { Label("\(context.state.runningCount)", systemImage: "circle.lefthalf.filled") .font(.headline) .foregroundStyle(ActivityPalette.active) } DynamicIslandExpandedRegion(.trailing) { if context.state.needsYouCount > 0 { Label("\(context.state.needsYouCount)", systemImage: "exclamationmark.triangle.fill") .font(.headline) .foregroundStyle(ActivityPalette.attention) } else { Label("0", systemImage: "checkmark.circle") .font(.headline) .foregroundStyle(.secondary) } } DynamicIslandExpandedRegion(.bottom) { if let title = context.state.topTitle { HStack(spacing: 6) { Image(systemName: context.state.topNeedsYou ? "exclamationmark.triangle.fill" : "circle.lefthalf.filled") .foregroundStyle(context.state.topNeedsYou ? ActivityPalette.attention : ActivityPalette.active) Text(title).font(.subheadline.weight(.medium)).lineLimit(1) Spacer() Text(statusLine(context.state)) .font(.caption).foregroundStyle(.secondary) } } else { Text(statusLine(context.state)) .font(.caption).foregroundStyle(.secondary) } } } compactLeading: { Image(systemName: "circle.lefthalf.filled") .foregroundStyle(ActivityPalette.active) } compactTrailing: { if context.state.needsYouCount > 0 { Text("\(context.state.needsYouCount)") .font(.caption2.bold()) .foregroundStyle(ActivityPalette.attention) } else { Text("\(context.state.runningCount)") .font(.caption2.bold()) .foregroundStyle(ActivityPalette.active) } } minimal: { Image(systemName: context.state.needsYouCount > 0 ? "exclamationmark.triangle.fill" : "circle.lefthalf.filled") .foregroundStyle(context.state.needsYouCount > 0 ? ActivityPalette.attention : ActivityPalette.active) } } } private func statusLine(_ state: NucleicSessionAttributes.ContentState) -> String { var parts: [String] = [] if state.runningCount > 0 { parts.append("\(state.runningCount) running") } if state.needsYouCount > 0 { parts.append("\(state.needsYouCount) need you") } return parts.isEmpty ? "Idle" : parts.joined(separator: " · ") } } private struct LockScreenView: View { let state: NucleicSessionAttributes.ContentState let hostName: String var body: some View { HStack(spacing: 12) { Image(systemName: state.needsYouCount > 0 ? "exclamationmark.triangle.fill" : "circle.lefthalf.filled") .font(.title2) .foregroundStyle(state.needsYouCount > 0 ? ActivityPalette.attention : ActivityPalette.active) VStack(alignment: .leading, spacing: 2) { if let title = state.topTitle { Text(title).font(.headline).lineLimit(1) } HStack(spacing: 4) { if state.runningCount > 0 { Text("\(state.runningCount) running").foregroundStyle(ActivityPalette.active) } if state.runningCount > 0 && state.needsYouCount > 0 { Text("·") } if state.needsYouCount > 0 { Text("\(state.needsYouCount) waiting for you") .foregroundStyle(ActivityPalette.attention) } if state.runningCount == 0 && state.needsYouCount == 0 { Text("Idle").foregroundStyle(.secondary) } } .font(.subheadline) } Spacer() Text(hostName) .font(.caption2).foregroundStyle(.secondary) .lineLimit(1) } .padding(14) .foregroundStyle(.white) } } /// The two categorical colors the Activity needs — same constants as the app's `Palette`. private enum ActivityPalette { static let active = Color(red: 0.20, green: 0.55, blue: 0.95) // running blue static let attention = Color(red: 1.00, green: 0.65, blue: 0.15) // needs-you amber }