From 5f0be5ae4317eeba963edd10f811b1649041e26f Mon Sep 17 00:00:00 2001 From: FooIbar <118464521+FooIbar@users.noreply.github.com> Date: Tue, 6 Feb 2024 21:05:33 +0800 Subject: [PATCH] Move reading progress to database In this way cbz archives can be read without SpiderInfo. --- .../com.hippo.ehviewer.dao.EhDatabase/17.json | 471 ++++++++++++++++++ .../main/kotlin/com/hippo/ehviewer/EhDB.kt | 10 + .../kotlin/com/hippo/ehviewer/dao/Database.kt | 12 +- .../com/hippo/ehviewer/dao/ProgressDao.kt | 29 ++ .../com/hippo/ehviewer/dao/ProgressInfo.kt | 22 + .../ehviewer/download/DownloadManager.kt | 27 +- .../ehviewer/gallery/ArchivePageLoader.kt | 16 +- .../hippo/ehviewer/gallery/EhPageLoader.kt | 23 +- .../com/hippo/ehviewer/gallery/PageLoader2.kt | 41 +- .../hippo/ehviewer/legacy/SpiderInfoCompat.kt | 2 +- .../com/hippo/ehviewer/spider/SpiderInfo.kt | 18 +- .../com/hippo/ehviewer/spider/SpiderQueen.kt | 19 +- .../ehviewer/ui/screen/GalleryDetailScreen.kt | 12 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 121 +++-- 14 files changed, 672 insertions(+), 151 deletions(-) create mode 100644 app/schemas/com.hippo.ehviewer.dao.EhDatabase/17.json create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/dao/ProgressDao.kt create mode 100644 app/src/main/kotlin/com/hippo/ehviewer/dao/ProgressInfo.kt diff --git a/app/schemas/com.hippo.ehviewer.dao.EhDatabase/17.json b/app/schemas/com.hippo.ehviewer.dao.EhDatabase/17.json new file mode 100644 index 000000000..a43873312 --- /dev/null +++ b/app/schemas/com.hippo.ehviewer.dao.EhDatabase/17.json @@ -0,0 +1,471 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "d6375317a9298b7feae5925a23272bb9", + "entities": [ + { + "tableName": "GALLERIES", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TOKEN` TEXT, `TITLE` TEXT, `TITLE_JPN` TEXT, `THUMB` TEXT, `CATEGORY` INTEGER NOT NULL, `POSTED` TEXT, `UPLOADER` TEXT, `RATING` REAL NOT NULL, `SIMPLE_LANGUAGE` TEXT, `FAVORITE_SLOT` INTEGER NOT NULL, PRIMARY KEY(`GID`))", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "token", + "columnName": "TOKEN", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "title", + "columnName": "TITLE", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "titleJpn", + "columnName": "TITLE_JPN", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbKey", + "columnName": "THUMB", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "category", + "columnName": "CATEGORY", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "posted", + "columnName": "POSTED", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "uploader", + "columnName": "UPLOADER", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "rating", + "columnName": "RATING", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "simpleLanguage", + "columnName": "SIMPLE_LANGUAGE", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favoriteSlot", + "columnName": "FAVORITE_SLOT", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DOWNLOAD_LABELS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`LABEL` TEXT NOT NULL, `POSITION` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "label", + "columnName": "LABEL", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "POSITION", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [ + { + "name": "index_DOWNLOAD_LABELS_LABEL", + "unique": true, + "columnNames": [ + "LABEL" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_DOWNLOAD_LABELS_LABEL` ON `${TABLE_NAME}` (`LABEL`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "DOWNLOADS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `STATE` INTEGER NOT NULL, `LEGACY` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, `LABEL` TEXT, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`LABEL`) REFERENCES `DOWNLOAD_LABELS`(`LABEL`) ON UPDATE CASCADE ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "STATE", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "legacy", + "columnName": "LEGACY", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "TIME", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "LABEL", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "position", + "columnName": "POSITION", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [ + { + "name": "index_DOWNLOADS_LABEL", + "unique": false, + "columnNames": [ + "LABEL" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_DOWNLOADS_LABEL` ON `${TABLE_NAME}` (`LABEL`)" + } + ], + "foreignKeys": [ + { + "table": "GALLERIES", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "GID" + ], + "referencedColumns": [ + "GID" + ] + }, + { + "table": "DOWNLOAD_LABELS", + "onDelete": "SET NULL", + "onUpdate": "CASCADE", + "columns": [ + "LABEL" + ], + "referencedColumns": [ + "LABEL" + ] + } + ] + }, + { + "tableName": "DOWNLOAD_DIRNAME", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `DIRNAME` TEXT NOT NULL, PRIMARY KEY(`GID`))", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dirname", + "columnName": "DIRNAME", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "FILTER", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`MODE` INTEGER NOT NULL, `TEXT` TEXT NOT NULL, `ENABLE` INTEGER NOT NULL, `_id` INTEGER, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "mode", + "columnName": "MODE", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "TEXT", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enable", + "columnName": "ENABLE", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "HISTORY", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "TIME", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GALLERIES", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "GID" + ], + "referencedColumns": [ + "GID" + ] + } + ] + }, + { + "tableName": "LOCAL_FAVORITES", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `TIME` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "TIME", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GALLERIES", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "GID" + ], + "referencedColumns": [ + "GID" + ] + } + ] + }, + { + "tableName": "PROGRESS", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`GID` INTEGER NOT NULL, `PAGE` INTEGER NOT NULL, PRIMARY KEY(`GID`), FOREIGN KEY(`GID`) REFERENCES `GALLERIES`(`GID`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "gid", + "columnName": "GID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "page", + "columnName": "PAGE", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "GID" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "GALLERIES", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "GID" + ], + "referencedColumns": [ + "GID" + ] + } + ] + }, + { + "tableName": "QUICK_SEARCH", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER, `NAME` TEXT NOT NULL, `MODE` INTEGER NOT NULL, `CATEGORY` INTEGER NOT NULL, `KEYWORD` TEXT, `ADVANCE_SEARCH` INTEGER NOT NULL, `MIN_RATING` INTEGER NOT NULL, `PAGE_FROM` INTEGER NOT NULL, `PAGE_TO` INTEGER NOT NULL, `POSITION` INTEGER NOT NULL, PRIMARY KEY(`_id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "NAME", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "MODE", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "CATEGORY", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keyword", + "columnName": "KEYWORD", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "advanceSearch", + "columnName": "ADVANCE_SEARCH", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "minRating", + "columnName": "MIN_RATING", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageFrom", + "columnName": "PAGE_FROM", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pageTo", + "columnName": "PAGE_TO", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "POSITION", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "_id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd6375317a9298b7feae5925a23272bb9')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt index df231c895..f0849b548 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhDB.kt @@ -27,6 +27,7 @@ import com.hippo.ehviewer.dao.EhDatabase import com.hippo.ehviewer.dao.Filter import com.hippo.ehviewer.dao.HistoryInfo import com.hippo.ehviewer.dao.LocalFavoriteInfo +import com.hippo.ehviewer.dao.ProgressInfo import com.hippo.ehviewer.dao.QuickSearch import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.util.sendTo @@ -45,6 +46,11 @@ object EhDB { runCatching { db.galleryDao().delete(galleryInfo) } } + fun getReadProgressFlow(gid: Long) = db.progressDao().getPageFlow(gid) + suspend fun getReadProgress(gid: Long) = db.progressDao().getPage(gid) + suspend fun putReadProgress(gid: Long, page: Int) = db.progressDao().upsert(ProgressInfo(gid, page)) + suspend fun clearProgressInfo() = db.progressDao().deleteAll() + suspend fun getAllDownloadInfo() = db.downloadsDao().joinList().onEach { if (it.state == DownloadInfo.STATE_WAIT || it.state == DownloadInfo.STATE_DOWNLOAD) { it.state = DownloadInfo.STATE_NONE @@ -251,6 +257,7 @@ object EhDB { context.deleteDatabase(ehExportName) } use { newDb -> db.galleryDao().list().let { newDb.galleryDao().insertOrIgnore(it) } + db.progressDao().list().let { newDb.progressDao().insertOrIgnore(it) } db.downloadLabelDao().list().let { newDb.downloadLabelDao().insert(it) } db.downloadDirnameDao().list().let { newDb.downloadDirnameDao().insertOrIgnore(it) } db.downloadsDao().list().let { newDb.downloadsDao().insert(it) } @@ -276,6 +283,9 @@ object EhDB { runCatching { db.galleryDao().insertOrIgnore(oldDB.galleryDao().list()) } + runCatching { + db.progressDao().insertOrIgnore(oldDB.progressDao().list()) + } runCatching { val downloadLabelList = oldDB.downloadLabelDao().list() DownloadManager.addDownloadLabel(downloadLabelList) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt index 535280f9e..893d1de27 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/Database.kt @@ -108,8 +108,11 @@ class ThumbKeyMigration : AutoMigrationSpec { } @Database( - entities = [BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class, Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, QuickSearch::class], - version = 16, + entities = [ + BaseGalleryInfo::class, DownloadLabel::class, DownloadEntity::class, DownloadDirname::class, + Filter::class, HistoryInfo::class, LocalFavoriteInfo::class, ProgressInfo::class, QuickSearch::class, + ], + version = 17, exportSchema = true, autoMigrations = [ AutoMigration( @@ -167,6 +170,10 @@ class ThumbKeyMigration : AutoMigrationSpec { from = 15, to = 16, ), + AutoMigration( + from = 16, + to = 17, + ), ], ) @TypeConverters(FilterModeConverter::class) @@ -178,6 +185,7 @@ abstract class EhDatabase : RoomDatabase() { abstract fun filterDao(): FilterDao abstract fun historyDao(): HistoryDao abstract fun localFavoritesDao(): LocalFavoritesDao + abstract fun progressDao(): ProgressDao abstract fun quickSearchDao(): QuickSearchDao } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/ProgressDao.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/ProgressDao.kt new file mode 100644 index 000000000..01ee85fd4 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/ProgressDao.kt @@ -0,0 +1,29 @@ +package com.hippo.ehviewer.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Upsert +import kotlinx.coroutines.flow.Flow + +@Dao +interface ProgressDao { + @Query("SELECT IFNULL(PAGE, 0) FROM PROGRESS WHERE GID = :gid") + suspend fun getPage(gid: Long): Int + + @Query("SELECT IFNULL(PAGE, 0) FROM PROGRESS WHERE GID = :gid") + fun getPageFlow(gid: Long): Flow + + @Query("SELECT * FROM PROGRESS") + suspend fun list(): List + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertOrIgnore(progress: List) + + @Upsert + suspend fun upsert(progress: ProgressInfo) + + @Query("DELETE FROM PROGRESS") + suspend fun deleteAll() +} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/dao/ProgressInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/dao/ProgressInfo.kt new file mode 100644 index 000000000..12f3711c2 --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/dao/ProgressInfo.kt @@ -0,0 +1,22 @@ +package com.hippo.ehviewer.dao + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey +import com.hippo.ehviewer.client.data.BaseGalleryInfo + +@Entity( + tableName = "PROGRESS", + foreignKeys = [ + ForeignKey(BaseGalleryInfo::class, ["GID"], ["GID"], onDelete = ForeignKey.CASCADE), + ], +) +class ProgressInfo( + @PrimaryKey + @ColumnInfo(name = "GID") + val gid: Long, + + @ColumnInfo(name = "PAGE") + val page: Int, +) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt index a1c85d07d..a2be73277 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/download/DownloadManager.kt @@ -37,8 +37,6 @@ import com.hippo.ehviewer.image.Image import com.hippo.ehviewer.spider.SpiderQueen import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener import com.hippo.ehviewer.spider.putToDownloadDir -import com.hippo.ehviewer.spider.readCompatFromUniFile -import com.hippo.ehviewer.spider.write import com.hippo.ehviewer.util.AppConfig import com.hippo.ehviewer.util.ConcurrentPool import com.hippo.ehviewer.util.SimpleHandler @@ -49,11 +47,9 @@ import com.hippo.unifile.UniFile import com.hippo.unifile.asUniFile import com.hippo.unifile.asUniFileOrNull import eu.kanade.tachiyomi.util.lang.launchIO -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.transform -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import splitties.preferences.edit @@ -457,26 +453,9 @@ object DownloadManager : OnSpiderListener { } } - suspend fun resetAllReadingProgress() = coroutineScope { - allInfoList.forEach { info -> - launch { - runCatching { - resetReadingProgress(info) - Log.d(TAG, "Current thread: ${Thread.currentThread().name}") - }.onFailure { - Log.e(TAG, "Can't write SpiderInfo", it) - } - } - } - } - - private fun resetReadingProgress(info: DownloadInfo) { - val downloadDir = info.downloadDir ?: return - val file = downloadDir.findFile(SpiderQueen.SPIDER_INFO_FILENAME) ?: return - val spiderInfo = readCompatFromUniFile(file) ?: return - spiderInfo.startPage = 0 - spiderInfo.write(file) - } + suspend fun resetAllReadingProgress() = runCatching { + EhDB.clearProgressInfo() + }.onFailure { it.printStackTrace() } // Update in DB // Update listener diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt index 0a68a3571..5764aa4bf 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/ArchivePageLoader.kt @@ -31,11 +31,8 @@ import com.hippo.ehviewer.jni.releaseByteBuffer import com.hippo.ehviewer.util.FileUtils import com.hippo.unifile.UniFile import java.nio.ByteBuffer -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import kotlinx.coroutines.cancel import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch @@ -47,8 +44,12 @@ import moe.tarsin.coroutines.withLock typealias PasswdInvalidator = (String) -> Boolean typealias PasswdProvider = suspend (PasswdInvalidator) -> String -class ArchivePageLoader(private val file: UniFile, passwdProvider: PasswdProvider? = null) : PageLoader2(), CoroutineScope { - override val coroutineContext = Dispatchers.IO + Job() +class ArchivePageLoader( + private val file: UniFile, + gid: Long = 0, + startPage: Int = 0, + passwdProvider: PasswdProvider? = null, +) : PageLoader2(gid, startPage) { private lateinit var pfd: ParcelFileDescriptor private val hostJob = launch(start = CoroutineStart.LAZY) { Log.d(DEBUG_TAG, "Open archive ${file.uri}") @@ -76,11 +77,10 @@ class ArchivePageLoader(private val file: UniFile, passwdProvider: PasswdProvide } override fun stop() { - cancel() + super.stop() closeArchive() pfd.close() Log.d(DEBUG_TAG, "Close archive ${file.uri} successfully!") - super.stop() } private val mJobMap = hashMapOf() @@ -135,7 +135,7 @@ class ArchivePageLoader(private val file: UniFile, passwdProvider: PasswdProvide override suspend fun awaitReady(): Boolean { hostJob.join() - return size != 0 + return super.awaitReady() && size != 0 } override val isReady: Boolean diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt index 187fa2053..b29e3a2fc 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/EhPageLoader.kt @@ -22,10 +22,10 @@ import com.hippo.ehviewer.spider.SpiderQueen import com.hippo.ehviewer.spider.SpiderQueen.Companion.obtainSpiderQueen import com.hippo.ehviewer.spider.SpiderQueen.Companion.releaseSpiderQueen import com.hippo.ehviewer.spider.SpiderQueen.OnSpiderListener -import com.hippo.ehviewer.util.SimpleHandler import com.hippo.unifile.UniFile -class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpiderListener { +class EhPageLoader(private val mGalleryInfo: GalleryInfo, startPage: Int) : + PageLoader2(mGalleryInfo.gid, startPage), OnSpiderListener { private lateinit var mSpiderQueen: SpiderQueen override fun start() { super.start() @@ -38,13 +38,9 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpi override fun stop() { super.stop() mSpiderQueen.removeOnSpiderListener(this) - // Activity recreate may called, so wait 0.5s - SimpleHandler.postDelayed(ReleaseTask(mSpiderQueen), 500) + releaseSpiderQueen(mSpiderQueen, SpiderQueen.MODE_READ) } - override val startPage - get() = mSpiderQueen.startPage - override val title by lazy { EhUtils.getSuitableTitle(mGalleryInfo) } @@ -57,10 +53,6 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpi return mSpiderQueen.save(index, file) } - override fun putStartPage(page: Int) { - mSpiderQueen.putStartPage(page) - } - override val size: Int get() = mSpiderQueen.size @@ -73,7 +65,7 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpi } override suspend fun awaitReady(): Boolean { - return mSpiderQueen.awaitReady() + return super.awaitReady() && mSpiderQueen.awaitReady() } override val isReady: Boolean @@ -121,11 +113,4 @@ class EhPageLoader(private val mGalleryInfo: GalleryInfo) : PageLoader2(), OnSpi override fun preloadPages(pages: List, pair: Pair) { mSpiderQueen.preloadPages(pages, pair) } - - private class ReleaseTask(private var mSpiderQueen: SpiderQueen?) : Runnable { - override fun run() { - mSpiderQueen?.let { releaseSpiderQueen(it, SpiderQueen.MODE_READ) } - mSpiderQueen = null - } - } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt b/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt index 3db556479..258df5044 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/gallery/PageLoader2.kt @@ -15,14 +15,47 @@ */ package com.hippo.ehviewer.gallery +import androidx.annotation.CallSuper +import com.hippo.ehviewer.EhDB import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.ui.reader.loader.PageLoader +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch -abstract class PageLoader2 : PageLoader() { - open val startPage: Int - get() = 0 +abstract class PageLoader2(private val gid: Long, var startPage: Int) : PageLoader(), CoroutineScope { + override val coroutineContext = Dispatchers.IO + Job() + private val progressJob = launch(start = CoroutineStart.LAZY) { + if (startPage == -1) { + startPage = EhDB.getReadProgress(gid) + } + } - open fun putStartPage(page: Int) {} + override fun start() { + super.start() + progressJob.start() + } + + @CallSuper + override suspend fun awaitReady(): Boolean { + progressJob.join() + return startPage != -1 + } + + override fun stop() { + super.stop() + if (gid != 0L) { + launch { + EhDB.putReadProgress(gid, startPage) + this@PageLoader2.cancel() + } + } else { + cancel() + } + } protected abstract val title: String diff --git a/app/src/main/kotlin/com/hippo/ehviewer/legacy/SpiderInfoCompat.kt b/app/src/main/kotlin/com/hippo/ehviewer/legacy/SpiderInfoCompat.kt index 5d29af6ad..809eae5fb 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/legacy/SpiderInfoCompat.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/legacy/SpiderInfoCompat.kt @@ -26,7 +26,7 @@ fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo { val previewPerPage = readInt() val pages = read().toInt() val pTokenMap = hashMapOf() - val info = SpiderInfo(gid, pages, pTokenMap, 0, token, previewPages, previewPerPage) + val info = SpiderInfo(gid, token, pages, pTokenMap, previewPages, previewPerPage) runCatching { while (true) { val line = read() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt index 46c541fe5..9c919b20b 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderInfo.kt @@ -19,29 +19,31 @@ import splitties.init.appCtx class SpiderInfo( val gid: Long, + val token: String, + val pages: Int, val pTokenMap: MutableMap = hashMapOf(), - var startPage: Int = 0, - - var token: String? = null, - var previewPages: Int = -1, var previewPerPage: Int = -1, ) +private val cbor = Cbor { + ignoreUnknownKeys = true +} + fun SpiderInfo.write(file: UniFile) { file.openOutputStream().use { - it.write(Cbor.encodeToByteArray(this)) + it.write(cbor.encodeToByteArray(this)) } } fun SpiderInfo.saveToCache() { runSuspendCatching { spiderInfoCache.edit(gid.toString()) { - data.toFile().writeBytes(Cbor.encodeToByteArray(this@saveToCache)) + data.toFile().writeBytes(cbor.encodeToByteArray(this@saveToCache)) } }.onFailure { it.printStackTrace() @@ -58,7 +60,7 @@ private val spiderInfoCache by lazy { fun readFromCache(gid: Long): SpiderInfo? { return spiderInfoCache.read(gid.toString()) { runCatching { - Cbor.decodeFromByteArray(data.toFile().readBytes()) + cbor.decodeFromByteArray(data.toFile().readBytes()) }.onFailure { it.printStackTrace() }.getOrNull() @@ -68,7 +70,7 @@ fun readFromCache(gid: Long): SpiderInfo? { fun readCompatFromUniFile(file: UniFile): SpiderInfo? { return runCatching { file.openInputStream().use { - Cbor.decodeFromByteArray(it.readBytes()) + cbor.decodeFromByteArray(it.readBytes()) } }.getOrNull() ?: runCatching { file.openInputStream().use { readLegacySpiderInfo(it) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt index f66c226c3..3377e4a28 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/spider/SpiderQueen.kt @@ -237,12 +237,6 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS return isReady } - suspend fun awaitStartPage(): Int { - prepareJob.join() - if (!isReady) return 0 - return mSpiderInfo.startPage - } - private fun stop() { val queenScope = this launchIO { @@ -317,13 +311,6 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS } } - val startPage: Int - get() = mSpiderInfo.startPage - - fun putStartPage(page: Int) { - mSpiderInfo.startPage = page - } - private fun readSpiderInfoFromLocal(): SpiderInfo? { return mSpiderDen.downloadDir?.run { findFile(SPIDER_INFO_FILENAME)?.let { file -> @@ -360,7 +347,7 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS referer, ).fetchUsingAsText { val pages = parsePages(this) - val spiderInfo = SpiderInfo(galleryInfo.gid, pages, token = galleryInfo.token) + val spiderInfo = SpiderInfo(galleryInfo.gid, galleryInfo.token!!, pages) readPreviews(this, 0, spiderInfo) spiderInfo } @@ -632,12 +619,12 @@ class SpiderQueen private constructor(val galleryInfo: GalleryInfo) : CoroutineS showKeyLock.withLock { localShowKey = showKey if (localShowKey == null || forceHtml) { - var pageUrl = EhUrl.getPageUrl(mSpiderInfo.gid, index, pToken) + var pageUrl = EhUrl.getPageUrl(galleryInfo.gid, index, pToken) // Skipping H@H costs 50 points, only use it as last resort if (skipHathKey != null) { pageUrl += "?nl=$skipHathKey" } - EhEngine.getGalleryPage(pageUrl, mSpiderInfo.gid, mSpiderInfo.token) + EhEngine.getGalleryPage(pageUrl, galleryInfo.gid, galleryInfo.token) .let { result -> check509(result.imageUrl) imageUrl = result.imageUrl diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt index 4be97022b..4845c5817 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ui/screen/GalleryDetailScreen.kt @@ -61,6 +61,7 @@ import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf @@ -120,8 +121,6 @@ import com.hippo.ehviewer.databinding.DialogTorrentListBinding import com.hippo.ehviewer.download.DownloadManager as EhDownloadManager import com.hippo.ehviewer.ktbuilder.imageRequest import com.hippo.ehviewer.spider.SpiderDen -import com.hippo.ehviewer.spider.SpiderQueen -import com.hippo.ehviewer.spider.SpiderQueen.Companion.MODE_READ import com.hippo.ehviewer.ui.GalleryInfoBottomSheet import com.hippo.ehviewer.ui.LocalSnackbarHostState import com.hippo.ehviewer.ui.LockDrawer @@ -150,6 +149,7 @@ import com.hippo.ehviewer.ui.tools.FilledTertiaryIconToggleButton import com.hippo.ehviewer.ui.tools.GalleryDetailRating import com.hippo.ehviewer.ui.tools.GalleryRatingBar import com.hippo.ehviewer.ui.tools.LocalDialogState +import com.hippo.ehviewer.ui.tools.rememberInVM import com.hippo.ehviewer.ui.tools.rememberLambda import com.hippo.ehviewer.util.AppHelper import com.hippo.ehviewer.util.ExceptionUtils @@ -905,13 +905,13 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa val windowSizeClass = calculateWindowSizeClass(activity) val thumbColumns by Settings.thumbColumns.collectAsState() val readText = stringResource(R.string.read) + val startPage by rememberInVM { + EhDB.getReadProgressFlow(galleryInfo.gid) + }.collectAsState(0) var readButtonText by rememberSaveable { mutableStateOf(readText) } LifecycleResumeEffect(Unit) { coroutineScope.launchIO { runSuspendCatching { - val queen = SpiderQueen.obtainSpiderQueen(galleryInfo, MODE_READ) - val startPage = queen.awaitStartPage() - SpiderQueen.releaseSpiderQueen(queen, MODE_READ) readButtonText = if (startPage == 0) { readText } else { @@ -931,7 +931,7 @@ fun GalleryDetailScreen(args: GalleryDetailScreenArgs, navigator: DestinationsNa DownloadInfo.STATE_FAILED -> stringResource(R.string.download_state_failed) else -> error("Invalid DownloadState!!!") } - fun onReadButtonClick() = context.navToReader(galleryInfo.findBaseInfo()) + fun onReadButtonClick() = context.navToReader(galleryInfo.findBaseInfo(), startPage) fun onCategoryChipClick() { val category = galleryInfo.category if (category == EhUtils.NONE || category == EhUtils.PRIVATE || category == EhUtils.UNKNOWN) { diff --git a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index ee2546ce0..20273a7f6 100644 --- a/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/kotlin/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -133,10 +133,9 @@ class GalleryModel : ViewModel() { class ReaderActivity : EhActivity() { lateinit var binding: ReaderActivityBinding private var mAction: String? = null - private var mFilename: String? = null private var mUri: Uri? = null private var mGalleryInfo: BaseGalleryInfo? = null - private var mPage: Int = 0 + private var mPage: Int = -1 private val vm: GalleryModel by viewModels() private val dialogState = DialogState() @@ -145,7 +144,11 @@ class ReaderActivity : EhActivity() { */ var menuVisible by mutableStateOf(false) private var mGalleryProvider by lazyMut { vm::galleryProvider } - private var mCurrentIndex: Int = 0 + private var mCurrentIndex: Int + get() = mGalleryProvider!!.startPage + set(value) { + mGalleryProvider!!.startPage = value + } private val galleryDetailUrl: String? get() { @@ -160,56 +163,56 @@ class ReaderActivity : EhActivity() { return EhUrl.getGalleryDetailUrl(gid, token, 0, false) } - private fun buildProvider(replace: Boolean = false) { - if (mGalleryProvider != null) { - if (replace) mGalleryProvider!!.stop() else return + private suspend fun buildProvider(replace: Boolean = false): PageLoader2? { + mGalleryProvider?.let { + if (replace) it.stop() else return it } - if (ACTION_EH == mAction) { - mGalleryInfo?.let { - mGalleryProvider = DownloadManager.getDownloadInfo(it.gid)?.downloadDir - ?.findFile("${it.gid}.cbz") - ?.let { file -> ArchivePageLoader(file) } - ?: EhPageLoader(it) - } - } else if (Intent.ACTION_VIEW == mAction) { - if (mUri != null) { - try { - grantUriPermission( - BuildConfig.APPLICATION_ID, - mUri, - Intent.FLAG_GRANT_READ_URI_PERMISSION, - ) - } catch (e: Exception) { - Toast.makeText(this, R.string.error_reading_failed, Toast.LENGTH_SHORT).show() + val provider = when (mAction) { + ACTION_EH -> { + mGalleryInfo?.let { + DownloadManager.getDownloadInfo(it.gid)?.downloadDir + ?.findFile("${it.gid}.cbz") + ?.let { file -> ArchivePageLoader(file, it.gid, mPage) } + ?: EhPageLoader(it, mPage) } + } - mGalleryProvider = ArchivePageLoader(mUri!!.asUniFile()) { invalidator -> - runCatching { - dialogState.awaitInputText( - title = getString(R.string.archive_need_passwd), - hint = getString(R.string.archive_passwd), - ) { - if (it.isBlank()) { - getString(R.string.passwd_cannot_be_empty) - } else if (invalidator(it)) { - null - } else { - getString(R.string.passwd_wrong) + Intent.ACTION_VIEW -> { + mUri?.run { + ArchivePageLoader(asUniFile()) { invalidator -> + runCatching { + dialogState.awaitInputText( + title = getString(R.string.archive_need_passwd), + hint = getString(R.string.archive_passwd), + ) { + if (it.isBlank()) { + getString(R.string.passwd_cannot_be_empty) + } else if (invalidator(it)) { + null + } else { + getString(R.string.passwd_wrong) + } } - } - }.onFailure { - finish() - }.getOrThrow() + }.onFailure { + finish() + }.getOrThrow() + } } } + + else -> null } + if (provider == null) { + makeToast(R.string.error_reading_failed) + finish() + } + return provider } private fun handleIntent(intent: Intent?) { intent ?: return mAction = intent.action - mFilename = intent.getStringExtra(KEY_FILENAME) mUri = intent.data mGalleryInfo = intent.getParcelableExtraCompat(KEY_GALLERY_INFO) mPage = intent.getIntExtra(KEY_PAGE, -1) @@ -221,19 +224,17 @@ class ReaderActivity : EhActivity() { private fun onRestore(savedInstanceState: Bundle) { mAction = savedInstanceState.getString(KEY_ACTION) - mFilename = savedInstanceState.getString(KEY_FILENAME) mUri = savedInstanceState.getParcelableCompat(KEY_URI) mGalleryInfo = savedInstanceState.getParcelableCompat(KEY_GALLERY_INFO) mPage = savedInstanceState.getInt(KEY_PAGE, -1) - mCurrentIndex = savedInstanceState.getInt(KEY_CURRENT_INDEX) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) handleIntent(intent) - buildProvider(true) - mGalleryProvider?.let { - lifecycleScope.launchIO { + lifecycleScope.launchIO { + buildProvider(true)?.let { + mGalleryProvider = it it.start() if (it.awaitReady()) { withUIContext { @@ -241,6 +242,9 @@ class ReaderActivity : EhActivity() { viewer?.setGalleryProvider(it) moveToPageIndex(0) } + } else { + makeToast(R.string.error_reading_failed) + finish() } } } @@ -249,13 +253,11 @@ class ReaderActivity : EhActivity() { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString(KEY_ACTION, mAction) - outState.putString(KEY_FILENAME, mFilename) outState.putParcelable(KEY_URI, mUri) if (mGalleryInfo != null) { outState.putParcelable(KEY_GALLERY_INFO, mGalleryInfo) } outState.putInt(KEY_PAGE, mPage) - outState.putInt(KEY_CURRENT_INDEX, mCurrentIndex) } override fun onCreate(savedInstanceState: Bundle?) { @@ -272,7 +274,6 @@ class ReaderActivity : EhActivity() { } else { onRestore(savedInstanceState) } - buildProvider() binding = ReaderActivityBinding.inflate(layoutInflater) binding.dialogStub.setMD3Content { val brightness by Settings.customBrightness.collectAsState() @@ -319,14 +320,16 @@ class ReaderActivity : EhActivity() { ) } setContentView(binding.root) - mGalleryProvider.let { - if (it == null) { - finish() - return - } - lifecycleScope.launchIO { + lifecycleScope.launchIO { + buildProvider()?.let { + mGalleryProvider = it it.start() - if (it.awaitReady()) withUIContext { setGallery() } + if (it.awaitReady()) { + withUIContext { setGallery() } + } else { + makeToast(R.string.error_reading_failed) + finish() + } } } @@ -348,10 +351,6 @@ class ReaderActivity : EhActivity() { private fun setGallery() { if (mGalleryProvider?.isReady != true) return - // Get start page - if (mCurrentIndex == 0) { - mCurrentIndex = if (mPage >= 0) mPage else mGalleryProvider!!.startPage - } totalPage = mGalleryProvider!!.size viewer?.destroy() viewer = ReadingModeType.toViewer(ReaderPreferences.defaultReadingMode().get(), this) @@ -503,11 +502,9 @@ class ReaderActivity : EhActivity() { companion object { const val ACTION_EH = "eh" const val KEY_ACTION = "action" - const val KEY_FILENAME = "filename" const val KEY_URI = "uri" const val KEY_GALLERY_INFO = "gallery_info" const val KEY_PAGE = "page" - const val KEY_CURRENT_INDEX = "current_index" } // Tachiyomi funcs @@ -637,13 +634,11 @@ class ReaderActivity : EhActivity() { * Called from the viewer whenever a [page] is marked as active. It updates the values of the * bottom menu and delegates the change to the presenter. */ - @SuppressLint("SetTextI18n") fun onPageSelected(page: ReaderPage) { // Set bottom page number currentPage = page.number mCurrentIndex = page.index - mGalleryProvider?.putStartPage(mCurrentIndex) } /**