montana/macOS/MontanaPresence/SitesView.swift

229 lines
9.0 KiB
Swift
Raw Permalink Normal View History

import SwiftUI
import WebKit
/// Montana Sites Embedded Browser for efir.org
struct SitesView: View {
@EnvironmentObject var engine: PresenceEngine
@State private var showSidebar = false
@State private var urlString = "https://efir.org"
@State private var currentTitle = "efir.org"
@State private var isLoading = true
@State private var canGoBack = false
@State private var canGoForward = false
private let gold = Color(red: 0.85, green: 0.68, blue: 0.25)
var body: some View {
ZStack(alignment: .leading) {
VStack(spacing: 0) {
// BURGER MENU BUTTON
HStack {
Button(action: { withAnimation(.easeInOut(duration: 0.3)) { showSidebar = true } }) {
Image(systemName: "line.3.horizontal")
.font(.system(size: 18, weight: .medium))
.foregroundColor(gold)
.padding(8)
}
.buttonStyle(.plain)
Spacer()
}
.padding(.horizontal, 12)
.padding(.top, 8)
// Navigation bar
HStack(spacing: 8) {
Button(action: { NotificationCenter.default.post(name: .webViewGoBack, object: nil) }) {
Image(systemName: "chevron.left")
.font(.system(size: 14, weight: .medium))
.foregroundColor(canGoBack ? gold : .secondary.opacity(0.4))
}
.buttonStyle(.plain)
.disabled(!canGoBack)
Button(action: { NotificationCenter.default.post(name: .webViewGoForward, object: nil) }) {
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .medium))
.foregroundColor(canGoForward ? gold : .secondary.opacity(0.4))
}
.buttonStyle(.plain)
.disabled(!canGoForward)
Button(action: { NotificationCenter.default.post(name: .webViewReload, object: nil) }) {
Image(systemName: isLoading ? "xmark" : "arrow.clockwise")
.font(.system(size: 12, weight: .medium))
.foregroundColor(gold)
}
.buttonStyle(.plain)
// URL bar
HStack(spacing: 6) {
Image(systemName: "lock.fill")
.font(.system(size: 10))
.foregroundColor(.green)
Text(currentTitle)
.font(.system(size: 12))
.foregroundColor(.primary)
.lineLimit(1)
.truncationMode(.tail)
}
.padding(.horizontal, 10)
.padding(.vertical, 6)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(NSColor.controlBackgroundColor))
.cornerRadius(8)
Button(action: {
urlString = "https://efir.org"
NotificationCenter.default.post(name: .webViewLoadURL, object: urlString)
}) {
Image(systemName: "house.fill")
.font(.system(size: 14))
.foregroundColor(gold)
}
.buttonStyle(.plain)
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
Divider()
// WebView
MontanaWebView(
urlString: urlString,
currentTitle: $currentTitle,
isLoading: $isLoading,
canGoBack: $canGoBack,
canGoForward: $canGoForward
)
}
.background(Color(NSColor.windowBackgroundColor))
// Sidebar overlay
if showSidebar {
Color.black.opacity(0.3)
.ignoresSafeArea()
.onTapGesture {
withAnimation(.easeInOut(duration: 0.3)) {
showSidebar = false
}
}
SharedSidebar(isVisible: $showSidebar)
.transition(.move(edge: .leading))
}
}
}
}
// MARK: - WebView Notifications
extension Notification.Name {
static let webViewGoBack = Notification.Name("webViewGoBack")
static let webViewGoForward = Notification.Name("webViewGoForward")
static let webViewReload = Notification.Name("webViewReload")
static let webViewLoadURL = Notification.Name("webViewLoadURL")
}
// MARK: - WKWebView Wrapper
struct MontanaWebView: NSViewRepresentable {
let urlString: String
@Binding var currentTitle: String
@Binding var isLoading: Bool
@Binding var canGoBack: Bool
@Binding var canGoForward: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> WKWebView {
let config = WKWebViewConfiguration()
config.defaultWebpagePreferences.allowsContentJavaScript = true
let webView = WKWebView(frame: .zero, configuration: config)
webView.navigationDelegate = context.coordinator
context.coordinator.webView = webView
// Load initial URL
if let url = URL(string: urlString) {
webView.load(URLRequest(url: url))
}
// Subscribe to navigation notifications
let nc = NotificationCenter.default
nc.addObserver(context.coordinator, selector: #selector(Coordinator.goBack), name: .webViewGoBack, object: nil)
nc.addObserver(context.coordinator, selector: #selector(Coordinator.goForward), name: .webViewGoForward, object: nil)
nc.addObserver(context.coordinator, selector: #selector(Coordinator.reload), name: .webViewReload, object: nil)
nc.addObserver(context.coordinator, selector: #selector(Coordinator.loadURL(_:)), name: .webViewLoadURL, object: nil)
// Observe navigation state
webView.addObserver(context.coordinator, forKeyPath: #keyPath(WKWebView.canGoBack), options: .new, context: nil)
webView.addObserver(context.coordinator, forKeyPath: #keyPath(WKWebView.canGoForward), options: .new, context: nil)
webView.addObserver(context.coordinator, forKeyPath: #keyPath(WKWebView.isLoading), options: .new, context: nil)
webView.addObserver(context.coordinator, forKeyPath: #keyPath(WKWebView.title), options: .new, context: nil)
return webView
}
func updateNSView(_ nsView: WKWebView, context: Context) {}
class Coordinator: NSObject, WKNavigationDelegate {
var parent: MontanaWebView
weak var webView: WKWebView?
init(_ parent: MontanaWebView) {
self.parent = parent
}
deinit {
webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoBack))
webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.canGoForward))
webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.isLoading))
webView?.removeObserver(self, forKeyPath: #keyPath(WKWebView.title))
NotificationCenter.default.removeObserver(self)
}
@objc func goBack() { webView?.goBack() }
@objc func goForward() { webView?.goForward() }
@objc func reload() {
if webView?.isLoading == true {
webView?.stopLoading()
} else {
webView?.reload()
}
}
@objc func loadURL(_ notification: Notification) {
if let urlStr = notification.object as? String, let url = URL(string: urlStr) {
webView?.load(URLRequest(url: url))
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard let webView = webView else { return }
DispatchQueue.main.async {
self.parent.canGoBack = webView.canGoBack
self.parent.canGoForward = webView.canGoForward
self.parent.isLoading = webView.isLoading
self.parent.currentTitle = webView.title ?? webView.url?.host ?? "efir.org"
}
}
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
DispatchQueue.main.async {
self.parent.isLoading = false
self.parent.currentTitle = webView.title ?? webView.url?.host ?? "efir.org"
}
}
}
}
// MARK: - Preview
#Preview {
SitesView()
.environmentObject(PresenceEngine.shared)
.frame(width: 600, height: 500)
}