commit
						12b3dd658a
					
				@ -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; | 
					        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> | 
				
			||||||
									
										
											File diff suppressed because one or more lines are too long
										
									
								
							
						
									
										
											File diff suppressed because one or more lines are too long
										
									
								
							
						@ -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) | 
				
			||||||
 | 
					//                } | 
				
			||||||
 | 
					//            } | 
				
			||||||
 | 
					//        } | 
				
			||||||
 | 
					//    } | 
				
			||||||
 | 
					} | 
				
			||||||
@ -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> | 
				
			||||||
					Loading…
					
					
				
		Reference in new issue