-
Notifications
You must be signed in to change notification settings - Fork 99
/
EPUBViewController.swift
168 lines (147 loc) · 6.16 KB
/
EPUBViewController.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//
// Copyright 2024 Readium Foundation. All rights reserved.
// Use of this source code is governed by the BSD-style license
// available in the top-level LICENSE file of the project.
//
import R2Navigator
import R2Shared
import ReadiumAdapterGCDWebServer
import SwiftSoup
import SwiftUI
import UIKit
import WebKit
public extension FontFamily {
// Example of adding a custom font embedded in the application.
static let literata: FontFamily = "Literata"
}
class EPUBViewController: VisualReaderViewController<EPUBNavigatorViewController> {
private let preferencesStore: AnyUserPreferencesStore<EPUBPreferences>
init(
publication: Publication,
locator: Locator?,
bookId: Book.Id,
books: BookRepository,
bookmarks: BookmarkRepository,
highlights: HighlightRepository,
initialPreferences: EPUBPreferences,
preferencesStore: AnyUserPreferencesStore<EPUBPreferences>
) throws {
var templates = HTMLDecorationTemplate.defaultTemplates()
templates[.pageList] = .pageList
let resources = Bundle.main.resourceURL!
let navigator = try EPUBNavigatorViewController(
publication: publication,
initialLocation: locator,
config: .init(
preferences: initialPreferences,
editingActions: EditingAction.defaultActions
.appending(EditingAction(
title: "Highlight",
action: #selector(highlightSelection)
)),
decorationTemplates: templates,
fontFamilyDeclarations: [
CSSFontFamilyDeclaration(
fontFamily: .literata,
fontFaces: [
// Literata is a variable font family, so we can provide a font weight range.
CSSFontFace(
file: resources.appendingPathComponent("Fonts/Literata-VariableFont_opsz,wght.ttf"),
style: .normal, weight: .variable(200 ... 900)
),
CSSFontFace(
file: resources.appendingPathComponent("Fonts/Literata-Italic-VariableFont_opsz,wght.ttf"),
style: .italic, weight: .variable(200 ... 900)
),
]
).eraseToAnyHTMLFontFamilyDeclaration(),
]
),
httpServer: GCDHTTPServer.shared
)
self.preferencesStore = preferencesStore
super.init(navigator: navigator, publication: publication, bookId: bookId, books: books, bookmarks: bookmarks, highlights: highlights)
navigator.delegate = self
}
override func presentUserPreferences() {
Task {
let userPrefs = await UserPreferences(
model: UserPreferencesViewModel(
bookId: bookId,
preferences: try! preferencesStore.preferences(for: bookId),
configurable: navigator,
store: preferencesStore
),
onClose: { [weak self] in
self?.dismiss(animated: true)
}
)
let vc = UIHostingController(rootView: userPrefs)
vc.modalPresentationStyle = .formSheet
present(vc, animated: true)
}
}
@objc func highlightSelection() {
if let selection = navigator.currentSelection {
let highlight = Highlight(bookId: bookId, locator: selection.locator, color: .yellow)
saveHighlight(highlight)
navigator.clearSelection()
}
}
// MARK: - Footnotes
private func presentFootnote(content: String, referrer: String?) -> Bool {
var title = referrer
if let t = title {
title = try? clean(t, .none())
}
if !suitableTitle(title) {
title = nil
}
let content = (try? clean(content, .none())) ?? ""
let page =
"""
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
\(content)
</body>
</html>
"""
let wk = WKWebView()
wk.loadHTMLString(page, baseURL: nil)
let vc = UIViewController()
vc.view = wk
vc.navigationItem.title = title
vc.navigationItem.leftBarButtonItem = BarButtonItem(barButtonSystemItem: .done, actionHandler: { _ in
vc.dismiss(animated: true, completion: nil)
})
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .formSheet
present(nav, animated: true, completion: nil)
return false
}
/// This regex matches any string with at least 2 consecutive letters (not limited to ASCII).
/// It's used when evaluating whether to display the body of a noteref referrer as the note's title.
/// I.e. a `*` or `1` would not be used as a title, but `on` or `好書` would.
private lazy var noterefTitleRegex: NSRegularExpression =
try! NSRegularExpression(pattern: "[\\p{Ll}\\p{Lu}\\p{Lt}\\p{Lo}]{2}")
/// Checks to ensure the title is non-nil and contains at least 2 letters.
private func suitableTitle(_ title: String?) -> Bool {
guard let title = title else { return false }
let range = NSRange(location: 0, length: title.utf16.count)
let match = noterefTitleRegex.firstMatch(in: title, range: range)
return match != nil
}
}
extension EPUBViewController: EPUBNavigatorDelegate {
func navigator(_ navigator: Navigator, shouldNavigateToNoteAt link: R2Shared.Link, content: String, referrer: String?) -> Bool {
presentFootnote(content: content, referrer: referrer)
}
}
extension EPUBViewController: UIGestureRecognizerDelegate {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
true
}
}