ReadViewSDK/Sources/RDEpubReaderView/EPUBUI/ReaderController/RDEPUBJumpSession.swift
shenlei d7fcda345d 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
2026-07-10 19:44:53 +09:00

192 lines
4.8 KiB
Swift

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
}
}