Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. Take a look

## [Unreleased]

### Changed

#### Shared

* Converting a `Link` to a `Locator` is now synchronous: `await publication.locate(link)` becomes `publication.locator(for: link)`. The logic moved to `Manifest`, so it is also available as `manifest.locator(for: link)` without a `Publication`.

### Fixed

#### Navigator
Expand Down
2 changes: 1 addition & 1 deletion Sources/Navigator/Audiobook/AudioNavigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ public final class AudioNavigator: Navigator, Configurable, AudioSessionUser, Lo
}

public func go(to link: Link, options: NavigatorGoOptions) async -> Bool {
guard let locator = await publication.locate(link) else {
guard let locator = publication.locator(for: link) else {
return false
}
return await go(to: locator, options: options)
Expand Down
6 changes: 3 additions & 3 deletions Sources/Navigator/EPUB/EPUBNavigatorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -666,10 +666,10 @@ open class EPUBNavigatorViewController: InputObservableViewController,
let (locator, viewport) = await EPUBViewportAndLocationCalculator.compute(
Comment thread
mickael-menu marked this conversation as resolved.
readingOrderIndices: spreadView.spread.readingOrderIndices,
progression: { spreadView.progression(in: $0) },
manifest: publication.manifest,
readingOrder: readingOrder,
positionsByReadingOrder: positionsByReadingOrder,
tableOfContentsTitleByHref: tableOfContentsTitleByHref,
fallbackLocator: { [publication] in await publication.locate($0) }
tableOfContentsTitleByHref: tableOfContentsTitleByHref
)
return (locator, viewport)
}
Expand Down Expand Up @@ -727,7 +727,7 @@ open class EPUBNavigatorViewController: InputObservableViewController,
}

