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,191 @@
|
||||
import Foundation
|
||||
|
||||
struct RDEPUBJumpSession {
|
||||
|
||||
let anchorSpineIndex: Int
|
||||
|
||||
let createdAt: CFAbsoluteTime
|
||||
|
||||
let protectedSpineIndices: Set<Int>
|
||||
|
||||
let sequenceNumber: Int
|
||||
|
||||
let expiresAt: CFAbsoluteTime
|
||||
|
||||
let reason: Reason
|
||||
|
||||
enum Reason {
|
||||
case tableOfContentsJump
|
||||
case bookmarkJump
|
||||
case searchJump
|
||||
}
|
||||
|
||||
enum EndReason {
|
||||
|
||||
case coverageComplete
|
||||
|
||||
case navigatedAway
|
||||
|
||||
case timeout
|
||||
|
||||
case superseded
|
||||
}
|
||||
}
|
||||
|
||||
public struct RDEPUBJumpSessionPolicy: Equatable {
|
||||
|
||||
public let exitPageThreshold: Int
|
||||
|
||||
public let timeout: TimeInterval
|
||||
|
||||
public let idleGracePeriod: TimeInterval
|
||||
|
||||
public let protectedNeighborRadius: Int
|
||||
|
||||
public static let `default` = RDEPUBJumpSessionPolicy(
|
||||
exitPageThreshold: 6,
|
||||
timeout: 20,
|
||||
idleGracePeriod: 1.5,
|
||||
protectedNeighborRadius: 1
|
||||
)
|
||||
|
||||
public init(
|
||||
exitPageThreshold: Int = 6,
|
||||
timeout: TimeInterval = 20,
|
||||
idleGracePeriod: TimeInterval = 1.5,
|
||||
protectedNeighborRadius: Int = 1
|
||||
) {
|
||||
self.exitPageThreshold = exitPageThreshold
|
||||
self.timeout = timeout
|
||||
self.idleGracePeriod = idleGracePeriod
|
||||
self.protectedNeighborRadius = protectedNeighborRadius
|
||||
}
|
||||
}
|
||||
|
||||
final class RDEPUBJumpSessionManager {
|
||||
|
||||
private unowned let context: RDEPUBReaderContext
|
||||
|
||||
private(set) var activeSession: RDEPUBJumpSession?
|
||||
|
||||
private var nextSequenceNumber: Int = 0
|
||||
|
||||
private var consecutivePageCount: Int = 0
|
||||
|
||||
private var lastPageDirection: PageDirection?
|
||||
|
||||
private var lastActivityTime: CFAbsoluteTime = 0
|
||||
|
||||
enum PageDirection {
|
||||
case forward
|
||||
case backward
|
||||
}
|
||||
|
||||
init(context: RDEPUBReaderContext) {
|
||||
self.context = context
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func createSession(
|
||||
anchorSpineIndex: Int,
|
||||
reason: RDEPUBJumpSession.Reason,
|
||||
totalSpineCount: Int
|
||||
) -> RDEPUBJumpSession {
|
||||
let policy = context.configuration.jumpSessionPolicy
|
||||
let now = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
var protectedIndices: Set<Int> = [anchorSpineIndex]
|
||||
for offset in 1...policy.protectedNeighborRadius {
|
||||
let lower = anchorSpineIndex - offset
|
||||
let upper = anchorSpineIndex + offset
|
||||
if lower >= 0 {
|
||||
protectedIndices.insert(lower)
|
||||
}
|
||||
if upper < totalSpineCount {
|
||||
protectedIndices.insert(upper)
|
||||
}
|
||||
}
|
||||
|
||||
nextSequenceNumber += 1
|
||||
let session = RDEPUBJumpSession(
|
||||
anchorSpineIndex: anchorSpineIndex,
|
||||
createdAt: now,
|
||||
protectedSpineIndices: protectedIndices,
|
||||
sequenceNumber: nextSequenceNumber,
|
||||
expiresAt: now + policy.timeout,
|
||||
reason: reason
|
||||
)
|
||||
|
||||
activeSession = session
|
||||
consecutivePageCount = 0
|
||||
lastPageDirection = nil
|
||||
lastActivityTime = now
|
||||
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func recordPageChange(fromSpineIndex: Int, toSpineIndex: Int) {
|
||||
guard activeSession != nil else { return }
|
||||
|
||||
let direction: PageDirection = toSpineIndex >= fromSpineIndex ? .forward : .backward
|
||||
lastActivityTime = CFAbsoluteTimeGetCurrent()
|
||||
|
||||
if direction == lastPageDirection {
|
||||
consecutivePageCount += 1
|
||||
} else {
|
||||
consecutivePageCount = 1
|
||||
lastPageDirection = direction
|
||||
}
|
||||
}
|
||||
|
||||
func shouldAllowPageMapTakeover(candidateSpineIndices: Set<Int>) -> Bool {
|
||||
guard let session = activeSession else {
|
||||
return true
|
||||
}
|
||||
|
||||
let protectedIndices = session.protectedSpineIndices
|
||||
let coverageRatio = Double(protectedIndices.intersection(candidateSpineIndices).count) /
|
||||
Double(protectedIndices.count)
|
||||
|
||||
return coverageRatio >= 0.8
|
||||
}
|
||||
|
||||
func checkSessionEnd(currentSpineIndex: Int, isIdle: Bool) -> RDEPUBJumpSession.EndReason? {
|
||||
guard let session = activeSession else { return nil }
|
||||
|
||||
let now = CFAbsoluteTimeGetCurrent()
|
||||
let policy = context.configuration.jumpSessionPolicy
|
||||
|
||||
if now >= session.expiresAt {
|
||||
if isIdle || (now - lastActivityTime) >= policy.idleGracePeriod {
|
||||
return .timeout
|
||||
}
|
||||
}
|
||||
|
||||
if !session.protectedSpineIndices.contains(currentSpineIndex) {
|
||||
if consecutivePageCount >= policy.exitPageThreshold {
|
||||
return .navigatedAway
|
||||
}
|
||||
} else {
|
||||
|
||||
consecutivePageCount = 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func endSession(_ reason: RDEPUBJumpSession.EndReason) {
|
||||
guard let session = activeSession else { return }
|
||||
activeSession = nil
|
||||
consecutivePageCount = 0
|
||||
lastPageDirection = nil
|
||||
}
|
||||
|
||||
func clearSession() {
|
||||
activeSession = nil
|
||||
consecutivePageCount = 0
|
||||
lastPageDirection = nil
|
||||
nextSequenceNumber = 0
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user