[新增] 为日后的迁移实现了一些与替换规则、书源管理有关的导入导出等可复用组件
This commit is contained in:
8 files changed
+543
-1
No files matched your search
@@ -0,0 +1,27 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import io.legado.app.help.DirectLinkUpload
|
||||||
|
|
||||||
|
interface UploadRepository {
|
||||||
|
suspend fun upload(
|
||||||
|
fileName: String,
|
||||||
|
file: Any,
|
||||||
|
contentType: String
|
||||||
|
): String
|
||||||
|
}
|
||||||
|
|
||||||
|
class DirectLinkUploadRepository : UploadRepository {
|
||||||
|
|
||||||
|
override suspend fun upload(
|
||||||
|
fileName: String,
|
||||||
|
file: Any,
|
||||||
|
contentType: String
|
||||||
|
): String {
|
||||||
|
return DirectLinkUpload.upLoad(
|
||||||
|
fileName = fileName,
|
||||||
|
file = file,
|
||||||
|
contentType = contentType
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package io.legado.app.di
|
package io.legado.app.di
|
||||||
|
|
||||||
import io.legado.app.data.AppDatabase
|
import io.legado.app.data.AppDatabase
|
||||||
|
import io.legado.app.data.repository.DirectLinkUploadRepository
|
||||||
import io.legado.app.data.repository.ExploreRepository
|
import io.legado.app.data.repository.ExploreRepository
|
||||||
import io.legado.app.data.repository.ExploreRepositoryImpl
|
import io.legado.app.data.repository.ExploreRepositoryImpl
|
||||||
import io.legado.app.data.repository.ReadRecordRepository
|
import io.legado.app.data.repository.ReadRecordRepository
|
||||||
|
import io.legado.app.data.repository.UploadRepository
|
||||||
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
||||||
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
||||||
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
||||||
@@ -26,6 +28,8 @@ val appModule = module {
|
|||||||
single { ReadRecordRepository(get()) }
|
single { ReadRecordRepository(get()) }
|
||||||
viewModel { ReadRecordViewModel(get(), get(), get()) }
|
viewModel { ReadRecordViewModel(get(), get(), get()) }
|
||||||
|
|
||||||
|
single<UploadRepository> { DirectLinkUploadRepository() }
|
||||||
|
|
||||||
// Explore
|
// Explore
|
||||||
single<ExploreRepository> { ExploreRepositoryImpl(get()) }
|
single<ExploreRepository> { ExploreRepositoryImpl(get()) }
|
||||||
viewModel { ExploreShowViewModel(get()) }
|
viewModel { ExploreShowViewModel(get()) }
|
||||||
|
|||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
package io.legado.app.ui.widget.components.exportComponents
|
||||||
|
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
|
||||||
|
import androidx.compose.material.icons.filled.CloudUpload
|
||||||
|
import androidx.compose.material.icons.filled.FolderOpen
|
||||||
|
import androidx.compose.material.icons.filled.SaveAlt
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
|
||||||
|
enum class FilePickerSheetMode {
|
||||||
|
DIR, FILE, EXPORT
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FilePickerSheet(
|
||||||
|
sheetState: SheetState,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
mode: FilePickerSheetMode,
|
||||||
|
onSelectSysDir: () -> Unit,
|
||||||
|
onSelectSysFile: (Array<String>) -> Unit,
|
||||||
|
onUpload: (() -> Unit)? = null,
|
||||||
|
allowExtensions: Array<String>? = null,
|
||||||
|
) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
sheetState = sheetState,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = 32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.select_operation),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
when (mode) {
|
||||||
|
FilePickerSheetMode.DIR -> {
|
||||||
|
FilePickerOptionCard(
|
||||||
|
icon = Icons.Default.FolderOpen,
|
||||||
|
text = stringResource(R.string.sys_folder_picker),
|
||||||
|
onClick = onSelectSysDir
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FilePickerSheetMode.FILE -> {
|
||||||
|
FilePickerOptionCard(
|
||||||
|
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||||
|
text = stringResource(R.string.sys_file_picker),
|
||||||
|
onClick = { onSelectSysFile(typesOfExtensions(allowExtensions)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
FilePickerSheetMode.EXPORT -> {
|
||||||
|
FilePickerOptionCard(
|
||||||
|
icon = Icons.Default.SaveAlt,
|
||||||
|
text = stringResource(R.string.save_to_local),
|
||||||
|
onClick = onSelectSysDir
|
||||||
|
)
|
||||||
|
|
||||||
|
if (onUpload != null) {
|
||||||
|
FilePickerOptionCard(
|
||||||
|
icon = Icons.Default.CloudUpload,
|
||||||
|
text = stringResource(R.string.upload_url),
|
||||||
|
onClick = onUpload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.FilePickerOptionCard(
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(100.dp),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
tonalElevation = 2.dp
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun typesOfExtensions(allowExtensions: Array<String>?): Array<String> {
|
||||||
|
val types = hashSetOf<String>()
|
||||||
|
if (allowExtensions.isNullOrEmpty()) {
|
||||||
|
types.add("*/*")
|
||||||
|
} else {
|
||||||
|
allowExtensions.forEach {
|
||||||
|
when (it) {
|
||||||
|
"*" -> types.add("*/*")
|
||||||
|
"txt", "xml" -> types.add("text/*")
|
||||||
|
else -> {
|
||||||
|
val mime = MimeTypeMap.getSingleton()
|
||||||
|
.getMimeTypeFromExtension(it)
|
||||||
|
?: "application/octet-stream"
|
||||||
|
types.add(mime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return types.toTypedArray()
|
||||||
|
}
|
||||||
+329
@@ -0,0 +1,329 @@
|
|||||||
|
package io.legado.app.ui.widget.components.importComponents
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
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.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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.filled.Info
|
||||||
|
import androidx.compose.material.icons.filled.SelectAll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.AssistChip
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.ListItemDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.OutlinedToggleButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.rememberModalBottomSheetState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.widget.components.AnimatedText
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SourceInputDialog(
|
||||||
|
title: String = "网络导入",
|
||||||
|
hint: String = "请输入 URL 或 JSON",
|
||||||
|
initialValue: String = "",
|
||||||
|
historyValues: List<String> = emptyList(), // 历史记录
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onConfirm: (String) -> Unit
|
||||||
|
) {
|
||||||
|
var text by remember { mutableStateOf(initialValue) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = { Text(title) },
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = { text = it },
|
||||||
|
label = { Text(hint) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
maxLines = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
if (historyValues.isNotEmpty()) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text("历史记录:", style = MaterialTheme.typography.labelSmall)
|
||||||
|
LazyRow(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
items(historyValues) { history ->
|
||||||
|
AssistChip(
|
||||||
|
onClick = { text = history },
|
||||||
|
label = { Text(history, maxLines = 1) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = { if (text.isNotBlank()) onConfirm(text) }
|
||||||
|
) { Text(stringResource(android.R.string.ok)) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ConfigurationScreenWidthHeight")
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun <T> BatchImportDialog(
|
||||||
|
title: String,
|
||||||
|
importState: BaseImportUiState.Success<T>,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onConfirm: (List<T>) -> Unit, // 返回选中的原始数据列表
|
||||||
|
onToggleItem: (index: Int) -> Unit,
|
||||||
|
onToggleAll: (isSelected: Boolean) -> Unit,
|
||||||
|
onItemInfoClick: (index: Int) -> Unit = {},
|
||||||
|
// 插槽:允许调用方自定义顶部菜单
|
||||||
|
topBarActions: @Composable RowScope.() -> Unit = {},
|
||||||
|
// 插槽:自定义每一行的显示内容
|
||||||
|
itemContent: @Composable (data: T, status: ImportStatus) -> Unit
|
||||||
|
) {
|
||||||
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
val selectedCount = importState.items.count { it.isSelected }
|
||||||
|
val totalCount = importState.items.size
|
||||||
|
|
||||||
|
ModalBottomSheet(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
sheetState = sheetState,
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(max = LocalConfiguration.current.screenHeightDp.dp * 0.8f),
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
AnimatedText(
|
||||||
|
if (selectedCount > 0)
|
||||||
|
stringResource(
|
||||||
|
R.string.select_count,
|
||||||
|
selectedCount,
|
||||||
|
totalCount
|
||||||
|
)
|
||||||
|
else
|
||||||
|
title
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = topBarActions,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
|
bottomBar = {
|
||||||
|
ImportBottomBar(
|
||||||
|
selectedCount = selectedCount,
|
||||||
|
totalCount = totalCount,
|
||||||
|
onToggleSelectAll = { isAll -> onToggleAll(isAll) },
|
||||||
|
onConfirm = {
|
||||||
|
scope.launch { sheetState.hide() }.invokeOnCompletion {
|
||||||
|
if (!sheetState.isVisible) {
|
||||||
|
val selectedData =
|
||||||
|
importState.items.filter { it.isSelected }.map { it.data }
|
||||||
|
onConfirm(selectedData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCancel = {
|
||||||
|
scope.launch { sheetState.hide() }.invokeOnCompletion {
|
||||||
|
if (!sheetState.isVisible) {
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(padding),
|
||||||
|
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
itemsIndexed(importState.items, key = { _, item -> item.data.hashCode() }) { index, itemWrapper ->
|
||||||
|
ImportItemRow(
|
||||||
|
isSelected = itemWrapper.isSelected,
|
||||||
|
status = itemWrapper.status,
|
||||||
|
onClick = { onToggleItem(index) },
|
||||||
|
onInfoClick = {
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar("其实还没写桀桀桀")
|
||||||
|
}
|
||||||
|
onItemInfoClick(index)
|
||||||
|
},
|
||||||
|
content = { itemContent(itemWrapper.data, itemWrapper.status) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun ImportItemRow(
|
||||||
|
isSelected: Boolean,
|
||||||
|
status: ImportStatus,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onInfoClick: () -> Unit,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val containerColor by animateColorAsState(
|
||||||
|
targetValue = if (isSelected)
|
||||||
|
MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.surface,
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 200,
|
||||||
|
easing = FastOutSlowInEasing
|
||||||
|
),
|
||||||
|
label = "CardColor"
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = containerColor
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
modifier = Modifier.animateContentSize(),
|
||||||
|
headlineContent = { content() },
|
||||||
|
leadingContent = {
|
||||||
|
Checkbox(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = when (status) {
|
||||||
|
ImportStatus.New -> "新增"
|
||||||
|
ImportStatus.Update -> "更新"
|
||||||
|
ImportStatus.Existing -> "已有"
|
||||||
|
ImportStatus.Error -> "错误"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = when (status) {
|
||||||
|
ImportStatus.New -> MaterialTheme.colorScheme.primary
|
||||||
|
ImportStatus.Update -> MaterialTheme.colorScheme.secondary
|
||||||
|
ImportStatus.Error -> MaterialTheme.colorScheme.error
|
||||||
|
else -> MaterialTheme.colorScheme.outline
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
IconButton(onClick = onInfoClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Info,
|
||||||
|
contentDescription = "详情"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = ListItemDefaults.colors(
|
||||||
|
containerColor = Color.Transparent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ImportBottomBar(
|
||||||
|
selectedCount: Int,
|
||||||
|
totalCount: Int,
|
||||||
|
onToggleSelectAll: (Boolean) -> Unit,
|
||||||
|
onConfirm: () -> Unit,
|
||||||
|
onCancel: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.navigationBarsPadding()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val allSelected = selectedCount == totalCount
|
||||||
|
OutlinedToggleButton(
|
||||||
|
checked = allSelected,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
onToggleSelectAll(checked)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.SelectAll,
|
||||||
|
contentDescription = if (allSelected) "全不选" else "全选"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
OutlinedButton(onClick = onCancel) { Text("取消") }
|
||||||
|
Button(
|
||||||
|
enabled = selectedCount > 0,
|
||||||
|
onClick = onConfirm
|
||||||
|
) {
|
||||||
|
Text("导入")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package io.legado.app.ui.widget.components.importComponents
|
||||||
|
|
||||||
|
// 单个条目的状态包装
|
||||||
|
data class ImportItemWrapper<T>(
|
||||||
|
val data: T,// 具体的数据对象 (ReplaceRule, BookSource, etc.)
|
||||||
|
val oldData: T? = null,
|
||||||
|
val isSelected: Boolean = true,
|
||||||
|
val status: ImportStatus = ImportStatus.New // 用于UI显示颜色
|
||||||
|
)
|
||||||
|
|
||||||
|
// 导入状态枚举
|
||||||
|
enum class ImportStatus {
|
||||||
|
New, // 新增 绿
|
||||||
|
Update, // 更新 黄
|
||||||
|
Existing, // 已有 灰
|
||||||
|
Error // 错误 红
|
||||||
|
}
|
||||||
|
|
||||||
|
// 整个导入流程的 UI State
|
||||||
|
sealed interface BaseImportUiState<out T> {
|
||||||
|
data object Idle : BaseImportUiState<Nothing>
|
||||||
|
data object Loading : BaseImportUiState<Nothing>
|
||||||
|
data class Error(val msg: String) : BaseImportUiState<Nothing>
|
||||||
|
data class Success<T>(
|
||||||
|
val source: String,
|
||||||
|
val items: List<ImportItemWrapper<T>>,
|
||||||
|
// 导入配置项
|
||||||
|
val keepOriginalName: Boolean = false,
|
||||||
|
val customGroup: String? = null,
|
||||||
|
val isAddGroup: Boolean = false
|
||||||
|
) : BaseImportUiState<T>
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -199,7 +199,7 @@
|
|||||||
<string name="replace_rule">替换规则</string>
|
<string name="replace_rule">替换规则</string>
|
||||||
<string name="replace_to">替换为</string>
|
<string name="replace_to">替换为</string>
|
||||||
<string name="img_cover">封面</string>
|
<string name="img_cover">封面</string>
|
||||||
<string name="book">本书</string>
|
<string name="book">本书</string>
|
||||||
<string name="volume_key_page">音量键翻页</string>
|
<string name="volume_key_page">音量键翻页</string>
|
||||||
<string name="mouse_wheel_page">鼠标滚轮翻页</string>
|
<string name="mouse_wheel_page">鼠标滚轮翻页</string>
|
||||||
<string name="click_turn_page">点击翻页</string>
|
<string name="click_turn_page">点击翻页</string>
|
||||||
@@ -1367,4 +1367,6 @@
|
|||||||
<string name="enable_slider_vibrator">滑动进度条时震动</string>
|
<string name="enable_slider_vibrator">滑动进度条时震动</string>
|
||||||
<string name="edit_remark">编辑备注</string>
|
<string name="edit_remark">编辑备注</string>
|
||||||
<string name="book_remark">书籍备注</string>
|
<string name="book_remark">书籍备注</string>
|
||||||
|
<string name="save_to_local">保存至本地</string>
|
||||||
|
<string name="select_operation">选择操作</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in new issue
Block a user