优化TXT导出
This commit is contained in:
@@ -113,6 +113,7 @@ object PreferKey {
|
||||
const val showAddToShelfAlert = "showAddToShelfAlert"
|
||||
const val asyncLoadImage = "asyncLoadImage"
|
||||
const val ignoreAudioFocus = "ignoreAudioFocus"
|
||||
const val parallelExportBook = "parallelExportBook"
|
||||
|
||||
const val cPrimary = "colorPrimary"
|
||||
const val cAccent = "colorAccent"
|
||||
|
||||
@@ -81,33 +81,35 @@ class ContentProcessor private constructor(
|
||||
reSegment: Boolean = true
|
||||
): List<String> {
|
||||
var mContent = content
|
||||
//去除重复标题
|
||||
try {
|
||||
val name = Pattern.quote(book.name)
|
||||
val title = Pattern.quote(chapter.title)
|
||||
val titleRegex = "^(\\s|\\p{P}|${name})*${title}(\\s)*".toRegex()
|
||||
mContent = mContent.replace(titleRegex, "")
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("去除重复标题出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
if (reSegment && book.getReSegment()) {
|
||||
//重新分段
|
||||
mContent = ContentHelp.reSegment(mContent, chapter.title)
|
||||
}
|
||||
if (chineseConvert) {
|
||||
//简繁转换
|
||||
if (content != "null") {
|
||||
//去除重复标题
|
||||
try {
|
||||
when (AppConfig.chineseConverterType) {
|
||||
1 -> mContent = ChineseUtils.t2s(mContent)
|
||||
2 -> mContent = ChineseUtils.s2t(mContent)
|
||||
}
|
||||
val name = Pattern.quote(book.name)
|
||||
val title = Pattern.quote(chapter.title)
|
||||
val titleRegex = "^(\\s|\\p{P}|${name})*${title}(\\s)*".toRegex()
|
||||
mContent = mContent.replace(titleRegex, "")
|
||||
} catch (e: Exception) {
|
||||
appCtx.toastOnUi("简繁转换出错")
|
||||
AppLog.put("去除重复标题出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
if (reSegment && book.getReSegment()) {
|
||||
//重新分段
|
||||
mContent = ContentHelp.reSegment(mContent, chapter.title)
|
||||
}
|
||||
if (chineseConvert) {
|
||||
//简繁转换
|
||||
try {
|
||||
when (AppConfig.chineseConverterType) {
|
||||
1 -> mContent = ChineseUtils.t2s(mContent)
|
||||
2 -> mContent = ChineseUtils.s2t(mContent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
appCtx.toastOnUi("简繁转换出错")
|
||||
}
|
||||
}
|
||||
if (useReplace && book.getUseReplaceRule()) {
|
||||
//替换
|
||||
mContent = replaceContent(mContent)
|
||||
}
|
||||
}
|
||||
if (useReplace && book.getUseReplaceRule()) {
|
||||
//替换
|
||||
mContent = replaceContent(mContent)
|
||||
}
|
||||
if (includeTitle) {
|
||||
//重新添加标题
|
||||
|
||||
@@ -255,6 +255,12 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
appCtx.putPrefBoolean(PreferKey.exportPictureFile, value)
|
||||
}
|
||||
|
||||
var parallelExportBook: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.parallelExportBook, false)
|
||||
set(value) {
|
||||
appCtx.putPrefBoolean(PreferKey.parallelExportBook, value)
|
||||
}
|
||||
|
||||
var changeSourceCheckAuthor: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.changeSourceCheckAuthor)
|
||||
set(value) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.legado.app.help.coroutine
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.PriorityBlockingQueue
|
||||
|
||||
class OrderCoroutine<T>(val threadCount: Int) {
|
||||
private val taskList = ConcurrentLinkedQueue<suspend CoroutineScope.() -> T>()
|
||||
private val taskResultMap = ConcurrentHashMap<Int, T>()
|
||||
private val finishTaskIndex = PriorityBlockingQueue<Int>()
|
||||
private val mutex = Mutex()
|
||||
|
||||
private suspend fun start() = coroutineScope {
|
||||
var taskIndex = 0
|
||||
for (i in 1..threadCount) {
|
||||
launch {
|
||||
while (true) {
|
||||
ensureActive()
|
||||
val task: suspend CoroutineScope.() -> T
|
||||
val curIndex: Int
|
||||
mutex.withLock {
|
||||
task = taskList.poll() ?: return@launch
|
||||
curIndex = taskIndex++
|
||||
}
|
||||
taskResultMap[curIndex] = task.invoke(this)
|
||||
finishTaskIndex.add(curIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun submit(block: suspend CoroutineScope.() -> T) {
|
||||
taskList.add(block)
|
||||
}
|
||||
|
||||
suspend fun collect(block: (index: Int, result: T) -> Unit) = withContext(IO) {
|
||||
var index = 0
|
||||
val taskSize = taskList.size
|
||||
launch { start() }
|
||||
while (index < taskSize) {
|
||||
ensureActive()
|
||||
if (finishTaskIndex.peek() == index) {
|
||||
finishTaskIndex.poll()
|
||||
block.invoke(index, taskResultMap.remove(index)!!)
|
||||
index++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -94,6 +94,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
||||
menu.findItem(R.id.menu_export_no_chapter_name)?.isChecked = AppConfig.exportNoChapterName
|
||||
menu.findItem(R.id.menu_export_web_dav)?.isChecked = AppConfig.exportToWebDav
|
||||
menu.findItem(R.id.menu_export_pics_file)?.isChecked = AppConfig.exportPictureFile
|
||||
menu.findItem(R.id.menu_parallel_export)?.isChecked = AppConfig.parallelExportBook
|
||||
menu.findItem(R.id.menu_export_type)?.title =
|
||||
"${getString(R.string.export_type)}(${getTypeName()})"
|
||||
menu.findItem(R.id.menu_export_charset)?.title =
|
||||
@@ -131,6 +132,7 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
|
||||
R.id.menu_export_no_chapter_name -> AppConfig.exportNoChapterName = !item.isChecked
|
||||
R.id.menu_export_web_dav -> AppConfig.exportToWebDav = !item.isChecked
|
||||
R.id.menu_export_pics_file -> AppConfig.exportPictureFile = !item.isChecked
|
||||
R.id.menu_parallel_export -> AppConfig.parallelExportBook = !item.isChecked
|
||||
R.id.menu_export_folder -> {
|
||||
selectExportFolder(-1)
|
||||
}
|
||||
|
||||
+70
-35
@@ -22,9 +22,9 @@ import io.legado.app.help.AppWebDav
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.help.ContentProcessor
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.coroutine.OrderCoroutine
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import me.ag2s.epublib.domain.*
|
||||
@@ -104,10 +104,13 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
||||
val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
|
||||
?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹")
|
||||
val stringBuilder = StringBuilder()
|
||||
val exportToWebDav = AppConfig.exportToWebDav
|
||||
context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
||||
getAllContents(book) { text, srcList ->
|
||||
bookOs.write(text.toByteArray(Charset.forName(AppConfig.exportCharset)))
|
||||
stringBuilder.append(text)
|
||||
if (exportToWebDav) {
|
||||
stringBuilder.append(text)
|
||||
}
|
||||
srcList?.forEach {
|
||||
val vFile = BookHelp.getImage(book, it.third)
|
||||
if (vFile.exists()) {
|
||||
@@ -133,9 +136,12 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
||||
val bookPath = FileUtils.getPath(file, filename)
|
||||
val bookFile = FileUtils.createFileWithReplace(bookPath)
|
||||
val stringBuilder = StringBuilder()
|
||||
val exportToWebDav = AppConfig.exportToWebDav
|
||||
getAllContents(book) { text, srcList ->
|
||||
bookFile.appendText(text, Charset.forName(AppConfig.exportCharset))
|
||||
stringBuilder.append(text)
|
||||
if (exportToWebDav) {
|
||||
stringBuilder.append(text)
|
||||
}
|
||||
srcList?.forEach {
|
||||
val vFile = BookHelp.getImage(book, it.third)
|
||||
if (vFile.exists()) {
|
||||
@@ -171,37 +177,61 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
||||
)
|
||||
}"
|
||||
append(qy, null)
|
||||
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
|
||||
coroutineContext.ensureActive()
|
||||
upAdapterLiveData.postValue(book.bookUrl)
|
||||
exportProgress[book.bookUrl] = index
|
||||
BookHelp.getContent(book, chapter).let { content ->
|
||||
val content1 = contentProcessor
|
||||
.getContent(
|
||||
book,
|
||||
chapter,
|
||||
content ?: "null",
|
||||
includeTitle = !AppConfig.exportNoChapterName,
|
||||
useReplace = useReplace,
|
||||
chineseConvert = false,
|
||||
reSegment = false
|
||||
).joinToString("\n")
|
||||
if (AppConfig.exportPictureFile) {
|
||||
//txt导出图片文件
|
||||
val srcList = arrayListOf<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()
|
||||
.load(book.getDisplayCover())
|
||||
.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()
|
||||
resource.compress(Bitmap.CompressFormat.JPEG, 100, stream)
|
||||
val byteArray: ByteArray = stream.toByteArray()
|
||||
@@ -491,8 +524,10 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
|
||||
while (matcher.find()) {
|
||||
matcher.group(1)?.let {
|
||||
val src = NetworkUtils.getAbsoluteURL(chapter.url, it)
|
||||
val originalHref = "${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
|
||||
val href = "Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
|
||||
val originalHref =
|
||||
"${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
|
||||
val href =
|
||||
"Images/${MD5Utils.md5Encode16(src)}.${BookHelp.getImageSuffix(src)}"
|
||||
val vFile = BookHelp.getImage(book, src)
|
||||
val fp = FileResourceProvider(vFile.parent)
|
||||
if (vFile.exists()) {
|
||||
|
||||
@@ -3,9 +3,9 @@ package io.legado.app.utils
|
||||
import androidx.core.os.postDelayed
|
||||
import io.legado.app.exception.RegexTimeoutException
|
||||
import io.legado.app.help.CrashHandler
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import splitties.init.appCtx
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
@@ -16,7 +16,7 @@ import kotlin.coroutines.resumeWithException
|
||||
suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Long): String {
|
||||
val charSequence = this
|
||||
return suspendCancellableCoroutine { block ->
|
||||
val thread = thread {
|
||||
val coroutine = Coroutine.async {
|
||||
try {
|
||||
val result = regex.replace(charSequence, replacement)
|
||||
block.resume(result)
|
||||
@@ -25,14 +25,14 @@ suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Lon
|
||||
}
|
||||
}
|
||||
mainHandler.postDelayed(timeout) {
|
||||
if (thread.isAlive) {
|
||||
if (coroutine.isActive) {
|
||||
val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}"
|
||||
val exception = RegexTimeoutException(timeoutMsg)
|
||||
block.cancel(exception)
|
||||
appCtx.longToastOnUi(timeoutMsg)
|
||||
CrashHandler.saveCrashInfo2File(exception)
|
||||
mainHandler.postDelayed(3000) {
|
||||
if (thread.isAlive) {
|
||||
if (coroutine.isActive) {
|
||||
appCtx.restart()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user