326 lines
12 KiB
Swift
326 lines
12 KiB
Swift
//
|
|
// RDReaderManager.swift
|
|
// RDReaderDemo
|
|
//
|
|
// Created by yangsq on 2021/8/20.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
struct Book {
|
|
var desc: String?
|
|
var chapters: [Chapter]
|
|
}
|
|
|
|
struct Chapter {
|
|
/// 第几章
|
|
var sort: Int
|
|
/// 章节名称
|
|
var title: String?
|
|
/// 划分了多少页
|
|
var pageCount: Int
|
|
/// 内容
|
|
var content: String
|
|
/// 分页
|
|
var pages: [ChapterPage]
|
|
|
|
/// 文字的长度
|
|
var range: NSRange
|
|
}
|
|
|
|
struct ChapterPage {
|
|
/// 第几章
|
|
var chapterSort: Int
|
|
/// 第几页
|
|
var pageNum: Int
|
|
/// 内容
|
|
var content: String
|
|
/// 内容
|
|
var attrContent: NSAttributedString
|
|
/// 文字的长度
|
|
var range: NSRange
|
|
}
|
|
|
|
public struct DemoBookItem {
|
|
public var title: String
|
|
public var source: BookSource
|
|
|
|
public init(title: String, source: BookSource) {
|
|
self.title = title
|
|
self.source = source
|
|
}
|
|
}
|
|
|
|
|
|
extension RDReaderManager {
|
|
static let themeTypeKey = "themeType.key"
|
|
static let brightValueKey = "brightValue.key"
|
|
static let fontValueKey = "fontValue.key"
|
|
static let lineSpaceKey = "lineSpace.key"
|
|
static let pageCurlTypeKey = "pageCurlType.key"
|
|
static let readProgressKey = "readProgress.key"
|
|
}
|
|
|
|
typealias BrightValue = CGFloat
|
|
extension BrightValue: RDReaderObserverType {
|
|
var cachesValue: CGFloat {
|
|
return self
|
|
}
|
|
var value: CGFloat {
|
|
return self
|
|
}
|
|
|
|
func transform(value: Any?) -> BrightValue {
|
|
return (value as? BrightValue) ?? self
|
|
}
|
|
}
|
|
|
|
extension Int: RDReaderObserverType {
|
|
var cachesValue: Int {
|
|
return self
|
|
}
|
|
var value: Int {
|
|
return self
|
|
}
|
|
|
|
func transform(value: Any?) -> Int {
|
|
return (value as? Int) ?? self
|
|
}
|
|
}
|
|
|
|
extension String: RDReaderObserverType {
|
|
var cachesValue: String {
|
|
return self
|
|
}
|
|
|
|
var value: String {
|
|
return self
|
|
}
|
|
|
|
func transform(value: Any?) -> String {
|
|
return (value as? String) ?? self
|
|
}
|
|
}
|
|
|
|
public class RDReaderManager: NSObject {
|
|
public static let shared = RDReaderManager()
|
|
let themeType = RDReaderObserver(key: RDReaderManager.themeTypeKey, defaultType: RDReaderCommon.Colors.white)
|
|
let brightValue = RDReaderObserver(key: RDReaderManager.brightValueKey, defaultType: UIScreen.main.brightness)
|
|
let fontValue = RDReaderObserver(key: RDReaderManager.fontValueKey, defaultType: CGFloat(15.0))
|
|
let lineSpaceType = RDReaderObserver(key: RDReaderManager.lineSpaceKey, defaultType: RDReaderCommon.SpaceType.min)
|
|
let pageCurlTypeType = RDReaderObserver(key: RDReaderManager.pageCurlTypeKey, defaultType: RDReaderCommon.PageCurlType.pageCurl)
|
|
let readProgress = RDReaderObserver(key: RDReaderManager.readProgressKey, defaultType: 0)
|
|
let epubReadLocation = RDReaderObserver(key: "epubReadLocation.key", defaultType: "")
|
|
let epubSelection = RDReaderObserver(key: "epubSelection.key", defaultType: "")
|
|
let epubHighlights = RDReaderObserver(key: "epubHighlights.key", defaultType: "")
|
|
|
|
}
|
|
|
|
protocol RDReaderObserverType {
|
|
associatedtype Value
|
|
associatedtype CacheValue
|
|
associatedtype CurrentType
|
|
var cachesValue: CacheValue { get }
|
|
var value: Value { get }
|
|
func transform(value: Any?) -> CurrentType
|
|
}
|
|
|
|
|
|
|
|
class RDReaderObserver<CurrentType> where CurrentType: RDReaderObserverType {
|
|
private var _currentType: CurrentType? = nil
|
|
private var key: String
|
|
private var defaultType: CurrentType
|
|
typealias Observer = ((CurrentType.Value) -> Void)
|
|
private var observers = [WeakObserver<Observer>]()
|
|
var currentType: CurrentType {
|
|
get {
|
|
if _currentType != nil {
|
|
return _currentType!
|
|
} else {
|
|
_currentType = (self.defaultType.transform(value: UserDefaults.standard.object(forKey: key)) as? CurrentType) ?? self.defaultType
|
|
return _currentType!
|
|
}
|
|
}
|
|
set {
|
|
_currentType = newValue
|
|
UserDefaults.standard.set(newValue.cachesValue, forKey: key)
|
|
UserDefaults.standard.synchronize()
|
|
observers.forEach { observer in
|
|
if let observer = observer.observer?.observer {
|
|
observer(_currentType!.value)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
init(key: String, defaultType: CurrentType) {
|
|
self.key = key
|
|
self.defaultType = defaultType
|
|
}
|
|
|
|
func observeChange(onNext: Observer?) {
|
|
if let onNext = onNext {
|
|
onNext(currentType.value)
|
|
observers.append(WeakObserver(observer: onNext))
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private class WeakObserver<T> {
|
|
class _Observer<T>: NSObject {
|
|
var observer: T
|
|
init(observer: T) {
|
|
self.observer = observer
|
|
super.init()
|
|
}
|
|
}
|
|
|
|
weak var observer: _Observer<T>?
|
|
private var _strongObserver: _Observer<T>?
|
|
init(observer: T) {
|
|
let t = _Observer(observer: observer)
|
|
_strongObserver = t
|
|
self.observer = _strongObserver
|
|
}
|
|
}
|
|
|
|
|
|
|
|
extension RDReaderManager {
|
|
func epubReaderConfiguration() -> RDEPUBReaderConfiguration {
|
|
let theme = themeType.currentType.value
|
|
return RDEPUBReaderConfiguration(
|
|
fontSize: fontValue.currentType,
|
|
lineHeightMultiple: lineSpaceType.currentType.mulitiple,
|
|
displayType: pageCurlTypeType.currentType.value,
|
|
landscapeDualPageEnabled: true,
|
|
reflowableContentInsets: UIEdgeInsets(top: 40, left: 16, bottom: 40, right: 16),
|
|
fixedContentInset: .zero,
|
|
theme: RDEPUBReaderTheme(
|
|
contentBackgroundColor: theme.contentBackgroudColor ?? .white,
|
|
contentTextColor: theme.contentTextColor ?? .black,
|
|
toolBackgroundColor: theme.toolBackgroudColor ?? .white,
|
|
toolControlTextColor: theme.toolControlTextColor ?? .black,
|
|
toolControlBorderUnselectColor: theme.toolControlBorderUnSelectColor ?? UIColor.lightGray.withAlphaComponent(0.5),
|
|
toolLineColor: theme.toolLineColor ?? UIColor.lightGray.withAlphaComponent(0.5)
|
|
),
|
|
fixedLayoutFit: .page,
|
|
fixedLayoutSpreadMode: .automatic,
|
|
textRenderingEngine: .dtCoreText
|
|
)
|
|
}
|
|
|
|
func epubPreferences(
|
|
theme: RDReaderTheme? = nil,
|
|
reflowableInsets: UIEdgeInsets,
|
|
fixedContentInset: UIEdgeInsets
|
|
) -> RDEPUBPreferences {
|
|
let resolvedTheme = theme ?? themeType.currentType.value
|
|
let backgroundColorCSS = resolvedTheme.contentBackgroudColor?.ss_cssString
|
|
return RDEPUBPreferences(
|
|
fontSize: fontValue.currentType,
|
|
lineHeightMultiple: lineSpaceType.currentType.mulitiple,
|
|
reflowableContentInsets: reflowableInsets,
|
|
fixedContentInset: fixedContentInset,
|
|
themeBackgroundColor: backgroundColorCSS,
|
|
themeTextColor: resolvedTheme.contentTextColor?.ss_cssString,
|
|
fixedBackgroundColor: backgroundColorCSS,
|
|
fixedLayoutFit: .page,
|
|
fixedLayoutSpreadMode: .automatic
|
|
)
|
|
}
|
|
|
|
func bundledEPUBURLs(in bundle: Bundle = .main) -> [URL] {
|
|
(bundle.urls(forResourcesWithExtension: "epub", subdirectory: nil) ?? [])
|
|
.sorted { lhs, rhs in
|
|
lhs.deletingPathExtension().lastPathComponent.localizedStandardCompare(rhs.deletingPathExtension().lastPathComponent) == .orderedAscending
|
|
}
|
|
}
|
|
|
|
public func demoBookItems(in bundle: Bundle = .main) -> [DemoBookItem] {
|
|
let textDemo = DemoBookItem(title: "宠她.txt", source: .textFile)
|
|
let epubItems = bundledEPUBURLs(in: bundle).map { url in
|
|
DemoBookItem(title: url.deletingPathExtension().lastPathComponent, source: .epubFile(url: url))
|
|
}
|
|
return [textDemo] + epubItems
|
|
}
|
|
|
|
@discardableResult
|
|
public func startReading(book item: DemoBookItem, from navigationController: UINavigationController?, animated: Bool = true) -> UIViewController? {
|
|
let readerController: UIViewController
|
|
switch item.source {
|
|
case .textFile:
|
|
guard let textURL = Bundle.main.url(forResource: "宠她", withExtension: "txt") else {
|
|
return nil
|
|
}
|
|
readerController = RDURLReaderController(bookURL: textURL)
|
|
case .epubFile(let url):
|
|
readerController = RDURLReaderController(
|
|
bookURL: url,
|
|
epubConfiguration: epubReaderConfiguration()
|
|
)
|
|
}
|
|
readerController.title = item.title
|
|
navigationController?.pushViewController(readerController, animated: animated)
|
|
return readerController
|
|
}
|
|
|
|
func encodeTextFile(font: UIFont, lineSapce: CGFloat, size: CGSize) -> Book {
|
|
let decodeString = String.encodeTextFile(url: URL(fileURLWithPath: Bundle.main.path(forResource: "宠她", ofType: ".txt")!))
|
|
let parten = "第[0-9一二三四五六七八九十百千]*[章回].*"
|
|
var chapters = [Chapter]()
|
|
var desc: String?
|
|
if let expression = try? NSRegularExpression(pattern: parten, options: .caseInsensitive) {
|
|
let results = expression.matches(in: decodeString, options: .reportCompletion, range: NSMakeRange(0, decodeString.count))
|
|
var startCount = -1
|
|
var lastTitle: String?
|
|
for result in results {
|
|
let index = results.firstIndex(of: result)
|
|
let range = result.range
|
|
|
|
if startCount != -1 && index! > 0 {
|
|
let contentRange = NSMakeRange(startCount, range.location - startCount)
|
|
|
|
let content = decodeString.substring(with: Range(contentRange, in: decodeString)!)
|
|
let pages = chapterPages(content: content, font: font, lineSapce: lineSapce, size: size, chapterSort: index!)
|
|
let chapter = Chapter(sort: index!,title: lastTitle, pageCount: pages.count, content: String(content), pages: pages, range: contentRange)
|
|
chapters.append(chapter)
|
|
if index == results.count - 1 {
|
|
let contentRange = NSMakeRange(range.location, decodeString.count - range.location)
|
|
let content = decodeString.substring(with: Range(contentRange, in: decodeString)!)
|
|
let title = decodeString.substring(with: Range(range, in: decodeString)!)
|
|
let pages = chapterPages(content: content, font: font, lineSapce: lineSapce, size: size, chapterSort: index!)
|
|
let chapter = Chapter(sort: index! + 1, title: title, pageCount: pages.count, content: String(content), pages: pages, range: contentRange)
|
|
chapters.append(chapter)
|
|
}
|
|
}
|
|
startCount = range.location
|
|
lastTitle = decodeString.substring(with: Range(range, in: decodeString)!)
|
|
if index == 0 && range.location > 0 {
|
|
desc = decodeString.substring(with: Range(NSMakeRange(0, range.location), in: decodeString)!)
|
|
}
|
|
}
|
|
}
|
|
return Book(desc: desc, chapters: chapters)
|
|
}
|
|
|
|
func chapterPages(content: String, font: UIFont, lineSapce: CGFloat, size: CGSize, chapterSort: Int) -> [ChapterPage] {
|
|
let attrString = NSMutableAttributedString(string: content)
|
|
let style = NSMutableParagraphStyle()
|
|
style.lineSpacing = lineSapce
|
|
attrString.addAttributes([NSAttributedString.Key.font : font, NSAttributedString.Key.paragraphStyle: style], range: NSMakeRange(0, content.count))
|
|
let pageRanges = attrString.pageRanges(size: size)
|
|
let pages = pageRanges.map({
|
|
pageRange -> ChapterPage in
|
|
let pageContent = content.substring(with: Range(pageRange, in: content)!)
|
|
let pageAttrContent = attrString.attributedSubstring(from: pageRange)
|
|
return ChapterPage(chapterSort: chapterSort, pageNum: pageRanges.firstIndex(of: pageRange)!, content: pageContent, attrContent: pageAttrContent, range: pageRange)
|
|
})
|
|
|
|
return pages
|
|
}
|
|
}
|