public func go(to link: Link, options: NavigatorGoOptions) async -> Bool {
guard let locator = await publication.locate(link) else {
guard let locator = publication.locator(for: link) else {
return false
}
return await go(to: locator, options: options)
Expand Down
18 changes: 9 additions & 9 deletions Sources/Navigator/EPUB/EPUBViewportAndLocationCalculator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import ReadiumShared

/// Computes the current `Locator` and `Viewport` from a spread's visible
/// progressions and the publication's position list.
@MainActor
enum EPUBViewportAndLocationCalculator {
/// Computes the locator and viewport for the currently visible spread.
///
Expand All @@ -20,22 +19,23 @@ enum EPUBViewportAndLocationCalculator {
/// - progression: Returns the visible scroll progression range (0–1)
/// for a given reading-order index. For fixed-layout resources this
/// is always `0...1`.
/// - readingOrder: The publication's reading order links.
/// - manifest: The publication's manifest, used to build a basic locator
/// for the first visible link when no positions are available.
/// - readingOrder: The links displayed by the navigator, indexed by
/// `readingOrderIndices`. May differ from the manifest's reading order
/// when the navigator was given a custom one.
/// - positionsByReadingOrder: Positions grouped by reading-order index.
/// May be empty if the publication has no positions.
/// - tableOfContentsTitleByHref: Mapping from resource URL to its table-
/// of-contents title, used to populate `Locator.title`.
/// - fallbackLocator: Called with the first visible link when no
/// positions are available; should return a basic locator for that
/// link (e.g. from `Publication.locate(_:)`).
static func compute(
readingOrderIndices: ClosedRange<Int>,
progression: (Int) -> ClosedRange<Double>,
manifest: Manifest,
readingOrder: [Link],
positionsByReadingOrder: [[Locator]],
tableOfContentsTitleByHref: [AnyURL: String],
fallbackLocator: (Link) async -> Locator?
) async -> (locator: Locator?, viewport: NavigatorViewport) {
tableOfContentsTitleByHref: [AnyURL: String]
) -> (locator: Locator?, viewport: NavigatorViewport) {
let firstIndex = readingOrderIndices.lowerBound
let lastIndex = readingOrderIndices.upperBound
let firstProgressionInFirstResource = min(max(progression(firstIndex).lowerBound, 0.0), 1.0)
Expand Down Expand Up @@ -117,7 +117,7 @@ enum EPUBViewportAndLocationCalculator {
return (locator, viewport)

} else {
locator = await fallbackLocator(link)?.copy(
locator = manifest.locator(for: link)?.copy(
locations: { $0.progression = firstProgressionInFirstResource }
)

Expand Down
2 changes: 1 addition & 1 deletion Sources/Navigator/PDF/PDFNavigatorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -728,7 +728,7 @@ open class PDFNavigatorViewController:
}

public func go(to link: Link, options: NavigatorGoOptions) async -> Bool {
guard let locator = await publication.locate(link) else {
guard let locator = publication.locator(for: link) else {
return false
}

Expand Down
27 changes: 27 additions & 0 deletions Sources/Shared/Publication/Manifest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -171,4 +171,31 @@ public struct Manifest: Hashable, Sendable, JSONValueDecodable, JSONObjectEncoda
public func linksMatching(_ predicate: (Link) -> Bool) -> [Link] {
(readingOrder + resources + links).filter(predicate)
}

/// Creates a new `Locator` pointing to the resource targeted by the given
/// `link`.
///
/// Returns `nil` if the resource is not found in the manifest.
public func locator(for link: Link) -> Locator? {
let originalHREF = link.url()
let fragment = originalHREF.fragment
let href = originalHREF.removingFragment()

guard
let resourceLink = linkWithHREF(href),
let mediaType = resourceLink.mediaType
else {
return nil
}

return Locator(
href: href,
mediaType: mediaType,
title: resourceLink.title ?? link.title,
locations: Locator.Locations(
fragments: Array(ofNotNil: fragment),
progression: (fragment == nil) ? 0.0 : nil
)
)
}
}
13 changes: 13 additions & 0 deletions Sources/Shared/Publication/Publication.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,19 @@ public final class Publication: Sendable, Loggable {
manifest.linksWithRel(rel)
}

/// Creates a new `Locator` pointing to the resource targeted by the given
/// `link`.
///
/// Returns `nil` if the resource is not found in the publication.
public func locator(for link: Link) -> Locator? {
manifest.locator(for: link)
}

@available(*, unavailable, renamed: "locator(for:)")
public func locate(_ link: Link) async -> Locator? {
fatalError()
}

/// Returns the resource targeted by the given `link`.
public func get(_ link: Link) -> Resource? {
assert(!link.templated, "You must expand templated links before calling `Publication.get`")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public actor PublicationContentIterator: ContentIterator, Loggable {
let link = publication.readingOrder[index]
guard
let resource = publication.get(link),
let locator = await location.toLocator(to: link, in: publication)
let locator = location.toLocator(to: link, in: publication)
else {
return nil
}
Expand All @@ -149,12 +149,12 @@ private enum LocatorOrProgression {
case locator(Locator)
case progression(Double)

func toLocator(to link: Link, in publication: Publication) async -> Locator? {
func toLocator(to link: Link, in publication: Publication) -> Locator? {
switch self {
case let .locator(locator):
return locator
case let .progression(progression):
return await publication.locate(link)?.copy(locations: { $0.progression = progression })
return publication.locator(for: link)?.copy(locations: { $0.progression = progression })
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,9 @@ public final class DefaultLocatorService: Sendable, LocatorService, Loggable {
return nil
}

@available(*, unavailable, message: "Use `Publication.locator(for:)` instead.")
public func locate(_ link: Link) async -> Locator? {
let originalHREF = link.url()
let fragment = originalHREF.fragment
let href = originalHREF.removingFragment()

guard
let resourceLink = publication()?.linkWithHREF(href),
let type = resourceLink.mediaType
else {
return nil
}

return Locator(
href: href,
mediaType: type,
title: resourceLink.title ?? link.title,
locations: Locator.Locations(
fragments: Array(ofNotNil: fragment),
progression: (fragment == nil) ? 0.0 : nil
)
)
fatalError()
}

public func locate(progression totalProgression: Double) async -> Locator? {
Expand Down
12 changes: 0 additions & 12 deletions Sources/Shared/Publication/Services/Locator/LocatorService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ public protocol LocatorService: PublicationService {
/// Locates the target of the given `locator`.
func locate(_ locator: Locator) async -> Locator?

/// Locates the target of the given `link`.
func locate(_ link: Link) async -> Locator?

/// Locates the target at the given `progression` relative to the whole publication.
func locate(progression: Double) async -> Locator?
}
Expand All @@ -31,10 +28,6 @@ public extension LocatorService {
nil
}

func locate(_ link: Link) async -> Locator? {
nil
}

func locate(progression: Double) async -> Locator? {
nil
}
Expand All @@ -52,11 +45,6 @@ public extension Publication {
func locate(progression: Double) async -> Locator? {
await findService(LocatorService.self)?.locate(progression: progression)
}

/// Locates the target of the given `link`.
func locate(_ link: Link) async -> Locator? {
await findService(LocatorService.self)?.locate(link)
}
}

// MARK: PublicationServicesBuilder Helpers
Expand Down
11 changes: 2 additions & 9 deletions Sources/Streamer/Parser/Audio/Services/AudioLocatorService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,13 @@ final class AudioLocatorService: LocatorService {
/// Total duration of the publication.
private let totalDuration: Double?

private let locatorService: DefaultLocatorService

init(readingOrder: [Link], publication: Weak<Publication>) {
self.publication = publication
self.readingOrder = readingOrder
let durations = readingOrder.map { $0.duration ?? 0 }
self.durations = durations
let total = durations.reduce(0, +)
totalDuration = (total > 0) ? total : nil
locatorService = DefaultLocatorService(publication: publication)
}

func locate(_ locator: Locator) async -> Locator? {
Expand All @@ -50,8 +47,8 @@ final class AudioLocatorService: LocatorService {

// Routes the `totalProgression` fallback through this service's audio
// `locate(progression:)`, which is duration-based. Delegating to
// `locatorService.locate(locator)` would instead use the default
// positions-based progression and lose the audio behavior.
// `DefaultLocatorService` would instead use the default positions-based
// progression and lose the audio behavior.
if
let totalProgression = locator.locations.totalProgression,
let target = await locate(progression: totalProgression)
Expand All @@ -65,10 +62,6 @@ final class AudioLocatorService: LocatorService {
return nil
}

func locate(_ link: Link) async -> Locator? {
await locatorService.locate(link)
}

func locate(progression: Double) async -> Locator? {
guard let totalDuration = totalDuration else {
return nil
Expand Down
6 changes: 2 additions & 4 deletions TestApp/Sources/Reader/Common/Outline/OutlineTableView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,8 @@ struct OutlineTableView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
Task {
if let locator = await publication.locate(item.link) {
locatorSubject.send(locator)
}
if let locator = publication.locator(for: item.link) {
locatorSubject.send(locator)
}
}
}
Expand Down
10 changes: 5 additions & 5 deletions TestApp/Sources/Reader/Common/VisualReaderViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -362,10 +362,10 @@ class VisualReaderViewController<N: UIViewController & Navigator>: ReaderViewCon
/// description of the image.
private func openFromImagePreview(_ link: ReadiumShared.Link) {
Task {
// `locate` runs first: the href is only inspected once it comes
// back nil, so a remote publication whose in-publication links are
// themselves absolute is not misrouted to the browser.
let locator = await publication.locate(link)
// `locator(for:)` runs first: the href is only inspected once it
// comes back nil, so a remote publication whose in-publication
// links are themselves absolute is not misrouted to the browser.
let locator = publication.locator(for: link)
let externalURL = (locator == nil) ? link.httpURL : nil
if locator == nil, externalURL == nil {
log(.error, "Cannot locate the extended description at \(link.href)")
Expand Down Expand Up @@ -416,7 +416,7 @@ extension VisualReaderViewController {
for (index, link) in publication.pageList.enumerated() {
guard
let title = link.title,
let locator = await publication.locate(link)
let locator = publication.locator(for: link)
else {
continue
}
Expand Down
Loading
Loading