[优化] 界面优化

This commit is contained in:
HapeLee
2026-04-11 17:25:09 +08:00
parent 42dda827bc
commit 1d585ecec9
41 changed files with 1041 additions and 451 deletions
@@ -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(
}
}
}
@@ -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(
)
}
}
@@ -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
)
}
}
}
@@ -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() }
) {
@@ -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(
}
}
}
}
}
@@ -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(
}
)
}
}
}
@@ -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
)
}
}