Reader Mode

This commit is contained in:
James Magahern
2021-02-15 22:34:05 -08:00
parent 6611e381a2
commit 2b5475d7f8
9 changed files with 2474 additions and 4 deletions

View File

@@ -240,6 +240,23 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
documentControls.navigationControlView.forwardButton.isEnabled = webView.canGoForward 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) present(documentControls, animated: true, completion: nil)
}), for: .touchUpInside) }), for: .touchUpInside)
@@ -708,3 +725,10 @@ class BrowserViewController: UIViewController, WKNavigationDelegate, WKUIDelegat
tab.webView._viewScale -= 0.10 tab.webView._viewScale -= 0.10
} }
} }
extension BrowserViewController: ReaderViewControllerDelegate
{
func readerViewController(_ reader: ReaderViewController, didRequestNavigationToURL navigationURL: URL) {
tab.beginLoadingURL(navigationURL)
}
}

View File

@@ -14,6 +14,7 @@ class DocumentControlViewController: UIViewController
let findOnPageControlView = DocumentControlView() let findOnPageControlView = DocumentControlView()
let navigationControlView = NavigationControlsView() let navigationControlView = NavigationControlsView()
let settingsView = DocumentControlView() let settingsView = DocumentControlView()
let readabilityView = DocumentControlView()
var observations: [NSKeyValueObservation] = [] var observations: [NSKeyValueObservation] = []
@@ -28,9 +29,13 @@ class DocumentControlViewController: UIViewController
settingsView.label.text = "Settings" settingsView.label.text = "Settings"
settingsView.imageView.image = UIImage(systemName: "gear") settingsView.imageView.image = UIImage(systemName: "gear")
readabilityView.label.text = "Reader Mode"
readabilityView.imageView.image = UIImage(systemName: "doc.richtext")
documentControlView.addArrangedSubview(navigationControlView) documentControlView.addArrangedSubview(navigationControlView)
documentControlView.addArrangedSubview(fontSizeAdjustView) documentControlView.addArrangedSubview(fontSizeAdjustView)
documentControlView.addArrangedSubview(findOnPageControlView) documentControlView.addArrangedSubview(findOnPageControlView)
documentControlView.addArrangedSubview(readabilityView)
documentControlView.addArrangedSubview(settingsView) documentControlView.addArrangedSubview(settingsView)
for (i, view) in documentControlView.arrangedSubviews.enumerated() { for (i, view) in documentControlView.arrangedSubviews.enumerated() {

View 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

File diff suppressed because it is too large Load Diff

27
App/Resources/reader.html Normal file
View 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>

View File

@@ -18,7 +18,7 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
public weak var delegate: TabDelegate? public weak var delegate: TabDelegate?
public let homeURL: URL? public let homeURL: URL?
public let bridge: SBRProcessBundleBridge public let bridge: ProcessBundleBridge
public var webView: WKWebView { public var webView: WKWebView {
if self.loadedWebView == nil { if self.loadedWebView == nil {
self.loadedWebView = bridge.webView self.loadedWebView = bridge.webView
@@ -83,7 +83,7 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
self.homeURL = url self.homeURL = url
self.policyManager = policyManager self.policyManager = policyManager
self.bridge = SBRProcessBundleBridge(webViewConfiguration: webViewConfiguration) self.bridge = ProcessBundleBridge(webViewConfiguration: webViewConfiguration)
self.bridge.policyDataSource = policyManager self.bridge.policyDataSource = policyManager
super.init() super.init()
@@ -102,12 +102,12 @@ class Tab: NSObject, SBRProcessBundleBridgeDelegate
// MARK: SBRProcessBundleBridgeDelegate // MARK: SBRProcessBundleBridgeDelegate
func webProcess(_ bridge: SBRProcessBundleBridge, didAllowScriptResourceFromOrigin origin: String) { func webProcess(_ bridge: ProcessBundleBridge, didAllowScriptResourceFromOrigin origin: String) {
print("Allowed script resource from origin: \(origin)") print("Allowed script resource from origin: \(origin)")
allowedScriptOrigins.formUnion([ 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)") print("Blocked script resource from origin: \(origin)")
blockedScriptOrigins.formUnion([ origin ]) blockedScriptOrigins.formUnion([ origin ])
delegate?.didBlockScriptOrigin(origin, forTab: self) delegate?.didBlockScriptOrigin(origin, forTab: self)

View File

@@ -23,6 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)webProcess:(SBRProcessBundleBridge *)bridge didBlockScriptResourceFromOrigin:(NSString *)origin; - (void)webProcess:(SBRProcessBundleBridge *)bridge didBlockScriptResourceFromOrigin:(NSString *)origin;
@end @end
NS_SWIFT_NAME(ProcessBundleBridge)
@interface SBRProcessBundleBridge : NSObject @interface SBRProcessBundleBridge : NSObject
@property (nonatomic, readonly) WKWebView *webView; @property (nonatomic, readonly) WKWebView *webView;
@@ -39,6 +40,8 @@ NS_ASSUME_NONNULL_BEGIN
- (void)policyDataSourceDidChange; - (void)policyDataSourceDidChange;
- (void)tearDown; - (void)tearDown;
- (void)parseDocumentForReaderMode:(void(^)(NSString *))completionBlock NS_SWIFT_NAME(parseDocumentForReaderMode(completion:));
- (instancetype)init NS_UNAVAILABLE; - (instancetype)init NS_UNAVAILABLE;
@end @end

View File

@@ -31,6 +31,7 @@
id<SBRWebProcessProxy> _webProcessProxy; id<SBRWebProcessProxy> _webProcessProxy;
_WKUserStyleSheet *_darkModeStyleSheet; _WKUserStyleSheet *_darkModeStyleSheet;
WKUserScript *_readabilityScript;
} }
- (void)tearDown - (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 @end

View File

@@ -49,6 +49,9 @@
CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */; }; CD853BD124E778B800D2BDCC /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = CD853BCF24E778B800D2BDCC /* History.xcdatamodeld */; };
CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BD324E77BF900D2BDCC /* HistoryItem.swift */; }; CD853BD424E77BF900D2BDCC /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD853BD324E77BF900D2BDCC /* HistoryItem.swift */; };
CD97CF9225D5BE6F00288FEE /* NavigationControlsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD97CF9125D5BE6F00288FEE /* NavigationControlsView.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 */; }; CDCE2664251AA80F007FE92A /* DocumentControlViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCE2663251AA80F007FE92A /* DocumentControlViewController.swift */; };
CDCE2666251AA840007FE92A /* StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCE2665251AA840007FE92A /* StackView.swift */; }; CDCE2666251AA840007FE92A /* StackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCE2665251AA840007FE92A /* StackView.swift */; };
CDCE2668251AAA9A007FE92A /* FontSizeAdjustView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDCE2667251AAA9A007FE92A /* FontSizeAdjustView.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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; CDCE2667251AAA9A007FE92A /* FontSizeAdjustView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontSizeAdjustView.swift; sourceTree = "<group>"; };
@@ -171,6 +177,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
1A14FC2524D251BD009B3F83 /* darkmode.css */, 1A14FC2524D251BD009B3F83 /* darkmode.css */,
CDC5DA3925DB774D00BA8D99 /* Readability.js */,
CDC5DA3F25DB7EAC00BA8D99 /* reader.html */,
); );
path = Resources; path = Resources;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -239,6 +247,7 @@
1A03810E24E71CCA00826501 /* Common UI */, 1A03810E24E71CCA00826501 /* Common UI */,
CDCE2662251AA7FC007FE92A /* Document Controls UI */, CDCE2662251AA7FC007FE92A /* Document Controls UI */,
1AD3104125254FA300A4A952 /* Find on Page */, 1AD3104125254FA300A4A952 /* Find on Page */,
CDC5DA3C25DB7A5500BA8D99 /* Reader View */,
1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */, 1ADFF4CE24CBBCBD006DC7AE /* Script Policy UI */,
1AB88F0324D3E1EC0006F850 /* Tabs */, 1AB88F0324D3E1EC0006F850 /* Tabs */,
1AB88F0424D3E1F90006F850 /* Titlebar and URL Bar */, 1AB88F0424D3E1F90006F850 /* Titlebar and URL Bar */,
@@ -350,6 +359,14 @@
path = History; path = History;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
CDC5DA3C25DB7A5500BA8D99 /* Reader View */ = {
isa = PBXGroup;
children = (
CDC5DA3D25DB7C2C00BA8D99 /* ReaderViewController.swift */,
);
path = "Reader View";
sourceTree = "<group>";
};
CDCE2662251AA7FC007FE92A /* Document Controls UI */ = { CDCE2662251AA7FC007FE92A /* Document Controls UI */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -443,8 +460,10 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
1A14FC2624D251BD009B3F83 /* darkmode.css in Resources */, 1A14FC2624D251BD009B3F83 /* darkmode.css in Resources */,
CDC5DA4025DB7EAC00BA8D99 /* reader.html in Resources */,
1ADFF46C24C7DE54006DC7AE /* LaunchScreen.storyboard in Resources */, 1ADFF46C24C7DE54006DC7AE /* LaunchScreen.storyboard in Resources */,
1ADFF46924C7DE54006DC7AE /* Assets.xcassets in Resources */, 1ADFF46924C7DE54006DC7AE /* Assets.xcassets in Resources */,
CDC5DA3A25DB774D00BA8D99 /* Readability.js in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -489,6 +508,7 @@
1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */, 1A03810B24E71C5600826501 /* ToolbarButtonContainerView.swift in Sources */,
1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */, 1ADFF4CB24CB8278006DC7AE /* ScriptControllerIconView.swift in Sources */,
CD7A8915251975B70075991E /* AutocompleteViewController.swift in Sources */, CD7A8915251975B70075991E /* AutocompleteViewController.swift in Sources */,
CDC5DA3E25DB7C2C00BA8D99 /* ReaderViewController.swift in Sources */,
1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */, 1AB88EFD24D3BA560006F850 /* TabController.swift in Sources */,
1ADFF4C324CA6AF6006DC7AE /* Geometry.swift in Sources */, 1ADFF4C324CA6AF6006DC7AE /* Geometry.swift in Sources */,
1ADFF4C924CA793E006DC7AE /* ToolbarViewController.swift in Sources */, 1ADFF4C924CA793E006DC7AE /* ToolbarViewController.swift in Sources */,