diff --git a/App/Mere.entitlements b/App/Mere.entitlements index daf76fa..0c67376 100644 --- a/App/Mere.entitlements +++ b/App/Mere.entitlements @@ -1,7 +1,5 @@ - - - + diff --git a/Mere.xcodeproj/project.pbxproj b/Mere.xcodeproj/project.pbxproj index a35380e..ea815cc 100644 --- a/Mere.xcodeproj/project.pbxproj +++ b/Mere.xcodeproj/project.pbxproj @@ -275,12 +275,19 @@ B3A100B001000000 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = App/Mere.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; - ENABLE_HARDENED_RUNTIME = YES; + ENABLE_HARDENED_RUNTIME = NO; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Mere; @@ -293,6 +300,12 @@ MARKETING_VERSION = 0.1.0; PRODUCT_BUNDLE_IDENTIFIER = sh.dunkirk.mere; PRODUCT_NAME = Mere; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; + RUNTIME_EXCEPTION_ALLOW_JIT = NO; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 6.0; }; @@ -301,12 +314,19 @@ B3A100B101000000 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + AUTOMATION_APPLE_EVENTS = NO; CODE_SIGN_ENTITLEMENTS = App/Mere.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 1; DEAD_CODE_STRIPPING = YES; - ENABLE_HARDENED_RUNTIME = YES; + ENABLE_HARDENED_RUNTIME = NO; + ENABLE_RESOURCE_ACCESS_AUDIO_INPUT = NO; + ENABLE_RESOURCE_ACCESS_CALENDARS = NO; + ENABLE_RESOURCE_ACCESS_CAMERA = NO; + ENABLE_RESOURCE_ACCESS_CONTACTS = NO; + ENABLE_RESOURCE_ACCESS_LOCATION = NO; + ENABLE_RESOURCE_ACCESS_PHOTO_LIBRARY = NO; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = App/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Mere; @@ -319,6 +339,12 @@ MARKETING_VERSION = 0.1.0; PRODUCT_BUNDLE_IDENTIFIER = sh.dunkirk.mere; PRODUCT_NAME = Mere; + RUNTIME_EXCEPTION_ALLOW_DYLD_ENVIRONMENT_VARIABLES = NO; + RUNTIME_EXCEPTION_ALLOW_JIT = NO; + RUNTIME_EXCEPTION_ALLOW_UNSIGNED_EXECUTABLE_MEMORY = NO; + RUNTIME_EXCEPTION_DEBUGGING_TOOL = NO; + RUNTIME_EXCEPTION_DISABLE_EXECUTABLE_PAGE_PROTECTION = NO; + RUNTIME_EXCEPTION_DISABLE_LIBRARY_VALIDATION = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 6.0; }; diff --git a/Sources/MereCore/Tab.swift b/Sources/MereCore/Tab.swift index 75cfc9e..590e283 100644 --- a/Sources/MereCore/Tab.swift +++ b/Sources/MereCore/Tab.swift @@ -42,7 +42,10 @@ public final class Tab: ObservableObject, Identifiable { // MARK: - Navigation passthrough - public func loadURL(_ url: URL) { content.loadURL(url) } + public func loadURL(_ url: URL) { + print("🔍 Tab.loadURL: \(url.absoluteString)") + content.loadURL(url) + } public func goBack() { content.goBack() } public func goForward() { content.goForward() } public func reload() { content.reload() } diff --git a/Sources/MereUI/BrowserWindowView.swift b/Sources/MereUI/BrowserWindowView.swift index 9df0067..71bef64 100644 --- a/Sources/MereUI/BrowserWindowView.swift +++ b/Sources/MereUI/BrowserWindowView.swift @@ -131,18 +131,16 @@ public struct BrowserWindowView: View { @ViewBuilder private var contentArea: some View { - if let tab = window.activeTab, tab.url != nil || tab.isLoading { - ZStack { + if let tab = window.activeTab { + if let error = tab.navigationError { + NavigationErrorView(error: error) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else if tab.url != nil || tab.isLoading { WebContentView(content: tab.content) .id(tab.id) - - if let error = tab.navigationError { - NavigationErrorView(error: error) - .padding() - } + } else { + NewTabView(hasBackground: window.newTabBackgroundColor != nil) } - } else { - NewTabView(hasBackground: window.newTabBackgroundColor != nil) } } } @@ -401,7 +399,10 @@ struct BrowserToolbarView: View { .padding(.horizontal, 10) .padding(.vertical, 3) .onChange(of: window.activeTab?.url) { _, url in - addressText = url?.absoluteString ?? "" + // Show normalized URL in address bar (0.0.0.0 -> 127.0.0.1) + var urlString = url?.absoluteString ?? "" + urlString = urlString.replacingOccurrences(of: "0.0.0.0", with: "127.0.0.1", options: .caseInsensitive) + addressText = urlString } .background( RoundedRectangle(cornerRadius: 7) @@ -514,8 +515,14 @@ struct BrowserToolbarView: View { return false } + func normalizeURL(_ input: String) -> String { + // Replace 0.0.0.0 with 127.0.0.1 (0.0.0.0 is blocked by WebKit) + return input.replacingOccurrences(of: "0.0.0.0", with: "127.0.0.1", options: .caseInsensitive) + } + if isURL(raw) { - let urlString = raw.hasPrefix("http") ? raw : "http://\(raw)" + let normalized = normalizeURL(raw) + let urlString = normalized.hasPrefix("http") ? normalized : "http://\(normalized)" if let u = URL(string: urlString) { url = u } else { diff --git a/Sources/WebKitEngine/WebKitWebContent.swift b/Sources/WebKitEngine/WebKitWebContent.swift index b49e826..5e4f060 100644 --- a/Sources/WebKitEngine/WebKitWebContent.swift +++ b/Sources/WebKitEngine/WebKitWebContent.swift @@ -40,6 +40,8 @@ public final class WebKitWebContent: NSObject, WebContent { let webView: WKWebView private var observations: [NSKeyValueObservation] = [] + private var pendingReloadURL: URL? + private var reloadAttempt = 0 // MARK: - Init @@ -70,6 +72,37 @@ public final class WebKitWebContent: NSObject, WebContent { forMainFrameOnly: false )) + // Console logging for debugging + configuration.userContentController.addUserScript(WKUserScript( + source: """ + (function() { + const originalLog = console.log; + const originalError = console.error; + const originalWarn = console.warn; + + console.log = function() { + originalLog.apply(console, arguments); + const args = Array.from(arguments).map(String); + window.webkit.messageHandlers.mereConsole.postMessage('LOG: ' + args.join(' ')); + }; + + console.error = function() { + originalError.apply(console, arguments); + const args = Array.from(arguments).map(String); + window.webkit.messageHandlers.mereConsole.postMessage('ERROR: ' + args.join(' ')); + }; + + console.warn = function() { + originalWarn.apply(console, arguments); + const args = Array.from(arguments).map(String); + window.webkit.messageHandlers.mereConsole.postMessage('WARN: ' + args.join(' ')); + }; + })(); + """, + injectionTime: .atDocumentStart, + forMainFrameOnly: false + )) + // Theme colour detection. Priority order: // 1. meta[name="theme-color"] — explicit, always wins // 2. elementFromPoint walk — finds the actual rendered background @@ -125,6 +158,8 @@ public final class WebKitWebContent: NSObject, WebContent { headObs.observe(document.head, { childList: true }); } })(); + console.log('📄 Page loaded, document.styleSheets.length:', document.styleSheets.length); + console.log('📄 StyleSheets:', Array.from(document.styleSheets).map(s => s.href)); """, injectionTime: .atDocumentEnd, forMainFrameOnly: true @@ -133,12 +168,18 @@ public final class WebKitWebContent: NSObject, WebContent { self.webView = WKWebView(frame: .zero, configuration: configuration) self.webView.allowsBackForwardNavigationGestures = true + #if DEBUG + // Enable Web Inspector for Safari + self.webView.isInspectable = true + #endif + super.init() // Use a weak wrapper to avoid retain cycles through userContentController. let weak = WeakScriptMessageHandler(self) configuration.userContentController.add(weak, name: "mereAudio") configuration.userContentController.add(weak, name: "mereTheme") + configuration.userContentController.add(weak, name: "mereConsole") webView.navigationDelegate = self webView.uiDelegate = self @@ -148,6 +189,7 @@ public final class WebKitWebContent: NSObject, WebContent { // MARK: - WebContent public func loadURL(_ url: URL) { + print("🌐 WebKitWebContent.loadURL: \(url.absoluteString)") webView.load(URLRequest(url: url)) } @@ -273,6 +315,8 @@ extension WebKitWebContent: WKNavigationDelegate { public func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) { if let url = webView.url { eventContinuation.yield(.committed(url: url)) } + reloadAttempt = 0 + pendingReloadURL = nil } public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { @@ -306,6 +350,27 @@ extension WebKitWebContent: WKNavigationDelegate { eventContinuation.yield(.faviconChanged(url: faviconURL)) } + public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + let nsError = error as NSError + let retryableCodes = [-1001, -1002, -1004, -1005] + + print("❌ Provisional load failed: domain=\(nsError.domain) code=\(nsError.code) url=\(webView.url?.absoluteString ?? "nil")") + + // Retry once for transient network errors + if retryableCodes.contains(nsError.code), reloadAttempt == 0, let url = webView.url { + reloadAttempt = 1 + pendingReloadURL = url + print("🔄 Attempting retry for url: \(url.absoluteString)") + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.webView.reload() + } + } else { + reloadAttempt = 0 + pendingReloadURL = nil + eventContinuation.yield(.failed(url: webView.url, error: error)) + } + } + public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { eventContinuation.yield(.failed(url: webView.url, error: error)) } @@ -344,6 +409,10 @@ extension WebKitWebContent: WKScriptMessageHandler { if let css = message.body as? String { eventContinuation.yield(.themeColorChanged(cssColor: css)) } + case "mereConsole": + if let log = message.body as? String { + print("🖥️ JS Console: \(log)") + } default: break } }