[优化] 界面和性能优化
This commit is contained in:
37 files changed
+788
-424
No files matched your search
@@ -13,7 +13,7 @@ import io.legado.app.help.http.text
|
|||||||
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||||
import io.legado.app.ui.widget.components.importComponents.ImportItemWrapper
|
import io.legado.app.ui.widget.components.importComponents.ImportItemWrapper
|
||||||
import io.legado.app.ui.widget.components.importComponents.ImportStatus
|
import io.legado.app.ui.widget.components.importComponents.ImportStatus
|
||||||
import io.legado.app.ui.widget.components.rules.RuleActionState
|
import io.legado.app.ui.widget.components.rules.ListUiState
|
||||||
import io.legado.app.ui.widget.components.rules.SelectableItem
|
import io.legado.app.ui.widget.components.rules.SelectableItem
|
||||||
import io.legado.app.utils.isAbsUrl
|
import io.legado.app.utils.isAbsUrl
|
||||||
import io.legado.app.utils.isUri
|
import io.legado.app.utils.isUri
|
||||||
@@ -42,7 +42,7 @@ sealed interface BaseRuleEvent {
|
|||||||
) : BaseRuleEvent
|
) : BaseRuleEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class BaseRuleViewModel<T : SelectableItem<ID>, Entity, ID, S : RuleActionState<T>>(
|
abstract class BaseRuleViewModel<T : SelectableItem<ID>, Entity, ID, S : ListUiState<T>>(
|
||||||
application: Application,
|
application: Application,
|
||||||
initialState: S,
|
initialState: S,
|
||||||
private val uploadRepository: UploadRepository? = null // 设为可空,提高灵活性
|
private val uploadRepository: UploadRepository? = null // 设为可空,提高灵活性
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import io.legado.app.data.dao.RssSourceDao
|
|||||||
import io.legado.app.data.dao.RssStarDao
|
import io.legado.app.data.dao.RssStarDao
|
||||||
import io.legado.app.data.dao.RuleSubDao
|
import io.legado.app.data.dao.RuleSubDao
|
||||||
import io.legado.app.data.dao.SearchBookDao
|
import io.legado.app.data.dao.SearchBookDao
|
||||||
|
import io.legado.app.data.dao.SearchContentHistoryDao
|
||||||
import io.legado.app.data.dao.SearchKeywordDao
|
import io.legado.app.data.dao.SearchKeywordDao
|
||||||
import io.legado.app.data.dao.ServerDao
|
import io.legado.app.data.dao.ServerDao
|
||||||
import io.legado.app.data.dao.TxtTocRuleDao
|
import io.legado.app.data.dao.TxtTocRuleDao
|
||||||
@@ -46,6 +47,7 @@ import io.legado.app.data.entities.RssSource
|
|||||||
import io.legado.app.data.entities.RssStar
|
import io.legado.app.data.entities.RssStar
|
||||||
import io.legado.app.data.entities.RuleSub
|
import io.legado.app.data.entities.RuleSub
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
|
import io.legado.app.data.entities.SearchContentHistory
|
||||||
import io.legado.app.data.entities.SearchKeyword
|
import io.legado.app.data.entities.SearchKeyword
|
||||||
import io.legado.app.data.entities.Server
|
import io.legado.app.data.entities.Server
|
||||||
import io.legado.app.data.entities.TxtTocRule
|
import io.legado.app.data.entities.TxtTocRule
|
||||||
@@ -67,14 +69,15 @@ val appDb by lazy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
version = 83,
|
version = 85,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||||
RssSource::class, Bookmark::class, RssArticle::class,
|
RssSource::class, Bookmark::class, RssArticle::class,
|
||||||
RssReadRecord::class, ReadRecordDetail::class, ReadRecordSession::class,
|
RssReadRecord::class, ReadRecordDetail::class, ReadRecordSession::class,
|
||||||
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
|
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
|
||||||
RuleSub::class, DictRule::class, KeyboardAssist::class, Server::class],
|
RuleSub::class, DictRule::class, KeyboardAssist::class, Server::class,
|
||||||
|
SearchContentHistory::class],
|
||||||
views = [BookSourcePart::class],
|
views = [BookSourcePart::class],
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 43, to = 44),
|
AutoMigration(from = 43, to = 44),
|
||||||
@@ -115,7 +118,10 @@ val appDb by lazy {
|
|||||||
AutoMigration(from = 78, to = 79),
|
AutoMigration(from = 78, to = 79),
|
||||||
AutoMigration(from = 79, to = 80),
|
AutoMigration(from = 79, to = 80),
|
||||||
AutoMigration(from = 80, to = 81),
|
AutoMigration(from = 80, to = 81),
|
||||||
AutoMigration(from = 81, to = 82)
|
AutoMigration(from = 81, to = 82),
|
||||||
|
AutoMigration(from = 82, to = 83),
|
||||||
|
AutoMigration(from = 83, to = 84),
|
||||||
|
AutoMigration(from = 84, to = 85)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
@@ -141,6 +147,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
abstract val dictRuleDao: DictRuleDao
|
abstract val dictRuleDao: DictRuleDao
|
||||||
abstract val keyboardAssistsDao: KeyboardAssistsDao
|
abstract val keyboardAssistsDao: KeyboardAssistsDao
|
||||||
abstract val serverDao: ServerDao
|
abstract val serverDao: ServerDao
|
||||||
|
abstract val searchContentHistoryDao: SearchContentHistoryDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@@ -272,4 +279,4 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,6 @@ import androidx.room.Insert
|
|||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import androidx.room.Update
|
import androidx.room.Update
|
||||||
import io.legado.app.data.entities.ReadRecordShow
|
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
||||||
@@ -23,29 +22,9 @@ interface ReadRecordDao {
|
|||||||
|
|
||||||
@get:Query("select * from readRecordSession")
|
@get:Query("select * from readRecordSession")
|
||||||
val allSession: List<ReadRecordSession>
|
val allSession: List<ReadRecordSession>
|
||||||
|
|
||||||
@get:Query(
|
|
||||||
"""
|
|
||||||
select bookName, bookAuthor, sum(readTime) as readTime, max(lastRead) as lastRead
|
|
||||||
from readRecord
|
|
||||||
group by bookName, bookAuthor
|
|
||||||
order by bookName collate localized, bookAuthor collate localized"""
|
|
||||||
)
|
|
||||||
val allShow: List<ReadRecordShow>
|
|
||||||
|
|
||||||
@Query("SELECT sum(readTime) FROM readRecord")
|
@Query("SELECT sum(readTime) FROM readRecord")
|
||||||
fun getTotalReadTime(): Flow<Long?>
|
fun getTotalReadTime(): Flow<Long?>
|
||||||
|
|
||||||
@Query(
|
|
||||||
"""
|
|
||||||
select bookName, bookAuthor, sum(readTime) as readTime, max(lastRead) as lastRead
|
|
||||||
from readRecord
|
|
||||||
where bookName like '%' || :searchKey || '%' or bookAuthor like '%' || :searchKey || '%'
|
|
||||||
group by bookName, bookAuthor
|
|
||||||
order by bookName collate localized, bookAuthor collate localized"""
|
|
||||||
)
|
|
||||||
fun search(searchKey: String): List<ReadRecordShow>
|
|
||||||
|
|
||||||
@Query("select sum(readTime) from readRecord where bookName = :bookName")
|
@Query("select sum(readTime) from readRecord where bookName = :bookName")
|
||||||
fun getReadTime(bookName: String): Long?
|
fun getReadTime(bookName: String): Long?
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.legado.app.data.entities
|
package io.legado.app.data.entities
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
@@ -15,6 +16,7 @@ data class Bookmark(
|
|||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val time: Long = System.currentTimeMillis(),
|
val time: Long = System.currentTimeMillis(),
|
||||||
val bookName: String = "",
|
val bookName: String = "",
|
||||||
|
@ColumnInfo(defaultValue = "")
|
||||||
val bookAuthor: String = "",
|
val bookAuthor: String = "",
|
||||||
var chapterIndex: Int = 0,
|
var chapterIndex: Int = 0,
|
||||||
var chapterPos: Int = 0,
|
var chapterPos: Int = 0,
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package io.legado.app.data.entities
|
|
||||||
|
|
||||||
data class ReadRecordShow(
|
|
||||||
var bookName: String,
|
|
||||||
var bookAuthor: String,
|
|
||||||
var readTime: Long,
|
|
||||||
var lastRead: Long
|
|
||||||
)
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.data.entities.readRecord
|
package io.legado.app.data.entities.readRecord
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
@@ -10,6 +11,7 @@ data class ReadRecordSession(
|
|||||||
|
|
||||||
val deviceId: String = "",
|
val deviceId: String = "",
|
||||||
val bookName: String = "",
|
val bookName: String = "",
|
||||||
|
@ColumnInfo(defaultValue = "")
|
||||||
val bookAuthor: String = "",
|
val bookAuthor: String = "",
|
||||||
|
|
||||||
// 一次阅读的开始/结束
|
// 一次阅读的开始/结束
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import io.legado.app.data.repository.DirectLinkUploadRepository
|
|||||||
import io.legado.app.data.repository.ExploreRepository
|
import io.legado.app.data.repository.ExploreRepository
|
||||||
import io.legado.app.data.repository.ExploreRepositoryImpl
|
import io.legado.app.data.repository.ExploreRepositoryImpl
|
||||||
import io.legado.app.data.repository.ReadRecordRepository
|
import io.legado.app.data.repository.ReadRecordRepository
|
||||||
|
import io.legado.app.data.repository.RemoteBookRepository
|
||||||
import io.legado.app.data.repository.SearchContentRepository
|
import io.legado.app.data.repository.SearchContentRepository
|
||||||
import io.legado.app.data.repository.UploadRepository
|
import io.legado.app.data.repository.UploadRepository
|
||||||
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
||||||
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
||||||
|
import io.legado.app.ui.book.import.remote.RemoteBookViewModel
|
||||||
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
||||||
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
||||||
import io.legado.app.ui.book.toc.TocViewModel
|
import io.legado.app.ui.book.toc.TocViewModel
|
||||||
@@ -34,6 +36,7 @@ val appModule = module {
|
|||||||
singleOf(::ReadRecordRepository)
|
singleOf(::ReadRecordRepository)
|
||||||
singleOf(::BookRepository)
|
singleOf(::BookRepository)
|
||||||
singleOf(::SearchContentRepository)
|
singleOf(::SearchContentRepository)
|
||||||
|
singleOf(::RemoteBookRepository)
|
||||||
|
|
||||||
single<UploadRepository> { DirectLinkUploadRepository() }
|
single<UploadRepository> { DirectLinkUploadRepository() }
|
||||||
single<ExploreRepository> { ExploreRepositoryImpl(get()) }
|
single<ExploreRepository> { ExploreRepositoryImpl(get()) }
|
||||||
@@ -47,6 +50,7 @@ val appModule = module {
|
|||||||
viewModelOf(::TxtTocRuleViewModel)
|
viewModelOf(::TxtTocRuleViewModel)
|
||||||
viewModelOf(::OtherConfigViewModel)
|
viewModelOf(::OtherConfigViewModel)
|
||||||
viewModelOf(::TocViewModel)
|
viewModelOf(::TocViewModel)
|
||||||
|
viewModelOf(::RemoteBookViewModel)
|
||||||
|
|
||||||
viewModel { (route: ReplaceEditRoute) ->
|
viewModel { (route: ReplaceEditRoute) ->
|
||||||
ReplaceEditViewModel(
|
ReplaceEditViewModel(
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
package io.legado.app.ui.book.explore
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.SearchBook
|
|
||||||
import io.legado.app.databinding.ItemSearchBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.model.BookShelfState
|
|
||||||
import io.legado.app.ui.widget.text.AccentBgTextView
|
|
||||||
import io.legado.app.utils.dpToPx
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
|
|
||||||
|
|
||||||
class ExploreShowAdapter(context: Context, val callBack: CallBack) :
|
|
||||||
RecyclerAdapter<SearchBook, ItemSearchBinding>(context) {
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemSearchBinding {
|
|
||||||
return ItemSearchBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemSearchBinding,
|
|
||||||
item: SearchBook,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
bind(binding, item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bindChange(binding, item, bundle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bind(binding: ItemSearchBinding, item: SearchBook) {
|
|
||||||
binding.run {
|
|
||||||
tvName.text = item.name
|
|
||||||
tvAuthor.text = context.getString(R.string.author_show, item.author)
|
|
||||||
when (callBack.getBookShelfState(item.name, item.author, item.bookUrl)) {
|
|
||||||
BookShelfState.IN_SHELF -> {
|
|
||||||
ivInBookshelf.isVisible = true
|
|
||||||
tvBookshelf.text = context.getString(R.string.remove_from_bookshelf)
|
|
||||||
}
|
|
||||||
BookShelfState.SAME_NAME_AUTHOR -> {
|
|
||||||
ivInBookshelf.isVisible = true
|
|
||||||
tvBookshelf.text = context.getString(R.string.same_name_book)
|
|
||||||
}
|
|
||||||
BookShelfState.NOT_IN_SHELF -> {
|
|
||||||
ivInBookshelf.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item.latestChapterTitle.isNullOrEmpty()) {
|
|
||||||
tvLasted.gone()
|
|
||||||
} else {
|
|
||||||
tvLasted.text = context.getString(R.string.lasted_show, item.latestChapterTitle)
|
|
||||||
tvLasted.visible()
|
|
||||||
}
|
|
||||||
tvIntroduce.text = item.trimIntro(context)
|
|
||||||
val kinds = item.getKindList()
|
|
||||||
if (kinds.isEmpty()) {
|
|
||||||
kindContainer.gone()
|
|
||||||
} else {
|
|
||||||
kindContainer.visible()
|
|
||||||
kindContainer.removeAllViews()
|
|
||||||
kinds.forEach { kind ->
|
|
||||||
val kindTextView = AccentBgTextView(context).apply {
|
|
||||||
layoutParams = LinearLayout.LayoutParams(
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
||||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
|
||||||
).apply {
|
|
||||||
marginEnd = 6.dpToPx()
|
|
||||||
}
|
|
||||||
textSize = 11f
|
|
||||||
text = kind
|
|
||||||
setLines(1)
|
|
||||||
setSingleLine(true)
|
|
||||||
}
|
|
||||||
kindContainer.addView(kindTextView)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ivCover.load(
|
|
||||||
item.coverUrl,
|
|
||||||
item.name,
|
|
||||||
item.author,
|
|
||||||
AppConfig.loadCoverOnlyWifi,
|
|
||||||
item.origin
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindChange(binding: ItemSearchBinding, item: SearchBook, bundle: Bundle) {
|
|
||||||
binding.run {
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"isInBookshelf" -> {
|
|
||||||
when (callBack.getBookShelfState(item.name, item.author, item.bookUrl)) {
|
|
||||||
BookShelfState.IN_SHELF -> {
|
|
||||||
ivInBookshelf.isVisible = true
|
|
||||||
tvBookshelf.text = context.getString(R.string.remove_from_bookshelf)
|
|
||||||
}
|
|
||||||
BookShelfState.SAME_NAME_AUTHOR -> {
|
|
||||||
ivInBookshelf.isVisible = true
|
|
||||||
tvBookshelf.text = context.getString(R.string.same_name_book)
|
|
||||||
}
|
|
||||||
BookShelfState.NOT_IN_SHELF -> {
|
|
||||||
ivInBookshelf.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemSearchBinding) {
|
|
||||||
binding.llContent.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.showBookInfo(it.toBook())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
/**
|
|
||||||
* 是否已经加入书架
|
|
||||||
*/
|
|
||||||
fun getBookShelfState(name: String, author: String, url: String?): BookShelfState
|
|
||||||
|
|
||||||
fun showBookInfo(book: Book)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -52,7 +52,6 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SheetValue
|
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -60,7 +59,6 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -186,12 +184,8 @@ fun ExploreShowScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showGridCountSheet) {
|
if (showGridCountSheet) {
|
||||||
val sheetState = rememberModalBottomSheetState(
|
|
||||||
skipPartiallyExpanded = true
|
|
||||||
)
|
|
||||||
|
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
sheetState = sheetState,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
onDismissRequest = { showGridCountSheet = false }
|
onDismissRequest = { showGridCountSheet = false }
|
||||||
@@ -244,16 +238,8 @@ fun ExploreShowScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showKindSheet) {
|
if (showKindSheet) {
|
||||||
val sheetState = rememberModalBottomSheetState(
|
|
||||||
skipPartiallyExpanded = true,
|
|
||||||
confirmValueChange = { newValue ->
|
|
||||||
newValue != SheetValue.PartiallyExpanded
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
onDismissRequest = { showKindSheet = false },
|
onDismissRequest = { showKindSheet = false },
|
||||||
sheetState = sheetState,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.72f)
|
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.72f)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,174 +1,372 @@
|
|||||||
package io.legado.app.ui.book.import.remote
|
package io.legado.app.ui.book.import.remote
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
|
import io.legado.app.constant.AppConst
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.constant.BookType
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.Server
|
||||||
|
import io.legado.app.data.repository.RemoteBookRepository
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.help.AppWebDav
|
import io.legado.app.help.AppWebDav
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.lib.webdav.Authorization
|
|
||||||
import io.legado.app.model.analyzeRule.CustomUrl
|
import io.legado.app.model.analyzeRule.CustomUrl
|
||||||
import io.legado.app.model.localBook.LocalBook
|
import io.legado.app.model.localBook.LocalBook
|
||||||
import io.legado.app.model.remote.RemoteBook
|
import io.legado.app.model.remote.RemoteBook
|
||||||
import io.legado.app.model.remote.RemoteBookWebDav
|
import io.legado.app.model.remote.RemoteBookWebDav
|
||||||
|
import io.legado.app.ui.widget.components.rules.InteractionState
|
||||||
|
import io.legado.app.ui.widget.components.rules.ListUiState
|
||||||
|
import io.legado.app.ui.widget.components.rules.SelectableItem
|
||||||
import io.legado.app.utils.AlphanumComparator
|
import io.legado.app.utils.AlphanumComparator
|
||||||
|
import io.legado.app.utils.ConvertUtils
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import java.util.Collections
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class RemoteBookViewModel(application: Application) : BaseViewModel(application) {
|
@Immutable
|
||||||
var sortKey = RemoteBookSort.Default
|
data class NavigationState(
|
||||||
var sortAscending = false
|
val pathNames: List<String> = emptyList(),
|
||||||
val dirList = arrayListOf<RemoteBook>()
|
val canGoBack: Boolean = false
|
||||||
val permissionDenialLiveData = MutableLiveData<Int>()
|
)
|
||||||
|
|
||||||
var dataCallback: DataCallback? = null
|
@Immutable
|
||||||
|
data class RemoteBookItemUi(
|
||||||
|
val remoteBook: RemoteBook,
|
||||||
|
val nameWithoutExtension: String,
|
||||||
|
val formattedSize: String,
|
||||||
|
val formattedDate: String,
|
||||||
|
override val id: String = remoteBook.path
|
||||||
|
) : SelectableItem<String>
|
||||||
|
|
||||||
val dataFlow = callbackFlow<List<RemoteBook>> {
|
@Immutable
|
||||||
|
data class RemoteBookUiState(
|
||||||
|
override val items: List<RemoteBookItemUi> = emptyList(),
|
||||||
|
override val selectedIds: Set<Any> = emptySet(),
|
||||||
|
override val searchKey: String = "",
|
||||||
|
val interaction: InteractionState = InteractionState(isLoading = true),
|
||||||
|
val navigation: NavigationState = NavigationState(),
|
||||||
|
val sortKey: RemoteBookSort = RemoteBookSort.Default,
|
||||||
|
val sortAscending: Boolean = false,
|
||||||
|
val servers: List<Server> = emptyList(),
|
||||||
|
val selectedServerId: Long = AppConfig.remoteServerId
|
||||||
|
) : ListUiState<RemoteBookItemUi> {
|
||||||
|
override val isSearch: Boolean get() = interaction.isSearchMode
|
||||||
|
override val isLoading: Boolean get() = interaction.isLoading
|
||||||
|
val pathNames: List<String> get() = navigation.pathNames
|
||||||
|
val canGoBack: Boolean get() = navigation.canGoBack
|
||||||
|
}
|
||||||
|
|
||||||
val list = Collections.synchronizedList(ArrayList<RemoteBook>())
|
class RemoteBookViewModel(
|
||||||
|
application: Application,
|
||||||
|
@Suppress("UNUSED_PARAMETER")
|
||||||
|
private val savedStateHandle: SavedStateHandle,
|
||||||
|
private val repository: RemoteBookRepository
|
||||||
|
) : BaseViewModel(application) {
|
||||||
|
|
||||||
dataCallback = object : DataCallback {
|
private data class InternalState(
|
||||||
|
val remoteBooks: List<RemoteBook> = emptyList(),
|
||||||
|
val sortKey: RemoteBookSort = RemoteBookSort.Default,
|
||||||
|
val sortAscending: Boolean = false,
|
||||||
|
val dirList: List<RemoteBook> = emptyList(),
|
||||||
|
val interaction: InteractionState = InteractionState(isLoading = true),
|
||||||
|
val searchKey: String = "",
|
||||||
|
val selectedIds: Set<String> = emptySet(),
|
||||||
|
val remoteBookWebDav: RemoteBookWebDav? = null,
|
||||||
|
val isDefaultWebdav: Boolean = false,
|
||||||
|
val selectedServerId: Long = AppConfig.remoteServerId
|
||||||
|
)
|
||||||
|
|
||||||
override fun setItems(remoteFiles: List<RemoteBook>) {
|
private val _state = MutableStateFlow(InternalState())
|
||||||
list.clear()
|
|
||||||
list.addAll(remoteFiles)
|
|
||||||
trySend(list)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun addItems(remoteFiles: List<RemoteBook>) {
|
val uiState: StateFlow<RemoteBookUiState> = combine(
|
||||||
list.addAll(remoteFiles)
|
_state,
|
||||||
trySend(list)
|
repository.flowLocalBooks(),
|
||||||
}
|
appDb.serverDao.observeAll()
|
||||||
|
) { state, localBooks, servers ->
|
||||||
|
val localFileNames = localBooks.map { it.originName }.toSet()
|
||||||
|
|
||||||
override fun clear() {
|
val sortedBooks = sortBooks(state.remoteBooks, state.sortKey, state.sortAscending)
|
||||||
list.clear()
|
val filteredBooks = if (state.searchKey.isBlank()) {
|
||||||
trySend(emptyList())
|
sortedBooks
|
||||||
}
|
} else {
|
||||||
|
sortedBooks.filter { it.filename.contains(state.searchKey, ignoreCase = true) }
|
||||||
override fun screen(key: String?) {
|
|
||||||
if (key.isNullOrBlank()) {
|
|
||||||
trySend(list)
|
|
||||||
} else {
|
|
||||||
trySend(
|
|
||||||
list.filter { it.filename.contains(key) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
awaitClose {
|
RemoteBookUiState(
|
||||||
dataCallback = null
|
items = filteredBooks.map { book ->
|
||||||
}
|
val isOnShelf = if (book.isDir) false else localFileNames.contains(book.filename)
|
||||||
}.map { list ->
|
val updatedBook =
|
||||||
if (sortAscending) when (sortKey) {
|
if (book.isOnBookShelf != isOnShelf) book.copy(isOnBookShelf = isOnShelf) else book
|
||||||
RemoteBookSort.Name -> list.sortedWith(compareBy<RemoteBook> { !it.isDir }
|
RemoteBookItemUi(
|
||||||
then compareBy(AlphanumComparator) { it.filename })
|
remoteBook = updatedBook,
|
||||||
|
nameWithoutExtension = book.filename.substringBeforeLast("."),
|
||||||
|
formattedSize = ConvertUtils.formatFileSize(book.size),
|
||||||
|
formattedDate = AppConst.dateFormat.format(book.lastModify)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
selectedIds = state.selectedIds,
|
||||||
|
searchKey = state.searchKey,
|
||||||
|
interaction = state.interaction,
|
||||||
|
navigation = NavigationState(
|
||||||
|
pathNames = listOf(if (state.isDefaultWebdav) "books" else "/").plus(state.dirList.map { it.filename }),
|
||||||
|
canGoBack = state.dirList.isNotEmpty()
|
||||||
|
),
|
||||||
|
sortKey = state.sortKey,
|
||||||
|
sortAscending = state.sortAscending,
|
||||||
|
servers = servers,
|
||||||
|
selectedServerId = state.selectedServerId
|
||||||
|
)
|
||||||
|
}.flowOn(Dispatchers.Default)
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
initialValue = RemoteBookUiState()
|
||||||
|
)
|
||||||
|
|
||||||
else -> list.sortedWith(compareBy({ !it.isDir }, { it.lastModify }))
|
private val _permissionDenialEvent = MutableSharedFlow<Int>(
|
||||||
} else when (sortKey) {
|
replay = 0,
|
||||||
RemoteBookSort.Name -> list.sortedWith { o1, o2 ->
|
extraBufferCapacity = 1,
|
||||||
val compare = -compareValues(o1.isDir, o2.isDir)
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
if (compare == 0) {
|
)
|
||||||
return@sortedWith -AlphanumComparator.compare(o1.filename, o2.filename)
|
val permissionDenialEvent: SharedFlow<Int> = _permissionDenialEvent
|
||||||
}
|
|
||||||
return@sortedWith compare
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> list.sortedWith { o1, o2 ->
|
|
||||||
val compare = -compareValues(o1.isDir, o2.isDir)
|
|
||||||
if (compare == 0) {
|
|
||||||
return@sortedWith -compareValues(o1.lastModify, o2.lastModify)
|
|
||||||
}
|
|
||||||
return@sortedWith compare
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.flowOn(Dispatchers.IO)
|
|
||||||
|
|
||||||
private var remoteBookWebDav: RemoteBookWebDav? = null
|
|
||||||
var isDefaultWebdav = false
|
|
||||||
|
|
||||||
fun initData(onSuccess: () -> Unit) {
|
fun initData(onSuccess: () -> Unit) {
|
||||||
execute {
|
viewModelScope.launch {
|
||||||
isDefaultWebdav = false
|
try {
|
||||||
appDb.serverDao.get(AppConfig.remoteServerId)?.getWebDavConfig()?.let {
|
val webDav = repository.createWebDav(AppConfig.remoteServerId)
|
||||||
val authorization = Authorization(it)
|
if (webDav != null) {
|
||||||
remoteBookWebDav = RemoteBookWebDav(it.url, authorization, AppConfig.remoteServerId)
|
_state.update {
|
||||||
return@execute
|
it.copy(
|
||||||
}
|
remoteBookWebDav = webDav,
|
||||||
isDefaultWebdav = true
|
isDefaultWebdav = false,
|
||||||
remoteBookWebDav = AppWebDav.defaultBookWebDav
|
selectedServerId = AppConfig.remoteServerId
|
||||||
?: throw NoStackTraceException("webDav没有配置")
|
)
|
||||||
}.onError {
|
}
|
||||||
context.toastOnUi("初始化webDav出错:${it.localizedMessage}")
|
onSuccess()
|
||||||
}.onSuccess {
|
} else {
|
||||||
onSuccess.invoke()
|
val defaultWebDav = AppWebDav.defaultBookWebDav
|
||||||
}
|
?: throw NoStackTraceException("webDav没有配置")
|
||||||
}
|
_state.update {
|
||||||
|
it.copy(
|
||||||
fun loadRemoteBookList(path: String?, loadCallback: (loading: Boolean) -> Unit) {
|
remoteBookWebDav = defaultWebDav,
|
||||||
executeLazy {
|
isDefaultWebdav = true,
|
||||||
val bookWebDav = remoteBookWebDav
|
selectedServerId = AppConst.DEFAULT_WEBDAV_ID
|
||||||
?: throw NoStackTraceException("没有配置webDav")
|
)
|
||||||
dataCallback?.clear()
|
}
|
||||||
val url = path ?: bookWebDav.rootBookUrl
|
onSuccess()
|
||||||
val bookList = bookWebDav.getRemoteBookList(url)
|
|
||||||
dataCallback?.setItems(bookList)
|
|
||||||
}.onError {
|
|
||||||
AppLog.put("获取webDav书籍出错\n${it.localizedMessage}", it)
|
|
||||||
context.toastOnUi("获取webDav书籍出错\n${it.localizedMessage}")
|
|
||||||
}.onStart {
|
|
||||||
loadCallback.invoke(true)
|
|
||||||
}.onFinally {
|
|
||||||
loadCallback.invoke(false)
|
|
||||||
}.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addToBookshelf(remoteBooks: HashSet<RemoteBook>, finally: () -> Unit) {
|
|
||||||
execute {
|
|
||||||
val bookWebDav = remoteBookWebDav
|
|
||||||
?: throw NoStackTraceException("没有配置webDav")
|
|
||||||
remoteBooks.forEach { remoteBook ->
|
|
||||||
val downloadBookUri = bookWebDav.downloadRemoteBook(remoteBook)
|
|
||||||
LocalBook.importFiles(downloadBookUri).forEach { book ->
|
|
||||||
book.origin = BookType.webDavTag + CustomUrl(remoteBook.path)
|
|
||||||
.putAttribute("serverID", bookWebDav.serverID)
|
|
||||||
.toString()
|
|
||||||
book.save()
|
|
||||||
}
|
}
|
||||||
remoteBook.isOnBookShelf = true
|
} catch (e: Exception) {
|
||||||
|
context.toastOnUi("初始化webDav出错:${e.localizedMessage}")
|
||||||
|
_state.update { it.copy(interaction = it.interaction.copy(isLoading = false)) }
|
||||||
}
|
}
|
||||||
}.onError {
|
|
||||||
AppLog.put("导入出错\n${it.localizedMessage}", it)
|
|
||||||
context.toastOnUi("导入出错\n${it.localizedMessage}")
|
|
||||||
if (it is SecurityException) {
|
|
||||||
permissionDenialLiveData.postValue(1)
|
|
||||||
}
|
|
||||||
}.onFinally {
|
|
||||||
finally.invoke()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateCallBackFlow(filterKey: String?) {
|
fun loadRemoteBookList(path: String? = null) {
|
||||||
dataCallback?.screen(filterKey)
|
_state.update { it.copy(interaction = it.interaction.copy(isLoading = true)) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val webDav = _state.value.remoteBookWebDav
|
||||||
|
?: throw NoStackTraceException("没有配置webDav")
|
||||||
|
val bookList = withContext(Dispatchers.IO) {
|
||||||
|
repository.loadBooks(webDav, path)
|
||||||
|
}
|
||||||
|
_state.update { it.copy(remoteBooks = bookList) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AppLog.put("获取webDav书籍出错\n${e.localizedMessage}", e)
|
||||||
|
context.toastOnUi("获取webDav书籍出错\n${e.localizedMessage}")
|
||||||
|
} finally {
|
||||||
|
_state.update { it.copy(interaction = it.interaction.copy(isLoading = false)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataCallback {
|
suspend fun addToBookshelf(remoteBooks: Set<RemoteBook>): Result<Unit> {
|
||||||
|
_state.update { it.copy(interaction = it.interaction.copy(isUploading = true)) }
|
||||||
|
return try {
|
||||||
|
val webDav = _state.value.remoteBookWebDav
|
||||||
|
?: throw NoStackTraceException("没有配置webDav")
|
||||||
|
|
||||||
fun setItems(remoteFiles: List<RemoteBook>)
|
withContext(Dispatchers.IO) {
|
||||||
|
remoteBooks.forEach { remoteBook ->
|
||||||
fun addItems(remoteFiles: List<RemoteBook>)
|
val downloadBookUri = repository.downloadBook(webDav, remoteBook)
|
||||||
|
LocalBook.importFiles(downloadBookUri).forEach { book ->
|
||||||
fun clear()
|
book.origin = BookType.webDavTag + CustomUrl(remoteBook.path)
|
||||||
|
.putAttribute("serverID", webDav.serverID)
|
||||||
fun screen(key: String?)
|
.toString()
|
||||||
|
book.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_state.update { it.copy(selectedIds = emptySet()) }
|
||||||
|
Result.success(Unit)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
_permissionDenialEvent.emit(1)
|
||||||
|
Result.failure(e)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AppLog.put("导入出错\n${e.localizedMessage}", e)
|
||||||
|
context.toastOnUi("导入出错\n${e.localizedMessage}")
|
||||||
|
Result.failure(e)
|
||||||
|
} finally {
|
||||||
|
_state.update { it.copy(interaction = it.interaction.copy(isUploading = false)) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
suspend fun getLocalBook(fileName: String): Book? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
appDb.bookDao.getBookByFileName(fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateToDir(book: RemoteBook) {
|
||||||
|
_state.update { it.copy(dirList = it.dirList + book, selectedIds = emptySet()) }
|
||||||
|
loadRemoteBookList(book.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateBack() {
|
||||||
|
val lastPath = _state.value.dirList.let {
|
||||||
|
if (it.size > 1) it[it.size - 2].path else null
|
||||||
|
}
|
||||||
|
_state.update {
|
||||||
|
if (it.dirList.isNotEmpty()) {
|
||||||
|
it.copy(dirList = it.dirList.dropLast(1), selectedIds = emptySet())
|
||||||
|
} else it
|
||||||
|
}
|
||||||
|
loadRemoteBookList(lastPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun navigateToLevel(index: Int) {
|
||||||
|
if (index < 0) return
|
||||||
|
val currentDirList = _state.value.dirList
|
||||||
|
if (index > currentDirList.size) return
|
||||||
|
|
||||||
|
if (index == 0) {
|
||||||
|
_state.update { it.copy(dirList = emptyList(), selectedIds = emptySet()) }
|
||||||
|
loadRemoteBookList(null)
|
||||||
|
} else {
|
||||||
|
val newDirList = currentDirList.take(index)
|
||||||
|
val targetPath = newDirList.last().path
|
||||||
|
_state.update { it.copy(dirList = newDirList, selectedIds = emptySet()) }
|
||||||
|
loadRemoteBookList(targetPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSearchMode(isSearch: Boolean) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
interaction = it.interaction.copy(isSearchMode = isSearch),
|
||||||
|
searchKey = if (isSearch) it.searchKey else ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSearchKey(key: String) {
|
||||||
|
_state.update { it.copy(searchKey = key) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateSort(sortKey: RemoteBookSort, ascending: Boolean) {
|
||||||
|
_state.update { it.copy(sortKey = sortKey, sortAscending = ascending) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSort(sortKey: RemoteBookSort) {
|
||||||
|
_state.update { state ->
|
||||||
|
if (state.sortKey == sortKey) {
|
||||||
|
state.copy(sortAscending = !state.sortAscending)
|
||||||
|
} else {
|
||||||
|
state.copy(sortKey = sortKey, sortAscending = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSelection(id: String) {
|
||||||
|
_state.update {
|
||||||
|
val newSelected = if (id in it.selectedIds) it.selectedIds - id else it.selectedIds + id
|
||||||
|
it.copy(selectedIds = newSelected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectAllCheckable() {
|
||||||
|
_state.update { state ->
|
||||||
|
val checkableIds = state.remoteBooks
|
||||||
|
.filter { !it.isDir && !it.isOnBookShelf }
|
||||||
|
.map { it.path }
|
||||||
|
.toSet()
|
||||||
|
state.copy(selectedIds = checkableIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invertSelection() {
|
||||||
|
_state.update { state ->
|
||||||
|
val checkableIds = state.remoteBooks
|
||||||
|
.filter { !it.isDir && !it.isOnBookShelf }
|
||||||
|
.map { it.path }
|
||||||
|
.toSet()
|
||||||
|
state.copy(selectedIds = checkableIds - state.selectedIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearSelection() {
|
||||||
|
_state.update { it.copy(selectedIds = emptySet()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshData() {
|
||||||
|
val currentPath = _state.value.dirList.lastOrNull()?.path
|
||||||
|
loadRemoteBookList(currentPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortBooks(
|
||||||
|
books: List<RemoteBook>,
|
||||||
|
sortKey: RemoteBookSort,
|
||||||
|
ascending: Boolean
|
||||||
|
): List<RemoteBook> {
|
||||||
|
return books.sortedWith { o1, o2 ->
|
||||||
|
val dirCompare = compareValues(o1.isDir, o2.isDir)
|
||||||
|
if (dirCompare != 0) return@sortedWith -dirCompare
|
||||||
|
|
||||||
|
val result = when (sortKey) {
|
||||||
|
RemoteBookSort.Name -> AlphanumComparator.compare(o1.filename, o2.filename)
|
||||||
|
else -> compareValues(o1.lastModify, o2.lastModify)
|
||||||
|
}
|
||||||
|
if (ascending) result else -result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveServer(server: Server) {
|
||||||
|
execute {
|
||||||
|
appDb.serverDao.insert(server)
|
||||||
|
}.onSuccess {
|
||||||
|
if (AppConfig.remoteServerId == server.id) {
|
||||||
|
initData { loadRemoteBookList() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteServer(server: Server) {
|
||||||
|
execute {
|
||||||
|
appDb.serverDao.delete(server)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectServer(serverId: Long) {
|
||||||
|
AppConfig.remoteServerId = serverId
|
||||||
|
_state.update { it.copy(selectedServerId = serverId, dirList = emptyList()) }
|
||||||
|
initData { loadRemoteBookList() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,6 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -319,11 +318,8 @@ fun ReadRecordScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showCalendar) {
|
if (showCalendar) {
|
||||||
val sheetState =
|
|
||||||
rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
onDismissRequest = { showCalendar = false },
|
onDismissRequest = { showCalendar = false }
|
||||||
sheetState = sheetState
|
|
||||||
) {
|
) {
|
||||||
HeatmapCalendarSection(
|
HeatmapCalendarSection(
|
||||||
dailyReadCounts = state.dailyReadCounts,
|
dailyReadCounts = state.dailyReadCounts,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import io.legado.app.model.ReadBook
|
|||||||
import io.legado.app.model.localBook.LocalBook
|
import io.legado.app.model.localBook.LocalBook
|
||||||
import io.legado.app.ui.config.readConfig.ReadConfig
|
import io.legado.app.ui.config.readConfig.ReadConfig
|
||||||
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||||
import io.legado.app.ui.widget.components.rules.RuleActionState
|
import io.legado.app.ui.widget.components.rules.ListUiState
|
||||||
import io.legado.app.ui.widget.components.rules.SelectableItem
|
import io.legado.app.ui.widget.components.rules.SelectableItem
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -74,9 +74,9 @@ data class TocActionState(
|
|||||||
override val selectedIds: Set<Int> = emptySet(),
|
override val selectedIds: Set<Int> = emptySet(),
|
||||||
override val searchKey: String = "",
|
override val searchKey: String = "",
|
||||||
override val isSearch: Boolean = false,
|
override val isSearch: Boolean = false,
|
||||||
override val isUploading: Boolean = false,
|
override val isLoading: Boolean = false,
|
||||||
val downloadSummary: String = ""
|
val downloadSummary: String = ""
|
||||||
) : RuleActionState<TocItemUi>
|
) : ListUiState<TocItemUi>
|
||||||
|
|
||||||
data class TocDomainItem(
|
data class TocDomainItem(
|
||||||
val chapter: BookChapter,
|
val chapter: BookChapter,
|
||||||
@@ -305,7 +305,7 @@ class TocViewModel(
|
|||||||
selectedIds = selectedIds,
|
selectedIds = selectedIds,
|
||||||
searchKey = _searchKey.value,
|
searchKey = _searchKey.value,
|
||||||
isSearch = isSearch,
|
isSearch = isSearch,
|
||||||
isUploading = isUploading,
|
isLoading = isUploading,
|
||||||
downloadSummary = downloadSummary.value
|
downloadSummary = downloadSummary.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,7 +155,6 @@ fun TxtRuleScreen(
|
|||||||
|
|
||||||
if (showFilePickerSheet) {
|
if (showFilePickerSheet) {
|
||||||
FilePickerSheet(
|
FilePickerSheet(
|
||||||
sheetState = sheetState,
|
|
||||||
onDismissRequest = { showFilePickerSheet = false },
|
onDismissRequest = { showFilePickerSheet = false },
|
||||||
onSelectSysDir = {
|
onSelectSysDir = {
|
||||||
showFilePickerSheet = false
|
showFilePickerSheet = false
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import io.legado.app.data.repository.TxtTocRuleRepository
|
|||||||
import io.legado.app.data.repository.UploadRepository
|
import io.legado.app.data.repository.UploadRepository
|
||||||
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||||
import io.legado.app.ui.widget.components.rules.InteractionState
|
import io.legado.app.ui.widget.components.rules.InteractionState
|
||||||
import io.legado.app.ui.widget.components.rules.RuleActionState
|
import io.legado.app.ui.widget.components.rules.ListUiState
|
||||||
import io.legado.app.ui.widget.components.rules.SelectableItem
|
import io.legado.app.ui.widget.components.rules.SelectableItem
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.fromJsonArray
|
import io.legado.app.utils.fromJsonArray
|
||||||
@@ -36,9 +36,9 @@ data class TxtTocRuleUiState(
|
|||||||
override val selectedIds: Set<Long> = emptySet(),
|
override val selectedIds: Set<Long> = emptySet(),
|
||||||
override val searchKey: String = "",
|
override val searchKey: String = "",
|
||||||
val interaction: InteractionState = InteractionState()
|
val interaction: InteractionState = InteractionState()
|
||||||
) : RuleActionState<TxtTocRuleItemUi> {
|
) : ListUiState<TxtTocRuleItemUi> {
|
||||||
override val isSearch: Boolean get() = interaction.isSearchMode
|
override val isSearch: Boolean get() = interaction.isSearchMode
|
||||||
override val isUploading: Boolean get() = interaction.isUploading
|
override val isLoading: Boolean get() = interaction.isUploading
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
@@ -32,13 +31,9 @@ fun CheckSourceBottomSheet(
|
|||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sheetState = rememberModalBottomSheetState(
|
|
||||||
skipPartiallyExpanded = true
|
|
||||||
)
|
|
||||||
|
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss
|
||||||
sheetState = sheetState
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
+1
-4
@@ -27,7 +27,6 @@ import androidx.compose.material3.OutlinedTextField
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -57,7 +56,6 @@ fun DirectLinkUploadBottomSheet(
|
|||||||
onDismiss: () -> Unit
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
var showTestResult by remember { mutableStateOf<String?>(null) }
|
var showTestResult by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
@@ -66,8 +64,7 @@ fun DirectLinkUploadBottomSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss
|
||||||
sheetState = sheetState,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -394,7 +393,6 @@ fun OtherConfigScreen(
|
|||||||
|
|
||||||
if (showFilePicker) {
|
if (showFilePicker) {
|
||||||
FilePickerSheet(
|
FilePickerSheet(
|
||||||
sheetState = rememberModalBottomSheetState(),
|
|
||||||
onDismissRequest = { showFilePicker = false },
|
onDismissRequest = { showFilePicker = false },
|
||||||
onSelectSysDir = {
|
onSelectSysDir = {
|
||||||
showFilePicker = false
|
showFilePicker = false
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -51,7 +50,6 @@ fun DictRuleEditSheet(
|
|||||||
onPaste: () -> DictRule?
|
onPaste: () -> DictRule?
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
|
|
||||||
var name by remember(rule) { mutableStateOf(rule?.name ?: "") }
|
var name by remember(rule) { mutableStateOf(rule?.name ?: "") }
|
||||||
var urlRule by remember(rule) { mutableStateOf(rule?.urlRule ?: "") }
|
var urlRule by remember(rule) { mutableStateOf(rule?.urlRule ?: "") }
|
||||||
@@ -59,8 +57,7 @@ fun DictRuleEditSheet(
|
|||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest
|
||||||
sheetState = sheetState,
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -152,7 +152,6 @@ fun DictRuleScreen(
|
|||||||
|
|
||||||
if (showFilePickerSheet) {
|
if (showFilePickerSheet) {
|
||||||
FilePickerSheet(
|
FilePickerSheet(
|
||||||
sheetState = sheetState,
|
|
||||||
onDismissRequest = { showFilePickerSheet = false },
|
onDismissRequest = { showFilePickerSheet = false },
|
||||||
onSelectSysDir = {
|
onSelectSysDir = {
|
||||||
showFilePickerSheet = false
|
showFilePickerSheet = false
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import io.legado.app.data.repository.DictRuleRepository
|
|||||||
import io.legado.app.data.repository.UploadRepository
|
import io.legado.app.data.repository.UploadRepository
|
||||||
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||||
import io.legado.app.ui.widget.components.rules.InteractionState
|
import io.legado.app.ui.widget.components.rules.InteractionState
|
||||||
import io.legado.app.ui.widget.components.rules.RuleActionState
|
import io.legado.app.ui.widget.components.rules.ListUiState
|
||||||
import io.legado.app.ui.widget.components.rules.SelectableItem
|
import io.legado.app.ui.widget.components.rules.SelectableItem
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.fromJsonArray
|
import io.legado.app.utils.fromJsonArray
|
||||||
@@ -39,9 +39,9 @@ data class DictRuleUiState(
|
|||||||
override val selectedIds: Set<String> = emptySet(),
|
override val selectedIds: Set<String> = emptySet(),
|
||||||
override val searchKey: String = "",
|
override val searchKey: String = "",
|
||||||
val interaction: InteractionState = InteractionState()
|
val interaction: InteractionState = InteractionState()
|
||||||
) : RuleActionState<DictRuleItemUi> {
|
) : ListUiState<DictRuleItemUi> {
|
||||||
override val isSearch: Boolean get() = interaction.isSearchMode
|
override val isSearch: Boolean get() = interaction.isSearchMode
|
||||||
override val isUploading: Boolean get() = interaction.isUploading
|
override val isLoading: Boolean get() = interaction.isUploading
|
||||||
}
|
}
|
||||||
|
|
||||||
class DictRuleViewModel(
|
class DictRuleViewModel(
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import androidx.compose.material3.IconButton
|
|||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.SheetState
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -36,7 +35,6 @@ import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
|||||||
@Composable
|
@Composable
|
||||||
fun GroupManageBottomSheet(
|
fun GroupManageBottomSheet(
|
||||||
groups: List<String>,
|
groups: List<String>,
|
||||||
sheetState: SheetState,
|
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
viewModel: ReplaceRuleViewModel
|
viewModel: ReplaceRuleViewModel
|
||||||
) {
|
) {
|
||||||
@@ -44,8 +42,7 @@ fun GroupManageBottomSheet(
|
|||||||
var updatedGroupName by remember { mutableStateOf("") }
|
var updatedGroupName by remember { mutableStateOf("") }
|
||||||
|
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest
|
||||||
sheetState = sheetState
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import androidx.compose.material3.TooltipAnchorPosition
|
|||||||
import androidx.compose.material3.TooltipBox
|
import androidx.compose.material3.TooltipBox
|
||||||
import androidx.compose.material3.TooltipDefaults
|
import androidx.compose.material3.TooltipDefaults
|
||||||
import androidx.compose.material3.animateFloatingActionButton
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.material3.rememberTooltipState
|
import androidx.compose.material3.rememberTooltipState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
@@ -112,7 +111,6 @@ fun ReplaceRuleScreen(
|
|||||||
var showGroupManageSheet by remember { mutableStateOf(false) }
|
var showGroupManageSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
val importState by viewModel.importState.collectAsStateWithLifecycle()
|
val importState by viewModel.importState.collectAsStateWithLifecycle()
|
||||||
val sheetState = rememberModalBottomSheetState()
|
|
||||||
|
|
||||||
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
var selectedTabIndex by remember { mutableIntStateOf(0) }
|
||||||
val tabItems = remember(groups) { listOf("全部") + groups }
|
val tabItems = remember(groups) { listOf("全部") + groups }
|
||||||
@@ -166,7 +164,6 @@ fun ReplaceRuleScreen(
|
|||||||
|
|
||||||
if (showFilePickerSheet) {
|
if (showFilePickerSheet) {
|
||||||
FilePickerSheet(
|
FilePickerSheet(
|
||||||
sheetState = sheetState,
|
|
||||||
onDismissRequest = { showFilePickerSheet = false },
|
onDismissRequest = { showFilePickerSheet = false },
|
||||||
onSelectSysDir = {
|
onSelectSysDir = {
|
||||||
showFilePickerSheet = false
|
showFilePickerSheet = false
|
||||||
@@ -255,7 +252,6 @@ fun ReplaceRuleScreen(
|
|||||||
GroupManageBottomSheet(
|
GroupManageBottomSheet(
|
||||||
groups = groups,
|
groups = groups,
|
||||||
onDismissRequest = { showGroupManageSheet = false },
|
onDismissRequest = { showGroupManageSheet = false },
|
||||||
sheetState = sheetState,
|
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import io.legado.app.data.repository.UploadRepository
|
|||||||
import io.legado.app.help.ReplaceAnalyzer
|
import io.legado.app.help.ReplaceAnalyzer
|
||||||
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
import io.legado.app.ui.widget.components.importComponents.BaseImportUiState
|
||||||
import io.legado.app.ui.widget.components.rules.InteractionState
|
import io.legado.app.ui.widget.components.rules.InteractionState
|
||||||
import io.legado.app.ui.widget.components.rules.RuleActionState
|
import io.legado.app.ui.widget.components.rules.ListUiState
|
||||||
import io.legado.app.ui.widget.components.rules.SelectableItem
|
import io.legado.app.ui.widget.components.rules.SelectableItem
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.getPrefString
|
import io.legado.app.utils.getPrefString
|
||||||
@@ -53,9 +53,9 @@ data class ReplaceRuleUiState(
|
|||||||
val sortMode: String = "desc",
|
val sortMode: String = "desc",
|
||||||
val groups: List<String> = emptyList(),
|
val groups: List<String> = emptyList(),
|
||||||
val interaction: InteractionState = InteractionState()
|
val interaction: InteractionState = InteractionState()
|
||||||
) : RuleActionState<ReplaceRuleItemUi> {
|
) : ListUiState<ReplaceRuleItemUi> {
|
||||||
override val isSearch: Boolean get() = interaction.isSearchMode
|
override val isSearch: Boolean get() = interaction.isSearchMode
|
||||||
override val isUploading: Boolean get() = interaction.isUploading
|
override val isLoading: Boolean get() = interaction.isUploading
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReplaceRuleViewModel(
|
class ReplaceRuleViewModel(
|
||||||
|
|||||||
+3
-4
@@ -63,13 +63,12 @@ object GlassTopAppBarDefaults {
|
|||||||
@Composable
|
@Composable
|
||||||
fun controlContainerColor(): Color {
|
fun controlContainerColor(): Color {
|
||||||
val enableBlur = ThemeConfig.enableBlur
|
val enableBlur = ThemeConfig.enableBlur
|
||||||
val baseColor = MaterialTheme.colorScheme.surfaceContainerHigh
|
val baseColor = MaterialTheme.colorScheme.surfaceContainerHighest
|
||||||
|
|
||||||
return if (enableBlur) {
|
return if (enableBlur) {
|
||||||
val alpha = 0.6f
|
baseColor.copy(alpha = 0.72f)
|
||||||
baseColor.copy(alpha = alpha)
|
|
||||||
} else {
|
} else {
|
||||||
baseColor.copy(alpha = ThemeConfig.containerOpacity / 100f)
|
baseColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,18 +7,22 @@ import androidx.compose.animation.fadeOut
|
|||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.scaleOut
|
import androidx.compose.animation.scaleOut
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
|
import androidx.compose.foundation.text.input.clearText
|
||||||
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Clear
|
import androidx.compose.material.icons.filled.Clear
|
||||||
import androidx.compose.material.icons.filled.Search
|
import androidx.compose.material.icons.filled.Search
|
||||||
import androidx.compose.material.icons.filled.VerticalAlignTop
|
import androidx.compose.material.icons.filled.VerticalAlignTop
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
@@ -26,16 +30,19 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -44,8 +51,13 @@ import kotlinx.coroutines.launch
|
|||||||
fun SearchBarSection(
|
fun SearchBarSection(
|
||||||
query: String,
|
query: String,
|
||||||
onQueryChange: (String) -> Unit,
|
onQueryChange: (String) -> Unit,
|
||||||
placeholder: String = "搜索书名",
|
placeholder: String = "搜索...",
|
||||||
leadingIcon: @Composable (() -> Unit)? = { Icon(Icons.Default.Search, null) },
|
leadingIcon: @Composable (() -> Unit)? = {
|
||||||
|
SmallIconButton(
|
||||||
|
icon = Icons.Default.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
onClick = {})
|
||||||
|
},
|
||||||
backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerLow,
|
backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
scrollState: LazyListState? = null,
|
scrollState: LazyListState? = null,
|
||||||
scope: CoroutineScope = rememberCoroutineScope(),
|
scope: CoroutineScope = rememberCoroutineScope(),
|
||||||
@@ -54,6 +66,25 @@ fun SearchBarSection(
|
|||||||
) {
|
) {
|
||||||
var showMenu by remember { mutableStateOf(false) }
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val textFieldState = rememberTextFieldState(initialText = query)
|
||||||
|
|
||||||
|
LaunchedEffect(query) {
|
||||||
|
if (query != textFieldState.text.toString()) {
|
||||||
|
textFieldState.edit {
|
||||||
|
replace(0, length, query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(textFieldState.text) {
|
||||||
|
snapshotFlow { textFieldState.text.toString() }
|
||||||
|
.collect { newText ->
|
||||||
|
if (newText != query) {
|
||||||
|
onQueryChange(newText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val showScrollToTop by remember(scrollState) {
|
val showScrollToTop by remember(scrollState) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
(scrollState?.firstVisibleItemIndex ?: 0) > 0
|
(scrollState?.firstVisibleItemIndex ?: 0) > 0
|
||||||
@@ -63,15 +94,15 @@ fun SearchBarSection(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
.padding(start = 16.dp, end = 16.dp, bottom = 4.dp),
|
||||||
shape = RoundedCornerShape(32.dp),
|
shape = RoundedCornerShape(32.dp),
|
||||||
color = backgroundColor
|
color = backgroundColor
|
||||||
) {
|
) {
|
||||||
TextField(
|
TextField(
|
||||||
value = query,
|
state = textFieldState,
|
||||||
onValueChange = onQueryChange,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 48.dp)
|
||||||
.padding(horizontal = 4.dp),
|
.padding(horizontal = 4.dp),
|
||||||
placeholder = { Text(placeholder) },
|
placeholder = { Text(placeholder) },
|
||||||
leadingIcon = leadingIcon,
|
leadingIcon = leadingIcon,
|
||||||
@@ -82,14 +113,17 @@ fun SearchBarSection(
|
|||||||
.padding(end = 4.dp)
|
.padding(end = 4.dp)
|
||||||
.animateContentSize()
|
.animateContentSize()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = query.isNotEmpty(),
|
visible = textFieldState.text.isNotEmpty(),
|
||||||
enter = fadeIn() + scaleIn(),
|
enter = fadeIn() + scaleIn(),
|
||||||
exit = fadeOut() + scaleOut()
|
exit = fadeOut() + scaleOut()
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = { onQueryChange("") }) {
|
SmallIconButton(
|
||||||
Icon(Icons.Default.Clear, "清空输入")
|
onClick = { textFieldState.clearText() },
|
||||||
}
|
icon = Icons.Default.Clear,
|
||||||
|
contentDescription = "清空输入"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
@@ -97,19 +131,27 @@ fun SearchBarSection(
|
|||||||
enter = fadeIn() + scaleIn(),
|
enter = fadeIn() + scaleIn(),
|
||||||
exit = fadeOut() + scaleOut()
|
exit = fadeOut() + scaleOut()
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = {
|
SmallIconButton(
|
||||||
scope.launch { scrollState?.animateScrollToItem(0) }
|
onClick = {
|
||||||
}) {
|
scope.launch {
|
||||||
Icon(Icons.Default.VerticalAlignTop, "回到顶部")
|
scrollState?.animateScrollToItem(0)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
icon = Icons.Default.VerticalAlignTop,
|
||||||
|
contentDescription = "回到顶部"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 外部自定义按钮
|
|
||||||
if (trailingIcon != null) {
|
if (trailingIcon != null) {
|
||||||
Box {
|
Box {
|
||||||
IconButton(onClick = { if (dropdownMenu != null) showMenu = true }) {
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
if (dropdownMenu != null) showMenu = true
|
||||||
|
}
|
||||||
|
) {
|
||||||
trailingIcon()
|
trailingIcon()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dropdownMenu != null) {
|
if (dropdownMenu != null) {
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = showMenu,
|
expanded = showMenu,
|
||||||
@@ -122,13 +164,19 @@ fun SearchBarSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
singleLine = true,
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
unfocusedContainerColor = Color.Transparent,
|
unfocusedContainerColor = Color.Transparent,
|
||||||
disabledContainerColor = Color.Transparent,
|
disabledContainerColor = Color.Transparent,
|
||||||
focusedIndicatorColor = Color.Transparent,
|
focusedIndicatorColor = Color.Transparent,
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = 4.dp,
|
||||||
|
bottom = 4.dp,
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package io.legado.app.ui.widget.components
|
package io.legado.app.ui.widget.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Refresh
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.material.icons.filled.SelectAll
|
import androidx.compose.material.icons.filled.SelectAll
|
||||||
@@ -30,6 +28,13 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
data class SelectionActions(
|
||||||
|
val primaryAction: ActionItem,
|
||||||
|
val secondaryActions: List<ActionItem> = emptyList(),
|
||||||
|
val onSelectAll: () -> Unit,
|
||||||
|
val onSelectInvert: () -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
data class ActionItem(
|
data class ActionItem(
|
||||||
val text: String,
|
val text: String,
|
||||||
val icon: @Composable (() -> Unit)? = null,
|
val icon: @Composable (() -> Unit)? = null,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import androidx.compose.material3.OutlinedButton
|
|||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -37,15 +36,13 @@ fun BookmarkEditSheet(
|
|||||||
onSave: (Bookmark) -> Unit,
|
onSave: (Bookmark) -> Unit,
|
||||||
onDelete: (Bookmark) -> Unit
|
onDelete: (Bookmark) -> Unit
|
||||||
) {
|
) {
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
|
|
||||||
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
|
var showDeleteConfirmDialog by remember { mutableStateOf(false) }
|
||||||
var bookText by remember { mutableStateOf(bookmark.bookText) }
|
var bookText by remember { mutableStateOf(bookmark.bookText) }
|
||||||
var content by remember { mutableStateOf(bookmark.content) }
|
var content by remember { mutableStateOf(bookmark.content) }
|
||||||
|
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss
|
||||||
sheetState = sheetState
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -1,19 +1,35 @@
|
|||||||
package io.legado.app.ui.widget.components.button
|
package io.legado.app.ui.widget.components.button
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.IconToggleButtonShapes
|
import androidx.compose.material3.IconToggleButtonShapes
|
||||||
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedIconToggleButton
|
import androidx.compose.material3.OutlinedIconToggleButton
|
||||||
|
import androidx.compose.material3.OutlinedToggleButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@@ -42,6 +58,35 @@ fun SmallIconButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SmallTonalIconButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String? = null
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
|
||||||
|
FilledTonalIconButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.size(
|
||||||
|
IconButtonDefaults.extraSmallContainerSize(
|
||||||
|
IconButtonDefaults.IconButtonWidthOption.Uniform
|
||||||
|
)
|
||||||
|
),
|
||||||
|
shape = IconButtonDefaults.extraSmallRoundShape,
|
||||||
|
colors = IconButtonDefaults.filledTonalIconButtonColors()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = Modifier.size(
|
||||||
|
IconButtonDefaults.extraSmallIconSize
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SmallOutlinedIconToggleButton(
|
fun SmallOutlinedIconToggleButton(
|
||||||
@@ -81,4 +126,62 @@ fun SmallOutlinedIconToggleButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SmallAnimatedActionButton(
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
iconChecked: ImageVector,
|
||||||
|
iconUnchecked: ImageVector,
|
||||||
|
activeText: String,
|
||||||
|
inactiveText: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var showText by remember { mutableStateOf(false) }
|
||||||
|
var lastCheckedState by remember { mutableStateOf(checked) }
|
||||||
|
|
||||||
|
LaunchedEffect(showText) {
|
||||||
|
if (showText) {
|
||||||
|
delay(1000)
|
||||||
|
showText = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
|
||||||
|
OutlinedToggleButton(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
lastCheckedState = it
|
||||||
|
onCheckedChange(it)
|
||||||
|
showText = true
|
||||||
|
},
|
||||||
|
modifier = modifier.height(36.dp),
|
||||||
|
contentPadding = PaddingValues(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
AnimatedIcon(
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
imageVector = if (checked) iconChecked else iconUnchecked,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showText
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (lastCheckedState) activeText else inactiveText,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
modifier = Modifier.padding(start = 6.dp),
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +1,42 @@
|
|||||||
package io.legado.app.ui.widget.components.button
|
package io.legado.app.ui.widget.components.button
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.scaleIn
|
import androidx.compose.animation.scaleIn
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.FilledTonalIconButton
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedIconButton
|
import androidx.compose.material3.OutlinedIconButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ToggleButton
|
||||||
import androidx.compose.runtime.Composable
|
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.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
enum class TopBarButtonVariant {
|
enum class TopBarButtonVariant {
|
||||||
Filled, Outlined, Icon
|
Filled, Outlined, Icon
|
||||||
@@ -49,7 +64,11 @@ fun TopBarButton(
|
|||||||
contentColor = MaterialTheme.colorScheme.onSurface
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
SmallIcon(imageVector, contentDescription)
|
AnimatedIcon(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = contentDescription
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,7 +78,11 @@ fun TopBarButton(
|
|||||||
modifier = commonModifier,
|
modifier = commonModifier,
|
||||||
border = ButtonDefaults.outlinedButtonBorder()
|
border = ButtonDefaults.outlinedButtonBorder()
|
||||||
) {
|
) {
|
||||||
SmallIcon(imageVector, contentDescription)
|
AnimatedIcon(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = contentDescription
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,7 +91,11 @@ fun TopBarButton(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = commonModifier
|
modifier = commonModifier
|
||||||
) {
|
) {
|
||||||
SmallIcon(imageVector, contentDescription)
|
AnimatedIcon(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
imageVector = imageVector,
|
||||||
|
contentDescription = contentDescription
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -86,7 +113,7 @@ fun TopbarNavigationButton(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
imageVector = imageVector,
|
imageVector = imageVector,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
modifier = modifier.padding(start = 12.dp, end = 4.dp),
|
modifier = modifier.padding(horizontal = 12.dp),
|
||||||
style = style
|
style = style
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -118,8 +145,74 @@ fun TopBarActionButton(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun SmallIcon(imageVector: ImageVector, contentDescription: String?) {
|
fun TopBarAnimatedActionButton(
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
iconChecked: ImageVector,
|
||||||
|
iconUnchecked: ImageVector,
|
||||||
|
activeText: String,
|
||||||
|
inactiveText: String,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var showText by remember { mutableStateOf(false) }
|
||||||
|
var lastCheckedState by remember { mutableStateOf(checked) }
|
||||||
|
|
||||||
|
LaunchedEffect(showText) {
|
||||||
|
if (showText) {
|
||||||
|
delay(1000)
|
||||||
|
showText = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToggleButton(
|
||||||
|
modifier = modifier.height(36.dp),
|
||||||
|
contentPadding = PaddingValues(horizontal = 8.dp),
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
lastCheckedState = it
|
||||||
|
onCheckedChange(it)
|
||||||
|
showText = true
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = checked,
|
||||||
|
label = "IconAnimation"
|
||||||
|
) { targetChecked ->
|
||||||
|
AnimatedIcon(
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
imageVector = if (targetChecked) iconChecked else iconUnchecked,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showText
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = if (lastCheckedState) activeText else inactiveText,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
maxLines = 1,
|
||||||
|
softWrap = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AnimatedIcon(
|
||||||
|
imageVector: ImageVector,
|
||||||
|
contentDescription: String?,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = imageVector,
|
targetState = imageVector,
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
@@ -131,7 +224,7 @@ private fun SmallIcon(imageVector: ImageVector, contentDescription: String?) {
|
|||||||
Icon(
|
Icon(
|
||||||
imageVector = targetIcon,
|
imageVector = targetIcon,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
-3
@@ -19,7 +19,6 @@ import androidx.compose.material.icons.filled.FolderOpen
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SheetState
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -38,7 +37,6 @@ enum class FilePickerSheetMode {
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun FilePickerSheet(
|
fun FilePickerSheet(
|
||||||
sheetState: SheetState,
|
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
title: String = stringResource(R.string.select_operation),
|
title: String = stringResource(R.string.select_operation),
|
||||||
onSelectSysDir: (() -> Unit)? = null,
|
onSelectSysDir: (() -> Unit)? = null,
|
||||||
@@ -49,7 +47,6 @@ fun FilePickerSheet(
|
|||||||
) {
|
) {
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
sheetState = sheetState,
|
|
||||||
containerColor = { it.surface }
|
containerColor = { it.surface }
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
-1
@@ -139,7 +139,6 @@ fun <T> BatchImportDialog(
|
|||||||
|
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
sheetState = sheetState,
|
|
||||||
containerColor = { it.surfaceContainer }
|
containerColor = { it.surfaceContainer }
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package io.legado.app.ui.widget.components.list
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.FloatingToolbarDefaults.ScreenOffset
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.PlainTooltip
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TooltipAnchorPosition
|
||||||
|
import androidx.compose.material3.TooltipBox
|
||||||
|
import androidx.compose.material3.TooltipDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.animateFloatingActionButton
|
||||||
|
import androidx.compose.material3.rememberTooltipState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import io.legado.app.ui.widget.components.SelectionActions
|
||||||
|
import io.legado.app.ui.widget.components.SelectionBottomBar
|
||||||
|
import io.legado.app.ui.widget.components.rules.ListUiState
|
||||||
|
import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun <T> ListScaffold(
|
||||||
|
title: String,
|
||||||
|
state: ListUiState<T>,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onSearchToggle: (Boolean) -> Unit,
|
||||||
|
onSearchQueryChange: (String) -> Unit,
|
||||||
|
searchPlaceholder: String = "搜索...",
|
||||||
|
topBarActions: @Composable RowScope.() -> Unit = {},
|
||||||
|
bottomContent: @Composable (ColumnScope.() -> Unit)? = null,
|
||||||
|
dropDownMenuContent: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit = {},
|
||||||
|
selectionActions: SelectionActions? = null,
|
||||||
|
onAddClick: (() -> Unit)? = null,
|
||||||
|
floatingActionButton: @Composable () -> Unit = {
|
||||||
|
onAddClick?.let { onClick ->
|
||||||
|
TooltipBox(
|
||||||
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
|
||||||
|
TooltipAnchorPosition.Above
|
||||||
|
),
|
||||||
|
tooltip = { PlainTooltip { Text("添加") } },
|
||||||
|
state = rememberTooltipState(),
|
||||||
|
) {
|
||||||
|
FloatingActionButton(
|
||||||
|
modifier = Modifier.animateFloatingActionButton(
|
||||||
|
visible = state.selectedIds.isEmpty(),
|
||||||
|
alignment = Alignment.BottomEnd,
|
||||||
|
),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Add, contentDescription = "Add")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
|
||||||
|
content: @Composable (PaddingValues) -> Unit
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
topBar = {
|
||||||
|
DynamicTopAppBar(
|
||||||
|
title = title,
|
||||||
|
state = state,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onSearchToggle = onSearchToggle,
|
||||||
|
onSearchQueryChange = onSearchQueryChange,
|
||||||
|
searchPlaceholder = searchPlaceholder,
|
||||||
|
onClearSelection = { selectionActions?.onSelectInvert?.invoke() }, // 或其他逻辑
|
||||||
|
topBarActions = topBarActions,
|
||||||
|
dropDownMenuContent = dropDownMenuContent,
|
||||||
|
bottomContent = bottomContent
|
||||||
|
)
|
||||||
|
},
|
||||||
|
floatingActionButton = floatingActionButton
|
||||||
|
) { paddingValues ->
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
content(paddingValues)
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.selectedIds.isNotEmpty() && selectionActions != null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.padding(bottom = 16.dp + ScreenOffset)
|
||||||
|
.zIndex(1f),
|
||||||
|
enter = slideInVertically { it } + fadeIn(),
|
||||||
|
exit = slideOutVertically { it } + fadeOut()
|
||||||
|
) {
|
||||||
|
selectionActions?.let { actions ->
|
||||||
|
SelectionBottomBar(
|
||||||
|
onSelectAll = actions.onSelectAll,
|
||||||
|
onSelectInvert = actions.onSelectInvert,
|
||||||
|
primaryAction = actions.primaryAction,
|
||||||
|
secondaryActions = actions.secondaryActions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-1
@@ -24,7 +24,9 @@ import io.legado.app.ui.theme.rememberOpaqueColorScheme
|
|||||||
fun GlassModalBottomSheet(
|
fun GlassModalBottomSheet(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
sheetState: SheetState = rememberModalBottomSheetState(),
|
sheetState: SheetState = rememberModalBottomSheetState(
|
||||||
|
skipPartiallyExpanded = true
|
||||||
|
),
|
||||||
containerColor: (ColorScheme) -> Color = { it.surfaceContainerLow },
|
containerColor: (ColorScheme) -> Color = { it.surfaceContainerLow },
|
||||||
content: @Composable ColumnScope.() -> Unit
|
content: @Composable ColumnScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
|
|||||||
+2
-2
@@ -1,11 +1,11 @@
|
|||||||
package io.legado.app.ui.widget.components.rules
|
package io.legado.app.ui.widget.components.rules
|
||||||
|
|
||||||
interface RuleActionState<T> {
|
interface ListUiState<T> {
|
||||||
val items: List<T>
|
val items: List<T>
|
||||||
val selectedIds: Set<Any>
|
val selectedIds: Set<Any>
|
||||||
val searchKey: String
|
val searchKey: String
|
||||||
val isSearch: Boolean
|
val isSearch: Boolean
|
||||||
val isUploading: Boolean
|
val isLoading: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
data class InteractionState(
|
data class InteractionState(
|
||||||
@@ -24,7 +24,6 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberModalBottomSheetState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@@ -65,7 +64,6 @@ fun <T> RuleEditSheet(
|
|||||||
fromFields: (RuleEditFields, T?) -> T
|
fromFields: (RuleEditFields, T?) -> T
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
||||||
|
|
||||||
val initialFields = remember(rule) { toFields(rule) }
|
val initialFields = remember(rule) { toFields(rule) }
|
||||||
var name by remember(initialFields) { mutableStateOf(initialFields.name) }
|
var name by remember(initialFields) { mutableStateOf(initialFields.name) }
|
||||||
@@ -77,8 +75,7 @@ fun <T> RuleEditSheet(
|
|||||||
fun getCurrentEntity() = fromFields(RuleEditFields(name, rule1, rule2), rule)
|
fun getCurrentEntity() = fromFields(RuleEditFields(name, rule1, rule2), rule)
|
||||||
|
|
||||||
GlassModalBottomSheet(
|
GlassModalBottomSheet(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest
|
||||||
sheetState = sheetState,
|
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
Box(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(
|
Column(
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Search
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.ButtonDefaults
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -44,7 +43,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -58,14 +56,11 @@ import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar
|
|||||||
@Composable
|
@Composable
|
||||||
fun <T> RuleListScaffold(
|
fun <T> RuleListScaffold(
|
||||||
title: String,
|
title: String,
|
||||||
state: RuleActionState<T>,
|
state: ListUiState<T>,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onSearchToggle: (Boolean) -> Unit,
|
onSearchToggle: (Boolean) -> Unit,
|
||||||
onSearchQueryChange: (String) -> Unit,
|
onSearchQueryChange: (String) -> Unit,
|
||||||
searchPlaceholder: String,
|
searchPlaceholder: String = "搜索...",
|
||||||
searchLeadingIcon: ImageVector = Icons.Default.Search,
|
|
||||||
searchTrailingIcon: @Composable (() -> Unit)? = null,
|
|
||||||
searchDropdownMenu: (@Composable (onDismiss: () -> Unit) -> Unit)? = null,
|
|
||||||
topBarActions: @Composable RowScope.() -> Unit = {},
|
topBarActions: @Composable RowScope.() -> Unit = {},
|
||||||
bottomContent: @Composable (ColumnScope.() -> Unit)? = null,
|
bottomContent: @Composable (ColumnScope.() -> Unit)? = null,
|
||||||
dropDownMenuContent: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit = {},
|
dropDownMenuContent: @Composable ColumnScope.(dismiss: () -> Unit) -> Unit = {},
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
|||||||
import io.legado.app.ui.widget.components.SearchBarSection
|
import io.legado.app.ui.widget.components.SearchBarSection
|
||||||
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
|
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.rules.RuleActionState
|
import io.legado.app.ui.widget.components.rules.ListUiState
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -39,7 +39,7 @@ import io.legado.app.ui.widget.components.rules.RuleActionState
|
|||||||
fun <T> DynamicTopAppBar(
|
fun <T> DynamicTopAppBar(
|
||||||
title: String,
|
title: String,
|
||||||
subtitle: String? = null,
|
subtitle: String? = null,
|
||||||
state: RuleActionState<T>,
|
state: ListUiState<T>,
|
||||||
scrollBehavior: TopAppBarScrollBehavior,
|
scrollBehavior: TopAppBarScrollBehavior,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onSearchToggle: (Boolean) -> Unit,
|
onSearchToggle: (Boolean) -> Unit,
|
||||||
@@ -60,13 +60,13 @@ fun <T> DynamicTopAppBar(
|
|||||||
GlassMediumFlexibleTopAppBar(
|
GlassMediumFlexibleTopAppBar(
|
||||||
title = {
|
title = {
|
||||||
val titleText = when {
|
val titleText = when {
|
||||||
state.isUploading -> "请稍后..."
|
state.isLoading -> "请稍后..."
|
||||||
isSelecting -> "已选择 ${state.selectedIds.size}/${state.items.size}"
|
isSelecting -> "已选择 ${state.selectedIds.size}/${state.items.size}"
|
||||||
else -> title
|
else -> title
|
||||||
}
|
}
|
||||||
AdaptiveAnimatedText(
|
AdaptiveAnimatedText(
|
||||||
text = titleText,
|
text = titleText,
|
||||||
useCharMode = isSelecting || state.isUploading,
|
useCharMode = isSelecting || state.isLoading,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in new issue
Block a user