导出添加进度

pull/1352/head
gedoor 3 years ago
parent 35a1b51969
commit d03729f964
  1. 45
      app/src/main/java/io/legado/app/ui/book/cache/CacheActivity.kt
  2. 29
      app/src/main/java/io/legado/app/ui/book/cache/CacheAdapter.kt
  3. 194
      app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt
  4. 46
      app/src/main/res/layout/item_download.xml

@ -6,7 +6,6 @@ import android.view.Menu
import android.view.MenuItem
import androidx.activity.viewModels
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import io.legado.app.R
import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppConst
@ -208,6 +207,13 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
}
override fun observeLiveBus() {
viewModel.upAdapterLiveData.observe(this) {
adapter.getItems().forEachIndexed { index, book ->
if (book.bookUrl == it) {
adapter.notifyItemChanged(index, true)
}
}
}
observeEvent<String>(EventBus.UP_DOWNLOAD) {
if (!CacheBook.isRun) {
menu?.findItem(R.id.menu_download)?.let { item ->
@ -263,25 +269,10 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
private fun startExport(path: String) {
if (exportPosition == -10) {
if (adapter.getItems().isNotEmpty()) {
Snackbar.make(binding.titleBar, R.string.exporting, Snackbar.LENGTH_INDEFINITE)
.show()
var exportSize = adapter.getItems().size
adapter.getItems().forEach { book ->
when (AppConfig.exportType) {
1 -> viewModel.exportEPUB(path, book) {
exportSize--
toastOnUi(it)
if (exportSize <= 0) {
binding.titleBar.snackbar(R.string.complete)
}
}
else -> viewModel.export(path, book) {
exportSize--
toastOnUi(it)
if (exportSize <= 0) {
binding.titleBar.snackbar(R.string.complete)
}
}
1 -> viewModel.exportEPUB(path, book)
else -> viewModel.export(path, book)
}
}
} else {
@ -289,15 +280,9 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
}
} else if (exportPosition >= 0) {
adapter.getItem(exportPosition)?.let { book ->
Snackbar.make(binding.titleBar, R.string.exporting, Snackbar.LENGTH_INDEFINITE)
.show()
when (AppConfig.exportType) {
1 -> viewModel.exportEPUB(path, book) {
binding.titleBar.snackbar(it)
}
else -> viewModel.export(path, book) {
binding.titleBar.snackbar(it)
}
1 -> viewModel.exportEPUB(path, book)
else -> viewModel.export(path, book)
}
}
}
@ -346,4 +331,12 @@ class CacheActivity : VMBaseActivity<ActivityCacheBookBinding, CacheViewModel>()
}.show()
}
override fun exportProgress(bookUrl: String): Int? {
return viewModel.exportProgress[bookUrl]
}
override fun exportMsg(bookUrl: String): String? {
return viewModel.exportMsg[bookUrl]
}
}

@ -3,12 +3,16 @@ package io.legado.app.ui.book.cache
import android.content.Context
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import io.legado.app.R
import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.data.entities.Book
import io.legado.app.databinding.ItemDownloadBinding
import io.legado.app.model.CacheBook
import io.legado.app.utils.gone
import io.legado.app.utils.visible
class CacheAdapter(context: Context, private val callBack: CallBack) :
RecyclerAdapter<Book, ItemDownloadBinding>(context) {
@ -36,13 +40,13 @@ class CacheAdapter(context: Context, private val callBack: CallBack) :
tvDownload.text =
context.getString(R.string.download_count, cs.size, item.totalChapterNum)
}
upDownloadIv(ivDownload, item)
} else {
val cacheSize = cacheChapters[item.bookUrl]?.size ?: 0
tvDownload.text =
context.getString(R.string.download_count, cacheSize, item.totalChapterNum)
upDownloadIv(ivDownload, item)
}
upDownloadIv(ivDownload, item)
upExportInfo(tvMsg, progressExport, item)
}
}
@ -79,7 +83,28 @@ class CacheAdapter(context: Context, private val callBack: CallBack) :
}
}
private fun upExportInfo(msgView: TextView, progressView: ProgressBar, book: Book) {
val msg = callBack.exportMsg(book.bookUrl)
if (msg != null) {
msgView.text = msg
msgView.visible()
progressView.gone()
return
}
msgView.gone()
val progress = callBack.exportProgress(book.bookUrl)
if (progress != null) {
progressView.max = book.totalChapterNum
progressView.progress = progress
progressView.visible()
return
}
progressView.gone()
}
interface CallBack {
fun export(position: Int)
fun exportProgress(bookUrl: String): Int?
fun exportMsg(bookUrl: String): String?
}
}

