From 05951a98ea90fdf0db866da3b5206018b6bc61e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B0=A2=E6=84=8F=E5=B8=86?= <1657098617@qq.com> Date: Tue, 17 May 2022 01:54:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=8A=E4=BC=A0=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=88=9D=E6=AD=A5=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/ui/book/info/BookInfoActivity.kt | 15 + .../app/ui/book/remote/RemoteBookActivity.kt | 9 +- .../app/ui/book/remote/RemoteBookAdapter.kt | 4 +- .../app/ui/book/remote/RemoteBookManager.kt | 51 ++++ .../app/ui/book/remote/RemoteBookViewModel.kt | 69 +++-- .../book/remote/manager/RemoteBookWebDav.kt | 260 ++++++++++++++++++ app/src/main/res/layout/item_remote_book.xml | 2 +- 7 files changed, 364 insertions(+), 46 deletions(-) create mode 100644 app/src/main/java/io/legado/app/ui/book/remote/RemoteBookManager.kt create mode 100644 app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt index efa1f1526..ce7562761 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt @@ -2,6 +2,7 @@ package io.legado.app.ui.book.info import android.annotation.SuppressLint import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -31,6 +32,7 @@ import io.legado.app.ui.book.changesource.ChangeBookSourceDialog import io.legado.app.ui.book.group.GroupSelectDialog import io.legado.app.ui.book.info.edit.BookInfoEditActivity import io.legado.app.ui.book.read.ReadBookActivity +import io.legado.app.ui.book.remote.manager.RemoteBookWebDav import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.toc.TocActivityResult @@ -198,6 +200,19 @@ class BookInfoActivity : item.isChecked = !item.isChecked if (!item.isChecked) longToastOnUi(R.string.need_more_time_load_content) } + + R.id.menu_upload -> { + launch { + val uri = Uri.parse(viewModel.bookData.value?.bookUrl.toString()) + RemoteBookWebDav.getFilePathFromContentUri(uri, contentResolver) +// val doc = DocumentFile.fromTreeUri(appCtx,uri) + + // if (uri.isContentScheme()){ +// uri.path +// } +// RemoteBookWebDav.upload() + } + } } return super.onCompatOptionsItemSelected(item) } diff --git a/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookActivity.kt index 4f8232a91..c011c5cdd 100644 --- a/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookActivity.kt @@ -1,19 +1,14 @@ package io.legado.app.ui.book.remote import android.os.Bundle -import android.util.Log import androidx.activity.viewModels import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.base.VMBaseActivity -import io.legado.app.data.entities.Book import io.legado.app.databinding.ActivityRemoteBookBinding -import io.legado.app.lib.theme.backgroundColor -import io.legado.app.utils.toastOnUi import io.legado.app.utils.viewbindingdelegate.viewBinding -import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.launch /** @@ -51,7 +46,7 @@ class RemoteBookActivity : VMBaseActivity @@ -66,6 +61,6 @@ class RemoteBookActivity : VMBaseActivity ) { binding.run { - tvName.text = item.name.substringBeforeLast(".") - tvContentType.text = item.name.substringAfterLast(".") + tvName.text = item.filename + tvContentType.text = item.contentType tvSize.text = ConvertUtils.formatFileSize(item.size) tvDate.text = LocalDateTimeUtil.format(LocalDateTimeUtil.of(item.lastModify), "yyyy-MM-dd") } diff --git a/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookManager.kt b/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookManager.kt new file mode 100644 index 000000000..2c4d428a5 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookManager.kt @@ -0,0 +1,51 @@ +package io.legado.app.ui.book.remote + +import android.content.ContentResolver +import android.database.Cursor +import android.net.Uri +import android.provider.MediaStore +import android.provider.OpenableColumns +import android.webkit.MimeTypeMap +import java.io.File +import kotlin.random.Random + + +abstract class RemoteBookManager { + protected val remoteBookFolder : String = "books" + protected val contentTypeList: ArrayList = arrayListOf("epub","txt") + abstract suspend fun initRemoteContext() + abstract suspend fun getRemoteBookList(): MutableList + abstract suspend fun upload(localBookUrl: String): Boolean + abstract suspend fun delete(remoteBookUrl: String): Boolean + abstract suspend fun getRemoteBook(remoteBookUrl: String): RemoteBook + + /** + * 把content uri转为 文件路径 + * + * @param contentUri 要转换的content uri + * @param contentResolver 解析器 + * @return + */ + + fun getFilePathFromContentUri( + contentUri: Uri, + contentResolver: ContentResolver + ): String? { + val filePath: String + if (contentUri.scheme == ContentResolver.SCHEME_FILE) + return File(requireNotNull(contentUri.path)).absolutePath + else if(contentUri.scheme == ContentResolver.SCHEME_CONTENT){ + val filePathColumn = arrayOf(MediaStore.MediaColumns.DATA) + val cursor: Cursor? = + contentResolver.query(contentUri, filePathColumn, null, null, null) + cursor!!.moveToFirst() + val columnIndex: Int = cursor.getColumnIndex(filePathColumn[0]) + filePath = cursor.getString(columnIndex) + cursor.close() + return filePath + } + return null + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookViewModel.kt index 31cef12b0..d1feebd02 100644 --- a/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/remote/RemoteBookViewModel.kt @@ -1,13 +1,15 @@ package io.legado.app.ui.book.remote import android.app.Application +import android.net.Uri import android.util.Log import io.legado.app.base.BaseViewModel import io.legado.app.constant.PreferKey import io.legado.app.lib.webdav.Authorization import io.legado.app.lib.webdav.WebDav +import io.legado.app.model.localBook.LocalBook +import io.legado.app.ui.book.remote.manager.RemoteBookWebDav import io.legado.app.utils.FileUtils -import io.legado.app.utils.exists import io.legado.app.utils.externalFiles import io.legado.app.utils.getPrefString import kotlinx.coroutines.* @@ -60,35 +62,18 @@ class RemoteBookViewModel(application: Application): BaseViewModel(application){ fun loadRemoteBookList() { execute { dataCallback?.clear() - kotlin.runCatching { - authorization = null - val account = appCtx.getPrefString(PreferKey.webDavAccount) - val password = appCtx.getPrefString(PreferKey.webDavPassword) - if (!account.isNullOrBlank() && !password.isNullOrBlank()) { - val mAuthorization = Authorization(account, password) - authorization = mAuthorization - } - } - authorization?.let { it -> - val remoteWebDavFileList = WebDav("http://txc.qianfanguojin.top/",it).listFiles() - val remoteList = remoteWebDavFileList.map { - RemoteBook(it.displayName,it.urlName,it.size,"epub",it.lastModify) - } - dataCallback?.setItems(remoteList) - } + RemoteBookWebDav.getRemoteBookList() + } // dataCallback?.setItems() } // dataCallback?.setItems(listOf("1", "2", "3")) - } + fun downloadRemoteBook(urlName: String) { + val saveFolder = "${appCtx.externalFiles.absolutePath}${File.separator}${remoteBookFolderName}" + val trueCodeURLName = String(urlName.toByteArray(Charset.forName("GBK")), Charset.forName("UTF-8")) + val saveFilePath = "${saveFolder}${trueCodeURLName}" execute { -// kotlin.runCatching { -// val remoteWebDavFile = WebDav("http://txc.qianfanguojin.top/",authorization!!).getFile(url) -// val remoteBook = RemoteBook(remoteWebDavFile.displayName,remoteWebDavFile.urlName,remoteWebDavFile.size,"epub",remoteWebDavFile.lastModify) -// dataCallback?.addItems(listOf(remoteBook)) -// } - kotlin.runCatching { authorization = null val account = appCtx.getPrefString(PreferKey.webDavAccount) @@ -100,20 +85,31 @@ class RemoteBookViewModel(application: Application): BaseViewModel(application){ } authorization?.let { it -> - Log.e("TAG", "downloadRemoteBook: 1", ) - val saveFolder = "${appCtx.externalFiles.absolutePath}${File.separator}${remoteBookFolderName}" FileUtils.createFolderIfNotExist(saveFolder).run{ - -// Log.e("TAG", "downloadRemoteBook: 2 ${appCtx.externalFiles.absoluteFile}", ) - val trueCodeURLName = String(urlName.toByteArray(Charset.forName("GBK")), Charset.forName("UTF-8")) - withTimeout(15000L) { - val webdav = WebDav("http://txc.qianfanguojin.top${trueCodeURLName}", it) - webdav.downloadTo("${saveFolder}${trueCodeURLName}", true).apply { + withTimeout(15000L) { + val webdav = WebDav( + "http://txc.qianfanguojin.top${trueCodeURLName}", + it + ) + webdav.downloadTo(saveFilePath, true) } } - } + } + }.onFinally { + addToBookshelf(hashSetOf("${saveFolder}${trueCodeURLName}")){ + Log.e("TAG", "downloadRemoteBook: add", ) } } + + } + fun addToBookshelf(uriList: HashSet, finally: () -> Unit) { + execute { + uriList.forEach { + LocalBook.importFile(Uri.parse(it)) + } + }.onFinally { + finally.invoke() + } } interface DataCallback { @@ -127,9 +123,10 @@ class RemoteBookViewModel(application: Application): BaseViewModel(application){ } data class RemoteBook( - val name: String, - val url: String, + val filename: String, + val urlName: String, val size: Long, val contentType: String, val lastModify: Long -) \ No newline at end of file +) + diff --git a/app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt b/app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt new file mode 100644 index 000000000..5bbe4e746 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt @@ -0,0 +1,260 @@ +package io.legado.app.ui.book.remote.manager + + +import io.legado.app.constant.PreferKey + +import io.legado.app.exception.NoStackTraceException +import io.legado.app.help.config.AppConfig + +import io.legado.app.lib.webdav.Authorization +import io.legado.app.lib.webdav.WebDav +import io.legado.app.lib.webdav.WebDavFile + +import io.legado.app.ui.book.remote.RemoteBook +import io.legado.app.ui.book.remote.RemoteBookManager +import io.legado.app.utils.* +import kotlinx.coroutines.runBlocking +import splitties.init.appCtx +import java.io.File +import java.nio.charset.Charset + +object RemoteBookWebDav : RemoteBookManager() { + private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/" + private var authorization: Authorization? = null + private val remoteBookUrl get() = "${rootWebDavUrl}${remoteBookFolder}" + + init { + runBlocking { + initRemoteContext() + } + } + + private val rootWebDavUrl: String + get() { + val configUrl = appCtx.getPrefString(PreferKey.webDavUrl)?.trim() + var url = if (configUrl.isNullOrEmpty()) defaultWebDavUrl else configUrl + if (!url.endsWith("/")) url = "${url}/" + AppConfig.webDavDir?.trim()?.let { + if (it.isNotEmpty()) { + url = "${url}${it}/" + } + } + return url + } + + override suspend fun initRemoteContext() { + kotlin.runCatching { + authorization = null + val account = appCtx.getPrefString(PreferKey.webDavAccount) + val password = appCtx.getPrefString(PreferKey.webDavPassword) + if (!account.isNullOrBlank() && !password.isNullOrBlank()) { + val mAuthorization = Authorization(account, password) + WebDav(rootWebDavUrl, mAuthorization).makeAsDir() + WebDav(remoteBookUrl, mAuthorization).makeAsDir() + authorization = mAuthorization + } + }.onFailure { + it.printStackTrace() + } + } + + @Throws(Exception::class) + override suspend fun getRemoteBookList(): MutableList { + val remoteBooks = mutableListOf() + + authorization?.let { + var remoteWebDavFileList : List? = null + kotlin.runCatching { + remoteWebDavFileList = WebDav(remoteBookUrl, it).listFiles() + } + + + remoteWebDavFileList = remoteWebDavFileList!!.reversed() + remoteWebDavFileList!!.forEach { webDavFile -> + val webDavFileName = webDavFile.displayName + val webDavUrlName = webDavFile.urlName + + // 转码 + val trueFileName = String(webDavFileName.toByteArray(Charset.forName("GBK")), Charset.forName("UTF-8")) + val trueUrlName = String(webDavUrlName.toByteArray(Charset.forName("GBK")), Charset.forName("UTF-8")) + + //分割文件名和后缀 + val filename = trueFileName.substringBeforeLast(".") + val fileExtension = trueFileName.substringAfterLast(".") + + //扩展名符合阅读的格式则认为是书籍 + if (contentTypeList.contains(fileExtension)) { + remoteBooks.add(RemoteBook(filename,trueUrlName,webDavFile.size,fileExtension,webDavFile.lastModify)) + } + } + } ?: throw NoStackTraceException("webDav没有配置") + return remoteBooks + } + + override suspend fun getRemoteBook(remoteBookUrl: String): RemoteBook { + TODO("Not yet implemented") + } + + /** + * 上传本地导入的书籍到远程 + */ + override suspend fun upload(localBookUrl: String): Boolean { + if (!NetworkUtils.isAvailable()) return false + val localBookName = localBookUrl.substringAfterLast(File.separator) + authorization?.let { + val putUrl = "${remoteBookUrl}${File.separator}${localBookName}" + WebDav(putUrl, it).upload(localBookUrl) + } + return true + } + + override suspend fun delete(remoteBookUrl: String): Boolean { + TODO("Not yet implemented") + } + +// suspend fun showRestoreDialog(context: Context) { +// val names = withContext(Dispatchers.IO) { getBackupNames() } +// if (names.isNotEmpty()) { +// withContext(Dispatchers.Main) { +// context.selector( +// title = context.getString(R.string.select_restore_file), +// items = names +// ) { _, index -> +// if (index in 0 until names.size) { +// Coroutine.async { +// restoreWebDav(names[index]) +// }.onError { +// appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}") +// } +// } +// } +// } +// } else { +// throw NoStackTraceException("Web dav no back up file") +// } +// } +// +// @Throws(WebDavException::class) +// suspend fun restoreWebDav(name: String) { +// authorization?.let { +// val webDav = WebDav(rootWebDavUrl + name, it) +// webDav.downloadTo(zipFilePath, true) +// @Suppress("BlockingMethodInNonBlockingContext") +// ZipUtils.unzipFile(zipFilePath, Backup.backupPath) +// Restore.restoreDatabase() +// Restore.restoreConfig() +// } +// } +// +// suspend fun hasBackUp(): Boolean { +// authorization?.let { +// val url = "${rootWebDavUrl}${backupFileName}" +// return WebDav(url, it).exists() +// } +// return false +// } +// +// suspend fun lastBackUp(): Result { +// return kotlin.runCatching { +// authorization?.let { +// var lastBackupFile: WebDavFile? = null +// WebDav(rootWebDavUrl, it).listFiles().reversed().forEach { webDavFile -> +// if (webDavFile.displayName.startsWith("backup")) { +// if (lastBackupFile == null +// || webDavFile.lastModify > lastBackupFile!!.lastModify +// ) { +// lastBackupFile = webDavFile +// } +// } +// } +// lastBackupFile +// } +// } +// } +// +// @Throws(Exception::class) +// suspend fun backUpWebDav(path: String) { +// if (!NetworkUtils.isAvailable()) return +// authorization?.let { +// val paths = arrayListOf(*Backup.backupFileNames) +// for (i in 0 until paths.size) { +// paths[i] = path + File.separator + paths[i] +// } +// FileUtils.delete(zipFilePath) +// if (ZipUtils.zipFiles(paths, zipFilePath)) { +// val putUrl = "${rootWebDavUrl}${backupFileName}" +// WebDav(putUrl, it).upload(zipFilePath) +// } +// } +// } +// +// suspend fun exportWebDav(byteArray: ByteArray, fileName: String) { +// if (!NetworkUtils.isAvailable()) return +// try { +// authorization?.let { +// // 如果导出的本地文件存在,开始上传 +// val putUrl = exportsWebDavUrl + fileName +// WebDav(putUrl, it).upload(byteArray, "text/plain") +// } +// } catch (e: Exception) { +// val msg = "WebDav导出\n${e.localizedMessage}" +// AppLog.put(msg) +// appCtx.toastOnUi(msg) +// } +// } +// +// fun uploadBookProgress(book: Book) { +// val authorization = authorization ?: return +// if (!syncBookProgress) return +// if (!NetworkUtils.isAvailable()) return +// Coroutine.async { +// val bookProgress = BookProgress(book) +// val json = GSON.toJson(bookProgress) +// val url = getProgressUrl(book) +// WebDav(url, authorization).upload(json.toByteArray(), "application/json") +// }.onError { +// AppLog.put("上传进度失败\n${it.localizedMessage}") +// } +// } +// +// private fun getProgressUrl(book: Book): String { +// return bookProgressUrl + book.name + "_" + book.author + ".json" +// } +// +// /** +// * 获取书籍进度 +// */ +// suspend fun getBookProgress(book: Book): BookProgress? { +// authorization?.let { +// val url = getProgressUrl(book) +// kotlin.runCatching { +// WebDav(url, it).download().let { byteArray -> +// val json = String(byteArray) +// if (json.isJson()) { +// return GSON.fromJsonObject(json).getOrNull() +// } +// } +// } +// } +// return null +// } +// +// suspend fun downloadAllBookProgress() { +// authorization ?: return +// if (!NetworkUtils.isAvailable()) return +// appDb.bookDao.all.forEach { book -> +// getBookProgress(book)?.let { bookProgress -> +// if (bookProgress.durChapterIndex > book.durChapterIndex +// || (bookProgress.durChapterIndex == book.durChapterIndex +// && bookProgress.durChapterPos > book.durChapterPos) +// ) { +// book.durChapterIndex = bookProgress.durChapterIndex +// book.durChapterPos = bookProgress.durChapterPos +// book.durChapterTitle = bookProgress.durChapterTitle +// book.durChapterTime = bookProgress.durChapterTime +// appDb.bookDao.update(book) +// } +// } +// } +// } +} \ No newline at end of file diff --git a/app/src/main/res/layout/item_remote_book.xml b/app/src/main/res/layout/item_remote_book.xml index ad986eaa6..4d5ae1264 100644 --- a/app/src/main/res/layout/item_remote_book.xml +++ b/app/src/main/res/layout/item_remote_book.xml @@ -55,7 +55,7 @@ android:layout_height="wrap_content" android:ellipsize="end" android:lines="1" - android:text="\u4e28" + android:text="丨" android:textColor="@color/tv_text_summary" android:textSize="11sp" />