Swift 100%
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// KAWidgets.swift — widget « Le pouls » : les compteurs EN DIRECT de3// l'écosystème Groupe-KA sur l'écran d'accueil (petit : 1 chiffre-choc ;4// moyen : 3 univers ; grand : 6 univers). Rafraîchi ~toutes les 30 min.5import WidgetKit6import SwiftUI78struct PulseEntry: TimelineEntry {9 let date: Date10 let counts: [(name: String, unit: String, value: Int, hex: String)]11}1213struct PulseProvider: TimelineProvider {14 static let sources: [(name: String, unit: String, url: String, keys: [String], hex: String)] = [15 ("Lou·Ka", "logements à louer", "https://www.lou-ka.com/api/stats", ["total"], "#ff6a00"),16 ("Immo·Ka", "propriétés à vendre", "https://www.immo-ka.com/api/stats", ["total"], "#e23744"),17 ("Job·Ka", "offres d'emploi", "https://www.job-ka.com/api/stats", ["total"], "#0c8599"),18 ("Sorti·Ka", "événements", "https://www.sorti-ka.com/api/stats", ["total_active", "total"], "#d6336c"),19 ("Auto·Ka", "véhicules", "https://www.auto-ka.com/api/stats", ["total"], "#ff5a2a"),20 ("Resto·Ka", "restaurants", "https://www.resto-ka.com/api/stats", ["restaurants", "total"], "#f08c00"),21 ]2223 static let placeholderCounts: [(String, String, Int, String)] = [24 ("Lou·Ka", "logements à louer", 33900, "#ff6a00"),25 ("Immo·Ka", "propriétés à vendre", 67000, "#e23744"),26 ("Job·Ka", "offres d'emploi", 4400, "#0c8599"),27 ("Sorti·Ka", "événements", 15300, "#d6336c"),28 ("Auto·Ka", "véhicules", 17000, "#ff5a2a"),29 ("Resto·Ka", "restaurants", 14000, "#f08c00"),30 ]3132 func placeholder(in context: Context) -> PulseEntry {33 PulseEntry(date: .now, counts: Self.placeholderCounts)34 }3536 func getSnapshot(in context: Context, completion: @escaping (PulseEntry) -> Void) {37 completion(placeholder(in: context))38 }3940 func getTimeline(in context: Context, completion: @escaping (Timeline<PulseEntry>) -> Void) {41 Task {42 var counts: [(String, String, Int, String)] = []43 for s in Self.sources {44 if let url = URL(string: s.url),45 let (data, _) = try? await URLSession.shared.data(from: url),46 let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {47 for k in s.keys {48 if let n = obj[k] as? Int { counts.append((s.name, s.unit, n, s.hex)); break }49 if let n = obj[k] as? Double { counts.append((s.name, s.unit, Int(n), s.hex)); break }50 }51 }52 }53 let entry = PulseEntry(date: .now, counts: counts.isEmpty ? Self.placeholderCounts : counts)54 completion(Timeline(entries: [entry], policy: .after(.now.addingTimeInterval(1800))))55 }56 }57}5859struct PulseWidgetView: View {60 var entry: PulseEntry61 @Environment(\.widgetFamily) private var family6263 private func hexColor(_ h: String) -> Color {64 let s = h.dropFirst(); let v = UInt64(s, radix: 16) ?? 065 return Color(.sRGB, red: Double((v >> 16) & 0xFF) / 255,66 green: Double((v >> 8) & 0xFF) / 255, blue: Double(v & 0xFF) / 255)67 }6869 var body: some View {70 VStack(alignment: .leading, spacing: family == .systemSmall ? 4 : 8) {71 HStack(spacing: 4) {72 Text("Groupe").font(.system(.caption, design: .rounded).weight(.bold))73 Text("KA").font(.system(.caption2, design: .rounded).weight(.bold))74 .foregroundStyle(Color(.sRGB, red: 0.85, green: 0.95, blue: 0.42))75 .padding(.horizontal, 5).padding(.vertical, 1)76 .background(.black, in: RoundedRectangle(cornerRadius: 5))77 Spacer()78 Text("en direct").font(.system(size: 8, design: .monospaced)).foregroundStyle(.secondary)79 }80 let rows = Array(entry.counts.prefix(family == .systemSmall ? 1 : family == .systemMedium ? 3 : 6))81 ForEach(rows, id: \.name) { c in82 VStack(alignment: .leading, spacing: 0) {83 Text(c.value.formatted(.number.locale(Locale(identifier: "fr_CA"))))84 .font(.system(family == .systemSmall ? .title2 : .headline, design: .rounded).weight(.bold))85 .foregroundStyle(hexColor(c.hex))86 .minimumScaleFactor(0.6).lineLimit(1)87 Text("\(c.name) · \(c.unit)")88 .font(.system(size: 9)).foregroundStyle(.secondary).lineLimit(1)89 }90 }91 Spacer(minLength: 0)92 }93 .containerBackground(for: .widget) { Color(.systemBackground) }94 }95}9697@main98struct KAWidgets: WidgetBundle {99 var body: some Widget { PulseWidget() }100}101102struct PulseWidget: Widget {103 var body: some WidgetConfiguration {104 StaticConfiguration(kind: "KAPulse", provider: PulseProvider()) { entry in105 PulseWidgetView(entry: entry)106 }107 .configurationDisplayName("Le pouls de l'écosystème")108 .description("Les compteurs en direct du Groupe KA : logements, propriétés, emplois, sorties…")109 .supportedFamilies([.systemSmall, .systemMedium, .systemLarge])110 }111}112