refactor: rename RDReaderView -> RDEpubReaderView, update pod config and docs
- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
This commit is contained in:
@@ -0,0 +1,322 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public final class RDEPUBReadingSession {
|
||||
|
||||
public typealias PaginationSnapshot = (pages: [EPUBPage], chapters: [EPUBChapterInfo])
|
||||
|
||||
public let publication: RDEPUBPublication
|
||||
|
||||
public private(set) var activePages: [EPUBPage] = []
|
||||
|
||||
public private(set) var activeChapters: [EPUBChapterInfo] = []
|
||||
|
||||
public private(set) var pendingNavigationLocation: RDEPUBLocation?
|
||||
|
||||
public private(set) var pendingNavigationPageNum: Int?
|
||||
|
||||
public private(set) var pendingNavigationHighlightRangeInfo: String?
|
||||
|
||||
public private(set) var currentViewport: RDEPUBViewport?
|
||||
|
||||
public private(set) var currentReadingContext: RDEPUBReadingContext?
|
||||
|
||||
public init(publication: RDEPUBPublication) {
|
||||
self.publication = publication
|
||||
}
|
||||
|
||||
public var resourceResolver: RDEPUBResourceResolver {
|
||||
publication.resourceResolver
|
||||
}
|
||||
|
||||
public func resetRuntimeState() {
|
||||
activePages = []
|
||||
activeChapters = []
|
||||
clearPendingNavigation()
|
||||
currentViewport = nil
|
||||
currentReadingContext = nil
|
||||
}
|
||||
|
||||
public func setActiveSnapshot(_ snapshot: PaginationSnapshot) {
|
||||
activePages = snapshot.pages
|
||||
activeChapters = snapshot.chapters
|
||||
}
|
||||
|
||||
public func clearPendingNavigation() {
|
||||
pendingNavigationLocation = nil
|
||||
pendingNavigationPageNum = nil
|
||||
pendingNavigationHighlightRangeInfo = nil
|
||||
}
|
||||
|
||||
public func pendingLocation(forPageNumber pageNumber: Int, spineIndex: Int?) -> RDEPUBLocation? {
|
||||
guard pendingNavigationPageNum == pageNumber,
|
||||
let pendingNavigationLocation else {
|
||||
return nil
|
||||
}
|
||||
guard let spineIndex else {
|
||||
return pendingNavigationLocation
|
||||
}
|
||||
let pageHref = resourceResolver.href(forSpineIndex: spineIndex)
|
||||
guard resourceResolver.normalizedHref(pageHref ?? "") == resourceResolver.normalizedHref(pendingNavigationLocation.href) else {
|
||||
return nil
|
||||
}
|
||||
return pendingNavigationLocation
|
||||
}
|
||||
|
||||
public func pendingHighlightRangeInfo(forPageNumber pageNumber: Int, spineIndex: Int?) -> String? {
|
||||
guard pendingNavigationPageNum == pageNumber,
|
||||
let pendingNavigationHighlightRangeInfo,
|
||||
!pendingNavigationHighlightRangeInfo.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
guard let pendingNavigationLocation else {
|
||||
return pendingNavigationHighlightRangeInfo
|
||||
}
|
||||
guard let spineIndex else {
|
||||
return pendingNavigationHighlightRangeInfo
|
||||
}
|
||||
let pageHref = resourceResolver.href(forSpineIndex: spineIndex)
|
||||
guard resourceResolver.normalizedHref(pageHref ?? "") == resourceResolver.normalizedHref(pendingNavigationLocation.href) else {
|
||||
return nil
|
||||
}
|
||||
return pendingNavigationHighlightRangeInfo
|
||||
}
|
||||
|
||||
public func pageContains(spineIndex: Int, in page: EPUBPage) -> Bool {
|
||||
if let fixedSpread = page.fixedSpread {
|
||||
return fixedSpread.resources.contains(where: { $0.spineIndex == spineIndex })
|
||||
}
|
||||
return page.spineIndex == spineIndex
|
||||
}
|
||||
|
||||
public func fallbackLocation(for page: EPUBPage, bookIdentifier: String?) -> RDEPUBLocation? {
|
||||
if let fixedSpread = page.fixedSpread {
|
||||
return RDEPUBLocation(
|
||||
bookIdentifier: bookIdentifier,
|
||||
href: fixedSpread.primaryResource.href,
|
||||
progression: 0,
|
||||
lastProgression: 1
|
||||
)
|
||||
}
|
||||
|
||||
guard let href = resourceResolver.href(forSpineIndex: page.spineIndex) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let progression: Double
|
||||
if page.totalPagesInChapter <= 1 {
|
||||
progression = 0
|
||||
} else {
|
||||
progression = Double(page.pageIndexInChapter) / Double(page.totalPagesInChapter - 1)
|
||||
}
|
||||
|
||||
return RDEPUBLocation(
|
||||
bookIdentifier: bookIdentifier,
|
||||
href: href,
|
||||
progression: progression,
|
||||
lastProgression: progression
|
||||
)
|
||||
}
|
||||
|
||||
public func currentVisibleLocation(currentPageNumber: Int, bookIdentifier: String?) -> RDEPUBLocation? {
|
||||
guard currentPageNumber > 0,
|
||||
activePages.indices.contains(currentPageNumber - 1) else {
|
||||
return nil
|
||||
}
|
||||
return fallbackLocation(for: activePages[currentPageNumber - 1], bookIdentifier: bookIdentifier)
|
||||
}
|
||||
|
||||
public func initialSpineIndex(for location: RDEPUBLocation?) -> Int {
|
||||
guard let location,
|
||||
let normalizedHref = resourceResolver.normalizedHref(location.href),
|
||||
let spineIndex = publication.spine.firstIndex(where: { resourceResolver.normalizedHref($0.href) == normalizedHref }) else {
|
||||
return 0
|
||||
}
|
||||
return spineIndex
|
||||
}
|
||||
|
||||
public func pageIndex(for location: RDEPUBLocation, bookIdentifier: String?) -> Int? {
|
||||
guard location.bookIdentifier == nil || location.bookIdentifier == bookIdentifier else {
|
||||
return nil
|
||||
}
|
||||
guard let targetHref = resourceResolver.normalizedHref(location.href) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if publication.layout == .fixed {
|
||||
return activePages.firstIndex(where: { page in
|
||||
if let fixedSpread = page.fixedSpread {
|
||||
return fixedSpread.contains(normalizedHref: targetHref, normalizer: { self.resourceResolver.normalizedHref($0) })
|
||||
}
|
||||
guard let href = self.resourceResolver.href(forSpineIndex: page.spineIndex) else {
|
||||
return false
|
||||
}
|
||||
return self.resourceResolver.normalizedHref(href) == targetHref
|
||||
})
|
||||
}
|
||||
|
||||
guard let spineIndex = publication.spine.firstIndex(where: { resourceResolver.normalizedHref($0.href) == targetHref }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let candidates = activePages.enumerated().filter { $0.element.spineIndex == spineIndex }
|
||||
guard !candidates.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
if candidates.count == 1 {
|
||||
return candidates[0].offset
|
||||
}
|
||||
|
||||
let navigationProgression = location.navigationProgression
|
||||
let localIndex = min(
|
||||
candidates.count - 1,
|
||||
max(0, Int(round(navigationProgression * Double(candidates.count - 1))))
|
||||
)
|
||||
return candidates[localIndex].offset
|
||||
}
|
||||
|
||||
public func queueNavigation(
|
||||
to location: RDEPUBLocation,
|
||||
relativeToSpineIndex spineIndex: Int? = nil,
|
||||
bookIdentifier: String?,
|
||||
targetHighlightRangeInfo: String? = nil
|
||||
) -> Int? {
|
||||
guard let normalizedLocation = resourceResolver.normalizedLocation(
|
||||
location,
|
||||
relativeToSpineIndex: spineIndex,
|
||||
bookIdentifier: bookIdentifier
|
||||
), let pageIndex = pageIndex(for: normalizedLocation, bookIdentifier: bookIdentifier) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let hasTargetHighlightRangeInfo = targetHighlightRangeInfo?.isEmpty == false
|
||||
let shouldKeepPendingNavigation =
|
||||
normalizedLocation.fragment != nil ||
|
||||
normalizedLocation.rangeAnchor != nil ||
|
||||
hasTargetHighlightRangeInfo
|
||||
pendingNavigationLocation = shouldKeepPendingNavigation ? normalizedLocation : nil
|
||||
pendingNavigationPageNum = shouldKeepPendingNavigation ? pageIndex + 1 : nil
|
||||
pendingNavigationHighlightRangeInfo = hasTargetHighlightRangeInfo ? targetHighlightRangeInfo : nil
|
||||
return pageIndex + 1
|
||||
}
|
||||
|
||||
public func updateReadingContext(
|
||||
pageNumber: Int,
|
||||
location: RDEPUBLocation,
|
||||
spineIndex: Int,
|
||||
chapterIndex: Int?,
|
||||
bookIdentifier: String?
|
||||
) {
|
||||
let normalizedLocation = resourceResolver.normalizedLocation(
|
||||
location,
|
||||
relativeToSpineIndex: spineIndex,
|
||||
bookIdentifier: bookIdentifier
|
||||
) ?? RDEPUBLocation(
|
||||
bookIdentifier: bookIdentifier,
|
||||
href: location.href,
|
||||
progression: location.progression,
|
||||
lastProgression: location.lastProgression,
|
||||
fragment: location.fragment,
|
||||
rangeAnchor: location.rangeAnchor,
|
||||
cfi: location.cfi,
|
||||
lastCFI: location.lastCFI,
|
||||
rangeCFI: location.rangeCFI
|
||||
)
|
||||
|
||||
let resource = RDEPUBViewportResource(
|
||||
href: normalizedLocation.href,
|
||||
spineIndex: spineIndex,
|
||||
progression: normalizedLocation.progression,
|
||||
lastProgression: normalizedLocation.lastProgression,
|
||||
fragment: normalizedLocation.fragment
|
||||
)
|
||||
let viewport = RDEPUBViewport(
|
||||
resources: [resource],
|
||||
visiblePageNumber: pageNumber,
|
||||
chapterIndex: chapterIndex,
|
||||
isFixedLayout: publication.layout == .fixed
|
||||
)
|
||||
currentViewport = viewport
|
||||
currentReadingContext = RDEPUBReadingContext(
|
||||
location: normalizedLocation,
|
||||
viewport: viewport,
|
||||
pageNumber: pageNumber,
|
||||
chapterIndex: chapterIndex
|
||||
)
|
||||
|
||||
if pendingNavigationPageNum == pageNumber {
|
||||
clearPendingNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
public func currentReadingLocation(bookIdentifier: String?) -> RDEPUBLocation? {
|
||||
currentReadingContext?.location ?? currentVisibleLocation(currentPageNumber: currentViewport?.visiblePageNumber ?? 0, bookIdentifier: bookIdentifier)
|
||||
}
|
||||
|
||||
public func makePaginationSnapshot(
|
||||
pageCounts: [Int],
|
||||
preferences: RDEPUBPreferences,
|
||||
layoutContext: RDEPUBNavigatorLayoutContext
|
||||
) -> PaginationSnapshot {
|
||||
if publication.layout == .fixed {
|
||||
let spreads = publication.makeFixedSpreads(
|
||||
preferences: preferences,
|
||||
viewportSize: layoutContext.viewportSize
|
||||
)
|
||||
|
||||
let chapters = spreads.enumerated().map { spreadIndex, spread in
|
||||
EPUBChapterInfo(
|
||||
spineIndex: spread.primaryResource.spineIndex,
|
||||
title: spread.primaryResource.title,
|
||||
pageCount: 1
|
||||
)
|
||||
}
|
||||
|
||||
let pages = spreads.enumerated().map { spreadIndex, spread in
|
||||
EPUBPage(
|
||||
spineIndex: spread.primaryResource.spineIndex,
|
||||
chapterIndex: spreadIndex,
|
||||
pageIndexInChapter: 0,
|
||||
totalPagesInChapter: 1,
|
||||
chapterTitle: spread.primaryResource.title,
|
||||
fixedSpread: spread
|
||||
)
|
||||
}
|
||||
|
||||
return (pages, chapters)
|
||||
}
|
||||
|
||||
var pages: [EPUBPage] = []
|
||||
var chapters: [EPUBChapterInfo] = []
|
||||
|
||||
for (spineIndex, pageCount) in pageCounts.enumerated() {
|
||||
guard pageCount > 0, spineIndex < publication.spine.count else { continue }
|
||||
|
||||
let chapterIndex = chapters.count
|
||||
let title = publication.spine[spineIndex].title
|
||||
|
||||
chapters.append(
|
||||
EPUBChapterInfo(
|
||||
spineIndex: spineIndex,
|
||||
title: title,
|
||||
pageCount: pageCount
|
||||
)
|
||||
)
|
||||
|
||||
for pageIndex in 0..<pageCount {
|
||||
pages.append(
|
||||
EPUBPage(
|
||||
spineIndex: spineIndex,
|
||||
chapterIndex: chapterIndex,
|
||||
pageIndexInChapter: pageIndex,
|
||||
totalPagesInChapter: pageCount,
|
||||
chapterTitle: title,
|
||||
fixedSpread: nil
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (pages, chapters)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user