[优化] 使用Compose重写的远程书籍界面,也许存在Bug
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
package io.legado.app.data.repository
|
||||
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.AppDatabase
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.lib.webdav.Authorization
|
||||
import io.legado.app.model.analyzeRule.CustomUrl
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.model.remote.RemoteBook
|
||||
import io.legado.app.model.remote.RemoteBookWebDav
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class RemoteBookRepository(
|
||||
private val appDb: AppDatabase
|
||||
) {
|
||||
|
||||
suspend fun createWebDav(serverId: Long): RemoteBookWebDav? {
|
||||
return appDb.serverDao.get(serverId)
|
||||
?.getWebDavConfig()
|
||||
?.let {
|
||||
val authorization = Authorization(it)
|
||||
RemoteBookWebDav(it.url, authorization, serverId)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadBooks(
|
||||
webDav: RemoteBookWebDav,
|
||||
path: String?
|
||||
): List<RemoteBook> {
|
||||
val url = path ?: webDav.rootBookUrl
|
||||
return webDav.getRemoteBookList(url)
|
||||
}
|
||||
|
||||
suspend fun downloadBook(
|
||||
webDav: RemoteBookWebDav,
|
||||
remoteBook: RemoteBook
|
||||
) = webDav.downloadRemoteBook(remoteBook)
|
||||
|
||||
suspend fun importRemoteBookToShelf(webDav: RemoteBookWebDav, remoteBook: RemoteBook): Book? {
|
||||
val downloadBookUri = downloadBook(webDav, remoteBook)
|
||||
val books = LocalBook.importFiles(downloadBookUri)
|
||||
val book = books.firstOrNull()
|
||||
book?.apply {
|
||||
origin = BookType.webDavTag + CustomUrl(remoteBook.path)
|
||||
.putAttribute("serverID", webDav.serverID)
|
||||
.toString()
|
||||
save()
|
||||
}
|
||||
return book
|
||||
}
|
||||
|
||||
fun flowLocalBooks(): Flow<List<Book>> {
|
||||
return appDb.bookDao.flowLocal()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,259 +1,25 @@
|
||||
package io.legado.app.ui.book.import.remote
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.SubMenu
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.LocalConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.model.remote.RemoteBook
|
||||
import io.legado.app.ui.about.AppLogDialog
|
||||
import io.legado.app.ui.book.import.BaseImportBookActivity
|
||||
import io.legado.app.ui.widget.SelectActionBar
|
||||
import io.legado.app.utils.ArchiveUtils
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.find
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.showHelp
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.legado.app.base.BaseComposeActivity
|
||||
import io.legado.app.ui.theme.AppTheme
|
||||
import io.legado.app.utils.startActivityForBook
|
||||
|
||||
/**
|
||||
* 展示远程书籍
|
||||
*/
|
||||
class RemoteBookActivity : BaseImportBookActivity<RemoteBookViewModel>(),
|
||||
RemoteBookAdapter.CallBack,
|
||||
SelectActionBar.CallBack,
|
||||
ServersDialog.Callback {
|
||||
|
||||
override val viewModel by viewModels<RemoteBookViewModel>()
|
||||
private val adapter by lazy { RemoteBookAdapter(this, this) }
|
||||
private var groupMenu: SubMenu? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
searchView.queryHint = getString(R.string.screen) + " • " + getString(R.string.remote_book)
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (!goBackDir()) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
lifecycleScope.launch {
|
||||
if (!setBookStorage()) {
|
||||
finish()
|
||||
return@launch
|
||||
}
|
||||
initView()
|
||||
initEvent()
|
||||
launch {
|
||||
viewModel.dataFlow.conflate().collect { sortedRemoteBooks ->
|
||||
binding.refreshProgressBar.isVisible = false
|
||||
binding.tvEmptyMsg.isGone = sortedRemoteBooks.isNotEmpty()
|
||||
adapter.setItems(sortedRemoteBooks)
|
||||
delay(500)
|
||||
}
|
||||
}
|
||||
viewModel.initData {
|
||||
upPath()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun observeLiveBus() {
|
||||
viewModel.permissionDenialLiveData.observe(this) {
|
||||
localBookTreeSelect.launch {
|
||||
title = getString(R.string.select_book_folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
//binding.layTop.setBackgroundColor(backgroundColor)
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.selectActionBar.setMainActionText(R.string.add_to_bookshelf)
|
||||
binding.selectActionBar.setCallBack(this)
|
||||
if (!LocalConfig.webDavBookHelpVersionIsLast) {
|
||||
showHelp("webDavBookHelp")
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortCheck(sortKey: RemoteBookSort) {
|
||||
if (viewModel.sortKey == sortKey) {
|
||||
viewModel.sortAscending = !viewModel.sortAscending
|
||||
} else {
|
||||
viewModel.sortAscending = true
|
||||
viewModel.sortKey = sortKey
|
||||
}
|
||||
}
|
||||
|
||||
private fun initEvent() {
|
||||
binding.tvGoBack.setOnClickListener {
|
||||
goBackDir()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.book_remote, menu)
|
||||
return super.onCompatCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_refresh -> upPath()
|
||||
R.id.menu_server_config -> showDialogFragment<ServersDialog>()
|
||||
R.id.menu_log -> showDialogFragment<AppLogDialog>()
|
||||
R.id.menu_help -> showHelp("webDavBookHelp")
|
||||
R.id.menu_sort_name -> {
|
||||
item.isChecked = true
|
||||
sortCheck(RemoteBookSort.Name)
|
||||
upPath()
|
||||
}
|
||||
R.id.menu_sort_time -> {
|
||||
item.isChecked = true
|
||||
sortCheck(RemoteBookSort.Default)
|
||||
upPath()
|
||||
}
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
groupMenu = menu.findItem(R.id.menu_sort)?.subMenu
|
||||
groupMenu?.setGroupCheckable(R.id.menu_group_sort, true, true)
|
||||
groupMenu?.findItem(R.id.menu_sort_name)?.isChecked =
|
||||
viewModel.sortKey == RemoteBookSort.Name
|
||||
groupMenu?.findItem(R.id.menu_sort_time)?.isChecked =
|
||||
viewModel.sortKey == RemoteBookSort.Default
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun revertSelection() {
|
||||
adapter.revertSelection()
|
||||
}
|
||||
|
||||
override fun selectAll(selectAll: Boolean) {
|
||||
adapter.selectAll(selectAll)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onClickSelectBarMainAction() {
|
||||
binding.refreshProgressBar.isVisible = true
|
||||
viewModel.addToBookshelf(adapter.selected) {
|
||||
adapter.selected.clear()
|
||||
adapter.notifyDataSetChanged()
|
||||
binding.refreshProgressBar.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun goBackDir(): Boolean {
|
||||
if (viewModel.dirList.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
viewModel.dirList.removeLastOrNull()
|
||||
upPath()
|
||||
return true
|
||||
}
|
||||
|
||||
private fun upPath() {
|
||||
binding.tvGoBack.isEnabled = viewModel.dirList.isNotEmpty()
|
||||
var path = if (viewModel.isDefaultWebdav) {
|
||||
"books" + File.separator
|
||||
} else {
|
||||
File.separator
|
||||
}
|
||||
viewModel.dirList.forEach {
|
||||
path = path + it.filename + File.separator
|
||||
}
|
||||
binding.tvPath.text = path
|
||||
viewModel.dataCallback?.clear()
|
||||
adapter.selected.clear()
|
||||
viewModel.loadRemoteBookList(
|
||||
viewModel.dirList.lastOrNull()?.path
|
||||
) {
|
||||
binding.refreshProgressBar.isVisible = it
|
||||
}
|
||||
}
|
||||
|
||||
override fun openDir(remoteBook: RemoteBook) {
|
||||
viewModel.dirList.add(remoteBook)
|
||||
upPath()
|
||||
}
|
||||
|
||||
override fun upCountView() {
|
||||
binding.selectActionBar.upCountView(adapter.selected.size, adapter.checkableCount)
|
||||
}
|
||||
|
||||
override fun onDialogDismiss(tag: String) {
|
||||
viewModel.initData {
|
||||
upPath()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSearchTextChange(newText: String?) {
|
||||
viewModel.updateCallBackFlow(newText)
|
||||
}
|
||||
|
||||
private fun showRemoteBookDownloadAlert(
|
||||
remoteBook: RemoteBook,
|
||||
onDownloadFinish: (() -> Unit)? = null
|
||||
) {
|
||||
alert(
|
||||
R.string.draw,
|
||||
R.string.archive_not_found
|
||||
) {
|
||||
okButton {
|
||||
viewModel.addToBookshelf(hashSetOf(remoteBook)) {
|
||||
onDownloadFinish?.invoke()
|
||||
}
|
||||
}
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
|
||||
override fun startRead(remoteBook: RemoteBook) {
|
||||
val downloadFileName = remoteBook.filename
|
||||
if (!ArchiveUtils.isArchive(downloadFileName)) {
|
||||
appDb.bookDao.getBookByFileName(downloadFileName)?.let {
|
||||
startReadBook(it)
|
||||
}
|
||||
} else {
|
||||
AppConfig.defaultBookTreeUri ?: return
|
||||
val downloadArchiveFileDoc = FileDoc.fromUri(Uri.parse(AppConfig.defaultBookTreeUri), true)
|
||||
.find(downloadFileName)
|
||||
if (downloadArchiveFileDoc == null) {
|
||||
showRemoteBookDownloadAlert(remoteBook) {
|
||||
startRead(remoteBook)
|
||||
}
|
||||
} else {
|
||||
onArchiveFileClick(downloadArchiveFileDoc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addToBookShelfAgain(remoteBook: RemoteBook) {
|
||||
alert(getString(R.string.sure), "是否重新加入书架?") {
|
||||
yesButton {
|
||||
binding.refreshProgressBar.isVisible = true
|
||||
viewModel.addToBookshelf(hashSetOf(remoteBook)) {
|
||||
binding.refreshProgressBar.isVisible = false
|
||||
}
|
||||
}
|
||||
noButton()
|
||||
class RemoteBookActivity : BaseComposeActivity() {
|
||||
@Composable
|
||||
override fun Content() {
|
||||
AppTheme {
|
||||
RemoteBookScreen(
|
||||
onBackClick = { finish() },
|
||||
startReadBook = { book ->
|
||||
startActivityForBook(book)
|
||||
},
|
||||
onArchiveFileClick = { },
|
||||
selectBookFolder = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
package io.legado.app.ui.book.import.remote
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.view.ViewGroup
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.databinding.ItemImportBookBinding
|
||||
import io.legado.app.model.remote.RemoteBook
|
||||
import io.legado.app.utils.ConvertUtils
|
||||
import io.legado.app.utils.gone
|
||||
import io.legado.app.utils.invisible
|
||||
import io.legado.app.utils.visible
|
||||
|
||||
|
||||
/**
|
||||
* 适配器
|
||||
* @author qianfanguojin
|
||||
*/
|
||||
class RemoteBookAdapter(context: Context, val callBack: CallBack) :
|
||||
RecyclerAdapter<RemoteBook, ItemImportBookBinding>(context) {
|
||||
var selected = hashSetOf<RemoteBook>()
|
||||
var checkableCount = 0
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemImportBookBinding {
|
||||
return ItemImportBookBinding.inflate(inflater, parent, false)
|
||||
}
|
||||
|
||||
override fun onCurrentListChanged() {
|
||||
upCheckableCount()
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定RecycleView 中每一个项的视图和数据
|
||||
*/
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemImportBookBinding,
|
||||
item: RemoteBook,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.run {
|
||||
if (payloads.isEmpty()) {
|
||||
if (item.isDir) {
|
||||
ivIcon.setImageResource(R.drawable.ic_folder)
|
||||
ivIcon.visible()
|
||||
cbSelect.invisible()
|
||||
llBrief.gone()
|
||||
cbSelect.isChecked = false
|
||||
} else {
|
||||
if (item.isOnBookShelf) {
|
||||
ivIcon.setImageResource(R.drawable.ic_book_has)
|
||||
ivIcon.visible()
|
||||
cbSelect.invisible()
|
||||
} else {
|
||||
ivIcon.invisible()
|
||||
cbSelect.visible()
|
||||
}
|
||||
llBrief.visible()
|
||||
tvTag.text = item.contentType
|
||||
tvSize.text = ConvertUtils.formatFileSize(item.size)
|
||||
tvDate.text = AppConst.dateFormat.format(item.lastModify)
|
||||
cbSelect.isChecked = selected.contains(item)
|
||||
}
|
||||
tvName.text = item.filename
|
||||
} else {
|
||||
cbSelect.isChecked = selected.contains(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun registerListener(holder: ItemViewHolder, binding: ItemImportBookBinding) {
|
||||
holder.itemView.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
if (it.isDir) {
|
||||
callBack.openDir(it)
|
||||
} else if (!it.isOnBookShelf) {
|
||||
if (!selected.contains(it)) {
|
||||
selected.add(it)
|
||||
} else {
|
||||
selected.remove(it)
|
||||
}
|
||||
notifyItemChanged(holder.layoutPosition, true)
|
||||
callBack.upCountView()
|
||||
} else {
|
||||
/* 点击开始阅读 */
|
||||
callBack.startRead(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
holder.itemView.setOnLongClickListener {
|
||||
getItem(holder.layoutPosition)?.let { remoteBook ->
|
||||
if (remoteBook.isOnBookShelf) {
|
||||
callBack.addToBookShelfAgain(remoteBook)
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun upCheckableCount() {
|
||||
checkableCount = 0
|
||||
getItems().forEach {
|
||||
if (!it.isDir && !it.isOnBookShelf) {
|
||||
checkableCount++
|
||||
}
|
||||
}
|
||||
callBack.upCountView()
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun selectAll(selectAll: Boolean) {
|
||||
if (selectAll) {
|
||||
getItems().forEach {
|
||||
if (!it.isDir && !it.isOnBookShelf) {
|
||||
selected.add(it)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selected.clear()
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
callBack.upCountView()
|
||||
}
|
||||
|
||||
fun revertSelection() {
|
||||
getItems().forEach {
|
||||
if (!it.isDir && !it.isOnBookShelf) {
|
||||
if (selected.contains(it)) {
|
||||
selected.remove(it)
|
||||
} else {
|
||||
selected.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyItemRangeChanged(0, itemCount, true)
|
||||
callBack.upCountView()
|
||||
}
|
||||
|
||||
fun removeSelection() {
|
||||
for (i in getItems().lastIndex downTo 0) {
|
||||
if (getItem(i) in selected) {
|
||||
removeItem(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface CallBack {
|
||||
fun openDir(remoteBook: RemoteBook)
|
||||
fun upCountView()
|
||||
fun startRead(remoteBook: RemoteBook)
|
||||
fun addToBookShelfAgain(remoteBook: RemoteBook)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,777 @@
|
||||
package io.legado.app.ui.book.import.remote
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.CloudDownload
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.Storage
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material.icons.outlined.AddCircleOutline
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.CloudSync
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.Server
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.model.remote.RemoteBook
|
||||
import io.legado.app.ui.widget.components.ActionItem
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.SelectionActions
|
||||
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
||||
import io.legado.app.ui.widget.components.card.TextCard
|
||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
||||
import io.legado.app.utils.ArchiveUtils
|
||||
import io.legado.app.utils.ConvertUtils
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.find
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
|
||||
sealed class RemoteBookDialog {
|
||||
data class DownloadArchive(val remoteBook: RemoteBook) : RemoteBookDialog()
|
||||
data class ReImport(val remoteBook: RemoteBook) : RemoteBookDialog()
|
||||
}
|
||||
|
||||
sealed class RemoteBookSheet {
|
||||
data object Servers : RemoteBookSheet()
|
||||
data class ServerConfig(val server: Server?) : RemoteBookSheet()
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun RemoteBookScreen(
|
||||
viewModel: RemoteBookViewModel = koinViewModel(),
|
||||
onBackClick: () -> Unit,
|
||||
startReadBook: (Book) -> Unit,
|
||||
onArchiveFileClick: (FileDoc) -> Unit,
|
||||
selectBookFolder: () -> Unit
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
val scope = rememberCoroutineScope()
|
||||
var dialogState by remember { mutableStateOf<RemoteBookDialog?>(null) }
|
||||
var showSheet by remember { mutableStateOf<RemoteBookSheet?>(null) }
|
||||
|
||||
val startRead: (RemoteBook) -> Unit =
|
||||
remember(viewModel, startReadBook, onArchiveFileClick, selectBookFolder) {
|
||||
{ remoteBook ->
|
||||
scope.launch {
|
||||
val downloadFileName = remoteBook.filename
|
||||
if (!ArchiveUtils.isArchive(downloadFileName)) {
|
||||
viewModel.getLocalBook(downloadFileName)?.let {
|
||||
startReadBook(it)
|
||||
}
|
||||
} else {
|
||||
val bookTreeUri = AppConfig.defaultBookTreeUri
|
||||
if (bookTreeUri == null) {
|
||||
selectBookFolder()
|
||||
} else {
|
||||
val downloadArchiveFileDoc = withContext(Dispatchers.IO) {
|
||||
FileDoc.fromUri(bookTreeUri.toUri(), true)
|
||||
.find(downloadFileName)
|
||||
}
|
||||
if (downloadArchiveFileDoc == null) {
|
||||
dialogState = RemoteBookDialog.DownloadArchive(remoteBook)
|
||||
} else {
|
||||
onArchiveFileClick(downloadArchiveFileDoc)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialogState?.let { state ->
|
||||
when (state) {
|
||||
is RemoteBookDialog.DownloadArchive -> {
|
||||
AlertDialog(
|
||||
onDismissRequest = { dialogState = null },
|
||||
title = { Text(stringResource(R.string.draw)) },
|
||||
text = { Text(stringResource(R.string.archive_not_found)) },
|
||||
confirmButton = {
|
||||
OutlinedButton(onClick = {
|
||||
scope.launch { viewModel.addToBookshelf(setOf(state.remoteBook)) }
|
||||
dialogState = null
|
||||
}) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { dialogState = null }) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is RemoteBookDialog.ReImport -> {
|
||||
AlertDialog(
|
||||
onDismissRequest = { dialogState = null },
|
||||
title = { Text("是否重新加入书架?") },
|
||||
text = { Text("将会覆盖书籍") },
|
||||
confirmButton = {
|
||||
OutlinedButton(onClick = {
|
||||
scope.launch { viewModel.addToBookshelf(setOf(state.remoteBook)) }
|
||||
dialogState = null
|
||||
}) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { dialogState = null }) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showSheet != null) {
|
||||
GlassModalBottomSheet(
|
||||
onDismissRequest = { showSheet = null }
|
||||
) {
|
||||
when (val state = showSheet) {
|
||||
is RemoteBookSheet.Servers -> {
|
||||
ServersSheetContent(
|
||||
servers = uiState.servers,
|
||||
selectedServerId = uiState.selectedServerId,
|
||||
onSelect = {
|
||||
viewModel.selectServer(it)
|
||||
showSheet = null
|
||||
},
|
||||
onEdit = { showSheet = RemoteBookSheet.ServerConfig(it) },
|
||||
onDelete = { viewModel.deleteServer(it) },
|
||||
onAdd = { showSheet = RemoteBookSheet.ServerConfig(null) },
|
||||
onDefault = {
|
||||
viewModel.selectServer(AppConst.DEFAULT_WEBDAV_ID)
|
||||
showSheet = null
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is RemoteBookSheet.ServerConfig -> {
|
||||
ServerConfigSheetContent(
|
||||
server = state.server,
|
||||
onSave = {
|
||||
viewModel.saveServer(it)
|
||||
showSheet = RemoteBookSheet.Servers
|
||||
},
|
||||
onCancel = { showSheet = RemoteBookSheet.Servers }
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.initData { viewModel.loadRemoteBookList() }
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.permissionDenialEvent.collect { selectBookFolder() }
|
||||
}
|
||||
|
||||
ListScaffold(
|
||||
title = "远程书籍",
|
||||
state = uiState,
|
||||
onBackClick = onBackClick,
|
||||
onSearchToggle = { viewModel.setSearchMode(it) },
|
||||
onSearchQueryChange = { viewModel.setSearchKey(it) },
|
||||
searchPlaceholder = "搜索",
|
||||
topBarActions = {
|
||||
IconButton(onClick = { showSheet = RemoteBookSheet.Servers }) {
|
||||
Icon(Icons.Default.Storage, contentDescription = "配置服务器")
|
||||
}
|
||||
},
|
||||
dropDownMenuContent = { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
text = { Text("按名称排序") },
|
||||
onClick = {
|
||||
viewModel.toggleSort(RemoteBookSort.Name)
|
||||
dismiss()
|
||||
},
|
||||
trailingIcon = {
|
||||
if (uiState.sortKey == RemoteBookSort.Name) {
|
||||
Icon(Icons.Default.Check, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = { Text("按时间排序") },
|
||||
onClick = {
|
||||
viewModel.toggleSort(RemoteBookSort.Default)
|
||||
dismiss()
|
||||
},
|
||||
trailingIcon = {
|
||||
if (uiState.sortKey == RemoteBookSort.Default) {
|
||||
Icon(Icons.Default.Check, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
PathNavigationBar(
|
||||
pathNames = uiState.pathNames,
|
||||
canGoBack = uiState.canGoBack,
|
||||
onNavigateBack = { viewModel.navigateBack() },
|
||||
onNavigateToLevel = { viewModel.navigateToLevel(it) }
|
||||
)
|
||||
},
|
||||
selectionActions = SelectionActions(
|
||||
onSelectAll = { viewModel.selectAllCheckable() },
|
||||
onSelectInvert = { viewModel.invertSelection() },
|
||||
primaryAction = ActionItem(
|
||||
text = "添加至书架",
|
||||
icon = { Icon(Icons.Default.CloudDownload, null) },
|
||||
onClick = {
|
||||
val selectedBooks = uiState.items
|
||||
.filter { it.id in uiState.selectedIds }
|
||||
.map { it.remoteBook }
|
||||
.toSet()
|
||||
scope.launch { viewModel.addToBookshelf(selectedBooks) }
|
||||
}
|
||||
),
|
||||
secondaryActions = emptyList()
|
||||
),
|
||||
onAddClick = null,
|
||||
) { paddingValues ->
|
||||
val pullToRefreshState = rememberPullToRefreshState()
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
isRefreshing = uiState.isLoading,
|
||||
state = pullToRefreshState,
|
||||
onRefresh = { viewModel.refreshData() }
|
||||
) {
|
||||
if (uiState.items.isEmpty()) {
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
} else {
|
||||
EmptyMessageView(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
message = "没有内容"
|
||||
)
|
||||
}
|
||||
} else {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(uiState.items, key = { it.id }) { itemUi ->
|
||||
val book = itemUi.remoteBook
|
||||
RemoteBookItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
book = book,
|
||||
isSelected = itemUi.id in uiState.selectedIds,
|
||||
onClick = {
|
||||
when {
|
||||
book.isDir -> viewModel.navigateToDir(book)
|
||||
book.isOnBookShelf -> startRead(book)
|
||||
else -> viewModel.toggleSelection(itemUi.id)
|
||||
}
|
||||
},
|
||||
onAddClick = { remoteBook ->
|
||||
scope.launch { viewModel.addToBookshelf(setOf(remoteBook)) }
|
||||
},
|
||||
onUpdateClick = { remoteBook ->
|
||||
dialogState = RemoteBookDialog.ReImport(remoteBook)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServersSheetContent(
|
||||
servers: List<Server>,
|
||||
selectedServerId: Long,
|
||||
onSelect: (Long) -> Unit,
|
||||
onEdit: (Server) -> Unit,
|
||||
onDelete: (Server) -> Unit,
|
||||
onAdd: () -> Unit,
|
||||
onDefault: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(bottom = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.server_config),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
IconButton(onClick = onAdd) {
|
||||
Icon(Icons.Default.Add, contentDescription = "添加")
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f, fill = false),
|
||||
contentPadding = PaddingValues(bottom = 16.dp)
|
||||
) {
|
||||
item {
|
||||
ServerItem(
|
||||
name = "默认",
|
||||
url = "应用备份的 WebDav 配置",
|
||||
isSelected = selectedServerId == AppConst.DEFAULT_WEBDAV_ID,
|
||||
onClick = onDefault
|
||||
)
|
||||
}
|
||||
items(servers, key = { it.id }) { server ->
|
||||
ServerItem(
|
||||
name = server.name,
|
||||
url = server.getConfigJsonObject()?.optString("url"),
|
||||
isSelected = server.id == selectedServerId,
|
||||
onClick = { onSelect(server.id) },
|
||||
onEdit = { onEdit(server) },
|
||||
onDelete = { onDelete(server) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerItem(
|
||||
name: String,
|
||||
url: String?,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onEdit: (() -> Unit)? = null,
|
||||
onDelete: (() -> Unit)? = null
|
||||
) {
|
||||
val containerColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surface
|
||||
}
|
||||
|
||||
Card(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = containerColor
|
||||
)
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.animateContentSize(),
|
||||
headlineContent = {
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
leadingContent = {
|
||||
RadioButton(
|
||||
selected = isSelected,
|
||||
onClick = null
|
||||
)
|
||||
},
|
||||
supportingContent = url?.let {
|
||||
{
|
||||
Text(
|
||||
text = it,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
},
|
||||
trailingContent = if (onEdit != null || onDelete != null) {
|
||||
{
|
||||
Row {
|
||||
onEdit?.let {
|
||||
IconButton(onClick = it) {
|
||||
Icon(Icons.Default.Edit, contentDescription = "编辑")
|
||||
}
|
||||
}
|
||||
onDelete?.let {
|
||||
IconButton(onClick = it) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "删除")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else null,
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServerConfigSheetContent(
|
||||
server: Server?,
|
||||
onSave: (Server) -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
var name by remember { mutableStateOf(server?.name ?: "") }
|
||||
val configJson = remember { server?.getConfigJsonObject() ?: JSONObject() }
|
||||
var url by remember { mutableStateOf(configJson.optString("url")) }
|
||||
var username by remember { mutableStateOf(configJson.optString("username")) }
|
||||
var password by remember { mutableStateOf(configJson.optString("password")) }
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
text = if (server == null) "添加服务器" else "编辑服务器",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text("名称") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = url,
|
||||
onValueChange = { url = it },
|
||||
label = { Text("URL") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = { username = it },
|
||||
label = { Text("用户名") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = { password = it },
|
||||
label = { Text("密码") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true,
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
if (passwordVisible) Icons.Default.Visibility else Icons.Default.VisibilityOff,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
onClick = onCancel,
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
Button(
|
||||
onClick = {
|
||||
val newServer = server?.copy(name = name) ?: Server(name = name)
|
||||
val newConfig = JSONObject().apply {
|
||||
put("url", url)
|
||||
put("username", username)
|
||||
put("password", password)
|
||||
}
|
||||
newServer.config = newConfig.toString()
|
||||
onSave(newServer)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = name.isNotBlank() && url.isNotBlank()
|
||||
) {
|
||||
Text("保存")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PathNavigationBar(
|
||||
pathNames: List<String>,
|
||||
canGoBack: Boolean,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToLevel: (Int) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 4.dp)
|
||||
.animateContentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
|
||||
Card(
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||
),
|
||||
shape = MaterialTheme.shapes.medium
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
itemsIndexed(pathNames) { index, name ->
|
||||
val isLast = index == pathNames.lastIndex
|
||||
|
||||
Text(
|
||||
text = name,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = if (isLast) FontWeight.SemiBold else FontWeight.Medium,
|
||||
color = if (isLast)
|
||||
MaterialTheme.colorScheme.primary
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.then(
|
||||
if (!isLast)
|
||||
Modifier.clickable { onNavigateToLevel(index) }
|
||||
else Modifier
|
||||
)
|
||||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
||||
)
|
||||
|
||||
if (!isLast) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(12.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canGoBack) {
|
||||
SmallTonalIconButton(
|
||||
onClick = onNavigateBack,
|
||||
icon = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "返回上级"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun RemoteBookItem(
|
||||
modifier: Modifier = Modifier,
|
||||
book: RemoteBook,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
onAddClick: (RemoteBook) -> Unit,
|
||||
onUpdateClick: (RemoteBook) -> Unit
|
||||
) {
|
||||
val containerColor = if (isSelected) {
|
||||
MaterialTheme.colorScheme.secondaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainerLow
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
onClick = onClick,
|
||||
colors = CardDefaults.cardColors(containerColor = containerColor)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
Icon(
|
||||
imageVector = when {
|
||||
book.isDir -> Icons.Default.Folder
|
||||
book.isOnBookShelf -> Icons.Outlined.Book
|
||||
else -> Icons.Outlined.Description
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = if (book.isDir) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
|
||||
Text(
|
||||
text = book.filename.substringBeforeLast("."),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
if (!book.isDir) {
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
||||
if (book.contentType.isNotEmpty()) {
|
||||
TextCard(
|
||||
text = book.contentType.uppercase(),
|
||||
textStyle = MaterialTheme.typography.labelSmall,
|
||||
horizontalPadding = 4.dp,
|
||||
verticalPadding = 2.dp,
|
||||
cornerRadius = 4.dp,
|
||||
icon = null,
|
||||
backgroundColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "${ConvertUtils.formatFileSize(book.size)} • ${
|
||||
AppConst.dateFormat.format(book.lastModify)
|
||||
}",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!book.isDir) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
SmallIconButton(
|
||||
onClick = {
|
||||
if (book.isOnBookShelf) {
|
||||
onUpdateClick(book)
|
||||
} else {
|
||||
onAddClick(book)
|
||||
}
|
||||
},
|
||||
icon = if (book.isOnBookShelf)
|
||||
Icons.Outlined.CloudSync
|
||||
else
|
||||
Icons.Outlined.AddCircleOutline,
|
||||
contentDescription = if (book.isOnBookShelf) "更新" else "加入",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user