mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-08-21 03:43:16 +09:00
296 lines
9.8 KiB
Swift
296 lines
9.8 KiB
Swift
/* 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
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|