Просмотр исходного кода

Merge remote-tracking branch 'origin/master'

kunfei 4 лет назад
Родитель
Сommit
0dcb0810ec

+ 4 - 0
app/src/main/AndroidManifest.xml

@@ -281,6 +281,10 @@
         <activity
             android:name=".ui.book.local.ImportBookActivity"
             android:launchMode="singleTop" />
+        <!-- 添加远程 -->
+        <activity
+            android:name=".ui.book.remote.RemoteBookActivity"
+            android:launchMode="singleTop" />
         <!-- 发现界面 -->
         <activity
             android:name=".ui.book.explore.ExploreShowActivity"

+ 4 - 0
app/src/main/assets/updateLog.md

@@ -11,6 +11,10 @@
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 
+**2022/05/18**
+
+* 修复更改本地文件后每次打开都刷新目录的bug
+
 **2022/05/16**
 
 * 添加firebase性能监测

+ 2 - 2
app/src/main/assets/web/bookshelf/index.html

@@ -1,3 +1,3 @@
-<!DOCTYPE html><html lang="en" style="padding: 0;height:100%"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="../favicon.ico" type="image/x-icon"><link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"><title>Legado Bookshelf</title><link href="css/about.65a00131.css" rel="prefetch"><link href="css/detail.12a39aa7.css" rel="prefetch"><link href="js/about.2456ab2e.js" rel="prefetch"><link href="js/detail.8e2caf8f.js" rel="prefetch"><link href="css/app.e4c919b7.css" rel="preload" as="style"><link href="css/chunk-vendors.bd1373b6.css" rel="preload" as="style"><link href="js/app.a68b7203.js" rel="preload" as="script"><link href="js/chunk-vendors.f36dda93.js" rel="preload" as="script"><link href="css/chunk-vendors.bd1373b6.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"></head><style>body::-webkit-scrollbar {
+<!DOCTYPE html><html lang="zh-CN" style="padding: 0;height:100%"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="../favicon.ico" type="image/x-icon"><link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"><title>Legado Bookshelf</title><link href="css/about.65a00131.css" rel="prefetch"><link href="css/detail.12a39aa7.css" rel="prefetch"><link href="js/about.cee6f6d7.js" rel="prefetch"><link href="js/detail.bb04bd6b.js" rel="prefetch"><link href="css/app.e4c919b7.css" rel="preload" as="style"><link href="css/chunk-vendors.bd1373b6.css" rel="preload" as="style"><link href="js/app.aa604b87.js" rel="preload" as="script"><link href="js/chunk-vendors.ca94fdd0.js" rel="preload" as="script"><link href="css/chunk-vendors.bd1373b6.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"></head><style>body::-webkit-scrollbar {
         display: none;
-    }</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.f36dda93.js"></script><script src="js/app.a68b7203.js"></script></body></html>
+    }</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.ca94fdd0.js"></script><script src="js/app.aa604b87.js"></script></body></html>

+ 0 - 0
app/src/main/assets/web/bookshelf/js/about.2456ab2e.js → app/src/main/assets/web/bookshelf/js/about.cee6f6d7.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/src/main/assets/web/bookshelf/js/app.aa604b87.js


Разница между файлами не показана из-за своего большого размера
+ 0 - 0
app/src/main/assets/web/bookshelf/js/chunk-vendors.ca94fdd0.js


+ 0 - 0
app/src/main/assets/web/bookshelf/js/detail.8e2caf8f.js → app/src/main/assets/web/bookshelf/js/detail.bb04bd6b.js


+ 8 - 6
app/src/main/java/io/legado/app/lib/webdav/WebDav.kt

@@ -19,14 +19,16 @@ import java.io.InputStream
 import java.net.MalformedURLException
 import java.net.URL
 import java.net.URLEncoder
