|
@@ -0,0 +1,930 @@
|
|
|
|
|
+package io.legado.app.service
|
|
|
|
|
+
|
|
|
|
|
+import android.content.Intent
|
|
|
|
|
+import android.graphics.Bitmap
|
|
|
|
|
+import android.graphics.drawable.Drawable
|
|
|
|
|
+import android.net.Uri
|
|
|
|
|
+import android.util.ArraySet
|
|
|
|
|
+import androidx.core.app.NotificationCompat
|
|
|
|
|
+import androidx.documentfile.provider.DocumentFile
|
|
|
|
|
+import com.bumptech.glide.Glide
|
|
|
|
|
+import com.bumptech.glide.request.target.CustomTarget
|
|
|
|
|
+import com.bumptech.glide.request.transition.Transition
|
|
|
|
|
+import io.legado.app.R
|
|
|
|
|
+import io.legado.app.base.BaseService
|
|
|
|
|
+import io.legado.app.constant.AppConst
|
|
|
|
|
+import io.legado.app.constant.AppLog
|
|
|
|
|
+import io.legado.app.constant.AppPattern
|
|
|
|
|
+import io.legado.app.constant.EventBus
|
|
|
|
|
+import io.legado.app.constant.IntentAction
|
|
|
|
|
+import io.legado.app.data.appDb
|
|
|
|
|
+import io.legado.app.data.entities.Book
|
|
|
|
|
+import io.legado.app.data.entities.BookChapter
|
|
|
|
|
+import io.legado.app.exception.NoStackTraceException
|
|
|
|
|
+import io.legado.app.help.AppWebDav
|
|
|
|
|
+import io.legado.app.help.book.BookHelp
|
|
|
|
|
+import io.legado.app.help.book.ContentProcessor
|
|
|
|
|
+import io.legado.app.help.book.getExportFileName
|
|
|
|
|
+import io.legado.app.help.config.AppConfig
|
|
|
|
|
+import io.legado.app.help.coroutine.OrderCoroutine
|
|
|
|
|
+import io.legado.app.ui.book.cache.CacheActivity
|
|
|
|
|
+import io.legado.app.utils.DocumentUtils
|
|
|
|
|
+import io.legado.app.utils.FileUtils
|
|
|
|
|
+import io.legado.app.utils.HtmlFormatter
|
|
|
|
|
+import io.legado.app.utils.MD5Utils
|
|
|
|
|
+import io.legado.app.utils.NetworkUtils
|
|
|
|
|
+import io.legado.app.utils.activityPendingIntent
|
|
|
|
|
+import io.legado.app.utils.cnCompare
|
|
|
|
|
+import io.legado.app.utils.createFolderIfNotExist
|
|
|
|
|
+import io.legado.app.utils.isContentScheme
|
|
|
|
|
+import io.legado.app.utils.postEvent
|
|
|
|
|
+import io.legado.app.utils.readBytes
|
|
|
|
|
+import io.legado.app.utils.readText
|
|
|
|
|
+import io.legado.app.utils.servicePendingIntent
|
|
|
|
|
+import io.legado.app.utils.toastOnUi
|
|
|
|
|
+import io.legado.app.utils.writeBytes
|
|
|
|
|
+import kotlinx.coroutines.Deferred
|
|
|
|
|
+import kotlinx.coroutines.Dispatchers.IO
|
|
|
|
|
+import kotlinx.coroutines.Dispatchers.Main
|
|
|
|
|
+import kotlinx.coroutines.Job
|
|
|
|
|
+import kotlinx.coroutines.async
|
|
|
|
|
+import kotlinx.coroutines.ensureActive
|
|
|
|
|
+import kotlinx.coroutines.launch
|
|
|
|
|
+import kotlinx.coroutines.withContext
|
|
|
|
|
+import me.ag2s.epublib.domain.Author
|
|
|
|
|
+import me.ag2s.epublib.domain.Date
|
|
|
|
|
+import me.ag2s.epublib.domain.EpubBook
|
|
|
|
|
+import me.ag2s.epublib.domain.FileResourceProvider
|
|
|
|
|
+import me.ag2s.epublib.domain.LazyResource
|
|
|
|
|
+import me.ag2s.epublib.domain.Metadata
|
|
|
|
|
+import me.ag2s.epublib.domain.Resource
|
|
|
|
|
+import me.ag2s.epublib.epub.EpubWriter
|
|
|
|
|
+import me.ag2s.epublib.epub.EpubWriterProcessor
|
|
|
|
|
+import me.ag2s.epublib.util.ResourceUtil
|
|
|
|
|
+import splitties.init.appCtx
|
|
|
|
|
+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
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 导出书籍服务
|
|
|
|
|
+ */
|
|
|
|
|
+class ExportBookService : BaseService() {
|
|
|
|
|
+
|
|
|
|
|
+ companion object {
|
|
|
|
|
+ val exportProgress = ConcurrentHashMap<String, Int>()
|
|
|
|
|
+ val exportMsg = ConcurrentHashMap<String, String>()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ data class ExportConfig(
|
|
|
|
|
+ val path: String,
|
|
|
|
|
+ val type: String,
|
|
|
|
|
+ val epubSize: Int = 1,
|
|
|
|
|
+ val epubScope: String? = null
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ private val waitExportBooks = linkedMapOf<String, ExportConfig>()
|
|
|
|
|
+ private var exportJob: Job? = null
|
|
|
|
|
+
|
|
|
|
|
+ private var notificationContent = appCtx.getString(R.string.service_starting)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
|
|
|
+ when (intent?.action) {
|
|
|
|
|
+ IntentAction.start -> kotlin.runCatching {
|
|
|
|
|
+ val bookUrl = intent.getStringExtra("bookUrl")!!
|
|
|
|
|
+ if (!exportProgress.contains(bookUrl)) {
|
|
|
|
|
+ val exportConfig = ExportConfig(
|
|
|
|
|
+ path = intent.getStringExtra("exportPath")!!,
|
|
|
|
|
+ type = intent.getStringExtra("exportType")!!,
|
|
|
|
|
+ epubSize = intent.getIntExtra("epubSize", 1),
|
|
|
|
|
+ epubScope = intent.getStringExtra("epubScope")
|
|
|
|
|
+ )
|
|
|
|
|
+ waitExportBooks[bookUrl] = exportConfig
|
|
|
|
|
+ export()
|
|
|
|
|
+ }
|
|
|
|
|
+ }.onFailure {
|
|
|
|
|
+ toastOnUi(it.localizedMessage)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ IntentAction.stop -> stopSelf()
|
|
|
|
|
+ }
|
|
|
|
|
+ return super.onStartCommand(intent, flags, startId)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onDestroy() {
|
|
|
|
|
+ super.onDestroy()
|
|
|
|
|
+ exportProgress.clear()
|
|
|
|
|
+ exportMsg.clear()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun upNotification() {
|
|
|
|
|
+ val notification = NotificationCompat.Builder(this, AppConst.channelIdDownload)
|
|
|
|
|
+ .setSmallIcon(R.drawable.ic_export)
|
|
|
|
|
+ .setOngoing(true)
|
|
|
|
|
+ .setContentTitle(getString(R.string.export))
|
|
|
|
|
+ .setContentIntent(activityPendingIntent<CacheActivity>("cacheActivity"))
|
|
|
|
|
+ notification.addAction(
|
|
|
|
|
+ R.drawable.ic_stop_black_24dp,
|
|
|
|
|
+ getString(R.string.cancel),
|
|
|
|
|
+ servicePendingIntent<ExportBookService>(IntentAction.stop)
|
|
|
|
|
+ )
|
|
|
|
|
+ notification.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
|
|
|
|
+ notification.setContentText(notificationContent)
|
|
|
|
|
+ startForeground(AppConst.notificationIdCache, notification.build())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun export() {
|
|
|
|
|
+ if (exportJob?.isActive == true) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ val entry = waitExportBooks.firstNotNullOfOrNull { it }
|
|
|
|
|
+ if (entry == null) {
|
|
|
|
|
+ notificationContent = "导出完成"
|
|
|
|
|
+ upNotification()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ val bookUrl = entry.key
|
|
|
|
|
+ val exportConfig = entry.value
|
|
|
|
|
+ if (exportProgress.contains(bookUrl)) return
|
|
|
|
|
+ exportProgress[bookUrl] = 0
|
|
|
|
|
+ waitExportBooks.remove(bookUrl)
|
|
|
|
|
+ exportJob = launch(IO) {
|
|
|
|
|
+ val book = appDb.bookDao.getBook(bookUrl)
|
|
|
|
|
+ try {
|
|
|
|
|
+ book ?: throw NoStackTraceException("获取${bookUrl}书籍出错")
|
|
|
|
|
+ notificationContent = "正在导出(${book.name}),还有${waitExportBooks.size}本待导出"
|
|
|
|
|
+ if (exportConfig.type == "epub") {
|
|
|
|
|
+ if (exportConfig.epubScope.isNullOrBlank()) {
|
|
|
|
|
+ exportEPUB(exportConfig.path, book)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ CustomExporter(paresScope(exportConfig.epubScope), exportConfig.epubSize)
|
|
|
|
|
+ .export(exportConfig.path, book)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ export(exportConfig.path, book)
|
|
|
|
|
+ }
|
|
|
|
|
+ exportMsg[book.bookUrl] = getString(R.string.export_success)
|
|
|
|
|
+ } catch (e: Throwable) {
|
|
|
|
|
+ exportMsg[bookUrl] = e.localizedMessage ?: "ERROR"
|
|
|
|
|
+ AppLog.put("导出书籍<${book?.name ?: bookUrl}>出错", e)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ exportProgress.remove(bookUrl)
|
|
|
|
|
+ postEvent(EventBus.EXPORT_BOOK, bookUrl)
|
|
|
|
|
+ }
|
|
|
|
|
+ withContext(Main) {
|
|
|
|
|
+ export()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private suspend fun export(path: String, book: Book) {
|
|
|
|
|
+ exportMsg.remove(book.bookUrl)
|
|
|
|
|
+ postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
|
|
|
|
+ if (path.isContentScheme()) {
|
|
|
|
|
+ val uri = Uri.parse(path)
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ val filename = book.getExportFileName("txt")
|
|
|
|
|
+ DocumentUtils.delete(doc, filename)
|
|
|
|
|
+ val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
|
|
|
|
|
+ ?: throw NoStackTraceException("创建文档失败,请尝试重新设置导出文件夹")
|
|
|
|
|
+ contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
|
|
|
|
+ getAllContents(book) { text, srcList ->
|
|
|
|
|
+ bookOs.write(text.toByteArray(Charset.forName(AppConfig.exportCharset)))
|
|
|
|
|
+ srcList?.forEach {
|
|
|
|
|
+ val vFile = BookHelp.getImage(book, it.third)
|
|
|
|
|
+ if (vFile.exists()) {
|
|
|
|
|
+ DocumentUtils.createFileIfNotExist(
|
|
|
|
|
+ doc,
|
|
|
|
|
+ "${it.second}-${MD5Utils.md5Encode16(it.third)}.jpg",
|
|
|
|
|
+ subDirs = arrayOf("${book.name}_${book.author}", "images", it.first)
|
|
|
|
|
+ )?.writeBytes(this, vFile.readBytes())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (AppConfig.exportToWebDav) {
|
|
|
|
|
+ // 导出到webdav
|
|
|
|
|
+ AppWebDav.exportWebDav(bookDoc.uri, filename)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private suspend fun export(file: File, book: Book) {
|
|
|
|
|
+ val filename = book.getExportFileName("txt")
|
|
|
|
|
+ val bookPath = FileUtils.getPath(file, filename)
|
|
|
|
|
+ val bookFile = FileUtils.createFileWithReplace(bookPath)
|
|
|
|
|
+ getAllContents(book) { text, srcList ->
|
|
|
|
|
+ bookFile.appendText(text, Charset.forName(AppConfig.exportCharset))
|
|
|
|
|
+ srcList?.forEach {
|
|
|
|
|
+ val vFile = BookHelp.getImage(book, it.third)
|
|
|
|
|
+ if (vFile.exists()) {
|
|
|
|
|
+ FileUtils.createFileIfNotExist(
|
|
|
|
|
+ file,
|
|
|
|
|
+ "${book.name}_${book.author}",
|
|
|
|
|
+ "images",
|
|
|
|
|
+ it.first,
|
|
|
|
|
+ "${it.second}-${MD5Utils.md5Encode16(it.third)}.jpg"
|
|
|
|
|
+ ).writeBytes(vFile.readBytes())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (AppConfig.exportToWebDav) {
|
|
|
|
|
+ AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename) // 导出到webdav
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private suspend fun getAllContents(
|
|
|
|
|
+ book: Book,
|
|
|
|
|
+ append: (text: String, srcList: ArrayList<Triple<String, Int, String>>?) -> Unit
|
|
|
|
|
+ ) {
|
|
|
|
|
+ val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
|
|
|
|
+ val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
|
|
|
|
+ val qy = "${book.name}\n${
|
|
|
|
|
+ getString(R.string.author_show, book.getRealAuthor())
|
|
|
|
|
+ }\n${
|
|
|
|
|
+ getString(
|
|
|
|
|
+ R.string.intro_show,
|
|
|
|
|
+ "\n" + HtmlFormatter.format(book.getDisplayIntro())
|
|
|
|
|
+ )
|
|
|
|
|
+ }"
|
|
|
|
|
+ append(qy, null)
|
|
|
|
|
+ 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 ->
|
|
|
|
|
+ postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
|
|
|
|
+ exportProgress[book.bookUrl] = index
|
|
|
|
|
+ append.invoke(result.first, result.second)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
|
|
|
|
|
+ kotlin.coroutines.coroutineContext.ensureActive()
|
|
|
|
|
+ postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
|
|
|
|
+ exportProgress[book.bookUrl] = index
|
|
|
|
|
+ val result = getExportData(book, chapter, contentProcessor, useReplace)
|
|
|
|
|
+ append.invoke(result.first, result.second)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private 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,
|
|
|
|
|
+ // 不导出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<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))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return Pair("\n\n$content1", srcList)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return Pair("\n\n$content1", null)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析范围字符串
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param scope 范围字符串
|
|
|
|
|
+ * @return 范围
|
|
|
|
|
+ *
|
|
|
|
|
+ * @since 2023/5/22
|
|
|
|
|
+ * @author Discut
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun paresScope(scope: String): IntArray {
|
|
|
|
|
+ val split = scope.split(",")
|
|
|
|
|
+ val result = ArraySet<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.toIntArray()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 导出Epub
|
|
|
|
|
+ */
|
|
|
|
|
+ private suspend fun exportEPUB(path: String, book: Book) {
|
|
|
|
|
+ exportMsg.remove(book.bookUrl)
|
|
|
|
|
+ postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
|
|
|
|
+ if (path.isContentScheme()) {
|
|
|
|
|
+ val uri = Uri.parse(path)
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ val filename = book.getExportFileName("epub")
|
|
|
|
|
+ DocumentUtils.delete(doc, filename)
|
|
|
|
|
+ val epubBook = EpubBook()
|
|
|
|
|
+ epubBook.version = "2.0"
|
|
|
|
|
+ //set metadata
|
|
|
|
|
+ setEpubMetadata(book, epubBook)
|
|
|
|
|
+ //set cover
|
|
|
|
|
+ setCover(book, epubBook)
|
|
|
|
|
+ //set css
|
|
|
|
|
+ val contentModel = setAssets(doc, book, epubBook)
|
|
|
|
|
+
|
|
|
|
|
+ //设置正文
|
|
|
|
|
+ setEpubContent(contentModel, book, epubBook)
|
|
|
|
|
+ DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
|
|
|
|
+ contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
|
|
|
|
+ EpubWriter().write(epubBook, BufferedOutputStream(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(book, epubBook)
|
|
|
|
|
+
|
|
|
|
|
+ val bookPath = FileUtils.getPath(file, filename)
|
|
|
|
|
+ val bookFile = FileUtils.createFileWithReplace(bookPath)
|
|
|
|
|
+ //设置正文
|
|
|
|
|
+ setEpubContent(contentModel, book, epubBook)
|
|
|
|
|
+ @Suppress("BlockingMethodInNonBlockingContext")
|
|
|
|
|
+ EpubWriter().write(epubBook, BufferedOutputStream(FileOutputStream(bookFile)))
|
|
|
|
|
+ if (AppConfig.exportToWebDav) {
|
|
|
|
|
+ // 导出到webdav
|
|
|
|
|
+ AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun setAssets(doc: DocumentFile, book: Book, epubBook: EpubBook): String {
|
|
|
|
|
+ var contentModel = ""
|
|
|
|
|
+ DocumentUtils.getDirDocument(doc, "Asset").let { customPath ->
|
|
|
|
|
+ if (customPath == null) {//使用内置模板
|
|
|
|
|
+ contentModel = setAssets(book, epubBook)
|
|
|
|
|
+ } else {//外部模板
|
|
|
|
|
+ customPath.listFiles().forEach { folder ->
|
|
|
|
|
+ if (folder.isDirectory && folder.name == "Text") {
|
|
|
|
|
+ folder.listFiles().sortedWith { o1, o2 ->
|
|
|
|
|
+ val name1 = o1.name ?: ""
|
|
|
|
|
+ val name2 = o2.name ?: ""
|
|
|
|
|
+ name1.cnCompare(name2)
|
|
|
|
|
+ }.forEach { file ->
|
|
|
|
|
+ if (file.isFile) {
|
|
|
|
|
+ when {
|
|
|
|
|
+ //正文模板
|
|
|
|
|
+ file.name.equals("chapter.html", true)
|
|
|
|
|
+ || file.name.equals("chapter.xhtml", true) -> {
|
|
|
|
|
+ contentModel = file.readText(this)
|
|
|
|
|
+ }
|
|
|
|
|
+ //封面等其他模板
|
|
|
|
|
+ true == file.name?.endsWith("html", true) -> {
|
|
|
|
|
+ epubBook.addSection(
|
|
|
|
|
+ FileUtils.getNameExcludeExtension(
|
|
|
|
|
+ file.name ?: "Cover.html"
|
|
|
|
|
+ ),
|
|
|
|
|
+ ResourceUtil.createPublicResource(
|
|
|
|
|
+ book.name,
|
|
|
|
|
+ book.getRealAuthor(),
|
|
|
|
|
+ book.getDisplayIntro(),
|
|
|
|
|
+ book.kind,
|
|
|
|
|
+ book.wordCount,
|
|
|
|
|
+ file.readText(this),
|
|
|
|
|
+ "${folder.name}/${file.name}"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ else -> {
|
|
|
|
|
+ //其他格式文件当做资源文件
|
|
|
|
|
+ folder.listFiles().forEach {
|
|
|
|
|
+ if (it.isFile)
|
|
|
|
|
+ epubBook.resources.add(
|
|
|
|
|
+ Resource(
|
|
|
|
|
+ it.readBytes(this),
|
|
|
|
|
+ "${folder.name}/${it.name}"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (folder.isDirectory) {
|
|
|
|
|
+ //资源文件
|
|
|
|
|
+ folder.listFiles().forEach {
|
|
|
|
|
+ if (it.isFile)
|
|
|
|
|
+ epubBook.resources.add(
|
|
|
|
|
+ Resource(
|
|
|
|
|
+ it.readBytes(this),
|
|
|
|
|
+ "${folder.name}/${it.name}"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {//Asset下面的资源文件
|
|
|
|
|
+ epubBook.resources.add(
|
|
|
|
|
+ Resource(
|
|
|
|
|
+ folder.readBytes(this),
|
|
|
|
|
+ "${folder.name}"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return contentModel
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun setAssets(book: Book, epubBook: EpubBook): String {
|
|
|
|
|
+ epubBook.resources.add(
|
|
|
|
|
+ Resource(
|
|
|
|
|
+ appCtx.assets.open("epub/fonts.css").readBytes(),
|
|
|
|
|
+ "Styles/fonts.css"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ epubBook.resources.add(
|
|
|
|
|
+ Resource(
|
|
|
|
|
+ appCtx.assets.open("epub/main.css").readBytes(),
|
|
|
|
|
+ "Styles/main.css"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ epubBook.resources.add(
|
|
|
|
|
+ Resource(
|
|
|
|
|
+ appCtx.assets.open("epub/logo.png").readBytes(),
|
|
|
|
|
+ "Images/logo.png"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ epubBook.addSection(
|
|
|
|
|
+ getString(R.string.img_cover),
|
|
|
|
|
+ ResourceUtil.createPublicResource(
|
|
|
|
|
+ book.name,
|
|
|
|
|
+ book.getRealAuthor(),
|
|
|
|
|
+ book.getDisplayIntro(),
|
|
|
|
|
+ book.kind,
|
|
|
|
|
+ book.wordCount,
|
|
|
|
|
+ String(appCtx.assets.open("epub/cover.html").readBytes()),
|
|
|
|
|
+ "Text/cover.html"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ epubBook.addSection(
|
|
|
|
|
+ getString(R.string.book_intro),
|
|
|
|
|
+ ResourceUtil.createPublicResource(
|
|
|
|
|
+ book.name,
|
|
|
|
|
+ book.getRealAuthor(),
|
|
|
|
|
+ book.getDisplayIntro(),
|
|
|
|
|
+ book.kind,
|
|
|
|
|
+ book.wordCount,
|
|
|
|
|
+ String(appCtx.assets.open("epub/intro.html").readBytes()),
|
|
|
|
|
+ "Text/intro.html"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ return String(appCtx.assets.open("epub/chapter.html").readBytes())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun setCover(book: Book, epubBook: EpubBook) {
|
|
|
|
|
+ Glide.with(this)
|
|
|
|
|
+ .asBitmap()
|
|
|
|
|
+ .load(book.getDisplayCover())
|
|
|
|
|
+ .into(object : CustomTarget<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()
|
|
|
|
|
+ stream.close()
|
|
|
|
|
+ epubBook.coverImage = Resource(byteArray, "Images/cover.jpg")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onLoadCleared(placeholder: Drawable?) {
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private suspend fun setEpubContent(
|
|
|
|
|
+ contentModel: String,
|
|
|
|
|
+ book: Book,
|
|
|
|
|
+ epubBook: EpubBook
|
|
|
|
|
+ ) {
|
|
|
|
|
+ //正文
|
|
|
|
|
+ val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
|
|
|
|
+ val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
|
|
|
|
+ appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
|
|
|
|
|
+ kotlin.coroutines.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(
|
|
|
|
|
+ book,
|
|
|
|
|
+ chapter,
|
|
|
|
|
+ content1,
|
|
|
|
|
+ includeTitle = false,
|
|
|
|
|
+ useReplace = useReplace,
|
|
|
|
|
+ chineseConvert = false,
|
|
|
|
|
+ reSegment = false
|
|
|
|
|
+ ).toString()
|
|
|
|
|
+ val title = chapter.run {
|
|
|
|
|
+ // 不导出vip标识
|
|
|
|
|
+ isVip = false
|
|
|
|
|
+ getDisplayTitle(
|
|
|
|
|
+ contentProcessor.getTitleReplaceRules(),
|
|
|
|
|
+ useReplace = useReplace
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ epubBook.addSection(
|
|
|
|
|
+ title,
|
|
|
|
|
+ ResourceUtil.createChapterResource(
|
|
|
|
|
+ title.replace("\uD83D\uDD12", ""),
|
|
|
|
|
+ content1,
|
|
|
|
|
+ contentModel,
|
|
|
|
|
+ "Text/chapter_${index}.html"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun fixPic(
|
|
|
|
|
+ epubBook: EpubBook,
|
|
|
|
|
+ book: Book,
|
|
|
|
|
+ content: String,
|
|
|
|
|
+ chapter: BookChapter
|
|
|
|
|
+ ): String {
|
|
|
|
|
+ val data = StringBuilder("")
|
|
|
|
|
+ content.split("\n").forEach { text ->
|
|
|
|
|
+ var text1 = text
|
|
|
|
|
+ val matcher = AppPattern.imgPattern.matcher(text)
|
|
|
|
|
+ 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 vFile = BookHelp.getImage(book, src)
|
|
|
|
|
+ val fp = FileResourceProvider(vFile.parent)
|
|
|
|
|
+ if (vFile.exists()) {
|
|
|
|
|
+ val img = LazyResource(fp, href, originalHref)
|
|
|
|
|
+ epubBook.resources.add(img)
|
|
|
|
|
+ }
|
|
|
|
|
+ text1 = text1.replace(src, "../${href}")
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ data.append(text1).append("\n")
|
|
|
|
|
+ }
|
|
|
|
|
+ return data.toString()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun setEpubMetadata(book: Book, epubBook: EpubBook) {
|
|
|
|
|
+ val metadata = Metadata()
|
|
|
|
|
+ metadata.titles.add(book.name)//书籍的名称
|
|
|
|
|
+ metadata.authors.add(Author(book.getRealAuthor()))//书籍的作者
|
|
|
|
|
+ metadata.language = "zh"//数据的语言
|
|
|
|
|
+ metadata.dates.add(Date())//数据的创建日期
|
|
|
|
|
+ metadata.publishers.add("Legado")//数据的创建者
|
|
|
|
|
+ metadata.descriptions.add(book.getDisplayIntro())//书籍的简介
|
|
|
|
|
+ //metadata.subjects.add("")//书籍的主题,在静读天下里面有使用这个分类书籍
|
|
|
|
|
+ epubBook.metadata = metadata
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ //////end of EPUB
|
|
|
|
|
+
|
|
|
|
|
+ //////start of custom exporter
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 自定义Exporter
|
|
|
|
|
+ * @param scope 导出范围
|
|
|
|
|
+ * @param size epub 文件包含最大章节数
|
|
|
|
|
+ */
|
|
|
|
|
+ inner class CustomExporter(private val scope: IntArray, private val size: Int) {
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 导出Epub
|
|
|
|
|
+ * @param path 导出的路径
|
|
|
|
|
+ * @param book 书籍
|
|
|
|
|
+ */
|
|
|
|
|
+ suspend fun export(
|
|
|
|
|
+ path: String,
|
|
|
|
|
+ book: Book
|
|
|
|
|
+ ) {
|
|
|
|
|
+ exportProgress[book.bookUrl] = 0
|
|
|
|
|
+ exportMsg.remove(book.bookUrl)
|
|
|
|
|
+ postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
|
|
|
|
+ val currentTimeMillis = System.currentTimeMillis()
|
|
|
|
|
+ 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
|
|
|
|
|
+ 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, progress ->
|
|
|
|
|
+ //写入硬盘时更新进度条
|
|
|
|
|
+ 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 file = File(path).createFolderIfNotExist()
|
|
|
|
|
+ val (contentModel, epubList) = createEpubs(book, null)
|
|
|
|
|
+ val asyncBlocks = ArrayList<Deferred<Unit>>(epubList.size)
|
|
|
|
|
+ var progressBar = 0.0
|
|
|
|
|
+ epubList.forEachIndexed { index, ep ->
|
|
|
|
|
+ val (filename, epubBook) = ep
|
|
|
|
|
+ val asyncBlock = async {
|
|
|
|
|
+ //设置正文
|
|
|
|
|
+ setEpubContent(
|
|
|
|
|
+ contentModel,
|
|
|
|
|
+ book,
|
|
|
|
|
+ epubBook,
|
|
|
|
|
+ index
|
|
|
|
|
+ ) { _, _ ->
|
|
|
|
|
+ postEvent(EventBus.EXPORT_BOOK, book.bookUrl)
|
|
|
|
|
+ exportProgress[book.bookUrl] =
|
|
|
|
|
+ exportProgress[book.bookUrl]?.plus(book.totalChapterNum / scope.size)
|
|
|
|
|
+ ?: 1
|
|
|
|
|
+ }
|
|
|
|
|
+ save2Drive(filename, epubBook, file) { total, progress ->
|
|
|
|
|
+ //设置进度
|
|
|
|
|
+ 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}")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 设置epub正文
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param contentModel 正文模板
|
|
|
|
|
+ * @param book 书籍
|
|
|
|
|
+ * @param epubBook 分割后的epub
|
|
|
|
|
+ * @param epubBookIndex 分割后的epub序号
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun setEpubContent(
|
|
|
|
|
+ contentModel: String,
|
|
|
|
|
+ book: Book,
|
|
|
|
|
+ epubBook: EpubBook,
|
|
|
|
|
+ epubBookIndex: Int,
|
|
|
|
|
+ updateProgress: (chapterList: MutableList<BookChapter>, index: Int) -> Unit
|
|
|
|
|
+ ) {
|
|
|
|
|
+ //正文
|
|
|
|
|
+ val useReplace = AppConfig.exportUseReplace && book.getUseReplaceRule()
|
|
|
|
|
+ val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
|
|
|
|
+ var chapterList: MutableList<BookChapter> = ArrayList()
|
|
|
|
|
+ appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
|
|
|
|
|
+ if (scope.indexOf(index) >= 0) {
|
|
|
|
|
+ chapterList.add(chapter)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (scope.size == chapterList.size) {
|
|
|
|
|
+ return@forEachIndexed
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // val totalChapterNum = book.totalChapterNum / scope.size
|
|
|
|
|
+ if (chapterList.size == 0) {
|
|
|
|
|
+ throw RuntimeException("书籍<${book.name}>(${epubBookIndex + 1})未找到章节信息")
|
|
|
|
|
+ }
|
|
|
|
|
+ chapterList = chapterList.subList(
|
|
|
|
|
+ epubBookIndex * size,
|
|
|
|
|
+ if ((epubBookIndex + 1) * size > scope.size) scope.size else (epubBookIndex + 1) * size
|
|
|
|
|
+ )
|
|
|
|
|
+ chapterList.forEachIndexed { index, chapter ->
|
|
|
|
|
+ coroutineContext.ensureActive()
|
|
|
|
|
+ updateProgress(chapterList, index)
|
|
|
|
|
+ BookHelp.getContent(book, chapter).let { content ->
|
|
|
|
|
+ var content1 = fixPic(
|
|
|
|
|
+ epubBook,
|
|
|
|
|
+ book,
|
|
|
|
|
+ content ?: if (chapter.isVolume) "" else "null",
|
|
|
|
|
+ chapter
|
|
|
|
|
+ )
|
|
|
|
|
+ content1 = contentProcessor
|
|
|
|
|
+ .getContent(
|
|
|
|
|
+ book,
|
|
|
|
|
+ chapter,
|
|
|
|
|
+ content1,
|
|
|
|
|
+ includeTitle = false,
|
|
|
|
|
+ useReplace = useReplace,
|
|
|
|
|
+ chineseConvert = false,
|
|
|
|
|
+ reSegment = false
|
|
|
|
|
+ ).toString()
|
|
|
|
|
+ val title = chapter.run {
|
|
|
|
|
+ // 不导出vip标识
|
|
|
|
|
+ isVip = false
|
|
|
|
|
+ getDisplayTitle(
|
|
|
|
|
+ contentProcessor.getTitleReplaceRules(),
|
|
|
|
|
+ useReplace = useReplace
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ epubBook.addSection(
|
|
|
|
|
+ title,
|
|
|
|
|
+ ResourceUtil.createChapterResource(
|
|
|
|
|
+ title.replace("\uD83D\uDD12", ""),
|
|
|
|
|
+ content1,
|
|
|
|
|
+ contentModel,
|
|
|
|
|
+ "Text/chapter_${index}.html"
|
|
|
|
|
+ )
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 创建多个epub 对象
|
|
|
|
|
+ *
|
|
|
|
|
+ * 分割epub时,一个书籍需要创建多个epub对象
|
|
|
|
|
+ * @param doc 导出文档
|
|
|
|
|
+ * @param book 书籍
|
|
|
|
|
+ *
|
|
|
|
|
+ * @return <内容模板字符串, <epub文件名, epub对象>>
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun createEpubs(
|
|
|
|
|
+ book: Book,
|
|
|
|
|
+ doc: DocumentFile?,
|
|
|
|
|
+ ): Pair<String, List<Pair<String, EpubBook>>> {
|
|
|
|
|
+ val paresNumOfEpub = paresNumOfEpub(scope.size, size)
|
|
|
|
|
+ val result: MutableList<Pair<String, EpubBook>> = ArrayList(paresNumOfEpub)
|
|
|
|
|
+ var contentModel = ""
|
|
|
|
|
+ for (i in 1..paresNumOfEpub) {
|
|
|
|
|
+ val filename = book.getExportFileName("epub", i)
|
|
|
|
|
+ doc?.let {
|
|
|
|
|
+ DocumentUtils.delete(it, filename)
|
|
|
|
|
+ }
|
|
|
|
|
+ val epubBook = EpubBook()
|
|
|
|
|
+ epubBook.version = "2.0"
|
|
|
|
|
+ //set metadata
|
|
|
|
|
+ setEpubMetadata(book, epubBook)
|
|
|
|
|
+ //set cover
|
|
|
|
|
+ setCover(book, epubBook)
|
|
|
|
|
+ //set css
|
|
|
|
|
+ contentModel = doc?.let {
|
|
|
|
|
+ setAssets(it, book, epubBook)
|
|
|
|
|
+ } ?: setAssets(book, epubBook)
|
|
|
|
|
+
|
|
|
|
|
+ // add epubBook
|
|
|
|
|
+ result.add(Pair(filename, epubBook))
|
|
|
|
|
+ }
|
|
|
|
|
+ return Pair(contentModel, result)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 保存文件到 设备
|
|
|
|
|
+ */
|
|
|
|
|
+ private suspend fun save2Drive(
|
|
|
|
|
+ filename: String,
|
|
|
|
|
+ epubBook: EpubBook,
|
|
|
|
|
+ doc: DocumentFile,
|
|
|
|
|
+ callback: (total: Int, progress: Int) -> Unit
|
|
|
|
|
+ ) {
|
|
|
|
|
+ DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
|
|
|
|
|
+ contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
|
|
|
|
|
+ EpubWriter()
|
|
|
|
|
+ .setCallback(object : EpubWriterProcessor.Callback {
|
|
|
|
|
+ override fun onProgressing(total: Int, progress: Int) {
|
|
|
|
|
+ callback(total, progress)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .write(epubBook, BufferedOutputStream(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)
|
|
|
|
|
+ @Suppress("BlockingMethodInNonBlockingContext")
|
|
|
|
|
+ EpubWriter()
|
|
|
|
|
+ .setCallback(object : EpubWriterProcessor.Callback {
|
|
|
|
|
+ override fun onProgressing(total: Int, progress: Int) {
|
|
|
|
|
+ callback(total, progress)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .write(epubBook, BufferedOutputStream(FileOutputStream(bookFile)))
|
|
|
|
|
+ if (AppConfig.exportToWebDav) {
|
|
|
|
|
+ // 导出到webdav
|
|
|
|
|
+ AppWebDav.exportWebDav(Uri.fromFile(bookFile), filename)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 解析 分割epub后的数量
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param total 章节总数
|
|
|
|
|
+ * @param size 每个epub文件包含多少章节
|
|
|
|
|
+ */
|
|
|
|
|
+ private fun paresNumOfEpub(total: Int, size: Int): Int {
|
|
|
|
|
+ val i = total % size
|
|
|
|
|
+ var result = total / size
|
|
|
|
|
+ if (i > 0) {
|
|
|
|
|
+ result++
|
|
|
|
|
+ }
|
|
|
|
|
+ return result
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|