--- /dev/null
+//
+// 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 kCurrentMarginSize = "com.folioreader.kCurrentMarginSize"
+internal let kCurrentInterlineSize = "com.folioreader.kCurrentInterlineSize"
+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: .UIApplicationWillResignActive, object: nil)
+ NotificationCenter.default.addObserver(self, selector: #selector(saveReaderState), name: .UIApplicationWillTerminate, object: nil)
+ }
+
+ /// Remove necessary observers
+ fileprivate func removeObservers() {
+ NotificationCenter.default.removeObserver(self, name: .UIApplicationWillResignActive, object: nil)
+ NotificationCenter.default.removeObserver(self, name: .UIApplicationWillTerminate, object: nil)
+ }
+
+ public func getProgressValues() -> (currentPage: Int, totalPages: Int)? {
+ guard let center = readerCenter else { return nil}
+ let totalPages = center.totalPages
+ let currentPage = center.currentPageNumber
+ return (currentPage, totalPages)
+ }
+
+}
+
+// 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: FolioReaderSliderParamSize {
+ get {
+ guard
+ let rawValue = self.defaults.value(forKey: kCurrentFontSize) as? Int,
+ let size = FolioReaderSliderParamSize(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(sliderType: SliderType.font) )')")
+ }
+ }
+
+ /// Check current margin size. Default .m
+ open var currentMarginSize: FolioReaderSliderParamSize {
+ get {
+ guard
+ let rawValue = self.defaults.value(forKey: kCurrentMarginSize) as? Int,
+ let size = FolioReaderSliderParamSize(rawValue: rawValue) else {
+ return .m
+ }
+
+ return size
+ }
+ set (value) {
+ self.defaults.set(value.rawValue, forKey: kCurrentMarginSize)
+
+ guard let currentPage = self.readerCenter?.currentPage else {
+ return
+ }
+
+ currentPage.webView?.js("setMarginSize('\(currentMarginSize.cssIdentifier(sliderType: SliderType.margin))')")
+ }
+ }
+
+ /// Check current interline size. Default .m
+ open var currentInterlineSize: FolioReaderSliderParamSize {
+ get {
+ guard
+ let rawValue = self.defaults.value(forKey: kCurrentInterlineSize) as? Int,
+ let size = FolioReaderSliderParamSize(rawValue: rawValue) else {
+ return .m
+ }
+
+ return size
+ }
+ set (value) {
+ self.defaults.set(value.rawValue, forKey: kCurrentInterlineSize)
+
+ guard let currentPage = self.readerCenter?.currentPage else {
+ return
+ }
+
+ currentPage.webView?.js("setInterlineSize('\(currentInterlineSize.cssIdentifier(sliderType: SliderType.interline))')")
+
+// print("\n\nhtmllllll\n\n" + currentPage.webView!.stringByEvaluatingJavaScript(from: "document.documentElement.outerHTML")!)
+
+ }
+ }
+
+ /// 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)
+ }
+}