--- /dev/null
+//
+// Extensions.swift
+// Pods
+//
+// Created by Kevin Delord on 01/04/17.
+//
+//
+
+import Foundation
+import UIKit
+
+extension UICollectionViewScrollDirection {
+ static func direction(withConfiguration readerConfig: FolioReaderConfig) -> UICollectionViewScrollDirection {
+ return readerConfig.isDirection(.vertical, .horizontal, .horizontal)
+ }
+}
+
+extension UICollectionViewScrollPosition {
+ static func direction(withConfiguration readerConfig: FolioReaderConfig) -> UICollectionViewScrollPosition {
+ return readerConfig.isDirection(.top, .left, .left)
+ }
+}
+
+extension CGPoint {
+ func forDirection(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> CGFloat {
+ return readerConfig.isDirection(self.y, self.x, ((scrollType == .page) ? self.y : self.x))
+ }
+}
+
+extension CGSize {
+ func forDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat {
+ return readerConfig.isDirection(height, width, height)
+ }
+
+ func forReverseDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat {
+ return readerConfig.isDirection(width, height, width)
+ }
+}
+
+extension CGRect {
+ func forDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat {
+ return readerConfig.isDirection(height, width, height)
+ }
+}
+
+extension ScrollDirection {
+ static func negative(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> ScrollDirection {
+ return readerConfig.isDirection(.down, .right, .right)
+ }
+
+ static func positive(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> ScrollDirection {
+ return readerConfig.isDirection(.up, .left, .left)
+ }
+}
+
+// MARK: Helpers
+
+/**
+ Delay function
+ From: http://stackoverflow.com/a/24318861/517707
+
+ - parameter delay: Delay in seconds
+ - parameter closure: Closure
+ */
+func delay(_ delay:Double, closure:@escaping ()->()) {
+ DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
+}
+
+
+// MARK: - Extensions
+
+internal extension Bundle {
+ class func frameworkBundle() -> Bundle {
+ return Bundle(for: FolioReader.self)
+ }
+}
+
+internal extension UIColor {
+ convenience init(rgba: String) {
+ var red: CGFloat = 0.0
+ var green: CGFloat = 0.0
+ var blue: CGFloat = 0.0
+ var alpha: CGFloat = 1.0
+
+ if rgba.hasPrefix("#") {
+ let index = rgba.index(rgba.startIndex, offsetBy: 1)
+ let hex = String(rgba[index...])
+ let scanner = Scanner(string: hex)
+ var hexValue: CUnsignedLongLong = 0
+ if scanner.scanHexInt64(&hexValue) {
+ switch (hex.count) {
+ case 3:
+ red = CGFloat((hexValue & 0xF00) >> 8) / 15.0
+ green = CGFloat((hexValue & 0x0F0) >> 4) / 15.0
+ blue = CGFloat(hexValue & 0x00F) / 15.0
+ break
+ case 4:
+ red = CGFloat((hexValue & 0xF000) >> 12) / 15.0
+ green = CGFloat((hexValue & 0x0F00) >> 8) / 15.0
+ blue = CGFloat((hexValue & 0x00F0) >> 4) / 15.0
+ alpha = CGFloat(hexValue & 0x000F) / 15.0
+ break
+ case 6:
+ red = CGFloat((hexValue & 0xFF0000) >> 16) / 255.0
+ green = CGFloat((hexValue & 0x00FF00) >> 8) / 255.0
+ blue = CGFloat(hexValue & 0x0000FF) / 255.0
+ break
+ case 8:
+ red = CGFloat((hexValue & 0xFF000000) >> 24) / 255.0
+ green = CGFloat((hexValue & 0x00FF0000) >> 16) / 255.0
+ blue = CGFloat((hexValue & 0x0000FF00) >> 8) / 255.0
+ alpha = CGFloat(hexValue & 0x000000FF) / 255.0
+ break
+ default:
+ print("Invalid RGB string, number of characters after '#' should be either 3, 4, 6 or 8", terminator: "")
+ break
+ }
+ } else {
+ print("Scan hex error")
+ }
+ } else {
+ print("Invalid RGB string, missing '#' as prefix", terminator: "")
+ }
+ self.init(red:red, green:green, blue:blue, alpha:alpha)
+ }
+
+ //
+ /// Hex string of a UIColor instance.
+ ///
+ /// from: https://github.com/yeahdongcn/UIColor-Hex-Swift
+ ///
+ /// - Parameter includeAlpha: Whether the alpha should be included.
+ /// - Returns: Hexa string
+ func hexString(_ includeAlpha: Bool) -> String {
+ var r: CGFloat = 0
+ var g: CGFloat = 0
+ var b: CGFloat = 0
+ var a: CGFloat = 0
+ self.getRed(&r, green: &g, blue: &b, alpha: &a)
+
+ if (includeAlpha == true) {
+ return String(format: "#%02X%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255), Int(a * 255))
+ } else {
+ return String(format: "#%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255))
+ }
+ }
+
+ // MARK: - color shades
+ // https://gist.github.com/mbigatti/c6be210a6bbc0ff25972
+
+ func highlightColor() -> UIColor {
+
+ var hue : CGFloat = 0
+ var saturation : CGFloat = 0
+ var brightness : CGFloat = 0
+ var alpha : CGFloat = 0
+
+ if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
+ return UIColor(hue: hue, saturation: 0.30, brightness: 1, alpha: alpha)
+ } else {
+ return self;
+ }
+
+ }
+
+ /**
+ Returns a lighter color by the provided percentage
+
+ :param: lighting percent percentage
+ :returns: lighter UIColor
+ */
+ func lighterColor(_ percent : Double) -> UIColor {
+ return colorWithBrightnessFactor(CGFloat(1 + percent));
+ }
+
+ /**
+ Returns a darker color by the provided percentage
+
+ :param: darking percent percentage
+ :returns: darker UIColor
+ */
+ func darkerColor(_ percent : Double) -> UIColor {
+ return colorWithBrightnessFactor(CGFloat(1 - percent));
+ }
+
+ /**
+ Return a modified color using the brightness factor provided
+
+ :param: factor brightness factor
+ :returns: modified color
+ */
+ func colorWithBrightnessFactor(_ factor: CGFloat) -> UIColor {
+ var hue : CGFloat = 0
+ var saturation : CGFloat = 0
+ var brightness : CGFloat = 0
+ var alpha : CGFloat = 0
+
+ if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
+ return UIColor(hue: hue, saturation: saturation, brightness: brightness * factor, alpha: alpha)
+ } else {
+ return self;
+ }
+ }
+}
+
+internal extension String {
+ /// Truncates the string to length number of characters and
+ /// appends optional trailing string if longer
+ func truncate(_ length: Int, trailing: String? = nil) -> String {
+ if count > length {
+ let indexOfText = index(startIndex, offsetBy: length)
+ return String(self[..<indexOfText])
+ } else {
+ return self
+ }
+ }
+
+ func stripHtml() -> String {
+ return self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression)
+ }
+
+ func stripLineBreaks() -> String {
+ return self.replacingOccurrences(of: "\n", with: "", options: .regularExpression)
+ }
+
+ /**
+ Converts a clock time such as `0:05:01.2` to seconds (`Double`)
+
+ Looks for media overlay clock formats as specified [here][1]
+
+ - Note: this may not be the most efficient way of doing this. It can be improved later on.
+
+ - Returns: seconds as `Double`
+
+ [1]: http://www.idpf.org/epub/301/spec/epub-mediaoverlays.html#app-clock-examples
+ */
+ func clockTimeToSeconds() -> Double {
+
+ let val = self.trimmingCharacters(in: CharacterSet.whitespaces)
+
+ if( val.isEmpty ){ return 0 }
+
+ let formats = [
+ "HH:mm:ss.SSS" : "^\\d{1,2}:\\d{2}:\\d{2}\\.\\d{1,3}$",
+ "HH:mm:ss" : "^\\d{1,2}:\\d{2}:\\d{2}$",
+ "mm:ss.SSS" : "^\\d{1,2}:\\d{2}\\.\\d{1,3}$",
+ "mm:ss" : "^\\d{1,2}:\\d{2}$",
+ "ss.SSS" : "^\\d{1,2}\\.\\d{1,3}$",
+ ]
+
+ // search for normal duration formats such as `00:05:01.2`
+ for (format, pattern) in formats {
+
+ if val.range(of: pattern, options: .regularExpression) != nil {
+
+ let formatter = DateFormatter()
+ formatter.dateFormat = format
+ let time = formatter.date(from: val)
+
+ if( time == nil ){ return 0 }
+
+ formatter.dateFormat = "ss.SSS"
+ let seconds = (formatter.string(from: time!) as NSString).doubleValue
+
+ formatter.dateFormat = "mm"
+ let minutes = (formatter.string(from: time!) as NSString).doubleValue
+
+ formatter.dateFormat = "HH"
+ let hours = (formatter.string(from: time!) as NSString).doubleValue
+
+ return seconds + (minutes*60) + (hours*60*60)
+ }
+ }
+
+ // if none of the more common formats match, check for other possible formats
+
+ // 2345ms
+ if val.range(of: "^\\d+ms$", options: .regularExpression) != nil{
+ return (val as NSString).doubleValue / 1000.0
+ }
+
+ // 7.25h
+ if val.range(of: "^\\d+(\\.\\d+)?h$", options: .regularExpression) != nil {
+ return (val as NSString).doubleValue * 60 * 60
+ }
+
+ // 13min
+ if val.range(of: "^\\d+(\\.\\d+)?min$", options: .regularExpression) != nil {
+ return (val as NSString).doubleValue * 60
+ }
+
+ return 0
+ }
+
+ func clockTimeToMinutesString() -> String {
+
+ let val = clockTimeToSeconds()
+
+ let min = floor(val / 60)
+ let sec = floor(val.truncatingRemainder(dividingBy: 60))
+
+ return String(format: "%02.f:%02.f", min, sec)
+ }
+
+ // MARK: - NSString helpers
+
+ var lastPathComponent: String {
+ return (self as NSString).lastPathComponent
+ }
+
+ var deletingLastPathComponent: String {
+ return (self as NSString).deletingLastPathComponent
+ }
+
+ var deletingPathExtension: String {
+ return (self as NSString).deletingPathExtension
+ }
+
+ var pathExtension: String {
+ return (self as NSString).pathExtension
+ }
+
+ var abbreviatingWithTildeInPath: String {
+ return (self as NSString).abbreviatingWithTildeInPath
+ }
+
+ func appendingPathComponent(_ str: String) -> String {
+ return (self as NSString).appendingPathComponent(str)
+ }
+
+ func appendingPathExtension(_ str: String) -> String {
+ return (self as NSString).appendingPathExtension(str) ?? self+"."+str
+ }
+}
+
+internal extension UIImage {
+
+ convenience init?(readerImageNamed: String) {
+ self.init(named: readerImageNamed, in: Bundle.frameworkBundle(), compatibleWith: nil)
+ }
+
+ /// Forces the image to be colored with Reader Config tintColor
+ ///
+ /// - Parameter readerConfig: Current folio reader configuration.
+ /// - Returns: Returns a colored image
+ func ignoreSystemTint(withConfiguration readerConfig: FolioReaderConfig) -> UIImage? {
+ return self.imageTintColor(readerConfig.tintColor)?.withRenderingMode(.alwaysOriginal)
+ }
+
+ /**
+ Colorize the image with a color
+
+ - parameter tintColor: The input color
+ - returns: Returns a colored image
+ */
+ func imageTintColor(_ tintColor: UIColor) -> UIImage? {
+ UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
+
+ let context = UIGraphicsGetCurrentContext()
+ context?.translateBy(x: 0, y: self.size.height)
+ context?.scaleBy(x: 1.0, y: -1.0)
+ context?.setBlendMode(CGBlendMode.normal)
+
+ let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height) as CGRect
+ if let cgImage = self.cgImage {
+ context?.clip(to: rect, mask: cgImage)
+ }
+
+ tintColor.setFill()
+ context?.fill(rect)
+
+ let newImage = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+
+ return newImage
+ }
+
+ /**
+ Generate a image with a color
+
+ - parameter color: The input color
+ - returns: Returns a colored image
+ */
+ class func imageWithColor(_ color: UIColor?) -> UIImage {
+ let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
+ UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
+ let context = UIGraphicsGetCurrentContext()
+
+ if let color = color {
+ color.setFill()
+ } else {
+ UIColor.white.setFill()
+ }
+
+ context!.fill(rect)
+ let image = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+
+ return image!
+ }
+
+ /**
+ Generates a image with a `CALayer`
+
+ - parameter layer: The input `CALayer`
+ - returns: Return a rendered image
+ */
+ class func imageWithLayer(_ layer: CALayer) -> UIImage {
+ UIGraphicsBeginImageContextWithOptions(layer.bounds.size, layer.isOpaque, 0.0)
+ layer.render(in: UIGraphicsGetCurrentContext()!)
+ let img = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+ return img!
+ }
+
+ /**
+ Generates a image from a `UIView`
+
+ - parameter view: The input `UIView`
+ - returns: Return a rendered image
+ */
+ class func imageWithView(_ view: UIView) -> UIImage {
+ UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
+ view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
+ let img = UIGraphicsGetImageFromCurrentImageContext()
+ UIGraphicsEndImageContext()
+ return img!
+ }
+}
+
+internal extension UIViewController {
+
+ func setCloseButton(withConfiguration readerConfig: FolioReaderConfig) {
+ let closeImage = UIImage(readerImageNamed: "icon-navbar-close")?.ignoreSystemTint(withConfiguration: readerConfig)
+ self.navigationItem.leftBarButtonItem = UIBarButtonItem(image: closeImage, style: .plain, target: self, action: #selector(dismiss as () -> Void))
+ }
+
+ @objc func dismiss() {
+ self.dismiss(nil)
+ }
+
+ func dismiss(_ completion: (() -> Void)?) {
+ DispatchQueue.main.async {
+ self.dismiss(animated: true, completion: {
+ completion?()
+ })
+ }
+ }
+
+ // MARK: - NavigationBar
+
+ func setTransparentNavigation() {
+ let navBar = self.navigationController?.navigationBar
+ navBar?.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
+ navBar?.hideBottomHairline()
+ navBar?.isTranslucent = true
+ }
+
+ func setTranslucentNavigation(_ translucent: Bool = true, color: UIColor, tintColor: UIColor = UIColor.white, titleColor: UIColor = UIColor.black, andFont font: UIFont = UIFont.systemFont(ofSize: 17)) {
+ let navBar = self.navigationController?.navigationBar
+ navBar?.setBackgroundImage(UIImage.imageWithColor(color), for: UIBarMetrics.default)
+ navBar?.showBottomHairline()
+ navBar?.isTranslucent = translucent
+ navBar?.tintColor = tintColor
+ navBar?.titleTextAttributes = [NSAttributedStringKey.foregroundColor: titleColor, NSAttributedStringKey.font: font]
+ }
+}
+
+internal extension UINavigationBar {
+
+ func hideBottomHairline() {
+ let navigationBarImageView = hairlineImageViewInNavigationBar(self)
+ navigationBarImageView!.isHidden = true
+ }
+
+ func showBottomHairline() {
+ let navigationBarImageView = hairlineImageViewInNavigationBar(self)
+ navigationBarImageView!.isHidden = false
+ }
+
+ fileprivate func hairlineImageViewInNavigationBar(_ view: UIView) -> UIImageView? {
+ if view.isKind(of: UIImageView.self) && view.bounds.height <= 1.0 {
+ return (view as! UIImageView)
+ }
+
+ let subviews = (view.subviews)
+ for subview: UIView in subviews {
+ if let imageView: UIImageView = hairlineImageViewInNavigationBar(subview) {
+ return imageView
+ }
+ }
+ return nil
+ }
+}
+
+extension UINavigationController {
+
+ open override var preferredStatusBarStyle : UIStatusBarStyle {
+ guard let viewController = visibleViewController else { return .default }
+ return viewController.preferredStatusBarStyle
+ }
+
+ open override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
+ guard let viewController = visibleViewController else { return .portrait }
+ return viewController.supportedInterfaceOrientations
+ }
+
+ open override var shouldAutorotate : Bool {
+ guard let viewController = visibleViewController else { return false }
+ return viewController.shouldAutorotate
+ }
+}
+
+/**
+ This fixes iOS 9 crash
+ http://stackoverflow.com/a/32010520/517707
+ */
+extension UIAlertController {
+ open override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
+ return .portrait
+ }
+
+ open override var shouldAutorotate : Bool {
+ return false
+ }
+}
+
+extension Array {
+
+ /**
+ Return index if is safe, if not return nil
+ http://stackoverflow.com/a/30593673/517707
+ */
+ subscript(safe index: Int) -> Element? {
+ return indices ~= index ? self[index] : nil
+ }
+}