Reader Mode
This commit is contained in:
@@ -240,6 +240,23 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
|
||||
documentControls.navigationControlView.forwardButton.isEnabled = webView.canGoForward
|
||||
}))
|
||||
|
||||
// Reader mode
|
||||
documentControls.readabilityView.addAction(UIAction { [unowned self] _ in
|
||||
tab.bridge.parseDocumentForReaderMode { string in
|
||||
DispatchQueue.main.async {
|
||||
documentControls.dismiss(animated: true, completion: nil)
|
||||
|
||||
let readableViewController = ReaderViewController(readableHTMLString: string, baseURL: tab.bridge.webView.url)
|
||||
readableViewController.title = tab.bridge.webView.title
|
||||
readableViewController.darkModeEnabled = tab.bridge.darkModeEnabled
|
||||
readableViewController.delegate = self
|
||||
|
||||
let navigationController = UINavigationController(rootViewController: readableViewController)
|
||||
present(navigationController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
}, for: .touchUpInside)
|
||||
|
||||
present(documentControls, animated: true, completion: nil)
|
||||
}), for: .touchUpInside)
|
||||
|
||||
@@ -708,3 +725,10 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
|
||||
tab.webView._viewScale -= 0.10
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowserViewController: ReaderViewControllerDelegate
|
||||
{
|
||||
func readerViewController(_ reader: ReaderViewController, didRequestNavigationToURL navigationURL: URL) {
|
||||
tab.beginLoadingURL(navigationURL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ class DocumentControlViewController: UIViewController
|
||||
let findOnPageControlView = DocumentControlView()
|
||||
let navigationControlView = NavigationControlsView()
|
||||
let settingsView = DocumentControlView()
|
||||
let readabilityView = DocumentControlView()
|
||||
|
||||
var observations: [NSKeyValueObservation] = []
|
||||
|
||||
@@ -28,9 +29,13 @@ class DocumentControlViewController: UIViewController
|
||||
settingsView.label.text = "Settings"
|
||||
settingsView.imageView.image = UIImage(systemName: "gear")
|
||||
|
||||
readabilityView.label.text = "Reader Mode"
|
||||
readabilityView.imageView.image = UIImage(systemName: "doc.richtext")
|
||||
|
||||
documentControlView.addArrangedSubview(navigationControlView)
|
||||
documentControlView.addArrangedSubview(fontSizeAdjustView)
|
||||
documentControlView.addArrangedSubview(findOnPageControlView)
|
||||
documentControlView.addArrangedSubview(readabilityView)
|
||||
documentControlView.addArrangedSubview(settingsView)
|
||||
|
||||
for (i, view) in documentControlView.arrangedSubviews.enumerated() {
|
||||
|
||||
102
App/Reader View/ReaderViewController.swift
Normal file
102
App/Reader View/ReaderViewController.swift
Normal file
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// ReaderViewController.swift
|
||||
// App
|
||||
//
|
||||
// Created by James Magahern on 2/15/21.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import WebKit
|
||||
|
||||
protocol ReaderViewControllerDelegate: AnyObject
|
||||
{
|
||||
func readerViewController(_ reader: ReaderViewController, didRequestNavigationToURL navigationURL: URL)
|
||||
}
|
||||
|
||||
class ReaderViewController: UIViewController
|
||||
{
|
||||
public let baseURL: URL?
|
||||
public let readableHTMLString: String
|
||||
public var darkModeEnabled: Bool = false { didSet { bridge.darkModeEnabled = darkModeEnabled; updateDarkModeButton() } }
|
||||
|
||||
public weak var delegate: ReaderViewControllerDelegate?
|
||||
|
||||
private let bridge = ProcessBundleBridge(webViewConfiguration: nil)
|
||||
|
||||
private let darkModeDisabledImage = UIImage(systemName: "moon.circle")
|
||||
private let darkModeEnabledImage = UIImage(systemName: "moon.circle.fill")
|
||||
|
||||
private lazy var darkModeButton: UIBarButtonItem = {
|
||||
UIBarButtonItem(image: darkModeEnabledImage, style: .plain, target: self, action: #selector(self.didTapDarkModeButton))
|
||||
}()
|
||||
|
||||
init(readableHTMLString: String, baseURL: URL?) {
|
||||
self.readableHTMLString = readableHTMLString
|
||||
self.baseURL = baseURL
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadView() {
|
||||
self.view = bridge.webView
|
||||
bridge.webView.navigationDelegate = self
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationItem.leftBarButtonItem = darkModeButton
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(self.didTapDoneButton))
|
||||
}
|
||||
|
||||
private func updateDarkModeButton() {
|
||||
if darkModeEnabled {
|
||||
darkModeButton.image = darkModeEnabledImage
|
||||
} else {
|
||||
darkModeButton.image = darkModeDisabledImage
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func didTapDoneButton(_ sender: Any?) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc
|
||||
private func didTapDarkModeButton(_ sender: Any?) {
|
||||
darkModeEnabled = !darkModeEnabled
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
let readerHTMLURL = Bundle.main.url(forResource: "reader", withExtension: "html")!
|
||||
var readerHTML = try! String(contentsOf: readerHTMLURL)
|
||||
|
||||
let bodyRange = readerHTML.range(of: "{{ body }}")!
|
||||
readerHTML.replaceSubrange(bodyRange, with: readableHTMLString)
|
||||
|
||||
bridge.webView.loadHTMLString(readerHTML, baseURL: baseURL)
|
||||
|
||||
updateDarkModeButton()
|
||||
}
|
||||
}
|
||||
|
||||
extension ReaderViewController: WKNavigationDelegate
|
||||
{
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
guard let url = navigationAction.request.url else { return }
|
||||
|
||||
if url == baseURL {
|
||||
decisionHandler(.allow)
|
||||
return
|
||||
} else {
|
||||
// Don't load links in here, dismiss the reader view and load them in the tab behind us.
|
||||
delegate?.readerViewController(self, didRequestNavigationToURL: url)
|
||||
|
||||
dismiss(animated: true, completion: nil)
|
||||
decisionHandler(.cancel)
|
||||
}
|
||||
}
|
||||
}
|
||||
2260
App/Resources/Readability.js
Normal file
2260
App/Resources/Readability.js
Normal file
File diff suppressed because it is too large
Load Diff
27
App/Resources/reader.html
Normal file
27
App/Resources/reader.html
Normal file
@@ -0,0 +1,27 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
margin: 2em;
|
||||
|
||||
line-height: 1.8em;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{{ body }}
|
||||
</body>
|
||||
</html>
|
||||
@@ -18,7 +18,7 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
|
||||
public weak var delegate: TabDelegate?
|
||||
|
||||
public let homeURL: URL?
|
||||
public let bridge: SBRProcessBundleBridge
|
||||
public let bridge: ProcessBundleBridge
|
||||
public var webView: WKWebView {
|
||||
if self.loadedWebView == nil {
|
||||
self.loadedWebView = bridge.webView
|
||||
@@ -83,7 +83,7 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
|
||||
self.homeURL = url
|
||||
self.policyManager = policyManager
|
||||
|
||||
self.bridge = SBRProcessBundleBridge(webViewConfiguration: webViewConfiguration)
|
||||
self.bridge = ProcessBundleBridge(webViewConfiguration: webViewConfiguration)
|
||||
self.bridge.policyDataSource = policyManager
|
||||
|
||||
super.init()
|
||||
@@ -102,12 +102,12 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
|
||||
|
||||
// MARK: SBRProcessBundleBridgeDelegate
|
||||
|
||||
func webProcess(_ bridge: SBRProcessBundleBridge, didAllowScriptResourceFromOrigin origin: String) {
|
||||
func webProcess(_ bridge: ProcessBundleBridge, didAllowScriptResourceFromOrigin origin: String) {
|
||||
print("Allowed script resource from origin: \(origin)")
|
||||
allowedScriptOrigins.formUnion([ origin ])
|
||||
}
|
||||
|
||||
func webProcess(_ bridge: SBRProcessBundleBridge, didBlockScriptResourceFromOrigin origin: String) {
|
||||
func webProcess(_ bridge: ProcessBundleBridge, didBlockScriptResourceFromOrigin origin: String) {
|
||||
print("Blocked script resource from origin: \(origin)")
|
||||
blockedScriptOrigins.formUnion([ origin ])
|
||||
delegate?.didBlockScriptOrigin(origin, forTab: self)
|
||||
|
||||
@@ -23,6 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (void)webProcess:(SBRProcessBundleBridge *)bridge didBlockScriptResourceFromOrigin:(NSString *)origin;
|
||||
@end
|
||||
|
||||
NS_SWIFT_NAME(ProcessBundleBridge)
|
||||
@interface SBRProcessBundleBridge : NSObject
|
||||
|
||||
@property (nonatomic, readonly) WKWebView *webView;
|
||||
@@ -39,6 +40,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (void)policyDataSourceDidChange;
|
||||
- (void)tearDown;
|
||||
|
||||
- (void)parseDocumentForReaderMode:(void(^)(NSString *))completionBlock NS_SWIFT_NAME(parseDocumentForReaderMode(completion:));
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@end
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
id<SBRWebProcessProxy> _webProcessProxy;
|
||||
|
||||
_WKUserStyleSheet *_darkModeStyleSheet;
|
||||
WKUserScript *_readabilityScript;
|
||||
}
|
||||
|
||||
- (void)tearDown
|
||||
@@ -161,4 +162,32 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)parseDocumentForReaderMode:(void (^)(NSString * _Nonnull))completionBlock
|
||||
{
|
||||
WKUserContentController *userContentController = [_webViewConfiguration userContentController];
|
||||
|
||||
if (!_readabilityScript) {
|
||||
NSURL *readabilityJSURL = [[NSBundle mainBundle] URLForResource:@"Readability" withExtension:@"js"];
|
||||
NSString *readabilityJSSource = [NSString stringWithContentsOfURL:readabilityJSURL encoding:NSUTF8StringEncoding error:nil];
|
||||
|
||||
_readabilityScript = [[WKUserScript alloc] initWithSource:readabilityJSSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
|
||||
}
|
||||
|
||||
[userContentController _addUserScriptImmediately:_readabilityScript];
|
||||
|
||||
NSString *script = @""
|
||||
"var documentClone = document.cloneNode(true);"
|
||||
"var article = new Readability(documentClone).parse();"
|
||||
"article.content";
|
||||
|
||||
[_webView evaluateJavaScript:script completionHandler:^(NSString *result, NSError * _Nullable error) {
|
||||
if (error != nil) {
|
||||
NSLog(@"Bridge: Readability error: %@", error.localizedDescription);
|
||||
} else {
|
||||
completionBlock(result);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -49,6 +49,9 @@
|
||||
CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */; };
|
||||
CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BD324E77BF900D2BDCC /* HistoryItem.swift */; };
|
||||
CD97CF9225D5BE6F00288FEE /* NavigationControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD97CF9125D5BE6F00288FEE /* NavigationControlsView.swift */; };
|
||||
CDC5DA3A25DB774D00BA8D99 /* Readability.js in Resources */ = {isa = PBXBuildFile; fileRef = CDC5DA3925DB774D00BA8D99 /* Readability.js */; };
|
||||
CDC5DA3E25DB7C2C00BA8D99 /* ReaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDC5DA3D25DB7C2C00BA8D99 /* ReaderViewController.swift */; };
|
||||
CDC5DA4025DB7EAC00BA8D99 /* reader.html in Resources */ = {isa = PBXBuildFile; fileRef = CDC5DA3F25DB7EAC00BA8D99 /* reader.html */; };
|
||||
CDCE2664251AA80F007FE92A /* DocumentControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCE2663251AA80F007FE92A /* DocumentControlViewController.swift */; };
|
||||
CDCE2666251AA840007FE92A /* StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCE2665251AA840007FE92A /* StackView.swift */; };
|
||||
CDCE2668251AAA9A007FE92A /* FontSizeAdjustView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCE2667251AAA9A007FE92A /* FontSizeAdjustView.swift */; };
|
||||
@@ -131,6 +134,9 @@
|
||||
CD853BD024E778B800D2BDCC /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = "<group>"; };
|
||||
CD853BD324E77BF900D2BDCC /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = "<group>"; };
|
||||
CD97CF9125D5BE6F00288FEE /* NavigationControlsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationControlsView.swift; sourceTree = "<group>"; };
|
||||
CDC5DA3925DB774D00BA8D99 /* Readability.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = Readability.js; sourceTree = "<group>"; };
|
||||
CDC5DA3D25DB7C2C00BA8D99 /* ReaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderViewController.swift; sourceTree = "<group>"; };
|
||||
CDC5DA3F25DB7EAC00BA8D99 /* reader.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = reader.html; sourceTree = "<group>"; };
|
||||
CDCE2663251AA80F007FE92A /* DocumentControlViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentControlViewController.swift; sourceTree = "<group>"; };
|
||||
CDCE2665251AA840007FE92A /* StackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StackView.swift; sourceTree = "<group>"; };
|
||||
CDCE2667251AAA9A007FE92A /* FontSizeAdjustView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSizeAdjustView.swift; sourceTree = "<group>"; };
|
||||
@@ -171,6 +177,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
1A14FC2524D251BD009B3F83 /* darkmode.css */,
|
||||
CDC5DA3925DB774D00BA8D99 /* Readability.js */,
|
||||
CDC5DA3F25DB7EAC00BA8D99 /* reader.html */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
@@ -239,6 +247,7 @@
|
||||
1A03810E24E71CCA00826501 /* Common UI */,
|
||||
CDCE2662251AA7FC007FE92A /* Document Controls UI */,
|
||||
1AD3104125254FA300A4A952 /* Find on Page */,
|
||||
CDC5DA3C25DB7A5500BA8D99 /* Reader View */,
|
||||
1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */,
|
||||
1AB88F0324D3E1EC0006F850 /* Tabs */,
|
||||
1AB88F0424D3E1F90006F850 /* Titlebar and URL Bar */,
|
||||
@@ -350,6 +359,14 @@
|
||||
path = History;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CDC5DA3C25DB7A5500BA8D99 /* Reader View */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
CDC5DA3D25DB7C2C00BA8D99 /* ReaderViewController.swift */,
|
||||
);
|
||||
path = "Reader View";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CDCE2662251AA7FC007FE92A /* Document Controls UI */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -443,8 +460,10 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
1A14FC2624D251BD009B3F83 /* darkmode.css in Resources */,
|
||||
CDC5DA4025DB7EAC00BA8D99 /* reader.html in Resources */,
|
||||
1ADFF46C24C7DE54006DC7AE /* LaunchScreen.storyboard in Resources */,
|
||||
1ADFF46924C7DE54006DC7AE /* Assets.xcassets in Resources */,
|
||||
CDC5DA3A25DB774D00BA8D99 /* Readability.js in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -489,6 +508,7 @@
|
||||
1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */,
|
||||
1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */,
|
||||
CD7A8915251975B70075991E /* AutocompleteViewController.swift in Sources */,
|
||||
CDC5DA3E25DB7C2C00BA8D99 /* ReaderViewController.swift in Sources */,
|
||||
1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */,
|
||||
1ADFF4C324CA6AF6006DC7AE /* Geometry.swift in Sources */,
|
||||
1ADFF4C924CA793E006DC7AE /* ToolbarViewController.swift in Sources */,
|
||||
|
||||
Reference in New Issue
Block a user