优化
This commit is contained in:
@@ -12,7 +12,6 @@ import io.legado.app.utils.isAbsUrl
|
|||||||
import io.legado.app.utils.isContentScheme
|
import io.legado.app.utils.isContentScheme
|
||||||
import io.legado.app.utils.isDataUrl
|
import io.legado.app.utils.isDataUrl
|
||||||
import io.legado.app.utils.lifecycle
|
import io.legado.app.utils.lifecycle
|
||||||
import splitties.init.appCtx
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
//https://bumptech.github.io/glide/doc/generatedapi.html
|
//https://bumptech.github.io/glide/doc/generatedapi.html
|
||||||
@@ -37,8 +36,8 @@ object ImageLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load(lifecycle: Lifecycle, path: String?): RequestBuilder<Drawable> {
|
fun load(context: Context, lifecycle: Lifecycle, path: String?): RequestBuilder<Drawable> {
|
||||||
val requestManager = Glide.with(appCtx).lifecycle(lifecycle)
|
val requestManager = Glide.with(context).lifecycle(lifecycle)
|
||||||
return when {
|
return when {
|
||||||
path.isNullOrEmpty() -> requestManager.load(path)
|
path.isNullOrEmpty() -> requestManager.load(path)
|
||||||
path.isDataUrl() -> requestManager.load(path)
|
path.isDataUrl() -> requestManager.load(path)
|
||||||
|
|||||||
@@ -2,12 +2,7 @@ package io.legado.app.service
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.ArraySet
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
@@ -31,7 +26,6 @@ import io.legado.app.help.book.isLocalModified
|
|||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.model.localBook.LocalBook
|
import io.legado.app.model.localBook.LocalBook
|
||||||
import io.legado.app.ui.book.cache.CacheActivity
|
import io.legado.app.ui.book.cache.CacheActivity
|
||||||
import io.legado.app.utils.DocumentUtils
|
|
||||||
import io.legado.app.utils.FileDoc
|
import io.legado.app.utils.FileDoc
|
||||||
import io.legado.app.utils.FileUtils
|
import io.legado.app.utils.FileUtils
|
||||||
import io.legado.app.utils.HtmlFormatter
|
import io.legado.app.utils.HtmlFormatter
|
||||||
@@ -39,21 +33,19 @@ import io.legado.app.utils.MD5Utils
|
|||||||
import io.legado.app.utils.NetworkUtils
|
import io.legado.app.utils.NetworkUtils
|
||||||
import io.legado.app.utils.activityPendingIntent
|
import io.legado.app.utils.activityPendingIntent
|
||||||
import io.legado.app.utils.cnCompare
|
import io.legado.app.utils.cnCompare
|
||||||
import io.legado.app.utils.createFolderIfNotExist
|
import io.legado.app.utils.createFileIfNotExist
|
||||||
|
import io.legado.app.utils.delete
|
||||||
import io.legado.app.utils.find
|
import io.legado.app.utils.find
|
||||||
import io.legado.app.utils.isContentScheme
|
|
||||||
import io.legado.app.utils.list
|
import io.legado.app.utils.list
|
||||||
import io.legado.app.utils.mapAsync
|
import io.legado.app.utils.mapAsync
|
||||||
import io.legado.app.utils.mapAsyncIndexed
|
import io.legado.app.utils.mapAsyncIndexed
|
||||||
import io.legado.app.utils.outputStream
|
import io.legado.app.utils.openOutputStream
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
import io.legado.app.utils.servicePendingIntent
|
import io.legado.app.utils.servicePendingIntent
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import io.legado.app.utils.writeBytes
|
import io.legado.app.utils.writeFile
|
||||||
import kotlinx.coroutines.Deferred
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.async
|
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
import kotlinx.coroutines.flow.collectIndexed
|
import kotlinx.coroutines.flow.collectIndexed
|
||||||
@@ -65,6 +57,7 @@ import me.ag2s.epublib.domain.Date
|
|||||||
import me.ag2s.epublib.domain.EpubBook
|
import me.ag2s.epublib.domain.EpubBook
|
||||||
import me.ag2s.epublib.domain.FileResourceProvider
|
import me.ag2s.epublib.domain.FileResourceProvider
|
||||||
import me.ag2s.epublib.domain.LazyResource
|
import me.ag2s.epublib.domain.LazyResource
|
||||||
|
import me.ag2s.epublib.domain.LazyResourceProvider
|
||||||
import me.ag2s.epublib.domain.Metadata
|
import me.ag2s.epublib.domain.Metadata
|
||||||
import me.ag2s.epublib.domain.Resource
|
import me.ag2s.epublib.domain.Resource
|
||||||
import me.ag2s.epublib.domain.TOCReference
|
import me.ag2s.epublib.domain.TOCReference
|
||||||
@@ -73,8 +66,6 @@ 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.ByteArrayOutputStream
|
|
||||||
import java.io.File
|
|
||||||
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
|
||||||
@@ -197,15 +188,15 @@ class ExportBookService : BaseService() {
|
|||||||
upExportNotification()
|
upExportNotification()
|
||||||
if (exportConfig.type == "epub") {
|
if (exportConfig.type == "epub") {
|
||||||
if (exportConfig.epubScope.isNullOrBlank()) {
|
if (exportConfig.epubScope.isNullOrBlank()) {
|
||||||
exportEPUB(exportConfig.path, book)
|
exportEpub(exportConfig.path, book)
|
||||||
} else {
|
} else {
|
||||||
CustomExporter(
|
CustomExporter(
|
||||||
paresScope(exportConfig.epubScope),
|
exportConfig.epubScope,
|
||||||
exportConfig.epubSize
|
exportConfig.epubSize
|
||||||
).export(exportConfig.path, book)
|
).export(exportConfig.path, book)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
export(exportConfig.path, book)
|
exportTxt(exportConfig.path, book)
|
||||||
}
|
}
|
||||||
exportMsg[book.bookUrl] = getString(R.string.export_success)
|
exportMsg[book.bookUrl] = getString(R.string.export_success)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
@@ -239,40 +230,33 @@ class ExportBookService : BaseService() {
|
|||||||
val src: String
|
val src: String
|
||||||
)
|
)
|
||||||
|
|
||||||
private suspend fun export(path: String, book: Book) {
|
private suspend fun exportTxt(path: String, book: Book) {
|
||||||
exportMsg.remove(book.bookUrl)
|
exportMsg.remove(book.bookUrl)
|
||||||
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
if (path.isContentScheme()) {
|
val fileDoc = FileDoc.fromDir(path)
|
||||||
val uri = Uri.parse(path)
|
exportTxt(fileDoc, book)
|
||||||
val doc = DocumentFile.fromTreeUri(this@ExportBookService, uri)
|
|
||||||
?: throw NoStackTraceException("获取导出文档失败")
|
|
||||||
export(doc, book)
|
|
||||||
} else {
|
|
||||||
export(File(path).createFolderIfNotExist(), book)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun export(doc: DocumentFile, book: Book) {
|
private suspend fun exportTxt(fileDoc: FileDoc, book: Book) {
|
||||||
val filename = book.getExportFileName("txt")
|
val filename = book.getExportFileName("txt")
|
||||||
DocumentUtils.delete(doc, filename)
|
fileDoc.find(filename)?.delete()
|
||||||
val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
|
|
||||||
?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹")
|
val bookDoc = fileDoc.createFileIfNotExist(filename)
|
||||||
val charset = Charset.forName(AppConfig.exportCharset)
|
val charset = Charset.forName(AppConfig.exportCharset)
|
||||||
contentResolver.openOutputStream(bookDoc.uri, "wa")?.bufferedWriter(charset)?.use { bw ->
|
bookDoc.openOutputStream().getOrThrow().bufferedWriter(charset).use { bw ->
|
||||||
getAllContents(book) { text, srcList ->
|
getAllContents(book) { text, srcList ->
|
||||||
bw.write(text)
|
bw.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()) {
|
||||||
DocumentUtils.createFileIfNotExist(
|
fileDoc.createFileIfNotExist(
|
||||||
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())
|
).writeFile(vFile)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,34 +267,6 @@ class ExportBookService : BaseService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun export(file: File, book: Book) {
|
|
||||||
val filename = book.getExportFileName("txt")
|
|
||||||
val bookPath = FileUtils.getPath(file, filename)
|
|
||||||
val bookFile = FileUtils.createFileWithReplace(bookPath)
|
|
||||||
val charset = Charset.forName(AppConfig.exportCharset)
|
|
||||||
val bw = bookFile.outputStream(true).bufferedWriter(charset)
|
|
||||||
bw.use {
|
|
||||||
getAllContents(book) { text, srcList ->
|
|
||||||
it.write(text)
|
|
||||||
srcList?.forEach {
|
|
||||||
val vFile = BookHelp.getImage(book, it.src)
|
|
||||||
if (vFile.exists()) {
|
|
||||||
FileUtils.createFileIfNotExist(
|
|
||||||
file,
|
|
||||||
"${book.name}_${book.author}",
|
|
||||||
"images",
|
|
||||||
it.chapterTitle,
|
|
||||||
"${it.index}-${MD5Utils.md5Encode16(it.src)}.jpg"
|
|
||||||
).writeBytes(vFile.readBytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (AppConfig.exportToWebDav) {
|
|
||||||
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename) // 导出到webdav
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
@@ -381,62 +337,20 @@ class ExportBookService : BaseService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析范围字符串
|
|
||||||
*
|
|
||||||
* @param scope 范围字符串
|
|
||||||
* @return 范围
|
|
||||||
*
|
|
||||||
* @since 2023/5/22
|
|
||||||
* @author Discut
|
|
||||||
*/
|
|
||||||
@SuppressLint("ObsoleteSdkInt")
|
|
||||||
private fun paresScope(scope: String): Set<Int> {
|
|
||||||
val split = scope.split(",")
|
|
||||||
|
|
||||||
@Suppress("RemoveExplicitTypeArguments")
|
|
||||||
val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
ArraySet<Int>()
|
|
||||||
} else {
|
|
||||||
HashSet<Int>()
|
|
||||||
}
|
|
||||||
for (s in split) {
|
|
||||||
val v = s.split("-")
|
|
||||||
if (v.size != 2) {
|
|
||||||
result.add(s.toInt() - 1)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val left = v[0].toInt()
|
|
||||||
val right = v[1].toInt()
|
|
||||||
if (left > right) {
|
|
||||||
AppLog.put("Error expression : $s; left > right")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for (i in left..right)
|
|
||||||
result.add(i - 1)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出Epub
|
* 导出Epub
|
||||||
*/
|
*/
|
||||||
private suspend fun exportEPUB(path: String, book: Book) {
|
private suspend fun exportEpub(path: String, book: Book) {
|
||||||
exportMsg.remove(book.bookUrl)
|
exportMsg.remove(book.bookUrl)
|
||||||
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
if (path.isContentScheme()) {
|
val fileDoc = FileDoc.fromDir(path)
|
||||||
val uri = Uri.parse(path)
|
exportEpub(fileDoc, book)
|
||||||
val doc = DocumentFile.fromTreeUri(this@ExportBookService, uri)
|
|
||||||
?: throw NoStackTraceException("获取导出文档失败")
|
|
||||||
exportEpub(doc, book)
|
|
||||||
} else {
|
|
||||||
exportEpub(File(path).createFolderIfNotExist(), book)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun exportEpub(doc: DocumentFile, book: Book) {
|
private suspend fun exportEpub(fileDoc: FileDoc, book: Book) {
|
||||||
val filename = book.getExportFileName("epub")
|
val filename = book.getExportFileName("epub")
|
||||||
DocumentUtils.delete(doc, filename)
|
fileDoc.find(filename)?.delete()
|
||||||
|
|
||||||
val epubBook = EpubBook()
|
val epubBook = EpubBook()
|
||||||
epubBook.version = "2.0"
|
epubBook.version = "2.0"
|
||||||
//set metadata
|
//set metadata
|
||||||
@@ -444,54 +358,22 @@ class ExportBookService : BaseService() {
|
|||||||
//set cover
|
//set cover
|
||||||
setCover(book, epubBook)
|
setCover(book, epubBook)
|
||||||
//set css
|
//set css
|
||||||
val contentModel = setAssets(doc, book, epubBook)
|
val contentModel = setAssets(fileDoc, book, epubBook)
|
||||||
|
|
||||||
//设置正文
|
//设置正文
|
||||||
setEpubContent(contentModel, book, epubBook)
|
setEpubContent(contentModel, book, epubBook)
|
||||||
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
|
||||||
contentResolver.openOutputStream(bookDoc.uri, "wa")?.buffered().use { bookOs ->
|
val bookDoc = fileDoc.createFileIfNotExist(filename)
|
||||||
EpubWriter().write(epubBook, bookOs)
|
bookDoc.openOutputStream().getOrThrow().buffered().use { bookOs ->
|
||||||
}
|
EpubWriter().write(epubBook, bookOs)
|
||||||
if (AppConfig.exportToWebDav) {
|
|
||||||
// 导出到webdav
|
|
||||||
AppWebDav.exportWebDav(bookDoc.uri, filename)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private suspend fun exportEpub(file: File, book: Book) {
|
|
||||||
val filename = book.getExportFileName("epub")
|
|
||||||
val epubBook = EpubBook()
|
|
||||||
epubBook.version = "2.0"
|
|
||||||
//set metadata
|
|
||||||
setEpubMetadata(book, epubBook)
|
|
||||||
//set cover
|
|
||||||
setCover(book, epubBook)
|
|
||||||
//set css
|
|
||||||
val contentModel = setAssets(file, book, epubBook)
|
|
||||||
|
|
||||||
val bookPath = FileUtils.getPath(file, filename)
|
|
||||||
val bookFile = FileUtils.createFileWithReplace(bookPath)
|
|
||||||
//设置正文
|
|
||||||
setEpubContent(contentModel, book, epubBook)
|
|
||||||
bookFile.outputStream().buffered().use {
|
|
||||||
EpubWriter().write(epubBook, it)
|
|
||||||
}
|
|
||||||
if (AppConfig.exportToWebDav) {
|
if (AppConfig.exportToWebDav) {
|
||||||
// 导出到webdav
|
// 导出到webdav
|
||||||
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
|
AppWebDav.exportWebDav(bookDoc.uri, filename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setAssets(doc: DocumentFile, book: Book, epubBook: EpubBook): String {
|
|
||||||
return setAssets(FileDoc.fromDocumentFile(doc), book, epubBook)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAssets(file: File, book: Book, epubBook: EpubBook): String {
|
|
||||||
return setAssets(FileDoc.fromFile(file), book, epubBook)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAssets(doc: FileDoc, book: Book, epubBook: EpubBook): String {
|
private fun setAssets(doc: FileDoc, book: Book, epubBook: EpubBook): String {
|
||||||
val customPath = doc.find("Asset")
|
val customPath = doc.find("Asset")
|
||||||
val contentModel = if (customPath == null) {//使用内置模板
|
val contentModel = if (customPath == null) {//使用内置模板
|
||||||
@@ -618,16 +500,15 @@ class ExportBookService : BaseService() {
|
|||||||
|
|
||||||
private fun setCover(book: Book, epubBook: EpubBook) {
|
private fun setCover(book: Book, epubBook: EpubBook) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val bitmap = Glide.with(this)
|
val file = Glide.with(this)
|
||||||
.asBitmap()
|
.asFile()
|
||||||
.load(book.getDisplayCover())
|
.load(book.getDisplayCover())
|
||||||
.submit()
|
.submit()
|
||||||
.get()
|
.get()
|
||||||
val byteArray = ByteArrayOutputStream().use {
|
val provider = LazyResourceProvider { _ ->
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
|
file.inputStream()
|
||||||
it.toByteArray()
|
|
||||||
}
|
}
|
||||||
epubBook.coverImage = Resource(byteArray, "Images/cover.jpg")
|
epubBook.coverImage = LazyResource(provider, "Images/cover.jpg")
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
AppLog.put("获取书籍封面出错\n${it.localizedMessage}", it)
|
AppLog.put("获取书籍封面出错\n${it.localizedMessage}", it)
|
||||||
}
|
}
|
||||||
@@ -758,7 +639,9 @@ class ExportBookService : BaseService() {
|
|||||||
* @param scope 导出范围
|
* @param scope 导出范围
|
||||||
* @param size epub 文件包含最大章节数
|
* @param size epub 文件包含最大章节数
|
||||||
*/
|
*/
|
||||||
inner class CustomExporter(private var scope: Set<Int>, private val size: Int) {
|
inner class CustomExporter(scopeStr: String, private val size: Int) {
|
||||||
|
|
||||||
|
private var scope = parseScope(scopeStr)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出Epub
|
* 导出Epub
|
||||||
@@ -775,78 +658,35 @@ class ExportBookService : BaseService() {
|
|||||||
val currentTimeMillis = System.currentTimeMillis()
|
val currentTimeMillis = System.currentTimeMillis()
|
||||||
val count = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
val count = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
||||||
scope = scope.filter { it < count }.toHashSet()
|
scope = scope.filter { it < count }.toHashSet()
|
||||||
when (path.isContentScheme()) {
|
|
||||||
true -> {
|
|
||||||
val uri = Uri.parse(path)
|
|
||||||
val doc = DocumentFile.fromTreeUri(this@ExportBookService, uri)
|
|
||||||
?: throw NoStackTraceException("获取导出文档失败")
|
|
||||||
val (contentModel, epubList) = createEpubs(book, doc)
|
|
||||||
val asyncBlocks = ArrayList<Deferred<Unit>>(epubList.size)
|
|
||||||
var progressBar = 0.0
|
|
||||||
epubList.forEachIndexed { index, ep ->
|
|
||||||
val (filename, epubBook) = ep
|
|
||||||
coroutineScope {
|
|
||||||
val asyncBlock = async {
|
|
||||||
//设置正文
|
|
||||||
setEpubContent(
|
|
||||||
contentModel,
|
|
||||||
book,
|
|
||||||
epubBook,
|
|
||||||
index
|
|
||||||
) { _, _ ->
|
|
||||||
// 将章节写入内存时更新进度条
|
|
||||||
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
|
||||||
progressBar += book.totalChapterNum.toDouble() / scope.size / 2
|
|
||||||
exportProgress[book.bookUrl] = progressBar.toInt()
|
|
||||||
}
|
|
||||||
save2Drive(filename, epubBook, doc) { total, _ ->
|
|
||||||
//写入硬盘时更新进度条
|
|
||||||
progressBar += book.totalChapterNum.toDouble() / epubList.size / total / 2
|
|
||||||
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
|
||||||
exportProgress[book.bookUrl] = progressBar.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
asyncBlocks.add(asyncBlock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
asyncBlocks.forEach { it.await() }
|
|
||||||
}
|
|
||||||
|
|
||||||
false -> {
|
val fileDoc = FileDoc.fromDir(path)
|
||||||
val file = File(path).createFolderIfNotExist()
|
|
||||||
val (contentModel, epubList) = createEpubs(book, null)
|
val (contentModel, epubList) = createEpubs(book, fileDoc)
|
||||||
val asyncBlocks = ArrayList<Deferred<Unit>>(epubList.size)
|
var progressBar = 0.0
|
||||||
var progressBar = 0.0
|
epubList.forEachIndexed { index, ep ->
|
||||||
epubList.forEachIndexed { index, ep ->
|
val (filename, epubBook) = ep
|
||||||
val (filename, epubBook) = ep
|
//设置正文
|
||||||
coroutineScope {
|
setEpubContent(
|
||||||
val asyncBlock = async {
|
contentModel,
|
||||||
//设置正文
|
book,
|
||||||
setEpubContent(
|
epubBook,
|
||||||
contentModel,
|
index
|
||||||
book,
|
) { _, _ ->
|
||||||
epubBook,
|
// 将章节写入内存时更新进度条
|
||||||
index
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
) { _, _ ->
|
progressBar += book.totalChapterNum.toDouble() / scope.size / 2
|
||||||
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
exportProgress[book.bookUrl] = progressBar.toInt()
|
||||||
exportProgress[book.bookUrl] =
|
}
|
||||||
exportProgress[book.bookUrl]?.plus(book.totalChapterNum / scope.size)
|
save2Drive(filename, epubBook, fileDoc) { total, _ ->
|
||||||
?: 1
|
//写入硬盘时更新进度条
|
||||||
}
|
progressBar += book.totalChapterNum.toDouble() / epubList.size / total / 2
|
||||||
save2Drive(filename, epubBook, file) { total, _ ->
|
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
||||||
//设置进度
|
exportProgress[book.bookUrl] = progressBar.toInt()
|
||||||
progressBar += book.totalChapterNum.toDouble() / epubList.size / total / 2
|
|
||||||
postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
|
||||||
exportProgress[book.bookUrl] = progressBar.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
asyncBlocks.add(asyncBlock)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
asyncBlocks.forEach { it.await() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppLog.put("分割导出书籍 ${book.name} 一共耗时 ${System.currentTimeMillis() - currentTimeMillis}")
|
|
||||||
|
val elapsed = System.currentTimeMillis() - currentTimeMillis
|
||||||
|
AppLog.put("分割导出书籍 ${book.name} 一共耗时 $elapsed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -878,7 +718,7 @@ class ExportBookService : BaseService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// val totalChapterNum = book.totalChapterNum / scope.size
|
// val totalChapterNum = book.totalChapterNum / scope.size
|
||||||
if (chapterList.size == 0) {
|
if (chapterList.isEmpty()) {
|
||||||
throw RuntimeException("书籍<${book.name}>(${epubBookIndex + 1})未找到章节信息")
|
throw RuntimeException("书籍<${book.name}>(${epubBookIndex + 1})未找到章节信息")
|
||||||
}
|
}
|
||||||
chapterList = chapterList.subList(
|
chapterList = chapterList.subList(
|
||||||
@@ -930,23 +770,22 @@ class ExportBookService : BaseService() {
|
|||||||
* 创建多个epub 对象
|
* 创建多个epub 对象
|
||||||
*
|
*
|
||||||
* 分割epub时,一个书籍需要创建多个epub对象
|
* 分割epub时,一个书籍需要创建多个epub对象
|
||||||
* @param doc 导出文档
|
|
||||||
* @param book 书籍
|
* @param book 书籍
|
||||||
|
* @param fileDoc 导出文件夹文档
|
||||||
*
|
*
|
||||||
* @return <内容模板字符串, <epub文件名, epub对象>>
|
* @return <内容模板字符串, <epub文件名, epub对象>>
|
||||||
*/
|
*/
|
||||||
private fun createEpubs(
|
private fun createEpubs(
|
||||||
book: Book,
|
book: Book,
|
||||||
doc: DocumentFile?,
|
fileDoc: FileDoc
|
||||||
): Pair<String, List<Pair<String, EpubBook>>> {
|
): Pair<String, List<Pair<String, EpubBook>>> {
|
||||||
val paresNumOfEpub = paresNumOfEpub(scope.size, size)
|
val paresNumOfEpub = paresNumOfEpub(scope.size, size)
|
||||||
val result: MutableList<Pair<String, EpubBook>> = ArrayList(paresNumOfEpub)
|
val result: MutableList<Pair<String, EpubBook>> = ArrayList(paresNumOfEpub)
|
||||||
var contentModel = ""
|
var contentModel = ""
|
||||||
for (i in 1..paresNumOfEpub) {
|
for (i in 1..paresNumOfEpub) {
|
||||||
val filename = book.getExportFileName("epub", i)
|
val filename = book.getExportFileName("epub", i)
|
||||||
doc?.let {
|
fileDoc.find(filename)?.delete()
|
||||||
DocumentUtils.delete(it, filename)
|
|
||||||
}
|
|
||||||
val epubBook = EpubBook()
|
val epubBook = EpubBook()
|
||||||
epubBook.version = "2.0"
|
epubBook.version = "2.0"
|
||||||
//set metadata
|
//set metadata
|
||||||
@@ -954,9 +793,7 @@ class ExportBookService : BaseService() {
|
|||||||
//set cover
|
//set cover
|
||||||
setCover(book, epubBook)
|
setCover(book, epubBook)
|
||||||
//set css
|
//set css
|
||||||
contentModel = doc?.let {
|
contentModel = setAssets(fileDoc, book, epubBook)
|
||||||
setAssets(it, book, epubBook)
|
|
||||||
} ?: setAssets(book, epubBook)
|
|
||||||
|
|
||||||
// add epubBook
|
// add epubBook
|
||||||
result.add(Pair(filename, epubBook))
|
result.add(Pair(filename, epubBook))
|
||||||
@@ -970,47 +807,23 @@ class ExportBookService : BaseService() {
|
|||||||
private suspend fun save2Drive(
|
private suspend fun save2Drive(
|
||||||
filename: String,
|
filename: String,
|
||||||
epubBook: EpubBook,
|
epubBook: EpubBook,
|
||||||
doc: DocumentFile,
|
fileDoc: FileDoc,
|
||||||
callback: (total: Int, progress: Int) -> Unit
|
callback: (total: Int, progress: Int) -> Unit
|
||||||
) {
|
) {
|
||||||
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
val bookDoc = fileDoc.createFileIfNotExist(filename)
|
||||||
contentResolver.openOutputStream(bookDoc.uri, "wa")?.buffered().use { bookOs ->
|
bookDoc.openOutputStream().getOrThrow().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, bookOs)
|
.write(epubBook, bookOs)
|
||||||
}
|
|
||||||
if (AppConfig.exportToWebDav) {
|
|
||||||
// 导出到webdav
|
|
||||||
AppWebDav.exportWebDav(bookDoc.uri, filename)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保存文件到 设备
|
|
||||||
*/
|
|
||||||
private suspend fun save2Drive(
|
|
||||||
filename: String,
|
|
||||||
epubBook: EpubBook,
|
|
||||||
file: File,
|
|
||||||
callback: (total: Int, progress: Int) -> Unit
|
|
||||||
) {
|
|
||||||
val bookPath = FileUtils.getPath(file, filename)
|
|
||||||
val bookFile = FileUtils.createFileWithReplace(bookPath)
|
|
||||||
EpubWriter()
|
|
||||||
.setCallback(object : EpubWriterProcessor.Callback {
|
|
||||||
override fun onProgressing(total: Int, progress: Int) {
|
|
||||||
callback(total, progress)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.write(epubBook, bookFile.outputStream().buffered())
|
|
||||||
if (AppConfig.exportToWebDav) {
|
if (AppConfig.exportToWebDav) {
|
||||||
// 导出到webdav
|
// 导出到webdav
|
||||||
AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
|
AppWebDav.exportWebDav(bookDoc.uri, filename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1028,5 +841,36 @@ class ExportBookService : BaseService() {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析范围字符串
|
||||||
|
*
|
||||||
|
* @param scope 范围字符串
|
||||||
|
* @return 范围
|
||||||
|
*
|
||||||
|
* @since 2023/5/22
|
||||||
|
* @author Discut
|
||||||
|
*/
|
||||||
|
private fun parseScope(scope: String): Set<Int> {
|
||||||
|
val split = scope.split(",")
|
||||||
|
|
||||||
|
val result = linkedSetOf<Int>()
|
||||||
|
for (s in split) {
|
||||||
|
val v = s.split("-")
|
||||||
|
if (v.size != 2) {
|
||||||
|
result.add(s.toInt() - 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val left = v[0].toInt()
|
||||||
|
val right = v[1].toInt()
|
||||||
|
if (left > right) {
|
||||||
|
AppLog.put("Error expression : $s; left > right")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (i in left..right)
|
||||||
|
result.add(i - 1)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +89,7 @@ class TextChapterLayout(
|
|||||||
|
|
||||||
var exception: Throwable? = null
|
var exception: Throwable? = null
|
||||||
|
|
||||||
var channel = Channel<TextPage>(Int.MAX_VALUE)
|
var channel = Channel<TextPage>(Channel.UNLIMITED)
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class RssAdapter(context: Context, val callBack: CallBack, val lifecycle: Lifecy
|
|||||||
tvName.text = item.sourceName
|
tvName.text = item.sourceName
|
||||||
val options = RequestOptions()
|
val options = RequestOptions()
|
||||||
.set(OkHttpModelLoader.sourceOriginOption, item.sourceUrl)
|
.set(OkHttpModelLoader.sourceOriginOption, item.sourceUrl)
|
||||||
ImageLoader.load(lifecycle, item.sourceIcon)
|
ImageLoader.load(context, lifecycle, item.sourceIcon)
|
||||||
.apply(options)
|
.apply(options)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
.placeholder(R.drawable.image_rss)
|
.placeholder(R.drawable.image_rss)
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ class CoverImageView @JvmOverloads constructor(
|
|||||||
options = options.set(OkHttpModelLoader.sourceOriginOption, sourceOrigin)
|
options = options.set(OkHttpModelLoader.sourceOriginOption, sourceOrigin)
|
||||||
}
|
}
|
||||||
val builder = if (lifecycle != null) {
|
val builder = if (lifecycle != null) {
|
||||||
ImageLoader.load(lifecycle, path)
|
ImageLoader.load(context, lifecycle, path)
|
||||||
} else {
|
} else {
|
||||||
ImageLoader.load(context, path)//Glide自动识别http://,content://和file://
|
ImageLoader.load(context, path)//Glide自动识别http://,content://和file://
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.database.Cursor
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.provider.DocumentsContract
|
import android.provider.DocumentsContract
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
@@ -67,6 +68,10 @@ data class FileDoc(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
fun fromDir(path: String): FileDoc {
|
||||||
|
return fromUri(path.toUri(), true)
|
||||||
|
}
|
||||||
|
|
||||||
fun fromUri(uri: Uri, isDir: Boolean): FileDoc {
|
fun fromUri(uri: Uri, isDir: Boolean): FileDoc {
|
||||||
if (uri.isContentScheme()) {
|
if (uri.isContentScheme()) {
|
||||||
val doc = if (isDir) {
|
val doc = if (isDir) {
|
||||||
@@ -284,6 +289,14 @@ fun FileDoc.writeText(text: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun FileDoc.writeFile(file: File) {
|
||||||
|
openOutputStream().getOrThrow().use { out ->
|
||||||
|
file.inputStream().use {
|
||||||
|
it.copyTo(out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun FileDoc.delete() {
|
fun FileDoc.delete() {
|
||||||
asFile()?.let {
|
asFile()?.let {
|
||||||
FileUtils.delete(it, true)
|
FileUtils.delete(it, true)
|
||||||
|
|||||||
@@ -3,9 +3,18 @@ package io.legado.app.utils
|
|||||||
import androidx.lifecycle.DefaultLifecycleObserver
|
import androidx.lifecycle.DefaultLifecycleObserver
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.RequestManager
|
import com.bumptech.glide.RequestManager
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
private val applicationRequestManager by lazy {
|
||||||
|
Glide.with(appCtx)
|
||||||
|
}
|
||||||
|
|
||||||
fun RequestManager.lifecycle(lifecycle: Lifecycle): RequestManager {
|
fun RequestManager.lifecycle(lifecycle: Lifecycle): RequestManager {
|
||||||
|
if (this === applicationRequestManager) {
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
val observer = object : DefaultLifecycleObserver {
|
val observer = object : DefaultLifecycleObserver {
|
||||||
override fun onResume(owner: LifecycleOwner) = onStart()
|
override fun onResume(owner: LifecycleOwner) = onStart()
|
||||||
|
|||||||
Reference in New Issue
Block a user