229 lines
9.0 KiB
Swift
229 lines
9.0 KiB
Swift
|
|
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)
|
||
|
|
}
|