// Auteur : Simon-Pierre Boucher — contact@spboucher.ai // KAWidgets.swift — widget « Le pouls » : les compteurs EN DIRECT de // l'écosystème Groupe-KA sur l'écran d'accueil (petit : 1 chiffre-choc ; // moyen : 3 univers ; grand : 6 univers). Rafraîchi ~toutes les 30 min. import WidgetKit import SwiftUI struct PulseEntry: TimelineEntry { let date: Date let counts: [(name: String, unit: String, value: Int, hex: String)] } struct PulseProvider: TimelineProvider { static let sources: [(name: String, unit: String, url: String, keys: [String], hex: String)] = [ ("Lou·Ka", "logements à louer", "https://www.lou-ka.com/api/stats", ["total"], "#ff6a00"), ("Immo·Ka", "propriétés à vendre", "https://www.immo-ka.com/api/stats", ["total"], "#e23744"), ("Job·Ka", "offres d'emploi", "https://www.job-ka.com/api/stats", ["total"], "#0c8599"), ("Sorti·Ka", "événements", "https://www.sorti-ka.com/api/stats", ["total_active", "total"], "#d6336c"), ("Auto·Ka", "véhicules", "https://www.auto-ka.com/api/stats", ["total"], "#ff5a2a"), ("Resto·Ka", "restaurants", "https://www.resto-ka.com/api/stats", ["restaurants", "total"], "#f08c00"), ] static let placeholderCounts: [(String, String, Int, String)] = [ ("Lou·Ka", "logements à louer", 33900, "#ff6a00"), ("Immo·Ka", "propriétés à vendre", 67000, "#e23744"), ("Job·Ka", "offres d'emploi", 4400, "#0c8599"), ("Sorti·Ka", "événements", 15300, "#d6336c"), ("Auto·Ka", "véhicules", 17000, "#ff5a2a"), ("Resto·Ka", "restaurants", 14000, "#f08c00"), ] func placeholder(in context: Context) -> PulseEntry { PulseEntry(date: .now, counts: Self.placeholderCounts) } func getSnapshot(in context: Context, completion: @escaping (PulseEntry) -> Void) { completion(placeholder(in: context)) } func getTimeline(in context: Context, completion: @escaping (Timeline) -> Void) { Task { var counts: [(String, String, Int, String)] = [] for s in Self.sources { if let url = URL(string: s.url), let (data, _) = try? await URLSession.shared.data(from: url), let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] { for k in s.keys { if let n = obj[k] as? Int { counts.append((s.name, s.unit, n, s.hex)); break } if let n = obj[k] as? Double { counts.append((s.name, s.unit, Int(n), s.hex)); break } } } } let entry = PulseEntry(date: .now, counts: counts.isEmpty ? Self.placeholderCounts : counts) completion(Timeline(entries: [entry], policy: .after(.now.addingTimeInterval(1800)))) } } } struct PulseWidgetView: View { var entry: PulseEntry @Environment(\.widgetFamily) private var family private func hexColor(_ h: String) -> Color { let s = h.dropFirst(); let v = UInt64(s, radix: 16) ?? 0 return Color(.sRGB, red: Double((v >> 16) & 0xFF) / 255, green: Double((v >> 8) & 0xFF) / 255, blue: Double(v & 0xFF) / 255) } var body: some View { VStack(alignment: .leading, spacing: family == .systemSmall ? 4 : 8) { HStack(spacing: 4) { Text("Groupe").font(.system(.caption, design: .rounded).weight(.bold)) Text("KA").font(.system(.caption2, design: .rounded).weight(.bold)) .foregroundStyle(Color(.sRGB, red: 0.85, green: 0.95, blue: 0.42)) .padding(.horizontal, 5).padding(.vertical, 1) .background(.black, in: RoundedRectangle(cornerRadius: 5)) Spacer() Text("en direct").font(.system(size: 8, design: .monospaced)).foregroundStyle(.secondary) } let rows = Array(entry.counts.prefix(family == .systemSmall ? 1 : family == .systemMedium ? 3 : 6)) ForEach(rows, id: \.name) { c in VStack(alignment: .leading, spacing: 0) { Text(c.value.formatted(.number.locale(Locale(identifier: "fr_CA")))) .font(.system(family == .systemSmall ? .title2 : .headline, design: .rounded).weight(.bold)) .foregroundStyle(hexColor(c.hex)) .minimumScaleFactor(0.6).lineLimit(1) Text("\(c.name) · \(c.unit)") .font(.system(size: 9)).foregroundStyle(.secondary).lineLimit(1) } } Spacer(minLength: 0) } .containerBackground(for: .widget) { Color(.systemBackground) } } } @main struct KAWidgets: WidgetBundle { var body: some Widget { PulseWidget() } } struct PulseWidget: Widget { var body: some WidgetConfiguration { StaticConfiguration(kind: "KAPulse", provider: PulseProvider()) { entry in PulseWidgetView(entry: entry) } .configurationDisplayName("Le pouls de l'écosystème") .description("Les compteurs en direct du Groupe KA : logements, propriétés, emplois, sorties…") .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) } }