Update: 2026-07-30 02:27:45
This commit is contained in:
@@ -2,83 +2,15 @@
|
||||
// MusadaqLiveActivity.swift
|
||||
// MusadaqLiveActivity
|
||||
//
|
||||
// Created by Hamza Aleghwairyeen on 07/05/2026.
|
||||
// Intentionally empty.
|
||||
//
|
||||
// This file used to hold the stock Xcode widget template (a "😀" static widget
|
||||
// with its own TimelineProvider). It was never added to
|
||||
// MusadaqLiveActivityBundle, so it only ever shipped as dead code — and its
|
||||
// file-scope `Provider` type collided conceptually with the control widget's.
|
||||
//
|
||||
// The real implementation is InvoiceBatchLiveActivity in
|
||||
// MusadaqLiveActivityBundle.swift.
|
||||
//
|
||||
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
struct Provider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> SimpleEntry {
|
||||
SimpleEntry(date: Date(), emoji: "😀")
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
||||
let entry = SimpleEntry(date: Date(), emoji: "😀")
|
||||
completion(entry)
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
||||
var entries: [SimpleEntry] = []
|
||||
|
||||
// Generate a timeline consisting of five entries an hour apart, starting from the current date.
|
||||
let currentDate = Date()
|
||||
for hourOffset in 0 ..< 5 {
|
||||
let entryDate = Calendar.current.date(byAdding: .hour, value: hourOffset, to: currentDate)!
|
||||
let entry = SimpleEntry(date: entryDate, emoji: "😀")
|
||||
entries.append(entry)
|
||||
}
|
||||
|
||||
let timeline = Timeline(entries: entries, policy: .atEnd)
|
||||
completion(timeline)
|
||||
}
|
||||
|
||||
// func relevances() async -> WidgetRelevances<Void> {
|
||||
// // Generate a list containing the contexts this widget is relevant in.
|
||||
// }
|
||||
}
|
||||
|
||||
struct SimpleEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let emoji: String
|
||||
}
|
||||
|
||||
struct MusadaqLiveActivityEntryView : View {
|
||||
var entry: Provider.Entry
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Time:")
|
||||
Text(entry.date, style: .time)
|
||||
|
||||
Text("Emoji:")
|
||||
Text(entry.emoji)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MusadaqLiveActivity: Widget {
|
||||
let kind: String = "MusadaqLiveActivity"
|
||||
|
||||
var body: some WidgetConfiguration {
|
||||
StaticConfiguration(kind: kind, provider: Provider()) { entry in
|
||||
if #available(iOS 17.0, *) {
|
||||
MusadaqLiveActivityEntryView(entry: entry)
|
||||
.containerBackground(.fill.tertiary, for: .widget)
|
||||
} else {
|
||||
MusadaqLiveActivityEntryView(entry: entry)
|
||||
.padding()
|
||||
.background()
|
||||
}
|
||||
}
|
||||
.configurationDisplayName("My Widget")
|
||||
.description("This is an example widget.")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview(as: .systemSmall) {
|
||||
MusadaqLiveActivity()
|
||||
} timeline: {
|
||||
SimpleEntry(date: .now, emoji: "😀")
|
||||
SimpleEntry(date: .now, emoji: "🤩")
|
||||
}
|
||||
import Foundation
|
||||
|
||||
@@ -10,11 +10,18 @@ import SwiftUI
|
||||
import ActivityKit
|
||||
|
||||
// ─── 1. Data Model ───────────────────────────────────
|
||||
//
|
||||
// Must stay identical to InvoiceBatchAttributes in Runner/AppDelegate.swift and
|
||||
// to the "content-state" keys the server sends in
|
||||
// NotificationService::dispatchLiveActivityUpdate(). A mismatch makes iOS drop
|
||||
// the update silently.
|
||||
struct InvoiceBatchAttributes: ActivityAttributes {
|
||||
public struct ContentState: Codable, Hashable {
|
||||
var current: Int
|
||||
var total: Int
|
||||
var isDone: Bool
|
||||
var failed: Int = 0
|
||||
var statusText: String = ""
|
||||
}
|
||||
var companyName: String
|
||||
}
|
||||
@@ -27,7 +34,31 @@ struct MusadaqLiveActivityBundle: WidgetBundle {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 3. Widget ───────────────────────────────────────
|
||||
// ─── 3. Copy helpers ─────────────────────────────────
|
||||
// A partially failed batch must not read as a clean success.
|
||||
private func headline(for state: InvoiceBatchAttributes.ContentState) -> String {
|
||||
if state.isDone {
|
||||
if state.failed > 0 && state.current > 0 { return "⚠️ اكتمل مع أخطاء" }
|
||||
if state.failed > 0 { return "❌ فشلت المعالجة" }
|
||||
return "✅ تم بنجاح"
|
||||
}
|
||||
return state.statusText.isEmpty
|
||||
? "مُصادَق — جارٍ الرفع..."
|
||||
: "مُصادَق — \(state.statusText)"
|
||||
}
|
||||
|
||||
private func subtitle(for state: InvoiceBatchAttributes.ContentState, company: String) -> String {
|
||||
var text = "\(state.current) / \(state.total) فاتورة"
|
||||
if state.failed > 0 {
|
||||
text += " — \(state.failed) فاشلة"
|
||||
}
|
||||
if !company.isEmpty {
|
||||
text += " — \(company)"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// ─── 4. Widget ───────────────────────────────────────
|
||||
struct InvoiceBatchLiveActivity: Widget {
|
||||
var body: some WidgetConfiguration {
|
||||
ActivityConfiguration(for: InvoiceBatchAttributes.self) { context in
|
||||
@@ -40,15 +71,17 @@ struct InvoiceBatchLiveActivity: Widget {
|
||||
.foregroundColor(Color(red: 0.831, green: 0.659, blue: 0.263)) // #D4A843
|
||||
.font(.title2)
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(context.state.isDone ? "✅ تم الرفع بنجاح" : "مُصادَق — جارٍ الرفع...")
|
||||
Text(headline(for: context.state))
|
||||
.font(.caption.bold())
|
||||
.foregroundColor(.white)
|
||||
ProgressView(
|
||||
value: Double(context.state.current),
|
||||
total: Double(context.state.total)
|
||||
value: Double(context.state.current + context.state.failed),
|
||||
total: Double(max(context.state.total, 1))
|
||||
)
|
||||
.tint(Color(red: 0.831, green: 0.659, blue: 0.263))
|
||||
Text("\(context.state.current) / \(context.state.total) فاتورة — \(context.attributes.companyName)")
|
||||
.tint(context.state.failed > 0
|
||||
? Color(red: 0.96, green: 0.62, blue: 0.04)
|
||||
: Color(red: 0.831, green: 0.659, blue: 0.263))
|
||||
Text(subtitle(for: context.state, company: context.attributes.companyName))
|
||||
.font(.caption2)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
@@ -66,8 +99,10 @@ struct InvoiceBatchLiveActivity: Widget {
|
||||
.font(.caption.bold()).foregroundColor(.white)
|
||||
}
|
||||
DynamicIslandExpandedRegion(.bottom) {
|
||||
ProgressView(value: Double(context.state.current),
|
||||
total: Double(context.state.total))
|
||||
// Divide-by-zero guard: total arrives as 0 if a push lands
|
||||
// before the first image is registered.
|
||||
ProgressView(value: Double(context.state.current + context.state.failed),
|
||||
total: Double(max(context.state.total, 1)))
|
||||
.tint(Color(red: 0.831, green: 0.659, blue: 0.263))
|
||||
}
|
||||
} compactLeading: {
|
||||
|
||||
@@ -2,53 +2,15 @@
|
||||
// MusadaqLiveActivityControl.swift
|
||||
// MusadaqLiveActivity
|
||||
//
|
||||
// Created by Hamza Aleghwairyeen on 07/05/2026.
|
||||
// Intentionally empty.
|
||||
//
|
||||
// This file used to hold the stock Xcode ControlWidget template — a "Start
|
||||
// Timer" toggle whose `perform()` body was an empty comment. It was not listed
|
||||
// in MusadaqLiveActivityBundle, so iOS never surfaced it; had it been surfaced,
|
||||
// it would have shown users a toggle that does nothing.
|
||||
//
|
||||
// Musadaq has no Control Center widget. If one is added later, declare it here
|
||||
// AND add it to MusadaqLiveActivityBundle.
|
||||
//
|
||||
|
||||
import AppIntents
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
struct MusadaqLiveActivityControl: ControlWidget {
|
||||
var body: some ControlWidgetConfiguration {
|
||||
StaticControlConfiguration(
|
||||
kind: "com.example.musadaqApp.MusadaqLiveActivity",
|
||||
provider: Provider()
|
||||
) { value in
|
||||
ControlWidgetToggle(
|
||||
"Start Timer",
|
||||
isOn: value,
|
||||
action: StartTimerIntent()
|
||||
) { isRunning in
|
||||
Label(isRunning ? "On" : "Off", systemImage: "timer")
|
||||
}
|
||||
}
|
||||
.displayName("Timer")
|
||||
.description("A an example control that runs a timer.")
|
||||
}
|
||||
}
|
||||
|
||||
extension MusadaqLiveActivityControl {
|
||||
struct Provider: ControlValueProvider {
|
||||
var previewValue: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
func currentValue() async throws -> Bool {
|
||||
let isRunning = true // Check if the timer is running
|
||||
return isRunning
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StartTimerIntent: SetValueIntent {
|
||||
static let title: LocalizedStringResource = "Start a timer"
|
||||
|
||||
@Parameter(title: "Timer is running")
|
||||
var value: Bool
|
||||
|
||||
func perform() async throws -> some IntentResult {
|
||||
// Start / stop the timer based on `value`.
|
||||
return .result()
|
||||
}
|
||||
}
|
||||
import Foundation
|
||||
|
||||
Reference in New Issue
Block a user