mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-06 15:58:39 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
7
mobile/ios/Client/Frontend/Reader/FSReadingList.h
Normal file
7
mobile/ios/Client/Frontend/Reader/FSReadingList.h
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#pragma once
|
||||
|
||||
extern NSString* const FSReadingListAddReadingListItemNotification;
|
||||
60
mobile/ios/Client/Frontend/Reader/FSReadingList.m
Normal file
60
mobile/ios/Client/Frontend/Reader/FSReadingList.m
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <SafariServices/SafariServices.h>
|
||||
|
||||
#import "Swizzling.h"
|
||||
|
||||
NSString* const FSReadingListAddReadingListItemNotification = @"FSReadingListAddReadingListItemNotification";
|
||||
|
||||
@interface FSReadingList: NSObject
|
||||
+ (id) sharedInstance;
|
||||
+ (BOOL)supportsURL:(NSURL *)URL;
|
||||
- (BOOL)addReadingListItemWithURL:(NSURL *)URL title:(NSString *)title previewText:(NSString *)previewText error:(NSError **)error;
|
||||
@end
|
||||
|
||||
@implementation FSReadingList
|
||||
+ (id) sharedInstance {
|
||||
static FSReadingList *sharedFSReadingList = nil;
|
||||
@synchronized (self) {
|
||||
if (sharedFSReadingList == nil) {
|
||||
sharedFSReadingList = [FSReadingList new];
|
||||
}
|
||||
}
|
||||
return sharedFSReadingList;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsURL:(NSURL *)URL {
|
||||
return [[URL scheme] isEqualToString: @"http"] || [[URL scheme] isEqualToString: @"https"];
|
||||
}
|
||||
|
||||
- (BOOL)addReadingListItemWithURL:(NSURL *)URL title:(NSString *)title previewText:(NSString *)previewText error:(NSError **)error {
|
||||
if (error != NULL) {
|
||||
*error = nil;
|
||||
}
|
||||
// To keep this as simple as possible and have as little as possible coupling between this Objective-C
|
||||
// singleton and our Swift world, we simply send out a notification that our AppDelegate (which has access
|
||||
// to the browser profile and reading list service) can respond to.
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName: FSReadingListAddReadingListItemNotification
|
||||
object:self userInfo: @{@"URL": URL, @"Title": title}];
|
||||
return YES;
|
||||
}
|
||||
@end
|
||||
|
||||
// This class extension on SSReadingList implements an initialize method that will be called when the class
|
||||
// is instantiated. It swizzles defaultReadingList to our own implementation which returns a shared instance
|
||||
// of the FSReadingList. ("FirefoxServices" Reading List)
|
||||
|
||||
@implementation SSReadingList (Firefox)
|
||||
+ (void) initialize {
|
||||
if ([SSReadingList class] == self) {
|
||||
SwizzleClassMethods([SSReadingList class], @selector(defaultReadingList), @selector(defaultFSReadingList));
|
||||
}
|
||||
}
|
||||
|
||||
+ (id) defaultFSReadingList {
|
||||
return [FSReadingList sharedInstance];
|
||||
}
|
||||
@end
|
||||
112
mobile/ios/Client/Frontend/Reader/ReadabilityService.swift
Normal file
112
mobile/ios/Client/Frontend/Reader/ReadabilityService.swift
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
private let ReadabilityServiceSharedInstance = ReadabilityService()
|
||||
|
||||
private let ReadabilityTaskDefaultTimeout = 15
|
||||
private let ReadabilityServiceDefaultConcurrency = 1
|
||||
|
||||
enum ReadabilityOperationResult {
|
||||
case success(ReadabilityResult)
|
||||
case error(NSError)
|
||||
case timeout
|
||||
}
|
||||
|
||||
class ReadabilityOperation: Operation, WKNavigationDelegate, ReadabilityTabHelperDelegate {
|
||||
var url: URL
|
||||
var semaphore: DispatchSemaphore
|
||||
var result: ReadabilityOperationResult?
|
||||
var tab: Tab!
|
||||
var readerModeCache: ReaderModeCache
|
||||
|
||||
init(url: URL, readerModeCache: ReaderModeCache) {
|
||||
self.url = url
|
||||
self.semaphore = DispatchSemaphore(value: 0)
|
||||
self.readerModeCache = readerModeCache
|
||||
}
|
||||
|
||||
override func main() {
|
||||
if self.isCancelled {
|
||||
return
|
||||
}
|
||||
|
||||
// Setup a tab, attach a Readability helper. Kick all this off on the main thread since UIKit
|
||||
// and WebKit are not safe from other threads.
|
||||
|
||||
DispatchQueue.main.async(execute: { () -> Void in
|
||||
let configuration = WKWebViewConfiguration()
|
||||
self.tab = Tab(configuration: configuration)
|
||||
self.tab.createWebview()
|
||||
self.tab.navigationDelegate = self
|
||||
|
||||
if let readabilityTabHelper = ReadabilityTabHelper(tab: self.tab) {
|
||||
readabilityTabHelper.delegate = self
|
||||
self.tab.addContentScript(readabilityTabHelper, name: ReadabilityTabHelper.name())
|
||||
}
|
||||
|
||||
// Load the page in the webview. This either fails with a navigation error, or we get a readability
|
||||
// callback. Or it takes too long, in which case the semaphore times out.
|
||||
self.tab.loadRequest(URLRequest(url: self.url))
|
||||
})
|
||||
let timeout = DispatchTime.now() + Double(Int64(Double(16) * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
|
||||
if semaphore.wait(timeout: timeout) == .timedOut {
|
||||
result = ReadabilityOperationResult.timeout
|
||||
}
|
||||
|
||||
// Maybe this is where we should store stuff in the cache / run a callback?
|
||||
|
||||
if let result = self.result {
|
||||
switch result {
|
||||
case .timeout:
|
||||
// Don't do anything on timeout
|
||||
break
|
||||
case .success(let readabilityResult):
|
||||
do {
|
||||
try readerModeCache.put(url, readabilityResult)
|
||||
} catch let error as NSError {
|
||||
print("Failed to store readability results in the cache: \(error.localizedDescription)")
|
||||
// TODO Fail
|
||||
}
|
||||
case .error(_):
|
||||
// TODO Not entitely sure what to do on error. Needs UX discussion and followup bug.
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
|
||||
result = ReadabilityOperationResult.error(error as NSError)
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) {
|
||||
result = ReadabilityOperationResult.error(error as NSError)
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
func readabilityTabHelper(_ readabilityTabHelper: ReadabilityTabHelper, didFinishWithReadabilityResult readabilityResult: ReadabilityResult) {
|
||||
result = ReadabilityOperationResult.success(readabilityResult)
|
||||
semaphore.signal()
|
||||
}
|
||||
}
|
||||
|
||||
class ReadabilityService {
|
||||
class var sharedInstance: ReadabilityService {
|
||||
return ReadabilityServiceSharedInstance
|
||||
}
|
||||
|
||||
var queue: OperationQueue
|
||||
|
||||
init() {
|
||||
queue = OperationQueue()
|
||||
queue.maxConcurrentOperationCount = ReadabilityServiceDefaultConcurrency
|
||||
}
|
||||
|
||||
func process(_ url: URL, cache: ReaderModeCache) {
|
||||
queue.addOperation(ReadabilityOperation(url: url, readerModeCache: cache))
|
||||
}
|
||||
}
|
||||
30
mobile/ios/Client/Frontend/Reader/ReadabilityTabHelper.js
Normal file
30
mobile/ios/Client/Frontend/Reader/ReadabilityTabHelper.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
(function() {
|
||||
// To keep this file readable and Readability.js separate, since we import it from an external
|
||||
// repository, we merge it in this file programatically. We do not include it as a user script
|
||||
// because that means pages can mess with it; by including it below, it is part of an anonymous
|
||||
// function that only exists once.
|
||||
|
||||
%READABILITYJS%
|
||||
|
||||
var uri = {
|
||||
spec: document.location.href,
|
||||
host: document.location.host,
|
||||
prePath: document.location.protocol + "//" + document.location.host, // TODO This is incomplete, needs username/password and port
|
||||
scheme: document.location.protocol.substr(0, document.location.protocol.indexOf(":")),
|
||||
pathBase: document.location.protocol + "//" + document.location.host + location.pathname.substr(0, location.pathname.lastIndexOf("/") + 1)
|
||||
}
|
||||
|
||||
// document.cloneNode() can cause the webview to break (bug 1128774).
|
||||
// Serialize and then parse the document instead.
|
||||
var docStr = new XMLSerializer().serializeToString(document);
|
||||
var doc = new DOMParser().parseFromString(docStr, "text/html");
|
||||
|
||||
var readability = new Readability(uri, doc);
|
||||
var readabilityResult = readability.parse();
|
||||
|
||||
webkit.messageHandlers.readabilityMessageHandler.postMessage(readabilityResult);
|
||||
})();
|
||||
39
mobile/ios/Client/Frontend/Reader/ReadabilityTabHelper.swift
Normal file
39
mobile/ios/Client/Frontend/Reader/ReadabilityTabHelper.swift
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import WebKit
|
||||
|
||||
protocol ReadabilityTabHelperDelegate {
|
||||
func readabilityTabHelper(_ readabilityTabHelper: ReadabilityTabHelper, didFinishWithReadabilityResult result: ReadabilityResult)
|
||||
}
|
||||
|
||||
class ReadabilityTabHelper: TabContentScript {
|
||||
var delegate: ReadabilityTabHelperDelegate?
|
||||
|
||||
class func name() -> String {
|
||||
return "ReadabilityTabHelper"
|
||||
}
|
||||
|
||||
init?(tab: Tab) {
|
||||
if let readabilityPath = Bundle.main.path(forResource: "Readability", ofType: "js"),
|
||||
let readabilitySource = try? NSMutableString(contentsOfFile: readabilityPath, encoding: String.Encoding.utf8.rawValue),
|
||||
let readabilityTabHelperPath = Bundle.main.path(forResource: ReadabilityTabHelper.name(), ofType: "js"),
|
||||
let readabilityTabHelperSource = try? NSMutableString(contentsOfFile: readabilityTabHelperPath, encoding: String.Encoding.utf8.rawValue) {
|
||||
readabilityTabHelperSource.replaceOccurrences(of: "%READABILITYJS%", with: readabilitySource as String, options: NSString.CompareOptions.literal, range: NSRange(location: 0, length: readabilityTabHelperSource.length))
|
||||
let userScript = WKUserScript(source: readabilityTabHelperSource as String, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true)
|
||||
tab.webView!.configuration.userContentController.addUserScript(userScript)
|
||||
}
|
||||
}
|
||||
|
||||
func scriptMessageHandlerName() -> String? {
|
||||
return "readabilityMessageHandler"
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
|
||||
if let readabilityResult = ReadabilityResult(object: message.body as AnyObject?) {
|
||||
delegate?.readabilityTabHelper(self, didFinishWithReadabilityResult: readabilityResult)
|
||||
}
|
||||
}
|
||||
}
|
||||
787
mobile/ios/Client/Frontend/Reader/Reader.css
Normal file
787
mobile/ios/Client/Frontend/Reader/Reader.css
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
@font-face {
|
||||
font-family: sans-serif;
|
||||
src: url('/reader-mode/fonts/FiraSans-Regular.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: sans-serif;
|
||||
src: url('/reader-mode/fonts/FiraSans-Book.ttf');
|
||||
font-weight: medium;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: sans-serif;
|
||||
src: url('/reader-mode/fonts/FiraSans-Bold.ttf');
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: sans-serif;
|
||||
src: url('/reader-mode/fonts/FiraSans-Italic.ttf');
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: sans-serif;
|
||||
src: url('/reader-mode/fonts/FiraSans-BoldItalic.ttf');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: serif;
|
||||
src: url('/reader-mode/fonts/CharisSILR.ttf');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: serif;
|
||||
src: url('/reader-mode/fonts/CharisSILB.ttf');
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: serif;
|
||||
src: url('/reader-mode/fonts/CharisSILI.ttf');
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: serif;
|
||||
src: url('/reader-mode/fonts/CharisSILBI.ttf');
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
html {
|
||||
-moz-text-size-adjust: none;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 2vw;
|
||||
transition-property: background-color, color;
|
||||
transition-duration: 0.4s;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.light {
|
||||
background-color: #ffffff;
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
.dark {
|
||||
background-color: #333333;
|
||||
color: #eeeeee;
|
||||
}
|
||||
|
||||
.sepia {
|
||||
background-color: #F0E6DC;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.sans-serif {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.serif {
|
||||
font-family: serif;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 40px;
|
||||
display: none;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#reader-header {
|
||||
text-align: start;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.domain,
|
||||
.credits {
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
.domain {
|
||||
margin-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
color: #00acff !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.domain-border {
|
||||
margin-top: 15px;
|
||||
border-bottom: 1.5px solid #777777;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.header > h1 {
|
||||
font-size: 1.5em;
|
||||
font-weight: 700;
|
||||
line-height: 1.1em;
|
||||
width: 100%;
|
||||
margin: 0px;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 16px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.header > .credits {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
margin-bottom: 24px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.light > .header > .domain {
|
||||
color: #ee7600;
|
||||
border-bottom-color: #d0d0d0;
|
||||
}
|
||||
|
||||
.light > .header > h1 {
|
||||
color: #222222;
|
||||
}
|
||||
|
||||
.light > .header > .credits {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.dark > .header > .domain {
|
||||
color: #ff9400;
|
||||
border-bottom-color: #777777;
|
||||
}
|
||||
|
||||
.dark > .header > h1 {
|
||||
color: #eeeeee;
|
||||
}
|
||||
|
||||
.dark > .header > .credits {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
|
||||
.font-size1 > .header > h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.font-size2 > .header > h1 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.font-size3 > .header > h1 {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.font-size4 > .header > h1 {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.font-size5 > .header > h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.font-size6 > .header > h1 {
|
||||
font-size: 44px;
|
||||
}
|
||||
|
||||
.font-size7 > .header > h1 {
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.font-size8 > .header > h1 {
|
||||
font-size: 52px;
|
||||
}
|
||||
|
||||
.font-size9 > .header > h1 {
|
||||
font-size: 56px;
|
||||
}
|
||||
|
||||
.font-size10 > .header > h1 {
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.font-size11 > .header > h1 {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.font-size12 > .header > h1 {
|
||||
font-size: 68px;
|
||||
}
|
||||
|
||||
.font-size13 > .header > h1 {
|
||||
font-size: 72px;
|
||||
}
|
||||
|
||||
/* This covers caption, domain, and credits
|
||||
texts in the reader UI */
|
||||
|
||||
.font-size1 > .content .wp-caption-text,
|
||||
.font-size1 > .content figcaption,
|
||||
.font-size1 > .header > .domain,
|
||||
.font-size1 > .header > .credits {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.font-size2 > .content .wp-caption-text,
|
||||
.font-size2 > .content figcaption,
|
||||
.font-size2 > .header > .domain,
|
||||
.font-size2 > .header > .credits {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.font-size3 > .content .wp-caption-text,
|
||||
.font-size3 > .content figcaption,
|
||||
.font-size3 > .header > .domain,
|
||||
.font-size3 > .header > .credits {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.font-size4 > .content .wp-caption-text,
|
||||
.font-size4 > .content figcaption,
|
||||
.font-size4 > .header > .domain,
|
||||
.font-size4 > .header > .credits {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.font-size5 > .content .wp-caption-text,
|
||||
.font-size5 > .content figcaption,
|
||||
.font-size5 > .header > .domain,
|
||||
.font-size5 > .header > .credits {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.font-size6 > .content .wp-caption-text,
|
||||
.font-size6 > .content figcaption,
|
||||
.font-size6 > .header > .domain,
|
||||
.font-size6 > .header > .credits {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.font-size7 > .content .wp-caption-text,
|
||||
.font-size7 > .content figcaption,
|
||||
.font-size7 > .header > .domain,
|
||||
.font-size7 > .header > .credits {
|
||||
font-size: 25px;
|
||||
}
|
||||
|
||||
.font-size8 > .content .wp-caption-text,
|
||||
.font-size8 > .content figcaption,
|
||||
.font-size8 > .header > .domain,
|
||||
.font-size8 > .header > .credits {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.font-size9 > .content .wp-caption-text,
|
||||
.font-size9 > .content figcaption,
|
||||
.font-size9 > .header > .domain,
|
||||
.font-size9 > .header > .credits {
|
||||
font-size: 31px;
|
||||
}
|
||||
|
||||
.font-size10 > .content .wp-caption-text,
|
||||
.font-size10 > .content figcaption,
|
||||
.font-size10 > .header > .domain,
|
||||
.font-size10 > .header > .credits {
|
||||
font-size: 35px;
|
||||
}
|
||||
|
||||
.font-size11 > .content .wp-caption-text,
|
||||
.font-size11 > .content figcaption,
|
||||
.font-size11 > .header > .domain,
|
||||
.font-size11 > .header > .credits {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
.font-size12 > .content .wp-caption-text,
|
||||
.font-size12 > .content figcaption,
|
||||
.font-size12 > .header > .domain,
|
||||
.font-size12 > .header > .credits {
|
||||
font-size: 45px;
|
||||
}
|
||||
|
||||
.font-size13 > .content .wp-caption-text,
|
||||
.font-size13 > .content figcaption,
|
||||
.font-size13 > .header > .domain,
|
||||
.font-size13 > .header > .credits {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
#reader-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.content a {
|
||||
text-decoration: none !important;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.light > .content a,
|
||||
.light > .content a:visited,
|
||||
.light > .content a:hover,
|
||||
.light > .content a:active {
|
||||
color: #00acff !important;
|
||||
}
|
||||
|
||||
.dark > .content a,
|
||||
.dark > .content a:visited,
|
||||
.dark > .content a:hover,
|
||||
.dark > .content a:active {
|
||||
color: #00acff !important;
|
||||
}
|
||||
|
||||
.sepia > .content a,
|
||||
.sepia > .content a:visited,
|
||||
.sepia > .content a:hover,
|
||||
.sepia > .content a:active {
|
||||
color: #00acff !important;
|
||||
}
|
||||
|
||||
.content * {
|
||||
max-width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.content p {
|
||||
line-height: 1.4em !important;
|
||||
margin: 0px !important;
|
||||
margin-bottom: 20px !important;
|
||||
}
|
||||
|
||||
/* Covers all images showing edge-to-edge using a
|
||||
an optional caption text */
|
||||
.content .wp-caption,
|
||||
.content figure {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
margin: 0px !important;
|
||||
margin-bottom: 32px !important;
|
||||
}
|
||||
|
||||
/* Images marked to be shown edge-to-edge with an
|
||||
optional captio ntext */
|
||||
.content p > img:only-child,
|
||||
.content p > a:only-child > img:only-child,
|
||||
.content .wp-caption img,
|
||||
.content figure img {
|
||||
max-width: none !important;
|
||||
height: auto !important;
|
||||
display: block !important;
|
||||
margin-top: 0px !important;
|
||||
margin-bottom: 32px !important;
|
||||
}
|
||||
|
||||
/* If image is place inside one of these blocks
|
||||
there's no need to add margin at the bottom */
|
||||
.content .wp-caption img,
|
||||
.content figure img {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
/* Image caption text */
|
||||
.content .caption,
|
||||
.content .wp-caption-text,
|
||||
.content figcaption {
|
||||
font-family: sans-serif;
|
||||
margin: 0px !important;
|
||||
padding-top: 4px !important;
|
||||
}
|
||||
|
||||
.light > .content .caption,
|
||||
.light > .content .wp-caption-text,
|
||||
.light > .content figcaption {
|
||||
color: #898989;
|
||||
}
|
||||
|
||||
.dark > .content .caption,
|
||||
.dark > .content .wp-caption-text,
|
||||
.dark > .content figcaption {
|
||||
color: #aaaaaa;
|
||||
}
|
||||
|
||||
/* Ensure all pre-formatted code inside the reader content
|
||||
are properly wrapped inside content width */
|
||||
.content code,
|
||||
.content pre {
|
||||
white-space: pre-wrap !important;
|
||||
margin-bottom: 20px !important;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.content blockquote {
|
||||
margin: 0px !important;
|
||||
margin-bottom: 20px !important;
|
||||
padding: 0px !important;
|
||||
-moz-padding-start: 16px !important;
|
||||
-webkit-padding-start: 16px !important;
|
||||
border: 0px !important;
|
||||
border-left: 2px solid !important;
|
||||
}
|
||||
|
||||
.light > .content blockquote {
|
||||
color: #898989 !important;
|
||||
border-left-color: #d0d0d0 !important;
|
||||
}
|
||||
|
||||
.dark > .content blockquote {
|
||||
color: #aaaaaa !important;
|
||||
border-left-color: #777777 !important;
|
||||
}
|
||||
|
||||
.content ul,
|
||||
.content ol {
|
||||
margin: 0px !important;
|
||||
margin-bottom: 20px !important;
|
||||
padding: 0px !important;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
.content ul {
|
||||
-moz-padding-start: 30px !important;
|
||||
-webkit-padding-start: 30px !important;
|
||||
list-style: disk !important;
|
||||
}
|
||||
|
||||
.content ol {
|
||||
-moz-padding-start: 35px !important;
|
||||
-webkit-padding-start: 35px !important;
|
||||
list-style: decimal !important;
|
||||
}
|
||||
|
||||
.font-size1-sample,
|
||||
.font-size1 > .content {
|
||||
font-size: 10px !important;
|
||||
}
|
||||
|
||||
.font-size2-sample,
|
||||
.font-size2 > .content {
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
.font-size3-sample,
|
||||
.font-size3 > .content {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.font-size4-sample,
|
||||
.font-size4 > .content {
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
||||
.font-size5-sample,
|
||||
.font-size5 > .content {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.font-size6-sample,
|
||||
.font-size6 > .content {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
.font-size7-sample,
|
||||
.font-size7 > .content {
|
||||
font-size: 21px !important;
|
||||
}
|
||||
|
||||
.font-size8-sample,
|
||||
.font-size8 > .content {
|
||||
font-size: 24px !important;
|
||||
}
|
||||
|
||||
.font-size9-sample,
|
||||
.font-size9 > .content {
|
||||
font-size: 28px !important;
|
||||
}
|
||||
|
||||
.font-size10-sample,
|
||||
.font-size10 > .content {
|
||||
font-size: 32px !important;
|
||||
}
|
||||
|
||||
.font-size11-sample,
|
||||
.font-size11 > .content {
|
||||
font-size: 37px !important;
|
||||
}
|
||||
|
||||
.font-size12-sample,
|
||||
.font-size12 > .content {
|
||||
font-size: 42px !important;
|
||||
}
|
||||
|
||||
.font-size13-sample,
|
||||
.font-size13 > .content {
|
||||
font-size: 48px !important;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
font-family: "Clear Sans",sans-serif;
|
||||
transition-property: visibility, opacity;
|
||||
transition-duration: 0.7s;
|
||||
visibility: visible;
|
||||
opacity: 1.0;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background-color: #EBEBF0;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.toolbar-hidden {
|
||||
transition-property: visibility, opacity;
|
||||
transition-duration: 0.7s;
|
||||
visibility: hidden;
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
.toolbar > * {
|
||||
float: right;
|
||||
width: 33%;
|
||||
}
|
||||
|
||||
.button {
|
||||
color: white;
|
||||
display: block;
|
||||
background-position: center;
|
||||
background-size: 30px 24px;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
list-style: none;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dropdown li {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.dropdown-popup {
|
||||
text-align: start;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
z-index: 1000;
|
||||
float: left;
|
||||
background: #EBEBF0;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 10px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 14px;
|
||||
box-shadow: 0px -1px 12px #333;
|
||||
border-radius: 3px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.dropdown-popup > hr {
|
||||
width: 100%;
|
||||
height: 0px;
|
||||
border: 0px;
|
||||
border-top: 1px solid #B5B5B5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.open > .dropdown-popup {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 6px;
|
||||
bottom: 100%;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.dropdown-arrow {
|
||||
position: absolute;
|
||||
width: 40px;
|
||||
height: 18px;
|
||||
bottom: -18px;
|
||||
background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-mdpi.png');
|
||||
background-size: 40px 18px;
|
||||
background-position: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#font-type-buttons,
|
||||
.segmented-button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
list-style: none;
|
||||
padding: 10px 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#font-type-buttons > li,
|
||||
.segmented-button > li {
|
||||
width: 50px; /* combined with flex, this acts as a minimum width */
|
||||
flex: 1 0 auto;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
#font-type-buttons > li {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.segmented-button > li {
|
||||
border-left: 1px solid #B5B5B5;
|
||||
}
|
||||
|
||||
.segmented-button > li:first-child {
|
||||
border-left: 0px;
|
||||
}
|
||||
|
||||
#font-type-buttons > li > a,
|
||||
.segmented-button > li > a {
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
|
||||
#font-type-buttons > li > a {
|
||||
display: inline-block;
|
||||
font-size: 48px;
|
||||
line-height: 50px;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: 3px solid transparent;
|
||||
}
|
||||
|
||||
.segmented-button > li > a {
|
||||
display: block;
|
||||
padding: 5px 0;
|
||||
font-family: "Clear Sans",sans-serif;
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
#font-type-buttons > li > a:active,
|
||||
#font-type-buttons > li.selected > a {
|
||||
border-color: #ff9400;
|
||||
}
|
||||
|
||||
.segmented-button > li > a:active,
|
||||
.segmented-button > li.selected > a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#font-type-buttons > li > .sans-serif {
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
#font-type-buttons > li > div {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toggle-button.on {
|
||||
background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-mdpi.png');
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-toggle-off-icon-mdpi.png');
|
||||
}
|
||||
|
||||
.share-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-share-icon-mdpi.png');
|
||||
}
|
||||
|
||||
.style-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-style-icon-mdpi.png');
|
||||
}
|
||||
|
||||
@media screen and (min-resolution: 1.25dppx) {
|
||||
.dropdown-arrow {
|
||||
background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-hdpi.png');
|
||||
}
|
||||
|
||||
.step-control > .plus-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-plus-icon-hdpi.png');
|
||||
}
|
||||
|
||||
.step-control > .minus-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-minus-icon-hdpi.png');
|
||||
}
|
||||
|
||||
.toggle-button.on {
|
||||
background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-hdpi.png');
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-toggle-off-icon-hdpi.png');
|
||||
}
|
||||
|
||||
.share-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-share-icon-hdpi.png');
|
||||
}
|
||||
|
||||
.style-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-style-icon-hdpi.png');
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-resolution: 2dppx) {
|
||||
.dropdown-arrow {
|
||||
background-image: url('chrome://browser/skin/images/reader-dropdown-arrow-xhdpi.png');
|
||||
}
|
||||
|
||||
.step-control > .plus-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-plus-icon-xhdpi.png');
|
||||
}
|
||||
|
||||
.step-control > .minus-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-minus-icon-xhdpi.png');
|
||||
}
|
||||
|
||||
.toggle-button.on {
|
||||
background-image: url('chrome://browser/skin/images/reader-toggle-on-icon-xhdpi.png');
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-toggle-off-icon-xhdpi.png');
|
||||
}
|
||||
|
||||
.share-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-share-icon-xhdpi.png');
|
||||
}
|
||||
|
||||
.style-button {
|
||||
background-image: url('chrome://browser/skin/images/reader-style-icon-xhdpi.png');
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (orientation: portrait) {
|
||||
.button {
|
||||
height: 48px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (orientation: landscape) {
|
||||
.button {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 960px) {
|
||||
.button {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.toolbar > * {
|
||||
width: 56px;
|
||||
}
|
||||
}
|
||||
46
mobile/ios/Client/Frontend/Reader/Reader.html
Normal file
46
mobile/ios/Client/Frontend/Reader/Reader.html
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta content="text/html; charset=UTF-8" http-equiv="content-type">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=.25, maximum-scale=1.6, initial-scale=1.0">
|
||||
<meta name="referrer" content="never">
|
||||
<link rel="stylesheet" type="text/css" href="/reader-mode/styles/Reader.css">
|
||||
<title>%READER-TITLE%</title>
|
||||
</head>
|
||||
|
||||
<body data-readerStyle='%READER-STYLE%'>
|
||||
<div id="reader-header" class="header">
|
||||
<h1 id="reader-title">%READER-TITLE%</h1>
|
||||
<div id="reader-credits" class="credits">%READER-CREDITS%</div>
|
||||
</div>
|
||||
|
||||
<div id="reader-content" class="content">
|
||||
%READER-CONTENT%
|
||||
</div>
|
||||
|
||||
<div id="reader-message" class="message">
|
||||
%READER-MESSAGE%
|
||||
</div>
|
||||
|
||||
<ul id="reader-toolbar" class="toolbar toolbar-hidden">
|
||||
<li><a id="share-button" class="button share-button" href="#"></a></li>
|
||||
<ul class="dropdown">
|
||||
<li><a class="dropdown-toggle button style-button" href="#"></a></li>
|
||||
<li class="dropdown-popup">
|
||||
<ul id="font-type-buttons"></ul>
|
||||
<hr></hr>
|
||||
<ul id="font-size-buttons" class="segmented-button"></ul>
|
||||
<hr></hr>
|
||||
<ul id="color-scheme-buttons" class="segmented-button"></ul>
|
||||
</li>
|
||||
</ul>
|
||||
<li><a id="toggle-button" class="button toggle-button" href="#"></a></li>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
222
mobile/ios/Client/Frontend/Reader/ReaderMode.js
Normal file
222
mobile/ios/Client/Frontend/Reader/ReaderMode.js
Normal file
|
|
@ -0,0 +1,222 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
(function() {
|
||||
"use strict";
|
||||
|
||||
if (!window.__firefox__) {
|
||||
Object.defineProperty(window, '__firefox__', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: {}
|
||||
});
|
||||
}
|
||||
|
||||
var readabilityResult = null;
|
||||
var currentStyle = null;
|
||||
|
||||
var readerModeURL = /^http:\/\/localhost:\d+\/reader-mode\/page/;
|
||||
|
||||
var BLOCK_IMAGES_SELECTOR = ".content p > img:only-child, " +
|
||||
".content p > a:only-child > img:only-child, " +
|
||||
".content .wp-caption img, " +
|
||||
".content figure img";
|
||||
|
||||
function debug(s) {
|
||||
if (!window.__firefox__.reader.DEBUG) {
|
||||
return;
|
||||
}
|
||||
console.log(s);
|
||||
}
|
||||
|
||||
function checkReadability() {
|
||||
if (document.location.href.match(readerModeURL)) {
|
||||
debug({Type: "ReaderModeStateChange", Value: "Active"});
|
||||
webkit.messageHandlers.readerModeMessageHandler.postMessage({Type: "ReaderModeStateChange", Value: "Active"});
|
||||
return;
|
||||
}
|
||||
|
||||
if ((document.location.protocol === "http:" || document.location.protocol === "https:") && document.location.pathname !== "/") {
|
||||
// Short circuit in case we already ran Readability. This mostly happens when going
|
||||
// back/forward: the page will be cached and the result will still be there.
|
||||
if (readabilityResult && readabilityResult.content) {
|
||||
debug({Type: "ReaderModeStateChange", Value: "Available"});
|
||||
webkit.messageHandlers.readerModeMessageHandler.postMessage({Type: "ReaderModeStateChange", Value: "Available"});
|
||||
return;
|
||||
}
|
||||
|
||||
var uri = {
|
||||
spec: document.location.href,
|
||||
host: document.location.host,
|
||||
prePath: document.location.protocol + "//" + document.location.host, // TODO This is incomplete, needs username/password and port
|
||||
scheme: document.location.protocol.substr(0, document.location.protocol.indexOf(":")),
|
||||
pathBase: document.location.protocol + "//" + document.location.host + location.pathname.substr(0, location.pathname.lastIndexOf("/") + 1)
|
||||
}
|
||||
|
||||
// document.cloneNode() can cause the webview to break (bug 1128774).
|
||||
// Serialize and then parse the document instead.
|
||||
var docStr = new XMLSerializer().serializeToString(document);
|
||||
var doc = new DOMParser().parseFromString(docStr, "text/html");
|
||||
var readability = new Readability(uri, doc);
|
||||
readabilityResult = readability.parse();
|
||||
|
||||
debug({Type: "ReaderModeStateChange", Value: readabilityResult !== null ? "Available" : "Unavailable"});
|
||||
webkit.messageHandlers.readerModeMessageHandler.postMessage({Type: "ReaderModeStateChange", Value: readabilityResult !== null ? "Available" : "Unavailable"});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
debug({Type: "ReaderModeStateChange", Value: "Unavailable"});
|
||||
webkit.messageHandlers.readerModeMessageHandler.postMessage({Type: "ReaderModeStateChange", Value: "Unavailable"});
|
||||
}
|
||||
|
||||
// Readerize the document. Since we did the actual readerization already in checkReadability, we
|
||||
// can simply return the results we already have.
|
||||
function readerize() {
|
||||
return readabilityResult;
|
||||
}
|
||||
|
||||
// TODO The following code only makes sense in about:reader context. It may be a good idea to move
|
||||
// it out of this file and into for example a Reader.js.
|
||||
|
||||
function setStyle(style) {
|
||||
// Configure the theme (light, dark)
|
||||
if (currentStyle != null) {
|
||||
document.body.classList.remove(currentStyle.theme);
|
||||
}
|
||||
document.body.classList.add(style.theme);
|
||||
|
||||
// Configure the font size (1-5)
|
||||
if (currentStyle != null) {
|
||||
document.body.classList.remove("font-size" + currentStyle.fontSize);
|
||||
}
|
||||
document.body.classList.add("font-size" + style.fontSize);
|
||||
|
||||
// Configure the font type
|
||||
if (currentStyle != null) {
|
||||
document.body.classList.remove(currentStyle.fontType);
|
||||
}
|
||||
document.body.classList.add(style.fontType);
|
||||
|
||||
// Remember the style
|
||||
currentStyle = style;
|
||||
}
|
||||
|
||||
function updateImageMargins() {
|
||||
var contentElement = document.getElementById('reader-content');
|
||||
|
||||
var windowWidth = window.innerWidth;
|
||||
var contentWidth = contentElement.offsetWidth;
|
||||
var maxWidthStyle = windowWidth + "px !important";
|
||||
|
||||
var setImageMargins = function(img) {
|
||||
if (!img._originalWidth) {
|
||||
img._originalWidth = img.offsetWidth;
|
||||
}
|
||||
|
||||
var imgWidth = img._originalWidth;
|
||||
|
||||
// If the image is taking more than half of the screen, just make
|
||||
// it fill edge-to-edge.
|
||||
if (imgWidth < contentWidth && imgWidth > windowWidth * 0.55) {
|
||||
imgWidth = windowWidth;
|
||||
}
|
||||
|
||||
var sideMargin = Math.max((contentWidth - windowWidth) / 2, (contentWidth - imgWidth) / 2);
|
||||
|
||||
var imageStyle = sideMargin + "px !important";
|
||||
var widthStyle = imgWidth + "px !important";
|
||||
|
||||
var cssText = "max-width: " + maxWidthStyle + ";" +
|
||||
"width: " + widthStyle + ";" +
|
||||
"margin-left: " + imageStyle + ";" +
|
||||
"margin-right: " + imageStyle + ";";
|
||||
|
||||
img.style.cssText = cssText;
|
||||
}
|
||||
|
||||
var imgs = document.querySelectorAll(BLOCK_IMAGES_SELECTOR);
|
||||
for (var i = imgs.length; --i >= 0;) {
|
||||
var img = imgs[i];
|
||||
if (img.width > 0) {
|
||||
setImageMargins(img);
|
||||
} else {
|
||||
img.onload = function() {
|
||||
setImageMargins(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function showContent() {
|
||||
// Make the reader visible
|
||||
var messageElement = document.getElementById('reader-message');
|
||||
messageElement.style.display = "none";
|
||||
var headerElement = document.getElementById('reader-header');
|
||||
headerElement.style.display = "block"
|
||||
var contentElement = document.getElementById('reader-content');
|
||||
contentElement.style.display = "block";
|
||||
}
|
||||
|
||||
function configureReader() {
|
||||
// Configure the reader with the initial style that was injected in the page.
|
||||
var style = JSON.parse(document.body.getAttribute("data-readerStyle"));
|
||||
setStyle(style);
|
||||
|
||||
// The order here is important. Because updateImageMargins depends on contentElement.offsetWidth which
|
||||
// will not be set until contentElement is visible. If this leads to annoying content reflowing then we
|
||||
// need to look at an alternative way to do
|
||||
showContent();
|
||||
updateImageMargins();
|
||||
}
|
||||
|
||||
Object.defineProperty(window.__firefox__, 'reader', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: {
|
||||
// If this is http or https content, and not an index page, then try to run Readability. If anything
|
||||
// fails, the app will never be notified and we don't show the button. That is ok for now since there
|
||||
// is no error feedback possible anyway.
|
||||
DEBUG: false
|
||||
}
|
||||
});
|
||||
|
||||
Object.defineProperty(window.__firefox__.reader, 'checkReadability', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: checkReadability
|
||||
});
|
||||
|
||||
Object.defineProperty(window.__firefox__.reader, 'readerize', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: readerize
|
||||
});
|
||||
|
||||
Object.defineProperty(window.__firefox__.reader, 'setStyle', {
|
||||
enumerable: false,
|
||||
configurable: false,
|
||||
writable: false,
|
||||
value: setStyle
|
||||
});
|
||||
|
||||
window.addEventListener('load', function(event) {
|
||||
// If this is an about:reader page that we are loading, apply the initial style to the page.
|
||||
if (document.location.href.match(readerModeURL)) {
|
||||
configureReader();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
window.addEventListener('pageshow', function(event) {
|
||||
// If this is an about:reader page that we are showing, fire an event to the native code
|
||||
if (document.location.href.match(readerModeURL)) {
|
||||
webkit.messageHandlers.readerModeMessageHandler.postMessage({Type: "ReaderPageEvent", Value: "PageShow"});
|
||||
}
|
||||
});
|
||||
})();
|
||||
296
mobile/ios/Client/Frontend/Reader/ReaderMode.swift
Normal file
296
mobile/ios/Client/Frontend/Reader/ReaderMode.swift
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import Shared
|
||||
import WebKit
|
||||
import SwiftyJSON
|
||||
|
||||
let ReaderModeProfileKeyStyle = "readermode.style"
|
||||
|
||||
enum ReaderModeMessageType: String {
|
||||
case stateChange = "ReaderModeStateChange"
|
||||
case pageEvent = "ReaderPageEvent"
|
||||
}
|
||||
|
||||
enum ReaderPageEvent: String {
|
||||
case pageShow = "PageShow"
|
||||
}
|
||||
|
||||
enum ReaderModeState: String {
|
||||
case available = "Available"
|
||||
case unavailable = "Unavailable"
|
||||
case active = "Active"
|
||||
}
|
||||
|
||||
enum ReaderModeTheme: String {
|
||||
case light = "light"
|
||||
case dark = "dark"
|
||||
case sepia = "sepia"
|
||||
}
|
||||
|
||||
enum ReaderModeFontType: String {
|
||||
case serif = "serif"
|
||||
case sansSerif = "sans-serif"
|
||||
}
|
||||
|
||||
enum ReaderModeFontSize: Int {
|
||||
case size1 = 1
|
||||
case size2 = 2
|
||||
case size3 = 3
|
||||
case size4 = 4
|
||||
case size5 = 5
|
||||
case size6 = 6
|
||||
case size7 = 7
|
||||
case size8 = 8
|
||||
case size9 = 9
|
||||
case size10 = 10
|
||||
case size11 = 11
|
||||
case size12 = 12
|
||||
case size13 = 13
|
||||
|
||||
func isSmallest() -> Bool {
|
||||
return self == ReaderModeFontSize.size1
|
||||
}
|
||||
|
||||
func smaller() -> ReaderModeFontSize {
|
||||
if isSmallest() {
|
||||
return self
|
||||
} else {
|
||||
return ReaderModeFontSize(rawValue: self.rawValue - 1)!
|
||||
}
|
||||
}
|
||||
|
||||
func isLargest() -> Bool {
|
||||
return self == ReaderModeFontSize.size13
|
||||
}
|
||||
|
||||
static var defaultSize: ReaderModeFontSize {
|
||||
switch UIApplication.shared.preferredContentSizeCategory {
|
||||
case UIContentSizeCategory.extraSmall:
|
||||
return .size1
|
||||
case UIContentSizeCategory.small:
|
||||
return .size2
|
||||
case UIContentSizeCategory.medium:
|
||||
return .size3
|
||||
case UIContentSizeCategory.large:
|
||||
return .size5
|
||||
case UIContentSizeCategory.extraLarge:
|
||||
return .size7
|
||||
case UIContentSizeCategory.extraExtraLarge:
|
||||
return .size9
|
||||
case UIContentSizeCategory.extraExtraExtraLarge:
|
||||
return .size12
|
||||
default:
|
||||
return .size5
|
||||
}
|
||||
}
|
||||
|
||||
func bigger() -> ReaderModeFontSize {
|
||||
if isLargest() {
|
||||
return self
|
||||
} else {
|
||||
return ReaderModeFontSize(rawValue: self.rawValue + 1)!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ReaderModeStyle {
|
||||
var theme: ReaderModeTheme
|
||||
var fontType: ReaderModeFontType
|
||||
var fontSize: ReaderModeFontSize
|
||||
|
||||
/// Encode the style to a JSON dictionary that can be passed to ReaderMode.js
|
||||
func encode() -> String {
|
||||
return JSON(["theme": theme.rawValue, "fontType": fontType.rawValue, "fontSize": fontSize.rawValue]).stringValue() ?? ""
|
||||
}
|
||||
|
||||
/// Encode the style to a dictionary that can be stored in the profile
|
||||
func encodeAsDictionary() -> [String: Any] {
|
||||
return ["theme": theme.rawValue, "fontType": fontType.rawValue, "fontSize": fontSize.rawValue]
|
||||
}
|
||||
|
||||
init(theme: ReaderModeTheme, fontType: ReaderModeFontType, fontSize: ReaderModeFontSize) {
|
||||
self.theme = theme
|
||||
self.fontType = fontType
|
||||
self.fontSize = fontSize
|
||||
}
|
||||
|
||||
/// Initialize the style from a dictionary, taken from the profile. Returns nil if the object cannot be decoded.
|
||||
init?(dict: [String: Any]) {
|
||||
let themeRawValue = dict["theme"] as? String
|
||||
let fontTypeRawValue = dict["fontType"] as? String
|
||||
let fontSizeRawValue = dict["fontSize"] as? Int
|
||||
if themeRawValue == nil || fontTypeRawValue == nil || fontSizeRawValue == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
let theme = ReaderModeTheme(rawValue: themeRawValue!)
|
||||
let fontType = ReaderModeFontType(rawValue: fontTypeRawValue!)
|
||||
let fontSize = ReaderModeFontSize(rawValue: fontSizeRawValue!)
|
||||
if theme == nil || fontType == nil || fontSize == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.theme = theme!
|
||||
self.fontType = fontType!
|
||||
self.fontSize = fontSize!
|
||||
}
|
||||
}
|
||||
|
||||
let DefaultReaderModeStyle = ReaderModeStyle(theme: .light, fontType: .sansSerif, fontSize: ReaderModeFontSize.defaultSize)
|
||||
|
||||
/// This struct captures the response from the Readability.js code.
|
||||
struct ReadabilityResult {
|
||||
var domain = ""
|
||||
var url = ""
|
||||
var content = ""
|
||||
var title = ""
|
||||
var credits = ""
|
||||
|
||||
init?(object: AnyObject?) {
|
||||
if let dict = object as? NSDictionary {
|
||||
if let uri = dict["uri"] as? NSDictionary {
|
||||
if let url = uri["spec"] as? String {
|
||||
self.url = url
|
||||
}
|
||||
if let host = uri["host"] as? String {
|
||||
self.domain = host
|
||||
}
|
||||
}
|
||||
if let content = dict["content"] as? String {
|
||||
self.content = content
|
||||
}
|
||||
if let title = dict["title"] as? String {
|
||||
self.title = title
|
||||
}
|
||||
if let credits = dict["byline"] as? String {
|
||||
self.credits = credits
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize from a JSON encoded string
|
||||
init?(string: String) {
|
||||
let object = JSON(parseJSON: string)
|
||||
let domain = object["domain"].string
|
||||
let url = object["url"].string
|
||||
let content = object["content"].string
|
||||
let title = object["title"].string
|
||||
let credits = object["credits"].string
|
||||
|
||||
if domain == nil || url == nil || content == nil || title == nil || credits == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.domain = domain!
|
||||
self.url = url!
|
||||
self.content = content!
|
||||
self.title = title!
|
||||
self.credits = credits!
|
||||
}
|
||||
|
||||
/// Encode to a dictionary, which can then for example be json encoded
|
||||
func encode() -> [String: Any] {
|
||||
return ["domain": domain, "url": url, "content": content, "title": title, "credits": credits]
|
||||
}
|
||||
|
||||
/// Encode to a JSON encoded string
|
||||
func encode() -> String {
|
||||
let dict: [String: Any] = self.encode()
|
||||
return JSON(object: dict).stringValue()!
|
||||
}
|
||||
}
|
||||
|
||||
/// Delegate that contains callbacks that we have added on top of the built-in WKWebViewDelegate
|
||||
protocol ReaderModeDelegate {
|
||||
func readerMode(_ readerMode: ReaderMode, didChangeReaderModeState state: ReaderModeState, forTab tab: Tab)
|
||||
func readerMode(_ readerMode: ReaderMode, didDisplayReaderizedContentForTab tab: Tab)
|
||||
}
|
||||
|
||||
let ReaderModeNamespace = "window.__firefox__.reader"
|
||||
|
||||
class ReaderMode: TabContentScript {
|
||||
var delegate: ReaderModeDelegate?
|
||||
|
||||
fileprivate weak var tab: Tab?
|
||||
var state: ReaderModeState = ReaderModeState.unavailable
|
||||
fileprivate var originalURL: URL?
|
||||
|
||||
class func name() -> String {
|
||||
return "ReaderMode"
|
||||
}
|
||||
|
||||
required init(tab: Tab) {
|
||||
self.tab = tab
|
||||
|
||||
// This is a WKUserScript at the moment because webView.evaluateJavaScript() fails with an unspecified error. Possibly script size related.
|
||||
if let path = Bundle.main.path(forResource: "Readability", ofType: "js") {
|
||||
if let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String {
|
||||
let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true)
|
||||
tab.webView!.configuration.userContentController.addUserScript(userScript)
|
||||
}
|
||||
}
|
||||
|
||||
// This is executed after a page has been loaded. It executes Readability and then fires a script message to let us know if the page is compatible with reader mode.
|
||||
if let path = Bundle.main.path(forResource: "ReaderMode", ofType: "js") {
|
||||
if let source = try? NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String {
|
||||
let userScript = WKUserScript(source: source, injectionTime: WKUserScriptInjectionTime.atDocumentEnd, forMainFrameOnly: true)
|
||||
tab.webView!.configuration.userContentController.addUserScript(userScript)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func scriptMessageHandlerName() -> String? {
|
||||
return "readerModeMessageHandler"
|
||||
}
|
||||
|
||||
fileprivate func handleReaderPageEvent(_ readerPageEvent: ReaderPageEvent) {
|
||||
switch readerPageEvent {
|
||||
case .pageShow:
|
||||
if let tab = tab {
|
||||
delegate?.readerMode(self, didDisplayReaderizedContentForTab: tab)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func handleReaderModeStateChange(_ state: ReaderModeState) {
|
||||
self.state = state
|
||||
guard let tab = tab else {
|
||||
return
|
||||
}
|
||||
delegate?.readerMode(self, didChangeReaderModeState: state, forTab: tab)
|
||||
}
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
|
||||
if let msg = message.body as? Dictionary<String, String> {
|
||||
if let messageType = ReaderModeMessageType(rawValue: msg["Type"] ?? "") {
|
||||
switch messageType {
|
||||
case .pageEvent:
|
||||
if let readerPageEvent = ReaderPageEvent(rawValue: msg["Value"] ?? "Invalid") {
|
||||
handleReaderPageEvent(readerPageEvent)
|
||||
}
|
||||
break
|
||||
case .stateChange:
|
||||
if let readerModeState = ReaderModeState(rawValue: msg["Value"] ?? "Invalid") {
|
||||
handleReaderModeStateChange(readerModeState)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var style: ReaderModeStyle = DefaultReaderModeStyle {
|
||||
didSet {
|
||||
if state == ReaderModeState.active {
|
||||
tab?.webView?.evaluateJavaScript("\(ReaderModeNamespace).setStyle(\(style.encode()))", completionHandler: { (object, error) -> Void in
|
||||
return
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
142
mobile/ios/Client/Frontend/Reader/ReaderModeCache.swift
Normal file
142
mobile/ios/Client/Frontend/Reader/ReaderModeCache.swift
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
private let DiskReaderModeCacheSharedInstance = DiskReaderModeCache()
|
||||
private let MemoryReaderModeCacheSharedInstance = MemoryReaderModeCache()
|
||||
|
||||
let ReaderModeCacheErrorDomain = "com.mozilla.client.readermodecache."
|
||||
enum ReaderModeCacheErrorCode: Int {
|
||||
case noPathsFound = 0
|
||||
}
|
||||
|
||||
// NSObject wrapper around ReadabilityResult Swift struct for adding into the NSCache
|
||||
private class ReadabilityResultWrapper: NSObject {
|
||||
let result: ReadabilityResult
|
||||
|
||||
init(readabilityResult: ReadabilityResult) {
|
||||
self.result = readabilityResult
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
protocol ReaderModeCache {
|
||||
func put(_ url: URL, _ readabilityResult: ReadabilityResult) throws
|
||||
|
||||
func get(_ url: URL) throws -> ReadabilityResult
|
||||
|
||||
func delete(_ url: URL, error: NSErrorPointer)
|
||||
|
||||
func contains(_ url: URL) -> Bool
|
||||
}
|
||||
|
||||
/// A non-persistent cache for readerized content for times when you don't want to write reader data to disk.
|
||||
/// For example, when the user is in a private tab, we want to make sure that we leave no trace on the file system
|
||||
class MemoryReaderModeCache: ReaderModeCache {
|
||||
var cache: NSCache<AnyObject, AnyObject>
|
||||
|
||||
init(cache: NSCache<AnyObject, AnyObject> = NSCache()) {
|
||||
self.cache = cache
|
||||
}
|
||||
|
||||
class var sharedInstance: ReaderModeCache {
|
||||
return MemoryReaderModeCacheSharedInstance
|
||||
}
|
||||
|
||||
func put(_ url: URL, _ readabilityResult: ReadabilityResult) throws {
|
||||
cache.setObject(ReadabilityResultWrapper(readabilityResult: readabilityResult), forKey: url as AnyObject)
|
||||
}
|
||||
|
||||
func get(_ url: URL) throws -> ReadabilityResult {
|
||||
guard let resultWrapper = cache.object(forKey: url as AnyObject) as? ReadabilityResultWrapper else {
|
||||
throw NSError(domain: ReaderModeCacheErrorDomain, code: ReaderModeCacheErrorCode.noPathsFound.rawValue, userInfo: nil)
|
||||
}
|
||||
return resultWrapper.result
|
||||
}
|
||||
|
||||
func delete(_ url: URL, error: NSErrorPointer) {
|
||||
cache.removeObject(forKey: url as AnyObject)
|
||||
}
|
||||
|
||||
func contains(_ url: URL) -> Bool {
|
||||
return cache.object(forKey: url as AnyObject) != nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Really basic persistent cache to store readerized content. Has a simple hashed structure
|
||||
/// to avoid storing many items in the same directory.
|
||||
///
|
||||
/// This currently lives in ~/Library/Caches so that the data can be pruned in case the OS needs
|
||||
/// more space. Whether that is a good idea or not is not sure. We have a bug on file to investigate
|
||||
/// and improve at a later time.
|
||||
class DiskReaderModeCache: ReaderModeCache {
|
||||
class var sharedInstance: ReaderModeCache {
|
||||
return DiskReaderModeCacheSharedInstance
|
||||
}
|
||||
|
||||
func put(_ url: URL, _ readabilityResult: ReadabilityResult) throws {
|
||||
guard let (cacheDirectoryPath, contentFilePath) = cachePathsForURL(url) else {
|
||||
throw NSError(domain: ReaderModeCacheErrorDomain, code: ReaderModeCacheErrorCode.noPathsFound.rawValue, userInfo: nil)
|
||||
}
|
||||
|
||||
try FileManager.default.createDirectory(atPath: cacheDirectoryPath, withIntermediateDirectories: true, attributes: nil)
|
||||
let string: String = readabilityResult.encode()
|
||||
try string.write(toFile: contentFilePath, atomically: true, encoding: String.Encoding(rawValue: String.Encoding.utf8.rawValue))
|
||||
return
|
||||
}
|
||||
|
||||
func get(_ url: URL) throws -> ReadabilityResult {
|
||||
if let (_, contentFilePath) = cachePathsForURL(url), FileManager.default.fileExists(atPath: contentFilePath) {
|
||||
let string = try NSString(contentsOfFile: contentFilePath, encoding: String.Encoding.utf8.rawValue)
|
||||
if let value = ReadabilityResult(string: string as String) {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
throw NSError(domain: ReaderModeCacheErrorDomain, code: ReaderModeCacheErrorCode.noPathsFound.rawValue, userInfo: nil)
|
||||
}
|
||||
|
||||
func delete(_ url: URL, error: NSErrorPointer) {
|
||||
guard let (cacheDirectoryPath, _) = cachePathsForURL(url) else { return }
|
||||
|
||||
if FileManager.default.fileExists(atPath: cacheDirectoryPath) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: cacheDirectoryPath)
|
||||
} catch let error1 as NSError {
|
||||
error?.pointee = error1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func contains(_ url: URL) -> Bool {
|
||||
if let (_, contentFilePath) = cachePathsForURL(url), FileManager.default.fileExists(atPath: contentFilePath) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fileprivate func cachePathsForURL(_ url: URL) -> (cacheDirectoryPath: String, contentFilePath: String)? {
|
||||
let paths = NSSearchPathForDirectoriesInDomains(FileManager.SearchPathDirectory.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true)
|
||||
if !paths.isEmpty, let hashedPath = hashedPathForURL(url) {
|
||||
let cacheDirectoryURL = URL(fileURLWithPath: NSString.path(withComponents: [paths[0], "ReaderView", hashedPath]))
|
||||
return (cacheDirectoryURL.path, cacheDirectoryURL.appendingPathComponent("content.json").path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
fileprivate func hashedPathForURL(_ url: URL) -> String? {
|
||||
guard let hash = hashForURL(url) else { return nil }
|
||||
|
||||
return NSString.path(withComponents: [hash.substring(with: NSRange(location: 0, length: 2)), hash.substring(with: NSRange(location: 2, length: 2)), hash.substring(from: 4)]) as String
|
||||
}
|
||||
|
||||
fileprivate func hashForURL(_ url: URL) -> NSString? {
|
||||
guard let data = url.absoluteString.data(using: String.Encoding.utf8) else { return nil }
|
||||
|
||||
return data.sha1.hexEncodedString as NSString?
|
||||
}
|
||||
}
|
||||
81
mobile/ios/Client/Frontend/Reader/ReaderModeHandlers.swift
Normal file
81
mobile/ios/Client/Frontend/Reader/ReaderModeHandlers.swift
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import GCDWebServers
|
||||
|
||||
struct ReaderModeHandlers {
|
||||
static var readerModeCache: ReaderModeCache = DiskReaderModeCache.sharedInstance
|
||||
|
||||
static func register(_ webServer: WebServer, profile: Profile) {
|
||||
// Register our fonts and css, which we want to expose to web content that we present in the WebView
|
||||
webServer.registerMainBundleResourcesOfType("ttf", module: "reader-mode/fonts")
|
||||
webServer.registerMainBundleResource("Reader.css", module: "reader-mode/styles")
|
||||
|
||||
// Register a handler that simply lets us know if a document is in the cache or not. This is called from the
|
||||
// reader view interstitial page to find out when it can stop showing the 'Loading...' page and instead load
|
||||
// the readerized content.
|
||||
webServer.registerHandlerForMethod("GET", module: "reader-mode", resource: "page-exists") { (request: GCDWebServerRequest?) -> GCDWebServerResponse! in
|
||||
guard let stringURL = request?.query["url"] as? String,
|
||||
let url = URL(string: stringURL) else {
|
||||
return GCDWebServerResponse(statusCode: 500)
|
||||
}
|
||||
|
||||
let status = readerModeCache.contains(url) ? 200 : 404
|
||||
return GCDWebServerResponse(statusCode: status)
|
||||
}
|
||||
|
||||
// Register the handler that accepts /reader-mode/page?url=http://www.example.com requests.
|
||||
webServer.registerHandlerForMethod("GET", module: "reader-mode", resource: "page") { (request: GCDWebServerRequest?) -> GCDWebServerResponse! in
|
||||
if let url = request?.query["url"] as? String {
|
||||
if let url = URL(string: url), url.isWebPage() {
|
||||
do {
|
||||
let readabilityResult = try readerModeCache.get(url)
|
||||
// We have this page in our cache, so we can display it. Just grab the correct style from the
|
||||
// profile and then generate HTML from the Readability results.
|
||||
var readerModeStyle = DefaultReaderModeStyle
|
||||
if let dict = profile.prefs.dictionaryForKey(ReaderModeProfileKeyStyle) {
|
||||
if let style = ReaderModeStyle(dict: dict) {
|
||||
readerModeStyle = style
|
||||
}
|
||||
}
|
||||
if let html = ReaderModeUtils.generateReaderContent(readabilityResult, initialStyle: readerModeStyle),
|
||||
let response = GCDWebServerDataResponse(html: html) {
|
||||
// Apply a Content Security Policy that disallows everything except images from anywhere and fonts and css from our internal server
|
||||
response.setValue("default-src 'none'; img-src *; style-src http://localhost:*; font-src http://localhost:*", forAdditionalHeader: "Content-Security-Policy")
|
||||
return response
|
||||
}
|
||||
} catch _ {
|
||||
// This page has not been converted to reader mode yet. This happens when you for example add an
|
||||
// item via the app extension and the application has not yet had a change to readerize that
|
||||
// page in the background.
|
||||
//
|
||||
// What we do is simply queue the page in the ReadabilityService and then show our loading
|
||||
// screen, which will periodically call page-exists to see if the readerized content has
|
||||
// become available.
|
||||
ReadabilityService.sharedInstance.process(url, cache: readerModeCache)
|
||||
if let readerViewLoadingPath = Bundle.main.path(forResource: "ReaderViewLoading", ofType: "html") {
|
||||
do {
|
||||
let readerViewLoading = try NSMutableString(contentsOfFile: readerViewLoadingPath, encoding: String.Encoding.utf8.rawValue)
|
||||
readerViewLoading.replaceOccurrences(of: "%ORIGINAL-URL%", with: url.absoluteString,
|
||||
options: NSString.CompareOptions.literal, range: NSRange(location: 0, length: readerViewLoading.length))
|
||||
readerViewLoading.replaceOccurrences(of: "%LOADING-TEXT%", with: NSLocalizedString("Loading content…", comment: "Message displayed when the reader mode page is loading. This message will appear only when sharing to Firefox reader mode from another app."),
|
||||
options: NSString.CompareOptions.literal, range: NSRange(location: 0, length: readerViewLoading.length))
|
||||
readerViewLoading.replaceOccurrences(of: "%LOADING-FAILED-TEXT%", with: NSLocalizedString("The page could not be displayed in Reader View.", comment: "Message displayed when the reader mode page could not be loaded. This message will appear only when sharing to Firefox reader mode from another app."),
|
||||
options: NSString.CompareOptions.literal, range: NSRange(location: 0, length: readerViewLoading.length))
|
||||
readerViewLoading.replaceOccurrences(of: "%LOAD-ORIGINAL-TEXT%", with: NSLocalizedString("Load original page", comment: "Link for going to the non-reader page when the reader view could not be loaded. This message will appear only when sharing to Firefox reader mode from another app."),
|
||||
options: NSString.CompareOptions.literal, range: NSRange(location: 0, length: readerViewLoading.length))
|
||||
return GCDWebServerDataResponse(html: readerViewLoading as String)
|
||||
} catch _ {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let errorString = NSLocalizedString("There was an error converting the page", comment: "Error displayed when reader mode cannot be enabled")
|
||||
return GCDWebServerDataResponse(html: errorString) // TODO Needs a proper error page
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,379 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Shared
|
||||
|
||||
private struct ReaderModeStyleViewControllerUX {
|
||||
static let RowHeight = 50
|
||||
|
||||
static let Width = 270
|
||||
static let Height = 4 * RowHeight
|
||||
|
||||
static let FontTypeRowBackground = UIColor(rgb: 0xfbfbfb)
|
||||
|
||||
static let FontTypeTitleSelectedColor = UIColor(rgb: 0x333333)
|
||||
static let FontTypeTitleNormalColor = UIColor.lightGray // TODO THis needs to be 44% of 0x333333
|
||||
|
||||
static let FontSizeRowBackground = UIColor(rgb: 0xf4f4f4)
|
||||
static let FontSizeLabelColor = UIColor(rgb: 0x333333)
|
||||
static let FontSizeButtonTextColorEnabled = UIColor(rgb: 0x333333)
|
||||
static let FontSizeButtonTextColorDisabled = UIColor.lightGray // TODO THis needs to be 44% of 0x333333
|
||||
|
||||
static let ThemeRowBackgroundColor = UIColor.white
|
||||
static let ThemeTitleColorLight = UIColor(rgb: 0x333333)
|
||||
static let ThemeTitleColorDark = UIColor.white
|
||||
static let ThemeTitleColorSepia = UIColor(rgb: 0x333333)
|
||||
static let ThemeBackgroundColorLight = UIColor.white
|
||||
static let ThemeBackgroundColorDark = UIColor(rgb: 0x333333)
|
||||
static let ThemeBackgroundColorSepia = UIColor(rgb: 0xF0E6DC)
|
||||
|
||||
static let BrightnessRowBackground = UIColor(rgb: 0xf4f4f4)
|
||||
static let BrightnessSliderTintColor = UIColor(rgb: 0xe66000)
|
||||
static let BrightnessSliderWidth = 140
|
||||
static let BrightnessIconOffset = 10
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
protocol ReaderModeStyleViewControllerDelegate {
|
||||
func readerModeStyleViewController(_ readerModeStyleViewController: ReaderModeStyleViewController, didConfigureStyle style: ReaderModeStyle)
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
class ReaderModeStyleViewController: UIViewController {
|
||||
var delegate: ReaderModeStyleViewControllerDelegate?
|
||||
var readerModeStyle: ReaderModeStyle = DefaultReaderModeStyle
|
||||
|
||||
fileprivate var fontTypeButtons: [FontTypeButton]!
|
||||
fileprivate var fontSizeLabel: FontSizeLabel!
|
||||
fileprivate var fontSizeButtons: [FontSizeButton]!
|
||||
fileprivate var themeButtons: [ThemeButton]!
|
||||
|
||||
override func viewDidLoad() {
|
||||
// Our preferred content size has a fixed width and height based on the rows + padding
|
||||
|
||||
preferredContentSize = CGSize(width: ReaderModeStyleViewControllerUX.Width, height: ReaderModeStyleViewControllerUX.Height)
|
||||
|
||||
popoverPresentationController?.backgroundColor = ReaderModeStyleViewControllerUX.FontTypeRowBackground
|
||||
|
||||
// Font type row
|
||||
|
||||
let fontTypeRow = UIView()
|
||||
view.addSubview(fontTypeRow)
|
||||
fontTypeRow.backgroundColor = ReaderModeStyleViewControllerUX.FontTypeRowBackground
|
||||
|
||||
fontTypeRow.snp.makeConstraints { (make) -> Void in
|
||||
make.top.equalTo(self.view)
|
||||
make.left.right.equalTo(self.view)
|
||||
make.height.equalTo(ReaderModeStyleViewControllerUX.RowHeight)
|
||||
}
|
||||
|
||||
fontTypeButtons = [
|
||||
FontTypeButton(fontType: ReaderModeFontType.sansSerif),
|
||||
FontTypeButton(fontType: ReaderModeFontType.serif)
|
||||
]
|
||||
|
||||
setupButtons(fontTypeButtons, inRow: fontTypeRow, action: #selector(ReaderModeStyleViewController.SELchangeFontType(_:)))
|
||||
|
||||
// Font size row
|
||||
|
||||
let fontSizeRow = UIView()
|
||||
view.addSubview(fontSizeRow)
|
||||
fontSizeRow.backgroundColor = ReaderModeStyleViewControllerUX.FontSizeRowBackground
|
||||
|
||||
fontSizeRow.snp.makeConstraints { (make) -> Void in
|
||||
make.top.equalTo(fontTypeRow.snp.bottom)
|
||||
make.left.right.equalTo(self.view)
|
||||
make.height.equalTo(ReaderModeStyleViewControllerUX.RowHeight)
|
||||
}
|
||||
|
||||
fontSizeLabel = FontSizeLabel()
|
||||
fontSizeRow.addSubview(fontSizeLabel)
|
||||
|
||||
fontSizeLabel.snp.makeConstraints { (make) -> Void in
|
||||
make.center.equalTo(fontSizeRow)
|
||||
return
|
||||
}
|
||||
|
||||
fontSizeButtons = [
|
||||
FontSizeButton(fontSizeAction: FontSizeAction.smaller),
|
||||
FontSizeButton(fontSizeAction: FontSizeAction.reset),
|
||||
FontSizeButton(fontSizeAction: FontSizeAction.bigger)
|
||||
]
|
||||
|
||||
setupButtons(fontSizeButtons, inRow: fontSizeRow, action: #selector(ReaderModeStyleViewController.SELchangeFontSize(_:)))
|
||||
|
||||
// Theme row
|
||||
|
||||
let themeRow = UIView()
|
||||
view.addSubview(themeRow)
|
||||
|
||||
themeRow.snp.makeConstraints { (make) -> Void in
|
||||
make.top.equalTo(fontSizeRow.snp.bottom)
|
||||
make.left.right.equalTo(self.view)
|
||||
make.height.equalTo(ReaderModeStyleViewControllerUX.RowHeight)
|
||||
}
|
||||
|
||||
themeButtons = [
|
||||
ThemeButton(theme: ReaderModeTheme.light),
|
||||
ThemeButton(theme: ReaderModeTheme.dark),
|
||||
ThemeButton(theme: ReaderModeTheme.sepia)
|
||||
]
|
||||
|
||||
setupButtons(themeButtons, inRow: themeRow, action: #selector(ReaderModeStyleViewController.SELchangeTheme(_:)))
|
||||
|
||||
// Brightness row
|
||||
|
||||
let brightnessRow = UIView()
|
||||
view.addSubview(brightnessRow)
|
||||
brightnessRow.backgroundColor = ReaderModeStyleViewControllerUX.BrightnessRowBackground
|
||||
|
||||
brightnessRow.snp.makeConstraints { (make) -> Void in
|
||||
make.top.equalTo(themeRow.snp.bottom)
|
||||
make.left.right.equalTo(self.view)
|
||||
make.height.equalTo(ReaderModeStyleViewControllerUX.RowHeight)
|
||||
}
|
||||
|
||||
let slider = UISlider()
|
||||
brightnessRow.addSubview(slider)
|
||||
slider.accessibilityLabel = NSLocalizedString("Brightness", comment: "Accessibility label for brightness adjustment slider in Reader Mode display settings")
|
||||
slider.tintColor = ReaderModeStyleViewControllerUX.BrightnessSliderTintColor
|
||||
slider.addTarget(self, action: #selector(ReaderModeStyleViewController.SELchangeBrightness(_:)), for: UIControlEvents.valueChanged)
|
||||
|
||||
slider.snp.makeConstraints { make in
|
||||
make.center.equalTo(brightnessRow)
|
||||
make.width.equalTo(ReaderModeStyleViewControllerUX.BrightnessSliderWidth)
|
||||
}
|
||||
|
||||
let brightnessMinImageView = UIImageView(image: UIImage(named: "brightnessMin"))
|
||||
brightnessRow.addSubview(brightnessMinImageView)
|
||||
|
||||
brightnessMinImageView.snp.makeConstraints { (make) -> Void in
|
||||
make.centerY.equalTo(slider)
|
||||
make.right.equalTo(slider.snp.left).offset(-ReaderModeStyleViewControllerUX.BrightnessIconOffset)
|
||||
}
|
||||
|
||||
let brightnessMaxImageView = UIImageView(image: UIImage(named: "brightnessMax"))
|
||||
brightnessRow.addSubview(brightnessMaxImageView)
|
||||
|
||||
brightnessMaxImageView.snp.makeConstraints { (make) -> Void in
|
||||
make.centerY.equalTo(slider)
|
||||
make.left.equalTo(slider.snp.right).offset(ReaderModeStyleViewControllerUX.BrightnessIconOffset)
|
||||
}
|
||||
|
||||
selectFontType(readerModeStyle.fontType)
|
||||
updateFontSizeButtons()
|
||||
selectTheme(readerModeStyle.theme)
|
||||
slider.value = Float(UIScreen.main.brightness)
|
||||
}
|
||||
|
||||
/// Setup constraints for a row of buttons. Left to right. They are all given the same width.
|
||||
fileprivate func setupButtons(_ buttons: [UIButton], inRow row: UIView, action: Selector) {
|
||||
for (idx, button) in buttons.enumerated() {
|
||||
row.addSubview(button)
|
||||
button.addTarget(self, action: action, for: UIControlEvents.touchUpInside)
|
||||
button.snp.makeConstraints { make in
|
||||
make.top.equalTo(row.snp.top)
|
||||
if idx == 0 {
|
||||
make.left.equalTo(row.snp.left)
|
||||
} else {
|
||||
make.left.equalTo(buttons[idx - 1].snp.right)
|
||||
}
|
||||
make.bottom.equalTo(row.snp.bottom)
|
||||
make.width.equalTo(self.preferredContentSize.width / CGFloat(buttons.count))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SELchangeFontType(_ button: FontTypeButton) {
|
||||
selectFontType(button.fontType)
|
||||
delegate?.readerModeStyleViewController(self, didConfigureStyle: readerModeStyle)
|
||||
}
|
||||
|
||||
fileprivate func selectFontType(_ fontType: ReaderModeFontType) {
|
||||
readerModeStyle.fontType = fontType
|
||||
for button in fontTypeButtons {
|
||||
button.isSelected = (button.fontType == fontType)
|
||||
}
|
||||
for button in themeButtons {
|
||||
button.fontType = fontType
|
||||
}
|
||||
fontSizeLabel.fontType = fontType
|
||||
}
|
||||
|
||||
func SELchangeFontSize(_ button: FontSizeButton) {
|
||||
switch button.fontSizeAction {
|
||||
case .smaller:
|
||||
readerModeStyle.fontSize = readerModeStyle.fontSize.smaller()
|
||||
case .bigger:
|
||||
readerModeStyle.fontSize = readerModeStyle.fontSize.bigger()
|
||||
case .reset:
|
||||
readerModeStyle.fontSize = ReaderModeFontSize.defaultSize
|
||||
}
|
||||
updateFontSizeButtons()
|
||||
delegate?.readerModeStyleViewController(self, didConfigureStyle: readerModeStyle)
|
||||
}
|
||||
|
||||
fileprivate func updateFontSizeButtons() {
|
||||
for button in fontSizeButtons {
|
||||
switch button.fontSizeAction {
|
||||
case .bigger:
|
||||
button.isEnabled = !readerModeStyle.fontSize.isLargest()
|
||||
break
|
||||
case .smaller:
|
||||
button.isEnabled = !readerModeStyle.fontSize.isSmallest()
|
||||
break
|
||||
case .reset:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func SELchangeTheme(_ button: ThemeButton) {
|
||||
selectTheme(button.theme)
|
||||
delegate?.readerModeStyleViewController(self, didConfigureStyle: readerModeStyle)
|
||||
}
|
||||
|
||||
fileprivate func selectTheme(_ theme: ReaderModeTheme) {
|
||||
readerModeStyle.theme = theme
|
||||
}
|
||||
|
||||
func SELchangeBrightness(_ slider: UISlider) {
|
||||
UIScreen.main.brightness = CGFloat(slider.value)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
class FontTypeButton: UIButton {
|
||||
var fontType: ReaderModeFontType = .sansSerif
|
||||
|
||||
convenience init(fontType: ReaderModeFontType) {
|
||||
self.init(frame: CGRect.zero)
|
||||
self.fontType = fontType
|
||||
setTitleColor(ReaderModeStyleViewControllerUX.FontTypeTitleSelectedColor, for: UIControlState.selected)
|
||||
setTitleColor(ReaderModeStyleViewControllerUX.FontTypeTitleNormalColor, for: UIControlState())
|
||||
backgroundColor = ReaderModeStyleViewControllerUX.FontTypeRowBackground
|
||||
accessibilityHint = NSLocalizedString("Changes font type.", comment: "Accessibility hint for the font type buttons in reader mode display settings")
|
||||
switch fontType {
|
||||
case .sansSerif:
|
||||
setTitle(NSLocalizedString("Sans-serif", comment: "Font type setting in the reading view settings"), for: UIControlState())
|
||||
let f = UIFont(name: "FiraSans-Book", size: DynamicFontHelper.defaultHelper.ReaderStandardFontSize)
|
||||
titleLabel?.font = f
|
||||
case .serif:
|
||||
setTitle(NSLocalizedString("Serif", comment: "Font type setting in the reading view settings"), for: UIControlState())
|
||||
let f = UIFont(name: "Charis SIL", size: DynamicFontHelper.defaultHelper.ReaderStandardFontSize)
|
||||
titleLabel?.font = f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
enum FontSizeAction {
|
||||
case smaller
|
||||
case reset
|
||||
case bigger
|
||||
}
|
||||
|
||||
class FontSizeButton: UIButton {
|
||||
var fontSizeAction: FontSizeAction = .bigger
|
||||
|
||||
convenience init(fontSizeAction: FontSizeAction) {
|
||||
self.init(frame: CGRect.zero)
|
||||
self.fontSizeAction = fontSizeAction
|
||||
|
||||
setTitleColor(ReaderModeStyleViewControllerUX.FontSizeButtonTextColorEnabled, for: UIControlState.normal)
|
||||
setTitleColor(ReaderModeStyleViewControllerUX.FontSizeButtonTextColorDisabled, for: UIControlState.disabled)
|
||||
|
||||
switch fontSizeAction {
|
||||
case .smaller:
|
||||
let smallerFontLabel = NSLocalizedString("-", comment: "Button for smaller reader font size. Keep this extremely short! This is shown in the reader mode toolbar.")
|
||||
let smallerFontAccessibilityLabel = NSLocalizedString("Decrease text size", comment: "Accessibility label for button decreasing font size in display settings of reader mode")
|
||||
setTitle(smallerFontLabel, for: UIControlState())
|
||||
accessibilityLabel = smallerFontAccessibilityLabel
|
||||
case .bigger:
|
||||
let largerFontLabel = NSLocalizedString("+", comment: "Button for larger reader font size. Keep this extremely short! This is shown in the reader mode toolbar.")
|
||||
let largerFontAccessibilityLabel = NSLocalizedString("Increase text size", comment: "Accessibility label for button increasing font size in display settings of reader mode")
|
||||
setTitle(largerFontLabel, for: UIControlState())
|
||||
accessibilityLabel = largerFontAccessibilityLabel
|
||||
case .reset:
|
||||
accessibilityLabel = Strings.ReaderModeResetFontSizeAccessibilityLabel
|
||||
}
|
||||
|
||||
// TODO Does this need to change with the selected font type? Not sure if makes sense for just +/-
|
||||
titleLabel?.font = UIFont(name: "FiraSans-Light", size: DynamicFontHelper.defaultHelper.ReaderBigFontSize)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
class FontSizeLabel: UILabel {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
let fontSizeLabel = NSLocalizedString("Aa", comment: "Button for reader mode font size. Keep this extremely short! This is shown in the reader mode toolbar.")
|
||||
text = fontSizeLabel
|
||||
isAccessibilityElement = false
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
// TODO
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
var fontType: ReaderModeFontType = .sansSerif {
|
||||
didSet {
|
||||
switch fontType {
|
||||
case .sansSerif:
|
||||
font = UIFont(name: "FiraSans-Book", size: DynamicFontHelper.defaultHelper.ReaderBigFontSize)
|
||||
case .serif:
|
||||
font = UIFont(name: "Charis SIL", size: DynamicFontHelper.defaultHelper.ReaderBigFontSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
class ThemeButton: UIButton {
|
||||
var theme: ReaderModeTheme!
|
||||
|
||||
convenience init(theme: ReaderModeTheme) {
|
||||
self.init(frame: CGRect.zero)
|
||||
self.theme = theme
|
||||
|
||||
setTitle(theme.rawValue, for: UIControlState())
|
||||
|
||||
accessibilityHint = NSLocalizedString("Changes color theme.", comment: "Accessibility hint for the color theme setting buttons in reader mode display settings")
|
||||
|
||||
switch theme {
|
||||
case .light:
|
||||
setTitle(NSLocalizedString("Light", comment: "Light theme setting in Reading View settings"), for: UIControlState())
|
||||
setTitleColor(ReaderModeStyleViewControllerUX.ThemeTitleColorLight, for: UIControlState.normal)
|
||||
backgroundColor = ReaderModeStyleViewControllerUX.ThemeBackgroundColorLight
|
||||
case .dark:
|
||||
setTitle(NSLocalizedString("Dark", comment: "Dark theme setting in Reading View settings"), for: UIControlState())
|
||||
setTitleColor(ReaderModeStyleViewControllerUX.ThemeTitleColorDark, for: UIControlState())
|
||||
backgroundColor = ReaderModeStyleViewControllerUX.ThemeBackgroundColorDark
|
||||
case .sepia:
|
||||
setTitle(NSLocalizedString("Sepia", comment: "Sepia theme setting in Reading View settings"), for: UIControlState())
|
||||
setTitleColor(ReaderModeStyleViewControllerUX.ThemeTitleColorSepia, for: UIControlState.normal)
|
||||
backgroundColor = ReaderModeStyleViewControllerUX.ThemeBackgroundColorSepia
|
||||
}
|
||||
}
|
||||
|
||||
var fontType: ReaderModeFontType = .sansSerif {
|
||||
didSet {
|
||||
switch fontType {
|
||||
case .sansSerif:
|
||||
titleLabel?.font = UIFont(name: "FiraSans-Book", size: DynamicFontHelper.defaultHelper.ReaderStandardFontSize)
|
||||
case .serif:
|
||||
titleLabel?.font = UIFont(name: "Charis SIL", size: DynamicFontHelper.defaultHelper.ReaderStandardFontSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
57
mobile/ios/Client/Frontend/Reader/ReaderModeUtils.swift
Normal file
57
mobile/ios/Client/Frontend/Reader/ReaderModeUtils.swift
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
struct ReaderModeUtils {
|
||||
|
||||
static let DomainPrefixesToSimplify = ["www.", "mobile.", "m.", "blog."]
|
||||
|
||||
static func simplifyDomain(_ domain: String) -> String {
|
||||
for prefix in DomainPrefixesToSimplify {
|
||||
if domain.hasPrefix(prefix) {
|
||||
return domain.substring(from: domain.characters.index(domain.startIndex, offsetBy: prefix.characters.count))
|
||||
}
|
||||
}
|
||||
return domain
|
||||
}
|
||||
|
||||
static func generateReaderContent(_ readabilityResult: ReadabilityResult, initialStyle: ReaderModeStyle) -> String? {
|
||||
if let stylePath = Bundle.main.path(forResource: "Reader", ofType: "css") {
|
||||
do {
|
||||
let css = try NSString(contentsOfFile: stylePath, encoding: String.Encoding.utf8.rawValue)
|
||||
if let tmplPath = Bundle.main.path(forResource: "Reader", ofType: "html") {
|
||||
do {
|
||||
let tmpl = try NSMutableString(contentsOfFile: tmplPath, encoding: String.Encoding.utf8.rawValue)
|
||||
tmpl.replaceOccurrences(of: "%READER-CSS%", with: css as String,
|
||||
options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length))
|
||||
|
||||
tmpl.replaceOccurrences(of: "%READER-STYLE%", with: initialStyle.encode(),
|
||||
options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length))
|
||||
|
||||
tmpl.replaceOccurrences(of: "%READER-DOMAIN%", with: simplifyDomain(readabilityResult.domain),
|
||||
options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length))
|
||||
|
||||
tmpl.replaceOccurrences(of: "%READER-URL%", with: readabilityResult.url,
|
||||
options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length))
|
||||
|
||||
tmpl.replaceOccurrences(of: "%READER-TITLE%", with: readabilityResult.title,
|
||||
options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length))
|
||||
|
||||
tmpl.replaceOccurrences(of: "%READER-CREDITS%", with: readabilityResult.credits,
|
||||
options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length))
|
||||
|
||||
tmpl.replaceOccurrences(of: "%READER-CONTENT%", with: readabilityResult.content,
|
||||
options: NSString.CompareOptions(), range: NSRange(location: 0, length: tmpl.length))
|
||||
|
||||
return tmpl as String
|
||||
} catch _ {
|
||||
}
|
||||
}
|
||||
} catch _ {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
86
mobile/ios/Client/Frontend/Reader/ReaderViewLoading.html
Normal file
86
mobile/ios/Client/Frontend/Reader/ReaderViewLoading.html
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<!DOCTYPE html>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0, initial-scale=1.0">
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: sans-serif;
|
||||
src: url('/reader-mode/fonts/FiraSans-Regular.ttf');
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
#container {
|
||||
color: #666;
|
||||
-webkit-animation: fadein 3s;
|
||||
animation: fadein 3s;
|
||||
margin: 20px auto 0 auto;
|
||||
}
|
||||
|
||||
p {
|
||||
font-family: sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #5af;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="container">
|
||||
<p id="message">%LOADING-TEXT%</p>
|
||||
<p id="link" style="visibility: hidden;"><a id="link" href="%ORIGINAL-URL%">%LOAD-ORIGINAL-TEXT%</a></p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
var numberOfChecks = 20; // 20 * 500ms = 10 seconds total
|
||||
|
||||
function triggerCheck() {
|
||||
if (numberOfChecks--) {
|
||||
setTimeout(function() { checkIfContentIsAvailable(); }, 500);
|
||||
} else {
|
||||
var message = document.getElementById("message")
|
||||
if (message != null) {
|
||||
message.innerText = "%LOADING-FAILED-TEXT%";
|
||||
}
|
||||
|
||||
var link = document.getElementById("link")
|
||||
if (link != null) {
|
||||
link.style.visibility = "visible";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkIfContentIsAvailable() {
|
||||
var request = new XMLHttpRequest();
|
||||
request.open("GET", "/reader-mode/page-exists" + document.location.search, true);
|
||||
request.onload = function() {
|
||||
if (request.status == 200) {
|
||||
webkit.messageHandlers.localRequestHelper.postMessage({ type: "reload" });
|
||||
} else {
|
||||
triggerCheck();
|
||||
}
|
||||
};
|
||||
request.onerror = function() {
|
||||
triggerCheck();
|
||||
};
|
||||
request.send();
|
||||
}
|
||||
|
||||
triggerCheck();
|
||||
</script>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue