added iOS source code
[wl-app.git] / iOS / Pods / FolioReaderKit / Source / FolioReaderKit.swift
1 //
2 //  FolioReaderKit.swift
3 //  FolioReaderKit
4 //
5 //  Created by Heberti Almeida on 08/04/15.
6 //  Copyright (c) 2015 Folio Reader. All rights reserved.
7 //
8
9 import Foundation
10 import UIKit
11
12 // MARK: - Internal constants
13
14 internal let kApplicationDocumentsDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
15 internal let kCurrentFontFamily = "com.folioreader.kCurrentFontFamily"
16 internal let kCurrentFontSize = "com.folioreader.kCurrentFontSize"
17 internal let kCurrentMarginSize = "com.folioreader.kCurrentMarginSize"
18 internal let kCurrentInterlineSize = "com.folioreader.kCurrentInterlineSize"
19 internal let kCurrentAudioRate = "com.folioreader.kCurrentAudioRate"
20 internal let kCurrentHighlightStyle = "com.folioreader.kCurrentHighlightStyle"
21 internal let kCurrentMediaOverlayStyle = "com.folioreader.kMediaOverlayStyle"
22 internal let kCurrentScrollDirection = "com.folioreader.kCurrentScrollDirection"
23 internal let kNightMode = "com.folioreader.kNightMode"
24 internal let kCurrentTOCMenu = "com.folioreader.kCurrentTOCMenu"
25 internal let kHighlightRange = 30
26 internal let kReuseCellIdentifier = "com.folioreader.Cell.ReuseIdentifier"
27
28 public enum FolioReaderError: Error, LocalizedError {
29     case bookNotAvailable
30     case errorInContainer
31     case errorInOpf
32     case authorNameNotAvailable
33     case coverNotAvailable
34     case invalidImage(path: String)
35     case titleNotAvailable
36     case fullPathEmpty
37
38     public var errorDescription: String? {
39         switch self {
40         case .bookNotAvailable:
41             return "Book not found"
42         case .errorInContainer, .errorInOpf:
43             return "Invalid book format"
44         case .authorNameNotAvailable:
45             return "Author name not available"
46         case .coverNotAvailable:
47             return "Cover image not available"
48         case let .invalidImage(path):
49             return "Invalid image at path: " + path
50         case .titleNotAvailable:
51             return "Book title not available"
52         case .fullPathEmpty:
53             return "Book corrupted"
54         }
55     }
56 }
57
58 /// Defines the media overlay and TTS selection
59 ///
60 /// - `default`: The background is colored
61 /// - underline: The underlined is colored
62 /// - textColor: The text is colored
63 public enum MediaOverlayStyle: Int {
64     case `default`
65     case underline
66     case textColor
67
68     init() {
69         self = .default
70     }
71
72     func className() -> String {
73         return "mediaOverlayStyle\(self.rawValue)"
74     }
75 }
76
77 /// FolioReader actions delegate
78 @objc public protocol FolioReaderDelegate: class {
79     
80     /// Did finished loading book.
81     ///
82     /// - Parameters:
83     ///   - folioReader: The FolioReader instance
84     ///   - book: The Book instance
85     @objc optional func folioReader(_ folioReader: FolioReader, didFinishedLoading book: FRBook)
86     
87     /// Called when reader did closed.
88     ///
89     /// - Parameter folioReader: The FolioReader instance
90     @objc optional func folioReaderDidClose(_ folioReader: FolioReader)
91     
92     /// Called when reader did closed.
93     @available(*, deprecated, message: "Use 'folioReaderDidClose(_ folioReader: FolioReader)' instead.")
94     @objc optional func folioReaderDidClosed()
95 }
96
97 /// Main Library class with some useful constants and methods
98 open class FolioReader: NSObject {
99
100     public override init() { }
101
102     deinit {
103         removeObservers()
104     }
105
106     /// Custom unzip path
107     open var unzipPath: String?
108
109     /// FolioReaderDelegate
110     open weak var delegate: FolioReaderDelegate?
111     
112     open weak var readerContainer: FolioReaderContainer?
113     open weak var readerAudioPlayer: FolioReaderAudioPlayer?
114     open weak var readerCenter: FolioReaderCenter? {
115         return self.readerContainer?.centerViewController
116     }
117
118     /// Check if reader is open
119     var isReaderOpen = false
120
121     /// Check if reader is open and ready
122     var isReaderReady = false
123
124     /// Check if layout needs to change to fit Right To Left
125     var needsRTLChange: Bool {
126         return (self.readerContainer?.book.spine.isRtl == true && self.readerContainer?.readerConfig.scrollDirection == .horizontal)
127     }
128
129     func isNight<T>(_ f: T, _ l: T) -> T {
130         return (self.nightMode == true ? f : l)
131     }
132
133     /// UserDefault for the current ePub file.
134     fileprivate var defaults: FolioReaderUserDefaults {
135         return FolioReaderUserDefaults(withIdentifier: self.readerContainer?.readerConfig.identifier)
136     }
137
138     // Add necessary observers
139     fileprivate func addObservers() {
140         removeObservers()
141         NotificationCenter.default.addObserver(self, selector: #selector(saveReaderState), name: .UIApplicationWillResignActive, object: nil)
142         NotificationCenter.default.addObserver(self, selector: #selector(saveReaderState), name: .UIApplicationWillTerminate, object: nil)
143     }
144
145     /// Remove necessary observers
146     fileprivate func removeObservers() {
147         NotificationCenter.default.removeObserver(self, name: .UIApplicationWillResignActive, object: nil)
148         NotificationCenter.default.removeObserver(self, name: .UIApplicationWillTerminate, object: nil)
149     }
150     
151     public func getProgressValues() -> (currentPage: Int, totalPages: Int)? {
152         guard let center = readerCenter else { return nil}
153         let totalPages = center.totalPages
154         let currentPage = center.currentPageNumber
155         return (currentPage, totalPages)
156     }
157
158 }
159
160 // MARK: - Present FolioReader
161
162 extension FolioReader {
163
164     /// Present a Folio Reader Container modally on a Parent View Controller.
165     ///
166     /// - Parameters:
167     ///   - parentViewController: View Controller that will present the reader container.
168     ///   - epubPath: String representing the path on the disk of the ePub file. Must not be nil nor empty string.
169         ///   - unzipPath: Path to unzip the compressed epub.
170     ///   - config: FolioReader configuration.
171     ///   - shouldRemoveEpub: Boolean to remove the epub or not. Default true.
172     ///   - animated: Pass true to animate the presentation; otherwise, pass false.
173     open func presentReader(parentViewController: UIViewController, withEpubPath epubPath: String, unzipPath: String? = nil, andConfig config: FolioReaderConfig, shouldRemoveEpub: Bool = true, animated:
174         Bool = true) {
175         let readerContainer = FolioReaderContainer(withConfig: config, folioReader: self, epubPath: epubPath, unzipPath: unzipPath, removeEpub: shouldRemoveEpub)
176         self.readerContainer = readerContainer
177         parentViewController.present(readerContainer, animated: animated, completion: nil)
178         addObservers()
179     }
180 }
181
182 // MARK: -  Getters and setters for stored values
183
184 extension FolioReader {
185
186     public func register(defaults: [String: Any]) {
187         self.defaults.register(defaults: defaults)
188     }
189
190     /// Check if current theme is Night mode
191     open var nightMode: Bool {
192         get { return self.defaults.bool(forKey: kNightMode) }
193         set (value) {
194             self.defaults.set(value, forKey: kNightMode)
195
196             if let readerCenter = self.readerCenter {
197                 UIView.animate(withDuration: 0.6, animations: {
198                     _ = readerCenter.currentPage?.webView?.js("nightMode(\(self.nightMode))")
199                     readerCenter.pageIndicatorView?.reloadColors()
200                     readerCenter.configureNavBar()
201                     readerCenter.scrollScrubber?.reloadColors()
202                     readerCenter.collectionView.backgroundColor = (self.nightMode == true ? self.readerContainer?.readerConfig.nightModeBackground : UIColor.white)
203                 }, completion: { (finished: Bool) in
204                     NotificationCenter.default.post(name: Notification.Name(rawValue: "needRefreshPageMode"), object: nil)
205                 })
206             }
207         }
208     }
209
210     /// Check current font name. Default .andada
211     open var currentFont: FolioReaderFont {
212         get {
213             guard
214                 let rawValue = self.defaults.value(forKey: kCurrentFontFamily) as? Int,
215                 let font = FolioReaderFont(rawValue: rawValue) else {
216                     return .andada
217             }
218
219             return font
220         }
221         set (font) {
222             self.defaults.set(font.rawValue, forKey: kCurrentFontFamily)
223             _ = self.readerCenter?.currentPage?.webView?.js("setFontName('\(font.cssIdentifier)')")
224         }
225     }
226
227     /// Check current font size. Default .m
228     open var currentFontSize: FolioReaderSliderParamSize {
229         get {
230             guard
231                 let rawValue = self.defaults.value(forKey: kCurrentFontSize) as? Int,
232                 let size = FolioReaderSliderParamSize(rawValue: rawValue) else {
233                     return .m
234             }
235
236             return size
237         }
238         set (value) {
239             self.defaults.set(value.rawValue, forKey: kCurrentFontSize)
240
241             guard let currentPage = self.readerCenter?.currentPage else {
242                 return
243             }
244
245             currentPage.webView?.js("setFontSize('\(currentFontSize.cssIdentifier(sliderType: SliderType.font) )')")
246         }
247     }
248     
249     /// Check current margin size. Default .m
250     open var currentMarginSize: FolioReaderSliderParamSize {
251         get {
252             guard
253                 let rawValue = self.defaults.value(forKey: kCurrentMarginSize) as? Int,
254                 let size = FolioReaderSliderParamSize(rawValue: rawValue) else {
255                     return .m
256             }
257             
258             return size
259         }
260         set (value) {
261             self.defaults.set(value.rawValue, forKey: kCurrentMarginSize)
262             
263             guard let currentPage = self.readerCenter?.currentPage else {
264                 return
265             }
266             
267             currentPage.webView?.js("setMarginSize('\(currentMarginSize.cssIdentifier(sliderType: SliderType.margin))')")
268         }
269     }
270
271     /// Check current interline size. Default .m
272     open var currentInterlineSize: FolioReaderSliderParamSize {
273         get {
274             guard
275                 let rawValue = self.defaults.value(forKey: kCurrentInterlineSize) as? Int,
276                 let size = FolioReaderSliderParamSize(rawValue: rawValue) else {
277                     return .m
278             }
279             
280             return size
281         }
282         set (value) {
283             self.defaults.set(value.rawValue, forKey: kCurrentInterlineSize)
284             
285             guard let currentPage = self.readerCenter?.currentPage else {
286                 return
287             }
288             
289             currentPage.webView?.js("setInterlineSize('\(currentInterlineSize.cssIdentifier(sliderType: SliderType.interline))')")
290             
291 //            print("\n\nhtmllllll\n\n" + currentPage.webView!.stringByEvaluatingJavaScript(from: "document.documentElement.outerHTML")!)
292
293         }
294     }
295
296     /// Check current audio rate, the speed of speech voice. Default 0
297     open var currentAudioRate: Int {
298         get { return self.defaults.integer(forKey: kCurrentAudioRate) }
299         set (value) {
300             self.defaults.set(value, forKey: kCurrentAudioRate)
301         }
302     }
303
304     /// Check the current highlight style.Default 0
305     open var currentHighlightStyle: Int {
306         get { return self.defaults.integer(forKey: kCurrentHighlightStyle) }
307         set (value) {
308             self.defaults.set(value, forKey: kCurrentHighlightStyle)
309         }
310     }
311
312     /// Check the current Media Overlay or TTS style
313     open var currentMediaOverlayStyle: MediaOverlayStyle {
314         get {
315             guard let rawValue = self.defaults.value(forKey: kCurrentMediaOverlayStyle) as? Int,
316                 let style = MediaOverlayStyle(rawValue: rawValue) else {
317                 return MediaOverlayStyle.default
318             }
319             return style
320         }
321         set (value) {
322             self.defaults.set(value.rawValue, forKey: kCurrentMediaOverlayStyle)
323         }
324     }
325
326     /// Check the current scroll direction. Default .defaultVertical
327     open var currentScrollDirection: Int {
328         get {
329             guard let value = self.defaults.value(forKey: kCurrentScrollDirection) as? Int else {
330                 return FolioReaderScrollDirection.defaultVertical.rawValue
331             }
332
333             return value
334         }
335         set (value) {
336             self.defaults.set(value, forKey: kCurrentScrollDirection)
337
338             let direction = (FolioReaderScrollDirection(rawValue: currentScrollDirection) ?? .defaultVertical)
339             self.readerCenter?.setScrollDirection(direction)
340         }
341     }
342
343     open var currentMenuIndex: Int {
344         get { return self.defaults.integer(forKey: kCurrentTOCMenu) }
345         set (value) {
346             self.defaults.set(value, forKey: kCurrentTOCMenu)
347         }
348     }
349
350     open var savedPositionForCurrentBook: [String: Any]? {
351         get {
352             guard let bookId = self.readerContainer?.book.name else {
353                 return nil
354             }
355             return self.defaults.value(forKey: bookId) as? [String : Any]
356         }
357         set {
358             guard let bookId = self.readerContainer?.book.name else {
359                 return
360             }
361             self.defaults.set(newValue, forKey: bookId)
362         }
363     }
364 }
365
366 // MARK: - Metadata
367
368 extension FolioReader {
369
370     // TODO QUESTION: The static `getCoverImage` function used the shared instance before and ignored the `unzipPath` parameter.
371     // Should we properly implement the parameter (what has been done now) or should change the API to only use the current FolioReader instance?
372
373     /**
374      Read Cover Image and Return an `UIImage`
375      */
376     open class func getCoverImage(_ epubPath: String, unzipPath: String? = nil) throws -> UIImage {
377         return try FREpubParser().parseCoverImage(epubPath, unzipPath: unzipPath)
378     }
379
380     open class func getTitle(_ epubPath: String, unzipPath: String? = nil) throws -> String {
381         return try FREpubParser().parseTitle(epubPath, unzipPath: unzipPath)
382     }
383
384     open class func getAuthorName(_ epubPath: String, unzipPath: String? = nil) throws-> String {
385         return try FREpubParser().parseAuthorName(epubPath, unzipPath: unzipPath)
386     }
387 }
388
389 // MARK: - Exit, save and close FolioReader
390
391 extension FolioReader {
392
393     /// Save Reader state, book, page and scroll offset.
394     @objc open func saveReaderState() {
395         guard isReaderOpen else {
396             return
397         }
398
399         guard let currentPage = self.readerCenter?.currentPage, let webView = currentPage.webView else {
400             return
401         }
402
403         let position = [
404             "pageNumber": (self.readerCenter?.currentPageNumber ?? 0),
405             "pageOffsetX": webView.scrollView.contentOffset.x,
406             "pageOffsetY": webView.scrollView.contentOffset.y
407             ] as [String : Any]
408
409         self.savedPositionForCurrentBook = position
410     }
411
412     /// Closes and save the reader current instance.
413     open func close() {
414         self.saveReaderState()
415         self.isReaderOpen = false
416         self.isReaderReady = false
417         self.readerAudioPlayer?.stop(immediate: true)
418         self.defaults.set(0, forKey: kCurrentTOCMenu)
419         self.delegate?.folioReaderDidClose?(self)
420     }
421 }