mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-08-03 20:47:22 -04:00
254 lines
9.1 KiB
Swift
254 lines
9.1 KiB
Swift
// This file is part of Kiwix for iOS & macOS.
|
|
//
|
|
// Kiwix is free software; you can redistribute it and/or modify it
|
|
// under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation; either version 3 of the License, or
|
|
// any later version.
|
|
//
|
|
// Kiwix is distributed in the hope that it will be useful, but
|
|
// WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
// General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
|
|
|
|
import Combine
|
|
import CoreData
|
|
import SwiftUI
|
|
import WebKit
|
|
import Defaults
|
|
|
|
#if os(macOS)
|
|
struct WebView: NSViewRepresentable {
|
|
@ObservedObject var browser: BrowserViewModel
|
|
|
|
func makeNSView(context: Context) -> NSView {
|
|
let nsView = NSView()
|
|
let webView = browser.webView
|
|
// auto-layout is not working
|
|
// when the video is paused in full screen
|
|
webView.translatesAutoresizingMaskIntoConstraints = true
|
|
webView.autoresizingMask = [.width, .height]
|
|
nsView.addSubview(webView)
|
|
return nsView
|
|
}
|
|
|
|
func updateNSView(_ nsView: NSView, context: Context) {
|
|
// without this, after closing video full screen
|
|
// a newly opened webview's frame is wrongly sized
|
|
browser.webView.frame = nsView.bounds
|
|
}
|
|
|
|
func makeCoordinator() -> Coordinator {
|
|
Coordinator(view: self)
|
|
}
|
|
|
|
final class Coordinator {
|
|
private let pageZoomObserver: Defaults.Observation
|
|
|
|
init(view: WebView) {
|
|
let browser = view.browser
|
|
pageZoomObserver = Defaults.observe(.webViewPageZoom) { [weak browser] change in
|
|
browser?.webView.pageZoom = change.newValue
|
|
}
|
|
}
|
|
|
|
deinit {
|
|
pageZoomObserver.invalidate()
|
|
}
|
|
}
|
|
}
|
|
#elseif os(iOS)
|
|
struct WebView: UIViewControllerRepresentable {
|
|
@ObservedObject var browser: BrowserViewModel
|
|
|
|
func makeUIViewController(context: Context) -> WebViewController {
|
|
WebViewController(webView: browser.webView)
|
|
}
|
|
|
|
func updateUIViewController(_ controller: WebViewController, context: Context) { }
|
|
}
|
|
|
|
final class WebViewController: UIViewController {
|
|
private let webView: WKWebView
|
|
private let pageZoomObserver: Defaults.Observation
|
|
private var webViewURLObserver: NSKeyValueObservation?
|
|
private var topSafeAreaConstraint: NSLayoutConstraint?
|
|
private var layoutSubject = PassthroughSubject<Void, Never>()
|
|
private var layoutCancellable: AnyCancellable?
|
|
private var currentScrollViewOffset: CGFloat = 0.0
|
|
private var compactViewNavigationController: UINavigationController?
|
|
|
|
init(webView: WKWebView) {
|
|
self.webView = webView
|
|
pageZoomObserver = Defaults.observe(.webViewPageZoom) { change in
|
|
webView.adjustTextSize(pageZoom: change.newValue)
|
|
}
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
|
|
override func viewDidAppear(_ animated: Bool) {
|
|
super.viewDidAppear(animated)
|
|
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(webView)
|
|
webView.alpha = 0
|
|
|
|
/*
|
|
HACK: Make sure the webview content does not jump after state restoration
|
|
It appears the webview's state restoration does not properly take into account of the content inset.
|
|
To mitigate, first pin the webview's top against safe area top anchor, after all viewDidLayoutSubviews calls,
|
|
pin the webview's top against view's top anchor, so that content does not appears to move up.
|
|
*/
|
|
NSLayoutConstraint.activate([
|
|
view.leftAnchor.constraint(equalTo: webView.leftAnchor),
|
|
view.bottomAnchor.constraint(equalTo: webView.bottomAnchor),
|
|
view.rightAnchor.constraint(equalTo: webView.rightAnchor)
|
|
])
|
|
topSafeAreaConstraint = view.safeAreaLayoutGuide.topAnchor.constraint(equalTo: webView.topAnchor)
|
|
topSafeAreaConstraint?.isActive = true
|
|
layoutCancellable = layoutSubject
|
|
.debounce(for: .seconds(0.15), scheduler: RunLoop.main)
|
|
.sink { [weak self] _ in
|
|
guard let view = self?.view,
|
|
let webView = self?.webView,
|
|
view.subviews.contains(webView) else { return }
|
|
webView.alpha = 1
|
|
guard self?.topSafeAreaConstraint?.isActive == true else { return }
|
|
self?.topSafeAreaConstraint?.isActive = false
|
|
self?.view.topAnchor.constraint(equalTo: webView.topAnchor).isActive = true
|
|
}
|
|
if !Brand.disableImmersiveReading {
|
|
configureImmersiveReading()
|
|
}
|
|
}
|
|
|
|
override func viewDidLayoutSubviews() {
|
|
super.viewDidLayoutSubviews()
|
|
if #unavailable(iOS 18.0) {
|
|
webView.setValue(view.safeAreaInsets, forKey: "_obscuredInsets")
|
|
}
|
|
layoutSubject.send()
|
|
}
|
|
}
|
|
|
|
// MARK: - UIScrollViewDelegate
|
|
extension WebViewController: UIScrollViewDelegate {
|
|
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
|
configureBars(on: scrollView)
|
|
}
|
|
|
|
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
|
currentScrollViewOffset = scrollView.contentOffset.y
|
|
}
|
|
|
|
private func configureBars(on scrollView: UIScrollView) {
|
|
guard let navigationController = compactViewNavigationController else {
|
|
return
|
|
}
|
|
|
|
var isScrollingDown: Bool {
|
|
scrollView.contentOffset.y > currentScrollViewOffset
|
|
}
|
|
|
|
if scrollView.isDragging {
|
|
if isScrollingDown {
|
|
hideBars(on: navigationController)
|
|
} else {
|
|
showBars(on: navigationController)
|
|
}
|
|
}
|
|
}
|
|
|
|
func hideBars(on navigationController: UINavigationController) {
|
|
navigationController.setNavigationBarHidden(true, animated: true)
|
|
navigationController.setToolbarHidden(true, animated: true)
|
|
}
|
|
|
|
func showBars(on navigationController: UINavigationController) {
|
|
navigationController.setNavigationBarHidden(false, animated: true)
|
|
if traitCollection.horizontalSizeClass == .compact {
|
|
navigationController.setToolbarHidden(false, animated: true)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Screen orientation change
|
|
|
|
extension WebViewController {
|
|
@objc func onOrientationChange() {
|
|
guard let navigationController = compactViewNavigationController else {
|
|
return
|
|
}
|
|
|
|
switch UIDevice.current.orientation {
|
|
case .landscapeLeft:
|
|
showBars(on: navigationController)
|
|
case .landscapeRight:
|
|
showBars(on: navigationController)
|
|
default:
|
|
showBars(on: navigationController)
|
|
}
|
|
}
|
|
|
|
private func configureImmersiveReading() {
|
|
configureDeviceOrientationNotifications()
|
|
configureNavigationController()
|
|
|
|
func configureDeviceOrientationNotifications() {
|
|
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
|
NotificationCenter.default.addObserver(self,
|
|
selector: #selector(self.onOrientationChange),
|
|
name: UIDevice.orientationDidChangeNotification,
|
|
object: nil)
|
|
}
|
|
|
|
func configureNavigationController() {
|
|
webView.scrollView.delegate = self
|
|
if parent?.navigationController != nil {
|
|
compactViewNavigationController = parent?.navigationController
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
extension WKWebView {
|
|
func adjustTextSize(pageZoom: Double? = nil) {
|
|
let pageZoom = pageZoom ?? Defaults[.webViewPageZoom]
|
|
let template = "document.getElementsByTagName('body')[0].style.webkitTextSizeAdjust='%.0f%%'"
|
|
let javascript = String(format: template, pageZoom * 100)
|
|
evaluateJavaScript(javascript, completionHandler: nil)
|
|
}
|
|
}
|
|
#endif
|
|
|
|
final class WebViewConfiguration: WKWebViewConfiguration {
|
|
override init() {
|
|
super.init()
|
|
setURLSchemeHandler(KiwixURLSchemeHandler(), forURLScheme: KiwixURLSchemeHandler.ZIMScheme)
|
|
#if os(macOS)
|
|
preferences.isElementFullscreenEnabled = true
|
|
#else
|
|
allowsInlineMediaPlayback = true
|
|
mediaTypesRequiringUserActionForPlayback = []
|
|
#endif
|
|
userContentController = {
|
|
let controller = WKUserContentController()
|
|
if let url = Bundle.main.url(forResource: "injection", withExtension: "js"),
|
|
let javascript = try? String(contentsOf: url) {
|
|
let script = WKUserScript(source: javascript, injectionTime: .atDocumentEnd, forMainFrameOnly: false)
|
|
controller.addUserScript(script)
|
|
}
|
|
return controller
|
|
}()
|
|
}
|
|
|
|
required init?(coder: NSCoder) {
|
|
fatalError("init(coder:) has not been implemented")
|
|
}
|
|
}
|