[优化] 界面优化
This commit is contained in:
@@ -22,6 +22,7 @@ import io.legado.app.help.http.okHttpClientManga
|
||||
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
||||
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
||||
import io.legado.app.ui.book.group.GroupViewModel
|
||||
import io.legado.app.ui.book.import.local.ImportBookViewModel
|
||||
import io.legado.app.ui.book.import.remote.RemoteBookViewModel
|
||||
import io.legado.app.ui.book.info.BookInfoViewModel
|
||||
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
||||
@@ -97,6 +98,7 @@ val appModule = module {
|
||||
viewModelOf(::ThemeConfigViewModel)
|
||||
viewModelOf(::BackupConfigViewModel)
|
||||
viewModelOf(::TocViewModel)
|
||||
viewModelOf(::ImportBookViewModel)
|
||||
viewModelOf(::RemoteBookViewModel)
|
||||
viewModelOf(::BookInfoViewModel)
|
||||
viewModelOf(::ExploreViewModel)
|
||||
|
||||
@@ -49,6 +49,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.data.entities.Bookmark
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
@@ -242,7 +243,7 @@ fun AllBookmarkScreen(
|
||||
FastScrollLazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = adaptiveContentPaddingOnlyVertical(
|
||||
contentPadding = adaptiveContentPadding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = 120.dp
|
||||
)
|
||||
|
||||
@@ -235,7 +235,6 @@ fun ExploreShowScreen(
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = showKindSheet,
|
||||
containerColor = LegadoTheme.colorScheme.surface,
|
||||
onDismissRequest = { showKindSheet = false }
|
||||
) {
|
||||
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
package io.legado.app.ui.book.import
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.databinding.ActivityImportBookBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
//import io.legado.app.lib.theme.primaryTextColor
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.ui.file.HandleFileContract
|
||||
import io.legado.app.utils.ArchiveUtils
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.hideSoftInput
|
||||
import io.legado.app.utils.shouldHideSoftInput
|
||||
import io.legado.app.utils.startActivityForBook
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
abstract class BaseImportBookActivity<VM : ViewModel> :
|
||||
VMBaseActivity<ActivityImportBookBinding, VM>() {
|
||||
|
||||
final override val binding by viewBinding(ActivityImportBookBinding::inflate)
|
||||
|
||||
private var localBookTreeSelectListener: ((Boolean) -> Unit)? = null
|
||||
protected val searchView: SearchView by lazy {
|
||||
binding.titleBar.findViewById(R.id.search_view)
|
||||
}
|
||||
|
||||
val localBookTreeSelect = registerForActivityResult(HandleFileContract()) {
|
||||
it.uri?.let { treeUri ->
|
||||
AppConfig.defaultBookTreeUri = treeUri.toString()
|
||||
localBookTreeSelectListener?.invoke(true)
|
||||
} ?: localBookTreeSelectListener?.invoke(false)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
initSearchView()
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置书籍保存位置
|
||||
*/
|
||||
protected suspend fun setBookStorage() = suspendCoroutine { block ->
|
||||
localBookTreeSelectListener = {
|
||||
localBookTreeSelectListener = null
|
||||
block.resume(it)
|
||||
}
|
||||
//测试书籍保存位置是否设置
|
||||
if (!AppConfig.defaultBookTreeUri.isNullOrBlank()) {
|
||||
localBookTreeSelectListener = null
|
||||
block.resume(true)
|
||||
return@suspendCoroutine
|
||||
}
|
||||
//测试读写??
|
||||
val storageHelp = String(assets.open("storageHelp.md").readBytes())
|
||||
val hint = getString(R.string.select_book_folder)
|
||||
alert(hint, storageHelp) {
|
||||
okButton {
|
||||
localBookTreeSelect.launch {
|
||||
title = hint
|
||||
}
|
||||
}
|
||||
cancelButton {
|
||||
localBookTreeSelectListener = null
|
||||
block.resume(false)
|
||||
}
|
||||
onCancelled {
|
||||
localBookTreeSelectListener = null
|
||||
block.resume(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onSearchTextChange(newText: String?)
|
||||
|
||||
protected fun startReadBook(book: Book) {
|
||||
startActivityForBook(book)
|
||||
}
|
||||
|
||||
protected fun onArchiveFileClick(fileDoc: FileDoc) {
|
||||
val fileNames = ArchiveUtils.getArchiveFilesName(fileDoc) {
|
||||
it.matches(AppPattern.bookFileRegex)
|
||||
}
|
||||
if (fileNames.size == 1) {
|
||||
val name = fileNames[0]
|
||||
appDb.bookDao.getBookByFileName(name)?.let {
|
||||
startReadBook(it)
|
||||
} ?: showImportAlert(fileDoc, name)
|
||||
} else {
|
||||
showSelectBookReadAlert(fileDoc, fileNames)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSelectBookReadAlert(fileDoc: FileDoc, fileNames: List<String>) {
|
||||
if (fileNames.isEmpty()) {
|
||||
toastOnUi(R.string.unsupport_archivefile_entry)
|
||||
return
|
||||
}
|
||||
selector(
|
||||
R.string.start_read,
|
||||
fileNames
|
||||
) { _, name, _ ->
|
||||
appDb.bookDao.getBookByFileName(name)?.let {
|
||||
startReadBook(it)
|
||||
} ?: showImportAlert(fileDoc, name)
|
||||
}
|
||||
}
|
||||
|
||||
/* 添加压缩包内指定文件到书架 */
|
||||
private inline fun addArchiveToBookShelf(
|
||||
fileDoc: FileDoc,
|
||||
fileName: String,
|
||||
onSuccess: (Book) -> Unit
|
||||
) {
|
||||
LocalBook.importArchiveFile(fileDoc.uri, fileName) {
|
||||
it.contains(fileName)
|
||||
}.firstOrNull()?.run {
|
||||
onSuccess.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
/* 提示是否重新导入所点击的压缩文件 */
|
||||
private fun showImportAlert(fileDoc: FileDoc, fileName: String) {
|
||||
alert(
|
||||
R.string.draw,
|
||||
R.string.no_book_found_bookshelf
|
||||
) {
|
||||
okButton {
|
||||
addArchiveToBookShelf(fileDoc, fileName) {
|
||||
startReadBook(it)
|
||||
}
|
||||
}
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initSearchView() {
|
||||
//searchView.applyTint(primaryTextColor)
|
||||
searchView.isSubmitButtonEnabled = true
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
onSearchTextChange(newText)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
package io.legado.app.ui.book.import.local
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.CloudDownload
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.FolderOpen
|
||||
import androidx.compose.material.icons.outlined.Book
|
||||
import androidx.compose.material.icons.outlined.Description
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
import io.legado.app.lib.permission.Permissions
|
||||
import io.legado.app.lib.permission.PermissionsCompat
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.ActionItem
|
||||
import io.legado.app.ui.widget.components.EmptyMessageView
|
||||
import io.legado.app.ui.widget.components.SelectionActions
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
||||
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.card.TextCard
|
||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.utils.ConvertUtils
|
||||
import io.legado.app.utils.ArchiveUtils
|
||||
import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.isContentScheme
|
||||
import io.legado.app.utils.isUri
|
||||
import io.legado.app.utils.startActivityForBook
|
||||
import io.legado.app.utils.takePersistablePermissionSafely
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import java.io.File
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ImportBookContent(
|
||||
state: ImportBookUiState,
|
||||
onBackClick: () -> Unit,
|
||||
onSearchToggle: (Boolean) -> Unit,
|
||||
onSearchQueryChange: (String) -> Unit,
|
||||
onSelectFolder: () -> Unit,
|
||||
onScanFolder: () -> Unit,
|
||||
onImportFileName: () -> Unit,
|
||||
onSortChange: (Int) -> Unit,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToLevel: (Int) -> Unit,
|
||||
onSelectAll: () -> Unit,
|
||||
onSelectInvert: () -> Unit,
|
||||
onAddToBookshelf: () -> Unit,
|
||||
onDeleteSelection: () -> Unit,
|
||||
onItemClick: (ImportBook) -> Unit
|
||||
) {
|
||||
ListScaffold(
|
||||
title = stringResource(R.string.local_book),
|
||||
state = state,
|
||||
onBackClick = onBackClick,
|
||||
onSearchToggle = onSearchToggle,
|
||||
onSearchQueryChange = onSearchQueryChange,
|
||||
searchPlaceholder = stringResource(R.string.screen),
|
||||
topBarActions = {
|
||||
TopBarActionButton(
|
||||
onClick = onSelectFolder,
|
||||
imageVector = Icons.Default.FolderOpen,
|
||||
contentDescription = stringResource(R.string.select_folder)
|
||||
)
|
||||
},
|
||||
dropDownMenuContent = { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.sort_by_name),
|
||||
onClick = {
|
||||
onSortChange(0)
|
||||
dismiss()
|
||||
},
|
||||
trailingIcon = {
|
||||
if (state.sort == 0) {
|
||||
Icon(Icons.Default.Check, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.sort_by_size),
|
||||
onClick = {
|
||||
onSortChange(1)
|
||||
dismiss()
|
||||
},
|
||||
trailingIcon = {
|
||||
if (state.sort == 1) {
|
||||
Icon(Icons.Default.Check, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.sort_by_time),
|
||||
onClick = {
|
||||
onSortChange(2)
|
||||
dismiss()
|
||||
},
|
||||
trailingIcon = {
|
||||
if (state.sort == 2) {
|
||||
Icon(Icons.Default.Check, null)
|
||||
}
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.scan_folder),
|
||||
onClick = {
|
||||
onScanFolder()
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.import_file_name),
|
||||
onClick = {
|
||||
onImportFileName()
|
||||
dismiss()
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomContent = {
|
||||
ImportPathNavigationBar(
|
||||
pathNames = state.pathNames,
|
||||
canGoBack = state.canGoBack,
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateToLevel = onNavigateToLevel
|
||||
)
|
||||
},
|
||||
selectionActions = SelectionActions(
|
||||
onSelectAll = onSelectAll,
|
||||
onSelectInvert = onSelectInvert,
|
||||
primaryAction = ActionItem(
|
||||
text = stringResource(R.string.add_to_bookshelf),
|
||||
icon = { Icon(Icons.Default.CloudDownload, null) },
|
||||
onClick = onAddToBookshelf
|
||||
),
|
||||
secondaryActions = listOf(
|
||||
ActionItem(
|
||||
text = stringResource(R.string.delete),
|
||||
icon = { Icon(Icons.Default.Delete, null) },
|
||||
onClick = onDeleteSelection
|
||||
)
|
||||
)
|
||||
),
|
||||
onAddClick = null
|
||||
) { paddingValues ->
|
||||
val refreshState = rememberPullToRefreshState()
|
||||
PullToRefreshBox(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(paddingValues),
|
||||
isRefreshing = state.isLoading,
|
||||
state = refreshState,
|
||||
onRefresh = onScanFolder
|
||||
) {
|
||||
when {
|
||||
state.items.isEmpty() && state.isLoading -> {
|
||||
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
|
||||
}
|
||||
|
||||
state.items.isEmpty() -> {
|
||||
EmptyMessageView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
message = stringResource(R.string.empty_msg_import_book)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(state.items, key = { it.selectionId }) { item ->
|
||||
ImportBookItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
item = item,
|
||||
isSelected = item.selectionId in state.selectedIds,
|
||||
onClick = { onItemClick(item) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ImportBookScreen(
|
||||
onBackClick: () -> Unit,
|
||||
viewModel: ImportBookViewModel = koinViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
var showFolderPicker by remember { mutableStateOf(false) }
|
||||
var showImportFileNameDialog by remember { mutableStateOf(false) }
|
||||
var fileNameJs by remember { mutableStateOf(AppConfig.bookImportFileName.orEmpty()) }
|
||||
var pickerTarget by remember { mutableStateOf(ImportFolderPickTarget.IMPORT_FOLDER) }
|
||||
val selectDocTree = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocumentTree()
|
||||
) { uri ->
|
||||
viewModel.dispatch(ImportBookIntent.FolderPicked(uri, pickerTarget))
|
||||
}
|
||||
|
||||
LaunchedEffect(viewModel) {
|
||||
viewModel.dispatch(ImportBookIntent.Initialize)
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is ImportBookEffect.RequestFolderPicker -> {
|
||||
pickerTarget = effect.target
|
||||
selectDocTree.launch(effect.initialUri)
|
||||
}
|
||||
|
||||
is ImportBookEffect.OpenBook -> {
|
||||
context.startActivityForBook(effect.book)
|
||||
}
|
||||
|
||||
is ImportBookEffect.ShowArchiveEntries -> {
|
||||
context.selector(R.string.start_read, effect.fileNames) { _, name, _ ->
|
||||
viewModel.dispatch(
|
||||
ImportBookIntent.ArchiveEntrySelected(effect.fileDoc, name)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is ImportBookEffect.ShowImportArchiveDialog -> {
|
||||
context.alert(R.string.draw, R.string.no_book_found_bookshelf) {
|
||||
okButton {
|
||||
viewModel.dispatch(
|
||||
ImportBookIntent.ImportArchiveConfirmed(
|
||||
effect.fileDoc,
|
||||
effect.fileName
|
||||
)
|
||||
)
|
||||
}
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
|
||||
is ImportBookEffect.ShowToastRes -> {
|
||||
context.toastOnUi(effect.resId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FilePickerSheet(
|
||||
show = showFolderPicker,
|
||||
onDismissRequest = { showFolderPicker = false },
|
||||
title = stringResource(R.string.select_folder),
|
||||
onSelectSysDir = {
|
||||
showFolderPicker = false
|
||||
viewModel.dispatch(ImportBookIntent.SelectFolderClick)
|
||||
}
|
||||
)
|
||||
|
||||
AppAlertDialog(
|
||||
show = showImportFileNameDialog,
|
||||
onDismissRequest = { showImportFileNameDialog = false },
|
||||
title = stringResource(R.string.import_file_name),
|
||||
content = {
|
||||
AppText("Use js to parse file name from src, then assign name/author.")
|
||||
OutlinedTextField(
|
||||
value = fileNameJs,
|
||||
onValueChange = { fileNameJs = it },
|
||||
label = { AppText("js") }
|
||||
)
|
||||
},
|
||||
confirmText = stringResource(android.R.string.ok),
|
||||
onConfirm = {
|
||||
AppConfig.bookImportFileName = fileNameJs
|
||||
showImportFileNameDialog = false
|
||||
},
|
||||
dismissText = stringResource(android.R.string.cancel),
|
||||
onDismiss = { showImportFileNameDialog = false }
|
||||
)
|
||||
|
||||
ImportBookContent(
|
||||
state = uiState,
|
||||
onBackClick = onBackClick,
|
||||
onSearchToggle = { viewModel.dispatch(ImportBookIntent.SearchToggle(it)) },
|
||||
onSearchQueryChange = { viewModel.dispatch(ImportBookIntent.SearchQueryChange(it)) },
|
||||
onSelectFolder = { showFolderPicker = true },
|
||||
onScanFolder = { viewModel.dispatch(ImportBookIntent.ScanFolder) },
|
||||
onImportFileName = {
|
||||
fileNameJs = AppConfig.bookImportFileName.orEmpty()
|
||||
showImportFileNameDialog = true
|
||||
},
|
||||
onSortChange = { viewModel.dispatch(ImportBookIntent.SortChange(it)) },
|
||||
onNavigateBack = { viewModel.dispatch(ImportBookIntent.NavigateBack) },
|
||||
onNavigateToLevel = { viewModel.dispatch(ImportBookIntent.NavigateToLevel(it)) },
|
||||
onSelectAll = { viewModel.dispatch(ImportBookIntent.SelectAll) },
|
||||
onSelectInvert = { viewModel.dispatch(ImportBookIntent.SelectInvert) },
|
||||
onAddToBookshelf = { viewModel.dispatch(ImportBookIntent.AddToBookshelf) },
|
||||
onDeleteSelection = { viewModel.dispatch(ImportBookIntent.DeleteSelection) },
|
||||
onItemClick = { viewModel.dispatch(ImportBookIntent.ItemClick(it)) }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportPathNavigationBar(
|
||||
pathNames: List<String>,
|
||||
canGoBack: Boolean,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToLevel: (Int) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, bottom = 4.dp)
|
||||
.animateContentSize(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
GlassCard(
|
||||
modifier = Modifier.weight(1f),
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainer
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
itemsIndexed(pathNames) { index, name ->
|
||||
val isLast = index == pathNames.lastIndex
|
||||
AppText(
|
||||
text = name,
|
||||
style = LegadoTheme.typography.labelSmall,
|
||||
fontWeight = if (isLast) FontWeight.SemiBold else FontWeight.Medium,
|
||||
color = if (isLast) {
|
||||
LegadoTheme.colorScheme.primary
|
||||
} else {
|
||||
LegadoTheme.colorScheme.onSurfaceVariant
|
||||
},
|
||||
modifier = Modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.then(if (!isLast) Modifier.clickable { onNavigateToLevel(index) } else Modifier)
|
||||
.padding(horizontal = 4.dp, vertical = 4.dp)
|
||||
)
|
||||
if (!isLast) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(12.dp),
|
||||
tint = LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (canGoBack) {
|
||||
SmallTonalIconButton(
|
||||
onClick = onNavigateBack,
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = "back"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ImportBookItem(
|
||||
modifier: Modifier,
|
||||
item: ImportBook,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val containerColor = if (isSelected) {
|
||||
LegadoTheme.colorScheme.secondaryContainer
|
||||
} else {
|
||||
LegadoTheme.colorScheme.surfaceContainer
|
||||
}
|
||||
|
||||
GlassCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||
onClick = onClick,
|
||||
containerColor = containerColor
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = when {
|
||||
item.isDir -> Icons.Default.Folder
|
||||
item.isOnBookShelf -> Icons.Outlined.Book
|
||||
else -> Icons.Outlined.Description
|
||||
},
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp),
|
||||
tint = if (item.isDir) {
|
||||
LegadoTheme.colorScheme.primary
|
||||
} else {
|
||||
LegadoTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
AppText(
|
||||
text = item.name,
|
||||
style = LegadoTheme.typography.bodyMedium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
|
||||
if (!item.isDir) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
TextCard(
|
||||
text = item.name.substringAfterLast('.', "").uppercase(),
|
||||
textStyle = LegadoTheme.typography.labelSmall,
|
||||
horizontalPadding = 4.dp,
|
||||
verticalPadding = 2.dp,
|
||||
cornerRadius = 4.dp,
|
||||
icon = null,
|
||||
backgroundColor = LegadoTheme.colorScheme.surfaceContainerHighest
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
AppText(
|
||||
text = "${ConvertUtils.formatFileSize(item.size)} - ${AppConst.dateFormat.format(item.lastModified)}",
|
||||
style = LegadoTheme.typography.labelMedium,
|
||||
color = LegadoTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!item.isDir && !item.isOnBookShelf) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Icon(
|
||||
imageVector = if (isSelected) Icons.Default.Check else Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
tint = LegadoTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,15 +145,15 @@ fun BookInfoEditContent(
|
||||
)
|
||||
)
|
||||
},
|
||||
icon = Icons.Default.ImageSearch
|
||||
imageVector = Icons.Default.ImageSearch
|
||||
)
|
||||
MediumOutlinedIconButton(
|
||||
onClick = { selectCover.launch() },
|
||||
icon = Icons.Default.FolderOpen
|
||||
imageVector = Icons.Default.FolderOpen
|
||||
)
|
||||
MediumOutlinedIconButton(
|
||||
onClick = { viewModel.resetCover() },
|
||||
icon = Icons.Default.Replay
|
||||
imageVector = Icons.Default.Replay
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
@@ -69,6 +69,8 @@ import cn.hutool.core.date.DateUtil
|
||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.CollapsibleHeader
|
||||
import io.legado.app.ui.widget.components.AppScaffold
|
||||
@@ -78,9 +80,12 @@ import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.AppIconButton
|
||||
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.card.TextCard
|
||||
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
|
||||
import io.legado.app.ui.widget.components.cover.Cover
|
||||
import io.legado.app.ui.widget.components.heatmap.HeatmapCalendarTopBar
|
||||
import io.legado.app.ui.widget.components.heatmap.HEATMAP_CALENDAR_TITLE
|
||||
import io.legado.app.ui.widget.components.heatmap.HeatmapCalendarEndAction
|
||||
import io.legado.app.ui.widget.components.heatmap.HeatmapCalendarStartAction
|
||||
import io.legado.app.ui.widget.components.heatmap.HeatmapConfig
|
||||
import io.legado.app.ui.widget.components.heatmap.HeatmapLegend
|
||||
import io.legado.app.ui.widget.components.heatmap.HeatmapMode
|
||||
@@ -91,6 +96,7 @@ import io.legado.app.ui.widget.components.heatmap.rememberDateRange
|
||||
import io.legado.app.ui.widget.components.heatmap.rememberDaysInRange
|
||||
import io.legado.app.ui.widget.components.heatmap.rememberWeeks
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
import io.legado.app.ui.widget.components.list.TopFloatingStickyItem
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.swipe.SwipeAction
|
||||
import io.legado.app.ui.widget.components.swipe.SwipeActionContainer
|
||||
@@ -118,6 +124,7 @@ fun ReadRecordScreen(
|
||||
val displayMode by viewModel.displayMode.collectAsState()
|
||||
var showSearch by remember { mutableStateOf(false) }
|
||||
var showCalendar by remember { mutableStateOf(false) }
|
||||
var heatmapMode by remember { mutableStateOf(HeatmapMode.COUNT) }
|
||||
val listState = rememberLazyListState()
|
||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||
|
||||
@@ -131,6 +138,36 @@ fun ReadRecordScreen(
|
||||
pendingDeleteAction = action
|
||||
}
|
||||
}
|
||||
val stickyDate by remember(displayMode, listState) {
|
||||
derivedStateOf {
|
||||
if (displayMode == DisplayMode.LATEST) return@derivedStateOf null
|
||||
val stickyKey = listState.layoutInfo.visibleItemsInfo
|
||||
.firstOrNull { info ->
|
||||
val key = info.key.toString()
|
||||
key.startsWith("header_") ||
|
||||
key.startsWith("timeline_header_") ||
|
||||
key.startsWith("agg_item_") ||
|
||||
key.startsWith("timeline_item_")
|
||||
}?.key?.toString() ?: return@derivedStateOf null
|
||||
|
||||
when {
|
||||
stickyKey.startsWith("header_") -> stickyKey.removePrefix("header_")
|
||||
stickyKey.startsWith("timeline_header_") -> stickyKey.removePrefix("timeline_header_")
|
||||
stickyKey.startsWith("agg_item_") -> stickyKey.removePrefix("agg_item_").substringBefore("|")
|
||||
stickyKey.startsWith("timeline_item_") -> stickyKey.removePrefix("timeline_item_").substringBefore("|")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
val floatingDate by remember(stickyDate, listState, displayMode) {
|
||||
derivedStateOf {
|
||||
if (displayMode == DisplayMode.LATEST) return@derivedStateOf null
|
||||
if (stickyDate == null) return@derivedStateOf null
|
||||
val shouldStick = listState.firstVisibleItemIndex > 1 ||
|
||||
listState.firstVisibleItemScrollOffset > 24
|
||||
if (shouldStick) stickyDate else null
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(state.searchKey) {
|
||||
if (state.searchKey.isNullOrBlank()) {
|
||||
@@ -198,41 +235,52 @@ fun ReadRecordScreen(
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxSize()) {
|
||||
val contentState = when {
|
||||
state.isLoading -> "LOADING"
|
||||
(displayMode == DisplayMode.AGGREGATE && state.groupedRecords.isEmpty()) ||
|
||||
(displayMode == DisplayMode.TIMELINE && state.timelineRecords.isEmpty()) ||
|
||||
(displayMode == DisplayMode.LATEST && state.latestRecords.isEmpty()) -> "EMPTY"
|
||||
else -> "CONTENT"
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = contentState,
|
||||
label = "MainContentAnimation"
|
||||
) { targetState ->
|
||||
when (targetState) {
|
||||
"LOADING" -> {
|
||||
EmptyMessageView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
message = "加载中",
|
||||
isLoading = true
|
||||
)
|
||||
}
|
||||
val contentState = when {
|
||||
state.isLoading -> "LOADING"
|
||||
(displayMode == DisplayMode.AGGREGATE && state.groupedRecords.isEmpty()) ||
|
||||
(displayMode == DisplayMode.TIMELINE && state.timelineRecords.isEmpty()) ||
|
||||
(displayMode == DisplayMode.LATEST && state.latestRecords.isEmpty()) -> "EMPTY"
|
||||
else -> "CONTENT"
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = contentState,
|
||||
label = "MainContentAnimation"
|
||||
) { targetState ->
|
||||
when (targetState) {
|
||||
"LOADING" -> {
|
||||
EmptyMessageView(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = padding.calculateTopPadding(),
|
||||
bottom = padding.calculateBottomPadding()
|
||||
),
|
||||
message = "加载中",
|
||||
isLoading = true
|
||||
)
|
||||
}
|
||||
|
||||
"EMPTY" -> {
|
||||
EmptyMessageView(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
message = "没有记录"
|
||||
)
|
||||
}
|
||||
"EMPTY" -> {
|
||||
EmptyMessageView(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = padding.calculateTopPadding(),
|
||||
bottom = padding.calculateBottomPadding()
|
||||
),
|
||||
message = "没有记录"
|
||||
)
|
||||
}
|
||||
|
||||
"CONTENT" -> {
|
||||
"CONTENT" -> {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = adaptiveContentPaddingOnlyVertical(
|
||||
top = padding.calculateTopPadding(),
|
||||
bottom = padding.calculateBottomPadding() + 16.dp
|
||||
)
|
||||
) {
|
||||
item(key = "summary_card") {
|
||||
SummarySection(state, viewModel)
|
||||
@@ -255,6 +303,32 @@ fun ReadRecordScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
TopFloatingStickyItem(
|
||||
item = floatingDate,
|
||||
modifier = Modifier.padding(
|
||||
top = padding.calculateTopPadding() + 4.dp,
|
||||
start = 8.dp
|
||||
)
|
||||
) { date ->
|
||||
val text = buildString {
|
||||
append(formatFriendlyDate(date))
|
||||
if (displayMode == DisplayMode.AGGREGATE) {
|
||||
val dailyTotal = state.groupedRecords[date]?.sumOf { it.readTime } ?: 0L
|
||||
append(" · ")
|
||||
append(formatDuring(dailyTotal))
|
||||
}
|
||||
}
|
||||
TextCard(
|
||||
text = text,
|
||||
textStyle = LegadoTheme.typography.labelLarge,
|
||||
backgroundColor = LegadoTheme.colorScheme.cardContainer,
|
||||
contentColor = LegadoTheme.colorScheme.onCardContainer,
|
||||
cornerRadius = 8.dp,
|
||||
horizontalPadding = 8.dp,
|
||||
verticalPadding = 8.dp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -292,19 +366,31 @@ fun ReadRecordScreen(
|
||||
|
||||
AppModalBottomSheet(
|
||||
show = showCalendar,
|
||||
onDismissRequest = { showCalendar = false }
|
||||
onDismissRequest = { showCalendar = false },
|
||||
title = HEATMAP_CALENDAR_TITLE,
|
||||
startAction = {
|
||||
HeatmapCalendarStartAction(
|
||||
currentMode = heatmapMode,
|
||||
onModeChanged = { heatmapMode = it }
|
||||
)
|
||||
},
|
||||
endAction = {
|
||||
HeatmapCalendarEndAction(
|
||||
onClearDate = {
|
||||
viewModel.setSelectedDate(null)
|
||||
showCalendar = false
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
HeatmapCalendarSection(
|
||||
dailyReadCounts = state.dailyReadCounts,
|
||||
dailyReadTimes = state.dailyReadTimes,
|
||||
currentMode = heatmapMode,
|
||||
selectedDate = state.selectedDate,
|
||||
onDateSelected = { date ->
|
||||
viewModel.setSelectedDate(date)
|
||||
showCalendar = false
|
||||
},
|
||||
onClearDate = {
|
||||
viewModel.setSelectedDate(null)
|
||||
showCalendar = false
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -404,13 +490,11 @@ fun HeatmapCalendarSection(
|
||||
modifier: Modifier = Modifier,
|
||||
dailyReadCounts: Map<LocalDate, Int>,
|
||||
dailyReadTimes: Map<LocalDate, Long>,
|
||||
currentMode: HeatmapMode,
|
||||
selectedDate: LocalDate?,
|
||||
onDateSelected: (LocalDate) -> Unit,
|
||||
onClearDate: () -> Unit,
|
||||
config: HeatmapConfig = HeatmapConfig()
|
||||
) {
|
||||
var currentMode by remember { mutableStateOf(HeatmapMode.COUNT) }
|
||||
|
||||
val (startDate, endDate) = rememberDateRange(dailyReadCounts, dailyReadTimes)
|
||||
val days = rememberDaysInRange(startDate, endDate)
|
||||
val weeks = rememberWeeks(days, startDate)
|
||||
@@ -452,14 +536,6 @@ fun HeatmapCalendarSection(
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.padding(bottom = 32.dp)
|
||||
) {
|
||||
HeatmapCalendarTopBar(
|
||||
currentMode = currentMode,
|
||||
onModeChanged = { currentMode = it },
|
||||
onClearDate = onClearDate
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
WeekdayLabelsColumn(
|
||||
cellSize = config.cellSize,
|
||||
@@ -543,10 +619,13 @@ fun LazyListScope.renderListByMode(
|
||||
when (displayMode) {
|
||||
DisplayMode.AGGREGATE -> {
|
||||
state.groupedRecords.forEach { (date, details) ->
|
||||
stickyHeader(key = "header_$date") {
|
||||
item(key = "header_$date") {
|
||||
DateHeader(date, details.sumOf { it.readTime })
|
||||
}
|
||||
items(items = details, key = { "${it.bookName}_${it.bookAuthor}_${it.date}" }) { detail ->
|
||||
items(
|
||||
items = details,
|
||||
key = { "agg_item_${date}|${it.bookName}_${it.bookAuthor}_${it.date}" }
|
||||
) { detail ->
|
||||
SwipeActionContainer(
|
||||
modifier = Modifier.animateItem(),
|
||||
startAction = SwipeAction(
|
||||
@@ -568,8 +647,8 @@ fun LazyListScope.renderListByMode(
|
||||
|
||||
DisplayMode.TIMELINE -> {
|
||||
state.timelineRecords.forEach { (date, sessions) ->
|
||||
stickyHeader(key = "timeline_header_$date") { DateHeader(date) }
|
||||
items(items = sessions, key = { it.id }) { session ->
|
||||
item(key = "timeline_header_$date") { DateHeader(date) }
|
||||
items(items = sessions, key = { "timeline_item_${date}|${it.id}" }) { session ->
|
||||
SwipeActionContainer(
|
||||
modifier = Modifier.animateItem(),
|
||||
startAction = SwipeAction(
|
||||
@@ -637,7 +716,7 @@ fun LatestReadItem(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
.adaptiveHorizontalPadding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Cover(coverPath)
|
||||
@@ -798,7 +877,7 @@ fun ReadRecordItem(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
.adaptiveHorizontalPadding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Cover(coverPath)
|
||||
@@ -877,7 +956,7 @@ fun ReadingSummaryCard(
|
||||
onClick = onClick,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
.adaptiveHorizontalPadding(vertical = 8.dp),
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainer
|
||||
) {
|
||||
Row(
|
||||
|
||||
@@ -345,7 +345,7 @@ fun SearchHistoryList(
|
||||
trailingContent = {
|
||||
SmallIconButton(
|
||||
onClick = { onDeleteHistory(item) },
|
||||
icon = Icons.Default.Close,
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "删除"
|
||||
)
|
||||
},
|
||||
@@ -366,7 +366,7 @@ fun SearchHistoryList(
|
||||
MediumOutlinedButton(
|
||||
onClick = onClearHistory,
|
||||
modifier = Modifier.fillMaxWidth(0.6f),
|
||||
icon = Icons.Outlined.DeleteSweep,
|
||||
imageVector = Icons.Outlined.DeleteSweep,
|
||||
text = "清除搜索历史"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -469,7 +469,7 @@ fun TocScreen(
|
||||
SmallOutlinedIconToggleButton(
|
||||
checked = showVolumeMenu,
|
||||
onCheckedChange = { showVolumeMenu = it },
|
||||
icon = Icons.AutoMirrored.Filled.FormatListBulleted,
|
||||
imageVector = Icons.AutoMirrored.Filled.FormatListBulleted,
|
||||
contentDescription = "卷管理"
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
|
||||
@@ -204,7 +204,7 @@ fun TxtRuleScreen(
|
||||
}
|
||||
|
||||
AppAlertDialog(
|
||||
data = showDeleteRuleDialog, // 传入 nullable 的 rule 对象
|
||||
data = showDeleteRuleDialog, // ä¼ å…¥ nullable çš?rule 对象
|
||||
onDismissRequest = { showDeleteRuleDialog = null },
|
||||
title = stringResource(R.string.delete),
|
||||
confirmText = stringResource(R.string.ok),
|
||||
@@ -228,7 +228,7 @@ fun TxtRuleScreen(
|
||||
editingRule = null
|
||||
},
|
||||
onSave = { updatedRule ->
|
||||
//TODO:我很想把他改为自增主键,但为了兼容性日后再说
|
||||
//TODO:我很想把他改为自增主键,但为了兼容性日���
|
||||
if (editingRule == null) {
|
||||
viewModel.insert(updatedRule)
|
||||
} else {
|
||||
@@ -346,7 +346,7 @@ fun TxtRuleScreen(
|
||||
trailingAction = {
|
||||
SmallIconButton(
|
||||
onClick = { showDeleteRuleDialog = item.rule },
|
||||
icon = Icons.Default.Delete
|
||||
imageVector = Icons.Default.Delete
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -368,3 +368,4 @@ fun TxtRuleScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -68,7 +68,7 @@ fun DirectLinkUploadBottomSheet(
|
||||
endAction = {
|
||||
MediumIconButton(
|
||||
onClick = { showMenu = true },
|
||||
icon = Icons.Default.MoreVert
|
||||
imageVector = Icons.Default.MoreVert
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
expanded = showMenu,
|
||||
|
||||
@@ -320,7 +320,7 @@ fun DictRuleScreen(
|
||||
trailingAction = {
|
||||
SmallIconButton(
|
||||
onClick = { showDeleteRuleDialog = item.rule },
|
||||
icon = Icons.Default.Delete
|
||||
imageVector = Icons.Default.Delete
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -341,4 +341,4 @@ fun DictRuleScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.service.WebService
|
||||
import io.legado.app.ui.about.CrashLogsDialog
|
||||
import io.legado.app.ui.about.UpdateDialog
|
||||
import io.legado.app.ui.book.import.local.ImportBookScreen
|
||||
import io.legado.app.ui.book.import.remote.RemoteBookScreen
|
||||
import io.legado.app.ui.book.read.ReadBookActivity
|
||||
import io.legado.app.ui.config.ConfigNavScreen
|
||||
import io.legado.app.ui.config.ConfigTag
|
||||
@@ -74,6 +76,8 @@ open class MainActivity : BaseComposeActivity() {
|
||||
private const val ROUTE_SETTINGS_COVER = "settings/cover"
|
||||
private const val ROUTE_SETTINGS_THEME = "settings/theme"
|
||||
private const val ROUTE_SETTINGS_BACKUP = "settings/backup"
|
||||
private const val ROUTE_IMPORT_LOCAL = "import/local"
|
||||
private const val ROUTE_IMPORT_REMOTE = "import/remote"
|
||||
|
||||
fun createIntent(context: Context, configTag: String? = null): Intent {
|
||||
return Intent(context, MainActivity::class.java).apply {
|
||||
@@ -120,6 +124,12 @@ open class MainActivity : BaseComposeActivity() {
|
||||
@Serializable
|
||||
private data object MainRouteSettingsBackup : MainRoute
|
||||
|
||||
@Serializable
|
||||
private data object MainRouteImportLocal : MainRoute
|
||||
|
||||
@Serializable
|
||||
private data object MainRouteImportRemote : MainRoute
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -260,6 +270,12 @@ open class MainActivity : BaseComposeActivity() {
|
||||
useRail = useRail,
|
||||
onOpenSettings = {
|
||||
navigateToRoute(backStack, MainRouteSettings)
|
||||
},
|
||||
onNavigateToRemoteImport = {
|
||||
navigateToRoute(backStack, MainRouteImportRemote)
|
||||
},
|
||||
onNavigateToLocalImport = {
|
||||
navigateToRoute(backStack, MainRouteImportLocal)
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -294,6 +310,18 @@ open class MainActivity : BaseComposeActivity() {
|
||||
entry<MainRouteSettingsBackup> {
|
||||
BackupConfigScreen(onBackClick = { navigateBack(backStack) })
|
||||
}
|
||||
|
||||
entry<MainRouteImportLocal> {
|
||||
ImportBookScreen(
|
||||
onBackClick = { navigateBack(backStack) }
|
||||
)
|
||||
}
|
||||
|
||||
entry<MainRouteImportRemote> {
|
||||
RemoteBookScreen(
|
||||
onBackClick = { navigateBack(backStack) }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -328,6 +356,17 @@ open class MainActivity : BaseComposeActivity() {
|
||||
backStack.add(MainRouteSettings)
|
||||
backStack.add(route)
|
||||
}
|
||||
|
||||
MainRouteImportLocal,
|
||||
MainRouteImportRemote -> {
|
||||
if (currentRoute == MainRouteHome) {
|
||||
backStack.add(route)
|
||||
} else {
|
||||
backStack.clear()
|
||||
backStack.add(MainRouteHome)
|
||||
backStack.add(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,6 +501,8 @@ open class MainActivity : BaseComposeActivity() {
|
||||
"settings/cover" -> MainRouteSettingsCover
|
||||
"settings/theme" -> MainRouteSettingsTheme
|
||||
"settings/backup" -> MainRouteSettingsBackup
|
||||
"import/local" -> MainRouteImportLocal
|
||||
"import/remote" -> MainRouteImportRemote
|
||||
else -> MainRouteHome
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,9 @@ import org.koin.androidx.compose.koinViewModel
|
||||
fun MainScreen(
|
||||
viewModel: MainViewModel = koinViewModel(),
|
||||
useRail: Boolean,
|
||||
onOpenSettings: () -> Unit
|
||||
onOpenSettings: () -> Unit,
|
||||
onNavigateToRemoteImport: () -> Unit,
|
||||
onNavigateToLocalImport: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
@@ -373,7 +375,9 @@ fun MainScreen(
|
||||
putExtra("author", book.author)
|
||||
putExtra("bookUrl", book.bookUrl)
|
||||
}
|
||||
}
|
||||
},
|
||||
onNavigateToRemoteImport = onNavigateToRemoteImport,
|
||||
onNavigateToLocalImport = onNavigateToLocalImport
|
||||
)
|
||||
|
||||
MainDestination.Explore -> ExploreScreen()
|
||||
|
||||
@@ -67,12 +67,11 @@ import io.legado.app.R
|
||||
import io.legado.app.base.BaseRuleEvent
|
||||
import io.legado.app.ui.about.AppLogSheet
|
||||
import io.legado.app.ui.book.cache.CacheActivity
|
||||
import io.legado.app.ui.book.import.local.ImportBookActivity
|
||||
import io.legado.app.ui.book.import.remote.RemoteBookActivity
|
||||
import io.legado.app.ui.book.manage.BookshelfManageActivity
|
||||
import io.legado.app.ui.book.search.SearchActivity
|
||||
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.button.SmallOutlinedIconToggleButton
|
||||
@@ -100,7 +99,9 @@ import org.koin.androidx.compose.koinViewModel
|
||||
fun BookshelfScreen(
|
||||
viewModel: BookshelfViewModel = koinViewModel(),
|
||||
onBookClick: (BookShelfItem) -> Unit,
|
||||
onBookLongClick: (BookShelfItem) -> Unit
|
||||
onBookLongClick: (BookShelfItem) -> Unit,
|
||||
onNavigateToRemoteImport: () -> Unit,
|
||||
onNavigateToLocalImport: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
@@ -196,7 +197,7 @@ fun BookshelfScreen(
|
||||
}
|
||||
|
||||
val bookGroupStyle = BookshelfConfig.bookGroupStyle
|
||||
// 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图
|
||||
// 控制是å�¦å¤„于“文件夹列表â€�æ ¹è§†å›¾ï¼Œè¿˜æ˜¯â€œæ–‡ä»¶å¤¹å†…éƒ¨â€�书ç±�视å›?
|
||||
var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) }
|
||||
val baseTitle = when {
|
||||
bookGroupStyle == 1 -> {
|
||||
@@ -232,6 +233,10 @@ fun BookshelfScreen(
|
||||
if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape else BookshelfConfig.bookshelfLayoutGridPortrait
|
||||
val bookshelfLayoutList =
|
||||
if (isLandscape) BookshelfConfig.bookshelfLayoutListLandscape else BookshelfConfig.bookshelfLayoutListPortrait
|
||||
val totalHorizontalPadding =
|
||||
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp
|
||||
val gridContentHorizontalPadding = totalHorizontalPadding / 2
|
||||
val gridInnerHorizontalPadding = totalHorizontalPadding / 2
|
||||
|
||||
ListScaffold(
|
||||
title = title,
|
||||
@@ -242,12 +247,12 @@ fun BookshelfScreen(
|
||||
dropDownMenuContent = { dismiss ->
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.add_remote_book),
|
||||
onClick = { context.startActivity<RemoteBookActivity>(); dismiss() },
|
||||
onClick = { onNavigateToRemoteImport(); dismiss() },
|
||||
leadingIcon = { Icon(Icons.Default.Wifi, null) }
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.book_local),
|
||||
onClick = { context.startActivity<ImportBookActivity>(); dismiss() },
|
||||
onClick = { onNavigateToLocalImport(); dismiss() },
|
||||
leadingIcon = { Icon(Icons.Default.Save, null) }
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
@@ -345,7 +350,7 @@ fun BookshelfScreen(
|
||||
SmallOutlinedIconToggleButton(
|
||||
checked = showGroupMenu,
|
||||
onCheckedChange = { showGroupMenu = it },
|
||||
icon = Icons.AutoMirrored.Filled.FormatListBulleted,
|
||||
imageVector = Icons.AutoMirrored.Filled.FormatListBulleted,
|
||||
contentDescription = stringResource(R.string.group_manage)
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
@@ -407,15 +412,19 @@ fun BookshelfScreen(
|
||||
if (bookGroupStyle == 2 && isRoot) {
|
||||
val folderColumns =
|
||||
if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
|
||||
val isGridMode = bookshelfLayoutMode != 0
|
||||
FastScrollLazyVerticalGrid(
|
||||
columns = GridCells.Fixed(folderColumns.coerceAtLeast(1)),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = if (isGridMode) gridInnerHorizontalPadding else 0.dp),
|
||||
contentPadding = adaptiveContentPadding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = 120.dp
|
||||
bottom = 120.dp,
|
||||
horizontal = if (isGridMode) gridContentHorizontalPadding else 0.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
||||
showFastScroll = BookshelfConfig.showBookshelfFastScroller
|
||||
) {
|
||||
itemsIndexed(
|
||||
@@ -592,16 +601,23 @@ fun BookshelfPage(
|
||||
onBookLongClick: (BookShelfItem) -> Unit
|
||||
) {
|
||||
val columns = if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
|
||||
val isGridMode = bookshelfLayoutMode != 0
|
||||
val totalHorizontalPadding =
|
||||
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) 12.dp else 16.dp
|
||||
val gridContentHorizontalPadding = totalHorizontalPadding / 2
|
||||
val gridInnerHorizontalPadding = totalHorizontalPadding / 2
|
||||
FastScrollLazyVerticalGrid(
|
||||
columns = GridCells.Fixed(columns.coerceAtLeast(1)),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = if (isGridMode) gridInnerHorizontalPadding else 0.dp),
|
||||
contentPadding = adaptiveContentPadding(
|
||||
top = paddingValues.calculateTopPadding(),
|
||||
bottom = 120.dp,
|
||||
horizontal = if (bookshelfLayoutMode != 0) 12.dp else 0.dp
|
||||
horizontal = if (isGridMode) gridContentHorizontalPadding else 0.dp
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(if (bookshelfLayoutMode != 0) 8.dp else 0.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
|
||||
showFastScroll = BookshelfConfig.showBookshelfFastScroller
|
||||
) {
|
||||
items(books, key = { it.bookUrl }) { book ->
|
||||
@@ -621,3 +637,4 @@ fun BookshelfPage(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ fun GroupManageSheet(
|
||||
editingGroup = null
|
||||
isEditing = true
|
||||
},
|
||||
icon = Icons.Default.Add
|
||||
imageVector = Icons.Default.Add
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -130,3 +130,4 @@ fun GroupManageSheet(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -249,8 +249,11 @@ fun ExploreScreen(
|
||||
TextCard(
|
||||
text = item.bookSourceName,
|
||||
textStyle = LegadoTheme.typography.labelLarge,
|
||||
backgroundColor = LegadoTheme.colorScheme.cardContainer,
|
||||
contentColor = LegadoTheme.colorScheme.onCardContainer,
|
||||
cornerRadius = 8.dp,
|
||||
horizontalPadding = 8.dp,
|
||||
verticalPadding = 6.dp,
|
||||
verticalPadding = 8.dp,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val index =
|
||||
|
||||
@@ -235,7 +235,7 @@ fun WebServiceSettingBlock(
|
||||
) {
|
||||
SmallTextButton(
|
||||
text = "复制地址",
|
||||
icon = Icons.Default.ContentCopy,
|
||||
imageVector = Icons.Default.ContentCopy,
|
||||
onClick = {
|
||||
onNavigate(PrefClickEvent.CopyUrl(uiState.webServiceAddress))
|
||||
}
|
||||
@@ -245,7 +245,7 @@ fun WebServiceSettingBlock(
|
||||
|
||||
SmallTextButton(
|
||||
text = "浏览器打开",
|
||||
icon = Icons.Default.OpenInBrowser,
|
||||
imageVector = Icons.Default.OpenInBrowser,
|
||||
onClick = {
|
||||
onNavigate(PrefClickEvent.OpenUrl(uiState.webServiceAddress))
|
||||
}
|
||||
@@ -254,3 +254,4 @@ fun WebServiceSettingBlock(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,9 @@ class ReplaceRuleActivity : BaseComposeActivity() {
|
||||
}
|
||||
|
||||
entry<ReplaceEditRoute> { route ->
|
||||
val viewModel: ReplaceEditViewModel = koinViewModel { parametersOf(route) }
|
||||
val viewModel: ReplaceEditViewModel = koinViewModel(
|
||||
key = "replace_edit_${System.identityHashCode(route)}"
|
||||
) { parametersOf(route) }
|
||||
|
||||
ReplaceEditScreen(
|
||||
viewModel = viewModel,
|
||||
|
||||
@@ -42,13 +42,12 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.animateFloatingActionButton
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -73,8 +72,11 @@ import io.legado.app.ui.widget.components.AppScaffold
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||
import io.legado.app.ui.widget.components.button.AlertButton
|
||||
import io.legado.app.ui.widget.components.button.ToggleChip
|
||||
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
||||
import io.legado.app.ui.widget.components.button.TopBarNavigationButton
|
||||
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
@@ -115,15 +117,19 @@ fun ReplaceEditScreen(
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
IconButton(onClick = {
|
||||
viewModel.save(onSaveSuccess)
|
||||
}) {
|
||||
Icon(Icons.Default.Save, contentDescription = "保存")
|
||||
}
|
||||
}
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = "更多操作")
|
||||
TopBarActionButton(
|
||||
onClick = {
|
||||
viewModel.save(onSaveSuccess)
|
||||
},
|
||||
imageVector = Icons.Default.Save,
|
||||
contentDescription = "保存"
|
||||
)
|
||||
}
|
||||
TopBarActionButton(
|
||||
onClick = { showMenu = true },
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "更多操作"
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
@@ -158,7 +164,7 @@ fun ReplaceEditScreen(
|
||||
onClick = { viewModel.save(onSaveSuccess) },
|
||||
tooltipText = "添加"
|
||||
) {
|
||||
Icon(Icons.Default.Save, contentDescription = "保存")
|
||||
AppIcon(Icons.Default.Save, contentDescription = "保存")
|
||||
}
|
||||
}, contentWindowInsets = WindowInsets(0, 0, 0, 0)
|
||||
) { innerPadding ->
|
||||
@@ -242,53 +248,29 @@ fun ReplaceEditScreen(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Start
|
||||
) {
|
||||
FilterChip(
|
||||
ToggleChip(
|
||||
label = "标题",
|
||||
selected = state.scopeTitle,
|
||||
onClick = { viewModel.onScopeTitleChange(!state.scopeTitle) },
|
||||
label = { AppText("标题") },
|
||||
leadingIcon = if (state.scopeTitle) {
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = "已选择",
|
||||
Modifier.size(FilterChipDefaults.IconSize)
|
||||
)
|
||||
}
|
||||
} else null
|
||||
checkedContentDescription = "已选择",
|
||||
onToggle = { viewModel.onScopeTitleChange(!state.scopeTitle) }
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
FilterChip(
|
||||
ToggleChip(
|
||||
label = "内容",
|
||||
selected = state.scopeContent,
|
||||
onClick = { viewModel.onScopeContentChange(!state.scopeContent) },
|
||||
label = { AppText("内容") },
|
||||
leadingIcon = if (state.scopeContent) {
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = "已选择",
|
||||
Modifier.size(FilterChipDefaults.IconSize)
|
||||
)
|
||||
}
|
||||
} else null
|
||||
checkedContentDescription = "已选择",
|
||||
onToggle = { viewModel.onScopeContentChange(!state.scopeContent) }
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
FilterChip(
|
||||
ToggleChip(
|
||||
label = "使用正则",
|
||||
selected = state.isRegex,
|
||||
onClick = { viewModel.onRegexChange(!state.isRegex) },
|
||||
label = { AppText("使用正则") },
|
||||
leadingIcon = if (state.isRegex) {
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = "正则已启用",
|
||||
Modifier.size(FilterChipDefaults.IconSize)
|
||||
)
|
||||
}
|
||||
} else null
|
||||
checkedContentDescription = "正则已启用",
|
||||
onToggle = { viewModel.onRegexChange(!state.isRegex) }
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ fun RssFavoritesScreen(
|
||||
SelectionItemCard(
|
||||
title = rssStar.title,
|
||||
subtitle = if (rssStar.group.isNotBlank()) {
|
||||
"${rssStar.group} • ${rssStar.pubDate ?: ""}"
|
||||
"${rssStar.group} �${rssStar.pubDate ?: ""}"
|
||||
} else {
|
||||
rssStar.pubDate
|
||||
},
|
||||
@@ -232,7 +232,7 @@ fun RssFavoritesScreen(
|
||||
}
|
||||
SmallIconButton(
|
||||
onClick = openAction,
|
||||
icon = Icons.AutoMirrored.Filled.OpenInNew,
|
||||
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
|
||||
contentDescription = "Open"
|
||||
)
|
||||
},
|
||||
@@ -258,3 +258,4 @@ fun RssFavoritesScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -365,7 +365,7 @@ fun RssSourceScreen(
|
||||
trailingAction = {
|
||||
SmallIconButton(
|
||||
onClick = { showDeleteRuleDialog = item.source },
|
||||
icon = Icons.Default.Delete
|
||||
imageVector = Icons.Default.Delete
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -387,3 +387,4 @@ fun RssSourceScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ fun adaptiveContentPadding(
|
||||
bottom: Dp,
|
||||
horizontal: Dp
|
||||
): PaddingValues {
|
||||
val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top else top + 4.dp
|
||||
val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top + 6.dp else top + 4.dp
|
||||
return PaddingValues(
|
||||
top = adjustedTop,
|
||||
bottom = bottom,
|
||||
|
||||
@@ -20,6 +20,7 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
|
||||
@@ -37,7 +38,7 @@ fun CollapsibleHeader(
|
||||
GlassCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 4.dp, end = 16.dp),
|
||||
.adaptiveHorizontalPadding(vertical = 4.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
containerColor = LegadoTheme.colorScheme.surfaceContainer,
|
||||
onClick = onToggle
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.state.ToggleableState
|
||||
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import top.yukonga.miuix.kmp.basic.Checkbox
|
||||
import top.yukonga.miuix.kmp.basic.RadioButton as MiuixRadioButton
|
||||
|
||||
@Composable
|
||||
fun AppRadioButton(
|
||||
selected: Boolean,
|
||||
onClick: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true
|
||||
) {
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
MiuixRadioButton(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled
|
||||
)
|
||||
} else {
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
@@ -120,7 +120,7 @@ private fun GroupItem(
|
||||
) {
|
||||
SmallTextButton(
|
||||
text = stringResource(id = R.string.ok),
|
||||
icon = Icons.Default.Check,
|
||||
imageVector = Icons.Default.Check,
|
||||
onClick = {
|
||||
onUpdateGroup(group, state.text.toString())
|
||||
expanded = false
|
||||
@@ -130,3 +130,4 @@ private fun GroupItem(
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -31,7 +31,7 @@ fun ValueStepper(
|
||||
val newValue = (value.toInt() - 1).toFloat().coerceIn(valueRange)
|
||||
onValueChange(newValue)
|
||||
},
|
||||
icon = Icons.Default.Remove
|
||||
imageVector = Icons.Default.Remove
|
||||
)
|
||||
TextCard(
|
||||
cornerRadius = 8.dp,
|
||||
@@ -46,7 +46,7 @@ fun ValueStepper(
|
||||
val newValue = (value.toInt() + 1).toFloat().coerceIn(valueRange)
|
||||
onValueChange(newValue)
|
||||
},
|
||||
icon = Icons.Default.Add
|
||||
imageVector = Icons.Default.Add
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import top.yukonga.miuix.kmp.basic.Text as MiuixText
|
||||
@Composable
|
||||
fun MediumIconButton(
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
imageVector: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
@@ -51,7 +51,7 @@ fun MediumIconButton(
|
||||
modifier = modifier
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
@@ -61,7 +61,7 @@ fun MediumIconButton(
|
||||
modifier = modifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
@@ -71,7 +71,7 @@ fun MediumIconButton(
|
||||
@Composable
|
||||
fun MediumOutlinedIconButton(
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
imageVector: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
@@ -83,7 +83,7 @@ fun MediumOutlinedIconButton(
|
||||
backgroundColor = LegadoTheme.colorScheme.surfaceContainerHigh
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
@@ -93,7 +93,7 @@ fun MediumOutlinedIconButton(
|
||||
border = ButtonDefaults.outlinedButtonBorder()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
@@ -103,7 +103,7 @@ fun MediumOutlinedIconButton(
|
||||
@Composable
|
||||
fun MediumOutlinedButton(
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
imageVector: ImageVector,
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null
|
||||
@@ -120,7 +120,7 @@ fun MediumOutlinedButton(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
MiuixText(text = text)
|
||||
@@ -136,7 +136,7 @@ fun MediumOutlinedButton(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
Text(text = text)
|
||||
@@ -148,7 +148,7 @@ fun MediumOutlinedButton(
|
||||
@Composable
|
||||
fun MediumTonalIconButton(
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
imageVector: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
@@ -161,7 +161,7 @@ fun MediumTonalIconButton(
|
||||
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
@@ -172,7 +172,7 @@ fun MediumTonalIconButton(
|
||||
colors = IconButtonDefaults.filledTonalIconButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
@@ -183,7 +183,7 @@ fun MediumTonalIconButton(
|
||||
fun MediumOutlinedIconToggleButton(
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
icon: ImageVector,
|
||||
imageVector: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
@@ -208,7 +208,7 @@ fun MediumOutlinedIconToggleButton(
|
||||
backgroundColor = containerColor
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
tint = iconTint
|
||||
)
|
||||
@@ -220,7 +220,7 @@ fun MediumOutlinedIconToggleButton(
|
||||
modifier = modifier
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ private fun SmallNoMinTouchTarget(content: @Composable () -> Unit) {
|
||||
@Composable
|
||||
fun SmallIconButton(
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
imageVector: ImageVector,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
@@ -72,7 +72,7 @@ fun SmallIconButton(
|
||||
onClick = onClick
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
@@ -84,7 +84,7 @@ fun SmallIconButton(
|
||||
shape = IconButtonDefaults.extraSmallRoundShape,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(smallIconSize),
|
||||
)
|
||||
@@ -97,11 +97,10 @@ fun SmallIconButton(
|
||||
@Composable
|
||||
fun SmallOutlinedIconButton(
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
imageVector: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
|
||||
val isMiuix = ThemeResolver.isMiuixEngine(composeEngine)
|
||||
if (isMiuix) {
|
||||
MiuixIconButton(
|
||||
@@ -110,7 +109,7 @@ fun SmallOutlinedIconButton(
|
||||
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(SmallMiuixIconSize)
|
||||
)
|
||||
@@ -124,7 +123,7 @@ fun SmallOutlinedIconButton(
|
||||
border = ButtonDefaults.outlinedButtonBorder()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(smallIconSize)
|
||||
)
|
||||
@@ -137,7 +136,7 @@ fun SmallOutlinedIconButton(
|
||||
@Composable
|
||||
fun SmallTonalIconButton(
|
||||
onClick: () -> Unit,
|
||||
icon: ImageVector,
|
||||
imageVector: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
@@ -150,7 +149,7 @@ fun SmallTonalIconButton(
|
||||
backgroundColor = LegadoTheme.colorScheme.surfaceContainer
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(SmallMiuixIconSize)
|
||||
)
|
||||
@@ -164,7 +163,7 @@ fun SmallTonalIconButton(
|
||||
colors = IconButtonDefaults.filledTonalIconButtonColors()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(smallIconSize)
|
||||
)
|
||||
@@ -178,7 +177,7 @@ fun SmallTonalIconButton(
|
||||
fun SmallOutlinedIconToggleButton(
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
icon: ImageVector,
|
||||
imageVector: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null
|
||||
) {
|
||||
@@ -205,7 +204,7 @@ fun SmallOutlinedIconToggleButton(
|
||||
backgroundColor = containerColor
|
||||
) {
|
||||
MiuixIcon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
tint = iconTint,
|
||||
modifier = Modifier.size(SmallMiuixIconSize)
|
||||
@@ -233,7 +232,7 @@ fun SmallOutlinedIconToggleButton(
|
||||
shapes = toggleShapes
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier.size(smallIconSize),
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ import top.yukonga.miuix.kmp.basic.Text as MiuixText
|
||||
@Composable
|
||||
fun SmallTextButton(
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
imageVector: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
@@ -44,7 +44,7 @@ fun SmallTextButton(
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
@@ -55,4 +55,4 @@ fun SmallTextButton(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package io.legado.app.ui.widget.components.button
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
|
||||
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
|
||||
import top.yukonga.miuix.kmp.basic.Text as MiuixText
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
|
||||
@Composable
|
||||
fun ToggleChip(
|
||||
label: String,
|
||||
selected: Boolean,
|
||||
onToggle: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
checkedContentDescription: String = "已选择"
|
||||
) {
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
MiuixIconButton(
|
||||
onClick = onToggle,
|
||||
modifier = modifier,
|
||||
backgroundColor = if (selected) {
|
||||
MiuixTheme.colorScheme.primaryContainer
|
||||
} else {
|
||||
MiuixTheme.colorScheme.surfaceContainer
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||
) {
|
||||
AnimatedVisibility(visible = selected) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
MiuixIcon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = checkedContentDescription,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
}
|
||||
}
|
||||
|
||||
MiuixText(
|
||||
text = label,
|
||||
style = LegadoTheme.typography.labelSmall,
|
||||
maxLines = 1,
|
||||
softWrap = false
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FilterChip(
|
||||
selected = selected,
|
||||
onClick = onToggle,
|
||||
modifier = modifier,
|
||||
label = { Text(label) },
|
||||
leadingIcon = if (selected) {
|
||||
{
|
||||
Icon(
|
||||
Icons.Default.Check,
|
||||
contentDescription = checkedContentDescription,
|
||||
Modifier.size(FilterChipDefaults.IconSize)
|
||||
)
|
||||
}
|
||||
} else null
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -181,7 +181,7 @@ fun SelectionItemCard(
|
||||
if (onClickEdit != null) {
|
||||
SmallIconButton(
|
||||
onClick = onClickEdit,
|
||||
icon = Icons.Default.Edit,
|
||||
imageVector = Icons.Default.Edit,
|
||||
contentDescription = "Edit"
|
||||
)
|
||||
}
|
||||
@@ -194,7 +194,7 @@ fun SelectionItemCard(
|
||||
Box {
|
||||
SmallIconButton(
|
||||
onClick = { showMenu = true },
|
||||
icon = Icons.Default.MoreVert,
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More"
|
||||
)
|
||||
RoundDropdownMenu(
|
||||
@@ -272,3 +272,4 @@ fun LazyItemScope.ReorderableSelectionItem(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+30
-33
@@ -19,7 +19,6 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AccessTime
|
||||
import androidx.compose.material.icons.filled.FormatListNumbered
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -43,43 +42,41 @@ import io.legado.app.ui.widget.components.text.AppText
|
||||
import java.time.LocalDate
|
||||
|
||||
/**
|
||||
* 热力图日历顶部操作栏
|
||||
* 热力图日历弹窗标题
|
||||
*/
|
||||
const val HEATMAP_CALENDAR_TITLE = "时间线"
|
||||
|
||||
/**
|
||||
* 热力图日历弹窗左侧操作
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun HeatmapCalendarTopBar(
|
||||
fun HeatmapCalendarStartAction(
|
||||
currentMode: HeatmapMode,
|
||||
onModeChanged: (HeatmapMode) -> Unit,
|
||||
onClearDate: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppText("时间线", style = LegadoTheme.typography.titleMedium)
|
||||
MediumAnimatedActionButton(
|
||||
checked = currentMode == HeatmapMode.TIME,
|
||||
onCheckedChange = {
|
||||
onModeChanged(if (it) HeatmapMode.TIME else HeatmapMode.COUNT)
|
||||
},
|
||||
iconChecked = Icons.Default.AccessTime,
|
||||
iconUnchecked = Icons.Default.FormatListNumbered,
|
||||
activeText = "按时长",
|
||||
inactiveText = "按次数"
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
MediumAnimatedActionButton(
|
||||
checked = currentMode == HeatmapMode.TIME,
|
||||
onCheckedChange = {
|
||||
onModeChanged(if (it) HeatmapMode.TIME else HeatmapMode.COUNT)
|
||||
},
|
||||
iconChecked = Icons.Default.AccessTime,
|
||||
iconUnchecked = Icons.Default.FormatListNumbered,
|
||||
activeText = "按时长",
|
||||
inactiveText = "按次数"
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
MediumOutlinedIconButton(
|
||||
onClick = onClearDate,
|
||||
icon = Icons.Outlined.Delete,
|
||||
)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 热力图日历弹窗右侧操作
|
||||
*/
|
||||
@Composable
|
||||
fun HeatmapCalendarEndAction(
|
||||
onClearDate: () -> Unit
|
||||
) {
|
||||
MediumOutlinedIconButton(
|
||||
onClick = onClearDate,
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,4 +293,4 @@ fun HeatmapLegend(
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -41,7 +41,6 @@ fun AppModalBottomSheet(
|
||||
title: String? = null,
|
||||
startAction: @Composable (() -> Unit)? = null,
|
||||
endAction: @Composable (() -> Unit)? = null,
|
||||
containerColor: Color = LegadoTheme.colorScheme.surfaceContainer,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
|
||||
@@ -52,14 +51,15 @@ fun AppModalBottomSheet(
|
||||
startAction = startAction,
|
||||
endAction = endAction,
|
||||
insideMargin = DpSize(16.dp, 12.dp),
|
||||
backgroundColor = containerColor,
|
||||
onDismissRequest = onDismissRequest,
|
||||
onDismissFinished = onDismissRequest,
|
||||
enableWindowDim = true,
|
||||
allowDismiss = true
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize(),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@@ -75,7 +75,6 @@ fun AppModalBottomSheet(
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = containerColor,
|
||||
contentColor = colorScheme.onSurface,
|
||||
dragHandle = { BottomSheetDefaults.DragHandle() }
|
||||
) {
|
||||
|
||||
+1
-2
@@ -32,8 +32,7 @@ fun OptionSheet(
|
||||
AppModalBottomSheet(
|
||||
show = show,
|
||||
title = title,
|
||||
onDismissRequest = onDismissRequest,
|
||||
containerColor = LegadoTheme.colorScheme.surface
|
||||
onDismissRequest = onDismissRequest
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -34,15 +34,19 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.AppFloatingActionButton
|
||||
import io.legado.app.ui.widget.components.AppTextField
|
||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 通用编辑数据包装,用于适配不同的规则实体
|
||||
* 通用编辑数据包装,用于适配不同的规则
|
||||
*/
|
||||
data class RuleEditFields(
|
||||
val name: String = "",
|
||||
@@ -80,41 +84,44 @@ fun <T> RuleEditSheet(
|
||||
AppModalBottomSheet(
|
||||
title = title,
|
||||
startAction = {
|
||||
IconButton(onClick = onDismissRequest) {
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.cancel)
|
||||
)
|
||||
}
|
||||
MediumIconButton(
|
||||
onClick = onDismissRequest,
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Close",
|
||||
)
|
||||
},
|
||||
endAction = {
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = "More")
|
||||
}
|
||||
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
||||
DropdownMenuItem(
|
||||
text = { AppText(stringResource(R.string.copy_rule)) },
|
||||
leadingIcon = { Icon(Icons.AutoMirrored.Filled.NoteAdd, null) },
|
||||
onClick = {
|
||||
onCopy(getCurrentEntity())
|
||||
showMenu = false
|
||||
}
|
||||
Box{
|
||||
MediumIconButton(
|
||||
onClick = { showMenu = true },
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = "More"
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { AppText(stringResource(R.string.paste_rule)) },
|
||||
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
|
||||
onClick = {
|
||||
scope.launch {
|
||||
onPaste()?.let { pasted ->
|
||||
val fields = toFields(pasted)
|
||||
name = fields.name
|
||||
rule1 = fields.rule1
|
||||
rule2 = fields.rule2
|
||||
}
|
||||
RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.copy_rule),
|
||||
leadingIcon = { Icon(Icons.AutoMirrored.Filled.NoteAdd, null) },
|
||||
onClick = {
|
||||
onCopy(getCurrentEntity())
|
||||
showMenu = false
|
||||
}
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
)
|
||||
RoundDropdownMenuItem(
|
||||
text = stringResource(R.string.paste_rule),
|
||||
leadingIcon = { Icon(Icons.Default.ContentPaste, null) },
|
||||
onClick = {
|
||||
scope.launch {
|
||||
onPaste()?.let { pasted ->
|
||||
val fields = toFields(pasted)
|
||||
name = fields.name
|
||||
rule1 = fields.rule1
|
||||
rule2 = fields.rule2
|
||||
}
|
||||
}
|
||||
showMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
show = show,
|
||||
@@ -132,6 +139,7 @@ fun <T> RuleEditSheet(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||
label = stringResource(R.string.name),
|
||||
singleLine = true
|
||||
)
|
||||
@@ -139,12 +147,14 @@ fun <T> RuleEditSheet(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = rule1,
|
||||
onValueChange = { rule1 = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||
label = label1
|
||||
)
|
||||
AppTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = rule2,
|
||||
onValueChange = { rule2 = it },
|
||||
backgroundColor = LegadoTheme.colorScheme.surface,
|
||||
label = label2,
|
||||
minLines = 3
|
||||
)
|
||||
@@ -160,4 +170,4 @@ fun <T> RuleEditSheet(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -84,14 +84,14 @@ fun InputSettingItem(
|
||||
) {
|
||||
SmallTextButton(
|
||||
text = "默认",
|
||||
icon = Icons.Default.Replay,
|
||||
imageVector = Icons.Default.Replay,
|
||||
onClick = {
|
||||
state.edit { replace(0, length, defaultValue.toString()) }
|
||||
}
|
||||
)
|
||||
SmallTextButton(
|
||||
text = "确认",
|
||||
icon = Icons.Default.Check,
|
||||
imageVector = Icons.Default.Check,
|
||||
onClick = {
|
||||
onConfirm(state.text.toString())
|
||||
expanded = false
|
||||
@@ -133,14 +133,14 @@ fun InputSettingItem(
|
||||
) {
|
||||
SmallTextButton(
|
||||
text = "默认",
|
||||
icon = Icons.Default.Replay,
|
||||
imageVector = Icons.Default.Replay,
|
||||
onClick = {
|
||||
state.edit { replace(0, length, defaultValue.toString()) }
|
||||
}
|
||||
)
|
||||
SmallTextButton(
|
||||
text = "确认",
|
||||
icon = Icons.Default.Check,
|
||||
imageVector = Icons.Default.Check,
|
||||
onClick = {
|
||||
onConfirm(state.text.toString())
|
||||
expanded = false
|
||||
@@ -150,4 +150,4 @@ fun InputSettingItem(
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-12
@@ -1,4 +1,4 @@
|
||||
package io.legado.app.ui.widget.components.settingItem
|
||||
package io.legado.app.ui.widget.components.settingItem
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
@@ -78,7 +78,7 @@ fun SliderSettingItem(
|
||||
MiuixTextField(
|
||||
state = textFieldState,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
label = "输入数值 (${valueRange.start.toInt()}-${valueRange.endInclusive.toInt()})",
|
||||
label = "杈撳叆鏁板€?(${valueRange.start.toInt()}-${valueRange.endInclusive.toInt()})",
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
inputTransformation = {
|
||||
@@ -120,16 +120,16 @@ fun SliderSettingItem(
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
SmallTextButton(
|
||||
text = if (isInputMode) "滑块" else "输入",
|
||||
icon = if (isInputMode) Icons.Default.LinearScale else Icons.Default.Edit,
|
||||
text = if (isInputMode) "婊戝潡" else "杈撳叆",
|
||||
imageVector = if (isInputMode) Icons.Default.LinearScale else Icons.Default.Edit,
|
||||
onClick = { isInputMode = !isInputMode }
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
SmallTextButton(
|
||||
text = "默认",
|
||||
icon = Icons.Default.RestartAlt,
|
||||
text = "榛樿",
|
||||
imageVector = Icons.Default.RestartAlt,
|
||||
onClick = {
|
||||
onValueChange(defaultValue)
|
||||
textFieldState.edit {
|
||||
@@ -163,7 +163,7 @@ fun SliderSettingItem(
|
||||
TextField(
|
||||
state = textFieldState,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
label = { AppText("输入数值 (${valueRange.start.toInt()}-${valueRange.endInclusive.toInt()})") },
|
||||
label = { AppText("杈撳叆鏁板€?(${valueRange.start.toInt()}-${valueRange.endInclusive.toInt()})") },
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -212,16 +212,16 @@ fun SliderSettingItem(
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
SmallTextButton(
|
||||
text = if (isInputMode) "滑块" else "输入",
|
||||
icon = if (isInputMode) Icons.Default.LinearScale else Icons.Default.Edit,
|
||||
text = if (isInputMode) "婊戝潡" else "杈撳叆",
|
||||
imageVector = if (isInputMode) Icons.Default.LinearScale else Icons.Default.Edit,
|
||||
onClick = { isInputMode = !isInputMode }
|
||||
)
|
||||
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
SmallTextButton(
|
||||
text = "默认",
|
||||
icon = Icons.Default.RestartAlt,
|
||||
text = "榛樿",
|
||||
imageVector = Icons.Default.RestartAlt,
|
||||
onClick = {
|
||||
onValueChange(defaultValue)
|
||||
textFieldState.edit {
|
||||
@@ -237,4 +237,5 @@ fun SliderSettingItem(
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -128,7 +129,7 @@ private fun SwipeBackground(
|
||||
targetValue = if (isThresholdReached)
|
||||
action.background
|
||||
else
|
||||
MaterialTheme.colorScheme.surfaceVariant,
|
||||
LegadoTheme.colorScheme.surfaceVariant,
|
||||
label = "bgColor"
|
||||
)
|
||||
|
||||
@@ -160,7 +161,7 @@ private fun SwipeBackground(
|
||||
tint = if (isThresholdReached)
|
||||
contentColorFor(action.background)
|
||||
else
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
LegadoTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user