This repository has been archived by the owner on Nov 26, 2020. It is now read-only.
/
FolioReaderKit.swift
executable file
路364 lines (304 loc) 路 12.7 KB
/
FolioReaderKit.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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
//
// FolioReaderKit.swift
// FolioReaderKit
//
// Created by Heberti Almeida on 08/04/15.
// Copyright (c) 2015 Folio Reader. All rights reserved.
//
import Foundation
import UIKit
// MARK: - Internal constants
internal let kApplicationDocumentsDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
internal let kCurrentFontFamily = "com.folioreader.kCurrentFontFamily"
internal let kCurrentFontSize = "com.folioreader.kCurrentFontSize"
internal let kCurrentAudioRate = "com.folioreader.kCurrentAudioRate"
internal let kCurrentHighlightStyle = "com.folioreader.kCurrentHighlightStyle"
internal let kCurrentMediaOverlayStyle = "com.folioreader.kMediaOverlayStyle"
internal let kCurrentScrollDirection = "com.folioreader.kCurrentScrollDirection"
internal let kNightMode = "com.folioreader.kNightMode"
internal let kCurrentTOCMenu = "com.folioreader.kCurrentTOCMenu"
internal let kHighlightRange = 30
internal let kReuseCellIdentifier = "com.folioreader.Cell.ReuseIdentifier"
public enum FolioReaderError: Error, LocalizedError {
case bookNotAvailable
case errorInContainer
case errorInOpf
case authorNameNotAvailable
case coverNotAvailable
case invalidImage(path: String)
case titleNotAvailable
case fullPathEmpty
public var errorDescription: String? {
switch self {
case .bookNotAvailable:
return "Book not found"
case .errorInContainer, .errorInOpf:
return "Invalid book format"
case .authorNameNotAvailable:
return "Author name not available"
case .coverNotAvailable:
return "Cover image not available"
case let .invalidImage(path):
return "Invalid image at path: " + path
case .titleNotAvailable:
return "Book title not available"
case .fullPathEmpty:
return "Book corrupted"
}
}
}
/// Defines the media overlay and TTS selection
///
/// - `default`: The background is colored
/// - underline: The underlined is colored
/// - textColor: The text is colored
public enum MediaOverlayStyle: Int {
case `default`
case underline
case textColor
init() {
self = .default
}
func className() -> String {
return "mediaOverlayStyle\(self.rawValue)"
}
}
/// FolioReader actions delegate
@objc public protocol FolioReaderDelegate: class {
/// Did finished loading book.
///
/// - Parameters:
/// - folioReader: The FolioReader instance
/// - book: The Book instance
@objc optional func folioReader(_ folioReader: FolioReader, didFinishedLoading book: FRBook)
/// Called when reader did closed.
///
/// - Parameter folioReader: The FolioReader instance
@objc optional func folioReaderDidClose(_ folioReader: FolioReader)
/// Called when reader did closed.
@available(*, deprecated, message: "Use 'folioReaderDidClose(_ folioReader: FolioReader)' instead.")
@objc optional func folioReaderDidClosed()
}
/// Main Library class with some useful constants and methods
open class FolioReader: NSObject {
public override init() { }
deinit {
removeObservers()
}
/// Custom unzip path
open var unzipPath: String?
/// FolioReaderDelegate
open weak var delegate: FolioReaderDelegate?
open weak var readerContainer: FolioReaderContainer?
open weak var readerAudioPlayer: FolioReaderAudioPlayer?
open weak var readerCenter: FolioReaderCenter? {
return self.readerContainer?.centerViewController
}
/// Check if reader is open
var isReaderOpen = false
/// Check if reader is open and ready
var isReaderReady = false
/// Check if layout needs to change to fit Right To Left
var needsRTLChange: Bool {
return (self.readerContainer?.book.spine.isRtl == true && self.readerContainer?.readerConfig.scrollDirection == .horizontal)
}
func isNight<T>(_ f: T, _ l: T) -> T {
return (self.nightMode == true ? f : l)
}
/// UserDefault for the current ePub file.
fileprivate var defaults: FolioReaderUserDefaults {
return FolioReaderUserDefaults(withIdentifier: self.readerContainer?.readerConfig.identifier)
}
// Add necessary observers
fileprivate func addObservers() {
removeObservers()
NotificationCenter.default.addObserver(self, selector: #selector(saveReaderState), name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(saveReaderState), name: UIApplication.willTerminateNotification, object: nil)
}
/// Remove necessary observers
fileprivate func removeObservers() {
NotificationCenter.default.removeObserver(self, name: UIApplication.willResignActiveNotification, object: nil)
NotificationCenter.default.removeObserver(self, name: UIApplication.willTerminateNotification, object: nil)
}
}
// MARK: - Present FolioReader
extension FolioReader {
/// Present a Folio Reader Container modally on a Parent View Controller.
///
/// - Parameters:
/// - parentViewController: View Controller that will present the reader container.
/// - epubPath: String representing the path on the disk of the ePub file. Must not be nil nor empty string.
/// - unzipPath: Path to unzip the compressed epub.
/// - config: FolioReader configuration.
/// - shouldRemoveEpub: Boolean to remove the epub or not. Default true.
/// - animated: Pass true to animate the presentation; otherwise, pass false.
open func presentReader(parentViewController: UIViewController, withEpubPath epubPath: String, unzipPath: String? = nil, andConfig config: FolioReaderConfig, shouldRemoveEpub: Bool = true, animated:
Bool = true) {
let readerContainer = FolioReaderContainer(withConfig: config, folioReader: self, epubPath: epubPath, unzipPath: unzipPath, removeEpub: shouldRemoveEpub)
self.readerContainer = readerContainer
parentViewController.present(readerContainer, animated: animated, completion: nil)
addObservers()
}
}
// MARK: - Getters and setters for stored values
extension FolioReader {
public func register(defaults: [String: Any]) {
self.defaults.register(defaults: defaults)
}
/// Check if current theme is Night mode
open var nightMode: Bool {
get { return self.defaults.bool(forKey: kNightMode) }
set (value) {
self.defaults.set(value, forKey: kNightMode)
if let readerCenter = self.readerCenter {
UIView.animate(withDuration: 0.6, animations: {
_ = readerCenter.currentPage?.webView?.js("nightMode(\(self.nightMode))")
readerCenter.pageIndicatorView?.reloadColors()
readerCenter.configureNavBar()
readerCenter.scrollScrubber?.reloadColors()
readerCenter.collectionView.backgroundColor = (self.nightMode == true ? self.readerContainer?.readerConfig.nightModeBackground : UIColor.white)
}, completion: { (finished: Bool) in
NotificationCenter.default.post(name: Notification.Name(rawValue: "needRefreshPageMode"), object: nil)
})
}
}
}
/// Check current font name. Default .andada
open var currentFont: FolioReaderFont {
get {
guard
let rawValue = self.defaults.value(forKey: kCurrentFontFamily) as? Int,
let font = FolioReaderFont(rawValue: rawValue) else {
return .andada
}
return font
}
set (font) {
self.defaults.set(font.rawValue, forKey: kCurrentFontFamily)
_ = self.readerCenter?.currentPage?.webView?.js("setFontName('\(font.cssIdentifier)')")
}
}
/// Check current font size. Default .m
open var currentFontSize: FolioReaderFontSize {
get {
guard
let rawValue = self.defaults.value(forKey: kCurrentFontSize) as? Int,
let size = FolioReaderFontSize(rawValue: rawValue) else {
return .m
}
return size
}
set (value) {
self.defaults.set(value.rawValue, forKey: kCurrentFontSize)
guard let currentPage = self.readerCenter?.currentPage else {
return
}
currentPage.webView?.js("setFontSize('\(currentFontSize.cssIdentifier)')")
}
}
/// Check current audio rate, the speed of speech voice. Default 0
open var currentAudioRate: Int {
get { return self.defaults.integer(forKey: kCurrentAudioRate) }
set (value) {
self.defaults.set(value, forKey: kCurrentAudioRate)
}
}
/// Check the current highlight style.Default 0
open var currentHighlightStyle: Int {
get { return self.defaults.integer(forKey: kCurrentHighlightStyle) }
set (value) {
self.defaults.set(value, forKey: kCurrentHighlightStyle)
}
}
/// Check the current Media Overlay or TTS style
open var currentMediaOverlayStyle: MediaOverlayStyle {
get {
guard let rawValue = self.defaults.value(forKey: kCurrentMediaOverlayStyle) as? Int,
let style = MediaOverlayStyle(rawValue: rawValue) else {
return MediaOverlayStyle.default
}
return style
}
set (value) {
self.defaults.set(value.rawValue, forKey: kCurrentMediaOverlayStyle)
}
}
/// Check the current scroll direction. Default .defaultVertical
open var currentScrollDirection: Int {
get {
guard let value = self.defaults.value(forKey: kCurrentScrollDirection) as? Int else {
return FolioReaderScrollDirection.defaultVertical.rawValue
}
return value
}
set (value) {
self.defaults.set(value, forKey: kCurrentScrollDirection)
let direction = (FolioReaderScrollDirection(rawValue: currentScrollDirection) ?? .defaultVertical)
self.readerCenter?.setScrollDirection(direction)
}
}
open var currentMenuIndex: Int {
get { return self.defaults.integer(forKey: kCurrentTOCMenu) }
set (value) {
self.defaults.set(value, forKey: kCurrentTOCMenu)
}
}
open var savedPositionForCurrentBook: [String: Any]? {
get {
guard let bookId = self.readerContainer?.book.name else {
return nil
}
return self.defaults.value(forKey: bookId) as? [String : Any]
}
set {
guard let bookId = self.readerContainer?.book.name else {
return
}
self.defaults.set(newValue, forKey: bookId)
}
}
}
// MARK: - Metadata
extension FolioReader {
// TODO QUESTION: The static `getCoverImage` function used the shared instance before and ignored the `unzipPath` parameter.
// Should we properly implement the parameter (what has been done now) or should change the API to only use the current FolioReader instance?
/**
Read Cover Image and Return an `UIImage`
*/
open class func getCoverImage(_ epubPath: String, unzipPath: String? = nil) throws -> UIImage {
return try FREpubParser().parseCoverImage(epubPath, unzipPath: unzipPath)
}
open class func getTitle(_ epubPath: String, unzipPath: String? = nil) throws -> String {
return try FREpubParser().parseTitle(epubPath, unzipPath: unzipPath)
}
open class func getAuthorName(_ epubPath: String, unzipPath: String? = nil) throws-> String {
return try FREpubParser().parseAuthorName(epubPath, unzipPath: unzipPath)
}
}
// MARK: - Exit, save and close FolioReader
extension FolioReader {
/// Save Reader state, book, page and scroll offset.
@objc open func saveReaderState() {
guard isReaderOpen else {
return
}
guard let currentPage = self.readerCenter?.currentPage, let webView = currentPage.webView else {
return
}
let position = [
"pageNumber": (self.readerCenter?.currentPageNumber ?? 0),
"pageOffsetX": webView.scrollView.contentOffset.x,
"pageOffsetY": webView.scrollView.contentOffset.y
] as [String : Any]
self.savedPositionForCurrentBook = position
}
/// Closes and save the reader current instance.
open func close() {
self.saveReaderState()
self.isReaderOpen = false
self.isReaderReady = false
self.readerAudioPlayer?.stop(immediate: true)
self.defaults.set(0, forKey: kCurrentTOCMenu)
self.delegate?.folioReaderDidClose?(self)
}
}