优化TXT导出

pull/2337/head
Horis 2 years ago
parent 332b951535
commit ac95d46058
  1. 1
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  2. 2
      app/src/main/java/io/legado/app/help/ContentProcessor.kt
  3. 6
      app/src/main/java/io/legado/app/help/config/AppConfig.kt
  4. 54
      app/src/main/java/io/legado/app/help/coroutine/OrderCoroutine.kt
  5. 2
      app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt
  6. 51
      app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt
  7. 8
      app/src/main/java/io/legado/app/utils/RegexExtensions.kt
  8. 6
      app/src/main/res/menu/book_cache.xml
  9. 1
      app/src/main/res/values-es-rES/strings.xml
  10. 1
      app/src/main/res/values-ja-rJP/strings.xml
  11. 1
      app/src/main/res/values-pt-rBR/strings.xml
  12. 1
      app/src/main/res/values-zh-rHK/strings.xml
  13. 1
      app/src/main/res/values-zh-rTW/strings.xml
  14. 1
      app/src/main/res/values-zh/strings.xml
  15. 1
      app/src/main/res/values/strings.xml

@ -113,6 +113,7 @@ object PreferKey {
const val showAddToShelfAlert = "showAddToShelfAlert" const val showAddToShelfAlert = "showAddToShelfAlert"
const val asyncLoadImage = "asyncLoadImage" const val asyncLoadImage = "asyncLoadImage"
const val ignoreAudioFocus = "ignoreAudioFocus" const val ignoreAudioFocus = "ignoreAudioFocus"
const val parallelExportBook = "parallelExportBook"
const val cPrimary = "colorPrimary" const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent" const val cAccent = "colorAccent"

@ -81,6 +81,7 @@ class ContentProcessor private constructor(
reSegment: Boolean = true reSegment: Boolean = true
): List<String> { ): List<String> {
var mContent = content var mContent = content
if (content != "null") {
//去除重复标题 //去除重复标题
try { try {
val name = Pattern.quote(book.name) val name = Pattern.quote(book.name)
@ -109,6 +110,7 @@ class ContentProcessor private constructor(
//替换 //替换
mContent = replaceContent(mContent) mContent = replaceContent(mContent)
} }
}
if (includeTitle) { if (includeTitle) {
//重新添加标题 //重新添加标题
mContent = chapter.getDisplayTitle( mContent = chapter.getDisplayTitle(

@ -255,6 +255,12 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
appCtx.putPrefBoolean(PreferKey.exportPictureFile, value) appCtx.putPrefBoolean(PreferKey.exportPictureFile, value)
} }
var parallelExportBook: Boolean
get() = appCtx.getPrefBoolean(PreferKey.parallelExportBook, false)
set(value) {
appCtx.putPrefBoolean(PreferKey.parallelExportBook, value)
}
var changeSourceCheckAuthor: Boolean var changeSourceCheckAuthor: Boolean
get() = appCtx.getPrefBoolean(PreferKey.changeSourceCheckAuthor) get() = appCtx.getPrefBoolean(PreferKey.changeSourceCheckAuthor)
set(value) { set(value) {

@ -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<T>(val threadCount: Int) {
private val taskList = ConcurrentLinkedQueue<suspend CoroutineScope.() -> T>()
private val taskResultMap = ConcurrentHashMap<Int, T>()
private val finishTaskIndex = PriorityBlockingQueue<Int>()
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++
}
}
}
}

@ -94,6 +94,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
menu.findItem(R.id.menu_export_no_chapter_name)?.isChecked = AppConfig.exportNoChapterName 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_web_dav)?.isChecked = AppConfig.exportToWebDav
menu.findItem(R.id.menu_export_pics_file)?.isChecked = AppConfig.exportPictureFile 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 = menu.findItem(R.id.menu_export_type)?.title =
"${getString(R.string.export_type)}(${getTypeName()})" "${getString(R.string.export_type)}(${getTypeName()})"
menu.findItem(R.id.menu_export_charset)?.title = menu.findItem(R.id.menu_export_charset)?.title =
@ -131,6 +132,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
R.id.menu_export_no_chapter_name -> AppConfig.exportNoChapterName = !item.isChecked 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_web_dav -> AppConfig.exportToWebDav = !item.isChecked
R.id.menu_export_pics_file -> AppConfig.exportPictureFile = !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 -> { R.id.menu_export_folder -> {
selectExportFolder(-1) selectExportFolder(-1)
} }

@ -22,9 +22,9 @@ import io.legado.app.help.AppWebDav
import io.legado.app.help.BookHelp import io.legado.app.help.BookHelp
import io.legado.app.help.ContentProcessor import io.legado.app.help.ContentProcessor
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.coroutine.OrderCoroutine
import io.legado.app.utils.* import io.legado.app.utils.*
import kotlinx.coroutines.delay import kotlinx.coroutines.*
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import me.ag2s.epublib.domain.* import me.ag2s.epublib.domain.*
@ -104,10 +104,13 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename) val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹") ?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹")
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
val exportToWebDav = AppConfig.exportToWebDav
context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs -> context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
getAllContents(book) { text, srcList -> getAllContents(book) { text, srcList ->
bookOs.write(text.toByteArray(Charset.forName(AppConfig.exportCharset))) bookOs.write(text.toByteArray(Charset.forName(AppConfig.exportCharset)))
if (exportToWebDav) {
stringBuilder.append(text) stringBuilder.append(text)
}
srcList?.forEach { srcList?.forEach {
val vFile = BookHelp.getImage(book, it.third) val vFile = BookHelp.getImage(book, it.third)
if (vFile.exists()) { if (vFile.exists()) {
@ -133,9 +136,12 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
val bookPath = FileUtils.getPath(file, filename) val bookPath = FileUtils.getPath(file, filename)
val bookFile = FileUtils.createFileWithReplace(bookPath) val bookFile = FileUtils.createFileWithReplace(bookPath)
val stringBuilder = StringBuilder() val stringBuilder = StringBuilder()
val exportToWebDav = AppConfig.exportToWebDav
getAllContents(book) { text, srcList -> getAllContents(book) { text, srcList ->
bookFile.appendText(text, Charset.forName(AppConfig.exportCharset)) bookFile.appendText(text, Charset.forName(AppConfig.exportCharset))
if (exportToWebDav) {
stringBuilder.append(text) stringBuilder.append(text)
}
srcList?.forEach { srcList?.forEach {
val vFile = BookHelp.getImage(book, it.third) val vFile = BookHelp.getImage(book, it.third)
if (vFile.exists()) { if (vFile.exists()) {
@ -171,10 +177,35 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
) )
}" }"
append(qy, null) append(qy, null)
if (AppConfig.parallelExportBook) {
val oc =
OrderCoroutine<Pair<String, ArrayList<Triple<String, Int, String>>?>>(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 -> appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
coroutineContext.ensureActive() coroutineContext.ensureActive()
upAdapterLiveData.postValue(book.bookUrl) upAdapterLiveData.postValue(book.bookUrl)
exportProgress[book.bookUrl] = index 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<String, ArrayList<Triple<String, Int, String>>?> {
BookHelp.getContent(book, chapter).let { content -> BookHelp.getContent(book, chapter).let { content ->
val content1 = contentProcessor val content1 = contentProcessor
.getContent( .getContent(
@ -198,10 +229,9 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
} }
} }
} }
append.invoke("\n\n$content1", srcList) return Pair("\n\n$content1", srcList)
} else { } else {
append.invoke("\n\n$content1", null) return Pair("\n\n$content1", null)
}
} }
} }
} }
@ -422,7 +452,10 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
.asBitmap() .asBitmap()
.load(book.getDisplayCover()) .load(book.getDisplayCover())
.into(object : CustomTarget<Bitmap>() { .into(object : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) { override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
val stream = ByteArrayOutputStream() val stream = ByteArrayOutputStream()
resource.compress(Bitmap.CompressFormat.JPEG, 100, stream) resource.compress(Bitmap.CompressFormat.JPEG, 100, stream)
val byteArray: ByteArray = stream.toByteArray() val byteArray: ByteArray = stream.toByteArray()
@ -491,8 +524,10 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
while (matcher.find()) { while (matcher.find()) {
matcher.group(1)?.let { matcher.group(1)?.let {
val src = NetworkUtils.getAbsoluteURL(chapter.url, it) val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
val originalHref = "${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" val originalHref =
val href = "Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}" "${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
val href =
"Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
val vFile = BookHelp.getImage(book, src) val vFile = BookHelp.getImage(book, src)
val fp = FileResourceProvider(vFile.parent) val fp = FileResourceProvider(vFile.parent)
if (vFile.exists()) { if (vFile.exists()) {

@ -3,9 +3,9 @@ package io.legado.app.utils
import androidx.core.os.postDelayed import androidx.core.os.postDelayed
import io.legado.app.exception.RegexTimeoutException import io.legado.app.exception.RegexTimeoutException
import io.legado.app.help.CrashHandler import io.legado.app.help.CrashHandler
import io.legado.app.help.coroutine.Coroutine
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import splitties.init.appCtx import splitties.init.appCtx
import kotlin.concurrent.thread
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@ -16,7 +16,7 @@ import kotlin.coroutines.resumeWithException
suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String { suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String {
val charSequence = this val charSequence = this
return suspendCancellableCoroutine { block -> return suspendCancellableCoroutine { block ->
val thread = thread { val coroutine = Coroutine.async {
try { try {
val result = regex.replace(charSequence, replacement) val result = regex.replace(charSequence, replacement)
block.resume(result) block.resume(result)
@ -25,14 +25,14 @@ suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Lon
} }
} }
mainHandler.postDelayed(timeout) { mainHandler.postDelayed(timeout) {
if (thread.isAlive) { if (coroutine.isActive) {
val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}" val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}"
val exception = RegexTimeoutException(timeoutMsg) val exception = RegexTimeoutException(timeoutMsg)
block.cancel(exception) block.cancel(exception)
appCtx.longToastOnUi(timeoutMsg) appCtx.longToastOnUi(timeoutMsg)
CrashHandler.saveCrashInfo2File(exception) CrashHandler.saveCrashInfo2File(exception)
mainHandler.postDelayed(3000) { mainHandler.postDelayed(3000) {
if (thread.isAlive) { if (coroutine.isActive) {
appCtx.restart() appCtx.restart()
} }
} }

@ -49,6 +49,12 @@
android:checkable="true" android:checkable="true"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/menu_parallel_export"
android:title="@string/parallel_export_book"
android:checkable="true"
app:showAsAction="never" />
<item <item
android:id="@+id/menu_export_folder" android:id="@+id/menu_export_folder"
android:title="@string/export_folder" android:title="@string/export_folder"

@ -1035,4 +1035,5 @@
<string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string> <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
<string name="net_no_group">网络为分组</string> <string name="net_no_group">网络为分组</string>
<string name="local_no_group">本地未分组</string> <string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
</resources> </resources>

@ -1038,4 +1038,5 @@
<string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string> <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
<string name="net_no_group">网络为分组</string> <string name="net_no_group">网络为分组</string>
<string name="local_no_group">本地未分组</string> <string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
</resources> </resources>

@ -1038,4 +1038,5 @@
<string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string> <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
<string name="net_no_group">网络为分组</string> <string name="net_no_group">网络为分组</string>
<string name="local_no_group">本地未分组</string> <string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
</resources> </resources>

@ -1035,4 +1035,5 @@
<string name="cover_decode_js">封面解密(coverDecodeJs)</string> <string name="cover_decode_js">封面解密(coverDecodeJs)</string>
<string name="net_no_group">网络为分组</string> <string name="net_no_group">网络为分组</string>
<string name="local_no_group">本地未分组</string> <string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
</resources> </resources>

@ -1037,4 +1037,5 @@
<string name="cover_decode_js">封面解密(coverDecodeJs)</string> <string name="cover_decode_js">封面解密(coverDecodeJs)</string>
<string name="net_no_group">网络为分组</string> <string name="net_no_group">网络为分组</string>
<string name="local_no_group">本地未分组</string> <string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
</resources> </resources>

@ -1037,4 +1037,5 @@
<string name="cover_decode_js">封面解密(coverDecodeJs)</string> <string name="cover_decode_js">封面解密(coverDecodeJs)</string>
<string name="net_no_group">网络为分组</string> <string name="net_no_group">网络为分组</string>
<string name="local_no_group">本地未分组</string> <string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
</resources> </resources>

@ -1038,4 +1038,5 @@
<string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string> <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
<string name="net_no_group">网络为分组</string> <string name="net_no_group">网络为分组</string>
<string name="local_no_group">本地未分组</string> <string name="local_no_group">本地未分组</string>
<string name="parallel_export_book">多线程导出TXT</string>
</resources> </resources>

Loading…
Cancel
Save