diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index f5f640bf9..c1d22ada3 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -113,6 +113,7 @@ object PreferKey { const val showAddToShelfAlert = "showAddToShelfAlert" const val asyncLoadImage = "asyncLoadImage" const val ignoreAudioFocus = "ignoreAudioFocus" + const val parallelExportBook = "parallelExportBook" const val cPrimary = "colorPrimary" const val cAccent = "colorAccent" diff --git a/app/src/main/java/io/legado/app/help/ContentProcessor.kt b/app/src/main/java/io/legado/app/help/ContentProcessor.kt index a8db44beb..92dbec3ad 100644 --- a/app/src/main/java/io/legado/app/help/ContentProcessor.kt +++ b/app/src/main/java/io/legado/app/help/ContentProcessor.kt @@ -81,33 +81,35 @@ class ContentProcessor private constructor( reSegment: Boolean = true ): List { var mContent = content - //去除重复标题 - try { - val name = Pattern.quote(book.name) - val title = Pattern.quote(chapter.title) - val titleRegex = "^(\\s|\\p{P}|${name})*${title}(\\s)*".toRegex() - mContent = mContent.replace(titleRegex, "") - } catch (e: Exception) { - AppLog.put("去除重复标题出错\n${e.localizedMessage}", e) - } - if (reSegment && book.getReSegment()) { - //重新分段 - mContent = ContentHelp.reSegment(mContent, chapter.title) - } - if (chineseConvert) { - //简繁转换 + if (content != "null") { + //去除重复标题 try { - when (AppConfig.chineseConverterType) { - 1 -> mContent = ChineseUtils.t2s(mContent) - 2 -> mContent = ChineseUtils.s2t(mContent) - } + val name = Pattern.quote(book.name) + val title = Pattern.quote(chapter.title) + val titleRegex = "^(\\s|\\p{P}|${name})*${title}(\\s)*".toRegex() + mContent = mContent.replace(titleRegex, "") } catch (e: Exception) { - appCtx.toastOnUi("简繁转换出错") + AppLog.put("去除重复标题出错\n${e.localizedMessage}", e) + } + if (reSegment && book.getReSegment()) { + //重新分段 + mContent = ContentHelp.reSegment(mContent, chapter.title) + } + if (chineseConvert) { + //简繁转换 + try { + when (AppConfig.chineseConverterType) { + 1 -> mContent = ChineseUtils.t2s(mContent) + 2 -> mContent = ChineseUtils.s2t(mContent) + } + } catch (e: Exception) { + appCtx.toastOnUi("简繁转换出错") + } + } + if (useReplace && book.getUseReplaceRule()) { + //替换 + mContent = replaceContent(mContent) } - } - if (useReplace && book.getUseReplaceRule()) { - //替换 - mContent = replaceContent(mContent) } if (includeTitle) { //重新添加标题 diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index 55a579868..db3b3be78 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -255,6 +255,12 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener { appCtx.putPrefBoolean(PreferKey.exportPictureFile, value) } + var parallelExportBook: Boolean + get() = appCtx.getPrefBoolean(PreferKey.parallelExportBook, false) + set(value) { + appCtx.putPrefBoolean(PreferKey.parallelExportBook, value) + } + var changeSourceCheckAuthor: Boolean get() = appCtx.getPrefBoolean(PreferKey.changeSourceCheckAuthor) set(value) { diff --git a/app/src/main/java/io/legado/app/help/coroutine/OrderCoroutine.kt b/app/src/main/java/io/legado/app/help/coroutine/OrderCoroutine.kt new file mode 100644 index 000000000..3b0989588 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/coroutine/OrderCoroutine.kt @@ -0,0 +1,54 @@ +package io.legado.app.help.coroutine + +import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.PriorityBlockingQueue + +class OrderCoroutine(val threadCount: Int) { + private val taskList = ConcurrentLinkedQueue T>() + private val taskResultMap = ConcurrentHashMap() + private val finishTaskIndex = PriorityBlockingQueue() + private val mutex = Mutex() + + private suspend fun start() = coroutineScope { + var taskIndex = 0 + for (i in 1..threadCount) { + launch { + while (true) { + ensureActive() + val task: suspend CoroutineScope.() -> T + val curIndex: Int + mutex.withLock { + task = taskList.poll() ?: return@launch + curIndex = taskIndex++ + } + taskResultMap[curIndex] = task.invoke(this) + finishTaskIndex.add(curIndex) + } + } + } + } + + fun submit(block: suspend CoroutineScope.() -> T) { + taskList.add(block) + } + + suspend fun collect(block: (index: Int, result: T) -> Unit) = withContext(IO) { + var index = 0 + val taskSize = taskList.size + launch { start() } + while (index < taskSize) { + ensureActive() + if (finishTaskIndex.peek() == index) { + finishTaskIndex.poll() + block.invoke(index, taskResultMap.remove(index)!!) + index++ + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt index b63eb9e7e..2976801ed 100644 --- a/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt @@ -94,6 +94,7 @@ class CacheActivity : VMBaseActivity() menu.findItem(R.id.menu_export_no_chapter_name)?.isChecked = AppConfig.exportNoChapterName menu.findItem(R.id.menu_export_web_dav)?.isChecked = AppConfig.exportToWebDav menu.findItem(R.id.menu_export_pics_file)?.isChecked = AppConfig.exportPictureFile + menu.findItem(R.id.menu_parallel_export)?.isChecked = AppConfig.parallelExportBook menu.findItem(R.id.menu_export_type)?.title = "${getString(R.string.export_type)}(${getTypeName()})" menu.findItem(R.id.menu_export_charset)?.title = @@ -131,6 +132,7 @@ class CacheActivity : VMBaseActivity() R.id.menu_export_no_chapter_name -> AppConfig.exportNoChapterName = !item.isChecked R.id.menu_export_web_dav -> AppConfig.exportToWebDav = !item.isChecked R.id.menu_export_pics_file -> AppConfig.exportPictureFile = !item.isChecked + R.id.menu_parallel_export -> AppConfig.parallelExportBook = !item.isChecked R.id.menu_export_folder -> { selectExportFolder(-1) } diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt index 2abcc602d..4ea354390 100644 --- a/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt @@ -22,9 +22,9 @@ import io.legado.app.help.AppWebDav import io.legado.app.help.BookHelp import io.legado.app.help.ContentProcessor import io.legado.app.help.config.AppConfig +import io.legado.app.help.coroutine.OrderCoroutine import io.legado.app.utils.* -import kotlinx.coroutines.delay -import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import me.ag2s.epublib.domain.* @@ -104,10 +104,13 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename) ?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹") val stringBuilder = StringBuilder() + val exportToWebDav = AppConfig.exportToWebDav context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs -> getAllContents(book) { text, srcList -> bookOs.write(text.toByteArray(Charset.forName(AppConfig.exportCharset))) - stringBuilder.append(text) + if (exportToWebDav) { + stringBuilder.append(text) + } srcList?.forEach { val vFile = BookHelp.getImage(book, it.third) if (vFile.exists()) { @@ -133,9 +136,12 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { val bookPath = FileUtils.getPath(file, filename) val bookFile = FileUtils.createFileWithReplace(bookPath) val stringBuilder = StringBuilder() + val exportToWebDav = AppConfig.exportToWebDav getAllContents(book) { text, srcList -> bookFile.appendText(text, Charset.forName(AppConfig.exportCharset)) - stringBuilder.append(text) + if (exportToWebDav) { + stringBuilder.append(text) + } srcList?.forEach { val vFile = BookHelp.getImage(book, it.third) if (vFile.exists()) { @@ -171,37 +177,61 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { ) }" append(qy, null) - appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter -> - coroutineContext.ensureActive() - upAdapterLiveData.postValue(book.bookUrl) - exportProgress[book.bookUrl] = index - BookHelp.getContent(book, chapter).let { content -> - val content1 = contentProcessor - .getContent( - book, - chapter, - content ?: "null", - includeTitle = !AppConfig.exportNoChapterName, - useReplace = useReplace, - chineseConvert = false, - reSegment = false - ).joinToString("\n") - if (AppConfig.exportPictureFile) { - //txt导出图片文件 - val srcList = arrayListOf>() - content?.split("\n")?.forEachIndexed { index, text -> - val matcher = AppPattern.imgPattern.matcher(text) - while (matcher.find()) { - matcher.group(1)?.let { - val src = NetworkUtils.getAbsoluteURL(chapter.url, it) - srcList.add(Triple(chapter.title, index, src)) - } + if (AppConfig.parallelExportBook) { + val oc = + OrderCoroutine>?>>(AppConfig.threadCount) + appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter -> + oc.submit { getExportData(book, chapter, contentProcessor, useReplace) } + } + oc.collect { index, result -> + upAdapterLiveData.postValue(book.bookUrl) + exportProgress[book.bookUrl] = index + append.invoke(result.first, result.second) + } + } else { + appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter -> + coroutineContext.ensureActive() + upAdapterLiveData.postValue(book.bookUrl) + exportProgress[book.bookUrl] = index + val result = getExportData(book, chapter, contentProcessor, useReplace) + append.invoke(result.first, result.second) + } + } + + } + + private suspend fun getExportData( + book: Book, + chapter: BookChapter, + contentProcessor: ContentProcessor, + useReplace: Boolean + ): Pair>?> { + BookHelp.getContent(book, chapter).let { content -> + val content1 = contentProcessor + .getContent( + book, + chapter, + content ?: "null", + includeTitle = !AppConfig.exportNoChapterName, + useReplace = useReplace, + chineseConvert = false, + reSegment = false + ).joinToString("\n") + if (AppConfig.exportPictureFile) { + //txt导出图片文件 + val srcList = arrayListOf>() + content?.split("\n")?.forEachIndexed { index, text -> + val matcher = AppPattern.imgPattern.matcher(text) + while (matcher.find()) { + matcher.group(1)?.let { + val src = NetworkUtils.getAbsoluteURL(chapter.url, it) + srcList.add(Triple(chapter.title, index, src)) } } - append.invoke("\n\n$content1", srcList) - } else { - append.invoke("\n\n$content1", null) } + return Pair("\n\n$content1", srcList) + } else { + return Pair("\n\n$content1", null) } } } @@ -422,7 +452,10 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { .asBitmap() .load(book.getDisplayCover()) .into(object : CustomTarget() { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { + override fun onResourceReady( + resource: Bitmap, + transition: Transition? + ) { val stream = ByteArrayOutputStream() resource.compress(Bitmap.CompressFormat.JPEG, 100, stream) val byteArray: ByteArray = stream.toByteArray() @@ -491,8 +524,10 @@ class CacheViewModel(application: Application) : BaseViewModel(application) { while (matcher.find()) { matcher.group(1)?.let { val src = NetworkUtils.getAbsoluteURL(chapter.url, it) - val originalHref = "${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" - val href = "Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" + val originalHref = + "${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" + val href = + "Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" val vFile = BookHelp.getImage(book, src) val fp = FileResourceProvider(vFile.parent) if (vFile.exists()) { diff --git a/app/src/main/java/io/legado/app/utils/RegexExtensions.kt b/app/src/main/java/io/legado/app/utils/RegexExtensions.kt index 0918ae232..81fb6d8f2 100644 --- a/app/src/main/java/io/legado/app/utils/RegexExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/RegexExtensions.kt @@ -3,9 +3,9 @@ package io.legado.app.utils import androidx.core.os.postDelayed import io.legado.app.exception.RegexTimeoutException import io.legado.app.help.CrashHandler +import io.legado.app.help.coroutine.Coroutine import kotlinx.coroutines.suspendCancellableCoroutine import splitties.init.appCtx -import kotlin.concurrent.thread import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException @@ -16,7 +16,7 @@ import kotlin.coroutines.resumeWithException suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String { val charSequence = this return suspendCancellableCoroutine { block -> - val thread = thread { + val coroutine = Coroutine.async { try { val result = regex.replace(charSequence, replacement) block.resume(result) @@ -25,14 +25,14 @@ suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Lon } } mainHandler.postDelayed(timeout) { - if (thread.isAlive) { + if (coroutine.isActive) { val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}" val exception = RegexTimeoutException(timeoutMsg) block.cancel(exception) appCtx.longToastOnUi(timeoutMsg) CrashHandler.saveCrashInfo2File(exception) mainHandler.postDelayed(3000) { - if (thread.isAlive) { + if (coroutine.isActive) { appCtx.restart() } } diff --git a/app/src/main/res/menu/book_cache.xml b/app/src/main/res/menu/book_cache.xml index a60ffc19e..8e06e17b4 100644 --- a/app/src/main/res/menu/book_cache.xml +++ b/app/src/main/res/menu/book_cache.xml @@ -49,6 +49,12 @@ android:checkable="true" app:showAsAction="never" /> + + Decode Cover Js(coverDecodeJs) 网络为分组 本地未分组 + 多线程导出TXT diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 86659c2fd..19bdfc9ea 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -1038,4 +1038,5 @@ Decode Cover Js(coverDecodeJs) 网络为分组 本地未分组 + 多线程导出TXT diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 46004fcb8..739f68937 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1038,4 +1038,5 @@ Decode Cover Js(coverDecodeJs) 网络为分组 本地未分组 + 多线程导出TXT diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 6eaa0b020..d75c7eb10 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1035,4 +1035,5 @@ 封面解密(coverDecodeJs) 网络为分组 本地未分组 + 多线程导出TXT diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index af1a24b4d..b8d48808f 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1037,4 +1037,5 @@ 封面解密(coverDecodeJs) 网络为分组 本地未分组 + 多线程导出TXT diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index edfc0df15..8b4294128 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1037,4 +1037,5 @@ 封面解密(coverDecodeJs) 网络为分组 本地未分组 + 多线程导出TXT diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eb031f4b2..467606950 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1038,4 +1038,5 @@ Decode Cover Js(coverDecodeJs) 网络为分组 本地未分组 + 多线程导出TXT