SPB Git forge

spb/ka-macos

Public
3commits 1branches 0releases
6.1 MBsize
maindefault branch
1 mo agolast push
Swift 98.3% Shell 1.7%
10.0 KB · 295 lines swift
Raw Blame History
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    /// Le site du hub groupe-ka.com, intégré (WKWebView).16    case hub17}1819@MainActor20final class AppState: ObservableObject {21    static let shared = AppState()22    @Published var selection: SidebarSelection? = .home23    /// Incrémenté pour redonner le focus au champ de recherche universel.24    @Published var searchFocusToken = 025    /// Pré-remplissage d'un explorateur d'univers (depuis Accueil/Alertes).26    @Published var prefill: (universeID: String, query: String, params: [String: String])?27    /// Ouvre (ou ramène) la fenêtre principale — capturé depuis la vue racine.28    var openMain: (() -> Void)?2930    func openUniversalSearch() {31        selection = .search32        searchFocusToken += 133        NSApp.activate(ignoringOtherApps: true)34        openMain?()35    }3637    func openUniverse(_ id: String, query: String = "", params: [String: String] = [:]) {38        if !query.isEmpty || !params.isEmpty {39            prefill = (id, query, params)40        }41        selection = .universe(id)42    }43}4445// MARK: - Apparence (clair par défaut, sombre complet, ou système)4647enum KAAppearance: String, CaseIterable, Identifiable {48    case clair, sombre, systeme49    var id: String { rawValue }50    var label: String {51        switch self {52        case .clair: return "Clair"53        case .sombre: return "Sombre"54        case .systeme: return "Système"55        }56    }57    var colorScheme: ColorScheme? {58        switch self {59        case .clair: return .light60        case .sombre: return .dark61        case .systeme: return nil62        }63    }64}6566// MARK: - Le pouls de l'écosystème (compteurs + statut, rafraîchi 5 min)6768@MainActor69final class EcosystemPulse: ObservableObject {70    static let shared = EcosystemPulse()71    @Published var totals: [String: Int] = [:]72    @Published var health: [ServiceHealth] = StatusService.allServices73    @Published var lastRefresh: Date?74    @Published var refreshing = false75    private var loop: Task<Void, Never>?7677    func start() {78        guard loop == nil else { return }79        loop = Task { [weak self] in80            while !Task.isCancelled {81                await self?.refresh()82                try? await Task.sleep(for: .seconds(300)) // 5 minutes83            }84        }85    }8687    func refresh() async {88        refreshing = true89        // Compteurs des univers (fan-out parallèle)90        await withTaskGroup(of: (String, Int?).self) { group in91            for u in Ecosystem.all {92                group.addTask { (u.id, await UniverseService.liveTotal(u)) }93            }94            for await (id, n) in group {95                if let n { totals[id] = n }96            }97        }98        // Statut des 13 services (HEAD + latence)99        var checked = health100        await withTaskGroup(of: (String, Bool, Int).self) { group in101            for s in checked {102                group.addTask {103                    let r = await StatusService.check(domain: s.id)104                    return (s.id, r.up, r.latencyMs)105                }106            }107            for await (id, up, ms) in group {108                if let i = checked.firstIndex(where: { $0.id == id }) {109                    checked[i].up = up110                    checked[i].latencyMs = ms111                    checked[i].checkedAt = Date()112                }113            }114        }115        health = checked116        lastRefresh = Date()117        refreshing = false118    }119120    var servicesUp: Int { health.filter { $0.up == true }.count }121}122123// MARK: - Délégué (raccourci global + démarrage du pouls + alertes)124125final class AppDelegate: NSObject, NSApplicationDelegate {126    private let hotKey = HotKeyManager()127128    func applicationDidFinishLaunching(_ notification: Notification) {129        hotKey.onHotKey = { Task { @MainActor in AppState.shared.openUniversalSearch() } }130        hotKey.register()131        Task { @MainActor in132            EcosystemPulse.shared.start()133            await RecentsStore.shared.refreshAlerts() // deltas réels des recherches sauvegardées134        }135    }136}137138// MARK: - App139140@main141struct KAApp: App {142    @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate143144    init() {145        // Polices officielles des sites (Space Grotesk, JetBrains Mono)146        KAFont.register()147    }148    @StateObject private var pulse = EcosystemPulse.shared149    @StateObject private var state = AppState.shared150    @StateObject private var favorites = FavoritesStore.shared151    @StateObject private var recents = RecentsStore.shared152    @AppStorage("ka.appearance") private var appearance = KAAppearance.clair.rawValue153154    private var scheme: ColorScheme? { KAAppearance(rawValue: appearance)?.colorScheme }155156    var body: some Scene {157        WindowGroup("KA", id: "main") {158            MainWindowView()159                .environmentObject(pulse)160                .environmentObject(state)161                .environmentObject(favorites)162                .environmentObject(recents)163                .preferredColorScheme(scheme)164        }165        .defaultSize(width: 1280, height: 820)166        .commands {167            CommandGroup(replacing: .appInfo) { AboutCommand() }168            CommandGroup(after: .sidebar) {169                Divider()170                Picker("Apparence", selection: $appearance) {171                    ForEach(KAAppearance.allCases) { a in172                        Text(a.label).tag(a.rawValue)173                    }174                }175                Divider()176                NavCommand(label: "Accueil", to: .home, key: "0")177                NavCommand(label: "Recherche universelle", to: .search, key: "f")178                NavCommand(label: "Carte", to: .map, key: "m")179                NavCommand(label: "KA Agent", to: .agent, key: "g")180                NavCommand(label: "Favoris", to: .favorites, key: "d")181                NavCommand(label: "Statut des plateformes", to: .status, key: "t")182            }183            CommandGroup(after: .sidebar) { SearchCommands() }184            CommandMenu("Univers") {185                ForEach(Array(Ecosystem.all.enumerated()), id: \.element.id) { i, u in186                    UniverseCommand(universe: u, index: i)187                }188                Divider()189                HubCommand()190            }191        }192193        Window("À propos de KA", id: "about") {194            AboutView()195                .preferredColorScheme(scheme)196        }197        .windowResizability(.contentSize)198        .defaultPosition(.center)199200        MenuBarExtra {201            MenuBarView()202                .environmentObject(pulse)203                .environmentObject(state)204                .environmentObject(favorites)205                .environmentObject(recents)206                .preferredColorScheme(scheme)207        } label: {208            Text("Ka").font(.system(size: 13, weight: .heavy, design: .rounded))209        }210        .menuBarExtraStyle(.window)211    }212}213214/// Ramène la fenêtre principale existante au premier plan — n'en crée une215/// nouvelle QUE s'il n'y en a aucune (évite l'empilement de fenêtres).216@MainActor217func focusMainWindow(_ openWindow: OpenWindowAction) {218    NSApp.activate(ignoringOtherApps: true)219    if let w = NSApp.windows.first(where: { ($0.identifier?.rawValue.hasPrefix("main") ?? false) && $0.canBecomeKey }) {220        w.makeKeyAndOrderFront(nil)221    } else {222        openWindow(id: "main")223    }224}225226/// « À propos de KA » (remplace l'élément standard du menu application).227private struct AboutCommand: View {228    @Environment(\.openWindow) private var openWindow229    var body: some View {230        Button("À propos de KA") { openWindow(id: "about") }231    }232}233234/// Navigation menu « Présentation » (⌘0 accueil, ⌘F recherche, ⌘M carte…).235private struct NavCommand: View {236    let label: String237    let to: SidebarSelection238    let key: KeyEquivalent239    @Environment(\.openWindow) private var openWindow240    var body: some View {241        Button(label) {242            focusMainWindow(openWindow)243            if to == .search { AppState.shared.openUniversalSearch() }244            else { AppState.shared.selection = to }245        }246        .keyboardShortcut(key, modifiers: .command)247    }248}249250/// Recherche universelle — ⌘K et ⌘⇧K (aussi raccourci GLOBAL Carbon).251private struct SearchCommands: View {252    @Environment(\.openWindow) private var openWindow253    var body: some View {254        Button("Recherche rapide") {255            focusMainWindow(openWindow)256            AppState.shared.openUniversalSearch()257        }258        .keyboardShortcut("k", modifiers: .command)259        Button("Recherche universelle (globale)") {260            focusMainWindow(openWindow)261            AppState.shared.openUniversalSearch()262        }263        .keyboardShortcut("k", modifiers: [.command, .shift])264    }265}266267/// Le hub groupe-ka.com, intégré dans l'app (menu Univers).268private struct HubCommand: View {269    @Environment(\.openWindow) private var openWindow270    var body: some View {271        Button("groupe-ka.com — le hub") {272            focusMainWindow(openWindow)273            AppState.shared.selection = .hub274        }275    }276}277278/// Menu « Univers » : ⌘1..⌘9 pour les 9 premiers.279private struct UniverseCommand: View {280    let universe: Universe281    let index: Int282    @Environment(\.openWindow) private var openWindow283    var body: some View {284        let btn = Button(universe.name) {285            focusMainWindow(openWindow)286            AppState.shared.selection = .universe(universe.id)287        }288        if index < 9 {289            btn.keyboardShortcut(KeyEquivalent(Character("\(index + 1)")), modifiers: .command)290        } else {291            btn292        }293    }294}295