You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
HMG_Patient_App_New/ios/AppWidget/AppWidgetLiveActivity.swift

368 lines
14 KiB
Swift

//
// AppWidgetLiveActivity.swift
// AppWidget
//
// Created by Cloud Solutions on 15/12/2025.
//
import ActivityKit
import WidgetKit
import SwiftUI
struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable {
public typealias LiveDeliveryData = ContentState // don't forget to add this line, otherwise, live activity will not display it.
public struct ContentState: Codable, Hashable { }
var id = UUID()
}
let sharedDefault = UserDefaults(suiteName: "group.alhabib.patientApp")!
struct AppWidgetAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
var emoji: String
}
// Fixed non-changing properties about your activity go here!
var name: String
}
extension Color {
static let redColorText = Color(hex: "#ED1C2B")
static let yellowColor = Color(hex: "#FFAF15")
static let successColor = Color(hex: "#18C273")
static let inactiveColor = Color(hex: "#ED1C2B")
static let textColor = Color(hex: "#A2A2A2")
static let colorWhite = Color(hex: "#FFFFFF")
// Helper initializer for hex colors
init(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
(a, r, g, b) = (255, 0, 0, 0)
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}
// MARK: - Widget Configuration based on nextStepBackground
struct WidgetColorConfig {
let nextStepBackground: Color
let nextStepTextColor: Color
let containerBorderColor: Color
static func config(for value: Int) -> WidgetColorConfig {
switch value {
case 0:
return WidgetColorConfig(
nextStepBackground: .inactiveColor,
nextStepTextColor: .textColor,
containerBorderColor: .yellowColor
)
case 1:
return WidgetColorConfig(
nextStepBackground: .redColorText,
nextStepTextColor: .colorWhite,
containerBorderColor: .redColorText
)
case 2:
return WidgetColorConfig(
nextStepBackground: .successColor,
nextStepTextColor: .colorWhite,
containerBorderColor: .successColor
)
default:
return WidgetColorConfig(
nextStepBackground: .inactiveColor,
nextStepTextColor: .textColor,
containerBorderColor: .yellowColor
)
}
}
}
// MARK: - Widget Data Model
struct NotificationWidgetData {
let currentTicket: String
let currentlyServingHeader: String
let currentlyServing: String
let status: String
let nextStep: String
let imageName: String
let colorConfig: WidgetColorConfig
}
// MARK: - Main Widget View
struct NotificationWidgetView: View {
private let data: NotificationWidgetData
init(data: NotificationWidgetData) {
self.data = data
}
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Top section with current ticket and image
HStack(alignment: .top, spacing: 0) {
// Current ticket value
Text(data.currentTicket)
.font(.custom("Poppins-Medium", size: 28))
.fontWeight(.bold)
.foregroundColor(.black)
.padding(.trailing, 10)
.frame(maxWidth: .infinity, alignment: .leading)
// Image
Image(systemName: data.imageName)
.resizable()
.scaledToFit()
.frame(width: 24, height: 24)
.foregroundColor(.gray)
}
// Currently Serving Header
Text(data.currentlyServingHeader)
.font(.custom("Poppins-Medium", size: 14))
.fontWeight(.bold)
.foregroundColor(.black)
.padding(.trailing, 12)
.padding(.bottom, 10)
// Currently Serving section with status
HStack(alignment: .center, spacing: 0) {
// Currently serving text
Text(data.currentlyServing)
.font(.custom("Poppins-Medium", size: 16))
.fontWeight(.bold)
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
// Status badge
Text(data.status)
.font(.custom("Poppins-Medium", size: 12))
.foregroundColor(data.colorConfig.nextStepTextColor)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.inactiveColor.opacity(0.2))
)
}
// Next Step badge
Text(data.nextStep)
.font(.custom("Poppins-Medium", size: 12))
.foregroundColor(data.colorConfig.nextStepTextColor)
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(data.colorConfig.nextStepBackground)
)
.padding(.top, 10)
}
.padding(12)
.background(
// 1. Use a Material for the glass/frosted effect
.thinMaterial,
// Define the shape for the material and the stroke
in: RoundedRectangle(cornerRadius: 12, style: .continuous)
)
.overlay(
// 2. Add the stroke (border) around the shape
RoundedRectangle(cornerRadius: 12, style: .continuous)
.stroke(data.colorConfig.containerBorderColor, lineWidth: 5)
)
}
}
// MARK: - Live Activity Widget
struct AppWidgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: LiveActivitiesAppAttributes.self) { context in
// Extract parameters from Flutter
let title = sharedDefault.string(forKey: context.attributes.prefixedKey("title")) ?? "test"
let contentText = sharedDefault.string(forKey: context.attributes.prefixedKey("contentText")) ?? "test"
let currentTicket = sharedDefault.string(forKey: context.attributes.prefixedKey("currentTicket")) ?? "test"
let status = sharedDefault.string(forKey: context.attributes.prefixedKey("status")) ?? "test"
let currentlyServing = sharedDefault.string(forKey: context.attributes.prefixedKey("currentlyServing")) ?? "test"
let nextStep = sharedDefault.string(forKey: context.attributes.prefixedKey("nextStep")) ?? "test"
let nextStepBackground = sharedDefault.integer(forKey: context.attributes.prefixedKey("nextStepBackground"))
// Get color configuration based on nextStepBackground
let colorConfig = WidgetColorConfig.config(for: nextStepBackground)
// Create widget data
let widgetData = NotificationWidgetData(
currentTicket: currentTicket,
currentlyServingHeader: "Currently Serving:",
currentlyServing: currentlyServing,
status: status,
nextStep: nextStep,
imageName: "bell.fill",
colorConfig: colorConfig
)
// Lock screen/banner UI
NotificationWidgetView(data: widgetData)
.activityBackgroundTint(Color.clear)
.activitySystemActionForegroundColor(Color.black)
} dynamicIsland: { context in
let currentTicket = sharedDefault.string(forKey: context.attributes.prefixedKey("currentTicket")) ?? ""
let status = sharedDefault.string(forKey: context.attributes.prefixedKey("status")) ?? ""
let currentlyServing = sharedDefault.string(forKey: context.attributes.prefixedKey("currentlyServing")) ?? ""
let nextStep = sharedDefault.string(forKey: context.attributes.prefixedKey("nextStep")) ?? ""
let nextStepBackground = sharedDefault.integer(forKey: context.attributes.prefixedKey("nextStepBackground"))
let colorConfig = WidgetColorConfig.config(for: nextStepBackground)
return DynamicIsland {
// Expanded UI
DynamicIslandExpandedRegion(.leading) {
VStack(alignment: .leading) {
Text(currentTicket)
.font(.system(size: 24, weight: .bold))
.foregroundColor(.primary)
}
}
DynamicIslandExpandedRegion(.trailing) {
Image(systemName: "bell.fill")
.foregroundColor(.gray)
}
DynamicIslandExpandedRegion(.center) {
VStack(spacing: 4) {
Text("Currently Serving:")
.font(.caption2)
.foregroundColor(.secondary)
Text(currentlyServing)
.font(.caption)
.fontWeight(.semibold)
}
}
DynamicIslandExpandedRegion(.bottom) {
HStack {
Text(status)
.font(.caption2)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(colorConfig.nextStepBackground.opacity(0.2))
)
Spacer()
Text(nextStep)
.font(.caption2)
.foregroundColor(colorConfig.nextStepTextColor)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(colorConfig.nextStepBackground)
)
}
.padding(.horizontal)
}
} compactLeading: {
// Compact leading view
Text(currentTicket)
.font(.caption2)
.fontWeight(.bold)
} compactTrailing: {
// Compact trailing view
Image(systemName: "bell.fill")
.foregroundColor(colorConfig.containerBorderColor)
} minimal: {
// Minimal view
Image(systemName: "bell.fill")
.foregroundColor(colorConfig.containerBorderColor)
}
.widgetURL(URL(string: "http://www.apple.com"))
.keylineTint(colorConfig.containerBorderColor)
}
}
}
extension LiveActivitiesAppAttributes {
func prefixedKey(_ key: String) -> String {
return "\(id)_\(key)"
}
}
// MARK: - Preview for Testing
struct NotificationWidgetView_Previews: PreviewProvider {
static var previews: some View {
Group {
// Preview with nextStepBackground = 0 (Yellow border)
NotificationWidgetView(
data: NotificationWidgetData(
currentTicket: "42",
currentlyServingHeader: "Currently Serving:",
currentlyServing: "Customer #15",
status: "Pending",
nextStep: "Next: Prepare Order",
imageName: "bell.fill",
colorConfig: .config(for: 0)
)
)
.previewDisplayName("Yellow Border (0)")
// Preview with nextStepBackground = 1 (Red border)
NotificationWidgetView(
data: NotificationWidgetData(
currentTicket: "89",
currentlyServingHeader: "Currently Serving:",
currentlyServing: "Customer #67",
status: "Active",
nextStep: "Next: Quality Check",
imageName: "bell.fill",
colorConfig: .config(for: 1)
)
)
.previewDisplayName("Red Border (1)")
// Preview with nextStepBackground = 2 (Green border)
NotificationWidgetView(
data: NotificationWidgetData(
currentTicket: "125",
currentlyServingHeader: "Currently Serving:",
currentlyServing: "Customer #98",
status: "Completed",
nextStep: "Next: Ready for Pickup",
imageName: "bell.fill",
colorConfig: .config(for: 2)
)
)
.previewDisplayName("Green Border (2)")
}
.padding()
.previewLayout(.sizeThatFits)
}
}