From 83507bce6ae474c0314b9edf3e7a11043861e72f Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Wed, 3 Apr 2024 16:00:37 +0000 Subject: [PATCH] Fix some race conditions This changes the API to require clients to pass in an initial list of bookmarks. This should ensure that it's not possible for the reader to switch to a page _before_ having been informed that there's already a bookmark for a later page. This also removes some deprecated events, and replaces the broken bookmark creation with a much simpler system. Additionally, view events are now published from a BehaviorSubject. This allows for activities to avoid mild race conditions when resuming. Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1120 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1119 --- gradle.properties | 2 +- .../librarysimplified/r2/api/SR2Bookmark.kt | 6 - .../librarysimplified/r2/api/SR2Command.kt | 10 - .../r2/api/SR2ControllerConfiguration.kt | 6 + .../r2/api/SR2ControllerType.kt | 2 +- .../org/librarysimplified/r2/api/SR2Event.kt | 44 +- .../librarysimplified/r2/demo/DemoActivity.kt | 27 +- .../r2/tests/SR2NavigationGraphsTest.kt | 4 +- .../r2/tests/TestDirectories.kt | 2 +- .../r2/vanilla/internal/SR2Controller.kt | 445 ++++++++---------- .../r2/views/SR2ReaderFragment.kt | 30 +- .../r2/views/SR2ReaderModel.kt | 6 +- .../r2/views/SR2SearchFragment.kt | 42 +- .../r2/views/internal/SR2DiffUtils.kt | 2 +- .../internal/SR2TOCBookmarkViewHolder.kt | 7 - 15 files changed, 262 insertions(+), 373 deletions(-) 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)