This commit is contained in:
Horis 2024-01-28 17:09:25 +08:00
parent b9d6616ca6
commit e3aa72b315
22 changed files with 350 additions and 160 deletions

View File

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

View File

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

View File

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

View File

@ -9,11 +9,16 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.toBookSource import io.legado.app.data.entities.toBookSource
import io.legado.app.help.config.SourceConfig 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 splitties.init.appCtx
import java.io.BufferedOutputStream
import java.io.File 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) { private fun saveToFile(sources: List<BookSource>, success: (file: File) -> Unit) {
execute { execute {
val path = "${context.filesDir}/shareBookSource.json" val path = "${context.filesDir}/shareBookSource.json"
FileUtils.delete(path) FileUtils.delete(path)
val file = FileUtils.createFileWithReplace(path) val file = FileUtils.createFileWithReplace(path)
FileOutputStream(file).use { out -> file.outputStream().buffered().use {
BufferedOutputStream(out, 64 * 1024).use { GSON.writeToOutputStream(it, sources)
GSON.writeToOutputStream(it, sources)
}
} }
file file
}.onSuccess { }.onSuccess {

View File

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

View File

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

View File

@ -1038,7 +1038,7 @@
<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> <string name="parallel_export_book">多线程导出</string>
<string name="progress_bar_behavior">进度条行为</string> <string name="progress_bar_behavior">进度条行为</string>
<string name="source_edit_text_max_line">源编辑框最大行数</string> <string name="source_edit_text_max_line">源编辑框最大行数</string>
<string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string> <string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

View File

@ -1041,7 +1041,7 @@
<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> <string name="parallel_export_book">多线程导出</string>
<string name="progress_bar_behavior">进度条行为</string> <string name="progress_bar_behavior">进度条行为</string>
<string name="source_edit_text_max_line">源编辑框最大行数</string> <string name="source_edit_text_max_line">源编辑框最大行数</string>
<string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string> <string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

View File

@ -1041,7 +1041,7 @@
<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> <string name="parallel_export_book">多线程导出</string>
<string name="progress_bar_behavior">进度条行为</string> <string name="progress_bar_behavior">进度条行为</string>
<string name="source_edit_text_max_line">源编辑框最大行数</string> <string name="source_edit_text_max_line">源编辑框最大行数</string>
<string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string> <string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

View File

@ -1037,7 +1037,7 @@ Còn </string>
<string name="cover_decode_js">Giải mã Bìa Js(coverDecodeJs)</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="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="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="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_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> <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>

View File

@ -1038,7 +1038,7 @@
<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> <string name="parallel_export_book">多线程导出</string>
<string name="progress_bar_behavior">进度条行为</string> <string name="progress_bar_behavior">进度条行为</string>
<string name="source_edit_text_max_line">源编辑框最大行数</string> <string name="source_edit_text_max_line">源编辑框最大行数</string>
<string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string> <string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

View File

@ -1040,7 +1040,7 @@
<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> <string name="parallel_export_book">多執行緒匯出</string>
<string name="progress_bar_behavior">進度條行為</string> <string name="progress_bar_behavior">進度條行為</string>
<string name="source_edit_text_max_line">源編輯框最大行數</string> <string name="source_edit_text_max_line">源編輯框最大行數</string>
<string name="source_edit_max_line_summary">%s設定行數小於螢幕可顯示的最大行數可以更方便的滑動到其他的欄位進行編輯</string> <string name="source_edit_max_line_summary">%s設定行數小於螢幕可顯示的最大行數可以更方便的滑動到其他的欄位進行編輯</string>

View File

@ -1041,7 +1041,7 @@
<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> <string name="parallel_export_book">多线程导出</string>
<string name="progress_bar_behavior">进度条行为</string> <string name="progress_bar_behavior">进度条行为</string>
<string name="source_edit_text_max_line">源编辑框最大行数</string> <string name="source_edit_text_max_line">源编辑框最大行数</string>
<string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string> <string name="source_edit_max_line_summary">%s,设置行数小于屏幕可显示的最大行数可以更方便的滑动到其他的字段进行编辑</string>

View File

@ -1041,7 +1041,7 @@
<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">Network ungrouped</string> <string name="net_no_group">Network ungrouped</string>
<string name="local_no_group">Local 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="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_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> <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>

View File

@ -32,4 +32,7 @@ android {
dependencies { dependencies {
api(fileTree(dir: 'lib', include: ['rhino-1.7.13-2.jar'])) 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")
} }

View File

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

View File

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

View File

@ -14,6 +14,12 @@ interface ScriptEngine {
@Throws(ScriptException::class) @Throws(ScriptException::class)
fun eval(reader: Reader, scope: Scriptable): Any? 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) @Throws(ScriptException::class)
fun eval(script: String, scope: Scriptable): Any? fun eval(script: String, scope: Scriptable): Any?

View File

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

View File

@ -28,7 +28,11 @@ import com.script.CompiledScript
import com.script.ScriptContext import com.script.ScriptContext
import com.script.ScriptEngine import com.script.ScriptEngine
import com.script.ScriptException import com.script.ScriptException
import kotlinx.coroutines.withContext
import org.mozilla.javascript.* import org.mozilla.javascript.*
import java.io.IOException
import kotlin.coroutines.Continuation
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
/** /**
* Represents compiled JavaScript code. * Represents compiled JavaScript code.
@ -91,4 +95,48 @@ internal class RhinoCompiledScript(
return result 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)
}
} }

View File

@ -25,6 +25,7 @@
package com.script.rhino package com.script.rhino
import com.script.* import com.script.*
import kotlinx.coroutines.withContext
import org.mozilla.javascript.* import org.mozilla.javascript.*
import org.mozilla.javascript.Function import org.mozilla.javascript.Function
import java.io.IOException import java.io.IOException
@ -32,6 +33,8 @@ import java.io.Reader
import java.io.StringReader import java.io.StringReader
import java.lang.reflect.Method import java.lang.reflect.Method
import java.security.* import java.security.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
/** /**
* Implementation of `ScriptEngine` using the Mozilla Rhino * Implementation of `ScriptEngine` using the Mozilla Rhino
@ -80,6 +83,54 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
return unwrapReturnValue(ret) 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 { override fun createBindings(): Bindings {
return SimpleBindings() return SimpleBindings()
} }
@ -227,7 +278,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
callable: Callable, callable: Callable,
cx: Context, cx: Context,
scope: Scriptable, scope: Scriptable,
thisObj: Scriptable, thisObj: Scriptable?,
args: Array<Any> args: Array<Any>
): Any? { ): Any? {
var accContext: AccessControlContext? = null var accContext: AccessControlContext? = null
@ -253,7 +304,7 @@ object RhinoScriptEngine : AbstractScriptEngine(), Invocable, Compilable {
callable: Callable, callable: Callable,
cx: Context, cx: Context,
scope: Scriptable, scope: Scriptable,
thisObj: Scriptable, thisObj: Scriptable?,
args: Array<Any> args: Array<Any>
): Any? { ): Any? {
return super.doTopCall(callable, cx, scope, thisObj, args) return super.doTopCall(callable, cx, scope, thisObj, args)

View File

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