added iOS source code
[wl-app.git] / iOS / Pods / FolioReaderKit / Source / Extensions.swift
1 //
2 //  Extensions.swift
3 //  Pods
4 //
5 //  Created by Kevin Delord on 01/04/17.
6 //
7 //
8
9 import Foundation
10 import UIKit
11
12 extension UICollectionViewScrollDirection {
13     static func direction(withConfiguration readerConfig: FolioReaderConfig) -> UICollectionViewScrollDirection {
14         return readerConfig.isDirection(.vertical, .horizontal, .horizontal)
15     }
16 }
17
18 extension UICollectionViewScrollPosition {
19     static func direction(withConfiguration readerConfig: FolioReaderConfig) -> UICollectionViewScrollPosition {
20         return readerConfig.isDirection(.top, .left, .left)
21     }
22 }
23
24 extension CGPoint {
25     func forDirection(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> CGFloat {
26         return readerConfig.isDirection(self.y, self.x, ((scrollType == .page) ? self.y : self.x))
27     }
28 }
29
30 extension CGSize {
31     func forDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat {
32         return readerConfig.isDirection(height, width, height)
33     }
34
35     func forReverseDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat {
36         return readerConfig.isDirection(width, height, width)
37     }
38 }
39
40 extension CGRect {
41     func forDirection(withConfiguration readerConfig: FolioReaderConfig) -> CGFloat {
42         return readerConfig.isDirection(height, width, height)
43     }
44 }
45
46 extension ScrollDirection {
47     static func negative(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> ScrollDirection {
48         return readerConfig.isDirection(.down, .right, .right)
49     }
50
51     static func positive(withConfiguration readerConfig: FolioReaderConfig, scrollType: ScrollType = .page) -> ScrollDirection {
52         return readerConfig.isDirection(.up, .left, .left)
53     }
54 }
55
56 // MARK: Helpers
57
58 /**
59  Delay function
60  From: http://stackoverflow.com/a/24318861/517707
61
62  - parameter delay:   Delay in seconds
63  - parameter closure: Closure
64  */
65 func delay(_ delay:Double, closure:@escaping ()->()) {
66     DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Double(Int64(delay * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC), execute: closure)
67 }
68
69
70 // MARK: - Extensions
71
72 internal extension Bundle {
73     class func frameworkBundle() -> Bundle {
74         return Bundle(for: FolioReader.self)
75     }
76 }
77
78 internal extension UIColor {
79     convenience init(rgba: String) {
80         var red:   CGFloat = 0.0
81         var green: CGFloat = 0.0
82         var blue:  CGFloat = 0.0
83         var alpha: CGFloat = 1.0
84
85         if rgba.hasPrefix("#") {
86             let index = rgba.index(rgba.startIndex, offsetBy: 1)
87             let hex = String(rgba[index...])
88             let scanner = Scanner(string: hex)
89             var hexValue: CUnsignedLongLong = 0
90             if scanner.scanHexInt64(&hexValue) {
91                 switch (hex.count) {
92                 case 3:
93                     red   = CGFloat((hexValue & 0xF00) >> 8)       / 15.0
94                     green = CGFloat((hexValue & 0x0F0) >> 4)       / 15.0
95                     blue  = CGFloat(hexValue & 0x00F)              / 15.0
96                     break
97                 case 4:
98                     red   = CGFloat((hexValue & 0xF000) >> 12)     / 15.0
99                     green = CGFloat((hexValue & 0x0F00) >> 8)      / 15.0
100                     blue  = CGFloat((hexValue & 0x00F0) >> 4)      / 15.0
101                     alpha = CGFloat(hexValue & 0x000F)             / 15.0
102                     break
103                 case 6:
104                     red   = CGFloat((hexValue & 0xFF0000) >> 16)   / 255.0
105                     green = CGFloat((hexValue & 0x00FF00) >> 8)    / 255.0
106                     blue  = CGFloat(hexValue & 0x0000FF)           / 255.0
107                     break
108                 case 8:
109                     red   = CGFloat((hexValue & 0xFF000000) >> 24) / 255.0
110                     green = CGFloat((hexValue & 0x00FF0000) >> 16) / 255.0
111                     blue  = CGFloat((hexValue & 0x0000FF00) >> 8)  / 255.0
112                     alpha = CGFloat(hexValue & 0x000000FF)         / 255.0
113                     break
114                 default:
115                     print("Invalid RGB string, number of characters after '#' should be either 3, 4, 6 or 8", terminator: "")
116                     break
117                 }
118             } else {
119                 print("Scan hex error")
120             }
121         } else {
122             print("Invalid RGB string, missing '#' as prefix", terminator: "")
123         }
124         self.init(red:red, green:green, blue:blue, alpha:alpha)
125     }
126
127     //
128     /// Hex string of a UIColor instance.
129     ///
130     /// from: https://github.com/yeahdongcn/UIColor-Hex-Swift
131     ///
132     /// - Parameter includeAlpha: Whether the alpha should be included.
133     /// - Returns: Hexa string
134     func hexString(_ includeAlpha: Bool) -> String {
135         var r: CGFloat = 0
136         var g: CGFloat = 0
137         var b: CGFloat = 0
138         var a: CGFloat = 0
139         self.getRed(&r, green: &g, blue: &b, alpha: &a)
140
141         if (includeAlpha == true) {
142             return String(format: "#%02X%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255), Int(a * 255))
143         } else {
144             return String(format: "#%02X%02X%02X", Int(r * 255), Int(g * 255), Int(b * 255))
145         }
146     }
147
148     // MARK: - color shades
149     // https://gist.github.com/mbigatti/c6be210a6bbc0ff25972
150
151     func highlightColor() -> UIColor {
152
153         var hue : CGFloat = 0
154         var saturation : CGFloat = 0
155         var brightness : CGFloat = 0
156         var alpha : CGFloat = 0
157
158         if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
159             return UIColor(hue: hue, saturation: 0.30, brightness: 1, alpha: alpha)
160         } else {
161             return self;
162         }
163
164     }
165
166     /**
167      Returns a lighter color by the provided percentage
168
169      :param: lighting percent percentage
170      :returns: lighter UIColor
171      */
172     func lighterColor(_ percent : Double) -> UIColor {
173         return colorWithBrightnessFactor(CGFloat(1 + percent));
174     }
175
176     /**
177      Returns a darker color by the provided percentage
178
179      :param: darking percent percentage
180      :returns: darker UIColor
181      */
182     func darkerColor(_ percent : Double) -> UIColor {
183         return colorWithBrightnessFactor(CGFloat(1 - percent));
184     }
185
186     /**
187      Return a modified color using the brightness factor provided
188
189      :param: factor brightness factor
190      :returns: modified color
191      */
192     func colorWithBrightnessFactor(_ factor: CGFloat) -> UIColor {
193         var hue : CGFloat = 0
194         var saturation : CGFloat = 0
195         var brightness : CGFloat = 0
196         var alpha : CGFloat = 0
197
198         if getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: &alpha) {
199             return UIColor(hue: hue, saturation: saturation, brightness: brightness * factor, alpha: alpha)
200         } else {
201             return self;
202         }
203     }
204 }
205
206 internal extension String {
207     /// Truncates the string to length number of characters and
208     /// appends optional trailing string if longer
209     func truncate(_ length: Int, trailing: String? = nil) -> String {
210         if count > length {
211             let indexOfText = index(startIndex, offsetBy: length)
212             return String(self[..<indexOfText])
213         } else {
214             return self
215         }
216     }
217
218     func stripHtml() -> String {
219         return self.replacingOccurrences(of: "<[^>]+>", with: "", options: .regularExpression)
220     }
221
222     func stripLineBreaks() -> String {
223         return self.replacingOccurrences(of: "\n", with: "", options: .regularExpression)
224     }
225
226     /**
227      Converts a clock time such as `0:05:01.2` to seconds (`Double`)
228
229      Looks for media overlay clock formats as specified [here][1]
230
231      - Note: this may not be the  most efficient way of doing this. It can be improved later on.
232
233      - Returns: seconds as `Double`
234
235      [1]: http://www.idpf.org/epub/301/spec/epub-mediaoverlays.html#app-clock-examples
236      */
237     func clockTimeToSeconds() -> Double {
238
239         let val = self.trimmingCharacters(in: CharacterSet.whitespaces)
240
241         if( val.isEmpty ){ return 0 }
242
243         let formats = [
244             "HH:mm:ss.SSS"  : "^\\d{1,2}:\\d{2}:\\d{2}\\.\\d{1,3}$",
245             "HH:mm:ss"      : "^\\d{1,2}:\\d{2}:\\d{2}$",
246             "mm:ss.SSS"     : "^\\d{1,2}:\\d{2}\\.\\d{1,3}$",
247             "mm:ss"         : "^\\d{1,2}:\\d{2}$",
248             "ss.SSS"         : "^\\d{1,2}\\.\\d{1,3}$",
249             ]
250
251         // search for normal duration formats such as `00:05:01.2`
252         for (format, pattern) in formats {
253
254             if val.range(of: pattern, options: .regularExpression) != nil {
255
256                 let formatter = DateFormatter()
257                 formatter.dateFormat = format
258                 let time = formatter.date(from: val)
259
260                 if( time == nil ){ return 0 }
261
262                 formatter.dateFormat = "ss.SSS"
263                 let seconds = (formatter.string(from: time!) as NSString).doubleValue
264
265                 formatter.dateFormat = "mm"
266                 let minutes = (formatter.string(from: time!) as NSString).doubleValue
267
268                 formatter.dateFormat = "HH"
269                 let hours = (formatter.string(from: time!) as NSString).doubleValue
270
271                 return seconds + (minutes*60) + (hours*60*60)
272             }
273         }
274
275         // if none of the more common formats match, check for other possible formats
276
277         // 2345ms
278         if val.range(of: "^\\d+ms$", options: .regularExpression) != nil{
279             return (val as NSString).doubleValue / 1000.0
280         }
281
282         // 7.25h
283         if val.range(of: "^\\d+(\\.\\d+)?h$", options: .regularExpression) != nil {
284             return (val as NSString).doubleValue * 60 * 60
285         }
286
287         // 13min
288         if val.range(of: "^\\d+(\\.\\d+)?min$", options: .regularExpression) != nil {
289             return (val as NSString).doubleValue * 60
290         }
291
292         return 0
293     }
294
295     func clockTimeToMinutesString() -> String {
296
297         let val = clockTimeToSeconds()
298
299         let min = floor(val / 60)
300         let sec = floor(val.truncatingRemainder(dividingBy: 60))
301
302         return String(format: "%02.f:%02.f", min, sec)
303     }
304
305     // MARK: - NSString helpers
306
307     var lastPathComponent: String {
308         return (self as NSString).lastPathComponent
309     }
310
311     var deletingLastPathComponent: String {
312         return (self as NSString).deletingLastPathComponent
313     }
314
315     var deletingPathExtension: String {
316         return (self as NSString).deletingPathExtension
317     }
318
319     var pathExtension: String {
320         return (self as NSString).pathExtension
321     }
322
323     var abbreviatingWithTildeInPath: String {
324         return (self as NSString).abbreviatingWithTildeInPath
325     }
326
327     func appendingPathComponent(_ str: String) -> String {
328         return (self as NSString).appendingPathComponent(str)
329     }
330
331     func appendingPathExtension(_ str: String) -> String {
332         return (self as NSString).appendingPathExtension(str) ?? self+"."+str
333     }
334 }
335
336 internal extension UIImage {
337
338     convenience init?(readerImageNamed: String) {
339         self.init(named: readerImageNamed, in: Bundle.frameworkBundle(), compatibleWith: nil)
340     }
341
342     /// Forces the image to be colored with Reader Config tintColor
343     ///
344     /// - Parameter readerConfig: Current folio reader configuration.
345     /// - Returns: Returns a colored image
346     func ignoreSystemTint(withConfiguration readerConfig: FolioReaderConfig) -> UIImage? {
347         return self.imageTintColor(readerConfig.tintColor)?.withRenderingMode(.alwaysOriginal)
348     }
349
350     /**
351      Colorize the image with a color
352
353      - parameter tintColor: The input color
354      - returns: Returns a colored image
355      */
356     func imageTintColor(_ tintColor: UIColor) -> UIImage? {
357         UIGraphicsBeginImageContextWithOptions(self.size, false, self.scale)
358
359         let context = UIGraphicsGetCurrentContext()
360         context?.translateBy(x: 0, y: self.size.height)
361         context?.scaleBy(x: 1.0, y: -1.0)
362         context?.setBlendMode(CGBlendMode.normal)
363
364         let rect = CGRect(x: 0, y: 0, width: self.size.width, height: self.size.height) as CGRect
365         if let cgImage = self.cgImage {
366             context?.clip(to: rect, mask:  cgImage)
367         }
368
369         tintColor.setFill()
370         context?.fill(rect)
371
372         let newImage = UIGraphicsGetImageFromCurrentImageContext()
373         UIGraphicsEndImageContext()
374
375         return newImage
376     }
377
378     /**
379      Generate a image with a color
380
381      - parameter color: The input color
382      - returns: Returns a colored image
383      */
384     class func imageWithColor(_ color: UIColor?) -> UIImage {
385         let rect = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
386         UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
387         let context = UIGraphicsGetCurrentContext()
388
389         if let color = color {
390             color.setFill()
391         } else {
392             UIColor.white.setFill()
393         }
394
395         context!.fill(rect)
396         let image = UIGraphicsGetImageFromCurrentImageContext()
397         UIGraphicsEndImageContext()
398
399         return image!
400     }
401
402     /**
403      Generates a image with a `CALayer`
404
405      - parameter layer: The input `CALayer`
406      - returns: Return a rendered image
407      */
408     class func imageWithLayer(_ layer: CALayer) -> UIImage {
409         UIGraphicsBeginImageContextWithOptions(layer.bounds.size, layer.isOpaque, 0.0)
410         layer.render(in: UIGraphicsGetCurrentContext()!)
411         let img = UIGraphicsGetImageFromCurrentImageContext()
412         UIGraphicsEndImageContext()
413         return img!
414     }
415
416     /**
417      Generates a image from a `UIView`
418
419      - parameter view: The input `UIView`
420      - returns: Return a rendered image
421      */
422     class func imageWithView(_ view: UIView) -> UIImage {
423         UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
424         view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
425         let img = UIGraphicsGetImageFromCurrentImageContext()
426         UIGraphicsEndImageContext()
427         return img!
428     }
429 }
430
431 internal extension UIViewController {
432
433     func setCloseButton(withConfiguration readerConfig: FolioReaderConfig) {
434         let closeImage = UIImage(readerImageNamed: "icon-navbar-close")?.ignoreSystemTint(withConfiguration: readerConfig)
435         self.navigationItem.leftBarButtonItem = UIBarButtonItem(image: closeImage, style: .plain, target: self, action: #selector(dismiss as () -> Void))
436     }
437
438     @objc func dismiss() {
439         self.dismiss(nil)
440     }
441
442     func dismiss(_ completion: (() -> Void)?) {
443         DispatchQueue.main.async {
444             self.dismiss(animated: true, completion: {
445                 completion?()
446             })
447         }
448     }
449
450     // MARK: - NavigationBar
451
452     func setTransparentNavigation() {
453         let navBar = self.navigationController?.navigationBar
454         navBar?.setBackgroundImage(UIImage(), for: UIBarMetrics.default)
455         navBar?.hideBottomHairline()
456         navBar?.isTranslucent = true
457     }
458
459     func setTranslucentNavigation(_ translucent: Bool = true, color: UIColor, tintColor: UIColor = UIColor.white, titleColor: UIColor = UIColor.black, andFont font: UIFont = UIFont.systemFont(ofSize: 17)) {
460         let navBar = self.navigationController?.navigationBar
461         navBar?.setBackgroundImage(UIImage.imageWithColor(color), for: UIBarMetrics.default)
462         navBar?.showBottomHairline()
463         navBar?.isTranslucent = translucent
464         navBar?.tintColor = tintColor
465         navBar?.titleTextAttributes = [NSAttributedStringKey.foregroundColor: titleColor, NSAttributedStringKey.font: font]
466     }
467 }
468
469 internal extension UINavigationBar {
470
471     func hideBottomHairline() {
472         let navigationBarImageView = hairlineImageViewInNavigationBar(self)
473         navigationBarImageView!.isHidden = true
474     }
475
476     func showBottomHairline() {
477         let navigationBarImageView = hairlineImageViewInNavigationBar(self)
478         navigationBarImageView!.isHidden = false
479     }
480
481     fileprivate func hairlineImageViewInNavigationBar(_ view: UIView) -> UIImageView? {
482         if view.isKind(of: UIImageView.self) && view.bounds.height <= 1.0 {
483             return (view as! UIImageView)
484         }
485
486         let subviews = (view.subviews)
487         for subview: UIView in subviews {
488             if let imageView: UIImageView = hairlineImageViewInNavigationBar(subview) {
489                 return imageView
490             }
491         }
492         return nil
493     }
494 }
495
496 extension UINavigationController {
497
498     open override var preferredStatusBarStyle : UIStatusBarStyle {
499         guard let viewController = visibleViewController else { return .default }
500         return viewController.preferredStatusBarStyle
501     }
502
503     open override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
504         guard let viewController = visibleViewController else { return .portrait }
505         return viewController.supportedInterfaceOrientations
506     }
507
508     open override var shouldAutorotate : Bool {
509         guard let viewController = visibleViewController else { return false }
510         return viewController.shouldAutorotate
511     }
512 }
513
514 /**
515  This fixes iOS 9 crash
516  http://stackoverflow.com/a/32010520/517707
517  */
518 extension UIAlertController {
519     open override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
520         return .portrait
521     }
522     
523     open override var shouldAutorotate : Bool {
524         return false
525     }
526 }
527
528 extension Array {
529     
530     /**
531      Return index if is safe, if not return nil
532      http://stackoverflow.com/a/30593673/517707
533      */
534     subscript(safe index: Int) -> Element? {
535         return indices ~= index ? self[index] : nil
536     }
537 }