Epub阅读器0.0.1

This commit is contained in:
shen
2026-05-21 19:40:51 +08:00
commit daa36d8fe7
559 changed files with 106266 additions and 0 deletions
@@ -0,0 +1,304 @@
import UIKit
struct EPUBTOCDisplayItem {
var title: String
var href: String
var depth: Int
var pageNumber: Int?
}
struct LegacyRDEPUBTextPage {
var absolutePageIndex: Int
var chapterIndex: Int
var spineIndex: Int
var href: String
var chapterTitle: String
var pageIndexInChapter: Int
var totalPagesInChapter: Int
var content: NSAttributedString
var contentRange: NSRange
var pageStartOffset: Int
var pageEndOffset: Int
}
struct LegacyRDEPUBTextChapter {
var chapterIndex: Int
var spineIndex: Int
var href: String
var title: String
var attributedContent: NSAttributedString
var fragmentOffsets: [String: Int]
var pages: [LegacyRDEPUBTextPage]
}
struct LegacyRDEPUBTextBook {
var chapters: [LegacyRDEPUBTextChapter]
var pages: [LegacyRDEPUBTextPage]
func page(at pageNumber: Int) -> LegacyRDEPUBTextPage? {
guard pageNumber > 0, pages.indices.contains(pageNumber - 1) else {
return nil
}
return pages[pageNumber - 1]
}
func pageNumber(for location: RDEPUBLocation, resolver: RDEPUBResourceResolver, bookIdentifier: String?) -> Int? {
guard let normalizedLocation = resolver.normalizedLocation(location, bookIdentifier: bookIdentifier),
let chapter = chapters.first(where: { $0.href == normalizedLocation.href }) else {
return nil
}
let targetOffset: Int
if let fragment = normalizedLocation.fragment, let fragmentOffset = chapter.fragmentOffsets[fragment] {
targetOffset = fragmentOffset
} else {
let lastOffset = max(chapter.attributedContent.length - 1, 0)
targetOffset = min(lastOffset, max(0, Int(round(Double(lastOffset) * normalizedLocation.navigationProgression))))
}
if let page = chapter.pages.first(where: { targetOffset >= $0.pageStartOffset && targetOffset <= $0.pageEndOffset }) {
return page.absolutePageIndex + 1
}
return chapter.pages.last.map { $0.absolutePageIndex + 1 }
}
func location(forPageNumber pageNumber: Int, bookIdentifier: String?) -> RDEPUBLocation? {
guard let page = page(at: pageNumber),
let chapter = chapters.first(where: { $0.chapterIndex == page.chapterIndex }) else {
return nil
}
let totalLength = max(chapter.attributedContent.length - 1, 1)
return RDEPUBLocation(
bookIdentifier: bookIdentifier,
href: page.href,
progression: Double(page.pageStartOffset) / Double(totalLength),
lastProgression: Double(page.pageEndOffset) / Double(totalLength),
fragment: nil
)
}
}
final class LegacyRDEPUBTextBookBuilder {
static func build(
parser: RDEPUBParser,
publication: RDEPUBPublication,
pageSize: CGSize,
font: UIFont,
lineSpacing: CGFloat
) -> LegacyRDEPUBTextBook {
var chapters: [LegacyRDEPUBTextChapter] = []
var flatPages: [LegacyRDEPUBTextPage] = []
for (spineIndex, item) in publication.spine.enumerated() where item.linear {
guard item.mediaType.contains("html") || item.mediaType.contains("xhtml"),
let rawHTML = parser.htmlString(forRelativePath: item.href) else {
continue
}
let chapterTitle = resolvedChapterTitle(for: item, toc: publication.tableOfContents)
let normalizedHTML = normalizeHTML(rawHTML)
let markedHTML = injectFragmentMarkers(into: normalizedHTML)
guard let attributedContent = attributedHTML(
html: markedHTML,
baseURL: parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent(),
font: font,
lineSpacing: lineSpacing
) else {
continue
}
let mutableContent = NSMutableAttributedString(attributedString: attributedContent)
let fragmentOffsets = extractFragmentOffsets(from: mutableContent)
normalizeReadingAttributes(in: mutableContent, font: font, lineSpacing: lineSpacing)
let plainText = mutableContent.string.trimmingCharacters(in: .whitespacesAndNewlines)
if shouldSkipChapter(item: item, text: plainText) {
continue
}
let chapterIndex = chapters.count
let pageRanges = mutableContent.length > 0 ? mutableContent.pageRanges(size: pageSize) : []
let effectivePageRanges = pageRanges.isEmpty && mutableContent.length > 0
? [NSRange(location: 0, length: mutableContent.length)]
: pageRanges
let pages = effectivePageRanges.enumerated().map { localPageIndex, range in
LegacyRDEPUBTextPage(
absolutePageIndex: flatPages.count + localPageIndex,
chapterIndex: chapterIndex,
spineIndex: spineIndex,
href: item.href,
chapterTitle: chapterTitle,
pageIndexInChapter: localPageIndex,
totalPagesInChapter: effectivePageRanges.count,
content: mutableContent.attributedSubstring(from: range),
contentRange: range,
pageStartOffset: range.location,
pageEndOffset: range.location + max(range.length - 1, 0)
)
}
let chapter = LegacyRDEPUBTextChapter(
chapterIndex: chapterIndex,
spineIndex: spineIndex,
href: item.href,
title: chapterTitle,
attributedContent: mutableContent.copy() as! NSAttributedString,
fragmentOffsets: fragmentOffsets,
pages: pages
)
chapters.append(chapter)
flatPages.append(contentsOf: pages)
}
return LegacyRDEPUBTextBook(chapters: chapters, pages: flatPages)
}
private static func resolvedChapterTitle(for item: RDEPUBSpineItem, toc: [EPUBTableOfContentsItem]) -> String {
if let title = flattenedTOCItems(from: toc).first(where: { tocItem in
tocItem.href.components(separatedBy: "#").first == item.href
})?.title.trimmingCharacters(in: .whitespacesAndNewlines), !title.isEmpty {
return title
}
let trimmedTitle = item.title.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmedTitle.isEmpty ? item.href : trimmedTitle
}
private static func flattenedTOCItems(from items: [EPUBTableOfContentsItem]) -> [EPUBTableOfContentsItem] {
items.flatMap { item in
[item] + flattenedTOCItems(from: item.children)
}
}
private static func shouldSkipChapter(item: RDEPUBSpineItem, text: String) -> Bool {
let lowercasedHref = item.href.lowercased()
if text.isEmpty && (lowercasedHref.contains("cover") || lowercasedHref.contains("title")) {
return true
}
return false
}
private static func normalizeHTML(_ html: String) -> String {
var cleanedHTML = html
let replacements: [(pattern: String, template: String)] = [
(#"<hr\s+lang="zh-CN">分页符</hr>"#, ""),
(#"\r"#, "\n"),
(#"\n+"#, "\n")
]
for replacement in replacements {
if let regex = try? NSRegularExpression(pattern: replacement.pattern, options: [.caseInsensitive]) {
cleanedHTML = regex.stringByReplacingMatches(
in: cleanedHTML,
options: [],
range: NSRange(location: 0, length: cleanedHTML.utf16.count),
withTemplate: replacement.template
)
}
}
return cleanedHTML
}
private static func injectFragmentMarkers(into html: String) -> String {
guard let regex = try? NSRegularExpression(pattern: #"(<[^>]+\sid="([^"]+)"[^>]*>)"#, options: [.caseInsensitive]) else {
return html
}
return regex.stringByReplacingMatches(
in: html,
options: [],
range: NSRange(location: 0, length: html.utf16.count),
withTemplate: "${id=$2}$1"
)
}
private static func attributedHTML(
html: String,
baseURL: URL?,
font: UIFont,
lineSpacing: CGFloat
) -> NSAttributedString? {
guard let data = html.data(using: .utf8) else {
return nil
}
let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
.documentType: NSAttributedString.DocumentType.html,
.characterEncoding: String.Encoding.utf8.rawValue,
NSAttributedString.DocumentReadingOptionKey(rawValue: "NSBaseURLDocumentOption"): baseURL as Any
]
if let attributed = try? NSMutableAttributedString(data: data, options: options, documentAttributes: nil) {
return attributed
}
let fallbackAttributes: [NSAttributedString.Key: Any] = [
.font: font,
.paragraphStyle: paragraphStyle(lineSpacing: lineSpacing)
]
return NSAttributedString(string: html, attributes: fallbackAttributes)
}
private static func extractFragmentOffsets(from attributedString: NSMutableAttributedString) -> [String: Int] {
let markerPattern = #"\$\{id=([^}]+)\}"#
guard let regex = try? NSRegularExpression(pattern: markerPattern, options: []) else {
return [:]
}
let mutableString = NSMutableString(string: attributedString.string)
var fragmentOffsets: [String: Int] = [:]
var searchRange = NSRange(location: 0, length: mutableString.length)
var offsetAdjustment = 0
while let match = regex.firstMatch(in: mutableString as String, options: [], range: searchRange) {
let fullMatch = mutableString.substring(with: match.range) as NSString
let fragmentID = fullMatch
.replacingOccurrences(of: #"\$\{id="#, with: "", options: .regularExpression, range: NSRange(location: 0, length: fullMatch.length))
.replacingOccurrences(of: #"\}"#, with: "", options: .regularExpression)
let adjustedLocation = max(0, match.range.location + offsetAdjustment)
fragmentOffsets[fragmentID] = adjustedLocation
attributedString.deleteCharacters(in: match.range)
mutableString.deleteCharacters(in: match.range)
offsetAdjustment -= match.range.length
searchRange = NSRange(location: match.range.location, length: mutableString.length - match.range.location)
}
return fragmentOffsets
}
private static func normalizeReadingAttributes(in attributedString: NSMutableAttributedString, font: UIFont, lineSpacing: CGFloat) {
let fullRange = NSRange(location: 0, length: attributedString.length)
attributedString.enumerateAttributes(in: fullRange) { attributes, range, _ in
let sourceFont = attributes[.font] as? UIFont
let paragraph = (attributes[.paragraphStyle] as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? paragraphStyle(lineSpacing: lineSpacing)
paragraph.lineSpacing = lineSpacing
paragraph.paragraphSpacing = max(paragraph.paragraphSpacing, lineSpacing / 2)
var updatedAttributes = attributes
updatedAttributes[.font] = normalizedFont(from: sourceFont, baseFont: font)
updatedAttributes[.paragraphStyle] = paragraph
attributedString.setAttributes(updatedAttributes, range: range)
}
}
private static func normalizedFont(from sourceFont: UIFont?, baseFont: UIFont) -> UIFont {
guard let sourceFont else {
return baseFont
}
let traits = sourceFont.fontDescriptor.symbolicTraits.intersection([.traitBold, .traitItalic])
if let descriptor = baseFont.fontDescriptor.withSymbolicTraits(traits) {
return UIFont(descriptor: descriptor, size: baseFont.pointSize)
}
return baseFont
}
private static func paragraphStyle(lineSpacing: CGFloat) -> NSMutableParagraphStyle {
let style = NSMutableParagraphStyle()
style.lineSpacing = lineSpacing
style.paragraphSpacing = max(6, lineSpacing / 2)
return style
}
}
@@ -0,0 +1,103 @@
//
// RDReaderBottomToolView.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/10.
//
import UIKit
import SnapKit
public class RDReaderBottomToolView: RDReaderToolView, SSEventTrigger {
enum Event {
case chapterList, highlight, settings, darkAndLight
}
private lazy var containerView: UIStackView = {
let stackView = UIStackView()
stackView.distribution = .fillEqually
stackView.axis = .horizontal
stackView.spacing = 20
return stackView
}()
lazy var chapterListButton: TintColorButton = {
let button = TintColorButton(type: .system)
button.setImage(toolbarImage(named: "read_edit_chapterlist", fallbackSystemName: "list.bullet"), for: .normal)
addSubview(button)
button.addTarget(self, action: #selector(chapterListAction), for: .touchUpInside)
return button
}()
lazy var settingsButton: TintColorButton = {
let button = TintColorButton(type: .system)
button.setImage(toolbarImage(named: "read_edit_font", fallbackSystemName: "textformat"), for: .normal)
addSubview(button)
button.addTarget(self, action: #selector(settingsAction), for: .touchUpInside)
return button
}()
lazy var highlightButton: TintColorButton = {
let button = TintColorButton(type: .system)
button.setImage(UIImage(systemName: "highlighter"), for: .normal)
addSubview(button)
button.addTarget(self, action: #selector(highlightAction), for: .touchUpInside)
return button
}()
lazy var lightModeButton: TintColorButton = {
let button = TintColorButton(type: .system)
button.setImage(toolbarImage(named: "read_edit_night", fallbackSystemName: "moon.fill"), for: .normal)
addSubview(button)
button.addTarget(self, action: #selector(lightAndDarkModeAction), for: .touchUpInside)
return button
}()
override func makeUI() {
super.makeUI()
addSubview(containerView)
containerView.addArrangedSubview(chapterListButton)
containerView.addArrangedSubview(highlightButton)
containerView.addArrangedSubview(settingsButton)
containerView.addArrangedSubview(lightModeButton)
containerView.snp.makeConstraints { make in
make.left.equalTo(16)
make.top.equalTo(0)
make.right.equalTo(-16)
make.bottom.equalTo(-RDReaderCommon.safeAreaInsets.bottom)
}
}
@objc private func chapterListAction() {
if let trigger = self.triggerEvent {
trigger(.chapterList)
}
}
@objc private func settingsAction() {
if let trigger = self.triggerEvent {
trigger(.settings)
}
}
@objc private func highlightAction() {
if let trigger = self.triggerEvent {
trigger(.highlight)
}
}
@objc private func lightAndDarkModeAction() {
if let trigger = self.triggerEvent {
trigger(.darkAndLight)
}
}
private func toolbarImage(named: String, fallbackSystemName: String) -> UIImage? {
if let image = UIImage(named: named) {
return image.withRenderingMode(.alwaysOriginal)
}
return UIImage(systemName: fallbackSystemName)
}
}
@@ -0,0 +1,198 @@
//
// RDReaderCommon.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/11.
//
import Foundation
import UIKit
import CoreText
struct RDReaderCommon {
static var safeAreaInsets: UIEdgeInsets = {
guard #available(iOS 11.0, *) else {
return .zero
}
return UIApplication.shared.windows[0].safeAreaInsets
}()
}
extension RDReaderCommon {
enum SpaceType: Int, RDReaderObserverType {
case min = 0
case meduim
case max
var mulitiple: CGFloat {
switch self {
case .min: return 1.6
case .meduim: return 2.0
case .max: return 3.0
}
}
var image: UIImage? {
switch self {
case .min:
return UIImage.rdToolbarImage(named: "read_edit_textSpace_3", fallbackSystemName: "text.justify.leading")
case .meduim:
return UIImage.rdToolbarImage(named: "read_edit_textSpace_2", fallbackSystemName: "text.justify")
case .max:
return UIImage.rdToolbarImage(named: "read_edit_textSpace_1", fallbackSystemName: "text.justify.right")
}
}
var cachesValue: Int {
return rawValue
}
var value: CGFloat {
return mulitiple
}
func transform(value: Any?) -> RDReaderCommon.SpaceType {
guard let value = value as? Int else {
return self
}
return RDReaderCommon.SpaceType(rawValue: value) ?? self
}
}
}
extension RDReaderCommon {
enum PageCurlType: Int, RDReaderObserverType {
case pageCurl
case horizontalScroll
case verticalScroll
case horizontalCoverScroll
var title: String {
switch self {
case .pageCurl:
return "仿真"
case .horizontalScroll:
return "左右滑动"
case .verticalScroll:
return "上下滚动"
case .horizontalCoverScroll:
return "左右覆盖"
}
}
var cachesValue: Int {
return rawValue
}
var value: RDReaderView.DisplayType {
switch self {
case .pageCurl:
return RDReaderView.DisplayType.pageCurl
case .horizontalScroll:
return RDReaderView.DisplayType.horizontalScroll
case .verticalScroll:
return RDReaderView.DisplayType.verticalScroll
case .horizontalCoverScroll:
return RDReaderView.DisplayType.horizontalScroll
}
}
func transform(value: Any?) -> RDReaderCommon.PageCurlType {
guard let value = value as? Int else {
return self
}
return RDReaderCommon.PageCurlType(rawValue: value) ?? self
}
}
}
extension RDReaderCommon {
enum Colors: Int, RDReaderObserverType{
case white, dark, pink, yellow, green, blue
var value: RDReaderTheme {
switch self {
case .white:
return whiteTheme
case .dark:
return darkTheme
case .pink:
return pinkTheme
case .yellow:
return yellowTheme
case .green:
return greenTheme
case .blue:
return blueTheme
}
}
var cachesValue: Int {
rawValue
}
var color: UIColor {
switch self {
case .white:
return UIColor(red: 0.98, green: 0.98, blue: 0.99, alpha: 1)
case .yellow:
return UIColor(red: 0.89, green: 0.87, blue: 0.79, alpha: 1)
case .green:
return UIColor(red: 0.87, green: 0.91, blue: 0.82, alpha: 1)
case .pink:
return UIColor(red: 1, green: 0.89, blue: 0.91, alpha: 1)
case .blue:
return UIColor(red: 0.8, green: 0.84, blue: 0.89, alpha: 1)
case .dark:
return UIColor(red: 0.09, green: 0.09, blue: 0.09, alpha: 1)
}
}
func transform(value: Any?) -> RDReaderCommon.Colors {
guard let value = value as? Int else {
return self
}
return RDReaderCommon.Colors(rawValue: value) ?? self
}
}
static let whiteTheme = WhiteTheme()
static let darkTheme = DarkTheme()
static let pinkTheme = PinkTheme()
static let yellowTheme = YellowTheme()
static let greenTheme = GreenTheme()
static let blueTheme = BlueTheme()
}
extension NSAttributedString {
func pageRanges(size: CGSize) -> [NSRange] {
var ranges = [NSRange]()
let framesetter = CTFramesetterCreateWithAttributedString(self)
let path = CGPath(rect: CGRect(origin: .zero, size: size), transform: nil)
var range = CFRangeMake(0, 0)
var loc = 0
while range.location + range.length < length {
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(loc, 0), path, nil)
range = CTFrameGetVisibleStringRange(frame)
ranges.append(NSMakeRange(loc, range.length))
loc += range.length
}
return ranges
}
}
extension String {
static func encodeTextFile(url: URL?) -> String {
var content: String? = nil
guard let url = url else {
return content ?? ""
}
content = try? NSString(contentsOf: url, encoding: String.Encoding.utf8.rawValue) as String
if content == nil {
content = try? NSString(contentsOf: url, encoding: 0x80000632) as String
}
if content == nil {
content = try? NSString(contentsOf: url, encoding: 0x80000631) as String
}
return content ?? ""
}
}
@@ -0,0 +1,102 @@
//
// RDReaderContentView.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/11.
//
import UIKit
import SnapKit
class RDReaderContentView: UIView {
lazy var textLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
return label
}()
lazy var pageNumLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 13)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(textLabel)
addSubview(pageNumLabel)
textLabel.text = """
孩子为了不进职校,压力之大,我甚至听说,有初三学生晚上八点睡觉,凌晨两点起床读书做作业,忙到天亮,吃好早饭赶往学校。
平心静气地讲,一个孩子如果不是读书的料,完全可以去职校学一门手艺。但家长却有三重焦虑:
第一,一个普通高校的学生可以轻松去读职校的课程,但职校学生要去读普通高校的课程,难上加难。同一年级的各类学校和专业,在学习上是有难易梯度的。家长多不会同意让孩子一开始就选择容易的学校和专业,否则在未来竞争中,将处于不利地位。
第二,初中毕业生社会经验匮乏,实际上根本没有能力做人生规划。选择职校学一门技术,同时也就意味着,将来从事其他工作的门槛是很高的。如果没有继续学习的能力,改行的成本之高,难以想象。
以我的个人经验来讲,初中阶段,一度想去学习屠宰的手艺,毕业时也有上建筑类职高的机会,但都没去,在普通高中混到高三才决定考大学,如果去上职高,很可能就是个小包工头。我不知道这是不是我的真实意愿,但我觉得当下的工作更符合秉性。
第三,中国的一些职校,风评并不十分好,家长并不十分放心把十五六岁的孩子送入这些学校,他们不担心孩子学艺不成,而是担心孩子“学坏了”。
并且,随着科技加速进步,很多好端端的传统职业,忽然消失了。以汽修为例,现在的汽修专业毕业生,谁能保证他的精湛技术在10年之后不会归零?一些新职业出现没几年又消失了,谁能保证中高职教育能够跟上科技潮流?
"""
textLabel.snp.makeConstraints { make in
make.edges.equalTo(UIEdgeInsets(top: 40, left: 16, bottom: 40, right: 16))
}
pageNumLabel.snp.makeConstraints { make in
make.bottom.equalTo(-20)
make.right.equalTo(-30)
}
}
override func layoutSubviews() {
super.layoutSubviews()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
final class RDReaderEPUBTextContentView: UIView {
private(set) lazy var textLabel: UILabel = {
let label = UILabel()
label.numberOfLines = 0
return label
}()
private(set) lazy var pageNumLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 13)
return label
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(textLabel)
addSubview(pageNumLabel)
textLabel.snp.makeConstraints { make in
make.edges.equalTo(UIEdgeInsets(top: 40, left: 16, bottom: 40, right: 16))
}
pageNumLabel.snp.makeConstraints { make in
make.bottom.equalTo(-20)
make.right.equalTo(-30)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(page: LegacyRDEPUBTextPage, pageNumber: Int, totalPages: Int, theme: RDReaderTheme) {
backgroundColor = theme.contentBackgroudColor
pageNumLabel.textColor = theme.contentTextColor
pageNumLabel.text = "\(pageNumber) / \(totalPages)"
let displayContent = NSMutableAttributedString(attributedString: page.content)
let fullRange = NSRange(location: 0, length: displayContent.length)
displayContent.addAttribute(.foregroundColor, value: theme.contentTextColor ?? UIColor.black, range: fullRange)
textLabel.attributedText = displayContent
}
}
@@ -0,0 +1,93 @@
import UIKit
extension RDReaderController {
func currentEPUBTextPageSize() -> CGSize {
let pageWidth: CGFloat
if readerView.pagesPerScreen > 1 {
pageWidth = view.frame.width / CGFloat(readerView.pagesPerScreen) - 16 * 2
} else {
pageWidth = view.frame.width - 16 * 2
}
return CGSize(width: max(pageWidth, 1), height: max(view.frame.height - 40 * 2, 1))
}
func loadEPUBTextBook(
parser: RDEPUBParser,
publication: RDEPUBPublication,
restoreLocation: RDEPUBLocation?
) {
let pageSize = currentEPUBTextPageSize()
let font = UIFont.systemFont(ofSize: RDReaderManager.shared.fontValue.currentType)
let lineSpacing = RDReaderManager.shared.lineSpaceType.currentType.mulitiple * 15
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let textBook = LegacyRDEPUBTextBookBuilder.build(
parser: parser,
publication: publication,
pageSize: pageSize,
font: font,
lineSpacing: lineSpacing
)
DispatchQueue.main.async {
self?.applyEPUBTextBook(textBook, publication: publication, restoreLocation: restoreLocation)
}
}
}
func applyEPUBTextBook(
_ textBook: LegacyRDEPUBTextBook,
publication: RDEPUBPublication,
restoreLocation: RDEPUBLocation?
) {
epubTextBook = textBook
epubTOCItems = flattenedTOCItems(from: publication.tableOfContents, textBook: textBook)
print("[EPUB] Text book ready chapters=\(textBook.chapters.count) pages=\(textBook.pages.count) toc=\(epubTOCItems.count)")
let resolvedRestorePage = restoreLocation.flatMap {
textBook.pageNumber(
for: $0,
resolver: epubResolver ?? publicationFallbackResolver(),
bookIdentifier: currentEPUBBookIdentifier
)
}
storeEPUBPaginationValidation(
mode: "textReflowable",
stage: "final",
pageCount: textBook.pages.count,
chapterCount: textBook.chapters.count,
tocCount: epubTOCItems.count,
restoreLocation: restoreLocation,
resolvedRestorePage: resolvedRestorePage
)
isReaderContentReady = true
hideLoading()
readerView.reloadData()
restoreEPUBLocation(restoreLocation)
if restoreLocation == nil {
epubSession?.transition(to: .idle)
}
}
func flattenedTOCItems(
from items: [EPUBTableOfContentsItem],
depth: Int = 0,
textBook: LegacyRDEPUBTextBook? = nil
) -> [EPUBTOCDisplayItem] {
var result: [EPUBTOCDisplayItem] = []
for item in items {
let pageNumber = textBook.flatMap { book in
book.pageNumber(
for: RDEPUBLocation(bookIdentifier: currentEPUBBookIdentifier, href: item.href, progression: 0),
resolver: epubResolver ?? publicationFallbackResolver(),
bookIdentifier: currentEPUBBookIdentifier
)
}
result.append(EPUBTOCDisplayItem(title: item.title, href: item.href, depth: depth, pageNumber: pageNumber))
result.append(contentsOf: flattenedTOCItems(from: item.children, depth: depth + 1, textBook: textBook))
}
return result
}
func publicationFallbackResolver() -> RDEPUBResourceResolver {
epubPublication?.resourceResolver ?? RDEPUBResourceResolver(parser: epubParser ?? RDEPUBParser())
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
//
// RDReaderCoverView.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/11.
//
import UIKit
import SnapKit
class RDReaderCoverView: UIView {
lazy var textLabel: UILabel = {
let textLabel = UILabel()
textLabel.numberOfLines = 0
return textLabel
}()
lazy var imageV: UIImageView = {
let imageV = UIImageView()
imageV.image = UIImage(named: "cover")
return imageV
}()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(imageV)
addSubview(textLabel)
imageV.snp.makeConstraints { make in
make.top.equalTo(60)
make.centerX.equalToSuperview()
make.size.equalTo(CGSize(width: 88 * 1.5, height: 120 * 1.5))
}
textLabel.snp.makeConstraints { make in
make.left.equalTo(16)
make.right.equalTo(-16)
make.top.equalTo(imageV.snp.bottom).offset(20)
make.bottom.equalTo(-30)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -0,0 +1,127 @@
import SnapKit
import UIKit
protocol RDReaderEPUBContentViewDelegate: AnyObject {
func epubContentView(_ contentView: RDReaderEPUBContentView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int)
func epubContentView(_ contentView: RDReaderEPUBContentView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int)
func epubContentView(_ contentView: RDReaderEPUBContentView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int)
func epubContentView(_ contentView: RDReaderEPUBContentView, didActivateExternalLink url: URL)
func epubContentView(_ contentView: RDReaderEPUBContentView, didLogJavaScriptError message: String)
}
final class RDReaderEPUBContentView: UIView {
weak var delegate: RDReaderEPUBContentViewDelegate?
lazy var pageNumLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 13)
return label
}()
private let epubWebView = RDEPUBWebView()
override init(frame: CGRect) {
super.init(frame: frame)
clipsToBounds = true
layer.masksToBounds = true
addSubview(epubWebView)
addSubview(pageNumLabel)
epubWebView.delegate = self
epubWebView.clipsToBounds = true
epubWebView.layer.masksToBounds = true
epubWebView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
pageNumLabel.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-30)
make.bottom.equalToSuperview().offset(-20)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func load(publication: RDEPUBPublication, request: RDEPUBRenderRequest) {
epubWebView.load(publication: publication, request: request)
}
func loadPage(
parser: RDEPUBParser,
spineIndex: Int,
pageIndex: Int,
totalPagesInChapter: Int,
viewportSize: CGSize,
padding: UIEdgeInsets,
fontSize: CGFloat,
lineHeightMultiple: CGFloat,
themeBackgroundColor: String?,
themeTextColor: String?,
targetLocation: RDEPUBLocation?,
highlights: [RDEPUBHighlight]
) {
epubWebView.loadPage(
parser: parser,
spineIndex: spineIndex,
pageIndex: pageIndex,
totalPagesInChapter: totalPagesInChapter,
viewportSize: viewportSize,
padding: padding,
fontSize: fontSize,
lineHeightMultiple: lineHeightMultiple,
themeBackgroundColor: themeBackgroundColor,
themeTextColor: themeTextColor,
targetLocation: targetLocation,
highlights: highlights
)
}
func loadFixedSpread(
parser: RDEPUBParser,
spread: EPUBFixedSpread,
viewportSize: CGSize,
contentInset: UIEdgeInsets,
backgroundColor: UIColor?
) {
epubWebView.loadFixedSpread(
parser: parser,
spread: spread,
viewportSize: viewportSize,
contentInset: contentInset,
backgroundColor: backgroundColor
)
}
func releaseResources() {
epubWebView.reset()
epubWebView.delegate = self
}
}
extension RDReaderEPUBContentView: RDEPUBWebViewDelegate {
func epubWebView(_ webView: RDEPUBWebView, didUpdateLocation location: RDEPUBLocation, spineIndex: Int) {
delegate?.epubContentView(self, didUpdateLocation: location, spineIndex: spineIndex)
}
func epubWebView(_ webView: RDEPUBWebView, didChangeSelection selection: RDEPUBSelection?, spineIndex: Int) {
delegate?.epubContentView(self, didChangeSelection: selection, spineIndex: spineIndex)
}
func epubWebView(_ webView: RDEPUBWebView, didActivateInternalLink location: RDEPUBLocation, fromSpineIndex: Int) {
delegate?.epubContentView(self, didActivateInternalLink: location, fromSpineIndex: fromSpineIndex)
}
func epubWebView(_ webView: RDEPUBWebView, didActivateExternalLink url: URL) {
delegate?.epubContentView(self, didActivateExternalLink: url)
}
func epubWebView(_ webView: RDEPUBWebView, didLogJavaScriptError message: String) {
delegate?.epubContentView(self, didLogJavaScriptError: message)
}
func epubWebViewDidFinishRendering(_ webView: RDEPUBWebView) {
}
}
@@ -0,0 +1,499 @@
//
// RDReaderEditView.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/19.
//
import UIKit
import SnapKit
///
class RDReaderBrightSlider: UISlider, SSEventTrigger {
enum Event {
case valueDidChange(value: CGFloat)
}
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
override func setValue(_ value: Float, animated: Bool) {
super.setValue(value, animated: animated)
UIScreen.main.brightness = CGFloat(value)
if let trigger = self.triggerEvent {
trigger(.valueDidChange(value: CGFloat(value)))
}
}
func makeUI() {
let color = UIColor(red: 0.97, green: 0.97, blue: 0.97, alpha: 1)
thumbTintColor = color
let image = UIImage.rdToolbarImage(named: "read_edit_slide", fallbackSystemName: "circle.fill")
setThumbImage(image, for: .normal)
setThumbImage(image, for: .highlighted)
minimumValueImage = UIImage.rdToolbarImage(named: "read_edit_bright_min", fallbackSystemName: "sun.min.fill")
maximumValueImage = UIImage.rdToolbarImage(named: "read_edit_bright_max", fallbackSystemName: "sun.max.fill")
}
override func trackRect(forBounds bounds: CGRect) -> CGRect {
self.layer.cornerRadius = 2.5 / 2
return CGRect(x: 30, y: (bounds.height - 2.5)/2, width: bounds.width - 30 * 2, height: 2.5)
}
override func thumbRect(forBounds bounds: CGRect, trackRect rect: CGRect, value: Float) -> CGRect {
// print(value)
return CGRect(x: 30 + (bounds.width - 30 * 2 - 16) * CGFloat(value), y: (bounds.height - 16)/2, width: 16, height: 16)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
class RDReaderFontView: UIView, SSEventTrigger {
enum Event {
case fontSize(size: CGFloat)
}
var maxFontSize: CGFloat = 30
var minFontSize: CGFloat = 15
var currentFontSize: CGFloat = 15 {
didSet {
fontLabel.text = "\(Int(currentFontSize))"
if currentFontSize != oldValue {
if let trigger = self.triggerEvent {
trigger(.fontSize(size: currentFontSize))
}
}
}
}
lazy var reduceButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage.rdToolbarImage(named: "read_edit_font_reduce", fallbackSystemName: "textformat.size.smaller"), for: .normal)
button.layer.cornerRadius = 37 / 2
button.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2)
return button
}()
lazy var increaseButton: UIButton = {
let button = UIButton(type: .system)
button.setImage(UIImage.rdToolbarImage(named: "read_edit_font_increase", fallbackSystemName: "textformat.size.larger"), for: .normal)
button.layer.cornerRadius = 37 / 2
button.backgroundColor = UIColor.lightGray.withAlphaComponent(0.2)
return button
}()
lazy var fontLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 17)
label.textAlignment = .center
return label
}()
private lazy var containerView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fill
stackView.spacing = 0
return stackView
}()
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
func makeUI() {
addSubview(containerView)
containerView.addArrangedSubview(reduceButton)
containerView.addArrangedSubview(fontLabel)
containerView.addArrangedSubview(increaseButton)
containerView.snp.makeConstraints { make in
make.edges.equalTo(0)
}
reduceButton.snp.makeConstraints { make in
make.width.equalTo(increaseButton.snp.width).priority(.high)
make.height.equalTo(37)
}
increaseButton.snp.makeConstraints { make in
make.height.equalTo(37)
}
fontLabel.snp.makeConstraints { make in
make.width.equalTo(50)
}
reduceButton.addTarget(self, action: #selector(reduceAction(button:)), for: .touchUpInside)
increaseButton.addTarget(self, action: #selector(increaseAction(button:)), for: .touchUpInside)
fontLabel.text = "\(Int(currentFontSize))"
}
@objc func reduceAction(button: UIButton) {
if currentFontSize == minFontSize {
return
}
currentFontSize -= 1
}
@objc func increaseAction(button: UIButton) {
if currentFontSize == maxFontSize {
return
}
currentFontSize += 1
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
class RDReaderLineSpaceView: UIView, SSEventTrigger {
enum Event {
case chose(type: RDReaderCommon.SpaceType)
}
private lazy var containerView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .fillEqually
stackView.spacing = 10
return stackView
}()
private var buttons = [UIButton]()
let allTypes: [RDReaderCommon.SpaceType] = [.min, .meduim, .max]
var currentType: RDReaderCommon.SpaceType = .min {
didSet {
let index = allTypes.firstIndex(of: currentType)
let button = buttons[index!]
button.sendActions(for: .touchUpInside)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
func makeUI() {
addSubview(containerView)
containerView.snp.makeConstraints { make in
make.edges.equalTo(0)
}
allTypes.forEach { type in
let button = UIButton(type: .system)
button.layer.cornerRadius = 37 / 2
button.layer.borderWidth = 1
button.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
button.setImage(type.image?.withRenderingMode(.alwaysOriginal), for: .normal)
containerView.addArrangedSubview(button)
button.addTarget(self, action: #selector(buttonAction(button:)), for: .touchUpInside)
button.snp.makeConstraints { make in
make.height.equalTo(37)
}
buttons.append(button)
if type == .min {
button.sendActions(for: .touchUpInside)
}
}
}
@objc func buttonAction(button: UIButton) {
for btn in buttons {
if btn == button {
btn.layer.borderColor = UIColor.black.cgColor
let index = buttons.firstIndex(of: btn)
let type = allTypes[index!]
if let trigger = self.triggerEvent {
trigger(.chose(type: type))
}
} else {
btn.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
class RDReaderPageCurlTypeView: UIView, SSEventTrigger {
enum Event {
case chose(type: RDReaderCommon.PageCurlType)
}
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
private lazy var containerView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
stackView.spacing = 10
return stackView
}()
var currentType: RDReaderCommon.PageCurlType = .pageCurl {
didSet {
let index = allTypes.firstIndex(of: currentType)
let button = buttons[index!]
button.sendActions(for: .touchUpInside)
}
}
private var buttons = [UIButton]()
let allTypes: [RDReaderCommon.PageCurlType] = [.pageCurl, .horizontalScroll, .verticalScroll, .horizontalCoverScroll]
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
func makeUI() {
addSubview(scrollView)
scrollView.addSubview(containerView)
scrollView.snp.makeConstraints { make in
make.edges.equalTo(0)
}
containerView.snp.makeConstraints { make in
make.edges.equalTo(0)
}
allTypes.forEach { type in
let button = UIButton(type: .system)
button.layer.cornerRadius = 37 / 2
button.layer.borderWidth = 1
button.titleLabel?.font = UIFont.systemFont(ofSize: 17)
button.setTitleColor(UIColor.black, for: .normal)
button.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
button.setTitle(type.title, for: .normal)
let size = button.titleLabel!.sizeThatFits(CGSize(width: CGFloat(MAXFLOAT), height: 37))
containerView.addArrangedSubview(button)
button.addTarget(self, action: #selector(buttonAction(button:)), for: .touchUpInside)
button.snp.makeConstraints { make in
make.height.equalTo(37)
make.width.equalTo(size.width + 30)
}
buttons.append(button)
if type == .pageCurl {
button.sendActions(for: .touchUpInside)
}
}
}
@objc func buttonAction(button: UIButton) {
for btn in buttons {
if btn == button {
btn.layer.borderColor = UIColor.black.cgColor
let index = buttons.firstIndex(of: btn)
let type = allTypes[index!]
if let trigger = self.triggerEvent {
trigger(.chose(type: type))
}
} else {
btn.layer.borderColor = UIColor.lightGray.withAlphaComponent(0.5).cgColor
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
class RDReaderThemeColorsView: UIView, SSEventTrigger {
enum Event {
case chose(color: RDReaderCommon.Colors)
}
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.showsVerticalScrollIndicator = false
scrollView.showsHorizontalScrollIndicator = false
return scrollView
}()
private lazy var containerView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.distribution = .equalSpacing
stackView.spacing = 30
return stackView
}()
var currentType: RDReaderCommon.Colors = .white {
didSet {
let index = allTypes.firstIndex(of: currentType)
let button = buttons[index!]
button.sendActions(for: .touchUpInside)
}
}
private var buttons = [UIButton]()
let allTypes: [RDReaderCommon.Colors] = [.white, .yellow, .green, .pink, .blue, .dark]
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
func makeUI() {
addSubview(scrollView)
scrollView.addSubview(containerView)
scrollView.snp.makeConstraints { make in
make.edges.equalTo(0)
}
containerView.snp.makeConstraints { make in
make.edges.equalTo(0)
}
allTypes.forEach { type in
let button = UIButton(type: .system)
button.layer.cornerRadius = 37 / 2
button.titleLabel?.font = UIFont.systemFont(ofSize: 17)
button.layer.borderColor = UIColor.black.cgColor
button.backgroundColor = type.color
containerView.addArrangedSubview(button)
button.addTarget(self, action: #selector(buttonAction(button:)), for: .touchUpInside)
button.snp.makeConstraints { make in
make.height.width.equalTo(37)
}
buttons.append(button)
if type == .white {
button.sendActions(for: .touchUpInside)
}
}
}
@objc func buttonAction(button: UIButton) {
for btn in buttons {
if btn == button {
btn.layer.borderWidth = 1
let index = buttons.firstIndex(of: btn)
let type = allTypes[index!]
if let trigger = self.triggerEvent {
trigger(.chose(color: type))
}
} else {
btn.layer.borderWidth = 0
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
let space = (frame.width - CGFloat(allTypes.count) * 37) / CGFloat(allTypes.count - 1)
containerView.spacing = space
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
///
class RDReaderEditView: UIView {
lazy var lineView: UIView = {
let view = UIView()
view.backgroundColor = UIColor.lightGray
return view
}()
lazy var brightSlider: RDReaderBrightSlider = {
let slider = RDReaderBrightSlider()
return slider
}()
lazy var fontView: RDReaderFontView = {
let view = RDReaderFontView()
return view
}()
lazy var lineSpaceView: RDReaderLineSpaceView = {
let view = RDReaderLineSpaceView()
return view
}()
lazy var pageCurlTypeView: RDReaderPageCurlTypeView = {
let view = RDReaderPageCurlTypeView()
return view
}()
lazy var colorView: RDReaderThemeColorsView = {
let view = RDReaderThemeColorsView()
return view
}()
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
func makeUI() {
backgroundColor = .white
addSubview(lineView)
addSubview(brightSlider)
addSubview(fontView)
addSubview(lineSpaceView)
addSubview(pageCurlTypeView)
addSubview(colorView)
lineView.snp.makeConstraints { make in
make.left.right.top.equalTo(0)
make.height.equalTo(0.5)
}
brightSlider.snp.makeConstraints { make in
make.top.equalTo(20)
make.left.equalTo(16)
make.right.equalTo(-16)
make.height.equalTo(20)
}
fontView.snp.makeConstraints { make in
make.top.equalTo(brightSlider.snp.bottom).offset(20)
make.left.equalTo(16)
make.right.equalTo(-16)
make.height.equalTo(37)
}
lineSpaceView.snp.makeConstraints { make in
make.top.equalTo(fontView.snp.bottom).offset(20)
make.left.equalTo(16)
make.right.equalTo(-16)
make.height.equalTo(37)
}
pageCurlTypeView.snp.makeConstraints { make in
make.top.equalTo(lineSpaceView.snp.bottom).offset(20)
make.left.equalTo(16)
make.right.equalTo(-16)
make.height.equalTo(37)
}
colorView.snp.makeConstraints { make in
make.top.equalTo(pageCurlTypeView.snp.bottom).offset(20)
make.left.equalTo(16)
make.right.equalTo(-16)
make.height.equalTo(37)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -0,0 +1,30 @@
//
// RDReaderEndView.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/11.
//
import UIKit
import SnapKit
class RDReaderEndView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let textLabel = UILabel()
textLabel.text = "结束页"
textLabel.backgroundColor = .red
textLabel.textAlignment = .center
addSubview(textLabel)
textLabel.snp.makeConstraints { make in
make.center.equalToSuperview()
make.size.equalTo(CGSize(width: 100, height: 100))
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
@@ -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
}
}
@@ -0,0 +1,75 @@
//
// RDReaderTheme.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/10.
//
import Foundation
import UIKit
public protocol RDReaderTheme {
var contentBackgroudColor: UIColor? { get }
var contentTextColor: UIColor? { get }
var toolBackgroudColor: UIColor? { get }
var toolControlTextColor: UIColor? { get }
var toolControlBorderUnSelectColor: UIColor? { get }
var toolLineColor: UIColor? { get }
}
struct WhiteTheme: RDReaderTheme {
var contentBackgroudColor: UIColor? = UIColor.white
var contentTextColor: UIColor? = UIColor.black
var toolBackgroudColor: UIColor? = UIColor.white
var toolControlTextColor: UIColor? = UIColor.black
var toolControlBorderUnSelectColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
var toolLineColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
}
struct DarkTheme: RDReaderTheme {
var contentBackgroudColor: UIColor? = UIColor.black
var contentTextColor: UIColor? = UIColor.white
var toolBackgroudColor: UIColor? = UIColor.black
var toolControlTextColor: UIColor? = UIColor.white
var toolControlBorderUnSelectColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
var toolLineColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
}
struct YellowTheme: RDReaderTheme {
var contentBackgroudColor: UIColor? = UIColor(red: 0.89, green: 0.87, blue: 0.79, alpha: 1)
var contentTextColor: UIColor? = UIColor.black
var toolBackgroudColor: UIColor? = UIColor(red: 0.89, green: 0.87, blue: 0.79, alpha: 1)
var toolControlTextColor: UIColor? = UIColor.black
var toolControlBorderUnSelectColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
var toolLineColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
}
struct GreenTheme: RDReaderTheme {
var contentBackgroudColor: UIColor? = UIColor(red: 0.87, green: 0.91, blue: 0.82, alpha: 1)
var contentTextColor: UIColor? = UIColor.black
var toolBackgroudColor: UIColor? = UIColor(red: 0.87, green: 0.91, blue: 0.82, alpha: 1)
var toolControlTextColor: UIColor? = UIColor.black
var toolControlBorderUnSelectColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
var toolLineColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
}
struct PinkTheme: RDReaderTheme {
var contentBackgroudColor: UIColor? = UIColor(red: 1, green: 0.89, blue: 0.91, alpha: 1)
var contentTextColor: UIColor? = UIColor.black
var toolBackgroudColor: UIColor? = UIColor(red: 1, green: 0.89, blue: 0.91, alpha: 1)
var toolControlTextColor: UIColor? = UIColor.black
var toolControlBorderUnSelectColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
var toolLineColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
}
struct BlueTheme: RDReaderTheme {
var contentBackgroudColor: UIColor? = UIColor(red: 0.8, green: 0.84, blue: 0.89, alpha: 1)
var contentTextColor: UIColor? = UIColor.black
var toolBackgroudColor: UIColor? = UIColor(red: 0.8, green: 0.84, blue: 0.89, alpha: 1)
var toolControlTextColor: UIColor? = UIColor.black
var toolControlBorderUnSelectColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
var toolLineColor: UIColor? = UIColor.lightGray.withAlphaComponent(0.5)
}
@@ -0,0 +1,67 @@
//
// RDReaderToolView.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/10.
//
import UIKit
public class RDReaderToolView: UIView {
lazy var lineView: UIView = {
let view = UIView()
return view
}()
var lineHeight: CGFloat = 0.5
override init(frame: CGRect) {
super.init(frame: frame)
makeUI()
}
func makeUI() {
addSubview(lineView)
}
public override func layoutSubviews() {
super.layoutSubviews()
lineView.frame = CGRect(x: 0, y: 0, width: frame.width, height: lineHeight)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
public class TintColorButton: UIButton {
public override func tintColorDidChange() {
super.tintColorDidChange()
if let image = self.currentImage {
let image = image.tintColorImage(color: tintColor)
setImage(image, for: .normal)
}
}
}
extension UIImage {
static func rdToolbarImage(named: String, fallbackSystemName: String) -> UIImage? {
if let image = UIImage(named: named) {
return image.withRenderingMode(.alwaysOriginal)
}
return UIImage(systemName: fallbackSystemName)
}
func tintColorImage(color: UIColor) -> UIImage? {
UIGraphicsBeginImageContextWithOptions(size, false, scale)
let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height)
color.set()
UIRectFill(rect)
draw(at: .zero, blendMode: .destinationIn, alpha: 1)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
}
@@ -0,0 +1,54 @@
//
// RDReaderTopToolView.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/10.
//
import UIKit
import SnapKit
public class RDReaderTopToolView: RDReaderToolView,SSEventTrigger {
enum Event {
case back
}
public override func layoutSubviews() {
super.layoutSubviews()
lineView.frame = CGRect(x: 0, y: frame.height - lineHeight, width: frame.width, height: lineHeight)
}
lazy var backButton: TintColorButton = {
let backButton = TintColorButton(type: .system)
backButton.setImage(toolbarImage(named: "arrow_left", fallbackSystemName: "chevron.left"), for: .normal)
return backButton
}()
override func makeUI() {
super.makeUI()
addSubview(backButton)
backButton.addTarget(self, action: #selector(backAction), for: .touchUpInside)
backButton.snp.makeConstraints { make in
make.left.equalTo(0)
make.bottom.equalTo(0)
make.height.equalTo(44)
make.width.equalTo(50)
}
}
@objc private func backAction() {
if let trigger = self.triggerEvent {
trigger(.back)
}
}
private func toolbarImage(named: String, fallbackSystemName: String) -> UIImage? {
if let image = UIImage(named: named) {
return image.withRenderingMode(.alwaysOriginal)
}
return UIImage(systemName: fallbackSystemName)
}
}
@@ -0,0 +1,50 @@
//
// SSChapterListController.swift
// RDReaderDemo
//
// Created by yangsq on 2021/8/19.
//
import UIKit
class SSChapterListController: UITableViewController {
var selectedChapter: ((Int) -> Void)?
var chapters: [Chapter]
var currentChapterNum: Int
init(chapters:[Chapter], currentChapterNum: Int) {
self.chapters = chapters
self.currentChapterNum = currentChapterNum
super.init(style: .plain)
tableView.tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 0))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "UITableViewCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.chapters.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "UITableViewCell")
cell?.textLabel?.text = self.chapters[indexPath.row].title
cell?.textLabel?.textColor = indexPath.row == currentChapterNum ? UIColor.red : UIColor.black
return cell!
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let selectedChapter = selectedChapter {
selectedChapter(indexPath.row)
}
}
func selectedChapter(onTrigger: @escaping (Int) -> Void) {
self.selectedChapter = onTrigger
}
}
@@ -0,0 +1,38 @@
//
// EventTrigger.swift
//
//
// Created by yangsq on 2020/11/4.
//
import Foundation
protocol SSEventTrigger {
associatedtype Event
typealias TriggerEvent = (Event) -> Void
var triggerEvent: TriggerEvent? { get set }
func trigger(event: TriggerEvent?)
}
private var triggerEventKey: UInt8 = 0
extension SSEventTrigger {
private var _triggerEvent:TriggerEvent? {
get {return objc_getAssociatedObject(self, &triggerEventKey) as? Self.TriggerEvent}
set {objc_setAssociatedObject(self, &triggerEventKey, newValue, .OBJC_ASSOCIATION_COPY_NONATOMIC)}
}
var triggerEvent: TriggerEvent? {
get{_triggerEvent}
set{
if newValue != nil {
_triggerEvent = newValue!
}
}
}
func trigger(event: TriggerEvent?) {
if event != nil {
objc_setAssociatedObject(self, &triggerEventKey, event!, .OBJC_ASSOCIATION_COPY_NONATOMIC)
}
}
}