Horis 2 лет назад
Родитель
Сommit
cc9fbf4f8d
22 измененных файлов с 350 добавлено и 160 удалено
  1. 2 5
      app/src/main/java/io/legado/app/help/storage/Backup.kt
  2. 126 120
      app/src/main/java/io/legado/app/service/ExportBookService.kt
  3. 2 7
      app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt
  4. 10 8
      app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt
  5. 30 0
      app/src/main/java/io/legado/app/utils/RhinoExtensions.kt
  6. 10 10
      app/src/main/res/menu/book_cache.xml
  7. 1 1
      app/src/main/res/values-es-rES/strings.xml
  8. 1 1
      app/src/main/res/values-ja-rJP/strings.xml
  9. 1 1
      app/src/main/res/values-pt-rBR/strings.xml
  10. 1 1
      app/src/main/res/values-vi/strings.xml
  11. 1 1
      app/src/main/res/values-zh-rHK/strings.xml
  12. 1 1
      app/src/main/res/values-zh-rTW/strings.xml
  13. 1 1
      app/src/main/res/values-zh/strings.xml
  14. 1 1
      app/src/main/res/values/strings.xml
  15. 3 0
      modules/rhino1.7.3/build.gradle
  16. 4 0
      modules/rhino1.7.3/src/main/java/com/script/AbstractScriptEngine.kt
  17. 3 0
      modules/rhino1.7.3/src/main/java/com/script/CompiledScript.kt
  18. 6 0
      modules/rhino1.7.3/src/main/java/com/script/ScriptEngine.kt
  19. 25 0
      modules/rhino1.7.3/src/main/java/com/script/rhino/ContextElement.kt
  20. 48 0
      modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoCompiledScript.kt
  21. 53 2
      modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoScriptEngine.kt
  22. 20 0
      modules/rhino1.7.3/src/main/java/com/script/rhino/VMBridgeReflect.kt

+ 2 - 5
app/src/main/java/io/legado/app/help/storage/Backup.kt

@@ -20,7 +20,6 @@ import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.withContext
 import splitties.init.appCtx
-import java.io.BufferedOutputStream
 import java.io.File
 import java.io.FileInputStream
 import java.io.FileOutputStream
