200 lines
9.1 KiB
Swift
200 lines
9.1 KiB
Swift
import ActivityKit
|
|
import SwiftUI
|
|
import WidgetKit
|
|
|
|
// Mantine v8 avatar palette — exact hex parity with RosettaColors.avatarColors
|
|
// tint = shade-6, text = shade-3
|
|
|
|
private struct AvatarPalette {
|
|
let tint: Color
|
|
let text: Color
|
|
}
|
|
|
|
private let mantinePalette: [AvatarPalette] = [
|
|
AvatarPalette(tint: Color(red: 0x22/255, green: 0x8B/255, blue: 0xE6/255), text: Color(red: 0x74/255, green: 0xC0/255, blue: 0xFC/255)), // blue
|
|
AvatarPalette(tint: Color(red: 0x15/255, green: 0xAA/255, blue: 0xBF/255), text: Color(red: 0x66/255, green: 0xD9/255, blue: 0xE8/255)), // cyan
|
|
AvatarPalette(tint: Color(red: 0xBE/255, green: 0x4B/255, blue: 0xDB/255), text: Color(red: 0xE5/255, green: 0x99/255, blue: 0xF7/255)), // grape
|
|
AvatarPalette(tint: Color(red: 0x40/255, green: 0xC0/255, blue: 0x57/255), text: Color(red: 0x8C/255, green: 0xE9/255, blue: 0x9A/255)), // green
|
|
AvatarPalette(tint: Color(red: 0x4C/255, green: 0x6E/255, blue: 0xF5/255), text: Color(red: 0x91/255, green: 0xA7/255, blue: 0xFF/255)), // indigo
|
|
AvatarPalette(tint: Color(red: 0x82/255, green: 0xC9/255, blue: 0x1E/255), text: Color(red: 0xC0/255, green: 0xEB/255, blue: 0x75/255)), // lime
|
|
AvatarPalette(tint: Color(red: 0xFD/255, green: 0x7E/255, blue: 0x14/255), text: Color(red: 0xFF/255, green: 0xC0/255, blue: 0x78/255)), // orange
|
|
AvatarPalette(tint: Color(red: 0xE6/255, green: 0x49/255, blue: 0x80/255), text: Color(red: 0xFA/255, green: 0xA2/255, blue: 0xC1/255)), // pink
|
|
AvatarPalette(tint: Color(red: 0xFA/255, green: 0x52/255, blue: 0x52/255), text: Color(red: 0xFF/255, green: 0xA8/255, blue: 0xA8/255)), // red
|
|
AvatarPalette(tint: Color(red: 0x12/255, green: 0xB8/255, blue: 0x86/255), text: Color(red: 0x63/255, green: 0xE6/255, blue: 0xBE/255)), // teal
|
|
AvatarPalette(tint: Color(red: 0x79/255, green: 0x50/255, blue: 0xF2/255), text: Color(red: 0xB1/255, green: 0x97/255, blue: 0xFC/255)), // violet
|
|
]
|
|
|
|
// Mantine dark body background
|
|
private let mantineDarkBody = Color(red: 0x1A/255, green: 0x1B/255, blue: 0x1E/255)
|
|
|
|
private let appGroupID = "group.com.rosetta.dev"
|
|
|
|
@main
|
|
struct CallLiveActivity: Widget {
|
|
var body: some WidgetConfiguration {
|
|
ActivityConfiguration(for: CallActivityAttributes.self) { context in
|
|
// MARK: - Lock Screen
|
|
HStack(spacing: 14) {
|
|
avatarView(context: context, size: 48, fontSize: 18)
|
|
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
Text(context.attributes.peerName)
|
|
.font(.system(size: 17, weight: .semibold))
|
|
.foregroundColor(.white)
|
|
.lineLimit(1)
|
|
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "lock.fill")
|
|
.font(.system(size: 10))
|
|
.foregroundColor(Color(white: 0.45))
|
|
|
|
if context.state.isActive {
|
|
Text(fmt(context.state.durationSec))
|
|
.font(.system(size: 14, weight: .medium).monospacedDigit())
|
|
.foregroundColor(Color(white: 0.55))
|
|
} else {
|
|
Text("Connecting...")
|
|
.font(.system(size: 14, weight: .medium))
|
|
.foregroundColor(Color(white: 0.55))
|
|
}
|
|
|
|
if context.state.isMuted {
|
|
Image(systemName: "mic.slash.fill")
|
|
.font(.system(size: 11))
|
|
.foregroundColor(.red)
|
|
}
|
|
}
|
|
}
|
|
Spacer()
|
|
Link(destination: URL(string: "rosetta://call/end")!) {
|
|
ZStack {
|
|
Circle().fill(Color.red)
|
|
Image(systemName: "phone.down.fill")
|
|
.font(.system(size: 15, weight: .semibold))
|
|
.foregroundColor(.white)
|
|
}
|
|
.frame(width: 40, height: 40)
|
|
}
|
|
}
|
|
.padding(.horizontal, 20)
|
|
.padding(.vertical, 14)
|
|
.activityBackgroundTint(Color(white: 0.08))
|
|
|
|
} dynamicIsland: { context in
|
|
DynamicIsland {
|
|
// MARK: - Expanded
|
|
DynamicIslandExpandedRegion(.leading) {
|
|
avatarView(context: context, size: 44, fontSize: 15)
|
|
.padding(.leading, 6)
|
|
.padding(.top, 4)
|
|
}
|
|
DynamicIslandExpandedRegion(.trailing) {
|
|
VStack(spacing: 2) {
|
|
if context.state.isActive {
|
|
Text(fmt(context.state.durationSec))
|
|
.font(.system(size: 16, weight: .bold).monospacedDigit())
|
|
.foregroundColor(.green)
|
|
} else {
|
|
Text("...")
|
|
.font(.system(size: 16, weight: .bold))
|
|
.foregroundColor(.orange)
|
|
}
|
|
if context.state.isMuted {
|
|
Image(systemName: "mic.slash.fill")
|
|
.font(.system(size: 11))
|
|
.foregroundColor(.red)
|
|
}
|
|
}
|
|
.padding(.trailing, 6)
|
|
.padding(.top, 4)
|
|
}
|
|
DynamicIslandExpandedRegion(.center) {
|
|
Text(context.attributes.peerName)
|
|
.font(.system(size: 16, weight: .semibold))
|
|
.foregroundColor(.white)
|
|
.lineLimit(1)
|
|
.padding(.top, 4)
|
|
}
|
|
DynamicIslandExpandedRegion(.bottom) {
|
|
HStack(spacing: 5) {
|
|
Circle()
|
|
.fill(context.state.isActive ? Color.green : Color.orange)
|
|
.frame(width: 6, height: 6)
|
|
Image(systemName: "lock.fill")
|
|
.font(.system(size: 9))
|
|
.foregroundColor(Color(white: 0.4))
|
|
Text("Rosetta · E2E encrypted")
|
|
.font(.system(size: 11, weight: .medium))
|
|
.foregroundColor(Color(white: 0.4))
|
|
}
|
|
.padding(.top, 4)
|
|
}
|
|
} compactLeading: {
|
|
avatarView(context: context, size: 26, fontSize: 9)
|
|
} compactTrailing: {
|
|
if context.state.isActive {
|
|
Text(fmt(context.state.durationSec))
|
|
.font(.system(size: 13, weight: .semibold).monospacedDigit())
|
|
.foregroundColor(.green)
|
|
} else {
|
|
Image(systemName: "phone.fill")
|
|
.font(.system(size: 12))
|
|
.foregroundColor(.green)
|
|
}
|
|
} minimal: {
|
|
Image(systemName: "phone.fill")
|
|
.font(.system(size: 11))
|
|
.foregroundColor(.green)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Avatar (Mantine "light" dark variant — matches AvatarView in main app)
|
|
|
|
@ViewBuilder
|
|
private func avatarView(context: ActivityViewContext<CallActivityAttributes>, size: CGFloat, fontSize: CGFloat) -> some View {
|
|
if let data = context.attributes.avatarThumb,
|
|
let img = UIImage(data: data) {
|
|
Image(uiImage: img)
|
|
.resizable()
|
|
.scaledToFill()
|
|
.frame(width: size, height: size)
|
|
.clipShape(Circle())
|
|
} else {
|
|
let idx = min(max(context.attributes.colorIndex, 0), mantinePalette.count - 1)
|
|
let palette = mantinePalette[idx]
|
|
ZStack {
|
|
// Base: Mantine dark body
|
|
Circle().fill(mantineDarkBody)
|
|
// Overlay: tint at 15% opacity (dark mode)
|
|
Circle().fill(palette.tint.opacity(0.15))
|
|
// Initials: shade-3 text color
|
|
Text(makeInitials(context.attributes.peerName, context.attributes.peerPublicKey))
|
|
.font(.system(size: fontSize, weight: .bold, design: .rounded))
|
|
.foregroundColor(palette.text)
|
|
}
|
|
.frame(width: size, height: size)
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func fmt(_ sec: Int) -> String {
|
|
String(format: "%d:%02d", sec / 60, sec % 60)
|
|
}
|
|
|
|
private func makeInitials(_ name: String, _ pubKey: String) -> String {
|
|
let words = name.trimmingCharacters(in: .whitespaces)
|
|
.split(whereSeparator: { $0.isWhitespace })
|
|
.filter { !$0.isEmpty }
|
|
switch words.count {
|
|
case 0:
|
|
return pubKey.isEmpty ? "?" : String(pubKey.prefix(2)).uppercased()
|
|
case 1:
|
|
return String(words[0].prefix(2)).uppercased()
|
|
default:
|
|
return "\(words[0].prefix(1))\(words[1].prefix(1))".uppercased()
|
|
}
|
|
}
|
|
}
|