diff --git a/gradle.properties b/gradle.properties index 3b86475..2b7c022 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ POM_SCM_CONNECTION=scm:git:git://github.com/ThePalaceProject/android-r2 POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/ThePalaceProject/android-r2 POM_SCM_URL=http://github.com/ThePalaceProject/android-r2 POM_URL=http://github.com/ThePalaceProject/android-r2 -VERSION_NAME=3.2.0-SNAPSHOT +VERSION_NAME=4.0.0-SNAPSHOT VERSION_PREVIOUS=3.1.0 android.useAndroidX=true diff --git a/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Bookmark.kt b/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Bookmark.kt index 2da5da4..bdc44d7 100644 --- a/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Bookmark.kt +++ b/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Bookmark.kt @@ -44,12 +44,6 @@ data class SR2Bookmark( */ val uri: URI?, - - /** - * A flag that indicates if the bookmark is being deleted or not. - */ - - var isBeingDeleted: Boolean = false, ) { init { diff --git a/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Command.kt b/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Command.kt index cbef72b..55955fc 100644 --- a/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Command.kt +++ b/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Command.kt @@ -99,16 +99,6 @@ sealed class SR2Command { data object CancelSearch : SR2Command() - /** - * Load a set of bookmarks into the controller. This merely has the effect of making - * the bookmarks available to the table of contents; it does not trigger any changes - * in navigation. - */ - - data class BookmarksLoad( - val bookmarks: List, - ) : SR2Command() - /** * Create a new (explicit) bookmark at the current reading position. */ diff --git a/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2ControllerConfiguration.kt b/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2ControllerConfiguration.kt index f0b4bb0..4ed383c 100644 --- a/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2ControllerConfiguration.kt +++ b/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2ControllerConfiguration.kt @@ -58,4 +58,10 @@ data class SR2ControllerConfiguration( */ val pageNumberingMode: SR2PageNumberingMode, + + /** + * The initial set of bookmarks. + */ + + val initialBookmarks: List, ) diff --git a/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2ControllerType.kt b/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2ControllerType.kt index 78346cd..b6e5570 100644 --- a/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2ControllerType.kt +++ b/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2ControllerType.kt @@ -51,7 +51,7 @@ interface SR2ControllerType : Closeable, SR2ControllerCommandQueueType { /** * The list of bookmarks currently loaded into the controller. This list is an immutable - * snapshots, and subsequent updates to bookmarks will not be reflected in the returned list. + * snapshot, and subsequent updates to bookmarks will not be reflected in the returned list. */ fun bookmarksNow(): List diff --git a/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Event.kt b/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Event.kt index 3f099ae..c00c786 100644 --- a/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Event.kt +++ b/org.librarysimplified.r2.api/src/main/java/org/librarysimplified/r2/api/SR2Event.kt @@ -1,6 +1,5 @@ package org.librarysimplified.r2.api -import org.readium.r2.shared.Search import org.readium.r2.shared.publication.Href import org.readium.r2.shared.publication.services.search.SearchIterator @@ -83,54 +82,20 @@ sealed class SR2Event { sealed class SR2BookmarkEvent : SR2Event() { /** - * Create a bookmark. + * A bookmark was created in the reader. The observer of this event should save the bookmark + * into persistent storage. */ - data class SR2BookmarkCreate( - val bookmark: SR2Bookmark, - val onBookmarkCreationCompleted: (SR2Bookmark?) -> Unit, - ) : SR2BookmarkEvent() - - /** - * A bookmark was created. - */ - - @Deprecated("This event will stop being published soon.") data class SR2BookmarkCreated( val bookmark: SR2Bookmark, ) : SR2BookmarkEvent() /** - * A bookmark was deleted. + * A bookmark was deleted in the reader. The observer of this event should delete the bookmark + * from persistent storage. */ - - @Deprecated("This event will stop being published soon.") data class SR2BookmarkDeleted( val bookmark: SR2Bookmark, ) : SR2BookmarkEvent() - - /** - * A bookmark failed to be deleted. - */ - - object SR2BookmarkFailedToBeDeleted : SR2BookmarkEvent() - - /** - * Try to delete a bookmark. - */ - - data class SR2BookmarkTryToDelete( - val bookmark: SR2Bookmark, - val onDeleteOperationFinished: (Boolean) -> Unit, - ) : SR2BookmarkEvent() - - /** - * Bookmarks were loaded into the controller. - */ - - object SR2BookmarksLoaded : SR2BookmarkEvent() { - override fun toString(): String = - "[SR2BookmarksLoaded]" - } } /** @@ -182,7 +147,6 @@ sealed class SR2Event { * The searching command is finished and returns a search iterator containing the results */ - @OptIn(Search::class) data class SR2CommandSearchResults constructor( override val command: SR2Command, val searchIterator: SearchIterator?, diff --git a/org.librarysimplified.r2.demo/src/main/java/org/librarysimplified/r2/demo/DemoActivity.kt b/org.librarysimplified.r2.demo/src/main/java/org/librarysimplified/r2/demo/DemoActivity.kt index b6a78ac..f948b3a 100644 --- a/org.librarysimplified.r2.demo/src/main/java/org/librarysimplified/r2/demo/DemoActivity.kt +++ b/org.librarysimplified.r2.demo/src/main/java/org/librarysimplified/r2/demo/DemoActivity.kt @@ -12,14 +12,9 @@ import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment import io.reactivex.disposables.CompositeDisposable import kotlinx.coroutines.runBlocking -import org.librarysimplified.r2.api.SR2Command import org.librarysimplified.r2.api.SR2Event -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkCreate import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkCreated import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkDeleted -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkFailedToBeDeleted -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkTryToDelete -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarksLoaded import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandEventCompleted.SR2CommandExecutionFailed import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandEventCompleted.SR2CommandExecutionSucceeded import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandExecutionRunningLong @@ -131,20 +126,6 @@ class DemoActivity : AppCompatActivity(R.layout.demo_activity_host) { @UiThread private fun onControllerBecameAvailable(reference: SR2ControllerReference) { this.switchFragment(SR2ReaderFragment()) - - if (reference.isFirstStartup) { - // Navigate to the first chapter or saved reading position. - val bookId = reference.controller.bookMetadata.id - reference.controller.submitCommand( - SR2Command.BookmarksLoad(DemoModel.database.bookmarksFor(bookId)), - ) - val lastRead = DemoModel.database.bookmarkFindLastReadLocation(bookId) - val startLocator = lastRead?.locator ?: reference.controller.bookMetadata.start - reference.controller.submitCommand(SR2Command.OpenChapter(startLocator)) - } else { - // Refresh whatever the controller was looking at previously. - reference.controller.submitCommand(SR2Command.Refresh) - } } /** @@ -178,6 +159,7 @@ class DemoActivity : AppCompatActivity(R.layout.demo_activity_host) { theme = DemoModel.database.theme(), context = DemoApplication.application, controllers = SR2Controllers(), + bookmarks = DemoModel.database.bookmarksFor(id), ) } @@ -275,12 +257,11 @@ class DemoActivity : AppCompatActivity(R.layout.demo_activity_host) { private fun onControllerEvent(event: SR2Event) { when (event) { - is SR2BookmarkCreate -> { + is SR2BookmarkCreated -> { DemoModel.database.bookmarkSave( SR2ReaderModel.controller().bookMetadata.id, event.bookmark, ) - event.onBookmarkCreationCompleted(event.bookmark) } is SR2BookmarkDeleted -> { @@ -294,12 +275,8 @@ class DemoActivity : AppCompatActivity(R.layout.demo_activity_host) { DemoModel.database.themeSet(event.theme) } - is SR2BookmarkCreated, is SR2OnCenterTapped, is SR2ReadingPositionChanged, - SR2BookmarksLoaded, - SR2BookmarkFailedToBeDeleted, - is SR2BookmarkTryToDelete, is SR2ChapterNonexistent, is SR2WebViewInaccessible, is SR2ExternalLinkSelected, diff --git a/org.librarysimplified.r2.tests/src/test/java/org/librarysimplified/r2/tests/SR2NavigationGraphsTest.kt b/org.librarysimplified.r2.tests/src/test/java/org/librarysimplified/r2/tests/SR2NavigationGraphsTest.kt index b1bed3a..d674492 100644 --- a/org.librarysimplified.r2.tests/src/test/java/org/librarysimplified/r2/tests/SR2NavigationGraphsTest.kt +++ b/org.librarysimplified.r2.tests/src/test/java/org/librarysimplified/r2/tests/SR2NavigationGraphsTest.kt @@ -85,7 +85,7 @@ class SR2NavigationGraphsTest { assertEquals(4, graph.resources.size) assertEquals("Something", graph.readingOrder[0].navigationPoint.title) - var target: SR2NavigationTarget? = null + var target: SR2NavigationTarget? target = graph.findNavigationNode(SR2LocatorPercent.start(Href("/epub/text/p1.xhtml")!!)) assertEquals("Something Nested", target!!.node.navigationPoint.title) assertEquals(null, target.extraFragment) @@ -142,7 +142,7 @@ class SR2NavigationGraphsTest { node = graph.findNextNode(node) assertEquals(null, node) - var target: SR2NavigationTarget? = null + var target: SR2NavigationTarget? target = graph.findNavigationNode(SR2LocatorPercent.start(Href("/epub/text/p0.xhtml")!!)) assertEquals("Chapter 0", target!!.node.navigationPoint.title) assertEquals(null, target.extraFragment) diff --git a/org.librarysimplified.r2.tests/src/test/java/org/librarysimplified/r2/tests/TestDirectories.kt b/org.librarysimplified.r2.tests/src/test/java/org/librarysimplified/r2/tests/TestDirectories.kt index 350106e..44e0c88 100644 --- a/org.librarysimplified.r2.tests/src/test/java/org/librarysimplified/r2/tests/TestDirectories.kt +++ b/org.librarysimplified.r2.tests/src/test/java/org/librarysimplified/r2/tests/TestDirectories.kt @@ -26,7 +26,7 @@ object TestDirectories { @Throws(IOException::class) fun temporaryBaseDirectory(): File { - val tmpBase = File(System.getProperty("java.io.tmpdir")) + val tmpBase = File(System.getProperty("java.io.tmpdir")!!) val path1 = File(tmpBase, "org.nypl.simplified") path1.mkdirs() return path1 diff --git a/org.librarysimplified.r2.vanilla/src/main/java/org/librarysimplified/r2/vanilla/internal/SR2Controller.kt b/org.librarysimplified.r2.vanilla/src/main/java/org/librarysimplified/r2/vanilla/internal/SR2Controller.kt index 9ad2c03..a500d9c 100644 --- a/org.librarysimplified.r2.vanilla/src/main/java/org/librarysimplified/r2/vanilla/internal/SR2Controller.kt +++ b/org.librarysimplified.r2.vanilla/src/main/java/org/librarysimplified/r2/vanilla/internal/SR2Controller.kt @@ -16,17 +16,14 @@ import net.jcip.annotations.GuardedBy import org.joda.time.DateTime import org.librarysimplified.r2.api.SR2BookMetadata import org.librarysimplified.r2.api.SR2Bookmark +import org.librarysimplified.r2.api.SR2Bookmark.Type.EXPLICIT import org.librarysimplified.r2.api.SR2Bookmark.Type.LAST_READ import org.librarysimplified.r2.api.SR2Command import org.librarysimplified.r2.api.SR2ControllerConfiguration import org.librarysimplified.r2.api.SR2ControllerType import org.librarysimplified.r2.api.SR2Event -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkCreate import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkCreated import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkDeleted -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkFailedToBeDeleted -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkTryToDelete -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarksLoaded import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandEventCompleted.SR2CommandExecutionFailed import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandEventCompleted.SR2CommandExecutionSucceeded import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandExecutionRunningLong @@ -80,6 +77,15 @@ internal class SR2Controller private constructor( private val configuration: SR2ControllerConfiguration, private val publication: Publication, private val assetRetriever: AssetRetriever, + + /* + * All navigation changes are implemented by first having the code atomically set the + * _navigation intent_, and then running a command that satisfies this intent by either + * opening a new chapter in the webview, scrolling the webview, or both. + */ + + @Volatile private var currentNavigationIntent: SR2Locator, + private val navigationGraph: SR2NavigationGraph, ) : SR2ControllerType { companion object { @@ -149,10 +155,35 @@ internal class SR2Controller private constructor( } this.logger.debug("Publication title: {}", publication.metadata.title) + + /* + * If the list of bookmarks used to open the controller contains a "last read" position, + * set this as the initial position for the controller. + */ + + val navigationGraph = + SR2NavigationGraphs.create(publication) + + val lastRead = + configuration.initialBookmarks.find { bookmark -> bookmark.type == LAST_READ } + + val navigationIntent = + if (lastRead != null) { + this.logger.debug("LastRead: Attempting to start from {}", lastRead) + lastRead.locator + } else { + SR2LocatorPercent( + chapterHref = navigationGraph.start().node.navigationPoint.locator.chapterHref, + chapterProgress = 0.0, + ) + } + return SR2Controller( configuration = configuration, publication = publication, assetRetriever = assetRetriever, + navigationGraph = navigationGraph, + currentNavigationIntent = navigationIntent, ) } } @@ -178,6 +209,21 @@ internal class SR2Controller private constructor( private val closed = AtomicBoolean(false) + /* + * Should the navigation intent be updated on the next chapter progress update? + * + * The web view will provide us with an endless stream of chapter progress updates, and not all + * of them should be used to update the [currentNavigationIntent], because often the published + * updates will be intermediate events while the web view loads and scrolls to locations. + * Typically we only want to be told about a reading position update after we've explicitly turned + * to the next (or previous) page. For all of the other explicit movements such as opening + * chapters, we don't trust the WebView to safely update the navigation intent, and we already + * know where we should be anyway! + */ + + private val updateNavigationIntentOnNextChapterProgressUpdate = + AtomicBoolean(false) + @OptIn(ExperimentalReadiumApi::class) private var searchIterator: SearchIterator? = null @@ -203,21 +249,11 @@ internal class SR2Controller private constructor( bookId = this.configuration.bookId, ) - private val navigationGraph: SR2NavigationGraph = - SR2NavigationGraphs.create(this.publication) - - @Volatile - private var currentTarget: SR2NavigationTarget = - this.navigationGraph.start() - - @Volatile - private var currentTargetProgress: Double = 0.0 - @Volatile private var currentBookProgress: Double? = 0.0 @Volatile - private var bookmarks = listOf() + private var bookmarks = this.configuration.initialBookmarks.toList() @Volatile private var uiVisible: Boolean = true @@ -228,6 +264,7 @@ internal class SR2Controller private constructor( this.subscriptions.add( this.eventSubject.subscribe { event -> this.logger.trace("event: {}", event) }, ) + this.subscriptions.add( this.eventSubject.ofType(SR2ReadingPositionChanged::class.java) .distinctUntilChanged() @@ -239,62 +276,24 @@ internal class SR2Controller private constructor( runBlocking { this@SR2Controller.publication.positionsByReadingOrder() } } - private fun serverLocationOfTarget( - target: SR2NavigationTarget, - ): Url { - return target.node.navigationPoint.locator.chapterHref.resolve(Url(PREFIX_PUBLICATION)) - } - - private fun setCurrentNode(target: SR2NavigationTarget) { - this.logger.debug("currentNode: {}", target.node.javaClass) - this.currentTarget = target - this.currentTargetProgress = when (val locator = target.node.navigationPoint.locator) { - is SR2LocatorPercent -> locator.chapterProgress - is SR2LocatorChapterEnd -> 1.0 - } - } - private fun updateBookmarkLastRead( position: SR2ReadingPositionChanged, ) { - /* - * This is pure paranoia; we only update the last-read location if the new position - * doesn't appear to point to the very start of the book. This is to defend against - * any future bugs that might cause a "reading position change" event to be published - * before the user's _real_ last-read position has been restored using a command or - * bookmark. If this happened, we'd accidentally overwrite the user's reading position with - * a pointer to the start of the book, so this check prevents that. - */ - - val currentNode = this.currentTarget.node - if (currentNode !is SR2NavigationNode.SR2NavigationReadingOrderNode || - currentNode.index != 0 || position.chapterProgress > 0.000_001 - ) { - this.queueExecutor.execute { - val newBookmark = SR2Bookmark( - date = DateTime.now(), - type = LAST_READ, - title = position.chapterTitle.orEmpty(), - locator = position.locator, - bookProgress = this.currentBookProgress, - uri = null, - ) + this.queueExecutor.execute { + val newBookmark = SR2Bookmark( + date = DateTime.now(), + type = LAST_READ, + title = position.chapterTitle.orEmpty(), + locator = position.locator, + bookProgress = this.currentBookProgress, + uri = null, + ) - this.publishEvent( - SR2BookmarkCreate( - newBookmark, - onBookmarkCreationCompleted = { createdBookmark -> - if (createdBookmark != null) { - val newBookmarks = this.bookmarks.toMutableList() - newBookmarks.removeAll { bookmark -> bookmark.type == LAST_READ } - newBookmarks.add(createdBookmark) - this.bookmarks = newBookmarks.distinct().toList() - this.publishEvent(SR2BookmarkCreated(createdBookmark)) - } - }, - ), - ) - } + val newBookmarks = this.bookmarks.toMutableList() + newBookmarks.removeAll { bookmark -> bookmark.type == LAST_READ } + newBookmarks.add(newBookmark) + this.bookmarks = newBookmarks.distinct().toList() + this.publishEvent(SR2BookmarkCreated(newBookmark)) } } @@ -330,9 +329,6 @@ internal class SR2Controller private constructor( is SR2Command.OpenChapterPrevious -> this.executeCommandOpenChapterPrevious(command) - is SR2Command.BookmarksLoad -> - this.executeCommandBookmarksLoad(apiCommand) - SR2Command.Refresh -> this.executeCommandRefresh(command) @@ -362,6 +358,43 @@ internal class SR2Controller private constructor( } } + private fun executeCommandBookmarkDelete( + apiCommand: SR2Command.BookmarkDelete, + ): CompletableFuture<*> { + val target = apiCommand.bookmark + return when (target.type) { + EXPLICIT -> { + this.bookmarks = this.bookmarks.filter { existing -> existing != apiCommand.bookmark } + this.publishEvent(SR2BookmarkDeleted(apiCommand.bookmark)) + CompletableFuture.completedFuture(Unit) + } + + LAST_READ -> { + CompletableFuture.completedFuture(Unit) + } + } + } + + private fun executeCommandBookmarkCreate(): CompletableFuture<*> { + val node = + this.navigationGraph.findNavigationNode(this.currentNavigationIntent) + ?: return CompletableFuture.completedFuture(Unit) + + val newBookmark = + SR2Bookmark( + date = DateTime.now(), + type = SR2Bookmark.Type.EXPLICIT, + title = node.node.title, + locator = this.positionNow(), + bookProgress = this.currentBookProgress, + uri = null, + ) + + this.bookmarks = this.bookmarks.plus(newBookmark).distinct().toList() + this.publishEvent(SR2BookmarkCreated(newBookmark)) + return CompletableFuture.completedFuture(Unit) + } + /** * Execute the [SR2Command.ThemeSet] command. */ @@ -394,81 +427,6 @@ internal class SR2Controller private constructor( return allFutures } - /** - * Execute the [SR2Command.BookmarkDelete] command. - */ - - private fun executeCommandBookmarkDelete( - apiCommand: SR2Command.BookmarkDelete, - ): CompletableFuture<*> { - this.bookmarks = this.bookmarks.map { bookmark -> - bookmark.copy( - isBeingDeleted = bookmark == apiCommand.bookmark, - ) - }.distinct().toList() - this.publishEvent( - SR2BookmarkTryToDelete( - bookmark = apiCommand.bookmark, - onDeleteOperationFinished = { wasDeleted -> - if (wasDeleted) { - val newBookmarks = this.bookmarks.toMutableList() - newBookmarks.remove(apiCommand.bookmark) - this.bookmarks = newBookmarks.distinct().toList() - this.publishEvent(SR2BookmarkDeleted(apiCommand.bookmark)) - } else { - this.bookmarks = this.bookmarks.map { bookmark -> - bookmark.copy( - isBeingDeleted = if (bookmark == apiCommand.bookmark) { - false - } else { - bookmark.isBeingDeleted - }, - ) - }.distinct().toList() - this.publishEvent(SR2BookmarkFailedToBeDeleted) - } - }, - ), - ) - - return CompletableFuture.completedFuture(Unit) - } - - /** - * Execute the [SR2Command.BookmarkCreate] command. - */ - - private fun executeCommandBookmarkCreate(): CompletableFuture<*> { - val bookmark = - SR2Bookmark( - date = DateTime.now(), - type = SR2Bookmark.Type.EXPLICIT, - title = this.currentTarget.node.title, - locator = SR2LocatorPercent( - this.currentTarget.node.navigationPoint.locator.chapterHref, - this.currentTargetProgress, - ), - bookProgress = this.currentBookProgress, - uri = null, - ) - - this.publishEvent( - SR2BookmarkCreate( - bookmark = bookmark, - onBookmarkCreationCompleted = { createdBookmark -> - if (createdBookmark != null) { - val newBookmarks = this.bookmarks.toMutableList() - newBookmarks.add(createdBookmark) - this.bookmarks = newBookmarks.distinct().toList() - this.publishEvent(SR2BookmarkCreated(createdBookmark)) - } - }, - ), - ) - - return CompletableFuture.completedFuture(Unit) - } - /** * Execute the [SR2Command.Refresh] command. */ @@ -476,27 +434,7 @@ internal class SR2Controller private constructor( private fun executeCommandRefresh( command: SR2CommandSubmission, ): CompletableFuture<*> { - return this.openNodeForLocator( - command, - SR2LocatorPercent( - chapterHref = this.currentTarget.node.navigationPoint.locator.chapterHref, - chapterProgress = this.currentTargetProgress, - ), - ) - } - - /** - * Execute the [SR2Command.BookmarksLoad] command. - */ - - private fun executeCommandBookmarksLoad( - apiCommand: SR2Command.BookmarksLoad, - ): CompletableFuture<*> { - val newBookmarks = this.bookmarks.toMutableList() - newBookmarks.addAll(apiCommand.bookmarks) - this.bookmarks = newBookmarks.distinct().toList() - this.publishEvent(SR2BookmarksLoaded) - return CompletableFuture.completedFuture(Unit) + return this.moveToSatisfyNavigationIntent(command) } /** @@ -506,14 +444,16 @@ internal class SR2Controller private constructor( private fun executeCommandOpenChapterPrevious( command: SR2CommandSubmission, ): CompletableFuture<*> { - val previousNode = - this.navigationGraph.findPreviousNode(this.currentTarget.node) + val currentNode = + this.navigationGraph.findNavigationNode(this.currentNavigationIntent) ?: return CompletableFuture.completedFuture(Unit) - return this.openNodeForLocator( - command, - SR2LocatorChapterEnd(chapterHref = previousNode.navigationPoint.locator.chapterHref), - ) + val previousNode = + this.navigationGraph.findPreviousNode(currentNode.node) + ?: return CompletableFuture.completedFuture(Unit) + val locator = SR2LocatorChapterEnd(previousNode.navigationPoint.locator.chapterHref) + this.setCurrentNavigationIntent(locator) + return this.moveToSatisfyNavigationIntent(command) } /** @@ -523,17 +463,16 @@ internal class SR2Controller private constructor( private fun executeCommandOpenChapterNext( command: SR2CommandSubmission, ): CompletableFuture<*> { + val currentNode = + this.navigationGraph.findNavigationNode(this.currentNavigationIntent) + ?: return CompletableFuture.completedFuture(Unit) + val nextNode = - this.navigationGraph.findNextNode(this.currentTarget.node) + this.navigationGraph.findNextNode(currentNode.node) ?: return CompletableFuture.completedFuture(Unit) - return this.openNodeForLocator( - command, - SR2LocatorPercent( - chapterHref = nextNode.navigationPoint.locator.chapterHref, - chapterProgress = 0.0, - ), - ) + this.setCurrentNavigationIntent(nextNode.navigationPoint.locator) + return this.moveToSatisfyNavigationIntent(command) } /** @@ -541,6 +480,7 @@ internal class SR2Controller private constructor( */ private fun executeCommandOpenPagePrevious(): CompletableFuture<*> { + this.updateNavigationIntentOnNextChapterProgressUpdate.set(true) return this.waitForWebViewAvailability().executeJS(SR2JavascriptAPIType::openPagePrevious) } @@ -594,6 +534,7 @@ internal class SR2Controller private constructor( */ private fun executeCommandOpenPageNext(): CompletableFuture<*> { + this.updateNavigationIntentOnNextChapterProgressUpdate.set(true) return this.waitForWebViewAvailability().executeJS(SR2JavascriptAPIType::openPageNext) } @@ -605,7 +546,11 @@ internal class SR2Controller private constructor( command: SR2CommandSubmission, apiCommand: SR2Command.OpenChapter, ): CompletableFuture<*> { - return this.openNodeForLocator(command, apiCommand.locator) + this.navigationGraph.findNavigationNode(apiCommand.locator) + ?: return CompletableFuture.completedFuture(Unit) + + this.setCurrentNavigationIntent(apiCommand.locator) + return this.moveToSatisfyNavigationIntent(command) } /** @@ -669,7 +614,6 @@ internal class SR2Controller private constructor( this@SR2Controller.searchIterator?.close() this@SR2Controller.searchIterator = null } - return CompletableFuture.completedFuture(Unit) } @@ -677,28 +621,21 @@ internal class SR2Controller private constructor( * Load the node for the given locator, and set the reading position appropriately. */ - private fun openNodeForLocator( + private fun moveToSatisfyNavigationIntent( command: SR2CommandSubmission, - locator: SR2Locator, ): CompletableFuture<*> { - val previousNode = this.currentTarget - return try { this.publishCommandRunningLong(command) - val target = - this.navigationGraph.findNavigationNode(locator) - ?: throw IllegalStateException("Unable to locate a chapter for locator $locator") - - val targetLocation = this.serverLocationOfTarget(target) - this.logger.debug("openNodeForLocator: {}", targetLocation) - this.setCurrentNode(target) - val connection = this.waitForWebViewAvailability() + val chapterURL = + this.currentNavigationIntent.chapterHref + val resolvedURL = + chapterURL.resolve(Url(PREFIX_PUBLICATION)) val future = - connection.openURL(targetLocation.toString()) + connection.openURL(resolvedURL.toString()) .thenCompose { this.executeThemeSet(connection, this.themeMostRecent) } @@ -706,14 +643,14 @@ internal class SR2Controller private constructor( connection.executeJS { js -> js.setScrollMode(this.configuration.scrollingMode) } } .thenCompose { - this.executeLocatorSet(connection, locator) + this.executeLocatorSet(connection, this.currentNavigationIntent) } /* * If there's a fragment, attempt to scroll to it. */ - when (val fragment = locator.chapterHref.toString().substringAfter('#', "")) { + when (val fragment = chapterURL.toString().substringAfter('#', "")) { "" -> future else -> @@ -722,12 +659,12 @@ internal class SR2Controller private constructor( } } } catch (e: Exception) { - this.logger.error("Unable to open chapter ${locator.chapterHref}: ", e) - this.setCurrentNode(previousNode) + this.logger.error("Unable to open chapter ${this.currentNavigationIntent.chapterHref}: ", e) this.publishEvent( SR2Event.SR2Error.SR2ChapterNonexistent( - chapterHref = locator.chapterHref.toString(), - message = e.message ?: "Unable to open chapter ${locator.chapterHref}", + chapterHref = this.currentNavigationIntent.chapterHref.toString(), + message = e.message + ?: "Unable to open chapter ${this.currentNavigationIntent.chapterHref}", ), ) val future = CompletableFuture() @@ -754,22 +691,28 @@ internal class SR2Controller private constructor( "Progress must be in [0, 1]; was $chapterProgress" } - val currentNode = this.currentTarget.node - if (currentNode !is SR2NavigationNode.SR2NavigationReadingOrderNode) { + val currentNode = + this.navigationGraph.findNavigationNode(this.currentNavigationIntent) + ?: return null + + if (currentNode.node !is SR2NavigationNode.SR2NavigationReadingOrderNode) { return null } - val currentIndex = currentNode.index + val currentIndex = currentNode.node.index val chapterCount = this.publication.readingOrder.size val result = ((currentIndex + 1 * chapterProgress) / chapterCount) - this.logger.debug("$result = ($currentIndex + 1 * $chapterProgress) / $chapterCount") + this.logger.debug("BookProgress: $result = ($currentIndex + 1 * $chapterProgress) / $chapterCount") return result } private suspend fun getCurrentPage(chapterProgress: Double): Pair { - val currentNode = this.currentTarget.node - if (currentNode !is SR2NavigationNode.SR2NavigationReadingOrderNode) { + val currentNode = + this.navigationGraph.findNavigationNode(this.currentNavigationIntent) + ?: return null to null + + if (currentNode.node !is SR2NavigationNode.SR2NavigationReadingOrderNode) { return null to null } @@ -779,7 +722,7 @@ internal class SR2Controller private constructor( else -> this.configuration.pageNumberingMode } - val indexInReadingOrder = currentNode.index + val indexInReadingOrder = currentNode.node.index val positionsByChapter = this.publication.positionsByReadingOrder() val currentChapterPositions = positionsByChapter[indexInReadingOrder] @@ -820,32 +763,58 @@ internal class SR2Controller private constructor( currentPage: Int, pageCount: Int, ) { - this@SR2Controller.coroutineScope.launch { - this@SR2Controller.currentBookProgress = - this@SR2Controller.getBookProgress(chapterProgress) - this@SR2Controller.currentTargetProgress = - chapterProgress - - val currentTarget = - this@SR2Controller.currentTarget - val targetHref = - currentTarget.node.navigationPoint.locator.chapterHref - val targetTitle = - currentTarget.node.title - val (currentPage, pageCount) = - this@SR2Controller.getCurrentPage(chapterProgress) + /* + * If the controller is indicating that the user explicitly performed some kind of + * navigation action, then this reading position update should be used to update the + * navigation intent. Typically, this _only_ applies for page turns. + */ - this@SR2Controller.publishEvent( - SR2ReadingPositionChanged( - chapterHref = targetHref, - chapterTitle = targetTitle, - chapterProgress = chapterProgress, - currentPage = currentPage, - pageCount = pageCount, - bookProgress = this@SR2Controller.currentBookProgress, - ), + val controller = this@SR2Controller + if (controller.updateNavigationIntentOnNextChapterProgressUpdate.compareAndSet(true, false)) { + controller.setCurrentNavigationIntent( + when (val i = controller.currentNavigationIntent) { + is SR2LocatorChapterEnd -> { + i + } + is SR2LocatorPercent -> { + SR2LocatorPercent( + i.chapterHref, + chapterProgress, + ) + } + }, ) } + + controller.coroutineScope.launch { + controller.currentBookProgress = + controller.getBookProgress(chapterProgress) + + val currentTarget = + controller.navigationGraph.findNavigationNode(controller.currentNavigationIntent) + + if (currentTarget != null) { + val targetHref = + currentTarget.node.navigationPoint.locator.chapterHref + val targetTitle = + currentTarget.node.title + val (resultCurrentPage, resultPageCount) = + controller.getCurrentPage(chapterProgress) + + controller.publishEvent( + SR2ReadingPositionChanged( + chapterHref = targetHref, + chapterTitle = targetTitle, + chapterProgress = chapterProgress, + currentPage = resultCurrentPage, + pageCount = resultPageCount, + bookProgress = controller.currentBookProgress, + ), + ) + } else { + this@JavascriptAPIReceiver.logger.warn("onReadingPositionChanged: currentTarget -> null") + } + } } @android.webkit.JavascriptInterface @@ -927,6 +896,11 @@ internal class SR2Controller private constructor( } } + private fun setCurrentNavigationIntent(locator: SR2Locator) { + this.logger.debug("Navigation intent is now {}", locator) + this.currentNavigationIntent = locator + } + private fun submitCommandActual( command: SR2CommandSubmission, ) { @@ -944,7 +918,7 @@ internal class SR2Controller private constructor( throw e.cause!! } } catch (e: SR2WebViewDisconnectedException) { - this.logger.debug("Webview disconnected: could not execute {}", command) + this.logger.warn("Webview disconnected: could not execute {}", command) this.publishEvent(SR2Event.SR2Error.SR2WebViewInaccessible("No web view is connected")) this.publishCommandFailed(command, e) } catch (e: Exception) { @@ -999,10 +973,7 @@ internal class SR2Controller private constructor( this.bookmarks override fun positionNow(): SR2Locator { - return SR2LocatorPercent( - this.currentTarget.node.navigationPoint.locator.chapterHref, - this.currentTargetProgress, - ) + return this.currentNavigationIntent } override fun themeNow(): SR2Theme { @@ -1112,7 +1083,7 @@ internal class SR2Controller private constructor( return resourceURL.openStream().use { stream -> WebResourceResponse( - guessMimeType(path), + this.guessMimeType(path), null, 200, "OK", diff --git a/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2ReaderFragment.kt b/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2ReaderFragment.kt index b01b274..48ac5a6 100644 --- a/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2ReaderFragment.kt +++ b/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2ReaderFragment.kt @@ -11,7 +11,6 @@ import android.view.ViewGroup import android.webkit.WebView import android.widget.ProgressBar import android.widget.TextView -import android.widget.Toast import androidx.appcompat.widget.Toolbar import androidx.core.view.MenuItemCompat import androidx.core.view.forEach @@ -22,12 +21,8 @@ import org.librarysimplified.r2.api.SR2Bookmark.Type.EXPLICIT import org.librarysimplified.r2.api.SR2Command import org.librarysimplified.r2.api.SR2ControllerType import org.librarysimplified.r2.api.SR2Event -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkCreate import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkCreated import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkDeleted -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkFailedToBeDeleted -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkTryToDelete -import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarksLoaded import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandEventCompleted.SR2CommandExecutionFailed import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandEventCompleted.SR2CommandExecutionSucceeded import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandExecutionRunningLong @@ -309,25 +304,8 @@ class SR2ReaderFragment : SR2Fragment() { this.onReadingPositionChanged(event) } - SR2BookmarksLoaded, - is SR2BookmarkDeleted, - is SR2BookmarkTryToDelete, - is SR2BookmarkCreated, - -> { - this.onBookmarksChanged() - } - - is SR2BookmarkCreate -> { - // Nothing - } - - SR2BookmarkFailedToBeDeleted -> { - this.onBookmarksChanged() - Toast.makeText( - this.requireContext(), - R.string.tocBookmarkDeleteErrorMessage, - Toast.LENGTH_SHORT, - ).show() + is SR2BookmarkCreated -> { + this.reconfigureBookmarkMenuItem(event.bookmark.locator) } is SR2ThemeChanged -> { @@ -364,6 +342,10 @@ class SR2ReaderFragment : SR2Fragment() { val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(event.link)) this.startActivity(browserIntent) } + + is SR2BookmarkDeleted -> { + this.reconfigureBookmarkMenuItem(event.bookmark.locator) + } } } diff --git a/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2ReaderModel.kt b/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2ReaderModel.kt index 0716fe2..4b5090c 100644 --- a/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2ReaderModel.kt +++ b/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2ReaderModel.kt @@ -8,10 +8,12 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.PublishSubject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import org.librarysimplified.r2.api.SR2Bookmark import org.librarysimplified.r2.api.SR2ControllerConfiguration import org.librarysimplified.r2.api.SR2ControllerProviderType import org.librarysimplified.r2.api.SR2ControllerType @@ -59,7 +61,7 @@ object SR2ReaderModel { .toSerialized() private val viewEventSource = - PublishSubject.create() + BehaviorSubject.create() .toSerialized() private val controllerEventSource = @@ -122,6 +124,7 @@ object SR2ReaderModel { bookId: String, theme: SR2Theme, controllers: SR2ControllerProviderType, + bookmarks: List, ): CompletableFuture { val future = CompletableFuture() SR2Executors.ioExecutor.execute { @@ -138,6 +141,7 @@ object SR2ReaderModel { uiExecutor = SR2UIThread::runOnUIThread, scrollingMode = this.scrollMode, pageNumberingMode = this.perChapterNumbering, + initialBookmarks = bookmarks, ), ), ) diff --git a/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2SearchFragment.kt b/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2SearchFragment.kt index 9d76223..6c6d0b3 100644 --- a/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2SearchFragment.kt +++ b/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/SR2SearchFragment.kt @@ -21,6 +21,18 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.librarysimplified.r2.api.SR2Command import org.librarysimplified.r2.api.SR2Event +import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkCreated +import org.librarysimplified.r2.api.SR2Event.SR2BookmarkEvent.SR2BookmarkDeleted +import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandEventCompleted.SR2CommandExecutionFailed +import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandEventCompleted.SR2CommandExecutionSucceeded +import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandExecutionRunningLong +import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandExecutionStarted +import org.librarysimplified.r2.api.SR2Event.SR2CommandEvent.SR2CommandSearchResults +import org.librarysimplified.r2.api.SR2Event.SR2Error +import org.librarysimplified.r2.api.SR2Event.SR2ExternalLinkSelected +import org.librarysimplified.r2.api.SR2Event.SR2OnCenterTapped +import org.librarysimplified.r2.api.SR2Event.SR2ReadingPositionChanged +import org.librarysimplified.r2.api.SR2Event.SR2ThemeChanged import org.librarysimplified.r2.api.SR2Locator import org.librarysimplified.r2.ui_thread.SR2UIThread import org.librarysimplified.r2.views.SR2ReaderViewCommand.SR2ReaderViewNavigationSearchClose @@ -204,27 +216,23 @@ class SR2SearchFragment : SR2Fragment() { SR2UIThread.checkIsUIThread() when (event) { - is SR2Event.SR2ReadingPositionChanged, - SR2Event.SR2BookmarkEvent.SR2BookmarksLoaded, - is SR2Event.SR2BookmarkEvent.SR2BookmarkDeleted, - is SR2Event.SR2BookmarkEvent.SR2BookmarkTryToDelete, - is SR2Event.SR2BookmarkEvent.SR2BookmarkCreated, - SR2Event.SR2BookmarkEvent.SR2BookmarkFailedToBeDeleted, - is SR2Event.SR2ThemeChanged, - is SR2Event.SR2Error.SR2ChapterNonexistent, - is SR2Event.SR2Error.SR2WebViewInaccessible, - is SR2Event.SR2OnCenterTapped, - is SR2Event.SR2BookmarkEvent.SR2BookmarkCreate, - is SR2Event.SR2CommandEvent.SR2CommandExecutionRunningLong, - is SR2Event.SR2CommandEvent.SR2CommandExecutionStarted, - is SR2Event.SR2CommandEvent.SR2CommandEventCompleted.SR2CommandExecutionSucceeded, - is SR2Event.SR2CommandEvent.SR2CommandEventCompleted.SR2CommandExecutionFailed, - is SR2Event.SR2ExternalLinkSelected, + is SR2ReadingPositionChanged, + is SR2BookmarkDeleted, + is SR2BookmarkCreated, + is SR2ThemeChanged, + is SR2Error.SR2ChapterNonexistent, + is SR2Error.SR2WebViewInaccessible, + is SR2OnCenterTapped, + is SR2CommandExecutionRunningLong, + is SR2CommandExecutionStarted, + is SR2CommandExecutionSucceeded, + is SR2CommandExecutionFailed, + is SR2ExternalLinkSelected, -> { // Nothing } - is SR2Event.SR2CommandEvent.SR2CommandSearchResults -> { + is SR2CommandSearchResults -> { SR2ReaderModel.consumeSearchResults(event) } } diff --git a/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/internal/SR2DiffUtils.kt b/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/internal/SR2DiffUtils.kt index 35ec24b..ee128c3 100644 --- a/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/internal/SR2DiffUtils.kt +++ b/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/internal/SR2DiffUtils.kt @@ -18,7 +18,7 @@ internal object SR2DiffUtils { oldItem: SR2Bookmark, newItem: SR2Bookmark, ): Boolean = - oldItem == newItem && oldItem.isBeingDeleted == newItem.isBeingDeleted + oldItem == newItem } val tocEntryCallback: DiffUtil.ItemCallback = diff --git a/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/internal/SR2TOCBookmarkViewHolder.kt b/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/internal/SR2TOCBookmarkViewHolder.kt index c49ed48..7e5e39f 100644 --- a/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/internal/SR2TOCBookmarkViewHolder.kt +++ b/org.librarysimplified.r2.views/src/main/java/org/librarysimplified/r2/views/internal/SR2TOCBookmarkViewHolder.kt @@ -3,9 +3,7 @@ package org.librarysimplified.r2.views.internal import android.content.res.Resources import android.view.View import android.widget.ImageView -import android.widget.ProgressBar import android.widget.TextView -import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.joda.time.format.DateTimeFormatterBuilder @@ -25,8 +23,6 @@ internal class SR2TOCBookmarkViewHolder( this.rootView.findViewById(R.id.bookmarkDelete) private val bookmarkDate: TextView = this.rootView.findViewById(R.id.bookmarkDate) - private val bookmarkDeleteLoading: ProgressBar = - this.rootView.findViewById(R.id.bookmarkDeleteLoading) private val bookmarkProgressText: TextView = this.rootView.findViewById(R.id.bookmarkProgressText) private val bookmarkTitleText: TextView = @@ -69,9 +65,6 @@ internal class SR2TOCBookmarkViewHolder( } } - bookmarkDelete.isVisible = !bookmark.isBeingDeleted - bookmarkDeleteLoading.isVisible = bookmark.isBeingDeleted - this.rootView.setOnClickListener { this.rootView.setOnClickListener(null) onBookmarkSelected.invoke(bookmark)