Skip to content

Commit 89d552b

Browse files
authored
Add EPUBNavigatorViewController's viewport (#612)
1 parent 126e48f commit 89d552b

File tree

14 files changed

+231
-132
lines changed

14 files changed

+231
-132
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
All notable changes to this project will be documented in this file. Take a look at [the migration guide](docs/Migration%20Guide.md) to upgrade between two major versions.
44

5-
<!-- ## [Unreleased] -->
5+
## [Unreleased]
6+
7+
### Added
8+
9+
#### Navigator
10+
11+
* You can now access the `viewport` property of an `EPUBNavigatorViewController` to obtain information about the visible portion of the publication, including the visible positions and reading order indices.
12+
613

714
## [3.3.0]
815

Sources/Navigator/Audiobook/AudioNavigator.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ public final class AudioNavigator: Navigator, Configurable, AudioSessionUser, Lo
194194

195195
/// Resumes or start the playback.
196196
public func play() {
197-
playTask = Task {
197+
playTask = Task { @MainActor in
198198
AudioSession.shared.start(with: self, isPlaying: false)
199199

200200
if player.currentItem == nil {
@@ -204,7 +204,7 @@ public final class AudioNavigator: Navigator, Configurable, AudioSessionUser, Lo
204204
await go(to: link)
205205
}
206206
}
207-
await player.playImmediately(atRate: Float(settings.speed))
207+
player.playImmediately(atRate: Float(settings.speed))
208208
}
209209
}
210210

Sources/Navigator/EPUB/Assets/Static/scripts/readium-fixed.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/Navigator/EPUB/Assets/Static/scripts/readium-reflowable.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/Navigator/EPUB/EPUBFixedSpreadView.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,11 @@ final class EPUBFixedSpreadView: EPUBSpreadView {
9595
}
9696
// We call this directly on the web view on purpose, because this needs
9797
// to be executed before the spread is loaded.
98-
webView.evaluateJavaScript("spread.load(\(spread.jsonString(forBaseURL: viewModel.publicationBaseURL)));")
98+
let spreadJSON = spread.jsonString(
99+
forBaseURL: viewModel.publicationBaseURL,
100+
readingOrder: viewModel.readingOrder
101+
)
102+
webView.evaluateJavaScript("spread.load(\(spreadJSON));")
99103
}
100104

