同步上游

This commit is contained in:
HapeLee
2025-11-15 17:11:06 +08:00
parent f42777d02a
commit 146b59dc96
28 changed files with 190 additions and 82 deletions
+2
View File
@@ -36,6 +36,7 @@ import io.legado.app.help.AppFreezeMonitor
import io.legado.app.help.AppWebDav import io.legado.app.help.AppWebDav
import io.legado.app.help.CrashHandler import io.legado.app.help.CrashHandler
import io.legado.app.help.DefaultData import io.legado.app.help.DefaultData
import io.legado.app.help.DispatchersMonitor
import io.legado.app.help.LifecycleHelp import io.legado.app.help.LifecycleHelp
import io.legado.app.help.RuleBigDataHelp import io.legado.app.help.RuleBigDataHelp
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
@@ -129,6 +130,7 @@ class App : Application() {
.setLogger(EventLogger()) .setLogger(EventLogger())
DefaultData.upVersion() DefaultData.upVersion()
AppFreezeMonitor.init(this@App) AppFreezeMonitor.init(this@App)
DispatchersMonitor.init()
URL.setURLStreamHandlerFactory(ObsoleteUrlFactory(okHttpClient)) URL.setURLStreamHandlerFactory(ObsoleteUrlFactory(okHttpClient))
launch { installGmsTlsProvider(appCtx) } launch { installGmsTlsProvider(appCtx) }
initRhino() initRhino()
@@ -29,6 +29,7 @@ object AppPattern {
val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著") val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著")
val authorRegex = Regex("^\\s*作\\s*者[:\\s]+|\\s+著") val authorRegex = Regex("^\\s*作\\s*者[:\\s]+|\\s+著")
val fileNameRegex = Regex("[\\\\/:*?\"<>|.]") val fileNameRegex = Regex("[\\\\/:*?\"<>|.]")
val fileNameRegex2 = Regex("[\\\\/:*?\"<>|]")
val splitGroupRegex = Regex("[,;,;]") val splitGroupRegex = Regex("[,;,;]")
val titleNumPattern: Pattern = Pattern.compile("(第)(.+?)(章)") val titleNumPattern: Pattern = Pattern.compile("(第)(.+?)(章)")
@@ -25,7 +25,9 @@ import io.legado.app.utils.compress.ZipUtils
import io.legado.app.utils.fromJsonObject import io.legado.app.utils.fromJsonObject
import io.legado.app.utils.getPrefString import io.legado.app.utils.getPrefString
import io.legado.app.utils.isJson import io.legado.app.utils.isJson
import io.legado.app.utils.normalizeFileName
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import splitties.init.appCtx import splitties.init.appCtx
@@ -243,7 +245,7 @@ object AppWebDav {
WebDav(putUrl, it).upload(byteArray, "text/plain") WebDav(putUrl, it).upload(byteArray, "text/plain")
} }
} catch (e: Exception) { } catch (e: Exception) {
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
AppLog.put("WebDav导出失败\n${e.localizedMessage}", e, true) AppLog.put("WebDav导出失败\n${e.localizedMessage}", e, true)
} }
} }
@@ -257,12 +259,16 @@ object AppWebDav {
WebDav(putUrl, it).upload(uri, "text/plain") WebDav(putUrl, it).upload(uri, "text/plain")
} }
} catch (e: Exception) { } catch (e: Exception) {
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
AppLog.put("WebDav导出失败\n${e.localizedMessage}", e, true) AppLog.put("WebDav导出失败\n${e.localizedMessage}", e, true)
} }
} }
suspend fun uploadBookProgress(book: Book) { suspend fun uploadBookProgress(
book: Book,
toast: Boolean = false,
onSuccess: (() -> Unit)? = null
) {
val authorization = authorization ?: return val authorization = authorization ?: return
if (!AppConfig.syncBookProgress) return if (!AppConfig.syncBookProgress) return
if (!NetworkUtils.isAvailable()) return if (!NetworkUtils.isAvailable()) return
@@ -272,9 +278,10 @@ object AppWebDav {
val url = getProgressUrl(book.name, book.author) val url = getProgressUrl(book.name, book.author)
WebDav(url, authorization).upload(json.toByteArray(), "application/json") WebDav(url, authorization).upload(json.toByteArray(), "application/json")
book.syncTime = System.currentTimeMillis() book.syncTime = System.currentTimeMillis()
onSuccess?.invoke()
} catch (e: Exception) { } catch (e: Exception) {
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
AppLog.put("上传进度失败\n${e.localizedMessage}", e) AppLog.put("上传进度失败\n${e.localizedMessage}", e, toast)
} }
} }
@@ -288,7 +295,7 @@ object AppWebDav {
WebDav(url, authorization).upload(json.toByteArray(), "application/json") WebDav(url, authorization).upload(json.toByteArray(), "application/json")
onSuccess?.invoke() onSuccess?.invoke()
} catch (e: Exception) { } catch (e: Exception) {
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
AppLog.put("上传进度失败\n${e.localizedMessage}", e) AppLog.put("上传进度失败\n${e.localizedMessage}", e)
} }
} }
@@ -298,27 +305,30 @@ object AppWebDav {
} }
private fun getProgressFileName(name: String, author: String): String { private fun getProgressFileName(name: String, author: String): String {
return UrlUtil.replaceReservedChar("${name}_${author}") + ".json" return UrlUtil.replaceReservedChar("${name}_${author}".normalizeFileName()) + ".json"
} }
/** /**
* 获取书籍进度 * 获取书籍进度
*/ */
suspend fun getBookProgress(book: Book): BookProgress? { suspend fun getBookProgress(book: Book): BookProgress? {
authorization?.let {
val url = getProgressUrl(book.name, book.author) val url = getProgressUrl(book.name, book.author)
kotlin.runCatching { kotlin.runCatching {
WebDav(url, it).download().let { byteArray -> val authorization = authorization ?: return null
WebDav(url, authorization).download().let { byteArray ->
val json = String(byteArray) val json = String(byteArray)
if (json.isJson()) { if (json.isJson()) {
return GSON.fromJsonObject<BookProgress>(json).getOrNull() return GSON.fromJsonObject<BookProgress>(json).getOrNull()
} }
} }
}.onFailure { }.onFailure {
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
AppLog.put("获取书籍进度失败\n${it.localizedMessage}", it) AppLog.put("获取书籍进度失败\n${it.localizedMessage}", it)
} }
}
return null return null
} }
@@ -33,6 +33,7 @@ import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import androidx.core.net.toUri import androidx.core.net.toUri
import io.legado.app.utils.normalizeFileName
val Book.isAudio: Boolean val Book.isAudio: Boolean
@@ -376,7 +377,7 @@ fun Book.getExportFileName(
RhinoScriptEngine.eval(jsStr, bindings).toString() + "." + suffix RhinoScriptEngine.eval(jsStr, bindings).toString() + "." + suffix
}.onFailure { }.onFailure {
AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it) AppLog.put("导出书名规则错误,使用默认规则\n${it.localizedMessage}", it)
}.getOrDefault(default) }.getOrDefault(default).normalizeFileName()
} }
// 根据当前日期计算章节总数 // 根据当前日期计算章节总数
@@ -77,7 +77,7 @@ object Backup {
"backup${backupDate}-${deviceName}.zip" "backup${backupDate}-${deviceName}.zip"
} else { } else {
"backup${backupDate}.zip" "backup${backupDate}.zip"
} }.normalizeFileName()
} }
private fun shouldBackup(): Boolean { private fun shouldBackup(): Boolean {
@@ -142,7 +142,7 @@ internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder<Ale
} }
override fun build(): AlertDialog { override fun build(): AlertDialog {
var dialog = builder.create() val dialog = builder.create()
if (AppConfig.isEInkMode) { if (AppConfig.isEInkMode) {
dialog.window?.run { dialog.window?.run {
val attr = attributes val attr = attributes
@@ -156,7 +156,7 @@ internal class AndroidAlertBuilder(override val ctx: Context) : AlertBuilder<Ale
} }
override fun show(): AlertDialog { override fun show(): AlertDialog {
var dialog = builder.show() val dialog = builder.show()
if (AppConfig.isEInkMode) { if (AppConfig.isEInkMode) {
dialog.window?.run { dialog.window?.run {
val attr = attributes val attr = attributes
@@ -34,7 +34,9 @@ import io.legado.app.utils.postEvent
import io.legado.app.utils.stackTraceStr import io.legado.app.utils.stackTraceStr
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Dispatchers.Main
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@@ -44,9 +46,13 @@ import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import java.util.concurrent.ConcurrentHashMap
import kotlin.coroutines.cancellation.CancellationException
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -69,6 +75,10 @@ object ReadBook : CoroutineScope by MainScope() {
var msg: String? = null var msg: String? = null
private val loadingChapters = arrayListOf<Int>() private val loadingChapters = arrayListOf<Int>()
private val readRecord = ReadRecord() private val readRecord = ReadRecord()
private val chapterLoadingJobs = ConcurrentHashMap<Int, Coroutine<*>>()
private val prevChapterLoadingLock = Mutex()
private val curChapterLoadingLock = Mutex()
private val nextChapterLoadingLock = Mutex()
var readStartTime: Long = System.currentTimeMillis() var readStartTime: Long = System.currentTimeMillis()
/* 跳转进度前进度记录 */ /* 跳转进度前进度记录 */
@@ -210,9 +220,7 @@ object ReadBook : CoroutineScope by MainScope() {
} }
fun clearTextChapter() { fun clearTextChapter() {
prevTextChapter?.cancelLayout() clearExpiredChapterLoadingJob(true)
curTextChapter?.cancelLayout()
nextTextChapter?.cancelLayout()
prevTextChapter = null prevTextChapter = null
curTextChapter = null curTextChapter = null
nextTextChapter = null nextTextChapter = null
@@ -224,13 +232,14 @@ object ReadBook : CoroutineScope by MainScope() {
nextTextChapter?.clearSearchResult() nextTextChapter?.clearSearchResult()
} }
fun uploadProgress(successAction: (() -> Unit)? = null) { fun uploadProgress(toast: Boolean = false, successAction: (() -> Unit)? = null) {
book?.let { book?.let {
launch(IO) { launch(IO) {
AppWebDav.uploadBookProgress(it) AppWebDav.uploadBookProgress(it, toast) {
successAction?.invoke()
}
ensureActive() ensureActive()
it.update() it.update()
successAction?.invoke()
} }
} }
} }
@@ -324,7 +333,7 @@ object ReadBook : CoroutineScope by MainScope() {
if (durChapterIndex < simulatedChapterSize - 1) { if (durChapterIndex < simulatedChapterSize - 1) {
durChapterPos = 0 durChapterPos = 0
durChapterIndex++ durChapterIndex++
prevTextChapter?.cancelLayout() clearExpiredChapterLoadingJob()
prevTextChapter = curTextChapter prevTextChapter = curTextChapter
curTextChapter = nextTextChapter curTextChapter = nextTextChapter
nextTextChapter = null nextTextChapter = null
@@ -355,7 +364,7 @@ object ReadBook : CoroutineScope by MainScope() {
if (durChapterIndex < simulatedChapterSize - 1) { if (durChapterIndex < simulatedChapterSize - 1) {
durChapterPos = 0 durChapterPos = 0
durChapterIndex++ durChapterIndex++
prevTextChapter?.cancelLayout() clearExpiredChapterLoadingJob()
prevTextChapter = curTextChapter prevTextChapter = curTextChapter
curTextChapter = nextTextChapter curTextChapter = nextTextChapter
nextTextChapter = null nextTextChapter = null
@@ -387,7 +396,7 @@ object ReadBook : CoroutineScope by MainScope() {
if (durChapterIndex > 0) { if (durChapterIndex > 0) {
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0 durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
durChapterIndex-- durChapterIndex--
nextTextChapter?.cancelLayout() clearExpiredChapterLoadingJob()
nextTextChapter = curTextChapter nextTextChapter = curTextChapter
curTextChapter = prevTextChapter curTextChapter = prevTextChapter
prevTextChapter = null prevTextChapter = null
@@ -438,10 +447,15 @@ object ReadBook : CoroutineScope by MainScope() {
} }
} }
fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) { fun openChapter(
index: Int,
durChapterPos: Int = 0,
upContent: Boolean = true,
success: (() -> Unit)? = null
) {
if (index < chapterSize) { if (index < chapterSize) {
clearTextChapter() clearTextChapter()
callBack?.upContent() if (upContent) callBack?.upContent()
durChapterIndex = index durChapterIndex = index
ReadBook.durChapterPos = durChapterPos ReadBook.durChapterPos = durChapterPos
saveRead() saveRead()
@@ -671,6 +685,7 @@ object ReadBook : CoroutineScope by MainScope() {
/** /**
* 内容加载完成 * 内容加载完成
*/ */
@Synchronized
fun contentLoadFinish( fun contentLoadFinish(
book: Book, book: Book,
chapter: BookChapter, chapter: BookChapter,
@@ -684,7 +699,8 @@ object ReadBook : CoroutineScope by MainScope() {
if (canceled || chapter.index !in durChapterIndex - 1..durChapterIndex + 1) { if (canceled || chapter.index !in durChapterIndex - 1..durChapterIndex + 1) {
return return
} }
Coroutine.async { chapterLoadingJobs[chapter.index]?.cancel()
val job = Coroutine.async(this, start = CoroutineStart.LAZY) {
val contentProcessor = ContentProcessor.get(book.name, book.origin) val contentProcessor = ContentProcessor.get(book.name, book.origin)
val displayTitle = chapter.getDisplayTitle( val displayTitle = chapter.getDisplayTitle(
contentProcessor.getTitleReplaceRules(), contentProcessor.getTitleReplaceRules(),
@@ -692,13 +708,16 @@ object ReadBook : CoroutineScope by MainScope() {
) )
val contents = contentProcessor val contents = contentProcessor
.getContent(book, chapter, content, includeTitle = false) .getContent(book, chapter, content, includeTitle = false)
ensureActive()
val textChapter = ChapterProvider.getTextChapterAsync( val textChapter = ChapterProvider.getTextChapterAsync(
this@ReadBook, book, chapter, displayTitle, contents, simulatedChapterSize this, book, chapter, displayTitle, contents, simulatedChapterSize
) )
when (val offset = chapter.index - durChapterIndex) { when (val offset = chapter.index - durChapterIndex) {
0 -> { 0 -> curChapterLoadingLock.withLock {
curTextChapter?.cancelLayout() withContext(Main) {
ensureActive()
curTextChapter = textChapter curTextChapter = textChapter
}
callBack?.upMenuView() callBack?.upMenuView()
var available = false var available = false
for (page in textChapter.layoutChannel) { for (page in textChapter.layoutChannel) {
@@ -721,16 +740,20 @@ object ReadBook : CoroutineScope by MainScope() {
callBack?.contentLoadFinish() callBack?.contentLoadFinish()
} }
-1 -> { -1 -> prevChapterLoadingLock.withLock {
prevTextChapter?.cancelLayout() withContext(Main) {
ensureActive()
prevTextChapter = textChapter prevTextChapter = textChapter
}
textChapter.layoutChannel.receiveAsFlow().collect() textChapter.layoutChannel.receiveAsFlow().collect()
if (upContent) callBack?.upContent(offset, resetPageOffset) if (upContent) callBack?.upContent(offset, resetPageOffset)
} }
1 -> { 1 -> nextChapterLoadingLock.withLock {
nextTextChapter?.cancelLayout() withContext(Main) {
ensureActive()
nextTextChapter = textChapter nextTextChapter = textChapter
}
for (page in textChapter.layoutChannel) { for (page in textChapter.layoutChannel) {
if (page.index > 1) { if (page.index > 1) {
continue continue
@@ -742,11 +765,16 @@ object ReadBook : CoroutineScope by MainScope() {
return@async return@async
}.onError { }.onError {
if (it is CancellationException) {
return@onError
}
AppLog.put("ChapterProvider ERROR", it) AppLog.put("ChapterProvider ERROR", it)
appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}") appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}")
}.onSuccess { }.onSuccess {
success?.invoke() success?.invoke()
} }
chapterLoadingJobs[chapter.index] = job
job.start()
} }
suspend fun contentLoadFinishAwait( suspend fun contentLoadFinishAwait(
@@ -774,7 +802,9 @@ object ReadBook : CoroutineScope by MainScope() {
when (val offset = chapter.index - durChapterIndex) { when (val offset = chapter.index - durChapterIndex) {
0 -> { 0 -> {
curTextChapter?.cancelLayout() curTextChapter?.cancelLayout()
withContext(Main) {
curTextChapter = textChapter curTextChapter = textChapter
}
callBack?.upMenuView() callBack?.upMenuView()
var available = false var available = false
for (page in textChapter.layoutChannel) { for (page in textChapter.layoutChannel) {
@@ -799,14 +829,18 @@ object ReadBook : CoroutineScope by MainScope() {
-1 -> { -1 -> {
prevTextChapter?.cancelLayout() prevTextChapter?.cancelLayout()
withContext(Main) {
prevTextChapter = textChapter prevTextChapter = textChapter
}
textChapter.layoutChannel.receiveAsFlow().collect() textChapter.layoutChannel.receiveAsFlow().collect()
if (upContent) callBack?.upContent(offset, resetPageOffset) if (upContent) callBack?.upContent(offset, resetPageOffset)
} }
1 -> { 1 -> {
nextTextChapter?.cancelLayout() nextTextChapter?.cancelLayout()
withContext(Main) {
nextTextChapter = textChapter nextTextChapter = textChapter
}
for (page in textChapter.layoutChannel) { for (page in textChapter.layoutChannel) {
if (page.index > 1) { if (page.index > 1) {
continue continue
@@ -818,6 +852,9 @@ object ReadBook : CoroutineScope by MainScope() {
return return
}.onFailure { }.onFailure {
if (it is CancellationException) {
return@onFailure
}
AppLog.put("ChapterProvider ERROR", it) AppLog.put("ChapterProvider ERROR", it)
appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}") appCtx.toastOnUi("ChapterProvider ERROR:\n${it.stackTraceStr}")
} }
@@ -913,6 +950,17 @@ object ReadBook : CoroutineScope by MainScope() {
} }
} }
private fun clearExpiredChapterLoadingJob(clearAll: Boolean = false) {
val iterator = chapterLoadingJobs.iterator()
while (iterator.hasNext()) {
val (index, job) = iterator.next()
if (clearAll || index !in durChapterIndex - 1..durChapterIndex + 1) {
job.cancel()
iterator.remove()
}
}
}
fun cancelPreDownloadTask() { fun cancelPreDownloadTask() {
if (contentLoadFinish) { if (contentLoadFinish) {
preDownloadTask?.cancel() preDownloadTask?.cancel()
@@ -956,6 +1004,7 @@ object ReadBook : CoroutineScope by MainScope() {
downloadScope.coroutineContext.cancelChildren() downloadScope.coroutineContext.cancelChildren()
coroutineContext.cancelChildren() coroutineContext.cancelChildren()
ImageProvider.clear() ImageProvider.clear()
clearExpiredChapterLoadingJob(true)
if (!CacheBookService.isRun) { if (!CacheBookService.isRun) {
CacheBook.close() CacheBook.close()
} }
@@ -482,7 +482,9 @@ object ReadManga : CoroutineScope by MainScope() {
fun uploadProgress(successAction: (() -> Unit)? = null) { fun uploadProgress(successAction: (() -> Unit)? = null) {
book?.let { book?.let {
launch(IO) { launch(IO) {
AppWebDav.uploadBookProgress(it) AppWebDav.uploadBookProgress(it) {
successAction?.invoke()
}
ensureActive() ensureActive()
it.update() it.update()
successAction?.invoke() successAction?.invoke()
@@ -172,6 +172,7 @@ class ExportBookService : BaseService() {
val (bookUrl, exportConfig) = waitExportBooks.entries.firstOrNull() ?: let { val (bookUrl, exportConfig) = waitExportBooks.entries.firstOrNull() ?: let {
notificationContentText = "导出完成" notificationContentText = "导出完成"
upExportNotification(true) upExportNotification(true)
stopSelf()
return@launch return@launch
} }
exportProgress[bookUrl] = 0 exportProgress[bookUrl] = 0
@@ -171,7 +171,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
when (item.itemId) { when (item.itemId) {
R.id.menu_download, R.id.menu_download,
R.id.menu_download_after -> { R.id.menu_download_after -> {
if (!CacheBook.isRun) { if (!CacheBook.isRun) sureCacheBook {
adapter.getItems().forEach { book -> adapter.getItems().forEach { book ->
val indices = (book.durChapterIndex..book.lastChapterIndex).toList() val indices = (book.durChapterIndex..book.lastChapterIndex).toList()
CacheBook.start(this@CacheActivity, book, indices) CacheBook.start(this@CacheActivity, book, indices)
@@ -182,7 +182,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
} }
R.id.menu_download_all -> { R.id.menu_download_all -> {
if (!CacheBook.isRun) { if (!CacheBook.isRun) sureCacheBook {
adapter.getItems().forEach { book -> adapter.getItems().forEach { book ->
val indices = (0..book.lastChapterIndex).toList() val indices = (0..book.lastChapterIndex).toList()
CacheBook.start(this@CacheActivity, book, indices) CacheBook.start(this@CacheActivity, book, indices)
@@ -546,7 +546,15 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
} }
} }
private fun sureCacheBook(action: () -> Unit) {
alert(R.string.draw) {
setMessage(R.string.sure_cache_book)
noButton()
yesButton {
action.invoke()
}
}
}
private fun getTypeName(): String { private fun getTypeName(): String {
return exportTypes.getOrElse(AppConfig.exportType) { return exportTypes.getOrElse(AppConfig.exportType) {
@@ -187,6 +187,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
binding.llRetry.isGone = true binding.llRetry.isGone = true
ReadManga.loadOrUpContent() ReadManga.loadOrUpContent()
} }
binding.flLoading.isVisible = !AppConfig.isEInkMode
mAdapter.addFooterView { mAdapter.addFooterView {
ViewLoadMoreBinding.bind(loadMoreView) ViewLoadMoreBinding.bind(loadMoreView)
} }
@@ -313,10 +314,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
override fun onPostCreate(savedInstanceState: Bundle?) { override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState) super.onPostCreate(savedInstanceState)
// Looper.myQueue().addIdleHandler { viewModel.initData(intent)
// viewModel.initData(intent)
// false
// }
justInitData = true justInitData = true
} }
@@ -19,7 +19,7 @@ import java.security.MessageDigest
* 仅当applyBinarization为true时有效。 * 仅当applyBinarization为true时有效。
*/ */
class EpaperTransformation( class EpaperTransformation(
@IntRange(0, 255) private val threshold: Int = 128, @param:IntRange(0, 255) private val threshold: Int = 128,
) : BitmapTransformation() { ) : BitmapTransformation() {
private val ID get() = "io.legado.app.model.EpaperTransformation.$threshold" private val ID get() = "io.legado.app.model.EpaperTransformation.$threshold"
@@ -671,7 +671,7 @@ class ReadBookActivity : BaseReadBookActivity(),
} }
R.id.menu_cover_progress -> ReadBook.book?.let { R.id.menu_cover_progress -> ReadBook.book?.let {
ReadBook.uploadProgress { toastOnUi(R.string.upload_book_success) } ReadBook.uploadProgress(true) { toastOnUi(R.string.upload_book_success) }
} }
R.id.menu_same_title_removed -> { R.id.menu_same_title_removed -> {
@@ -1415,10 +1415,17 @@ class ReadBookActivity : BaseReadBookActivity(),
ReadAloud.upReadAloudClass() ReadAloud.upReadAloudClass()
val scrollPageAnim = ReadBook.pageAnim() == 3 val scrollPageAnim = ReadBook.pageAnim() == 3
if (scrollPageAnim) { if (scrollPageAnim) {
val line = binding.readView.getCurVisibleFirstLine() val pos = binding.readView.getReadAloudPos()
if (line != null) { if (pos != null) {
val (index, line) = pos
if (ReadBook.durChapterIndex != index) {
ReadBook.openChapter(index, line.chapterPosition, false) {
ReadBook.readAloud(startPos = line.pagePosition)
}
} else {
ReadBook.durChapterPos = line.chapterPosition ReadBook.durChapterPos = line.chapterPosition
ReadBook.readAloud(startPos = line.pagePosition) ReadBook.readAloud(startPos = line.pagePosition)
}
} else { } else {
ReadBook.readAloud() ReadBook.readAloud()
} }
@@ -1431,10 +1438,17 @@ class ReadBookActivity : BaseReadBookActivity(),
val scrollPageAnim = ReadBook.pageAnim() == 3 val scrollPageAnim = ReadBook.pageAnim() == 3
if (scrollPageAnim && pageChanged) { if (scrollPageAnim && pageChanged) {
pageChanged = false pageChanged = false
val line = binding.readView.getCurVisibleFirstLine() val pos = binding.readView.getReadAloudPos()
if (line != null) { if (pos != null) {
val (index, line) = pos
if (ReadBook.durChapterIndex != index) {
ReadBook.openChapter(index, line.chapterPosition, false) {
ReadBook.readAloud(startPos = line.pagePosition)
}
} else {
ReadBook.durChapterPos = line.chapterPosition ReadBook.durChapterPos = line.chapterPosition
ReadBook.readAloud(startPos = line.pagePosition) ReadBook.readAloud(startPos = line.pagePosition)
}
} else { } else {
ReadBook.readAloud() ReadBook.readAloud()
} }
@@ -321,7 +321,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) { fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) {
ReadBook.openChapter(index, durChapterPos, success) ReadBook.openChapter(index, durChapterPos, success = success)
} }
fun removeFromBookshelf(success: (() -> Unit)?) { fun removeFromBookshelf(success: (() -> Unit)?) {
@@ -459,7 +459,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
return visiblePage return visiblePage
} }
fun getCurVisibleFirstLine(): TextLine? { fun getReadAloudPos(): Pair<Int, TextLine>? {
var relativeOffset: Float var relativeOffset: Float
for (relativePos in 0..2) { for (relativePos in 0..2) {
relativeOffset = relativeOffset(relativePos) relativeOffset = relativeOffset(relativePos)
@@ -477,7 +477,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
lineTop += relativeOffset lineTop += relativeOffset
lineBottom += relativeOffset lineBottom += relativeOffset
} }
return visibleLine return textPage.chapterIndex to visibleLine
} }
} }
} }
@@ -496,8 +496,8 @@ class PageView(context: Context) : FrameLayout(context) {
return binding.contentTextView.getCurVisiblePage() return binding.contentTextView.getCurVisiblePage()
} }
fun getCurVisibleFirstLine(): TextLine? { fun getReadAloudPos(): Pair<Int, TextLine>? {
return binding.contentTextView.getCurVisibleFirstLine() return binding.contentTextView.getReadAloudPos()
} }
fun markAsMainView() { fun markAsMainView() {
@@ -673,8 +673,8 @@ class ReadView(context: Context, attrs: AttributeSet) :
return curPage.getCurVisiblePage() return curPage.getCurVisiblePage()
} }
fun getCurVisibleFirstLine(): TextLine? { fun getReadAloudPos(): Pair<Int, TextLine>? {
return curPage.getCurVisibleFirstLine() return curPage.getReadAloudPos()
} }
fun invalidateTextPage() { fun invalidateTextPage() {
@@ -34,6 +34,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.LinkedList import java.util.LinkedList
@@ -120,6 +121,8 @@ class TextChapterLayout(
}.onError { }.onError {
exception = it exception = it
onException(it) onException(it)
}.onCancel {
channel.cancel()
}.onFinally { }.onFinally {
isCompleted = true isCompleted = true
} }
@@ -304,7 +307,7 @@ class TextChapterLayout(
var isSetTypedImage = false var isSetTypedImage = false
var wordCount = 0 var wordCount = 0
contents.forEach { content -> contents.forEach { content ->
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
if (isTextImageStyle) { if (isTextImageStyle) {
//图片样式为文字嵌入类型 //图片样式为文字嵌入类型
var text = content.replace(ChapterProvider.srcReplaceChar, "") //▣ var text = content.replace(ChapterProvider.srcReplaceChar, "") //▣
@@ -342,7 +345,7 @@ class TextChapterLayout(
val text = content.replace(ChapterProvider.srcReplaceChar, "") val text = content.replace(ChapterProvider.srcReplaceChar, "")
val matcher = AppPattern.imgPattern.matcher(text) val matcher = AppPattern.imgPattern.matcher(text)
while (matcher.find()) { while (matcher.find()) {
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
val imgSrc = matcher.group(1)!! val imgSrc = matcher.group(1)!!
var iStyle = imageStyle var iStyle = imageStyle
var isSmallImage = true var isSmallImage = true
@@ -450,7 +453,7 @@ class TextChapterLayout(
textPage.height += endPadding textPage.height += endPadding
} }
textPage.text = stringBuilder.toString() textPage.text = stringBuilder.toString()
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
onPageCompleted() onPageCompleted()
onCompleted() onCompleted()
} }
@@ -915,7 +918,7 @@ class TextChapterLayout(
textPage.leftLineSize = textPage.lineSize textPage.leftLineSize = textPage.lineSize
} }
textPage.text = stringBuilder.toString() textPage.text = stringBuilder.toString()
coroutineContext.ensureActive() currentCoroutineContext().ensureActive()
onPageCompleted() onPageCompleted()
//新建页面 //新建页面
pendingTextPage = TextPage() pendingTextPage = TextPage()
@@ -20,6 +20,7 @@ import io.legado.app.R
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.databinding.DialogEditTextBinding
import io.legado.app.help.AppFreezeMonitor import io.legado.app.help.AppFreezeMonitor
import io.legado.app.help.DispatchersMonitor
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig import io.legado.app.help.config.LocalConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
@@ -203,6 +204,7 @@ class OtherConfigFragment : PreferenceFragment(),
LogUtils.logDeviceInfo() LogUtils.logDeviceInfo()
LiveEventBus.config().enableLogger(AppConfig.recordLog) LiveEventBus.config().enableLogger(AppConfig.recordLog)
AppFreezeMonitor.init(appCtx) AppFreezeMonitor.init(appCtx)
DispatchersMonitor.init()
} }
PreferKey.processText -> sharedPreferences?.let { PreferKey.processText -> sharedPreferences?.let {
@@ -29,7 +29,9 @@ import io.legado.app.utils.onEachParallel
import io.legado.app.utils.postEvent import io.legado.app.utils.postEvent
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
@@ -175,6 +177,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
ReadBook.onChapterListUpdated(book) ReadBook.onChapterListUpdated(book)
addDownload(source, book) addDownload(source, book)
}.onFailure { }.onFailure {
currentCoroutineContext().ensureActive()
AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it) AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it)
//这里可能因为时间太长书籍信息已经更改,所以重新获取 //这里可能因为时间太长书籍信息已经更改,所以重新获取
appDb.bookDao.getBook(book.bookUrl)?.let { book -> appDb.bookDao.getBook(book.bookUrl)?.let { book ->
@@ -201,7 +201,7 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
booksFlowJob?.cancel() booksFlowJob?.cancel()
booksFlowJob = viewLifecycleOwner.lifecycleScope.launch { booksFlowJob = viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
appDb.bookDao.flowByGroup(groupId) appDb.bookDao.flowByGroup(groupId)
.map { list -> .map { list ->
@@ -250,7 +250,7 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
if (!AppConfig.showLastUpdateTime || (bookshelfLayoutMode != 0 && bookshelfLayoutMode != 4)) { if (!AppConfig.showLastUpdateTime || (bookshelfLayoutMode != 0 && bookshelfLayoutMode != 4)) {
return return
} }
upLastUpdateTimeJob = lifecycleScope.launch { upLastUpdateTimeJob = viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) { repeatOnLifecycle(Lifecycle.State.RESUMED) {
while (isActive) { while (isActive) {
booksAdapter.upLastUpdateTime() booksAdapter.upLastUpdateTime()
@@ -3,6 +3,7 @@ package io.legado.app.ui.widget.recycler
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import io.legado.app.R import io.legado.app.R
@@ -26,7 +27,7 @@ class LoadMoreView(context: Context, attrs: AttributeSet? = null) : FrameLayout(
init { init {
super.setOnClickListener { super.setOnClickListener {
if (!showErrorDialog()) { if (!showErrorDialog(it)) {
onClickListener?.onClick(it) onClickListener?.onClick(it)
} }
} }
@@ -38,7 +39,7 @@ class LoadMoreView(context: Context, attrs: AttributeSet? = null) : FrameLayout(
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT layoutParams.width = LayoutParams.MATCH_PARENT
} }
fun startLoad() { fun startLoad() {
@@ -79,12 +80,17 @@ class LoadMoreView(context: Context, attrs: AttributeSet? = null) : FrameLayout(
binding.tvText.visible() binding.tvText.visible()
} }
private fun showErrorDialog(): Boolean { private fun showErrorDialog(view: View): Boolean {
if (errorMsg.isBlank()) { if (errorMsg.isBlank()) {
return false return false
} }
context.alert(R.string.error) { context.alert(R.string.error) {
setMessage(errorMsg) setMessage(errorMsg)
if (onClickListener != null) {
neutralButton(R.string.retry) {
onClickListener?.onClick(view)
}
}
} }
return true return true
} }
@@ -140,3 +140,7 @@ fun String.escapeRegex(): String {
} }
fun String.encodeURI(): String = URLEncodeUtil.encodeQuery(this) fun String.encodeURI(): String = URLEncodeUtil.encodeQuery(this)
fun String.normalizeFileName(): String {
return replace(AppPattern.fileNameRegex2, "_")
}
@@ -1211,4 +1211,5 @@
<string name="hide_manga_title">隐藏漫画列表标题</string> <string name="hide_manga_title">隐藏漫画列表标题</string>
<string name="refresh_explore">刷新发现</string> <string name="refresh_explore">刷新发现</string>
<string name="padding_display_cutouts">填充刘海区域</string> <string name="padding_display_cutouts">填充刘海区域</string>
<string name="sure_cache_book">是否确认开始缓存?</string>
</resources> </resources>
@@ -1213,4 +1213,5 @@
<string name="hide_manga_title">隐藏漫画列表标题</string> <string name="hide_manga_title">隐藏漫画列表标题</string>
<string name="refresh_explore">刷新发现</string> <string name="refresh_explore">刷新发现</string>
<string name="padding_display_cutouts">填充刘海区域</string> <string name="padding_display_cutouts">填充刘海区域</string>
<string name="sure_cache_book">是否确认开始缓存?</string>
</resources> </resources>
+1
View File
@@ -1345,4 +1345,5 @@
<string name="dotted_line_while">空隙比例</string> <string name="dotted_line_while">空隙比例</string>
<string name="underline_height">线段高度</string> <string name="underline_height">线段高度</string>
<string name="title_bar_mode">菜单标题显示方式</string> <string name="title_bar_mode">菜单标题显示方式</string>
<string name="sure_cache_book">是否确认开始缓存?</string>
</resources> </resources>
+1
View File
@@ -1346,4 +1346,5 @@
<string name="dotted_line_while">空隙比例</string> <string name="dotted_line_while">空隙比例</string>
<string name="underline_height">线段高度</string> <string name="underline_height">线段高度</string>
<string name="title_bar_mode">菜单标题显示方式</string> <string name="title_bar_mode">菜单标题显示方式</string>
<string name="sure_cache_book">是否确认开始缓存?</string>
</resources> </resources>
+1 -1
View File
@@ -169,7 +169,7 @@ liveeventbus = { module = "com.github.michaellee123:LiveEventBus", version.ref =
material = { module = "com.google.android.material:material", version.ref = "material" } material = { module = "com.google.android.material:material", version.ref = "material" }
mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "rhino" } mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
renderscript-intrinsics-replacement-toolkit = { module = "com.github.TomasValenta:renderscript-intrinsics-replacement-toolkit", version = "8eaa829ddd" }
zxing-lite = { module = "com.github.jenly1314:zxing-lite", version.ref = "zxingLite" } zxing-lite = { module = "com.github.jenly1314:zxing-lite", version.ref = "zxingLite" }
[bundles] [bundles]