@@ -206,10 +205,8 @@ object Backup {
         withContext(IO) {
             if (list.isNotEmpty()) {
                 val file = FileUtils.createFileIfNotExist(path + File.separator + fileName)
-                FileOutputStream(file).use { fos ->
-                    BufferedOutputStream(fos, 64 * 1024).use {
-                        GSON.writeToOutputStream(it, list)
-                    }
+                file.outputStream().buffered().use {
+                    GSON.writeToOutputStream(it, list)
                 }
             }
         }

+ 126 - 120
app/src/main/java/io/legado/app/service/ExportBookService.kt

@@ -75,10 +75,8 @@ import me.ag2s.epublib.epub.EpubWriterProcessor
 import me.ag2s.epublib.util.ResourceUtil
 import splitties.init.appCtx
 import splitties.systemservices.notificationManager
-import java.io.BufferedOutputStream
 import java.io.ByteArrayOutputStream
 import java.io.File
-import java.io.FileOutputStream
 import java.nio.charset.Charset
 import java.util.concurrent.ConcurrentHashMap
 import kotlin.coroutines.coroutineContext
@@ -257,23 +255,21 @@ class ExportBookService : BaseService() {
         val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
             ?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹")
         val charset = Charset.forName(AppConfig.exportCharset)
-        contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
-            BufferedOutputStream(bookOs, 64 * 1024).use { bos ->
-                getAllContents(book) { text, srcList ->
-                    bos.write(text.toByteArray(charset))
-                    srcList?.forEach {
-                        val vFile = BookHelp.getImage(book, it.src)
-                        if (vFile.exists()) {
-                            DocumentUtils.createFileIfNotExist(
-                                doc,
-                                "${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg",
-                                subDirs = arrayOf(
-                                    "${book.name}_${book.author}",
-                                    "images",
-                                    it.chapterTitle
-                                )
-                            )?.writeBytes(this, vFile.readBytes())
-                        }
+        contentResolver.openOutputStream(bookDoc.uri, "wa")?.bufferedWriter(charset)?.use { bw ->
+            getAllContents(book) { text, srcList ->
+                bw.write(text)
+                srcList?.forEach {
+                    val vFile = BookHelp.getImage(book, it.src)
+                    if (vFile.exists()) {
+                        DocumentUtils.createFileIfNotExist(
+                            doc,
+                            "${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg",
+                            subDirs = arrayOf(
+                                "${book.name}_${book.author}",
+                                "images",
+                                it.chapterTitle
+                            )
+                        )?.writeBytes(this, vFile.readBytes())
                     }
                 }
             }
@@ -289,10 +285,10 @@ class ExportBookService : BaseService() {
         val bookPath = FileUtils.getPath(file, filename)
         val bookFile = FileUtils.createFileWithReplace(bookPath)
         val charset = Charset.forName(AppConfig.exportCharset)
-        val bos = BufferedOutputStream(bookFile.outputStream(true), 64 * 1024)
-        bos.use {
+        val bw = bookFile.outputStream(true).bufferedWriter(charset)
+        bw.use {
             getAllContents(book) { text, srcList ->
-                bos.write(text.toByteArray(charset))
+                it.write(text)
                 srcList?.forEach {
                     val vFile = BookHelp.getImage(book, it.src)
                     if (vFile.exists()) {
@@ -315,7 +311,7 @@ class ExportBookService : BaseService() {
     private suspend fun getAllContents(
         book: Book,
         append: (text: String, srcList: ArrayList<SrcData>?) -> Unit
-    ) {
+    ) = coroutineScope {
         val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
         val contentProcessor = ContentProcessor.get(book.name, book.origin)
         val qy = "${book.name}\n${
@@ -327,34 +323,27 @@ class ExportBookService : BaseService() {
             )
         }"
         append(qy, null)
-        if (AppConfig.parallelExportBook) {
-            coroutineScope {
-                flow {
-                    appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
-                        val task = async(Default, start = CoroutineStart.LAZY) {
-                            getExportData(book, chapter, contentProcessor, useReplace)
-                        }
-                        emit(task)
-                    }
-                }.onEach { it.start() }
-                    .buffer(AppConfig.threadCount)
-                    .map { it.await() }
-                    .withIndex()
-                    .collect { (index, result) ->
-                        postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
-                        exportProgress[book.bookUrl] = index
-                        append.invoke(result.first, result.second)
-                    }
-            }
+        val threads = if (AppConfig.parallelExportBook) {
+            AppConst.MAX_THREAD
         } else {
-            appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
-                coroutineContext.ensureActive()
+            0
+        }
+        flow {
+            appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
+                val task = async(Default, start = CoroutineStart.LAZY) {
+                    getExportData(book, chapter, contentProcessor, useReplace)
+                }
+                emit(task)
+            }
+        }.onEach { it.start() }
+            .buffer(threads)
+            .map { it.await() }
+            .withIndex()
+            .collect { (index, result) ->
                 postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
                 exportProgress[book.bookUrl] = index
-                val result = getExportData(book, chapter, contentProcessor, useReplace)
                 append.invoke(result.first, result.second)
             }
-        }
 
     }
 
@@ -364,34 +353,33 @@ class ExportBookService : BaseService() {
         contentProcessor: ContentProcessor,
         useReplace: Boolean
     ): Pair<String, ArrayList<SrcData>?> {
-        BookHelp.getContent(book, chapter).let { content ->
-            val content1 = contentProcessor
-                .getContent(
-                    book,
-                    // 不导出vip标识
-                    chapter.apply { isVip = false },
-                    content ?: if (chapter.isVolume) "" else "null",
-                    includeTitle = !AppConfig.exportNoChapterName,
-                    useReplace = useReplace,
-                    chineseConvert = false,
-                    reSegment = false
-                ).toString()
-            if (AppConfig.exportPictureFile) {
-                //txt导出图片文件
-                val srcList = arrayListOf<SrcData>()
-                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(SrcData(chapter.title, index, src))
-                        }
+        val content = BookHelp.getContent(book, chapter)
+        val content1 = contentProcessor
+            .getContent(
+                book,
+                // 不导出vip标识
+                chapter.apply { isVip = false },
+                content ?: if (chapter.isVolume) "" else "null",
+                includeTitle = !AppConfig.exportNoChapterName,
+                useReplace = useReplace,
+                chineseConvert = false,
+                reSegment = false
+            ).toString()
+        if (AppConfig.exportPictureFile) {
+            //txt导出图片文件
+            val srcList = arrayListOf<SrcData>()
+            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(SrcData(chapter.title, index, src))
                     }
                 }
-                return Pair("\n\n$content1", srcList)
-            } else {
-                return Pair("\n\n$content1", null)
             }
+            return Pair("\n\n$content1", srcList)
+        } else {
+            return Pair("\n\n$content1", null)
         }
     }
 
@@ -463,8 +451,8 @@ class ExportBookService : BaseService() {
         //设置正文
         setEpubContent(contentModel, book, epubBook)
         DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
-            contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
-                EpubWriter().write(epubBook, BufferedOutputStream(bookOs))
+            contentResolver.openOutputStream(bookDoc.uri, "wa")?.buffered().use { bookOs ->
+                EpubWriter().write(epubBook, bookOs)
             }
             if (AppConfig.exportToWebDav) {
                 // 导出到webdav
@@ -489,8 +477,7 @@ class ExportBookService : BaseService() {
         val bookFile = FileUtils.createFileWithReplace(bookPath)
         //设置正文
         setEpubContent(contentModel, book, epubBook)
-        @Suppress("BlockingMethodInNonBlockingContext")
-        EpubWriter().write(epubBook, BufferedOutputStream(FileOutputStream(bookFile)))
+        EpubWriter().write(epubBook, bookFile.outputStream().buffered())
         if (AppConfig.exportToWebDav) {
             // 导出到webdav
             AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
@@ -647,59 +634,79 @@ class ExportBookService : BaseService() {
         contentModel: String,
         book: Book,
         epubBook: EpubBook
-    ) {
+    ) = coroutineScope {
         //正文
         val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
         val contentProcessor = ContentProcessor.get(book.name, book.origin)
-        appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
-            coroutineContext.ensureActive()
-            postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
-            exportProgress[book.bookUrl] = index
-            BookHelp.getContent(book, chapter).let { content ->
-                var content1 = fixPic(
-                    epubBook,
-                    book,
-                    content ?: if (chapter.isVolume) "" else "null",
-                    chapter
-                )
-                content1 = contentProcessor
-                    .getContent(
+        val threads = if (AppConfig.parallelExportBook) {
+            AppConst.MAX_THREAD
+        } else {
+            0
+        }
+        flow {
+            appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
+                val task = async(Default, start = CoroutineStart.LAZY) {
+                    val content = BookHelp.getContent(book, chapter)
+                    val (contentFix, resources) = fixPic(
                         book,
-                        chapter,
-                        content1,
-                        includeTitle = false,
-                        useReplace = useReplace,
-                        chineseConvert = false,
-                        reSegment = false
-                    ).toString()
-                val title = chapter.run {
-                    // 不导出vip标识
-                    isVip = false
-                    getDisplayTitle(
-                        contentProcessor.getTitleReplaceRules(),
-                        useReplace = useReplace
+                        content ?: if (chapter.isVolume) "" else "null",
+                        chapter
                     )
-                }
-                epubBook.addSection(
-                    title,
-                    ResourceUtil.createChapterResource(
+                    // 不导出vip标识
+                    chapter.isVip = false
+                    val content1 = contentProcessor
+                        .getContent(
+                            book,
+                            chapter,
+                            contentFix,
+                            includeTitle = false,
+                            useReplace = useReplace,
+                            chineseConvert = false,
+                            reSegment = false
+                        ).toString()
+                    val title = chapter.run {
+                        // 不导出vip标识
+                        isVip = false
+                        getDisplayTitle(
+                            contentProcessor.getTitleReplaceRules(),
+                            useReplace = useReplace
+                        )
+                    }
+                    val chapterResource = ResourceUtil.createChapterResource(
                         title.replace("\uD83D\uDD12", ""),
                         content1,
                         contentModel,
                         "Text/chapter_${index}.html"
                     )
-                )
+                    ExportChapter(title, chapterResource, resources)
+                }
+                emit(task)
+            }
+        }.onEach { it.start() }
+            .buffer(threads)
+            .map { it.await() }
+            .withIndex()
+            .collect { (index, exportChapter) ->
+                postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
+                exportProgress[book.bookUrl] = index
+                epubBook.resources.addAll(exportChapter.resources)
+                epubBook.addSection(exportChapter.title, exportChapter.chapterResource)
             }
-        }
     }
 
+    data class ExportChapter(
+        val title: String,
+        val chapterResource: Resource,
+        val resources: ArrayList<Resource>
+    )
+
     private fun fixPic(
-        epubBook: EpubBook,
         book: Book,
         content: String,
         chapter: BookChapter
-    ): String {
+    ): Pair<String, ArrayList<Resource>> {
         val data = StringBuilder("")
+        val resources = arrayListOf<Resource>()
         content.split("\n").forEach { text ->
             var text1 = text
             val matcher = AppPattern.imgPattern.matcher(text)
@@ -714,14 +721,14 @@ class ExportBookService : BaseService() {
                     val fp = FileResourceProvider(vFile.parent)
                     if (vFile.exists()) {
                         val img = LazyResource(fp, href, originalHref)
-                        epubBook.resources.add(img)
+                        resources.add(img)
                     }
                     text1 = text1.replace(src, "../${href}")
                 }
             }
             data.append(text1).append("\n")
         }
-        return data.toString()
+        return data.toString() to resources
     }
 
     private fun setEpubMetadata(book: Book, epubBook: EpubBook) {
@@ -875,17 +882,17 @@ class ExportBookService : BaseService() {
                 coroutineContext.ensureActive()
                 updateProgress(chapterList, index)
                 BookHelp.getContent(book, chapter).let { content ->
-                    var content1 = fixPic(
-                        epubBook,
+                    val (contentFix, resources) = fixPic(
                         book,
                         content ?: if (chapter.isVolume) "" else "null",
                         chapter
                     )
-                    content1 = contentProcessor
+                    epubBook.resources.addAll(resources)
+                    val content1 = contentProcessor
                         .getContent(
                             book,
                             chapter,
-                            content1,
+                            contentFix,
                             includeTitle = false,
                             useReplace = useReplace,
                             chineseConvert = false,
@@ -960,14 +967,14 @@ class ExportBookService : BaseService() {
             callback: (total: Int, progress: Int) -> Unit
         ) {
             DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
-                contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
+                contentResolver.openOutputStream(bookDoc.uri, "wa")?.buffered().use { bookOs ->
                     EpubWriter()
                         .setCallback(object : EpubWriterProcessor.Callback {
                             override fun onProgressing(total: Int, progress: Int) {
                                 callback(total, progress)
                             }
                         })
-                        .write(epubBook, BufferedOutputStream(bookOs))
+                        .write(epubBook, bookOs)
                 }
                 if (AppConfig.exportToWebDav) {
                     // 导出到webdav
@@ -987,14 +994,13 @@ class ExportBookService : BaseService() {
         ) {
             val bookPath = FileUtils.getPath(file, filename)
             val bookFile = FileUtils.createFileWithReplace(bookPath)
-            @Suppress("BlockingMethodInNonBlockingContext")
             EpubWriter()
                 .setCallback(object : EpubWriterProcessor.Callback {
                     override fun onProgressing(total: Int, progress: Int) {
                         callback(total, progress)
                     }
                 })
-                .write(epubBook, BufferedOutputStream(FileOutputStream(bookFile)))
+                .write(epubBook, bookFile.outputStream().buffered())
             if (AppConfig.exportToWebDav) {
                 // 导出到webdav
                 AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)

+ 2 - 7
app/src/main/java/io/legado/app/ui/book/manage/BookshelfManageViewModel.kt

@@ -22,9 +22,7 @@ import io.legado.app.utils.stackTraceStr
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.writeToOutputStream
 import kotlinx.coroutines.delay
-import java.io.BufferedOutputStream
 import java.io.File
-import java.io.FileOutputStream
 
 
 class BookshelfManageViewModel(application: Application) : BaseViewModel(application) {
@@ -64,17 +62,14 @@ class BookshelfManageViewModel(application: Application) : BaseViewModel(applica
         }
     }
 
-    @Suppress("BlockingMethodInNonBlockingContext")
     fun saveAllUseBookSourceToFile(success: (file: File) -> Unit) {
         execute {
             val path = "${context.filesDir}/shareBookSource.json"
             FileUtils.delete(path)
             val file = FileUtils.createFileWithReplace(path)
             val sources = appDb.bookDao.getAllUseBookSource()
-            FileOutputStream(file).use { out ->
-                BufferedOutputStream(out, 64 * 1024).use {
-                    GSON.writeToOutputStream(it, sources)
-                }
+            file.outputStream().buffered().use {
+                GSON.writeToOutputStream(it, sources)
             }
             file
         }.onSuccess {

+ 10 - 8
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt

@@ -9,11 +9,16 @@ import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.data.entities.toBookSource
 import io.legado.app.help.config.SourceConfig
-import io.legado.app.utils.*
+import io.legado.app.utils.FileUtils
+import io.legado.app.utils.GSON
+import io.legado.app.utils.cnCompare
+import io.legado.app.utils.outputStream
+import io.legado.app.utils.splitNotBlank
+import io.legado.app.utils.stackTraceStr
+import io.legado.app.utils.toastOnUi
+import io.legado.app.utils.writeToOutputStream
 import splitties.init.appCtx
-import java.io.BufferedOutputStream
 import java.io.File
-import java.io.FileOutputStream
 
 /**
  * 书源管理数据修改
@@ -121,16 +126,13 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
         }
     }
 
-    @Suppress("BlockingMethodInNonBlockingContext")
     private fun saveToFile(sources: List<BookSource>, success: (file: File) -> Unit) {
         execute {
             val path = "${context.filesDir}/shareBookSource.json"
             FileUtils.delete(path)
             val file = FileUtils.createFileWithReplace(path)
-            FileOutputStream(file).use { out ->
-                BufferedOutputStream(out, 64 * 1024).use {
-                    GSON.writeToOutputStream(it, sources)
-                }
+            file.outputStream().buffered().use {
+                GSON.writeToOutputStream(it, sources)
             }
             file
         }.onSuccess {

+ 30 - 0
app/src/main/java/io/legado/app/utils/RhinoExtensions.kt

@@ -0,0 +1,30 @@
+package io.legado.app.utils
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.supervisorScope
+import org.mozilla.javascript.Context
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
+
+@OptIn(ExperimentalContracts::class)
+inline fun <T> suspendContinuation(crossinline block: suspend CoroutineScope.() -> T): T {
+    contract {
+        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+    }
+    val cx = Context.enter()
+    try {
+        val pending = cx.captureContinuation()
+        pending.applicationState = suspend {
+            supervisorScope {
+                block()
+            }
+        }
+        throw pending
+    } catch (e: IllegalStateException) {
+        return runBlocking { block() }
+    } finally {
+        Context.exit()
+    }
+}

+ 10 - 10
app/src/main/res/menu/book_cache.xml

@@ -1,19 +1,19 @@
 <?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:tools="http://schemas.android.com/tools"
-    xmlns:android="http://schemas.android.com/apk/res/android"
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
     tools:ignore="AlwaysShowAction">
 
     <item
         android:id="@+id/menu_download"
-        android:title="@string/action_download"
         android:icon="@drawable/ic_play_24dp"
+        android:title="@string/action_download"
         app:showAsAction="always" />
 
     <item
         android:id="@+id/menu_book_group"
-        android:title="@string/group"
         android:icon="@drawable/ic_groups"
+        android:title="@string/group"
         app:showAsAction="always">
 
         <menu />
@@ -27,39 +27,39 @@
 
     <item
         android:id="@+id/menu_enable_replace"
-        android:title="@string/replace_purify"
         android:checkable="true"
+        android:title="@string/replace_purify"
         app:showAsAction="never" />
 
     <!--自定义导出章节 选择菜单-->
     <item
         android:id="@+id/menu_enable_custom_export"
-        android:title="@string/custom_export_section"
         android:checkable="true"
+        android:title="@string/custom_export_section"
         app:showAsAction="never" />
 
     <item
         android:id="@+id/menu_export_web_dav"
-        android:title="@string/export_to_web_dav"
         android:checkable="true"
+        android:title="@string/export_to_web_dav"
         app:showAsAction="never" />
 
     <item
         android:id="@+id/menu_export_no_chapter_name"
-        android:title="@string/export_no_chapter_name"
         android:checkable="true"
+        android:title="@string/export_no_chapter_name"
         app:showAsAction="never" />
 
     <item
         android:id="@+id/menu_export_pics_file"
-        android:title="@string/export_pics_file"
         android:checkable="true"
+        android:title="@string/export_pics_file"
         app:showAsAction="never" />
 
     <item
         android:id="@+id/menu_parallel_export"
-        android:title="@string/parallel_export_book"
         android:checkable="true"
+        android:title="@string/parallel_export_book"
         app:showAsAction="never" />
 
     <item

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

@@ -1038,7 +1038,7 @@
     <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
     <string name="net_no_group">网络未分组</string>
     <string name="local_no_group">本地未分组</string>
-    <string name="parallel_export_book">多线程导出TXT</string>
+    <string name="parallel_export_book">多线程导出</string>
     <string name="progress_bar_behavior">进度条行为</string>
     <string name="source_edit_text_max_line">源编辑框最大行数</string>
     <string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

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

@@ -1041,7 +1041,7 @@
     <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
     <string name="net_no_group">网络未分组</string>
     <string name="local_no_group">本地未分组</string>
-    <string name="parallel_export_book">多线程导出TXT</string>
+    <string name="parallel_export_book">多线程导出</string>
     <string name="progress_bar_behavior">进度条行为</string>
     <string name="source_edit_text_max_line">源编辑框最大行数</string>
     <string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

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

@@ -1041,7 +1041,7 @@
     <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
     <string name="net_no_group">网络未分组</string>
     <string name="local_no_group">本地未分组</string>
-    <string name="parallel_export_book">多线程导出TXT</string>
+    <string name="parallel_export_book">多线程导出</string>
     <string name="progress_bar_behavior">进度条行为</string>
     <string name="source_edit_text_max_line">源编辑框最大行数</string>
     <string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

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

@@ -1037,7 +1037,7 @@ Còn </string>
     <string name="cover_decode_js">Giải mã Bìa Js(coverDecodeJs)</string>
     <string name="net_no_group">Đã tách nhóm mạng</string>
     <string name="local_no_group">Đã tách nhóm cục bộ</string>
-    <string name="parallel_export_book">XT xuất đa luồng</string>
+    <string name="parallel_export_book">Xuất đa luồng</string>
     <string name="progress_bar_behavior">Hành vi của thanh tiến trình</string>
     <string name="source_edit_text_max_line">Số hàng tối đa trong hộp chỉnh sửa nguồn</string>
     <string name="source_edit_max_line_summary">%s,Đặt số hàng ít hơn số hàng tối đa có thể hiển thị trên màn hình giúp trượt sang các trường khác để chỉnh sửa dễ dàng hơn.</string>

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

@@ -1038,7 +1038,7 @@
     <string name="cover_decode_js">封面解密(coverDecodeJs)</string>
     <string name="net_no_group">网络未分组</string>
     <string name="local_no_group">本地未分组</string>
-    <string name="parallel_export_book">多线程导出TXT</string>
+    <string name="parallel_export_book">多线程导出</string>
     <string name="progress_bar_behavior">进度条行为</string>
     <string name="source_edit_text_max_line">源编辑框最大行数</string>
     <string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

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

@@ -1040,7 +1040,7 @@
     <string name="cover_decode_js">封面解密(coverDecodeJs)</string>
     <string name="net_no_group">網路未分組</string>
     <string name="local_no_group">本機未分組</string>
-    <string name="parallel_export_book">多執行緒匯出TXT</string>
+    <string name="parallel_export_book">多執行緒匯出</string>
     <string name="progress_bar_behavior">進度條行為</string>
     <string name="source_edit_text_max_line">源編輯框最大行數</string>
     <string name="source_edit_max_line_summary">%s,設定行數小於螢幕可顯示的最大行數可以更方便的滑動到其他的欄位進行編輯</string>

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

@@ -1041,7 +1041,7 @@
     <string name="cover_decode_js">封面解密(coverDecodeJs)</string>
     <string name="net_no_group">网络未分组</string>
     <string name="local_no_group">本地未分组</string>
-    <string name="parallel_export_book">多线程导出TXT</string>
+    <string name="parallel_export_book">多线程导出</string>
     <string name="progress_bar_behavior">进度条行为</string>
     <string name="source_edit_text_max_line">源编辑框最大行数</string>
     <string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

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

@@ -1041,7 +1041,7 @@
     <string name="cover_decode_js">Decode Cover Js(coverDecodeJs)</string>
     <string name="net_no_group">Network ungrouped</string>
     <string name="local_no_group">Local ungrouped</string>
-    <string name="parallel_export_book">Multithreaded export TXT</string>
+    <string name="parallel_export_book">Multithreaded export</string>
     <string name="progress_bar_behavior">Progress bar behavior</string>
     <string name="source_edit_text_max_line">Maximum number of rows in the source edit box</string>
     <string name="source_edit_max_line_summary">%s,Setting the number of rows less than the maximum number of rows that can be displayed on the screen makes it easier to slide to other fields for editing.</string>

+ 3 - 0
modules/rhino1.7.3/build.gradle

@@ -32,4 +32,7 @@ android {
 
 dependencies {
     api(fileTree(dir: 'lib', include: ['rhino-1.7.13-2.jar']))
+
+    def coroutines_version = '1.7.3'
+    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version")
 }

+ 4 - 0
modules/rhino1.7.3/src/main/java/com/script/AbstractScriptEngine.kt

@@ -51,6 +51,10 @@ abstract class AbstractScriptEngine(val bindings: Bindings? = null) : ScriptEngi
         return getBindings(100)?.get(key)
     }
 
+    override suspend fun evalSuspend(script: String, scope: Scriptable): Any? {
+        return this.evalSuspend(StringReader(script), scope)
+    }
+
     override fun eval(script: String, scope: Scriptable): Any? {
         return this.eval(StringReader(script), scope)
     }

+ 3 - 0
modules/rhino1.7.3/src/main/java/com/script/CompiledScript.kt

@@ -15,6 +15,9 @@ abstract class CompiledScript {
     @Throws(ScriptException::class)
     abstract fun eval(scope: Scriptable): Any?
 
+    @Throws(ScriptException::class)
+    abstract suspend fun evalSuspend(scope: Scriptable): Any?
+
     @Throws(ScriptException::class)
     fun eval(bindings: Bindings?): Any? {
         var ctxt = getEngine().context

+ 6 - 0
modules/rhino1.7.3/src/main/java/com/script/ScriptEngine.kt

@@ -14,6 +14,12 @@ interface ScriptEngine {
     @Throws(ScriptException::class)
     fun eval(reader: Reader, scope: Scriptable): Any?
 
+    @Throws(ScriptException::class)
+    suspend fun evalSuspend(reader: Reader, scope: Scriptable): Any?
+
+    @Throws(ScriptException::class)
+    suspend fun evalSuspend(script: String, scope: Scriptable): Any?
+
     @Throws(ScriptException::class)
     fun eval(script: String, scope: Scriptable): Any?
 

+ 25 - 0
modules/rhino1.7.3/src/main/java/com/script/rhino/ContextElement.kt

@@ -0,0 +1,25 @@
+package com.script.rhino
+
+import kotlinx.coroutines.ThreadContextElement
+import kotlin.coroutines.CoroutineContext
+
+class ContextElement : ThreadContextElement<Any?> {
+
+    companion object Key : CoroutineContext.Key<ContextElement>
+
+    override val key: CoroutineContext.Key<ContextElement>
+        get() = Key
+
+    private val contextHelper: Any? = VMBridgeReflect.contextLocal.get()
+
+    override fun updateThreadContext(context: CoroutineContext): Any? {
+        val oldState = VMBridgeReflect.contextLocal.get()
+        VMBridgeReflect.contextLocal.set(contextHelper)
+        return oldState
+    }
+
+    override fun restoreThreadContext(context: CoroutineContext, oldState: Any?) {
+        VMBridgeReflect.contextLocal.set(oldState)
+    }
+
+}

+ 48 - 0
modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoCompiledScript.kt

@@ -28,7 +28,11 @@ import com.script.CompiledScript
 import com.script.ScriptContext
 import com.script.ScriptEngine
 import com.script.ScriptException
+import kotlinx.coroutines.withContext
 import org.mozilla.javascript.*
+import java.io.IOException
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
 
 /**
  * Represents compiled JavaScript code.
@@ -91,4 +95,48 @@ internal class RhinoCompiledScript(
         return result
     }
 
+    override suspend fun evalSuspend(scope: Scriptable): Any? {
+        val cx = Context.enter()
+        var ret: Any?
+        withContext(ContextElement()) {
+            try {
+                try {
+                    ret = cx.executeScriptWithContinuations(script, scope)
+                } catch (e: ContinuationPending) {
+                    var pending = e
+                    while (true) {
+                        try {
+                            @Suppress("UNCHECKED_CAST")
+                            val suspendFunction =
+                                pending.applicationState as Function1<Continuation<Any?>, Any?>
+                            val functionResult = suspendCoroutineUninterceptedOrReturn { cout ->
+                                suspendFunction.invoke(cout)
+                            }
+                            val continuation = pending.continuation
+                            ret = cx.resumeContinuation(continuation, scope, functionResult)
+                            break
+                        } catch (e: ContinuationPending) {
+                            pending = e
+                        }
+                    }
+                }
+            } catch (re: RhinoException) {
+                val line = if (re.lineNumber() == 0) -1 else re.lineNumber()
+                val msg: String = if (re is JavaScriptException) {
+                    re.value.toString()
+                } else {
+                    re.toString()
+                }
+                val se = ScriptException(msg, re.sourceName(), line)
+                se.initCause(re)
+                throw se
+            } catch (var14: IOException) {
+                throw ScriptException(var14)
+            } finally {
+                Context.exit()
+            }
+        }
+        return engine.unwrapReturnValue(ret)
+    }
+
 }

+ 53 - 2
modules/rhino1.7.3/src/main/java/com/script/rhino/RhinoScriptEngine.kt

@@ -25,6 +25,7 @@
 package com.script.rhino
 
 import com.script.*
+import kotlinx.coroutines.withContext
 import org.mozilla.javascript.*
 import org.mozilla.javascript.Function
 import java.io.IOException
@@ -32,6 +33,8 @@ import java.io.Reader
 import java.io.StringReader
 import java.lang.reflect.Method
 import java.security.*
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
 
 /**
  * Implementation of `ScriptEngine` using the Mozilla Rhino
@@ -80,6 +83,54 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
         return unwrapReturnValue(ret)
     }
 
+    @Throws(ContinuationPending::class)
+    override suspend fun evalSuspend(reader: Reader, scope: Scriptable): Any? {
+        val cx = Context.enter()
+        var ret: Any?
+        withContext(ContextElement()) {
+            try {
+                var filename = this@RhinoScriptEngine["javax.script.filename"] as? String
+                filename = filename ?: "<Unknown source>"
+                val script = cx.compileReader(reader, filename, 1, null)
+                try {
+                    ret = cx.executeScriptWithContinuations(script, scope)
+                } catch (e: ContinuationPending) {
+                    var pending = e
+                    while (true) {
+                        try {
+                            @Suppress("UNCHECKED_CAST")
+                            val suspendFunction =
+                                pending.applicationState as Function1<Continuation<Any?>, Any?>
+                            val functionResult = suspendCoroutineUninterceptedOrReturn { cout ->
+                                suspendFunction.invoke(cout)
+                            }
+                            val continuation = pending.continuation
+                            ret = cx.resumeContinuation(continuation, scope, functionResult)
+                            break
+                        } catch (e: ContinuationPending) {
+                            pending = e
+                        }
+                    }
+                }
+            } catch (re: RhinoException) {
+                val line = if (re.lineNumber() == 0) -1 else re.lineNumber()
+                val msg: String = if (re is JavaScriptException) {
+                    re.value.toString()
+                } else {
+                    re.toString()
+                }
+                val se = ScriptException(msg, re.sourceName(), line)
+                se.initCause(re)
+                throw se
+            } catch (var14: IOException) {
+                throw ScriptException(var14)
+            } finally {
+                Context.exit()
+            }
+        }
+        return unwrapReturnValue(ret)
+    }
+
     override fun createBindings(): Bindings {
         return SimpleBindings()
     }
@@ -227,7 +278,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
                 callable: Callable,
                 cx: Context,
                 scope: Scriptable,
-                thisObj: Scriptable,
+                thisObj: Scriptable?,
                 args: Array<Any>
             ): Any? {
                 var accContext: AccessControlContext? = null
@@ -253,7 +304,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
                 callable: Callable,
                 cx: Context,
                 scope: Scriptable,
-                thisObj: Scriptable,
+                thisObj: Scriptable?,
                 args: Array<Any>
             ): Any? {
                 return super.doTopCall(callable, cx, scope, thisObj, args)

+ 20 - 0
modules/rhino1.7.3/src/main/java/com/script/rhino/VMBridgeReflect.kt

@@ -0,0 +1,20 @@
+package com.script.rhino
+
+import org.mozilla.javascript.VMBridge
+
+object VMBridgeReflect {
+
+    val instance: VMBridge by lazy {
+        VMBridge::class.java.getDeclaredField("instance").apply {
+            isAccessible = true
+        }.get(null) as VMBridge
+    }
+
+    val contextLocal: ThreadLocal<Any> by lazy {
+        @Suppress("UNCHECKED_CAST")
+        instance::class.java.getDeclaredField("contextLocal").apply {
+            isAccessible = true
+        }.get(null) as ThreadLocal<Any>
+    }
+
+}