Move reading progress to database

In this way cbz archives can be read without SpiderInfo.
This commit is contained in:
FooIbar committed 2024-02-06 21:33:24 +08:00
1 parent 9a7cabb221
commit 5f0be5ae43
14 files changed
+672 -151

No files matched your search

@@ -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')"
]
}
}
@@ -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)
@@ -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
}
@@ -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<Int>
@Query("SELECT * FROM PROGRESS")
suspend fun list(): List<ProgressInfo>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertOrIgnore(progress: List<ProgressInfo>)
@Upsert
suspend fun upsert(progress: ProgressInfo)
@Query("DELETE FROM PROGRESS")
suspend fun deleteAll()
}
@@ -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,
)
@@ -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
@@ -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<Int, Job>()
@@ -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
@@ -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<Int>, pair: Pair<Int, Int>) {
mSpiderQueen.preloadPages(pages, pair)
}
private class ReleaseTask(private var mSpiderQueen: SpiderQueen?) : Runnable {
override fun run() {
mSpiderQueen?.let { releaseSpiderQueen(it, SpiderQueen.MODE_READ) }
mSpiderQueen = null
}
}
}
@@ -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
@@ -26,7 +26,7 @@ fun readLegacySpiderInfo(inputStream: InputStream): SpiderInfo {
val previewPerPage = readInt()
val pages = read().toInt()
val pTokenMap = hashMapOf<Int, String>()
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()
@@ -19,29 +19,31 @@ import splitties.init.appCtx
class SpiderInfo(
val gid: Long,
val token: String,
val pages: Int,
val pTokenMap: MutableMap<Int, String> = 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<SpiderInfo>(data.toFile().readBytes())
cbor.decodeFromByteArray<SpiderInfo>(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<SpiderInfo>(it.readBytes())
cbor.decodeFromByteArray<SpiderInfo>(it.readBytes())
}
}.getOrNull() ?: runCatching {
file.openInputStream().use { readLegacySpiderInfo(it) }
@@ -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
@@ -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) {
@@ -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)
}
/**