Epub阅读器0.0.1
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
//
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user