This commit is contained in:
Horis
2025-02-22 15:37:21 +08:00
parent 3d9744ed2e
commit f6bd4a1704
7 changed files with 139 additions and 274 deletions
@@ -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()