// // LNWebViewController.swift // Lanu // // Created by OneeChan on 2025/12/12. // import Foundation import UIKit import SnapKit import WebKit import Combine import AutoCodable class LNJumpWebViewConfig { var url: String var customTitle: String = "" var showNavigationBar = false var setSafeArea = false static func normalConfig(url: String) -> LNJumpWebViewConfig { let config = LNJumpWebViewConfig(url: url) config.showNavigationBar = true config.setSafeArea = true return config } init(url: String, title: String = "") { self.url = url customTitle = title } init(param: LNWebPageDeeplinkParams) { url = param.url showNavigationBar = param.header setSafeArea = param.safeArea } } extension UIView { func pushToWebView(_ config: LNJumpWebViewConfig) { if config.url.isDeeplink, let url = URL(string: config.url) { LNDeeplinkManager.shared.handleDeepLink(url) return } let vc = LNWebViewController(config: config) navigationController?.pushViewController(vc, animated: true) } func pushToWebView(_ param: LNWebPageDeeplinkParams) { let config = LNJumpWebViewConfig(param: param) let vc = LNWebViewController(config: config) navigationController?.pushViewController(vc, animated: true) } } class LNWebViewController: LNViewController { private let config: LNJumpWebViewConfig private var webView: WKWebView? init(config: LNJumpWebViewConfig) { self.config = config super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() setupViews() loadUrl() } } extension LNWebViewController { private func loadUrl() { guard let url = URL(string: config.url) else { return } webView?.load(URLRequest(url: url)) } } extension LNWebViewController: WKNavigationDelegate { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping @MainActor (WKNavigationActionPolicy) -> Void) { guard let url = navigationAction.request.url, let scheme = url.scheme else { decisionHandler(.allow) // 无 URL/Schema,允许默认行为 return } guard scheme.lowercased() == LNDeeplinkUrls.appScheme else { decisionHandler(.allow) return } let fullPath = if let host = url.host { "\(host)\(url.path)" } else { url.path } if fullPath == LNDeeplinkUrls.web.closeWindow.deeplinkPath { navigationController?.popViewController(animated: true) } else if fullPath == LNDeeplinkUrls.web.userCancellation.deeplinkPath { LNAccountManager.shared.clean() } else { LNDeeplinkManager.shared.handleDeepLink(url) } decisionHandler(.cancel) } } extension LNWebViewController { private func setupViews() { if !config.customTitle.isEmpty { title = config.customTitle } showNavigationBar = config.showNavigationBar let webView = buildWebView() view.addSubview(webView) webView.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() if config.showNavigationBar || !config.setSafeArea { make.top.equalToSuperview() } else if config.setSafeArea { make.top.equalToSuperview().offset(UIView.statusBarHeight) } if config.setSafeArea { make.bottom.equalTo(view.safeAreaLayoutGuide.snp.bottom) } else { make.bottom.equalToSuperview() } } } private func buildWebView() -> UIView { let webConfig = WKWebViewConfiguration() setupDefaultLocalStorage(webConfig) let webView = WKWebView(frame: .zero, configuration: webConfig) webView.navigationDelegate = self #if DEBUG if #available(iOS 16.4, *) { webView.isInspectable = true } #endif webView.scrollView.contentInsetAdjustmentBehavior = .never webView.scrollView.bounces = false setupDefaultCookie(webView) webView.publisher(for: \.title).removeDuplicates().sink { [weak self] webTitle in guard let self else { return } guard config.customTitle.isEmpty else { return } title = webTitle }.store(in: &cancellables) self.webView = webView return webView } private func setupDefaultLocalStorage(_ webConfig: WKWebViewConfiguration) { let tokenScript = WKUserScript( source: "window.localStorage.setItem('GAMI-web_auth_token', '\(LNAccountManager.shared.token)')", injectionTime: .atDocumentStart, forMainFrameOnly: true) webConfig.userContentController.addUserScript(tokenScript) let languageScript = WKUserScript( source: "window.localStorage.setItem('GAMI-web_app_locale', '\(LNAppConfig.shared.curLang.bundleName)')", injectionTime: .atDocumentStart, forMainFrameOnly: true) webConfig.userContentController.addUserScript(languageScript) var header: [String: Any] = [:] header["time"] = curTimeInMicro header["network"] = LNNetworkMonitor.curNetworkType.desc header["version"] = curBuildVersion header["api"] = 1 header["channel"] = "appStore" header["platform"] = "2" header["device"] = curDeviceModelName header["app"] = curAppBundleIdentifier header["udid"] = curDeviceId header["token"] = LNAccountManager.shared.token if let jsonData = try? JSONSerialization.data(withJSONObject: header), let jsonStr = String(data: jsonData, encoding: .utf8) { let headerScript = WKUserScript( source: "javascript:window.GAMI_BRIDGE={'requestHeader': \(jsonStr), 'safeArea': \"\(UIView.statusBarHeight)|\(view.safeBottomInset)\"}", injectionTime: .atDocumentStart, forMainFrameOnly: true) webConfig.userContentController.addUserScript(headerScript) } } private func setupDefaultCookie(_ webView: WKWebView) { // 创建 Cookie if let tokenCookie = HTTPCookie(properties: [ .domain: ".gami.vip", .path: "/", .name: "GAMI-cookie_auth_token", .value: LNAccountManager.shared.token, .secure: "TRUE", .expires: NSDate(timeIntervalSinceNow: 3600) ]) { webView.configuration.websiteDataStore.httpCookieStore.setCookie(tokenCookie) } if let languageCookie = HTTPCookie(properties: [ .domain: ".gami.vip", .path: "/", .name: "GAMI-cookie_app_locale", .value: LNAppConfig.shared.curLang.bundleName, .secure: "TRUE", .expires: NSDate(timeIntervalSinceNow: 3600) ]) { webView.configuration.websiteDataStore.httpCookieStore.setCookie(languageCookie) } if let languageCookie = HTTPCookie(properties: [ .domain: ".gami.vip", .path: "/", .name: "GAMI-cookie_safe_area", .value: "\(UIView.statusBarHeight)|\(view.safeBottomInset)", .secure: "TRUE", .expires: NSDate(timeIntervalSinceNow: 3600) ]) { webView.configuration.websiteDataStore.httpCookieStore.setCookie(languageCookie) } } }