-import java.text.SimpleDateFormat
+import java.time.LocalDateTime
+import java.time.ZoneOffset
+import java.time.format.DateTimeFormatter
 
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 open class WebDav(urlStr: String, val authorization: Authorization) {
     companion object {
 
-        @SuppressLint("SimpleDateFormat")
-        private val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss ZZZ")
+        @SuppressLint("DateTimeFormatter")
+        private val dateTimeFormatter = DateTimeFormatter.RFC_1123_DATE_TIME
 
         // 指定返回哪些属性
         @Language("xml")
@@ -133,11 +135,11 @@ open class WebDav(urlStr: String, val authorization: Authorization) {
                                 .firstOrNull()?.text()?.toLong() ?: 0
                         }.getOrDefault(0)
                         val lastModify: Long = kotlin.runCatching {
-                            element.getElementsByTag("d:getcontentlength")
+                            element.getElementsByTag("d:getlastmodified")
                                 .firstOrNull()?.text()?.let {
-                                    dateFormat.parse(it)
+                                    LocalDateTime.parse(it, dateTimeFormatter).toInstant(ZoneOffset.of("+8")).toEpochMilli()
                                 }
-                        }.getOrNull()?.time ?: 0
+                        }.getOrNull() ?: 0
                         webDavFile = WebDavFile(
                             baseUrl + fileName,
                             authorization,

+ 15 - 0
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
@@ -33,6 +34,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
@@ -129,6 +131,8 @@ class BookInfoActivity :
             viewModel.bookSource != null
         menu.findItem(R.id.menu_split_long_chapter)?.isVisible =
             viewModel.bookData.value?.isLocalTxt() ?: false
+        menu.findItem(R.id.menu_upload)?.isVisible =
+            viewModel.bookData.value?.isLocalBook() ?: false
         return super.onMenuOpened(featureId, menu)
     }
 
@@ -198,6 +202,17 @@ 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())
+                    if (RemoteBookWebDav.upload(uri))
+                        toastOnUi(getString(R.string.upload_book_success))
+                    else
+                        toastOnUi(getString(R.string.upload_book_fail))
+
+                }
+            }
         }
         return super.onCompatOptionsItemSelected(item)
     }

+ 1 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt

@@ -120,6 +120,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
         if (book.isLocalBook()) {
             execute {
                 LocalBook.getChapterList(book).let {
+                    book.latestChapterTime = System.currentTimeMillis()
                     appDb.bookChapterDao.delByBook(book.bookUrl)
                     appDb.bookChapterDao.insert(*it.toTypedArray())
                     appDb.bookDao.update(book)

+ 65 - 0
app/src/main/java/io/legado/app/ui/book/remote/RemoteBookActivity.kt

@@ -0,0 +1,65 @@
+package io.legado.app.ui.book.remote
+
+import android.annotation.SuppressLint
+import android.os.Bundle
+import androidx.activity.viewModels
+import androidx.recyclerview.widget.LinearLayoutManager
+import io.legado.app.R
+import io.legado.app.base.VMBaseActivity
+
+
+import io.legado.app.databinding.ActivityRemoteBookBinding
+import io.legado.app.utils.toastOnUi
+
+import io.legado.app.utils.viewbindingdelegate.viewBinding
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.launch
+
+/**
+ * 展示远程书籍
+ * @author qianfanguojin
+ * @time 2022/05/12
+ */
+class RemoteBookActivity : VMBaseActivity<ActivityRemoteBookBinding,RemoteBookViewModel>(),
+    RemoteBookAdapter.CallBack {
+    override val binding by viewBinding(ActivityRemoteBookBinding::inflate)
+    override val viewModel by viewModels<RemoteBookViewModel>()
+    private val adapter by lazy { RemoteBookAdapter(this, this) }
+
+    override fun onActivityCreated(savedInstanceState: Bundle?) {
+        initView()
+//        initEvent()
+        initData()
+//        toastOnUi("远程书籍")
+        onFinally()
+    }
+
+
+
+
+    private fun initView() {
+        binding.recyclerView.layoutManager = LinearLayoutManager(this)
+        binding.recyclerView.adapter = adapter
+    }
+    private fun initData() {
+        binding.refreshProgressBar.isAutoLoading = true
+        viewModel.loadRemoteBookList()
+        launch {
+            viewModel.dataFlow.conflate().collect { remoteBooks ->
+                adapter.setItems(remoteBooks)
+            }
+            binding.refreshProgressBar.isAutoLoading = false
+        }
+    }
+
+    private fun onFinally() {
+
+    }
+    @SuppressLint("NotifyDataSetChanged")
+    override fun addToBookshelf(remoteBook: RemoteBook) {
+        viewModel.addToBookshelf(remoteBook){
+            toastOnUi(getString(R.string.download_book_fail))
+            adapter.notifyDataSetChanged()
+        }
+    }
+}

+ 58 - 0
app/src/main/java/io/legado/app/ui/book/remote/RemoteBookAdapter.kt

@@ -0,0 +1,58 @@
+package io.legado.app.ui.book.remote
+
+import android.content.Context
+import android.view.ViewGroup
+import cn.hutool.core.date.LocalDateTimeUtil
+import io.legado.app.base.adapter.ItemViewHolder
+import io.legado.app.base.adapter.RecyclerAdapter
+import io.legado.app.databinding.ItemRemoteBookBinding
+import io.legado.app.utils.ConvertUtils
+
+
+/**
+ * 适配器
+ * @author qianfanguojin
+ */
+class RemoteBookAdapter (context: Context, val callBack: CallBack) :
+    RecyclerAdapter<RemoteBook, ItemRemoteBookBinding>(context){
+
+    override fun getViewBinding(parent: ViewGroup): ItemRemoteBookBinding {
+        return ItemRemoteBookBinding.inflate(inflater, parent, false)
+    }
+
+    override fun onCurrentListChanged() {
+
+    }
+
+    /**
+     * 绑定RecycleView 中每一个项的视图和数据
+     */
+    override fun convert(
+        holder: ItemViewHolder,
+        binding: ItemRemoteBookBinding,
+        item: RemoteBook,
+        payloads: MutableList<Any>
+    ) {
+        binding.run {
+            //Todo:需要判断书籍是否已经加入书架,来改变“下载”按钮的文本,暂时还没有比较好的方案
+            tvName.text = item.filename.substringBeforeLast(".")
+            tvContentType.text = item.contentType
+            tvSize.text = ConvertUtils.formatFileSize(item.size)
+            tvDate.text = LocalDateTimeUtil.format(LocalDateTimeUtil.of(item.lastModify), "yyyy-MM-dd")
+        }
+    }
+
+    override fun registerListener(holder: ItemViewHolder, binding: ItemRemoteBookBinding) {
+        binding.btnDownload.setOnClickListener {
+                getItem(holder.layoutPosition)?.let {
+                    callBack.addToBookshelf(it)
+                }
+        }
+
+
+    }
+
+    interface CallBack {
+        fun addToBookshelf(remoteBook: RemoteBook)
+    }
+}

+ 19 - 0
app/src/main/java/io/legado/app/ui/book/remote/RemoteBookManager.kt

@@ -0,0 +1,19 @@
+package io.legado.app.ui.book.remote
+
+import android.net.Uri
+
+
+
+abstract class RemoteBookManager {
+    protected val remoteBookFolder : String = "books"
+    protected val contentTypeList: ArrayList<String> = arrayListOf("epub","txt")
+    abstract suspend fun initRemoteContext()
+    abstract suspend fun getRemoteBookList(): MutableList<RemoteBook>
+    abstract suspend fun upload(localBookUri: Uri): Boolean
+    abstract suspend fun delete(remoteBookUrl: String): Boolean
+
+    /**
+     * @return String:下载到本地的路径
+     */
+    abstract suspend fun getRemoteBook(remoteBook: RemoteBook): String?
+}

+ 104 - 0
app/src/main/java/io/legado/app/ui/book/remote/RemoteBookViewModel.kt

@@ -0,0 +1,104 @@
+package io.legado.app.ui.book.remote
+
+import android.app.Application
+import android.net.Uri
+import androidx.lifecycle.MutableLiveData
+import io.legado.app.base.BaseViewModel
+import io.legado.app.model.localBook.LocalBook
+import io.legado.app.ui.book.remote.manager.RemoteBookWebDav
+import kotlinx.coroutines.*
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.flowOn
+import java.util.*
+
+class RemoteBookViewModel(application: Application): BaseViewModel(application){
+    private val remoteBookFolderName = "book_remote"
+    private var dataCallback : DataCallback? = null
+    var isRemoteBookLiveData = MutableLiveData<Boolean>()
+    var dataFlowStart: (() -> Unit)? = null
+
+
+    val dataFlow = callbackFlow<List<RemoteBook>> {
+
+        val list = Collections.synchronizedList(ArrayList<RemoteBook>())
+
+        dataCallback = object : DataCallback {
+
+            override fun setItems(remoteFiles: List<RemoteBook>) {
+                list.clear()
+                list.addAll(remoteFiles)
+                trySend(list)
+            }
+
+            override fun addItems(remoteFiles: List<RemoteBook>) {
+                list.addAll(remoteFiles)
+                trySend(list)
+            }
+
+            override fun clear() {
+                list.clear()
+                trySend(emptyList())
+            }
+        }
+//        withContext(Dispatchers.Main) {
+//            dataFlowStart?.invoke()
+//        }
+
+        awaitClose {
+            dataCallback = null
+        }
+    }.flowOn(Dispatchers.IO)
+
+    fun loadRemoteBookList() {
+        execute {
+            dataCallback?.clear()
+            val bookList = RemoteBookWebDav.getRemoteBookList()
+            dataCallback?.setItems(bookList)
+        }
+    }
+
+
+
+    fun addToBookshelf(uriList: HashSet<String>, finally: () -> Unit) {
+        execute {
+            uriList.forEach {
+                LocalBook.importFile(Uri.parse(it))
+            }
+        }.onFinally {
+            finally.invoke()
+        }
+    }
+
+    /**
+     * 添加书籍到本地书架
+     */
+    fun addToBookshelf(remoteBook: RemoteBook, finally: () -> Unit) {
+        execute {
+            val downloadBookPath = RemoteBookWebDav.getRemoteBook(remoteBook)
+            downloadBookPath?.let {
+                LocalBook.importFile(Uri.parse(it))
+            }
+        }.onFinally {
+            finally.invoke()
+        }
+    }
+    interface DataCallback {
+
+        fun setItems(remoteFiles: List<RemoteBook>)
+
+        fun addItems(remoteFiles: List<RemoteBook>)
+
+        fun clear()
+
+    }
+}
+
+data class RemoteBook(
+    val filename: String,
+    val urlName: String,
+    val size: Long,
+    val contentType: String,
+    val lastModify: Long
+)
+

+ 287 - 0
app/src/main/java/io/legado/app/ui/book/remote/manager/RemoteBookWebDav.kt

@@ -0,0 +1,287 @@
+package io.legado.app.ui.book.remote.manager
+
+
+import android.net.Uri
+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.WebDavException
+import io.legado.app.lib.webdav.WebDavFile
+import io.legado.app.ui.book.info.BookInfoActivity
+
+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 kotlinx.coroutines.withTimeout
+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}"
+    private val localSaveFolder get() = "${appCtx.externalFiles.absolutePath}${File.separator}${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<RemoteBook> {
+        val remoteBooks = mutableListOf<RemoteBook>()
+        authorization?.let {
+            //读取文件列表
+            var remoteWebDavFileList : List<WebDavFile>? = null
+            kotlin.runCatching {
+                remoteWebDavFileList = WebDav(remoteBookUrl, it).listFiles()
+            }
+            //逆序文件排序
+            remoteWebDavFileList = remoteWebDavFileList!!.reversed()
+            //转化远程文件信息到本地对象
+            remoteWebDavFileList!!.forEach { webDavFile ->
+                val webDavFileName = webDavFile.displayName
+                val webDavUrlName = "${remoteBookUrl}${File.separator}${webDavFile.displayName}"
+
+                // 转码
+                //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 fileExtension = webDavFileName.substringAfterLast(".")
+
+                //扩展名符合阅读的格式则认为是书籍
+                if (contentTypeList.contains(fileExtension)) {
+                    remoteBooks.add(RemoteBook(webDavFileName,webDavUrlName,webDavFile.size,fileExtension,webDavFile.lastModify))
+                }
+            }
+        } ?: throw NoStackTraceException("webDav没有配置")
+        return remoteBooks
+    }
+
+    override suspend fun getRemoteBook(remoteBook: RemoteBook): String? {
+        val saveFilePath= "${localSaveFolder}${File.separator}${remoteBook.filename}"
+        kotlin.runCatching {
+            authorization?.let {
+                FileUtils.createFolderIfNotExist(localSaveFolder).run{
+                        val webdav = WebDav(
+                            remoteBook.urlName,
+                            it
+                        )
+                        webdav.downloadTo(saveFilePath, true)
+                }
+            }
+        }.onFailure {
+            it.printStackTrace()
+            return null
+        }
+        return saveFilePath
+    }
+
+    /**
+     * 上传本地导入的书籍到远程
+     */
+    override suspend fun upload(localBookUri: Uri): Boolean {
+        if (!NetworkUtils.isAvailable()) return false
+
+        val localBookName = localBookUri.path?.substringAfterLast(File.separator)
+        val putUrl = "${remoteBookUrl}${File.separator}${localBookName}"
+        kotlin.runCatching {
+            authorization?.let {
+                if (localBookUri.isContentScheme()){
+                    WebDav(putUrl, it).upload(byteArray = localBookUri.readBytes(appCtx),contentType = "application/octet-stream")
+                }else{
+                    WebDav(putUrl, it).upload(localBookUri.path!!)
+                }
+            }
+        }.onFailure {
+            return false
+        }
+        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<WebDavFile?> {
+//        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<BookProgress>(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)
+//                }
+//            }
+//        }
+//    }
+}

+ 4 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BaseBookshelfFragment.kt

@@ -1,6 +1,7 @@
 package io.legado.app.ui.main.bookshelf
 
 import android.annotation.SuppressLint
+import android.content.Intent
 import android.view.Menu
 import android.view.MenuItem
 import androidx.fragment.app.activityViewModels
@@ -23,6 +24,7 @@ import io.legado.app.ui.book.cache.CacheActivity
 import io.legado.app.ui.book.group.GroupManageDialog
 import io.legado.app.ui.book.local.ImportBookActivity
 import io.legado.app.ui.book.manage.BookshelfManageActivity
+import io.legado.app.ui.book.remote.RemoteBookActivity
 import io.legado.app.ui.book.search.SearchActivity
 import io.legado.app.ui.document.HandleFileContract
 import io.legado.app.ui.main.MainViewModel
@@ -74,6 +76,8 @@ abstract class BaseBookshelfFragment(layoutId: Int) : VMBaseFragment<BookshelfVi
     override fun onCompatOptionsItemSelected(item: MenuItem) {
         super.onCompatOptionsItemSelected(item)
         when (item.itemId) {
+            // 查看远程书籍
+            R.id.menu_remote -> startActivity<RemoteBookActivity>()
             R.id.menu_search -> startActivity<SearchActivity>()
             R.id.menu_update_toc -> activityViewModel.upToc(books)
             R.id.menu_bookshelf_layout -> configBookshelf()

+ 46 - 0
app/src/main/res/layout/activity_remote_book.xml

@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <io.legado.app.ui.widget.TitleBar
+        android:id="@+id/titleBar"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        app:layout_constraintTop_toTopOf="parent"
+        app:title="远程书籍" />
+
+<!--    <io.legado.app.ui.widget.anima.RefreshProgressBar-->
+<!--        android:id="@+id/refresh_progress_bar"-->
+<!--        android:layout_width="match_parent"-->
+<!--        android:layout_height="2dp"-->
+<!--        app:layout_constraintTop_toBottomOf="@id/lay_top" />-->
+    <io.legado.app.ui.widget.anima.RefreshProgressBar
+        android:id="@+id/refresh_progress_bar"
+        android:layout_width="match_parent"
+        android:layout_height="2dp"
+        app:layout_constraintTop_toBottomOf="@id/titleBar" />
+
+    <io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout
+        android:id="@+id/content_view"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toBottomOf="@id/refresh_progress_bar">
+
+        <androidx.recyclerview.widget.RecyclerView
+            android:id="@+id/recycler_view"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" />
+
+    </io.legado.app.ui.widget.dynamiclayout.DynamicFrameLayout>
+
+<!--    <io.legado.app.ui.widget.SelectActionBar-->
+<!--        android:id="@+id/select_action_bar"-->
+<!--        android:layout_width="match_parent"-->
+<!--        android:layout_height="wrap_content"-->
+<!--        app:layout_constraintBottom_toBottomOf="parent"-->
+<!--        app:layout_constraintTop_toBottomOf="@id/content_view"-->
+<!--        />-->
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 110 - 0
app/src/main/res/layout/item_remote_book.xml

@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    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">
+
+    <io.legado.app.ui.widget.image.CoverImageView
+        android:id="@+id/iv_cover"
+        android:layout_width="80dp"
+        android:layout_height="110dp"
+        android:layout_margin="8dp"
+        android:contentDescription="@string/img_cover"
+        android:scaleType="centerCrop"
+        android:src="@drawable/image_cover_default"
+        android:transitionName="img_cover"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintLeft_toLeftOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        tools:ignore="UnusedAttribute" />
+
+
+    <TextView
+        android:id="@+id/tv_name"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_margin="8dp"
+        android:text="@string/app_name"
+        android:textColor="@color/primaryText"
+        android:textSize="16sp"
+        app:layout_constraintStart_toEndOf="@+id/iv_cover"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <LinearLayout
+        android:id="@+id/ll_info"
+        android:layout_width="wrap_content"
+        android:layout_height="0dp"
+        android:orientation="horizontal"
+        android:gravity="top"
+        android:layout_marginTop="8dp"
+        app:layout_constraintLeft_toLeftOf="@+id/tv_name"
+        app:layout_constraintTop_toBottomOf="@+id/tv_name">
+
+        <TextView
+            android:id="@+id/tv_size"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:lines="1"
+            tools:text="128kb"
+            android:textColor="@color/tv_text_summary"
+            android:textSize="13sp" />
+        <TextView
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:lines="1"
+            android:text="@string/separator"
+            android:textColor="@color/tv_text_summary"
+            android:textSize="11sp" />
+
+        <TextView
+            android:id="@+id/tv_date"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:ellipsize="end"
+            android:lines="1"
+            tools:text="2022-12-7"
+            android:textColor="@color/tv_text_summary"
+            android:textSize="13sp" />
+
+
+
+    </LinearLayout>
+    <io.legado.app.ui.widget.text.AccentBgTextView
+        android:id="@+id/tv_content_type"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginRight="15dp"
+        android:layout_marginTop="8dp"
+        android:paddingStart="5dp"
+        android:paddingEnd="5dp"
+        android:text="TXT"
+        android:maxLines="1"
+        android:maxWidth="50dp"
+        app:radius="2dp"
+        tools:ignore="HardcodedText,RtlHardcoded"
+        app:layout_constraintLeft_toLeftOf="@+id/tv_name"
+        app:layout_constraintTop_toBottomOf="@id/ll_info"
+        />
+    <LinearLayout
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:orientation="vertical"
+        app:layout_constraintRight_toRightOf="parent"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintTop_toTopOf="parent"
+        android:layout_marginTop="3dp"
+        >
+
+        <Button
+            android:id="@+id/btn_download"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            tools:text="加入书架"
+            android:text="@string/nb_file_add_shelf">
+
+        </Button>
+    </LinearLayout>
+</androidx.constraintlayout.widget.ConstraintLayout>

+ 5 - 0
app/src/main/res/menu/book_info.xml

@@ -14,6 +14,11 @@
         android:title="@string/share"
         app:showAsAction="ifRoom" />
 
+    <item
+        android:id="@+id/menu_upload"
+        android:title="@string/upload_to_remote"
+        app:showAsAction="never" />
+
     <item
         android:id="@+id/menu_refresh"
         android:title="@string/refresh"

+ 6 - 0
app/src/main/res/menu/main_bookshelf.xml

@@ -10,6 +10,12 @@
         android:title="@string/search"
         app:showAsAction="always" />
 
+    <item
+        android:id="@+id/menu_remote"
+        android:icon="@drawable/ic_add"
+        android:title="@string/add_remote_book"
+        app:showAsAction="always" />
+
     <item
         android:id="@+id/menu_update_toc"
         android:icon="@drawable/ic_refresh_black_24dp"

+ 7 - 0
app/src/main/res/values/strings.xml

@@ -6,6 +6,13 @@
     <string name="tip_perm_request_storage">Legado needs storage access to find and read books. please go "App Settings" to allow "Storage permission".</string>
 
     <!--Other-->
+    <string name="upload_book_success" translatable="false">Upload Success</string>
+    <string name="upload_book_fail" translatable="false">Upload Fail</string>
+    <string name="download_book_success" translatable="false">Download Success</string>
+    <string name="download_book_fail" translatable="false">Download Fail</string>
+    <string name="separator" translatable="false">丨</string>
+    <string name="upload_to_remote" translatable="false">Upload</string>
+    <string name="add_remote_book" translatable="false">Add Remote</string>
     <string name="menu_backup">Home</string>
     <string name="menu_restore">Restore</string>
     <string name="menu_import_old">Import Legado data</string>

Некоторые файлы не были показаны из-за большого количества измененных файлов