101105
override func spreadDidLoad() async {

Sources/Navigator/EPUB/EPUBNavigatorViewController.swift

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,18 @@ import SwiftSoup
1111
import UIKit
1212
import WebKit
1313

14-
public protocol EPUBNavigatorDelegate: VisualNavigatorDelegate, SelectableNavigatorDelegate {
14+
@MainActor public protocol EPUBNavigatorDelegate: VisualNavigatorDelegate, SelectableNavigatorDelegate {
15+
/// Called when the viewport is updated.
16+
func navigator(_ navigator: EPUBNavigatorViewController, viewportDidChange viewport: EPUBNavigatorViewController.Viewport?)
17+
1518
// MARK: - WebView Customization
1619

1720
func navigator(_ navigator: EPUBNavigatorViewController, setupUserScripts userContentController: WKUserContentController)
1821
}
1922

2023
public extension EPUBNavigatorDelegate {
24+
func navigator(_ navigator: EPUBNavigatorViewController, viewportDidChange viewport: EPUBNavigatorViewController.Viewport?) {}
25+
2126
func navigator(_ navigator: EPUBNavigatorViewController, setupUserScripts userContentController: WKUserContentController) {}
2227
}
2328

@@ -117,6 +122,24 @@ open class EPUBNavigatorViewController: InputObservableViewController,
117122

118123
public weak var delegate: EPUBNavigatorDelegate?
119124

125+
/// Information about the visible portion of the publication, when rendered.
126+
public private(set) var viewport: Viewport? {
127+
didSet {
128+
if oldValue != viewport {
129+
delegate?.navigator(self, viewportDidChange: viewport)
130+
}
131+
}
132+
}
133+
134+
/// Information about the visible portion of the publication.
135+
public struct Viewport: Equatable {
136+
/// Indices of the visible reading order resources.
137+
public var readingOrderIndices: ClosedRange<[Link].Index>
138+
139+
/// Range of visible positions.
140+
public var positions: ClosedRange<Int>?
141+
}
142+
120143
/// Navigation state.
121144
private enum State: Equatable {
122145
/// Initializing the navigator.
@@ -488,15 +511,6 @@ open class EPUBNavigatorViewController: InputObservableViewController,
488511
paginationView?.currentIndex ?? 0
489512
}
490513

491-
// Reading order index of the left-most resource in the visible spread.
492-
private var currentResourceIndex: Int? {
493-
guard spreads.indices.contains(currentSpreadIndex) else {
494-
return nil
495-
}
496-
497-
return readingOrder.firstIndexWithHREF(spreads[currentSpreadIndex].left.url())
498-
}
499-
500514
private var reloadSpreadsContinuations = [CheckedContinuation<Void, Never>]()
501515
private var needsReloadSpreads = false
502516

@@ -542,8 +556,12 @@ open class EPUBNavigatorViewController: InputObservableViewController,
542556
spread: viewModel.spreadEnabled
543557
)
544558

545-
let initialIndex: Int = {
546-
if let href = locator?.href, let foundIndex = self.spreads.firstIndexWithHREF(href) {
559+
let initialIndex: ReadingOrder.Index = {
560+
if
561+
let href = locator?.href,
562+
let index = readingOrder.firstIndexWithHREF(href),
563+
let foundIndex = self.spreads.firstIndexWithReadingOrderIndex(index)
564+
{
547565
return foundIndex
548566
} else {
549567
return 0
@@ -561,9 +579,16 @@ open class EPUBNavigatorViewController: InputObservableViewController,
561579
}
562580

563581
private func loadedSpreadViewForHREF<T: URLConvertible>(_ href: T) -> EPUBSpreadView? {
564-
paginationView?.loadedViews
582+
guard
583+
let loadedViews = paginationView?.loadedViews,
584+
let index = readingOrder.firstIndexWithHREF(href)
585+
else {
586+
return nil
587+
}
588+
589+
return loadedViews
565590
.compactMap { _, view in view as? EPUBSpreadView }
566-
.first { $0.spread.links.firstWithHREF(href) != nil }
591+
.first { $0.spread.contains(index: index) }
567592
}
568593

569594
// MARK: - Navigator
@@ -582,45 +607,64 @@ open class EPUBNavigatorViewController: InputObservableViewController,
582607
)
583608
}
584609

585-
private func computeCurrentLocation() async -> Locator? {
610+
private func computeCurrentLocationAndViewport() async -> (Locator?, Viewport?) {
586611
if case .initializing = state {
587612
assertionFailure("Cannot update current location when initializing the navigator")
588-
return nil
613+
return (nil, nil)
589614
}
590615

591616
// Returns any pending locator to prevent returning invalid locations
592617
// while loading it.
593618
if let pendingLocator = state.pendingLocator {
594-
return pendingLocator
619+
return (pendingLocator, nil)
595620
}
596621

597622
guard let spreadView = paginationView?.currentView as? EPUBSpreadView else {
598-
return nil
623+
return (nil, nil)
599624
}
600625

601-
let link = spreadView.focusedResource ?? spreadView.spread.leading
626+
let index = spreadView.focusedResource ?? spreadView.spread.leading
627+
let link = readingOrder[index]
602628
let href = link.url()
603-
let progression = min(max(spreadView.progression(in: href), 0.0), 1.0)
629+
let progressionRange = spreadView.progression(in: index)
630+
let firstProgression = min(max(progressionRange.lowerBound, 0.0), 1.0)
631+
let lastProgression = min(max(progressionRange.upperBound, 0.0), 1.0)
632+
633+
let location: Locator?
634+
var viewport = Viewport(
635+
readingOrderIndices: spreadView.spread.readingOrderIndices,
636+
positions: nil
637+
)
604638

605639
if
606640
// The positions are not always available, for example a Readium
607641
// WebPub doesn't have any unless a Publication Positions Web
608642
// Service is provided
609643
let index = readingOrder.firstIndexWithHREF(href),
610644
let positionList = positionsByReadingOrder.getOrNil(index),
611-
positionList.count > 0
645+
positionList.count > 0,
646+
let positionOffset = positionList[0].locations.position
612647
{
613648
// Gets the current locator from the positionList, and fill its missing data.
614-
let positionIndex = Int(ceil(progression * Double(positionList.count - 1)))
615-
return await positionList[positionIndex].copy(
649+
let firstPositionIndex = Int(ceil(firstProgression * Double(positionList.count - 1)))
650+
let lastPositionIndex = (lastProgression == 1.0)
651+
? positionList.count - 1
652+
: max(firstPositionIndex, Int(ceil(lastProgression * Double(positionList.count - 1))) - 1)
653+
654+
location = await positionList[firstPositionIndex].copy(
616655
title: tableOfContentsTitleByHref[equivalent: href],
617-
locations: { $0.progression = progression }
656+
locations: { $0.progression = firstProgression }
618657
)
658+
659+
viewport.positions = (positionOffset + firstPositionIndex) ... (positionOffset + lastPositionIndex)
660+
619661
} else {
620-
return await publication.locate(link)?.copy(
621-
locations: { $0.progression = progression }
662+
location = await publication.locate(link)?.copy(
663+
locations: { $0.progression = firstProgression }
622664
)
623665
}
666+
667+
return (location, viewport)
624668
}
625669

626670
public func firstVisibleElementLocator() async -> Locator? {
@@ -643,7 +687,7 @@ open class EPUBNavigatorViewController: InputObservableViewController,
643687
return
644688
}
645689

646-
currentLocation = await computeCurrentLocation()
690+
(currentLocation, viewport) = await computeCurrentLocationAndViewport()
647691

648692
if
649693
let delegate = delegate,
@@ -660,7 +704,8 @@ open class EPUBNavigatorViewController: InputObservableViewController,
660704

661705
guard
662706
let paginationView = paginationView,
663-
let spreadIndex = spreads.firstIndexWithHREF(locator.href),
707+
let index = readingOrder.firstIndexWithHREF(locator.href),
708+
let spreadIndex = spreads.firstIndexWithReadingOrderIndex(index),
664709
on(.jump(locator))
665710
else {
666711
return false
@@ -900,7 +945,8 @@ extension EPUBNavigatorViewController: EPUBNavigatorViewModelDelegate {
900945
for (_, view) in paginationView.loadedViews {
901946
guard
902947
let view = view as? EPUBSpreadView,
903-
view.spread.links.firstWithHREF(href) != nil
948+
let index = readingOrder.firstIndexWithHREF(href),
949+
view.spread.contains(index: index)
904950
else {
905951
continue
906952
}
@@ -943,7 +989,10 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate {
943989
}
944990
.joined(separator: "\n")
945991

946-
for link in spreadView.spread.links {
992+
let links = spreadView.spread.readingOrderIndices
993+
.compactMap { readingOrder.getOrNil($0) }
994+
995+
for link in links {
947996
let href = link.url()
948997
for (group, decorations) in decorations {
949998
let decorations = decorations

Sources/Navigator/EPUB/EPUBNavigatorViewModel.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ final class EPUBNavigatorViewModel: Loggable {
3838
/// `httpServer`. This is used to serve custom font files, for example.
3939
@Atomic private var servedFiles: [FileURL: HTTPURL] = [:]
4040

41+
var readingOrder: ReadingOrder { publication.readingOrder }
42+
4143
convenience init(
4244
publication: Publication,
4345
config: EPUBNavigatorViewController.Configuration,

Sources/Navigator/EPUB/EPUBReflowableSpreadView.swift

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -67,11 +67,11 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
6767
}
6868

6969
override func loadSpread() {
70-
guard spread.links.count == 1 else {
70+
guard spread.readingOrderIndices.count == 1 else {
7171
log(.error, "Only one document at a time can be displayed in a reflowable spread")
7272
return
7373
}
74-
let link = spread.leading
74+
let link = viewModel.readingOrder[spread.leading]
7575
let url = viewModel.url(to: link)
7676
webView.load(URLRequest(url: url.url))
7777
}
@@ -131,18 +131,21 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
131131

132132
// MARK: - Location and progression
133133

134-
override func progression<T>(in href: T) -> Double where T: URLConvertible {
134+
override func progression(in index: ReadingOrder.Index) -> Range<Double> {
135135
guard
136-
spread.leading.url().isEquivalentTo(href),
136+
spread.leading == index,
137137
let progression = progression
138138
else {
139-
return 0
139+
return 0 ..< 0
140140
}
141141
return progression
142142
}
143143

144144
override func spreadDidLoad() async {
145-
if let linkJSON = serializeJSONString(spread.leading.json) {
145+
if
146+
let link = viewModel.readingOrder.getOrNil(spread.leading),
147+
let linkJSON = serializeJSONString(link.json)
148+
{
146149
await evaluateScript("readium.link = \(linkJSON);")
147150
}
148151

@@ -240,9 +243,14 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
240243

241244
@discardableResult
242245
private func go(to locator: Locator) async -> Bool {
243-
guard ["", "#"].contains(locator.href.string) || spread.contains(href: locator.href) else {
244-
log(.warning, "The locator's href is not in the spread")
245-
return false
246+
if !["", "#"].contains(locator.href.string) {
247+
guard
248+
let index = viewModel.readingOrder.firstIndexWithHREF(locator.href),
249+
spread.contains(index: index)
250+
else {
251+
log(.warning, "The locator's href is not in the spread")
252+
return false
253+
}
246254
}
247255

248256
if locator.text.highlight != nil {
@@ -310,22 +318,29 @@ final class EPUBReflowableSpreadView: EPUBSpreadView {
310318

311319
// MARK: - Progression
312320

313-
// Current progression in the page.
314-
private var progression: Double?
321+
// Current progression range in the page.
322+
private var progression: Range<Double>?
315323
// To check if a progression change was cancelled or not.
316-
private var previousProgression: Double?
324+
private var previousProgression: Range<Double>?
317325

318326
// Called by the javascript code to notify that scrolling ended.
319327
private func progressionDidChange(_ body: Any) {
320-
guard isSpreadLoaded, let bodyString = body as? String, var newProgression = Double(bodyString) else {
328+
guard
329+
isSpreadLoaded,
330+
let body = body as? [String: Any],
331+
var firstProgression = body["first"] as? Double,
332+
var lastProgression = body["last"] as? Double
333+
else {
321334
return
322335
}
323-
newProgression = min(max(newProgression, 0.0), 1.0)
336+
precondition(firstProgression <= lastProgression)
337+
firstProgression = min(max(firstProgression, 0.0), 1.0)
338+
lastProgression = min(max(lastProgression, 0.0), 1.0)
324339

325340
if previousProgression == nil {
326341
previousProgression = progression
327342
}
328-
progression = newProgression
343+
progression = firstProgression ..< lastProgression
329344

330345
setNeedsNotifyPagesDidChange()
331346
}

0 commit comments

Comments
 (0)