@ -5,6 +5,7 @@ import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.MutableLiveData
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
@ -19,7 +20,10 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.BookHelp
import io.legado.app.help.ContentProcessor
import io.legado.app.help.storage.AppWebDav
import io.legado.app.model.NoStackTraceException
import io.legado.app.utils.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ensureActive
import me.ag2s.epublib.domain.*
import me.ag2s.epublib.epub.EpubWriter
import me.ag2s.epublib.util.ResourceUtil
@ -28,10 +32,14 @@ import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.nio.charset.Charset
import java.util.concurrent.ConcurrentHashMap
import javax.script.SimpleBindings
class CacheViewModel(application: Application) : BaseViewModel(application) {
val upAdapterLiveData = MutableLiveData<String>()
val exportProgress = ConcurrentHashMap<String, Int>()
val exportMsg = ConcurrentHashMap<String, String>()
private fun getExportFileName(book: Book): String {
val jsStr = AppConfig.bookExportFileName
@ -44,100 +52,111 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
return AppConst.SCRIPT_ENGINE.eval(jsStr, bindings).toString()
}
fun export(path: String, book: Book, finally: (msg: String) -> Unit) {
fun export(path: String, book: Book) {
if (exportProgress.contains(book.bookUrl)) return
exportProgress[book.bookUrl] = 0
exportMsg.remove(book.bookUrl)
upAdapterLiveData.postValue(book.bookUrl)
execute {
if (path.isContentScheme()) {
val uri = Uri.parse(path)
DocumentFile.fromTreeUri(context, uri)?.let {
export(it, book)
}
val doc = DocumentFile.fromTreeUri(context, uri)
?: throw NoStackTraceException("获取导出文档失败")
export(this, doc, book)
} else {
export(FileUtils.createFolderIfNotExist(path), book)
export(this, FileUtils.createFolderIfNotExist(path), book)
}
}.onError {
finally(it.localizedMessage ?: "ERROR")
exportProgress.remove(book.bookUrl)
exportMsg[book.bookUrl] = it.localizedMessage ?: "ERROR"
upAdapterLiveData.postValue(book.bookUrl)
it.printOnDebug()
}.onSuccess {
finally(context.getString(R.string.success))
exportProgress.remove(book.bookUrl)
exportMsg[book.bookUrl] = context.getString(R.string.export_success)
upAdapterLiveData.postValue(book.bookUrl)
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun export(doc: DocumentFile, book: Book) {
private suspend fun export(scope: CoroutineScope, doc: DocumentFile, book: Book) {
val filename = "${getExportFileName(book)}.txt"
DocumentUtils.delete(doc, filename)
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
val stringBuilder = StringBuilder()
context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
getAllContents(book) {
bookOs.write(it.toByteArray(Charset.forName(AppConfig.exportCharset)))
stringBuilder.append(it)
val bookDoc = DocumentUtils.createFileIfNotExist(doc, filename)
?: throw NoStackTraceException("创建文档失败")
val stringBuilder = StringBuilder()
context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
getAllContents(scope, book) { text, srcList ->
bookOs.write(text.toByteArray(Charset.forName(AppConfig.exportCharset)))
stringBuilder.append(text)
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(context, vFile.readBytes())
}
}
}
if (AppConfig.exportToWebDav) {
// 导出到webdav
val byteArray =
stringBuilder.toString().toByteArray(Charset.forName(AppConfig.exportCharset))
AppWebDav.exportWebDav(byteArray, filename)
}
}
getSrcList(book).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(context, vFile.readBytes())
}
if (AppConfig.exportToWebDav) {
// 导出到webdav
val byteArray =
stringBuilder.toString().toByteArray(Charset.forName(AppConfig.exportCharset))
AppWebDav.exportWebDav(byteArray, filename)
}
}
private suspend fun export(file: File, book: Book) {
private suspend fun export(scope: CoroutineScope, file: File, book: Book) {
val filename = "${getExportFileName(book)}.txt"
val bookPath = FileUtils.getPath(file, filename)
val bookFile = FileUtils.createFileWithReplace(bookPath)
val stringBuilder = StringBuilder()
getAllContents(book) {
bookFile.appendText(it, Charset.forName(AppConfig.exportCharset))
stringBuilder.append(it)
getAllContents(scope, book) { text, srcList ->
bookFile.appendText(text, Charset.forName(AppConfig.exportCharset))
stringBuilder.append(text)
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) {
val byteArray =
stringBuilder.toString().toByteArray(Charset.forName(AppConfig.exportCharset))
AppWebDav.exportWebDav(byteArray, filename) // 导出到webdav
}
getSrcList(book).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())
}
}
}
private fun getAllContents(book: Book, append: (text: String) -> Unit) {
private fun getAllContents(
scope: CoroutineScope,
book: Book,
append: (text: String, srcList: ArrayList<Triple<String, Int, String>>?) -> Unit
) {
val useReplace = AppConfig.exportUseReplace
val contentProcessor = ContentProcessor.get(book.name, book.origin)
append(
"${book.name}\n${
context.getString(
R.string.author_show,
book.getRealAuthor()
)
}\n${
context.getString(
R.string.intro_show,
"\n" + HtmlFormatter.format(book.getDisplayIntro())
)
}"
)
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
val qy = "${book.name}\n${
context.getString(R.string.author_show, book.getRealAuthor())
}\n${
context.getString(
R.string.intro_show,
"\n" + HtmlFormatter.format(book.getDisplayIntro())
)
}"
append(qy, null)
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
scope.ensureActive()
upAdapterLiveData.postValue(book.bookUrl)
exportProgress[book.bookUrl] = index
BookHelp.getContent(book, chapter).let { content ->
val content1 = contentProcessor
.getContent(
@ -149,16 +168,8 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
chineseConvert = false,
reSegment = false
).joinToString("\n")
append.invoke("\n\n$content1")
}
}
}
private fun getSrcList(book: Book): ArrayList<Triple<String, Int, String>> {
val srcList = arrayListOf<Triple<String, Int, String>>()
appDb.bookChapterDao.getChapterList(book.bookUrl).forEach { chapter ->
BookHelp.getContent(book, chapter)?.let { content ->
content.split("\n").forEachIndexed { index, text ->
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 {
@ -167,34 +178,43 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
}
}
}
append.invoke("\n\n$content1", srcList)
}
}
return srcList
}
//////////////////Start EPUB
/**
* 导出Epub
*/
fun exportEPUB(path: String, book: Book, finally: (msg: String) -> Unit) {
fun exportEPUB(path: String, book: Book) {
if (exportProgress.contains(book.bookUrl)) return
exportProgress[book.bookUrl] = 0
exportMsg.remove(book.bookUrl)
upAdapterLiveData.postValue(book.bookUrl)
execute {
if (path.isContentScheme()) {
val uri = Uri.parse(path)
DocumentFile.fromTreeUri(context, uri)?.let {
exportEpub(it, book)
}
val doc = DocumentFile.fromTreeUri(context, uri)
?: throw NoStackTraceException("获取导出文档失败")
exportEpub(this, doc, book)
} else {
exportEpub(FileUtils.createFolderIfNotExist(path), book)
exportEpub(this, FileUtils.createFolderIfNotExist(path), book)
}
}.onError {
finally(it.localizedMessage ?: "ERROR")
exportProgress.remove(book.bookUrl)
exportMsg[book.bookUrl] = it.localizedMessage ?: "ERROR"
upAdapterLiveData.postValue(book.bookUrl)
it.printOnDebug()
}.onSuccess {
finally(context.getString(R.string.success))
exportProgress.remove(book.bookUrl)
exportMsg[book.bookUrl] = context.getString(R.string.export_success)
upAdapterLiveData.postValue(book.bookUrl)
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private fun exportEpub(doc: DocumentFile, book: Book) {
private fun exportEpub(scope: CoroutineScope, doc: DocumentFile, book: Book) {
val filename = "${getExportFileName(book)}.epub"
DocumentUtils.delete(doc, filename)
val epubBook = EpubBook()
@ -207,7 +227,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
val contentModel = setAssets(doc, book, epubBook)
//设置正文
setEpubContent(contentModel, book, epubBook)
setEpubContent(scope, contentModel, book, epubBook)
DocumentUtils.createFileIfNotExist(doc, filename)?.let { bookDoc ->
context.contentResolver.openOutputStream(bookDoc.uri, "wa")?.use { bookOs ->
EpubWriter().write(epubBook, bookOs)
@ -217,7 +237,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
}
private fun exportEpub(file: File, book: Book) {
private fun exportEpub(scope: CoroutineScope, file: File, book: Book) {
val filename = "${getExportFileName(book)}.epub"
val epubBook = EpubBook()
epubBook.version = "2.0"
@ -231,7 +251,7 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
val bookPath = FileUtils.getPath(file, filename)
val bookFile = FileUtils.createFileWithReplace(bookPath)
//设置正文
setEpubContent(contentModel, book, epubBook)
setEpubContent(scope, contentModel, book, epubBook)
EpubWriter().write(epubBook, FileOutputStream(bookFile))
}
@ -386,11 +406,19 @@ class CacheViewModel(application: Application) : BaseViewModel(application) {
})
}
private fun setEpubContent(contentModel: String, book: Book, epubBook: EpubBook) {
private fun setEpubContent(
scope: CoroutineScope,
contentModel: String,
book: Book,
epubBook: EpubBook
) {
//正文
val useReplace = AppConfig.exportUseReplace
val contentProcessor = ContentProcessor.get(book.name, book.origin)
appDb.bookChapterDao.getChapterList(book.bookUrl).forEachIndexed { index, chapter ->
scope.ensureActive()
upAdapterLiveData.postValue(book.bookUrl)
exportProgress[book.bookUrl] = index
BookHelp.getContent(book, chapter).let { content ->
var content1 = fixPic(epubBook, book, content ?: "null", chapter)
content1 = contentProcessor

@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
@ -12,10 +12,10 @@
android:layout_height="wrap_content"
android:singleLine="true"
android:textColor="@color/primaryText"
tools:text="@string/name"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/iv_download"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
tools:text="@string/name" />
<TextView
android:id="@+id/tv_author"
@ -23,10 +23,10 @@
android:layout_height="wrap_content"
android:singleLine="true"
android:textColor="@color/primaryText"
tools:text="@string/author"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/iv_download"
app:layout_constraintTop_toBottomOf="@+id/tv_name"
app:layout_constraintLeft_toLeftOf="parent" />
tools:text="@string/author" />
<TextView
android:id="@+id/tv_download"
@ -34,22 +34,22 @@
android:layout_height="wrap_content"
android:singleLine="true"
android:textColor="@color/secondaryText"
tools:text="@string/action_download"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toLeftOf="@+id/iv_download"
app:layout_constraintTop_toBottomOf="@id/tv_author"
app:layout_constraintLeft_toLeftOf="parent" />
tools:text="@string/action_download" />
<ImageView
android:id="@+id/iv_download"
android:layout_width="36dp"
android:layout_height="36dp"
android:padding="6dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_play_24dp"
android:contentDescription="@string/start"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:padding="6dp"
android:src="@drawable/ic_play_24dp"
app:layout_constraintBottom_toBottomOf="@+id/tv_download"
app:layout_constraintRight_toLeftOf="@+id/tv_export"
app:layout_constraintTop_toTopOf="@id/tv_name"
app:tint="@color/primaryText" />
<TextView
@ -60,8 +60,24 @@
android:padding="10dp"
android:text="@string/export"
android:textColor="@color/primaryText"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent" />
app:layout_constraintBottom_toBottomOf="@+id/tv_download"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="@id/tv_name" />
<TextView
android:id="@+id/tv_msg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@color/secondaryText"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/tv_download" />
<io.legado.app.lib.theme.view.ATEProgressBar
android:id="@+id/progress_export"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/tv_download" />
</androidx.constraintlayout.widget.ConstraintLayout>
Loading…
Cancel
Save