Quellcode durchsuchen

优化TXT导出

Horis vor 4 Jahren
Ursprung
Commit
19af2f7258

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -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"

+ 26 - 24
app/src/main/java/io/legado/app/help/ContentProcessor.kt

@@ -81,33 +81,35 @@ class ContentProcessor private constructor(
         reSegment: Boolean = true
         reSegment: Boolean = true
     ): List<String> {
     ): List<String> {
         var mContent = content
         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 {
             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) {
             } 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) {
         if (includeTitle) {
             //重新添加标题
             //重新添加标题

+ 6 - 0
app/src/main/java/io/legado/app/help/config/AppConfig.kt

@@ -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) {

+ 54 - 0
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<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++
+            }
+        }
+    }
+
+}

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt

@@ -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)
             }
             }

+ 70 - 35
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.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.ensureActive
+import kotlinx.coroutines.*
 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)))
-                stringBuilder.append(text)
+                if (exportToWebDav) {
+                    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))
-            stringBuilder.append(text)
+            if (exportToWebDav) {
+                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,37 +177,61 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
             )
             )
         }"
         }"
         append(qy, null)
         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<Triple<String, Int, String>>()
-                    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<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 ->
+                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<String, ArrayList<Triple<String, Int, String>>?> {
+        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<Triple<String, Int, String>>()
+                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()
             .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 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 vFile = BookHelp.getImage(book, src)
                     val fp = FileResourceProvider(vFile.parent)
                     val fp = FileResourceProvider(vFile.parent)
                     if (vFile.exists()) {
                     if (vFile.exists()) {

+ 4 - 4
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 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()
                     }
                     }
                 }
                 }

+ 6 - 0
app/src/main/res/menu/book_cache.xml

@@ -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"

+ 1 - 0
app/src/main/res/values-es-rES/strings.xml

@@ -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>

+ 1 - 0
app/src/main/res/values-ja-rJP/strings.xml

@@ -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>

+ 1 - 0
app/src/main/res/values-pt-rBR/strings.xml

@@ -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>

+ 1 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -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>

+ 1 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -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>

+ 1 - 0
app/src/main/res/values-zh/strings.xml

@@ -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>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -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>