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