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%
7.2 KB · 185 lines swift
Raw Blame History
1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// SiteView.swift — le SITE WEB d'une plateforme Ka, intégré dans l'app :3// WKWebView plein volet avec chrome minimal (retour/avancer, recharger,4// domaine courant, partager, ouvrir dans le navigateur) et barre de5// progression teintée à l'accent de l'univers. Les liens externes non6// web (tel:, mailto:…) partent vers le système.7import SwiftUI8import WebKit910// MARK: - Modèle observable du WKWebView (progression, historique, titre)1112@MainActor13final class SiteWebModel: NSObject, ObservableObject, WKNavigationDelegate, WKUIDelegate {14    let webView: WKWebView15    @Published var progress: Double = 016    @Published var isLoading = false17    @Published var canGoBack = false18    @Published var canGoForward = false19    @Published var currentURL: URL?2021    private var observers: [NSKeyValueObservation] = []2223    init(url: URL) {24        let config = WKWebViewConfiguration()25        config.defaultWebpagePreferences.allowsContentJavaScript = true26        webView = WKWebView(frame: .zero, configuration: config)27        webView.allowsBackForwardNavigationGestures = true28        webView.allowsMagnification = true29        super.init()30        webView.navigationDelegate = self31        webView.uiDelegate = self32        observers = [33            webView.observe(\.estimatedProgress) { [weak self] wv, _ in34                Task { @MainActor in self?.progress = wv.estimatedProgress }35            },36            webView.observe(\.isLoading) { [weak self] wv, _ in37                Task { @MainActor in self?.isLoading = wv.isLoading }38            },39            webView.observe(\.canGoBack) { [weak self] wv, _ in40                Task { @MainActor in self?.canGoBack = wv.canGoBack }41            },42            webView.observe(\.canGoForward) { [weak self] wv, _ in43                Task { @MainActor in self?.canGoForward = wv.canGoForward }44            },45            webView.observe(\.url) { [weak self] wv, _ in46                Task { @MainActor in self?.currentURL = wv.url }47            },48        ]49        currentURL = url50        webView.load(URLRequest(url: url))51    }5253    // Les schémas non web (tel:, mailto:, facetime:…) partent vers le système.54    nonisolated func webView(_ webView: WKWebView,55                             decidePolicyFor navigationAction: WKNavigationAction,56                             decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {57        guard let url = navigationAction.request.url else { return decisionHandler(.allow) }58        if let scheme = url.scheme?.lowercased(), scheme != "http", scheme != "https", scheme != "about" {59            NSWorkspace.shared.open(url)60            return decisionHandler(.cancel)61        }62        decisionHandler(.allow)63    }6465    // target="_blank" : on reste dans la même vue plutôt que d'ouvrir une fenêtre.66    nonisolated func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,67                             for navigationAction: WKNavigationAction,68                             windowFeatures: WKWindowFeatures) -> WKWebView? {69        if let url = navigationAction.request.url {70            Task { @MainActor in webView.load(URLRequest(url: url)) }71        }72        return nil73    }74}7576private struct SiteWebViewRepresentable: NSViewRepresentable {77    let model: SiteWebModel78    func makeNSView(context: Context) -> WKWebView { model.webView }79    func updateNSView(_ nsView: WKWebView, context: Context) {}80}8182// MARK: - Le navigateur intégré (chrome + progression + web)8384struct SiteView: View {85    let url: URL86    var title: String87    var accent: Color88    var logoID: String?89    @StateObject private var model: SiteWebModel90    @Environment(\.colorScheme) private var scheme9192    init(url: URL, title: String, accent: Color, logoID: String? = nil) {93        self.url = url94        self.title = title95        self.accent = accent96        self.logoID = logoID97        _model = StateObject(wrappedValue: SiteWebModel(url: url))98    }99100    init(universe: Universe) {101        self.init(url: universe.baseURL, title: universe.name,102                  accent: universe.accent, logoID: universe.id)103    }104105    var body: some View {106        VStack(spacing: 0) {107            chrome108            ZStack(alignment: .top) {109                SiteWebViewRepresentable(model: model)110                if model.isLoading {111                    GeometryReader { geo in112                        Rectangle()113                            .fill(accent)114                            .frame(width: geo.size.width * max(model.progress, 0.06), height: 2.5)115                            .animation(.easeOut(duration: 0.25), value: model.progress)116                    }117                    .frame(height: 2.5)118                    .accessibilityHidden(true)119                }120            }121        }122        .background(KATheme.paper(scheme))123    }124125    private var chrome: some View {126        HStack(spacing: 10) {127            if let logoID {128                KALogo(id: logoID, size: 20, fallbackAccent: accent)129            }130            Button { model.webView.goBack() } label: {131                Image(systemName: "chevron.left")132            }133            .buttonStyle(.plain)134            .disabled(!model.canGoBack)135            .help("Page précédente")136            Button { model.webView.goForward() } label: {137                Image(systemName: "chevron.right")138            }139            .buttonStyle(.plain)140            .disabled(!model.canGoForward)141            .help("Page suivante")142            Button {143                if model.isLoading { model.webView.stopLoading() }144                else { model.webView.reload() }145            } label: {146                Image(systemName: model.isLoading ? "xmark" : "arrow.clockwise")147            }148            .buttonStyle(.plain)149            .help(model.isLoading ? "Arrêter" : "Recharger")150151            // Domaine courant, façon champ d'adresse (lecture seule)152            HStack(spacing: 6) {153                Image(systemName: "lock.fill").font(.caption2).foregroundStyle(.secondary)154                Text(model.currentURL?.host() ?? url.host() ?? "")155                    .font(.system(.caption, design: .monospaced).weight(.semibold))156                    .lineLimit(1)157                    .truncationMode(.middle)158            }159            .padding(.horizontal, 10).padding(.vertical, 5)160            .frame(maxWidth: .infinity)161            .background(KATheme.surface(scheme), in: Capsule())162            .overlay(Capsule().strokeBorder(KATheme.ink(scheme).opacity(0.35), lineWidth: 1))163            .accessibilityLabel("Adresse : \(model.currentURL?.absoluteString ?? url.absoluteString)")164165            if let current = model.currentURL {166                ShareLink(item: current, subject: Text(title)) {167                    Image(systemName: "square.and.arrow.up")168                }169                .buttonStyle(.plain)170                .help("Partager la page")171            }172            Button {173                NSWorkspace.shared.open(model.currentURL ?? url)174            } label: {175                Image(systemName: "safari")176            }177            .buttonStyle(.plain)178            .help("Ouvrir dans le navigateur")179        }180        .font(.body)181        .padding(.horizontal, 12).padding(.vertical, 8)182        .background(KATheme.paper(scheme))183    }184}185