SPB Git

spb/ka-macos

Public
9.4 KB · 275 lines swift
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// KAApp.swift — point d'entrée : fenêtre principale (Accueil, Recherche,3// Carte, 12 univers, Favoris, Historique, Alertes, KA Agent, Statut),4// MenuBarExtra « Ka », menus natifs (⌘1..9 univers, ⌘F/⌘K recherche, ⌘M5// carte), raccourci global ⌘⇧K, apparence clair/sombre/système, pouls 5 min.6// Spec des données : app iOS KA (~/Desktop/KA).7import SwiftUI89// MARK: - Navigation1011enum SidebarSelection: Hashable {12    case home, search, map, agent, status13    case favorites, history, alerts14    case universe(String)15}1617@MainActor18final class AppState: ObservableObject {19    static let shared = AppState()20    @Published var selection: SidebarSelection? = .home21    /// Incrémenté pour redonner le focus au champ de recherche universel.22    @Published var searchFocusToken = 023    /// Pré-remplissage d'un explorateur d'univers (depuis Accueil/Alertes).24    @Published var prefill: (universeID: String, query: String, params: [String: String])?25    /// Ouvre (ou ramène) la fenêtre principale — capturé depuis la vue racine.26    var openMain: (() -> Void)?2728    func openUniversalSearch() {29        selection = .search30        searchFocusToken += 131        NSApp.activate(ignoringOtherApps: true)32        openMain?()33    }3435    func openUniverse(_ id: String, query: String = "", params: [String: String] = [:]) {36        if !query.isEmpty || !params.isEmpty {37            prefill = (id, query, params)38        }39        selection = .universe(id)40    }41}4243// MARK: - Apparence (clair par défaut, sombre complet, ou système)4445enum KAAppearance: String, CaseIterable, Identifiable {46    case clair, sombre, systeme47    var id: String { rawValue }48    var label: String {49        switch self {50        case .clair: return "Clair"51        case .sombre: return "Sombre"52        case .systeme: return "Système"53        }54    }55    var colorScheme: ColorScheme? {56        switch self {57        case .clair: return .light58        case .sombre: return .dark59        case .systeme: return nil60        }61    }62}6364// MARK: - Le pouls de l'écosystème (compteurs + statut, rafraîchi 5 min)6566@MainActor67final class EcosystemPulse: ObservableObject {68    static let shared = EcosystemPulse()69    @Published var totals: [String: Int] = [:]70    @Published var health: [ServiceHealth] = StatusService.allServices71    @Published var lastRefresh: Date?72    @Published var refreshing = false73    private var loop: Task<Void, Never>?7475    func start() {76        guard loop == nil else { return }77        loop = Task { [weak self] in78            while !Task.isCancelled {79                await self?.refresh()80                try? await Task.sleep(for: .seconds(300)) // 5 minutes81            }82        }83    }8485    func refresh() async {86        refreshing = true87        // Compteurs des univers (fan-out parallèle)88        await withTaskGroup(of: (String, Int?).self) { group in89            for u in Ecosystem.all {90                group.addTask { (u.id, await UniverseService.liveTotal(u)) }91            }92            for await (id, n) in group {93                if let n { totals[id] = n }94            }95        }96        // Statut des 13 services (HEAD + latence)97        var checked = health98        await withTaskGroup(of: (String, Bool, Int).self) { group in99            for s in checked {100                group.addTask {101                    let r = await StatusService.check(domain: s.id)102                    return (s.id, r.up, r.latencyMs)103                }104            }105            for await (id, up, ms) in group {106                if let i = checked.firstIndex(where: { $0.id == id }) {107                    checked[i].up = up108                    checked[i].latencyMs = ms109                    checked[i].checkedAt = Date()110                }111            }112        }113        health = checked114        lastRefresh = Date()115        refreshing = false116    }117118    var servicesUp: Int { health.filter { $0.up == true }.count }119}120121// MARK: - Délégué (raccourci global + démarrage du pouls + alertes)122123final class AppDelegate: NSObject, NSApplicationDelegate {124    private let hotKey = HotKeyManager()125126    func applicationDidFinishLaunching(_ notification: Notification) {127        hotKey.onHotKey = { Task { @MainActor in AppState.shared.openUniversalSearch() } }128        hotKey.register()129        Task { @MainActor in130            EcosystemPulse.shared.start()131            await RecentsStore.shared.refreshAlerts() // deltas réels des recherches sauvegardées132        }133    }134}135136// MARK: - App137138@main139struct KAApp: App {140    @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate141    @StateObject private var pulse = EcosystemPulse.shared142    @StateObject private var state = AppState.shared143    @StateObject private var favorites = FavoritesStore.shared144    @StateObject private var recents = RecentsStore.shared145    @AppStorage("ka.appearance") private var appearance = KAAppearance.clair.rawValue146147    private var scheme: ColorScheme? { KAAppearance(rawValue: appearance)?.colorScheme }148149    var body: some Scene {150        WindowGroup("KA", id: "main") {151            MainWindowView()152                .environmentObject(pulse)153                .environmentObject(state)154                .environmentObject(favorites)155                .environmentObject(recents)156                .preferredColorScheme(scheme)157        }158        .defaultSize(width: 1280, height: 820)159        .commands {160            CommandGroup(replacing: .appInfo) { AboutCommand() }161            CommandGroup(after: .sidebar) {162                Divider()163                Picker("Apparence", selection: $appearance) {164                    ForEach(KAAppearance.allCases) { a in165                        Text(a.label).tag(a.rawValue)166                    }167                }168                Divider()169                NavCommand(label: "Accueil", to: .home, key: "0")170                NavCommand(label: "Recherche universelle", to: .search, key: "f")171                NavCommand(label: "Carte", to: .map, key: "m")172                NavCommand(label: "KA Agent", to: .agent, key: "g")173                NavCommand(label: "Favoris", to: .favorites, key: "d")174                NavCommand(label: "Statut des plateformes", to: .status, key: "t")175            }176            CommandGroup(after: .sidebar) { SearchCommands() }177            CommandMenu("Univers") {178                ForEach(Array(Ecosystem.all.enumerated()), id: \.element.id) { i, u in179                    UniverseCommand(universe: u, index: i)180                }181            }182        }183184        Window("À propos de KA", id: "about") {185            AboutView()186                .preferredColorScheme(scheme)187        }188        .windowResizability(.contentSize)189        .defaultPosition(.center)190191        MenuBarExtra {192            MenuBarView()193                .environmentObject(pulse)194                .environmentObject(state)195                .environmentObject(favorites)196                .environmentObject(recents)197                .preferredColorScheme(scheme)198        } label: {199            Text("Ka").font(.system(size: 13, weight: .heavy, design: .rounded))200        }201        .menuBarExtraStyle(.window)202    }203}204205/// Ramène la fenêtre principale existante au premier plan — n'en crée une206/// nouvelle QUE s'il n'y en a aucune (évite l'empilement de fenêtres).207@MainActor208func focusMainWindow(_ openWindow: OpenWindowAction) {209    NSApp.activate(ignoringOtherApps: true)210    if let w = NSApp.windows.first(where: { ($0.identifier?.rawValue.hasPrefix("main") ?? false) && $0.canBecomeKey }) {211        w.makeKeyAndOrderFront(nil)212    } else {213        openWindow(id: "main")214    }215}216217/// « À propos de KA » (remplace l'élément standard du menu application).218private struct AboutCommand: View {219    @Environment(\.openWindow) private var openWindow220    var body: some View {221        Button("À propos de KA") { openWindow(id: "about") }222    }223}224225/// Navigation menu « Présentation » (⌘0 accueil, ⌘F recherche, ⌘M carte…).226private struct NavCommand: View {227    let label: String228    let to: SidebarSelection229    let key: KeyEquivalent230    @Environment(\.openWindow) private var openWindow231    var body: some View {232        Button(label) {233            focusMainWindow(openWindow)234            if to == .search { AppState.shared.openUniversalSearch() }235            else { AppState.shared.selection = to }236        }237        .keyboardShortcut(key, modifiers: .command)238    }239}240241/// Recherche universelle — ⌘K et ⌘⇧K (aussi raccourci GLOBAL Carbon).242private struct SearchCommands: View {243    @Environment(\.openWindow) private var openWindow244    var body: some View {245        Button("Recherche rapide") {246            focusMainWindow(openWindow)247            AppState.shared.openUniversalSearch()248        }249        .keyboardShortcut("k", modifiers: .command)250        Button("Recherche universelle (globale)") {251            focusMainWindow(openWindow)252            AppState.shared.openUniversalSearch()253        }254        .keyboardShortcut("k", modifiers: [.command, .shift])255    }256}257258/// Menu « Univers » : ⌘1..⌘9 pour les 9 premiers.259private struct UniverseCommand: View {260    let universe: Universe261    let index: Int262    @Environment(\.openWindow) private var openWindow263    var body: some View {264        let btn = Button(universe.name) {265            focusMainWindow(openWindow)266            AppState.shared.selection = .universe(universe.id)267        }268        if index < 9 {269            btn.keyboardShortcut(KeyEquivalent(Character("\(index + 1)")), modifiers: .command)270        } else {271            btn272        }273    }274}275
Files · e2e01af1478899e68831e7da506cabe4fc68c49a
Menu

Theme