//
// FolioReaderCenter.swift
// FolioReaderKit
//
// Created by Heberti Almeida on 08/04/15.
// Copyright (c) 2015 Folio Reader. All rights reserved.
//
import UIKit
import ZFDragableModalTransition
/// Protocol which is used from `FolioReaderCenter`s.
@objc public protocol FolioReaderCenterDelegate: class {
/// Notifies that a page appeared. This is triggered when a page is chosen and displayed.
///
/// - Parameter page: The appeared page
@objc optional func pageDidAppear(_ page: FolioReaderPage)
/// Passes and returns the HTML content as `String`. Implement this method if you want to modify the HTML content of a `FolioReaderPage`.
///
/// - Parameters:
/// - page: The `FolioReaderPage`.
/// - htmlContent: The current HTML content as `String`.
/// - Returns: The adjusted HTML content as `String`. This is the content which will be loaded into the given `FolioReaderPage`.
@objc optional func htmlContentForPage(_ page: FolioReaderPage, htmlContent: String) -> String
/// Notifies that a page changed. This is triggered when collection view cell is changed.
///
/// - Parameter pageNumber: The appeared page item
@objc optional func pageItemChanged(_ pageNumber: Int)
}
/// The base reader class
open class FolioReaderCenter: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
/// This delegate receives the events from the current `FolioReaderPage`s delegate.
open weak var delegate: FolioReaderCenterDelegate?
/// This delegate receives the events from current page
open weak var pageDelegate: FolioReaderPageDelegate?
/// The base reader container
open weak var readerContainer: FolioReaderContainer?
/// The current visible page on reader
open fileprivate(set) var currentPage: FolioReaderPage?
/// The collection view with pages
open var collectionView: UICollectionView!
let collectionViewLayout = UICollectionViewFlowLayout()
var loadingView: UIActivityIndicatorView!
var pages: [String]!
var totalPages: Int = 0
var tempFragment: String?
var animator: ZFModalTransitionAnimator!
var pageIndicatorView: FolioReaderPageIndicator?
var pageIndicatorHeight: CGFloat = 20
var recentlyScrolled = false
var recentlyScrolledDelay = 2.0 // 2 second delay until we clear recentlyScrolled
var recentlyScrolledTimer: Timer!
var scrollScrubber: ScrollScrubber?
var activityIndicator = UIActivityIndicatorView()
var isScrolling = false
var pageScrollDirection = ScrollDirection()
var nextPageNumber: Int = 0
var previousPageNumber: Int = 0
var currentPageNumber: Int = 0
var pageWidth: CGFloat = 0.0
var pageHeight: CGFloat = 0.0
var anchor: String!
var lastRow: Int?
fileprivate var screenBounds: CGRect!
fileprivate var pointNow = CGPoint.zero
fileprivate var pageOffsetRate: CGFloat = 0
fileprivate var tempReference: FRTocReference?
fileprivate var isFirstLoad = true
fileprivate var currentWebViewScrollPositions = [Int: CGPoint]()
fileprivate var currentOrientation: UIInterfaceOrientation?
fileprivate var changingChapter = false
fileprivate var readerConfig: FolioReaderConfig {
guard let readerContainer = readerContainer else { return FolioReaderConfig() }
return readerContainer.readerConfig
}
fileprivate var book: FRBook {
guard let readerContainer = readerContainer else { return FRBook() }
return readerContainer.book
}
fileprivate var folioReader: FolioReader {
guard let readerContainer = readerContainer else { return FolioReader() }
return readerContainer.folioReader
}
// MARK: - Init
init(withContainer readerContainer: FolioReaderContainer) {
self.readerContainer = readerContainer
super.init(nibName: nil, bundle: Bundle.frameworkBundle())
self.initialization()
}
required public init?(coder aDecoder: NSCoder) {
fatalError("This class doesn't support NSCoding.")
}
/**
Common Initialization
*/
fileprivate func initialization() {
if (self.readerConfig.hideBars == true) {
self.pageIndicatorHeight = 0
}
self.totalPages = book.spine.spineReferences.count
// Loading indicator
let style: UIActivityIndicatorViewStyle = folioReader.isNight(.white, .gray)
loadingView = UIActivityIndicatorView(activityIndicatorStyle: style)
loadingView.hidesWhenStopped = true
loadingView.startAnimating()
self.view.addSubview(loadingView)
}
// MARK: - View life cicle
override open func viewDidLoad() {
super.viewDidLoad()
screenBounds = self.getScreenBounds()
setPageSize(UIApplication.shared.statusBarOrientation)
// Layout
collectionViewLayout.sectionInset = UIEdgeInsets.zero
collectionViewLayout.minimumLineSpacing = 0
collectionViewLayout.minimumInteritemSpacing = 0
collectionViewLayout.scrollDirection = .direction(withConfiguration: self.readerConfig)
let background = folioReader.isNight(self.readerConfig.nightModeBackground, UIColor.white)
view.backgroundColor = background
// CollectionView
collectionView = UICollectionView(frame: screenBounds, collectionViewLayout: collectionViewLayout)
collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
collectionView.delegate = self
collectionView.dataSource = self
collectionView.isPagingEnabled = true
collectionView.showsVerticalScrollIndicator = false
collectionView.showsHorizontalScrollIndicator = false
collectionView.backgroundColor = background
collectionView.decelerationRate = UIScrollViewDecelerationRateFast
enableScrollBetweenChapters(scrollEnabled: true)
view.addSubview(collectionView)
if #available(iOS 11.0, *) {
collectionView.contentInsetAdjustmentBehavior = .never
}
// Activity Indicator
self.activityIndicator.activityIndicatorViewStyle = .gray
self.activityIndicator.hidesWhenStopped = true
self.activityIndicator = UIActivityIndicatorView(frame: CGRect(x: screenBounds.size.width/2, y: screenBounds.size.height/2, width: 30, height: 30))
self.activityIndicator.backgroundColor = UIColor.gray
self.view.addSubview(self.activityIndicator)
self.view.bringSubview(toFront: self.activityIndicator)
if #available(iOS 10.0, *) {
collectionView.isPrefetchingEnabled = false
}
// Register cell classes
collectionView?.register(FolioReaderPage.self, forCellWithReuseIdentifier: kReuseCellIdentifier)
// Configure navigation bar and layout
automaticallyAdjustsScrollViewInsets = false
extendedLayoutIncludesOpaqueBars = true
configureNavBar()
// Page indicator view
if (self.readerConfig.hidePageIndicator == false) {
let frame = self.frameForPageIndicatorView()
pageIndicatorView = FolioReaderPageIndicator(frame: frame, readerConfig: readerConfig, folioReader: folioReader)
if let pageIndicatorView = pageIndicatorView {
view.addSubview(pageIndicatorView)
}
}
guard let readerContainer = readerContainer else { return }
self.scrollScrubber = ScrollScrubber(frame: frameForScrollScrubber(), withReaderContainer: readerContainer)
self.scrollScrubber?.delegate = self
if let scrollScrubber = scrollScrubber {
view.addSubview(scrollScrubber.slider)
}
}
override open func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
configureNavBar()
// Update pages
pagesForCurrentPage(currentPage)
pageIndicatorView?.reloadView(updateShadow: true)
}
override open func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
screenBounds = self.getScreenBounds()
loadingView.center = view.center
setPageSize(UIApplication.shared.statusBarOrientation)
updateSubviewFrames()
}
// MARK: Layout
/**
Enable or disable the scrolling between chapters (`FolioReaderPage`s). If this is enabled it's only possible to read the current chapter. If another chapter should be displayed is has to be triggered programmatically with `changePageWith`.
- parameter scrollEnabled: `Bool` which enables or disables the scrolling between `FolioReaderPage`s.
*/
open func enableScrollBetweenChapters(scrollEnabled: Bool) {
self.collectionView.isScrollEnabled = scrollEnabled
}
fileprivate func updateSubviewFrames() {
self.pageIndicatorView?.frame = self.frameForPageIndicatorView()
self.scrollScrubber?.frame = self.frameForScrollScrubber()
}
fileprivate func frameForPageIndicatorView() -> CGRect {
var bounds = CGRect(x: 0, y: screenBounds.size.height-pageIndicatorHeight, width: screenBounds.size.width, height: pageIndicatorHeight)
if #available(iOS 11.0, *) {
bounds.size.height = bounds.size.height + view.safeAreaInsets.bottom
}
return bounds
}
fileprivate func frameForScrollScrubber() -> CGRect {
let scrubberY: CGFloat = ((self.readerConfig.shouldHideNavigationOnTap == true || self.readerConfig.hideBars == true) ? 50 : 74)
return CGRect(x: self.pageWidth + 10, y: scrubberY, width: 40, height: (self.pageHeight - 100))
}
func configureNavBar() {
let greenColor = UIColor(red:0.00, green:0.51, blue:0.53, alpha:1.00)
let navBackground = folioReader.isNight(self.readerConfig.nightModeMenuBackground, greenColor)
let tintColor = folioReader.isNight(greenColor, UIColor.white)// readerConfig.tintColor
let navText = tintColor// folioReader.isNight(UIColor.white, UIColor.black)
let font = UIFont(name: "Avenir-Light", size: 17)!
setTranslucentNavigation(color: navBackground, tintColor: tintColor, titleColor: navText, andFont: font)
}
func configureNavBarButtons() {
// Navbar buttons
let shareIcon = UIImage(readerImageNamed: "icon-navbar-share")//.ignoreSystemTint(withConfiguration: self.readerConfig)
let audioIcon = UIImage(readerImageNamed: "icon-navbar-tts")//?.ignoreSystemTint(withConfiguration: self.readerConfig) //man-speech-icon
let closeIcon = UIImage(readerImageNamed: "icon-navbar-close")//?.ignoreSystemTint(withConfiguration: self.readerConfig)
let tocIcon = UIImage(readerImageNamed: "icon-navbar-toc")//?.ignoreSystemTint(withConfiguration: self.readerConfig)
let fontIcon = UIImage(readerImageNamed: "icon-navbar-font")//?.ignoreSystemTint(withConfiguration: self.readerConfig)
let space = 70 as CGFloat
let menu = UIBarButtonItem(image: closeIcon, style: .plain, target: self, action:#selector(closeReader(_:)))
let toc = UIBarButtonItem(image: tocIcon, style: .plain, target: self, action:#selector(presentChapterList(_:)))
navigationItem.leftBarButtonItems = [menu, toc]
var rightBarIcons = [UIBarButtonItem]()
if (self.readerConfig.allowSharing == true) {
rightBarIcons.append(UIBarButtonItem(image: shareIcon, style: .plain, target: self, action:#selector(shareChapter(_:))))
}
if self.book.hasAudio || self.readerConfig.enableTTS {
rightBarIcons.append(UIBarButtonItem(image: audioIcon, style: .plain, target: self, action:#selector(presentPlayerMenu(_:))))
}
let font = UIBarButtonItem(image: fontIcon, style: .plain, target: self, action: #selector(presentFontsMenu))
font.width = space
rightBarIcons.append(contentsOf: [font])
navigationItem.rightBarButtonItems = rightBarIcons
if(self.readerConfig.displayTitle){
navigationItem.title = book.title
}
}
func reloadData() {
self.loadingView.stopAnimating()
self.totalPages = book.spine.spineReferences.count
self.collectionView.reloadData()
self.configureNavBarButtons()
self.setCollectionViewProgressiveDirection()
if self.readerConfig.loadSavedPositionForCurrentBook {
guard let position = folioReader.savedPositionForCurrentBook, let pageNumber = position["pageNumber"] as? Int, pageNumber > 0 else {
self.currentPageNumber = 1
return
}
self.changePageWith(page: pageNumber)
self.currentPageNumber = pageNumber
}
}
// MARK: Change page progressive direction
private func transformViewForRTL(_ view: UIView?) {
if folioReader.needsRTLChange {
view?.transform = CGAffineTransform(scaleX: -1, y: 1)
} else {
view?.transform = CGAffineTransform.identity
}
}
func setCollectionViewProgressiveDirection() {
self.transformViewForRTL(self.collectionView)
}
func setPageProgressiveDirection(_ page: FolioReaderPage) {
self.transformViewForRTL(page)
}
// MARK: Change layout orientation
/// Get internal page offset before layout change
private func updatePageOffsetRate() {
guard let currentPage = self.currentPage, let webView = currentPage.webView else {
return
}
let pageScrollView = webView.scrollView
let contentSize = pageScrollView.contentSize.forDirection(withConfiguration: self.readerConfig)
let contentOffset = pageScrollView.contentOffset.forDirection(withConfiguration: self.readerConfig)
self.pageOffsetRate = (contentSize != 0 ? (contentOffset / contentSize) : 0)
}
func setScrollDirection(_ direction: FolioReaderScrollDirection) {
guard let currentPage = self.currentPage, let webView = currentPage.webView else {
return
}
let pageScrollView = webView.scrollView
// Get internal page offset before layout change
self.updatePageOffsetRate()
// Change layout
self.readerConfig.scrollDirection = direction
self.collectionViewLayout.scrollDirection = .direction(withConfiguration: self.readerConfig)
self.currentPage?.setNeedsLayout()
self.collectionView.collectionViewLayout.invalidateLayout()
self.collectionView.setContentOffset(frameForPage(self.currentPageNumber).origin, animated: false)
// Page progressive direction
self.setCollectionViewProgressiveDirection()
delay(0.2) { self.setPageProgressiveDirection(currentPage) }
/**
* This delay is needed because the page will not be ready yet
* so the delay wait until layout finished the changes.
*/
delay(0.1) {
var pageOffset = (pageScrollView.contentSize.forDirection(withConfiguration: self.readerConfig) * self.pageOffsetRate)
// Fix the offset for paged scroll
if (self.readerConfig.scrollDirection == .horizontal && self.pageWidth != 0) {
let page = round(pageOffset / self.pageWidth)
pageOffset = (page * self.pageWidth)
}
let pageOffsetPoint = self.readerConfig.isDirection(CGPoint(x: 0, y: pageOffset), CGPoint(x: pageOffset, y: 0), CGPoint(x: 0, y: pageOffset))
pageScrollView.setContentOffset(pageOffsetPoint, animated: true)
}
}
// MARK: Status bar and Navigation bar
func hideBars() {
guard self.readerConfig.shouldHideNavigationOnTap == true else {
return
}
self.updateBarsStatus(true)
}
func showBars() {
self.configureNavBar()
self.updateBarsStatus(false)
}
func toggleBars() {
guard self.readerConfig.shouldHideNavigationOnTap == true else {
return
}
let shouldHide = !self.navigationController!.isNavigationBarHidden
if shouldHide == false {
self.configureNavBar()
}
self.updateBarsStatus(shouldHide)
}
private func updateBarsStatus(_ shouldHide: Bool, shouldShowIndicator: Bool = false) {
guard let readerContainer = readerContainer else { return }
readerContainer.shouldHideStatusBar = shouldHide
UIView.animate(withDuration: 0.25, animations: {
readerContainer.setNeedsStatusBarAppearanceUpdate()
// Show minutes indicator
if (shouldShowIndicator == true) {
self.pageIndicatorView?.minutesLabel.alpha = shouldHide ? 0 : 1
}
})
self.navigationController?.setNavigationBarHidden(shouldHide, animated: true)
}
// MARK: UICollectionViewDataSource
open func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return totalPages
}
open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let reuseableCell = collectionView.dequeueReusableCell(withReuseIdentifier: kReuseCellIdentifier, for: indexPath) as? FolioReaderPage
return self.configure(readerPageCell: reuseableCell, atIndexPath: indexPath)
}
private func configure(readerPageCell cell: FolioReaderPage?, atIndexPath indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = cell, let readerContainer = readerContainer else {
return UICollectionViewCell()
}
var isNextChapter = true
if let lastRow = lastRow {
isNextChapter = lastRow < indexPath.row
}
if changingChapter {
isNextChapter = true
}
lastRow = indexPath.row
cell.setup(withReaderContainer: readerContainer)
cell.pageNumber = indexPath.row+1
cell.webView?.scrollView.delegate = self
if #available(iOS 11.0, *) {
cell.webView?.scrollView.contentInsetAdjustmentBehavior = .never
}
cell.webView?.setupScrollDirection()
cell.webView?.frame = cell.webViewFrame()
cell.delegate = self
cell.backgroundColor = .clear
setPageProgressiveDirection(cell)
// Configure the cell
let resource = self.book.spine.spineReferences[indexPath.row].resource
guard var html = try? String(contentsOfFile: resource.fullHref, encoding: String.Encoding.utf8) else {
return cell
}
let mediaOverlayStyleColors = "\"\(self.readerConfig.mediaOverlayColor.hexString(false))\", \"\(self.readerConfig.mediaOverlayColor.highlightColor().hexString(false))\""
// Inject CSS
let jsFilePath = Bundle.frameworkBundle().path(forResource: "Bridge", ofType: "js")
let cssFilePath = Bundle.frameworkBundle().path(forResource: "Style", ofType: "css")
let cssTag = ""
let jsTag = "" +
""
let toInject = "\n\(cssTag)\n\(jsTag)\n"
html = html.replacingOccurrences(of: "", with: toInject)
// Font class name
var classes = folioReader.currentFont.cssIdentifier
classes += " " + folioReader.currentMediaOverlayStyle.className()
// Night mode
if folioReader.nightMode {
classes += " nightMode"
}
// Font Size
classes += " \(folioReader.currentFontSize.cssIdentifier(sliderType: .font))"
classes += " \(folioReader.currentMarginSize.cssIdentifier(sliderType: .margin))"
classes += " \(folioReader.currentInterlineSize.cssIdentifier(sliderType: .interline))"
html = html.replacingOccurrences(of: " CGSize {
var size = CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
if #available(iOS 11.0, *) {
let orientation = UIDevice.current.orientation
if orientation == .portrait || orientation == .portraitUpsideDown {
if readerConfig.scrollDirection == .horizontal {
size.height = size.height - view.safeAreaInsets.bottom
}
}
}
return size
}
// MARK: - Device rotation
override open func willRotate(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
guard folioReader.isReaderReady else { return }
setPageSize(toInterfaceOrientation)
updateCurrentPage()
if self.currentOrientation == nil || (self.currentOrientation?.isPortrait != toInterfaceOrientation.isPortrait) {
var pageIndicatorFrame = pageIndicatorView?.frame
pageIndicatorFrame?.origin.y = ((screenBounds.size.height < screenBounds.size.width) ? (self.collectionView.frame.height - pageIndicatorHeight) : (self.collectionView.frame.width - pageIndicatorHeight))
pageIndicatorFrame?.origin.x = 0
pageIndicatorFrame?.size.width = ((screenBounds.size.height < screenBounds.size.width) ? (self.collectionView.frame.width) : (self.collectionView.frame.height))
pageIndicatorFrame?.size.height = pageIndicatorHeight
var scrollScrubberFrame = scrollScrubber?.slider.frame;
scrollScrubberFrame?.origin.x = ((screenBounds.size.height < screenBounds.size.width) ? (screenBounds.size.width - 100) : (screenBounds.size.height + 10))
scrollScrubberFrame?.size.height = ((screenBounds.size.height < screenBounds.size.width) ? (self.collectionView.frame.height - 100) : (self.collectionView.frame.width - 100))
self.collectionView.collectionViewLayout.invalidateLayout()
UIView.animate(withDuration: duration, animations: {
// Adjust page indicator view
if let pageIndicatorFrame = pageIndicatorFrame {
self.pageIndicatorView?.frame = pageIndicatorFrame
self.pageIndicatorView?.reloadView(updateShadow: true)
}
// Adjust scroll scrubber slider
if let scrollScrubberFrame = scrollScrubberFrame {
self.scrollScrubber?.slider.frame = scrollScrubberFrame
}
// Adjust collectionView
self.collectionView.contentSize = self.readerConfig.isDirection(
CGSize(width: self.pageWidth, height: self.pageHeight * CGFloat(self.totalPages)),
CGSize(width: self.pageWidth * CGFloat(self.totalPages), height: self.pageHeight),
CGSize(width: self.pageWidth * CGFloat(self.totalPages), height: self.pageHeight)
)
self.collectionView.setContentOffset(self.frameForPage(self.currentPageNumber).origin, animated: false)
self.collectionView.collectionViewLayout.invalidateLayout()
// Adjust internal page offset
self.updatePageOffsetRate()
})
}
self.currentOrientation = toInterfaceOrientation
}
override open func didRotate(from fromInterfaceOrientation: UIInterfaceOrientation) {
guard folioReader.isReaderReady == true, let currentPage = currentPage else {
return
}
// Update pages
pagesForCurrentPage(currentPage)
currentPage.refreshPageMode()
scrollScrubber?.setSliderVal()
// After rotation fix internal page offset
var pageOffset = (currentPage.webView?.scrollView.contentSize.forDirection(withConfiguration: self.readerConfig) ?? 0) * pageOffsetRate
// Fix the offset for paged scroll
if (self.readerConfig.scrollDirection == .horizontal && self.pageWidth != 0) {
let page = round(pageOffset / self.pageWidth)
pageOffset = page * self.pageWidth
}
let pageOffsetPoint = self.readerConfig.isDirection(CGPoint(x: 0, y: pageOffset), CGPoint(x: pageOffset, y: 0), CGPoint(x: 0, y: pageOffset))
currentPage.webView?.scrollView.setContentOffset(pageOffsetPoint, animated: true)
}
override open func willAnimateRotation(to toInterfaceOrientation: UIInterfaceOrientation, duration: TimeInterval) {
guard folioReader.isReaderReady else {
return
}
self.collectionView.scrollToItem(at: IndexPath(row: self.currentPageNumber - 1, section: 0), at: UICollectionViewScrollPosition(), animated: false)
if (self.currentPageNumber + 1) >= totalPages {
UIView.animate(withDuration: duration, animations: {
self.collectionView.setContentOffset(self.frameForPage(self.currentPageNumber).origin, animated: false)
})
}
}
// MARK: - Page
func setPageSize(_ orientation: UIInterfaceOrientation) {
guard orientation.isPortrait else {
if screenBounds.size.width > screenBounds.size.height {
self.pageWidth = screenBounds.size.width
self.pageHeight = screenBounds.size.height
} else {
self.pageWidth = screenBounds.size.height
self.pageHeight = screenBounds.size.width
}
return
}
if screenBounds.size.width < screenBounds.size.height {
self.pageWidth = screenBounds.size.width
self.pageHeight = screenBounds.size.height
} else {
self.pageWidth = screenBounds.size.height
self.pageHeight = screenBounds.size.width
}
}
func updateCurrentPage(_ page: FolioReaderPage? = nil, completion: (() -> Void)? = nil) {
if let page = page {
currentPage = page
self.previousPageNumber = page.pageNumber-1
self.currentPageNumber = page.pageNumber
} else {
let currentIndexPath = getCurrentIndexPath()
currentPage = collectionView.cellForItem(at: currentIndexPath) as? FolioReaderPage
self.previousPageNumber = currentIndexPath.row
self.currentPageNumber = currentIndexPath.row+1
}
self.nextPageNumber = (((self.currentPageNumber + 1) <= totalPages) ? (self.currentPageNumber + 1) : self.currentPageNumber)
// Set pages
guard let currentPage = currentPage else {
completion?()
return
}
scrollScrubber?.setSliderVal()
if let readingTime = currentPage.webView?.js("getReadingTime()") {
pageIndicatorView?.totalMinutes = Int(readingTime)!
} else {
pageIndicatorView?.totalMinutes = 0
}
pagesForCurrentPage(currentPage)
delegate?.pageDidAppear?(currentPage)
delegate?.pageItemChanged?(self.getCurrentPageItemNumber())
completion?()
}
func pagesForCurrentPage(_ page: FolioReaderPage?) {
guard let page = page, let webView = page.webView else { return }
let pageSize = self.readerConfig.isDirection(pageHeight, self.pageWidth, pageHeight)
let contentSize = page.webView?.scrollView.contentSize.forDirection(withConfiguration: self.readerConfig) ?? 0
self.pageIndicatorView?.totalPages = ((pageSize != 0) ? Int(ceil(contentSize / pageSize)) : 0)
let pageOffSet = self.readerConfig.isDirection(webView.scrollView.contentOffset.x, webView.scrollView.contentOffset.x, webView.scrollView.contentOffset.y)
let webViewPage = pageForOffset(pageOffSet, pageHeight: pageSize)
self.pageIndicatorView?.currentPage = webViewPage
}
func pageForOffset(_ offset: CGFloat, pageHeight height: CGFloat) -> Int {
guard (height != 0) else {
return 0
}
let page = Int(ceil(offset / height))+1
return page
}
func getCurrentIndexPath() -> IndexPath {
let indexPaths = collectionView.indexPathsForVisibleItems
var indexPath = IndexPath()
if indexPaths.count > 1 {
let first = indexPaths.first!
let last = indexPaths.last!
switch self.pageScrollDirection {
case .up, .left:
if first.compare(last) == .orderedAscending {
indexPath = last
} else {
indexPath = first
}
default:
if first.compare(last) == .orderedAscending {
indexPath = first
} else {
indexPath = last
}
}
} else {
indexPath = indexPaths.first ?? IndexPath(row: 0, section: 0)
}
return indexPath
}
func frameForPage(_ page: Int) -> CGRect {
return self.readerConfig.isDirection(
CGRect(x: 0, y: self.pageHeight * CGFloat(page-1), width: self.pageWidth, height: self.pageHeight),
CGRect(x: self.pageWidth * CGFloat(page-1), y: 0, width: self.pageWidth, height: self.pageHeight),
CGRect(x: 0, y: self.pageHeight * CGFloat(page-1), width: self.pageWidth, height: self.pageHeight)
)
}
open func changePageWith(page: Int, andFragment fragment: String, animated: Bool = false, completion: (() -> Void)? = nil) {
if (self.currentPageNumber == page) {
if let currentPage = currentPage , fragment != "" {
currentPage.handleAnchor(fragment, avoidBeginningAnchors: true, animated: animated)
}
completion?()
} else {
tempFragment = fragment
changePageWith(page: page, animated: animated, completion: { () -> Void in
self.updateCurrentPage {
completion?()
}
})
}
}
open func changePageWith(href: String, animated: Bool = false, completion: (() -> Void)? = nil) {
let item = findPageByHref(href)
let indexPath = IndexPath(row: item, section: 0)
changePageWith(indexPath: indexPath, animated: animated, completion: { () -> Void in
self.updateCurrentPage {
completion?()
}
})
}
open func changePageWith(href: String, andAudioMarkID markID: String) {
if recentlyScrolled { return } // if user recently scrolled, do not change pages or scroll the webview
guard let currentPage = currentPage else { return }
let item = findPageByHref(href)
let pageUpdateNeeded = item+1 != currentPage.pageNumber
let indexPath = IndexPath(row: item, section: 0)
changePageWith(indexPath: indexPath, animated: true) { () -> Void in
if pageUpdateNeeded {
self.updateCurrentPage {
currentPage.audioMarkID(markID)
}
} else {
currentPage.audioMarkID(markID)
}
}
}
open func changePageWith(indexPath: IndexPath, animated: Bool = false, completion: (() -> Void)? = nil) {
guard indexPathIsValid(indexPath) else {
print("ERROR: Attempt to scroll to invalid index path")
completion?()
return
}
UIView.animate(withDuration: animated ? 0.3 : 0, delay: 0, options: UIViewAnimationOptions(), animations: { () -> Void in
self.collectionView.scrollToItem(at: indexPath, at: .direction(withConfiguration: self.readerConfig), animated: false)
}) { (finished: Bool) -> Void in
completion?()
}
}
open func changePageWith(href: String, pageItem: Int, animated: Bool = false, completion: (() -> Void)? = nil) {
changePageWith(href: href, animated: animated) {
self.changePageItem(to: pageItem)
}
}
func indexPathIsValid(_ indexPath: IndexPath) -> Bool {
let section = indexPath.section
let row = indexPath.row
let lastSectionIndex = numberOfSections(in: collectionView) - 1
//Make sure the specified section exists
if section > lastSectionIndex {
return false
}
let rowCount = self.collectionView(collectionView, numberOfItemsInSection: indexPath.section) - 1
return row <= rowCount
}
open func isLastPage() -> Bool{
return (currentPageNumber == self.nextPageNumber)
}
public func changePageToNext(_ completion: (() -> Void)? = nil) {
changePageWith(page: self.nextPageNumber, animated: true) { () -> Void in
completion?()
}
}
public func changePageToPrevious(_ completion: (() -> Void)? = nil) {
changePageWith(page: self.previousPageNumber, animated: true) { () -> Void in
completion?()
}
}
public func changePageItemToNext(_ completion: (() -> Void)? = nil) {
// TODO: It was implemented for horizontal orientation.
// Need check page orientation (v/h) and make correct calc for vertical
guard
let cell = collectionView.cellForItem(at: getCurrentIndexPath()) as? FolioReaderPage,
let contentOffset = cell.webView?.scrollView.contentOffset,
let contentOffsetXLimit = cell.webView?.scrollView.contentSize.width else {
completion?()
return
}
let cellSize = cell.frame.size
let contentOffsetX = contentOffset.x + cellSize.width
if contentOffsetX >= contentOffsetXLimit {
changePageToNext(completion)
} else {
cell.scrollPageToOffset(contentOffsetX, animated: true)
}
completion?()
}
public func getCurrentPageItemNumber() -> Int {
guard let page = currentPage, let webView = page.webView else { return 0 }
let pageSize = readerConfig.isDirection(pageHeight, pageWidth, pageHeight)
let pageOffSet = readerConfig.isDirection(webView.scrollView.contentOffset.x, webView.scrollView.contentOffset.x, webView.scrollView.contentOffset.y)
let webViewPage = pageForOffset(pageOffSet, pageHeight: pageSize)
return webViewPage
}
public func changePageItemToPrevious(_ completion: (() -> Void)? = nil) {
// TODO: It was implemented for horizontal orientation.
// Need check page orientation (v/h) and make correct calc for vertical
guard
let cell = collectionView.cellForItem(at: getCurrentIndexPath()) as? FolioReaderPage,
let contentOffset = cell.webView?.scrollView.contentOffset else {
completion?()
return
}
let cellSize = cell.frame.size
let contentOffsetX = contentOffset.x - cellSize.width
if contentOffsetX < 0 {
changePageToPrevious(completion)
} else {
cell.scrollPageToOffset(contentOffsetX, animated: true)
}
completion?()
}
public func changePageItemToLast(animated: Bool = true, _ completion: (() -> Void)? = nil) {
// TODO: It was implemented for horizontal orientation.
// Need check page orientation (v/h) and make correct calc for vertical
guard
let cell = collectionView.cellForItem(at: getCurrentIndexPath()) as? FolioReaderPage,
let contentSize = cell.webView?.scrollView.contentSize else {
completion?()
return
}
let cellSize = cell.frame.size
var contentOffsetX: CGFloat = 0.0
if contentSize.width > 0 && cellSize.width > 0 {
contentOffsetX = (cellSize.width * (contentSize.width / cellSize.width)) - cellSize.width
}
if contentOffsetX < 0 {
contentOffsetX = 0
}
cell.scrollPageToOffset(contentOffsetX, animated: animated)
completion?()
}
public func changePageItem(to: Int, animated: Bool = true, completion: (() -> Void)? = nil) {
// TODO: It was implemented for horizontal orientation.
// Need check page orientation (v/h) and make correct calc for vertical
guard
let cell = collectionView.cellForItem(at: getCurrentIndexPath()) as? FolioReaderPage,
let contentSize = cell.webView?.scrollView.contentSize else {
delegate?.pageItemChanged?(getCurrentPageItemNumber())
completion?()
return
}
let cellSize = cell.frame.size
var contentOffsetX: CGFloat = 0.0
if contentSize.width > 0 && cellSize.width > 0 {
contentOffsetX = (cellSize.width * CGFloat(to)) - cellSize.width
}
if contentOffsetX > contentSize.width {
contentOffsetX = contentSize.width - cellSize.width
}
if contentOffsetX < 0 {
contentOffsetX = 0
}
UIView.animate(withDuration: animated ? 0.3 : 0, delay: 0, options: UIViewAnimationOptions(), animations: { () -> Void in
cell.scrollPageToOffset(contentOffsetX, animated: animated)
}) { (finished: Bool) -> Void in
self.updateCurrentPage {
completion?()
}
}
}
/**
Find a page by FRTocReference.
*/
public func findPageByResource(_ reference: FRTocReference) -> Int {
var count = 0
for item in self.book.spine.spineReferences {
if let resource = reference.resource, item.resource == resource {
return count
}
count += 1
}
return count
}
/**
Find a page by href.
*/
public func findPageByHref(_ href: String) -> Int {
var count = 0
for item in self.book.spine.spineReferences {
if item.resource.href == href {
return count
}
count += 1
}
return count
}
/**
Find and return the current chapter resource.
*/
public func getCurrentChapter() -> FRResource? {
for item in self.book.flatTableOfContents {
if
let reference = self.book.spine.spineReferences[safe: (self.currentPageNumber - 1)],
let resource = item.resource,
(resource == reference.resource) {
return item.resource
}
}
return nil
}
/**
Return the current chapter progress based on current chapter and total of chapters.
*/
public func getCurrentChapterProgress() -> CGFloat {
let total = totalPages
let current = currentPageNumber
if total == 0 {
return 0
}
return CGFloat((100 * current) / total)
}
/**
Find and return the current chapter name.
*/
public func getCurrentChapterName() -> String? {
for item in self.book.flatTableOfContents {
guard
let reference = self.book.spine.spineReferences[safe: (self.currentPageNumber - 1)],
let resource = item.resource,
(resource == reference.resource),
let title = item.title else {
continue
}
return title
}
return nil
}
// MARK: Public page methods
/**
Changes the current page of the reader.
- parameter page: The target page index. Note: The page index starts at 1 (and not 0).
- parameter animated: En-/Disables the animation of the page change.
- parameter completion: A Closure which is called if the page change is completed.
*/
public func changePageWith(page: Int, animated: Bool = false, completion: (() -> Void)? = nil) {
if page > 0 && page-1 < totalPages {
let indexPath = IndexPath(row: page-1, section: 0)
changePageWith(indexPath: indexPath, animated: animated, completion: { () -> Void in
self.updateCurrentPage {
completion?()
}
})
}
}
// MARK: - Audio Playing
func audioMark(href: String, fragmentID: String) {
changePageWith(href: href, andAudioMarkID: fragmentID)
}
// MARK: - Sharing
/**
Sharing chapter method.
*/
@objc func shareChapter(_ sender: UIBarButtonItem) {
guard let currentPage = currentPage else { return }
if let chapterText = currentPage.webView?.js("getBodyText()") {
let htmlText = chapterText.replacingOccurrences(of: "[\\n\\r]+", with: "
", options: .regularExpression)
var subject = readerConfig.localizedShareChapterSubject
var html = ""
var text = ""
var bookTitle = ""
var chapterName = ""
var authorName = ""
var shareItems = [AnyObject]()
// Get book title
if let title = self.book.title {
bookTitle = title
subject += " “\(title)”"
}
// Get chapter name
if let chapter = getCurrentChapterName() {
chapterName = chapter
}
// Get author name
if let author = self.book.metadata.creators.first {
authorName = author.name
}
// Sharing html and text
html = "
\(htmlText)
"+readerConfig.localizedShareAllExcerptsFrom+"
" html += "\(bookTitle)\(chapterName)
" html += "\(string)
"+readerConfig.localizedShareAllExcerptsFrom+"
" html += "\(bookTitle)