@@ -281,6 +281,10 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".ui.book.local.ImportBookActivity"
|
android:name=".ui.book.local.ImportBookActivity"
|
||||||
android:launchMode="singleTop" />
|
android:launchMode="singleTop" />
|
||||||
|
<!-- 添加远程 -->
|
||||||
|
<activity
|
||||||
|
android:name=".ui.book.remote.RemoteBookActivity"
|
||||||
|
android:launchMode="singleTop" />
|
||||||
<!-- 发现界面 -->
|
<!-- 发现界面 -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.book.explore.ExploreShowActivity"
|
android:name=".ui.book.explore.ExploreShowActivity"
|
||||||
|
|||||||
@@ -19,14 +19,16 @@ import java.io.InputStream
|
|||||||
import java.net.MalformedURLException
|
import java.net.MalformedURLException
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.text.SimpleDateFormat
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||||
open class WebDav(urlStr: String, val authorization: Authorization) {
|
open class WebDav(urlStr: String, val authorization: Authorization) {
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@SuppressLint("SimpleDateFormat")
|
@SuppressLint("DateTimeFormatter")
|
||||||
private val dateFormat = SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss ZZZ")
|
private val dateTimeFormatter = DateTimeFormatter.RFC_1123_DATE_TIME
|
||||||
|
|
||||||
// 指定返回哪些属性
|
// 指定返回哪些属性
|
||||||
@Language("xml")
|
@Language("xml")
|
||||||
@@ -133,11 +135,11 @@ open class WebDav(urlStr: String, val authorization: Authorization) {
|
|||||||
.firstOrNull()?.text()?.toLong() ?: 0
|
.firstOrNull()?.text()?.toLong() ?: 0
|
||||||
}.getOrDefault(0)
|
}.getOrDefault(0)
|
||||||
val lastModify: Long = kotlin.runCatching {
|
val lastModify: Long = kotlin.runCatching {
|
||||||
element.getElementsByTag("d:getcontentlength")
|
element.getElementsByTag("d:getlastmodified")
|
||||||
.firstOrNull()?.text()?.let {
|
.firstOrNull()?.text()?.let {
|
||||||
dateFormat.parse(it)
|
LocalDateTime.parse(it, dateTimeFormatter).toInstant(ZoneOffset.of("+8")).toEpochMilli()
|
||||||
}
|
}
|
||||||
}.getOrNull()?.time ?: 0
|
}.getOrNull() ?: 0
|
||||||
webDavFile = WebDavFile(
|
webDavFile = WebDavFile(
|
||||||
baseUrl + fileName,
|
baseUrl + fileName,
|
||||||
authorization,
|
authorization,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.legado.app.ui.book.info
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
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.group.GroupSelectDialog
|
||||||
import io.legado.app.ui.book.info.edit.BookInfoEditActivity
|
import io.legado.app.ui.book.info.edit.BookInfoEditActivity
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
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.search.SearchActivity
|
||||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||||
import io.legado.app.ui.book.toc.TocActivityResult
|
import io.legado.app.ui.book.toc.TocActivityResult
|
||||||
@@ -129,6 +131,8 @@ class BookInfoActivity :
|
|||||||
viewModel.bookSource != null
|
viewModel.bookSource != null
|
||||||
menu.findItem(R.id.menu_split_long_chapter)?.isVisible =
|
menu.findItem(R.id.menu_split_long_chapter)?.isVisible =
|
||||||
viewModel.bookData.value?.isLocalTxt() ?: false
|
viewModel.bookData.value?.isLocalTxt() ?: false
|
||||||
|
menu.findItem(R.id.menu_upload)?.isVisible =
|
||||||
|
viewModel.bookData.value?.isLocalBook() ?: false
|
||||||
return super.onMenuOpened(featureId, menu)
|
return super.onMenuOpened(featureId, menu)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,6 +202,17 @@ class BookInfoActivity :
|
|||||||
item.isChecked = !item.isChecked
|
item.isChecked = !item.isChecked
|
||||||
if (!item.isChecked) longToastOnUi(R.string.need_more_time_load_content)
|
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)
|
return super.onCompatOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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?
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.legado.app.ui.main.bookshelf
|
package io.legado.app.ui.main.bookshelf
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Intent
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import androidx.fragment.app.activityViewModels
|
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.group.GroupManageDialog
|
||||||
import io.legado.app.ui.book.local.ImportBookActivity
|
import io.legado.app.ui.book.local.ImportBookActivity
|
||||||
import io.legado.app.ui.book.manage.BookshelfManageActivity
|
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.book.search.SearchActivity
|
||||||
import io.legado.app.ui.document.HandleFileContract
|
import io.legado.app.ui.document.HandleFileContract
|
||||||
import io.legado.app.ui.main.MainViewModel
|
import io.legado.app.ui.main.MainViewModel
|
||||||
@@ -74,6 +76,8 @@ abstract class BaseBookshelfFragment(layoutId: Int) : VMBaseFragment<BookshelfVi
|
|||||||
override fun onCompatOptionsItemSelected(item: MenuItem) {
|
override fun onCompatOptionsItemSelected(item: MenuItem) {
|
||||||
super.onCompatOptionsItemSelected(item)
|
super.onCompatOptionsItemSelected(item)
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
|
// 查看远程书籍
|
||||||
|
R.id.menu_remote -> startActivity<RemoteBookActivity>()
|
||||||
R.id.menu_search -> startActivity<SearchActivity>()
|
R.id.menu_search -> startActivity<SearchActivity>()
|
||||||
R.id.menu_update_toc -> activityViewModel.upToc(books)
|
R.id.menu_update_toc -> activityViewModel.upToc(books)
|
||||||
R.id.menu_bookshelf_layout -> configBookshelf()
|
R.id.menu_bookshelf_layout -> configBookshelf()
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -14,6 +14,11 @@
|
|||||||
android:title="@string/share"
|
android:title="@string/share"
|
||||||
app:showAsAction="ifRoom" />
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_upload"
|
||||||
|
android:title="@string/upload_to_remote"
|
||||||
|
app:showAsAction="never" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_refresh"
|
android:id="@+id/menu_refresh"
|
||||||
android:title="@string/refresh"
|
android:title="@string/refresh"
|
||||||
|
|||||||
@@ -10,6 +10,12 @@
|
|||||||
android:title="@string/search"
|
android:title="@string/search"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/menu_remote"
|
||||||
|
android:icon="@drawable/ic_add"
|
||||||
|
android:title="@string/add_remote_book"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/menu_update_toc"
|
android:id="@+id/menu_update_toc"
|
||||||
android:icon="@drawable/ic_refresh_black_24dp"
|
android:icon="@drawable/ic_refresh_black_24dp"
|
||||||
|
|||||||
@@ -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>
|
<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-->
|
<!--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_backup">Home</string>
|
||||||
<string name="menu_restore">Restore</string>
|
<string name="menu_restore">Restore</string>
|
||||||
<string name="menu_import_old">Import Legado data</string>
|
<string name="menu_import_old">Import Legado data</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user